All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.org.openrndr.svg.SVGElement.kt Maven / Gradle / Ivy

There is a newer version: 0.4.5-alpha2
Show newest version
package org.openrndr.svg

import mu.*
import org.jsoup.nodes.*
import org.openrndr.math.*
import org.openrndr.shape.*

private val logger = KotlinLogging.logger {}

internal sealed class SVGElement(element: Element?) {
    var tag: String = element?.tagName() ?: ""
    var id: String = element?.id() ?: ""

    open var style = Style()
    abstract fun handleAttribute(attribute: Attribute)

    // Any element can have a style attribute to pass down properties
    fun styleProperty(key: String, value: String) {
        when (key) {
            Prop.STROKE -> style.stroke = SVGParse.color(value)
            Prop.STROKE_OPACITY -> style.strokeOpacity = SVGParse.number(value)
            Prop.STROKE_WIDTH -> style.strokeWeight = SVGParse.length(value)
            Prop.STROKE_MITERLIMIT -> style.miterLimit = SVGParse.number(value)
            Prop.STROKE_LINECAP -> style.lineCap = SVGParse.lineCap(value)
            Prop.STROKE_LINEJOIN -> style.lineJoin = SVGParse.lineJoin(value)
            Prop.FILL -> style.fill = SVGParse.color(value)
            Prop.FILL_OPACITY -> style.fillOpacity = SVGParse.number(value)
            Prop.OPACITY -> style.opacity = SVGParse.number(value)
            else -> logger.warn("Unknown property: $key")
        }
    }

    /** Special case of parsing an inline style attribute. */
    fun inlineStyles(attribute: Attribute) {
        attribute.value.split(";").forEach {
            val result = it.split(":").map { s -> s.trim() }

            if (result.size >= 2) {
                styleProperty(result[0], result[1])
            }
        }
    }
}

/**  element */
internal class SVGSVGElement(element: Element) : SVGGroup(element) {
    var documentStyle: DocumentStyle = DocumentStyle()

    init {
        documentStyle.viewBox = SVGParse.viewBox(this.element)
        documentStyle.preserveAspectRatio = SVGParse.preserveAspectRatio(this.element)
    }

    var bounds = SVGParse.bounds(this.element)
}

/**  element but practically works with anything that has child elements */
internal open class SVGGroup(val element: Element, val elements: MutableList = mutableListOf()) :
    SVGElement(element) {

    init {
        this.element.attributes().forEach {
            if (it.key == Attr.STYLE) {
                inlineStyles(it)
            } else {
                handleAttribute(it)
            }
        }

        handleChildren()
    }

    private fun handleChildren() {
        this.element.children().forEach { child ->
            when (child.tagName()) {
                in Tag.graphicsList -> elements.add(SVGPath(child))
                else -> elements.add(SVGGroup(child))
            }
        }
    }

    override fun handleAttribute(attribute: Attribute) {
        when (attribute.key) {
            // Attributes can also be style properties, in which case they're passed on
            in Prop.list -> styleProperty(attribute.key, attribute.value)
            Attr.TRANSFORM -> style.transform = SVGParse.transform(this.element)
        }
    }
}

internal class Command(val op: String, vararg val operands: Double) {
    fun asVectorList(): List? {
        return if (operands.size % 2 == 0) {
            operands.asList().chunked(2) { Vector2(it[0], it[1]) }
        } else {
            null
        }
    }
}

// For evaluating elliptical arc arguments according to the SVG spec
internal fun Double.toBoolean(): Boolean? = when (this) {
    0.0 -> false
    1.0 -> true
    else -> null
}

internal class SVGPath(val element: Element? = null) : SVGElement(element) {
    val commands = mutableListOf()

    private fun compounds(): List {
        val compounds = mutableListOf()
        val compoundIndices = mutableListOf()

        commands.forEachIndexed { index, it ->
            if (it.op == "M" || it.op == "m") {
                compoundIndices.add(index)
            }
        }

        compoundIndices.forEachIndexed { index, _ ->
            val cs = compoundIndices[index]
            val ce = if (index + 1 < compoundIndices.size) (compoundIndices[index + 1]) else commands.size

            // TODO: We shouldn't be making new SVGPaths without Elements to provide.
            // Then we could make SVGPath's constructor non-nullable
            val path = SVGPath()
            path.commands.addAll(commands.subList(cs, ce))

            compounds.add(path)
        }
        return compounds
    }

    fun shape(): Shape {
        var cursor = Vector2.ZERO
        var anchor = Vector2.ZERO
        // Still problematic
        var prevCubicCtrlPoint: Vector2? = null
        var prevQuadCtrlPoint: Vector2? = null

        val contours = compounds().map { compound ->
            val segments = mutableListOf()
            var closed = false
            // If an argument is invalid, an error is logged,
            // further interpreting is stopped and compound is returned as-is.
            compound.commands.forEach { command ->

                if (command.op !in listOf("z", "Z") && command.operands.isEmpty()) {
                    logger.error("Invalid amount of arguments provided for: ${command.op}")
                    return@forEach
                }

                val points = command.asVectorList()

                // TODO: Rethink this check
                if (points == null && command.op.lowercase() !in listOf("a", "h", "v")) {
                    logger.error("Invalid amount of arguments provided for: ${command.op}")
                    return@forEach
                }

                when (command.op) {
                    "A", "a" -> {
                        // If size == step, only the last window can be partial
                        // Special case as it also has boolean values
                        val contours = command.operands.toList().windowed(7, 7, true).map m@{
                            if (it.size != 7) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val rx = it[0]
                                val ry = it[1]
                                val xAxisRot = it[2]
                                val largeArcFlag = it[3].toBoolean()
                                val sweepFlag = it[4].toBoolean()

                                if (largeArcFlag == null || sweepFlag == null || rx == 0.0 || ry == 0.0) {
                                    logger.error("Invalid values provided for: ${command.op}")
                                    return@forEach
                                }

                                val end = Vector2(it[5], it[6]).let { v ->
                                    if (command.op == "a") {
                                        v + cursor
                                    } else {
                                        v
                                    }
                                }

                                contour {
                                    moveTo(cursor)
                                    arcTo(rx, ry, xAxisRot, largeArcFlag, sweepFlag, end)
                                    cursor = end
                                }
                            }
                        }

                        // I don't know why we can't just have segments from the above map,
                        // but this is the only way this works.
                        segments += contours.flatMap { it.segments}
                    }
                    "M" -> {
                        // TODO: Log an error when this nulls
                        cursor = points!!.firstOrNull() ?: return@forEach
                        anchor = cursor

                        // Following points are implicit lineto arguments
                        segments += points.drop(1).map {
                            Segment(cursor, it).apply {
                                cursor = it
                            }
                        }
                    }
                    "m" -> {
                        // TODO: Log an error when this nulls
                        cursor += points!!.firstOrNull() ?: return@forEach
                        anchor = cursor

                        // Following points are implicit lineto arguments
                        segments += points.drop(1).map {
                            Segment(cursor, cursor + it).apply {
                                cursor += it
                            }
                        }
                    }
                    "L" -> {
                        segments += points!!.map {
                            Segment(cursor, it).apply {
                                cursor = it
                            }
                        }
                    }
                    "l" -> {
                        segments += points!!.map {
                            Segment(cursor, cursor + it).apply {
                                cursor += it
                            }
                        }
                    }
                    "H" -> {
                        segments += command.operands.map {
                            val target = Vector2(it, cursor.y)
                            Segment(cursor, target).apply {
                                cursor = target
                            }
                        }
                    }
                    "h" -> {
                        segments += command.operands.map {
                            val target = cursor + Vector2(it, 0.0)
                            Segment(cursor, target).apply {
                                cursor = target
                            }
                        }
                    }
                    "V" -> {
                        segments += command.operands.map {
                            val target = Vector2(cursor.x, it)
                            Segment(cursor, target).apply {
                                cursor = target
                            }
                        }
                    }
                    "v" -> {
                        segments += command.operands.map {
                            val target = cursor + Vector2(0.0, it)
                            Segment(cursor, target).apply {
                                cursor = target
                            }
                        }
                    }
                    "C" -> {
                        segments += points!!.windowed(3, 3, true).map {
                            if (it.size != 3) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val (cp1, cp2, target) = it
                                Segment(cursor, cp1, cp2, target).also {
                                    cursor = target
                                    prevCubicCtrlPoint = cp2
                                }
                            }
                        }
                    }
                    "c" -> {
                        segments += points!!.windowed(3, 3, true).map {
                            if (it.size != 3) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val (cp1, cp2, target) = it.map { v -> cursor + v }
                                Segment(cursor, cp1, cp2, target).apply {
                                    cursor = target
                                    prevCubicCtrlPoint = cp2
                                }
                            }
                        }
                    }
                    "S" -> {
                        segments += points!!.windowed(2, 2, true).map {
                            if (it.size != 2) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor)
                                val (cp2, target) = it
                                Segment(cursor, cp1, cp2, target).also {
                                    cursor = target
                                    prevCubicCtrlPoint = cp2
                                }
                            }
                        }
                    }
                    "s" -> {
                        segments += points!!.windowed(2, 2, true).map {
                            if (it.size != 2) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor)
                                val (cp2, target) = it.map { v -> cursor + v }
                                Segment(cursor, cp1, cp2, target).also {
                                    cursor = target
                                    prevCubicCtrlPoint = cp2
                                }
                            }
                        }
                    }
                    "Q" -> {
                        segments += points!!.windowed(2, 2, true).map {
                            if (it.size != 2) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val (cp, target) = it
                                Segment(cursor, cp, target).also {
                                    cursor = target
                                    prevQuadCtrlPoint = cp
                                }
                            }
                        }
                    }
                    "q" -> {
                        segments += points!!.windowed(2, 2, true).map {
                            if (it.size != 2) {
                                logger.error("Invalid amount of arguments provided for: ${command.op}")
                                return@forEach
                            } else {
                                val (cp, target) = it.map { v -> cursor + v }
                                Segment(cursor, cp, target).also {
                                    cursor = target
                                    prevQuadCtrlPoint = cp
                                }
                            }
                        }
                    }
                    "T" -> {
                        points!!.forEach {
                            val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor)
                            Segment(cursor, cp, it).also { _ ->
                                cursor = it
                                prevQuadCtrlPoint = cp
                            }
                        }
                    }
                    "t" -> {
                        points!!.forEach {
                            val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor)
                            Segment(cursor, cp, cursor + it).also { _ ->
                                cursor = it
                                prevQuadCtrlPoint = cp
                            }
                        }
                    }
                    "Z", "z" -> {
                        if ((cursor - anchor).length >= 0.001) {
                            segments += Segment(cursor, anchor)
                        }
                        cursor = anchor
                        closed = true
                    }
                    else -> {
                        // The spec declares we should still attempt to render
                        // the path up until the erroneous command as to visually
                        // signal the user where the error occurred.
                        logger.error("Invalid path operator: ${command.op}")
                        return@forEach
                    }
                }
            }
            ShapeContour(segments, closed, YPolarity.CW_NEGATIVE_Y)
        }
        return Shape(contours)
    }

    override fun handleAttribute(attribute: Attribute) {
        if (this.element is Element) {
            when (attribute.key) {
                // Attributes can also be style properties, in which case they're passed on
                in Prop.list -> styleProperty(attribute.key, attribute.value)
                Attr.TRANSFORM -> style.transform = SVGParse.transform(this.element)
            }
        }
    }

    init {
        if (this.element is Element) {
            commands += when (tag) {
                Tag.PATH -> SVGParse.path(this.element)
                Tag.LINE -> SVGParse.line(this.element)
                Tag.RECT -> SVGParse.rectangle(this.element)
                Tag.ELLIPSE -> SVGParse.ellipse(this.element)
                Tag.CIRCLE -> SVGParse.circle(this.element)
                Tag.POLYGON -> SVGParse.polygon(this.element)
                Tag.POLYLINE -> SVGParse.polyline(this.element)
                else -> emptyList()
            }

            element.attributes().forEach {
                if (it.key == Attr.STYLE) {
                    inlineStyles(it)
                } else {
                    handleAttribute(it)
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy