Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8caddd9
upgrade to Unicode 17
benoitkugler Jan 16, 2026
f1bba90
port latest changes of Harfbuzz upstream
benoitkugler Jan 16, 2026
82d1106
add support for STAT table
benoitkugler Mar 29, 2024
87531b0
add STAT table in Font object
benoitkugler Jan 16, 2026
2689d3a
testing & debugging
benoitkugler Jan 17, 2026
3189e76
some cleanup
benoitkugler Jan 17, 2026
d1cc145
test and fixes
benoitkugler Jan 19, 2026
57a437e
fix slice growth
benoitkugler Jan 21, 2026
c371b35
fixes slanting
benoitkugler Jan 21, 2026
af1f4da
fixes test
benoitkugler Jan 22, 2026
51a635d
debug test
benoitkugler Jan 22, 2026
5332786
clarify GSUB ligature
benoitkugler Jan 22, 2026
79afa4d
fix default value in test
benoitkugler Jan 23, 2026
86f45b5
restore non pointer field
benoitkugler Jan 23, 2026
814f4df
test & fix
benoitkugler Jan 24, 2026
7e16e9a
fix benchmarks
benoitkugler Jan 24, 2026
672164d
hide api
benoitkugler Jan 26, 2026
7a7055b
bump version
benoitkugler Jan 26, 2026
932aa24
minor format tweak
benoitkugler Jan 26, 2026
4221e48
blocklist invalid GDEF tables
benoitkugler Jan 26, 2026
0d026f0
cleanup duplicate test
benoitkugler Jan 27, 2026
d9a04a5
WIP : upgrade UAX segmenter to Unicode 17
benoitkugler Feb 2, 2026
b2eeaae
fix grapheme segmenter
benoitkugler Feb 2, 2026
45023d1
[shaping] use constructor in test, now mandatory since cmap is cached
benoitkugler Feb 2, 2026
b641d56
move unicodedata to internal/unicodedata
benoitkugler Feb 2, 2026
5230d65
bump typesetting dep
benoitkugler Feb 3, 2026
31a14d4
fix staticcheck warning
benoitkugler Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions font/aat_layout_kern_kerx.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type KernSubtable struct {
// high bit of the Coverage field, following 'kerx' conventions
coverage byte

// IsExtended [true] for AAT `kerx` subtables, false for 'kern' subtables
// IsExtended is [true] for AAT `kerx` subtables, false for 'kern' subtables
IsExtended bool

// 0 for scalar values
Expand Down Expand Up @@ -199,7 +199,7 @@ func newKern1(k tables.KernData1) Kern1 {
Values: k.Values,
Machine: AATStateTable{
nClass: uint32(k.StateSize),
class: class,
Class: class,
states: states,
entries: k.Entries,
},
Expand Down Expand Up @@ -282,15 +282,15 @@ func (kd Kern6) KernPair(left, right GID) int16 {
// AATStateTable supports both regular and extended AAT state machines
type AATStateTable struct {
nClass uint32
class tables.AATLookup
Class tables.AATLookup
states [][]uint16 // each sub array has length stateSize
entries []tables.AATStateEntry // length is the maximum state + 1
}

func newAATStableTable(k tables.AATStateTableExt) AATStateTable {
return AATStateTable{
nClass: k.StateSize,
class: k.Class,
Class: k.Class,
states: k.States,
entries: k.Entries,
}
Expand All @@ -301,7 +301,7 @@ func (st *AATStateTable) GetClass(glyph GID) uint16 {
if glyph == 0xFFFF { // deleted glyph
return 2 // class deleted
}
c, ok := st.class.Class(tables.GlyphID(glyph))
c, ok := st.Class.Class(tables.GlyphID(glyph))
if !ok {
return 1 // class out of bounds
}
Expand Down
195 changes: 192 additions & 3 deletions font/cmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package font
import (
"encoding/binary"
"errors"
"sort"

"github.com/go-text/typesetting/font/opentype/tables"
)
Expand Down Expand Up @@ -145,6 +146,17 @@ func ProcessCmap(cmap tables.Cmap, os2FontPage tables.FontPage) (Cmap, UnicodeVa
return candidates[index], uv, nil
}

/* MacRoman subtable. */
if index := findSubtable(cmapID{tables.PlatformMac, 0}, candidateIds); index != -1 {
cm := candidates[index]
return remaperMacroman{cm}, uv, nil
}
/* Any other Mac subtable; we just map ASCII for these. */
if index := findSubtable(cmapID{tables.PlatformMac, 0xFFFF}, candidateIds); index != -1 {
cm := candidates[index]
return remaperAscii{cm}, uv, nil
}

// uuh... fallback to the first cmap and hope for the best
if len(candidates) != 0 {
return candidates[0], uv, nil
Expand All @@ -158,15 +170,22 @@ type cmapID struct {
encoding tables.EncodingID
}

func (c cmapID) key() uint32 { return uint32(c.platform)<<16 | uint32(c.encoding) }
func (c cmapID) key(ignoreEncoding bool) uint32 {
if ignoreEncoding {
c.encoding = 0
}
return uint32(c.platform)<<16 | uint32(c.encoding)
}

// findSubtable returns the cmap index for the given platform and encoding, or -1 if not found.
// as a special case, if [id.encoding] is 0xFFFF, encoding is ignored
func findSubtable(id cmapID, cmaps []cmapID) int {
key := id.key()
ignoreEncoding := id.encoding == 0xFFFF
key := id.key(ignoreEncoding)
// binary search
for i, j := 0, len(cmaps); i < j; {
h := i + (j-i)/2
entryKey := cmaps[h].key()
entryKey := cmaps[h].key(ignoreEncoding)
if key < entryKey {
j = h
} else if entryKey < key {
Expand Down Expand Up @@ -639,6 +658,176 @@ func (rs remaperPUATrad) Lookup(r rune) (GID, bool) {
return 0, false
}

type remaperAscii struct {
Cmap
}

func lookupAscii(cmap Cmap, r rune) (GID, bool) {
if r < 0x80 {
return cmap.Lookup(r)
}
return 0, false
}

func (rs remaperAscii) Lookup(r rune) (GID, bool) { return lookupAscii(rs.Cmap, r) }

type remaperMacroman struct {
Cmap
}

func (rs remaperMacroman) Lookup(r rune) (GID, bool) {
if g, ok := lookupAscii(rs.Cmap, r); ok {
return g, ok
}
if mapped := unicodeToMacroman(r); mapped != 0 {
return rs.Cmap.Lookup(mapped)
}

return 0, false
}

// assume u is not in ASCII range
func unicodeToMacroman(u rune) rune {
mapping := [...]struct {
unicode uint16
macroman uint8
}{
{0x00A0, 0xCA},
{0x00A1, 0xC1},
{0x00A2, 0xA2},
{0x00A3, 0xA3},
{0x00A5, 0xB4},
{0x00A7, 0xA4},
{0x00A8, 0xAC},
{0x00A9, 0xA9},
{0x00AA, 0xBB},
{0x00AB, 0xC7},
{0x00AC, 0xC2},
{0x00AE, 0xA8},
{0x00AF, 0xF8},
{0x00B0, 0xA1},
{0x00B1, 0xB1},
{0x00B4, 0xAB},
{0x00B5, 0xB5},
{0x00B6, 0xA6},
{0x00B7, 0xE1},
{0x00B8, 0xFC},
{0x00BA, 0xBC},
{0x00BB, 0xC8},
{0x00BF, 0xC0},
{0x00C0, 0xCB},
{0x00C1, 0xE7},
{0x00C2, 0xE5},
{0x00C3, 0xCC},
{0x00C4, 0x80},
{0x00C5, 0x81},
{0x00C6, 0xAE},
{0x00C7, 0x82},
{0x00C8, 0xE9},
{0x00C9, 0x83},
{0x00CA, 0xE6},
{0x00CB, 0xE8},
{0x00CC, 0xED},
{0x00CD, 0xEA},
{0x00CE, 0xEB},
{0x00CF, 0xEC},
{0x00D1, 0x84},
{0x00D2, 0xF1},
{0x00D3, 0xEE},
{0x00D4, 0xEF},
{0x00D5, 0xCD},
{0x00D6, 0x85},
{0x00D8, 0xAF},
{0x00D9, 0xF4},
{0x00DA, 0xF2},
{0x00DB, 0xF3},
{0x00DC, 0x86},
{0x00DF, 0xA7},
{0x00E0, 0x88},
{0x00E1, 0x87},
{0x00E2, 0x89},
{0x00E3, 0x8B},
{0x00E4, 0x8A},
{0x00E5, 0x8C},
{0x00E6, 0xBE},
{0x00E7, 0x8D},
{0x00E8, 0x8F},
{0x00E9, 0x8E},
{0x00EA, 0x90},
{0x00EB, 0x91},
{0x00EC, 0x93},
{0x00ED, 0x92},
{0x00EE, 0x94},
{0x00EF, 0x95},
{0x00F1, 0x96},
{0x00F2, 0x98},
{0x00F3, 0x97},
{0x00F4, 0x99},
{0x00F5, 0x9B},
{0x00F6, 0x9A},
{0x00F7, 0xD6},
{0x00F8, 0xBF},
{0x00F9, 0x9D},
{0x00FA, 0x9C},
{0x00FB, 0x9E},
{0x00FC, 0x9F},
{0x00FF, 0xD8},
{0x0131, 0xF5},
{0x0152, 0xCE},
{0x0153, 0xCF},
{0x0178, 0xD9},
{0x0192, 0xC4},
{0x02C6, 0xF6},
{0x02C7, 0xFF},
{0x02D8, 0xF9},
{0x02D9, 0xFA},
{0x02DA, 0xFB},
{0x02DB, 0xFE},
{0x02DC, 0xF7},
{0x02DD, 0xFD},
{0x03A9, 0xBD},
{0x03C0, 0xB9},
{0x2013, 0xD0},
{0x2014, 0xD1},
{0x2018, 0xD4},
{0x2019, 0xD5},
{0x201A, 0xE2},
{0x201C, 0xD2},
{0x201D, 0xD3},
{0x201E, 0xE3},
{0x2020, 0xA0},
{0x2021, 0xE0},
{0x2022, 0xA5},
{0x2026, 0xC9},
{0x2030, 0xE4},
{0x2039, 0xDC},
{0x203A, 0xDD},
{0x2044, 0xDA},
{0x20AC, 0xDB},
{0x2122, 0xAA},
{0x2202, 0xB6},
{0x2206, 0xC6},
{0x220F, 0xB8},
{0x2211, 0xB7},
{0x221A, 0xC3},
{0x221E, 0xB0},
{0x222B, 0xBA},
{0x2248, 0xC5},
{0x2260, 0xAD},
{0x2264, 0xB2},
{0x2265, 0xB3},
{0x25CA, 0xD7},
{0xF8FF, 0xF0},
{0xFB01, 0xDE},
{0xFB02, 0xDF},
}
i := sort.Search(len(mapping), func(i int) bool { return u <= rune(mapping[i].unicode) })
if i < len(mapping) && rune(mapping[i].unicode) == u {
return rune(mapping[i].macroman)
}
return 0
}

// ---------------------------- efficent rune set support -----------------------------------------

// CmapRuneRanger is implemented by cmaps whose coverage is defined in terms
Expand Down
70 changes: 70 additions & 0 deletions font/cmap_cache.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions font/cmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,11 @@ func assertRuneRangesEqual(t *testing.T, cm Cmap) {
t.Fatal("inconsistent rune ranges")
}
}

func TestMacromanCmap(t *testing.T) {
ld := readFontFile(t, "cmap/Brushstroke-Plain.otf")
ft, err := NewFont(ld)
tu.AssertNoErr(t, err)
_, ok := ft.Cmap.(remaperMacroman)
tu.Assert(t, ok)
}
Loading