Merge pull request #211 from guzba/master

2.0.1, fixes, more emoji test
This commit is contained in:
treeform 2021-05-29 14:13:32 -07:00 committed by GitHub
commit 1df4ed5b89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 155 deletions

View file

@ -95,33 +95,14 @@ proc prepare(
c.setFillRule(FillRuleEvenOdd) c.setFillRule(FillRuleEvenOdd)
c.processCommands(path) c.processCommands(path)
proc fillPath(
c: ptr Context,
path: Path,
color: ColorRGBA,
mat: Mat3,
windingRule = wrNonZero
) =
prepare(c, path, color, mat, windingRule)
c.fill()
proc strokePath(
c: ptr Context,
path: Path,
color: ColorRGBA,
mat: Mat3,
strokeWidth: float32
) =
prepare(c, path, color, mat)
c.setLineWidth(strokeWidth)
c.stroke()
type Ctx = object type Ctx = object
fillRule: WindingRule fillRule: WindingRule
fill, stroke: ColorRGBA fill, stroke: ColorRGBA
strokeWidth: float32 strokeWidth: float32
strokeLineCap: paths.LineCap strokeLineCap: paths.LineCap
strokeLineJoin: paths.LineJoin strokeLineJoin: paths.LineJoin
strokeMiterLimit: float32
strokeDashArray: seq[float32]
transform: Mat3 transform: Mat3
shouldStroke: bool shouldStroke: bool
@ -138,10 +119,18 @@ proc initCtx(): Ctx =
result.stroke = parseHtmlColor("black").rgba result.stroke = parseHtmlColor("black").rgba
result.strokeWidth = 1 result.strokeWidth = 1
result.transform = mat3() result.transform = mat3()
result.strokeMiterLimit = defaultMiterLimit
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
result = inherited result = inherited
proc splitArgs(s: string): seq[string] =
# Handles (1,1) or (1 1) or (1, 1) or (1,1 2,2) etc
let tmp = s.replace(',', ' ').split(' ')
for entry in tmp:
if entry.len > 0:
result.add(entry)
var var
fillRule = node.attr("fill-rule") fillRule = node.attr("fill-rule")
fill = node.attr("fill") fill = node.attr("fill")
@ -149,6 +138,8 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
strokeWidth = node.attr("stroke-width") strokeWidth = node.attr("stroke-width")
strokeLineCap = node.attr("stroke-linecap") strokeLineCap = node.attr("stroke-linecap")
strokeLineJoin = node.attr("stroke-linejoin") strokeLineJoin = node.attr("stroke-linejoin")
strokeMiterLimit = node.attr("stroke-miterlimit")
strokeDashArray = node.attr("stroke-dasharray")
transform = node.attr("transform") transform = node.attr("transform")
style = node.attr("style") style = node.attr("style")
@ -173,6 +164,12 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
of "stroke-width": of "stroke-width":
if strokeWidth.len == 0: if strokeWidth.len == 0:
strokeWidth = parts[1].strip() 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()
if fillRule == "": if fillRule == "":
discard # Inherit discard # Inherit
@ -247,6 +244,18 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
PixieError, "Invalid stroke-linejoin value " & strokeLineJoin 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 == "": if transform == "":
discard # Inherit discard # Inherit
else: else:
@ -264,46 +273,83 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
remaining = remaining[index + 1 .. ^1] remaining = remaining[index + 1 .. ^1]
if f.startsWith("matrix("): if f.startsWith("matrix("):
let arr = let arr = splitArgs(f[7 .. ^2])
if f.contains(","):
f[7 .. ^2].split(",")
else:
f[7 .. ^2].split(" ")
if arr.len != 6: if arr.len != 6:
failInvalidTransform(transform) failInvalidTransform(transform)
var m = mat3() var m = mat3()
m[0] = parseFloat(arr[0].strip()) m[0, 0] = parseFloat(arr[0])
m[1] = parseFloat(arr[1].strip()) m[0, 1] = parseFloat(arr[1])
m[3] = parseFloat(arr[2].strip()) m[1, 0] = parseFloat(arr[2])
m[4] = parseFloat(arr[3].strip()) m[1, 1] = parseFloat(arr[3])
m[6] = parseFloat(arr[4].strip()) m[2, 0] = parseFloat(arr[4])
m[7] = parseFloat(arr[5].strip()) m[2, 1] = parseFloat(arr[5])
result.transform = result.transform * m result.transform = result.transform * m
elif f.startsWith("translate("): elif f.startsWith("translate("):
let let
components = f[10 .. ^2].split(" ") components = splitArgs(f[10 .. ^2])
tx = parseFloat(components[0].strip()) tx = parseFloat(components[0])
ty = ty =
if components[1].len == 0: if components.len == 1:
0.0 0.0
else: else:
parseFloat(components[1].strip()) parseFloat(components[1])
result.transform = result.transform * translate(vec2(tx, ty)) result.transform = result.transform * translate(vec2(tx, ty))
elif f.startsWith("rotate("): elif f.startsWith("rotate("):
let let
values = f[7 .. ^2].split(" ") values = splitArgs(f[7 .. ^2])
angle = parseFloat(values[0].strip()) * -PI / 180 angle: float32 = parseFloat(values[0]) * -PI / 180
var cx, cy: float32 var cx, cy: float32
if values.len > 1: if values.len > 1:
cx = parseFloat(values[1].strip()) cx = parseFloat(values[1])
if values.len > 2: if values.len > 2:
cy = parseFloat(values[2].strip()) cy = parseFloat(values[2])
let center = vec2(cx, cy) let center = vec2(cx, cy)
result.transform = result.transform * result.transform = result.transform *
translate(center) * rotationMat3(angle) * translate(-center) 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: else:
failInvalidTransform(transform) failInvalidTransform(transform)
proc cairoLineCap(lineCap: paths.LineCap): cairo.LineCap =
case lineCap:
of lcButt:
LineCapButt
of lcRound:
LineCapRound
of lcSquare:
LineCapSquare
proc cairoLineJoin(lineJoin: paths.LineJoin): cairo.LineJoin =
case lineJoin:
of ljMiter:
LineJoinMiter
of ljBevel:
LineJoinBevel
of ljRound:
LineJoinRound
proc fill(c: ptr Context, ctx: Ctx, path: Path) {.inline.} =
# img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule)
prepare(c, path, ctx.fill, ctx.transform, ctx.fillRule)
c.fill()
proc stroke(c: ptr Context, ctx: Ctx, path: Path) {.inline.} =
prepare(c, path, ctx.stroke, ctx.transform)
c.setLineWidth(ctx.strokeWidth)
c.setLineCap(ctx.strokeLineCap.cairoLineCap())
c.setLineJoin(ctx.strokeLineJoin.cairoLineJoin())
c.setMiterLimit(ctx.strokeMiterLimit)
c.stroke()
proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
if node.kind != xnElement: if node.kind != xnElement:
# Skip <!-- comments --> # Skip <!-- comments -->
@ -326,9 +372,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
ctx = decodeCtx(ctxStack[^1], node) ctx = decodeCtx(ctxStack[^1], node)
path = parsePath(d) path = parsePath(d)
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) img.stroke(ctx, path)
of "line": of "line":
let let
@ -341,12 +387,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
var path: Path var path: Path
path.moveTo(x1, y1) path.moveTo(x1, y1)
path.lineTo(x2, y2) path.lineTo(x2, y2)
path.closePath()
if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform)
if ctx.shouldStroke: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) img.stroke(ctx, path)
of "polyline", "polygon": of "polyline", "polygon":
let let
@ -376,21 +419,26 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
path.lineTo(vecs[i]) path.lineTo(vecs[i])
# The difference between polyline and polygon is whether we close the path # The difference between polyline and polygon is whether we close the path
# and fill or not
if node.tag == "polygon": if node.tag == "polygon":
path.closePath() path.closePath()
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) img.stroke(ctx, path)
of "rect": of "rect":
let let
ctx = decodeCtx(ctxStack[^1], node) ctx = decodeCtx(ctxStack[^1], node)
x = parseFloat(node.attrOrDefault("x", "0")) x = parseFloat(node.attrOrDefault("x", "0"))
y = parseFloat(node.attrOrDefault("y", "0")) y = parseFloat(node.attrOrDefault("y", "0"))
width = parseFloat(node.attr("width")) width = parseFloat(node.attrOrDefault("width", "0"))
height = parseFloat(node.attr("height")) height = parseFloat(node.attrOrDefault("height", "0"))
if width == 0 or height == 0:
return
var var
rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0) rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0)
@ -418,9 +466,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
path.rect(x, y, width, height) path.rect(x, y, width, height)
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) img.stroke(ctx, path)
of "circle", "ellipse": of "circle", "ellipse":
let let
@ -440,9 +488,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) =
path.ellipse(cx, cy, rx, ry) path.ellipse(cx, cy, rx, ry)
if ctx.fill != ColorRGBA(): if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform) img.fill(ctx, path)
if ctx.shouldStroke: if ctx.shouldStroke:
img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) img.stroke(ctx, path)
else: else:
raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".")
@ -457,12 +505,19 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image =
let let
viewBox = root.attr("viewBox") viewBox = root.attr("viewBox")
box = viewBox.split(" ") box = viewBox.split(" ")
viewBoxMinX = parseInt(box[0])
viewBoxMinY = parseInt(box[1])
viewBoxWidth = parseInt(box[2]) viewBoxWidth = parseInt(box[2])
viewBoxHeight = parseInt(box[3]) viewBoxHeight = parseInt(box[3])
var rootCtx = initCtx() var rootCtx = initCtx()
rootCtx = decodeCtx(rootCtx, root) rootCtx = decodeCtx(rootCtx, root)
if viewBoxMinX != 0 or viewBoxMinY != 0:
rootCtx.transform = rootCtx.transform * translate(
vec2(-viewBoxMinX.float32, -viewBoxMinY.float32)
)
var surface: ptr Surface var 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) result = newImage(viewBoxWidth, viewBoxHeight)
@ -476,7 +531,7 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image =
let let
scaleX = width.float32 / viewBoxWidth.float32 scaleX = width.float32 / viewBoxWidth.float32
scaleY = height.float32 / viewBoxHeight.float32 scaleY = height.float32 / viewBoxHeight.float32
rootCtx.transform = scale(vec2(scaleX, scaleY)) rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY))
let c = surface.create() let c = surface.create()

View file

@ -14,10 +14,5 @@ const files = [
] ]
for file in files: for file in files:
let let image = decodeSvg(readFile(&"tests/images/svg/{file}.svg"))
original = readFile(&"tests/images/svg/{file}.svg")
image = decodeSvg(original)
gold = readImage(&"tests/images/svg/{file}.png")
# doAssert image.data == gold.data
image.writeFile(&"tests/images/svg/{file}.png") image.writeFile(&"tests/images/svg/{file}.png")

View file

@ -1,4 +1,4 @@
version = "2.0.0" version = "2.0.1"
author = "Andre von Houck and Ryan Oldenburg" author = "Andre von Houck and Ryan Oldenburg"
description = "Full-featured 2d graphics library for Nim." description = "Full-featured 2d graphics library for Nim."
license = "MIT" license = "MIT"

View file

@ -36,6 +36,10 @@ type
SomePath* = Path | string SomePath* = Path | string
Partitioning = object
partitions: seq[seq[(Segment, int16)]]
startY, partitionHeight: uint32
const const
epsilon = 0.0001 * PI ## Tiny value used for some computations. epsilon = 0.0001 * PI ## Tiny value used for some computations.
defaultMiterLimit*: float32 = 4 defaultMiterLimit*: float32 = 4
@ -604,8 +608,8 @@ proc polygon*(path: var Path, pos: Vec2, size: float32, sides: int) {.inline.} =
## Adds a n-sided regular polygon at (x, y) with the parameter size. ## Adds a n-sided regular polygon at (x, y) with the parameter size.
path.polygon(pos.x, pos.y, size, sides) path.polygon(pos.x, pos.y, size, sides)
proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = proc commandsToShapes(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] =
## Converts SVG-like commands to line segments. ## Converts SVG-like commands to sequences of vectors.
var var
start, at: Vec2 start, at: Vec2
shape: seq[Vec2] shape: seq[Vec2]
@ -962,6 +966,90 @@ proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] =
if shape.len > 0: if shape.len > 0:
result.add(shape) result.add(shape)
proc shapesToSegments(shapes: seq[seq[Vec2]]): seq[(Segment, int16)] =
## Converts the shapes into a set of filtered segments with winding value.
var segmentCount: int
for shape in shapes:
segmentCount += shape.len - 1
for shape in shapes:
for segment in shape.segments:
if segment.at.y == segment.to.y: # Skip horizontal
continue
var
segment = segment
winding = 1.int16
if segment.at.y > segment.to.y:
swap(segment.at, segment.to)
winding = -1
result.add((segment, winding))
proc computePixelBounds(segments: seq[(Segment, int16)]): Rect =
## Compute the bounds of the segments.
var
xMin = float32.high
xMax = float32.low
yMin = float32.high
yMax = float32.low
for (segment, _) in segments:
xMin = min(xMin, min(segment.at.x, segment.to.x))
xMax = max(xMax, max(segment.at.x, segment.to.x))
yMin = min(yMin, min(segment.at.y, segment.to.y))
yMax = max(yMax, max(segment.at.y, segment.to.y))
xMin = floor(xMin)
xMax = ceil(xMax)
yMin = floor(yMin)
yMax = ceil(yMax)
if xMin.isNaN() or xMax.isNaN() or yMin.isNaN() or yMax.isNaN():
discard
else:
result.x = xMin
result.y = yMin
result.w = xMax - xMin
result.h = yMax - yMin
proc computePixelBounds*(path: Path): Rect =
## Compute the bounds of the path.
path.commandsToShapes().shapesToSegments().computePixelBounds()
proc partitionSegments(
segments: seq[(Segment, int16)], top, height: int
): Partitioning =
## Puts segments into the height partitions they intersect with.
let
maxPartitions = max(1, height div 10).uint32
numPartitions = min(maxPartitions, max(1, segments.len div 10).uint32)
result.partitions.setLen(numPartitions)
result.startY = top.uint32
result.partitionHeight = height.uint32 div numPartitions
for (segment, winding) in segments:
if result.partitionHeight == 0:
result.partitions[0].add((segment, winding))
else:
var
atPartition = max(0, segment.at.y - result.startY.float32).uint32
toPartition = max(0, ceil(segment.to.y - result.startY.float32)).uint32
atPartition = atPartition div result.partitionHeight
toPartition = toPartition div result.partitionHeight
atPartition = clamp(atPartition, 0, result.partitions.high.uint32)
toPartition = clamp(toPartition, 0, result.partitions.high.uint32)
for i in atPartition .. toPartition:
result.partitions[i].add((segment, winding))
proc getIndexForY(partitioning: Partitioning, y: int): uint32 {.inline.} =
if partitioning.partitionHeight == 0 or partitioning.partitions.len == 1:
0.uint32
else:
min(
(y.uint32 - partitioning.startY) div partitioning.partitionHeight,
partitioning.partitions.high.uint32
)
proc quickSort(a: var seq[(float32, int16)], inl, inr: int) = proc quickSort(a: var seq[(float32, int16)], inl, inr: int) =
## Sorts in place + faster than standard lib sort. ## Sorts in place + faster than standard lib sort.
var var
@ -983,30 +1071,6 @@ proc quickSort(a: var seq[(float32, int16)], inl, inr: int) =
quickSort(a, inl, r) quickSort(a, inl, r)
quickSort(a, l, inr) quickSort(a, l, inr)
proc computeBounds(partitions: seq[seq[(Segment, int16)]]): Rect =
## Compute bounds of a shape segments with windings.
var
xMin = float32.high
xMax = float32.low
yMin = float32.high
yMax = float32.low
for partition in partitions:
for (segment, _) in partition:
xMin = min(xMin, min(segment.at.x, segment.to.x))
xMax = max(xMax, max(segment.at.x, segment.to.x))
yMin = min(yMin, min(segment.at.y, segment.to.y))
yMax = max(yMax, max(segment.at.y, segment.to.y))
xMin = floor(xMin)
xMax = ceil(xMax)
yMin = floor(yMin)
yMax = ceil(yMax)
result.x = xMin
result.y = yMin
result.w = xMax - xMin
result.h = yMax - yMin
proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} = proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} =
## Should we fill based on the current winding rule and count? ## Should we fill based on the current winding rule and count?
case windingRule: case windingRule:
@ -1015,49 +1079,12 @@ proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} =
of wrEvenOdd: of wrEvenOdd:
count mod 2 != 0 count mod 2 != 0
proc partitionSegments(
shapes: seq[seq[Vec2]], height: int
): seq[seq[(Segment, int16)]] =
## Puts segments into the height partitions they intersect with.
var segmentCount: int
for shape in shapes:
segmentCount += shape.len - 1
let
maxPartitions = max(1, height div 10).uint32
numPartitions = min(maxPartitions, max(1, segmentCount div 10).uint32)
partitionHeight = (height.uint32 div numPartitions)
result.setLen(numPartitions)
for shape in shapes:
for segment in shape.segments:
if segment.at.y == segment.to.y: # Skip horizontal
continue
var
segment = segment
winding = 1.int16
if segment.at.y > segment.to.y:
swap(segment.at, segment.to)
winding = -1
if partitionHeight == 0:
result[0].add((segment, winding))
else:
var
atPartition = max(0, segment.at.y).uint32 div partitionHeight
toPartition = max(0, ceil(segment.to.y)).uint32 div partitionHeight
atPartition = clamp(atPartition, 0, result.high.uint32)
toPartition = clamp(toPartition, 0, result.high.uint32)
for i in atPartition .. toPartition:
result[i].add((segment, winding))
template computeCoverages( template computeCoverages(
coverages: var seq[uint8], coverages: var seq[uint8],
hits: var seq[(float32, int16)], hits: var seq[(float32, int16)],
size: Vec2, size: Vec2,
y: int, y: int,
partitions: seq[seq[(Segment, int16)]], partitioning: Partitioning,
partitionHeight: uint32,
windingRule: WindingRule windingRule: WindingRule
) = ) =
const const
@ -1066,16 +1093,10 @@ template computeCoverages(
offset = 1 / quality.float32 offset = 1 / quality.float32
initialOffset = offset / 2 + epsilon initialOffset = offset / 2 + epsilon
let
partition =
if partitionHeight == 0 or partitions.len == 1:
0.uint32
else:
min(y.uint32 div partitionHeight, partitions.high.uint32)
zeroMem(coverages[0].addr, coverages.len) zeroMem(coverages[0].addr, coverages.len)
# Do scanlines for this row # Do scanlines for this row
let partition = getIndexForY(partitioning, y)
var var
yLine = y.float32 + initialOffset - offset yLine = y.float32 + initialOffset - offset
numHits: int numHits: int
@ -1083,10 +1104,10 @@ template computeCoverages(
yLine += offset yLine += offset
let scanline = line(vec2(0, yLine), vec2(size.x, yLine)) let scanline = line(vec2(0, yLine), vec2(size.x, yLine))
numHits = 0 numHits = 0
for (segment, winding) in partitions[partition]: for (segment, winding) in partitioning.partitions[partition]:
if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y: if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y:
var at: Vec2 var at: Vec2
if scanline.intersects(segment, at): # and segment.to != at: if scanline.intersects(segment, at) and segment.to != at:
if numHits == hits.len: if numHits == hits.len:
hits.setLen(hits.len * 2) hits.setLen(hits.len * 2)
hits[numHits] = (min(at.x, size.x), winding) hits[numHits] = (min(at.x, size.x), winding)
@ -1150,19 +1171,18 @@ proc fillShapes(
windingRule: WindingRule, windingRule: WindingRule,
blendMode: BlendMode blendMode: BlendMode
) = ) =
let
rgbx = color.asRgbx()
partitions = partitionSegments(shapes, image.height)
partitionHeight = image.height.uint32 div partitions.len.uint32
# Figure out the total bounds of all the shapes, # Figure out the total bounds of all the shapes,
# rasterize only within the total bounds # rasterize only within the total bounds
let let
bounds = computeBounds(partitions) rgbx = color.asRgbx()
blender = blendMode.blender()
segments = shapes.shapesToSegments()
bounds = computePixelBounds(segments)
startX = max(0, bounds.x.int) startX = max(0, bounds.x.int)
startY = max(0, bounds.y.int) startY = max(0, bounds.y.int)
stopY = min(image.height, (bounds.y + bounds.h).int) stopY = min(image.height, (bounds.y + bounds.h).int)
blender = blendMode.blender() pathHeight = stopY - startY
partitions = partitionSegments(segments, startY, pathHeight)
var var
coverages = newSeq[uint8](image.width) coverages = newSeq[uint8](image.width)
@ -1175,7 +1195,6 @@ proc fillShapes(
image.wh, image.wh,
y, y,
partitions, partitions,
partitionHeight,
windingRule windingRule
) )
@ -1264,17 +1283,16 @@ proc fillShapes(
shapes: seq[seq[Vec2]], shapes: seq[seq[Vec2]],
windingRule: WindingRule windingRule: WindingRule
) = ) =
let
partitions = partitionSegments(shapes, mask.height)
partitionHeight = mask.height.uint32 div partitions.len.uint32
# Figure out the total bounds of all the shapes, # Figure out the total bounds of all the shapes,
# rasterize only within the total bounds # rasterize only within the total bounds
let let
bounds = computeBounds(partitions) segments = shapes.shapesToSegments()
bounds = computePixelBounds(segments)
startX = max(0, bounds.x.int) startX = max(0, bounds.x.int)
startY = max(0, bounds.y.int) startY = max(0, bounds.y.int)
stopY = min(mask.height, (bounds.y + bounds.h).int) stopY = min(mask.height, (bounds.y + bounds.h).int)
pathHeight = stopY - startY
partitions = partitionSegments(segments, startY, pathHeight)
when defined(amd64) and not defined(pixieNoSimd): when defined(amd64) and not defined(pixieNoSimd):
let maskerSimd = bmNormal.maskerSimd() let maskerSimd = bmNormal.maskerSimd()
@ -1290,7 +1308,6 @@ proc fillShapes(
mask.wh, mask.wh,
y, y,
partitions, partitions,
partitionHeight,
windingRule windingRule
) )
@ -1488,7 +1505,7 @@ proc parseSomePath(
proc transform(shapes: var seq[seq[Vec2]], transform: Vec2 | Mat3) = proc transform(shapes: var seq[seq[Vec2]], transform: Vec2 | Mat3) =
when type(transform) is Vec2: when type(transform) is Vec2:
if transform != vec2(0, 0): if transform != vec2():
for shape in shapes.mitems: for shape in shapes.mitems:
for segment in shape.mitems: for segment in shape.mitems:
segment += transform segment += transform
@ -1590,8 +1607,7 @@ proc strokePath*(
dashes dashes
) )
strokeShapes.transform(transform) strokeShapes.transform(transform)
image.fillShapes( image.fillShapes(strokeShapes, paint.color, wrNonZero, paint.blendMode)
strokeShapes, paint.color, wrNonZero, blendMode = paint.blendMode)
return return
let let

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

View file

@ -6,6 +6,12 @@ import cligen, os, pixie, pixie/fileformats/svg, strformat
# Clone https://github.com/hfg-gmuend/openmoji # Clone https://github.com/hfg-gmuend/openmoji
# Check out commit c1f14ae0be29b20c7eed215d1e03df23b1c9a5d5 # Check out commit c1f14ae0be29b20c7eed215d1e03df23b1c9a5d5
# Clone https://github.com/EmojiTwo/emojitwo
# Check out commit d79b4477eb8f9110fc3ce7bed2cc66030a77933e
# Clone https://github.com/googlefonts/noto-emoji
# Check out commit 948b1a7f1ed4ec7e27930ad8e027a740db3fe25e
type EmojiSet = object type EmojiSet = object
name: string name: string
path: string path: string
@ -13,7 +19,9 @@ type EmojiSet = object
const const
emojiSets = [ emojiSets = [
EmojiSet(name: "twemoji", path: "../twemoji/assets/svg/*"), EmojiSet(name: "twemoji", path: "../twemoji/assets/svg/*"),
EmojiSet(name: "openmoji", path: "../openmoji/color/svg/*") EmojiSet(name: "openmoji", path: "../openmoji/color/svg/*"),
EmojiSet(name: "emojitwo", path: "../emojitwo/svg/*"),
EmojiSet(name: "noto-emoji", path: "../noto-emoji/svg/*")
] ]
width = 32 width = 32
height = 32 height = 32