update svg_cairo

This commit is contained in:
Ryan Oldenburg 2021-02-23 17:58:16 -06:00
parent 11fe02c25e
commit c32bd0c290

View file

@ -112,9 +112,13 @@ proc strokePath(
const svgSignature* = "<?xml" const svgSignature* = "<?xml"
type Ctx = object type Ctx = object
fillRule: WindingRule
fill, stroke: ColorRGBA fill, stroke: ColorRGBA
strokeWidth: float32 strokeWidth: float32
strokeLineCap: paths.LineCap
strokeLineJoin: paths.LineJoin
transform: Mat3 transform: Mat3
shouldStroke: bool
when defined(pixieTestCairo): when defined(pixieTestCairo):
type RenderTarget = ptr Context type RenderTarget = ptr Context
@ -124,21 +128,64 @@ else:
template failInvalid() = template failInvalid() =
raise newException(PixieError, "Invalid SVG data") raise newException(PixieError, "Invalid SVG data")
proc attrOrDefault(node: XmlNode, name, default: string): string =
result = node.attr(name)
if result.len == 0:
result = default
proc initCtx(): Ctx = proc initCtx(): Ctx =
result.fill = parseHtmlColor("black").rgba.toPremultipliedAlpha() result.fill = parseHtmlColor("black").rgba
result.stroke = parseHtmlColor("black").rgba
result.strokeWidth = 1 result.strokeWidth = 1
result.transform = mat3() result.transform = mat3()
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
result = inherited result = inherited
let var
fillRule = node.attr("fill-rule")
fill = node.attr("fill") fill = node.attr("fill")
stroke = node.attr("stroke") stroke = node.attr("stroke")
strokeWidth = node.attr("stroke-width") strokeWidth = node.attr("stroke-width")
strokeLineCap = node.attr("stroke-linecap")
strokeLineJoin = node.attr("stroke-linejoin")
transform = node.attr("transform") transform = node.attr("transform")
style = node.attr("style")
if fill == "": let pairs = style.split(';')
for pair in pairs:
let parts = pair.split(':')
if parts.len == 2:
# Do not override element properties
case parts[0].strip():
of "fill":
if fill.len == 0:
fill = parts[1].strip()
of "stroke":
if stroke.len == 0:
stroke = parts[1].strip()
of "stroke-linecap":
if strokeLineCap.len == 0:
strokeLineCap = parts[1].strip()
of "stroke-linejoin":
if strokeLineJoin.len == 0:
strokeLineJoin = parts[1].strip()
of "stroke-width":
if strokeWidth.len == 0:
strokeWidth = parts[1].strip()
if fillRule == "":
discard # Inherit
elif fillRule == "nonzero":
result.fillRule = wrNonZero
elif fillRule == "evenodd":
result.fillRule = wrEvenOdd
else:
raise newException(
PixieError, "Invalid fill-rule value " & fillRule
)
if fill == "" or fill == "currentColor":
discard # Inherit discard # Inherit
elif fill == "none": elif fill == "none":
result.fill = ColorRGBA() result.fill = ColorRGBA()
@ -147,15 +194,58 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
if stroke == "": if stroke == "":
discard # Inherit discard # Inherit
elif stroke == "currentColor":
result.shouldStroke = true
elif stroke == "none": elif stroke == "none":
result.stroke = ColorRGBA() result.stroke = ColorRGBA()
else: else:
result.stroke = parseHtmlColor(stroke).rgba.toPremultipliedAlpha() result.stroke = parseHtmlColor(stroke).rgba.toPremultipliedAlpha()
result.shouldStroke = true
if strokeWidth == "": if strokeWidth == "":
discard # Inherit discard # Inherit
else: else:
if strokeWidth.endsWith("px"):
strokeWidth = strokeWidth[0 .. ^3]
result.strokeWidth = parseFloat(strokeWidth) result.strokeWidth = parseFloat(strokeWidth)
result.shouldStroke = true
if result.stroke == ColorRGBA() or result.strokeWidth <= 0:
result.shouldStroke = false
if strokeLineCap == "":
discard # Inherit
else:
case strokeLineCap:
of "butt":
result.strokeLineCap = lcButt
of "round":
result.strokeLineCap = lcRound
of "square":
result.strokeLineCap = lcSquare
of "inherit":
discard
else:
raise newException(
PixieError, "Invalid stroke-linecap value " & strokeLineCap
)
if strokeLineJoin == "":
discard # Inherit
else:
case strokeLineJoin:
of "miter":
result.strokeLineJoin = ljMiter
of "round":
result.strokeLineJoin = ljRound
of "bevel":
result.strokeLineJoin = ljBevel
of "inherit":
discard
else:
raise newException(
PixieError, "Invalid stroke-linejoin value " & strokeLineJoin
)
if transform == "": if transform == "":
discard # Inherit discard # Inherit
@ -174,38 +264,53 @@ 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 = f[7 .. ^2].split(",") let arr =
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] = parseFloat(arr[0]) m[0] = parseFloat(arr[0].strip())
m[1] = parseFloat(arr[1]) m[1] = parseFloat(arr[1].strip())
m[3] = parseFloat(arr[2]) m[3] = parseFloat(arr[2].strip())
m[4] = parseFloat(arr[3]) m[4] = parseFloat(arr[3].strip())
m[6] = parseFloat(arr[4]) m[6] = parseFloat(arr[4].strip())
m[7] = parseFloat(arr[5]) m[7] = parseFloat(arr[5].strip())
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 = f[10 .. ^2].split(" ")
tx = parseFloat(components[0]) tx = parseFloat(components[0].strip())
ty = parseFloat(components[1]) ty =
if components[1].len == 0:
0.0
else:
parseFloat(components[1].strip())
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 angle = parseFloat(f[7 .. ^2]) * -PI / 180 let
result.transform = result.transform * rotationMat3(angle) values = f[7 .. ^2].split(" ")
angle = parseFloat(values[0].strip()) * -PI / 180
var cx, cy: float32
if values.len > 1:
cx = parseFloat(values[1].strip())
if values.len > 2:
cy = parseFloat(values[2].strip())
let center = vec2(cx, cy)
result.transform = result.transform *
translate(center) * rotationMat3(angle) * translate(-center)
else: else:
failInvalidTransform(transform) failInvalidTransform(transform)
proc draw( proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]
) =
if node.kind != xnElement: if node.kind != xnElement:
# Skip <!-- comments --> # Skip <!-- comments -->
return return
case node.tag: case node.tag:
of "title", "desc": of "title", "desc", "defs":
discard discard
of "g": of "g":
@ -221,17 +326,17 @@ proc draw(
ctx = decodeCtx(ctxStack[^1], node) ctx = decodeCtx(ctxStack[^1], node)
path = parsePath(d) path = parsePath(d)
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule)
if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth)
of "line": of "line":
let let
ctx = decodeCtx(ctxStack[^1], node) ctx = decodeCtx(ctxStack[^1], node)
x1 = parseFloat(node.attr("x1")) x1 = parseFloat(node.attrOrDefault("x1", "0"))
y1 = parseFloat(node.attr("y1")) y1 = parseFloat(node.attrOrDefault("y1", "0"))
x2 = parseFloat(node.attr("x2")) x2 = parseFloat(node.attrOrDefault("x2", "0"))
y2 = parseFloat(node.attr("y2")) y2 = parseFloat(node.attrOrDefault("y2", "0"))
var path: Path var path: Path
path.moveTo(x1, y1) path.moveTo(x1, y1)
@ -240,8 +345,8 @@ proc draw(
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fillPath(path, ctx.fill, ctx.transform)
if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth)
of "polyline", "polygon": of "polyline", "polygon":
let let
@ -249,11 +354,18 @@ proc draw(
points = node.attr("points") points = node.attr("points")
var vecs: seq[Vec2] var vecs: seq[Vec2]
for pair in points.split(" "): if points.contains(","):
let parts = pair.split(",") for pair in points.split(" "):
if parts.len != 2: let parts = pair.split(",")
if parts.len != 2:
failInvalid()
vecs.add(vec2(parseFloat(parts[0]), parseFloat(parts[1])))
else:
let points = points.split(" ")
if points.len mod 2 != 0:
failInvalid() failInvalid()
vecs.add(vec2(parseFloat(parts[0]), parseFloat(parts[1]))) for i in countup(0, points.len - 2, 2):
vecs.add(vec2(parseFloat(points[i]), parseFloat(points[i + 1])))
if vecs.len == 0: if vecs.len == 0:
failInvalid() failInvalid()
@ -269,65 +381,73 @@ proc draw(
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fillPath(path, ctx.fill, ctx.transform)
if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth)
of "rect": of "rect":
let let
ctx = decodeCtx(ctxStack[^1], node) ctx = decodeCtx(ctxStack[^1], node)
x = parseFloat(node.attr("x")) x = parseFloat(node.attrOrDefault("x", "0"))
y = parseFloat(node.attr("y")) y = parseFloat(node.attrOrDefault("y", "0"))
width = parseFloat(node.attr("width")) width = parseFloat(node.attr("width"))
height = parseFloat(node.attr("height")) height = parseFloat(node.attr("height"))
var
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
ry = max(parseFloat(node.attrOrDefault("ry", "0")), 0)
var path: Path var path: Path
path.rect(x, y, width, height) if rx > 0 or ry > 0:
if rx == 0:
rx = ry
elif ry == 0:
ry = rx
rx = min(rx, width / 2)
ry = min(ry, height / 2)
path.moveTo(x + rx, y)
path.lineTo(x + width - rx, y)
path.ellipticalArcTo(rx, ry, 0, false, true, x + width, y + ry)
path.lineTo(x + width, y + height - ry)
path.ellipticalArcTo(rx, ry, 0, false, true, x + width - rx, y + height)
path.lineTo(x + rx, y + height)
path.ellipticalArcTo(rx, ry, 0, false, true, x, y + height - ry)
path.lineTo(x, y + ry)
path.ellipticalArcTo(rx, ry, 0, false, true, x + rx, y)
else:
path.rect(x, y, width, height)
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fillPath(path, ctx.fill, ctx.transform)
if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth)
of "circle", "ellipse": of "circle", "ellipse":
# Reference for magic constant: let
# https://dl3.pushbulletusercontent.com/a3fLVC8boTzRoxevD1OgCzRzERB9z2EZ/unknown.png ctx = decodeCtx(ctxStack[^1], node)
let ctx = decodeCtx(ctxStack[^1], node) cx = parseFloat(node.attrOrDefault("cx", "0"))
cy = parseFloat(node.attrOrDefault("cy", "0"))
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 var rx, ry: float32
if node.tag == "circle": if node.tag == "circle":
rx = parseFloat(node.attr("r")) rx = parseFloat(node.attr("r"))
ry = rx ry = rx
else: else:
rx = parseFloat(node.attr("rx")) rx = parseFloat(node.attrOrDefault("rx", "0"))
ry = parseFloat(node.attr("ry")) ry = parseFloat(node.attrOrDefault("ry", "0"))
let
magicX = (4.0 * (-1.0 + sqrt(2.0)) / 3) * rx
magicY = (4.0 * (-1.0 + sqrt(2.0)) / 3) * ry
var path: Path var path: Path
path.moveTo(cx + rx, cy) path.ellipse(cx, cy, rx, ry)
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(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fillPath(path, ctx.fill, ctx.transform)
if ctx.stroke != ColorRGBA() and ctx.strokeWidth > 0: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.strokeWidth, ctx.transform) img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth)
else: else:
raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") 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. ## Render SVG file and return the image.
try: try:
let root = parseXml(data) let root = parseXml(data)
@ -337,27 +457,24 @@ proc decodeSvg*(data: string): Image =
let let
viewBox = root.attr("viewBox") viewBox = root.attr("viewBox")
box = viewBox.split(" ") box = viewBox.split(" ")
if parseInt(box[0]) != 0 or parseInt(box[1]) != 0: viewBoxWidth = parseInt(box[2])
failInvalid() viewBoxHeight = parseInt(box[3])
let var rootCtx = initCtx()
width = parseInt(box[2]) rootCtx = decodeCtx(rootCtx, root)
height = parseInt(box[3]) if width == 0 and height == 0: # Default to the view box size
var ctxStack = @[initCtx()] result = newImage(viewBoxWidth, viewBoxHeight)
result = newImage(width, height) else:
let result = newImage(width, height)
surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32)
c = surface.create() let
scaleX = width.float32 / viewBoxWidth.float32
scaleY = height.float32 / viewBoxHeight.float32
rootCtx.transform = scale(vec2(scaleX, scaleY))
var ctxStack = @[rootCtx]
for node in root: for node in root:
c.draw(node, ctxStack) result.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() result.toStraightAlpha()
except PixieError as e: except PixieError as e:
raise e raise e