typeface + cleaner

This commit is contained in:
Ryan Oldenburg 2021-04-27 01:44:17 -05:00
parent a076205882
commit 8574b12900

View file

@ -3,10 +3,13 @@ import pixie/fontformats/opentype, pixie/paths, unicode, vmath
const AutoLineHeight* = -1.float32 const AutoLineHeight* = -1.float32
type type
Font* = ref object Typeface* = ref object
opentype: OpenType opentype: OpenType
glyphPaths: Table[Rune, Path] glyphPaths: Table[Rune, Path]
kerningPairs: Table[(Rune, Rune), float32] kerningPairs: Table[(Rune, Rune), float32]
Font* = ref 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.
@ -28,48 +31,46 @@ 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.
font.opentype.hhea.ascender.float32 typeface.opentype.hhea.ascender.float32
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.
font.opentype.hhea.descender.float32 typeface.opentype.hhea.descender.float32
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.
font.opentype.hhea.lineGap.float32 typeface.opentype.hhea.lineGap.float32
proc getGlyphPath*(typeface: Typeface, rune: Rune): Path =
## The glyph path for the rune.
if rune notin typeface.glyphPaths:
typeface.glyphPaths[rune] = typeface.opentype.parseGlyph(rune)
typeface.glyphPaths[rune].transform(scale(vec2(1, -1)))
typeface.glyphPaths[rune]
proc getGlyphAdvance(typeface: Typeface, rune: Rune): float32 =
## The advance for the rune in pixels.
let glyphId = typeface.opentype.getGlyphId(rune).int
if glyphId < typeface.opentype.hmtx.hMetrics.len:
result = typeface.opentype.hmtx.hMetrics[glyphId].advanceWidth.float32
else:
result = typeface.opentype.hmtx.hMetrics[^1].advanceWidth.float32
proc getKerningAdjustment(typeface: Typeface, left, right: Rune): float32 =
## The kerning adjustment for the rune pair, in pixels.
let pair = (left, right)
if pair in typeface.kerningPairs:
result = typeface.kerningPairs[pair]
proc scale*(font: Font): float32 = proc scale*(font: Font): float32 =
## The scale factor to transform font units into pixels. ## The scale factor to transform font units into pixels.
font.size / font.opentype.head.unitsPerEm.float32 font.size / font.typeface.opentype.head.unitsPerEm.float32
proc defaultLineHeight*(font: Font): float32 = proc defaultLineHeight*(font: Font): float32 =
## The default line height in pixels for the current font size. ## The default line height in pixels for the current font size.
round((font.ascent + abs(font.descent) + font.lineGap) * font.scale) round((font.typeface.ascent + abs(font.typeface.descent) + font.typeface.lineGap) * font.scale)
proc getGlyphPath*(font: Font, rune: Rune): Path =
## The glyph path for the rune.
if rune notin font.glyphPaths:
font.glyphPaths[rune] = font.opentype.parseGlyph(rune)
font.glyphPaths[rune].transform(scale(vec2(1, -1)))
font.glyphPaths[rune]
proc getGlyphAdvance*(font: Font, rune: Rune): float32 =
## The advance for the rune in pixels.
let glyphId = font.opentype.getGlyphId(rune).int
if glyphId < font.opentype.hmtx.hMetrics.len:
result = font.opentype.hmtx.hMetrics[glyphId].advanceWidth.float32
else:
result = font.opentype.hmtx.hMetrics[^1].advanceWidth.float32
result *= font.scale
proc getKerningAdjustment*(font: Font, left, right: Rune): float32 =
## The kerning adjustment for the rune pair, in pixels.
let pair = (left, right)
if pair in font.kerningPairs:
result = font.kerningPairs[pair]
result *= font.scale
proc convertTextCase(runes: var seq[Rune], textCase: TextCase) = proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
case textCase: case textCase:
@ -105,20 +106,23 @@ proc typeset*(
else: else:
font.defaultLineHeight font.defaultLineHeight
proc glyphAdvance(runes: seq[Rune], font: Font, i: int): float32 =
if i + 1 < runes.len:
result += font.typeface.getKerningAdjustment(runes[i], runes[i + 1])
result += font.typeface.getGlyphAdvance(runes[i])
result *= font.scale
var var
positions = newSeq[Vec2](runes.len) positions = newSeq[Vec2](runes.len)
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 runes: for i, rune in runes:
if rune.canWrap(): if rune.canWrap():
prevCanWrap = i prevCanWrap = i
if i > 0: let advance = glyphAdvance(runes, font, i)
at.x += font.getKerningAdjustment(runes[i - 1], rune)
let advance = font.getGlyphAdvance(rune)
if bounds.x > 0 and at.x + advance > bounds.x: # Wrap to new line if bounds.x > 0 and at.x + advance > bounds.x: # Wrap to new line
at.x = 0 at.x = 0
at.y += lineHeight at.y += lineHeight
@ -126,43 +130,42 @@ proc typeset*(
# Go back and wrap glyphs after the wrap index down to the next line # Go back and wrap glyphs after the wrap index down to the next line
if prevCanWrap > 0 and prevCanWrap != i: if prevCanWrap > 0 and prevCanWrap != i:
for j in prevCanWrap + 1 ..< i: for j in prevCanWrap + 1 ..< i:
if j > 0:
at.x += font.getKerningAdjustment(runes[j - 1], runes[j])
positions[j] = at positions[j] = at
at.x += font.getGlyphAdvance(runes[j]) at.x += glyphAdvance(runes, font, j)
positions[i] = at positions[i] = at
at.x += advance at.x += advance
for i, rune in runes: for i, rune in runes:
var path = font.getGlyphPath(rune) var path = font.typeface.getGlyphPath(rune)
path.transform(translate(positions[i]) * scale(vec2(font.scale))) path.transform(translate(positions[i]) * scale(vec2(font.scale)))
result.add(path) result.add(path)
proc parseOtf*(buf: string): Font = proc parseOtf*(buf: string): Font =
result = Font() result = Font()
result.opentype = parseOpenType(buf) result.typeface = Typeface()
result.typeface.opentype = parseOpenType(buf)
result.size = 12 result.size = 12
result.lineHeight = AutoLineHeight result.lineHeight = AutoLineHeight
if result.opentype.kern != nil: if result.typeface.opentype.kern != nil:
for table in result.opentype.kern.subTables: for table in result.typeface.opentype.kern.subTables:
if (table.coverage and 1) != 0: # Horizontal data if (table.coverage and 1) != 0: # Horizontal data
for pair in table.kernPairs: for pair in table.kernPairs:
if pair.value != 0 and if pair.value != 0 and
pair.left in result.opentype.cmap.glyphIdToRune and pair.left in result.typeface.opentype.cmap.glyphIdToRune and
pair.right in result.opentype.cmap.glyphIdToRune: pair.right in result.typeface.opentype.cmap.glyphIdToRune:
let key = ( let key = (
result.opentype.cmap.glyphIdToRune[pair.left], result.typeface.opentype.cmap.glyphIdToRune[pair.left],
result.opentype.cmap.glyphIdToRune[pair.right] result.typeface.opentype.cmap.glyphIdToRune[pair.right]
) )
var value = pair.value.float32 var value = pair.value.float32
if key in result.kerningPairs: if key in result.typeface.kerningPairs:
if (table.coverage and 0b1000) != 0: # Override if (table.coverage and 0b1000) != 0: # Override
discard discard
else: # Accumulate else: # Accumulate
value += result.kerningPairs[key] value += result.typeface.kerningPairs[key]
result.kerningPairs[key] = value result.typeface.kerningPairs[key] = value
proc parseTtf*(buf: string): Font = proc parseTtf*(buf: string): Font =
parseOtf(buf) parseOtf(buf)