Merge pull request #326 from guzba/master
fix cairo svg, add svg upscale test, stroke optimization
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
@ -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")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
1
tests/fileformats/svg/accessibility-outline.svg
Normal 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 |
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
BIN
tests/fileformats/svg/diffs/accessibility-outline.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 280 KiB |
Before Width: | Height: | Size: 644 KiB After Width: | Height: | Size: 644 KiB |
BIN
tests/fileformats/svg/masters/accessibility-outline.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 783 KiB After Width: | Height: | Size: 783 KiB |
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 357 KiB |
BIN
tests/fileformats/svg/rendered/accessibility-outline.png
Normal file
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 610 KiB After Width: | Height: | Size: 610 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 8 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 8 KiB After Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.2 KiB |
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|