diff --git a/README.md b/README.md
index ce3a080..e6117a6 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ This library is being actively developed and we'd be happy for you to use it.
`nimble install pixie`
Features:
+* Typesetting and rasterizing text, including styled rich text via spans.
* Drawing paths, shapes and curves with even-odd and non-zero windings.
* Pixel-perfect AA quality.
* Supported file formats are PNG, BMP, JPG, SVG + more in development.
@@ -16,13 +17,13 @@ Features:
* Shadows, glows and blurs.
* Complex masking: Subtract, Intersect, Exclude.
* Complex blends: Darken, Multiply, Color Dodge, Hue, Luminosity... etc.
-* Many operations are SIMD accelerated where possible.
+* Many operations are SIMD accelerated.
### Documentation
API reference: https://treeform.github.io/pixie/pixie.html
-### File formats
+### Image file formats
Format | Read | Write |
------------- | ------------- | ------------- |
@@ -32,6 +33,14 @@ BMP | ✅ | ✅ |
GIF | ✅ | |
SVG | ✅ | |
+### Font file formats
+
+Format | Read
+------------- | -------------
+TTF | ✅
+OTF | ✅
+SVG | ✅
+
### Joins and caps
Supported Caps:
@@ -91,6 +100,40 @@ z | ✅ | close path |
## Examples
+### Text
+[examples/text.nim](examples/text.nim)
+```nim
+var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+font.size = 20
+
+let text = "Typesetting is the arrangement and composition of text in graphic design and publishing in both digital and traditional medias."
+
+image.fillText(font.typeset(text, bounds = vec2(180, 180)), vec2(10, 10))
+```
+
+
+### Text spans
+[examples/text_spans.nim](examples/text_spans.nim)
+```nim
+let font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+
+proc style(font: Font, size: float32, color: ColorRGBA): Font =
+ result = font
+ result.size = size
+ result.paint.color = color
+
+let spans = @[
+ newSpan("verb [with object] ", font.style(12, rgba(200, 200, 200, 255))),
+ newSpan("strallow\n", font.style(36, rgba(0, 0, 0, 255))),
+ newSpan("\nstral·low\n", font.style(13, rgba(0, 127, 244, 255))),
+ newSpan("\n1. free (something) from restrictive restrictions \"the regulations are intended to strallow changes in public policy\" ",
+ font.style(14, rgba(80, 80, 80, 255)))
+]
+
+image.fillText(typeset(spans, bounds = vec2(180, 180)), vec2(10, 10))
+```
+
+
### Square
[examples/square.nim](examples/square.nim)
```nim
diff --git a/examples/blur.png b/examples/blur.png
index a56cd1b..04340b4 100644
Binary files a/examples/blur.png and b/examples/blur.png differ
diff --git a/examples/gradient.png b/examples/gradient.png
index 9e76abb..ae1ba1b 100644
Binary files a/examples/gradient.png and b/examples/gradient.png differ
diff --git a/examples/heart.png b/examples/heart.png
index 968e975..7082326 100644
Binary files a/examples/heart.png and b/examples/heart.png differ
diff --git a/examples/image_tiled.png b/examples/image_tiled.png
index a892e15..897b8da 100644
Binary files a/examples/image_tiled.png and b/examples/image_tiled.png differ
diff --git a/examples/line.png b/examples/line.png
index 7bcf2f6..0791cdd 100644
Binary files a/examples/line.png and b/examples/line.png differ
diff --git a/examples/masking.png b/examples/masking.png
index c95e421..19d6f33 100644
Binary files a/examples/masking.png and b/examples/masking.png differ
diff --git a/examples/rounded_rectangle.png b/examples/rounded_rectangle.png
index f8596ec..a52a9a8 100644
Binary files a/examples/rounded_rectangle.png and b/examples/rounded_rectangle.png differ
diff --git a/examples/shadow.png b/examples/shadow.png
index 4bcbae3..d476390 100644
Binary files a/examples/shadow.png and b/examples/shadow.png differ
diff --git a/examples/square.png b/examples/square.png
index 1f0a431..e828ae2 100644
Binary files a/examples/square.png and b/examples/square.png differ
diff --git a/examples/text.nim b/examples/text.nim
new file mode 100644
index 0000000..0048b79
--- /dev/null
+++ b/examples/text.nim
@@ -0,0 +1,12 @@
+import pixie
+
+let image = newImage(200, 200)
+image.fill(rgba(255, 255, 255, 255))
+
+var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+font.size = 20
+
+let text = "Typesetting is the arrangement and composition of text in graphic design and publishing in both digital and traditional medias."
+
+image.fillText(font.typeset(text, bounds = vec2(180, 180)), vec2(10, 10))
+image.writeFile("examples/text.png")
diff --git a/examples/text.png b/examples/text.png
new file mode 100644
index 0000000..7483842
Binary files /dev/null and b/examples/text.png differ
diff --git a/examples/text_spans.nim b/examples/text_spans.nim
new file mode 100644
index 0000000..fdd0894
--- /dev/null
+++ b/examples/text_spans.nim
@@ -0,0 +1,22 @@
+import pixie
+
+let image = newImage(200, 200)
+image.fill(rgba(255, 255, 255, 255))
+
+let font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+
+proc style(font: Font, size: float32, color: ColorRGBA): Font =
+ result = font
+ result.size = size
+ result.paint.color = color
+
+let spans = @[
+ newSpan("verb [with object] ", font.style(12, rgba(200, 200, 200, 255))),
+ newSpan("strallow\n", font.style(36, rgba(0, 0, 0, 255))),
+ newSpan("\nstral·low\n", font.style(13, rgba(0, 127, 244, 255))),
+ newSpan("\n1. free (something) from restrictive restrictions \"the regulations are intended to strallow changes in public policy\" ",
+ font.style(14, rgba(80, 80, 80, 255)))
+]
+
+image.fillText(typeset(spans, bounds = vec2(180, 180)), vec2(10, 10))
+image.writeFile("examples/text_spans.png")
diff --git a/examples/text_spans.png b/examples/text_spans.png
new file mode 100644
index 0000000..41bf361
Binary files /dev/null and b/examples/text_spans.png differ
diff --git a/examples/tiger.png b/examples/tiger.png
index 1a77f38..6dc2d67 100644
Binary files a/examples/tiger.png and b/examples/tiger.png differ
diff --git a/pixie.nimble b/pixie.nimble
index fd7bae7..b490a5f 100644
--- a/pixie.nimble
+++ b/pixie.nimble
@@ -6,7 +6,7 @@ license = "MIT"
srcDir = "src"
requires "nim >= 1.2.6"
-requires "vmath >= 1.0.3"
+requires "vmath >= 1.0.4"
requires "chroma >= 0.2.5"
requires "zippy >= 0.3.5"
requires "flatty >= 0.1.3"
diff --git a/src/pixie.nim b/src/pixie.nim
index fbdc2e9..cb6f3f6 100644
--- a/src/pixie.nim
+++ b/src/pixie.nim
@@ -1,14 +1,28 @@
import bumpy, chroma, flatty/binny, os, pixie/blends, pixie/common,
pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpg,
- pixie/fileformats/png, pixie/fileformats/svg, pixie/images, pixie/masks,
- pixie/paints, pixie/paths, vmath
+ pixie/fileformats/png, pixie/fileformats/svg, pixie/fonts, pixie/images,
+ pixie/masks, pixie/paints, pixie/paths, strutils, vmath
-export blends, bumpy, chroma, common, images, masks, paints, paths, vmath
+export blends, bumpy, chroma, common, fonts, images, masks, paints, paths, vmath
type
FileFormat* = enum
ffPng, ffBmp, ffJpg, ffGif
+proc readFont*(filePath: string): Font =
+ ## Loads a font from a file.
+ result =
+ case splitFile(filePath).ext.toLowerAscii():
+ of ".ttf":
+ parseTtf(readFile(filePath))
+ of ".otf":
+ parseOtf(readFile(filePath))
+ of ".svg":
+ parseSvgFont(readFile(filePath))
+ else:
+ raise newException(PixieError, "Unsupported font format")
+ result.typeface.filePath = filePath
+
converter autoStraightAlpha*(c: ColorRGBX): ColorRGBA {.inline.} =
## Convert a paremultiplied alpha RGBA to a straight alpha RGBA.
c.rgba()
@@ -55,7 +69,7 @@ proc writeFile*(image: Image, filePath: string, fileFormat: FileFormat) =
proc writeFile*(image: Image, filePath: string) =
## Writes an image to a file.
- let fileFormat = case splitFile(filePath).ext:
+ let fileFormat = case splitFile(filePath).ext.toLowerAscii():
of ".png": ffPng
of ".bmp": ffBmp
of ".jpg", ".jpeg": ffJpg
@@ -313,3 +327,80 @@ proc strokePolygon*(
var path: Path
path.polygon(pos, size, sides)
mask.strokePath(path, strokeWidth)
+
+proc fillText*(
+ target: Image | Mask,
+ arrangement: Arrangement,
+ transform: Vec2 | Mat3 = vec2(0, 0)
+) =
+ ## Fills the text arrangement.
+ for spanIndex, (start, stop) in arrangement.spans:
+ let font = arrangement.fonts[spanIndex]
+ for runeIndex in start .. stop:
+ var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
+ path.transform(
+ translate(arrangement.positions[runeIndex]) *
+ scale(vec2(font.scale))
+ )
+ when type(target) is Image:
+ target.fillPath(path, font.paint, transform)
+ else: # target is Mask
+ target.fillPath(path, transform)
+
+proc fillText*(
+ target: Image | Mask,
+ font: Font,
+ text: string,
+ transform: Vec2 | Mat3 = vec2(0, 0),
+ bounds = vec2(0, 0),
+ hAlign = haLeft,
+ vAlign = vaTop
+) {.inline.} =
+ ## Typesets and fills the text. Optional parameters:
+ ## transform: translation or matrix to apply
+ ## bounds: width determines wrapping and hAlign, height for vAlign
+ ## hAlign: horizontal alignment of the text
+ ## vAlign: vertical alignment of the text
+ fillText(target, font.typeset(text, bounds, hAlign, vAlign), transform)
+
+proc strokeText*(
+ target: Image | Mask,
+ arrangement: Arrangement,
+ transform: Vec2 | Mat3 = vec2(0, 0),
+ strokeWidth = 1.0
+) =
+ ## Strokes the text arrangement.
+ for spanIndex, (start, stop) in arrangement.spans:
+ let font = arrangement.fonts[spanIndex]
+ for runeIndex in start .. stop:
+ var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
+ path.transform(
+ translate(arrangement.positions[runeIndex]) *
+ scale(vec2(font.scale))
+ )
+ when type(target) is Image:
+ target.strokePath(path, font.paint, transform, strokeWidth)
+ else: # target is Mask
+ target.strokePath(path, transform, strokeWidth)
+
+proc strokeText*(
+ target: Image | Mask,
+ font: Font,
+ text: string,
+ transform: Vec2 | Mat3 = vec2(0, 0),
+ strokeWidth = 1.0,
+ bounds = vec2(0, 0),
+ hAlign = haLeft,
+ vAlign = vaTop
+) {.inline.} =
+ ## Typesets and strokes the text. Optional parameters:
+ ## transform: translation or matrix to apply
+ ## bounds: width determines wrapping and hAlign, height for vAlign
+ ## hAlign: horizontal alignment of the text
+ ## vAlign: vertical alignment of the text
+ strokeText(
+ target,
+ font.typeset(text, bounds, hAlign, vAlign),
+ transform,
+ strokeWidth
+ )
diff --git a/src/pixie/demo.nim b/src/pixie/demo.nim
index 58b4f4e..fae1dbb 100644
--- a/src/pixie/demo.nim
+++ b/src/pixie/demo.nim
@@ -1,7 +1,6 @@
-import staticglfw except Image
-import opengl, pixie
-export pixie
-export staticglfw except Image
+import opengl, pixie, staticglfw except Image
+
+export pixie, staticglfw except Image
var
screen* = newImage(800, 600)
diff --git a/src/pixie/fileformats/png.nim b/src/pixie/fileformats/png.nim
index d44b3ec..939f5fb 100644
--- a/src/pixie/fileformats/png.nim
+++ b/src/pixie/fileformats/png.nim
@@ -104,9 +104,11 @@ proc unfilter(
let filterType = uncompressed[uncompressedIdx(0, y)]
case filterType:
of 0: # None
- for x in 0 ..< rowBytes:
- var value = uncompressed[uncompressedIdx(x + 1, y)]
- result[unfiteredIdx(x, y)] = value
+ copyMem(
+ result[unfiteredIdx(0, y)].addr,
+ uncompressed[uncompressedIdx(1, y)].unsafeAddr,
+ rowBytes
+ )
of 1: # Sub
for x in 0 ..< rowBytes:
var value = uncompressed[uncompressedIdx(x + 1, y)]
@@ -413,7 +415,7 @@ proc decodePng*(data: seq[uint8]): Image =
prevChunkType = chunkType
- if pos == data.len:
+ if pos == data.len or prevChunkType == "IEND":
break
if prevChunkType != "IEND":
diff --git a/src/pixie/fontformats/opentype.nim b/src/pixie/fontformats/opentype.nim
new file mode 100644
index 0000000..bb6353d
--- /dev/null
+++ b/src/pixie/fontformats/opentype.nim
@@ -0,0 +1,1575 @@
+import bitops, flatty/binny, math, pixie/common, pixie/paths, sets, tables,
+ unicode, vmath
+
+## See https://docs.microsoft.com/en-us/typography/opentype/spec/
+
+export tables
+
+type
+ EncodingRecord* = object
+ platformID*: uint16
+ encodingID*: uint16
+ offset*: uint32
+
+ CmapTable* = ref object
+ version*: uint16
+ numTables*: uint16
+ encodingRecords*: seq[EncodingRecord]
+ runeToGlyphId*: Table[Rune, uint16]
+ glyphIdToRune*: Table[uint16, Rune]
+
+ HeadTable* = ref object
+ majorVersion*: uint16
+ minorVersion*: uint16
+ fontRevision*: float32
+ checkSumAdjustment*: uint32
+ magicNumber*: uint32
+ flags*: uint16
+ unitsPerEm*: uint16
+ created*: float64
+ modified*: float64
+ xMin*: int16
+ yMin*: int16
+ xMax*: int16
+ yMax*: int16
+ macStyle*: uint16
+ lowestRecPPEM*: uint16
+ fontDirectionHint*: int16
+ indexToLocFormat*: int16
+ glyphDataFormat*: int16
+
+ HheaTable* = ref object
+ majorVersion*: uint16
+ minorVersion*: uint16
+ ascender*: int16
+ descender*: int16
+ lineGap*: int16
+ advanceWidthMax*: uint16
+ minLeftSideBearing*: int16
+ minRightSideBearing*: int16
+ xMaxExtent*: int16
+ caretSlopeRise*: int16
+ caretSlopeRun*: int16
+ caretOffset*: int16
+ metricDataFormat*: int16
+ numberOfHMetrics*: uint16
+
+ MaxpTable* = ref object
+ version*: float32
+ numGlyphs*: uint16
+ maxPoints*: uint16
+ maxContours*: uint16
+ maxCompositePoints*: uint16
+ maxCompositeContours*: uint16
+ maxZones*: uint16
+ maxTwilightPoints*: uint16
+ maxStorage*: uint16
+ maxFunctionDefs*: uint16
+ maxInstructionDefs*: uint16
+ maxStackElements*: uint16
+ maxSizeOfInstructions*: uint16
+ maxComponentElements*: uint16
+ maxComponentDepth*: uint16
+
+ LongHorMetricRecord* = object
+ advanceWidth*: uint16
+ leftSideBearing*: int16
+
+ HmtxTable* = ref object
+ hMetrics*: seq[LongHorMetricRecord]
+ leftSideBearings*: seq[int16]
+
+ NameRecord* = object
+ platformID*: uint16
+ encodingID*: uint16
+ languageID*: uint16
+ nameID*: uint16
+ length*: uint16
+ offset*: uint16
+
+ NameTable* = ref object
+ format*: uint16
+ count*: uint16
+ stringOffset*: uint16
+ nameRecords*: seq[NameRecord]
+
+ OS2Table* = ref object
+ version*: uint16
+ xAvgCharWidth*: int16
+ usWeightClass*: uint16
+ usWidthClass*: uint16
+ fsType*: uint16
+ ySubscriptXSize*: int16
+ ySubscriptYSize*: int16
+ ySubscriptXOffset*: int16
+ ySubscriptYOffset*: int16
+ ySuperscriptXSize*: int16
+ ySuperscriptYSize*: int16
+ ySuperscriptXOffset*: int16
+ ySuperscriptYOffset*: int16
+ yStrikeoutSize*: int16
+ yStrikeoutPosition*: int16
+ sFamilyClass*: int16
+ panose*: array[10, uint8]
+ ulUnicodeRange1*: uint32
+ ulUnicodeRange2*: uint32
+ ulUnicodeRange3*: uint32
+ ulUnicodeRange4*: uint32
+ achVendID*: string
+ fsSelection*: uint16
+ usFirstCharIndex*: uint16
+ usLastCharIndex*: uint16
+ sTypoAscender*: int16
+ sTypoDescender*: int16
+ sTypoLineGap*: int16
+ usWinAscent*: uint16
+ usWinDescent*: uint16
+ ulCodePageRange1*: uint32
+ ulCodePageRange2*: uint32
+ sxHeight*: int16
+ sCapHeight*: int16
+ usDefaultChar*: uint16
+ usBreakChar*: uint16
+ usMaxContext*: uint16
+ usLowerOpticalPointSize*: uint16
+ usUpperOpticalPointSize*: uint16
+
+ LocaTable* = ref object
+ offsets*: seq[uint32]
+
+ GlyfTable* = ref object
+ offsets*: seq[uint32]
+
+ KernPair* = object
+ left*: uint16
+ right*: uint16
+ value*: int16
+
+ KernSubTable* = object
+ version*: uint16
+ length*: uint16
+ coverage*: uint16
+ nPairs*: uint16
+ searchRange*: uint16
+ entrySelector*: uint16
+ rangeShift*: uint16
+ kernPairs*: seq[KernPair]
+
+ KernTable* = ref object
+ version*: uint16
+ nTables*: uint16
+ subTables*: seq[KernSubTable]
+ kerningPairs: Table[(uint16, uint16), float32]
+
+ TableRecord* = object
+ tag*: string
+ checksum*: uint32
+ offset*: uint32
+ length*: uint32
+
+ # LangSys = object
+ # lookupOrderOffset: uint16
+ # requiredFeatureIndex: uint16
+ # featureIndexCount: uint16
+ # featureIndices: seq[uint16]
+
+ # LangSysRecord = object
+ # langSysTag: string
+ # langSysOffset: uint16
+ # langSys: LangSys
+
+ # Script = object
+ # defaultLangSysOffset: uint16
+ # langSysCount: uint16
+ # langSysRecords: seq[LangSysRecord]
+
+ # ScriptRecord = object
+ # scriptTag: string
+ # scriptOffset: uint16
+ # script: Script
+
+ # ScriptList = object
+ # scriptCount: uint16
+ # scriptRecords: seq[ScriptRecord]
+
+ # Feature = object
+ # featureParamsOffset: uint16
+ # lookupIndexCount: uint16
+ # lookupListIndices: seq[uint16]
+
+ # FeatureRecord = object
+ # featureTag: string
+ # featureOffset: uint16
+ # feature: Feature
+
+ # FeatureList = object
+ # featureCount: uint16
+ # featureRecords: seq[FeatureRecord]
+
+ RangeRecord = object
+ startGlyphID: uint16
+ endGlyphID: uint16
+ startCoverageIndex: uint16
+
+ Coverage = object
+ coverageFormat: uint16
+ glyphCount: uint16
+ glyphArray: seq[uint16]
+ rangeCount: uint16
+ rangeRecords: seq[RangeRecord]
+ coveredGlyphs: HashSet[uint16]
+
+ ValueRecord = object
+ xPlacement: int16
+ yPlacement: int16
+ xAdvance: int16
+ yAdvance: int16
+ xPlaDeviceOffset: uint16
+ yPlaDeviceOffset: uint16
+ xAdvDeviceOffset: uint16
+ yAdvDeviceOffset: uint16
+
+ PairValueRecord = object
+ secondGlyph: uint16
+ valueRecord1: ValueRecord
+ valueRecord2: ValueRecord
+
+ PairSet = object
+ pairValueCount: uint16
+ pairValueRecords: seq[PairValueRecord]
+
+ Class2Record = object
+ valueRecord1: ValueRecord
+ valueRecord2: ValueRecord
+
+ Class1Record = object
+ class2Records: seq[Class2Record]
+
+ ClassRangeRecord = object
+ startGlyphID: uint16
+ endGlyphID: uint16
+ class: uint16
+
+ ClassDef = object
+ classFormat: uint16
+ startGlyphID: uint16
+ glyphCount: uint16
+ classValueArray: seq[uint16]
+ classRangeCount: uint16
+ classRangeRecords: seq[ClassRangeRecord]
+
+ PairPos = ref object
+ posFormat: uint16
+ coverageOffset: uint16
+ valueFormat1: uint16
+ valueFormat2: uint16
+ pairSetCount: uint16
+ pairSetOffsets: seq[uint16]
+ pairSets: seq[PairSet]
+ classDef1Offset: uint16
+ classDef2Offset: uint16
+ class1Count: uint16
+ class2Count: uint16
+ class1Records: seq[Class1Record]
+ classDef1: ClassDef
+ classDef2: ClassDef
+ coverage: Coverage
+ glyphIdToClass1: Table[uint16, uint16]
+ glyphIdToClass2: Table[uint16, uint16]
+ classPairAdjustments: Table[(uint16, uint16), int16]
+ glyphPairAdjustments: Table[(uint16, uint16), int16]
+
+ Lookup = object
+ lookupType: uint16
+ lookupFlag: uint16
+ subTableCount: uint16
+ subTableOffsets: seq[uint16]
+ markFilteringSet: uint16
+
+ LookupList = object
+ lookupCount: uint16
+ lookupOffsets: seq[uint16]
+ lookups: seq[Lookup]
+ pairPosTables: seq[PairPos]
+
+ GposTable = ref object
+ majorVersion: uint16
+ minorVersion: uint16
+ scriptListOffset: uint16
+ featureListOffset: uint16
+ lookupListOffset: uint16
+ featureVariationsOffset: uint32
+ # scriptList: ScriptList
+ # featureList: FeatureList
+ lookupList: LookupList
+
+ OpenType* = ref object
+ buf*: string
+ version*: uint32
+ numTables*: uint16
+ searchRange*: uint16
+ entrySelector*: uint16
+ rangeShift*: uint16
+ tableRecords*: Table[string, TableRecord]
+ cmap*: CmapTable
+ head*: HeadTable
+ hhea*: HheaTable
+ maxp*: MaxpTable
+ hmtx*: HmtxTable
+ name*: NameTable
+ os2*: OS2Table
+ loca*: LocaTable
+ glyf*: GlyfTable
+ kern*: KernTable
+ gpos*: GposTable
+ glyphPaths: Table[Rune, Path]
+
+when defined(release):
+ {.push checks: off.}
+
+proc eofCheck(buf: string, readTo: int) {.inline.} =
+ if readTo > buf.len:
+ raise newException(PixieError, "Unexpected error reading font data, EOF")
+
+proc failUnsupported() =
+ raise newException(PixieError, "Unsupported font data")
+
+proc readUint16Seq(buf: string, offset, len: int): seq[uint16] =
+ result = newSeq[uint16](len)
+ for i in 0 ..< len:
+ result[i] = buf.readUint16(offset + i * 2).swap()
+
+proc readFixed32(buf: string, offset: int): float32 =
+ ## Packed 32-bit value with major and minor version numbers.
+ ceil(buf.readInt32(offset).swap().float32 / 65536.0 * 100000.0) / 100000.0
+
+proc readFixed16(buf: string, offset: int): float32 =
+ ## Reads 16-bit signed fixed number with the low 14 bits of fraction (2.14).
+ buf.readInt16(offset).swap().float32 / 16384.0
+
+proc readLongDateTime(buf: string, offset: int): float64 =
+ ## Date and time represented in number of seconds since 12:00 midnight,
+ ## January 1, 1904, UTC.
+ buf.readInt64(offset).swap().float64 - 2082844800
+
+proc parseCmapTable(buf: string, offset: int): CmapTable =
+ var i = offset
+ buf.eofCheck(i + 4)
+
+ result = CmapTable()
+ result.version = buf.readUint16(i + 0).swap()
+ result.numTables = buf.readUint16(i + 2).swap()
+ i += 4
+
+ for j in 0 ..< result.numTables.int:
+ buf.eofCheck(i + 8)
+
+ var encodingRecord: EncodingRecord
+ encodingRecord.platformID = buf.readUint16(i + 0).swap()
+ encodingRecord.encodingID = buf.readUint16(i + 2).swap()
+ encodingRecord.offset = buf.readUint32(i + 4).swap()
+ i += 8
+
+ if encodingRecord.platformID == 3:
+ # Windows
+ var i = offset + encodingRecord.offset.int
+ buf.eofCheck(i + 2)
+
+ let format = buf.readUint16(i + 0).swap()
+ if format == 4:
+ type Format4 = object
+ format: uint16
+ length: uint16
+ language: uint16
+ segCountX2: uint16
+ searchRange: uint16
+ entrySelector: uint16
+ rangeShift: uint16
+ endCodes: seq[uint16]
+ reservedPad: uint16
+ startCodes: seq[uint16]
+ idDeltas: seq[uint16]
+ idRangeOffsets: seq[uint16]
+
+ buf.eofCheck(i + 14)
+
+ var subTable: Format4
+ subTable.format = format
+ subTable.length = buf.readUint16(i + 2).swap()
+ subTable.language = buf.readUint16(i + 4).swap()
+ subTable.segCountX2 = buf.readUint16(i + 6).swap()
+ let segCount = (subtable.segCountX2 div 2).int
+ subTable.searchRange = buf.readUint16(i + 8).swap()
+ subTable.entrySelector = buf.readUint16(i + 10).swap()
+ subTable.rangeShift = buf.readUint16(i + 12).swap()
+ i += 14
+
+ buf.eofCheck(i + 2 + 4 * segCount * 2)
+
+ subTable.endCodes = buf.readUint16Seq(i, segCount)
+ i += segCount * 2
+ subTable.reservedPad = buf.readUint16(i + 0).swap()
+ i += 2
+ subTable.startCodes = buf.readUint16Seq(i, segCount)
+ i += segCount * 2
+ subTable.idDeltas = buf.readUint16Seq(i, segCount)
+ i += segCount * 2
+ let idRangeOffsetPos = i
+ subTable.idRangeOffsets = buf.readUint16Seq(i, segCount)
+ i += segCount * 2
+
+ for k in 0 ..< segCount:
+ let
+ endCode = subTable.endCodes[k]
+ startCode = subTable.startCodes[k]
+ idDelta = subTable.idDeltas[k].int
+ idRangeOffset = subTable.idRangeOffsets[k].int
+ for c in startCode .. endCode:
+ var glyphId: int
+ if idRangeOffset != 0:
+ var glyphIdOffset = idRangeOffsetPos + k * 2
+ glyphIdOffset += idRangeOffset
+ glyphIdOffset += (c - startCode).int * 2
+ buf.eofCheck(glyphIdOffset + 2)
+ glyphId = buf.readUint16(glyphIdOffset).swap().int
+ if glyphId != 0:
+ glyphId = (glyphId + idDelta) and 0xFFFF
+ else:
+ glyphId = (c.int + idDelta) and 0xFFFF
+
+ if c != 65535:
+ result.runeToGlyphId[Rune(c)] = glyphId.uint16
+ result.glyphIdToRune[glyphId.uint16] = Rune(c)
+ else:
+ # TODO implement other Windows encodingIDs
+ discard
+ else:
+ # TODO implement other cmap platformIDs
+ discard
+
+proc parseHeadTable(buf: string, offset: int): HeadTable =
+ buf.eofCheck(offset + 54)
+
+ result = HeadTable()
+ result.majorVersion = buf.readUint16(offset + 0).swap()
+ if result.majorVersion != 1:
+ failUnsupported()
+ result.minorVersion = buf.readUint16(offset + 2).swap()
+ if result.minorVersion != 0:
+ failUnsupported()
+ result.fontRevision = buf.readFixed32(offset + 4)
+ result.checkSumAdjustment = buf.readUint32(offset + 8).swap()
+ result.magicNumber = buf.readUint32(offset + 12).swap()
+ result.flags = buf.readUint16(offset + 16).swap()
+ result.unitsPerEm = buf.readUint16(offset + 18).swap()
+ result.created = buf.readLongDateTime(offset + 20)
+ result.modified = buf.readLongDateTime(offset + 28)
+ result.xMin = buf.readInt16(offset + 36).swap()
+ result.yMin = buf.readInt16(offset + 38).swap()
+ result.xMax = buf.readInt16(offset + 40).swap()
+ result.yMax = buf.readInt16(offset + 42).swap()
+ result.macStyle = buf.readUint16(offset + 44).swap()
+ result.lowestRecPPEM = buf.readUint16(offset + 46).swap()
+ result.fontDirectionHint = buf.readInt16(offset + 48).swap()
+ result.indexToLocFormat = buf.readInt16(offset + 50).swap()
+ result.glyphDataFormat = buf.readInt16(offset + 52).swap()
+ if result.glyphDataFormat != 0:
+ failUnsupported()
+
+proc parseHheaTable(buf: string, offset: int): HheaTable =
+ buf.eofCheck(offset + 36)
+
+ result = HheaTable()
+ result.majorVersion = buf.readUint16(offset + 0).swap()
+ if result.majorVersion != 1:
+ failUnsupported()
+ result.minorVersion = buf.readUint16(offset + 2).swap()
+ if result.minorVersion != 0:
+ failUnsupported()
+ result.ascender = buf.readInt16(offset + 4).swap()
+ result.descender = buf.readInt16(offset + 6).swap()
+ result.lineGap = buf.readInt16(offset + 8).swap()
+ result.advanceWidthMax = buf.readUint16(offset + 10).swap()
+ result.minLeftSideBearing = buf.readInt16(offset + 12).swap()
+ result.minRightSideBearing = buf.readInt16(offset + 14).swap()
+ result.xMaxExtent = buf.readInt16(offset + 16).swap()
+ result.caretSlopeRise = buf.readInt16(offset + 18).swap()
+ result.caretSlopeRun = buf.readInt16(offset + 20).swap()
+ result.caretOffset = buf.readInt16(offset + 22).swap()
+ # discard buf.readUint16(offset + 24).swap() # Reserved
+ # discard buf.readUint16(offset + 26).swap() # Reserved
+ # discard buf.readUint16(offset + 28).swap() # Reserved
+ # discard buf.readUint16(offset + 30).swap() # Reserved
+ result.metricDataFormat = buf.readInt16(offset + 32).swap()
+ if result.metricDataFormat != 0:
+ failUnsupported()
+ result.numberOfHMetrics = buf.readUint16(offset + 34).swap()
+
+proc parseMaxpTable(buf: string, offset: int): MaxpTable =
+ buf.eofCheck(offset + 32)
+
+ result = MaxpTable()
+ result.version = buf.readFixed32(offset + 0)
+ if result.version != 1.0:
+ failUnsupported()
+ result.numGlyphs = buf.readUint16(offset + 4).swap()
+ result.maxPoints = buf.readUint16(offset + 6).swap()
+ result.maxContours = buf.readUint16(offset + 8).swap()
+ result.maxCompositePoints = buf.readUint16(offset + 10).swap()
+ result.maxCompositeContours = buf.readUint16(offset + 12).swap()
+ result.maxZones = buf.readUint16(offset + 14).swap()
+ result.maxTwilightPoints = buf.readUint16(offset + 16).swap()
+ result.maxStorage = buf.readUint16(offset + 18).swap()
+ result.maxFunctionDefs = buf.readUint16(offset + 20).swap()
+ result.maxInstructionDefs = buf.readUint16(offset + 22).swap()
+ result.maxStackElements = buf.readUint16(offset + 24).swap()
+ result.maxSizeOfInstructions = buf.readUint16(offset + 26).swap()
+ result.maxComponentElements = buf.readUint16(offset + 28).swap()
+ result.maxComponentDepth = buf.readUint16(offset + 30).swap()
+
+proc parseHmtxTable(
+ buf: string, offset: int, hhea: HheaTable, maxp: MaxpTable
+): HmtxTable =
+ var i = offset
+
+ let
+ hMetricsSize = hhea.numberOfHMetrics.int * 4
+ leftSideBearingsSize = (maxp.numGlyphs - hhea.numberOfHMetrics).int * 2
+
+ buf.eofCheck(i + hMetricsSize + leftSideBearingsSize)
+
+ result = HmtxTable()
+ for glyph in 0 ..< maxp.numGlyphs.int:
+ if glyph < hhea.numberOfHMetrics.int:
+ var record = LongHorMetricRecord()
+ record.advanceWidth = buf.readUint16(i + 0).swap()
+ record.leftSideBearing = buf.readInt16(i + 2).swap()
+ result.hMetrics.add(record)
+ i += 4
+ else:
+ result.leftSideBearings.add(buf.readInt16(i).swap())
+ i += 2
+
+proc parseNameTable(buf: string, offset: int): NameTable =
+ var i = offset
+
+ buf.eofCheck(i + 6)
+
+ result = NameTable()
+ result.format = buf.readUint16(i + 0).swap()
+ if result.format != 0:
+ failUnsupported()
+ result.count = buf.readUint16(i + 2).swap()
+ result.stringOffset = buf.readUint16(i + 4).swap()
+
+ i += 6
+
+ buf.eofCheck(i + result.count.int * 12)
+
+ for j in 0 ..< result.count.int:
+ var record: NameRecord
+ record.platformID = buf.readUint16(i + 0).swap()
+ record.encodingID = buf.readUint16(i + 2).swap()
+ record.languageID = buf.readUint16(i + 4).swap()
+ record.nameID = buf.readUint16(i + 6).swap()
+ record.length = buf.readUint16(i + 8).swap()
+ record.offset = buf.readUint16(i + 10).swap()
+ i += 12
+
+proc parseOS2Table(buf: string, offset: int): OS2Table =
+ var i = offset
+
+ buf.eofCheck(i + 78)
+
+ result = OS2Table()
+ result.version = buf.readUint16(i + 0).swap()
+ result.xAvgCharWidth = buf.readInt16(i + 2).swap()
+ result.usWeightClass = buf.readUint16(i + 4).swap()
+ result.usWidthClass = buf.readUint16(i + 6).swap()
+ result.fsType = buf.readUint16(i + 8).swap()
+ result.ySubscriptXSize = buf.readInt16(i + 10).swap()
+ result.ySubscriptYSize = buf.readInt16(i + 12).swap()
+ result.ySubscriptXOffset = buf.readInt16(i + 14).swap()
+ result.ySubscriptYOffset = buf.readInt16(i + 16).swap()
+ result.ySuperscriptXSize = buf.readInt16(i + 18).swap()
+ result.ySuperscriptYSize = buf.readInt16(i + 20).swap()
+ result.ySuperscriptXOffset = buf.readInt16(i + 22).swap()
+ result.ySuperscriptYOffset = buf.readInt16(i + 24).swap()
+ result.yStrikeoutSize = buf.readInt16(i + 26).swap()
+ result.yStrikeoutPosition = buf.readInt16(i + 28).swap()
+ result.sFamilyClass = buf.readInt16(i + 30).swap()
+ i += 32
+ for i in 0 ..< 10:
+ result.panose[i] = buf.readUint8(i + i)
+ i += 10
+ result.ulUnicodeRange1 = buf.readUint32(i + 0).swap()
+ result.ulUnicodeRange2 = buf.readUint32(i + 4).swap()
+ result.ulUnicodeRange3 = buf.readUint32(i + 8).swap()
+ result.ulUnicodeRange4 = buf.readUint32(i + 12).swap()
+ result.achVendID = buf.readStr(i + 16, 4)
+ result.fsSelection = buf.readUint16(i + 20).swap()
+ result.usFirstCharIndex = buf.readUint16(i + 22).swap()
+ result.usLastCharIndex = buf.readUint16(i + 24).swap()
+ result.sTypoAscender = buf.readInt16(i + 26).swap()
+ result.sTypoDescender = buf.readInt16(i + 28).swap()
+ result.sTypoLineGap = buf.readInt16(i + 30).swap()
+ result.usWinAscent = buf.readUint16(i + 32).swap()
+ result.usWinDescent = buf.readUint16(i + 34).swap()
+ i += 36
+
+ if result.version >= 1.uint16:
+ buf.eofCheck(i + 8)
+ result.ulCodePageRange1 = buf.readUint32(i + 0).swap()
+ result.ulCodePageRange2 = buf.readUint32(i + 4).swap()
+ i += 8
+
+ if result.version >= 2.uint16:
+ buf.eofCheck(i + 10)
+ result.sxHeight = buf.readInt16(i + 0).swap()
+ result.sCapHeight = buf.readInt16(i + 2).swap()
+ result.usDefaultChar = buf.readUint16(i + 4).swap()
+ result.usBreakChar = buf.readUint16(i + 6).swap()
+ result.usMaxContext = buf.readUint16(i + 8).swap()
+ i += 10
+
+ if result.version >= 5.uint16:
+ buf.eofCheck(i + 4)
+ result.usLowerOpticalPointSize = buf.readUint16(i + 0).swap()
+ result.usUpperOpticalPointSize = buf.readUint16(i + 2).swap()
+ i += 4
+
+proc parseLocaTable(
+ buf: string, offset: int, head: HeadTable, maxp: MaxpTable
+): LocaTable =
+ var i = offset
+
+ result = LocaTable()
+ if head.indexToLocFormat == 0:
+ # uint16
+ buf.eofCheck(i + maxp.numGlyphs.int * 2)
+ for _ in 0 ..< maxp.numGlyphs.int:
+ result.offsets.add(buf.readUint16(i).swap().uint32 * 2)
+ i += 2
+ else:
+ # uint32
+ buf.eofCheck(i + maxp.numGlyphs.int * 4)
+ for _ in 0 ..< maxp.numGlyphs.int:
+ result.offsets.add(buf.readUint32(i).swap())
+ i += 4
+
+proc parseGlyfTable(buf: string, offset: int, loca: LocaTable): GlyfTable =
+ result = GlyfTable()
+ result.offsets.setLen(loca.offsets.len)
+ for glyphId in 0 ..< loca.offsets.len:
+ result.offsets[glyphId] = offset.uint32 + loca.offsets[glyphId]
+
+proc parseKernTable(buf: string, offset: int): KernTable =
+ var i = offset
+
+ buf.eofCheck(i + 2)
+
+ let version = buf.readUint16(i + 0).swap()
+ i += 2
+
+ if version == 0:
+ buf.eofCheck(i + 2)
+
+ result = KernTable()
+ result.version = version
+ result.nTables = buf.readUint16(i + 0).swap()
+ i += 2
+
+ for _ in 0 ..< result.nTables.int:
+ buf.eofCheck(i + 14)
+
+ var subTable: KernSubTable
+ subtable.version = buf.readUint16(i + 0).swap()
+ if subTable.version != 0:
+ failUnsupported()
+ subTable.length = buf.readUint16(i + 2).swap()
+ subTable.coverage = buf.readUint16(i + 4).swap()
+ if subTable.coverage shr 8 != 0:
+ failUnsupported()
+ subTable.nPairs = buf.readUint16(i + 6).swap()
+ subTable.searchRange = buf.readUint16(i + 8).swap()
+ subTable.entrySelector = buf.readUint16(i + 10).swap()
+ subTable.rangeShift = buf.readUint16(i + 12).swap()
+ i += 14
+
+ for _ in 0 ..< subTable.nPairs.int:
+ buf.eofCheck(i + 6)
+
+ var pair: KernPair
+ pair.left = buf.readUint16(i + 0).swap()
+ pair.right = buf.readUint16(i + 2).swap()
+ pair.value = buf.readInt16(i + 4).swap()
+ subTable.kernPairs.add(pair)
+ i += 6
+
+ result.subTables.add(subTable)
+
+ for table in result.subTables:
+ if (table.coverage and 1) != 0: # Horizontal data
+ for pair in table.kernPairs:
+ if pair.value != 0:
+ let key = (pair.left, pair.right)
+ var value = pair.value.float32
+ if key in result.kerningPairs:
+ if (table.coverage and 0b1000) != 0: # Override
+ discard
+ else: # Accumulate
+ value += result.kerningPairs[key]
+ result.kerningPairs[key] = value
+
+ elif version == 1:
+ discard # Mac format
+ else:
+ failUnsupported()
+
+# proc parseLangSys(buf: string, offset: int): LangSys =
+# var i = offset
+
+# buf.eofCheck(i + 6)
+
+# result.lookupOrderOffset = buf.readUint16(i + 0).swap()
+# result.requiredFeatureIndex = buf.readUint16(i + 2).swap()
+# result.featureIndexCount = buf.readUint16(i + 4).swap()
+# i += 6
+
+# buf.eofCheck(i + 2 * result.featureIndexCount.int)
+
+# result.featureIndices = buf.readUint16Seq(i, result.featureIndexCount.int)
+
+# proc parseScript(buf: string, offset: int): Script =
+# var i = offset
+
+# buf.eofCheck(i + 4)
+
+# result.defaultLangSysOffset = buf.readUint16(i + 0).swap()
+# result.langSysCount = buf.readUint16(i + 2).swap()
+# i += 4
+
+# buf.eofCheck(i + result.langSysCount.int * 6)
+
+# for _ in 0 ..< result.langSysCount.int:
+# var langSysRecord: LangSysRecord
+# langSysRecord.langSysTag = buf.readStr(i + 0, 4)
+# langSysRecord.langSysOffset = buf.readUint16(i + 4).swap()
+# langSysRecord.langSys = parseLangSys(
+# buf, offset + langSysRecord.langSysOffset.int
+# )
+# result.langSysRecords.add(langSysRecord)
+# i += 6
+
+# proc parseScriptList(buf: string, offset: int): ScriptList =
+# var i = offset
+
+# buf.eofCheck(i + 2)
+
+# result.scriptCount = buf.readUint16(i + 0).swap()
+# i += 2
+
+# buf.eofCheck(i + 6 * result.scriptCount.int)
+
+# for _ in 0 ..< result.scriptCount.int:
+# var scriptRecord: ScriptRecord
+# scriptRecord.scriptTag = buf.readStr(i + 0, 4)
+# scriptRecord.scriptOffset = buf.readUint16(i + 4).swap()
+# scriptRecord.script = parseScript(
+# buf, offset + scriptRecord.scriptOffset.int
+# )
+# result.scriptRecords.add(scriptRecord)
+# i += 6
+
+# proc parseFeature(buf: string, offset: int): Feature =
+# var i = offset
+
+# buf.eofCheck(i + 4)
+
+# result.featureParamsOffset = buf.readUint16(i + 0).swap()
+# result.lookupIndexCount = buf.readUint16(i + 2).swap()
+# i += 4
+
+# buf.eofCheck(i + 2 * result.lookupIndexCount.int)
+
+# result.lookupListIndices = buf.readUint16Seq(i, result.lookupIndexCount.int)
+
+# proc parseFeatureList(buf: string, offset: int): FeatureList =
+# var i = offset
+
+# buf.eofCheck(i + 2)
+
+# result.featureCount = buf.readUint16(i + 0).swap()
+# i += 2
+
+# buf.eofCheck(i + 6 * result.featureCount.int)
+
+# for _ in 0 ..< result.featureCount.int:
+# var featureRecord: FeatureRecord
+# featureRecord.featureTag = buf.readStr(i + 0, 4)
+# featureRecord.featureOffset = buf.readUint16(i + 4).swap()
+# featureRecord.feature = parseFeature(
+# buf, offset + featureRecord.featureOffset.int
+# )
+# result.featureRecords.add(featureRecord)
+# i += 6
+
+proc parseRangeRecord(buf: string, offset: int): RangeRecord =
+ buf.eofCheck(offset + 6)
+
+ result.startGlyphID = buf.readUint16(offset + 0).swap()
+ result.endGlyphID = buf.readUint16(offset + 2).swap()
+ result.startCoverageIndex = buf.readUint16(offset + 4).swap()
+
+proc parseCoverage(buf: string, offset: int): Coverage =
+ var i = offset
+
+ buf.eofCheck(i + 4)
+
+ result.coverageFormat = buf.readUint16(i + 0).swap()
+ i += 2
+
+ case result.coverageFormat:
+ of 1:
+ result.glyphCount = buf.readUint16(i + 0).swap()
+ i += 2
+
+ buf.eofCheck(i + result.glyphCount.int * 2)
+
+ result.glyphArray = buf.readUint16Seq(i, result.glyphCount.int)
+
+ for ci, glyphId in result.glyphArray:
+ result.coveredGlyphs.incl(glyphId)
+
+ of 2:
+ result.rangeCount = buf.readUint16(i + 0).swap()
+ i += 2
+
+ result.rangeRecords.setLen(result.rangeCount.int)
+ for j in 0 ..< result.rangeCount.int:
+ result.rangeRecords[j] = parseRangeRecord(buf, i)
+ i += 6
+
+ for rangeRecord in result.rangeRecords:
+ var ci = rangeRecord.startCoverageIndex.int
+ for glyphId in rangeRecord.startGlyphID .. rangeRecord.endGlyphID:
+ result.coveredGlyphs.incl(glyphId)
+ inc ci
+
+ else:
+ failUnsupported()
+
+proc valueFormatSize(valueFormat: uint16): int {.inline.} =
+ countSetBits(valueFormat) * 2
+
+proc parseValueRecord(
+ buf: string, offset: int, valueFormat: uint16
+): ValueRecord =
+ buf.eofCheck(offset + valueFormatSize(valueFormat))
+
+ var i = offset
+ if (valueFormat and 0b1) != 0:
+ result.xPlacement = buf.readInt16(i).swap()
+ i += 2
+ if (valueFormat and 0b10) != 0:
+ result.yPlacement = buf.readInt16(i).swap()
+ i += 2
+ if (valueFormat and 0b100) != 0:
+ result.xAdvance = buf.readInt16(i).swap()
+ i += 2
+ if (valueFormat and 0b1000) != 0:
+ result.yAdvance = buf.readInt16(i).swap()
+ i += 2
+ if (valueFormat and 0b10000) != 0:
+ result.xPlaDeviceOffset = buf.readUint16(i).swap()
+ i += 2
+ if (valueFormat and 0b100000) != 0:
+ result.yPlaDeviceOffset = buf.readUint16(i).swap()
+ i += 2
+ if (valueFormat and 0b1000000) != 0:
+ result.xAdvDeviceOffset = buf.readUint16(i).swap()
+ i += 2
+ if (valueFormat and 0b10000000) != 0:
+ result.yAdvDeviceOffset = buf.readUint16(i).swap()
+ i += 2
+
+proc parsePairValueRecord(
+ buf: string, offset: int, valueFormat1, valueFormat2: uint16
+): PairValueRecord =
+ var i = offset
+
+ buf.eofCheck(i + 2)
+
+ result.secondGlyph = buf.readUint16(i + 0).swap()
+ i += 2
+
+ result.valueRecord1 = parseValueRecord(buf, i, valueFormat1)
+ i += valueFormatSize(valueFormat1)
+ result.valueRecord2 = parseValueRecord(buf, i, valueFormat2)
+
+proc parsePairSet(
+ buf: string, offset: int, valueFormat1, valueFormat2: uint16
+): PairSet =
+ var i = offset
+
+ buf.eofCheck(i + 2)
+
+ result.pairValueCount = buf.readUint16(i + 0).swap()
+ i += 2
+
+ let pairValueRecordSize =
+ 2 + valueFormatSize(valueFormat1) + valueFormatSize(valueFormat2)
+
+ result.pairValueRecords.setLen(result.pairValueCount.int)
+ for j in 0 ..< result.pairValueCount.int:
+ result.pairValueRecords[j] =
+ parsePairValueRecord(buf, i, valueFormat1, valueFormat2)
+ i += pairValueRecordSize
+
+proc parseClass2Record(
+ buf: string, offset: int, valueFormat1, valueFormat2: uint16
+): Class2Record =
+ var i = offset
+
+ buf.eofCheck(
+ i + valueFormatSize(valueFormat1) + valueFormatSize(valueFormat2)
+ )
+
+ result.valueRecord1 = parseValueRecord(buf, i, valueFormat1)
+ i += valueFormatSize(valueFormat1)
+ result.valueRecord2 = parseValueRecord(buf, i, valueFormat2)
+
+proc parseClass1Record(
+ buf: string, offset: int, valueFormat1, valueFormat2, class2Count: uint16
+): Class1Record =
+ var i = offset
+
+ result.class2Records.setLen(class2Count.int)
+ for j in 0 ..< class2Count.int:
+ result.class2Records[j] =
+ parseClass2Record(buf, i, valueFormat1, valueFormat2)
+ i += valueFormatSize(valueFormat1) + valueFormatSize(valueFormat2)
+
+proc parseClassRangeRecord(buf: string, offset: int): ClassRangeRecord =
+ buf.eofCheck(offset + 6)
+
+ result.startGlyphID = buf.readUint16(offset + 0).swap()
+ result.endGlyphID = buf.readUint16(offset + 2).swap()
+ result.class = buf.readUint16(offset + 4).swap()
+
+proc parseClassDef(buf: string, offset: int): ClassDef =
+ var i = offset
+
+ buf.eofCheck(i + 2)
+
+ result.classFormat = buf.readUint16(i + 0).swap()
+ i += 2
+
+ case result.classFormat:
+ of 1:
+ buf.eofCheck(i + 4)
+
+ result.startGlyphID = buf.readUint16(i + 0).swap()
+ result.glyphCount = buf.readUint16(i + 2).swap()
+ i += 4
+
+ buf.eofCheck(i + result.glyphCount.int * 2)
+
+ result.classValueArray = buf.readUint16Seq(i + 0, result.glyphCount.int)
+ of 2:
+ buf.eofCheck(i + 2)
+
+ result.classRangeCount = buf.readUint16(i + 0).swap()
+ i += 2
+
+ result.classRangeRecords.setLen(result.classRangeCount.int)
+ for j in 0 ..< result.classRangeCount.int:
+ result.classRangeRecords[j] = parseClassRangeRecord(buf, i)
+ i += 6
+ else:
+ failUnsupported()
+
+proc parsePairPos(buf: string, offset: int): PairPos =
+ var i = offset
+
+ buf.eofCheck(i + 4)
+
+ result = PairPos()
+ result.posFormat = buf.readUint16(i + 0).swap()
+ i += 2
+
+ case result.posFormat:
+ of 1: # Glyph ID pairs
+ buf.eofCheck(i + 8)
+
+ result.coverageOffset = buf.readUint16(i + 0).swap()
+ result.valueFormat1 = buf.readUint16(i + 2).swap()
+ result.valueFormat2 = buf.readUint16(i + 4).swap()
+ result.pairSetCount = buf.readUint16(i + 6).swap()
+ i += 8
+
+ buf.eofCheck(i + 2 * result.pairSetCount.int)
+
+ let pairSetOffsets = buf.readUint16Seq(i + 0, result.pairSetCount.int)
+ i += 2 * result.pairSetCount.int
+
+ result.pairSets.setLen(result.pairSetCount.int)
+ for j in 0 ..< result.pairSetCount.int:
+ result.pairSets[j] = parsePairSet(
+ buf,
+ offset + pairSetOffsets[j].int,
+ result.valueFormat1,
+ result.valueFormat2
+ )
+
+ result.coverage = parseCoverage(buf, offset + result.coverageOffset.int)
+
+ if (result.valueFormat1 and 0b100) != 0:
+ case result.coverage.coverageFormat:
+ of 1:
+ if result.coverage.glyphCount != result.pairSetCount:
+ failUnsupported()
+ for ci, glyphId in result.coverage.glyphArray:
+ if ci < 0 or ci >= result.pairSets.len:
+ failUnsupported()
+ for pairValueRecord in result.pairSets[ci].pairValueRecords:
+ if pairValueRecord.valueRecord1.xAdvance != 0:
+ let glyphPair = (glyphId, pairValueRecord.secondGlyph)
+ result.glyphPairAdjustments[glyphPair] =
+ pairValueRecord.valueRecord1.xAdvance
+ of 2:
+ for rangeRecord in result.coverage.rangeRecords:
+ var ci = rangeRecord.startCoverageIndex.int
+ for glyphId in rangeRecord.startGlyphID .. rangeRecord.endGlyphID:
+ if ci < 0 or ci >= result.pairSets.len:
+ failUnsupported()
+ for pairValueRecord in result.pairSets[ci].pairValueRecords:
+ if pairValueRecord.valueRecord1.xAdvance != 0:
+ let glyphPair = (glyphId, pairValueRecord.secondGlyph)
+ result.glyphPairAdjustments[glyphPair] =
+ pairValueRecord.valueRecord1.xAdvance
+ inc ci
+ else:
+ discard
+ of 2: # Class pairs
+ buf.eofCheck(i + 14)
+
+ result.coverageOffset = buf.readUint16(i + 0).swap()
+ result.valueFormat1 = buf.readUint16(i + 2).swap()
+ result.valueFormat2 = buf.readUint16(i + 4).swap()
+ result.classDef1Offset = buf.readUint16(i + 6).swap()
+ result.classDef2Offset = buf.readUint16(i + 8).swap()
+ result.class1Count = buf.readUint16(i + 10).swap()
+ result.class2Count = buf.readUint16(i + 12).swap()
+
+ i += 14
+
+ let class2RecordSize =
+ valueFormatSize(result.valueFormat1) +
+ valueFormatSize(result.valueFormat2)
+
+ result.class1Records.setLen(result.class1Count.int)
+ for j in 0 ..< result.class1Count.int:
+ result.class1Records[j] = parseClass1Record(
+ buf, i, result.valueFormat1, result.valueFormat2, result.class2Count
+ )
+ i += class2RecordSize * result.class2Count.int
+
+ result.classDef1 = parseClassDef(buf, offset + result.classDef1Offset.int)
+ result.classDef2 = parseClassDef(buf, offset + result.classDef2Offset.int)
+
+ result.coverage = parseCoverage(buf, offset + result.coverageOffset.int)
+
+ proc classDefFormat1(
+ classDef: ClassDef, table: var Table[uint16, uint16]
+ ) =
+ for i in 0.uint16 ..< classDef.glyphCount:
+ if classDef.classValueArray[i] != 0:
+ table[classDef.startGlyphID + i] = classDef.classValueArray[i]
+
+ proc classDefFormat2(
+ classDef: ClassDef, table: var Table[uint16, uint16]
+ ) =
+ for record in classDef.classRangeRecords:
+ if record.startGlyphID > record.endGlyphID:
+ failUnsupported()
+ if record.class != 0:
+ for glyphId in record.startGlyphID .. record.endGlyphID:
+ table[glyphId] = record.class
+
+ case result.classDef1.classFormat:
+ of 1:
+ classDefFormat1(result.classDef1, result.glyphIdToClass1)
+ of 2:
+ classDefFormat2(result.classDef1, result.glyphIdToClass1)
+ else:
+ discard
+
+ case result.classDef2.classFormat:
+ of 1:
+ classDefFormat1(result.classDef2, result.glyphIdToClass2)
+ of 2:
+ classDefFormat2(result.classDef2, result.glyphIdToClass2)
+ else:
+ discard
+
+ if (result.valueFormat1 and 0b100) != 0:
+ for class1, class1Record in result.class1Records:
+ for class2, class2Record in class1Record.class2Records:
+ if class2Record.valueRecord1.xAdvance != 0:
+ result.classPairAdjustments[(class1.uint16, class2.uint16)] =
+ class2Record.valueRecord1.xAdvance
+ else:
+ failUnsupported()
+
+proc parseLookup(buf: string, offset: int, gpos: GposTable): Lookup =
+ var i = offset
+
+ buf.eofCheck(i + 6)
+
+ result.lookupType = buf.readUint16(i + 0).swap()
+ result.lookupFlag = buf.readUint16(i + 2).swap()
+ result.subTableCount = buf.readUint16(i + 4).swap()
+ i += 6
+
+ buf.eofCheck(i + 2 * result.subTableCount.int)
+
+ result.subTableOffsets = buf.readUint16Seq(i, result.subTableCount.int)
+ i += 2 * result.subTableCount.int
+
+ if (result.lookupFlag and 0x0010) != 0: # USE_MARK_FILTERING_SET
+ buf.eofCheck(i + 2)
+ result.markFilteringSet = buf.readUint16(i).swap()
+
+ for subTableOffset in result.subTableOffsets:
+ if result.lookupType == 2:
+ let pairPos = parsePairPos(buf, offset + subTableOffset.int)
+ if pairPos.glyphPairAdjustments.len > 0 or
+ pairPos.classPairAdjustments.len > 0:
+ gpos.lookupList.pairPosTables.add(pairPos)
+
+proc parseLookupList(buf: string, offset: int, gpos: GposTable): LookupList =
+ var i = offset
+
+ buf.eofCheck(i + 2)
+
+ result.lookupCount = buf.readUint16(i + 0).swap()
+ i += 2
+
+ buf.eofCheck(i + 2 * result.lookupCount.int)
+
+ result.lookupOffsets = buf.readUint16Seq(i, result.lookupCount.int)
+
+ for lookupOffset in result.lookupoffsets:
+ result.lookups.add(parseLookup(buf, offset + lookupOffset.int, gpos))
+
+proc parseGposTable(buf: string, offset: int): GPOSTable =
+ var i = offset
+
+ buf.eofCheck(i + 10)
+
+ result = GPOSTable()
+ result.majorVersion = buf.readUint16(i + 0).swap()
+ result.minorVersion = buf.readUint16(i + 2).swap()
+ result.scriptListOffset = buf.readUint16(i + 4).swap()
+ result.featureListOffset = buf.readUint16(i + 6).swap()
+ result.lookupListOffset = buf.readUint16(i + 8).swap()
+ i += 10
+
+ if result.majorVersion != 1:
+ failUnsupported()
+
+ if result.minorVersion == 0:
+ discard
+ elif result.minorVersion == 1:
+ buf.eofCheck(i + 4)
+ result.featureVariationsOffset = buf.readUint32(i + 0).swap()
+ i += 4
+ else:
+ failUnsupported()
+
+ # result.scriptList = parseScriptList(buf, offset + result.scriptListOffset.int)
+ # result.featureList =
+ # parseFeatureList(buf, offset + result.featureListOffset.int)
+ result.lookupList =
+ parseLookupList(buf, offset + result.lookupListOffset.int, result)
+
+proc getGlyphId(opentype: OpenType, rune: Rune): uint16 {.inline.} =
+ if rune in opentype.cmap.runeToGlyphId:
+ result = opentype.cmap.runeToGlyphId[rune]
+ else:
+ discard # Index 0 is the "missing character" glyph
+
+proc parseGlyph(opentype: OpenType, glyphId: uint16): Path
+
+proc parseGlyphPath(buf: string, offset, numberOfContours: int): Path =
+ if numberOfContours < 0:
+ raise newException(PixieError, "Glyph numberOfContours must be >= 0")
+ if numberOfContours == 0:
+ return
+
+ var i = offset
+
+ buf.eofCheck(i + 2 * numberOfContours + 2)
+
+ let endPtsOfContours = buf.readUint16Seq(i, numberOfContours)
+ i += 2 * numberOfContours
+
+ let instructionLength = buf.readUint16(i + 0).swap().int
+ i += 2
+
+ buf.eofCheck(instructionLength)
+
+ # let instructions = buf.readUint8Seq(i, instructionLength)
+ i += instructionLength
+
+ let
+ numPoints = endPtsOfContours[^1].int + 1
+ flags = block:
+ var
+ flags: seq[uint8]
+ point = 0
+ while point < numPoints:
+ buf.eofCheck(i + 1)
+ let flag = buf.readUint8(i)
+ flags.add(flag)
+ i += 1
+ point += 1
+
+ if (flag and 0b1000) != 0: # REPEAT_FLAG
+ buf.eofCheck(i + 1)
+ let repeatCount = buf.readUint8(i).int
+ i += 1
+ for j in 0 ..< repeatCount:
+ flags.add(flag)
+ point += 1
+ flags
+
+ type TtfCoordinate = object
+ x*: float32
+ y*: float32
+ isOnCurve*: bool
+
+ var points = newSeq[TtfCoordinate](numPoints)
+
+ var prevX = 0
+ for point, flag in flags:
+ var x: int
+ if (flag and 0b10) != 0:
+ buf.eofCheck(i + 1)
+ x = buf.readUint8(i).int
+ i += 1
+ if (flag and 0b10000) == 0:
+ x = -x
+ else:
+ if (flag and 0b10000) != 0:
+ x = 0
+ else:
+ buf.eofCheck(i + 2)
+ x = buf.readInt16(i).swap().int
+ i += 2
+ prevX += x
+ if point >= points.len:
+ failUnsupported()
+ points[point].x = prevX.float32
+ points[point].isOnCurve = (flag and 1) != 0
+
+ var prevY = 0
+ for point, flag in flags:
+ var y: int
+ if (flag and 0b100) != 0:
+ buf.eofCheck(i + 1)
+ y = buf.readUint8(i).int
+ i += 1
+ if (flag and 0b100000) == 0:
+ y = -y
+ else:
+ if (flag and 0b100000) != 0:
+ y = 0
+ else:
+ buf.eofCheck(i + 2)
+ y = buf.readInt16(i).swap().int
+ i += 2
+ prevY += y
+ points[point].y = prevY.float32
+
+ var
+ contours: seq[seq[TtfCoordinate]]
+ startIdx = 0
+ for endIdx in endPtsOfContours:
+ contours.add(points[startIdx .. endIdx.int])
+ startIdx = endIdx.int + 1
+
+ for contour in contours:
+ var prev, curr, next: TtfCoordinate
+ curr = contour[^1]
+ next = contour[0]
+
+ if curr.isOnCurve:
+ result.moveTo(curr.x, curr.y)
+ else:
+ if next.isOnCurve:
+ result.moveTo(next.x, next.y)
+ else:
+ result.moveTo((curr.x + next.x) / 2, (curr.y + next.y) / 2)
+
+ for point in 0 ..< contour.len:
+ prev = curr
+ curr = next
+ next = contour[(point + 1) mod contour.len]
+
+ if curr.isOnCurve:
+ result.lineTo(curr.x, curr.y)
+ else:
+ var next2 = next
+ if not next.isOnCurve:
+ next2 = TtfCoordinate(
+ x: (curr.x + next.x) / 2,
+ y: (curr.y + next.y) / 2
+ )
+
+ result.quadraticCurveTo(curr.x, curr.y, next2.x, next2.y)
+
+ result.closePath()
+
+proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
+ var
+ i = offset
+ moreComponents = true
+ while moreComponents:
+ opentype.buf.eofCheck(i + 4)
+
+ let flags = opentype.buf.readUint16(i + 0).swap()
+
+ i += 2
+
+ type TtfComponent = object
+ glyphId: uint16
+ xScale: float32
+ scale01: float32
+ scale10: float32
+ yScale: float32
+ dx: float32
+ dy: float32
+ matchedPoints: array[2, int]
+
+ var component = TtfComponent()
+ component.glyphId = opentype.buf.readUint16(i + 0).swap()
+ component.xScale = 1
+ component.yScale = 1
+
+ i += 2
+
+ if (flags and 1) != 0: # The arguments are uint16
+ opentype.buf.eofCheck(i + 4)
+ if (flags and 0b10) != 0: # The arguments are offets
+ component.dx = opentype.buf.readInt16(i + 0).swap().float32
+ component.dy = opentype.buf.readInt16(i + 2).swap().float32
+ else: # The arguments are matched points
+ component.matchedPoints = [
+ opentype.buf.readUint16(i + 0).swap().int,
+ opentype.buf.readUint16(i + 2).swap().int
+ ]
+ i += 4
+ else: # The arguments are uint8
+ opentype.buf.eofCheck(i + 2)
+ if (flags and 0b10) != 0: # Arguments are offsets
+ component.dx = opentype.buf.readInt8(i + 0).float32
+ component.dy = opentype.buf.readInt8(i + 1).float32
+ else: # The arguments are matched points
+ component.matchedPoints = [
+ opentype.buf.readInt8(i + 0).int,
+ opentype.buf.readInt8(i + 1).int
+ ]
+ i += 2
+
+ # TODO: ROUND_XY_TO_GRID
+
+ if (flags and 0b1000) != 0: # WE_HAVE_A_SCALE
+ opentype.buf.eofCheck(i + 2)
+ component.xScale = opentype.buf.readFixed16(i + 0)
+ component.yScale = component.xScale
+ i += 2
+ elif (flags and 0b1000000) != 0: # WE_HAVE_AN_X_AND_Y_SCALE
+ opentype.buf.eofCheck(i + 4)
+ component.xScale = opentype.buf.readFixed16(i + 0)
+ component.yScale = opentype.buf.readFixed16(i + 2)
+ i += 4
+ elif (flags and 0b10000000) != 0: # WE_HAVE_A_TWO_BY_TWO
+ opentype.buf.eofCheck(i + 8)
+ component.xScale = opentype.buf.readFixed16(i + 0)
+ component.scale10 = opentype.buf.readFixed16(i + 2)
+ component.scale01 = opentype.buf.readFixed16(i + 4)
+ component.yScale = opentype.buf.readFixed16(i + 6)
+ i += 8
+
+ # if (flags and 0b100000000) != 0: # WE_HAVE_INSTRUCTIONS
+ # discard
+ # elif (flags and 0b1000000000) != 0: # USE_MY_METRICS
+ # discard
+ # elif (flags and 0b10000000000) != 0: # OVERLAP_COMPOUND
+ # discard
+ # elif (flags and 0b100000000000) != 0: # SCALED_COMPONENT_OFFSET
+ # discard
+ # elif (flags and 0b1000000000000) != 0: # UNSCALED_COMPONENT_OFFSET
+ # discard
+
+ var subPath = opentype.parseGlyph(component.glyphId)
+ subPath.transform(mat3(
+ component.xScale, component.scale10, 0.0,
+ component.scale01, component.yScale, 0.0,
+ component.dx, component.dy, 1.0
+ ))
+
+ result.commands.add(subPath.commands)
+
+ moreComponents = (flags and 0b100000) != 0
+
+proc parseGlyph(opentype: OpenType, glyphId: uint16): Path =
+ if glyphId.int >= opentype.glyf.offsets.len:
+ raise newException(PixieError, "Invalid glyph ID " & $glyphId)
+
+ let glyphOffset = opentype.glyf.offsets[glyphId]
+
+ if glyphId.int + 1 < opentype.glyf.offsets.len and
+ glyphOffset == opentype.glyf.offsets[glyphId + 1]:
+ # Empty glyph
+ return
+
+ var i = glyphOffset.int
+ opentype.buf.eofCheck(i + 10)
+
+ let
+ numberOfContours = opentype.buf.readInt16(i + 0).swap().int
+ # xMin = opentype.buf.readInt16(i + 2).swap()
+ # yMin = opentype.buf.readInt16(i + 4).swap()
+ # xMax = opentype.buf.readInt16(i + 6).swap()
+ # yMax = opentype.buf.readInt16(i + 8).swap()
+
+ i += 10
+
+ if numberOfContours < 0:
+ opentype.parseCompositeGlyph(i)
+ else:
+ parseGlyphPath(opentype.buf, i, numberOfContours)
+
+proc parseGlyph(opentype: OpenType, rune: Rune): Path {.inline.} =
+ opentype.parseGlyph(opentype.getGlyphId(rune))
+
+proc getGlyphPath*(opentype: OpenType, rune: Rune): Path =
+ if rune notin opentype.glyphPaths:
+ opentype.glyphPaths[rune] = opentype.parseGlyph(rune)
+ opentype.glyphPaths[rune].transform(scale(vec2(1, -1)))
+ opentype.glyphPaths[rune]
+
+proc getLeftSideBearing*(opentype: OpenType, rune: Rune): float32 =
+ let glyphId = opentype.getGlyphId(rune).int
+ if glyphId < opentype.hmtx.hMetrics.len:
+ result = opentype.hmtx.hMetrics[glyphId].leftSideBearing.float32
+ else:
+ let index = glyphId - opentype.hmtx.hMetrics.len
+ if index > 0 and index < opentype.hmtx.leftSideBearings.len:
+ result = opentype.hmtx.leftSideBearings[index].float32
+
+proc getAdvance*(opentype: OpenType, rune: Rune): float32 =
+ let glyphId = opentype.getGlyphId(rune).int
+ if glyphId < opentype.hmtx.hMetrics.len:
+ result = opentype.hmtx.hMetrics[glyphId].advanceWidth.float32
+ else:
+ result = opentype.hmtx.hMetrics[^1].advanceWidth.float32
+
+proc getKerningAdjustment*(opentype: OpenType, left, right: Rune): float32 =
+ if left notin opentype.cmap.runeToGlyphId or
+ right notin opentype.cmap.runeToGlyphId:
+ return
+
+ let
+ leftGlyphId = opentype.cmap.runeToGlyphId[left]
+ rightGlyphId = opentype.cmap.runeToGlyphId[right]
+ glyphPair = (leftGlyphId, rightGlyphId)
+
+ if opentype.gpos != nil:
+ for pairPos in opentype.gpos.lookupList.pairPosTables:
+ if leftGlyphId notin pairPos.coverage.coveredGlyphs:
+ continue
+
+ case pairPos.posFormat:
+ of 1:
+ if glyphPair in pairPos.glyphPairAdjustments:
+ result = pairPos.glyphPairAdjustments[glyphPair].float32
+ break
+ of 2:
+ var leftClass, rightClass: uint16
+ if leftGlyphId in pairPos.glyphIdToClass1:
+ leftClass = pairPos.glyphIdToClass1[leftGlyphId]
+ if rightGlyphId in pairPos.glyphIdToClass2:
+ rightClass = pairPos.glyphIdToClass2[rightGlyphId]
+
+ let classPair = (leftClass, rightClass)
+ if classPair in pairPos.classPairAdjustments:
+ result = pairPos.classPairAdjustments[classPair].float32
+ break
+ else:
+ discard
+
+ elif opentype.kern != nil:
+ if glyphPair in opentype.kern.kerningPairs:
+ result = opentype.kern.kerningPairs[glyphPair]
+
+proc parseOpenType*(buf: string): OpenType =
+ result = OpenType()
+ result.buf = buf
+
+ var i: int
+
+ buf.eofCheck(i + 12)
+
+ result.version = buf.readUint32(i + 0).swap()
+ result.numTables = buf.readUint16(i + 4).swap()
+ result.searchRange = buf.readUint16(i + 6).swap()
+ result.entrySelector = buf.readUint16(i + 8).swap()
+ result.rangeShift = buf.readUint16(i + 10).swap()
+
+ i += 12
+
+ buf.eofCheck(i + result.numTables.int * 16)
+
+ for j in 0 ..< result.numTables.int:
+ var tableRecord: TableRecord
+ tableRecord.tag = buf.readStr(i + 0, 4)
+ tableRecord.checksum = buf.readUint32(i + 4).swap()
+ tableRecord.offset = buf.readUint32(i + 8).swap()
+ tableRecord.length = buf.readUint32(i + 12).swap()
+ result.tableRecords[tableRecord.tag] = tableRecord
+ i += 16
+
+ const requiredTables = [
+ "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "loca", "glyf"
+ ]
+ for table in requiredTables:
+ if table notin result.tableRecords:
+ raise newException(PixieError, "Missing required font table " & table)
+
+ result.cmap = parseCmapTable(buf, result.tableRecords["cmap"].offset.int)
+ result.head = parseHeadTable(buf, result.tableRecords["head"].offset.int)
+ result.hhea = parseHheaTable(buf, result.tableRecords["hhea"].offset.int)
+ result.maxp = parseMaxpTable(buf, result.tableRecords["maxp"].offset.int)
+ result.hmtx = parseHmtxTable(
+ buf, result.tableRecords["hmtx"].offset.int, result.hhea, result.maxp
+ )
+ result.name = parseNameTable(buf, result.tableRecords["name"].offset.int)
+ result.os2 = parseOS2Table(buf, result.tableRecords["OS/2"].offset.int)
+ result.loca = parseLocaTable(
+ buf, result.tableRecords["loca"].offset.int, result.head, result.maxp
+ )
+ result.glyf =
+ parseGlyfTable(buf, result.tableRecords["glyf"].offset.int, result.loca)
+
+ if "kern" in result.tableRecords:
+ result.kern = parseKernTable(buf, result.tableRecords["kern"].offset.int)
+
+ if "GPOS" in result.tableRecords:
+ result.gpos = parseGposTable(buf, result.tableRecords["GPOS"].offset.int)
+
+when defined(release):
+ {.pop.}
diff --git a/src/pixie/fontformats/svgfont.nim b/src/pixie/fontformats/svgfont.nim
new file mode 100644
index 0000000..62a73b8
--- /dev/null
+++ b/src/pixie/fontformats/svgfont.nim
@@ -0,0 +1,106 @@
+import pixie/common, pixie/paths, strutils, tables, unicode, vmath, xmlparser, xmltree
+
+type SvgFont* = ref object
+ unitsPerEm*, ascent*, descent*: float32
+ advances: Table[Rune, float32]
+ glyphPaths: Table[Rune, Path]
+ kerningPairs: Table[(Rune, Rune), float32]
+ missingGlyphAdvance: float32
+ missingGlyphPath: Path
+
+proc getGlyphPath*(svgFont: SvgFont, rune: Rune): Path =
+ if rune in svgFont.glyphPaths:
+ svgFont.glyphPaths[rune]
+ else:
+ svgFont.missingGlyphPath
+
+proc getAdvance*(svgFont: SvgFont, rune: Rune): float32 =
+ if rune in svgFont.advances:
+ svgFont.advances[rune]
+ else:
+ svgFont.missingGlyphAdvance
+
+proc getKerningAdjustment*(svgFont: SvgFont, left, right: Rune): float32 =
+ let pair = (left, right)
+ if pair in svgFont.kerningPairs:
+ result = svgFont.kerningPairs[pair]
+
+proc failInvalid() =
+ raise newException(PixieError, "Invalid SVG font data")
+
+proc parseFloat(node: XmlNode, attr: string): float32 =
+ let value = node.attr(attr)
+ if value.len == 0:
+ raise newException(PixieError, "SVG font missing attr " & attr)
+ try:
+ result = parseFloat(value)
+ except:
+ failInvalid()
+
+proc parseSvgFont*(buf: string): SvgFont =
+ result = SvgFont()
+
+ let
+ root = parseXml(buf)
+ defs = root.child("defs")
+ if defs == nil:
+ failInvalid()
+
+ let font = defs.child("font")
+ if font == nil:
+ failInvalid()
+
+ let defaultAdvance = font.parseFloat("horiz-adv-x")
+
+ for node in font.items:
+ case node.tag:
+ of "font-face":
+ result.unitsPerEm = node.parseFloat("units-per-em")
+ result.ascent = node.parseFloat("ascent")
+ result.descent = node.parseFloat("descent")
+ of "glyph":
+ let
+ name = node.attr("glyph-name")
+ unicode = node.attr("unicode")
+ if unicode.len > 0 or name == "space":
+ var
+ i: int
+ rune: Rune
+ if name == "space":
+ rune = Rune(32)
+ else:
+ fastRuneAt(unicode, i, rune, true)
+ if i == unicode.len:
+ var advance = defaultAdvance
+ if node.attr("horiz-adv-x").len > 0:
+ advance = node.parseFloat("horiz-adv-x")
+ result.advances[rune] = advance
+ result.glyphPaths[rune] = parsePath(node.attr("d"))
+ result.glyphPaths[rune].transform(scale(vec2(1, -1)))
+ else:
+ discard # Multi-rune unicode?
+ of "hkern":
+ # TODO "g" kerning
+ let
+ u1 = node.attr("u1")
+ u2 = node.attr("u2")
+ if u1.len > 0 and u2.len > 0:
+ var
+ i1, i2: int
+ left, right: Rune
+ fastRuneAt(u1, i1, left, true)
+ fastRuneAt(u2, i2, right, true)
+ if i1 == u1.len and i2 == u2.len:
+ let adjustment = -node.parseFloat("k")
+ result.kerningPairs[(left, right)] = adjustment
+ else:
+ discard # Multi-rune unicode?
+ of "missing-glyph":
+ var advance = defaultAdvance
+ if node.attr("horiz-adv-x").len > 0:
+ advance = node.parseFloat("horiz-adv-x")
+ result.missingGlyphAdvance = advance
+ result.missingGlyphPath = parsePath(node.attr("d"))
+ result.missingGlyphPath.transform(scale(vec2(1, -1)))
+ else:
+ discard # Unrecognized font node child
diff --git a/src/pixie/fonts.nim b/src/pixie/fonts.nim
new file mode 100644
index 0000000..64747ed
--- /dev/null
+++ b/src/pixie/fonts.nim
@@ -0,0 +1,402 @@
+import bumpy, chroma, pixie/fontformats/opentype, pixie/fontformats/svgfont,
+ pixie/paints, pixie/paths, unicode, vmath
+
+const
+ AutoLineHeight* = -1.float32 ## Use default line height for the font size
+ LF = Rune(10)
+ SP = Rune(32)
+
+type
+ Typeface* = ref object
+ opentype: OpenType
+ svgFont: SvgFont
+ filePath*: string
+
+ Font* = object
+ typeface*: Typeface
+ size*: float32 ## Font size in pixels.
+ lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height.
+ paint*: Paint
+ textCase*: TextCase
+ noKerningAdjustments*: bool ## Optionally disable kerning pair adjustments
+
+ Span* = ref object
+ text*: string
+ font*: Font
+
+ Arrangement* = ref object
+ spans*: seq[(int, int)]
+ fonts*: seq[Font]
+ runes*: seq[Rune]
+ positions*: seq[Vec2]
+ selectionRects*: seq[Rect]
+
+ HAlignMode* = enum
+ haLeft
+ haCenter
+ haRight
+
+ VAlignMode* = enum
+ vaTop
+ vaMiddle
+ vaBottom
+
+ TextCase* = enum
+ tcNormal
+ tcUpper
+ tcLower
+ tcTitle
+ # tcSmallCaps
+ # tcSmallCapsForced
+
+proc ascent*(typeface: Typeface): float32 {.inline.} =
+ ## The font ascender value in font units.
+ if typeface.opentype != nil:
+ typeface.opentype.hhea.ascender.float32
+ else:
+ typeface.svgFont.ascent
+
+proc descent*(typeface: Typeface): float32 {.inline.} =
+ ## The font descender value in font units.
+ if typeface.opentype != nil:
+ typeface.opentype.hhea.descender.float32
+ else:
+ typeface.svgFont.descent
+
+proc lineGap*(typeface: Typeface): float32 {.inline.} =
+ ## The font line gap value in font units.
+ if typeface.opentype != nil:
+ result = typeface.opentype.hhea.lineGap.float32
+
+proc lineHeight*(typeface: Typeface): float32 {.inline.} =
+ ## The default line height in font units.
+ typeface.ascent - typeface.descent + typeface.lineGap
+
+proc getGlyphPath*(typeface: Typeface, rune: Rune): Path {.inline.} =
+ ## The glyph path for the rune.
+ if rune.uint32 > SP.uint32: # Empty paths for control runes (not tofu)
+ if typeface.opentype != nil:
+ result = typeface.opentype.getGlyphPath(rune)
+ else:
+ result = typeface.svgFont.getGlyphPath(rune)
+
+proc getAdvance*(typeface: Typeface, rune: Rune): float32 {.inline.} =
+ ## The advance for the rune in pixels.
+ if typeface.opentype != nil:
+ typeface.opentype.getAdvance(rune)
+ else:
+ typeface.svgFont.getAdvance(rune)
+
+proc getKerningAdjustment*(
+ typeface: Typeface, left, right: Rune
+): float32 {.inline.} =
+ ## The kerning adjustment for the rune pair, in pixels.
+ if typeface.opentype != nil:
+ typeface.opentype.getKerningAdjustment(left, right)
+ else:
+ typeface.svgfont.getKerningAdjustment(left, right)
+
+proc scale*(font: Font): float32 {.inline.} =
+ ## The scale factor to transform font units into pixels.
+ if font.typeface.opentype != nil:
+ font.size / font.typeface.opentype.head.unitsPerEm.float32
+ else:
+ font.size / font.typeface.svgFont.unitsPerEm
+
+proc defaultLineHeight*(font: Font): float32 {.inline.} =
+ ## The default line height in pixels for the current font size.
+ let fontUnits =
+ font.typeface.ascent - font.typeface.descent + font.typeface.lineGap
+ round(fontUnits * font.scale)
+
+proc newSpan*(text: string, font: Font): Span =
+ result = Span()
+ result.text = text
+ result.font = font
+
+proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
+ case textCase:
+ of tcNormal:
+ discard
+ of tcUpper:
+ for rune in runes.mitems:
+ rune = rune.toUpper()
+ of tcLower:
+ for rune in runes.mitems:
+ rune = rune.toLower()
+ of tcTitle:
+ var prevRune = SP
+ for rune in runes.mitems:
+ if prevRune.isWhiteSpace:
+ rune = rune.toUpper()
+ prevRune = rune
+
+proc canWrap(rune: Rune): bool {.inline.} =
+ rune == Rune(32) or rune.isWhiteSpace()
+
+proc typeset*(
+ spans: seq[Span],
+ bounds = vec2(0, 0),
+ hAlign = haLeft,
+ vAlign = vaTop,
+ wrap = true
+): Arrangement =
+ ## Lays out the character glyphs and returns the arrangement.
+ ## Optional parameters:
+ ## bounds: width determines wrapping and hAlign, height for vAlign
+ ## hAlign: horizontal alignment of the text
+ ## vAlign: vertical alignment of the text
+ ## wrap: enable/disable text wrapping
+
+ result = Arrangement()
+
+ block: # Walk and filter the spans
+ var start: int
+ for span in spans:
+ var
+ i = 0
+ rune: Rune
+ runes: seq[Rune]
+ while i < span.text.len:
+ fastRuneAt(span.text, i, rune, true)
+ # Ignore control runes (0 - 31) except LF for now
+ if rune.uint32 >= SP.uint32 or rune.uint32 == LF.uint32:
+ runes.add(rune)
+
+ if runes.len > 0:
+ runes.convertTextCase(span.font.textCase)
+ result.runes.add(runes)
+ result.spans.add((start, start + runes.len - 1))
+ result.fonts.add(span.font)
+ start += runes.len
+
+ if result.runes.len == 0:
+ return
+
+ result.positions.setLen(result.runes.len)
+ result.selectionRects.setLen(result.runes.len)
+
+ var lines = @[(0, 0)] # (start, stop)
+
+ block: # Arrange the glyphs horizontally first (handling line breaks)
+ proc advance(font: Font, runes: seq[Rune], i: int): float32 {.inline.} =
+ if not font.noKerningAdjustments and i + 1 < runes.len:
+ result += font.typeface.getKerningAdjustment(runes[i], runes[i + 1])
+ result += font.typeface.getAdvance(runes[i])
+ result *= font.scale
+
+ var
+ at: Vec2
+ prevCanWrap: int
+ for spanIndex, (start, stop) in result.spans:
+ let font = result.fonts[spanIndex]
+ for runeIndex in start .. stop:
+ let rune = result.runes[runeIndex]
+ if rune == LF:
+ let advance = font.typeface.getAdvance(SP) * font.scale
+ result.positions[runeIndex] = at
+ result.selectionRects[runeIndex] = rect(at.x, at.y, advance, 0)
+ at.x = 0
+ at.y += 1
+ prevCanWrap = 0
+ lines[^1][1] = runeIndex
+ # Start a new line if we are not at the end
+ if runeIndex + 1 < result.runes.len:
+ lines.add((runeIndex + 1, 0))
+ else:
+ let advance = advance(font, result.runes, runeIndex)
+ if wrap and rune != SP and bounds.x > 0 and at.x + advance > bounds.x:
+ # Wrap to new line
+ at.x = 0
+ at.y += 1
+
+ var lineStart = runeIndex
+
+ # Go back and wrap glyphs after the wrap index down to the next line
+ if prevCanWrap > 0 and prevCanWrap != runeIndex:
+ for i in prevCanWrap + 1 ..< runeIndex:
+ result.positions[i] = at
+ result.selectionRects[i].xy = vec2(at.x, at.y)
+ at.x += advance(font, result.runes, i)
+ dec lineStart
+
+ lines[^1][1] = lineStart - 1
+ lines.add((lineStart, 0))
+
+ if rune.canWrap():
+ prevCanWrap = runeIndex
+
+ result.positions[runeIndex] = at
+ result.selectionRects[runeIndex] = rect(at.x, at.y, advance, 0)
+ at.x += advance
+
+ lines[^1][1] = result.runes.len - 1
+
+ if hAlign != haLeft:
+ # Since horizontal alignment adjustments are different for each line,
+ # find the start and stop of each line of text.
+ for (start, stop) in lines:
+ var furthestX: float32
+ for i in countdown(stop, start):
+ if result.runes[i] != SP and result.runes[i] != LF:
+ furthestX = result.selectionRects[i].x + result.selectionRects[i].w
+ break
+
+ var xAdjustment: float32
+ case hAlign:
+ of haLeft:
+ discard
+ of haCenter:
+ xAdjustment = (bounds.x - furthestX) / 2
+ of haRight:
+ xAdjustment = bounds.x - furthestX
+
+ if xAdjustment != 0:
+ for i in start .. stop:
+ result.positions[i].x += xAdjustment
+ result.selectionRects[i].x += xAdjustment
+
+ block: # Nudge selection rects to pixel grid
+ var at = result.selectionRects[0]
+ at.x = round(at.x)
+ for rect in result.selectionRects.mitems:
+ if rect.y == at.y:
+ rect.x = at.x
+ rect.w = round(rect.w)
+ at.x = rect.x + rect.w
+ else:
+ rect.w = round(rect.w)
+ at.x = rect.w
+ at.y = rect.y
+
+ block: # Arrange the lines vertically
+ let initialY = block:
+ var maxInitialY: float32
+ block outer:
+ for spanIndex, (start, stop) in result.spans:
+ let
+ font = result.fonts[spanIndex]
+ lineHeight =
+ if font.lineheight >= 0:
+ font.lineheight
+ else:
+ font.defaultLineHeight
+ var fontUnitInitialY = font.typeface.ascent + font.typeface.lineGap / 2
+ if lineHeight != font.defaultLineHeight:
+ fontUnitInitialY += (
+ (lineHeight / font.scale) - font.typeface.lineHeight
+ ) / 2
+ maxInitialY = max(maxInitialY, round(fontUnitInitialY * font.scale))
+ for runeIndex in start .. stop:
+ if runeIndex == lines[0][1]:
+ break outer
+ maxInitialY
+
+ var lineHeights = newSeq[float32](lines.len)
+ block: # Compute each line's line height
+ var line: int
+ for spanIndex, (start, stop) in result.spans:
+ let
+ font = result.fonts[spanIndex]
+ fontLineHeight =
+ if font.lineheight >= 0:
+ font.lineheight
+ else:
+ font.defaultLineHeight
+ lineHeights[line] = max(lineHeights[line], fontLineHeight)
+ for runeIndex in start .. stop:
+ if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
+ inc line
+ lineHeights[line] = max(lineHeights[line], fontLineHeight)
+ # Handle when span and line endings coincide
+ if line + 1 < lines.len and stop == lines[line][1]:
+ inc line
+
+ block: # Vertically position the glyphs
+ var
+ line: int
+ baseline = initialY
+ for spanIndex, (start, stop) in result.spans:
+ let
+ font = result.fonts[spanIndex]
+ lineHeight =
+ if font.lineheight >= 0:
+ font.lineheight
+ else:
+ font.defaultLineHeight
+ for runeIndex in start .. stop:
+ if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
+ inc line
+ baseline += lineHeights[line]
+ result.positions[runeIndex].y = baseline
+ result.selectionRects[runeIndex].y =
+ baseline - round(font.typeface.ascent * font.scale)
+ result.selectionRects[runeIndex].h = lineHeight
+
+ if vAlign != vaTop:
+ let
+ finalSelectionRect = result.selectionRects[^1]
+ furthestY = finalSelectionRect.y + finalSelectionRect.h
+
+ var yAdjustment: float32
+ case vAlign:
+ of vaTop:
+ discard
+ of vaMiddle:
+ yAdjustment = round((bounds.y - furthestY) / 2)
+ of vaBottom:
+ yAdjustment = bounds.y - furthestY
+
+ if yAdjustment != 0:
+ for i in 0 ..< result.positions.len:
+ result.positions[i].y += yAdjustment
+ result.selectionRects[i].y += yAdjustment
+
+proc typeset*(
+ font: Font,
+ text: string,
+ bounds = vec2(0, 0),
+ hAlign = haLeft,
+ vAlign = vaTop,
+ wrap = true
+): Arrangement {.inline.} =
+ ## Lays out the character glyphs and returns the arrangement.
+ ## Optional parameters:
+ ## bounds: width determines wrapping and hAlign, height for vAlign
+ ## hAlign: horizontal alignment of the text
+ ## vAlign: vertical alignment of the text
+ ## wrap: enable/disable text wrapping
+ typeset(@[newSpan(text, font)], bounds, hAlign, vAlign, wrap)
+
+proc computeBounds*(arrangement: Arrangement): Vec2 =
+ if arrangement.runes.len > 0:
+ for i in 0 ..< arrangement.runes.len:
+ if arrangement.runes[i] != LF:
+ let rect = arrangement.selectionRects[i]
+ result.x = max(result.x, rect.x + rect.w)
+ let finalRect = arrangement.selectionRects[^1]
+ result.y = finalRect.y + finalRect.h
+
+proc computeBounds*(font: Font, text: string): Vec2 {.inline.} =
+ ## Computes the width and height of the text in pixels.
+ font.typeset(text).computeBounds()
+
+proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} =
+ typeset(spans).computeBounds()
+
+proc parseOtf*(buf: string): Font =
+ result.typeface = Typeface()
+ result.typeface.opentype = parseOpenType(buf)
+ result.size = 12
+ result.lineHeight = AutoLineHeight
+ result.paint = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255))
+
+proc parseTtf*(buf: string): Font =
+ parseOtf(buf)
+
+proc parseSvgFont*(buf: string): Font =
+ result.typeface = Typeface()
+ result.typeface.svgFont = svgfont.parseSvgFont(buf)
+ result.size = 12
+ result.lineHeight = AutoLineHeight
+ result.paint = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255))
diff --git a/src/pixie/paints.nim b/src/pixie/paints.nim
index 36212a9..2449b5b 100644
--- a/src/pixie/paints.nim
+++ b/src/pixie/paints.nim
@@ -1,4 +1,4 @@
-import blends, chroma, images, vmath
+import blends, chroma, common, images, vmath
type
PaintKind* = enum
@@ -9,7 +9,7 @@ type
pkGradientRadial
pkGradientAngular
- Paint* = ref object
+ Paint* = object
## Paint used to fill paths.
case kind*: PaintKind
of pkSolid:
@@ -27,7 +27,7 @@ type
color*: ColorRGBX ## Color of the stop
position*: float32 ## Gradient Stop position 0..1.
-proc toLineSpace(at, to, point: Vec2): float32 =
+proc toLineSpace(at, to, point: Vec2): float32 {.inline.} =
## Convert position on to where it would fall on a line between at and to.
let
d = to - at
@@ -60,25 +60,44 @@ proc gradientPut(image: Image, x, y: int, a: float32, stops: seq[ColorStop]) =
)
image.setRgbaUnsafe(x, y, color.rgba.rgbx())
-proc fillLinearGradient*(
- image: Image,
- at, to: Vec2,
- stops: seq[ColorStop]
-) =
+proc fillGradientLinear*(image: Image, paint: Paint) =
## Fills a linear gradient.
+
+ if paint.kind != pkGradientLinear:
+ raise newException(PixieError, "Paint kind must be " & $pkGradientLinear)
+
+ if paint.gradientHandlePositions.len != 2:
+ raise newException(PixieError, "Linear gradient requires 2 handles")
+
+ if paint.gradientStops.len == 0:
+ raise newException(PixieError, "Gradient must have at least 1 color stop")
+
+ let
+ at = paint.gradientHandlePositions[0]
+ to = paint.gradientHandlePositions[1]
for y in 0 ..< image.height:
for x in 0 ..< image.width:
- let xy = vec2(x.float32, y.float32)
- let a = toLineSpace(at, to, xy)
- image.gradientPut(x, y, a, stops)
+ let
+ xy = vec2(x.float32, y.float32)
+ a = toLineSpace(at, to, xy)
+ image.gradientPut(x, y, a, paint.gradientStops)
-proc fillRadialGradient*(
- image: Image,
- center, edge, skew: Vec2,
- stops: seq[ColorStop]
-) =
+proc fillGradientRadial*(image: Image, paint: Paint) =
## Fills a radial gradient.
+
+ if paint.kind != pkGradientRadial:
+ raise newException(PixieError, "Paint kind must be " & $pkGradientRadial)
+
+ if paint.gradientHandlePositions.len != 3:
+ raise newException(PixieError, "Radial gradient requires 3 handles")
+
+ if paint.gradientStops.len == 0:
+ raise newException(PixieError, "Gradient must have at least 1 color stop")
+
let
+ center = paint.gradientHandlePositions[0]
+ edge = paint.gradientHandlePositions[1]
+ skew = paint.gradientHandlePositions[2]
distanceX = dist(center, edge)
distanceY = dist(center, skew)
gradientAngle = normalize(center - edge).angle().fixAngle()
@@ -89,23 +108,32 @@ proc fillRadialGradient*(
).inverse()
for y in 0 ..< image.height:
for x in 0 ..< image.width:
- let xy = vec2(x.float32, y.float32)
- let b = (mat * xy).length()
- image.gradientPut(x, y, b, stops)
+ let
+ xy = vec2(x.float32, y.float32)
+ b = (mat * xy).length()
+ image.gradientPut(x, y, b, paint.gradientStops)
+
+proc fillGradientAngular*(image: Image, paint: Paint) =
+ ## Fills an angular gradient.
+
+ if paint.kind != pkGradientAngular:
+ raise newException(PixieError, "Paint kind must be " & $pkGradientAngular)
+
+ if paint.gradientHandlePositions.len != 3:
+ raise newException(PixieError, "Angular gradient requires 2 handles")
+
+ if paint.gradientStops.len == 0:
+ raise newException(PixieError, "Gradient must have at least 1 color stop")
-proc fillAngularGradient*(
- image: Image,
- center, edge, skew: Vec2,
- stops: seq[ColorStop]
-) =
- ## Angular gradient.
- # TODO: make edge between start and end anti-aliased.
let
- gradientAngle = normalize(edge - center).angle().fixAngle()
+ center = paint.gradientHandlePositions[0]
+ edge = paint.gradientHandlePositions[1]
+ # TODO: make edge between start and end anti-aliased.
+ let gradientAngle = normalize(edge - center).angle().fixAngle()
for y in 0 ..< image.height:
for x in 0 ..< image.width:
let
xy = vec2(x.float32, y.float32)
angle = normalize(xy - center).angle()
a = (angle + gradientAngle + PI/2).fixAngle() / 2 / PI + 0.5
- image.gradientPut(x, y, a, stops)
+ image.gradientPut(x, y, a, paint.gradientStops)
diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim
index 49fb2ca..618a2c0 100644
--- a/src/pixie/paths.nim
+++ b/src/pixie/paths.nim
@@ -48,6 +48,11 @@ proc maxScale(m: Mat3): float32 =
vec2(m[1, 0], m[1, 1]).length
)
+proc isRelative(kind: PathCommandKind): bool =
+ kind in {
+ RMove, RLine, TQuad, RTQuad, RHLine, RVLine, RCubic, RSCubic, RQuad, RArc
+ }
+
proc parameterCount(kind: PathCommandKind): int =
## Returns number of parameters a path command has.
case kind:
@@ -231,7 +236,17 @@ proc parsePath*(path: string): Path =
proc transform*(path: var Path, mat: Mat3) =
## Apply a matrix transform to a path.
+ if mat == mat3():
+ return
+
+ if path.commands.len > 0 and path.commands[0].kind == RMove:
+ path.commands[0].kind = Move
+
for command in path.commands.mitems:
+ var mat = mat
+ if command.kind.isRelative():
+ mat.pos = vec2(0)
+
case command.kind:
of Close:
discard
@@ -984,7 +999,7 @@ proc partitionSegments(
numPartitions = min(maxPartitions, max(1, segmentCount div 10).uint32)
partitionHeight = (height.uint32 div numPartitions)
- var partitions = newSeq[seq[(Segment, int16)]](numPartitions)
+ result.setLen(numPartitions)
for shape in shapes:
for segment in shape.segments:
if segment.at.y == segment.to.y: # Skip horizontal
@@ -997,17 +1012,15 @@ proc partitionSegments(
winding = -1
if partitionHeight == 0:
- partitions[0].add((segment, winding))
+ result[0].add((segment, winding))
else:
var
atPartition = max(0, segment.at.y).uint32 div partitionHeight
toPartition = max(0, ceil(segment.to.y)).uint32 div partitionHeight
- atPartition = clamp(atPartition, 0, partitions.high.uint32)
- toPartition = clamp(toPartition, 0, partitions.high.uint32)
+ atPartition = clamp(atPartition, 0, result.high.uint32)
+ toPartition = clamp(toPartition, 0, result.high.uint32)
for i in atPartition .. toPartition:
- partitions[i].add((segment, winding))
-
- partitions
+ result[i].add((segment, winding))
template computeCoverages(
coverages: var seq[uint8],
@@ -1415,6 +1428,18 @@ proc parseSomePath(
elif type(path) is seq[seq[Vec2]]:
path
+proc transform(shapes: var seq[seq[Vec2]], transform: Vec2 | Mat3) =
+ when type(transform) is Vec2:
+ if transform != vec2(0, 0):
+ for shape in shapes.mitems:
+ for segment in shape.mitems:
+ segment += transform
+ else:
+ if transform != mat3():
+ for shape in shapes.mitems:
+ for segment in shape.mitems:
+ segment = transform * segment
+
proc fillPath*(
image: Image,
path: SomePath,
@@ -1439,12 +1464,7 @@ proc fillPath*(
else:
let pixelScale = 1.0
var shapes = parseSomePath(path, pixelScale)
- for shape in shapes.mitems:
- for segment in shape.mitems:
- when type(transform) is Vec2:
- segment += transform
- else:
- segment = transform * segment
+ shapes.transform(transform)
image.fillShapes(shapes, color, windingRule, blendMode)
proc fillPath*(
@@ -1467,30 +1487,26 @@ proc fillPath*(
else:
let pixelScale = 1.0
var shapes = parseSomePath(path, pixelScale)
- for shape in shapes.mitems:
- for segment in shape.mitems:
- when type(transform) is Vec2:
- segment += transform
- else:
- segment = transform * segment
+ shapes.transform(transform)
mask.fillShapes(shapes, windingRule)
proc fillPath*(
image: Image,
path: SomePath,
paint: Paint,
- windingRule = wrNonZero,
+ transform: Vec2 | Mat3 = vec2(),
+ windingRule = wrNonZero
) =
## Fills a path.
if paint.kind == pkSolid:
- image.fillPath(path, paint.color)
+ image.fillPath(path, paint.color, transform)
return
let
mask = newMask(image.width, image.height)
fill = newImage(image.width, image.height)
- mask.fillPath(parseSomePath(path), windingRule)
+ mask.fillPath(parseSomePath(path), transform, windingRule)
case paint.kind:
of pkSolid:
@@ -1500,25 +1516,11 @@ proc fillPath*(
of pkImageTiled:
fill.drawTiled(paint.image, paint.imageMat)
of pkGradientLinear:
- fill.fillLinearGradient(
- paint.gradientHandlePositions[0],
- paint.gradientHandlePositions[1],
- paint.gradientStops
- )
+ fill.fillGradientLinear(paint)
of pkGradientRadial:
- fill.fillRadialGradient(
- paint.gradientHandlePositions[0],
- paint.gradientHandlePositions[1],
- paint.gradientHandlePositions[2],
- paint.gradientStops
- )
+ fill.fillGradientRadial(paint)
of pkGradientAngular:
- fill.fillAngularGradient(
- paint.gradientHandlePositions[0],
- paint.gradientHandlePositions[1],
- paint.gradientHandlePositions[2],
- paint.gradientStops
- )
+ fill.fillGradientAngular(paint)
fill.draw(mask)
image.draw(fill, blendMode = paint.blendMode)
@@ -1556,12 +1558,7 @@ proc strokePath*(
var strokeShapes = strokeShapes(
parseSomePath(path, pixelScale), strokeWidth, lineCap, lineJoin
)
- for shape in strokeShapes.mitems:
- for segment in shape.mitems:
- when type(transform) is Vec2:
- segment += transform
- else:
- segment = transform * segment
+ strokeShapes.transform(transform)
image.fillShapes(strokeShapes, color, wrNonZero, blendMode)
proc strokePath*(
@@ -1593,13 +1590,47 @@ proc strokePath*(
var strokeShapes = strokeShapes(
parseSomePath(path, pixelScale), strokeWidth, lineCap, lineJoin
)
- for shape in strokeShapes.mitems:
- for segment in shape.mitems:
- when type(transform) is Vec2:
- segment += transform
- else:
- segment = transform * segment
+ strokeShapes.transform(transform)
mask.fillShapes(strokeShapes, wrNonZero)
+proc strokePath*(
+ image: Image,
+ path: SomePath,
+ paint: Paint,
+ transform: Vec2 | Mat3 = vec2(),
+ strokeWidth = 1.0,
+ lineCap = lcButt,
+ lineJoin = ljMiter
+) =
+ ## Fills a path.
+ if paint.kind == pkSolid:
+ image.strokePath(
+ path, paint.color, transform, strokeWidth, lineCap, lineJoin
+ )
+ return
+
+ let
+ mask = newMask(image.width, image.height)
+ fill = newImage(image.width, image.height)
+
+ mask.strokePath(parseSomePath(path), transform)
+
+ case paint.kind:
+ of pkSolid:
+ discard # Handled above
+ of pkImage:
+ fill.draw(paint.image, paint.imageMat)
+ of pkImageTiled:
+ fill.drawTiled(paint.image, paint.imageMat)
+ of pkGradientLinear:
+ fill.fillGradientLinear(paint)
+ of pkGradientRadial:
+ fill.fillGradientRadial(paint)
+ of pkGradientAngular:
+ fill.fillGradientAngular(paint)
+
+ fill.draw(mask)
+ image.draw(fill, blendMode = paint.blendMode)
+
when defined(release):
{.pop.}
diff --git a/tests/benchmark_fonts.nim b/tests/benchmark_fonts.nim
new file mode 100644
index 0000000..301b3b8
--- /dev/null
+++ b/tests/benchmark_fonts.nim
@@ -0,0 +1,19 @@
+import benchy, pixie
+
+const text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis in quam in nulla bibendum luctus. Integer dui lectus, ultricies commodo enim quis, laoreet lacinia erat. Vivamus ultrices maximus risus, non aliquam quam sagittis quis. Ut nec diam vitae tortor interdum ullamcorper in aliquet velit. Ut sed lobortis mi. Nulla venenatis lectus varius justo lacinia, quis sollicitudin nunc ultrices. Donec a suscipit arcu, id egestas neque. Nullam commodo pharetra est. Nullam gravida nibh eget quam venenatis lacinia. Vestibulum et libero arcu. Sed dignissim enim eros. Nullam eleifend luctus erat sed luctus. Nunc tincidunt, mi nec tincidunt tristique, ex nulla lobortis sem, sit amet finibus purus justo non massa."
+
+var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+font.size = 16
+
+let
+ image = newImage(500, 300)
+ mask = newMask(500, 300)
+
+timeIt "typeset":
+ discard font.typeset(text, bounds = image.wh)
+
+timeIt "rasterize":
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, text, bounds = image.wh)
+ # mask.fill(0)
+ # mask.fillText(font, text, bounds = mask.wh)
diff --git a/tests/common.nim b/tests/common.nim
new file mode 100644
index 0000000..697c081
--- /dev/null
+++ b/tests/common.nim
@@ -0,0 +1,7 @@
+import algorithm, os
+
+proc findAllFonts*(rootPath: string): seq[string] =
+ for fontPath in walkDirRec(rootPath):
+ if splitFile(fontPath).ext in [".ttf", ".otf"]:
+ result.add(fontPath)
+ result.sort()
diff --git a/tests/fonts/Aclonica-Regular_1.ttf b/tests/fonts/Aclonica-Regular_1.ttf
new file mode 100644
index 0000000..bbe191d
Binary files /dev/null and b/tests/fonts/Aclonica-Regular_1.ttf differ
diff --git a/tests/fonts/Changa-Bold.svg b/tests/fonts/Changa-Bold.svg
new file mode 100644
index 0000000..ca4cefa
--- /dev/null
+++ b/tests/fonts/Changa-Bold.svg
@@ -0,0 +1,2420 @@
+
+
+
diff --git a/tests/fonts/DejaVuSans.svg b/tests/fonts/DejaVuSans.svg
new file mode 100644
index 0000000..d703ac0
--- /dev/null
+++ b/tests/fonts/DejaVuSans.svg
@@ -0,0 +1,18479 @@
+
+
+
diff --git a/tests/fonts/IBMPlexSans-Regular.svg b/tests/fonts/IBMPlexSans-Regular.svg
new file mode 100644
index 0000000..5c37f3e
--- /dev/null
+++ b/tests/fonts/IBMPlexSans-Regular.svg
@@ -0,0 +1,22584 @@
+
+
+
diff --git a/tests/fonts/IBMPlexSans-Regular_2.ttf b/tests/fonts/IBMPlexSans-Regular_2.ttf
new file mode 100644
index 0000000..705fdf6
Binary files /dev/null and b/tests/fonts/IBMPlexSans-Regular_2.ttf differ
diff --git a/tests/fonts/Moon-Bold.svg b/tests/fonts/Moon-Bold.svg
new file mode 100644
index 0000000..2b5002b
--- /dev/null
+++ b/tests/fonts/Moon-Bold.svg
@@ -0,0 +1,616 @@
+
+
+
diff --git a/tests/fonts/NotoSans-Regular_4.ttf b/tests/fonts/NotoSans-Regular_4.ttf
new file mode 100644
index 0000000..170c15c
Binary files /dev/null and b/tests/fonts/NotoSans-Regular_4.ttf differ
diff --git a/tests/fonts/Pacifico-Regular_4.ttf b/tests/fonts/Pacifico-Regular_4.ttf
new file mode 100644
index 0000000..27765d0
Binary files /dev/null and b/tests/fonts/Pacifico-Regular_4.ttf differ
diff --git a/tests/fonts/Roboto-Regular_1.ttf b/tests/fonts/Roboto-Regular_1.ttf
new file mode 100644
index 0000000..2c97eea
Binary files /dev/null and b/tests/fonts/Roboto-Regular_1.ttf differ
diff --git a/tests/fonts/Ubuntu-Regular_1.ttf b/tests/fonts/Ubuntu-Regular_1.ttf
new file mode 100644
index 0000000..2001d6e
Binary files /dev/null and b/tests/fonts/Ubuntu-Regular_1.ttf differ
diff --git a/tests/fonts/Ubuntu.svg b/tests/fonts/Ubuntu.svg
new file mode 100644
index 0000000..ddb765e
--- /dev/null
+++ b/tests/fonts/Ubuntu.svg
@@ -0,0 +1,19406 @@
+
+
+
diff --git a/tests/fonts/diffs/alignments.png b/tests/fonts/diffs/alignments.png
new file mode 100644
index 0000000..016cd6e
Binary files /dev/null and b/tests/fonts/diffs/alignments.png differ
diff --git a/tests/fonts/diffs/basic1.png b/tests/fonts/diffs/basic1.png
new file mode 100644
index 0000000..cb654e8
Binary files /dev/null and b/tests/fonts/diffs/basic1.png differ
diff --git a/tests/fonts/diffs/basic2.png b/tests/fonts/diffs/basic2.png
new file mode 100644
index 0000000..b933717
Binary files /dev/null and b/tests/fonts/diffs/basic2.png differ
diff --git a/tests/fonts/diffs/basic3.png b/tests/fonts/diffs/basic3.png
new file mode 100644
index 0000000..c179aaf
Binary files /dev/null and b/tests/fonts/diffs/basic3.png differ
diff --git a/tests/fonts/diffs/basic4.png b/tests/fonts/diffs/basic4.png
new file mode 100644
index 0000000..8058ed1
Binary files /dev/null and b/tests/fonts/diffs/basic4.png differ
diff --git a/tests/fonts/diffs/basic5.png b/tests/fonts/diffs/basic5.png
new file mode 100644
index 0000000..3dcbe4e
Binary files /dev/null and b/tests/fonts/diffs/basic5.png differ
diff --git a/tests/fonts/diffs/basic6.png b/tests/fonts/diffs/basic6.png
new file mode 100644
index 0000000..8abcfb7
Binary files /dev/null and b/tests/fonts/diffs/basic6.png differ
diff --git a/tests/fonts/diffs/basic7.png b/tests/fonts/diffs/basic7.png
new file mode 100644
index 0000000..7cae15b
Binary files /dev/null and b/tests/fonts/diffs/basic7.png differ
diff --git a/tests/fonts/diffs/basic8.png b/tests/fonts/diffs/basic8.png
new file mode 100644
index 0000000..1bbbae0
Binary files /dev/null and b/tests/fonts/diffs/basic8.png differ
diff --git a/tests/fonts/diffs/basic9.png b/tests/fonts/diffs/basic9.png
new file mode 100644
index 0000000..1cac49a
Binary files /dev/null and b/tests/fonts/diffs/basic9.png differ
diff --git a/tests/fonts/diffs/huge1.png b/tests/fonts/diffs/huge1.png
new file mode 100644
index 0000000..4a543a2
Binary files /dev/null and b/tests/fonts/diffs/huge1.png differ
diff --git a/tests/fonts/diffs/huge1_nokern.png b/tests/fonts/diffs/huge1_nokern.png
new file mode 100644
index 0000000..6bfeb09
Binary files /dev/null and b/tests/fonts/diffs/huge1_nokern.png differ
diff --git a/tests/fonts/diffs/huge2.png b/tests/fonts/diffs/huge2.png
new file mode 100644
index 0000000..8bcfbe9
Binary files /dev/null and b/tests/fonts/diffs/huge2.png differ
diff --git a/tests/fonts/diffs/huge2_nokern.png b/tests/fonts/diffs/huge2_nokern.png
new file mode 100644
index 0000000..a1d635e
Binary files /dev/null and b/tests/fonts/diffs/huge2_nokern.png differ
diff --git a/tests/fonts/diffs/huge3.png b/tests/fonts/diffs/huge3.png
new file mode 100644
index 0000000..0b05db3
Binary files /dev/null and b/tests/fonts/diffs/huge3.png differ
diff --git a/tests/fonts/diffs/huge3_nokern.png b/tests/fonts/diffs/huge3_nokern.png
new file mode 100644
index 0000000..f22cb70
Binary files /dev/null and b/tests/fonts/diffs/huge3_nokern.png differ
diff --git a/tests/fonts/diffs/lines1.png b/tests/fonts/diffs/lines1.png
new file mode 100644
index 0000000..6adf135
Binary files /dev/null and b/tests/fonts/diffs/lines1.png differ
diff --git a/tests/fonts/diffs/lines2.png b/tests/fonts/diffs/lines2.png
new file mode 100644
index 0000000..bc3a8f3
Binary files /dev/null and b/tests/fonts/diffs/lines2.png differ
diff --git a/tests/fonts/diffs/pairs1.png b/tests/fonts/diffs/pairs1.png
new file mode 100644
index 0000000..3040a43
Binary files /dev/null and b/tests/fonts/diffs/pairs1.png differ
diff --git a/tests/fonts/diffs/pairs2.png b/tests/fonts/diffs/pairs2.png
new file mode 100644
index 0000000..9c0df3e
Binary files /dev/null and b/tests/fonts/diffs/pairs2.png differ
diff --git a/tests/fonts/diffs/pairs3.png b/tests/fonts/diffs/pairs3.png
new file mode 100644
index 0000000..8d1eed3
Binary files /dev/null and b/tests/fonts/diffs/pairs3.png differ
diff --git a/tests/fonts/diffs/paragraph1.png b/tests/fonts/diffs/paragraph1.png
new file mode 100644
index 0000000..d774c46
Binary files /dev/null and b/tests/fonts/diffs/paragraph1.png differ
diff --git a/tests/fonts/diffs/paragraph1_2.png b/tests/fonts/diffs/paragraph1_2.png
new file mode 100644
index 0000000..323d9a5
Binary files /dev/null and b/tests/fonts/diffs/paragraph1_2.png differ
diff --git a/tests/fonts/diffs/paragraph1_3.png b/tests/fonts/diffs/paragraph1_3.png
new file mode 100644
index 0000000..30bdd17
Binary files /dev/null and b/tests/fonts/diffs/paragraph1_3.png differ
diff --git a/tests/fonts/diffs/paragraph1_nokern.png b/tests/fonts/diffs/paragraph1_nokern.png
new file mode 100644
index 0000000..e5e4952
Binary files /dev/null and b/tests/fonts/diffs/paragraph1_nokern.png differ
diff --git a/tests/fonts/diffs/paragraph1_nokern_2.png b/tests/fonts/diffs/paragraph1_nokern_2.png
new file mode 100644
index 0000000..dbc6e04
Binary files /dev/null and b/tests/fonts/diffs/paragraph1_nokern_2.png differ
diff --git a/tests/fonts/diffs/paragraph1_nokern_3.png b/tests/fonts/diffs/paragraph1_nokern_3.png
new file mode 100644
index 0000000..96a410d
Binary files /dev/null and b/tests/fonts/diffs/paragraph1_nokern_3.png differ
diff --git a/tests/fonts/diffs/paragraph2.png b/tests/fonts/diffs/paragraph2.png
new file mode 100644
index 0000000..40d8b94
Binary files /dev/null and b/tests/fonts/diffs/paragraph2.png differ
diff --git a/tests/fonts/diffs/paragraph2_2.png b/tests/fonts/diffs/paragraph2_2.png
new file mode 100644
index 0000000..a71d81d
Binary files /dev/null and b/tests/fonts/diffs/paragraph2_2.png differ
diff --git a/tests/fonts/diffs/paragraph2_3.png b/tests/fonts/diffs/paragraph2_3.png
new file mode 100644
index 0000000..6e9f6e9
Binary files /dev/null and b/tests/fonts/diffs/paragraph2_3.png differ
diff --git a/tests/fonts/diffs/paragraph2_nokern.png b/tests/fonts/diffs/paragraph2_nokern.png
new file mode 100644
index 0000000..11da7fb
Binary files /dev/null and b/tests/fonts/diffs/paragraph2_nokern.png differ
diff --git a/tests/fonts/diffs/paragraph2_nokern_2.png b/tests/fonts/diffs/paragraph2_nokern_2.png
new file mode 100644
index 0000000..d95bc93
Binary files /dev/null and b/tests/fonts/diffs/paragraph2_nokern_2.png differ
diff --git a/tests/fonts/diffs/paragraph2_nokern_3.png b/tests/fonts/diffs/paragraph2_nokern_3.png
new file mode 100644
index 0000000..5c7d38b
Binary files /dev/null and b/tests/fonts/diffs/paragraph2_nokern_3.png differ
diff --git a/tests/fonts/diffs/paragraph3.png b/tests/fonts/diffs/paragraph3.png
new file mode 100644
index 0000000..bc51484
Binary files /dev/null and b/tests/fonts/diffs/paragraph3.png differ
diff --git a/tests/fonts/diffs/paragraph3_2.png b/tests/fonts/diffs/paragraph3_2.png
new file mode 100644
index 0000000..8100141
Binary files /dev/null and b/tests/fonts/diffs/paragraph3_2.png differ
diff --git a/tests/fonts/diffs/paragraph3_3.png b/tests/fonts/diffs/paragraph3_3.png
new file mode 100644
index 0000000..c9c2e7b
Binary files /dev/null and b/tests/fonts/diffs/paragraph3_3.png differ
diff --git a/tests/fonts/diffs/paragraph3_nokern.png b/tests/fonts/diffs/paragraph3_nokern.png
new file mode 100644
index 0000000..25c02cb
Binary files /dev/null and b/tests/fonts/diffs/paragraph3_nokern.png differ
diff --git a/tests/fonts/diffs/paragraph3_nokern_2.png b/tests/fonts/diffs/paragraph3_nokern_2.png
new file mode 100644
index 0000000..4c6b49a
Binary files /dev/null and b/tests/fonts/diffs/paragraph3_nokern_2.png differ
diff --git a/tests/fonts/diffs/paragraph3_nokern_3.png b/tests/fonts/diffs/paragraph3_nokern_3.png
new file mode 100644
index 0000000..f748d82
Binary files /dev/null and b/tests/fonts/diffs/paragraph3_nokern_3.png differ
diff --git a/tests/fonts/diffs/paragraph4.png b/tests/fonts/diffs/paragraph4.png
new file mode 100644
index 0000000..8696251
Binary files /dev/null and b/tests/fonts/diffs/paragraph4.png differ
diff --git a/tests/fonts/diffs/paragraph4_2.png b/tests/fonts/diffs/paragraph4_2.png
new file mode 100644
index 0000000..fde9201
Binary files /dev/null and b/tests/fonts/diffs/paragraph4_2.png differ
diff --git a/tests/fonts/diffs/paragraph4_3.png b/tests/fonts/diffs/paragraph4_3.png
new file mode 100644
index 0000000..2ecbcd1
Binary files /dev/null and b/tests/fonts/diffs/paragraph4_3.png differ
diff --git a/tests/fonts/diffs/paragraph4_nokern.png b/tests/fonts/diffs/paragraph4_nokern.png
new file mode 100644
index 0000000..bfd42d2
Binary files /dev/null and b/tests/fonts/diffs/paragraph4_nokern.png differ
diff --git a/tests/fonts/diffs/paragraph4_nokern_2.png b/tests/fonts/diffs/paragraph4_nokern_2.png
new file mode 100644
index 0000000..51cf884
Binary files /dev/null and b/tests/fonts/diffs/paragraph4_nokern_2.png differ
diff --git a/tests/fonts/diffs/paragraph4_nokern_3.png b/tests/fonts/diffs/paragraph4_nokern_3.png
new file mode 100644
index 0000000..e2cd98e
Binary files /dev/null and b/tests/fonts/diffs/paragraph4_nokern_3.png differ
diff --git a/tests/fonts/diffs/paragraph5.png b/tests/fonts/diffs/paragraph5.png
new file mode 100644
index 0000000..6c54306
Binary files /dev/null and b/tests/fonts/diffs/paragraph5.png differ
diff --git a/tests/fonts/diffs/paragraph5_2.png b/tests/fonts/diffs/paragraph5_2.png
new file mode 100644
index 0000000..f84f601
Binary files /dev/null and b/tests/fonts/diffs/paragraph5_2.png differ
diff --git a/tests/fonts/diffs/paragraph5_3.png b/tests/fonts/diffs/paragraph5_3.png
new file mode 100644
index 0000000..af155ee
Binary files /dev/null and b/tests/fonts/diffs/paragraph5_3.png differ
diff --git a/tests/fonts/diffs/paragraph5_nokern.png b/tests/fonts/diffs/paragraph5_nokern.png
new file mode 100644
index 0000000..479da65
Binary files /dev/null and b/tests/fonts/diffs/paragraph5_nokern.png differ
diff --git a/tests/fonts/diffs/paragraph5_nokern_2.png b/tests/fonts/diffs/paragraph5_nokern_2.png
new file mode 100644
index 0000000..7cfeff7
Binary files /dev/null and b/tests/fonts/diffs/paragraph5_nokern_2.png differ
diff --git a/tests/fonts/diffs/paragraph5_nokern_3.png b/tests/fonts/diffs/paragraph5_nokern_3.png
new file mode 100644
index 0000000..5540d68
Binary files /dev/null and b/tests/fonts/diffs/paragraph5_nokern_3.png differ
diff --git a/tests/fonts/diffs/selection_rects1.png b/tests/fonts/diffs/selection_rects1.png
new file mode 100644
index 0000000..bffddd6
Binary files /dev/null and b/tests/fonts/diffs/selection_rects1.png differ
diff --git a/tests/fonts/diffs/selection_rects2.png b/tests/fonts/diffs/selection_rects2.png
new file mode 100644
index 0000000..28aad88
Binary files /dev/null and b/tests/fonts/diffs/selection_rects2.png differ
diff --git a/tests/fonts/diffs/selection_rects3.png b/tests/fonts/diffs/selection_rects3.png
new file mode 100644
index 0000000..8349b78
Binary files /dev/null and b/tests/fonts/diffs/selection_rects3.png differ
diff --git a/tests/fonts/diffs/spans1.png b/tests/fonts/diffs/spans1.png
new file mode 100644
index 0000000..ab20d31
Binary files /dev/null and b/tests/fonts/diffs/spans1.png differ
diff --git a/tests/fonts/diffs/spans2.png b/tests/fonts/diffs/spans2.png
new file mode 100644
index 0000000..87ef007
Binary files /dev/null and b/tests/fonts/diffs/spans2.png differ
diff --git a/tests/fonts/diffs/spans4.png b/tests/fonts/diffs/spans4.png
new file mode 100644
index 0000000..ea0cd1f
Binary files /dev/null and b/tests/fonts/diffs/spans4.png differ
diff --git a/tests/fonts/diffs/spans5.png b/tests/fonts/diffs/spans5.png
new file mode 100644
index 0000000..754ece4
Binary files /dev/null and b/tests/fonts/diffs/spans5.png differ
diff --git a/tests/fonts/image_fill.png b/tests/fonts/image_fill.png
new file mode 100644
index 0000000..74ca38d
Binary files /dev/null and b/tests/fonts/image_fill.png differ
diff --git a/tests/fonts/image_paint_fill.png b/tests/fonts/image_paint_fill.png
new file mode 100644
index 0000000..76693da
Binary files /dev/null and b/tests/fonts/image_paint_fill.png differ
diff --git a/tests/fonts/image_stroke.png b/tests/fonts/image_stroke.png
new file mode 100644
index 0000000..ee6ea66
Binary files /dev/null and b/tests/fonts/image_stroke.png differ
diff --git a/tests/fonts/mask_fill.png b/tests/fonts/mask_fill.png
new file mode 100644
index 0000000..73183c4
Binary files /dev/null and b/tests/fonts/mask_fill.png differ
diff --git a/tests/fonts/mask_stroke.png b/tests/fonts/mask_stroke.png
new file mode 100644
index 0000000..f3d82d5
Binary files /dev/null and b/tests/fonts/mask_stroke.png differ
diff --git a/tests/fonts/masters/alignments.png b/tests/fonts/masters/alignments.png
new file mode 100644
index 0000000..9e267e4
Binary files /dev/null and b/tests/fonts/masters/alignments.png differ
diff --git a/tests/fonts/masters/basic1.png b/tests/fonts/masters/basic1.png
new file mode 100644
index 0000000..b5a34a7
Binary files /dev/null and b/tests/fonts/masters/basic1.png differ
diff --git a/tests/fonts/masters/basic2.png b/tests/fonts/masters/basic2.png
new file mode 100644
index 0000000..2510d23
Binary files /dev/null and b/tests/fonts/masters/basic2.png differ
diff --git a/tests/fonts/masters/basic3.png b/tests/fonts/masters/basic3.png
new file mode 100644
index 0000000..db8492d
Binary files /dev/null and b/tests/fonts/masters/basic3.png differ
diff --git a/tests/fonts/masters/basic4.png b/tests/fonts/masters/basic4.png
new file mode 100644
index 0000000..c1111f7
Binary files /dev/null and b/tests/fonts/masters/basic4.png differ
diff --git a/tests/fonts/masters/basic5.png b/tests/fonts/masters/basic5.png
new file mode 100644
index 0000000..0b3a14a
Binary files /dev/null and b/tests/fonts/masters/basic5.png differ
diff --git a/tests/fonts/masters/basic6.png b/tests/fonts/masters/basic6.png
new file mode 100644
index 0000000..9337c98
Binary files /dev/null and b/tests/fonts/masters/basic6.png differ
diff --git a/tests/fonts/masters/basic7.png b/tests/fonts/masters/basic7.png
new file mode 100644
index 0000000..c652d44
Binary files /dev/null and b/tests/fonts/masters/basic7.png differ
diff --git a/tests/fonts/masters/basic8.png b/tests/fonts/masters/basic8.png
new file mode 100644
index 0000000..3823985
Binary files /dev/null and b/tests/fonts/masters/basic8.png differ
diff --git a/tests/fonts/masters/basic9.png b/tests/fonts/masters/basic9.png
new file mode 100644
index 0000000..eb8b15b
Binary files /dev/null and b/tests/fonts/masters/basic9.png differ
diff --git a/tests/fonts/masters/huge1.png b/tests/fonts/masters/huge1.png
new file mode 100644
index 0000000..fb83009
Binary files /dev/null and b/tests/fonts/masters/huge1.png differ
diff --git a/tests/fonts/masters/huge1_nokern.png b/tests/fonts/masters/huge1_nokern.png
new file mode 100644
index 0000000..a6200f5
Binary files /dev/null and b/tests/fonts/masters/huge1_nokern.png differ
diff --git a/tests/fonts/masters/huge2.png b/tests/fonts/masters/huge2.png
new file mode 100644
index 0000000..5f5ae91
Binary files /dev/null and b/tests/fonts/masters/huge2.png differ
diff --git a/tests/fonts/masters/huge2_nokern.png b/tests/fonts/masters/huge2_nokern.png
new file mode 100644
index 0000000..aa15e3b
Binary files /dev/null and b/tests/fonts/masters/huge2_nokern.png differ
diff --git a/tests/fonts/masters/huge3.png b/tests/fonts/masters/huge3.png
new file mode 100644
index 0000000..a646cae
Binary files /dev/null and b/tests/fonts/masters/huge3.png differ
diff --git a/tests/fonts/masters/huge3_nokern.png b/tests/fonts/masters/huge3_nokern.png
new file mode 100644
index 0000000..3eb6d2a
Binary files /dev/null and b/tests/fonts/masters/huge3_nokern.png differ
diff --git a/tests/fonts/masters/lines1.png b/tests/fonts/masters/lines1.png
new file mode 100644
index 0000000..1d1ac0d
Binary files /dev/null and b/tests/fonts/masters/lines1.png differ
diff --git a/tests/fonts/masters/lines2.png b/tests/fonts/masters/lines2.png
new file mode 100644
index 0000000..0b1308c
Binary files /dev/null and b/tests/fonts/masters/lines2.png differ
diff --git a/tests/fonts/masters/pairs1.png b/tests/fonts/masters/pairs1.png
new file mode 100644
index 0000000..c8bcde9
Binary files /dev/null and b/tests/fonts/masters/pairs1.png differ
diff --git a/tests/fonts/masters/pairs2.png b/tests/fonts/masters/pairs2.png
new file mode 100644
index 0000000..71bc2e5
Binary files /dev/null and b/tests/fonts/masters/pairs2.png differ
diff --git a/tests/fonts/masters/pairs3.png b/tests/fonts/masters/pairs3.png
new file mode 100644
index 0000000..680ba3f
Binary files /dev/null and b/tests/fonts/masters/pairs3.png differ
diff --git a/tests/fonts/masters/paragraph1.png b/tests/fonts/masters/paragraph1.png
new file mode 100644
index 0000000..30db82d
Binary files /dev/null and b/tests/fonts/masters/paragraph1.png differ
diff --git a/tests/fonts/masters/paragraph1_2.png b/tests/fonts/masters/paragraph1_2.png
new file mode 100644
index 0000000..2a789dc
Binary files /dev/null and b/tests/fonts/masters/paragraph1_2.png differ
diff --git a/tests/fonts/masters/paragraph1_3.png b/tests/fonts/masters/paragraph1_3.png
new file mode 100644
index 0000000..7cecdcd
Binary files /dev/null and b/tests/fonts/masters/paragraph1_3.png differ
diff --git a/tests/fonts/masters/paragraph1_nokern.png b/tests/fonts/masters/paragraph1_nokern.png
new file mode 100644
index 0000000..472f5ec
Binary files /dev/null and b/tests/fonts/masters/paragraph1_nokern.png differ
diff --git a/tests/fonts/masters/paragraph1_nokern_2.png b/tests/fonts/masters/paragraph1_nokern_2.png
new file mode 100644
index 0000000..bf782ab
Binary files /dev/null and b/tests/fonts/masters/paragraph1_nokern_2.png differ
diff --git a/tests/fonts/masters/paragraph1_nokern_3.png b/tests/fonts/masters/paragraph1_nokern_3.png
new file mode 100644
index 0000000..d9f5d3a
Binary files /dev/null and b/tests/fonts/masters/paragraph1_nokern_3.png differ
diff --git a/tests/fonts/masters/paragraph2.png b/tests/fonts/masters/paragraph2.png
new file mode 100644
index 0000000..59d9583
Binary files /dev/null and b/tests/fonts/masters/paragraph2.png differ
diff --git a/tests/fonts/masters/paragraph2_2.png b/tests/fonts/masters/paragraph2_2.png
new file mode 100644
index 0000000..11e0f5c
Binary files /dev/null and b/tests/fonts/masters/paragraph2_2.png differ
diff --git a/tests/fonts/masters/paragraph2_3.png b/tests/fonts/masters/paragraph2_3.png
new file mode 100644
index 0000000..92de359
Binary files /dev/null and b/tests/fonts/masters/paragraph2_3.png differ
diff --git a/tests/fonts/masters/paragraph2_nokern.png b/tests/fonts/masters/paragraph2_nokern.png
new file mode 100644
index 0000000..1f5583e
Binary files /dev/null and b/tests/fonts/masters/paragraph2_nokern.png differ
diff --git a/tests/fonts/masters/paragraph2_nokern_2.png b/tests/fonts/masters/paragraph2_nokern_2.png
new file mode 100644
index 0000000..29fcf52
Binary files /dev/null and b/tests/fonts/masters/paragraph2_nokern_2.png differ
diff --git a/tests/fonts/masters/paragraph2_nokern_3.png b/tests/fonts/masters/paragraph2_nokern_3.png
new file mode 100644
index 0000000..aef7850
Binary files /dev/null and b/tests/fonts/masters/paragraph2_nokern_3.png differ
diff --git a/tests/fonts/masters/paragraph3.png b/tests/fonts/masters/paragraph3.png
new file mode 100644
index 0000000..12e8996
Binary files /dev/null and b/tests/fonts/masters/paragraph3.png differ
diff --git a/tests/fonts/masters/paragraph3_2.png b/tests/fonts/masters/paragraph3_2.png
new file mode 100644
index 0000000..6b1dcd1
Binary files /dev/null and b/tests/fonts/masters/paragraph3_2.png differ
diff --git a/tests/fonts/masters/paragraph3_3.png b/tests/fonts/masters/paragraph3_3.png
new file mode 100644
index 0000000..847962c
Binary files /dev/null and b/tests/fonts/masters/paragraph3_3.png differ
diff --git a/tests/fonts/masters/paragraph3_nokern.png b/tests/fonts/masters/paragraph3_nokern.png
new file mode 100644
index 0000000..daabb7c
Binary files /dev/null and b/tests/fonts/masters/paragraph3_nokern.png differ
diff --git a/tests/fonts/masters/paragraph3_nokern_2.png b/tests/fonts/masters/paragraph3_nokern_2.png
new file mode 100644
index 0000000..740df9f
Binary files /dev/null and b/tests/fonts/masters/paragraph3_nokern_2.png differ
diff --git a/tests/fonts/masters/paragraph3_nokern_3.png b/tests/fonts/masters/paragraph3_nokern_3.png
new file mode 100644
index 0000000..ab5ee84
Binary files /dev/null and b/tests/fonts/masters/paragraph3_nokern_3.png differ
diff --git a/tests/fonts/masters/paragraph4.png b/tests/fonts/masters/paragraph4.png
new file mode 100644
index 0000000..a24f2a6
Binary files /dev/null and b/tests/fonts/masters/paragraph4.png differ
diff --git a/tests/fonts/masters/paragraph4_2.png b/tests/fonts/masters/paragraph4_2.png
new file mode 100644
index 0000000..aae6d57
Binary files /dev/null and b/tests/fonts/masters/paragraph4_2.png differ
diff --git a/tests/fonts/masters/paragraph4_3.png b/tests/fonts/masters/paragraph4_3.png
new file mode 100644
index 0000000..20b0af4
Binary files /dev/null and b/tests/fonts/masters/paragraph4_3.png differ
diff --git a/tests/fonts/masters/paragraph4_nokern.png b/tests/fonts/masters/paragraph4_nokern.png
new file mode 100644
index 0000000..05b41cb
Binary files /dev/null and b/tests/fonts/masters/paragraph4_nokern.png differ
diff --git a/tests/fonts/masters/paragraph4_nokern_2.png b/tests/fonts/masters/paragraph4_nokern_2.png
new file mode 100644
index 0000000..e932025
Binary files /dev/null and b/tests/fonts/masters/paragraph4_nokern_2.png differ
diff --git a/tests/fonts/masters/paragraph4_nokern_3.png b/tests/fonts/masters/paragraph4_nokern_3.png
new file mode 100644
index 0000000..a7711b3
Binary files /dev/null and b/tests/fonts/masters/paragraph4_nokern_3.png differ
diff --git a/tests/fonts/masters/paragraph5.png b/tests/fonts/masters/paragraph5.png
new file mode 100644
index 0000000..ab683c6
Binary files /dev/null and b/tests/fonts/masters/paragraph5.png differ
diff --git a/tests/fonts/masters/paragraph5_2.png b/tests/fonts/masters/paragraph5_2.png
new file mode 100644
index 0000000..23a4470
Binary files /dev/null and b/tests/fonts/masters/paragraph5_2.png differ
diff --git a/tests/fonts/masters/paragraph5_3.png b/tests/fonts/masters/paragraph5_3.png
new file mode 100644
index 0000000..7192f1b
Binary files /dev/null and b/tests/fonts/masters/paragraph5_3.png differ
diff --git a/tests/fonts/masters/paragraph5_nokern.png b/tests/fonts/masters/paragraph5_nokern.png
new file mode 100644
index 0000000..b353821
Binary files /dev/null and b/tests/fonts/masters/paragraph5_nokern.png differ
diff --git a/tests/fonts/masters/paragraph5_nokern_2.png b/tests/fonts/masters/paragraph5_nokern_2.png
new file mode 100644
index 0000000..83713ea
Binary files /dev/null and b/tests/fonts/masters/paragraph5_nokern_2.png differ
diff --git a/tests/fonts/masters/paragraph5_nokern_3.png b/tests/fonts/masters/paragraph5_nokern_3.png
new file mode 100644
index 0000000..8b5d847
Binary files /dev/null and b/tests/fonts/masters/paragraph5_nokern_3.png differ
diff --git a/tests/fonts/masters/selection_rects1.png b/tests/fonts/masters/selection_rects1.png
new file mode 100644
index 0000000..a5fdfef
Binary files /dev/null and b/tests/fonts/masters/selection_rects1.png differ
diff --git a/tests/fonts/masters/selection_rects2.png b/tests/fonts/masters/selection_rects2.png
new file mode 100644
index 0000000..7ff88fd
Binary files /dev/null and b/tests/fonts/masters/selection_rects2.png differ
diff --git a/tests/fonts/masters/selection_rects3.png b/tests/fonts/masters/selection_rects3.png
new file mode 100644
index 0000000..ed80e33
Binary files /dev/null and b/tests/fonts/masters/selection_rects3.png differ
diff --git a/tests/fonts/masters/spans1.png b/tests/fonts/masters/spans1.png
new file mode 100644
index 0000000..659eb30
Binary files /dev/null and b/tests/fonts/masters/spans1.png differ
diff --git a/tests/fonts/masters/spans2.png b/tests/fonts/masters/spans2.png
new file mode 100644
index 0000000..d792d55
Binary files /dev/null and b/tests/fonts/masters/spans2.png differ
diff --git a/tests/fonts/masters/spans4.png b/tests/fonts/masters/spans4.png
new file mode 100644
index 0000000..763903d
Binary files /dev/null and b/tests/fonts/masters/spans4.png differ
diff --git a/tests/fonts/masters/spans5.png b/tests/fonts/masters/spans5.png
new file mode 100644
index 0000000..615908b
Binary files /dev/null and b/tests/fonts/masters/spans5.png differ
diff --git a/tests/fonts/rendered/alignments.png b/tests/fonts/rendered/alignments.png
new file mode 100644
index 0000000..fda3688
Binary files /dev/null and b/tests/fonts/rendered/alignments.png differ
diff --git a/tests/fonts/rendered/basic1.png b/tests/fonts/rendered/basic1.png
new file mode 100644
index 0000000..07ecae7
Binary files /dev/null and b/tests/fonts/rendered/basic1.png differ
diff --git a/tests/fonts/rendered/basic2.png b/tests/fonts/rendered/basic2.png
new file mode 100644
index 0000000..8b2ed35
Binary files /dev/null and b/tests/fonts/rendered/basic2.png differ
diff --git a/tests/fonts/rendered/basic3.png b/tests/fonts/rendered/basic3.png
new file mode 100644
index 0000000..b6729aa
Binary files /dev/null and b/tests/fonts/rendered/basic3.png differ
diff --git a/tests/fonts/rendered/basic4.png b/tests/fonts/rendered/basic4.png
new file mode 100644
index 0000000..7409f47
Binary files /dev/null and b/tests/fonts/rendered/basic4.png differ
diff --git a/tests/fonts/rendered/basic5.png b/tests/fonts/rendered/basic5.png
new file mode 100644
index 0000000..2e048f5
Binary files /dev/null and b/tests/fonts/rendered/basic5.png differ
diff --git a/tests/fonts/rendered/basic6.png b/tests/fonts/rendered/basic6.png
new file mode 100644
index 0000000..abb7064
Binary files /dev/null and b/tests/fonts/rendered/basic6.png differ
diff --git a/tests/fonts/rendered/basic7.png b/tests/fonts/rendered/basic7.png
new file mode 100644
index 0000000..ddf03e2
Binary files /dev/null and b/tests/fonts/rendered/basic7.png differ
diff --git a/tests/fonts/rendered/basic8.png b/tests/fonts/rendered/basic8.png
new file mode 100644
index 0000000..5e66e06
Binary files /dev/null and b/tests/fonts/rendered/basic8.png differ
diff --git a/tests/fonts/rendered/basic9.png b/tests/fonts/rendered/basic9.png
new file mode 100644
index 0000000..4b747ff
Binary files /dev/null and b/tests/fonts/rendered/basic9.png differ
diff --git a/tests/fonts/rendered/huge1.png b/tests/fonts/rendered/huge1.png
new file mode 100644
index 0000000..661ce69
Binary files /dev/null and b/tests/fonts/rendered/huge1.png differ
diff --git a/tests/fonts/rendered/huge1_nokern.png b/tests/fonts/rendered/huge1_nokern.png
new file mode 100644
index 0000000..3c9e80d
Binary files /dev/null and b/tests/fonts/rendered/huge1_nokern.png differ
diff --git a/tests/fonts/rendered/huge2.png b/tests/fonts/rendered/huge2.png
new file mode 100644
index 0000000..cfa5ccd
Binary files /dev/null and b/tests/fonts/rendered/huge2.png differ
diff --git a/tests/fonts/rendered/huge2_nokern.png b/tests/fonts/rendered/huge2_nokern.png
new file mode 100644
index 0000000..5753c16
Binary files /dev/null and b/tests/fonts/rendered/huge2_nokern.png differ
diff --git a/tests/fonts/rendered/huge3.png b/tests/fonts/rendered/huge3.png
new file mode 100644
index 0000000..2a7c8ca
Binary files /dev/null and b/tests/fonts/rendered/huge3.png differ
diff --git a/tests/fonts/rendered/huge3_nokern.png b/tests/fonts/rendered/huge3_nokern.png
new file mode 100644
index 0000000..0df1b96
Binary files /dev/null and b/tests/fonts/rendered/huge3_nokern.png differ
diff --git a/tests/fonts/rendered/lines1.png b/tests/fonts/rendered/lines1.png
new file mode 100644
index 0000000..8332af3
Binary files /dev/null and b/tests/fonts/rendered/lines1.png differ
diff --git a/tests/fonts/rendered/lines2.png b/tests/fonts/rendered/lines2.png
new file mode 100644
index 0000000..5450444
Binary files /dev/null and b/tests/fonts/rendered/lines2.png differ
diff --git a/tests/fonts/rendered/pairs1.png b/tests/fonts/rendered/pairs1.png
new file mode 100644
index 0000000..6367f97
Binary files /dev/null and b/tests/fonts/rendered/pairs1.png differ
diff --git a/tests/fonts/rendered/pairs2.png b/tests/fonts/rendered/pairs2.png
new file mode 100644
index 0000000..0dfca9a
Binary files /dev/null and b/tests/fonts/rendered/pairs2.png differ
diff --git a/tests/fonts/rendered/pairs3.png b/tests/fonts/rendered/pairs3.png
new file mode 100644
index 0000000..cf0493c
Binary files /dev/null and b/tests/fonts/rendered/pairs3.png differ
diff --git a/tests/fonts/rendered/paragraph1.png b/tests/fonts/rendered/paragraph1.png
new file mode 100644
index 0000000..b51f2d5
Binary files /dev/null and b/tests/fonts/rendered/paragraph1.png differ
diff --git a/tests/fonts/rendered/paragraph1_2.png b/tests/fonts/rendered/paragraph1_2.png
new file mode 100644
index 0000000..359dd30
Binary files /dev/null and b/tests/fonts/rendered/paragraph1_2.png differ
diff --git a/tests/fonts/rendered/paragraph1_3.png b/tests/fonts/rendered/paragraph1_3.png
new file mode 100644
index 0000000..fabe147
Binary files /dev/null and b/tests/fonts/rendered/paragraph1_3.png differ
diff --git a/tests/fonts/rendered/paragraph1_nokern.png b/tests/fonts/rendered/paragraph1_nokern.png
new file mode 100644
index 0000000..1d22360
Binary files /dev/null and b/tests/fonts/rendered/paragraph1_nokern.png differ
diff --git a/tests/fonts/rendered/paragraph1_nokern_2.png b/tests/fonts/rendered/paragraph1_nokern_2.png
new file mode 100644
index 0000000..dc0e255
Binary files /dev/null and b/tests/fonts/rendered/paragraph1_nokern_2.png differ
diff --git a/tests/fonts/rendered/paragraph1_nokern_3.png b/tests/fonts/rendered/paragraph1_nokern_3.png
new file mode 100644
index 0000000..404c13d
Binary files /dev/null and b/tests/fonts/rendered/paragraph1_nokern_3.png differ
diff --git a/tests/fonts/rendered/paragraph2.png b/tests/fonts/rendered/paragraph2.png
new file mode 100644
index 0000000..45ee6f3
Binary files /dev/null and b/tests/fonts/rendered/paragraph2.png differ
diff --git a/tests/fonts/rendered/paragraph2_2.png b/tests/fonts/rendered/paragraph2_2.png
new file mode 100644
index 0000000..253c396
Binary files /dev/null and b/tests/fonts/rendered/paragraph2_2.png differ
diff --git a/tests/fonts/rendered/paragraph2_3.png b/tests/fonts/rendered/paragraph2_3.png
new file mode 100644
index 0000000..9bdb327
Binary files /dev/null and b/tests/fonts/rendered/paragraph2_3.png differ
diff --git a/tests/fonts/rendered/paragraph2_nokern.png b/tests/fonts/rendered/paragraph2_nokern.png
new file mode 100644
index 0000000..d63cf94
Binary files /dev/null and b/tests/fonts/rendered/paragraph2_nokern.png differ
diff --git a/tests/fonts/rendered/paragraph2_nokern_2.png b/tests/fonts/rendered/paragraph2_nokern_2.png
new file mode 100644
index 0000000..992aa19
Binary files /dev/null and b/tests/fonts/rendered/paragraph2_nokern_2.png differ
diff --git a/tests/fonts/rendered/paragraph2_nokern_3.png b/tests/fonts/rendered/paragraph2_nokern_3.png
new file mode 100644
index 0000000..412f442
Binary files /dev/null and b/tests/fonts/rendered/paragraph2_nokern_3.png differ
diff --git a/tests/fonts/rendered/paragraph3.png b/tests/fonts/rendered/paragraph3.png
new file mode 100644
index 0000000..d4aa2a8
Binary files /dev/null and b/tests/fonts/rendered/paragraph3.png differ
diff --git a/tests/fonts/rendered/paragraph3_2.png b/tests/fonts/rendered/paragraph3_2.png
new file mode 100644
index 0000000..ff50720
Binary files /dev/null and b/tests/fonts/rendered/paragraph3_2.png differ
diff --git a/tests/fonts/rendered/paragraph3_3.png b/tests/fonts/rendered/paragraph3_3.png
new file mode 100644
index 0000000..6a622e6
Binary files /dev/null and b/tests/fonts/rendered/paragraph3_3.png differ
diff --git a/tests/fonts/rendered/paragraph3_nokern.png b/tests/fonts/rendered/paragraph3_nokern.png
new file mode 100644
index 0000000..aab3e48
Binary files /dev/null and b/tests/fonts/rendered/paragraph3_nokern.png differ
diff --git a/tests/fonts/rendered/paragraph3_nokern_2.png b/tests/fonts/rendered/paragraph3_nokern_2.png
new file mode 100644
index 0000000..5d380cb
Binary files /dev/null and b/tests/fonts/rendered/paragraph3_nokern_2.png differ
diff --git a/tests/fonts/rendered/paragraph3_nokern_3.png b/tests/fonts/rendered/paragraph3_nokern_3.png
new file mode 100644
index 0000000..62255e2
Binary files /dev/null and b/tests/fonts/rendered/paragraph3_nokern_3.png differ
diff --git a/tests/fonts/rendered/paragraph4.png b/tests/fonts/rendered/paragraph4.png
new file mode 100644
index 0000000..e8b47d2
Binary files /dev/null and b/tests/fonts/rendered/paragraph4.png differ
diff --git a/tests/fonts/rendered/paragraph4_2.png b/tests/fonts/rendered/paragraph4_2.png
new file mode 100644
index 0000000..e6ab9c0
Binary files /dev/null and b/tests/fonts/rendered/paragraph4_2.png differ
diff --git a/tests/fonts/rendered/paragraph4_3.png b/tests/fonts/rendered/paragraph4_3.png
new file mode 100644
index 0000000..c439944
Binary files /dev/null and b/tests/fonts/rendered/paragraph4_3.png differ
diff --git a/tests/fonts/rendered/paragraph4_nokern.png b/tests/fonts/rendered/paragraph4_nokern.png
new file mode 100644
index 0000000..2524bca
Binary files /dev/null and b/tests/fonts/rendered/paragraph4_nokern.png differ
diff --git a/tests/fonts/rendered/paragraph4_nokern_2.png b/tests/fonts/rendered/paragraph4_nokern_2.png
new file mode 100644
index 0000000..09ecd3f
Binary files /dev/null and b/tests/fonts/rendered/paragraph4_nokern_2.png differ
diff --git a/tests/fonts/rendered/paragraph4_nokern_3.png b/tests/fonts/rendered/paragraph4_nokern_3.png
new file mode 100644
index 0000000..cceca7f
Binary files /dev/null and b/tests/fonts/rendered/paragraph4_nokern_3.png differ
diff --git a/tests/fonts/rendered/paragraph5.png b/tests/fonts/rendered/paragraph5.png
new file mode 100644
index 0000000..637a59d
Binary files /dev/null and b/tests/fonts/rendered/paragraph5.png differ
diff --git a/tests/fonts/rendered/paragraph5_2.png b/tests/fonts/rendered/paragraph5_2.png
new file mode 100644
index 0000000..60cb8fa
Binary files /dev/null and b/tests/fonts/rendered/paragraph5_2.png differ
diff --git a/tests/fonts/rendered/paragraph5_3.png b/tests/fonts/rendered/paragraph5_3.png
new file mode 100644
index 0000000..f8a280c
Binary files /dev/null and b/tests/fonts/rendered/paragraph5_3.png differ
diff --git a/tests/fonts/rendered/paragraph5_nokern.png b/tests/fonts/rendered/paragraph5_nokern.png
new file mode 100644
index 0000000..ec1d181
Binary files /dev/null and b/tests/fonts/rendered/paragraph5_nokern.png differ
diff --git a/tests/fonts/rendered/paragraph5_nokern_2.png b/tests/fonts/rendered/paragraph5_nokern_2.png
new file mode 100644
index 0000000..f9317e3
Binary files /dev/null and b/tests/fonts/rendered/paragraph5_nokern_2.png differ
diff --git a/tests/fonts/rendered/paragraph5_nokern_3.png b/tests/fonts/rendered/paragraph5_nokern_3.png
new file mode 100644
index 0000000..5967957
Binary files /dev/null and b/tests/fonts/rendered/paragraph5_nokern_3.png differ
diff --git a/tests/fonts/rendered/selection_rects1.png b/tests/fonts/rendered/selection_rects1.png
new file mode 100644
index 0000000..a5fdfef
Binary files /dev/null and b/tests/fonts/rendered/selection_rects1.png differ
diff --git a/tests/fonts/rendered/selection_rects2.png b/tests/fonts/rendered/selection_rects2.png
new file mode 100644
index 0000000..7ff88fd
Binary files /dev/null and b/tests/fonts/rendered/selection_rects2.png differ
diff --git a/tests/fonts/rendered/selection_rects3.png b/tests/fonts/rendered/selection_rects3.png
new file mode 100644
index 0000000..ed80e33
Binary files /dev/null and b/tests/fonts/rendered/selection_rects3.png differ
diff --git a/tests/fonts/rendered/spans1.png b/tests/fonts/rendered/spans1.png
new file mode 100644
index 0000000..5aff4e2
Binary files /dev/null and b/tests/fonts/rendered/spans1.png differ
diff --git a/tests/fonts/rendered/spans2.png b/tests/fonts/rendered/spans2.png
new file mode 100644
index 0000000..5060f36
Binary files /dev/null and b/tests/fonts/rendered/spans2.png differ
diff --git a/tests/fonts/rendered/spans4.png b/tests/fonts/rendered/spans4.png
new file mode 100644
index 0000000..0756b5f
Binary files /dev/null and b/tests/fonts/rendered/spans4.png differ
diff --git a/tests/fonts/rendered/spans5.png b/tests/fonts/rendered/spans5.png
new file mode 100644
index 0000000..3c1ba92
Binary files /dev/null and b/tests/fonts/rendered/spans5.png differ
diff --git a/tests/fonts/svg_changa.png b/tests/fonts/svg_changa.png
new file mode 100644
index 0000000..4bfe9f4
Binary files /dev/null and b/tests/fonts/svg_changa.png differ
diff --git a/tests/fonts/svg_dejavu.png b/tests/fonts/svg_dejavu.png
new file mode 100644
index 0000000..22384d9
Binary files /dev/null and b/tests/fonts/svg_dejavu.png differ
diff --git a/tests/fonts/svg_ibm.png b/tests/fonts/svg_ibm.png
new file mode 100644
index 0000000..9ba89b6
Binary files /dev/null and b/tests/fonts/svg_ibm.png differ
diff --git a/tests/fonts/svg_moon.png b/tests/fonts/svg_moon.png
new file mode 100644
index 0000000..6b8dfd7
Binary files /dev/null and b/tests/fonts/svg_moon.png differ
diff --git a/tests/fonts/svg_ubuntu.png b/tests/fonts/svg_ubuntu.png
new file mode 100644
index 0000000..3509d71
Binary files /dev/null and b/tests/fonts/svg_ubuntu.png differ
diff --git a/tests/fonts_megatest.nim b/tests/fonts_megatest.nim
new file mode 100644
index 0000000..f76f15c
--- /dev/null
+++ b/tests/fonts_megatest.nim
@@ -0,0 +1,12 @@
+import common, pixie
+
+# Clone https://github.com/treeform/fidgetfonts
+
+let fontPaths = findAllFonts("../fidgetfonts")
+
+for fontPath in fontPaths:
+ echo fontPath
+ try:
+ var font = readFont(fontPath)
+ except PixieError:
+ echo "ERROR: ", getCurrentExceptionMsg()
diff --git a/tests/fuzz_opentype.nim b/tests/fuzz_opentype.nim
new file mode 100644
index 0000000..c7d0401
--- /dev/null
+++ b/tests/fuzz_opentype.nim
@@ -0,0 +1,32 @@
+import common, pixie, random, strformat, unicode
+
+randomize()
+
+let fontPaths = findAllFonts("tests/fonts")
+
+doAssert fontPaths.len > 0
+
+for i in 0 ..< 10000:
+ var
+ file = fontPaths[rand(fontPaths.len - 1)]
+ data = readFile(file)
+ pos = rand(data.len)
+ value = rand(255).char
+ data[pos] = value
+ echo &"{i} {file} {pos} {value.uint8}"
+ try:
+ let font = parseOtf(data)
+ doAssert font != nil
+ for i in 0.uint16 ..< uint16.high:
+ discard font.getGlyphPath(Rune(i.int))
+ except PixieError:
+ discard
+
+ data = data[0 ..< pos]
+ try:
+ let font = parseOtf(data)
+ doAssert font != nil
+ for i in 0.uint16 ..< uint16.high:
+ discard font.getGlyphPath(Rune(i.int))
+ except PixieError:
+ discard
diff --git a/tests/megatest.nim b/tests/icons_megatest.nim
similarity index 100%
rename from tests/megatest.nim
rename to tests/icons_megatest.nim
diff --git a/tests/images/drawEllipse.png b/tests/images/drawEllipse.png
index a816ac3..f245ddf 100644
Binary files a/tests/images/drawEllipse.png and b/tests/images/drawEllipse.png differ
diff --git a/tests/images/drawPolygon.png b/tests/images/drawPolygon.png
index 7f2ba6c..4f50858 100644
Binary files a/tests/images/drawPolygon.png and b/tests/images/drawPolygon.png differ
diff --git a/tests/images/drawRoundedRect.png b/tests/images/drawRoundedRect.png
index c3050ad..2c6f3f6 100644
Binary files a/tests/images/drawRoundedRect.png and b/tests/images/drawRoundedRect.png differ
diff --git a/tests/images/drawSegment.png b/tests/images/drawSegment.png
index a2a446b..7ab2592 100644
Binary files a/tests/images/drawSegment.png and b/tests/images/drawSegment.png differ
diff --git a/tests/images/flipped1.png b/tests/images/flipped1.png
index 2fa1421..74a2974 100644
Binary files a/tests/images/flipped1.png and b/tests/images/flipped1.png differ
diff --git a/tests/images/flipped2.png b/tests/images/flipped2.png
index 442446e..b4a0af9 100644
Binary files a/tests/images/flipped2.png and b/tests/images/flipped2.png differ
diff --git a/tests/images/flipped3.png b/tests/images/flipped3.png
index 344d923..e3e4a4f 100644
Binary files a/tests/images/flipped3.png and b/tests/images/flipped3.png differ
diff --git a/tests/images/gif/audrey.png b/tests/images/gif/audrey.png
index a2aace5..7498f59 100644
Binary files a/tests/images/gif/audrey.png and b/tests/images/gif/audrey.png differ
diff --git a/tests/images/gif/sunflower.png b/tests/images/gif/sunflower.png
index e9b8c9b..cdaaa16 100644
Binary files a/tests/images/gif/sunflower.png and b/tests/images/gif/sunflower.png differ
diff --git a/tests/images/imageblur20.png b/tests/images/imageblur20.png
index 9704d36..978c3cd 100644
Binary files a/tests/images/imageblur20.png and b/tests/images/imageblur20.png differ
diff --git a/tests/images/imageblur20oob.png b/tests/images/imageblur20oob.png
index d283fc5..1934f31 100644
Binary files a/tests/images/imageblur20oob.png and b/tests/images/imageblur20oob.png differ
diff --git a/tests/images/maskblur20.png b/tests/images/maskblur20.png
index bab2143..3b76a26 100644
Binary files a/tests/images/maskblur20.png and b/tests/images/maskblur20.png differ
diff --git a/tests/images/masks/circleMask.png b/tests/images/masks/circleMask.png
index 89de309..ca9744c 100644
Binary files a/tests/images/masks/circleMask.png and b/tests/images/masks/circleMask.png differ
diff --git a/tests/images/masks/circleMaskSharpened.png b/tests/images/masks/circleMaskSharpened.png
index 88dc9b9..2746046 100644
Binary files a/tests/images/masks/circleMaskSharpened.png and b/tests/images/masks/circleMaskSharpened.png differ
diff --git a/tests/images/masks/drawEllipse.png b/tests/images/masks/drawEllipse.png
index 1bd4af3..f219dc5 100644
Binary files a/tests/images/masks/drawEllipse.png and b/tests/images/masks/drawEllipse.png differ
diff --git a/tests/images/masks/drawPolygon.png b/tests/images/masks/drawPolygon.png
index 089d0d3..b17c742 100644
Binary files a/tests/images/masks/drawPolygon.png and b/tests/images/masks/drawPolygon.png differ
diff --git a/tests/images/masks/drawRect.png b/tests/images/masks/drawRect.png
index 9afe3cc..c8039a7 100644
Binary files a/tests/images/masks/drawRect.png and b/tests/images/masks/drawRect.png differ
diff --git a/tests/images/masks/drawRoundedRect.png b/tests/images/masks/drawRoundedRect.png
index 13bb852..993dc2d 100644
Binary files a/tests/images/masks/drawRoundedRect.png and b/tests/images/masks/drawRoundedRect.png differ
diff --git a/tests/images/masks/drawSegment.png b/tests/images/masks/drawSegment.png
index fc209a6..ec89fbe 100644
Binary files a/tests/images/masks/drawSegment.png and b/tests/images/masks/drawSegment.png differ
diff --git a/tests/images/masks/imageMaskedMask.png b/tests/images/masks/imageMaskedMask.png
index eb13fae..e4a360f 100644
Binary files a/tests/images/masks/imageMaskedMask.png and b/tests/images/masks/imageMaskedMask.png differ
diff --git a/tests/images/masks/maskMinified.png b/tests/images/masks/maskMinified.png
index a6d9d58..7db6efe 100644
Binary files a/tests/images/masks/maskMinified.png and b/tests/images/masks/maskMinified.png differ
diff --git a/tests/images/masks/maskedMask.png b/tests/images/masks/maskedMask.png
index eb13fae..e4a360f 100644
Binary files a/tests/images/masks/maskedMask.png and b/tests/images/masks/maskedMask.png differ
diff --git a/tests/images/masks/shifted.png b/tests/images/masks/shifted.png
index 2beb2a4..7093b03 100644
Binary files a/tests/images/masks/shifted.png and b/tests/images/masks/shifted.png differ
diff --git a/tests/images/masks/spread.png b/tests/images/masks/spread.png
index 96bfceb..81b4ca4 100644
Binary files a/tests/images/masks/spread.png and b/tests/images/masks/spread.png differ
diff --git a/tests/images/masks/strokeEllipse.png b/tests/images/masks/strokeEllipse.png
index af1c50c..e0d06da 100644
Binary files a/tests/images/masks/strokeEllipse.png and b/tests/images/masks/strokeEllipse.png differ
diff --git a/tests/images/masks/strokePolygon.png b/tests/images/masks/strokePolygon.png
index 4d88470..bb78208 100644
Binary files a/tests/images/masks/strokePolygon.png and b/tests/images/masks/strokePolygon.png differ
diff --git a/tests/images/masks/strokeRect.png b/tests/images/masks/strokeRect.png
index b663c6e..23a86df 100644
Binary files a/tests/images/masks/strokeRect.png and b/tests/images/masks/strokeRect.png differ
diff --git a/tests/images/masks/strokeRoundedRect.png b/tests/images/masks/strokeRoundedRect.png
index 7f7a31c..09cf8fb 100644
Binary files a/tests/images/masks/strokeRoundedRect.png and b/tests/images/masks/strokeRoundedRect.png differ
diff --git a/tests/images/minifiedBy2.png b/tests/images/minifiedBy2.png
index 854cdfc..2099e3c 100644
Binary files a/tests/images/minifiedBy2.png and b/tests/images/minifiedBy2.png differ
diff --git a/tests/images/minifiedBy4.png b/tests/images/minifiedBy4.png
index 6e5769b..4d51d16 100644
Binary files a/tests/images/minifiedBy4.png and b/tests/images/minifiedBy4.png differ
diff --git a/tests/images/paths/boxBevel.png b/tests/images/paths/boxBevel.png
index 814a7e1..7551e66 100644
Binary files a/tests/images/paths/boxBevel.png and b/tests/images/paths/boxBevel.png differ
diff --git a/tests/images/paths/boxMiter.png b/tests/images/paths/boxMiter.png
index 69b02a3..4e92425 100644
Binary files a/tests/images/paths/boxMiter.png and b/tests/images/paths/boxMiter.png differ
diff --git a/tests/images/paths/boxRound.png b/tests/images/paths/boxRound.png
index 53d0e9a..c3f8ee1 100644
Binary files a/tests/images/paths/boxRound.png and b/tests/images/paths/boxRound.png differ
diff --git a/tests/images/paths/gradientAngular.png b/tests/images/paths/gradientAngular.png
index 260e7d5..1835cfc 100644
Binary files a/tests/images/paths/gradientAngular.png and b/tests/images/paths/gradientAngular.png differ
diff --git a/tests/images/paths/gradientLinear.png b/tests/images/paths/gradientLinear.png
index e1a15fc..c6145ec 100644
Binary files a/tests/images/paths/gradientLinear.png and b/tests/images/paths/gradientLinear.png differ
diff --git a/tests/images/paths/gradientRadial.png b/tests/images/paths/gradientRadial.png
index cb2c7ee..84fe26e 100644
Binary files a/tests/images/paths/gradientRadial.png and b/tests/images/paths/gradientRadial.png differ
diff --git a/tests/images/paths/lcButt.png b/tests/images/paths/lcButt.png
index f7997bc..c02645f 100644
Binary files a/tests/images/paths/lcButt.png and b/tests/images/paths/lcButt.png differ
diff --git a/tests/images/paths/lcRound.png b/tests/images/paths/lcRound.png
index f926b4d..5982eb2 100644
Binary files a/tests/images/paths/lcRound.png and b/tests/images/paths/lcRound.png differ
diff --git a/tests/images/paths/lcSquare.png b/tests/images/paths/lcSquare.png
index 5f2a5d8..c5848c7 100644
Binary files a/tests/images/paths/lcSquare.png and b/tests/images/paths/lcSquare.png differ
diff --git a/tests/images/paths/paintImage.png b/tests/images/paths/paintImage.png
index dc0db98..c8be7ff 100644
Binary files a/tests/images/paths/paintImage.png and b/tests/images/paths/paintImage.png differ
diff --git a/tests/images/paths/paintImageTiled.png b/tests/images/paths/paintImageTiled.png
index 9cac35d..47996bf 100644
Binary files a/tests/images/paths/paintImageTiled.png and b/tests/images/paths/paintImageTiled.png differ
diff --git a/tests/images/paths/paintSolid.png b/tests/images/paths/paintSolid.png
index 2e1e02d..d6a7820 100644
Binary files a/tests/images/paths/paintSolid.png and b/tests/images/paths/paintSolid.png differ
diff --git a/tests/images/paths/pathBlackRectangle.png b/tests/images/paths/pathBlackRectangle.png
index fb9d80e..5e05967 100644
Binary files a/tests/images/paths/pathBlackRectangle.png and b/tests/images/paths/pathBlackRectangle.png differ
diff --git a/tests/images/paths/pathBlackRectangleZ.png b/tests/images/paths/pathBlackRectangleZ.png
index fb9d80e..5e05967 100644
Binary files a/tests/images/paths/pathBlackRectangleZ.png and b/tests/images/paths/pathBlackRectangleZ.png differ
diff --git a/tests/images/paths/pathBottomArc.png b/tests/images/paths/pathBottomArc.png
index 8894c59..c00777c 100644
Binary files a/tests/images/paths/pathBottomArc.png and b/tests/images/paths/pathBottomArc.png differ
diff --git a/tests/images/paths/pathCornerArc.png b/tests/images/paths/pathCornerArc.png
index 690cc82..84a6a27 100644
Binary files a/tests/images/paths/pathCornerArc.png and b/tests/images/paths/pathCornerArc.png differ
diff --git a/tests/images/paths/pathHeart.png b/tests/images/paths/pathHeart.png
index 560cd66..b37ba3c 100644
Binary files a/tests/images/paths/pathHeart.png and b/tests/images/paths/pathHeart.png differ
diff --git a/tests/images/paths/pathInvertedCornerArc.png b/tests/images/paths/pathInvertedCornerArc.png
index 9990332..f39e9a4 100644
Binary files a/tests/images/paths/pathInvertedCornerArc.png and b/tests/images/paths/pathInvertedCornerArc.png differ
diff --git a/tests/images/paths/pathRectangleMask.png b/tests/images/paths/pathRectangleMask.png
index 0df19b1..8b595a5 100644
Binary files a/tests/images/paths/pathRectangleMask.png and b/tests/images/paths/pathRectangleMask.png differ
diff --git a/tests/images/paths/pathRedRectangle.png b/tests/images/paths/pathRedRectangle.png
index d649f68..885c1b3 100644
Binary files a/tests/images/paths/pathRedRectangle.png and b/tests/images/paths/pathRedRectangle.png differ
diff --git a/tests/images/paths/pathRotatedArc.png b/tests/images/paths/pathRotatedArc.png
index 4e3886a..617dd0c 100644
Binary files a/tests/images/paths/pathRotatedArc.png and b/tests/images/paths/pathRotatedArc.png differ
diff --git a/tests/images/paths/pathRoundRect.png b/tests/images/paths/pathRoundRect.png
index 5506d6b..7a94dd9 100644
Binary files a/tests/images/paths/pathRoundRect.png and b/tests/images/paths/pathRoundRect.png differ
diff --git a/tests/images/paths/pathRoundRectMask.png b/tests/images/paths/pathRoundRectMask.png
index 1205ccc..91f1833 100644
Binary files a/tests/images/paths/pathRoundRectMask.png and b/tests/images/paths/pathRoundRectMask.png differ
diff --git a/tests/images/paths/pathStroke1.png b/tests/images/paths/pathStroke1.png
index b738549..3245ce0 100644
Binary files a/tests/images/paths/pathStroke1.png and b/tests/images/paths/pathStroke1.png differ
diff --git a/tests/images/paths/pathStroke2.png b/tests/images/paths/pathStroke2.png
index 832c16e..a41899d 100644
Binary files a/tests/images/paths/pathStroke2.png and b/tests/images/paths/pathStroke2.png differ
diff --git a/tests/images/paths/pathStroke3.png b/tests/images/paths/pathStroke3.png
index 428b9fa..ffee6ee 100644
Binary files a/tests/images/paths/pathStroke3.png and b/tests/images/paths/pathStroke3.png differ
diff --git a/tests/images/paths/pathYellowRectangle.png b/tests/images/paths/pathYellowRectangle.png
index c1b1b2b..3b75c7b 100644
Binary files a/tests/images/paths/pathYellowRectangle.png and b/tests/images/paths/pathYellowRectangle.png differ
diff --git a/tests/images/paths/pixelScale.png b/tests/images/paths/pixelScale.png
index 0abcddc..41e8907 100644
Binary files a/tests/images/paths/pixelScale.png and b/tests/images/paths/pixelScale.png differ
diff --git a/tests/images/png/trailing_data.png b/tests/images/png/trailing_data.png
new file mode 100644
index 0000000..44ee3c1
Binary files /dev/null and b/tests/images/png/trailing_data.png differ
diff --git a/tests/images/rotate0.png b/tests/images/rotate0.png
index 7982907..2a0ecb0 100644
Binary files a/tests/images/rotate0.png and b/tests/images/rotate0.png differ
diff --git a/tests/images/rotate180.png b/tests/images/rotate180.png
index bfda507..a0114ee 100644
Binary files a/tests/images/rotate180.png and b/tests/images/rotate180.png differ
diff --git a/tests/images/rotate270.png b/tests/images/rotate270.png
index 8cd7328..58e1f37 100644
Binary files a/tests/images/rotate270.png and b/tests/images/rotate270.png differ
diff --git a/tests/images/rotate360.png b/tests/images/rotate360.png
index 8f9fa85..d9f0b84 100644
Binary files a/tests/images/rotate360.png and b/tests/images/rotate360.png differ
diff --git a/tests/images/rotate90.png b/tests/images/rotate90.png
index 4e40c7b..7f666d4 100644
Binary files a/tests/images/rotate90.png and b/tests/images/rotate90.png differ
diff --git a/tests/images/strokeEllipse.png b/tests/images/strokeEllipse.png
index 7c95036..9ab565c 100644
Binary files a/tests/images/strokeEllipse.png and b/tests/images/strokeEllipse.png differ
diff --git a/tests/images/strokePolygon.png b/tests/images/strokePolygon.png
index e124ee3..85d18e1 100644
Binary files a/tests/images/strokePolygon.png and b/tests/images/strokePolygon.png differ
diff --git a/tests/images/strokeRect.png b/tests/images/strokeRect.png
index fb80260..b0549e1 100644
Binary files a/tests/images/strokeRect.png and b/tests/images/strokeRect.png differ
diff --git a/tests/images/superimage3.png b/tests/images/superimage3.png
index 1e7232f..b68dee8 100644
Binary files a/tests/images/superimage3.png and b/tests/images/superimage3.png differ
diff --git a/tests/images/superimage4.png b/tests/images/superimage4.png
index 1b58d46..66a30e5 100644
Binary files a/tests/images/superimage4.png and b/tests/images/superimage4.png differ
diff --git a/tests/images/superimage5.png b/tests/images/superimage5.png
index 4ed0c8e..6ac6283 100644
Binary files a/tests/images/superimage5.png and b/tests/images/superimage5.png differ
diff --git a/tests/images/svg/Ghostscript_Tiger.png b/tests/images/svg/Ghostscript_Tiger.png
index 07b4959..e82f56a 100644
Binary files a/tests/images/svg/Ghostscript_Tiger.png and b/tests/images/svg/Ghostscript_Tiger.png differ
diff --git a/tests/images/svg/circle01.png b/tests/images/svg/circle01.png
index fac810b..db14d25 100644
Binary files a/tests/images/svg/circle01.png and b/tests/images/svg/circle01.png differ
diff --git a/tests/images/svg/ellipse01.png b/tests/images/svg/ellipse01.png
index 23d454c..26a1061 100644
Binary files a/tests/images/svg/ellipse01.png and b/tests/images/svg/ellipse01.png differ
diff --git a/tests/images/svg/ionicons.png b/tests/images/svg/ionicons.png
index b9209f2..2b2838a 100644
Binary files a/tests/images/svg/ionicons.png and b/tests/images/svg/ionicons.png differ
diff --git a/tests/images/svg/line01.png b/tests/images/svg/line01.png
index f00b96f..ac93d43 100644
Binary files a/tests/images/svg/line01.png and b/tests/images/svg/line01.png differ
diff --git a/tests/images/svg/polygon01.png b/tests/images/svg/polygon01.png
index c7fe070..e1a0209 100644
Binary files a/tests/images/svg/polygon01.png and b/tests/images/svg/polygon01.png differ
diff --git a/tests/images/svg/polyline01.png b/tests/images/svg/polyline01.png
index 77c3e57..8861b01 100644
Binary files a/tests/images/svg/polyline01.png and b/tests/images/svg/polyline01.png differ
diff --git a/tests/images/svg/quad01.png b/tests/images/svg/quad01.png
index 9722cce..da0526b 100644
Binary files a/tests/images/svg/quad01.png and b/tests/images/svg/quad01.png differ
diff --git a/tests/images/svg/rect01.png b/tests/images/svg/rect01.png
index 250f7ef..b47a8d0 100644
Binary files a/tests/images/svg/rect01.png and b/tests/images/svg/rect01.png differ
diff --git a/tests/images/svg/rect02.png b/tests/images/svg/rect02.png
index 90f9702..85e098a 100644
Binary files a/tests/images/svg/rect02.png and b/tests/images/svg/rect02.png differ
diff --git a/tests/images/svg/simple-icons.png b/tests/images/svg/simple-icons.png
index 71d5386..4b78ac7 100644
Binary files a/tests/images/svg/simple-icons.png and b/tests/images/svg/simple-icons.png differ
diff --git a/tests/images/svg/tabler-icons.png b/tests/images/svg/tabler-icons.png
index e1f982e..9c838ee 100644
Binary files a/tests/images/svg/tabler-icons.png and b/tests/images/svg/tabler-icons.png differ
diff --git a/tests/images/svg/triangle01.png b/tests/images/svg/triangle01.png
index 994bc97..954d28a 100644
Binary files a/tests/images/svg/triangle01.png and b/tests/images/svg/triangle01.png differ
diff --git a/tests/images/svg/twbs-icons.png b/tests/images/svg/twbs-icons.png
index 5f5ac82..8921a4d 100644
Binary files a/tests/images/svg/twbs-icons.png and b/tests/images/svg/twbs-icons.png differ
diff --git a/tests/test_fonts.nim b/tests/test_fonts.nim
new file mode 100644
index 0000000..4009eb6
--- /dev/null
+++ b/tests/test_fonts.nim
@@ -0,0 +1,805 @@
+import pixie, pixie/fileformats/png, strformat
+
+proc doDiff(rendered: Image, name: string) =
+ rendered.writeFile(&"tests/fonts/rendered/{name}.png")
+ let
+ master = readImage(&"tests/fonts/masters/{name}.png")
+ (diffScore, diffImage) = diff(master, rendered)
+ echo &"{name} score: {diffScore}"
+ diffImage.writeFile(&"tests/fonts/diffs/{name}.png")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 24
+
+ let bounds = font.computeBounds("Word")
+ doAssert bounds == vec2(57, 28)
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 64
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "fill")
+ image.writeFile("tests/fonts/image_fill.png")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 64
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.strokeText(font, "stroke")
+ image.writeFile("tests/fonts/image_stroke.png")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 64
+ let mask = newMask(200, 100)
+ mask.fillText(font, "fill")
+ writeFile("tests/fonts/mask_fill.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 64
+ let mask = newMask(200, 100)
+ mask.strokeText(font, "stroke")
+ writeFile("tests/fonts/mask_stroke.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/Changa-Bold.svg")
+ font.size = 48
+ let mask = newMask(200, 100)
+ mask.fillText(font, "Changa")
+ writeFile("tests/fonts/svg_changa.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/DejaVuSans.svg")
+ font.size = 48
+ let mask = newMask(200, 100)
+ mask.fillText(font, "Deja vu ")
+ writeFile("tests/fonts/svg_dejavu.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/IBMPlexSans-Regular.svg")
+ font.size = 48
+ let mask = newMask(200, 100)
+ mask.fillText(font, "IBM ")
+ writeFile("tests/fonts/svg_ibm.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/Moon-Bold.svg")
+ font.size = 48
+ let mask = newMask(200, 100)
+ mask.fillText(font, "Moon ")
+ writeFile("tests/fonts/svg_moon.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/Ubuntu.svg")
+ font.size = 48
+ let mask = newMask(200, 100)
+ mask.fillText(font, "Ubuntu ")
+ writeFile("tests/fonts/svg_ubuntu.png", mask.encodePng())
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 72
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "asdf")
+
+ doDiff(image, "basic1")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 72
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "A cow")
+
+ doDiff(image, "basic2")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 24
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "A bit of text HERE")
+
+ doDiff(image, "basic3")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 24
+ font.lineHeight = 100
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "Line height")
+
+ doDiff(image, "basic4")
+
+block:
+ var font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font.size = 24
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "Another font")
+
+ doDiff(image, "basic5")
+
+block:
+ var font = readFont("tests/fonts/Aclonica-Regular_1.ttf")
+ font.size = 24
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "Different font")
+
+ doDiff(image, "basic6")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 24
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(font, "First line")
+ image.fillText(font, "Second line", vec2(0, font.defaultLineHeight))
+
+ doDiff(image, "basic7")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 24
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Wrapping text to new line",
+ bounds = vec2(200, 0)
+ )
+
+ doDiff(image, "basic8")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 24
+
+ let image = newImage(200, 100)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Supercalifragilisticexpialidocious",
+ bounds = vec2(200, 0)
+ )
+
+ doDiff(image, "basic9")
+
+const
+ paragraph = "ShehadcometotheconclusionthatyoucouldtellalotaboutapersonbytheirearsThewaytheystuckoutandthesizeoftheearlobescouldgiveyou"
+ paragraph_2 = "She had come to the conclusion that you could tell a lot about a person by their ears The way they stuck out and the size of the earlobes could give you wonderful insights into the person Of course she couldnt scientifically prove any of this but that didnt matter to her Before anything else she would size up the ears of the person she was talking to Shes asked the question so many times that she barely listened to the answers anymore The answers were always the same Well not exactly the same but the same in a general sense A more accurate description was the answers never surprised her"
+ paragraph_3 = "She had come to the conclusion that you could tell a lot about a person by their ears The way they stuck out and the size of the earlobes could give you wonderful insights into the person. Of course, she couldn't scientifically prove any of this, but that didn't matter to her. Before anything else, she would size up the ears of the person she was talking to. She's asked the question so many times that she barely listened to the answers anymore. The answers were always the same. Well, not exactly the same, but the same in a general sense. A more accurate description was the answers never surprised her."
+ paragraphs = [paragraph, paragraph_2, paragraph_3]
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 16
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph1_{i + 1}" else: "paragraph1"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 16
+ font.noKerningAdjustments = true
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph1_nokern_{i + 1}" else: "paragraph1_nokern"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font.size = 16
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph2_{i + 1}" else: "paragraph2"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font.size = 16
+ font.noKerningAdjustments = true
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph2_nokern_{i + 1}" else: "paragraph2_nokern"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/IBMPlexSans-Regular_2.ttf")
+ font.size = 16
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph3_{i + 1}" else: "paragraph3"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/IBMPlexSans-Regular_2.ttf")
+ font.size = 16
+ font.noKerningAdjustments = true
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph3_nokern_{i + 1}" else: "paragraph3_nokern"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/NotoSans-Regular_4.ttf")
+ font.size = 16
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph4_{i + 1}" else: "paragraph4"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/NotoSans-Regular_4.ttf")
+ font.size = 16
+ font.noKerningAdjustments = true
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph4_nokern_{i + 1}" else: "paragraph4_nokern"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/Pacifico-Regular_4.ttf")
+ font.size = 16
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph5_{i + 1}" else: "paragraph5"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/Pacifico-Regular_4.ttf")
+ font.size = 16
+ font.noKerningAdjustments = true
+
+ let image = newImage(1000, 150)
+
+ for i, text in paragraphs:
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ text,
+ bounds = image.wh
+ )
+
+ let name = if i > 0: &"paragraph5_nokern_{i + 1}" else: "paragraph5_nokern"
+ doDiff(image, name)
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 200
+
+ let image = newImage(2800, 400)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Shehadcometotheconclusion",
+ bounds = image.wh
+ )
+
+ doDiff(image, "huge1")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 200
+ font.noKerningAdjustments = true
+
+ let image = newImage(2800, 400)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Shehadcometotheconclusion",
+ bounds = image.wh
+ )
+
+ doDiff(image, "huge1_nokern")
+
+block:
+ var font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font.size = 200
+
+ let image = newImage(2800, 400)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Shehadcometotheconclusion",
+ bounds = image.wh
+ )
+
+ doDiff(image, "huge2")
+
+block:
+ var font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font.size = 200
+ font.noKerningAdjustments = true
+
+ let image = newImage(2800, 400)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Shehadcometotheconclusion",
+ bounds = image.wh
+ )
+
+ doDiff(image, "huge2_nokern")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 200
+
+ let image = newImage(2800, 400)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Wrapping text to the next line",
+ bounds = image.wh
+ )
+
+ doDiff(image, "huge3")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 200
+ font.noKerningAdjustments = true
+
+ let image = newImage(2800, 400)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "Wrapping text to the next line",
+ bounds = image.wh
+ )
+
+ doDiff(image, "huge3_nokern")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 100
+
+ let image = newImage(2800, 200)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "HA HT HX HY IA IT IX IY MA MT MX MY NA NT NX NY",
+ bounds = image.wh
+ )
+
+ doDiff(image, "pairs1")
+
+block:
+ var font = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font.size = 100
+
+ let image = newImage(2800, 200)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "V( V) V- V/ V: v; v? v@ VT VV VW VX VY V] Vu Vz V{",
+ bounds = image.wh
+ )
+
+ doDiff(image, "pairs2")
+
+block:
+ var font = readFont("tests/fonts/IBMPlexSans-Regular_2.ttf")
+ font.size = 100
+
+ let image = newImage(2800, 200)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ "B, B. BA BJ BT BW BY Bf Bg Bt bw By",
+ bounds = image.wh
+ )
+
+ doDiff(image, "pairs3")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 18
+
+ let image = newImage(200, 150)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ """First line
+Second line
+Third line
+Fourth line
+Fifth line
+Sixth line
+Seventh line""",
+ bounds = image.wh
+ )
+
+ doDiff(image, "lines1")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 18
+ font.lineHeight = 30
+
+ let image = newImage(200, 150)
+ image.fill(rgba(255, 255, 255, 255))
+ image.fillText(
+ font,
+ """First line
+Second line
+Third line
+Fourth line
+Fifth line""",
+ bounds = image.wh
+ )
+
+ doDiff(image, "lines2")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 36
+
+ let image = newImage(800, 800)
+ image.fill(rgba(255, 255, 255, 255))
+
+ image.fillText(
+ font,
+ "TopLeft",
+ bounds = image.wh,
+ hAlign = haLeft,
+ vAlign = vaTop
+ )
+
+ image.fillText(
+ font,
+ "TopCenter",
+ bounds = image.wh,
+ hAlign = haCenter,
+ vAlign = vaTop
+ )
+
+ image.fillText(
+ font,
+ "TopRight",
+ bounds = image.wh,
+ hAlign = haRight,
+ vAlign = vaTop
+ )
+
+ image.fillText(
+ font,
+ "MiddleLeft",
+ bounds = image.wh,
+ hAlign = haLeft,
+ vAlign = vaMiddle
+ )
+
+ image.fillText(
+ font,
+ "MiddleCenter",
+ bounds = image.wh,
+ hAlign = haCenter,
+ vAlign = vaMiddle
+ )
+
+ image.fillText(
+ font,
+ "MiddleRight",
+ bounds = image.wh,
+ hAlign = haRight,
+ vAlign = vaMiddle
+ )
+
+ image.fillText(
+ font,
+ "BottomLeft",
+ bounds = image.wh,
+ hAlign = haLeft,
+ vAlign = vaBottom
+ )
+
+ image.fillText(
+ font,
+ "BottomCenter",
+ bounds = image.wh,
+ hAlign = haCenter,
+ vAlign = vaBottom
+ )
+
+ image.fillText(
+ font,
+ "BottomRight",
+ bounds = image.wh,
+ hAlign = haRight,
+ vAlign = vaBottom
+ )
+
+ doDiff(image, "alignments")
+
+block:
+ var font = readFont("tests/fonts/IBMPlexSans-Regular_2.ttf")
+ font.size = 48
+ font.paint = Paint(
+ kind: pkGradientLinear,
+ gradientHandlePositions: @[
+ vec2(0, 50),
+ vec2(100, 50),
+ ],
+ gradientStops: @[
+ ColorStop(color: rgba(255, 0, 0, 255), position: 0),
+ ColorStop(color: rgba(255, 0, 0, 40), position: 1.0),
+ ]
+ )
+
+ let image = newImage(100, 100)
+ image.fillText(font, "Text")
+
+ image.writeFile("tests/fonts/image_paint_fill.png")
+
+block:
+ var font1 = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font1.size = 80
+
+ var font2 = readFont("tests/fonts/Aclonica-Regular_1.ttf")
+ font2.size = 100
+
+ var font3 = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font3.size = 48
+
+ let spans = @[
+ newSpan("One span ", font1),
+ newSpan("Two span", font2),
+ newSpan(" Three span", font3)
+ ]
+
+ let image = newImage(700, 250)
+ image.fill(rgba(255, 255, 255, 255))
+
+ let arrangement = typeset(spans, bounds = image.wh)
+
+ image.fillText(arrangement)
+
+ doDiff(image, "spans1")
+
+ for i, rect in arrangement.selectionRects:
+ image.fillRect(rect, rgba(128, 128, 128, 128))
+
+ doDiff(image, "selection_rects1")
+
+block:
+ var font1 = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font1.size = 80
+
+ var font2 = readFont("tests/fonts/Aclonica-Regular_1.ttf")
+ font2.size = 100
+
+ var font3 = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ font3.size = 48
+
+ let spans = @[
+ newSpan("One span ", font1),
+ newSpan("Two span", font2),
+ newSpan(" Three span", font3)
+ ]
+
+ let image = newImage(475, 400)
+ image.fill(rgba(255, 255, 255, 255))
+
+ let arrangement = typeset(spans, bounds = image.wh)
+
+ image.fillText(arrangement)
+
+ doDiff(image, "spans2")
+
+ for i, rect in arrangement.selectionRects:
+ image.fillRect(rect, rgba(128, 128, 128, 128))
+
+ doDiff(image, "selection_rects2")
+
+block:
+ var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ font.size = 16
+
+ let image = newImage(75, 75)
+ image.fill(rgba(255, 255, 255, 255))
+
+ let arrangement = typeset(
+ font, "Wrapping text to new line", bounds = image.wh
+ )
+
+ image.fillText(arrangement)
+
+ for i, rect in arrangement.selectionRects:
+ image.fillRect(rect, rgba(128, 128, 128, 128))
+
+ doDiff(image, "selection_rects3")
+
+block:
+ let
+ roboto = readFont("tests/fonts/Roboto-Regular_1.ttf")
+ aclonica = readFont("tests/fonts/Aclonica-Regular_1.ttf")
+ ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+ ibm = readFont("tests/fonts/IBMPlexSans-Regular_2.ttf")
+ noto = readFont("tests/fonts/NotoSans-Regular_4.ttf")
+
+ var font1 = roboto
+ font1.size = 64
+
+ var font2 = aclonica
+ font2.size = 80
+
+ var font3 = ibm
+ font3.size = 40
+
+ var font4 = ubuntu
+ font4.size = 56
+
+ var font5 = noto
+ font5.size = 72
+
+ var font6 = roboto
+ font6.size = 48
+
+ var font7 = noto
+ font7.size = 64
+
+ var font8 = ubuntu
+ font8.size = 54
+ font8.paint.color = rgba(255, 0, 0, 255)
+
+ var font9 = roboto
+ font9.size = 48
+
+ var font10 = aclonica
+ font10.size = 48
+ font10.lineHeight = 120
+
+ let spans = @[
+ newSpan("Using spans, ", font1),
+ newSpan("Pixie ", font2),
+ newSpan("can arrange and rasterize ", font3),
+ newSpan("very complex text layouts. ", font4),
+ newSpan("Spans", font5),
+ newSpan(" can have different ", font6),
+ newSpan("font sizes,", font7),
+ newSpan(" colors", font8),
+ newSpan(" and ", font9),
+ newSpan("line heights.", font10)
+ ]
+
+ let image = newImage(600, 600)
+ image.fill(rgba(255, 255, 255, 255))
+
+ let arrangement = typeset(spans, bounds = image.wh)
+
+ image.fillText(arrangement)
+
+ doDiff(image, "spans4")
+
+block:
+ let ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
+
+ var font1 = ubuntu
+ font1.size = 15
+ font1.paint.color = parseHtmlColor("#CACACA").rgba()
+
+ var font2 = ubuntu
+ font2.size = 84
+
+ var font3 = ubuntu
+ font3.size = 18
+ font3.paint.color = parseHtmlColor("#007FF4").rgba()
+
+ var font4 = ubuntu
+ font4.size = 20
+ font4.paint.color = parseHtmlColor("#4F4F4F").rgba()
+
+ let spans = @[
+ newSpan("verb [with object] ", font1),
+ newSpan("strallow\n", font2),
+ newSpan("\nstral·low\n", font3),
+ newSpan("\n1. free (something) from restrictive restrictions \"the regulations are intended to strallow changes in public policy\" ", font4)
+ ]
+
+ let image = newImage(400, 400)
+ image.fill(rgba(255, 255, 255, 255))
+
+ let arrangement = typeset(spans, bounds = vec2(360, 360))
+
+ image.fillText(arrangement, vec2(20, 20))
+
+ doDiff(image, "spans5")
diff --git a/tests/test_paints.nim b/tests/test_paints.nim
index 8b7b439..304f9f2 100644
--- a/tests/test_paints.nim
+++ b/tests/test_paints.nim
@@ -9,8 +9,7 @@ const heartShape = """
"""
block:
- let
- image = newImage(100, 100)
+ let image = newImage(100, 100)
image.fillPath(
heartShape,
Paint(
@@ -21,8 +20,7 @@ block:
image.writeFile("tests/images/paths/paintSolid.png")
block:
- let
- image = newImage(100, 100)
+ let image = newImage(100, 100)
image.fillPath(
heartShape,
Paint(
@@ -34,8 +32,7 @@ block:
image.writeFile("tests/images/paths/paintImage.png")
block:
- let
- image = newImage(100, 100)
+ let image = newImage(100, 100)
image.fillPath(
heartShape,
Paint(
@@ -47,8 +44,7 @@ block:
image.writeFile("tests/images/paths/paintImageTiled.png")
block:
- let
- image = newImage(100, 100)
+ let image = newImage(100, 100)
image.fillPath(
heartShape,
Paint(
@@ -66,8 +62,7 @@ block:
image.writeFile("tests/images/paths/gradientLinear.png")
block:
- let
- image = newImage(100, 100)
+ let image = newImage(100, 100)
image.fillPath(
heartShape,
Paint(
@@ -87,8 +82,7 @@ block:
image.writeFile("tests/images/paths/gradientRadial.png")
block:
- let
- image = newImage(100, 100)
+ let image = newImage(100, 100)
image.fillPath(
heartShape,
Paint(
diff --git a/tests/test_png.nim b/tests/test_png.nim
index 056bdfc..57a618e 100644
--- a/tests/test_png.nim
+++ b/tests/test_png.nim
@@ -1,4 +1,4 @@
-import pixie/common, pixie/fileformats/png, pngsuite, strformat
+import pixie, pixie/fileformats/png, pngsuite, strformat
# for file in pngSuiteFiles:
# let
@@ -13,19 +13,23 @@ import pixie/common, pixie/fileformats/png, pngsuite, strformat
# doAssert decoded.width == decoded2.width
# doAssert decoded.data == decoded2.data
-for channels in 1 .. 4:
- var data: seq[uint8]
- for x in 0 ..< 16:
- for y in 0 ..< 16:
- var components = newSeq[uint8](channels)
- for i in 0 ..< channels:
- components[i] = (x * 16).uint8
- data.add(components)
- let encoded = encodePng(16, 16, channels, data[0].addr, data.len)
+block:
+ for channels in 1 .. 4:
+ var data: seq[uint8]
+ for x in 0 ..< 16:
+ for y in 0 ..< 16:
+ var components = newSeq[uint8](channels)
+ for i in 0 ..< channels:
+ components[i] = (x * 16).uint8
+ data.add(components)
+ let encoded = encodePng(16, 16, channels, data[0].addr, data.len)
-for file in pngSuiteCorruptedFiles:
- try:
- discard decodePng(readFile(&"tests/images/png/pngsuite/{file}.png"))
- doAssert false
- except PixieError:
- discard
+ for file in pngSuiteCorruptedFiles:
+ try:
+ discard decodePng(readFile(&"tests/images/png/pngsuite/{file}.png"))
+ doAssert false
+ except PixieError:
+ discard
+
+block:
+ discard readImage("tests/images/png/trailing_data.png")
diff --git a/tests/validate_fonts.nim b/tests/validate_fonts.nim
new file mode 100644
index 0000000..131c3e1
--- /dev/null
+++ b/tests/validate_fonts.nim
@@ -0,0 +1,42 @@
+import pixie, stb_truetype, unicode
+
+let fontFiles = [
+ # "tests/fonts/Roboto-Regular_1.ttf"
+ # "tests/fonts/Aclonica-Regular_1.ttf"
+ # "tests/fonts/Ubuntu-Regular_1.ttf"
+ # "tests/fonts/IBMPlexSans-Regular_2.ttf"
+ # "tests/fonts/NotoSans-Regular_4.ttf"
+ "tests/fonts/Pacifico-Regular_4.ttf"
+]
+
+for fontFile in fontFiles:
+ let stbtt = initFont(readFile(fontFile))
+ var font = readFont(fontFile)
+
+ var ascent, descent, lineGap: cint
+ stbtt.getFontVMetrics(ascent, descent, lineGap)
+
+ doAssert font.typeface.ascent == ascent.float32
+ doAssert font.typeface.descent == descent.float32
+ doAssert font.typeface.lineGap == lineGap.float32
+
+ for i in 32 .. 126:
+ var advanceWidth, leftSideBearing: cint
+ stbtt.getCodepointHMetrics(Rune(i), advanceWidth, leftSideBearing)
+
+ doAssert font.typeface.getAdvance(Rune(i)) == advanceWidth.float32
+
+ for i in 32 .. 126:
+ for j in 32 .. 126:
+ # echo i, ": ", $Rune(i), " ", j, ": ", $Rune(j)
+ let
+ a = stbtt.getCodepointKernAdvance(Rune(i), Rune(j)).float32
+ b = font.typeface.getKerningAdjustment(Rune(i), Rune(j))
+ if a != b:
+ # echo fontFile
+ echo i, ": ", $Rune(i), " ", j, ": ", $Rune(j)
+ echo "DISAGREE: ", a, " != ", b, " <<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ # quit()
+
+ # echo stbtt.getCodepointKernAdvance(Rune('r'), Rune('s')).float32
+ # echo font.typeface.getKerningAdjustment(Rune('r'), Rune('s'))
diff --git a/tools/gen_readme.nim b/tools/gen_readme.nim
index a8a5c64..57360cd 100644
--- a/tools/gen_readme.nim
+++ b/tools/gen_readme.nim
@@ -11,6 +11,8 @@ proc cutBetween(str, a, b: string): string =
var md: seq[string]
var exampleFiles = [
+ "examples/text.nim",
+ "examples/text_spans.nim",
"examples/square.nim",
"examples/line.nim",
"examples/rounded_rectangle.nim",