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
- `alpha` extension for feature attribute builder
- PNG export
### Changed
- 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.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<XY> 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())
}
)
}
) {

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.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<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)
@ -90,7 +96,7 @@ public fun <T : Any> FeatureCanvas(
) {
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
.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
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<T : Any>(
public val features: Map<String, Feature<T>>,
internal val painterCache: Map<PainterFeature<T>, Painter>,
)
public fun <T : Any> FeatureDrawScope<T>.features(featureSet: FeatureSet<T>) {
featureSet.features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { (id, feature) ->
val attributesCache = mutableMapOf<List<String>, Attributes>()
@Composable
public fun <T : Any> FeatureSet<T>.snapshot(): FeatureSetSnapshot<T> = FeatureSetSnapshot(
features,
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)
fun computeGroupAttributes(path: List<String>): 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 <T : Any> FeatureSetSnapshot<T>.generateSvg(
public fun <T : Any> FeatureSet<T>.generateSvg(
canvasState: CanvasState<T>,
painterCache: Map<PainterFeature<T>, Painter>,
id: String? = null,
): String {
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(
@ -30,25 +39,7 @@ public fun <T : Any> FeatureSetSnapshot<T>.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<List<String>, Attributes>()
svgScope.features(this)
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)
}

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)
}