diff --git a/examples/tiger.png b/examples/tiger.png index cacc455..5288d5d 100644 Binary files a/examples/tiger.png and b/examples/tiger.png differ diff --git a/experiments/benchmark_cairo.nim b/experiments/benchmark_cairo.nim index 754f661..ba0da0f 100644 --- a/experiments/benchmark_cairo.nim +++ b/experiments/benchmark_cairo.nim @@ -1,96 +1,141 @@ -import benchy, cairo, chroma, math, pixie +import benchy, cairo, chroma, math, pixie, pixie/paths {.all.}, strformat + +proc doDiff(a, b: Image, name: string) = + let (diffScore, diffImage) = diff(a, b) + echo &"{name} score: {diffScore}" + diffImage.writeFile(&"{name}_diff.png") block: + let path = newPath() + path.moveTo(0, 0) + path.lineTo(1920, 0) + path.lineTo(1920, 1080) + path.lineTo(0, 1080) + path.closePath() + + let shapes = path.commandsToShapes(true, 1) + let surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080) ctx = surface.create() - ctx.setSourceRgba(0, 0, 1, 1) timeIt "cairo1": ctx.newPath() - ctx.moveTo(0, 0) - ctx.lineTo(1920, 0) - ctx.lineTo(1920, 1080) - ctx.lineTo(0, 1080) - ctx.closePath() + ctx.moveTo(shapes[0][0].x, shapes[0][0].y) + for shape in shapes: + for v in shape: + ctx.lineTo(v.x, v.y) ctx.fill() surface.flush() # discard surface.writeToPng("cairo1.png") let a = newImage(1920, 1080) - a.fill(rgba(255, 255, 255, 255)) timeIt "pixie1": let p = newPath() - p.moveTo(0, 0) - p.lineTo(1920, 0) - p.lineTo(1920, 1080) - p.lineTo(0, 1080) - p.closePath() + p.moveTo(shapes[0][0]) + for shape in shapes: + for v in shape: + p.lineTo(v) a.fillPath(p, rgba(0, 0, 255, 255)) # a.writeFile("pixie1.png") block: + let path = newPath() + path.moveTo(500, 240) + path.lineTo(1500, 240) + path.lineTo(1920, 600) + path.lineTo(0, 600) + path.closePath() + + let shapes = path.commandsToShapes(true, 1) + let surface = imageSurfaceCreate(FORMAT_ARGB32, 1920, 1080) ctx = surface.create() - ctx.setSourceRgba(0, 0, 1, 1) - timeIt "cairo2": + ctx.setSourceRgba(1, 1, 1, 1) + let operator = ctx.getOperator() + ctx.setOperator(OperatorSource) + ctx.paint() + ctx.setOperator(operator) + + ctx.setSourceRgba(0, 0, 1, 1) + ctx.newPath() - ctx.moveTo(500, 240) - ctx.lineTo(1500, 240) - ctx.lineTo(1920, 600) - ctx.lineTo(0, 600) - ctx.closePath() + ctx.moveTo(shapes[0][0].x, shapes[0][0].y) + for shape in shapes: + for v in shape: + ctx.lineTo(v.x, v.y) ctx.fill() - surface.flush() + surface.flush() # discard surface.writeToPng("cairo2.png") let a = newImage(1920, 1080) - a.fill(rgba(255, 255, 255, 255)) timeIt "pixie2": + a.fill(rgba(255, 255, 255, 255)) + let p = newPath() - p.moveTo(500, 240) - p.lineTo(1500, 240) - p.lineTo(1920, 600) - p.lineTo(0, 600) - p.closePath() + p.moveTo(shapes[0][0]) + for shape in shapes: + for v in shape: + p.lineTo(v) a.fillPath(p, rgba(0, 0, 255, 255)) # a.writeFile("pixie2.png") -# block: -# let -# a = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) -# b = imageSurfaceCreate(FORMAT_ARGB32, 500, 500) -# ac = a.create() -# bc = b.create() +block: + let path = parsePath(""" + M 100,300 + A 200,200 0,0,1 500,300 + A 200,200 0,0,1 900,300 + Q 900,600 500,900 + Q 100,600 100,300 z + """) -# ac.setSourceRgba(1, 0, 0, 1) -# ac.newPath() -# ac.rectangle(0, 0, 1000, 1000) -# ac.fill() + let shapes = path.commandsToShapes(true, 1) -# bc.setSourceRgba(0, 1, 0, 1) -# bc.newPath() -# bc.rectangle(0, 0, 500, 500) -# bc.fill() + let + surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) + ctx = surface.create() -# let pattern = patternCreateForSurface(b) + timeIt "cairo3": + ctx.setSourceRgba(1, 1, 1, 1) + let operator = ctx.getOperator() + ctx.setOperator(OperatorSource) + ctx.paint() + ctx.setOperator(operator) -# timeIt "a": -# ac.setSource(pattern) -# ac.save() -# ac.translate(25.2, 25.2) -# ac.rectangle(0, 0, 500, 500) -# ac.fill() -# ac.restore() + ctx.setSourceRgba(1, 0, 0, 1) -# discard a.writeToPng("a.png") + ctx.newPath() + ctx.moveTo(shapes[0][0].x, shapes[0][0].y) + for shape in shapes: + for v in shape: + ctx.lineTo(v.x, v.y) + ctx.fill() + surface.flush() + + # discard surface.writeToPng("cairo3.png") + + let a = newImage(1000, 1000) + + timeIt "pixie3": + a.fill(rgba(255, 255, 255, 255)) + + let p = newPath() + p.moveTo(shapes[0][0]) + for shape in shapes: + for v in shape: + p.lineTo(v) + a.fillPath(p, rgba(255, 0, 0, 255)) + + # a.writeFile("pixie3.png") + + # doDiff(readImage("cairo3.png"), a, "cairo3") diff --git a/experiments/svg_cairo.nim b/experiments/svg_cairo.nim index 9d6bb27..086f802 100644 --- a/experiments/svg_cairo.nim +++ b/experiments/svg_cairo.nim @@ -1,20 +1,28 @@ ## Load and Save SVG files. -import cairo, chroma, pixie/common, pixie/images, strutils, vmath, xmlparser, xmltree +import cairo, chroma, pixie/common, pixie/images, pixie/paints, strutils, tables, + vmath, xmlparser, xmltree include pixie/paths -# type Path = paths.Path - proc processCommands(c: ptr Context, path: Path) = c.newPath() c.moveTo(0, 0) - for i, command in path.commands: + + var + prevCommandKind = Move + start, at, prevCtrl2: Vec2 + for command in path.commands: case command.kind of Move: c.moveTo(command.numbers[0], command.numbers[1]) + at.x = command.numbers[0] + at.y = command.numbers[1] + start = at of Line: c.lineTo(command.numbers[0], command.numbers[1]) + at.x = command.numbers[0] + at.y = command.numbers[1] of HLine: echo "HLine not yet supported for Cairo" of VLine: @@ -28,6 +36,9 @@ proc processCommands(c: ptr Context, path: Path) = command.numbers[4], command.numbers[5] ) + at.x = command.numbers[4] + at.y = command.numbers[5] + prevCtrl2 = vec2(command.numbers[2], command.numbers[3]) of SCubic: echo "SCubic not yet supported for Cairo" of Quad: @@ -38,12 +49,19 @@ proc processCommands(c: ptr Context, path: Path) = echo "Arc not yet supported for Cairo" of RMove: c.relMoveTo(command.numbers[0], command.numbers[1]) + at.x += command.numbers[0] + at.y += command.numbers[1] + start = at of RLine: c.relLineTo(command.numbers[0], command.numbers[1]) + at.x += command.numbers[0] + at.y += command.numbers[1] of RHLine: c.relLineTo(command.numbers[0], 0) + at.x += command.numbers[0] of RVLine: c.relLineTo(0, command.numbers[0]) + at.y += command.numbers[0] of RCubic: c.relCurveTo( command.numbers[0], @@ -53,12 +71,28 @@ proc processCommands(c: ptr Context, path: Path) = command.numbers[4], command.numbers[5] ) + prevCtrl2 = vec2(at.x + command.numbers[2], at.y + command.numbers[3]) + at.x += command.numbers[4] + at.y += command.numbers[5] of RSCubic: - # This is not correct but good enough for now - c.relLineTo( - command.numbers[2], - command.numbers[3] + let + ctrl1 = + if prevCommandKind in {Cubic, SCubic, RCubic, RSCubic}: + at * 2 - prevCtrl2 + else: + at + ctrl2 = vec2(at.x + command.numbers[0], at.y + command.numbers[1]) + to = vec2(at.x + command.numbers[2], at.y + command.numbers[3]) + c.curveTo( + ctrl1.x, + ctrl1.y, + ctrl2.x, + ctrl2.y, + to.x, + to.y ) + prevCtrl2 = ctrl2 + at = to of RQuad: echo "RQuad not supported by Cairo" of RTQuad: @@ -67,18 +101,21 @@ proc processCommands(c: ptr Context, path: Path) = echo "RArc not yet supported for Cairo" of Close: c.closePath() + at = start + + prevCommandKind = command.kind checkStatus(c.status()) proc prepare( c: ptr Context, path: Path, - color: ColorRGBA, + paint: Paint, mat: Mat3, windingRule = wrNonZero ) = let - color = color.color() + color = paint.color matrix = Matrix( xx: mat[0, 0], yx: mat[0, 1], @@ -96,16 +133,25 @@ proc prepare( c.setFillRule(FillRuleEvenOdd) c.processCommands(path) -type Ctx = object - fillRule: WindingRule - fill, stroke: ColorRGBA - strokeWidth: float32 - strokeLineCap: LineCap - strokeLineJoin: LineJoin - strokeMiterLimit: float32 - strokeDashArray: seq[float32] - transform: Mat3 - shouldStroke: bool +type + LinearGradient = object + x1, y1, x2, y2: float32 + stops: seq[ColorStop] + + Ctx = object + display: bool + fillRule: WindingRule + fill: Paint + stroke: ColorRGBX + strokeWidth: float32 + strokeLineCap: LineCap + strokeLineJoin: LineJoin + strokeMiterLimit: float32 + strokeDashArray: seq[float32] + transform: Mat3 + shouldStroke: bool + opacity, strokeOpacity: float32 + linearGradients: TableRef[string, LinearGradient] template failInvalid() = raise newException(PixieError, "Invalid SVG data") @@ -116,13 +162,21 @@ proc attrOrDefault(node: XmlNode, name, default: string): string = result = default proc initCtx(): Ctx = - result.fill = parseHtmlColor("black").rgba - result.stroke = parseHtmlColor("black").rgba + result.display = true + try: + result.fill = parseHtmlColor("black").rgbx + result.stroke = parseHtmlColor("black").rgbx + except: + let e = getCurrentException() + raise newException(PixieError, e.msg, e) result.strokeWidth = 1 result.transform = mat3() result.strokeMiterLimit = defaultMiterLimit + result.opacity = 1 + result.strokeOpacity = 1 + result.linearGradients = newTable[string, LinearGradient]() -proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = +proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx = result = inherited proc splitArgs(s: string): seq[string] = @@ -143,6 +197,10 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = strokeDashArray = node.attr("stroke-dasharray") transform = node.attr("transform") style = node.attr("style") + display = node.attr("display") + opacity = node.attr("opacity") + fillOpacity = node.attr("fill-opacity") + strokeOpacity = node.attr("stroke-opacity") let pairs = style.split(';') for pair in pairs: @@ -150,6 +208,9 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = if parts.len == 2: # Do not override element properties case parts[0].strip(): + of "fill-rule": + if fillRule.len == 0: + fillRule = parts[1].strip() of "fill": if fill.len == 0: fill = parts[1].strip() @@ -171,6 +232,35 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = of "stroke-dasharray": if strokeDashArray.len == 0: strokeDashArray = parts[1].strip() + of "display": + if display.len == 0: + display = parts[1].strip() + of "opacity": + if opacity.len == 0: + opacity = parts[1].strip() + of "fillOpacity": + if fillOpacity.len == 0: + fillOpacity = parts[1].strip() + of "strokeOpacity": + if strokeOpacity.len == 0: + strokeOpacity = parts[1].strip() + else: + discard + elif pair.len > 0: + when defined(pixieDebugSvg): + echo "Invalid style pair: ", pair + + if display.len > 0: + result.display = display.strip() != "none" + + if opacity.len > 0: + result.opacity = clamp(parseFloat(opacity), 0, 1) + + if fillOpacity.len > 0: + result.fill.opacity = clamp(parseFloat(fillOpacity), 0, 1) + + if strokeOpacity.len > 0: + result.strokeOpacity = clamp(parseFloat(strokeOpacity), 0, 1) if fillRule == "": discard # Inherit @@ -186,18 +276,30 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = if fill == "" or fill == "currentColor": discard # Inherit elif fill == "none": - result.fill = ColorRGBA() + 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(pkGradientLinear) + result.fill.gradientHandlePositions = @[ + result.transform * vec2(linearGradient.x1, linearGradient.y1), + result.transform * vec2(linearGradient.x2, linearGradient.y2) + ] + result.fill.gradientStops = linearGradient.stops + else: + raise newException(PixieError, "Missing SVG resource " & id) else: - result.fill = parseHtmlColor(fill).rgba + result.fill = parseHtmlColor(fill).rgbx if stroke == "": discard # Inherit elif stroke == "currentColor": result.shouldStroke = true elif stroke == "none": - result.stroke = ColorRGBA() + result.stroke = ColorRGBX() else: - result.stroke = parseHtmlColor(stroke).rgba + result.stroke = parseHtmlColor(stroke).rgbx result.shouldStroke = true if strokeWidth == "": @@ -208,7 +310,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = result.strokeWidth = parseFloat(strokeWidth) result.shouldStroke = true - if result.stroke == ColorRGBA() or result.strokeWidth <= 0: + if result.stroke == ColorRGBX() or result.strokeWidth <= 0: result.shouldStroke = false if strokeLineCap == "": @@ -262,7 +364,7 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = else: template failInvalidTransform(transform: string) = raise newException( - PixieError, "Unsupported SVG transform: " & transform & "." + PixieError, "Unsupported SVG transform: " & transform ) var remaining = transform @@ -320,6 +422,15 @@ proc decodeCtx(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: + let e = getCurrentException() + raise newException(PixieError, e.msg, e) + proc cairoLineCap(lineCap: LineCap): cairo.LineCap = case lineCap: of lcButt: @@ -339,19 +450,24 @@ proc cairoLineJoin(lineJoin: LineJoin): cairo.LineJoin = 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() + if ctx.display and ctx.opacity > 0: + let paint = newPaint(ctx.fill) + paint.opacity = paint.opacity * ctx.opacity + prepare(c, path, paint, 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() + if ctx.display and ctx.opacity > 0: + let paint = newPaint(ctx.stroke) + paint.color.a *= (ctx.opacity * ctx.strokeOpacity) + prepare(c, path, paint, ctx.transform) + 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 drawInternal(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = if node.kind != xnElement: # Skip return @@ -364,7 +480,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = let ctx = decodeCtx(ctxStack[^1], node) ctxStack.add(ctx) for child in node: - img.draw(child, ctxStack) + img.drawInternal(child, ctxStack) discard ctxStack.pop() of "path": @@ -372,8 +488,8 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = d = node.attr("d") ctx = decodeCtx(ctxStack[^1], node) path = parsePath(d) - if ctx.fill != ColorRGBA(): - img.fill(ctx, path) + + img.fill(ctx, path) if ctx.shouldStroke: img.stroke(ctx, path) @@ -385,7 +501,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = x2 = parseFloat(node.attrOrDefault("x2", "0")) y2 = parseFloat(node.attrOrDefault("y2", "0")) - var path: Path + let path = newPath() path.moveTo(x1, y1) path.lineTo(x2, y2) @@ -414,7 +530,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = if vecs.len == 0: failInvalid() - var path: Path + let path = newPath() path.moveTo(vecs[0]) for i in 1 ..< vecs.len: path.lineTo(vecs[i]) @@ -423,9 +539,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = # and fill or not if node.tag == "polygon": path.closePath() - - if ctx.fill != ColorRGBA(): - img.fill(ctx, path) + img.fill(ctx, path) if ctx.shouldStroke: img.stroke(ctx, path) @@ -445,7 +559,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0) ry = max(parseFloat(node.attrOrDefault("ry", "0")), 0) - var path: Path + let path = newPath() if rx > 0 or ry > 0: if rx == 0: rx = ry @@ -466,8 +580,7 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = else: path.rect(x, y, width, height) - if ctx.fill != ColorRGBA(): - img.fill(ctx, path) + img.fill(ctx, path) if ctx.shouldStroke: img.stroke(ctx, path) @@ -485,17 +598,25 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = rx = parseFloat(node.attrOrDefault("rx", "0")) ry = parseFloat(node.attrOrDefault("ry", "0")) - var path: Path + let path = newPath() path.ellipse(cx, cy, rx, ry) - if ctx.fill != ColorRGBA(): - img.fill(ctx, path) + img.fill(ctx, path) if ctx.shouldStroke: img.stroke(ctx, path) else: raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") +proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = + try: + drawInternal(img, node, ctxStack) + except PixieError as e: + raise e + except: + let e = getCurrentException() + raise newException(PixieError, e.msg, e) + proc decodeSvg*(data: string, width = 0, height = 0): Image = ## Render SVG file and return the image. Defaults to the SVG's view box size. try: @@ -519,21 +640,21 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image = vec2(-viewBoxMinX.float32, -viewBoxMinY.float32) ) - var surface: ptr Surface + var + width = width + height = height + surface: ptr Surface if width == 0 and height == 0: # Default to the view box size - result = newImage(viewBoxWidth, viewBoxHeight) - surface = imageSurfaceCreate( - FORMAT_ARGB32, viewBoxWidth.int32, viewBoxHeight.int32 - ) + width = viewBoxWidth.int32 + height = viewBoxHeight.int32 else: - result = newImage(width, height) - surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32) - let scaleX = width.float32 / viewBoxWidth.float32 scaleY = height.float32 / viewBoxHeight.float32 rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY)) + surface = imageSurfaceCreate(FORMAT_ARGB32, width.int32, height.int32) + let c = surface.create() var ctxStack = @[rootCtx] @@ -542,6 +663,8 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image = surface.flush() + result = newImage(width, height) + let pixels = cast[ptr UncheckedArray[array[4, uint8]]](surface.getData()) for y in 0 ..< result.height: for x in 0 ..< result.width: diff --git a/experiments/test_svg_cairo.nim b/experiments/test_svg_cairo.nim index 25f96e1..ee5b065 100644 --- a/experiments/test_svg_cairo.nim +++ b/experiments/test_svg_cairo.nim @@ -10,9 +10,19 @@ const files = [ "ellipse01", "triangle01", "quad01", - "Ghostscript_Tiger" + "Ghostscript_Tiger", + "scale", + "miterlimit", + "dashes" ] +proc doDiff(rendered: Image, name: string) = + rendered.writeFile(&"tests/fileformats/svg/rendered/{name}.png") + let + master = readImage(&"tests/fileformats/svg/masters/{name}.png") + (diffScore, diffImage) = diff(master, rendered) + echo &"{name} score: {diffScore}" + diffImage.writeFile(&"tests/fileformats/svg/diffs/{name}.png") + for file in files: - let image = decodeSvg(readFile(&"tests/images/svg/{file}.svg")) - image.writeFile(&"tests/images/svg/{file}.png") + doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file) diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index df60f73..3982508 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -329,15 +329,13 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = proc fill(img: Image, ctx: Ctx, path: Path) {.inline.} = if ctx.display and ctx.opacity > 0: let paint = newPaint(ctx.fill) - if ctx.opacity != 1: - paint.opacity = paint.opacity * ctx.opacity + 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) - if ctx.opacity != 1: - paint.color.a *= (ctx.opacity * ctx.strokeOpacity) + paint.color.a *= (ctx.opacity * ctx.strokeOpacity) img.strokePath( path, paint, diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index eac73a5..5d22dfe 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -46,6 +46,7 @@ type const epsilon: float64 = 0.0001 * PI ## Tiny value used for some computations. Must be float64 to prevent leaks. + pixelErrorMargin: float32 = 0.2 defaultMiterLimit*: float32 = 4 when defined(release): @@ -639,7 +640,7 @@ proc polygon*( path.polygon(pos.x, pos.y, size, sides) proc commandsToShapes( - path: Path, closeSubpaths = false, pixelScale: float32 = 1.0 + path: Path, closeSubpaths: bool, pixelScale: float32 ): seq[seq[Vec2]] = ## Converts SVG-like commands to sequences of vectors. var @@ -651,7 +652,7 @@ proc commandsToShapes( prevCommandKind = Move prevCtrl, prevCtrl2: Vec2 - let errorMarginSq = pow(0.2.float32 / pixelScale, 2) + let errorMarginSq = pow(pixelErrorMargin / pixelScale, 2) proc addSegment(shape: var seq[Vec2], at, to: Vec2) = # Don't add any 0 length lines @@ -1080,7 +1081,7 @@ proc computeBounds*( path: Path, transform = mat3() ): Rect {.raises: [PixieError].} = ## Compute the bounds of the path. - var shapes = path.commandsToShapes() + var shapes = path.commandsToShapes(true, pixelScale(transform)) shapes.transform(transform) computeBounds(shapes.shapesToSegments()) @@ -1135,23 +1136,18 @@ proc maxEntryCount(partitioning: Partitioning): int = for i in 0 ..< partitioning.partitions.len: result = max(result, partitioning.partitions[i].len) -proc insertionSort( - hits: var seq[(float32, int16)], lo, hi: int -) {.inline.} = - for i in lo + 1 .. hi: - var - j = i - 1 - k = i - while j >= 0 and hits[j][0] > hits[k][0]: - swap(hits[j + 1], hits[j]) - dec j - dec k - -proc sort(hits: var seq[(float32, int16)], inl, inr: int) = +proc sortHits(hits: var seq[(float32, int16)], inl, inr: int) = ## Quicksort + insertion sort, in-place and faster than standard lib sort. let n = inr - inl + 1 - if n < 32: - insertionSort(hits, inl, inr) + if n < 32: # Use insertion sort for the rest + for i in inl + 1 .. inr: + var + j = i - 1 + k = i + while j >= 0 and hits[j][0] > hits[k][0]: + swap(hits[j + 1], hits[j]) + dec j + dec k return var l = inl @@ -1166,8 +1162,8 @@ proc sort(hits: var seq[(float32, int16)], inl, inr: int) = swap(hits[l], hits[r]) inc l dec r - sort(hits, inl, r) - sort(hits, l, inr) + sortHits(hits, inl, r) + sortHits(hits, l, inr) proc shouldFill( windingRule: WindingRule, count: int @@ -1257,7 +1253,7 @@ proc computeCoverage( inc numHits if numHits > 0: - sort(hits, 0, numHits - 1) + sortHits(hits, 0, numHits - 1) if aa: for (prevAt, at, count) in hits.walk(numHits, windingRule, y, width): @@ -1657,7 +1653,8 @@ proc strokeShapes( lineCap: LineCap, lineJoin: LineJoin, miterLimit: float32, - dashes: seq[float32] + dashes: seq[float32], + pixelScale: float32 ): seq[seq[Vec2]] = if strokeWidth <= 0: return @@ -1669,7 +1666,7 @@ proc strokeShapes( proc makeCircle(at: Vec2): seq[Vec2] = let path = newPath() path.ellipse(at, halfStroke, halfStroke) - path.commandsToShapes()[0] + path.commandsToShapes(true, pixelScale)[0] proc makeRect(at, to: Vec2): seq[Vec2] = # Rectangle corners @@ -1695,7 +1692,15 @@ proc strokeShapes( @[a, b, c, d, a] - proc makeJoin(prevPos, pos, nextPos: Vec2): seq[Vec2] = + proc addJoin(shape: var seq[seq[Vec2]], prevPos, pos, nextPos: Vec2) = + let minArea = pixelErrorMargin / pixelScale + + if lineJoin == ljRound: + let area = PI.float32 * halfStroke * halfStroke + if area > minArea: + shape.add makeCircle(pos) + return + let angle = fixAngle(angle(nextPos - pos) - angle(prevPos - pos)) if abs(abs(angle) - PI) > epsilon: var @@ -1719,13 +1724,21 @@ proc strokeShapes( lb = line(nextPos + b, pos + b) var at: Vec2 if la.intersects(lb, at): - return @[pos + a, at, pos + b, pos, pos + a] + let + bisectorLengthSq = (at - pos).lengthSq + areaSq = 0.25.float32 * ( + a.lengthSq * bisectorLengthSq + b.lengthSq * bisectorLengthSq + ) + if areaSq > (minArea * minArea): + shape.add @[pos + a, at, pos + b, pos, pos + a] of ljBevel: - return @[a + pos, b + pos, pos, a + pos] + let areaSq = 0.25.float32 * a.lengthSq * b.lengthSq + if areaSq > (minArea * minArea): + shape.add @[a + pos, b + pos, pos, a + pos] of ljRound: - return makeCircle(pos) + discard # Handled above, skipping angle calculation for shape in shapes: var shapeStroke: seq[seq[Vec2]] @@ -1773,10 +1786,10 @@ proc strokeShapes( # If we need a line join if i < shape.len - 1: - shapeStroke.add(makeJoin(prevPos, pos, shape[i + 1])) + shapeStroke.addJoin(prevPos, pos, shape[i + 1]) if shape[0] == shape[^1]: - shapeStroke.add(makeJoin(shape[^2], shape[^1], shape[1])) + shapeStroke.addJoin(shape[^2], shape[^1], shape[1]) else: case lineCap: of lcButt: @@ -1793,7 +1806,7 @@ proc strokeShapes( result.add(shapeStroke) proc parseSomePath( - path: SomePath, closeSubpaths: bool, pixelScale: float32 = 1.0 + path: SomePath, closeSubpaths: bool, pixelScale: float32 ): seq[seq[Vec2]] {.inline.} = ## Given SomePath, parse it in different ways. when type(path) is string: @@ -1874,13 +1887,15 @@ proc strokePath*( blendMode = bmNormal ) {.raises: [PixieError].} = ## Strokes a path. + let pixelScale = transform.pixelScale() var strokeShapes = strokeShapes( - parseSomePath(path, false, transform.pixelScale()), + parseSomePath(path, false, pixelScale), strokeWidth, lineCap, lineJoin, miterLimit, - dashes + dashes, + pixelScale ) strokeShapes.transform(transform) mask.fillShapes(strokeShapes, wrNonZero, blendMode) @@ -1908,7 +1923,8 @@ proc strokePath*( lineCap, lineJoin, miterLimit, - dashes + dashes, + pixelScale(transform) ) strokeShapes.transform(transform) var color = paint.color @@ -1970,7 +1986,7 @@ proc overlaps( if segment.to != at: hits.add((at.x, winding)) - sort(hits, 0, hits.high) + sortHits(hits, 0, hits.high) var count: int for (at, winding) in hits: @@ -1985,7 +2001,7 @@ proc fillOverlaps*( windingRule = wrNonZero ): bool {.raises: [PixieError].} = ## Returns whether or not the specified point is contained in the current path. - var shapes = parseSomePath(path, true, transform.pixelScale()) + var shapes = path.commandsToShapes(true, transform.pixelScale()) shapes.transform(transform) shapes.overlaps(test, windingRule) @@ -2001,13 +2017,15 @@ proc strokeOverlaps*( ): bool {.raises: [PixieError].} = ## Returns whether or not the specified point is inside the area contained ## by the stroking of a path. + let pixelScale = transform.pixelScale() var strokeShapes = strokeShapes( - parseSomePath(path, false, transform.pixelScale()), + path.commandsToShapes(false, pixelScale), strokeWidth, lineCap, lineJoin, miterLimit, - dashes + dashes, + pixelScale ) strokeShapes.transform(transform) strokeShapes.overlaps(test, wrNonZero) diff --git a/tests/benchmark_fonts.nim b/tests/benchmark_fonts.nim index 429bbf7..6d7df49 100644 --- a/tests/benchmark_fonts.nim +++ b/tests/benchmark_fonts.nim @@ -12,7 +12,7 @@ let timeIt "typeset": discard font.typeset(text, bounds = vec2(image.width.float32, 0)) -timeIt "rasterize": +timeIt "fill text": image.fill(rgba(255, 255, 255, 255)) image.fillText(font, text, bounds = vec2(image.width.float32, 0)) # mask.fill(0) diff --git a/tests/contexts/bezierCurveTo_1.png b/tests/contexts/bezierCurveTo_1.png index d409fb8..1f3411e 100644 Binary files a/tests/contexts/bezierCurveTo_1.png and b/tests/contexts/bezierCurveTo_1.png differ diff --git a/tests/contexts/bezierCurveTo_2.png b/tests/contexts/bezierCurveTo_2.png index e7a04f5..ce15580 100644 Binary files a/tests/contexts/bezierCurveTo_2.png and b/tests/contexts/bezierCurveTo_2.png differ diff --git a/tests/contexts/ellipse_1.png b/tests/contexts/ellipse_1.png index 83ef76f..1717754 100644 Binary files a/tests/contexts/ellipse_1.png and b/tests/contexts/ellipse_1.png differ diff --git a/tests/contexts/quadracticCurveTo_1.png b/tests/contexts/quadracticCurveTo_1.png index 9cf79ce..e6b8c7b 100644 Binary files a/tests/contexts/quadracticCurveTo_1.png and b/tests/contexts/quadracticCurveTo_1.png differ diff --git a/tests/contexts/quadracticCurveTo_2.png b/tests/contexts/quadracticCurveTo_2.png index 07bf885..d33297f 100644 Binary files a/tests/contexts/quadracticCurveTo_2.png and b/tests/contexts/quadracticCurveTo_2.png differ diff --git a/tests/contexts/strokeText_1.png b/tests/contexts/strokeText_1.png index 1bf3eec..3d61ae9 100644 Binary files a/tests/contexts/strokeText_1.png and b/tests/contexts/strokeText_1.png differ diff --git a/tests/fileformats/svg/accessibility-outline.svg b/tests/fileformats/svg/accessibility-outline.svg new file mode 100644 index 0000000..499b826 --- /dev/null +++ b/tests/fileformats/svg/accessibility-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/fileformats/svg/diffs/Ghostscript_Tiger.png b/tests/fileformats/svg/diffs/Ghostscript_Tiger.png index b72862e..61f9f97 100644 Binary files a/tests/fileformats/svg/diffs/Ghostscript_Tiger.png and b/tests/fileformats/svg/diffs/Ghostscript_Tiger.png differ diff --git a/tests/fileformats/svg/diffs/accessibility-outline.png b/tests/fileformats/svg/diffs/accessibility-outline.png new file mode 100644 index 0000000..7334803 Binary files /dev/null and b/tests/fileformats/svg/diffs/accessibility-outline.png differ diff --git a/tests/fileformats/svg/emojitwo.png b/tests/fileformats/svg/emojitwo.png index 0ffb9b4..2364c97 100644 Binary files a/tests/fileformats/svg/emojitwo.png and b/tests/fileformats/svg/emojitwo.png differ diff --git a/tests/fileformats/svg/flat-color-icons.png b/tests/fileformats/svg/flat-color-icons.png index 041469a..b36b155 100644 Binary files a/tests/fileformats/svg/flat-color-icons.png and b/tests/fileformats/svg/flat-color-icons.png differ diff --git a/tests/fileformats/svg/ionicons.png b/tests/fileformats/svg/ionicons.png index 9e6b036..d94787c 100644 Binary files a/tests/fileformats/svg/ionicons.png and b/tests/fileformats/svg/ionicons.png differ diff --git a/tests/fileformats/svg/masters/accessibility-outline.png b/tests/fileformats/svg/masters/accessibility-outline.png new file mode 100644 index 0000000..218e7c6 Binary files /dev/null and b/tests/fileformats/svg/masters/accessibility-outline.png differ diff --git a/tests/fileformats/svg/noto-emoji.png b/tests/fileformats/svg/noto-emoji.png index 2e9e40d..a0e8864 100644 Binary files a/tests/fileformats/svg/noto-emoji.png and b/tests/fileformats/svg/noto-emoji.png differ diff --git a/tests/fileformats/svg/openmoji.png b/tests/fileformats/svg/openmoji.png index 71e2c31..b06cdb7 100644 Binary files a/tests/fileformats/svg/openmoji.png and b/tests/fileformats/svg/openmoji.png differ diff --git a/tests/fileformats/svg/rendered/Ghostscript_Tiger.png b/tests/fileformats/svg/rendered/Ghostscript_Tiger.png index 2431f99..0350d84 100644 Binary files a/tests/fileformats/svg/rendered/Ghostscript_Tiger.png and b/tests/fileformats/svg/rendered/Ghostscript_Tiger.png differ diff --git a/tests/fileformats/svg/rendered/accessibility-outline.png b/tests/fileformats/svg/rendered/accessibility-outline.png new file mode 100644 index 0000000..44e5f8b Binary files /dev/null and b/tests/fileformats/svg/rendered/accessibility-outline.png differ diff --git a/tests/fileformats/svg/tabler-icons.png b/tests/fileformats/svg/tabler-icons.png index f08a085..3bb93cb 100644 Binary files a/tests/fileformats/svg/tabler-icons.png and b/tests/fileformats/svg/tabler-icons.png differ diff --git a/tests/fonts/diffs/image_stroke.png b/tests/fonts/diffs/image_stroke.png index 1dc1929..214f20f 100644 Binary files a/tests/fonts/diffs/image_stroke.png and b/tests/fonts/diffs/image_stroke.png differ diff --git a/tests/fonts/diffs/mask_stroke.png b/tests/fonts/diffs/mask_stroke.png index 2cdf448..7bb605b 100644 Binary files a/tests/fonts/diffs/mask_stroke.png and b/tests/fonts/diffs/mask_stroke.png differ diff --git a/tests/fonts/diffs/strikethrough3.png b/tests/fonts/diffs/strikethrough3.png index 57772dc..e416f15 100644 Binary files a/tests/fonts/diffs/strikethrough3.png and b/tests/fonts/diffs/strikethrough3.png differ diff --git a/tests/fonts/diffs/underline3.png b/tests/fonts/diffs/underline3.png index 0f3be74..576c230 100644 Binary files a/tests/fonts/diffs/underline3.png and b/tests/fonts/diffs/underline3.png differ diff --git a/tests/fonts/rendered/image_stroke.png b/tests/fonts/rendered/image_stroke.png index f90121a..071f392 100644 Binary files a/tests/fonts/rendered/image_stroke.png and b/tests/fonts/rendered/image_stroke.png differ diff --git a/tests/fonts/rendered/mask_stroke.png b/tests/fonts/rendered/mask_stroke.png index a0ac5c1..324015c 100644 Binary files a/tests/fonts/rendered/mask_stroke.png and b/tests/fonts/rendered/mask_stroke.png differ diff --git a/tests/fonts/rendered/strikethrough3.png b/tests/fonts/rendered/strikethrough3.png index b9b0e7d..b4a3d90 100644 Binary files a/tests/fonts/rendered/strikethrough3.png and b/tests/fonts/rendered/strikethrough3.png differ diff --git a/tests/fonts/rendered/underline3.png b/tests/fonts/rendered/underline3.png index 4e0631d..7a42400 100644 Binary files a/tests/fonts/rendered/underline3.png and b/tests/fonts/rendered/underline3.png differ diff --git a/tests/paths/arc.png b/tests/paths/arc.png index f6abfbf..032a73d 100644 Binary files a/tests/paths/arc.png and b/tests/paths/arc.png differ diff --git a/tests/paths/arcTo1.png b/tests/paths/arcTo1.png index a07975e..0fdbac6 100644 Binary files a/tests/paths/arcTo1.png and b/tests/paths/arcTo1.png differ diff --git a/tests/paths/arcTo3.png b/tests/paths/arcTo3.png index 0b6c20f..6582704 100644 Binary files a/tests/paths/arcTo3.png and b/tests/paths/arcTo3.png differ diff --git a/tests/paths/pixelScale.png b/tests/paths/pixelScale.png index d27f29d..945095a 100644 Binary files a/tests/paths/pixelScale.png and b/tests/paths/pixelScale.png differ diff --git a/tests/test_paths.nim b/tests/test_paths.nim index bc72467..8c81a5a 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -605,7 +605,7 @@ block: doAssert not path.strokeOverlaps(vec2(0, 0)) doAssert not path.strokeOverlaps(vec2(20, 20)) doAssert path.strokeOverlaps(vec2(0, 20)) - doAssert path.strokeOverlaps(vec2(40, 20)) + doAssert path.strokeOverlaps(vec2(39.9, 19.9)) doAssert path.strokeOverlaps(vec2(19.8, 30.2)) doAssert not path.strokeOverlaps(vec2(19.4, 30.6)) diff --git a/tests/test_svg.nim b/tests/test_svg.nim index 5d09d79..e978626 100644 --- a/tests/test_svg.nim +++ b/tests/test_svg.nim @@ -26,3 +26,8 @@ proc doDiff(rendered: Image, name: string) = for file in files: doDiff(decodeSvg(readFile(&"tests/fileformats/svg/{file}.svg")), file) + +doDiff( + decodeSvg(readFile("tests/fileformats/svg/accessibility-outline.svg"), 512, 512), + "accessibility-outline" +)