PNG export

This commit is contained in:
Alexander Nozik 2024-10-03 09:22:42 +03:00
parent bd2804d772
commit 4e08680b22
9 changed files with 190 additions and 92 deletions

View File

@ -4,6 +4,7 @@
### Added ### Added
- `alpha` extension for feature attribute builder - `alpha` extension for feature attribute builder
- PNG export
### Changed ### Changed
- avoid drawing features with VisibleAttribute false - avoid drawing features with VisibleAttribute false

View File

@ -4,21 +4,20 @@ import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.maps.features.FeatureStore import space.kscience.maps.features.*
import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color
import space.kscience.maps.scheme.* import space.kscience.maps.scheme.*
import space.kscience.maps.svg.exportToPng
import space.kscience.maps.svg.exportToSvg import space.kscience.maps.svg.exportToSvg
import space.kscience.maps.svg.snapshot
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Files import java.nio.file.Files
@ -57,19 +56,31 @@ fun App() {
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) } var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
val snapshot = key(features) { val painterCache = features.pointerCache()
features.snapshot()
} val textMeasurer = rememberTextMeasurer()
ContextMenuArea( ContextMenuArea(
items = { items = {
listOf( listOf(
ContextMenuItem("Export to SVG") { ContextMenuItem("Export to SVG") {
val path = Files.createTempFile("scheme-kt-", ".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()) println(path.toFile())
Desktop.getDesktop().browse(path.toFile().toURI()) 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())
}
) )
} }
) { ) {

View File

@ -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<Gmc>.exportToSvg(
mapTileProvider: MapTileProvider,
viewPoint: ViewPoint<Gmc>,
painterCache: Map<PainterFeature<Gmc>, 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<Gmc>.exportToPng(
mapTileProvider: MapTileProvider,
viewPoint: ViewPoint<Gmc>,
painterCache: Map<PainterFeature<Gmc>, 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)
}
}

View File

@ -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<Gmc>.exportToSvg(
mapTileProvider: MapTileProvider,
viewPoint: ViewPoint<Gmc>,
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)
}

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -75,6 +76,11 @@ public class ComposeFeatureDrawScope<T : Any>(
} }
} }
@Composable
public fun <T: Any> FeatureSet<T>.pointerCache(): Map<PainterFeature<T>, Painter> = key(features) {
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
}
/** /**
* Create a canvas with extended functionality (e.g., drawing text) * Create a canvas with extended functionality (e.g., drawing text)
@ -90,7 +96,7 @@ public fun <T : Any> FeatureCanvas(
) { ) {
val textMeasurer = rememberTextMeasurer(0) val textMeasurer = rememberTextMeasurer(0)
val features by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value) val features: Map<String, Feature<T>> by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value)
val painterCache = features.values val painterCache = features.values
.filterIsInstance<PainterFeature<T>>() .filterIsInstance<PainterFeature<T>>()

View File

@ -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 <T : Any> FeatureSet<T>.generateBitmap(
canvasState: CanvasState<T>,
painterCache: Map<PainterFeature<T>, 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
}

View File

@ -1,6 +1,5 @@
package space.kscience.maps.svg package space.kscience.maps.svg
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import space.kscience.attributes.Attributes import space.kscience.attributes.Attributes
@ -8,20 +7,30 @@ import space.kscience.attributes.plus
import space.kscience.maps.features.* import space.kscience.maps.features.*
public class FeatureSetSnapshot<T : Any>( public fun <T : Any> FeatureDrawScope<T>.features(featureSet: FeatureSet<T>) {
public val features: Map<String, Feature<T>>, featureSet.features.entries.sortedBy { it.value.z }
internal val painterCache: Map<PainterFeature<T>, Painter>, .filter { state.viewPoint.zoom in it.value.zoomRange }
) .forEach { (id, feature) ->
val attributesCache = mutableMapOf<List<String>, Attributes>()
@Composable fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path) {
public fun <T : Any> FeatureSet<T>.snapshot(): FeatureSetSnapshot<T> = FeatureSetSnapshot( if (path.isEmpty()) return Attributes.EMPTY
features, else if (path.size == 1) {
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() } 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 <T : Any> FeatureSetSnapshot<T>.generateSvg( public fun <T : Any> FeatureSet<T>.generateSvg(
canvasState: CanvasState<T>, canvasState: CanvasState<T>,
painterCache: Map<PainterFeature<T>, Painter>,
id: String? = null, id: String? = null,
): String { ): String {
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D( val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(
@ -30,25 +39,7 @@ public fun <T : Any> FeatureSetSnapshot<T>.generateSvg(
) )
val svgScope = SvgDrawScope(canvasState, svgGraphics2D, painterCache) val svgScope = SvgDrawScope(canvasState, svgGraphics2D, painterCache)
svgScope.apply { svgScope.features(this)
features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { (id, feature) ->
val attributesCache = mutableMapOf<List<String>, Attributes>()
fun computeGroupAttributes(path: List<String>): 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) return svgGraphics2D.getSVGElement(id)
} }

View File

@ -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<XY>.exportToSvg(
viewPoint: ViewPoint<XY>,
painterCache: Map<PainterFeature<XY>, 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<XY>.exportToPng(
viewPoint: ViewPoint<XY>,
painterCache: Map<PainterFeature<XY>, 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)
}
}

View File

@ -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<XY>.exportToSvg(
viewPoint: ViewPoint<XY>,
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)
}