From 81c9a10c03925a2335594051f54b486d6fb53b66 Mon Sep 17 00:00:00 2001 From: Moritz Lindner Date: Tue, 19 May 2026 16:05:23 +0200 Subject: [PATCH 1/5] remove old sample and update readme to contain new sample --- README.md | 38 +++-- library/build.gradle.kts | 9 -- .../treelayout/sample/PngExporter.kt | 72 --------- .../linde9821/treelayout/sample/SampleApp.kt | 152 ------------------ 4 files changed, 27 insertions(+), 244 deletions(-) delete mode 100644 library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/PngExporter.kt delete mode 100644 library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/SampleApp.kt diff --git a/README.md b/README.md index 584eff9..de73691 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ ![Tree Layout KMP Visualization Example](images/output.gif) A pure **Kotlin Multiplatform** library for computing tidy, aesthetically pleasing tree visualizations. It implements -the Walker algorithm (Buchheim–Jünger–Leipert variant) in $O(n)$ time with zero platform dependencies — no JVM, Android, +the Walker algorithm (Buchheim–Jünger–Leipert variant) in O(n) time with zero platform dependencies — no JVM, Android, or iOS frameworks required. -TreeLayoutKMP works with *any* tree structure you already have. You provide a thin adapter describing how to traverse +`TreeLayoutKMP` works with *any* tree structure you already have. You provide a thin adapter describing how to traverse your nodes; the library computes optimal (x, y) coordinates for every node in the tree. +**[Try the live demo on GitHub Pages](https://linde9821.github.io/TreeLayoutKMP/)** + ## Supported Targets | JVM | Android | iOS | Linux x64 | wasmJs | js | @@ -139,7 +141,7 @@ The layout engine uses node extents to compute center-to-center distances: ### 5. Layout Orientation -By default the tree grows top-to-bottom. Use the `orientation` property to change direction: +By default, the tree grows top-to-bottom. Use the `orientation` property to change direction: ```kotlin val config = WalkerLayoutConfiguration( @@ -223,26 +225,40 @@ The layout is computed using the Buchheim–Jünger–Leipert improvement of the ## Running the Samples -### Compose Desktop (interactive visualization) +The `sample/` module is a Compose Multiplatform application that visualizes a **prefix tree** built from user-provided +words. It demonstrates variable node sizes, all four orientations, and interactive layout controls. -The `sample/` module contains a Compose Desktop application that renders an interactive tree visualization using the -library. +### Desktop (JVM) ```bash ./gradlew :sample:run ``` -This opens a window displaying an org-chart tree with variable node sizes and edges drawn between parent and child -nodes. +Opens a window with a side panel for layout controls, a text input for words, and a zoomable tree canvas. + +### Web (wasmJs) + +```bash +./gradlew :sample:wasmJsBrowserRun +``` + +Launches a local dev server and opens the sample in your browser. This is the same app deployed to GitHub Pages. -### JVM CLI (ASCII + PNG export) +### Android -The library module includes a JVM sample that renders an ASCII visualization to the console and exports a PNG image. +The standalone Android sample lives in `sample/android/`. Open it in Android Studio and run on a device or emulator, or +build from the command line: ```bash -./gradlew :library:runSample +cd sample/android +./gradlew installDebug ``` +### iOS + +Open `sample/iosApp/iosApp.xcodeproj` in Xcode and run on a simulator or device. The shared Kotlin code is compiled +into a static framework via the `:sample` module's iOS targets. + ## License Apache License 2.0 — see [LICENSE](LICENSE) for details. diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 0914251..448c3f7 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -104,12 +104,3 @@ mavenPublishing { } } } - -tasks.register("runSample") { - description = "Runs the JVM sample application demonstrating tree layout" - val jvmJar = tasks.named("jvmJar") - dependsOn(jvmJar) - val runtimeClasspath = kotlin.jvm().compilations["main"].runtimeDependencyFiles - classpath = files(jvmJar.map { (it as Jar).archiveFile }, runtimeClasspath) - mainClass.set("io.github.linde9821.treelayout.sample.SampleAppKt") -} diff --git a/library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/PngExporter.kt b/library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/PngExporter.kt deleted file mode 100644 index 7038cd8..0000000 --- a/library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/PngExporter.kt +++ /dev/null @@ -1,72 +0,0 @@ -package io.github.linde9821.treelayout.sample - -import io.github.linde9821.treelayout.TreeAdapter -import io.github.linde9821.treelayout.TreeLayoutResult -import java.awt.BasicStroke -import java.awt.Color -import java.awt.Font -import java.awt.RenderingHints -import java.awt.image.BufferedImage -import java.io.File -import javax.imageio.ImageIO - -private const val SCALE = 50 -private const val PADDING = 100 -private const val NODE_RADIUS = 22 - -public fun exportLayoutToPng( - result: TreeLayoutResult, - adapter: TreeAdapter, - outputFile: File, -): Unit { - val positions = result.getPositions() - val minX = positions.values.minOf { it.x } - val maxX = positions.values.maxOf { it.x } - val minY = positions.values.minOf { it.y } - val maxY = positions.values.maxOf { it.y } - - val width = ((maxX - minX) * SCALE).toInt() + PADDING * 2 - val height = ((maxY - minY) * SCALE).toInt() + PADDING * 2 - - val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - val g = image.createGraphics() - - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g.color = Color.WHITE - g.fillRect(0, 0, width, height) - - fun px(x: Float): Int = ((x - minX) * SCALE).toInt() + PADDING - fun py(y: Float): Int = ((y - minY) * SCALE).toInt() + PADDING - - // Draw edges - g.color = Color(70, 90, 140) - g.stroke = BasicStroke(2f) - fun drawEdges(node: String) { - val pos = result.getPosition(node) - for (child in adapter.children(node)) { - val cpos = result.getPosition(child) - g.drawLine(px(pos.x), py(pos.y), px(cpos.x), py(cpos.y)) - drawEdges(child) - } - } - drawEdges(adapter.root()) - - // Draw nodes - val font = Font("SansSerif", Font.BOLD, 12) - g.font = font - val fm = g.fontMetrics - for ((node, pt) in positions) { - val cx = px(pt.x) - val cy = py(pt.y) - g.color = Color(220, 235, 255) - g.fillOval(cx - NODE_RADIUS, cy - NODE_RADIUS, NODE_RADIUS * 2, NODE_RADIUS * 2) - g.color = Color(70, 90, 140) - g.drawOval(cx - NODE_RADIUS, cy - NODE_RADIUS, NODE_RADIUS * 2, NODE_RADIUS * 2) - g.color = Color.BLACK - val tw = fm.stringWidth(node) - g.drawString(node, cx - tw / 2, cy + fm.ascent / 2) - } - - g.dispose() - ImageIO.write(image, "png", outputFile) -} diff --git a/library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/SampleApp.kt b/library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/SampleApp.kt deleted file mode 100644 index 736067e..0000000 --- a/library/src/jvmMain/kotlin/io/github/linde9821/treelayout/sample/SampleApp.kt +++ /dev/null @@ -1,152 +0,0 @@ -package io.github.linde9821.treelayout.sample - -import io.github.linde9821.treelayout.NodeExtentProvider -import io.github.linde9821.treelayout.TreeAdapter -import io.github.linde9821.treelayout.walker.WalkerLayoutConfiguration -import io.github.linde9821.treelayout.walker.WalkerTreeLayout - -internal data class SampleNode(val name: String, val children: List = emptyList()) - -internal class SampleAdapter(private val tree: SampleNode) : TreeAdapter { - private val parentMap: Map = buildMap { - fun walk(node: SampleNode, parent: SampleNode?) { - put(node, parent) - node.children.forEach { walk(it, node) } - } - walk(tree, null) - } - - override fun root(): SampleNode = tree - override fun children(node: SampleNode): List = node.children - override fun parent(node: SampleNode): SampleNode? = parentMap[node] -} - -internal fun renderAscii(layout: WalkerTreeLayout, adapter: SampleAdapter) { - val result = layout.layout() - val positions = result.getPositions() - - // Scale: each unit X -> 8 chars, each unit Y -> 3 rows - val scaleX = 8 - val scaleY = 3 - - data class Placed(val name: String, val col: Int, val row: Int) - - val minX = positions.values.minOf { it.x } - val placed = positions.map { (node, pt) -> - Placed(node.name, ((pt.x - minX) * scaleX).toInt(), (pt.y * scaleY).toInt()) - } - - val maxCol = placed.maxOf { it.col + it.name.length } - val maxRow = placed.maxOf { it.row } - - val grid = Array(maxRow + 1) { CharArray(maxCol + 1) { ' ' } } - - for (p in placed) { - for ((i, ch) in p.name.withIndex()) { - if (p.col + i <= maxCol) grid[p.row][p.col + i] = ch - } - } - - for (row in grid) { - println(String(row).trimEnd()) - } -} - -internal class StringTreeAdapter( - private val rootNode: String, - private val childrenMap: Map>, -) : TreeAdapter { - private val parentMap: Map = buildMap { - fun walk(node: String, parent: String?) { - put(node, parent) - (childrenMap[node] ?: emptyList()).forEach { walk(it, node) } - } - walk(rootNode, null) - } - - override fun root(): String = rootNode - override fun children(node: String): List = childrenMap[node] ?: emptyList() - override fun parent(node: String): String? = parentMap[node] -} - -public fun main(): Unit { - // Asymmetric tree: 4 levels deep, varying widths - val tree = SampleNode( - "root", listOf( - SampleNode( - "A", listOf( - SampleNode("A1"), - SampleNode( - "A2", listOf( - SampleNode("A2a"), - SampleNode("A2b"), - SampleNode("A2c") - ) - ), - SampleNode("A3") - ) - ), - SampleNode("B"), - SampleNode( - "C", listOf( - SampleNode( - "C1", listOf( - SampleNode("C1x") - ) - ), - SampleNode("C2") - ) - ) - ) - ) - - val adapter = SampleAdapter(tree) - val layout = WalkerTreeLayout( - adapter = adapter, - configuration = WalkerLayoutConfiguration( - horizontalDistance = 5.0f, - verticalDistance = 2.0f - ) - ) - - renderAscii(layout, adapter) - - // PNG export using String-based adapter - val childrenMap = mapOf( - "root" to listOf("A", "B", "C"), - "A" to listOf("A1", "A2", "A3"), - "A2" to listOf("A2a", "A2b", "A2c"), - "C" to listOf("C1", "C2"), - "C1" to listOf("C1x"), - ) - val stringAdapter = StringTreeAdapter("root", childrenMap) - val stringLayout = WalkerTreeLayout( - adapter = stringAdapter, - configuration = WalkerLayoutConfiguration( - horizontalDistance = 5.0f, - verticalDistance = 2.0f - ) - ) - val result = stringLayout.layout() - val outputFile = java.io.File("tree_layout.png") - exportLayoutToPng(result, stringAdapter, outputFile) - println("PNG exported to: ${outputFile.absolutePath}") - - // Demo: Variable node sizes with NodeExtentProvider - println("\n--- Variable Node Sizes Demo ---") - val sizedExtents = object : NodeExtentProvider { - override fun width(node: SampleNode): Float = node.name.length.toFloat() * 2f - override fun height(node: SampleNode): Float = 3f - } - val sizedLayout = WalkerTreeLayout( - adapter = adapter, - configuration = WalkerLayoutConfiguration(horizontalDistance = 2.0f, verticalDistance = 1.0f), - nodeExtentProvider = sizedExtents, - ) - val sizedResult = sizedLayout.layout() - sizedResult.getPositions().forEach { (node, point) -> - val w = sizedExtents.width(node) - val h = sizedExtents.height(node) - println("${node.name.padEnd(4)} -> (${"%6.1f".format(point.x)}, ${"%4.1f".format(point.y)}) size=${w}x${h}") - } -} From 8004302dccb30e3555a01acaea5a6369ab7ec275 Mon Sep 17 00:00:00 2001 From: Moritz Lindner Date: Tue, 19 May 2026 18:58:20 +0200 Subject: [PATCH 2/5] add benchmark to readme --- README.md | 13 +++++++++++++ sample/android/build.gradle.kts | 9 +++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index de73691..8272d8a 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,19 @@ The layout is computed using the Buchheim–Jünger–Leipert improvement of the as possible while preserving symmetry. - **Deterministic output** — the same tree always produces the same coordinates. +## Benchmark + +The `benchmark/` module measures layout computation time across trees ranging from 1 to ~6 million nodes. Results +confirm the algorithm's **O(n) linear time complexity** — computation time scales proportionally with node count. + +![Benchmark Results](benchmark/lets-plot-images/benchmark_results.png) + +Run the benchmark yourself: + +```bash +./gradlew :benchmark:jvmRun +``` + ## Running the Samples The `sample/` module is a Compose Multiplatform application that visualizes a **prefix tree** built from user-provided diff --git a/sample/android/build.gradle.kts b/sample/android/build.gradle.kts index cbb43f1..beccbd3 100644 --- a/sample/android/build.gradle.kts +++ b/sample/android/build.gradle.kts @@ -17,13 +17,14 @@ android { buildFeatures { compose = true } + compileSdkMinor = 0 } dependencies { implementation("io.github.linde9821:treelayout-kmp:0.2.0") implementation("androidx.activity:activity-compose:1.13.0") - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material) - implementation(compose.ui) + implementation("org.jetbrains.compose.runtime:runtime:1.11.0") + implementation("org.jetbrains.compose.foundation:foundation:1.11.0") + implementation("org.jetbrains.compose.material:material:1.11.0") + implementation("org.jetbrains.compose.ui:ui:1.11.0") } From dbc1f08adf6b0cde11ad3c8971454ff9cc470d84 Mon Sep 17 00:00:00 2001 From: Moritz Lindner Date: Tue, 19 May 2026 18:58:30 +0200 Subject: [PATCH 3/5] add node to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8272d8a..61b7289 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ or iOS frameworks required. `TreeLayoutKMP` works with *any* tree structure you already have. You provide a thin adapter describing how to traverse your nodes; the library computes optimal (x, y) coordinates for every node in the tree. +> **Note:** This library is a *layout engine*, not a rendering library. It outputs coordinates — how you draw the tree +> (Canvas, SVG, Compose, HTML, etc.) is entirely up to you. + **[Try the live demo on GitHub Pages](https://linde9821.github.io/TreeLayoutKMP/)** ## Supported Targets From c467c9557f75cb5743b7baf07490baaef6d12455 Mon Sep 17 00:00:00 2001 From: Moritz Lindner Date: Tue, 19 May 2026 19:19:12 +0200 Subject: [PATCH 4/5] add link to paper --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 61b7289..baf9f83 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,7 @@ ![Tree Layout KMP Visualization Example](images/output.gif) -A pure **Kotlin Multiplatform** library for computing tidy, aesthetically pleasing tree visualizations. It implements -the Walker algorithm (Buchheim–Jünger–Leipert variant) in O(n) time with zero platform dependencies — no JVM, Android, -or iOS frameworks required. +A pure **Kotlin Multiplatform** library for computing tidy, aesthetically pleasing tree visualizations. It implements the [Walker algorithm (Buchheim–Jünger–Leipert variant)](https://link.springer.com/chapter/10.1007/3-540-36151-0_32) in $O(n)$ time with zero platform dependencies — no JVM, Android, or iOS frameworks required. `TreeLayoutKMP` works with *any* tree structure you already have. You provide a thin adapter describing how to traverse your nodes; the library computes optimal (x, y) coordinates for every node in the tree. From bf7511fd1c4324d52944a505608dd01c55f1f0b7 Mon Sep 17 00:00:00 2001 From: Moritz Lindner Date: Wed, 20 May 2026 10:02:48 +0200 Subject: [PATCH 5/5] remove old test --- benchmark/README.md | 4 +- benchmark/build.gradle.kts | 2 +- .../linde9821/treelayout/benchmark/Main.kt | 21 ++-- library/build.gradle.kts | 1 - .../treelayout/sample/SampleAppTest.kt | 102 ------------------ 5 files changed, 14 insertions(+), 116 deletions(-) delete mode 100644 library/src/jvmTest/kotlin/io/github/linde9821/treelayout/sample/SampleAppTest.kt diff --git a/benchmark/README.md b/benchmark/README.md index fd64397..6cf7384 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -4,7 +4,9 @@ Performance benchmark for the TreeLayoutKMP library. ## What it does -Generates balanced trees with increasing node counts (stepping by 15K up to ~1.1M) and measures the time to compute a full layout for each. Results are plotted as a line chart and exported to `benchmark_results.png` using Kotlin's lets-plot library. +Generates balanced trees with increasing node counts (stepping by 15K up to ~1.1M) and measures the time to compute a +full layout for each. Results are plotted as a line chart and exported to `benchmark_results.png` using Kotlin's +lets-plot library. ## Output diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index b42f431..8e6e627 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -22,6 +22,6 @@ tasks.register("runBenchmark") { description = "Run the TreeLayoutKMP benchmark" mainClass.set("io.github.linde9821.treelayout.benchmark.MainKt") classpath = kotlin.jvm().compilations["main"].runtimeDependencyFiles + - kotlin.jvm().compilations["main"].output.allOutputs + kotlin.jvm().compilations["main"].output.allOutputs jvmArgs = listOf("-Xmx8g", "-XX:+UseG1GC", "-XX:+AlwaysPreTouch") } diff --git a/benchmark/src/jvmMain/kotlin/io/github/linde9821/treelayout/benchmark/Main.kt b/benchmark/src/jvmMain/kotlin/io/github/linde9821/treelayout/benchmark/Main.kt index c2e30b7..e5eb651 100644 --- a/benchmark/src/jvmMain/kotlin/io/github/linde9821/treelayout/benchmark/Main.kt +++ b/benchmark/src/jvmMain/kotlin/io/github/linde9821/treelayout/benchmark/Main.kt @@ -12,7 +12,6 @@ import org.jetbrains.letsPlot.label.ylab import org.jetbrains.letsPlot.letsPlot import org.jetbrains.letsPlot.scale.scaleColorManual import org.jetbrains.letsPlot.scale.scaleLinetypeManual -import org.jetbrains.letsPlot.scale.scaleSizeManual import org.jetbrains.letsPlot.themes.themeMinimal import kotlin.time.measureTime @@ -123,16 +122,16 @@ fun main() { val os = System.getProperty("os.name") val plot = letsPlot(data) { x = "nodes"; y = "time_ms" } + - geomLine(data = referenceData) { color = "series"; linetype = "series" } + - geomLine(data = measuredData) { color = "series"; linetype = "series" } + - geomPoint(data = measuredData, size = 1.5) { color = "series" } + - scaleColorManual(values = listOf("#999999", "#2166AC")) + - scaleLinetypeManual(values = listOf("dashed", "solid")) + - xlab("Number of Nodes") + - ylab("Time (ms)") + - labs(subtitle = "JVM $jvmVersion · $os") + - ggtitle("TreeLayoutKMP Benchmark") + - themeMinimal() + geomLine(data = referenceData) { color = "series"; linetype = "series" } + + geomLine(data = measuredData) { color = "series"; linetype = "series" } + + geomPoint(data = measuredData, size = 1.5) { color = "series" } + + scaleColorManual(values = listOf("#999999", "#2166AC")) + + scaleLinetypeManual(values = listOf("dashed", "solid")) + + xlab("Number of Nodes") + + ylab("Time (ms)") + + labs(subtitle = "JVM $jvmVersion · $os") + + ggtitle("TreeLayoutKMP Benchmark") + + themeMinimal() ggsave(plot, "benchmark_results.png", dpi = 150, w = 10.0, h = 6.0) println("\nChart written to benchmark_results.png") diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 448c3f7..7f42c81 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -1,6 +1,5 @@ @file:OptIn(ExperimentalWasmDsl::class) -import org.gradle.jvm.tasks.Jar import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget diff --git a/library/src/jvmTest/kotlin/io/github/linde9821/treelayout/sample/SampleAppTest.kt b/library/src/jvmTest/kotlin/io/github/linde9821/treelayout/sample/SampleAppTest.kt deleted file mode 100644 index be3ca4c..0000000 --- a/library/src/jvmTest/kotlin/io/github/linde9821/treelayout/sample/SampleAppTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.linde9821.treelayout.sample - -import io.github.linde9821.treelayout.walker.WalkerLayoutConfiguration -import io.github.linde9821.treelayout.walker.WalkerTreeLayout -import java.io.File -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -internal class SampleAppTest { - - private val tree = SampleNode( - "root", listOf( - SampleNode( - "A", listOf( - SampleNode("A1"), - SampleNode( - "A2", listOf( - SampleNode("A2a"), - SampleNode("A2b"), - SampleNode("A2c") - ) - ), - SampleNode("A3") - ) - ), - SampleNode("B"), - SampleNode( - "C", listOf( - SampleNode( - "C1", listOf( - SampleNode("C1x") - ) - ), - SampleNode("C2") - ) - ) - ) - ) - - private val adapter = SampleAdapter(tree) - private val layout = WalkerTreeLayout( - adapter = adapter, - configuration = WalkerLayoutConfiguration( - horizontalDistance = 5.0f, - verticalDistance = 2.0f - ) - ) - - @Test - fun layoutProducesPositionsForAllNodes(): Unit { - val result = layout.layout() - assertEquals(13, result.getPositions().size) - } - - @Test - fun maxDepthIsThree(): Unit { - val result = layout.layout() - assertEquals(3, result.getMaxDepth()) - } - - @Test - fun noNodesOverlap(): Unit { - val result = layout.layout() - val positions = result.getPositions().values.toList() - for (i in positions.indices) { - for (j in i + 1 until positions.size) { - val a = positions[i] - val b = positions[j] - assertTrue(a.x != b.x || a.y != b.y, "Two nodes share position ($a)") - } - } - } - - @Test - fun rootIsAtDepthZero(): Unit { - val result = layout.layout() - assertEquals(0.0f, result.getPosition(tree).y) - } - - @Test - fun exportLayoutToPngCreatesFile(): Unit { - val childrenMap = mapOf( - "root" to listOf("A", "B", "C"), - "A" to listOf("A1", "A2", "A3"), - "A2" to listOf("A2a", "A2b", "A2c"), - "C" to listOf("C1", "C2"), - "C1" to listOf("C1x"), - ) - val stringAdapter = StringTreeAdapter("root", childrenMap) - val stringLayout = WalkerTreeLayout( - adapter = stringAdapter, - configuration = WalkerLayoutConfiguration(horizontalDistance = 5.0f, verticalDistance = 2.0f) - ) - val result = stringLayout.layout() - val tmpFile = File.createTempFile("tree_layout_test", ".png") - tmpFile.deleteOnExit() - exportLayoutToPng(result, stringAdapter, tmpFile) - assertTrue(tmpFile.exists(), "PNG file should exist") - assertTrue(tmpFile.length() > 0, "PNG file should not be empty") - } -}