diff --git a/lib/font/font.go b/lib/font/font.go new file mode 100644 index 000000000..5f1141a1f --- /dev/null +++ b/lib/font/font.go @@ -0,0 +1,214 @@ +package font + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "fmt" + "math" + "sort" +) + +var ( + // sfnt2Woff offset + SFNT_OFFSET_TAG = 0 + SFNT_OFFSET_CHECKSUM = 4 + SFNT_OFFSET_OFFSET = 8 + SFNT_OFFSET_LENGTH = 12 + + // sfnt2Woff entry offset + SFNT_ENTRY_OFFSET_FLAVOR = 0 + SFNT_ENTRY_OFFSET_VERSION_MAJ = 4 + SFNT_ENTRY_OFFSET_VERSION_MIN = 6 + SFNT_ENTRY_OFFSET_CHECKSUM_ADJUSTMENT = 8 + + // woff offset + WOFF_OFFSET_MAGIC = 0 + WOFF_OFFSET_FLAVOR = 4 + WOFF_OFFSET_SIZE = 8 + WOFF_OFFSET_NUM_TABLES = 12 + WOFF_OFFSET_RESERVED = 14 + WOFF_OFFSET_SFNT_SIZE = 16 + WOFF_OFFSET_VERSION_MAJ = 20 + WOFF_OFFSET_VERSION_MIN = 22 + WOFF_OFFSET_META_OFFSET = 24 + WOFF_OFFSET_META_LENGTH = 28 + WOFF_OFFSET_META_ORIG_LENGTH = 32 + WOFF_OFFSET_PRIV_OFFSET = 36 + WOFF_OFFSET_PRIV_LENGTH = 40 + + // woff entry offset + WOFF_ENTRY_OFFSET_TAG = 0 + WOFF_ENTRY_OFFSET_OFFSET = 4 + WOFF_ENTRY_OFFSET_COMPR_LENGTH = 8 + WOFF_ENTRY_OFFSET_LENGTH = 12 + WOFF_ENTRY_OFFSET_CHECKSUM = 16 + + // magic + MAGIC_WOFF = 0x774f4646 + MAGIC_CHECKSUM_ADJUSTMENT = 0xb1b0afba + + // sizes + SIZE_OF_WOFF_HEADER = 44 + SIZE_OF_WOFF_ENTRY = 20 + SIZE_OF_SFNT_HEADER = 12 + SIZE_OF_SFNT_TABLE_ENTRY = 16 +) + +type TableEntry struct { + Tag []byte + CheckSum uint32 + Offset uint32 + Length uint32 +} + +func longAlign(n uint32) uint32 { + return (n + 3) & ^uint32(3) +} + +func calcChecksum(buf []byte) uint32 { + var sum uint32 = 0 + var nlongs = len(buf) / 4 + + for i := 0; i < nlongs; i++ { + var t = binary.BigEndian.Uint32(buf[i*4:]) + sum = sum + t + } + return sum +} + +// Sfnt2Woff is a native go port of the JS library +// https://github.com/fontello/ttf2woff +// that converts sfnt fonts (.ttf and .otf) to .woff fonts +func Sfnt2Woff(fontBuf []byte) ([]byte, error) { + numTables := binary.BigEndian.Uint16(fontBuf[4:]) + + woffHeader := make([]byte, SIZE_OF_WOFF_HEADER) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_MAGIC:], uint32(MAGIC_WOFF)) + binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_NUM_TABLES:], numTables) + binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_SFNT_SIZE:], 0) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_META_OFFSET:], 0) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_META_LENGTH:], 0) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_META_ORIG_LENGTH:], 0) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_PRIV_OFFSET:], 0) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_PRIV_LENGTH:], 0) + + var entries []TableEntry + + for i := 0; i < int(numTables); i++ { + table := fontBuf[SIZE_OF_SFNT_HEADER+i*SIZE_OF_SFNT_TABLE_ENTRY:] + + entry := TableEntry{ + Tag: table[SFNT_OFFSET_TAG : SFNT_OFFSET_TAG+4], + CheckSum: binary.BigEndian.Uint32(table[SFNT_OFFSET_CHECKSUM:]), + Offset: binary.BigEndian.Uint32(table[SFNT_OFFSET_OFFSET:]), + Length: binary.BigEndian.Uint32(table[SFNT_OFFSET_LENGTH:]), + } + + entries = append(entries, entry) + } + sort.Slice(entries, func(i, j int) bool { + return string(entries[i].Tag) < string(entries[j].Tag) + }) + + sfntSize := uint32(SIZE_OF_SFNT_HEADER + int(numTables)*SIZE_OF_SFNT_TABLE_ENTRY) + tableInfo := make([]byte, int(numTables)*SIZE_OF_WOFF_ENTRY) + + for i := 0; i < len(entries); i++ { + tableEntry := entries[i] + if string(tableEntry.Tag) != "head" { + alignTable := fontBuf[tableEntry.Offset : tableEntry.Offset+longAlign(tableEntry.Length)] + + if calcChecksum(alignTable) != tableEntry.CheckSum { + return nil, fmt.Errorf("checksum error in table: %v", string(tableEntry.Tag)) + } + } + + binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_TAG:], binary.BigEndian.Uint32(tableEntry.Tag)) + binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_LENGTH:], tableEntry.Length) + binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_CHECKSUM:], tableEntry.CheckSum) + + sfntSize += longAlign(tableEntry.Length) + } + + sfntOffset := uint32(SIZE_OF_SFNT_HEADER + len(entries)*SIZE_OF_SFNT_TABLE_ENTRY) + csum := calcChecksum(fontBuf[:SIZE_OF_SFNT_HEADER]) + for i := 0; i < len(entries); i++ { + tableEntry := entries[i] + + b := make([]byte, SIZE_OF_SFNT_TABLE_ENTRY) + binary.BigEndian.PutUint32(b[SFNT_OFFSET_TAG:], binary.BigEndian.Uint32(tableEntry.Tag)) + binary.BigEndian.PutUint32(b[SFNT_OFFSET_CHECKSUM:], tableEntry.CheckSum) + binary.BigEndian.PutUint32(b[SFNT_OFFSET_OFFSET:], sfntOffset) + binary.BigEndian.PutUint32(b[SFNT_OFFSET_LENGTH:], tableEntry.Length) + + sfntOffset += longAlign(tableEntry.Length) + csum += calcChecksum(b) + csum += tableEntry.CheckSum + } + + var checksumAdjustment = uint32(MAGIC_CHECKSUM_ADJUSTMENT) - csum + + majorVersion := uint16(0) + minVersion := uint16(1) + flavor := uint32(0) + offset := SIZE_OF_WOFF_HEADER + int(numTables)*SIZE_OF_WOFF_ENTRY + var tableBytes []byte + + for i := 0; i < len(entries); i++ { + tableEntry := entries[i] + + sfntData := fontBuf[tableEntry.Offset : tableEntry.Offset+tableEntry.Length] + if string(tableEntry.Tag) == "head" { + majorVersion = binary.BigEndian.Uint16(sfntData[SFNT_ENTRY_OFFSET_VERSION_MAJ:]) + minVersion = binary.BigEndian.Uint16(sfntData[SFNT_ENTRY_OFFSET_VERSION_MIN:]) + flavor = binary.BigEndian.Uint32(sfntData[SFNT_ENTRY_OFFSET_FLAVOR:]) + binary.BigEndian.PutUint32(sfntData[SFNT_ENTRY_OFFSET_CHECKSUM_ADJUSTMENT:], uint32(checksumAdjustment)) + } + + var res bytes.Buffer + w := zlib.NewWriter(&res) + w.Write(sfntData) + w.Flush() + w.Close() + + compLength := math.Min(float64(len(res.Bytes())), float64(len(sfntData))) + length := longAlign(uint32(compLength)) + + table := make([]byte, length) + // only deflate data if the deflated data is actually smaller + if len(res.Bytes()) >= len(sfntData) { + copy(table, sfntData) + } else { + copy(table, res.Bytes()) + } + + binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_OFFSET:], uint32(offset)) + + offset += len(table) + + binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_COMPR_LENGTH:], uint32(compLength)) + + tableBytes = append(tableBytes, table...) + + } + + woffSize := uint32(len(woffHeader) + len(tableInfo) + len(tableBytes)) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_SIZE:], woffSize) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_SFNT_SIZE:], sfntSize) + binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_VERSION_MAJ:], majorVersion) + binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_VERSION_MIN:], minVersion) + binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_FLAVOR:], flavor) + + var out []byte + out = append(out, woffHeader...) + out = append(out, tableInfo...) + out = append(out, tableBytes...) + + return out, nil +} + +// gofpdf subset only accepts .ttf fonts +// func Subset(fontBuf []byte, cutset string) []byte { +// return gofpdf.UTF8CutFont(fontBuf, cutset) +// }