org.openrndr.svg.SVGWriter.kt Maven / Gradle / Ivy
package org.openrndr.svg
import org.jsoup.nodes.Entities
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.shape.*
import java.io.File
fun Composition.saveToFile(file: File) {
if (file.extension == "svg") {
val svg = writeSVG(this)
file.writeText(svg)
} else {
throw IllegalArgumentException("can only write svg files, the extension '${file.extension}' is not supported")
}
}
fun Composition.toSVG() = writeSVG(this)
private val CompositionNode.svgId: String
get() = if (id != null) {
"id=\"${id ?: error("id = null")}\""
} else {
""
}
private val CompositionNode.svgAttributes: String
get() {
return attributes.map {
if (it.value != null) {
"${it.key}=\"${Entities.escape(it.value)}\""
} else {
"${it.key}"
}
}.joinToString(" ")
}
fun writeSVG(composition: Composition,
topLevelId: String = "openrndr-svg"): String {
val sb = StringBuilder()
sb.append("\n")
sb.append("\n")
val defaultNamespaces = mapOf(
"xmlns" to "http://www.w3.org/2000/svg",
"xmlns:xlink" to "http://www.w3.org/1999/xlink"
)
val namespaces = (defaultNamespaces + composition.namespaces).map { (k, v) ->
"""$k="$v""""
}.joinToString(" ")
fun Rectangle.svgAttributes() = mapOf("x" to corner.x.toInt().toString(),
"y" to corner.y.toInt().toString(),
"width" to width.toInt(),
"height" to height.toInt())
.map { """${it.key}="${it.value}px"""" }.joinToString(" ")
sb.append("")
return sb.toString()
}
private val ColorRGBa.svg: String
get() {
val ir = (r.coerceIn(0.0, 1.0) * 255.0).toInt()
val ig = (g.coerceIn(0.0, 1.0) * 255.0).toInt()
val ib = (b.coerceIn(0.0, 1.0) * 255.0).toInt()
return String.format("#%02x%02x%02x", ir, ig, ib)
}
private val Matrix44.svgTransform get() = if (this == Matrix44.IDENTITY) null else "matrix(${this.c0r0}, ${this.c0r1}, ${this.c1r0}, ${this.c1r1}, ${this.c3r0}, ${this.c3r1})"
private val Shape.svg: String
get() {
val sb = StringBuilder()
contours.forEach {
it.segments.forEachIndexed { index, segment ->
if (index == 0) {
sb.append("M ${segment.start.x}, ${segment.start.y}")
}
sb.append(when (segment.control.size) {
1 -> "Q${segment.control[0].x}, ${segment.control[0].y}, ${segment.end.x}, ${segment.end.y}"
2 -> "C${segment.control[0].x}, ${segment.control[0].y}, ${segment.control[1].x}, ${segment.control[1].y}, ${segment.end.x}, ${segment.end.y}"
else -> "L${segment.end.x}, ${segment.end.y}"
})
}
if (it.closed) {
sb.append("Z ")
}
}
return sb.toString()
}
private val ShapeContour.svg: String
get() {
val sb = StringBuilder()
segments.forEachIndexed { index, segment ->
if (index == 0) {
sb.append("M ${segment.start.x}, ${segment.start.y}")
}
sb.append(when (segment.control.size) {
1 -> "C${segment.control[0].x}, ${segment.control[0].y}, ${segment.end.x}, ${segment.end.y}"
2 -> "C${segment.control[0].x}, ${segment.control[0].y}, ${segment.control[1].x}, ${segment.control[1].y}, ${segment.end.x}, ${segment.end.y}"
else -> "L${segment.end.x}, ${segment.end.y}"
})
}
if (closed) {
sb.append("Z ")
}
return sb.toString()
}
private enum class VisitStage {
PRE,
POST
}
private fun process(compositionNode: CompositionNode, visitor: CompositionNode.(stage: VisitStage) -> Unit) {
compositionNode.visitor(VisitStage.PRE)
if (compositionNode is GroupNode) {
compositionNode.children.forEach { process(it, visitor) }
}
compositionNode.visitor(VisitStage.POST)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy