Merge pull request #326 from guzba/master

fix cairo svg, add svg upscale test, stroke optimization
This commit is contained in:
treeform 2021-11-26 19:50:12 -08:00 committed by GitHub
commit 4b43f1fb35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 357 additions and 157 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,96 +1,141 @@
import benchy, cairo, chroma, math, pixie import benchy, cairo, chroma, math, pixie, pixie/paths {.all.}, strformat
proc doDiff(a, b: Image, name: string) =
let (diffScore, diffImage) = diff(a, b)
echo &"{name} score: {diffScore}"
diffImage.writeFile(&"{name}_diff.png")
block: block:
let path = newPath()
path.moveTo(0, 0)
path.lineTo(1920, 0)
path.lineTo(1920, 1080)
path.lineTo(0, 1080)
path.closePath()
let shapes = path.commandsToShapes(true, 1)
let let
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080) surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
ctx = surface.create() ctx = surface.create()
ctx.setSourceRgba(0, 0, 1, 1) ctx.setSourceRgba(0, 0, 1, 1)
timeIt "cairo1": timeIt "cairo1":
ctx.newPath() ctx.newPath()
ctx.moveTo(0, 0) ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
ctx.lineTo(1920, 0) for shape in shapes:
ctx.lineTo(1920, 1080) for v in shape:
ctx.lineTo(0, 1080) ctx.lineTo(v.x, v.y)
ctx.closePath()
ctx.fill() ctx.fill()
surface.flush() surface.flush()
# discard surface.writeToPng("cairo1.png") # discard surface.writeToPng("cairo1.png")
let a = newImage(1920, 1080) let a = newImage(1920, 1080)
a.fill(rgba(255, 255, 255, 255))
timeIt "pixie1": timeIt "pixie1":
let p = newPath() let p = newPath()
p.moveTo(0, 0) p.moveTo(shapes[0][0])
p.lineTo(1920, 0) for shape in shapes:
p.lineTo(1920, 1080) for v in shape:
p.lineTo(0, 1080) p.lineTo(v)
p.closePath()
a.fillPath(p, rgba(0, 0, 255, 255)) a.fillPath(p, rgba(0, 0, 255, 255))
# a.writeFile("pixie1.png") # a.writeFile("pixie1.png")
block: block:
let path = newPath()
path.moveTo(500, 240)
path.lineTo(1500, 240)
path.lineTo(1920, 600)
path.lineTo(0, 600)
path.closePath()
let shapes = path.commandsToShapes(true, 1)
let let
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080) surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
ctx = surface.create() ctx = surface.create()
timeIt "cairo2":
ctx.setSourceRgba(1, 1, 1, 1)
let operator = ctx.getOperator()
ctx.setOperator(OperatorSource)
ctx.paint()
ctx.setOperator(operator)
ctx.setSourceRgba(0, 0, 1, 1) ctx.setSourceRgba(0, 0, 1, 1)
timeIt "cairo2":
ctx.newPath() ctx.newPath()
ctx.moveTo(500, 240) ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
ctx.lineTo(1500, 240) for shape in shapes:
ctx.lineTo(1920, 600) for v in shape:
ctx.lineTo(0, 600) ctx.lineTo(v.x, v.y)
ctx.closePath()
ctx.fill() ctx.fill()
surface.flush() surface.flush()
# discard surface.writeToPng("cairo2.png") # discard surface.writeToPng("cairo2.png")
let a = newImage(1920, 1080) let a = newImage(1920, 1080)
a.fill(rgba(255, 255, 255, 255))
timeIt "pixie2": timeIt "pixie2":
a.fill(rgba(255, 255, 255, 255))
let p = newPath() let p = newPath()
p.moveTo(500, 240) p.moveTo(shapes[0][0])
p.lineTo(1500, 240) for shape in shapes:
p.lineTo(1920, 600) for v in shape:
p.lineTo(0, 600) p.lineTo(v)
p.closePath()
a.fillPath(p, rgba(0, 0, 255, 255)) a.fillPath(p, rgba(0, 0, 255, 255))
# a.writeFile("pixie2.png") # a.writeFile("pixie2.png")
# block: block:
# let let path = parsePath("""
# a = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) M 100,300
# b = imageSurfaceCreate(FORMAT_ARGB32, 500, 500) A 200,200 0,0,1 500,300
# ac = a.create() A 200,200 0,0,1 900,300
# bc = b.create() Q 900,600 500,900
Q 100,600 100,300 z
""")
# ac.setSourceRgba(1, 0, 0, 1) let shapes = path.commandsToShapes(true, 1)
# ac.newPath()
# ac.rectangle(0, 0, 1000, 1000)
# ac.fill()
# bc.setSourceRgba(0, 1, 0, 1) let
# bc.newPath() surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
# bc.rectangle(0, 0, 500, 500) ctx = surface.create()
# bc.fill()
# let pattern = patternCreateForSurface(b) timeIt "cairo3":
ctx.setSourceRgba(1, 1, 1, 1)
let operator = ctx.getOperator()
ctx.setOperator(OperatorSource)
ctx.paint()
ctx.setOperator(operator)
# timeIt "a": ctx.setSourceRgba(1, 0, 0, 1)
# ac.setSource(pattern)
# ac.save()
# ac.translate(25.2, 25.2)
# ac.rectangle(0, 0, 500, 500)
# ac.fill()
# ac.restore()
# discard a.writeToPng("a.png") ctx.newPath()
ctx.moveTo(shapes[0][0].x, shapes[0][0].y)
for shape in shapes:
for v in shape:
ctx.lineTo(v.x, v.y)
ctx.fill()
surface.flush()
# discard surface.writeToPng("cairo3.png")
let a = newImage(1000, 1000)
timeIt "pixie3":
a.fill(rgba(255, 255, 255, 255))
let p = newPath()
p.moveTo(shapes[0][0])
for shape in shapes:
for v in shape:
p.lineTo(v)
a.fillPath(p, rgba(255, 0, 0, 255))
# a.writeFile("pixie3.png")
# doDiff(readImage("cairo3.png"), a, "cairo3")

View file

@ -1,20 +1,28 @@
## Load and Save SVG files. ## Load and Save SVG files.
import cairo, chroma, pixie/common, pixie/images, strutils, vmath, xmlparser, xmltree import cairo, chroma, pixie/common, pixie/images, pixie/paints, strutils, tables,
vmath, xmlparser, xmltree
include pixie/paths include pixie/paths
# type Path = paths.Path
proc processCommands(c: ptr Context, path: Path) = proc processCommands(c: ptr Context, path: Path) =
c.newPath() c.newPath()
c.moveTo(0, 0) c.moveTo(0, 0)
for i, command in path.commands:
var
prevCommandKind = Move
start, at, prevCtrl2: Vec2
for command in path.commands:
case command.kind case command.kind
of Move: of Move:
c.moveTo(command.numbers[0], command.numbers[1]) c.moveTo(command.numbers[0], command.numbers[1])
at.x = command.numbers[0]
at.y = command.numbers[1]
start = at
of Line: of Line:
c.lineTo(command.numbers[0], command.numbers[1]) c.lineTo(command.numbers[0], command.numbers[1])
at.x = command.numbers[0]
at.y = command.numbers[1]
of HLine: of HLine:
echo "HLine not yet supported for Cairo" echo "HLine not yet supported for Cairo"
of VLine: of VLine:
@ -28,6 +36,9 @@ proc processCommands(c: ptr Context, path: Path) =
command.numbers[4], command.numbers[4],
command.numbers[5] command.numbers[5]
) )
at.x = command.numbers[4]
at.y = command.numbers[5]
prevCtrl2 = vec2(command.numbers[2], command.numbers[3])
of SCubic: of SCubic:
echo "SCubic not yet supported for Cairo" echo "SCubic not yet supported for Cairo"
of Quad: of Quad:
@ -38,12 +49,19 @@ proc processCommands(c: ptr Context, path: Path) =
echo "Arc not yet supported for Cairo" echo "Arc not yet supported for Cairo"
of RMove: of RMove:
c.relMoveTo(command.numbers[0], command.numbers[1]) c.relMoveTo(command.numbers[0], command.numbers[1])
at.x += command.numbers[0]
at.y += command.numbers[1]
start = at
of RLine: of RLine:
c.relLineTo(command.numbers[0], command.numbers[1]) c.relLineTo(command.numbers[0], command.numbers[1])
at.x += command.numbers[0]
at.y += command.numbers[1]
of RHLine: of RHLine:
c.relLineTo(command.numbers[0], 0) c.relLineTo(command.numbers[0], 0)
at.x += command.numbers[0]
of RVLine: of RVLine:
c.relLineTo(0, command.numbers[0]) c.relLineTo(0, command.numbers[0])
at.y += command.numbers[0]
of RCubic: of RCubic:
c.relCurveTo( c.relCurveTo(
command.numbers[0], command.numbers[0],
@ -53,12 +71,28 @@ proc processCommands(c: ptr Context, path: Path) =
command.numbers[4], command.numbers[4],
command.numbers[5] command.numbers[5]
) )
prevCtrl2 = vec2(at.x + command.numbers[2], at.y + command.numbers[3])
at.x += command.numbers[4]
at.y += command.numbers[5]
of RSCubic: of RSCubic:
# This is not correct but good enough for now let
c.relLineTo( ctrl1 =
command.numbers[2], if prevCommandKind in {Cubic, SCubic, RCubic, RSCubic}:
command.numbers[3] at * 2 - prevCtrl2
else:
at
ctrl2 = vec2(at.x + command.numbers[0], at.y + command.numbers[1])
to = vec2(at.x + command.numbers[2], at.y + command.numbers[3])
c.curveTo(
ctrl1.x,
ctrl1.y,
ctrl2.x,
ctrl2.y,
to.x,
to.y
) )
prevCtrl2 = ctrl2
at = to
of RQuad: of RQuad:
echo "RQuad not supported by Cairo" echo "RQuad not supported by Cairo"
of RTQuad: of RTQuad:
@ -67,18 +101,21 @@ proc processCommands(c: ptr Context, path: Path) =
echo "RArc not yet supported for Cairo" echo "RArc not yet supported for Cairo"
of Close: of Close:
c.closePath() c.closePath()
at = start
prevCommandKind = command.kind
checkStatus(c.status()) checkStatus(c.status())
proc prepare( proc prepare(
c: ptr Context, c: ptr Context,
path: Path, path: Path,
color: ColorRGBA, paint: Paint,
mat: Mat3, mat: Mat3,
windingRule = wrNonZero windingRule = wrNonZero
) = ) =
let let
color = color.color() color = paint.color
matrix = Matrix( matrix = Matrix(
xx: mat[0, 0], xx: mat[0, 0],
yx: mat[0, 1], yx: mat[0, 1],
@ -96,9 +133,16 @@ proc prepare(
c.setFillRule(FillRuleEvenOdd) c.setFillRule(FillRuleEvenOdd)
c.processCommands(path) c.processCommands(path)
type Ctx = object type
LinearGradient = object
x1, y1, x2, y2: float32
stops: seq[ColorStop]
Ctx = object
display: bool
fillRule: WindingRule fillRule: WindingRule
fill, stroke: ColorRGBA fill: Paint
stroke: ColorRGBX
strokeWidth: float32 strokeWidth: float32
strokeLineCap: LineCap strokeLineCap: LineCap
strokeLineJoin: LineJoin strokeLineJoin: LineJoin
@ -106,6 +150,8 @@ type Ctx = object
strokeDashArray: seq[float32] strokeDashArray: seq[float32]
transform: Mat3 transform: Mat3
shouldStroke: bool shouldStroke: bool
opacity, strokeOpacity: float32
linearGradients: TableRef[string, LinearGradient]
template failInvalid() = template failInvalid() =
raise newException(PixieError, "Invalid SVG data") raise newException(PixieError, "Invalid SVG data")
@ -116,13 +162,21 @@ proc attrOrDefault(node: XmlNode, name, default: string): string =
result = default result = default
proc initCtx(): Ctx = proc initCtx(): Ctx =
result.fill = parseHtmlColor("black").rgba result.display = true
result.stroke = parseHtmlColor("black").rgba try:
result.fill = parseHtmlColor("black").rgbx
result.stroke = parseHtmlColor("black").rgbx
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
result.strokeWidth = 1 result.strokeWidth = 1
result.transform = mat3() result.transform = mat3()
result.strokeMiterLimit = defaultMiterLimit result.strokeMiterLimit = defaultMiterLimit
result.opacity = 1
result.strokeOpacity = 1
result.linearGradients = newTable[string, LinearGradient]()
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
result = inherited result = inherited
proc splitArgs(s: string): seq[string] = proc splitArgs(s: string): seq[string] =
@ -143,6 +197,10 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
strokeDashArray = node.attr("stroke-dasharray") strokeDashArray = node.attr("stroke-dasharray")
transform = node.attr("transform") transform = node.attr("transform")
style = node.attr("style") style = node.attr("style")
display = node.attr("display")
opacity = node.attr("opacity")
fillOpacity = node.attr("fill-opacity")
strokeOpacity = node.attr("stroke-opacity")
let pairs = style.split(';') let pairs = style.split(';')
for pair in pairs: for pair in pairs:
@ -150,6 +208,9 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
if parts.len == 2: if parts.len == 2:
# Do not override element properties # Do not override element properties
case parts[0].strip(): case parts[0].strip():
of "fill-rule":
if fillRule.len == 0:
fillRule = parts[1].strip()
of "fill": of "fill":
if fill.len == 0: if fill.len == 0:
fill = parts[1].strip() fill = parts[1].strip()
@ -171,6 +232,35 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
of "stroke-dasharray": of "stroke-dasharray":
if strokeDashArray.len == 0: if strokeDashArray.len == 0:
strokeDashArray = parts[1].strip() strokeDashArray = parts[1].strip()
of "display":
if display.len == 0:
display = parts[1].strip()
of "opacity":
if opacity.len == 0:
opacity = parts[1].strip()
of "fillOpacity":
if fillOpacity.len == 0:
fillOpacity = parts[1].strip()
of "strokeOpacity":
if strokeOpacity.len == 0:
strokeOpacity = parts[1].strip()
else:
discard
elif pair.len > 0:
when defined(pixieDebugSvg):
echo "Invalid style pair: ", pair
if display.len > 0:
result.display = display.strip() != "none"
if opacity.len > 0:
result.opacity = clamp(parseFloat(opacity), 0, 1)
if fillOpacity.len > 0:
result.fill.opacity = clamp(parseFloat(fillOpacity), 0, 1)
if strokeOpacity.len > 0:
result.strokeOpacity = clamp(parseFloat(strokeOpacity), 0, 1)
if fillRule == "": if fillRule == "":
discard # Inherit discard # Inherit
@ -186,18 +276,30 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
if fill == "" or fill == "currentColor": if fill == "" or fill == "currentColor":
discard # Inherit discard # Inherit
elif fill == "none": elif fill == "none":
result.fill = ColorRGBA() result.fill = ColorRGBX()
elif fill.startsWith("url("):
let id = fill[5 .. ^2]
if id in result.linearGradients:
let linearGradient = result.linearGradients[id]
result.fill = newPaint(pkGradientLinear)
result.fill.gradientHandlePositions = @[
result.transform * vec2(linearGradient.x1, linearGradient.y1),
result.transform * vec2(linearGradient.x2, linearGradient.y2)
]
result.fill.gradientStops = linearGradient.stops
else: else:
result.fill = parseHtmlColor(fill).rgba raise newException(PixieError, "Missing SVG resource " & id)
else:
result.fill = parseHtmlColor(fill).rgbx
if stroke == "": if stroke == "":
discard # Inherit discard # Inherit
elif stroke == "currentColor": elif stroke == "currentColor":
result.shouldStroke = true result.shouldStroke = true
elif stroke == "none": elif stroke == "none":
result.stroke = ColorRGBA() result.stroke = ColorRGBX()
else: else:
result.stroke = parseHtmlColor(stroke).rgba result.stroke = parseHtmlColor(stroke).rgbx
result.shouldStroke = true result.shouldStroke = true
if strokeWidth == "": if strokeWidth == "":
@ -208,7 +310,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
result.strokeWidth = parseFloat(strokeWidth) result.strokeWidth = parseFloat(strokeWidth)
result.shouldStroke = true result.shouldStroke = true
if result.stroke == ColorRGBA() or result.strokeWidth <= 0: if result.stroke == ColorRGBX() or result.strokeWidth <= 0:
result.shouldStroke = false result.shouldStroke = false
if strokeLineCap == "": if strokeLineCap == "":
@ -262,7 +364,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
else: else:
template failInvalidTransform(transform: string) = template failInvalidTransform(transform: string) =
raise newException( raise newException(
PixieError, "Unsupported SVG transform: " & transform & "." PixieError, "Unsupported SVG transform: " & transform
) )
var remaining = transform var remaining = transform
@ -320,6 +422,15 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
else: else:
failInvalidTransform(transform) failInvalidTransform(transform)
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
try:
decodeCtxInternal(inherited, node)
except PixieError as e:
raise e
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
proc cairoLineCap(lineCap: LineCap): cairo.LineCap = proc cairoLineCap(lineCap: LineCap): cairo.LineCap =
case lineCap: case lineCap:
of lcButt: of lcButt:
@ -339,19 +450,24 @@ proc cairoLineJoin(lineJoin: LineJoin): cairo.LineJoin =
LineJoinRound LineJoinRound
proc fill(c: ptr Context, ctx: Ctx, path: Path) {.inline.} = proc fill(c: ptr Context, ctx: Ctx, path: Path) {.inline.} =
# img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule) if ctx.display and ctx.opacity > 0:
prepare(c, path, ctx.fill, ctx.transform, ctx.fillRule) let paint = newPaint(ctx.fill)
paint.opacity = paint.opacity * ctx.opacity
prepare(c, path, paint, ctx.transform, ctx.fillRule)
c.fill() c.fill()
proc stroke(c: ptr Context, ctx: Ctx, path: Path) {.inline.} = proc stroke(c: ptr Context, ctx: Ctx, path: Path) {.inline.} =
prepare(c, path, ctx.stroke, ctx.transform) if ctx.display and ctx.opacity > 0:
let paint = newPaint(ctx.stroke)
paint.color.a *= (ctx.opacity * ctx.strokeOpacity)
prepare(c, path, paint, ctx.transform)
c.setLineWidth(ctx.strokeWidth) c.setLineWidth(ctx.strokeWidth)
c.setLineCap(ctx.strokeLineCap.cairoLineCap()) c.setLineCap(ctx.strokeLineCap.cairoLineCap())
c.setLineJoin(ctx.strokeLineJoin.cairoLineJoin()) c.setLineJoin(ctx.strokeLineJoin.cairoLineJoin())
c.setMiterLimit(ctx.strokeMiterLimit) c.setMiterLimit(ctx.strokeMiterLimit)
c.stroke() c.stroke()
proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = proc drawInternal(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
if node.kind != xnElement: if node.kind != xnElement:
# Skip <!-- comments --> # Skip <!-- comments -->
return return
@ -364,7 +480,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
let ctx = decodeCtx(ctxStack[^1], node) let ctx = decodeCtx(ctxStack[^1], node)
ctxStack.add(ctx) ctxStack.add(ctx)
for child in node: for child in node:
img.draw(child, ctxStack) img.drawInternal(child, ctxStack)
discard ctxStack.pop() discard ctxStack.pop()
of "path": of "path":
@ -372,7 +488,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
d = node.attr("d") d = node.attr("d")
ctx = decodeCtx(ctxStack[^1], node) ctx = decodeCtx(ctxStack[^1], node)
path = parsePath(d) path = parsePath(d)
if ctx.fill != ColorRGBA():
img.fill(ctx, path) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.stroke(ctx, path) img.stroke(ctx, path)
@ -385,7 +501,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
x2 = parseFloat(node.attrOrDefault("x2", "0")) x2 = parseFloat(node.attrOrDefault("x2", "0"))
y2 = parseFloat(node.attrOrDefault("y2", "0")) y2 = parseFloat(node.attrOrDefault("y2", "0"))
var path: Path let path = newPath()
path.moveTo(x1, y1) path.moveTo(x1, y1)
path.lineTo(x2, y2) path.lineTo(x2, y2)
@ -414,7 +530,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
if vecs.len == 0: if vecs.len == 0:
failInvalid() failInvalid()
var path: Path let path = newPath()
path.moveTo(vecs[0]) path.moveTo(vecs[0])
for i in 1 ..< vecs.len: for i in 1 ..< vecs.len:
path.lineTo(vecs[i]) path.lineTo(vecs[i])
@ -423,8 +539,6 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
# and fill or not # and fill or not
if node.tag == "polygon": if node.tag == "polygon":
path.closePath() path.closePath()
if ctx.fill != ColorRGBA():
img.fill(ctx, path) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
@ -445,7 +559,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0) rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
ry = max(parseFloat(node.attrOrDefault("ry", "0")), 0) ry = max(parseFloat(node.attrOrDefault("ry", "0")), 0)
var path: Path let path = newPath()
if rx > 0 or ry > 0: if rx > 0 or ry > 0:
if rx == 0: if rx == 0:
rx = ry rx = ry
@ -466,7 +580,6 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
else: else:
path.rect(x, y, width, height) path.rect(x, y, width, height)
if ctx.fill != ColorRGBA():
img.fill(ctx, path) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.stroke(ctx, path) img.stroke(ctx, path)
@ -485,10 +598,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
rx = parseFloat(node.attrOrDefault("rx", "0")) rx = parseFloat(node.attrOrDefault("rx", "0"))
ry = parseFloat(node.attrOrDefault("ry", "0")) ry = parseFloat(node.attrOrDefault("ry", "0"))
var path: Path let path = newPath()
path.ellipse(cx, cy, rx, ry) path.ellipse(cx, cy, rx, ry)
if ctx.fill != ColorRGBA():
img.fill(ctx, path) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.stroke(ctx, path) img.stroke(ctx, path)
@ -496,6 +608,15 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
else: else:
raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".")
proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
try:
drawInternal(img, node, ctxStack)
except PixieError as e:
raise e
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
proc decodeSvg*(data: string, width = 0, height = 0): Image = proc decodeSvg*(data: string, width = 0, height = 0): Image =
## Render SVG file and return the image. Defaults to the SVG's view box size. ## Render SVG file and return the image. Defaults to the SVG's view box size.
try: try:
@ -519,21 +640,21 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image =
vec2(-viewBoxMinX.float32, -viewBoxMinY.float32) vec2(-viewBoxMinX.float32, -viewBoxMinY.float32)
) )
var surface: ptr Surface var
width = width
height = height
surface: ptr Surface
if width == 0 and height == 0: # Default to the view box size if width == 0 and height == 0: # Default to the view box size
result = newImage(viewBoxWidth, viewBoxHeight) width = viewBoxWidth.int32
surface = imageSurfaceCreate( height = viewBoxHeight.int32
FORMAT_ARGB32, viewBoxWidth.int32, viewBoxHeight.int32
)
else: else:
result = newImage(width, height)
surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32)
let let
scaleX = width.float32 / viewBoxWidth.float32 scaleX = width.float32 / viewBoxWidth.float32
scaleY = height.float32 / viewBoxHeight.float32 scaleY = height.float32 / viewBoxHeight.float32
rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY)) rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY))
surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32)
let c = surface.create() let c = surface.create()
var ctxStack = @[rootCtx] var ctxStack = @[rootCtx]
@ -542,6 +663,8 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image =
surface.flush() surface.flush()
result = newImage(width, height)
let pixels = cast[ptr UncheckedArray[array[4, uint8]]](surface.getData()) let pixels = cast[ptr UncheckedArray[array[4, uint8]]](surface.getData())
for y in 0 ..< result.height: for y in 0 ..< result.height:
for x in 0 ..< result.width: for x in 0 ..< result.width:

View file

@ -10,9 +10,19 @@ const files = [
"ellipse01", "ellipse01",
"triangle01", "triangle01",
"quad01", "quad01",
"Ghostscript_Tiger" "Ghostscript_Tiger",
"scale",
"miterlimit",
"dashes"
] ]
proc doDiff(rendered: Image, name: string) =
rendered.writeFile(&"tests/fileformats/svg/rendered/{name}.png")
let
master = readImage(&"tests/fileformats/svg/masters/{name}.png")
(diffScore, diffImage) = diff(master, rendered)
echo &"{name} score: {diffScore}"
diffImage.writeFile(&"tests/fileformats/svg/diffs/{name}.png")
for file in files: for file in files:
let image = decodeSvg(readFile(&"tests/images/svg/{file}.svg")) doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file)
image.writeFile(&"tests/images/svg/{file}.png")

View file

@ -329,14 +329,12 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
proc fill(img: Image, ctx: Ctx, path: Path) {.inline.} = proc fill(img: Image, ctx: Ctx, path: Path) {.inline.} =
if ctx.display and ctx.opacity > 0: if ctx.display and ctx.opacity > 0:
let paint = newPaint(ctx.fill) let paint = newPaint(ctx.fill)
if ctx.opacity != 1:
paint.opacity = paint.opacity * ctx.opacity paint.opacity = paint.opacity * ctx.opacity
img.fillPath(path, paint, ctx.transform, ctx.fillRule) img.fillPath(path, paint, ctx.transform, ctx.fillRule)
proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} = proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} =
if ctx.display and ctx.opacity > 0: if ctx.display and ctx.opacity > 0:
let paint = newPaint(ctx.stroke) let paint = newPaint(ctx.stroke)
if ctx.opacity != 1:
paint.color.a *= (ctx.opacity * ctx.strokeOpacity) paint.color.a *= (ctx.opacity * ctx.strokeOpacity)
img.strokePath( img.strokePath(
path, path,

View file

@ -46,6 +46,7 @@ type
const const
epsilon: float64 = 0.0001 * PI ## Tiny value used for some computations. Must be float64 to prevent leaks. epsilon: float64 = 0.0001 * PI ## Tiny value used for some computations. Must be float64 to prevent leaks.
pixelErrorMargin: float32 = 0.2
defaultMiterLimit*: float32 = 4 defaultMiterLimit*: float32 = 4
when defined(release): when defined(release):
@ -639,7 +640,7 @@ proc polygon*(
path.polygon(pos.x, pos.y, size, sides) path.polygon(pos.x, pos.y, size, sides)
proc commandsToShapes( proc commandsToShapes(
path: Path, closeSubpaths = false, pixelScale: float32 = 1.0 path: Path, closeSubpaths: bool, pixelScale: float32
): seq[seq[Vec2]] = ): seq[seq[Vec2]] =
## Converts SVG-like commands to sequences of vectors. ## Converts SVG-like commands to sequences of vectors.
var var
@ -651,7 +652,7 @@ proc commandsToShapes(
prevCommandKind = Move prevCommandKind = Move
prevCtrl, prevCtrl2: Vec2 prevCtrl, prevCtrl2: Vec2
let errorMarginSq = pow(0.2.float32 / pixelScale, 2) let errorMarginSq = pow(pixelErrorMargin / pixelScale, 2)
proc addSegment(shape: var seq[Vec2], at, to: Vec2) = proc addSegment(shape: var seq[Vec2], at, to: Vec2) =
# Don't add any 0 length lines # Don't add any 0 length lines
@ -1080,7 +1081,7 @@ proc computeBounds*(
path: Path, transform = mat3() path: Path, transform = mat3()
): Rect {.raises: [PixieError].} = ): Rect {.raises: [PixieError].} =
## Compute the bounds of the path. ## Compute the bounds of the path.
var shapes = path.commandsToShapes() var shapes = path.commandsToShapes(true, pixelScale(transform))
shapes.transform(transform) shapes.transform(transform)
computeBounds(shapes.shapesToSegments()) computeBounds(shapes.shapesToSegments())
@ -1135,10 +1136,11 @@ proc maxEntryCount(partitioning: Partitioning): int =
for i in 0 ..< partitioning.partitions.len: for i in 0 ..< partitioning.partitions.len:
result = max(result, partitioning.partitions[i].len) result = max(result, partitioning.partitions[i].len)
proc insertionSort( proc sortHits(hits: var seq[(float32, int16)], inl, inr: int) =
hits: var seq[(float32, int16)], lo, hi: int ## Quicksort + insertion sort, in-place and faster than standard lib sort.
) {.inline.} = let n = inr - inl + 1
for i in lo + 1 .. hi: if n < 32: # Use insertion sort for the rest
for i in inl + 1 .. inr:
var var
j = i - 1 j = i - 1
k = i k = i
@ -1146,12 +1148,6 @@ proc insertionSort(
swap(hits[j + 1], hits[j]) swap(hits[j + 1], hits[j])
dec j dec j
dec k dec k
proc sort(hits: var seq[(float32, int16)], inl, inr: int) =
## Quicksort + insertion sort, in-place and faster than standard lib sort.
let n = inr - inl + 1
if n < 32:
insertionSort(hits, inl, inr)
return return
var var
l = inl l = inl
@ -1166,8 +1162,8 @@ proc sort(hits: var seq[(float32, int16)], inl, inr: int) =
swap(hits[l], hits[r]) swap(hits[l], hits[r])
inc l inc l
dec r dec r
sort(hits, inl, r) sortHits(hits, inl, r)
sort(hits, l, inr) sortHits(hits, l, inr)
proc shouldFill( proc shouldFill(
windingRule: WindingRule, count: int windingRule: WindingRule, count: int
@ -1257,7 +1253,7 @@ proc computeCoverage(
inc numHits inc numHits
if numHits > 0: if numHits > 0:
sort(hits, 0, numHits - 1) sortHits(hits, 0, numHits - 1)
if aa: if aa:
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, width): for (prevAt, at, count) in hits.walk(numHits, windingRule, y, width):
@ -1657,7 +1653,8 @@ proc strokeShapes(
lineCap: LineCap, lineCap: LineCap,
lineJoin: LineJoin, lineJoin: LineJoin,
miterLimit: float32, miterLimit: float32,
dashes: seq[float32] dashes: seq[float32],
pixelScale: float32
): seq[seq[Vec2]] = ): seq[seq[Vec2]] =
if strokeWidth <= 0: if strokeWidth <= 0:
return return
@ -1669,7 +1666,7 @@ proc strokeShapes(
proc makeCircle(at: Vec2): seq[Vec2] = proc makeCircle(at: Vec2): seq[Vec2] =
let path = newPath() let path = newPath()
path.ellipse(at, halfStroke, halfStroke) path.ellipse(at, halfStroke, halfStroke)
path.commandsToShapes()[0] path.commandsToShapes(true, pixelScale)[0]
proc makeRect(at, to: Vec2): seq[Vec2] = proc makeRect(at, to: Vec2): seq[Vec2] =
# Rectangle corners # Rectangle corners
@ -1695,7 +1692,15 @@ proc strokeShapes(
@[a, b, c, d, a] @[a, b, c, d, a]
proc makeJoin(prevPos, pos, nextPos: Vec2): seq[Vec2] = proc addJoin(shape: var seq[seq[Vec2]], prevPos, pos, nextPos: Vec2) =
let minArea = pixelErrorMargin / pixelScale
if lineJoin == ljRound:
let area = PI.float32 * halfStroke * halfStroke
if area > minArea:
shape.add makeCircle(pos)
return
let angle = fixAngle(angle(nextPos - pos) - angle(prevPos - pos)) let angle = fixAngle(angle(nextPos - pos) - angle(prevPos - pos))
if abs(abs(angle) - PI) > epsilon: if abs(abs(angle) - PI) > epsilon:
var var
@ -1719,13 +1724,21 @@ proc strokeShapes(
lb = line(nextPos + b, pos + b) lb = line(nextPos + b, pos + b)
var at: Vec2 var at: Vec2
if la.intersects(lb, at): if la.intersects(lb, at):
return @[pos + a, at, pos + b, pos, pos + a] let
bisectorLengthSq = (at - pos).lengthSq
areaSq = 0.25.float32 * (
a.lengthSq * bisectorLengthSq + b.lengthSq * bisectorLengthSq
)
if areaSq > (minArea * minArea):
shape.add @[pos + a, at, pos + b, pos, pos + a]
of ljBevel: of ljBevel:
return @[a + pos, b + pos, pos, a + pos] let areaSq = 0.25.float32 * a.lengthSq * b.lengthSq
if areaSq > (minArea * minArea):
shape.add @[a + pos, b + pos, pos, a + pos]
of ljRound: of ljRound:
return makeCircle(pos) discard # Handled above, skipping angle calculation
for shape in shapes: for shape in shapes:
var shapeStroke: seq[seq[Vec2]] var shapeStroke: seq[seq[Vec2]]
@ -1773,10 +1786,10 @@ proc strokeShapes(
# If we need a line join # If we need a line join
if i < shape.len - 1: if i < shape.len - 1:
shapeStroke.add(makeJoin(prevPos, pos, shape[i + 1])) shapeStroke.addJoin(prevPos, pos, shape[i + 1])
if shape[0] == shape[^1]: if shape[0] == shape[^1]:
shapeStroke.add(makeJoin(shape[^2], shape[^1], shape[1])) shapeStroke.addJoin(shape[^2], shape[^1], shape[1])
else: else:
case lineCap: case lineCap:
of lcButt: of lcButt:
@ -1793,7 +1806,7 @@ proc strokeShapes(
result.add(shapeStroke) result.add(shapeStroke)
proc parseSomePath( proc parseSomePath(
path: SomePath, closeSubpaths: bool, pixelScale: float32 = 1.0 path: SomePath, closeSubpaths: bool, pixelScale: float32
): seq[seq[Vec2]] {.inline.} = ): seq[seq[Vec2]] {.inline.} =
## Given SomePath, parse it in different ways. ## Given SomePath, parse it in different ways.
when type(path) is string: when type(path) is string:
@ -1874,13 +1887,15 @@ proc strokePath*(
blendMode = bmNormal blendMode = bmNormal
) {.raises: [PixieError].} = ) {.raises: [PixieError].} =
## Strokes a path. ## Strokes a path.
let pixelScale = transform.pixelScale()
var strokeShapes = strokeShapes( var strokeShapes = strokeShapes(
parseSomePath(path, false, transform.pixelScale()), parseSomePath(path, false, pixelScale),
strokeWidth, strokeWidth,
lineCap, lineCap,
lineJoin, lineJoin,
miterLimit, miterLimit,
dashes dashes,
pixelScale
) )
strokeShapes.transform(transform) strokeShapes.transform(transform)
mask.fillShapes(strokeShapes, wrNonZero, blendMode) mask.fillShapes(strokeShapes, wrNonZero, blendMode)
@ -1908,7 +1923,8 @@ proc strokePath*(
lineCap, lineCap,
lineJoin, lineJoin,
miterLimit, miterLimit,
dashes dashes,
pixelScale(transform)
) )
strokeShapes.transform(transform) strokeShapes.transform(transform)
var color = paint.color var color = paint.color
@ -1970,7 +1986,7 @@ proc overlaps(
if segment.to != at: if segment.to != at:
hits.add((at.x, winding)) hits.add((at.x, winding))
sort(hits, 0, hits.high) sortHits(hits, 0, hits.high)
var count: int var count: int
for (at, winding) in hits: for (at, winding) in hits:
@ -1985,7 +2001,7 @@ proc fillOverlaps*(
windingRule = wrNonZero windingRule = wrNonZero
): bool {.raises: [PixieError].} = ): bool {.raises: [PixieError].} =
## Returns whether or not the specified point is contained in the current path. ## Returns whether or not the specified point is contained in the current path.
var shapes = parseSomePath(path, true, transform.pixelScale()) var shapes = path.commandsToShapes(true, transform.pixelScale())
shapes.transform(transform) shapes.transform(transform)
shapes.overlaps(test, windingRule) shapes.overlaps(test, windingRule)
@ -2001,13 +2017,15 @@ proc strokeOverlaps*(
): bool {.raises: [PixieError].} = ): bool {.raises: [PixieError].} =
## Returns whether or not the specified point is inside the area contained ## Returns whether or not the specified point is inside the area contained
## by the stroking of a path. ## by the stroking of a path.
let pixelScale = transform.pixelScale()
var strokeShapes = strokeShapes( var strokeShapes = strokeShapes(
parseSomePath(path, false, transform.pixelScale()), path.commandsToShapes(false, pixelScale),
strokeWidth, strokeWidth,
lineCap, lineCap,
lineJoin, lineJoin,
miterLimit, miterLimit,
dashes dashes,
pixelScale
) )
strokeShapes.transform(transform) strokeShapes.transform(transform)
strokeShapes.overlaps(test, wrNonZero) strokeShapes.overlaps(test, wrNonZero)

View file

@ -12,7 +12,7 @@ let
timeIt "typeset": timeIt "typeset":
discard font.typeset(text, bounds = vec2(image.width.float32, 0)) discard font.typeset(text, bounds = vec2(image.width.float32, 0))
timeIt "rasterize": timeIt "fill text":
image.fill(rgba(255, 255, 255, 255)) image.fill(rgba(255, 255, 255, 255))
image.fillText(font, text, bounds = vec2(image.width.float32, 0)) image.fillText(font, text, bounds = vec2(image.width.float32, 0))
# mask.fill(0) # mask.fill(0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle fill="none" stroke="#000" stroke-linejoin="round" stroke-width="32" cx="256" cy="56" r="40"/><path fill="none" stroke="#000" stroke-linejoin="round" stroke-width="32" d="M204.23,274.44c2.9-18.06,4.2-35.52-.5-47.59-4-10.38-12.7-16.19-23.2-20.15L88,176.76c-12-4-23.21-10.7-24-23.94-1-17,14-28,29-24,0,0,88,31.14,163,31.14s162-31,162-31c18-5,30,9,30,23.79,0,14.21-11,19.21-24,23.94l-88,31.91c-8,3-21,9-26,18.18-6,10.75-5,29.53-2.1,47.59l5.9,29.63L351.21,467.9c2.8,13.15-6.3,25.44-19.4,27.74S308,489,304.12,476.28L266.56,360.35q-2.71-8.34-4.8-16.87L256,320l-5.3,21.65q-2.52,10.35-5.8,20.48L208,476.18c-4,12.85-14.5,21.75-27.6,19.46S158,480.05,160.94,467.9l37.39-163.83Z"/></svg>

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 KiB

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -605,7 +605,7 @@ block:
doAssert not path.strokeOverlaps(vec2(0, 0)) doAssert not path.strokeOverlaps(vec2(0, 0))
doAssert not path.strokeOverlaps(vec2(20, 20)) doAssert not path.strokeOverlaps(vec2(20, 20))
doAssert path.strokeOverlaps(vec2(0, 20)) doAssert path.strokeOverlaps(vec2(0, 20))
doAssert path.strokeOverlaps(vec2(40, 20)) doAssert path.strokeOverlaps(vec2(39.9, 19.9))
doAssert path.strokeOverlaps(vec2(19.8, 30.2)) doAssert path.strokeOverlaps(vec2(19.8, 30.2))
doAssert not path.strokeOverlaps(vec2(19.4, 30.6)) doAssert not path.strokeOverlaps(vec2(19.4, 30.6))

View file

@ -26,3 +26,8 @@ proc doDiff(rendered: Image, name: string) =
for file in files: for file in files:
doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file) doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file)
doDiff(
decodeSvg(readFile("tests/fileformats/svg/accessibility-outline.svg"), 512, 512),
"accessibility-outline"
)