Merge pull request #425 from guzba/master

random things
This commit is contained in:
treeform 2022-05-21 19:23:00 -07:00 committed by GitHub
commit fe1b1483a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 136 additions and 152 deletions

View file

@ -11,7 +11,7 @@ requires "chroma >= 0.2.5"
requires "zippy >= 0.9.7"
requires "flatty >= 0.3.0"
requires "nimsimd >= 1.0.0"
requires "bumpy >= 1.1.0"
requires "bumpy >= 1.1.1"
task bindings, "Generate bindings":

View file

@ -21,18 +21,18 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].
proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory.
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):
decodeJpeg(data)
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
decodeBmp(data)
elif data.len > 5 and
(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:
decodeGif(data)
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:
decodePpm(data)
else:
@ -41,7 +41,7 @@ proc decodeImage*(data: string): Image {.raises: [PixieError].} =
proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
## Loads a mask from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
newMask(decodePng(data))
newMask(newImage(decodePng(data)))
else:
raise newException(PixieError, "Unsupported mask file format")

View file

@ -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
type
BaselineAlignment* = enum
TopBaseline
HangingBaseline

View file

@ -342,7 +342,7 @@ proc newImage*(png: Png): Image {.raises: [PixieError].} =
copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4)
result.data.toPremultipliedAlpha()
proc decodePngRaw*(data: string): Png {.raises: [PixieError].} =
proc decodePng*(data: string): Png {.raises: [PixieError].} =
## Decodes the PNG data.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
failInvalid()
@ -450,10 +450,6 @@ proc decodePngRaw*(data: string): Png {.raises: [PixieError].} =
result.channels = 4
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*(
width, height, channels: int, data: pointer, len: int
): string {.raises: [PixieError].} =

View file

@ -35,7 +35,7 @@ func newImage*(qoi: Qoi): Image =
copyMem(result.data[0].addr, qoi.data[0].addr, qoi.data.len * 4)
result.data.toPremultipliedAlpha()
proc decodeQoiRaw*(data: string): Qoi {.raises: [PixieError].} =
proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
## Decompress QOI file format data.
if data.len <= 14 or data[0 .. 3] != qoiSignature:
raise newException(PixieError, "Invalid QOI header")
@ -121,10 +121,6 @@ proc decodeQoiRaw*(data: string): Qoi {.raises: [PixieError].} =
raise newException(PixieError, "Invalid QOI padding")
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].} =
## Encodes raw QOI pixels to the QOI file format.

View file

@ -11,14 +11,15 @@ const
svgSignature* = "<svg"
type
LinearGradient = object
x1, y1, x2, y2: float32
stops: seq[ColorStop]
Svg* = ref object
width*, height*: int
elements: seq[(Path, SvgProperties)]
linearGradients: Table[string, LinearGradient]
Ctx = object
SvgProperties = object
display: bool
fillRule: WindingRule
fill: Paint
fill: string
stroke: ColorRGBX
strokeWidth: float32
strokeLineCap: LineCap
@ -26,9 +27,11 @@ type
strokeMiterLimit: float32
strokeDashArray: seq[float32]
transform: Mat3
shouldStroke: bool
opacity, strokeOpacity: float32
linearGradients: TableRef[string, LinearGradient]
opacity, fillOpacity, strokeOpacity: float32
LinearGradient = object
x1, y1, x2, y2: float32
stops: seq[ColorStop]
template failInvalid() =
raise newException(PixieError, "Invalid SVG data")
@ -38,21 +41,17 @@ proc attrOrDefault(node: XmlNode, name, default: string): string =
if result.len == 0:
result = default
proc initCtx(): Ctx =
proc initSvgProperties(): SvgProperties =
result.display = true
try:
result.fill = parseHtmlColor("black").rgbx
result.stroke = parseHtmlColor("black").rgbx
except:
raise currentExceptionAsPixieError()
result.fill = "black"
result.strokeWidth = 1
result.transform = mat3()
result.strokeMiterLimit = defaultMiterLimit
result.opacity = 1
result.fillOpacity = 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
proc splitArgs(s: string): seq[string] =
@ -162,36 +161,22 @@ proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
)
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)
result.fill = inherited.fill
else:
result.fill = parseHtmlColor(fill).rgbx
result.fill = fill
if stroke == "":
discard # Inherit
elif stroke == "currentColor":
result.shouldStroke = true
if result.stroke == rgbx(0, 0, 0, 0):
result.stroke = rgbx(0, 0, 0, 255)
elif stroke == "none":
result.stroke = ColorRGBX()
else:
result.stroke = parseHtmlColor(stroke).rgbx
result.shouldStroke = true
if fillOpacity.len > 0:
result.fill.opacity = parseFloat(fillOpacity).clamp(0, 1)
result.fillOpacity = parseFloat(fillOpacity).clamp(0, 1)
if strokeOpacity.len > 0:
result.strokeOpacity = parseFloat(strokeOpacity).clamp(0, 1)
@ -202,10 +187,8 @@ proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
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 result.stroke == rgbx(0, 0, 0, 0):
result.stroke = rgbx(0, 0, 0, 255)
if strokeLineCap == "":
discard # Inherit
@ -316,36 +299,9 @@ proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
else:
failInvalidTransform(transform)
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
try:
decodeCtxInternal(inherited, node)
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]) =
proc parseSvgElement(
node: XmlNode, svg: Svg, propertiesStack: var seq[SvgProperties]
): seq[(Path, SvgProperties)] =
if node.kind != xnElement:
# Skip <!-- comments -->
return
@ -359,25 +315,23 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
echo node
of "g":
let ctx = decodeCtx(ctxStack[^1], node)
ctxStack.add(ctx)
let props = node.parseSvgProperties(propertiesStack[^1])
propertiesStack.add(props)
for child in node:
img.drawInternal(child, ctxStack)
discard ctxStack.pop()
result.add child.parseSvgElement(svg, propertiesStack)
discard propertiesStack.pop()
of "path":
let
d = node.attr("d")
ctx = decodeCtx(ctxStack[^1], node)
props = node.parseSvgProperties(propertiesStack[^1])
path = parsePath(d)
img.fill(ctx, path)
if ctx.shouldStroke:
img.stroke(ctx, path)
result.add (path, props)
of "line":
let
ctx = decodeCtx(ctxStack[^1], node)
props = node.parseSvgProperties(propertiesStack[^1])
x1 = parseFloat(node.attrOrDefault("x1", "0"))
y1 = parseFloat(node.attrOrDefault("y1", "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.lineTo(x2, y2)
if ctx.shouldStroke:
img.stroke(ctx, path)
result.add (path, props)
of "polyline", "polygon":
let
ctx = decodeCtx(ctxStack[^1], node)
props = node.parseSvgProperties(propertiesStack[^1])
points = node.attr("points")
var vecs: seq[Vec2]
@ -421,14 +374,12 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
# and fill or not
if node.tag == "polygon":
path.closePath()
img.fill(ctx, path)
if ctx.shouldStroke:
img.stroke(ctx, path)
result.add (path, props)
of "rect":
let
ctx = decodeCtx(ctxStack[^1], node)
props = node.parseSvgProperties(propertiesStack[^1])
x = parseFloat(node.attrOrDefault("x", "0"))
y = parseFloat(node.attrOrDefault("y", "0"))
width = parseFloat(node.attrOrDefault("width", "0"))
@ -462,13 +413,11 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
else:
path.rect(x, y, width, height)
img.fill(ctx, path)
if ctx.shouldStroke:
img.stroke(ctx, path)
result.add (path, props)
of "circle", "ellipse":
let
ctx = decodeCtx(ctxStack[^1], node)
props = node.parseSvgProperties(propertiesStack[^1])
cx = parseFloat(node.attrOrDefault("cx", "0"))
cy = parseFloat(node.attrOrDefault("cy", "0"))
@ -483,16 +432,14 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
let path = newPath()
path.ellipse(cx, cy, rx, ry)
img.fill(ctx, path)
if ctx.shouldStroke:
img.stroke(ctx, path)
result.add (path, props)
of "radialGradient":
discard
of "linearGradient":
let
ctx = decodeCtx(ctxStack[^1], node)
props = node.parseSvgProperties(propertiesStack[^1])
id = node.attr("id")
gradientUnits = node.attr("gradientUnits")
gradientTransform = node.attr("gradientTransform")
@ -547,23 +494,15 @@ proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
else:
raise newException(PixieError, "Unexpected SVG tag: " & child.tag)
ctx.linearGradients[id] = linearGradient
svg.linearGradients[id] = linearGradient
else:
raise newException(PixieError, "Unsupported SVG tag: " & node.tag)
proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
try:
drawInternal(img, node, ctxStack)
except PixieError as e:
raise e
except:
raise currentExceptionAsPixieError()
proc decodeSvg*(
proc parseSvg*(
data: string | XmlNode, width = 0, height = 0
): Image {.raises: [PixieError].} =
## Render SVG XML and return the image. Defaults to the SVG's view box size.
): Svg {.raises: [PixieError].} =
## Parse SVG XML. Defaults to the SVG's view box size.
try:
let root = parseXml(data)
if root.tag != "svg":
@ -577,27 +516,81 @@ proc decodeSvg*(
viewBoxWidth = parseInt(box[2])
viewBoxHeight = parseInt(box[3])
var rootCtx = initCtx()
rootCtx = decodeCtx(rootCtx, root)
var rootProps = initSvgProperties()
rootProps = root.parseSvgProperties(rootProps)
if viewBoxMinX != 0 or viewBoxMinY != 0:
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
result = newImage(viewBoxWidth, viewBoxHeight)
result.width = viewBoxWidth
result.height = viewBoxHeight
else:
result = newImage(width, height)
result.width = width
result.height = height
let
scaleX = width.float32 / viewBoxWidth.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:
result.draw(node, ctxStack)
result.elements.add node.parseSvgElement(result, propertiesStack)
except PixieError as e:
raise e
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()

View file

@ -637,11 +637,12 @@ proc polygon*(
if sides <= 2:
raise newException(PixieError, "Invalid polygon sides value")
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(
x + size * sin(side.float32 * 2.0 * PI / sides.float32),
y - size * cos(side.float32 * 2.0 * PI / sides.float32)
)
path.closePath()
proc polygon*(
path: Path, pos: Vec2, size: float32, sides: int

View file

@ -1,6 +1,6 @@
import benchy, jpegsuite, pixie/fileformats/jpg, strformat
import benchy, jpegsuite, pixie/fileformats/jpeg, strformat
for file in jpegSuiteFiles:
let data = readFile(file)
timeIt &"jpeg {(data.len div 1024)}k decode":
discard decodeJpg(data)
discard decodeJpeg(data)

View file

@ -2,5 +2,5 @@ import benchy, pixie/fileformats/svg
let data = readFile("tests/fileformats/svg/Ghostscript_Tiger.svg")
timeIt "svg decode":
discard decodeSvg(data)
timeIt "svg parse + render":
discard newImage(parseSvg(data))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 KiB

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 3.9 MiB

View file

@ -35,7 +35,7 @@ proc renderEmojiSet(index: int) =
let (_, name, _) = splitFile(filePath)
var image: Image
try:
image = decodeSvg(readFile(filePath), width, height)
image = newImage(parseSvg(readFile(filePath), width, height))
except PixieError:
echo &"Failed decoding {name}"
image = newImage(width, height)

View file

@ -38,7 +38,7 @@ proc renderIconSet(index: int) =
for filePath in walkFiles(iconSet.path):
let
(_, name, _) = splitFile(filePath)
image = decodeSvg(readFile(filePath), width, height)
image = newImage(parseSvg(readFile(filePath), width, height))
images.add((name, image))

View file

@ -1,4 +1,4 @@
import chroma, pixie, pixie/fileformats/png, vmath
import chroma, pixie, vmath
const heartShape = """
M 10,30
@ -18,7 +18,7 @@ block:
block:
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))
let image = newImage(100, 100)
@ -27,7 +27,7 @@ block:
block:
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.opacity = 0.5
@ -37,7 +37,7 @@ block:
block:
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))
let image = newImage(100, 100)
@ -46,7 +46,7 @@ block:
block:
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.opacity = 0.5

View file

@ -1,17 +1,17 @@
import pixie, pixie/fileformats/png, pixie/fileformats/qoi
import pixie, pixie/fileformats/qoi
const tests = ["testcard", "testcard_rgba"]
for name in tests:
let
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
control = decodePng(readFile("tests/fileformats/qoi/" & name & ".png"))
input = readImage("tests/fileformats/qoi/" & name & ".qoi")
control = readImage("tests/fileformats/qoi/" & name & ".png")
doAssert input.data == control.data, "input mismatch of " & name
discard encodeQoi(control)
for name in tests:
let
input = decodeQoiRaw(readFile("tests/fileformats/qoi/" & name & ".qoi"))
output = decodeQoiRaw(encodeQoi(input))
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
output = decodeQoi(encodeQoi(input))
doAssert output.data.len == input.data.len
doAssert output.data == input.data

View file

@ -26,9 +26,8 @@ proc doDiff(rendered: Image, name: string) =
diffImage.writeFile(&"tests/fileformats/svg/diffs/{name}.png")
for file in files:
doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file)
doDiff(readImage(&"tests/fileformats/svg/{file}.svg"), file)
doDiff(
decodeSvg(readFile("tests/fileformats/svg/accessibility-outline.svg"), 512, 512),
"accessibility-outline"
)
block:
let svg = parseSvg(readFile("tests/fileformats/svg/accessibility-outline.svg"), 512, 512)
doDiff(newImage(svg), "accessibility-outline")