|
@ -11,7 +11,7 @@ requires "chroma >= 0.2.5"
|
||||||
requires "zippy >= 0.9.7"
|
requires "zippy >= 0.9.7"
|
||||||
requires "flatty >= 0.3.0"
|
requires "flatty >= 0.3.0"
|
||||||
requires "nimsimd >= 1.0.0"
|
requires "nimsimd >= 1.0.0"
|
||||||
requires "bumpy >= 1.1.0"
|
requires "bumpy >= 1.1.1"
|
||||||
|
|
||||||
|
|
||||||
task bindings, "Generate bindings":
|
task bindings, "Generate bindings":
|
||||||
|
|
|
@ -21,18 +21,18 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].
|
||||||
proc decodeImage*(data: string): Image {.raises: [PixieError].} =
|
proc decodeImage*(data: string): Image {.raises: [PixieError].} =
|
||||||
## Loads an image from memory.
|
## Loads an image from memory.
|
||||||
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
|
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
|
||||||
decodePng(data)
|
newImage(decodePng(data))
|
||||||
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
|
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
|
||||||
decodeJpeg(data)
|
decodeJpeg(data)
|
||||||
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
|
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
|
||||||
decodeBmp(data)
|
decodeBmp(data)
|
||||||
elif data.len > 5 and
|
elif data.len > 5 and
|
||||||
(data.readStr(0, 5) == xmlSignature or data.readStr(0, 4) == svgSignature):
|
(data.readStr(0, 5) == xmlSignature or data.readStr(0, 4) == svgSignature):
|
||||||
decodeSvg(data)
|
newImage(parseSvg(data))
|
||||||
elif data.len > 6 and data.readStr(0, 6) in gifSignatures:
|
elif data.len > 6 and data.readStr(0, 6) in gifSignatures:
|
||||||
decodeGif(data)
|
decodeGif(data)
|
||||||
elif data.len > (14+8) and data.readStr(0, 4) == qoiSignature:
|
elif data.len > (14+8) and data.readStr(0, 4) == qoiSignature:
|
||||||
decodeQoi(data)
|
newImage(decodeQoi(data))
|
||||||
elif data.len > 9 and data.readStr(0, 2) in ppmSignatures:
|
elif data.len > 9 and data.readStr(0, 2) in ppmSignatures:
|
||||||
decodePpm(data)
|
decodePpm(data)
|
||||||
else:
|
else:
|
||||||
|
@ -41,7 +41,7 @@ proc decodeImage*(data: string): Image {.raises: [PixieError].} =
|
||||||
proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
|
proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
|
||||||
## Loads a mask from memory.
|
## Loads a mask from memory.
|
||||||
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
|
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
|
||||||
newMask(decodePng(data))
|
newMask(newImage(decodePng(data)))
|
||||||
else:
|
else:
|
||||||
raise newException(PixieError, "Unsupported mask file format")
|
raise newException(PixieError, "Unsupported mask file format")
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import bumpy, chroma, pixie/common, pixie/fonts, pixie/images, pixie/masks,
|
||||||
## https://developer.mozilla.org/en-US/docs/Web/API/ContextRenderingContext2D
|
## https://developer.mozilla.org/en-US/docs/Web/API/ContextRenderingContext2D
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
||||||
BaselineAlignment* = enum
|
BaselineAlignment* = enum
|
||||||
TopBaseline
|
TopBaseline
|
||||||
HangingBaseline
|
HangingBaseline
|
||||||
|
|
|
@ -342,7 +342,7 @@ proc newImage*(png: Png): Image {.raises: [PixieError].} =
|
||||||
copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4)
|
copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4)
|
||||||
result.data.toPremultipliedAlpha()
|
result.data.toPremultipliedAlpha()
|
||||||
|
|
||||||
proc decodePngRaw*(data: string): Png {.raises: [PixieError].} =
|
proc decodePng*(data: string): Png {.raises: [PixieError].} =
|
||||||
## Decodes the PNG data.
|
## Decodes the PNG data.
|
||||||
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
|
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
|
||||||
failInvalid()
|
failInvalid()
|
||||||
|
@ -450,10 +450,6 @@ proc decodePngRaw*(data: string): Png {.raises: [PixieError].} =
|
||||||
result.channels = 4
|
result.channels = 4
|
||||||
result.data = decodeImageData(data, header, palette, transparency, idats)
|
result.data = decodeImageData(data, header, palette, transparency, idats)
|
||||||
|
|
||||||
proc decodePng*(data: string): Image {.raises: [PixieError].} =
|
|
||||||
## Decodes the PNG data into an Image.
|
|
||||||
newImage(decodePngRaw(data))
|
|
||||||
|
|
||||||
proc encodePng*(
|
proc encodePng*(
|
||||||
width, height, channels: int, data: pointer, len: int
|
width, height, channels: int, data: pointer, len: int
|
||||||
): string {.raises: [PixieError].} =
|
): string {.raises: [PixieError].} =
|
||||||
|
|
|
@ -35,7 +35,7 @@ func newImage*(qoi: Qoi): Image =
|
||||||
copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4)
|
copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4)
|
||||||
result.data.toPremultipliedAlpha()
|
result.data.toPremultipliedAlpha()
|
||||||
|
|
||||||
proc decodeQoiRaw*(data: string): Qoi {.raises: [PixieError].} =
|
proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
|
||||||
## Decompress QOI file format data.
|
## Decompress QOI file format data.
|
||||||
if data.len <= 14 or data[0 .. 3] != qoiSignature:
|
if data.len <= 14 or data[0 .. 3] != qoiSignature:
|
||||||
raise newException(PixieError, "Invalid QOI header")
|
raise newException(PixieError, "Invalid QOI header")
|
||||||
|
@ -121,10 +121,6 @@ proc decodeQoiRaw*(data: string): Qoi {.raises: [PixieError].} =
|
||||||
raise newException(PixieError, "Invalid QOI padding")
|
raise newException(PixieError, "Invalid QOI padding")
|
||||||
inc(p)
|
inc(p)
|
||||||
|
|
||||||
proc decodeQoi*(data: string): Image {.raises: [PixieError].} =
|
|
||||||
## Decodes data in the QOI file format to an `Image`.
|
|
||||||
newImage(decodeQoiRaw(data))
|
|
||||||
|
|
||||||
proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
|
proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
|
||||||
## Encodes raw QOI pixels to the QOI file format.
|
## Encodes raw QOI pixels to the QOI file format.
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,15 @@ const
|
||||||
svgSignature* = "<svg"
|
svgSignature* = "<svg"
|
||||||
|
|
||||||
type
|
type
|
||||||
LinearGradient = object
|
Svg* = ref object
|
||||||
x1, y1, x2, y2: float32
|
width*, height*: int
|
||||||
stops: seq[ColorStop]
|
elements: seq[(Path, SvgProperties)]
|
||||||
|
linearGradients: Table[string, LinearGradient]
|
||||||
|
|
||||||
Ctx = object
|
SvgProperties = object
|
||||||
display: bool
|
display: bool
|
||||||
fillRule: WindingRule
|
fillRule: WindingRule
|
||||||
fill: Paint
|
fill: string
|
||||||
stroke: ColorRGBX
|
stroke: ColorRGBX
|
||||||
strokeWidth: float32
|
strokeWidth: float32
|
||||||
strokeLineCap: LineCap
|
strokeLineCap: LineCap
|
||||||
|
@ -26,9 +27,11 @@ type
|
||||||
strokeMiterLimit: float32
|
strokeMiterLimit: float32
|
||||||
strokeDashArray: seq[float32]
|
strokeDashArray: seq[float32]
|
||||||
transform: Mat3
|
transform: Mat3
|
||||||
shouldStroke: bool
|
opacity, fillOpacity, strokeOpacity: float32
|
||||||
opacity, strokeOpacity: float32
|
|
||||||
linearGradients: TableRef[string, LinearGradient]
|
LinearGradient = object
|
||||||
|
x1, y1, x2, y2: float32
|
||||||
|
stops: seq[ColorStop]
|
||||||
|
|
||||||
template failInvalid() =
|
template failInvalid() =
|
||||||
raise newException(PixieError, "Invalid SVG data")
|
raise newException(PixieError, "Invalid SVG data")
|
||||||
|
@ -38,21 +41,17 @@ proc attrOrDefault(node: XmlNode, name, default: string): string =
|
||||||
if result.len == 0:
|
if result.len == 0:
|
||||||
result = default
|
result = default
|
||||||
|
|
||||||
proc initCtx(): Ctx =
|
proc initSvgProperties(): SvgProperties =
|
||||||
result.display = true
|
result.display = true
|
||||||
try:
|
result.fill = "black"
|
||||||
result.fill = parseHtmlColor("black").rgbx
|
|
||||||
result.stroke = parseHtmlColor("black").rgbx
|
|
||||||
except:
|
|
||||||
raise currentExceptionAsPixieError()
|
|
||||||
result.strokeWidth = 1
|
result.strokeWidth = 1
|
||||||
result.transform = mat3()
|
result.transform = mat3()
|
||||||
result.strokeMiterLimit = defaultMiterLimit
|
result.strokeMiterLimit = defaultMiterLimit
|
||||||
result.opacity = 1
|
result.opacity = 1
|
||||||
|
result.fillOpacity = 1
|
||||||
result.strokeOpacity = 1
|
result.strokeOpacity = 1
|
||||||
result.linearGradients = newTable[string, LinearGradient]()
|
|
||||||
|
|
||||||
proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
|
proc parseSvgProperties(node: XmlNode, inherited: SvgProperties): SvgProperties =
|
||||||
result = inherited
|
result = inherited
|
||||||
|
|
||||||
proc splitArgs(s: string): seq[string] =
|
proc splitArgs(s: string): seq[string] =
|
||||||
|
@ -162,36 +161,22 @@ proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
)
|
)
|
||||||
|
|
||||||
if fill == "" or fill == "currentColor":
|
if fill == "" or fill == "currentColor":
|
||||||
discard # Inherit
|
result.fill = inherited.fill
|
||||||
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:
|
else:
|
||||||
result.fill = parseHtmlColor(fill).rgbx
|
result.fill = fill
|
||||||
|
|
||||||
if stroke == "":
|
if stroke == "":
|
||||||
discard # Inherit
|
discard # Inherit
|
||||||
elif stroke == "currentColor":
|
elif stroke == "currentColor":
|
||||||
result.shouldStroke = true
|
if result.stroke == rgbx(0, 0, 0, 0):
|
||||||
|
result.stroke = rgbx(0, 0, 0, 255)
|
||||||
elif stroke == "none":
|
elif stroke == "none":
|
||||||
result.stroke = ColorRGBX()
|
result.stroke = ColorRGBX()
|
||||||
else:
|
else:
|
||||||
result.stroke = parseHtmlColor(stroke).rgbx
|
result.stroke = parseHtmlColor(stroke).rgbx
|
||||||
result.shouldStroke = true
|
|
||||||
|
|
||||||
if fillOpacity.len > 0:
|
if fillOpacity.len > 0:
|
||||||
result.fill.opacity = parseFloat(fillOpacity).clamp(0, 1)
|
result.fillOpacity = parseFloat(fillOpacity).clamp(0, 1)
|
||||||
|
|
||||||
if strokeOpacity.len > 0:
|
if strokeOpacity.len > 0:
|
||||||
result.strokeOpacity = parseFloat(strokeOpacity).clamp(0, 1)
|
result.strokeOpacity = parseFloat(strokeOpacity).clamp(0, 1)
|
||||||
|
@ -202,10 +187,8 @@ proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
if strokeWidth.endsWith("px"):
|
if strokeWidth.endsWith("px"):
|
||||||
strokeWidth = strokeWidth[0 .. ^3]
|
strokeWidth = strokeWidth[0 .. ^3]
|
||||||
result.strokeWidth = parseFloat(strokeWidth)
|
result.strokeWidth = parseFloat(strokeWidth)
|
||||||
result.shouldStroke = true
|
if result.stroke == rgbx(0, 0, 0, 0):
|
||||||
|
result.stroke = rgbx(0, 0, 0, 255)
|
||||||
if result.stroke == ColorRGBX() or result.strokeWidth <= 0:
|
|
||||||
result.shouldStroke = false
|
|
||||||
|
|
||||||
if strokeLineCap == "":
|
if strokeLineCap == "":
|
||||||
discard # Inherit
|
discard # Inherit
|
||||||
|
@ -316,36 +299,9 @@ proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
|
||||||
else:
|
else:
|
||||||
failInvalidTransform(transform)
|
failInvalidTransform(transform)
|
||||||
|
|
||||||
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
|
proc parseSvgElement(
|
||||||
try:
|
node: XmlNode, svg: Svg, propertiesStack: var seq[SvgProperties]
|
||||||
decodeCtxInternal(inherited, node)
|
): seq[(Path, SvgProperties)] =
|
||||||
except PixieError as e:
|
|
||||||
raise e
|
|
||||||
except:
|
|
||||||
raise currentExceptionAsPixieError()
|
|
||||||
|
|
||||||
proc fill(img: Image, ctx: Ctx, path: Path) {.inline.} =
|
|
||||||
if ctx.display and ctx.opacity > 0:
|
|
||||||
let paint = newPaint(ctx.fill)
|
|
||||||
paint.opacity = paint.opacity * ctx.opacity
|
|
||||||
img.fillPath(path, paint, ctx.transform, ctx.fillRule)
|
|
||||||
|
|
||||||
proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} =
|
|
||||||
if ctx.display and ctx.opacity > 0:
|
|
||||||
let paint = newPaint(ctx.stroke)
|
|
||||||
paint.color.a *= (ctx.opacity * ctx.strokeOpacity)
|
|
||||||
img.strokePath(
|
|
||||||
path,
|
|
||||||
paint,
|
|
||||||
ctx.transform,
|
|
||||||
ctx.strokeWidth,
|
|
||||||
ctx.strokeLineCap,
|
|
||||||
ctx.strokeLineJoin,
|
|
||||||
miterLimit = ctx.strokeMiterLimit,
|
|
||||||
dashes = ctx.strokeDashArray
|
|
||||||
)
|
|
||||||
|
|
||||||
proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
|
||||||
if node.kind != xnElement:
|
if node.kind != xnElement:
|
||||||
# Skip <!-- comments -->
|
# Skip <!-- comments -->
|
||||||
return
|
return
|
||||||
|
@ -359,25 +315,23 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
echo node
|
echo node
|
||||||
|
|
||||||
of "g":
|
of "g":
|
||||||
let ctx = decodeCtx(ctxStack[^1], node)
|
let props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
ctxStack.add(ctx)
|
propertiesStack.add(props)
|
||||||
for child in node:
|
for child in node:
|
||||||
img.drawInternal(child, ctxStack)
|
result.add child.parseSvgElement(svg, propertiesStack)
|
||||||
discard ctxStack.pop()
|
discard propertiesStack.pop()
|
||||||
|
|
||||||
of "path":
|
of "path":
|
||||||
let
|
let
|
||||||
d = node.attr("d")
|
d = node.attr("d")
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
path = parsePath(d)
|
path = parsePath(d)
|
||||||
|
|
||||||
img.fill(ctx, path)
|
result.add (path, props)
|
||||||
if ctx.shouldStroke:
|
|
||||||
img.stroke(ctx, path)
|
|
||||||
|
|
||||||
of "line":
|
of "line":
|
||||||
let
|
let
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
x1 = parseFloat(node.attrOrDefault("x1", "0"))
|
x1 = parseFloat(node.attrOrDefault("x1", "0"))
|
||||||
y1 = parseFloat(node.attrOrDefault("y1", "0"))
|
y1 = parseFloat(node.attrOrDefault("y1", "0"))
|
||||||
x2 = parseFloat(node.attrOrDefault("x2", "0"))
|
x2 = parseFloat(node.attrOrDefault("x2", "0"))
|
||||||
|
@ -387,12 +341,11 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
path.moveTo(x1, y1)
|
path.moveTo(x1, y1)
|
||||||
path.lineTo(x2, y2)
|
path.lineTo(x2, y2)
|
||||||
|
|
||||||
if ctx.shouldStroke:
|
result.add (path, props)
|
||||||
img.stroke(ctx, path)
|
|
||||||
|
|
||||||
of "polyline", "polygon":
|
of "polyline", "polygon":
|
||||||
let
|
let
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
points = node.attr("points")
|
points = node.attr("points")
|
||||||
|
|
||||||
var vecs: seq[Vec2]
|
var vecs: seq[Vec2]
|
||||||
|
@ -421,14 +374,12 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
# and fill or not
|
# and fill or not
|
||||||
if node.tag == "polygon":
|
if node.tag == "polygon":
|
||||||
path.closePath()
|
path.closePath()
|
||||||
img.fill(ctx, path)
|
|
||||||
|
|
||||||
if ctx.shouldStroke:
|
result.add (path, props)
|
||||||
img.stroke(ctx, path)
|
|
||||||
|
|
||||||
of "rect":
|
of "rect":
|
||||||
let
|
let
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
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.attrOrDefault("width", "0"))
|
width = parseFloat(node.attrOrDefault("width", "0"))
|
||||||
|
@ -462,13 +413,11 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
else:
|
else:
|
||||||
path.rect(x, y, width, height)
|
path.rect(x, y, width, height)
|
||||||
|
|
||||||
img.fill(ctx, path)
|
result.add (path, props)
|
||||||
if ctx.shouldStroke:
|
|
||||||
img.stroke(ctx, path)
|
|
||||||
|
|
||||||
of "circle", "ellipse":
|
of "circle", "ellipse":
|
||||||
let
|
let
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
cx = parseFloat(node.attrOrDefault("cx", "0"))
|
cx = parseFloat(node.attrOrDefault("cx", "0"))
|
||||||
cy = parseFloat(node.attrOrDefault("cy", "0"))
|
cy = parseFloat(node.attrOrDefault("cy", "0"))
|
||||||
|
|
||||||
|
@ -483,16 +432,14 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
let path = newPath()
|
let path = newPath()
|
||||||
path.ellipse(cx, cy, rx, ry)
|
path.ellipse(cx, cy, rx, ry)
|
||||||
|
|
||||||
img.fill(ctx, path)
|
result.add (path, props)
|
||||||
if ctx.shouldStroke:
|
|
||||||
img.stroke(ctx, path)
|
|
||||||
|
|
||||||
of "radialGradient":
|
of "radialGradient":
|
||||||
discard
|
discard
|
||||||
|
|
||||||
of "linearGradient":
|
of "linearGradient":
|
||||||
let
|
let
|
||||||
ctx = decodeCtx(ctxStack[^1], node)
|
props = node.parseSvgProperties(propertiesStack[^1])
|
||||||
id = node.attr("id")
|
id = node.attr("id")
|
||||||
gradientUnits = node.attr("gradientUnits")
|
gradientUnits = node.attr("gradientUnits")
|
||||||
gradientTransform = node.attr("gradientTransform")
|
gradientTransform = node.attr("gradientTransform")
|
||||||
|
@ -547,23 +494,15 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
||||||
else:
|
else:
|
||||||
raise newException(PixieError, "Unexpected SVG tag: " & child.tag)
|
raise newException(PixieError, "Unexpected SVG tag: " & child.tag)
|
||||||
|
|
||||||
ctx.linearGradients[id] = linearGradient
|
svg.linearGradients[id] = linearGradient
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise newException(PixieError, "Unsupported SVG tag: " & node.tag)
|
raise newException(PixieError, "Unsupported SVG tag: " & node.tag)
|
||||||
|
|
||||||
proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
|
proc parseSvg*(
|
||||||
try:
|
|
||||||
drawInternal(img, node, ctxStack)
|
|
||||||
except PixieError as e:
|
|
||||||
raise e
|
|
||||||
except:
|
|
||||||
raise currentExceptionAsPixieError()
|
|
||||||
|
|
||||||
proc decodeSvg*(
|
|
||||||
data: string | XmlNode, width = 0, height = 0
|
data: string | XmlNode, width = 0, height = 0
|
||||||
): Image {.raises: [PixieError].} =
|
): Svg {.raises: [PixieError].} =
|
||||||
## Render SVG XML and return the image. Defaults to the SVG's view box size.
|
## Parse SVG XML. Defaults to the SVG's view box size.
|
||||||
try:
|
try:
|
||||||
let root = parseXml(data)
|
let root = parseXml(data)
|
||||||
if root.tag != "svg":
|
if root.tag != "svg":
|
||||||
|
@ -577,27 +516,81 @@ proc decodeSvg*(
|
||||||
viewBoxWidth = parseInt(box[2])
|
viewBoxWidth = parseInt(box[2])
|
||||||
viewBoxHeight = parseInt(box[3])
|
viewBoxHeight = parseInt(box[3])
|
||||||
|
|
||||||
var rootCtx = initCtx()
|
var rootProps = initSvgProperties()
|
||||||
rootCtx = decodeCtx(rootCtx, root)
|
rootProps = root.parseSvgProperties(rootProps)
|
||||||
|
|
||||||
|
|
||||||
if viewBoxMinX != 0 or viewBoxMinY != 0:
|
if viewBoxMinX != 0 or viewBoxMinY != 0:
|
||||||
let viewBoxMin = vec2(-viewBoxMinX.float32, -viewBoxMinY.float32)
|
let viewBoxMin = vec2(-viewBoxMinX.float32, -viewBoxMinY.float32)
|
||||||
rootCtx.transform = rootCtx.transform * translate(viewBoxMin)
|
rootprops.transform = rootprops.transform * translate(viewBoxMin)
|
||||||
|
|
||||||
|
result = Svg()
|
||||||
|
|
||||||
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.width = viewBoxWidth
|
||||||
|
result.height = viewBoxHeight
|
||||||
else:
|
else:
|
||||||
result = newImage(width, height)
|
result.width = width
|
||||||
|
result.height = height
|
||||||
|
|
||||||
let
|
let
|
||||||
scaleX = width.float32 / viewBoxWidth.float32
|
scaleX = width.float32 / viewBoxWidth.float32
|
||||||
scaleY = height.float32 / viewBoxHeight.float32
|
scaleY = height.float32 / viewBoxHeight.float32
|
||||||
rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY))
|
rootprops.transform = rootprops.transform * scale(vec2(scaleX, scaleY))
|
||||||
|
|
||||||
var ctxStack = @[rootCtx]
|
var propertiesStack = @[rootProps]
|
||||||
for node in root.items:
|
for node in root.items:
|
||||||
result.draw(node, ctxStack)
|
result.elements.add node.parseSvgElement(result, propertiesStack)
|
||||||
except PixieError as e:
|
except PixieError as e:
|
||||||
raise e
|
raise e
|
||||||
except:
|
except:
|
||||||
raise newException(PixieError, "Unable to load SVG")
|
raise currentExceptionAsPixieError()
|
||||||
|
|
||||||
|
proc newImage*(svg: Svg): Image {.raises: [PixieError].} =
|
||||||
|
## Render SVG and return the image.
|
||||||
|
result = newImage(svg.width, svg.height)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for (path, props) in svg.elements:
|
||||||
|
if props.display and props.opacity > 0:
|
||||||
|
if props.fill != "none":
|
||||||
|
var paint: Paint
|
||||||
|
if props.fill.startsWith("url("):
|
||||||
|
let closingParen = props.fill.find(")", 5)
|
||||||
|
if closingParen == -1:
|
||||||
|
raise newException(PixieError, "Malformed fill: " & props.fill)
|
||||||
|
let id = props.fill[5 .. closingParen - 1]
|
||||||
|
if id in svg.linearGradients:
|
||||||
|
let linearGradient = svg.linearGradients[id]
|
||||||
|
paint = newPaint(LinearGradientPaint)
|
||||||
|
paint.gradientHandlePositions = @[
|
||||||
|
props.transform * vec2(linearGradient.x1, linearGradient.y1),
|
||||||
|
props.transform * vec2(linearGradient.x2, linearGradient.y2)
|
||||||
|
]
|
||||||
|
paint.gradientStops = linearGradient.stops
|
||||||
|
else:
|
||||||
|
raise newException(PixieError, "Missing SVG resource " & id)
|
||||||
|
else:
|
||||||
|
paint = parseHtmlColor(props.fill).rgbx
|
||||||
|
|
||||||
|
paint.opacity = props.fillOpacity * props.opacity
|
||||||
|
|
||||||
|
result.fillPath(path, paint, props.transform, props.fillRule)
|
||||||
|
|
||||||
|
if props.stroke != rgbx(0, 0, 0, 0) and props.strokeWidth > 0:
|
||||||
|
let paint = newPaint(props.stroke)
|
||||||
|
paint.color.a *= (props.opacity * props.strokeOpacity)
|
||||||
|
result.strokePath(
|
||||||
|
path,
|
||||||
|
paint,
|
||||||
|
props.transform,
|
||||||
|
props.strokeWidth,
|
||||||
|
props.strokeLineCap,
|
||||||
|
props.strokeLineJoin,
|
||||||
|
miterLimit = props.strokeMiterLimit,
|
||||||
|
dashes = props.strokeDashArray
|
||||||
|
)
|
||||||
|
except PixieError as e:
|
||||||
|
raise e
|
||||||
|
except:
|
||||||
|
raise currentExceptionAsPixieError()
|
||||||
|
|
|
@ -637,11 +637,12 @@ proc polygon*(
|
||||||
if sides <= 2:
|
if sides <= 2:
|
||||||
raise newException(PixieError, "Invalid polygon sides value")
|
raise newException(PixieError, "Invalid polygon sides value")
|
||||||
path.moveTo(x + size * sin(0.0), y - size * cos(0.0))
|
path.moveTo(x + size * sin(0.0), y - size * cos(0.0))
|
||||||
for side in 1 .. sides:
|
for side in 1 .. sides - 1:
|
||||||
path.lineTo(
|
path.lineTo(
|
||||||
x + size * sin(side.float32 * 2.0 * PI / sides.float32),
|
x + size * sin(side.float32 * 2.0 * PI / sides.float32),
|
||||||
y - size * cos(side.float32 * 2.0 * PI / sides.float32)
|
y - size * cos(side.float32 * 2.0 * PI / sides.float32)
|
||||||
)
|
)
|
||||||
|
path.closePath()
|
||||||
|
|
||||||
proc polygon*(
|
proc polygon*(
|
||||||
path: Path, pos: Vec2, size: float32, sides: int
|
path: Path, pos: Vec2, size: float32, sides: int
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import benchy, jpegsuite, pixie/fileformats/jpg, strformat
|
import benchy, jpegsuite, pixie/fileformats/jpeg, strformat
|
||||||
|
|
||||||
for file in jpegSuiteFiles:
|
for file in jpegSuiteFiles:
|
||||||
let data = readFile(file)
|
let data = readFile(file)
|
||||||
timeIt &"jpeg {(data.len div 1024)}k decode":
|
timeIt &"jpeg {(data.len div 1024)}k decode":
|
||||||
discard decodeJpg(data)
|
discard decodeJpeg(data)
|
||||||
|
|
|
@ -2,5 +2,5 @@ import benchy, pixie/fileformats/svg
|
||||||
|
|
||||||
let data = readFile("tests/fileformats/svg/Ghostscript_Tiger.svg")
|
let data = readFile("tests/fileformats/svg/Ghostscript_Tiger.svg")
|
||||||
|
|
||||||
timeIt "svg decode":
|
timeIt "svg parse + render":
|
||||||
discard decodeSvg(data)
|
discard newImage(parseSvg(data))
|
||||||
|
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 787 KiB |
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 3.4 MiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 519 KiB |
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 3.9 MiB |
|
@ -35,7 +35,7 @@ proc renderEmojiSet(index: int) =
|
||||||
let (_, name, _) = splitFile(filePath)
|
let (_, name, _) = splitFile(filePath)
|
||||||
var image: Image
|
var image: Image
|
||||||
try:
|
try:
|
||||||
image = decodeSvg(readFile(filePath), width, height)
|
image = newImage(parseSvg(readFile(filePath), width, height))
|
||||||
except PixieError:
|
except PixieError:
|
||||||
echo &"Failed decoding {name}"
|
echo &"Failed decoding {name}"
|
||||||
image = newImage(width, height)
|
image = newImage(width, height)
|
||||||
|
|
|
@ -38,7 +38,7 @@ proc renderIconSet(index: int) =
|
||||||
for filePath in walkFiles(iconSet.path):
|
for filePath in walkFiles(iconSet.path):
|
||||||
let
|
let
|
||||||
(_, name, _) = splitFile(filePath)
|
(_, name, _) = splitFile(filePath)
|
||||||
image = decodeSvg(readFile(filePath), width, height)
|
image = newImage(parseSvg(readFile(filePath), width, height))
|
||||||
|
|
||||||
images.add((name, image))
|
images.add((name, image))
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import chroma, pixie, pixie/fileformats/png, vmath
|
import chroma, pixie, vmath
|
||||||
|
|
||||||
const heartShape = """
|
const heartShape = """
|
||||||
M 10,30
|
M 10,30
|
||||||
|
@ -18,7 +18,7 @@ block:
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let paint = newPaint(ImagePaint)
|
let paint = newPaint(ImagePaint)
|
||||||
paint.image = decodePng(readFile("tests/fileformats/png/mandrill.png"))
|
paint.image = readImage("tests/fileformats/png/mandrill.png")
|
||||||
paint.imageMat = scale(vec2(0.2, 0.2))
|
paint.imageMat = scale(vec2(0.2, 0.2))
|
||||||
|
|
||||||
let image = newImage(100, 100)
|
let image = newImage(100, 100)
|
||||||
|
@ -27,7 +27,7 @@ block:
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let paint = newPaint(ImagePaint)
|
let paint = newPaint(ImagePaint)
|
||||||
paint.image = decodePng(readFile("tests/fileformats/png/mandrill.png"))
|
paint.image = readImage("tests/fileformats/png/mandrill.png")
|
||||||
paint.imageMat = scale(vec2(0.2, 0.2))
|
paint.imageMat = scale(vec2(0.2, 0.2))
|
||||||
paint.opacity = 0.5
|
paint.opacity = 0.5
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ block:
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let paint = newPaint(TiledImagePaint)
|
let paint = newPaint(TiledImagePaint)
|
||||||
paint.image = decodePng(readFile("tests/fileformats/png/mandrill.png"))
|
paint.image = readImage("tests/fileformats/png/mandrill.png")
|
||||||
paint.imageMat = scale(vec2(0.02, 0.02))
|
paint.imageMat = scale(vec2(0.02, 0.02))
|
||||||
|
|
||||||
let image = newImage(100, 100)
|
let image = newImage(100, 100)
|
||||||
|
@ -46,7 +46,7 @@ block:
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let paint = newPaint(TiledImagePaint)
|
let paint = newPaint(TiledImagePaint)
|
||||||
paint.image = decodePng(readFile("tests/fileformats/png/mandrill.png"))
|
paint.image = readImage("tests/fileformats/png/mandrill.png")
|
||||||
paint.imageMat = scale(vec2(0.02, 0.02))
|
paint.imageMat = scale(vec2(0.02, 0.02))
|
||||||
paint.opacity = 0.5
|
paint.opacity = 0.5
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import pixie, pixie/fileformats/png, pixie/fileformats/qoi
|
import pixie, pixie/fileformats/qoi
|
||||||
|
|
||||||
const tests = ["testcard", "testcard_rgba"]
|
const tests = ["testcard", "testcard_rgba"]
|
||||||
|
|
||||||
for name in tests:
|
for name in tests:
|
||||||
let
|
let
|
||||||
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
|
input = readImage("tests/fileformats/qoi/" & name & ".qoi")
|
||||||
control = decodePng(readFile("tests/fileformats/qoi/" & name & ".png"))
|
control = readImage("tests/fileformats/qoi/" & name & ".png")
|
||||||
doAssert input.data == control.data, "input mismatch of " & name
|
doAssert input.data == control.data, "input mismatch of " & name
|
||||||
discard encodeQoi(control)
|
discard encodeQoi(control)
|
||||||
|
|
||||||
for name in tests:
|
for name in tests:
|
||||||
let
|
let
|
||||||
input = decodeQoiRaw(readFile("tests/fileformats/qoi/" & name & ".qoi"))
|
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
|
||||||
output = decodeQoiRaw(encodeQoi(input))
|
output = decodeQoi(encodeQoi(input))
|
||||||
doAssert output.data.len == input.data.len
|
doAssert output.data.len == input.data.len
|
||||||
doAssert output.data == input.data
|
doAssert output.data == input.data
|
||||||
|
|
|
@ -26,9 +26,8 @@ proc doDiff(rendered: Image, name: string) =
|
||||||
diffImage.writeFile(&"tests/fileformats/svg/diffs/{name}.png")
|
diffImage.writeFile(&"tests/fileformats/svg/diffs/{name}.png")
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file)
|
doDiff(readImage(&"tests/fileformats/svg/{file}.svg"), file)
|
||||||
|
|
||||||
doDiff(
|
block:
|
||||||
decodeSvg(readFile("tests/fileformats/svg/accessibility-outline.svg"), 512, 512),
|
let svg = parseSvg(readFile("tests/fileformats/svg/accessibility-outline.svg"), 512, 512)
|
||||||
"accessibility-outline"
|
doDiff(newImage(svg), "accessibility-outline")
|
||||||
)
|
|
||||||
|
|