diff --git a/experiments/benchmark_cairo.nim b/experiments/benchmark_cairo.nim index 49f91ad..f34ea93 100644 --- a/experiments/benchmark_cairo.nim +++ b/experiments/benchmark_cairo.nim @@ -24,7 +24,7 @@ var a = newImage(1000, 1000) a.fill(rgba(0, 0, 0, 255)) timeIt "pixie": - let p = newPath() + var p: paths.Path p.moveTo(0, 0) p.lineTo(500, 0) p.lineTo(500, 500) diff --git a/experiments/benchmark_svg_cairo.nim b/experiments/benchmark_svg_cairo.nim new file mode 100644 index 0000000..51ebf16 --- /dev/null +++ b/experiments/benchmark_svg_cairo.nim @@ -0,0 +1,6 @@ +import svg_cairo, benchy + +let data = readFile("tests/images/svg/Ghostscript_Tiger.svg") + +timeIt "svg decode": + keep decodeSvg(data) diff --git a/experiments/svg_cairo.nim b/experiments/svg_cairo.nim new file mode 100644 index 0000000..db910aa --- /dev/null +++ b/experiments/svg_cairo.nim @@ -0,0 +1,365 @@ +## Load and Save SVG files. + +import chroma, pixie/images, pixie/common, pixie/paths, vmath, xmlparser, + xmltree, strutils, strutils, cairo + +type Path = paths.Path + +proc processCommands(c: ptr Context, path: paths.Path) = + c.newPath() + c.moveTo(0, 0) + for i, command in path.commands: + case command.kind + of Move: + c.moveTo(command.numbers[0], command.numbers[1]) + of Line: + c.lineTo(command.numbers[0], command.numbers[1]) + of HLine: + echo "HLine not yet supported for Cairo" + of VLine: + echo "VLine not yet supported for Cairo" + of Cubic: + c.curveTo( + command.numbers[0], + command.numbers[1], + command.numbers[2], + command.numbers[3], + command.numbers[4], + command.numbers[5] + ) + of SCubic: + echo "SCubic not yet supported for Cairo" + of Quad: + echo "Quad not supported by Cairo" + of TQuad: + echo "TQuad not supported by Cairo" + of Arc: + echo "Arc not yet supported for Cairo" + of RMove: + c.relMoveTo(command.numbers[0], command.numbers[1]) + of RLine: + c.relLineTo(command.numbers[0], command.numbers[1]) + of RHLine: + c.relLineTo(command.numbers[0], 0) + of RVLine: + c.relLineTo(0, command.numbers[0]) + of RCubic: + c.relCurveTo( + command.numbers[0], + command.numbers[1], + command.numbers[2], + command.numbers[3], + command.numbers[4], + command.numbers[5] + ) + of RSCubic: + # This is not correct but good enough for now + c.relLineTo( + command.numbers[2], + command.numbers[3] + ) + of RQuad: + echo "RQuad not supported by Cairo" + of RTQuad: + echo "RTQuad not supported by Cairo" + of RArc: + echo "RArc not yet supported for Cairo" + of Close: + c.closePath() + + checkStatus(c.status()) + +proc prepare( + c: ptr Context, + path: Path, + color: ColorRGBA, + mat: Mat3 +) = + let + color = color.color() + matrix = Matrix( + xx: mat[0, 0], + yx: mat[0, 1], + xy: mat[1, 0], + yy: mat[1, 1], + x0: mat[2, 0], + y0: mat[2, 1], + ) + c.setSourceRgba(color.r, color.g, color.b, color.a) + c.setMatrix(matrix.unsafeAddr) + c.processCommands(path) + +proc fillPath( + c: ptr Context, + path: Path, + color: ColorRGBA, + mat: Mat3 +) = + prepare(c, path, color, mat) + c.fill() + +proc strokePath( + c: ptr Context, + path: Path, + color: ColorRGBA, + strokeWidth: float32, + mat: Mat3 +) = + prepare(c, path, color, mat) + c.setLineWidth(strokeWidth) + c.stroke() + +const svgSignature* = " 0: + let index = remaining.find(")") + if index == -1: + failInvalidTransform(transform) + let f = remaining[0 .. index].strip() + remaining = remaining[index + 1 .. ^1] + + if f.startsWith("matrix("): + let arr = f[7 .. ^2].split(",") + if arr.len != 6: + failInvalidTransform(transform) + var m = mat3() + m[0] = parseFloat(arr[0]) + m[1] = parseFloat(arr[1]) + m[3] = parseFloat(arr[2]) + m[4] = parseFloat(arr[3]) + m[6] = parseFloat(arr[4]) + m[7] = parseFloat(arr[5]) + result.transform = result.transform * m + elif f.startsWith("translate("): + let + components = f[10 .. ^2].split(" ") + tx = parseFloat(components[0]) + ty = parseFloat(components[1]) + result.transform = result.transform * translate(vec2(tx, ty)) + elif f.startsWith("rotate("): + let angle = parseFloat(f[7 .. ^2]) * -PI / 180 + result.transform = result.transform * rotationMat3(angle) + else: + failInvalidTransform(transform) + +proc draw( + img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx] +) = + if node.kind != xnElement: + # Skip + return + + case node.tag: + of "title", "desc": + discard + + of "g": + let ctx = decodeCtx(ctxStack[^1], node) + ctxStack.add(ctx) + for child in node: + img.draw(child, ctxStack) + discard ctxStack.pop() + + of "path": + let + d = node.attr("d") + ctx = decodeCtx(ctxStack[^1], node) + path = parsePath(d) + if ctx.fill != ColorRGBA(): + img.fillPath(path, ctx.fill, ctx.transform) + if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: + img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) + + of "line": + let + ctx = decodeCtx(ctxStack[^1], node) + x1 = parseFloat(node.attr("x1")) + y1 = parseFloat(node.attr("y1")) + x2 = parseFloat(node.attr("x2")) + y2 = parseFloat(node.attr("y2")) + + var path: Path + path.moveTo(x1, y1) + path.lineTo(x2, y2) + path.closePath() + + if ctx.fill != ColorRGBA(): + img.fillPath(path, ctx.fill, ctx.transform) + if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: + img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) + + of "polyline", "polygon": + let + ctx = decodeCtx(ctxStack[^1], node) + points = node.attr("points") + + var vecs: seq[Vec2] + for pair in points.split(" "): + let parts = pair.split(",") + if parts.len != 2: + failInvalid() + vecs.add(vec2(parseFloat(parts[0]), parseFloat(parts[1]))) + + if vecs.len == 0: + failInvalid() + + var path: Path + path.moveTo(vecs[0]) + for i in 1 ..< vecs.len: + path.lineTo(vecs[i]) + + # The difference between polyline and polygon is whether we close the path + if node.tag == "polygon": + path.closePath() + + if ctx.fill != ColorRGBA(): + img.fillPath(path, ctx.fill, ctx.transform) + if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: + img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) + + of "rect": + let + ctx = decodeCtx(ctxStack[^1], node) + x = parseFloat(node.attr("x")) + y = parseFloat(node.attr("y")) + width = parseFloat(node.attr("width")) + height = parseFloat(node.attr("height")) + + var path: Path + path.rect(x, y, width, height) + + if ctx.fill != ColorRGBA(): + img.fillPath(path, ctx.fill, ctx.transform) + if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: + img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) + + of "circle", "ellipse": + # Reference for magic constant: + # https://dl3.pushbulletusercontent.com/a3fLVC8boTzRoxevD1OgCzRzERB9z2EZ/unknown.png + let ctx = decodeCtx(ctxStack[^1], node) + + var cx, cy: float32 # Default to 0.0 unless set by cx and cy on node + if node.attr("cx") != "": + cx = parseFloat(node.attr("cx")) + if node.attr("cy") != "": + cy = parseFloat(node.attr("cy")) + + var rx, ry: float32 + if node.tag == "circle": + rx = parseFloat(node.attr("r")) + ry = rx + else: + rx = parseFloat(node.attr("rx")) + ry = parseFloat(node.attr("ry")) + + let + magicX = (4.0 * (-1.0 + sqrt(2.0)) / 3) * rx + magicY = (4.0 * (-1.0 + sqrt(2.0)) / 3) * ry + + var path: Path + path.moveTo(cx + rx, cy) + path.bezierCurveTo(cx + rx, cy + magicY, cx + magicX, cy + ry, cx, cy + ry) + path.bezierCurveTo(cx - magicX, cy + ry, cx - rx, cy + magicY, cx - rx, cy) + path.bezierCurveTo(cx - rx, cy - magicY, cx - magicX, cy - ry, cx, cy - ry) + path.bezierCurveTo(cx + magicX, cy - ry, cx + rx, cy - magicY, cx + rx, cy) + path.closePath() + + if ctx.fill != ColorRGBA(): + img.fillPath(path, ctx.fill, ctx.transform) + if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: + img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) + + else: + raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") + +proc decodeSvg*(data: string): Image = + ## Render SVG file and return the image. + try: + let root = parseXml(data) + if root.tag != "svg": + failInvalid() + + let + viewBox = root.attr("viewBox") + box = viewBox.split(" ") + if parseInt(box[0]) != 0 or parseInt(box[1]) != 0: + failInvalid() + + let + width = parseInt(box[2]) + height = parseInt(box[3]) + var ctxStack = @[initCtx()] + result = newImage(width, height) + let + surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32) + c = surface.create() + for node in root: + c.draw(node, ctxStack) + surface.flush() + let pixels = cast[ptr UncheckedArray[array[4, uint8]]](surface.getData()) + for y in 0 ..< result.height: + for x in 0 ..< result.width: + let + bgra = pixels[result.dataIndex(x, y)] + rgba = rgba(bgra[2], bgra[1], bgra[0], bgra[3]) + result.setRgbaUnsafe(x, y, rgba) + result.toStraightAlpha() + except PixieError as e: + raise e + except: + raise newException(PixieError, "Unable to load SVG") diff --git a/experiments/test_svg_cairo.nim b/experiments/test_svg_cairo.nim new file mode 100644 index 0000000..20efe86 --- /dev/null +++ b/experiments/test_svg_cairo.nim @@ -0,0 +1,23 @@ +import svg_cairo, pixie, strformat + +const files = [ + "line01", + "polyline01", + "polygon01", + "rect01", + "rect02", + "circle01", + "ellipse01", + "triangle01", + "quad01", + "Ghostscript_Tiger" +] + +for file in files: + let + original = readFile(&"tests/images/svg/{file}.svg") + image = decodeSvg(original) + gold = readImage(&"tests/images/svg/{file}.png") + + # doAssert image.data == gold.data + image.writeFile(&"tests/images/svg/{file}.png")