svg bugfixes, improvements, emoji megatests
|
@ -36,6 +36,13 @@ proc initCtx(): Ctx =
|
||||||
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
result = inherited
|
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
|
var
|
||||||
fillRule = node.attr("fill-rule")
|
fillRule = node.attr("fill-rule")
|
||||||
fill = node.attr("fill")
|
fill = node.attr("fill")
|
||||||
|
@ -157,7 +164,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
if strokeDashArray == "":
|
if strokeDashArray == "":
|
||||||
discard
|
discard
|
||||||
else:
|
else:
|
||||||
var values = strokeDashArray.replace(',', ' ').split(' ')
|
var values = splitArgs(strokeDashArray)
|
||||||
for value in values:
|
for value in values:
|
||||||
result.strokeDashArray.add(parseFloat(value))
|
result.strokeDashArray.add(parseFloat(value))
|
||||||
|
|
||||||
|
@ -178,55 +185,46 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
remaining = remaining[index + 1 .. ^1]
|
remaining = remaining[index + 1 .. ^1]
|
||||||
|
|
||||||
if f.startsWith("matrix("):
|
if f.startsWith("matrix("):
|
||||||
let arr =
|
let arr = splitArgs(f[7 .. ^2])
|
||||||
if f.contains(","):
|
|
||||||
f[7 .. ^2].split(",")
|
|
||||||
else:
|
|
||||||
f[7 .. ^2].split(" ")
|
|
||||||
if arr.len != 6:
|
if arr.len != 6:
|
||||||
failInvalidTransform(transform)
|
failInvalidTransform(transform)
|
||||||
var m = mat3()
|
var m = mat3()
|
||||||
m[0, 0] = parseFloat(arr[0].strip())
|
m[0, 0] = parseFloat(arr[0])
|
||||||
m[0, 1] = parseFloat(arr[1].strip())
|
m[0, 1] = parseFloat(arr[1])
|
||||||
m[1, 0] = parseFloat(arr[2].strip())
|
m[1, 0] = parseFloat(arr[2])
|
||||||
m[1, 1] = parseFloat(arr[3].strip())
|
m[1, 1] = parseFloat(arr[3])
|
||||||
m[2, 0] = parseFloat(arr[4].strip())
|
m[2, 0] = parseFloat(arr[4])
|
||||||
m[2, 1] = parseFloat(arr[5].strip())
|
m[2, 1] = parseFloat(arr[5])
|
||||||
result.transform = result.transform * m
|
result.transform = result.transform * m
|
||||||
elif f.startsWith("translate("):
|
elif f.startsWith("translate("):
|
||||||
let
|
let
|
||||||
components = f[10 .. ^2].split(" ")
|
components = splitArgs(f[10 .. ^2])
|
||||||
tx = parseFloat(components[0].strip())
|
tx = parseFloat(components[0])
|
||||||
ty =
|
ty =
|
||||||
if components[1].len == 0:
|
if components.len == 1:
|
||||||
0.0
|
0.0
|
||||||
else:
|
else:
|
||||||
parseFloat(components[1].strip())
|
parseFloat(components[1])
|
||||||
result.transform = result.transform * translate(vec2(tx, ty))
|
result.transform = result.transform * translate(vec2(tx, ty))
|
||||||
elif f.startsWith("rotate("):
|
elif f.startsWith("rotate("):
|
||||||
let
|
let
|
||||||
values = f[7 .. ^2].split(" ")
|
values = splitArgs(f[7 .. ^2])
|
||||||
angle: float32 = parseFloat(values[0].strip()) * -PI / 180
|
angle: float32 = parseFloat(values[0]) * -PI / 180
|
||||||
var cx, cy: float32
|
var cx, cy: float32
|
||||||
if values.len > 1:
|
if values.len > 1:
|
||||||
cx = parseFloat(values[1].strip())
|
cx = parseFloat(values[1])
|
||||||
if values.len > 2:
|
if values.len > 2:
|
||||||
cy = parseFloat(values[2].strip())
|
cy = parseFloat(values[2])
|
||||||
let center = vec2(cx, cy)
|
let center = vec2(cx, cy)
|
||||||
result.transform = result.transform *
|
result.transform = result.transform *
|
||||||
translate(center) * rotate(angle) * translate(-center)
|
translate(center) * rotate(angle) * translate(-center)
|
||||||
elif f.startsWith("scale("):
|
elif f.startsWith("scale("):
|
||||||
let
|
let
|
||||||
values =
|
values = splitArgs(f[6 .. ^2])
|
||||||
if f.contains(","):
|
sx: float32 = parseFloat(values[0])
|
||||||
f[6 .. ^2].split(",")
|
|
||||||
else:
|
|
||||||
f[6 .. ^2].split(" ")
|
|
||||||
let
|
|
||||||
sx: float32 = parseFloat(values[0].strip())
|
|
||||||
sy: float32 =
|
sy: float32 =
|
||||||
if values.len > 1:
|
if values.len > 1:
|
||||||
parseFloat(values[1].strip())
|
parseFloat(values[1])
|
||||||
else:
|
else:
|
||||||
sx
|
sx
|
||||||
result.transform = result.transform * scale(vec2(sx, sy))
|
result.transform = result.transform * scale(vec2(sx, sy))
|
||||||
|
@ -242,6 +240,8 @@ proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} =
|
||||||
ctx.stroke,
|
ctx.stroke,
|
||||||
ctx.transform,
|
ctx.transform,
|
||||||
ctx.strokeWidth,
|
ctx.strokeWidth,
|
||||||
|
ctx.strokeLineCap,
|
||||||
|
ctx.strokeLineJoin,
|
||||||
miterLimit = ctx.strokeMiterLimit,
|
miterLimit = ctx.strokeMiterLimit,
|
||||||
dashes = ctx.strokeDashArray
|
dashes = ctx.strokeDashArray
|
||||||
)
|
)
|
||||||
|
@ -330,8 +330,11 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
ctx = decodeCtx(ctxStack[^1], node)
|
||||||
x = parseFloat(node.attrOrDefault("x", "0"))
|
x = parseFloat(node.attrOrDefault("x", "0"))
|
||||||
y = parseFloat(node.attrOrDefault("y", "0"))
|
y = parseFloat(node.attrOrDefault("y", "0"))
|
||||||
width = parseFloat(node.attr("width"))
|
width = parseFloat(node.attrOrDefault("width", "0"))
|
||||||
height = parseFloat(node.attr("height"))
|
height = parseFloat(node.attrOrDefault("height", "0"))
|
||||||
|
|
||||||
|
if width == 0 or height == 0:
|
||||||
|
return
|
||||||
|
|
||||||
var
|
var
|
||||||
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
|
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
|
||||||
|
|
|
@ -604,7 +604,7 @@ proc polygon*(path: var Path, pos: Vec2, size: float32, sides: int) {.inline.} =
|
||||||
## Adds a n-sided regular polygon at (x, y) with the parameter size.
|
## Adds a n-sided regular polygon at (x, y) with the parameter size.
|
||||||
path.polygon(pos.x, pos.y, size, sides)
|
path.polygon(pos.x, pos.y, size, sides)
|
||||||
|
|
||||||
proc commandsToShapes(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] =
|
proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] =
|
||||||
## Converts SVG-like commands to line segments.
|
## Converts SVG-like commands to line segments.
|
||||||
var
|
var
|
||||||
start, at: Vec2
|
start, at: Vec2
|
||||||
|
|
59
tests/emoji_megatest.nim
Normal file
|
@ -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)
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 280 KiB |
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 642 KiB |
BIN
tests/images/svg/openmoji.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 594 KiB After Width: | Height: | Size: 611 KiB |
BIN
tests/images/svg/twemoji.png
Normal file
After Width: | Height: | Size: 3.9 MiB |