diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index 35e0b00..0592938 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -36,6 +36,13 @@ proc initCtx(): Ctx = proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = result = inherited + proc splitArgs(s: string): seq[string] = + # Handles (1,1) or (1 1) or (1, 1) or (1,1 2,2) etc + let tmp = s.replace(',', ' ').split(' ') + for entry in tmp: + if entry.len > 0: + result.add(entry) + var fillRule = node.attr("fill-rule") fill = node.attr("fill") @@ -157,7 +164,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = if strokeDashArray == "": discard else: - var values = strokeDashArray.replace(',', ' ').split(' ') + var values = splitArgs(strokeDashArray) for value in values: result.strokeDashArray.add(parseFloat(value)) @@ -178,55 +185,46 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = remaining = remaining[index + 1 .. ^1] if f.startsWith("matrix("): - let arr = - if f.contains(","): - f[7 .. ^2].split(",") - else: - f[7 .. ^2].split(" ") + let arr = splitArgs(f[7 .. ^2]) if arr.len != 6: failInvalidTransform(transform) var m = mat3() - m[0, 0] = parseFloat(arr[0].strip()) - m[0, 1] = parseFloat(arr[1].strip()) - m[1, 0] = parseFloat(arr[2].strip()) - m[1, 1] = parseFloat(arr[3].strip()) - m[2, 0] = parseFloat(arr[4].strip()) - m[2, 1] = parseFloat(arr[5].strip()) + m[0, 0] = parseFloat(arr[0]) + m[0, 1] = parseFloat(arr[1]) + m[1, 0] = parseFloat(arr[2]) + m[1, 1] = parseFloat(arr[3]) + m[2, 0] = parseFloat(arr[4]) + m[2, 1] = parseFloat(arr[5]) result.transform = result.transform * m elif f.startsWith("translate("): let - components = f[10 .. ^2].split(" ") - tx = parseFloat(components[0].strip()) + components = splitArgs(f[10 .. ^2]) + tx = parseFloat(components[0]) ty = - if components[1].len == 0: + if components.len == 1: 0.0 else: - parseFloat(components[1].strip()) + parseFloat(components[1]) result.transform = result.transform * translate(vec2(tx, ty)) elif f.startsWith("rotate("): let - values = f[7 .. ^2].split(" ") - angle: float32 = parseFloat(values[0].strip()) * -PI / 180 + values = splitArgs(f[7 .. ^2]) + angle: float32 = parseFloat(values[0]) * -PI / 180 var cx, cy: float32 if values.len > 1: - cx = parseFloat(values[1].strip()) + cx = parseFloat(values[1]) if values.len > 2: - cy = parseFloat(values[2].strip()) + cy = parseFloat(values[2]) let center = vec2(cx, cy) result.transform = result.transform * translate(center) * rotate(angle) * translate(-center) elif f.startsWith("scale("): let - values = - if f.contains(","): - f[6 .. ^2].split(",") - else: - f[6 .. ^2].split(" ") - let - sx: float32 = parseFloat(values[0].strip()) + values = splitArgs(f[6 .. ^2]) + sx: float32 = parseFloat(values[0]) sy: float32 = if values.len > 1: - parseFloat(values[1].strip()) + parseFloat(values[1]) else: sx result.transform = result.transform * scale(vec2(sx, sy)) @@ -242,6 +240,8 @@ proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} = ctx.stroke, ctx.transform, ctx.strokeWidth, + ctx.strokeLineCap, + ctx.strokeLineJoin, miterLimit = ctx.strokeMiterLimit, dashes = ctx.strokeDashArray ) @@ -330,8 +330,11 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) = ctx = decodeCtx(ctxStack[^1], node) x = parseFloat(node.attrOrDefault("x", "0")) y = parseFloat(node.attrOrDefault("y", "0")) - width = parseFloat(node.attr("width")) - height = parseFloat(node.attr("height")) + width = parseFloat(node.attrOrDefault("width", "0")) + height = parseFloat(node.attrOrDefault("height", "0")) + + if width == 0 or height == 0: + return var rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0) diff --git a/src/pixie/fonts.nim b/src/pixie/fonts.nim index 4159620..764b0b0 100644 --- a/src/pixie/fonts.nim +++ b/src/pixie/fonts.nim @@ -25,11 +25,11 @@ type font*: Font Arrangement* = ref object - spans*: seq[(int, int)] - fonts*: seq[Font] - runes*: seq[Rune] - positions*: seq[Vec2] - selectionRects*: seq[Rect] + spans*: seq[(int, int)] ## The (start, stop) of the spans in the text. + fonts*: seq[Font] ## The font for each span. + runes*: seq[Rune] ## The runes of the text. + positions*: seq[Vec2] ## The positions of the glyphs for each rune. + selectionRects*: seq[Rect] ## The selection rects for each glyph. HAlignMode* = enum haLeft @@ -110,6 +110,7 @@ proc defaultLineHeight*(font: Font): float32 {.inline.} = round(fontUnits * font.scale) proc newSpan*(text: string, font: Font): Span = + ## Creates a span, associating a font with the text. result = Span() result.text = text result.font = font @@ -369,6 +370,7 @@ proc typeset*( typeset(@[newSpan(text, font)], bounds, hAlign, vAlign, wrap) proc computeBounds*(arrangement: Arrangement): Vec2 = + ## Computes the width and height of the arrangement in pixels. if arrangement.runes.len > 0: for i in 0 ..< arrangement.runes.len: if arrangement.runes[i] != LF: @@ -382,6 +384,7 @@ proc computeBounds*(font: Font, text: string): Vec2 {.inline.} = font.typeset(text).computeBounds() proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} = + ## Computes the width and height of the spans in pixels. typeset(spans).computeBounds() proc parseOtf*(buf: string): Font = diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 9c82c71..70ce9fb 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -34,7 +34,7 @@ type commands*: seq[PathCommand] start, at: Vec2 # Maintained by moveTo, lineTo, etc. Used by arcTo. - SomePath* = Path | string | seq[seq[Vec2]] + SomePath* = Path | string const epsilon = 0.0001 * PI ## Tiny value used for some computations. @@ -1164,14 +1164,6 @@ proc fillShapes( stopY = min(image.height, (bounds.y + bounds.h).int) blender = blendMode.blender() - when defined(amd64) and not defined(pixieNoSimd): - let - blenderSimd = blendMode.blenderSimd() - first32 = cast[M128i]([uint32.high, 0, 0, 0]) # First 32 bits - oddMask = mm_set1_epi16(cast[int16](0xff00)) - div255 = mm_set1_epi16(cast[int16](0x8081)) - vColor = mm_set1_epi32(cast[int32](rgbx)) - var coverages = newSeq[uint8](image.width) hits = newSeq[(float32, int16)](4) @@ -1190,51 +1182,58 @@ proc fillShapes( # Apply the coverage and blend var x = startX when defined(amd64) and not defined(pixieNoSimd): - # When supported, SIMD blend as much as possible - for _ in countup(x, image.width - 16, 4): - var coverage = mm_loadu_si128(coverages[x].addr) - coverage = mm_and_si128(coverage, first32) - + if blendMode.hasSimdBlender(): + # When supported, SIMD blend as much as possible let - index = image.dataIndex(x, y) - eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) - if mm_movemask_epi8(eqZero) != 0xffff: - # If the coverages are not all zero - if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff: - # Coverages are all 255 - if rgbx.a == 255 and blendMode == bmNormal: - mm_storeu_si128(image.data[index].addr, vColor) + blenderSimd = blendMode.blenderSimd() + first32 = cast[M128i]([uint32.high, 0, 0, 0]) # First 32 bits + oddMask = mm_set1_epi16(cast[int16](0xff00)) + div255 = mm_set1_epi16(cast[int16](0x8081)) + vColor = mm_set1_epi32(cast[int32](rgbx)) + for _ in countup(x, image.width - 16, 4): + var coverage = mm_loadu_si128(coverages[x].addr) + coverage = mm_and_si128(coverage, first32) + + let + index = image.dataIndex(x, y) + eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) + if mm_movemask_epi8(eqZero) != 0xffff: + # If the coverages are not all zero + if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff: + # Coverages are all 255 + if rgbx.a == 255 and blendMode == bmNormal: + mm_storeu_si128(image.data[index].addr, vColor) + else: + let backdrop = mm_loadu_si128(image.data[index].addr) + mm_storeu_si128( + image.data[index].addr, + blenderSimd(backdrop, vColor) + ) else: + # Coverages are not all 255 + coverage = unpackAlphaValues(coverage) + # Shift the coverages from `a` to `g` and `a` for multiplying + coverage = mm_or_si128(coverage, mm_srli_epi32(coverage, 16)) + + var + source = vColor + sourceEven = mm_slli_epi16(source, 8) + sourceOdd = mm_and_si128(source, oddMask) + + sourceEven = mm_mulhi_epu16(sourceEven, coverage) + sourceOdd = mm_mulhi_epu16(sourceOdd, coverage) + + sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7) + sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7) + + source = mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8)) + let backdrop = mm_loadu_si128(image.data[index].addr) mm_storeu_si128( image.data[index].addr, - blenderSimd(backdrop, vColor) + blenderSimd(backdrop, source) ) - else: - # Coverages are not all 255 - coverage = unpackAlphaValues(coverage) - # Shift the coverages from `a` to `g` and `a` for multiplying - coverage = mm_or_si128(coverage, mm_srli_epi32(coverage, 16)) - - var - source = vColor - sourceEven = mm_slli_epi16(source, 8) - sourceOdd = mm_and_si128(source, oddMask) - - sourceEven = mm_mulhi_epu16(sourceEven, coverage) - sourceOdd = mm_mulhi_epu16(sourceOdd, coverage) - - sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7) - sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7) - - source = mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8)) - - let backdrop = mm_loadu_si128(image.data[index].addr) - mm_storeu_si128( - image.data[index].addr, - blenderSimd(backdrop, source) - ) - x += 4 + x += 4 while x < image.width: if x + 8 <= coverages.len: @@ -1328,11 +1327,11 @@ proc fillShapes( inc x proc miterLimitToAngle*(limit: float32): float32 = - ## Converts milter-limit-ratio to miter-limit-angle. + ## Converts miter-limit-ratio to miter-limit-angle. arcsin(1 / limit) * 2 proc angleToMiterLimit*(angle: float32): float32 = - ## Converts miter-limit-angle to milter-limit-ratio. + ## Converts miter-limit-angle to miter-limit-ratio. 1 / sin(angle / 2) proc strokeShapes( @@ -1528,11 +1527,7 @@ proc fillPath*( mask = newMask(image.width, image.height) fill = newImage(image.width, image.height) - mask.fillPath( - parseSomePath(path, transform.pixelScale()), - transform, - windingRule - ) + mask.fillPath(path, transform, windingRule) case paint.kind: of pkSolid: @@ -1604,7 +1599,7 @@ proc strokePath*( fill = newImage(image.width, image.height) mask.strokePath( - parseSomePath(path, transform.pixelScale()), + path, transform, strokeWidth, lineCap, diff --git a/tests/emoji_megatest.nim b/tests/emoji_megatest.nim new file mode 100644 index 0000000..0807ccc --- /dev/null +++ b/tests/emoji_megatest.nim @@ -0,0 +1,59 @@ +import cligen, os, pixie, pixie/fileformats/svg, strformat + +# Clone https://github.com/twitter/twemoji +# Check out commit 59cb0eacce837d0f5de30223bd8f530e447f547a + +# Clone https://github.com/hfg-gmuend/openmoji +# Check out commit c1f14ae0be29b20c7eed215d1e03df23b1c9a5d5 + +type EmojiSet = object + name: string + path: string + +const + emojiSets = [ + EmojiSet(name: "twemoji", path: "../twemoji/assets/svg/*"), + EmojiSet(name: "openmoji", path: "../openmoji/color/svg/*") + ] + width = 32 + height = 32 + +proc renderEmojiSet(index: int) = + let emojiSet = emojiSets[index] + + var images: seq[(string, Image)] + + for filePath in walkFiles(emojiSet.path): + let (_, name, _) = splitFile(filePath) + var image: Image + try: + image = decodeSvg(readFile(filePath), width, height) + except PixieError: + echo &"Failed decoding {name}" + image = newImage(width, height) + images.add((name, image)) + + let + columns = 40 + 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/{emojiSet.name}.png") + +proc main(index = -1) = + if index >= 0: + renderEmojiSet(index) + else: + for i in 0 ..< emojiSets.len: + renderEmojiSet(i) + +dispatch(main) diff --git a/tests/images/context/blendmode_1.png b/tests/images/context/blendmode_1.png new file mode 100644 index 0000000..a34bcdd Binary files /dev/null and b/tests/images/context/blendmode_1.png differ diff --git a/tests/images/svg/flat-color-icons.png b/tests/images/svg/flat-color-icons.png index 1af1b33..d121a74 100644 Binary files a/tests/images/svg/flat-color-icons.png and b/tests/images/svg/flat-color-icons.png differ diff --git a/tests/images/svg/ionicons.png b/tests/images/svg/ionicons.png index 3dbcea9..fe48a91 100644 Binary files a/tests/images/svg/ionicons.png and b/tests/images/svg/ionicons.png differ diff --git a/tests/images/svg/openmoji.png b/tests/images/svg/openmoji.png new file mode 100644 index 0000000..1b2931a Binary files /dev/null and b/tests/images/svg/openmoji.png differ diff --git a/tests/images/svg/tabler-icons.png b/tests/images/svg/tabler-icons.png index 9c838ee..646add2 100644 Binary files a/tests/images/svg/tabler-icons.png and b/tests/images/svg/tabler-icons.png differ diff --git a/tests/images/svg/twemoji.png b/tests/images/svg/twemoji.png new file mode 100644 index 0000000..363da42 Binary files /dev/null and b/tests/images/svg/twemoji.png differ diff --git a/tests/test_context.nim b/tests/test_context.nim index b3d3152..49c0437 100644 --- a/tests/test_context.nim +++ b/tests/test_context.nim @@ -515,3 +515,19 @@ block: drawDashedLine(@[12.float32, 3, 3]); image.writeFile("tests/images/context/setLineDash_1.png") + +block: + let + image = newImage(300, 150) + ctx = newContext(image) + + image.fill(rgba(255, 255, 255, 255)) + + var paint = Paint(kind: pkSolid, color: rgba(0, 0, 255, 255)) + paint.blendMode = bmExclusion + + ctx.fillStyle = paint + + ctx.fillRect(10, 10, 100, 100) + + image.writeFile("tests/images/context/blendmode_1.png")