diff --git a/src/pixie.nim b/src/pixie.nim index bc4862d..2f11d08 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -3,7 +3,7 @@ import bumpy, chroma, flatty/binny, os, pixie/blends, pixie/common, pixie/fileformats/svg, pixie/gradients, pixie/images, pixie/masks, pixie/paths, vmath -export blends, bumpy, chroma, common, gradients, images, masks, paths, vmath +export blends, bumpy, chroma, common, gradients, images, masks, paths, svg, vmath type FileFormat* = enum @@ -17,8 +17,6 @@ proc decodeImage*(data: string | seq[uint8]): Image = decodeJpg(data) elif data.len > 2 and data.readStr(0, 2) == bmpSignature: decodeBmp(data) - elif data.len > 5 and data.readStr(0, 5) == svgSignature: - decodeSvg(data) else: raise newException(PixieError, "Unsupported image file format") diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index c9e2ec5..e36a34c 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -3,9 +3,8 @@ import chroma, pixie/common, pixie/images, pixie/paths, strutils, vmath, xmlparser, xmltree -const svgSignature* = " 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")) + x1 = parseFloat(node.attrOrDefault("x1", "0")) + y1 = parseFloat(node.attrOrDefault("y1", "0")) + x2 = parseFloat(node.attrOrDefault("x2", "0")) + y2 = parseFloat(node.attrOrDefault("y2", "0")) var path: Path path.moveTo(x1, y1) @@ -202,16 +222,14 @@ proc draw( of "rect": let ctx = decodeCtx(ctxStack[^1], node) - x = parseFloat(node.attr("x")) - y = parseFloat(node.attr("y")) + x = parseFloat(node.attrOrDefault("x", "0")) + y = parseFloat(node.attrOrDefault("y", "0")) width = parseFloat(node.attr("width")) height = parseFloat(node.attr("height")) - var rx, ry: float32 - if node.attr("rx").len > 0: - rx = max(parseFloat(node.attr("rx")), 0) - if node.attr("ry").len > 0: - ry = max(parseFloat(node.attr("ry")), 0) + var + rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0) + ry = max(parseFloat(node.attrOrDefault("ry", "0")), 0) var path: Path if rx > 0 or ry > 0: @@ -240,21 +258,18 @@ proc draw( img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) of "circle", "ellipse": - 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")) + let + ctx = decodeCtx(ctxStack[^1], node) + cx = parseFloat(node.attrOrDefault("cx", "0")) + cy = parseFloat(node.attrOrDefault("cy", "0")) 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")) + rx = parseFloat(node.attrOrDefault("rx", "0")) + ry = parseFloat(node.attrOrDefault("ry", "0")) var path: Path path.ellipse(cx, cy, rx, ry) @@ -267,7 +282,7 @@ proc draw( else: raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") -proc decodeSvg*(data: string): Image = +proc decodeSvg*(data: string, width = 0, height = 0): Image = ## Render SVG file and return the image. try: let root = parseXml(data) @@ -277,14 +292,26 @@ proc decodeSvg*(data: string): Image = 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) + viewBoxWidth = parseInt(box[2]) + viewBoxHeight = parseInt(box[3]) + + var rootCtx = initCtx() + if width == 0 and height == 0: # Default to the view box size + result = newImage(viewBoxWidth, viewBoxHeight) + else: + result = newImage(width, height) + + let + scaleX = width.float32 / viewBoxWidth.float32 + scaleY = height.float32 / viewBoxHeight.float32 + rootCtx.transform = scale(vec2(scaleX, scaleY)) + + var ctxStack = @[rootCtx] for node in root: result.draw(node, ctxStack) result.toStraightAlpha() diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 40d064f..89be30e 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -43,6 +43,36 @@ proc parameterCount(kind: PathCommandKind): int = of SCubic, RSCubic, Quad, RQuad: 4 of Arc, RArc: 7 +proc `$`*(path: Path): string = + for i, command in path.commands: + case command.kind + of Move: result.add "M" + of Line: result.add "L" + of HLine: result.add "H" + of VLine: result.add "V" + of Cubic: result.add "C" + of SCubic: result.add "S" + of Quad: result.add "Q" + of TQuad: result.add "T" + of Arc: result.add "A" + of RMove: result.add "m" + of RLine: result.add "l" + of RHLine: result.add "h" + of RVLine: result.add "v" + of RCubic: result.add "c" + of RSCubic: result.add "s" + of RQuad: result.add "q" + of RTQuad: result.add "t" + of RArc: result.add "a" + of Close: result.add "Z" + for j, number in command.numbers: + if floor(number) == number: + result.add $number.int + else: + result.add $number + if i != path.commands.len - 1 or j != command.numbers.len - 1: + result.add " " + proc parsePath*(path: string): Path = ## Converts a SVG style path into seq of commands. @@ -51,7 +81,7 @@ proc parsePath*(path: string): Path = var p, numberStart: int - armed: bool + armed, hitDecimal: bool kind: PathCommandKind numbers: seq[float32] @@ -62,6 +92,7 @@ proc parsePath*(path: string): Path = except ValueError: raise newException(PixieError, "Invalid path, parsing paramter failed") numberStart = 0 + hitDecimal = false proc finishCommand(result: var Path) = finishNumber() @@ -157,6 +188,12 @@ proc parsePath*(path: string): Path = else: finishNumber() numberStart = p + of '.': + if hitDecimal: + finishNumber() + hitDecimal = true + if numberStart == 0: + numberStart = p of ' ', ',', '\r', '\n', '\t': finishNumber() else: @@ -167,36 +204,6 @@ proc parsePath*(path: string): Path = finishCommand(result) -proc `$`*(path: Path): string = - for i, command in path.commands: - case command.kind - of Move: result.add "M" - of Line: result.add "L" - of HLine: result.add "H" - of VLine: result.add "V" - of Cubic: result.add "C" - of SCubic: result.add "S" - of Quad: result.add "Q" - of TQuad: result.add "T" - of Arc: result.add "A" - of RMove: result.add "m" - of RLine: result.add "l" - of RHLine: result.add "h" - of RVLine: result.add "v" - of RCubic: result.add "c" - of RSCubic: result.add "s" - of RQuad: result.add "q" - of RTQuad: result.add "t" - of RArc: result.add "a" - of Close: result.add "Z" - for j, number in command.numbers: - if floor(number) == number: - result.add $number.int - else: - result.add $number - if i != path.commands.len - 1 or j != command.numbers.len - 1: - result.add " " - proc transform*(path: var Path, mat: Mat3) = for command in path.commands.mitems: case command.kind: diff --git a/tests/images/svg/twbs-icons.png b/tests/images/svg/twbs-icons.png new file mode 100644 index 0000000..cc2b58f Binary files /dev/null and b/tests/images/svg/twbs-icons.png differ diff --git a/tests/megatest.nim b/tests/megatest.nim new file mode 100644 index 0000000..c07c7dc --- /dev/null +++ b/tests/megatest.nim @@ -0,0 +1,34 @@ +import os, pixie, strformat + +# Clone https://github.com/twbs/icons +# Check out commit f364cb14dfc0703b9e3ef10c8b490a71dfef1e9d + +const + iconsPath = "../icons/icons/*" + width = 32 + height = 32 + +var images: seq[(string, Image)] + +for path in walkFiles(iconsPath): + let + (_, name, _) = splitFile(path) + image = decodeSvg(readFile(path), width, height) + + images.add((name, image)) + +let + columns = 10 + rows = (images.len + columns - 1) div columns + rendered = newImage((width + 4) * columns, (height + 4) * rows) + +for i in 0 ..< rows: + for j in 0 ..< max(images.len - i * columns, 0): + let (_, icon) = images[i * columns + j] + rendered.draw( + icon, + vec2(((width + 4) * j + 2).float32, ((height + 4) * i + 2).float32), + bmOverwrite + ) + +rendered.writeFile(&"tests/images/svg/twbs-icons.png")