From 62c81e7061ae4743fb659c3a2e7a9e25a145e526 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 11 Oct 2026 01:22:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E5=A4=84=E7=90=86):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AD=97=E4=BD=93=E7=BC=96=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=97=E4=BD=93=E8=A3=81=E5=89=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font_cff.go | 9 +- ofdgo_font_cff_subset.go | 225 ++++++++++++++++++++++++++++++++++++ ofdgo_font_resource_sfnt.go | 37 +++++- ofdgo_font_type2.go | 6 +- ofdgo_font_type2_stack.go | 3 + 5 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 ofdgo_font_cff_subset.go diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index d0be8d3..7e404cd 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -725,7 +725,7 @@ func parseCFFReal(data []byte) (string, int) { return sb.String(), i } -// encodeCFFDict 编码CFF字典 (仅使用float64操作数) +// encodeCFFDict 编码CFF字典,将字体类型标识置于首项 // 入参: dict CFF字典映射 // 返回: []byte 编码后的字典数据 func encodeCFFDict(dict cffDict) []byte { @@ -735,6 +735,13 @@ func encodeCFFDict(dict cffDict) []byte { keys = append(keys, k) } sort.Ints(keys) + for index, op := range keys { + if op == 1220 || op == 1230 { + copy(keys[1:index+1], keys[:index]) + keys[0] = op + break + } + } for _, op := range keys { vals := dict[op] if len(vals) == 0 && (op >= 6 && op <= 9 || op == 1212 || op == 1213) { diff --git a/ofdgo_font_cff_subset.go b/ofdgo_font_cff_subset.go new file mode 100644 index 0000000..5208437 --- /dev/null +++ b/ofdgo_font_cff_subset.go @@ -0,0 +1,225 @@ +// 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 ofdgo + +import ( + "bytes" + "encoding/binary" + "fmt" + "maps" +) + +// subsetNamedCFFFont 裁剪非CID的CFF字体,保留字形编号、组合组件和度量 +// 入参: sfnt 已解析字体, glyphs 使用的字形 +// 返回: []byte 字体子集, error 裁剪错误 +func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { + keep := map[uint16]bool{0: true} + for _, gid := range glyphs { + if gid >= sfnt.NumGlyphs() { + return nil, fmt.Errorf("CFF glyph index out of range") + } + keep[gid] = true + } + data, err := subsetCFFCharstrings(sfnt.Tables["CFF "], keep) + if err != nil { + return bytes.Clone(sfnt.Write()), nil + } + tables := maps.Clone(sfnt.Tables) + tables["CFF "] = data + mapping := make(map[rune]uint16) + for gid := range keep { + for _, char := range sfnt.glyphCharacters(gid) { + mapping[char] = gid + } + } + tables["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) + for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} { + delete(tables, tag) + } + return serializeOTF(tables) +} + +// subsetCFFCharstrings 清空未引用的Type2轮廓,保留原字宽、提示和子程序 +// 入参: data 非CID的CFF数据, keep 保留的字形,调用后包含组合组件 +// 返回: []byte 重建的CFF数据, error 结构或依赖错误 +func subsetCFFCharstrings(data []byte, keep map[uint16]bool) ([]byte, error) { + font, err := readType2Font(data) + if err != nil { + return nil, err + } + keep[0] = true + for gid := range keep { + if int(gid) >= len(font.chars) { + return nil, fmt.Errorf("CFF glyph index out of range") + } + steps := 0 + state := type2State{font: font, width: font.def, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep} + if _, err := state.run(font.chars[gid], 0); err != nil { + return nil, err + } + } + chars := make([][]byte, len(font.chars)) + empty := make(map[float64][]byte) + for gid, program := range font.chars { + if keep[uint16(gid)] { + chars[gid] = program + continue + } + steps := 0 + state := type2State{font: font, width: font.def, widthOnly: true, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep} + if _, err := state.run(program, 0); err != nil { + return nil, err + } + if encoded, ok := empty[state.width]; ok { + chars[gid] = encoded + continue + } + var encoded bytes.Buffer + if state.width != font.def { + value := state.width - font.nominal + if err := encodeType2Number(&encoded, value); err != nil { + return nil, err + } + if parseNumberType2(encoded.Bytes(), 0) != value { + return nil, fmt.Errorf("CFF width cannot be represented exactly") + } + } + encoded.WriteByte(14) + chars[gid] = encoded.Bytes() + empty[state.width] = chars[gid] + } + return rebuildNamedCFF(data, font, chars) +} + +// rebuildNamedCFF 重建非CID的CFF偏移,保留字符集、编码和私有提示参数 +// 入参: data 原始CFF数据, font 执行资源, chars 替换后的字形程序 +// 返回: []byte 重建的CFF数据, error 不支持的结构 +func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, error) { + names, end, err := readType2Index(data, int(data[2])) + if err != nil { + return nil, err + } + tops, end, err := readType2Index(data, end) + if err != nil || len(tops) != 1 { + return nil, fmt.Errorf("invalid CFF top dictionary") + } + strings, _, err := readType2Index(data, end) + if err != nil { + return nil, err + } + dict, err := readCFFDict(tops[0]) + if err != nil { + return nil, err + } + for op := range dict { + switch op { + case 0, 1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1222, 1223: + default: + return nil, fmt.Errorf("unsupported CFF top operator %d", op) + } + } + sids, _, _, _, ok := getCFFCharsetInfo(data, len(chars)) + if !ok { + return nil, fmt.Errorf("invalid CFF charset") + } + charset := []byte{0} + for _, sid := range sids[1:] { + charset = binary.BigEndian.AppendUint16(charset, uint16(sid)) + } + encoding, err := cffSubsetEncoding(data, dict) + if err != nil { + return nil, err + } + private, err := readType2Private(data, dict[18], font) + if err != nil { + return nil, err + } + for op := range private { + switch op { + case 6, 7, 8, 9, 10, 11, 19, 20, 21, 1209, 1210, 1211, 1212, 1213, 1214, 1217, 1218, 1219: + default: + return nil, fmt.Errorf("unsupported CFF private operator %d", op) + } + } + privateData := encodeCFFDict(private) + if _, ok := private[19]; ok { + for private[19][0] != float64(len(privateData)) { + private[19] = []float64{float64(len(privateData))} + privateData = encodeCFFDict(private) + } + } + prefix := bytes.Clone(data[:int(data[2])]) + prefix = append(prefix, encodeCFFIndex(names)...) + suffix := encodeCFFIndex(strings) + suffix = append(suffix, encodeCFFIndex(font.globals)...) + encodedChars := encodeCFFIndex(chars) + var top []byte + for attempt := 0; attempt < 16; attempt++ { + top = encodeCFFIndex([][]byte{encodeCFFDict(dict)}) + offset := len(prefix) + len(top) + len(suffix) + updated := maps.Clone(dict) + updated[15] = []float64{float64(offset)} + offset += len(charset) + if encoding != nil { + updated[16] = []float64{float64(offset)} + } + offset += len(encoding) + updated[17] = []float64{float64(offset)} + updated[18] = []float64{float64(len(privateData)), float64(offset + len(encodedChars))} + encoded := encodeCFFIndex([][]byte{encodeCFFDict(updated)}) + if len(encoded) == len(top) { + result := prefix + for _, part := range [][]byte{encoded, suffix, charset, encoding, encodedChars, privateData, encodeCFFIndex(font.locals)} { + result = append(result, part...) + } + return result, nil + } + dict = updated + } + return nil, fmt.Errorf("CFF dictionary offsets did not converge") +} + +// cffSubsetEncoding 读取自定义CFF编码及补充映射,预定义编码无需复制 +// 入参: data CFF数据, dict 顶层字典 +// 返回: []byte 编码数据, error 编码格式错误 +func cffSubsetEncoding(data []byte, dict cffDict) ([]byte, error) { + values := dict[16] + if values == nil || len(values) == 1 && (values[0] == 0 || values[0] == 1) { + return nil, nil + } + start, err := cffDictOffset(data, dict, 16) + if err != nil || start > len(data)-2 { + return nil, fmt.Errorf("invalid CFF encoding offset") + } + end := start + 2 + switch data[start] & 0x7f { + case 0: + end += int(data[start+1]) + case 1: + end += 2 * int(data[start+1]) + default: + return nil, fmt.Errorf("invalid CFF encoding format") + } + if data[start]&0x80 != 0 { + if end >= len(data) { + return nil, fmt.Errorf("truncated CFF encoding supplements") + } + end += 1 + 3*int(data[end]) + } + if end > len(data) { + return nil, fmt.Errorf("truncated CFF encoding") + } + return data[start:end], nil +} diff --git a/ofdgo_font_resource_sfnt.go b/ofdgo_font_resource_sfnt.go index e4fea9f..12cfdab 100644 --- a/ofdgo_font_resource_sfnt.go +++ b/ofdgo_font_resource_sfnt.go @@ -98,7 +98,11 @@ func sfntSubsettable(sfnt *sfntFont) bool { return false } if sfnt.IsCFF { - return sfnt.Tables["fvar"] == nil && staticCFFPrograms(sfnt) != nil + if sfnt.Tables["fvar"] != nil { + return false + } + font, err := readType2Font(sfnt.Tables["CFF "]) + return err == nil && len(font.chars) == int(sfnt.NumGlyphs()) || staticCFFPrograms(sfnt) != nil } return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt)) } @@ -162,6 +166,12 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) { return bytes.Clone(data), nil } if sfnt.IsCFF { + if font, err := readType2Font(sfnt.Tables["CFF "]); err == nil { + if len(font.chars) != int(sfnt.NumGlyphs()) { + return nil, fmt.Errorf("CFF glyph count does not match maxp") + } + return subsetNamedCFFFont(sfnt, glyphs) + } return subsetStaticCFFFont(sfnt, glyphs) } if sfnt.Tables["fvar"] != nil { @@ -193,7 +203,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) { return subsetCompactSFNTFont(sfnt, glyphs) } -// subsetSourceSFNTFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令 +// subsetSourceSFNTFont 裁剪静态TrueType或非CID的CFF原有字体,保留字形编号、组合依赖、度量和提示指令 // 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复 // 入参: data 字体数据, usage 全包用字记录 // 返回: []byte 更小的字体子集,无确定收益时为空 @@ -202,12 +212,12 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { return nil } sfnt, err := parseSFNTFont(data) - if err != nil || !sfnt.IsTrueType || fontEmbeddingFlags(sfnt)&0x0100 != 0 { + if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 { return nil } for tag := range sfnt.Tables { switch tag { - case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM": + case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM": default: return nil } @@ -239,6 +249,25 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { mapping[char] = id } } + if sfnt.IsCFF { + font, err := readType2Font(sfnt.Tables["CFF "]) + if err != nil || len(font.chars) != int(sfnt.NumGlyphs()) { + return nil + } + cff, err := subsetCFFCharstrings(sfnt.Tables["CFF "], glyphs) + if err != nil { + return nil + } + tables := maps.Clone(sfnt.Tables) + tables["CFF "] = cff + tables["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) + delete(tables, "DSIG") + result, err := serializeOTF(tables) + if err != nil || len(result) >= len(data) { + return nil + } + return result + } for id := 0; id < int(sfnt.NumGlyphs()); id++ { if _, err := trueTypeGlyphData(sfnt.Tables, uint16(id)); err != nil { return nil diff --git a/ofdgo_font_type2.go b/ofdgo_font_type2.go index e97d3ee..30a6d2c 100644 --- a/ofdgo_font_type2.go +++ b/ofdgo_font_type2.go @@ -54,6 +54,7 @@ type type2State struct { output *bytes.Buffer seed uint64 steps *int + dependencies map[uint16]bool } // readType2Index 读取并校验CFF索引的全部偏移,不复制程序字节 @@ -499,7 +500,10 @@ func (s *type2State) appendComponent(code, x, y float64) error { if !ok { return fmt.Errorf("missing Type2 component %s", name) } - child := type2State{font: s.font, matrix: s.matrix, width: s.font.def, component: true, output: s.output, originX: x, originY: y, outX: s.outX, outY: s.outY, seed: s.font.seed + uint64(gid), steps: s.steps, stripHints: s.stripHints} + if s.dependencies != nil { + s.dependencies[uint16(gid)] = true + } + child := type2State{font: s.font, matrix: s.matrix, width: s.font.def, component: true, output: s.output, originX: x, originY: y, outX: s.outX, outY: s.outY, seed: s.font.seed + uint64(gid), steps: s.steps, stripHints: s.stripHints, dependencies: s.dependencies} _, err := child.run(s.font.chars[gid], 0) if err != nil { return fmt.Errorf("invalid Type2 component %s: %w", name, err) diff --git a/ofdgo_font_type2_stack.go b/ofdgo_font_type2_stack.go index a4ffe90..e113e3b 100644 --- a/ofdgo_font_type2_stack.go +++ b/ofdgo_font_type2_stack.go @@ -27,6 +27,9 @@ func (s *type2State) calculate(op int) error { count := 0 switch op { case 1223: + if s.dependencies != nil { + return fmt.Errorf("random Type2 program cannot be subset safely") + } s.seed = uint64(uint32(s.seed*1664525 + 1013904223)) return s.push(float64(s.seed+1) / 4294967296) case 1205, 1209, 1214, 1218, 1221, 1226, 1227, 1229: