From 4e08680b22e8c8ac4c958e516b8797035bf15092 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 3 Oct 2024 09:22:42 +0300 Subject: [PATCH] PNG export --- CHANGELOG.md | 1 + demo/scheme/src/jvmMain/kotlin/Main.kt | 29 +++++++--- .../space/kscience/maps/compose/mapExport.kt | 55 +++++++++++++++++++ .../kscience/maps/compose/mapExportToSvg.kt | 27 --------- .../maps/features/FeatureDrawScope.kt | 8 ++- .../space/kscience/maps/svg/exportToBitMap.kt | 34 ++++++++++++ .../space/kscience/maps/svg/exportToSvg.kt | 51 +++++++---------- .../space/kscience/maps/svg/schemeExport.kt | 52 ++++++++++++++++++ .../kscience/maps/svg/schemeExportToSvg.kt | 25 --------- 9 files changed, 190 insertions(+), 92 deletions(-) create mode 100644 maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExport.kt delete mode 100644 maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExportToSvg.kt create mode 100644 maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToBitMap.kt create mode 100644 maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExport.kt delete mode 100644 maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExportToSvg.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfd7af..a554400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - `alpha` extension for feature attribute builder +- PNG export ### Changed - avoid drawing features with VisibleAttribute false diff --git a/demo/scheme/src/jvmMain/kotlin/Main.kt b/demo/scheme/src/jvmMain/kotlin/Main.kt index 297bd98..6662ec7 100644 --- a/demo/scheme/src/jvmMain/kotlin/Main.kt +++ b/demo/scheme/src/jvmMain/kotlin/Main.kt @@ -4,21 +4,20 @@ import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ContextMenuItem import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.kmath.geometry.Angle -import space.kscience.maps.features.FeatureStore -import space.kscience.maps.features.ViewConfig -import space.kscience.maps.features.ViewPoint -import space.kscience.maps.features.color +import space.kscience.maps.features.* import space.kscience.maps.scheme.* +import space.kscience.maps.svg.exportToPng import space.kscience.maps.svg.exportToSvg -import space.kscience.maps.svg.snapshot import java.awt.Desktop import java.nio.file.Files @@ -57,19 +56,31 @@ fun App() { var viewPoint: ViewPoint by remember { mutableStateOf(initialViewPoint) } - val snapshot = key(features) { - features.snapshot() - } + val painterCache = features.pointerCache() + + val textMeasurer = rememberTextMeasurer() ContextMenuArea( items = { listOf( ContextMenuItem("Export to SVG") { val path = Files.createTempFile("scheme-kt-", ".svg") - snapshot.exportToSvg(viewPoint, 800.0, 800.0, path) + features.exportToSvg(viewPoint, painterCache, Size(800f, 800f), path) println(path.toFile()) Desktop.getDesktop().browse(path.toFile().toURI()) }, + ContextMenuItem("Export to PNG") { + val path = Files.createTempFile("scheme-kt-", ".png") + features.exportToPng( + viewPoint, + painterCache, + textMeasurer, + Size(800f, 800f), + path + ) + println(path.toFile()) + Desktop.getDesktop().browse(path.toFile().toURI()) + } ) } ) { diff --git a/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExport.kt b/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExport.kt new file mode 100644 index 0000000..1acefad --- /dev/null +++ b/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExport.kt @@ -0,0 +1,55 @@ +package space.kscience.maps.compose + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.asSkiaBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.jetbrains.skia.Image +import org.jfree.svg.SVGUtils +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.features.FeatureSet +import space.kscience.maps.features.PainterFeature +import space.kscience.maps.features.ViewConfig +import space.kscience.maps.features.ViewPoint +import space.kscience.maps.svg.generateBitmap +import space.kscience.maps.svg.generateSvg +import java.nio.file.Path +import kotlin.io.path.writeBytes + +public fun FeatureSet.exportToSvg( + mapTileProvider: MapTileProvider, + viewPoint: ViewPoint, + painterCache: Map, Painter>, + size: Size, + path: Path, +) { + val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply { + this.viewPoint = viewPoint + this.canvasSize = DpSize(size.width.dp, size.height.dp) + } + + val svgString: String = generateSvg(mapCanvasState, painterCache) + SVGUtils.writeToSVG(path.toFile(), svgString) +} + +public fun FeatureSet.exportToPng( + mapTileProvider: MapTileProvider, + viewPoint: ViewPoint, + painterCache: Map, Painter>, + textMeasurer: TextMeasurer, + size: Size, + path: Path, +) { + val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply { + this.viewPoint = viewPoint + this.canvasSize = DpSize(size.width.dp, size.height.dp) + } + + val bitmap = generateBitmap(mapCanvasState, painterCache, textMeasurer, size) + + Image.makeFromBitmap(bitmap.asSkiaBitmap()).encodeToData()?.bytes?.let { + path.writeBytes(it) + } +} \ No newline at end of file diff --git a/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExportToSvg.kt b/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExportToSvg.kt deleted file mode 100644 index 8b15723..0000000 --- a/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/mapExportToSvg.kt +++ /dev/null @@ -1,27 +0,0 @@ -package space.kscience.maps.compose - -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import org.jfree.svg.SVGUtils -import space.kscience.maps.coordinates.Gmc -import space.kscience.maps.features.ViewConfig -import space.kscience.maps.features.ViewPoint -import space.kscience.maps.svg.FeatureSetSnapshot -import space.kscience.maps.svg.generateSvg -import java.nio.file.Path - -public fun FeatureSetSnapshot.exportToSvg( - mapTileProvider: MapTileProvider, - viewPoint: ViewPoint, - width: Double, - height: Double, - path: Path, -) { - val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply { - this.viewPoint = viewPoint - this.canvasSize = DpSize(width.dp, height.dp) - } - - val svgString: String = generateSvg(mapCanvasState) - SVGUtils.writeToSVG(path.toFile(), svgString) -} \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt index 028fd9f..f8c3a1a 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -75,6 +76,11 @@ public class ComposeFeatureDrawScope( } } +@Composable +public fun FeatureSet.pointerCache(): Map, Painter> = key(features) { + features.values.filterIsInstance>().associateWith { it.getPainter() } +} + /** * Create a canvas with extended functionality (e.g., drawing text) @@ -90,7 +96,7 @@ public fun FeatureCanvas( ) { val textMeasurer = rememberTextMeasurer(0) - val features by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value) + val features: Map> by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value) val painterCache = features.values .filterIsInstance>() diff --git a/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToBitMap.kt b/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToBitMap.kt new file mode 100644 index 0000000..65e942b --- /dev/null +++ b/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToBitMap.kt @@ -0,0 +1,34 @@ +package space.kscience.maps.svg + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import space.kscience.maps.features.CanvasState +import space.kscience.maps.features.ComposeFeatureDrawScope +import space.kscience.maps.features.FeatureSet +import space.kscience.maps.features.PainterFeature + +public fun FeatureSet.generateBitmap( + canvasState: CanvasState, + painterCache: Map, Painter>, + textMeasurer: TextMeasurer, + size: Size +): ImageBitmap { + + val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt()) + + CanvasDrawScope().draw( + density = Density(1f), + layoutDirection = LayoutDirection.Ltr, + canvas = Canvas(bitmap), + size = size, + ) { + ComposeFeatureDrawScope(this, canvasState, painterCache, textMeasurer).features(this@generateBitmap) + } + return bitmap +} \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt b/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt index 2cf2cf3..4e8f2e2 100644 --- a/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt +++ b/maps-kt-features/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt @@ -1,6 +1,5 @@ package space.kscience.maps.svg -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter import org.jfree.svg.SVGGraphics2D import space.kscience.attributes.Attributes @@ -8,20 +7,30 @@ import space.kscience.attributes.plus import space.kscience.maps.features.* -public class FeatureSetSnapshot( - public val features: Map>, - internal val painterCache: Map, Painter>, -) +public fun FeatureDrawScope.features(featureSet: FeatureSet) { + featureSet.features.entries.sortedBy { it.value.z } + .filter { state.viewPoint.zoom in it.value.zoomRange } + .forEach { (id, feature) -> + val attributesCache = mutableMapOf, Attributes>() -@Composable -public fun FeatureSet.snapshot(): FeatureSetSnapshot = FeatureSetSnapshot( - features, - features.values.filterIsInstance>().associateWith { it.getPainter() } -) + fun computeGroupAttributes(path: List): Attributes = attributesCache.getOrPut(path) { + if (path.isEmpty()) return Attributes.EMPTY + else if (path.size == 1) { + featureSet.features[path.first()]?.attributes ?: Attributes.EMPTY + } else { + computeGroupAttributes(path.dropLast(1)) + + (featureSet.features[path.first()]?.attributes ?: Attributes.EMPTY) + } + } + val path = id.split("/") + drawFeature(feature, computeGroupAttributes(path.dropLast(1))) + } +} -public fun FeatureSetSnapshot.generateSvg( +public fun FeatureSet.generateSvg( canvasState: CanvasState, + painterCache: Map, Painter>, id: String? = null, ): String { val svgGraphics2D: SVGGraphics2D = SVGGraphics2D( @@ -30,25 +39,7 @@ public fun FeatureSetSnapshot.generateSvg( ) val svgScope = SvgDrawScope(canvasState, svgGraphics2D, painterCache) - svgScope.apply { - features.entries.sortedBy { it.value.z } - .filter { state.viewPoint.zoom in it.value.zoomRange } - .forEach { (id, feature) -> - val attributesCache = mutableMapOf, Attributes>() + svgScope.features(this) - fun computeGroupAttributes(path: List): Attributes = attributesCache.getOrPut(path) { - if (path.isEmpty()) return Attributes.EMPTY - else if (path.size == 1) { - features[path.first()]?.attributes ?: Attributes.EMPTY - } else { - computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes - ?: Attributes.EMPTY) - } - } - - val path = id.split("/") - drawFeature(feature, computeGroupAttributes(path.dropLast(1))) - } - } return svgGraphics2D.getSVGElement(id) } \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExport.kt b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExport.kt new file mode 100644 index 0000000..e6d571d --- /dev/null +++ b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExport.kt @@ -0,0 +1,52 @@ +package space.kscience.maps.svg + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.asSkiaBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.jetbrains.skia.Image +import org.jfree.svg.SVGUtils +import space.kscience.maps.features.FeatureSet +import space.kscience.maps.features.PainterFeature +import space.kscience.maps.features.ViewConfig +import space.kscience.maps.features.ViewPoint +import space.kscience.maps.scheme.XY +import space.kscience.maps.scheme.XYCanvasState +import java.nio.file.Path +import kotlin.io.path.writeBytes + +public fun FeatureSet.exportToSvg( + viewPoint: ViewPoint, + painterCache: Map, Painter>, + size: Size, + path: Path, +) { + val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply { + this.viewPoint = viewPoint + this.canvasSize = DpSize(size.width.dp, size.height.dp) + } + + val svgString: String = generateSvg(svgCanvasState, painterCache) + SVGUtils.writeToSVG(path.toFile(), svgString) +} + +public fun FeatureSet.exportToPng( + viewPoint: ViewPoint, + painterCache: Map, Painter>, + textMeasurer: TextMeasurer, + size: Size, + path: Path, +) { + val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply { + this.viewPoint = viewPoint + this.canvasSize = DpSize(size.width.dp, size.height.dp) + } + + val bitmap = generateBitmap(svgCanvasState, painterCache, textMeasurer, size) + + Image.makeFromBitmap(bitmap.asSkiaBitmap()).encodeToData()?.bytes?.let { + path.writeBytes(it) + } +} \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExportToSvg.kt b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExportToSvg.kt deleted file mode 100644 index 0383659..0000000 --- a/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/schemeExportToSvg.kt +++ /dev/null @@ -1,25 +0,0 @@ -package space.kscience.maps.svg - -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import org.jfree.svg.SVGUtils -import space.kscience.maps.features.ViewConfig -import space.kscience.maps.features.ViewPoint -import space.kscience.maps.scheme.XY -import space.kscience.maps.scheme.XYCanvasState -import java.nio.file.Path - -public fun FeatureSetSnapshot.exportToSvg( - viewPoint: ViewPoint, - width: Double, - height: Double, - path: Path, -) { - val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply { - this.viewPoint = viewPoint - this.canvasSize = DpSize(width.dp, height.dp) - } - - val svgString: String = generateSvg(svgCanvasState) - SVGUtils.writeToSVG(path.toFile(), svgString) -} \ No newline at end of file