From 825df63c35553d6c4c05505727ae5079794c9c6f Mon Sep 17 00:00:00 2001 From: Benoit KUGLER Date: Thu, 12 Oct 2023 14:13:47 +0200 Subject: [PATCH] [opentype] support packing font tables into a .ttf file --- opentype/loader/reader.go | 12 +++++ opentype/loader/reader_otf.go | 9 +++- opentype/loader/writer.go | 85 ++++++++++++++++++++++++++++++++++ opentype/loader/writer_test.go | 38 +++++++++++++++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 opentype/loader/writer.go create mode 100644 opentype/loader/writer_test.go diff --git a/opentype/loader/reader.go b/opentype/loader/reader.go index 1080a35a..1a940293 100644 --- a/opentype/loader/reader.go +++ b/opentype/loader/reader.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "sort" ) var ( @@ -161,6 +162,17 @@ func (pr *Loader) HasTable(table Tag) bool { return has } +// Tables returns all the tables found in the file, +// as a sorted slice. +func (ld *Loader) Tables() []Tag { + out := make([]Tag, 0, len(ld.tables)) + for tag := range ld.tables { + out = append(out, tag) + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} + // RawTable returns the binary content of the given table, // or an error if not found. func (pr *Loader) RawTable(tag Tag) ([]byte, error) { diff --git a/opentype/loader/reader_otf.go b/opentype/loader/reader_otf.go index 1539fac3..d1620963 100644 --- a/opentype/loader/reader_otf.go +++ b/opentype/loader/reader_otf.go @@ -17,8 +17,13 @@ type otfEntry struct { Length uint32 } +const ( + otfHeaderSize = 12 + otfEntrySize = 16 +) + func readOTFHeader(r io.Reader) (flavor Tag, numTables uint16, err error) { - var buf [12]byte + var buf [otfHeaderSize]byte if _, err := r.Read(buf[:]); err != nil { return 0, 0, fmt.Errorf("invalid OpenType header: %s", err) } @@ -28,7 +33,7 @@ func readOTFHeader(r io.Reader) (flavor Tag, numTables uint16, err error) { func readOTFEntry(r io.Reader) (otfEntry, error) { var ( - buf [16]byte + buf [otfEntrySize]byte entry otfEntry ) if _, err := io.ReadFull(r, buf[:]); err != nil { diff --git a/opentype/loader/writer.go b/opentype/loader/writer.go new file mode 100644 index 00000000..8f106df5 --- /dev/null +++ b/opentype/loader/writer.go @@ -0,0 +1,85 @@ +package loader + +import ( + "encoding/binary" + "math" +) + +// Table is one opentype binary table and its tag. +type Table struct { + Content []byte + Tag Tag +} + +// Write creates a single font file from the given [tables] slice, +// which must be sorted by Tag +func Write(tables []Table) []byte { + buffer := writeTTF(tables) + return buffer +} + +func writeTTF(tables []Table) []byte { + introLength := uint32(otfHeaderSize + len(tables)*otfEntrySize) + buffer := make([]byte, introLength) + + writeTTFHeader(len(tables), buffer) + + tableOffset := introLength // the actual content will start after the header + table directory + for i, table := range tables { + cs := checksum(table.Content) + tableLength := uint32(len(table.Content)) + + slice := buffer[otfHeaderSize+i*otfEntrySize:] + binary.BigEndian.PutUint32(slice, uint32(table.Tag)) + binary.BigEndian.PutUint32(slice[4:], cs) + binary.BigEndian.PutUint32(slice[8:], tableOffset) + binary.BigEndian.PutUint32(slice[12:], tableLength) + + // update the offset + tableOffset = tableOffset + tableLength + } + + // append the actual table content : + // allocate only once + buffer = append(buffer, make([]byte, tableOffset-introLength)...) + tableOffset = introLength + for _, table := range tables { + copy(buffer[tableOffset:], table.Content) + tableOffset = tableOffset + uint32(len(table.Content)) + } + + return buffer +} + +// out is assumed to have a length >= ttfHeaderSize +func writeTTFHeader(nTables int, out []byte) { + log2 := math.Floor(math.Log2(float64(nTables))) + // Maximum power of 2 less than or equal to numTables, times 16 ((2**floor(log2(numTables))) * 16, where “**” is an exponentiation operator). + searchRange := math.Pow(2, log2) * 16 + // Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/16), which is equal to floor(log2(numTables))). + entrySelector := log2 + // numTables times 16, minus searchRange ((numTables * 16) - searchRange). + rangeShift := nTables*16 - int(searchRange) + + binary.BigEndian.PutUint32(out[:], uint32(TrueType)) + binary.BigEndian.PutUint16(out[4:], uint16(nTables)) + binary.BigEndian.PutUint16(out[6:], uint16(searchRange)) + binary.BigEndian.PutUint16(out[8:], uint16(entrySelector)) + binary.BigEndian.PutUint16(out[10:], uint16(rangeShift)) +} + +func checksum(table []byte) uint32 { + // "To accommodate data with a length that is not a multiple of four, + // the above algorithm must be modified to treat the data as though + // it contains zero padding to a length that is a multiple of four." + if r := len(table) % 4; r != 0 { + table = append(table, make([]byte, r)...) + } + + var sum uint32 + for i := 0; i < len(table)/4; i++ { + sum += binary.BigEndian.Uint32(table[i*4:]) + } + + return sum +} diff --git a/opentype/loader/writer_test.go b/opentype/loader/writer_test.go new file mode 100644 index 00000000..9768c82f --- /dev/null +++ b/opentype/loader/writer_test.go @@ -0,0 +1,38 @@ +package loader + +import ( + "bytes" + "testing" + + td "github.com/go-text/typesetting-utils/opentype" + tu "github.com/go-text/typesetting/opentype/testutils" +) + +func TestWrite(t *testing.T) { + for _, filename := range tu.Filenames(t, "common") { + f, err := td.Files.ReadFile(filename) + tu.AssertNoErr(t, err) + + font, err := NewLoader(bytes.NewReader(f)) + tu.AssertNoErr(t, err) + + tags := font.Tables() + tables := make([]Table, len(tags)) + for i, tag := range tags { + tables[i].Tag = tag + tables[i].Content, err = font.RawTable(tag) + tu.AssertNoErr(t, err) + } + + content := Write(tables) + font2, err := NewLoader(bytes.NewReader(content)) + tu.AssertNoErr(t, err) + + for _, table := range tables { + t2, err := font2.RawTable(table.Tag) + tu.AssertNoErr(t, err) + + tu.Assert(t, bytes.Equal(table.Content, t2)) + } + } +}