mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
361 lines
11 KiB
Go
361 lines
11 KiB
Go
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package font
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/go-text/typesetting/font/opentype/tables"
|
|
)
|
|
|
|
// SFNT 保存独立字体表和基础度量,不包含绘图库对象
|
|
type SFNT struct {
|
|
Tables map[string][]byte
|
|
IsTrueType, IsCFF bool
|
|
cmaps []sfntCmap
|
|
units, count, horizontal, vertical uint16
|
|
reverseOnce sync.Once
|
|
reverse map[uint16][]rune
|
|
}
|
|
|
|
// sfntCmap 保存编码记录及对应字符映射,标记已解析的共享子表
|
|
type sfntCmap struct {
|
|
platform, encoding, format uint16
|
|
mapping map[rune]uint16
|
|
duplicate bool
|
|
}
|
|
|
|
// ParseSFNT 校验独立字体的必需表、度量范围及字符映射
|
|
// 入参: data 独立SFNT数据
|
|
// 返回: *SFNT 字体, error 格式错误
|
|
func ParseSFNT(data []byte) (*SFNT, error) {
|
|
table, err := FileTables(data, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ParseSFNTTables(table)
|
|
}
|
|
|
|
// ParseSFNTTables 校验字体表并解析度量和字符映射,借用表数据且不修改其内容
|
|
// 入参: table 字体表
|
|
// 返回: *SFNT 字体, error 格式错误
|
|
func ParseSFNTTables(table map[string][]byte) (*SFNT, error) {
|
|
head, maxp, hhea := table["head"], table["maxp"], table["hhea"]
|
|
if len(head) != 54 || len(maxp) < 6 || len(hhea) < 36 {
|
|
return nil, fmt.Errorf("missing or truncated font metrics")
|
|
}
|
|
f := &SFNT{Tables: maps.Clone(table), IsTrueType: table["glyf"] != nil, IsCFF: table["CFF "] != nil}
|
|
f.units, f.count = binary.BigEndian.Uint16(head[18:]), binary.BigEndian.Uint16(maxp[4:])
|
|
f.horizontal = binary.BigEndian.Uint16(hhea[34:])
|
|
if f.units < 16 || f.units > 16384 || f.count == 0 || f.IsTrueType == f.IsCFF || !validSFNTMetrics(table["hmtx"], f.count, f.horizontal) {
|
|
return nil, fmt.Errorf("invalid font outlines or horizontal metrics")
|
|
}
|
|
if vhea := table["vhea"]; vhea != nil {
|
|
if len(vhea) < 36 {
|
|
return nil, fmt.Errorf("truncated vertical metrics header")
|
|
}
|
|
f.vertical = binary.BigEndian.Uint16(vhea[34:])
|
|
if !validSFNTMetrics(table["vmtx"], f.count, f.vertical) {
|
|
return nil, fmt.Errorf("invalid vertical metrics")
|
|
}
|
|
}
|
|
if f.IsTrueType {
|
|
format := binary.BigEndian.Uint16(head[50:])
|
|
if format > 1 || len(table["loca"]) < (int(f.count)+1)*(2+2*int(format)) {
|
|
return nil, fmt.Errorf("invalid glyph location table")
|
|
}
|
|
}
|
|
cmaps, err := readSFNTCmaps(table["cmap"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.cmaps = cmaps
|
|
for _, cmap := range f.cmaps {
|
|
if cmap.duplicate {
|
|
continue
|
|
}
|
|
for _, glyph := range cmap.mapping {
|
|
if glyph >= f.count {
|
|
return nil, fmt.Errorf("cmap glyph index out of range")
|
|
}
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// validSFNTMetrics 校验完整度量记录和末尾边距数组的范围
|
|
// 入参: data 度量表, count 字形数, metrics 完整度量记录数
|
|
// 返回: bool 是否有效
|
|
func validSFNTMetrics(data []byte, count, metrics uint16) bool {
|
|
return metrics != 0 && metrics <= count && len(data) >= int(metrics)*4+int(count-metrics)*2
|
|
}
|
|
|
|
// readSFNTCmaps 校验各编码子表并保留原顺序,共享子表只解析一次
|
|
// 入参: data cmap表
|
|
// 返回: []sfntCmap 编码记录, error 表结构错误
|
|
func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
|
data, err := NormalizeCmap(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, _, err := tables.ParseCmap(data); err != nil {
|
|
return nil, err
|
|
}
|
|
count := int(binary.BigEndian.Uint16(data[2:]))
|
|
result := make([]sfntCmap, 0, count)
|
|
parsed := make(map[uint32]map[rune]uint16)
|
|
for i := range count {
|
|
record := data[4+8*i:]
|
|
offset := binary.BigEndian.Uint32(record[4:])
|
|
if uint64(offset)+2 > uint64(len(data)) {
|
|
return nil, fmt.Errorf("invalid cmap offset")
|
|
}
|
|
sub := data[offset:]
|
|
format := binary.BigEndian.Uint16(sub)
|
|
mapping, found := parsed[offset]
|
|
if !found {
|
|
mapping = make(map[rune]uint16)
|
|
var err error
|
|
switch format {
|
|
case 0:
|
|
parseCmapFormat0(sub, mapping)
|
|
case 4:
|
|
parseCmapFormat4(sub, mapping)
|
|
case 6:
|
|
parseCmapFormat6(sub, mapping)
|
|
case 12:
|
|
parseCmapFormat12(sub, mapping)
|
|
case 2:
|
|
mapping, err = parseCmapFormat2(sub)
|
|
case 8, 10, 13:
|
|
mapping, err = parseCmapExtended(sub)
|
|
case 14:
|
|
default:
|
|
return nil, fmt.Errorf("unsupported cmap format %d", format)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsed[offset] = mapping
|
|
}
|
|
result = append(result, sfntCmap{platform: binary.BigEndian.Uint16(record), encoding: binary.BigEndian.Uint16(record[2:]), format: format, mapping: mapping, duplicate: found})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// UnitsPerEm 返回字体设计单位
|
|
// 返回: uint16 每em单位数
|
|
func (f *SFNT) UnitsPerEm() uint16 { return f.units }
|
|
|
|
// NumGlyphs 返回字形总数
|
|
// 返回: uint16 字形数
|
|
func (f *SFNT) NumGlyphs() uint16 { return f.count }
|
|
|
|
// CmapEntries 返回各编码记录的映射项总数,共享子表按记录分别计数
|
|
// 返回: uint64 用于缓存预算的映射项数量
|
|
func (f *SFNT) CmapEntries() uint64 {
|
|
var count uint64
|
|
for _, cmap := range f.cmaps {
|
|
count += uint64(len(cmap.mapping))
|
|
}
|
|
return count
|
|
}
|
|
|
|
// GlyphIndex 按编码表顺序查找字符,不替换缺失字形
|
|
// 入参: char 字符
|
|
// 返回: uint16 字形编号
|
|
func (f *SFNT) GlyphIndex(char rune) uint16 {
|
|
for _, cmap := range f.cmaps {
|
|
if cmap.duplicate {
|
|
continue
|
|
}
|
|
if gid := cmap.mapping[char]; gid != 0 {
|
|
return gid
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// glyphCharacters 返回指向指定字形的全部字符
|
|
// 入参: gid 字形编号
|
|
// 返回: []rune 排序后的字符
|
|
func (f *SFNT) glyphCharacters(gid uint16) []rune {
|
|
f.reverseOnce.Do(func() {
|
|
f.reverse = make(map[uint16][]rune)
|
|
seen := make(map[rune]bool)
|
|
for _, cmap := range f.cmaps {
|
|
if cmap.duplicate {
|
|
continue
|
|
}
|
|
for char, glyph := range cmap.mapping {
|
|
if glyph != 0 && !seen[char] {
|
|
f.reverse[glyph] = append(f.reverse[glyph], char)
|
|
seen[char] = true
|
|
}
|
|
}
|
|
}
|
|
for _, chars := range f.reverse {
|
|
slices.Sort(chars)
|
|
}
|
|
})
|
|
return f.reverse[gid]
|
|
}
|
|
|
|
// GlyphAdvance 返回水平字宽
|
|
// 入参: gid 字形编号
|
|
// 返回: uint16 字宽
|
|
func (f *SFNT) GlyphAdvance(gid uint16) uint16 {
|
|
if gid >= f.count {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint16(f.Tables["hmtx"][int(min(gid, f.horizontal-1))*4:])
|
|
}
|
|
|
|
// GlyphVerticalAdvance 返回纵向字距,无垂直度量时使用em高度
|
|
// 入参: gid 字形编号
|
|
// 返回: uint16 纵向字距
|
|
func (f *SFNT) GlyphVerticalAdvance(gid uint16) uint16 {
|
|
if f.vertical == 0 {
|
|
return f.units
|
|
}
|
|
if gid >= f.count {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint16(f.Tables["vmtx"][int(min(gid, f.vertical-1))*4:])
|
|
}
|
|
|
|
// sideBearing 返回指定方向的字形边距
|
|
// 入参: gid 字形编号, vertical 是否读取顶部边距
|
|
// 返回: int16 边距
|
|
func (f *SFNT) sideBearing(gid uint16, vertical bool) int16 {
|
|
data, count := f.Tables["hmtx"], f.horizontal
|
|
if vertical {
|
|
data, count = f.Tables["vmtx"], f.vertical
|
|
}
|
|
if count == 0 || gid >= f.count {
|
|
return 0
|
|
}
|
|
offset := int(gid)*4 + 2
|
|
if gid >= count {
|
|
offset = int(count)*4 + int(gid-count)*2
|
|
}
|
|
return int16(binary.BigEndian.Uint16(data[offset:]))
|
|
}
|
|
|
|
// VerticalMetrics 依据OS/2排版标志选择行度量,缺失时使用hhea
|
|
// 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙
|
|
func (f *SFNT) VerticalMetrics() (uint16, uint16, uint16) {
|
|
hhea, os2 := f.Tables["hhea"], f.Tables["OS/2"]
|
|
a, d, gap := int(int16(binary.BigEndian.Uint16(hhea[4:]))), int(int16(binary.BigEndian.Uint16(hhea[6:]))), int(int16(binary.BigEndian.Uint16(hhea[8:])))
|
|
if len(os2) >= 78 {
|
|
if binary.BigEndian.Uint16(os2[62:])&0x80 != 0 {
|
|
ta, td := int(int16(binary.BigEndian.Uint16(os2[68:]))), int(int16(binary.BigEndian.Uint16(os2[70:])))
|
|
if ta > 0 && td < 0 {
|
|
a, d, gap = ta, td, int(int16(binary.BigEndian.Uint16(os2[72:])))
|
|
}
|
|
} else {
|
|
wa, wd := int(binary.BigEndian.Uint16(os2[74:])), int(binary.BigEndian.Uint16(os2[76:]))
|
|
if wa != 0 && wd != 0 {
|
|
gap, a, d = a-d+gap-wa-wd, wa, -wd
|
|
}
|
|
}
|
|
}
|
|
return uint16(max(0, a)), uint16(max(0, -d)), uint16(min(65535, max(0, gap)))
|
|
}
|
|
|
|
// Write 保留原始表数据并重新计算字体校验和
|
|
// 返回: []byte 独立字体
|
|
func (f *SFNT) Write() []byte { data, _ := SerializeOTF(f.Tables); return data }
|
|
|
|
// glyphDependencies 批量收集复合字形依赖,校验引用、记录长度和嵌套深度
|
|
// 入参: glyphs 字形编号
|
|
// 返回: []uint16 依赖字形, error 字形错误
|
|
func (f *SFNT) glyphDependencies(glyphs ...uint16) ([]uint16, error) {
|
|
heights, active := make(map[uint16]int), make(map[uint16]bool)
|
|
var result []uint16
|
|
var visit func(uint16) error
|
|
visit = func(id uint16) error {
|
|
if id >= f.count || active[id] || len(active) > 64 {
|
|
return fmt.Errorf("invalid composite glyph reference")
|
|
}
|
|
if height := heights[id]; height != 0 {
|
|
if len(active)+height > 65 {
|
|
return fmt.Errorf("invalid composite glyph reference")
|
|
}
|
|
return nil
|
|
}
|
|
heights[id], active[id] = 1, true
|
|
defer delete(active, id)
|
|
data, err := trueTypeGlyphData(f.Tables, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
if len(data) < 10 {
|
|
return fmt.Errorf("truncated glyph header")
|
|
}
|
|
if int16(binary.BigEndian.Uint16(data)) >= 0 {
|
|
return nil
|
|
}
|
|
for pos := 10; ; {
|
|
if len(data)-pos < 4 {
|
|
return fmt.Errorf("truncated composite glyph")
|
|
}
|
|
flags, child := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
|
|
pos += 6
|
|
if flags&1 != 0 {
|
|
pos += 2
|
|
}
|
|
switch flags & 0xc8 {
|
|
case 8:
|
|
pos += 2
|
|
case 0x40:
|
|
pos += 4
|
|
case 0x80:
|
|
pos += 8
|
|
case 0:
|
|
default:
|
|
return fmt.Errorf("invalid composite transform")
|
|
}
|
|
if pos > len(data) {
|
|
return fmt.Errorf("truncated composite transform")
|
|
}
|
|
if heights[child] == 0 {
|
|
result = append(result, child)
|
|
}
|
|
if err := visit(child); err != nil {
|
|
return err
|
|
}
|
|
heights[id] = max(heights[id], heights[child]+1)
|
|
if flags&0x20 == 0 {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for _, id := range glyphs {
|
|
if err := visit(id); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|