mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
ee3fd02a0a
commit
64f9ec4241
87 files changed
+2693
-2289
No files matched your search
@@ -0,0 +1,322 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
)
|
||||
|
||||
// subsetCFFFont 裁剪CFF字体,保留组合组件和度量,按需重排字形
|
||||
// 入参: sfnt 已解析字体, glyphs 使用的字形, preserve 是否保留原字形编号
|
||||
// 返回: []byte 字体子集, error 裁剪错误
|
||||
func subsetCFFFont(sfnt *SFNT, glyphs []uint16, preserve bool) ([]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
|
||||
}
|
||||
if !preserve {
|
||||
if compact, ids, err := compactCFFGlyphs(data, keep); err == nil {
|
||||
return compactCFFTables(sfnt, compact, ids)
|
||||
}
|
||||
}
|
||||
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 CFF数据, keep 保留的字形,调用后包含组合组件
|
||||
// 返回: []byte 重建的CFF数据, error 结构或依赖错误
|
||||
func subsetCFFCharstrings(data []byte, keep map[uint16]bool) ([]byte, error) {
|
||||
font, err := readType2Font(data)
|
||||
if err != nil {
|
||||
cid, err := readCIDSubsetFont(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chars, err := subsetType2Programs(cid.fonts, cid.selects, keep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cid.write(chars)
|
||||
}
|
||||
chars, err := subsetType2Programs([]*type2Font{font}, nil, keep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rebuildNamedCFF(data, font, chars, nil)
|
||||
}
|
||||
|
||||
// subsetType2Programs 按字形所属字典收集子程序依赖并清空未使用的轮廓
|
||||
// 入参: fonts 执行资源, selects 字形所属字典,单字典时可为空, keep 保留的字形
|
||||
// 返回: [][]byte 字形程序, error 执行或字宽错误
|
||||
func subsetType2Programs(fonts []*type2Font, selects []int, keep map[uint16]bool) ([][]byte, error) {
|
||||
keep[0] = true
|
||||
programs := fonts[0].chars
|
||||
locals := make([][]bool, len(fonts))
|
||||
empty := make([]map[float64][]byte, len(fonts))
|
||||
for index, font := range fonts {
|
||||
locals[index] = make([]bool, len(font.locals))
|
||||
empty[index] = make(map[float64][]byte)
|
||||
}
|
||||
globals := make([]bool, len(fonts[0].globals))
|
||||
var stack [48]float64
|
||||
for gid := range keep {
|
||||
if int(gid) >= len(programs) {
|
||||
return nil, fmt.Errorf("CFF glyph index out of range")
|
||||
}
|
||||
fd := 0
|
||||
if selects != nil {
|
||||
fd = selects[gid]
|
||||
}
|
||||
font := fonts[fd]
|
||||
steps := 0
|
||||
state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep, localUsage: locals[fd], globalUsage: globals}
|
||||
if _, err := state.run(font.chars[gid], 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
chars := make([][]byte, len(programs))
|
||||
for gid, program := range programs {
|
||||
if keep[uint16(gid)] {
|
||||
chars[gid] = program
|
||||
continue
|
||||
}
|
||||
fd := 0
|
||||
if selects != nil {
|
||||
fd = selects[gid]
|
||||
}
|
||||
font := fonts[fd]
|
||||
steps := 0
|
||||
state := type2State{font: font, args: stack[:0], 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[fd][state.width]; ok {
|
||||
chars[gid] = encoded
|
||||
continue
|
||||
}
|
||||
var encoded bytes.Buffer
|
||||
if state.width != font.def {
|
||||
value := state.width - font.nominal
|
||||
divisor := 1
|
||||
if value < -32768 || value >= 32768 {
|
||||
divisor = 2
|
||||
}
|
||||
for range divisor {
|
||||
if err := encodeType2Number(&encoded, value/float64(divisor)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if parseNumberType2(encoded.Bytes(), 0)*float64(divisor) != value {
|
||||
return nil, fmt.Errorf("CFF width cannot be represented exactly")
|
||||
}
|
||||
if divisor == 2 {
|
||||
encoded.Write([]byte{12, 10})
|
||||
}
|
||||
}
|
||||
encoded.WriteByte(14)
|
||||
chars[gid] = encoded.Bytes()
|
||||
empty[fd][state.width] = chars[gid]
|
||||
}
|
||||
globalPrograms := subsetCFFSubroutines(fonts[0].globals, globals)
|
||||
for index, font := range fonts {
|
||||
font.locals = subsetCFFSubroutines(font.locals, locals[index])
|
||||
font.globals = globalPrograms
|
||||
}
|
||||
return chars, nil
|
||||
}
|
||||
|
||||
// subsetCFFSubroutines 将未引用的子程序替换为空返回,保持调用编号及偏移基数
|
||||
// 入参: programs 子程序索引, used 引用标记
|
||||
// 返回: [][]byte 裁剪后的子程序索引
|
||||
func subsetCFFSubroutines(programs [][]byte, used []bool) [][]byte {
|
||||
result := make([][]byte, len(programs))
|
||||
empty := []byte{11}
|
||||
for index, program := range programs {
|
||||
if used[index] {
|
||||
result[index] = program
|
||||
} else {
|
||||
result[index] = empty
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// rebuildNamedCFF 重建非CID的CFF偏移,保留字符集、编码和私有提示参数
|
||||
// 入参: data 原始CFF数据, font 执行资源, chars 替换后的字形程序, ids 字形重排顺序,nil时保留原编号
|
||||
// 返回: []byte 重建的CFF数据, error 不支持的结构
|
||||
func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte, ids []uint16) ([]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(font.chars))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid CFF charset")
|
||||
}
|
||||
encoding, err := cffSubsetEncoding(data, dict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ids != nil {
|
||||
if encoding != nil {
|
||||
codes, err := readCFFSubsetEncoding(encoding, sids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoding, err = encodeCFFSubsetEncoding(codes, sids, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
selected := make([]int, len(ids))
|
||||
for index, gid := range ids {
|
||||
selected[index] = sids[gid]
|
||||
}
|
||||
sids = selected
|
||||
}
|
||||
charset := encodeCFFSubsetCharset(sids)
|
||||
private, err := readType2Private(data, dict[18], &type2Font{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privateData, err := encodeCFFSubsetPrivate(private)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
// encodeCFFSubsetPrivate 保留私有字典参数,将局部子程序安排在字典之后
|
||||
// 入参: private 私有字典
|
||||
// 返回: []byte 字典数据, error 不支持的参数或偏移错误
|
||||
func encodeCFFSubsetPrivate(private cffDict) ([]byte, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
for range 16 {
|
||||
data := encodeCFFDict(private)
|
||||
value, ok := private[19]
|
||||
if !ok || len(value) == 1 && value[0] == float64(len(data)) {
|
||||
return data, nil
|
||||
}
|
||||
private[19] = []float64{float64(len(data))}
|
||||
}
|
||||
return nil, fmt.Errorf("CFF private offset 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
|
||||
}
|
||||
Reference in new issue
Block a user