Merge pull request #211 from guzba/master
2.0.1, fixes, more emoji test
This commit is contained in:
commit
1df4ed5b89
10 changed files with 229 additions and 155 deletions
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
BIN
tests/images/svg/emojitwo.png
Normal file
BIN
tests/images/svg/emojitwo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
BIN
tests/images/svg/noto-emoji.png
Normal file
BIN
tests/images/svg/noto-emoji.png
Normal file
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 |
|
@ -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
|
Loading…
Reference in a new issue