more tests
|
@ -332,12 +332,22 @@ proc fillText*(
|
||||||
text: string,
|
text: string,
|
||||||
color: SomeColor,
|
color: SomeColor,
|
||||||
transform: Vec2 | Mat3 = vec2(0, 0),
|
transform: Vec2 | Mat3 = vec2(0, 0),
|
||||||
bounds = vec2(0, 0)
|
bounds = vec2(0, 0),
|
||||||
|
hAlign = haLeft,
|
||||||
|
vAlign = vaTop,
|
||||||
|
textCase = tcNormal,
|
||||||
|
wrap = true,
|
||||||
|
kerning = true
|
||||||
) =
|
) =
|
||||||
let typeset = font.typeset(text, bounds)
|
for path in font.typesetPaths(
|
||||||
for i in 0 ..< typeset.runes.len:
|
text,
|
||||||
var path = font.getGlyphPath(typeset.runes[i])
|
bounds,
|
||||||
path.transform(translate(typeset.positions[i]) * scale(vec2(font.scale)))
|
hAlign,
|
||||||
|
vAlign,
|
||||||
|
textCase,
|
||||||
|
wrap,
|
||||||
|
kerning
|
||||||
|
):
|
||||||
image.fillPath(path, color, transform)
|
image.fillPath(path, color, transform)
|
||||||
|
|
||||||
proc fillText*(
|
proc fillText*(
|
||||||
|
@ -345,12 +355,22 @@ proc fillText*(
|
||||||
font: Font,
|
font: Font,
|
||||||
text: string,
|
text: string,
|
||||||
transform: Vec2 | Mat3 = vec2(0, 0),
|
transform: Vec2 | Mat3 = vec2(0, 0),
|
||||||
bounds = vec2(0, 0)
|
bounds = vec2(0, 0),
|
||||||
|
hAlign = haLeft,
|
||||||
|
vAlign = vaTop,
|
||||||
|
textCase = tcNormal,
|
||||||
|
wrap = true,
|
||||||
|
kerning = true
|
||||||
) =
|
) =
|
||||||
let typeset = font.typeset(text, bounds)
|
for path in font.typesetPaths(
|
||||||
for i in 0 ..< typeset.runes.len:
|
text,
|
||||||
var path = font.getGlyphPath(typeset.runes[i])
|
bounds,
|
||||||
path.transform(translate(typeset.positions[i]) * scale(vec2(font.scale)))
|
hAlign,
|
||||||
|
vAlign,
|
||||||
|
textCase,
|
||||||
|
wrap,
|
||||||
|
kerning
|
||||||
|
):
|
||||||
mask.fillPath(path, transform)
|
mask.fillPath(path, transform)
|
||||||
|
|
||||||
proc strokeText*(
|
proc strokeText*(
|
||||||
|
@ -360,12 +380,22 @@ proc strokeText*(
|
||||||
color: SomeColor,
|
color: SomeColor,
|
||||||
transform: Vec2 | Mat3 = vec2(0, 0),
|
transform: Vec2 | Mat3 = vec2(0, 0),
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
bounds = vec2(0, 0)
|
bounds = vec2(0, 0),
|
||||||
|
hAlign = haLeft,
|
||||||
|
vAlign = vaTop,
|
||||||
|
textCase = tcNormal,
|
||||||
|
wrap = true,
|
||||||
|
kerning = true
|
||||||
) =
|
) =
|
||||||
let typeset = font.typeset(text, bounds)
|
for path in font.typesetPaths(
|
||||||
for i in 0 ..< typeset.runes.len:
|
text,
|
||||||
var path = font.getGlyphPath(typeset.runes[i])
|
bounds,
|
||||||
path.transform(translate(typeset.positions[i]) * scale(vec2(font.scale)))
|
hAlign,
|
||||||
|
vAlign,
|
||||||
|
textCase,
|
||||||
|
wrap,
|
||||||
|
kerning
|
||||||
|
):
|
||||||
image.strokePath(path, color, transform, strokeWidth)
|
image.strokePath(path, color, transform, strokeWidth)
|
||||||
|
|
||||||
proc strokeText*(
|
proc strokeText*(
|
||||||
|
@ -374,10 +404,20 @@ proc strokeText*(
|
||||||
text: string,
|
text: string,
|
||||||
transform: Vec2 | Mat3 = vec2(0, 0),
|
transform: Vec2 | Mat3 = vec2(0, 0),
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
bounds = vec2(0, 0)
|
bounds = vec2(0, 0),
|
||||||
|
hAlign = haLeft,
|
||||||
|
vAlign = vaTop,
|
||||||
|
textCase = tcNormal,
|
||||||
|
wrap = true,
|
||||||
|
kerning = true
|
||||||
) =
|
) =
|
||||||
let typeset = font.typeset(text, bounds)
|
for path in font.typesetPaths(
|
||||||
for i in 0 ..< typeset.runes.len:
|
text,
|
||||||
var path = font.getGlyphPath(typeset.runes[i])
|
bounds,
|
||||||
path.transform(translate(typeset.positions[i]) * scale(vec2(font.scale)))
|
hAlign,
|
||||||
|
vAlign,
|
||||||
|
textCase,
|
||||||
|
wrap,
|
||||||
|
kerning
|
||||||
|
):
|
||||||
mask.strokePath(path, transform, strokeWidth)
|
mask.strokePath(path, transform, strokeWidth)
|
||||||
|
|
|
@ -235,6 +235,26 @@ type
|
||||||
pairValueCount: uint16
|
pairValueCount: uint16
|
||||||
pairValueRecords: seq[PairValueRecord]
|
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
|
PairPos = ref object
|
||||||
posFormat: uint16
|
posFormat: uint16
|
||||||
coverageOffset: uint16
|
coverageOffset: uint16
|
||||||
|
@ -243,6 +263,13 @@ type
|
||||||
pairSetCount: uint16
|
pairSetCount: uint16
|
||||||
pairSetOffsets: seq[uint16]
|
pairSetOffsets: seq[uint16]
|
||||||
pairSets: seq[PairSet]
|
pairSets: seq[PairSet]
|
||||||
|
classDef1Offset: uint16
|
||||||
|
classDef2Offset: uint16
|
||||||
|
class1Count: uint16
|
||||||
|
class2Count: uint16
|
||||||
|
class1Records: seq[Class1Record]
|
||||||
|
classDef1: ClassDef
|
||||||
|
classDef2: ClassDef
|
||||||
coverage: Coverage
|
coverage: Coverage
|
||||||
|
|
||||||
Lookup = object
|
Lookup = object
|
||||||
|
@ -865,6 +892,66 @@ proc parsePairSet(
|
||||||
buf, i + j * pairValueRecordSize, valueFormat1, valueFormat2
|
buf, i + j * pairValueRecordSize, valueFormat1, valueFormat2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
proc parseClass2Record(
|
||||||
|
buf: string, offset: int, valueFormat1, valueFormat2: uint16
|
||||||
|
): Class2Record =
|
||||||
|
result.valueRecord1 = parseValueRecord(buf, offset, valueFormat1)
|
||||||
|
result.valueRecord2 = parseValueRecord(buf, offset, valueFormat2)
|
||||||
|
|
||||||
|
proc parseClass1Record(
|
||||||
|
buf: string, offset: int, valueFormat1, valueFormat2, class2Count: uint16
|
||||||
|
): Class1Record =
|
||||||
|
var i = offset
|
||||||
|
|
||||||
|
let class2RecordSize = (
|
||||||
|
countSetBits(valueFormat1) + countSetBits(valueFormat2)
|
||||||
|
) * 2
|
||||||
|
|
||||||
|
result.class2Records.setLen(class2Count.int)
|
||||||
|
for j in 0 ..< class2Count.int:
|
||||||
|
result.class2Records[j] =
|
||||||
|
parseClass2Record(buf, i, valueFormat1, valueFormat2)
|
||||||
|
i += class2RecordSize
|
||||||
|
|
||||||
|
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 += sizeof(ClassRangeRecord)
|
||||||
|
else:
|
||||||
|
failUnsupported()
|
||||||
|
|
||||||
proc parsePairPos(buf: string, offset: int): PairPos =
|
proc parsePairPos(buf: string, offset: int): PairPos =
|
||||||
var i = offset
|
var i = offset
|
||||||
|
|
||||||
|
@ -902,7 +989,36 @@ proc parsePairPos(buf: string, offset: int): PairPos =
|
||||||
|
|
||||||
result.coverage = parseCoverage(buf, offset + result.coverageOffset.int)
|
result.coverage = parseCoverage(buf, offset + result.coverageOffset.int)
|
||||||
of 2:
|
of 2:
|
||||||
discard
|
result = PairPos()
|
||||||
|
result.posFormat = posFormat
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
countSetBits(result.valueFormat1) + countSetBits(result.valueFormat2)
|
||||||
|
) * 2
|
||||||
|
|
||||||
|
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.classDef1 = parseClassDef(buf, offset + result.classDef1Offset.int)
|
||||||
|
result.classDef2 = parseClassDef(buf, offset + result.classDef2Offset.int)
|
||||||
|
|
||||||
|
result.coverage = parseCoverage(buf, offset + result.coverageOffset.int)
|
||||||
else:
|
else:
|
||||||
failUnsupported()
|
failUnsupported()
|
||||||
|
|
||||||
|
@ -1244,12 +1360,21 @@ proc getGlyphPath*(opentype: OpenType, rune: Rune): Path =
|
||||||
opentype.glyphPaths[rune].transform(scale(vec2(1, -1)))
|
opentype.glyphPaths[rune].transform(scale(vec2(1, -1)))
|
||||||
opentype.glyphPaths[rune]
|
opentype.glyphPaths[rune]
|
||||||
|
|
||||||
proc getGlyphAdvance*(opentype: OpenType, rune: Rune): float32 =
|
proc getLeftSideBearing*(opentype: OpenType, rune: Rune): float32 =
|
||||||
let glyphId = opentype.getGlyphId(rune).int
|
let glyphId = opentype.getGlyphId(rune).int
|
||||||
if glyphId < opentype.hmtx.hMetrics.len:
|
if glyphId < opentype.hmtx.hMetrics.len:
|
||||||
opentype.hmtx.hMetrics[glyphId].advanceWidth.float32
|
result = opentype.hmtx.hMetrics[glyphId].leftSideBearing.float32
|
||||||
else:
|
else:
|
||||||
opentype.hmtx.hMetrics[^1].advanceWidth.float32
|
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 =
|
proc getKerningAdjustment*(opentype: OpenType, left, right: Rune): float32 =
|
||||||
let pair = (left, right)
|
let pair = (left, right)
|
||||||
|
@ -1328,21 +1453,93 @@ proc parseOpenType*(buf: string): OpenType =
|
||||||
value += result.kerningPairs[key]
|
value += result.kerningPairs[key]
|
||||||
result.kerningPairs[key] = value
|
result.kerningPairs[key] = value
|
||||||
|
|
||||||
if "GPOS" in result.tableRecords:
|
# if "GPOS" in result.tableRecords:
|
||||||
result.gpos = parseGposTable(buf, result.tableRecords["GPOS"].offset.int)
|
# result.gpos = parseGposTable(buf, result.tableRecords["GPOS"].offset.int)
|
||||||
|
|
||||||
if result.gpos != nil and result.gpos.lookupList.pairPos != nil:
|
# if result.gpos != nil and result.gpos.lookupList.pairPos != nil:
|
||||||
case result.gpos.lookupList.pairPos.coverage.coverageFormat:
|
# # case result.gpos.lookupList.pairPos.coverage.coverageFormat:
|
||||||
of 1:
|
# # of 1:
|
||||||
echo result.gpos.lookupList.pairPos.coverage.glyphCount
|
# # echo "GLYPH COUNT:", result.gpos.lookupList.pairPos.coverage.glyphCount
|
||||||
of 2:
|
# # of 2:
|
||||||
echo result.gpos.lookupList.pairPos.coverage.rangeCount
|
# # echo "RANGE COUNT:", result.gpos.lookupList.pairPos.coverage.rangeCount
|
||||||
else:
|
# # else:
|
||||||
failUnsupported()
|
# # failUnsupported()
|
||||||
|
|
||||||
for pairSet in result.gpos.lookupList.pairPos.pairSets:
|
# case result.gpos.lookupList.pairPos.posFormat:
|
||||||
for pairValue in pairSet.pairValueRecords:
|
# of 1:
|
||||||
discard
|
# echo "posFormat 1 not implemented"
|
||||||
|
# of 2:
|
||||||
|
# proc classDefFormat1(classDef: ClassDef): Table[uint16, uint16] =
|
||||||
|
# for i in 0.uint16 ..< classDef.glyphCount:
|
||||||
|
# result[classDef.startGlyphID + i] = classDef.classValueArray[i]
|
||||||
|
|
||||||
|
# proc classDefFormat2(classDef: ClassDef): Table[uint16, uint16] =
|
||||||
|
# for record in classDef.classRangeRecords:
|
||||||
|
# if record.startGlyphID > record.endGlyphID:
|
||||||
|
# failUnsupported()
|
||||||
|
# for glyphId in record.startGlyphID .. record.endGlyphID:
|
||||||
|
# result[glyphId] = record.class
|
||||||
|
|
||||||
|
# var glyphIdToClass1: Table[uint16, uint16]
|
||||||
|
# case result.gpos.lookupList.pairPos.classDef1.classFormat:
|
||||||
|
# of 1:
|
||||||
|
# glyphIdToClass1 =
|
||||||
|
# classDefFormat1(result.gpos.lookupList.pairPos.classDef1)
|
||||||
|
# of 2:
|
||||||
|
# glyphIdToClass1 =
|
||||||
|
# classDefFormat2(result.gpos.lookupList.pairPos.classDef1)
|
||||||
|
# else:
|
||||||
|
# failUnsupported()
|
||||||
|
|
||||||
|
# var glyphIdToClass2: Table[uint16, uint16]
|
||||||
|
# case result.gpos.lookupList.pairPos.classDef2.classFormat:
|
||||||
|
# of 1:
|
||||||
|
# glyphIdToClass2 =
|
||||||
|
# classDefFormat1(result.gpos.lookupList.pairPos.classDef2)
|
||||||
|
# of 2:
|
||||||
|
# glyphIdToClass2 =
|
||||||
|
# classDefFormat2(result.gpos.lookupList.pairPos.classDef2)
|
||||||
|
# else:
|
||||||
|
# failUnsupported()
|
||||||
|
|
||||||
|
# var runeToClass1: Table[Rune, uint16]
|
||||||
|
# for glyphId, class in glyphIdToClass1:
|
||||||
|
# if glyphId in result.cmap.glyphIdToRune:
|
||||||
|
# let rune = result.cmap.glyphIdToRune[glyphId]
|
||||||
|
# runeToClass1[rune] = class
|
||||||
|
|
||||||
|
# var runeToClass2: Table[Rune, uint16]
|
||||||
|
# for glyphId, class in glyphIdToClass2:
|
||||||
|
# if glyphId in result.cmap.glyphIdToRune:
|
||||||
|
# let rune = result.cmap.glyphIdToRune[glyphId]
|
||||||
|
# runeToClass2[rune] = class
|
||||||
|
|
||||||
|
# var classPairs: Table[(uint16, uint16), Class2Record]
|
||||||
|
# for i, class1Record in result.gpos.lookupList.pairPos.class1Records:
|
||||||
|
# for j, class2Record in class1Record.class2Records:
|
||||||
|
# classPairs[(i.uint16, j.uint16)] = class2Record
|
||||||
|
|
||||||
|
# for left in result.cmap.runeToGlyphId.keys:
|
||||||
|
# for right in result.cmap.runeToGlyphId.keys:
|
||||||
|
# var leftClass, rightClass: uint16
|
||||||
|
# if left in runeToClass1:
|
||||||
|
# leftClass = runeToClass1[left]
|
||||||
|
# if right in runeToClass2:
|
||||||
|
# rightClass = runeToClass2[right]
|
||||||
|
|
||||||
|
# let pair = (leftClass, rightClass)
|
||||||
|
# if pair in classPairs:
|
||||||
|
# let classPair = classPairs[pair]
|
||||||
|
# if classPair.valueRecord1.xAdvance != 0:
|
||||||
|
# result.kerningPairs[(left, right)] =
|
||||||
|
# classPair.valueRecord1.xAdvance.float32
|
||||||
|
# else:
|
||||||
|
# failUnsupported()
|
||||||
|
|
||||||
|
# echo Rune(32) in result.cmap.runeToGlyphId
|
||||||
|
# echo getAdvance(result, Rune(32))
|
||||||
|
# echo result.head.unitsPerEm
|
||||||
|
# echo result.getGlyphId(Rune(32))
|
||||||
|
|
||||||
when defined(release):
|
when defined(release):
|
||||||
{.pop.}
|
{.pop.}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pixie/common, pixie/paths, strutils, tables, unicode, vmath, xmlparser, x
|
||||||
|
|
||||||
type SvgFont* = ref object
|
type SvgFont* = ref object
|
||||||
unitsPerEm*, ascent*, descent*: float32
|
unitsPerEm*, ascent*, descent*: float32
|
||||||
glyphAdvances: Table[Rune, float32]
|
advances: Table[Rune, float32]
|
||||||
glyphPaths: Table[Rune, Path]
|
glyphPaths: Table[Rune, Path]
|
||||||
kerningPairs: Table[(Rune, Rune), float32]
|
kerningPairs: Table[(Rune, Rune), float32]
|
||||||
missingGlyphAdvance: float32
|
missingGlyphAdvance: float32
|
||||||
|
@ -14,9 +14,9 @@ proc getGlyphPath*(svgFont: SvgFont, rune: Rune): Path =
|
||||||
else:
|
else:
|
||||||
svgFont.missingGlyphPath
|
svgFont.missingGlyphPath
|
||||||
|
|
||||||
proc getGlyphAdvance*(svgFont: SvgFont, rune: Rune): float32 =
|
proc getAdvance*(svgFont: SvgFont, rune: Rune): float32 =
|
||||||
if rune in svgFont.glyphAdvances:
|
if rune in svgFont.advances:
|
||||||
svgFont.glyphAdvances[rune]
|
svgFont.advances[rune]
|
||||||
else:
|
else:
|
||||||
svgFont.missingGlyphAdvance
|
svgFont.missingGlyphAdvance
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ proc parseSvgFont*(buf: string): SvgFont =
|
||||||
var advance = defaultAdvance
|
var advance = defaultAdvance
|
||||||
if node.attr("horiz-adv-x").len > 0:
|
if node.attr("horiz-adv-x").len > 0:
|
||||||
advance = node.parseFloat("horiz-adv-x")
|
advance = node.parseFloat("horiz-adv-x")
|
||||||
result.glyphAdvances[rune] = advance
|
result.advances[rune] = advance
|
||||||
result.glyphPaths[rune] = parsePath(node.attr("d"))
|
result.glyphPaths[rune] = parsePath(node.attr("d"))
|
||||||
result.glyphPaths[rune].transform(scale(vec2(1, -1)))
|
result.glyphPaths[rune].transform(scale(vec2(1, -1)))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -4,13 +4,16 @@ import pixie/fontformats/opentype, pixie/fontformats/svgfont, pixie/paths,
|
||||||
const AutoLineHeight* = -1.float32 ## Use default line height for the font size
|
const AutoLineHeight* = -1.float32 ## Use default line height for the font size
|
||||||
|
|
||||||
type
|
type
|
||||||
Font* = ref object
|
Typeface = ref object
|
||||||
opentype: OpenType
|
opentype: OpenType
|
||||||
svgFont: SvgFont
|
svgFont: SvgFont
|
||||||
|
|
||||||
|
Font* = object
|
||||||
|
typeface*: Typeface
|
||||||
size*: float32 ## Font size in pixels.
|
size*: float32 ## Font size in pixels.
|
||||||
lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height.
|
lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height.
|
||||||
|
|
||||||
TypesetText* = ref object
|
Typesetting* = ref object
|
||||||
runes*: seq[Rune]
|
runes*: seq[Rune]
|
||||||
positions*: seq[Vec2]
|
positions*: seq[Vec2]
|
||||||
|
|
||||||
|
@ -32,56 +35,59 @@ type
|
||||||
# tcSmallCaps
|
# tcSmallCaps
|
||||||
# tcSmallCapsForced
|
# tcSmallCapsForced
|
||||||
|
|
||||||
proc ascent(font: Font): float32 {.inline.} =
|
proc ascent(typeface: Typeface): float32 {.inline.} =
|
||||||
## The font ascender value in font units.
|
## The font ascender value in font units.
|
||||||
if font.opentype != nil:
|
if typeface.opentype != nil:
|
||||||
font.opentype.hhea.ascender.float32
|
typeface.opentype.hhea.ascender.float32
|
||||||
else:
|
else:
|
||||||
font.svgFont.ascent
|
typeface.svgFont.ascent
|
||||||
|
|
||||||
proc descent(font: Font): float32 {.inline.} =
|
proc descent(typeface: Typeface): float32 {.inline.} =
|
||||||
## The font descender value in font units.
|
## The font descender value in font units.
|
||||||
if font.opentype != nil:
|
if typeface.opentype != nil:
|
||||||
font.opentype.hhea.descender.float32
|
typeface.opentype.hhea.descender.float32
|
||||||
else:
|
else:
|
||||||
font.svgFont.descent
|
typeface.svgFont.descent
|
||||||
|
|
||||||
proc lineGap(font: Font): float32 {.inline.} =
|
proc lineGap(typeface: Typeface): float32 {.inline.} =
|
||||||
## The font line gap value in font units.
|
## The font line gap value in font units.
|
||||||
if font.opentype != nil:
|
if typeface.opentype != nil:
|
||||||
result = font.opentype.hhea.lineGap.float32
|
result = typeface.opentype.hhea.lineGap.float32
|
||||||
|
|
||||||
proc getGlyphPath*(font: Font, rune: Rune): Path {.inline.} =
|
proc getGlyphPath*(typeface: Typeface, rune: Rune): Path {.inline.} =
|
||||||
## The glyph path for the rune.
|
## The glyph path for the rune.
|
||||||
if font.opentype != nil:
|
if typeface.opentype != nil:
|
||||||
font.opentype.getGlyphPath(rune)
|
typeface.opentype.getGlyphPath(rune)
|
||||||
else:
|
else:
|
||||||
font.svgFont.getGlyphPath(rune)
|
typeface.svgFont.getGlyphPath(rune)
|
||||||
|
|
||||||
proc getGlyphAdvance(font: Font, rune: Rune): float32 {.inline.} =
|
proc getAdvance(typeface: Typeface, rune: Rune): float32 {.inline.} =
|
||||||
## The advance for the rune in pixels.
|
## The advance for the rune in pixels.
|
||||||
if font.opentype != nil:
|
if typeface.opentype != nil:
|
||||||
font.opentype.getGlyphAdvance(rune)
|
typeface.opentype.getAdvance(rune)
|
||||||
else:
|
else:
|
||||||
font.svgFont.getGlyphAdvance(rune)
|
typeface.svgFont.getAdvance(rune)
|
||||||
|
|
||||||
proc getKerningAdjustment(font: Font, left, right: Rune): float32 {.inline.} =
|
proc getKerningAdjustment(
|
||||||
|
typeface: Typeface, left, right: Rune
|
||||||
|
): float32 {.inline.} =
|
||||||
## The kerning adjustment for the rune pair, in pixels.
|
## The kerning adjustment for the rune pair, in pixels.
|
||||||
if font.opentype != nil:
|
if typeface.opentype != nil:
|
||||||
font.opentype.getKerningAdjustment(left, right)
|
typeface.opentype.getKerningAdjustment(left, right)
|
||||||
else:
|
else:
|
||||||
font.svgfont.getKerningAdjustment(left, right)
|
typeface.svgfont.getKerningAdjustment(left, right)
|
||||||
|
|
||||||
proc scale*(font: Font): float32 {.inline.} =
|
proc scale*(font: Font): float32 {.inline.} =
|
||||||
## The scale factor to transform font units into pixels.
|
## The scale factor to transform font units into pixels.
|
||||||
if font.opentype != nil:
|
if font.typeface.opentype != nil:
|
||||||
font.size / font.opentype.head.unitsPerEm.float32
|
font.size / font.typeface.opentype.head.unitsPerEm.float32
|
||||||
else:
|
else:
|
||||||
font.size / font.svgFont.unitsPerEm
|
font.size / font.typeface.svgFont.unitsPerEm
|
||||||
|
|
||||||
proc defaultLineHeight*(font: Font): float32 {.inline.} =
|
proc defaultLineHeight*(font: Font): float32 {.inline.} =
|
||||||
## The default line height in pixels for the current font size.
|
## The default line height in pixels for the current font size.
|
||||||
let fontUnits = (font.ascent + abs(font.descent) + font.lineGap)
|
let fontUnits =
|
||||||
|
font.typeface.ascent - font.typeface.descent + font.typeface.lineGap
|
||||||
round(fontUnits * font.scale)
|
round(fontUnits * font.scale)
|
||||||
|
|
||||||
proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
|
proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
|
||||||
|
@ -107,12 +113,13 @@ proc typeset*(
|
||||||
bounds = vec2(0, 0),
|
bounds = vec2(0, 0),
|
||||||
hAlign = haLeft,
|
hAlign = haLeft,
|
||||||
vAlign = vaTop,
|
vAlign = vaTop,
|
||||||
textCase = tcNormal
|
textCase = tcNormal,
|
||||||
): TypesetText =
|
wrap = true,
|
||||||
result = TypesetText()
|
kerning = true
|
||||||
|
): Typesetting =
|
||||||
|
result = Typesetting()
|
||||||
result.runes = toRunes(text)
|
result.runes = toRunes(text)
|
||||||
result.runes.convertTextCase(textCase)
|
result.runes.convertTextCase(textCase)
|
||||||
|
|
||||||
result.positions.setLen(result.runes.len)
|
result.positions.setLen(result.runes.len)
|
||||||
|
|
||||||
let lineHeight =
|
let lineHeight =
|
||||||
|
@ -121,22 +128,24 @@ proc typeset*(
|
||||||
else:
|
else:
|
||||||
font.defaultLineHeight
|
font.defaultLineHeight
|
||||||
|
|
||||||
proc glyphAdvance(runes: seq[Rune], font: Font, i: int): float32 {.inline.} =
|
proc glyphAdvance(
|
||||||
if i + 1 < runes.len:
|
font: Font, runes: seq[Rune], i: int, kerning: bool
|
||||||
result += font.getKerningAdjustment(runes[i], runes[i + 1])
|
): float32 {.inline.} =
|
||||||
result += font.getGlyphAdvance(runes[i])
|
if kerning and i + 1 < runes.len:
|
||||||
|
result += font.typeface.getKerningAdjustment(runes[i], runes[i + 1])
|
||||||
|
result += font.typeface.getAdvance(runes[i])
|
||||||
result *= font.scale
|
result *= font.scale
|
||||||
|
|
||||||
var
|
var
|
||||||
at: Vec2
|
at: Vec2
|
||||||
prevCanWrap: int
|
prevCanWrap: int
|
||||||
at.y = round(font.ascent * font.scale)
|
at.y = round(font.typeface.ascent * font.scale)
|
||||||
at.y += (lineheight - font.defaultLineHeight) / 2
|
at.y += (lineheight - font.defaultLineHeight) / 2
|
||||||
for i, rune in result.runes:
|
for i, rune in result.runes:
|
||||||
if rune.canWrap():
|
if rune.canWrap():
|
||||||
prevCanWrap = i
|
prevCanWrap = i
|
||||||
|
|
||||||
let advance = glyphAdvance(result.runes, font, i)
|
let advance = glyphAdvance(font, result.runes, i, kerning)
|
||||||
if rune != Rune(32) and bounds.x > 0 and at.x + advance > bounds.x:
|
if rune != Rune(32) and bounds.x > 0 and at.x + advance > bounds.x:
|
||||||
# Wrap to new line
|
# Wrap to new line
|
||||||
at.x = 0
|
at.x = 0
|
||||||
|
@ -146,14 +155,40 @@ proc typeset*(
|
||||||
if prevCanWrap > 0 and prevCanWrap != i:
|
if prevCanWrap > 0 and prevCanWrap != i:
|
||||||
for j in prevCanWrap + 1 ..< i:
|
for j in prevCanWrap + 1 ..< i:
|
||||||
result.positions[j] = at
|
result.positions[j] = at
|
||||||
at.x += glyphAdvance(result.runes, font, j)
|
at.x += glyphAdvance(font, result.runes, j, kerning)
|
||||||
|
|
||||||
result.positions[i] = at
|
result.positions[i] = at
|
||||||
at.x += advance
|
at.x += advance
|
||||||
|
|
||||||
|
iterator typesetPaths*(
|
||||||
|
font: Font,
|
||||||
|
text: string,
|
||||||
|
bounds = vec2(0, 0),
|
||||||
|
hAlign = haLeft,
|
||||||
|
vAlign = vaTop,
|
||||||
|
textCase = tcNormal,
|
||||||
|
wrap = true,
|
||||||
|
kerning = true
|
||||||
|
): Path =
|
||||||
|
let typesetText = font.typeset(
|
||||||
|
text,
|
||||||
|
bounds,
|
||||||
|
hAlign,
|
||||||
|
vAlign,
|
||||||
|
textCase,
|
||||||
|
wrap,
|
||||||
|
kerning
|
||||||
|
)
|
||||||
|
for i in 0 ..< typesetText.runes.len:
|
||||||
|
var path = font.typeface.getGlyphPath(typesetText.runes[i])
|
||||||
|
path.transform(
|
||||||
|
translate(typesetText.positions[i]) * scale(vec2(font.scale))
|
||||||
|
)
|
||||||
|
yield path
|
||||||
|
|
||||||
proc parseOtf*(buf: string): Font =
|
proc parseOtf*(buf: string): Font =
|
||||||
result = Font()
|
result.typeface = Typeface()
|
||||||
result.opentype = parseOpenType(buf)
|
result.typeface.opentype = parseOpenType(buf)
|
||||||
result.size = 12
|
result.size = 12
|
||||||
result.lineHeight = AutoLineHeight
|
result.lineHeight = AutoLineHeight
|
||||||
|
|
||||||
|
@ -161,7 +196,7 @@ proc parseTtf*(buf: string): Font =
|
||||||
parseOtf(buf)
|
parseOtf(buf)
|
||||||
|
|
||||||
proc parseSvgFont*(buf: string): Font =
|
proc parseSvgFont*(buf: string): Font =
|
||||||
result = Font()
|
result.typeface = Typeface()
|
||||||
result.svgFont = svgfont.parseSvgFont(buf)
|
result.typeface.svgFont = svgfont.parseSvgFont(buf)
|
||||||
result.size = 12
|
result.size = 12
|
||||||
result.lineHeight = AutoLineHeight
|
result.lineHeight = AutoLineHeight
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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."
|
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."
|
||||||
|
|
||||||
let font = readFont("tests/fonts/Roboto-Regular.ttf")
|
var font = readFont("tests/fonts/Roboto-Regular.ttf")
|
||||||
font.size = 16
|
font.size = 16
|
||||||
|
|
||||||
timeIt "typeset":
|
timeIt "typeset":
|
||||||
|
@ -13,7 +13,7 @@ let
|
||||||
mask = newMask(500, 300)
|
mask = newMask(500, 300)
|
||||||
|
|
||||||
timeIt "rasterize":
|
timeIt "rasterize":
|
||||||
# image.fill(rgba(255, 255, 255, 255))
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
# image.fillText(font, text, rgba(0, 0, 0, 255), bounds = image.wh)
|
image.fillText(font, text, rgba(0, 0, 0, 255), bounds = image.wh)
|
||||||
mask.fill(0)
|
# mask.fill(0)
|
||||||
mask.fillText(font, text, bounds = mask.wh)
|
# mask.fillText(font, text, bounds = mask.wh)
|
||||||
|
|
BIN
tests/fonts/IBMPlexSans-Regular.ttf
Normal file
BIN
tests/fonts/NotoSans-Regular.ttf
Normal file
BIN
tests/fonts/Pacifico-Regular.ttf
Normal file
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 80 KiB |
BIN
tests/fonts/diffs/paragraph1.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
tests/fonts/diffs/paragraph1_2.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph1_3.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph1_nokern.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
tests/fonts/diffs/paragraph1_nokern_2.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
tests/fonts/diffs/paragraph1_nokern_3.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
tests/fonts/diffs/paragraph2.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
tests/fonts/diffs/paragraph2_2.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph2_3.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
tests/fonts/diffs/paragraph2_nokern.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
tests/fonts/diffs/paragraph2_nokern_2.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph2_nokern_3.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph3.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
tests/fonts/diffs/paragraph3_2.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
tests/fonts/diffs/paragraph3_3.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
tests/fonts/diffs/paragraph3_nokern.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
tests/fonts/diffs/paragraph3_nokern_2.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph3_nokern_3.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
tests/fonts/diffs/paragraph4.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
tests/fonts/diffs/paragraph4_2.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
tests/fonts/diffs/paragraph4_3.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
tests/fonts/diffs/paragraph4_nokern.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
tests/fonts/diffs/paragraph4_nokern_2.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
tests/fonts/diffs/paragraph4_nokern_3.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
tests/fonts/diffs/paragraph5.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
tests/fonts/diffs/paragraph5_2.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
tests/fonts/diffs/paragraph5_3.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
tests/fonts/diffs/paragraph5_nokern.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
tests/fonts/diffs/paragraph5_nokern_2.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
tests/fonts/diffs/paragraph5_nokern_3.png
Normal file
After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 709 B |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 31 KiB |
BIN
tests/fonts/masters/paragraph1.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
tests/fonts/masters/paragraph1_2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph1_3.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph1_nokern.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
tests/fonts/masters/paragraph1_nokern_2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph1_nokern_3.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph2.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
tests/fonts/masters/paragraph2_2.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
tests/fonts/masters/paragraph2_3.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph2_nokern.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
tests/fonts/masters/paragraph2_nokern_2.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
tests/fonts/masters/paragraph2_nokern_3.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph3.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
tests/fonts/masters/paragraph3_2.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
tests/fonts/masters/paragraph3_3.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
tests/fonts/masters/paragraph3_nokern.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
tests/fonts/masters/paragraph3_nokern_2.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
tests/fonts/masters/paragraph3_nokern_3.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph4.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
tests/fonts/masters/paragraph4_2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph4_3.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
tests/fonts/masters/paragraph4_nokern.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
tests/fonts/masters/paragraph4_nokern_2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
tests/fonts/masters/paragraph4_nokern_3.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
tests/fonts/masters/paragraph5.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
tests/fonts/masters/paragraph5_2.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
tests/fonts/masters/paragraph5_3.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
tests/fonts/masters/paragraph5_nokern.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
tests/fonts/masters/paragraph5_nokern_2.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
tests/fonts/masters/paragraph5_nokern_3.png
Normal file
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 8 KiB After Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 76 KiB |
BIN
tests/fonts/rendered/paragraph1.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
tests/fonts/rendered/paragraph1_1.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
tests/fonts/rendered/paragraph1_2.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
tests/fonts/rendered/paragraph1_3.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
tests/fonts/rendered/paragraph1_nokern.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
tests/fonts/rendered/paragraph1_nokern_2.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
tests/fonts/rendered/paragraph1_nokern_3.png
Normal file
After Width: | Height: | Size: 68 KiB |