better cairo benchmarking
This commit is contained in:
parent
412757d4c0
commit
d2b84e76ca
4 changed files with 530 additions and 952 deletions
|
@ -1,264 +1,55 @@
|
|||
import benchy, cairo, chroma, math, pixie, pixie/paths {.all.}, strformat
|
||||
import benchy, cairo, pixie, pixie/fileformats/svg {.all.}, pixie/paths {.all.}
|
||||
|
||||
when defined(amd64) and not defined(pixieNoSimd):
|
||||
import nimsimd/sse2, pixie/internal
|
||||
type
|
||||
Fill = object
|
||||
shapes: seq[Polygon]
|
||||
transform: Mat3
|
||||
paint: Paint
|
||||
windingRule: WindingRule
|
||||
|
||||
proc doDiff(a, b: Image, name: string) =
|
||||
let (diffScore, diffImage) = diff(a, b)
|
||||
echo &"{name} score: {diffScore}"
|
||||
diffImage.writeFile(&"{name}_diff.png")
|
||||
Benchmark = object
|
||||
name: string
|
||||
fills: seq[Fill]
|
||||
|
||||
when defined(release):
|
||||
{.push checks: off.}
|
||||
var benchmarks: seq[Benchmark]
|
||||
|
||||
proc fillMask(
|
||||
shapes: seq[seq[Vec2]], width, height: int, windingRule = NonZero
|
||||
): Mask =
|
||||
result = newMask(width, height)
|
||||
|
||||
let
|
||||
segments = shapes.shapesToSegments()
|
||||
bounds = computeBounds(segments).snapToPixels()
|
||||
startY = max(0, bounds.y.int)
|
||||
pathHeight = min(height, (bounds.y + bounds.h).int)
|
||||
partitioning = partitionSegments(segments, startY, pathHeight)
|
||||
width = width.float32
|
||||
|
||||
var
|
||||
hits = newSeq[(float32, int16)](partitioning.maxEntryCount)
|
||||
numHits: int
|
||||
aa: bool
|
||||
for y in startY ..< pathHeight:
|
||||
computeCoverage(
|
||||
cast[ptr UncheckedArray[uint8]](result.data[result.dataIndex(0, y)].addr),
|
||||
hits,
|
||||
numHits,
|
||||
aa,
|
||||
width,
|
||||
y,
|
||||
0,
|
||||
partitioning,
|
||||
windingRule
|
||||
)
|
||||
if not aa:
|
||||
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, width):
|
||||
let
|
||||
startIndex = result.dataIndex(prevAt.int, y)
|
||||
len = at.int - prevAt.int
|
||||
fillUnsafe(result.data, 255, startIndex, len)
|
||||
|
||||
proc fillMask*(
|
||||
path: SomePath, width, height: int, windingRule = NonZero
|
||||
): Mask =
|
||||
## Returns a new mask with the path filled. This is a faster alternative
|
||||
## to `newMask` + `fillPath`.
|
||||
let shapes = parseSomePath(path, true, 1)
|
||||
shapes.fillMask(width, height, windingRule)
|
||||
|
||||
proc fillImage(
|
||||
shapes: seq[seq[Vec2]],
|
||||
width, height: int,
|
||||
color: SomeColor,
|
||||
windingRule = NonZero
|
||||
): Image =
|
||||
result = newImage(width, height)
|
||||
|
||||
let
|
||||
mask = shapes.fillMask(width, height, windingRule)
|
||||
rgbx = color.rgbx()
|
||||
|
||||
var i: int
|
||||
when defined(amd64) and not defined(pixieNoSimd):
|
||||
let
|
||||
colorVec = mm_set1_epi32(cast[int32](rgbx))
|
||||
oddMask = mm_set1_epi16(cast[int16](0xff00))
|
||||
div255 = mm_set1_epi16(cast[int16](0x8081))
|
||||
vec255 = mm_set1_epi32(cast[int32](uint32.high))
|
||||
vecZero = mm_setzero_si128()
|
||||
colorVecEven = mm_slli_epi16(colorVec, 8)
|
||||
colorVecOdd = mm_and_si128(colorVec, oddMask)
|
||||
iterations = result.data.len div 16
|
||||
for _ in 0 ..< iterations:
|
||||
var coverageVec = mm_loadu_si128(mask.data[i].addr)
|
||||
if mm_movemask_epi8(mm_cmpeq_epi16(coverageVec, vecZero)) != 0xffff:
|
||||
if mm_movemask_epi8(mm_cmpeq_epi32(coverageVec, vec255)) == 0xffff:
|
||||
for q in [0, 4, 8, 12]:
|
||||
mm_storeu_si128(result.data[i + q].addr, colorVec)
|
||||
else:
|
||||
for q in [0, 4, 8, 12]:
|
||||
var unpacked = unpackAlphaValues(coverageVec)
|
||||
# Shift the coverages from `a` to `g` and `a` for multiplying
|
||||
unpacked = mm_or_si128(unpacked, mm_srli_epi32(unpacked, 16))
|
||||
|
||||
var
|
||||
sourceEven = mm_mulhi_epu16(colorVecEven, unpacked)
|
||||
sourceOdd = mm_mulhi_epu16(colorVecOdd, unpacked)
|
||||
sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7)
|
||||
sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7)
|
||||
|
||||
mm_storeu_si128(
|
||||
result.data[i + q].addr,
|
||||
mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8))
|
||||
)
|
||||
|
||||
coverageVec = mm_srli_si128(coverageVec, 4)
|
||||
|
||||
i += 16
|
||||
|
||||
let channels = [rgbx.r.uint32, rgbx.g.uint32, rgbx.b.uint32, rgbx.a.uint32]
|
||||
for i in i ..< result.data.len:
|
||||
let coverage = mask.data[i]
|
||||
if coverage == 255:
|
||||
result.data[i] = rgbx
|
||||
elif coverage != 0:
|
||||
result.data[i].r = ((channels[0] * coverage) div 255).uint8
|
||||
result.data[i].g = ((channels[1] * coverage) div 255).uint8
|
||||
result.data[i].b = ((channels[2] * coverage) div 255).uint8
|
||||
result.data[i].a = ((channels[3] * coverage) div 255).uint8
|
||||
|
||||
proc fillImage*(
|
||||
path: SomePath, width, height: int, color: SomeColor, windingRule = NonZero
|
||||
): Image =
|
||||
## Returns a new image with the path filled. This is a faster alternative
|
||||
## to `newImage` + `fillPath`.
|
||||
let shapes = parseSomePath(path, false, 1)
|
||||
shapes.fillImage(width, height, color, windingRule)
|
||||
|
||||
proc strokeMask*(
|
||||
path: SomePath,
|
||||
width, height: int,
|
||||
strokeWidth: float32 = 1.0,
|
||||
lineCap = ButtCap,
|
||||
lineJoin = MiterJoin,
|
||||
miterLimit = defaultMiterLimit,
|
||||
dashes: seq[float32] = @[]
|
||||
): Mask =
|
||||
## Returns a new mask with the path stroked. This is a faster alternative
|
||||
## to `newImage` + `strokePath`.
|
||||
let strokeShapes = strokeShapes(
|
||||
parseSomePath(path, false, 1),
|
||||
strokeWidth,
|
||||
lineCap,
|
||||
lineJoin,
|
||||
miterLimit,
|
||||
dashes,
|
||||
1
|
||||
)
|
||||
result = strokeShapes.fillMask(width, height, NonZero)
|
||||
|
||||
proc strokeImage*(
|
||||
path: SomePath,
|
||||
width, height: int,
|
||||
color: SomeColor,
|
||||
strokeWidth: float32 = 1.0,
|
||||
lineCap = ButtCap,
|
||||
lineJoin = MiterJoin,
|
||||
miterLimit = defaultMiterLimit,
|
||||
dashes: seq[float32] = @[]
|
||||
): Image =
|
||||
## Returns a new image with the path stroked. This is a faster alternative
|
||||
## to `newImage` + `strokePath`.
|
||||
let strokeShapes = strokeShapes(
|
||||
parseSomePath(path, false, 1),
|
||||
strokeWidth,
|
||||
lineCap,
|
||||
lineJoin,
|
||||
miterLimit,
|
||||
dashes,
|
||||
1
|
||||
)
|
||||
result = strokeShapes.fillImage(width, height, color, NonZero)
|
||||
|
||||
when defined(release):
|
||||
{.pop.}
|
||||
|
||||
|
||||
block:
|
||||
block: # Basic rect
|
||||
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)
|
||||
path.rect(rect(0, 0, 900, 900))
|
||||
|
||||
let
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
|
||||
ctx = surface.create()
|
||||
ctx.setSourceRgba(0, 0, 1, 1)
|
||||
shapes = path.commandsToShapes(true, 1)
|
||||
paint = newPaint(SolidPaint)
|
||||
paint.color = color(0, 0, 0, 1)
|
||||
|
||||
timeIt "cairo1":
|
||||
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()
|
||||
benchmarks.add(Benchmark(
|
||||
name: "rect",
|
||||
fills: @[Fill(
|
||||
shapes: shapes,
|
||||
transform: mat3(),
|
||||
paint: paint,
|
||||
windingRule: NonZero
|
||||
)]))
|
||||
|
||||
# discard surface.writeToPng("cairo1.png")
|
||||
|
||||
let a = newImage(1920, 1080)
|
||||
|
||||
timeIt "pixie1":
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
a.fillPath(p, rgbx(0, 0, 255, 255))
|
||||
|
||||
# a.writeFile("pixie1.png")
|
||||
|
||||
block:
|
||||
block: # Rounded rect
|
||||
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)
|
||||
path.roundedRect(rect(0, 0, 900, 900), 20, 20, 20, 20)
|
||||
|
||||
let
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
|
||||
ctx = surface.create()
|
||||
shapes = path.commandsToShapes(true, 1)
|
||||
paint = newPaint(SolidPaint)
|
||||
paint.color = color(0, 0, 0, 1)
|
||||
|
||||
timeIt "cairo2":
|
||||
ctx.setSourceRgba(1, 1, 1, 1)
|
||||
let operator = ctx.getOperator()
|
||||
ctx.setOperator(OperatorSource)
|
||||
ctx.paint()
|
||||
ctx.setOperator(operator)
|
||||
benchmarks.add(Benchmark(
|
||||
name: "roundedRect",
|
||||
fills: @[Fill(
|
||||
shapes: shapes,
|
||||
transform: mat3(),
|
||||
paint: paint,
|
||||
windingRule: NonZero
|
||||
)]))
|
||||
|
||||
ctx.setSourceRgba(0, 0, 1, 1)
|
||||
|
||||
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("cairo2.png")
|
||||
|
||||
let a = newImage(1920, 1080)
|
||||
|
||||
timeIt "pixie2":
|
||||
a.fill(rgbx(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, rgbx(0, 0, 255, 255))
|
||||
|
||||
# a.writeFile("pixie2.png")
|
||||
|
||||
block:
|
||||
block: # Heart
|
||||
let path = parsePath("""
|
||||
M 100,300
|
||||
A 200,200 0,0,1 500,300
|
||||
|
@ -267,131 +58,114 @@ block:
|
|||
Q 100,600 100,300 z
|
||||
""")
|
||||
|
||||
let shapes = path.commandsToShapes(true, 1)
|
||||
|
||||
let
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
|
||||
ctx = surface.create()
|
||||
shapes = path.commandsToShapes(true, 1)
|
||||
paint = newPaint(SolidPaint)
|
||||
paint.color = color(0, 0, 0, 1)
|
||||
|
||||
timeIt "cairo3":
|
||||
ctx.setSourceRgba(1, 1, 1, 1)
|
||||
let operator = ctx.getOperator()
|
||||
ctx.setOperator(OperatorSource)
|
||||
ctx.paint()
|
||||
ctx.setOperator(operator)
|
||||
benchmarks.add(Benchmark(
|
||||
name: "Heart",
|
||||
fills: @[Fill(
|
||||
shapes: shapes,
|
||||
transform: mat3(),
|
||||
paint: paint,
|
||||
windingRule: NonZero
|
||||
)]))
|
||||
|
||||
ctx.setSourceRgba(1, 0, 0, 1)
|
||||
block: # Tiger
|
||||
let
|
||||
data = readFile("tests/fileformats/svg/Ghostscript_Tiger.svg")
|
||||
parsed = parseSvg(data)
|
||||
|
||||
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()
|
||||
var fills: seq[Fill]
|
||||
|
||||
# discard surface.writeToPng("cairo3.png")
|
||||
for (path, props) in parsed.elements:
|
||||
if props.display and props.opacity > 0:
|
||||
if props.fill != "none":
|
||||
let
|
||||
shapes = path.commandsToShapes(true, 1)
|
||||
paint = parseSomePaint(props.fill)
|
||||
fills.add(Fill(
|
||||
shapes: shapes,
|
||||
transform: props.transform,
|
||||
paint: paint,
|
||||
windingRule: props.fillRule
|
||||
))
|
||||
|
||||
let a = newImage(1000, 1000)
|
||||
if props.stroke != rgbx(0, 0, 0, 0) and props.strokeWidth > 0:
|
||||
let strokeShapes = strokeShapes(
|
||||
parseSomePath(path, false, props.transform.pixelScale),
|
||||
props.strokeWidth,
|
||||
props.strokeLineCap,
|
||||
props.strokeLineJoin,
|
||||
props.strokeMiterLimit,
|
||||
props.strokeDashArray,
|
||||
props.transform.pixelScale
|
||||
)
|
||||
let paint = newPaint(props.stroke)
|
||||
paint.color.a *= (props.opacity * props.strokeOpacity)
|
||||
fills.add(Fill(
|
||||
shapes: strokeShapes,
|
||||
transform: props.transform,
|
||||
paint: paint,
|
||||
windingRule: NonZero
|
||||
))
|
||||
|
||||
timeIt "pixie3":
|
||||
a.fill(rgbx(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, rgbx(255, 0, 0, 255))
|
||||
|
||||
# a.writeFile("pixie3.png")
|
||||
|
||||
# doDiff(readImage("cairo3.png"), a, "cairo3")
|
||||
# benchmarks.add(fills)
|
||||
|
||||
block:
|
||||
let path = newPath()
|
||||
path.roundedRect(200, 200, 600, 600, 10, 10, 10, 10)
|
||||
|
||||
let shapes = path.commandsToShapes(true, 1)
|
||||
|
||||
# let
|
||||
# surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
|
||||
# ctx = surface.create()
|
||||
|
||||
# timeIt "cairo4":
|
||||
# ctx.setSourceRgba(0, 0, 0, 0)
|
||||
# let operator = ctx.getOperator()
|
||||
# ctx.setOperator(OperatorSource)
|
||||
# ctx.paint()
|
||||
# ctx.setOperator(operator)
|
||||
|
||||
timeIt "cairo4":
|
||||
for benchmark in benchmarks:
|
||||
let
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 900, 900)
|
||||
ctx = surface.create()
|
||||
|
||||
ctx.setSourceRgba(1, 0, 0, 0.5)
|
||||
timeIt "[cairo] " & benchmark.name:
|
||||
for fill in benchmark.fills:
|
||||
if fill.shapes.len > 0:
|
||||
ctx.newPath()
|
||||
for shape in fill.shapes:
|
||||
ctx.moveTo(shape[0].x, shape[0].y)
|
||||
for v in shape:
|
||||
ctx.lineTo(v.x, v.y)
|
||||
let
|
||||
color = fill.paint.color
|
||||
matrix = Matrix(
|
||||
xx: fill.transform[0, 0],
|
||||
yx: fill.transform[0, 1],
|
||||
xy: fill.transform[1, 0],
|
||||
yy: fill.transform[1, 1],
|
||||
x0: fill.transform[2, 0],
|
||||
y0: fill.transform[2, 1],
|
||||
)
|
||||
ctx.setSourceRgba(color.r, color.g, color.b, color.a)
|
||||
ctx.setMatrix(matrix.unsafeAddr)
|
||||
ctx.setFillRule(
|
||||
if fill.windingRule == NonZero:
|
||||
FillRuleWinding
|
||||
else:
|
||||
FillRuleEvenOdd
|
||||
)
|
||||
ctx.fill()
|
||||
|
||||
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(("cairo_" & benchmark.name & ".png").cstring)
|
||||
|
||||
# discard surface.writeToPng("cairo4.png")
|
||||
block:
|
||||
for benchmark in benchmarks:
|
||||
let image = newImage(900, 900)
|
||||
|
||||
var a: Image
|
||||
timeIt "pixie4":
|
||||
a = newImage(1000, 1000)
|
||||
timeIt "[pixie] " & benchmark.name:
|
||||
for fill in benchmark.fills:
|
||||
if fill.shapes.len > 0:
|
||||
let p = newPath()
|
||||
for shape in fill.shapes:
|
||||
p.moveTo(shape[0])
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
image.fillPath(
|
||||
p,
|
||||
fill.paint,
|
||||
fill.transform,
|
||||
fill.windingRule
|
||||
)
|
||||
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
a.fillPath(p, rgbx(127, 0, 0, 127))
|
||||
|
||||
# a.writeFile("pixie4.png")
|
||||
|
||||
# doDiff(readImage("cairo4.png"), a, "4")
|
||||
|
||||
var b: Image
|
||||
let paint = newPaint(SolidPaint)
|
||||
paint.color = color(1, 0, 0, 0.5)
|
||||
paint.blendMode = OverwriteBlend
|
||||
|
||||
timeIt "pixie4 overwrite":
|
||||
b = newImage(1000, 1000)
|
||||
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
b.fillPath(p, paint)
|
||||
|
||||
# b.writeFile("b.png")
|
||||
|
||||
timeIt "pixie4 mask":
|
||||
let mask = newMask(1000, 1000)
|
||||
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
mask.fillPath(p)
|
||||
|
||||
var tmp: Image
|
||||
timeIt "pixie fillImage":
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
|
||||
tmp = p.fillImage(1000, 1000, rgbx(127, 0, 0, 127))
|
||||
|
||||
# tmp.writeFile("tmp.png")
|
||||
# image.writeFile("pixie_" & benchmark.name & ".png")
|
||||
|
|
397
experiments/benchmark_cairo_old.nim
Normal file
397
experiments/benchmark_cairo_old.nim
Normal file
|
@ -0,0 +1,397 @@
|
|||
import benchy, cairo, chroma, math, pixie, pixie/paths {.all.}, strformat
|
||||
|
||||
when defined(amd64) and not defined(pixieNoSimd):
|
||||
import nimsimd/sse2, pixie/internal
|
||||
|
||||
proc doDiff(a, b: Image, name: string) =
|
||||
let (diffScore, diffImage) = diff(a, b)
|
||||
echo &"{name} score: {diffScore}"
|
||||
diffImage.writeFile(&"{name}_diff.png")
|
||||
|
||||
when defined(release):
|
||||
{.push checks: off.}
|
||||
|
||||
proc fillMask(
|
||||
shapes: seq[seq[Vec2]], width, height: int, windingRule = NonZero
|
||||
): Mask =
|
||||
result = newMask(width, height)
|
||||
|
||||
let
|
||||
segments = shapes.shapesToSegments()
|
||||
bounds = computeBounds(segments).snapToPixels()
|
||||
startY = max(0, bounds.y.int)
|
||||
pathHeight = min(height, (bounds.y + bounds.h).int)
|
||||
partitioning = partitionSegments(segments, startY, pathHeight)
|
||||
width = width.float32
|
||||
|
||||
var
|
||||
hits = newSeq[(float32, int16)](partitioning.maxEntryCount)
|
||||
numHits: int
|
||||
aa: bool
|
||||
for y in startY ..< pathHeight:
|
||||
computeCoverage(
|
||||
cast[ptr UncheckedArray[uint8]](result.data[result.dataIndex(0, y)].addr),
|
||||
hits,
|
||||
numHits,
|
||||
aa,
|
||||
width,
|
||||
y,
|
||||
0,
|
||||
partitioning,
|
||||
windingRule
|
||||
)
|
||||
if not aa:
|
||||
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, width):
|
||||
let
|
||||
startIndex = result.dataIndex(prevAt.int, y)
|
||||
len = at.int - prevAt.int
|
||||
fillUnsafe(result.data, 255, startIndex, len)
|
||||
|
||||
proc fillMask*(
|
||||
path: SomePath, width, height: int, windingRule = NonZero
|
||||
): Mask =
|
||||
## Returns a new mask with the path filled. This is a faster alternative
|
||||
## to `newMask` + `fillPath`.
|
||||
let shapes = parseSomePath(path, true, 1)
|
||||
shapes.fillMask(width, height, windingRule)
|
||||
|
||||
proc fillImage(
|
||||
shapes: seq[seq[Vec2]],
|
||||
width, height: int,
|
||||
color: SomeColor,
|
||||
windingRule = NonZero
|
||||
): Image =
|
||||
result = newImage(width, height)
|
||||
|
||||
let
|
||||
mask = shapes.fillMask(width, height, windingRule)
|
||||
rgbx = color.rgbx()
|
||||
|
||||
var i: int
|
||||
when defined(amd64) and not defined(pixieNoSimd):
|
||||
let
|
||||
colorVec = mm_set1_epi32(cast[int32](rgbx))
|
||||
oddMask = mm_set1_epi16(cast[int16](0xff00))
|
||||
div255 = mm_set1_epi16(cast[int16](0x8081))
|
||||
vec255 = mm_set1_epi32(cast[int32](uint32.high))
|
||||
vecZero = mm_setzero_si128()
|
||||
colorVecEven = mm_slli_epi16(colorVec, 8)
|
||||
colorVecOdd = mm_and_si128(colorVec, oddMask)
|
||||
iterations = result.data.len div 16
|
||||
for _ in 0 ..< iterations:
|
||||
var coverageVec = mm_loadu_si128(mask.data[i].addr)
|
||||
if mm_movemask_epi8(mm_cmpeq_epi16(coverageVec, vecZero)) != 0xffff:
|
||||
if mm_movemask_epi8(mm_cmpeq_epi32(coverageVec, vec255)) == 0xffff:
|
||||
for q in [0, 4, 8, 12]:
|
||||
mm_storeu_si128(result.data[i + q].addr, colorVec)
|
||||
else:
|
||||
for q in [0, 4, 8, 12]:
|
||||
var unpacked = unpackAlphaValues(coverageVec)
|
||||
# Shift the coverages from `a` to `g` and `a` for multiplying
|
||||
unpacked = mm_or_si128(unpacked, mm_srli_epi32(unpacked, 16))
|
||||
|
||||
var
|
||||
sourceEven = mm_mulhi_epu16(colorVecEven, unpacked)
|
||||
sourceOdd = mm_mulhi_epu16(colorVecOdd, unpacked)
|
||||
sourceEven = mm_srli_epi16(mm_mulhi_epu16(sourceEven, div255), 7)
|
||||
sourceOdd = mm_srli_epi16(mm_mulhi_epu16(sourceOdd, div255), 7)
|
||||
|
||||
mm_storeu_si128(
|
||||
result.data[i + q].addr,
|
||||
mm_or_si128(sourceEven, mm_slli_epi16(sourceOdd, 8))
|
||||
)
|
||||
|
||||
coverageVec = mm_srli_si128(coverageVec, 4)
|
||||
|
||||
i += 16
|
||||
|
||||
let channels = [rgbx.r.uint32, rgbx.g.uint32, rgbx.b.uint32, rgbx.a.uint32]
|
||||
for i in i ..< result.data.len:
|
||||
let coverage = mask.data[i]
|
||||
if coverage == 255:
|
||||
result.data[i] = rgbx
|
||||
elif coverage != 0:
|
||||
result.data[i].r = ((channels[0] * coverage) div 255).uint8
|
||||
result.data[i].g = ((channels[1] * coverage) div 255).uint8
|
||||
result.data[i].b = ((channels[2] * coverage) div 255).uint8
|
||||
result.data[i].a = ((channels[3] * coverage) div 255).uint8
|
||||
|
||||
proc fillImage*(
|
||||
path: SomePath, width, height: int, color: SomeColor, windingRule = NonZero
|
||||
): Image =
|
||||
## Returns a new image with the path filled. This is a faster alternative
|
||||
## to `newImage` + `fillPath`.
|
||||
let shapes = parseSomePath(path, false, 1)
|
||||
shapes.fillImage(width, height, color, windingRule)
|
||||
|
||||
proc strokeMask*(
|
||||
path: SomePath,
|
||||
width, height: int,
|
||||
strokeWidth: float32 = 1.0,
|
||||
lineCap = ButtCap,
|
||||
lineJoin = MiterJoin,
|
||||
miterLimit = defaultMiterLimit,
|
||||
dashes: seq[float32] = @[]
|
||||
): Mask =
|
||||
## Returns a new mask with the path stroked. This is a faster alternative
|
||||
## to `newImage` + `strokePath`.
|
||||
let strokeShapes = strokeShapes(
|
||||
parseSomePath(path, false, 1),
|
||||
strokeWidth,
|
||||
lineCap,
|
||||
lineJoin,
|
||||
miterLimit,
|
||||
dashes,
|
||||
1
|
||||
)
|
||||
result = strokeShapes.fillMask(width, height, NonZero)
|
||||
|
||||
proc strokeImage*(
|
||||
path: SomePath,
|
||||
width, height: int,
|
||||
color: SomeColor,
|
||||
strokeWidth: float32 = 1.0,
|
||||
lineCap = ButtCap,
|
||||
lineJoin = MiterJoin,
|
||||
miterLimit = defaultMiterLimit,
|
||||
dashes: seq[float32] = @[]
|
||||
): Image =
|
||||
## Returns a new image with the path stroked. This is a faster alternative
|
||||
## to `newImage` + `strokePath`.
|
||||
let strokeShapes = strokeShapes(
|
||||
parseSomePath(path, false, 1),
|
||||
strokeWidth,
|
||||
lineCap,
|
||||
lineJoin,
|
||||
miterLimit,
|
||||
dashes,
|
||||
1
|
||||
)
|
||||
result = strokeShapes.fillImage(width, height, color, NonZero)
|
||||
|
||||
when defined(release):
|
||||
{.pop.}
|
||||
|
||||
|
||||
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
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
|
||||
ctx = surface.create()
|
||||
ctx.setSourceRgba(0, 0, 1, 1)
|
||||
|
||||
timeIt "cairo1":
|
||||
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("cairo1.png")
|
||||
|
||||
let a = newImage(1920, 1080)
|
||||
|
||||
timeIt "pixie1":
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
a.fillPath(p, rgbx(0, 0, 255, 255))
|
||||
|
||||
# a.writeFile("pixie1.png")
|
||||
|
||||
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
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080)
|
||||
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.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("cairo2.png")
|
||||
|
||||
let a = newImage(1920, 1080)
|
||||
|
||||
timeIt "pixie2":
|
||||
a.fill(rgbx(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, rgbx(0, 0, 255, 255))
|
||||
|
||||
# a.writeFile("pixie2.png")
|
||||
|
||||
block:
|
||||
let path = parsePath("""
|
||||
M 100,300
|
||||
A 200,200 0,0,1 500,300
|
||||
A 200,200 0,0,1 900,300
|
||||
Q 900,600 500,900
|
||||
Q 100,600 100,300 z
|
||||
""")
|
||||
|
||||
let shapes = path.commandsToShapes(true, 1)
|
||||
|
||||
let
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
|
||||
ctx = surface.create()
|
||||
|
||||
timeIt "cairo3":
|
||||
ctx.setSourceRgba(1, 1, 1, 1)
|
||||
let operator = ctx.getOperator()
|
||||
ctx.setOperator(OperatorSource)
|
||||
ctx.paint()
|
||||
ctx.setOperator(operator)
|
||||
|
||||
ctx.setSourceRgba(1, 0, 0, 1)
|
||||
|
||||
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(rgbx(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, rgbx(255, 0, 0, 255))
|
||||
|
||||
# a.writeFile("pixie3.png")
|
||||
|
||||
# doDiff(readImage("cairo3.png"), a, "cairo3")
|
||||
|
||||
block:
|
||||
let path = newPath()
|
||||
path.roundedRect(200, 200, 600, 600, 10, 10, 10, 10)
|
||||
|
||||
let shapes = path.commandsToShapes(true, 1)
|
||||
|
||||
# let
|
||||
# surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
|
||||
# ctx = surface.create()
|
||||
|
||||
# timeIt "cairo4":
|
||||
# ctx.setSourceRgba(0, 0, 0, 0)
|
||||
# let operator = ctx.getOperator()
|
||||
# ctx.setOperator(OperatorSource)
|
||||
# ctx.paint()
|
||||
# ctx.setOperator(operator)
|
||||
|
||||
timeIt "cairo4":
|
||||
let
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000)
|
||||
ctx = surface.create()
|
||||
|
||||
ctx.setSourceRgba(1, 0, 0, 0.5)
|
||||
|
||||
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("cairo4.png")
|
||||
|
||||
var a: Image
|
||||
timeIt "pixie4":
|
||||
a = newImage(1000, 1000)
|
||||
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
a.fillPath(p, rgbx(127, 0, 0, 127))
|
||||
|
||||
# a.writeFile("pixie4.png")
|
||||
|
||||
# doDiff(readImage("cairo4.png"), a, "4")
|
||||
|
||||
var b: Image
|
||||
let paint = newPaint(SolidPaint)
|
||||
paint.color = color(1, 0, 0, 0.5)
|
||||
paint.blendMode = OverwriteBlend
|
||||
|
||||
timeIt "pixie4 overwrite":
|
||||
b = newImage(1000, 1000)
|
||||
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
b.fillPath(p, paint)
|
||||
|
||||
# b.writeFile("b.png")
|
||||
|
||||
timeIt "pixie4 mask":
|
||||
let mask = newMask(1000, 1000)
|
||||
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
mask.fillPath(p)
|
||||
|
||||
var tmp: Image
|
||||
timeIt "pixie fillImage":
|
||||
let p = newPath()
|
||||
p.moveTo(shapes[0][0])
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
p.lineTo(v)
|
||||
|
||||
tmp = p.fillImage(1000, 1000, rgbx(127, 0, 0, 127))
|
||||
|
||||
# tmp.writeFile("tmp.png")
|
|
@ -1,6 +0,0 @@
|
|||
import benchy, svg_cairo
|
||||
|
||||
let data = readFile("tests/fileformats/svg/Ghostscript_Tiger.svg")
|
||||
|
||||
timeIt "svg decode":
|
||||
discard decodeSvg(data)
|
|
@ -1,587 +0,0 @@
|
|||
## Load and Save SVG files.
|
||||
|
||||
import cairo, chroma, pixie/common, pixie/images, pixie/paints, strutils,
|
||||
tables, vmath, xmlparser, xmltree
|
||||
|
||||
include pixie/paths
|
||||
|
||||
proc processCommands(
|
||||
c: ptr Context, path: Path, closeSubpaths: bool, mat: Mat3
|
||||
) =
|
||||
let shapes = path.commandsToShapes(closeSubpaths, mat.pixelScale())
|
||||
if shapes.len == 0:
|
||||
return
|
||||
|
||||
c.newPath()
|
||||
c.moveTo(shapes[0][0].x, shapes[0][0].y)
|
||||
for shape in shapes:
|
||||
for v in shape:
|
||||
c.lineTo(v.x, v.y)
|
||||
|
||||
proc prepare(
|
||||
c: ptr Context,
|
||||
path: Path,
|
||||
paint: Paint,
|
||||
mat: Mat3,
|
||||
closeSubpaths: bool,
|
||||
windingRule = NonZero
|
||||
) =
|
||||
let
|
||||
color = paint.color
|
||||
matrix = Matrix(
|
||||
xx: mat[0, 0],
|
||||
yx: mat[0, 1],
|
||||
xy: mat[1, 0],
|
||||
yy: mat[1, 1],
|
||||
x0: mat[2, 0],
|
||||
y0: mat[2, 1],
|
||||
)
|
||||
c.setSourceRgba(color.r, color.g, color.b, color.a)
|
||||
c.setMatrix(matrix.unsafeAddr)
|
||||
case windingRule:
|
||||
of NonZero:
|
||||
c.setFillRule(FillRuleWinding)
|
||||
else:
|
||||
c.setFillRule(FillRuleEvenOdd)
|
||||
c.processCommands(path, closeSubpaths, mat)
|
||||
|
||||
type
|
||||
LinearGradient = object
|
||||
x1, y1, x2, y2: float32
|
||||
stops: seq[ColorStop]
|
||||
|
||||
Ctx = object
|
||||
display: bool
|
||||
fillRule: WindingRule
|
||||
fill: Paint
|
||||
stroke: ColorRGBX
|
||||
strokeWidth: float32
|
||||
strokeLineCap: LineCap
|
||||
strokeLineJoin: LineJoin
|
||||
strokeMiterLimit: float32
|
||||
strokeDashArray: seq[float32]
|
||||
transform: Mat3
|
||||
shouldStroke: bool
|
||||
opacity, strokeOpacity: float32
|
||||
linearGradients: TableRef[string, LinearGradient]
|
||||
|
||||
template failInvalid() =
|
||||
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 =
|
||||
result.display = true
|
||||
try:
|
||||
result.fill = parseHtmlColor("black").rgbx
|
||||
result.stroke = parseHtmlColor("black").rgbx
|
||||
except:
|
||||
raise currentExceptionAsPixieError()
|
||||
result.strokeWidth = 1
|
||||
result.transform = mat3()
|
||||
result.strokeMiterLimit = defaultMiterLimit
|
||||
result.opacity = 1
|
||||
result.strokeOpacity = 1
|
||||
result.linearGradients = newTable[string, LinearGradient]()
|
||||
|
||||
proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
|
||||
result = inherited
|
||||
|
||||
proc splitArgs(s: string): seq[string] =
|
||||
# Handles (1,1) or (1 1) or (1, 1) or (1,1 2,2) etc
|
||||
let tmp = s.replace(',', ' ').split(' ')
|
||||
for entry in tmp:
|
||||
if entry.len > 0:
|
||||
result.add(entry)
|
||||
|
||||
var
|
||||
fillRule = node.attr("fill-rule")
|
||||
fill = node.attr("fill")
|
||||
stroke = node.attr("stroke")
|
||||
strokeWidth = node.attr("stroke-width")
|
||||
strokeLineCap = node.attr("stroke-linecap")
|
||||
strokeLineJoin = node.attr("stroke-linejoin")
|
||||
strokeMiterLimit = node.attr("stroke-miterlimit")
|
||||
strokeDashArray = node.attr("stroke-dasharray")
|
||||
transform = node.attr("transform")
|
||||
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(';')
|
||||
for pair in pairs:
|
||||
let parts = pair.split(':')
|
||||
if parts.len == 2:
|
||||
# Do not override element properties
|
||||
case parts[0].strip():
|
||||
of "fill-rule":
|
||||
if fillRule.len == 0:
|
||||
fillRule = parts[1].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()
|
||||
of "stroke-miterlimit":
|
||||
if strokeMiterLimit.len == 0:
|
||||
strokeMiterLimit = parts[1].strip()
|
||||
of "stroke-dasharray":
|
||||
if strokeDashArray.len == 0:
|
||||
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 == "":
|
||||
discard # Inherit
|
||||
elif fillRule == "nonzero":
|
||||
result.fillRule = NonZero
|
||||
elif fillRule == "evenodd":
|
||||
result.fillRule = EvenOdd
|
||||
else:
|
||||
raise newException(
|
||||
PixieError, "Invalid fill-rule value " & fillRule
|
||||
)
|
||||
|
||||
if fill == "" or fill == "currentColor":
|
||||
discard # Inherit
|
||||
elif fill == "none":
|
||||
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(LinearGradientPaint)
|
||||
result.fill.gradientHandlePositions = @[
|
||||
result.transform * vec2(linearGradient.x1, linearGradient.y1),
|
||||
result.transform * vec2(linearGradient.x2, linearGradient.y2)
|
||||
]
|
||||
result.fill.gradientStops = linearGradient.stops
|
||||
else:
|
||||
raise newException(PixieError, "Missing SVG resource " & id)
|
||||
else:
|
||||
result.fill = parseHtmlColor(fill).rgbx
|
||||
|
||||
if stroke == "":
|
||||
discard # Inherit
|
||||
elif stroke == "currentColor":
|
||||
result.shouldStroke = true
|
||||
elif stroke == "none":
|
||||
result.stroke = ColorRGBX()
|
||||
else:
|
||||
result.stroke = parseHtmlColor(stroke).rgbx
|
||||
result.shouldStroke = true
|
||||
|
||||
if strokeWidth == "":
|
||||
discard # Inherit
|
||||
else:
|
||||
if strokeWidth.endsWith("px"):
|
||||
strokeWidth = strokeWidth[0 .. ^3]
|
||||
result.strokeWidth = parseFloat(strokeWidth)
|
||||
result.shouldStroke = true
|
||||
|
||||
if result.stroke == ColorRGBX() or result.strokeWidth <= 0:
|
||||
result.shouldStroke = false
|
||||
|
||||
if strokeLineCap == "":
|
||||
discard # Inherit
|
||||
else:
|
||||
case strokeLineCap:
|
||||
of "butt":
|
||||
result.strokeLineCap = ButtCap
|
||||
of "round":
|
||||
result.strokeLineCap = RoundCap
|
||||
of "square":
|
||||
result.strokeLineCap = SquareCap
|
||||
of "inherit":
|
||||
discard
|
||||
else:
|
||||
raise newException(
|
||||
PixieError, "Invalid stroke-linecap value " & strokeLineCap
|
||||
)
|
||||
|
||||
if strokeLineJoin == "":
|
||||
discard # Inherit
|
||||
else:
|
||||
case strokeLineJoin:
|
||||
of "miter":
|
||||
result.strokeLineJoin = MiterJoin
|
||||
of "round":
|
||||
result.strokeLineJoin = RoundJoin
|
||||
of "bevel":
|
||||
result.strokeLineJoin = BevelJoin
|
||||
of "inherit":
|
||||
discard
|
||||
else:
|
||||
raise newException(
|
||||
PixieError, "Invalid stroke-linejoin value " & strokeLineJoin
|
||||
)
|
||||
|
||||
if strokeMiterLimit == "":
|
||||
discard
|
||||
else:
|
||||
result.strokeMiterLimit = parseFloat(strokeMiterLimit)
|
||||
|
||||
if strokeDashArray == "":
|
||||
discard
|
||||
else:
|
||||
var values = splitArgs(strokeDashArray)
|
||||
for value in values:
|
||||
result.strokeDashArray.add(parseFloat(value))
|
||||
|
||||
if transform == "":
|
||||
discard # Inherit
|
||||
else:
|
||||
template failInvalidTransform(transform: string) =
|
||||
raise newException(
|
||||
PixieError, "Unsupported SVG transform: " & transform
|
||||
)
|
||||
|
||||
var remaining = transform
|
||||
while remaining.len > 0:
|
||||
let index = remaining.find(")")
|
||||
if index == -1:
|
||||
failInvalidTransform(transform)
|
||||
let f = remaining[0 .. index].strip()
|
||||
remaining = remaining[index + 1 .. ^1]
|
||||
|
||||
if f.startsWith("matrix("):
|
||||
let arr = splitArgs(f[7 .. ^2])
|
||||
if arr.len != 6:
|
||||
failInvalidTransform(transform)
|
||||
var m = mat3()
|
||||
m[0, 0] = parseFloat(arr[0])
|
||||
m[0, 1] = parseFloat(arr[1])
|
||||
m[1, 0] = parseFloat(arr[2])
|
||||
m[1, 1] = parseFloat(arr[3])
|
||||
m[2, 0] = parseFloat(arr[4])
|
||||
m[2, 1] = parseFloat(arr[5])
|
||||
result.transform = result.transform * m
|
||||
elif f.startsWith("translate("):
|
||||
let
|
||||
components = splitArgs(f[10 .. ^2])
|
||||
tx = parseFloat(components[0])
|
||||
ty =
|
||||
if components.len == 1:
|
||||
0.0
|
||||
else:
|
||||
parseFloat(components[1])
|
||||
result.transform = result.transform * translate(vec2(tx, ty))
|
||||
elif f.startsWith("rotate("):
|
||||
let
|
||||
values = splitArgs(f[7 .. ^2])
|
||||
angle: float32 = parseFloat(values[0]) * -PI / 180
|
||||
var cx, cy: float32
|
||||
if values.len > 1:
|
||||
cx = parseFloat(values[1])
|
||||
if values.len > 2:
|
||||
cy = parseFloat(values[2])
|
||||
let center = vec2(cx, cy)
|
||||
result.transform = result.transform *
|
||||
translate(center) * rotate(angle) * translate(-center)
|
||||
elif f.startsWith("scale("):
|
||||
let
|
||||
values = splitArgs(f[6 .. ^2])
|
||||
sx: float32 = parseFloat(values[0])
|
||||
sy: float32 =
|
||||
if values.len > 1:
|
||||
parseFloat(values[1])
|
||||
else:
|
||||
sx
|
||||
result.transform = result.transform * scale(vec2(sx, sy))
|
||||
else:
|
||||
failInvalidTransform(transform)
|
||||
|
||||
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
||||
try:
|
||||
decodeCtxInternal(inherited, node)
|
||||
except PixieError as e:
|
||||
raise e
|
||||
except:
|
||||
raise currentExceptionAsPixieError()
|
||||
|
||||
proc cairoLineCap(lineCap: LineCap): cairo.LineCap =
|
||||
case lineCap:
|
||||
of ButtCap:
|
||||
LineCapButt
|
||||
of RoundCap:
|
||||
LineCapRound
|
||||
of SquareCap:
|
||||
LineCapSquare
|
||||
|
||||
proc cairoLineJoin(lineJoin: LineJoin): cairo.LineJoin =
|
||||
case lineJoin:
|
||||
of MiterJoin:
|
||||
LineJoinMiter
|
||||
of BevelJoin:
|
||||
LineJoinBevel
|
||||
of RoundJoin:
|
||||
LineJoinRound
|
||||
|
||||
proc fill(c: ptr Context, ctx: Ctx, path: Path) {.inline.} =
|
||||
if ctx.display and ctx.opacity > 0:
|
||||
let paint = newPaint(ctx.fill)
|
||||
paint.opacity = paint.opacity * ctx.opacity
|
||||
prepare(c, path, paint, ctx.transform, true, ctx.fillRule)
|
||||
c.fill()
|
||||
|
||||
proc stroke(c: ptr Context, ctx: Ctx, path: Path) {.inline.} =
|
||||
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, false)
|
||||
c.setLineWidth(ctx.strokeWidth)
|
||||
c.setLineCap(ctx.strokeLineCap.cairoLineCap())
|
||||
c.setLineJoin(ctx.strokeLineJoin.cairoLineJoin())
|
||||
c.setMiterLimit(ctx.strokeMiterLimit)
|
||||
c.stroke()
|
||||
|
||||
proc drawInternal(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||
if node.kind != xnElement:
|
||||
# Skip <!-- comments -->
|
||||
return
|
||||
|
||||
case node.tag:
|
||||
of "title", "desc", "defs":
|
||||
discard
|
||||
|
||||
of "g":
|
||||
let ctx = decodeCtx(ctxStack[^1], node)
|
||||
ctxStack.add(ctx)
|
||||
for child in node:
|
||||
img.drawInternal(child, ctxStack)
|
||||
discard ctxStack.pop()
|
||||
|
||||
of "path":
|
||||
let
|
||||
d = node.attr("d")
|
||||
ctx = decodeCtx(ctxStack[^1], node)
|
||||
path = parsePath(d)
|
||||
|
||||
img.fill(ctx, path)
|
||||
if ctx.shouldStroke:
|
||||
img.stroke(ctx, path)
|
||||
|
||||
of "line":
|
||||
let
|
||||
ctx = decodeCtx(ctxStack[^1], node)
|
||||
x1 = parseFloat(node.attrOrDefault("x1", "0"))
|
||||
y1 = parseFloat(node.attrOrDefault("y1", "0"))
|
||||
x2 = parseFloat(node.attrOrDefault("x2", "0"))
|
||||
y2 = parseFloat(node.attrOrDefault("y2", "0"))
|
||||
|
||||
let path = newPath()
|
||||
path.moveTo(x1, y1)
|
||||
path.lineTo(x2, y2)
|
||||
|
||||
if ctx.shouldStroke:
|
||||
img.stroke(ctx, path)
|
||||
|
||||
of "polyline", "polygon":
|
||||
let
|
||||
ctx = decodeCtx(ctxStack[^1], node)
|
||||
points = node.attr("points")
|
||||
|
||||
var vecs: seq[Vec2]
|
||||
if points.contains(","):
|
||||
for pair in points.split(" "):
|
||||
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()
|
||||
for i in 0 ..< points.len div 2:
|
||||
vecs.add(vec2(parseFloat(points[i * 2]), parseFloat(points[i * 2 + 1])))
|
||||
|
||||
if vecs.len == 0:
|
||||
failInvalid()
|
||||
|
||||
let path = newPath()
|
||||
path.moveTo(vecs[0])
|
||||
for i in 1 ..< vecs.len:
|
||||
path.lineTo(vecs[i])
|
||||
|
||||
# The difference between polyline and polygon is whether we close the path
|
||||
# and fill or not
|
||||
if node.tag == "polygon":
|
||||
path.closePath()
|
||||
img.fill(ctx, path)
|
||||
|
||||
if ctx.shouldStroke:
|
||||
img.stroke(ctx, path)
|
||||
|
||||
of "rect":
|
||||
let
|
||||
ctx = decodeCtx(ctxStack[^1], node)
|
||||
x = parseFloat(node.attrOrDefault("x", "0"))
|
||||
y = parseFloat(node.attrOrDefault("y", "0"))
|
||||
width = parseFloat(node.attrOrDefault("width", "0"))
|
||||
height = parseFloat(node.attrOrDefault("height", "0"))
|
||||
|
||||
if width == 0 or height == 0:
|
||||
return
|
||||
|
||||
var
|
||||
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
|
||||
ry = max(parseFloat(node.attrOrDefault("ry", "0")), 0)
|
||||
|
||||
let path = newPath()
|
||||
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)
|
||||
|
||||
img.fill(ctx, path)
|
||||
if ctx.shouldStroke:
|
||||
img.stroke(ctx, path)
|
||||
|
||||
of "circle", "ellipse":
|
||||
let
|
||||
ctx = decodeCtx(ctxStack[^1], node)
|
||||
cx = parseFloat(node.attrOrDefault("cx", "0"))
|
||||
cy = parseFloat(node.attrOrDefault("cy", "0"))
|
||||
|
||||
var rx, ry: float32
|
||||
if node.tag == "circle":
|
||||
rx = parseFloat(node.attr("r"))
|
||||
ry = rx
|
||||
else:
|
||||
rx = parseFloat(node.attrOrDefault("rx", "0"))
|
||||
ry = parseFloat(node.attrOrDefault("ry", "0"))
|
||||
|
||||
let path = newPath()
|
||||
path.ellipse(cx, cy, rx, ry)
|
||||
|
||||
img.fill(ctx, path)
|
||||
if ctx.shouldStroke:
|
||||
img.stroke(ctx, path)
|
||||
|
||||
else:
|
||||
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:
|
||||
raise currentExceptionAsPixieError()
|
||||
|
||||
proc decodeSvg*(data: string, width = 0, height = 0): Image =
|
||||
## Render SVG file and return the image. Defaults to the SVG's view box size.
|
||||
try:
|
||||
let root = parseXml(data)
|
||||
if root.tag != "svg":
|
||||
failInvalid()
|
||||
|
||||
let
|
||||
viewBox = root.attr("viewBox")
|
||||
box = viewBox.split(" ")
|
||||
viewBoxMinX = parseInt(box[0])
|
||||
viewBoxMinY = parseInt(box[1])
|
||||
viewBoxWidth = parseInt(box[2])
|
||||
viewBoxHeight = parseInt(box[3])
|
||||
|
||||
var rootCtx = initCtx()
|
||||
rootCtx = decodeCtx(rootCtx, root)
|
||||
|
||||
if viewBoxMinX != 0 or viewBoxMinY != 0:
|
||||
rootCtx.transform = rootCtx.transform * translate(
|
||||
vec2(-viewBoxMinX.float32, -viewBoxMinY.float32)
|
||||
)
|
||||
|
||||
var
|
||||
width = width
|
||||
height = height
|
||||
surface: ptr Surface
|
||||
if width == 0 and height == 0: # Default to the view box size
|
||||
width = viewBoxWidth.int32
|
||||
height = viewBoxHeight.int32
|
||||
else:
|
||||
let
|
||||
scaleX = width.float32 / viewBoxWidth.float32
|
||||
scaleY = height.float32 / viewBoxHeight.float32
|
||||
rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY))
|
||||
|
||||
surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32)
|
||||
|
||||
let c = surface.create()
|
||||
|
||||
var ctxStack = @[rootCtx]
|
||||
for node in root:
|
||||
c.draw(node, ctxStack)
|
||||
|
||||
surface.flush()
|
||||
|
||||
result = newImage(width, height)
|
||||
|
||||
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.unsafe[x, y] = rgba.rgbx()
|
||||
except PixieError as e:
|
||||
raise e
|
||||
except:
|
||||
raise newException(PixieError, "Unable to load SVG")
|
Loading…
Reference in a new issue