Skip to content

Commit

Permalink
[opentype] support packing font tables into a .ttf file
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Oct 12, 2023
1 parent afa02a8 commit 825df63
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 2 deletions.
12 changes: 12 additions & 0 deletions opentype/loader/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"sort"
)

var (
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions opentype/loader/reader_otf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
85 changes: 85 additions & 0 deletions opentype/loader/writer.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions opentype/loader/writer_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
}

0 comments on commit 825df63

Please sign in to comment.