From 4bdcc67baa7a6efb27b62eb8647718928a4cc6b4 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Mon, 26 Apr 2021 22:14:08 -0500 Subject: [PATCH] kerning --- src/pixie/fontformats/opentype.nim | 14 ++++++++------ src/pixie/fonts.nim | 24 +++++++++++++++++++++++- tests/fonts/diffs/basic6.png | Bin 4138 -> 4041 bytes tests/fonts/rendered/basic6.png | Bin 4239 -> 4283 bytes 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/pixie/fontformats/opentype.nim b/src/pixie/fontformats/opentype.nim index e3e51e0..1cb5233 100644 --- a/src/pixie/fontformats/opentype.nim +++ b/src/pixie/fontformats/opentype.nim @@ -15,6 +15,7 @@ type numTables*: uint16 encodingRecords*: seq[EncodingRecord] runeToGlyphId*: Table[Rune, uint16] + glyphIdToRune*: Table[uint16, Rune] HeadTable* = ref object majorVersion*: uint16 @@ -295,6 +296,7 @@ proc parseCmapTable(buf: string, offset: int): CmapTable = if c != 65535: result.runeToGlyphId[Rune(c)] = glyphId.uint16 + result.glyphIdToRune[glyphId.uint16] = Rune(c) else: # TODO implement other Windows encodingIDs discard @@ -563,13 +565,13 @@ proc parseKernTable(buf: string, offset: int): KernTable = else: failUnsupported() -proc getGlyphId*(opentype: OpenType, rune: Rune): int = +proc getGlyphId*(opentype: OpenType, rune: Rune): uint16 = if rune in opentype.cmap.runeToGlyphId: - result = opentype.cmap.runeToGlyphId[rune].int + result = opentype.cmap.runeToGlyphId[rune] else: discard # Index 0 is the "missing character" glyph -proc parseGlyph(opentype: OpenType, glyphId: int): Path +proc parseGlyph(opentype: OpenType, glyphId: uint16): Path proc parseGlyphPath(buf: string, offset, numberOfContours: int): Path = if numberOfContours < 0: @@ -781,7 +783,7 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path = # elif (flags and 0b1000000000000) != 0: # UNSCALED_COMPONENT_OFFSET # discard - var subPath = opentype.parseGlyph(component.glyphId.int) + var subPath = opentype.parseGlyph(component.glyphId) subPath.transform(mat3( component.xScale, component.scale10, 0.0, component.scale01, component.yScale, 0.0, @@ -792,8 +794,8 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path = moreComponents = (flags and 0b100000) != 0 -proc parseGlyph(opentype: OpenType, glyphId: int): Path = - if glyphId < 0 or glyphId >= opentype.glyf.offsets.len: +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] diff --git a/src/pixie/fonts.nim b/src/pixie/fonts.nim index af56b89..f695139 100644 --- a/src/pixie/fonts.nim +++ b/src/pixie/fonts.nim @@ -6,6 +6,7 @@ type Font* = ref object opentype: OpenType glyphPaths: Table[Rune, Path] + kerningPairs: Table[(Rune, Rune), float32] size*: float32 ## Font size in pixels. lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height. @@ -46,12 +47,17 @@ proc getGlyphPath*(font: Font, rune: Rune): Path = font.glyphPaths[rune] proc getGlyphAdvance*(font: Font, rune: Rune): float32 = - let glyphId = font.opentype.getGlyphId(rune) + let glyphId = font.opentype.getGlyphId(rune).int if glyphId < font.opentype.hmtx.hMetrics.len: font.opentype.hmtx.hMetrics[glyphId].advanceWidth.float32 else: font.opentype.hmtx.hMetrics[^1].advanceWidth.float32 +proc getKerningAdjustment*(font: Font, left, right: Rune): float32 = + let pair = (left, right) + if pair in font.kerningPairs: + result = font.kerningPairs[pair] + proc scale*(font: Font): float32 = ## The scale factor to transform font units into pixels. font.size / font.opentype.head.unitsPerEm.float32 @@ -104,6 +110,9 @@ proc typeset*( if rune.canWrap(): prevCanWrap = i + if i > 0: + at.x += font.getKerningAdjustment(runes[i - 1], rune) * font.scale + let advance = font.getGlyphAdvance(rune) * font.scale if bounds.x > 0 and at.x + advance > bounds.x: # Wrap to new line at.x = 0 @@ -112,6 +121,8 @@ proc typeset*( # Go back and wrap glyphs after the wrap index down to the next line if prevCanWrap > 0 and prevCanWrap != i: for j in prevCanWrap + 1 ..< i: + if j > 0: + at.x += font.getKerningAdjustment(runes[j - 1], runes[j]) * font.scale positions[j] = at at.x += font.getGlyphAdvance(runes[j]) * font.scale @@ -129,5 +140,16 @@ proc parseOtf*(buf: string): Font = result.size = 12 result.lineHeight = AutoLineHeight + if result.opentype.kern != nil: + for table in result.opentype.kern.subTables: + for pair in table.kernPairs: + if pair.value != 0 and + pair.left in result.opentype.cmap.glyphIdToRune and + pair.right in result.opentype.cmap.glyphIdToRune: + result.kerningPairs[( + result.opentype.cmap.glyphIdToRune[pair.left], + result.opentype.cmap.glyphIdToRune[pair.right] + )] = pair.value.float32 + proc parseTtf*(buf: string): Font = parseOtf(buf) diff --git a/tests/fonts/diffs/basic6.png b/tests/fonts/diffs/basic6.png index 88724dafc8b242be3911a1483b1f9b33d1fe4531..8e468a675c0679d1f490c5b4c4c2c770bd5c4a9b 100644 GIT binary patch literal 4041 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 H-Qxcbsqji4 literal 4138 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 diff --git a/tests/fonts/rendered/basic6.png b/tests/fonts/rendered/basic6.png index 51e6a5f27d06a3e811d9e5e88a0ad602f1a7521b..8388fd2863706ef8d22af0e90eb33646d9a64a1f 100644 GIT binary patch delta 4015 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