Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 63 additions & 21 deletions src/pixie/fileformats/svg.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ type
Svg* = ref object
width*, height*: int
elements: seq[(Path, SvgProperties)]
prepared: bool
linearGradients: Table[string, LinearGradient]

SvgProperties = object
display: bool
fillRule: WindingRule
fill: string
fillColor: ColorRGBX
fillPrepared: PreparedShapes
stroke: ColorRGBX
strokeWidth: float32
strokeLineCap: LineCap
strokeLineJoin: LineJoin
strokeMiterLimit: float32
strokeDashArray: seq[float32]
strokePrepared: PreparedShapes
transform: Mat3
opacity, fillOpacity, strokeOpacity: float32

Expand Down Expand Up @@ -554,53 +558,91 @@ proc parseSvg*(data: string, width = 0, height = 0): Svg {.raises: [PixieError].
except:
raise currentExceptionAsPixieError()

proc prepareDraws(svg: Svg) {.raises: [PixieError].} =
## Tessellates SVG paths once so repeated renders reuse polygons.
if svg.prepared:
return

try:
for i in 0 ..< svg.elements.len:
let
path = svg.elements[i][0]
props = svg.elements[i][1].addr
if not props.display or props.opacity <= 0:
continue

if props.fill != "none" and not props.fill.startsWith("url("):
props.fillColor =
parseHtmlColor(props.fill).rgbx * (props.fillOpacity * props.opacity)
if props.fillColor.a > 0:
props.fillPrepared = path.polygonsForFill(props.transform).prepareShapes(
svg.width, svg.height
)

if props.stroke != rgbx(0, 0, 0, 0) and props.strokeWidth > 0:
let strokeColor =
props.stroke * (props.opacity * props.strokeOpacity)
if strokeColor.a > 0:
props.stroke = strokeColor
props.strokePrepared = path.polygonsForStroke(
props.transform,
props.strokeWidth,
props.strokeLineCap,
props.strokeLineJoin,
props.strokeMiterLimit,
props.strokeDashArray
).prepareShapes(svg.width, svg.height)
except PixieError as e:
raise e
except:
raise currentExceptionAsPixieError()

svg.prepared = true

proc newImage*(svg: Svg): Image {.raises: [PixieError].} =
## Render SVG and return the image.
svg.prepareDraws()
result = newImage(svg.width, svg.height)

try:
var blendMode = OverwriteBlend # Start as overwrite
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)
var paint = newPaint(LinearGradientPaint)
paint.gradientHandlePositions = @[
props.transform * vec2(linearGradient.x1, linearGradient.y1),
props.transform * vec2(linearGradient.x2, linearGradient.y2)
]
paint.gradientStops = linearGradient.stops
paint.opacity = props.fillOpacity * props.opacity
paint.blendMode = blendMode
result.fillPath(path, paint, props.transform, props.fillRule)
else:
raise newException(PixieError, "Missing SVG resource " & id)
else:
paint = parseHtmlColor(props.fill).rgbx

paint.opacity = props.fillOpacity * props.opacity
paint.blendMode = blendMode

result.fillPath(path, paint, props.transform, props.fillRule)
elif props.fillPrepared != nil:
result.fillPreparedShapes(
props.fillPrepared,
props.fillColor,
props.fillRule,
blendMode
)

blendMode = NormalBlend # Switch to normal when compositing multiple paths

if props.stroke != rgbx(0, 0, 0, 0) and props.strokeWidth > 0:
let paint = props.stroke.copy()
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
if props.strokePrepared != nil:
result.fillPreparedShapes(
props.strokePrepared,
props.stroke,
NonZero,
blendMode
)
except PixieError as e:
raise e
Expand Down
107 changes: 89 additions & 18 deletions src/pixie/paths.nim
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ type
requiresAntiAliasing, twoNonintersectingSpanningSegments: bool
top, bottom: int

PreparedShapes* = ref object
## Cached scanline partitions for repeated solid fills of the same shapes.
partitions: seq[Partition]
startX*, startY*, pathWidth*, pathHeight*: int
maxEntryCount: int

Fixed32 = int32 ## 24.8 fixed point

const
Expand Down Expand Up @@ -1261,7 +1267,7 @@ proc partitionSegments(
if entry0.midpointX > entry1.midpointX:
swap partition.entries[1], partition.entries[0]

proc maxEntryCount(partitions: var seq[Partition]): int =
proc maxEntryCount(partitions: seq[Partition]): int =
for i in 0 ..< partitions.len:
result = max(result, partitions[i].entries.len)

Expand Down Expand Up @@ -1353,7 +1359,7 @@ proc computeCoverage(
numHits: var int,
width: int,
y, startX: int,
partitions: var seq[Partition],
partitions: seq[Partition],
partitionIndex: int,
entryIndices: seq[int],
numEntryIndices: int,
Expand Down Expand Up @@ -1590,38 +1596,75 @@ proc fillHits(
image.data[dataIndex] = blender(backdrop, rgbx)
inc dataIndex

proc fillShapes(
proc prepareShapes*(
shapes: seq[Polygon],
imageWidth, imageHeight: int
): PreparedShapes {.raises: [PixieError].} =
## Builds cached partitions for shapes in image space.
result = PreparedShapes()
let
segments = shapes.shapesToSegments()
bounds = computeBounds(segments).snapToPixels()
result.startX = max(0, bounds.x.int)
result.startY = max(0, bounds.y.int)
result.pathWidth =
if result.startX < imageWidth:
min(bounds.w.int, imageWidth - result.startX)
else:
0
result.pathHeight = min(imageHeight, (bounds.y + bounds.h).int)

if result.pathWidth == 0:
return

if result.pathWidth < 0:
raise newException(PixieError, "Path int overflow detected")

result.partitions = partitionSegments(
segments,
result.startY,
result.pathHeight - result.startY
)
result.maxEntryCount = result.partitions.maxEntryCount

proc fillShapes*(
image: Image,
shapes: seq[Polygon],
color: SomeColor,
windingRule: WindingRule,
blendMode: BlendMode
) =
image.fillPreparedShapes(
shapes.prepareShapes(image.width, image.height),
color,
windingRule,
blendMode
)

proc fillPreparedShapes*(
image: Image,
prepared: PreparedShapes,
color: SomeColor,
windingRule: WindingRule,
blendMode: BlendMode
) =
## Fills previously prepared shapes.
# Figure out the total bounds of all the shapes,
# rasterize only within the total bounds
let
rgbx = color.asRgbx()
segments = shapes.shapesToSegments()
bounds = computeBounds(segments).snapToPixels()
startX = max(0, bounds.x.int)
startY = max(0, bounds.y.int)
pathWidth =
if startX < image.width:
min(bounds.w.int, image.width - startX)
else:
0
pathHeight = min(image.height, (bounds.y + bounds.h).int)
startX = prepared.startX
startY = prepared.startY
pathWidth = prepared.pathWidth
pathHeight = prepared.pathHeight

if pathWidth == 0:
return

if pathWidth < 0:
raise newException(PixieError, "Path int overflow detected")

let partitions = prepared.partitions
var
partitions = partitionSegments(segments, startY, pathHeight - startY)
partitionIndex: int
entryIndices = newSeq[int](partitions.maxEntryCount)
entryIndices = newSeq[int](prepared.maxEntryCount)
numEntryIndices: int
trapezoidSegments = newSeq[Segment](entryIndices.len)
coverages = newSeq[uint8](pathWidth)
Expand Down Expand Up @@ -2090,6 +2133,34 @@ proc parseSomePath(
elif type(path) is Path:
path.commandsToShapes(closeSubpaths, pixelScale)

proc polygonsForFill*(
path: Path, transform = mat3()
): seq[Polygon] {.raises: [PixieError].} =
## Tessellates a path into fill polygons in transform space.
result = parseSomePath(path, true, transform.pixelScale())
result.transform(transform)

proc polygonsForStroke*(
path: Path,
transform = mat3(),
strokeWidth: float32 = 1.0,
lineCap = ButtCap,
lineJoin = MiterJoin,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[]
): seq[Polygon] {.raises: [PixieError].} =
## Tessellates a stroked path into fill polygons in transform space.
result = strokeShapes(
parseSomePath(path, false, transform.pixelScale()),
strokeWidth,
lineCap,
lineJoin,
miterLimit,
dashes,
pixelScale(transform)
)
result.transform(transform)

proc fillPath*(
image: Image,
path: SomePath,
Expand Down
Loading