feat(文件拆分): 拆分字体图像基础能力
build.yaml / build (push) Successful in 49s

This commit is contained in:
xiaoqidun committed 2026-10-14 00:12:00 +08:00
1 parent ee3fd02a0a
commit 64f9ec4241
87 files changed
+2693 -2289

No files matched your search

File diff suppressed because it is too large. Load diff
+205
View File
@@ -0,0 +1,205 @@
// 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"
"encoding/binary"
"fmt"
"math"
"github.com/go-text/typesetting/font/cff"
"github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
// DefaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓
// 入参: data 独立字体数据
// 返回: []byte 字体数据,非CFF2原样返回, error 解析或编码错误
func DefaultCFF2Font(data []byte) (result []byte, err error) {
if len(data) < 12 || string(data[:4]) != "OTTO" {
return data, nil
}
found := false
for i, count := 0, int(binary.BigEndian.Uint16(data[4:])); i < count && i < (len(data)-12)/16; i++ {
if string(data[12+i*16:16+i*16]) == "CFF2" {
found = true
break
}
}
if !found {
return data, nil
}
table, err := FileTables(data, 0)
if err != nil || table["CFF2"] == nil {
return data, err
}
if table["CFF "] != nil || table["glyf"] != nil || len(table["head"]) < 54 || len(table["maxp"]) < 6 || len(table["hhea"]) < 36 {
return nil, fmt.Errorf("invalid CFF2 font tables")
}
defer func() {
if failure := recover(); failure != nil {
result, err = nil, fmt.Errorf("invalid CFF2 font: %v", failure)
}
}()
cffData := table["CFF2"]
if len(cffData) < 5 || cffData[0] != 2 || cffData[1] != 0 || cffData[2] < 5 || binary.BigEndian.Uint32(table["maxp"]) != 0x5000 {
return nil, fmt.Errorf("invalid CFF2 header")
}
start := int(cffData[2])
end := start + int(binary.BigEndian.Uint16(cffData[3:]))
if end > len(cffData) {
return nil, fmt.Errorf("invalid CFF2 dictionary length")
}
dict, err := readCFFDictionary(cffData[start:end], true)
if err != nil {
return nil, err
}
matrix, err := readCFFMatrix(dict, [6]float64{.001, 0, 0, .001, 0, 0})
units := binary.BigEndian.Uint16(table["head"][18:])
if err != nil || units < 16 || units > 16384 || matrix[0] != matrix[3] || matrix[1] != 0 || matrix[2] != 0 || matrix[4] != 0 || matrix[5] != 0 || math.Abs(matrix[0]*float64(units)-1) > 1e-6 {
return nil, fmt.Errorf("CFF2 FontMatrix does not match design units")
}
program, err := cff.ParseCFF2(table["CFF2"])
if err != nil {
return nil, err
}
if len(dict[24]) != 0 {
fvar := table["fvar"]
if len(fvar) < 16 || binary.BigEndian.Uint32(fvar) != 0x10000 || int(binary.BigEndian.Uint16(fvar[8:])) != program.VarStore.AxisCount() {
return nil, fmt.Errorf("CFF2 variation axis count mismatch")
}
}
count := int(binary.BigEndian.Uint16(table["maxp"][4:]))
metrics := int(binary.BigEndian.Uint16(table["hhea"][34:]))
if count == 0 || len(program.Charstrings) != count || metrics == 0 || metrics > count || len(table["hmtx"]) < metrics*4+(count-metrics)*2 {
return nil, fmt.Errorf("invalid CFF2 glyph metrics")
}
chars := make([][]byte, count)
for gid := range chars {
segments, _, err := program.LoadGlyph(tables.GlyphID(gid), nil)
if err != nil {
return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err)
}
width := binary.BigEndian.Uint16(table["hmtx"][min(gid, metrics-1)*4:])
chars[gid], err = cffOutlineProgram(segments, width)
if err != nil {
return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err)
}
}
table["CFF "], err = staticCIDFont(table, chars)
if err != nil {
return nil, err
}
for _, tag := range []string{"CFF2", "fvar", "avar", "STAT", "HVAR", "VVAR", "MVAR", "DSIG"} {
delete(table, tag)
}
return SerializeOTF(table)
}
// cffOutlineProgram 将默认设计轮廓编码为无子程序的Type2字形
// 入参: segments 字体设计坐标路径, width 水平字宽
// 返回: []byte 字形程序, error 坐标超出编码范围
func cffOutlineProgram(segments []opentype.Segment, width uint16) ([]byte, error) {
var output bytes.Buffer
if width < 32768 {
if err := encodeType2Number(&output, float64(width)); err != nil {
return nil, err
}
} else {
for range 2 {
if err := encodeType2Number(&output, float64(width)/2); err != nil {
return nil, err
}
}
output.Write([]byte{12, 10})
}
var x, y float64
for _, segment := range segments {
count, op := 1, byte(21)
switch segment.Op {
case opentype.SegmentOpMoveTo:
case opentype.SegmentOpLineTo:
op = 5
case opentype.SegmentOpCubeTo:
count, op = 3, 8
default:
return nil, fmt.Errorf("invalid CFF2 path operation")
}
for _, point := range segment.Args[:count] {
nx, ny := float64(point.X), float64(point.Y)
if err := encodeType2Number(&output, nx-x); err != nil {
return nil, err
}
if err := encodeType2Number(&output, ny-y); err != nil {
return nil, err
}
x, y = nx, ny
}
output.WriteByte(op)
}
output.WriteByte(14)
return output.Bytes(), nil
}
// staticCIDFont 封装独立Type2字形,CID与字形编号一致,不依赖字形名称
// 入参: table OpenType表, chars 字形程序
// 返回: []byte CFF字体, error 字体结构错误
func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
if len(chars) == 0 || len(chars) > 65535 || len(table["head"]) < 54 {
return nil, fmt.Errorf("invalid static CFF glyph count")
}
units := binary.BigEndian.Uint16(table["head"][18:])
if units < 16 || units > 16384 {
return nil, fmt.Errorf("invalid CFF design units")
}
name := FaceInfo(table["name"], 0).PostScriptName
if name == "" {
name = "OpenTypeInstance"
}
names := encodeCFFIndex([][]byte{[]byte(name)})
strings := encodeCFFIndex([][]byte{[]byte("Adobe"), []byte("Identity")})
cids := make([]int, len(chars))
for gid := 1; gid < len(chars); gid++ {
cids[gid] = gid
}
charset := encodeCFFSubsetCharset(cids)
selects := []byte{3, 0, 1, 0, 0, 0}
selects = binary.BigEndian.AppendUint16(selects, uint16(len(chars)))
fonts := encodeCFFIndex([][]byte{encodeCFFDict(cffDict{18: {0, 0}})})
charIndex := encodeCFFIndex(chars)
scale := 1 / float64(units)
dict := cffDict{1230: {391, 392, 0}, 1234: {float64(len(chars))}, 1207: {scale, 0, 0, scale, 0, 0}, 15: {0}, 17: {0}, 1236: {0}, 1237: {0}}
for i := 0; i < 4; i++ {
dict[5] = append(dict[5], float64(int16(binary.BigEndian.Uint16(table["head"][36+2*i:]))))
}
var top []byte
for range 16 {
top = encodeCFFIndex([][]byte{encodeCFFDict(dict)})
offset := 4 + len(names) + len(top) + len(strings) + 2
if dict[15][0] == float64(offset) {
var output bytes.Buffer
for _, part := range [][]byte{{1, 0, 4, 4}, names, top, strings, {0, 0}, charset, selects, fonts, charIndex} {
output.Write(part)
}
return output.Bytes(), nil
}
dict[15][0] = float64(offset)
dict[1237][0] = float64(offset + len(charset))
dict[1236][0] = float64(offset + len(charset) + len(selects))
dict[17][0] = float64(offset + len(charset) + len(selects) + len(fonts))
}
return nil, fmt.Errorf("CFF offsets did not converge")
}
+239
View File
@@ -0,0 +1,239 @@
// 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"
"encoding/binary"
"fmt"
"math"
)
// cidSubsetFont 保存CID字体裁剪所需的字典、映射和执行资源
type cidSubsetFont struct {
prefix []byte
strings [][]byte
top cffDict
dicts []cffDict
private []cffDict
fonts []*type2Font
selects []int
charset []byte
}
// readCIDSubsetFont 读取CID字体的原始字典及逐字形归属,不合并字体字典
// 入参: data CFF数据
// 返回: *cidSubsetFont 裁剪资源, error 结构错误或不支持的字典
func readCIDSubsetFont(data []byte) (*cidSubsetFont, error) {
if !isBareCFFData(data) {
return nil, fmt.Errorf("invalid CFF header")
}
names, end, err := readType2Index(data, int(data[2]))
if err != nil || len(names) != 1 {
return nil, fmt.Errorf("invalid CFF name index")
}
tops, end, err := readType2Index(data, end)
if err != nil || len(tops) != 1 {
return nil, fmt.Errorf("invalid CFF top index")
}
strings, end, err := readType2Index(data, end)
if err != nil {
return nil, err
}
globals, _, err := readType2Index(data, end)
if err != nil {
return nil, err
}
top, err := readCFFDict(tops[0])
if err != nil {
return nil, err
}
ros := top[1230]
if len(ros) != 3 {
return nil, fmt.Errorf("invalid CFF ROS")
}
for index, value := range ros {
limit := float64(math.MaxInt32)
if index < 2 {
limit = float64(min(64999, 390+len(strings)))
}
if !finite(value) || value < 0 || value > limit || value != math.Trunc(value) {
return nil, fmt.Errorf("invalid CFF ROS operand")
}
}
for op := range top {
switch op {
case 0, 1, 2, 3, 4, 5, 13, 14, 15, 17, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1230, 1231, 1232, 1233, 1234, 1236, 1237:
default:
return nil, fmt.Errorf("unsupported CID CFF top operator %d", op)
}
}
if value, ok := top[1206]; ok && (len(value) != 1 || value[0] != 2) {
return nil, fmt.Errorf("unsupported CFF charstring type")
}
if _, err := readCFFMatrix(top, [6]float64{.001, 0, 0, .001, 0, 0}); err != nil {
return nil, err
}
charOffset, err := cffDictOffset(data, top, 17)
if err != nil {
return nil, err
}
chars, _, err := readType2Index(data, charOffset)
if err != nil || len(chars) == 0 {
return nil, fmt.Errorf("invalid CFF charstrings")
}
charsetOffset, err := cffDictOffset(data, top, 15)
if err != nil || charsetOffset <= 2 {
return nil, fmt.Errorf("invalid CID CFF charset offset")
}
cids, err := parseCFFCharset(data, charsetOffset, len(chars))
if err != nil {
return nil, err
}
fdOffset, err := cffDictOffset(data, top, 1236)
if err != nil {
return nil, err
}
fds, _, err := readType2Index(data, fdOffset)
if err != nil || len(fds) == 0 || len(fds) > 256 {
return nil, fmt.Errorf("invalid CFF font dictionary count")
}
selectOffset, err := cffDictOffset(data, top, 1237)
if err != nil {
return nil, err
}
selects, err := readType2FDSelect(data, selectOffset, len(chars), len(fds))
if err != nil {
return nil, err
}
if data[selectOffset] == 3 && binary.BigEndian.Uint16(data[selectOffset+3:]) != 0 {
return nil, fmt.Errorf("CFF FDSelect must include glyph zero")
}
font := &cidSubsetFont{
prefix: append(bytes.Clone(data[:int(data[2])]), encodeCFFIndex(names)...),
strings: strings, top: top, selects: selects, charset: encodeCFFSubsetCharset(cids),
dicts: make([]cffDict, len(fds)), private: make([]cffDict, len(fds)), fonts: make([]*type2Font, len(fds)),
}
for index, fd := range fds {
dict, err := readCFFDict(fd)
if err != nil {
return nil, err
}
for op := range dict {
switch op {
case 18, 1207, 1238:
default:
return nil, fmt.Errorf("unsupported CFF font dictionary operator %d", op)
}
}
if _, err := readCFFMatrix(dict, [6]float64{1, 0, 0, 1, 0, 0}); err != nil {
return nil, err
}
if len(dict[18]) != 2 {
return nil, fmt.Errorf("missing CFF private dictionary")
}
resource := &type2Font{data: data, chars: chars, globals: globals, seed: 1, cid: true}
private, err := readType2Private(data, dict[18], resource)
if err != nil {
return nil, err
}
if _, err := encodeCFFSubsetPrivate(private); err != nil {
return nil, err
}
font.dicts[index], font.private[index], font.fonts[index] = dict, private, resource
}
return font, nil
}
// write 重建CID字体偏移,保留CID映射、字体矩阵及各字典的私有参数
// 入参: chars 裁剪后的字形程序
// 返回: []byte CFF数据, error 偏移或字典错误
func (f *cidSubsetFont) write(chars [][]byte) ([]byte, error) {
suffix := append(encodeCFFIndex(f.strings), encodeCFFIndex(f.fonts[0].globals)...)
selects := encodeCFFSubsetFDSelect(f.selects)
programs := encodeCFFIndex(chars)
private := make([][]byte, len(f.fonts))
locals := make([][]byte, len(f.fonts))
fds := make([][]byte, len(f.fonts))
for index, font := range f.fonts {
var err error
private[index], err = encodeCFFSubsetPrivate(f.private[index])
if err != nil {
return nil, err
}
if _, ok := f.private[index][19]; ok {
locals[index] = encodeCFFIndex(font.locals)
}
fds[index] = encodeCFFDict(f.dicts[index])
}
for range 16 {
top := encodeCFFIndex([][]byte{encodeCFFDict(f.top)})
array := encodeCFFIndex(fds)
offset := len(f.prefix) + len(top) + len(suffix)
f.top[15] = []float64{float64(offset)}
offset += len(f.charset)
f.top[1237] = []float64{float64(offset)}
offset += len(selects)
f.top[17] = []float64{float64(offset)}
offset += len(programs)
f.top[1236] = []float64{float64(offset)}
offset += len(array)
for index := range f.fonts {
f.dicts[index][18] = []float64{float64(len(private[index])), float64(offset)}
fds[index] = encodeCFFDict(f.dicts[index])
offset += len(private[index]) + len(locals[index])
}
updatedTop := encodeCFFIndex([][]byte{encodeCFFDict(f.top)})
updatedArray := encodeCFFIndex(fds)
if len(updatedTop) != len(top) || len(updatedArray) != len(array) {
continue
}
result := make([]byte, 0, offset)
for _, part := range [][]byte{f.prefix, updatedTop, suffix, f.charset, selects, programs, updatedArray} {
result = append(result, part...)
}
for index := range f.fonts {
result = append(result, private[index]...)
result = append(result, locals[index]...)
}
return result, nil
}
return nil, fmt.Errorf("CID CFF offsets did not converge")
}
// encodeCFFSubsetFDSelect 选择较短的逐字形或分段格式保存字体字典归属
// 入参: selects 字形所属字典
// 返回: []byte FDSelect数据
func encodeCFFSubsetFDSelect(selects []int) []byte {
ranges := []byte{3, 0, 0}
count := 0
for gid, fd := range selects {
if gid == 0 || fd != selects[gid-1] {
ranges = binary.BigEndian.AppendUint16(ranges, uint16(gid))
ranges = append(ranges, byte(fd))
count++
}
}
binary.BigEndian.PutUint16(ranges[1:], uint16(count))
ranges = binary.BigEndian.AppendUint16(ranges, uint16(len(selects)))
if len(ranges) < len(selects)+1 {
return ranges
}
result := make([]byte, len(selects)+1)
for gid, fd := range selects {
result[gid+1] = byte(fd)
}
return result
}
+339
View File
@@ -0,0 +1,339 @@
// 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"
"encoding/binary"
"fmt"
"slices"
)
// compactCFFGlyphs 重排已裁剪字体的字形,同步字符集、编码和字体字典归属
// 入参: data 已裁剪的CFF数据, keep 保留的原字形编号
// 返回: []byte 紧凑CFF数据, []uint16 新编号对应的原编号, error 结构或编码错误
func compactCFFGlyphs(data []byte, keep map[uint16]bool) ([]byte, []uint16, error) {
ids := []uint16{0}
for gid, used := range keep {
if used && gid != 0 {
ids = append(ids, gid)
}
}
slices.Sort(ids)
font, err := readType2Font(data)
if err == nil {
sids, _, _, _, ok := getCFFCharsetInfo(data, len(font.chars))
if !ok {
return nil, nil, fmt.Errorf("invalid CFF charset")
}
_, end, _ := readType2Index(data, int(data[2]))
tops, _, _ := readType2Index(data, end)
dict, _ := readCFFDict(tops[0])
encoding, err := cffSubsetEncoding(data, dict)
if err != nil {
return nil, nil, err
}
if encoding != nil {
codes, err := readCFFSubsetEncoding(encoding, sids)
if err != nil {
return nil, nil, err
}
ordered := []uint16{0}
seen := map[uint16]bool{0: true}
for _, gid := range codes {
if keep[gid] && !seen[gid] {
ordered = append(ordered, gid)
seen[gid] = true
}
}
for _, gid := range ids {
if !seen[gid] {
ordered = append(ordered, gid)
}
}
ids = ordered
}
chars := make([][]byte, len(ids))
for index, gid := range ids {
if int(gid) >= len(font.chars) {
return nil, nil, fmt.Errorf("CFF glyph index out of range")
}
chars[index] = font.chars[gid]
}
result, err := rebuildNamedCFF(data, font, chars, ids)
return result, ids, err
}
cid, err := readCIDSubsetFont(data)
if err != nil {
return nil, nil, err
}
cids, _, _, _, ok := getCFFCharsetInfo(data, len(cid.selects))
if !ok {
return nil, nil, fmt.Errorf("invalid CID CFF charset")
}
chars := make([][]byte, len(ids))
selected := make([]int, len(ids))
selects := make([]int, len(ids))
dicts, private, fonts := []cffDict{}, []cffDict{}, []*type2Font{}
remap := make(map[int]int)
for index, gid := range ids {
if int(gid) >= len(cid.selects) {
return nil, nil, fmt.Errorf("CFF glyph index out of range")
}
fd := cid.selects[gid]
mapped, exists := remap[fd]
if !exists {
mapped = len(fonts)
remap[fd] = mapped
dicts = append(dicts, cid.dicts[fd])
private = append(private, cid.private[fd])
fonts = append(fonts, cid.fonts[fd])
}
chars[index], selected[index], selects[index] = cid.fonts[fd].chars[gid], cids[gid], mapped
}
cid.dicts, cid.private, cid.fonts = dicts, private, fonts
cid.charset, cid.selects = encodeCFFSubsetCharset(selected), selects
result, err := cid.write(chars)
return result, ids, err
}
// compactCFFTables 重建按字形编号索引的度量和字符映射,不保留旧编号的布局表
// 入参: font 原字体, cff 紧凑CFF数据, ids 新编号对应的原编号
// 返回: []byte 独立OpenType字体, error 封装错误
func compactCFFTables(font *SFNT, cff []byte, ids []uint16) ([]byte, error) {
tables := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "vhea", "gasp"} {
if data := font.Tables[tag]; data != nil {
tables[tag] = bytes.Clone(data)
}
}
var hmtx, vmtx []byte
mapping := make(map[rune]uint16)
for index, gid := range ids {
hmtx = binary.BigEndian.AppendUint16(hmtx, font.GlyphAdvance(gid))
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(font.sideBearing(gid, false)))
if font.vertical != 0 {
vmtx = binary.BigEndian.AppendUint16(vmtx, font.GlyphVerticalAdvance(gid))
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(font.sideBearing(gid, true)))
}
for _, char := range font.glyphCharacters(gid) {
mapping[char] = uint16(index)
}
}
tables["CFF "], tables["hmtx"] = cff, hmtx
tables["cmap"] = BuildCmapTable(uint16(len(ids)), mapping)
binary.BigEndian.PutUint16(tables["hhea"][34:], uint16(len(ids)))
binary.BigEndian.PutUint16(tables["maxp"][4:], uint16(len(ids)))
if len(tables["post"]) >= 32 {
tables["post"] = tables["post"][:32]
binary.BigEndian.PutUint32(tables["post"], 0x30000)
}
if font.vertical != 0 {
tables["vmtx"] = vmtx
binary.BigEndian.PutUint16(tables["vhea"][34:], uint16(len(ids)))
}
if data := font.Tables["VORG"]; data != nil {
var err error
tables["VORG"], err = compactCFFVerticalOrigins(data, font.NumGlyphs(), ids)
if err != nil {
return nil, err
}
}
return SerializeOTF(tables)
}
// compactCFFVerticalOrigins 重排CFF字体的逐字形纵向原点,保留默认原点
// 入参: data VORG表, count 原字形数, ids 新编号对应的原编号
// 返回: []byte 重建的VORG表, error 表结构或记录顺序错误
func compactCFFVerticalOrigins(data []byte, count uint16, ids []uint16) ([]byte, error) {
if len(data) < 8 || binary.BigEndian.Uint32(data) != 0x10000 {
return nil, fmt.Errorf("invalid CFF vertical origin header")
}
length := int(binary.BigEndian.Uint16(data[6:]))
if length > (len(data)-8)/4 {
return nil, fmt.Errorf("truncated CFF vertical origins")
}
origins := make(map[uint16]uint16, length)
previous := -1
for index := range length {
pos := 8 + index*4
gid := binary.BigEndian.Uint16(data[pos:])
if gid >= count || int(gid) <= previous {
return nil, fmt.Errorf("invalid CFF vertical origin glyph")
}
origins[gid] = binary.BigEndian.Uint16(data[pos+2:])
previous = int(gid)
}
result := bytes.Clone(data[:8])
for index, gid := range ids {
if origin, ok := origins[gid]; ok {
result = binary.BigEndian.AppendUint16(result, uint16(index))
result = binary.BigEndian.AppendUint16(result, origin)
}
}
binary.BigEndian.PutUint16(result[6:], uint16((len(result)-8)/4))
return result, nil
}
// encodeCFFSubsetCharset 选择最短的标准字符集格式,保持SID或CID及字形顺序
// 入参: ids 按字形编号排列的SID或CID,包含.notdef
// 返回: []byte 字符集数据
func encodeCFFSubsetCharset(ids []int) []byte {
sizes := [3]int{1 + 2*(len(ids)-1), 1, 1}
for first := 1; first < len(ids); {
end := first + 1
for end < len(ids) && ids[end] == ids[end-1]+1 {
end++
}
sizes[1] += 3 * ((end - first + 255) / 256)
sizes[2] += 4
first = end
}
format := 0
for index, size := range sizes {
if size < sizes[format] {
format = index
}
}
result := make([]byte, 1, sizes[format])
result[0] = byte(format)
for first := 1; first < len(ids); {
end := first + 1
if format != 0 {
limit := len(ids)
if format == 1 {
limit = min(limit, first+256)
}
for end < limit && ids[end] == ids[end-1]+1 {
end++
}
}
result = binary.BigEndian.AppendUint16(result, uint16(ids[first]))
if format == 1 {
result = append(result, byte(end-first-1))
} else if format == 2 {
result = binary.BigEndian.AppendUint16(result, uint16(end-first-1))
}
first = end
}
return result
}
// readCFFSubsetEncoding 解析自定义编码及补充映射,校验重复编码和字形归属
// 入参: data 编码数据, sids 原字符集
// 返回: [256]uint16 编码对应的字形编号, error 编码结构错误
func readCFFSubsetEncoding(data []byte, sids []int) ([256]uint16, error) {
var codes [256]uint16
if len(data) < 2 {
return codes, fmt.Errorf("truncated CFF encoding")
}
pos, gid := 2, 1
for index := 0; index < int(data[1]); index++ {
if pos >= len(data) {
return codes, fmt.Errorf("truncated CFF encoding")
}
first, count := int(data[pos]), 1
pos++
switch data[0] & 0x7f {
case 0:
case 1:
if pos >= len(data) {
return codes, fmt.Errorf("truncated CFF encoding range")
}
count += int(data[pos])
pos++
default:
return codes, fmt.Errorf("invalid CFF encoding format")
}
if first+count > len(codes) || gid+count > len(sids) {
return codes, fmt.Errorf("CFF encoding range out of bounds")
}
for code := first; code < first+count; code++ {
if codes[code] != 0 {
return codes, fmt.Errorf("duplicate CFF encoding")
}
codes[code] = uint16(gid)
gid++
}
}
if data[0]&0x80 != 0 {
if pos >= len(data) {
return codes, fmt.Errorf("truncated CFF encoding supplements")
}
count := int(data[pos])
pos++
if count > (len(data)-pos)/3 {
return codes, fmt.Errorf("truncated CFF encoding supplements")
}
for range count {
code, sid := data[pos], int(binary.BigEndian.Uint16(data[pos+1:]))
pos += 3
mapped := slices.Index(sids[:gid], sid)
if codes[code] != 0 || mapped <= 0 {
return codes, fmt.Errorf("invalid CFF supplemental glyph")
}
codes[code] = uint16(mapped)
}
}
if data[0]&0x7f > 1 || pos != len(data) {
return codes, fmt.Errorf("invalid CFF encoding length or format")
}
return codes, nil
}
// encodeCFFSubsetEncoding 按新字形顺序重建主编码及同字形的补充编码
// 入参: codes 原编码映射, sids 原字符集, ids 新编号对应的原编号
// 返回: []byte 编码数据, error 字形顺序不满足编码约束
func encodeCFFSubsetEncoding(codes [256]uint16, sids []int, ids []uint16) ([]byte, error) {
var base, supplements []byte
encodings := make(map[uint16][]byte)
for code, gid := range codes {
if gid != 0 {
encodings[gid] = append(encodings[gid], byte(code))
}
}
unencoded := false
for _, gid := range ids[1:] {
values := encodings[gid]
if len(values) == 0 {
unencoded = true
} else if unencoded {
return nil, fmt.Errorf("CFF encoded glyph follows unencoded glyph")
} else {
base = append(base, values[0])
for _, code := range values[1:] {
supplements = append(supplements, code)
supplements = binary.BigEndian.AppendUint16(supplements, uint16(sids[gid]))
}
}
}
var result []byte
if len(base) == 256 {
for index, code := range base {
if int(code) != index {
return nil, fmt.Errorf("CFF encoding exceeds format zero capacity")
}
}
result = []byte{1, 1, 0, 255}
} else {
result = append([]byte{0, byte(len(base))}, base...)
}
if len(supplements) != 0 {
result[0] |= 0x80
result = append(result, byte(len(supplements)/3))
result = append(result, supplements...)
}
return result, nil
}
+81
View File
@@ -0,0 +1,81 @@
// 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 (
"strings"
)
// cffExpertCharset 按CFF规范附录C保存Expert字符集的SID顺序
var cffExpertCharset = [...]uint16{
0, 1, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 13, 14, 15, 99,
239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 27, 28, 249, 250, 251, 252,
253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 109, 110,
267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282,
283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298,
299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314,
315, 316, 317, 318, 158, 155, 163, 319, 320, 321, 322, 323, 324, 325, 326, 150,
164, 169, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340,
341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356,
357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372,
373, 374, 375, 376, 377, 378,
}
// cffExpertSubsetCharset 按CFF规范附录C保存ExpertSubset字符集的SID顺序
var cffExpertSubsetCharset = [...]uint16{
0, 1, 231, 232, 235, 236, 237, 238, 13, 14, 15, 99, 239, 240, 241, 242,
243, 244, 245, 246, 247, 248, 27, 28, 249, 250, 251, 253, 254, 255, 256, 257,
258, 259, 260, 261, 262, 263, 264, 265, 266, 109, 110, 267, 268, 269, 270, 272,
300, 301, 302, 305, 314, 315, 158, 155, 163, 320, 321, 322, 323, 324, 325, 326,
150, 164, 169, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339,
340, 341, 342, 343, 344, 345, 346,
}
// cffStandardStrings 按CFF标准SID顺序保存字形名称
var cffStandardStrings = strings.Fields(`
.notdef space exclam quotedbl numbersign dollar percent ampersand quoteright parenleft parenright asterisk
plus comma hyphen period slash zero one two three four five six
seven eight nine colon semicolon less equal greater question at A B
C D E F G H I J K L M N
O P Q R S T U V W X Y Z
bracketleft backslash bracketright asciicircum underscore quoteleft a b c d e f
g h i j k l m n o p q r
s t u v w x y z braceleft bar braceright asciitilde
exclamdown cent sterling fraction yen florin section currency quotesingle quotedblleft guillemotleft guilsinglleft
guilsinglright fi fl endash dagger daggerdbl periodcentered paragraph bullet quotesinglbase quotedblbase quotedblright
guillemotright ellipsis perthousand questiondown grave acute circumflex tilde macron breve dotaccent dieresis
ring cedilla hungarumlaut ogonek caron emdash AE ordfeminine Lslash Oslash OE ordmasculine
ae dotlessi lslash oslash oe germandbls onesuperior logicalnot mu trademark Eth onehalf
plusminus Thorn onequarter divide brokenbar degree thorn threequarters twosuperior registered minus eth
multiply threesuperior copyright Aacute Acircumflex Adieresis Agrave Aring Atilde Ccedilla Eacute Ecircumflex
Edieresis Egrave Iacute Icircumflex Idieresis Igrave Ntilde Oacute Ocircumflex Odieresis Ograve Otilde
Scaron Uacute Ucircumflex Udieresis Ugrave Yacute Ydieresis Zcaron aacute acircumflex adieresis agrave
aring atilde ccedilla eacute ecircumflex edieresis egrave iacute icircumflex idieresis igrave ntilde
oacute ocircumflex odieresis ograve otilde scaron uacute ucircumflex udieresis ugrave yacute ydieresis
zcaron exclamsmall Hungarumlautsmall dollaroldstyle dollarsuperior ampersandsmall Acutesmall parenleftsuperior parenrightsuperior twodotenleader onedotenleader zerooldstyle
oneoldstyle twooldstyle threeoldstyle fouroldstyle fiveoldstyle sixoldstyle sevenoldstyle eightoldstyle nineoldstyle commasuperior threequartersemdash periodsuperior
questionsmall asuperior bsuperior centsuperior dsuperior esuperior isuperior lsuperior msuperior nsuperior osuperior rsuperior
ssuperior tsuperior ff ffi ffl parenleftinferior parenrightinferior Circumflexsmall hyphensuperior Gravesmall Asmall Bsmall
Csmall Dsmall Esmall Fsmall Gsmall Hsmall Ismall Jsmall Ksmall Lsmall Msmall Nsmall
Osmall Psmall Qsmall Rsmall Ssmall Tsmall Usmall Vsmall Wsmall Xsmall Ysmall Zsmall
colonmonetary onefitted rupiah Tildesmall exclamdownsmall centoldstyle Lslashsmall Scaronsmall Zcaronsmall Dieresissmall Brevesmall Caronsmall
Dotaccentsmall Macronsmall figuredash hypheninferior Ogoneksmall Ringsmall Cedillasmall questiondownsmall oneeighth threeeighths fiveeighths seveneighths
onethird twothirds zerosuperior foursuperior fivesuperior sixsuperior sevensuperior eightsuperior ninesuperior zeroinferior oneinferior twoinferior
threeinferior fourinferior fiveinferior sixinferior seveninferior eightinferior nineinferior centinferior dollarinferior periodinferior commainferior Agravesmall
Aacutesmall Acircumflexsmall Atildesmall Adieresissmall Aringsmall AEsmall Ccedillasmall Egravesmall Eacutesmall Ecircumflexsmall Edieresissmall Igravesmall
Iacutesmall Icircumflexsmall Idieresissmall Ethsmall Ntildesmall Ogravesmall Oacutesmall Ocircumflexsmall Otildesmall Odieresissmall OEsmall Oslashsmall
Ugravesmall Uacutesmall Ucircumflexsmall Udieresissmall Yacutesmall Thornsmall Ydieresissmall 001.000 001.001 001.002 001.003 Black
Bold Book Light Medium Regular Roman Semibold
`)
+322
View File
@@ -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
}
+104
View File
@@ -0,0 +1,104 @@
// 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 (
"fmt"
)
// cffCharstringType 读取CFF字形程序类型,缺省为Type2
// 入参: dict 顶层字典
// 返回: int 字形程序类型, error 不支持的类型
func cffCharstringType(dict cffDict) (int, error) {
values, ok := dict[1206]
if !ok {
return 2, nil
}
if len(values) != 1 || values[0] != 1 && values[0] != 2 {
return 0, fmt.Errorf("unsupported CFF charstring type")
}
return int(values[0]), nil
}
// readCFFType1Program 读取Type1字形及无偏置局部子程序,保留源字形顺序
// 入参: data CFF字体, chars 字形程序, private 私有字典范围, cid 是否为CID字体
// 返回: *Type1Program 执行资源, error 字符集或私有字典错误
func readCFFType1Program(data []byte, chars [][]byte, private []float64, cid bool) (*Type1Program, error) {
resources := &type2Font{}
if _, err := readType2Private(data, private, resources); err != nil {
return nil, err
}
program := &Type1Program{subrs: resources.locals, lenIV: -1}
if cid {
return program, nil
}
identifiers, _, _, stringsOffset, ok := getCFFCharsetInfo(data, len(chars))
if !ok {
return nil, fmt.Errorf("invalid CFF Type1 charset")
}
strings, _, err := readType2Index(data, stringsOffset)
if err != nil {
return nil, err
}
program.glyphs = make([]type1Glyph, len(chars))
for gid, charstring := range chars {
name := ".notdef"
if gid != 0 {
sid := identifiers[gid]
if sid < len(cffStandardStrings) {
name = cffStandardStrings[sid]
} else if sid-len(cffStandardStrings) < len(strings) {
name = string(strings[sid-len(cffStandardStrings)])
} else {
return nil, fmt.Errorf("invalid CFF Type1 glyph name")
}
if name == "" {
return nil, fmt.Errorf("invalid CFF Type1 glyph name")
}
}
program.glyphs[gid] = type1Glyph{name: name, data: charstring}
}
return program, nil
}
// normalizeCFFType1 转换非CID字体的Type1程序,保留字符集、编码及字形编号
// 入参: data CFF字体, dict 顶层字典
// 返回: []byte Type2格式CFF, error 字形程序或范围错误
func normalizeCFFType1(data []byte, dict cffDict) ([]byte, error) {
_, nameSize := getCFFIndexCount(data, int(data[2]))
topStart := int(data[2]) + nameSize
_, topSize := getCFFIndexCount(data, topStart)
charStart, err := cffDictOffset(data, dict, 17)
if err != nil {
return nil, err
}
chars, charEnd, err := readType2Index(data, charStart)
if err != nil || len(chars) == 0 || charStart < topStart+topSize {
return nil, fmt.Errorf("invalid CFF Type1 charstrings")
}
program, err := readCFFType1Program(data, chars, dict[18], false)
if err != nil {
return nil, err
}
converted := make([][]byte, len(chars))
for gid, glyph := range program.glyphs {
converted[gid], err = program.outline(glyph)
if err != nil {
return nil, err
}
}
dict[1206], dict[18] = []float64{2}, []float64{0, 0}
return replaceCFFCharstrings(data, dict, topStart, topSize, charStart, charEnd-charStart, converted, nil), nil
}
+649
View File
@@ -0,0 +1,649 @@
// 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"
"encoding/binary"
"fmt"
"sort"
"unicode/utf16"
"unicode/utf8"
)
// cmapSegment cmap表段结构
// 字段: start 开始字符, end 结束字符, delta 增量, offset 偏移
type cmapSegment struct {
start, end uint16
delta int16
offset uint16
}
// NormalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号
// 入参: data 字符映射表
// 返回: []byte 等价字符映射表, error 子表错误
func NormalizeCmap(data []byte) ([]byte, error) {
if len(data) < 4 {
return data, nil
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count > (len(data)-4)/8 {
return nil, fmt.Errorf("invalid cmap encoding records")
}
var out []byte
converted := make(map[uint32]uint32)
for n := 0; n < count; n++ {
record := 8 + 8*n
offset := binary.BigEndian.Uint32(data[record:])
if uint64(offset)+2 > uint64(len(data)) {
return nil, fmt.Errorf("invalid cmap subtable offset")
}
format := binary.BigEndian.Uint16(data[offset:])
if format != 2 && format != 8 && format != 10 && format != 13 {
continue
}
if out == nil {
out = bytes.Clone(data)
}
next, ok := converted[offset]
if !ok {
var mapping map[rune]uint16
var err error
language := uint32(0)
if format == 2 {
mapping, err = parseCmapFormat2(data[offset:])
if err == nil {
language = uint32(binary.BigEndian.Uint16(data[offset+4:]))
}
} else {
mapping, err = parseCmapExtended(data[offset:])
if err == nil {
language = binary.BigEndian.Uint32(data[offset+8:])
}
}
if err != nil {
return nil, err
}
table := BuildCmapTable(0, mapping)
if language > 65535 {
table = buildCmapTableFormat12(0, mapping)
}
subtable := table[binary.BigEndian.Uint32(table[8:]):]
if binary.BigEndian.Uint16(subtable) == 12 {
binary.BigEndian.PutUint32(subtable[8:], language)
} else {
binary.BigEndian.PutUint16(subtable[4:], uint16(language))
}
if uint64(len(out))+uint64(len(subtable)) > 1<<32-1 {
return nil, fmt.Errorf("normalized cmap exceeds offset range")
}
next = uint32(len(out))
converted[offset] = next
out = append(out, subtable...)
}
binary.BigEndian.PutUint32(out[record:], next)
}
if out == nil {
return data, nil
}
return out, nil
}
// parseCmapExtended 读取混合字符、扩展数组及常量分组,限制到有效Unicode范围
// 入参: data 格式8、10或13子表
// 返回: map[rune]uint16 独立字符映射, error 结构或映射错误
func parseCmapExtended(data []byte) (map[rune]uint16, error) {
if len(data) < 12 {
return nil, fmt.Errorf("truncated extended cmap")
}
length := uint64(binary.BigEndian.Uint32(data[4:]))
if length < 12 || length > uint64(len(data)) {
return nil, fmt.Errorf("invalid extended cmap length")
}
data = data[:int(length)]
format := binary.BigEndian.Uint16(data)
result := make(map[rune]uint16)
add := func(code uint32, glyph uint16) error {
char := rune(code)
if code > utf8.MaxRune {
high, low := rune(code>>16), rune(code&65535)
if format != 8 || high < 0xd800 || high > 0xdbff || low < 0xdc00 || low > 0xdfff {
return fmt.Errorf("invalid extended cmap character")
}
char = utf16.DecodeRune(high, low)
}
if glyph != 0 && utf8.ValidRune(char) {
if previous, exists := result[char]; exists && previous != glyph {
return fmt.Errorf("ambiguous extended cmap character")
}
result[char] = glyph
}
return nil
}
if format == 10 {
if len(data) < 20 {
return nil, fmt.Errorf("truncated cmap format 10")
}
first, count := uint64(binary.BigEndian.Uint32(data[12:])), uint64(binary.BigEndian.Uint32(data[16:]))
if count > uint64((len(data)-20)/2) || first+count > utf8.MaxRune+1 {
return nil, fmt.Errorf("invalid cmap format 10 range")
}
for index := uint64(0); index < count; index++ {
if err := add(uint32(first+index), binary.BigEndian.Uint16(data[20+int(index)*2:])); err != nil {
return nil, err
}
}
return result, nil
}
offset := 16
if format == 8 {
offset = 8208
} else if format != 13 {
return nil, fmt.Errorf("invalid extended cmap format %d", format)
}
if len(data) < offset {
return nil, fmt.Errorf("truncated cmap format %d", format)
}
count := uint64(binary.BigEndian.Uint32(data[offset-4:]))
if count > uint64((len(data)-offset)/12) {
return nil, fmt.Errorf("invalid extended cmap group count")
}
var previous uint32
for index := uint64(0); index < count; index++ {
group := data[offset+int(index)*12:]
first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), uint64(binary.BigEndian.Uint32(group[8:]))
if first > last || index != 0 && first <= previous {
return nil, fmt.Errorf("invalid extended cmap group order")
}
previous = last
endGlyph := glyph
if format == 8 {
endGlyph += uint64(last) - uint64(first)
if first <= 65535 && last > 65535 {
return nil, fmt.Errorf("mixed cmap character range")
}
if last > utf8.MaxRune && (first>>16 != last>>16 || first>>16 < 0xd800 || last>>16 > 0xdbff || first&65535 < 0xdc00 || last&65535 > 0xdfff) {
return nil, fmt.Errorf("invalid UTF-16 cmap group")
}
} else if last > utf8.MaxRune {
return nil, fmt.Errorf("invalid cmap Unicode range")
}
if endGlyph > 65535 {
return nil, fmt.Errorf("extended cmap glyph index overflow")
}
for code := uint64(first); code <= uint64(last); code++ {
if format == 8 {
word := code
if code > 65535 {
word >>= 16
}
if (data[12+word/8]&(1<<(7-word%8)) != 0) != (code > 65535) {
return nil, fmt.Errorf("invalid cmap character width")
}
}
id := glyph
if format == 8 {
id += code - uint64(first)
}
if err := add(uint32(code), uint16(id)); err != nil {
return nil, err
}
}
}
return result, nil
}
// parseCmapFormat2 解析混合单字节与双字节字符的子头映射
// 入参: data format 2子表
// 返回: map[rune]uint16 编码值到字形编号, error 子头或字形数组越界
func parseCmapFormat2(data []byte) (map[rune]uint16, error) {
if len(data) < 526 {
return nil, fmt.Errorf("truncated cmap format 2")
}
length := int(binary.BigEndian.Uint16(data[2:]))
if length < 526 || length > len(data) {
return nil, fmt.Errorf("invalid cmap format 2 length")
}
data = data[:length]
maxKey := 0
for n := 0; n < 256; n++ {
key := int(binary.BigEndian.Uint16(data[6+2*n:]))
if key%8 != 0 || 518+key+8 > len(data) {
return nil, fmt.Errorf("invalid cmap format 2 subheader")
}
maxKey = max(maxKey, key)
}
mapping := make(map[rune]uint16)
for high := 0; high < 256; high++ {
key := 0
if high != 0 {
key = int(binary.BigEndian.Uint16(data[6+2*high:]))
if key == 0 {
continue
}
}
pos := 518 + key
first, count := int(binary.BigEndian.Uint16(data[pos:])), int(binary.BigEndian.Uint16(data[pos+2:]))
delta := binary.BigEndian.Uint16(data[pos+4:])
start := pos + 6 + int(binary.BigEndian.Uint16(data[pos+6:]))
if first+count > 256 || count > 0 && (start < 526+maxKey || start+count*2 > len(data)) {
return nil, fmt.Errorf("invalid cmap format 2 glyph array")
}
for n := 0; n < count; n++ {
low := first + n
if high == 0 && binary.BigEndian.Uint16(data[6+2*low:]) != 0 {
continue
}
glyph := binary.BigEndian.Uint16(data[start+2*n:])
if glyph != 0 {
mapping[rune(high*256+low)] = glyph + delta
}
}
}
return mapping, nil
}
// PackedGlyphRune 获取包装字体字符
// 入参: gid 字形ID
// 返回: rune 包装字体字符
func PackedGlyphRune(gid uint16) rune {
return 0xF0000 + rune(gid)
}
// ParseCmapMappings 解析cmap字符映射
// 入参: data cmap表数据
// 返回: map[rune]uint16 字符到字形映射
func ParseCmapMappings(data []byte) map[rune]uint16 {
if len(data) < 4 {
return nil
}
numTables := int(binary.BigEndian.Uint16(data[2:4]))
result := make(map[rune]uint16)
for i := 0; i < numTables; i++ {
pos := 4 + i*8
if pos+8 > len(data) {
break
}
platformID := binary.BigEndian.Uint16(data[pos : pos+2])
if platformID != 0 && platformID != 3 {
continue
}
offset := uint64(binary.BigEndian.Uint32(data[pos+4 : pos+8]))
if offset+2 > uint64(len(data)) {
continue
}
switch binary.BigEndian.Uint16(data[offset : offset+2]) {
case 0:
parseCmapFormat0(data[offset:], result)
case 4:
parseCmapFormat4(data[offset:], result)
case 6:
parseCmapFormat6(data[offset:], result)
case 12:
parseCmapFormat12(data[offset:], result)
case 2, 8, 10, 13:
var mapping map[rune]uint16
var err error
if binary.BigEndian.Uint16(data[offset:]) == 2 {
mapping, err = parseCmapFormat2(data[offset:])
} else {
mapping, err = parseCmapExtended(data[offset:])
}
if err == nil {
for char, glyph := range mapping {
result[char] = glyph
}
}
}
}
if len(result) == 0 {
return nil
}
return result
}
// parseCmapFormat0 解析cmap format 0
// 入参: data 子表数据, result 字符映射
func parseCmapFormat0(data []byte, result map[rune]uint16) {
if len(data) < 262 {
return
}
for i := 0; i < 256; i++ {
gid := uint16(data[6+i])
if gid != 0 {
result[rune(i)] = gid
}
}
}
// parseCmapFormat4 解析cmap format 4
// 入参: data 子表数据, result 字符映射
func parseCmapFormat4(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint16(data[2:4]))
if length > len(data) {
length = len(data)
}
segCount := int(binary.BigEndian.Uint16(data[6:8]) / 2)
endPos := 14
startPos := endPos + segCount*2 + 2
deltaPos := startPos + segCount*2
rangePos := deltaPos + segCount*2
if rangePos+segCount*2 > length {
return
}
for i := 0; i < segCount; i++ {
end := binary.BigEndian.Uint16(data[endPos+i*2:])
start := binary.BigEndian.Uint16(data[startPos+i*2:])
delta := int16(binary.BigEndian.Uint16(data[deltaPos+i*2:]))
rangeOffsetPos := rangePos + i*2
rangeOffset := binary.BigEndian.Uint16(data[rangeOffsetPos:])
if start == 0xFFFF && end == 0xFFFF {
continue
}
for c := start; c <= end; c++ {
var gid uint16
if rangeOffset == 0 {
gid = uint16(int(c) + int(delta))
} else {
gidPos := rangeOffsetPos + int(rangeOffset) + int(c-start)*2
if gidPos+2 > length {
continue
}
gid = binary.BigEndian.Uint16(data[gidPos:])
if gid != 0 {
gid = uint16(int(gid) + int(delta))
}
}
if gid != 0 {
result[rune(c)] = gid
}
if c == 0xFFFF {
break
}
}
}
}
// parseCmapFormat6 解析cmap format 6
// 入参: data 子表数据, result 字符映射
func parseCmapFormat6(data []byte, result map[rune]uint16) {
if len(data) < 10 {
return
}
firstCode := int(binary.BigEndian.Uint16(data[6:8]))
entryCount := int(binary.BigEndian.Uint16(data[8:10]))
for i := 0; i < entryCount && 10+i*2+2 <= len(data); i++ {
gid := binary.BigEndian.Uint16(data[10+i*2:])
if gid != 0 {
result[rune(firstCode+i)] = gid
}
}
}
// parseCmapFormat12 解析cmap format 12
// 入参: data 子表数据, result 字符映射
func parseCmapFormat12(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := uint64(binary.BigEndian.Uint32(data[4:8]))
if length < 16 || length > uint64(len(data)) {
return
}
nGroups := uint64(binary.BigEndian.Uint32(data[12:16]))
if nGroups > (length-16)/12 {
return
}
for i := uint64(0); i < nGroups; i++ {
pos := 16 + int(i)*12
startChar := binary.BigEndian.Uint32(data[pos:])
endChar := binary.BigEndian.Uint32(data[pos+4:])
startGID := binary.BigEndian.Uint32(data[pos+8:])
if startChar > endChar || startGID > 65535 || startChar > utf8.MaxRune {
continue
}
last := min(uint64(endChar), uint64(startChar)+65535-uint64(startGID), uint64(utf8.MaxRune))
for c := uint64(startChar); c <= last; c++ {
gid := uint64(startGID) + c - uint64(startChar)
if gid != 0 && utf8.ValidRune(rune(c)) {
result[rune(c)] = uint16(gid)
}
}
}
}
// AddPackedGlyphMapping 添加字形ID私有映射
// 入参: mapping 字符映射, numGlyphs 字形数量
func AddPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) {
for i := uint16(0); i < numGlyphs; i++ {
mapping[PackedGlyphRune(i)] = i
}
}
// BuildCmapTable 构建格式4或12字符映射,省略缺字映射
// 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据
func BuildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
if shouldBuildCmapFormat12(mapping) {
return buildCmapTableFormat12(numGlyphs, mapping)
}
var segs []cmapSegment
if mapping == nil {
if numGlyphs > 0 {
segs = append(segs, cmapSegment{start: 0, end: numGlyphs - 1, delta: 0, offset: 0})
}
} else {
var codes []int
for r, gid := range mapping {
if r >= 0 && r <= 0xFFFF && gid != 0 {
codes = append(codes, int(r))
}
}
sort.Ints(codes)
if len(codes) > 0 {
start := codes[0]
prev := start
for i := 1; i < len(codes); i++ {
curr := codes[i]
if curr != prev+1 {
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
start = curr
}
prev = curr
}
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
}
}
segs = append(segs, cmapSegment{start: 0xFFFF, end: 0xFFFF, delta: 1, offset: 0})
segCount := uint16(len(segs))
searchRange := uint16(1)
entrySelector := uint16(0)
for searchRange*2 <= segCount {
searchRange *= 2
entrySelector++
}
searchRange *= 2
rangeShift := segCount*2 - searchRange
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint16(4))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(segCount*2))
binary.Write(buf, binary.BigEndian, searchRange)
binary.Write(buf, binary.BigEndian, entrySelector)
binary.Write(buf, binary.BigEndian, rangeShift)
var endCounts, startCounts, idDeltas, idRangeOffsets []uint16
var glyphIds []uint16
for _, s := range segs {
endCounts = append(endCounts, s.end)
startCounts = append(startCounts, s.start)
if s.start == 0xFFFF {
idDeltas = append(idDeltas, 1)
idRangeOffsets = append(idRangeOffsets, 0)
continue
}
if mapping == nil {
idDeltas = append(idDeltas, 0)
idRangeOffsets = append(idRangeOffsets, 0)
continue
}
idDeltas = append(idDeltas, 0)
currentGlyphIdx := len(glyphIds)
for c := int(s.start); c <= int(s.end); c++ {
gid := mapping[rune(c)]
glyphIds = append(glyphIds, gid)
}
offset := (int(segCount)-int(len(idRangeOffsets))-1)*2 + 2 + currentGlyphIdx*2
idRangeOffsets = append(idRangeOffsets, uint16(offset))
}
for _, v := range endCounts {
binary.Write(buf, binary.BigEndian, v)
}
binary.Write(buf, binary.BigEndian, uint16(0))
for _, v := range startCounts {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range idDeltas {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range idRangeOffsets {
binary.Write(buf, binary.BigEndian, v)
}
for _, v := range glyphIds {
binary.Write(buf, binary.BigEndian, v)
}
data := buf.Bytes()
binary.BigEndian.PutUint16(data[2:4], uint16(len(data)))
mainBuf := new(bytes.Buffer)
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(1))
binary.Write(mainBuf, binary.BigEndian, uint16(3))
binary.Write(mainBuf, binary.BigEndian, uint16(1))
binary.Write(mainBuf, binary.BigEndian, uint32(12))
mainBuf.Write(data)
return mainBuf.Bytes()
}
// shouldBuildCmapFormat12 判断终止码、补充平面字符或format 4长度溢出时是否需要format 12
// 入参: mapping 字符到字形的映射
// 返回: bool 是否需要构建format 12子表
func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
if mapping == nil {
return false
}
var codes []int
for r, gid := range mapping {
if r < 0 || gid == 0 {
continue
}
if r >= 0xFFFF {
return true
}
codes = append(codes, int(r))
}
if len(codes) == 0 {
return false
}
sort.Ints(codes)
segCount := 1
glyphIDCount := 1
prev := codes[0]
for i := 1; i < len(codes); i++ {
curr := codes[i]
if curr == prev {
continue
}
if curr != prev+1 {
segCount++
}
glyphIDCount++
prev = curr
}
segCount++
length := 16 + segCount*8 + glyphIDCount*2
return length > 0xFFFF
}
// buildCmapTableFormat12 构建cmap表 (Format 12)
// 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据
func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte {
var codes []int
if mapping == nil {
for i := 0; i < int(numGlyphs); i++ {
codes = append(codes, i)
}
} else {
for r, gid := range mapping {
if r >= 0 && gid != 0 {
codes = append(codes, int(r))
}
}
}
sort.Ints(codes)
type cmapGroup struct {
startChar uint32
endChar uint32
startGID uint32
}
var groups []cmapGroup
for i := 0; i < len(codes); {
r := rune(codes[i])
gid := uint16(codes[i])
if mapping != nil {
gid = mapping[r]
}
group := cmapGroup{startChar: uint32(r), endChar: uint32(r), startGID: uint32(gid)}
prevRune := r
prevGID := gid
i++
for i < len(codes) {
nextRune := rune(codes[i])
nextGID := uint16(codes[i])
if mapping != nil {
nextGID = mapping[nextRune]
}
if nextRune != prevRune+1 || uint32(nextGID) != uint32(prevGID)+1 {
break
}
group.endChar = uint32(nextRune)
prevRune = nextRune
prevGID = nextGID
i++
}
groups = append(groups, group)
}
data := make([]byte, 36+12*len(groups))
binary.BigEndian.PutUint16(data[2:], 2)
binary.BigEndian.PutUint16(data[6:], 4)
binary.BigEndian.PutUint32(data[8:], 20)
binary.BigEndian.PutUint16(data[12:], 3)
binary.BigEndian.PutUint16(data[14:], 10)
binary.BigEndian.PutUint32(data[16:], 20)
sub := data[20:]
binary.BigEndian.PutUint16(sub, 12)
binary.BigEndian.PutUint32(sub[4:], uint32(len(sub)))
binary.BigEndian.PutUint32(sub[12:], uint32(len(groups)))
for index, group := range groups {
position := 16 + 12*index
binary.BigEndian.PutUint32(sub[position:], group.startChar)
binary.BigEndian.PutUint32(sub[position+4:], group.endChar)
binary.BigEndian.PutUint32(sub[position+8:], group.startGID)
}
return data
}
+234
View File
@@ -0,0 +1,234 @@
// 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"
"maps"
"slices"
"unicode/utf8"
"github.com/go-text/typesetting/font/opentype/tables"
)
// subsetCmapVariations 裁剪Unicode变体映射,将非默认变体的字形加入保留集合
// 入参: data 字符映射表, chars 使用字符, glyphs 保留字形, count 字形总数
// 返回: []byte 格式14子表,无变体时为空, bool 映射是否有效
func subsetCmapVariations(data []byte, chars map[rune]bool, glyphs map[uint16]bool, count uint16) ([]byte, bool) {
if len(data) < 4 {
return nil, false
}
n := int(binary.BigEndian.Uint16(data[2:]))
if n > (len(data)-4)/8 {
return nil, false
}
var source []byte
for i := 0; i < n; i++ {
record := data[4+i*8:]
offset := uint64(binary.BigEndian.Uint32(record[4:]))
if offset < uint64(4+n*8) || offset+2 > uint64(len(data)) {
return nil, false
}
if binary.BigEndian.Uint16(data[offset:]) != 14 {
continue
}
if source != nil || binary.BigEndian.Uint16(record) != 0 || binary.BigEndian.Uint16(record[2:]) != 5 || offset+10 > uint64(len(data)) {
return nil, false
}
length := uint64(binary.BigEndian.Uint32(data[offset+2:]))
if length < 10 || offset+length > uint64(len(data)) {
return nil, false
}
source = data[offset : offset+length]
}
if source == nil {
return nil, true
}
return subsetCmapVariationTable(source, chars, glyphs, count)
}
// subsetCmapVariationTable 按使用字符筛选默认及非默认变体,保持选择符和映射有序
// 入参: data 格式14子表, chars 使用字符, glyphs 保留字形, count 字形总数
// 返回: []byte 裁剪后的子表, bool 子表是否有效
func subsetCmapVariationTable(data []byte, chars map[rune]bool, glyphs map[uint16]bool, count uint16) ([]byte, bool) {
n := uint64(binary.BigEndian.Uint32(data[6:]))
header := 10 + n*11
if header > uint64(len(data)) {
return nil, false
}
used := slices.Sorted(maps.Keys(chars))
var records, payload []byte
previous := rune(-1)
for i := uint64(0); i < n; i++ {
raw := data[10+i*11:]
for j, size := range []uint64{4, 5} {
offset := uint64(binary.BigEndian.Uint32(raw[3+j*4:]))
if offset == 0 {
continue
}
if offset < header || offset+4 > uint64(len(data)) {
return nil, false
}
entries := uint64(binary.BigEndian.Uint32(data[offset:]))
if offset+4+entries*size > uint64(len(data)) {
return nil, false
}
}
record, _, err := tables.ParseVariationSelector(raw, data)
selector := cmapVariationRune(record.VarSelector)
if err != nil || !utf8.ValidRune(selector) || selector <= previous || !validCmapVariation(record, count) {
return nil, false
}
previous = selector
if !chars[selector] {
continue
}
defaults := subsetCmapDefaultVariations(record.DefaultUVS.Ranges, used)
var alternatives []byte
for _, entry := range record.NonDefaultUVS.Ranges {
if chars[cmapVariationRune(entry.UnicodeValue)] {
alternatives = append(alternatives, entry.UnicodeValue[:]...)
alternatives = binary.BigEndian.AppendUint16(alternatives, uint16(entry.GlyphID))
glyphs[uint16(entry.GlyphID)] = true
}
}
if len(defaults)+len(alternatives) == 0 {
continue
}
start := len(records)
records = append(records, record.VarSelector[:]...)
records = append(records, make([]byte, 8)...)
for j, entries := range [][]byte{defaults, alternatives} {
if len(entries) == 0 {
continue
}
binary.BigEndian.PutUint32(records[start+3+j*4:], uint32(len(payload)+1))
payload = binary.BigEndian.AppendUint32(payload, uint32(len(entries)/(4+j)))
payload = append(payload, entries...)
}
}
if len(records) == 0 {
return nil, true
}
if uint64(len(records))+uint64(len(payload))+10 > 1<<32-1 {
return nil, false
}
for i := 0; i < len(records); i += 11 {
for _, j := range []int{3, 7} {
offset := binary.BigEndian.Uint32(records[i+j:])
if offset != 0 {
binary.BigEndian.PutUint32(records[i+j:], offset-1+uint32(10+len(records)))
}
}
}
out := make([]byte, 10, 10+len(records)+len(payload))
binary.BigEndian.PutUint16(out, 14)
binary.BigEndian.PutUint32(out[2:], uint32(cap(out)))
binary.BigEndian.PutUint32(out[6:], uint32(len(records)/11))
out = append(out, records...)
return append(out, payload...), true
}
// validCmapVariation 检查变体映射的字符范围、排序、互斥关系及字形编号
// 入参: record 选择符记录, count 字形总数
// 返回: bool 映射是否有效
func validCmapVariation(record tables.VariationSelector, count uint16) bool {
defaults := record.DefaultUVS.Ranges
previous := rune(-1)
for _, entry := range defaults {
first := cmapVariationRune(entry.StartUnicodeValue)
last := first + rune(entry.AdditionalCount)
if first <= previous || !utf8.ValidRune(first) || !utf8.ValidRune(last) || first < 0xd800 && last >= 0xd800 {
return false
}
previous = last
}
previous = -1
index := 0
for _, entry := range record.NonDefaultUVS.Ranges {
char := cmapVariationRune(entry.UnicodeValue)
if char <= previous || !utf8.ValidRune(char) || uint16(entry.GlyphID) >= count {
return false
}
previous = char
for index < len(defaults) && cmapVariationRune(defaults[index].StartUnicodeValue)+rune(defaults[index].AdditionalCount) < char {
index++
}
if index < len(defaults) && cmapVariationRune(defaults[index].StartUnicodeValue) <= char {
return false
}
}
return true
}
// subsetCmapDefaultVariations 筛选默认变体的使用字符,将连续字符合并为格式14范围
// 入参: ranges 默认变体范围, chars 已排序的使用字符
// 返回: []byte 范围记录,不含数量字段
func subsetCmapDefaultVariations(ranges []tables.UnicodeRange, chars []rune) []byte {
var out []byte
index := 0
last := rune(-2)
for _, char := range chars {
for index < len(ranges) && cmapVariationRune(ranges[index].StartUnicodeValue)+rune(ranges[index].AdditionalCount) < char {
index++
}
if index == len(ranges) {
break
}
if cmapVariationRune(ranges[index].StartUnicodeValue) > char {
continue
}
if char == last+1 && out[len(out)-1] < 255 {
out[len(out)-1]++
} else {
out = append(out, byte(char>>16), byte(char>>8), byte(char), 0)
}
last = char
}
return out
}
// cmapVariationRune 读取变体映射中的24位Unicode码点
// 入参: value 三字节码点
// 返回: rune Unicode码点
func cmapVariationRune(value [3]byte) rune {
return rune(value[0])<<16 | rune(value[1])<<8 | rune(value[2])
}
// buildCmapWithVariations 在生成的Unicode映射中插入格式14记录,保持编码记录有序
// 入参: count 字形总数, mapping 字符映射, variations 格式14子表
// 返回: []byte 字符映射表
func buildCmapWithVariations(count uint16, mapping map[rune]uint16, variations []byte) []byte {
base := BuildCmapTable(count, mapping)
if len(variations) == 0 {
return base
}
payload := base[binary.BigEndian.Uint32(base[8:]):]
out := make([]byte, 28, 28+len(payload)+len(variations))
binary.BigEndian.PutUint16(out[2:], 3)
encoding := uint16(3)
if binary.BigEndian.Uint16(payload) == 12 {
encoding = 4
}
binary.BigEndian.PutUint16(out[6:], encoding)
binary.BigEndian.PutUint32(out[8:], 28)
binary.BigEndian.PutUint16(out[14:], 5)
binary.BigEndian.PutUint32(out[16:], uint32(28+len(payload)))
last := 4 + 8*(int(binary.BigEndian.Uint16(base[2:]))-1)
copy(out[20:28], base[last:last+8])
binary.BigEndian.PutUint32(out[24:], 28)
out = append(out, payload...)
return append(out, variations...)
}
+154
View File
@@ -0,0 +1,154 @@
// 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"
"encoding/binary"
"fmt"
"strings"
"unicode/utf8"
)
// Face 字体文件中的字体名称与零起始索引,不依赖文件名
type Face struct {
Index int `json:"index"`
FullName string `json:"fullName"`
Family string `json:"family"`
Style string `json:"style"`
PostScriptName string `json:"postscriptName"`
Names []string `json:"names"`
}
// FaceInfo 从名称表读取字体信息
// 入参: data 名称表数据, index 零起始字体索引
// 返回: Face 字体信息
func FaceInfo(data []byte, index int) Face {
records, values := fontNameValues(data)
name := func(ids ...uint16) string {
for _, id := range ids {
for _, record := range records {
if record.Name == id {
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
return value
}
}
}
}
return ""
}
return Face{Index: index, FullName: name(4, 6, 16, 1), Family: name(16, 1), Style: name(17, 2), PostScriptName: name(6), Names: NamesFromTable(data)}
}
// FileCount 校验字体或集合头并读取字体数量
// 入参: data OpenType数据
// 返回: int 字体数量, error 格式错误
func FileCount(data []byte) (int, error) {
if len(data) < 12 {
return 0, fmt.Errorf("invalid font header")
}
switch string(data[:4]) {
case "\x00\x01\x00\x00", "OTTO", "true":
return 1, nil
case "ttcf":
version := binary.BigEndian.Uint32(data[4:])
count := uint64(binary.BigEndian.Uint32(data[8:]))
if (version != 0x00010000 && version != 0x00020000) || count == 0 || count > uint64(len(data)-12)/4 {
return 0, fmt.Errorf("invalid font collection header")
}
return int(count), nil
default:
return 0, fmt.Errorf("invalid OpenType header")
}
}
// ReadFaces 读取已解封装字体或集合的名称信息
// 入参: data OpenType数据
// 返回: []Face 字体列表, error 格式或名称表错误
func ReadFaces(data []byte) ([]Face, error) {
count, err := FileCount(data)
if err != nil {
return nil, err
}
faces := make([]Face, 0, count)
for index := 0; index < count; index++ {
tables, err := FileTables(data, index)
if err != nil {
return nil, err
}
face := FaceInfo(tables["name"], index)
if face.FullName == "" {
return nil, fmt.Errorf("font %d has no usable name", index)
}
faces = append(faces, face)
}
return faces, nil
}
// ExtractFace 提取已解封装字体中的集合项,规范字符映射并保留原始表
// 入参: data OpenType数据, index 零起始字体索引
// 返回: []byte 独立字体数据, error 格式或索引错误
func ExtractFace(data []byte, index int) ([]byte, error) {
var err error
if bytes.HasPrefix(data, []byte("ttcf")) {
data, err = ExtractCollectionFont(data, index)
if err != nil {
return nil, err
}
index = 0
}
tables, err := FileTables(data, index)
if err != nil {
return nil, err
}
cmap, err := NormalizeCmap(tables["cmap"])
if err != nil {
return nil, err
}
if !bytes.Equal(cmap, tables["cmap"]) {
tables["cmap"] = cmap
return SerializeOTF(tables)
}
return bytes.Clone(data), nil
}
// MissingGlyphs 检查独立字体的字符覆盖,忽略段落换行并按首次出现顺序去重
// 入参: data 独立OpenType数据, text 待检查文字
// 返回: string 缺失字符, error 字体或文字格式错误
func MissingGlyphs(data []byte, text string) (string, error) {
if !utf8.ValidString(text) {
return "", fmt.Errorf("text must be UTF-8")
}
data, err := DefaultCFF2Font(data)
if err != nil {
return "", err
}
sfnt, err := ParseSFNT(data)
if err != nil {
return "", err
}
seen := make(map[rune]bool)
var missing strings.Builder
for _, char := range text {
if char == '\r' || char == '\n' || seen[char] {
continue
}
seen[char] = true
if sfnt.GlyphIndex(char) == 0 {
missing.WriteRune(char)
}
}
return missing.String(), nil
}
+555
View File
@@ -0,0 +1,555 @@
// 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"
"errors"
"fmt"
"math"
"sync"
"github.com/go-text/typesetting/font/cff"
"github.com/go-text/typesetting/font/opentype"
)
// Outliner 缓存字体程序并提取独立字形轮廓
type Outliner struct {
Font *SFNT
cffOnce sync.Once
cff *cff.CFF
cffErr error
once sync.Once
program *ttInterpreter
err error
mu sync.Mutex
warnings map[uint16]error
}
// ttGlyph 保存字形轮廓及四个度量虚拟点
type ttGlyph struct {
points []ttPoint
ends []uint16
metrics *[4]ttPoint
warning error
}
// Path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理
// 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放
// 返回: error 字形解析错误
func (f *Outliner) Path(path glyphPather, glyph uint16, scale float64) error {
type point struct{ X, Y float64 }
if !f.Font.IsTrueType {
f.cffOnce.Do(func() {
data, err := SanitizeCFF(f.Font.Tables["CFF "])
if err == nil {
data, err = NormalizeCFFCharstringsAt(data, f.Font.UnitsPerEm())
}
if err != nil {
f.cffErr = err
return
}
f.cff, f.cffErr = cff.Parse(data)
})
if f.cffErr != nil {
return f.cffErr
}
segments, _, err := f.cff.LoadGlyph(glyph)
if err != nil {
return err
}
open := false
for _, segment := range segments {
p := segment.Args
switch segment.Op {
case opentype.SegmentOpMoveTo:
if open {
path.Close()
}
path.MoveTo(float64(p[0].X)*scale, float64(p[0].Y)*scale)
open = true
case opentype.SegmentOpLineTo:
path.LineTo(float64(p[0].X)*scale, float64(p[0].Y)*scale)
case opentype.SegmentOpCubeTo:
path.CubeTo(float64(p[0].X)*scale, float64(p[0].Y)*scale, float64(p[1].X)*scale, float64(p[1].Y)*scale, float64(p[2].X)*scale, float64(p[2].Y)*scale)
default:
return fmt.Errorf("invalid CFF curve")
}
}
if open {
path.Close()
}
return nil
}
contour, err := f.glyph(glyph, make(map[uint16]bool))
if err != nil {
return err
}
points := contour.points
first := 0
for _, endpoint := range contour.ends {
last := int(endpoint)
if last < first || last >= len(points)-4 {
return fmt.Errorf("invalid TrueType contour endpoints")
}
if last == first {
first = last + 1
continue
}
coordinate := func(i int) point {
return point{X: float64(points[i].x) * scale / 64, Y: float64(points[i].y) * scale / 64}
}
start, end := coordinate(first), coordinate(last)
begin, stop := first, last
if points[first].on {
begin++
} else if points[last].on {
start, stop = end, last-1
} else {
start = point{X: (start.X + end.X) / 2, Y: (start.Y + end.Y) / 2}
}
path.MoveTo(start.X, start.Y)
var control point
pending := false
for i := begin; i <= stop; i++ {
p := coordinate(i)
if points[i].on {
if pending {
path.QuadTo(control.X, control.Y, p.X, p.Y)
} else {
path.LineTo(p.X, p.Y)
}
pending = false
} else {
if pending {
path.QuadTo(control.X, control.Y, (control.X+p.X)/2, (control.Y+p.Y)/2)
}
control, pending = p, true
}
}
if pending {
path.QuadTo(control.X, control.Y, start.X, start.Y)
}
path.Close()
first = last + 1
}
return nil
}
// glyph 解码简单或复合字形并执行对应字节码,不改写字体资源
// 入参: id 字形编号, active 递归访问集合
// 返回: ttGlyph 字形轮廓, error 字形错误
func (f *Outliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) {
var result ttGlyph
if id >= f.Font.NumGlyphs() || active[id] {
return result, fmt.Errorf("invalid TrueType component reference %d", id)
}
active[id] = true
defer delete(active, id)
data, err := trueTypeGlyphData(f.Font.Tables, id)
if err != nil {
return result, err
}
if len(data) == 0 {
data = make([]byte, 12)
}
if len(data) < 10 {
return result, fmt.Errorf("truncated TrueType glyph %d", id)
}
n := int(int16(binary.BigEndian.Uint16(data)))
phantoms := f.phantomPoints(id, data)
var program []byte
if n >= 0 {
result, program, err = readTTSimple(data, n)
} else {
result, program, err = f.composite(id, data, phantoms, active)
}
if err != nil {
return result, err
}
if result.warning != nil {
f.recordGlyphWarning(id, result.warning)
}
if result.metrics != nil {
result.points = append(result.points, result.metrics[:]...)
} else {
result.points = append(result.points, phantoms[:]...)
}
if len(program) == 0 {
return result, nil
}
outlineOnly, err := ttOutlineOnly(f.Font.Tables, program)
if err != nil {
return result, err
}
if outlineOnly {
return result, nil
}
f.once.Do(func() { f.program, f.err = newTTInterpreter(f.Font.Tables, f.Font.UnitsPerEm()) })
if f.err != nil {
if ttRecoverableHint(f.err) {
result.warning = f.err
f.recordGlyphWarning(id, f.err)
return result, nil
}
return result, f.err
}
if f.program.graphics.inhibit {
return result, nil
}
vm := f.program.clone()
vm.zones[1], vm.ends = append([]ttPoint(nil), result.points...), result.ends
if err := vm.run(program); err != nil {
if ttRecoverableHint(err) {
result.warning = err
f.recordGlyphWarning(id, err)
return result, nil
}
return result, fmt.Errorf("TrueType glyph %d: %w", id, err)
}
result.points = vm.zones[1]
return result, nil
}
// phantomPoints 按水平和垂直度量生成字形末尾的四个虚拟点
// 入参: id 字形编号, data 完整字形头
// 返回: [4]ttPoint 左右边距与上下原点
func (f *Outliner) phantomPoints(id uint16, data []byte) [4]ttPoint {
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.Font.sideBearing(id, false))
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
if f.Font.vertical != 0 {
top += int32(f.Font.sideBearing(id, true))
}
return [4]ttPoint{{x: left * 64, ox: left * 64}, {x: (left + int32(f.Font.GlyphAdvance(id))) * 64, ox: (left + int32(f.Font.GlyphAdvance(id))) * 64}, {y: top * 64, oy: top * 64}, {y: (top - int32(f.Font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.Font.GlyphVerticalAdvance(id))) * 64}}
}
// ttRecoverableHint 判断栈参数、指令引用或控制流错误是否允许保留执行前轮廓
// 入参: err 指令错误
// 返回: bool 是否可恢复
func ttRecoverableHint(err error) bool {
return errors.Is(err, errTTStackUnderflow) || errors.Is(err, errTTInvalidReference) || errors.Is(err, errTTInvalidControlFlow)
}
// trueTypeGlyphData 按loca索引读取字形,拒绝倒序或越界的数据范围
// 入参: tables 字体表, id 字形编号
// 返回: []byte 字形数据, error 索引错误
func trueTypeGlyphData(tables map[string][]byte, id uint16) ([]byte, error) {
head, loca, glyf := tables["head"], tables["loca"], tables["glyf"]
if len(head) < 52 {
return nil, fmt.Errorf("truncated TrueType head")
}
var start, end uint64
switch binary.BigEndian.Uint16(head[50:]) {
case 0:
offset := int(id) * 2
if offset+4 > len(loca) {
return nil, fmt.Errorf("truncated TrueType loca")
}
start, end = uint64(binary.BigEndian.Uint16(loca[offset:]))*2, uint64(binary.BigEndian.Uint16(loca[offset+2:]))*2
case 1:
offset := int(id) * 4
if offset+8 > len(loca) {
return nil, fmt.Errorf("truncated TrueType loca")
}
start, end = uint64(binary.BigEndian.Uint32(loca[offset:])), uint64(binary.BigEndian.Uint32(loca[offset+4:]))
default:
return nil, fmt.Errorf("invalid TrueType loca format")
}
if start > end || end > uint64(len(glyf)) {
return nil, fmt.Errorf("invalid TrueType glyph %d range", id)
}
return glyf[start:end], nil
}
// recordGlyphWarning 记录字形恢复提示,允许并发提取轮廓
// 入参: id 字形编号, warning 恢复原因
func (f *Outliner) recordGlyphWarning(id uint16, warning error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.warnings == nil {
f.warnings = make(map[uint16]error)
}
f.warnings[id] = warning
}
// GlyphWarning 返回字形恢复为执行前轮廓的原因
// 入参: id 字形编号
// 返回: error 恢复原因,无恢复时为空
func (f *Outliner) GlyphWarning(id uint16) error {
f.mu.Lock()
defer f.mu.Unlock()
return f.warnings[id]
}
// readTTSimple 读取简单字形的重复标志、差分坐标和字节码
// 入参: data 字形数据, count 轮廓数
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) {
var result ttGlyph
if count == 0 && len(data) == 10 {
return result, nil, nil
}
if len(data) < 12+2*count {
return result, nil, fmt.Errorf("truncated TrueType contour endpoints")
}
pos := 10
for i := 0; i < count; i++ {
end := binary.BigEndian.Uint16(data[pos:])
pos += 2
if i > 0 && end <= result.ends[i-1] {
return result, nil, fmt.Errorf("unordered TrueType contour endpoints")
}
result.ends = append(result.ends, end)
}
length := int(binary.BigEndian.Uint16(data[pos:]))
pos += 2
if length > len(data)-pos {
return result, nil, fmt.Errorf("truncated TrueType glyph instructions")
}
program := data[pos : pos+length]
pos += length
points := 0
if count > 0 {
points = int(result.ends[count-1]) + 1
}
flags := make([]byte, 0, points)
for len(flags) < points {
if pos >= len(data) {
return result, nil, fmt.Errorf("truncated TrueType point flags")
}
flag := data[pos]
pos++
repeat := 1
if flag&8 != 0 {
if pos >= len(data) {
return result, nil, fmt.Errorf("truncated TrueType flag repeat")
}
repeat += int(data[pos])
pos++
}
if repeat > points-len(flags) {
return result, nil, fmt.Errorf("excessive TrueType flag repeat")
}
for i := 0; i < repeat; i++ {
flags = append(flags, flag)
}
}
result.points = make([]ttPoint, points)
for axis := 0; axis < 2; axis++ {
coordinate := int32(0)
short, same := byte(2<<axis), byte(16<<axis)
for i, flag := range flags {
if flag&short != 0 {
if pos >= len(data) {
return result, nil, fmt.Errorf("truncated TrueType short coordinate")
}
delta := int32(data[pos])
pos++
if flag&same == 0 {
delta = -delta
}
coordinate += delta
} else if flag&same == 0 {
if len(data)-pos < 2 {
return result, nil, fmt.Errorf("truncated TrueType coordinate")
}
coordinate += int32(int16(binary.BigEndian.Uint16(data[pos:])))
pos += 2
}
p := &result.points[i]
p.on = flag&1 != 0
if axis == 0 {
p.x, p.ox = coordinate*64, coordinate*64
} else {
p.y, p.oy = coordinate*64, coordinate*64
}
}
}
return result, program, nil
}
// composite 合成分量变换和点匹配,保留分量顺序及父级字节码
// 入参: glyph 父字形编号, data 复合字形数据, phantoms 父字形虚拟点, active 递归访问集合
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
func (f *Outliner) composite(glyph uint16, data []byte, phantoms [4]ttPoint, active map[uint16]bool) (ttGlyph, []byte, error) {
var result ttGlyph
pos := 10
total := -1
first := true
hasInstructions := false
for {
if len(data)-pos < 6 {
return result, nil, fmt.Errorf("truncated TrueType component")
}
flags, id := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
if first && flags&2 == 0 {
return result, nil, fmt.Errorf("first TrueType component requires coordinate offsets")
}
first = false
pos += 4
a, b := int32(data[pos]), int32(data[pos+1])
pos += 2
if flags&1 != 0 {
pos -= 2
if len(data)-pos < 4 {
return result, nil, fmt.Errorf("truncated TrueType component arguments")
}
a, b = int32(binary.BigEndian.Uint16(data[pos:])), int32(binary.BigEndian.Uint16(data[pos+2:]))
pos += 4
}
if flags&2 != 0 {
if flags&1 != 0 {
a, b = int32(int16(a)), int32(int16(b))
} else {
a, b = int32(int8(a)), int32(int8(b))
}
}
matrix := [4]float64{1, 0, 0, 1}
indices := []int(nil)
switch flags & 0xc8 {
case 8:
indices = []int{0}
case 0x40:
indices = []int{0, 3}
case 0x80:
indices = []int{0, 1, 2, 3}
case 0:
default:
return result, nil, fmt.Errorf("conflicting TrueType component transforms")
}
for _, index := range indices {
if len(data)-pos < 2 {
return result, nil, fmt.Errorf("truncated TrueType component transform")
}
matrix[index] = float64(int16(binary.BigEndian.Uint16(data[pos:]))) / 16384
pos += 2
}
if flags&8 != 0 {
matrix[3] = matrix[0]
}
part, err := f.glyph(id, active)
if err != nil {
return result, nil, err
}
if part.warning != nil {
result.warning = fmt.Errorf("TrueType component %d: %w", id, part.warning)
}
if flags&0x200 != 0 {
if result.metrics == nil {
result.metrics = new([4]ttPoint)
}
copy(result.metrics[:], part.points[len(part.points)-4:])
}
pointCount := len(part.points) - 4
for i := range pointCount {
p := &part.points[i]
x, y := float64(p.x), float64(p.y)
p.x = int32(math.Round(matrix[0]*x + matrix[2]*y))
p.y = int32(math.Round(matrix[1]*x + matrix[3]*y))
}
dx, dy := a*64, b*64
if flags&2 == 0 {
if b < 0 || int64(b) >= int64(len(part.points)) {
return result, nil, fmt.Errorf("invalid TrueType component point matching")
}
child := part.points[b]
if int64(b) >= int64(pointCount) {
x, y := float64(child.x), float64(child.y)
child.x = int32(math.Round(matrix[0]*x + matrix[2]*y))
child.y = int32(math.Round(matrix[1]*x + matrix[3]*y))
}
var parent ttPoint
if a >= 0 && int64(a) < int64(len(result.points)) {
parent = result.points[a]
} else {
if total < 0 {
var err error
total, err = trueTypePointCount(f.Font.Tables, glyph, map[uint16]int{}, map[uint16]bool{})
if err != nil {
return result, nil, err
}
}
if a < int32(total) || a >= int32(total)+4 {
return result, nil, fmt.Errorf("invalid TrueType component point matching")
}
metrics := &phantoms
if result.metrics != nil {
metrics = result.metrics
}
parent = metrics[a-int32(total)]
}
dx, dy = parent.x-child.x, parent.y-child.y
} else if flags&0x1800 == 0x800 {
x, y := float64(dx), float64(dy)
dx, dy = int32(math.Round(matrix[0]*x+matrix[2]*y)), int32(math.Round(matrix[1]*x+matrix[3]*y))
}
if flags&6 == 6 {
dx, dy = int32(math.Round(float64(dx)/64))*64, int32(math.Round(float64(dy)/64))*64
}
part.points = part.points[:pointCount]
if len(result.points)+len(part.points) > 65536 {
return result, nil, fmt.Errorf("excessive TrueType component points")
}
for _, end := range part.ends {
result.ends = append(result.ends, uint16(len(result.points)+int(end)))
}
for i := range part.points {
p := &part.points[i]
p.x += dx
p.y += dy
p.ox, p.oy = p.x, p.y
p.tx, p.ty = false, false
}
result.points = append(result.points, part.points...)
hasInstructions = hasInstructions || flags&0x100 != 0
if flags&0x20 == 0 {
break
}
}
var program []byte
if hasInstructions {
if len(data)-pos < 2 {
return result, nil, fmt.Errorf("truncated TrueType composite instructions")
}
n := int(binary.BigEndian.Uint16(data[pos:]))
pos += 2
if n > len(data)-pos {
return result, nil, fmt.Errorf("truncated TrueType composite instructions")
}
program = data[pos : pos+n]
}
return result, program, nil
}
// ttOutlineOnly 识别已剥离提示表但仍引用字体函数或控制值的子集字形,使用设计轮廓
// 入参: tables 字体表, code 字形程序
// 返回: bool 是否只提取轮廓, error 字节码错误
func ttOutlineOnly(tables map[string][]byte, code []byte) (bool, error) {
if len(tables["fpgm"])+len(tables["prep"])+len(tables["cvt "]) != 0 {
return false, nil
}
for pc := 0; pc < len(code); {
op := code[pc]
if op == 0x2a || op == 0x2b || op == 0x3e || op == 0x3f || op == 0x44 || op == 0x45 || op == 0x70 || op >= 0x73 && op <= 0x75 || op >= 0xe0 {
return true, nil
}
var err error
pc, err = ttInstructionEnd(code, pc)
if err != nil {
return false, err
}
}
return false, nil
}
File diff suppressed because it is too large. Load diff
+299
View File
@@ -0,0 +1,299 @@
// 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"
"encoding/binary"
"errors"
"fmt"
"slices"
)
// ErrTTInvalidLocations 表示字形索引不满足递增偏移要求
var ErrTTInvalidLocations = errors.New("invalid TrueType glyph locations")
// NormalizeTrueTypeLocations 验证物理字形边界后按原编号重排,不改变空字形和分量引用
// 入参: tables 独占字体表目录,表内容只读
// 返回: bool 是否重建索引, error 无法可靠恢复的索引或轮廓错误
func NormalizeTrueTypeLocations(tables map[string][]byte) (bool, error) {
if _, ok := tables["glyf"]; !ok {
return false, nil
}
head, maxp, loca, glyf := tables["head"], tables["maxp"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(maxp) < 6 {
return false, fmt.Errorf("%w: missing header", ErrTTInvalidLocations)
}
count := int(binary.BigEndian.Uint16(maxp[4:]))
width := 2
switch binary.BigEndian.Uint16(head[50:]) {
case 0:
case 1:
width = 4
default:
return false, fmt.Errorf("%w: format", ErrTTInvalidLocations)
}
if count == 0 || len(loca) < (count+1)*width {
return false, fmt.Errorf("%w: truncated index", ErrTTInvalidLocations)
}
location := func(i int) uint64 {
if width == 2 {
return uint64(binary.BigEndian.Uint16(loca[i*2:])) * 2
}
return uint64(binary.BigEndian.Uint32(loca[i*4:]))
}
ordered, previous := true, uint64(0)
for i := range count + 1 {
offset := location(i)
if offset > uint64(len(glyf)) {
return false, fmt.Errorf("%w: offset %d outside table", ErrTTInvalidLocations, i)
}
ordered = ordered && previous <= offset
previous = offset
}
if ordered {
return false, nil
}
offsets := make([]int, count+1)
for i := range offsets {
offsets[i] = int(location(i))
}
physical := slices.Clone(offsets)
physical = append(physical, len(glyf))
slices.Sort(physical)
physical = slices.Compact(physical)
bodies := make(map[int][]byte, count)
components := make([][]uint16, count)
var total uint64
for id := range count {
start := offsets[id]
if start == offsets[id+1] {
continue
}
if _, duplicate := bodies[start]; duplicate {
return false, fmt.Errorf("%w: ambiguous shared offset %d", ErrTTInvalidLocations, start)
}
index, _ := slices.BinarySearch(physical, start)
if index+1 >= len(physical) {
return false, fmt.Errorf("%w: missing glyph %d body", ErrTTInvalidLocations, id)
}
data := glyf[start:physical[index+1]]
size, refs, err := trueTypeGlyphSize(data, count)
if err != nil {
return false, fmt.Errorf("%w: glyph %d: %w", ErrTTInvalidLocations, id, err)
}
if len(data)-size > 3 || len(bytes.Trim(data[size:], "\x00")) != 0 {
return false, fmt.Errorf("%w: ambiguous glyph %d padding", ErrTTInvalidLocations, id)
}
bodies[start], components[id] = data[:size], refs
total += (uint64(size) + 3) &^ 3
if total > uint64(^uint(0)>>1) || total > uint64(^uint32(0)) {
return false, fmt.Errorf("%w: rebuilt table exceeds capacity", ErrTTInvalidLocations)
}
}
state := make([]uint8, count)
var visit func(int) error
visit = func(id int) error {
if state[id] == 1 {
return fmt.Errorf("%w: cyclic component %d", ErrTTInvalidLocations, id)
}
if state[id] == 2 {
return nil
}
state[id] = 1
for _, ref := range components[id] {
if err := visit(int(ref)); err != nil {
return err
}
}
state[id] = 2
return nil
}
for id := range count {
if err := visit(id); err != nil {
return false, err
}
}
result, locations := make([]byte, 0, total), make([]byte, (count+1)*4)
for id := range count {
binary.BigEndian.PutUint32(locations[id*4:], uint32(len(result)))
if offsets[id] != offsets[id+1] {
result = append(result, bodies[offsets[id]]...)
result = append(result, make([]byte, (-len(result))&3)...)
}
}
binary.BigEndian.PutUint32(locations[count*4:], uint32(len(result)))
header := bytes.Clone(head)
binary.BigEndian.PutUint16(header[50:], 1)
tables["head"], tables["loca"], tables["glyf"] = header, locations, result
return true, nil
}
// trueTypeGlyphSize 检查单个字形的完整记录长度及复合引用,不执行指令或分配坐标
// 入参: data 字形及尾部对齐数据, count 字形总数
// 返回: int 记录长度, []uint16 分量编号, error 结构错误
func trueTypeGlyphSize(data []byte, count int) (int, []uint16, error) {
if len(data) < 10 {
return 0, nil, fmt.Errorf("truncated glyph header")
}
contours := int(int16(binary.BigEndian.Uint16(data)))
pos := 10
if contours >= 0 {
if contours == 0 && len(data) == 10 {
return 10, nil, nil
}
if contours > (len(data)-12)/2 || len(data) < 12 {
return 0, nil, fmt.Errorf("truncated contour endpoints")
}
points := 0
for range contours {
end := int(binary.BigEndian.Uint16(data[pos:])) + 1
if end <= points {
return 0, nil, fmt.Errorf("unordered contour endpoints")
}
points, pos = end, pos+2
}
instructions := int(binary.BigEndian.Uint16(data[pos:]))
pos += 2
if instructions > len(data)-pos {
return 0, nil, fmt.Errorf("truncated glyph instructions")
}
pos += instructions
coordinates := 0
for seen := 0; seen < points; {
if pos >= len(data) {
return 0, nil, fmt.Errorf("truncated glyph flags")
}
flag, repeat := data[pos], 1
pos++
if flag&0x80 != 0 {
return 0, nil, fmt.Errorf("reserved glyph flag")
}
if flag&8 != 0 {
if pos >= len(data) {
return 0, nil, fmt.Errorf("truncated glyph repeat")
}
repeat += int(data[pos])
pos++
}
if repeat > points-seen {
return 0, nil, fmt.Errorf("excessive glyph repeat")
}
for axis := range 2 {
if flag&byte(2<<axis) != 0 {
coordinates += repeat
} else if flag&byte(16<<axis) == 0 {
coordinates += 2 * repeat
}
}
seen += repeat
}
if coordinates > len(data)-pos {
return 0, nil, fmt.Errorf("truncated glyph coordinates")
}
return pos + coordinates, nil, nil
}
var refs []uint16
instructions := false
for {
if len(data)-pos < 6 {
return 0, nil, fmt.Errorf("truncated glyph component")
}
flags, id := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
if int(id) >= count || flags&0xe010 != 0 {
return 0, nil, fmt.Errorf("invalid glyph component")
}
refs = append(refs, id)
pos += 6
if flags&1 != 0 {
pos += 2
}
switch flags & 0xc8 {
case 0:
case 8:
pos += 2
case 0x40:
pos += 4
case 0x80:
pos += 8
default:
return 0, nil, fmt.Errorf("conflicting component transforms")
}
if pos > len(data) {
return 0, nil, fmt.Errorf("truncated component arguments")
}
instructions = instructions || flags&0x100 != 0
if flags&0x20 == 0 {
break
}
}
if instructions {
if len(data)-pos < 2 {
return 0, nil, fmt.Errorf("truncated component instructions")
}
length := int(binary.BigEndian.Uint16(data[pos:]))
pos += 2
if length > len(data)-pos {
return 0, nil, fmt.Errorf("truncated component instructions")
}
pos += length
}
return pos, refs, nil
}
// trueTypePointCount 按完整轮廓计算虚拟点编号,局部复用分量计数,不执行字形指令
// 入参: tables 字体表, id 字形编号, counts 本次计数缓存, active 递归访问集合
// 返回: int 不含虚拟点的轮廓点数, error 结构或循环引用错误
func trueTypePointCount(tables map[string][]byte, id uint16, counts map[uint16]int, active map[uint16]bool) (int, error) {
if count, ok := counts[id]; ok {
return count, nil
}
maxp := tables["maxp"]
if len(maxp) < 6 || id >= binary.BigEndian.Uint16(maxp[4:]) || active[id] {
return 0, fmt.Errorf("invalid TrueType component reference %d", id)
}
active[id] = true
defer delete(active, id)
data, err := trueTypeGlyphData(tables, id)
if err != nil {
return 0, err
}
if len(data) == 0 {
counts[id] = 0
return 0, nil
}
_, refs, err := trueTypeGlyphSize(data, int(binary.BigEndian.Uint16(maxp[4:])))
if err != nil {
return 0, err
}
contours := int(int16(binary.BigEndian.Uint16(data)))
count := 0
if contours > 0 {
count = int(binary.BigEndian.Uint16(data[10+2*(contours-1):])) + 1
} else {
for _, ref := range refs {
points, err := trueTypePointCount(tables, ref, counts, active)
if err != nil {
return 0, err
}
count += points
if count > 65536 {
return 0, fmt.Errorf("excessive TrueType component points")
}
}
}
counts[id] = count
return count, nil
}
+64
View File
@@ -0,0 +1,64 @@
// 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 (
"path"
"strings"
)
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
// NormalizeName 规范化字体名称
// 入参: name 字体名称
// 返回: string 规范化后的字体名称
func NormalizeName(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = path.Base(strings.TrimSpace(name))
if IsFontFileName(name) {
name = strings.TrimSuffix(name, path.Ext(name))
}
name = strings.ToLower(strings.TrimSpace(name))
return fontNameReplacer.Replace(name)
}
// IsFontFileName 判断是否为字体文件名
// 入参: name 文件名
// 返回: bool 是否为字体文件
func IsFontFileName(name string) bool {
switch strings.ToLower(path.Ext(name)) {
case ".ttf", ".otf", ".ttc":
return true
default:
return false
}
}
// AppendFontName 追加字体名称
// 入参: names 字体名称列表, seen 去重映射, name 字体名称
// 返回: []string 字体名称列表
func AppendFontName(names []string, seen map[string]bool, name string) []string {
name = strings.TrimSpace(name)
if name == "" {
return names
}
key := NormalizeName(name)
if key == "" || seen[key] {
return names
}
seen[key] = true
return append(names, name)
}
+24
View File
@@ -0,0 +1,24 @@
// 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
// glyphPather 接收字体设计坐标中的轮廓指令
type glyphPather interface {
MoveTo(float64, float64)
LineTo(float64, float64)
QuadTo(float64, float64, float64, float64)
CubeTo(float64, float64, float64, float64, float64, float64)
Close()
}
+166
View File
@@ -0,0 +1,166 @@
// 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"
"encoding/binary"
"io"
"unicode/utf16"
"golang.org/x/text/encoding/charmap"
)
// fontNameRecord 字体名称表记录
type fontNameRecord struct {
Platform uint16
Encoding uint16
Language uint16
Name uint16
Length uint16
Offset uint16
}
// FileNames 按字体索引读取字体或集合中各项的名称
// 入参: file 字体数据
// 返回: [][]string 各字体名称及带样式的字体族名称
func FileNames(file io.ReaderAt) [][]string {
var header [12]byte
if _, err := file.ReadAt(header[:], 0); err != nil {
return nil
}
if string(header[:4]) != "ttcf" {
return [][]string{fontFaceNames(file, 0)}
}
version := binary.BigEndian.Uint32(header[4:])
if version != 0x00010000 && version != 0x00020000 {
return nil
}
var faces [][]string
for i, count := uint32(0), binary.BigEndian.Uint32(header[8:]); i < count; i++ {
var entry [4]byte
if _, err := file.ReadAt(entry[:], 12+int64(i)*4); err != nil {
return nil
}
faces = append(faces, fontFaceNames(file, int64(binary.BigEndian.Uint32(entry[:]))))
}
return faces
}
// fontFaceNames 读取指定字体表目录中的名称
// 入参: file 字体数据, offset 字体表目录偏移
// 返回: []string 字体名称及带样式的字体族名称
func fontFaceNames(file io.ReaderAt, offset int64) []string {
var header [12]byte
if _, err := file.ReadAt(header[:], offset); err != nil {
return nil
}
switch string(header[:4]) {
case "\x00\x01\x00\x00", "OTTO", "true":
default:
return nil
}
entries := make([]byte, int(binary.BigEndian.Uint16(header[4:]))*16)
if _, err := file.ReadAt(entries, offset+12); err != nil {
return nil
}
for i := 0; i < len(entries); i += 16 {
if string(entries[i:i+4]) != "name" {
continue
}
offset := int64(binary.BigEndian.Uint32(entries[i+8:]))
length := int64(binary.BigEndian.Uint32(entries[i+12:]))
data, err := io.ReadAll(io.NewSectionReader(file, offset, length))
if err != nil {
return nil
}
return NamesFromTable(data)
}
return nil
}
// NamesFromTable 解析字体名称表
// 入参: data 名称表数据
// 返回: []string 字体名称列表
func NamesFromTable(data []byte) []string {
records, values := fontNameValues(data)
var names []string
seen := make(map[string]bool)
for _, record := range records {
key := [4]uint16{record.Platform, record.Encoding, record.Language, record.Name}
name := values[key]
switch record.Name {
case 1, 16:
key[3]++
if style := values[key]; style != "" && name != "" {
name += " " + style
}
case 4, 6:
default:
continue
}
names = AppendFontName(names, seen, name)
}
return names
}
// fontNameValues 解码字体名称表的多语言记录
// 入参: data 名称表数据
// 返回: []fontNameRecord 名称记录, map[[4]uint16]string 平台、编码、语言和名称对应的文本
func fontNameValues(data []byte) ([]fontNameRecord, map[[4]uint16]string) {
if len(data) < 6 || binary.BigEndian.Uint16(data) > 1 {
return nil, nil
}
count := int(binary.BigEndian.Uint16(data[2:]))
storage := int(binary.BigEndian.Uint16(data[4:]))
if 6+count*12 > storage || storage > len(data) {
return nil, nil
}
records := make([]fontNameRecord, count)
if err := binary.Read(bytes.NewReader(data[6:]), binary.BigEndian, records); err != nil {
return nil, nil
}
values := make(map[[4]uint16]string)
for _, record := range records {
switch record.Name {
case 1, 2, 4, 6, 16, 17:
default:
continue
}
start := storage + int(record.Offset)
end := start + int(record.Length)
if end > len(data) {
continue
}
var name string
switch {
case record.Platform == 0 || record.Platform == 3 && (record.Encoding == 0 || record.Encoding == 1 || record.Encoding == 10):
if record.Length%2 != 0 {
continue
}
chars := make([]uint16, int(record.Length)/2)
for i := range chars {
chars[i] = binary.BigEndian.Uint16(data[start+i*2:])
}
name = string(utf16.Decode(chars))
case record.Platform == 1 && record.Encoding == 0:
name, _ = charmap.Macintosh.NewDecoder().String(string(data[start:end]))
default:
continue
}
values[[4]uint16{record.Platform, record.Encoding, record.Language, record.Name}] = name
}
return records, values
}
+73
View File
@@ -0,0 +1,73 @@
// 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"
)
// FixDataAggressive 激进修复字体数据
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func FixDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
if len(data) < 4 {
return false, data, nil, false, nil
}
tag := string(data[0:4])
u32Tag := binary.BigEndian.Uint32(data[0:4])
isTrueType := u32Tag == 0x00010000 || tag == "true"
isOpenType := tag == "OTTO"
isTTC := tag == "ttcf"
if isTTC {
return false, data, nil, false, nil
}
if isBareCFFData(data) {
otf, mapping, err := WrapCFFToOTF(data)
if err != nil {
return false, data, nil, false, err
}
return true, otf, mapping, false, nil
}
if !isTrueType && !isOpenType {
return false, data, nil, false, nil
}
fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName)
return fixed, newData, mapping, mc, err
}
// SFNTData 保留字体原始表数据和时间,重新计算目录与校验和
// 入参: tables 原始字体表
// 返回: []byte 独立SFNT数据
func SFNTData(tables map[string][]byte) []byte {
data, _ := SerializeOTF(tables)
return data
}
// isBareCFFData 检查是否为CFF裸字体数据
// 入参: data 字体数据
// 返回: bool 是否为CFF裸数据
func isBareCFFData(data []byte) bool {
if len(data) < 4 {
return false
}
if data[0] != 1 || data[1] != 0 {
return false
}
hdrSize := int(data[2])
offSize := int(data[3])
return 4 <= hdrSize && hdrSize <= len(data) && 1 <= offSize && offSize <= 4
}
+350
View File
@@ -0,0 +1,350 @@
// 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"
"sort"
"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
}
// 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 {
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{binary.BigEndian.Uint16(record), binary.BigEndian.Uint16(record[2:]), format, mapping})
}
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 返回各编码记录的映射项总数,共享子表按记录分别计数
// 返回: int 用于缓存预算的映射项数量
func (f *SFNT) CmapEntries() int {
count := 0
for _, cmap := range f.cmaps {
count += len(cmap.mapping)
}
return count
}
// GlyphIndex 按编码表顺序查找字符,不替换缺失字形
// 入参: char 字符
// 返回: uint16 字形编号
func (f *SFNT) GlyphIndex(char rune) uint16 {
for _, cmap := range f.cmaps {
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 {
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 {
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
}
})
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
}
+433
View File
@@ -0,0 +1,433 @@
// 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"
"encoding/binary"
"fmt"
"maps"
"slices"
"github.com/go-text/typesetting/font/opentype/tables"
)
// Subsettable 判断默认资源后端能否无损裁剪该字体
// 入参: sfnt 已解析字体
// 返回: bool 是否支持裁剪
func Subsettable(sfnt *SFNT) bool {
if fontEmbeddingFlags(sfnt)&0x0100 != 0 || slices.ContainsFunc([]string{"COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) {
return false
}
if sfnt.IsCFF {
if sfnt.Tables["fvar"] != nil {
return false
}
font, err := readType2Font(sfnt.Tables["CFF "])
if err == nil {
return len(font.chars) == int(sfnt.NumGlyphs())
}
cid, err := readCIDSubsetFont(sfnt.Tables["CFF "])
return err == nil && len(cid.fonts[0].chars) == int(sfnt.NumGlyphs())
}
return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt))
}
// fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零
// 入参: sfnt 已解析字体
// 返回: uint16 嵌入标志
func fontEmbeddingFlags(sfnt *SFNT) uint16 {
if table := sfnt.Tables["OS/2"]; len(table) >= 10 {
return binary.BigEndian.Uint16(table[8:10])
}
return 0
}
// ValidateEmbedding 校验字体是否允许作为可编辑轮廓字体嵌入
// 入参: data 独立OpenType字体
// 返回: error 字体解析或嵌入限制
func ValidateEmbedding(data []byte) error {
sfnt, err := ParseSFNT(data)
if err != nil {
return err
}
flags := fontEmbeddingFlags(sfnt)
if flags&0x0200 != 0 || flags&0x000e != 0 && flags&0x0008 == 0 {
return fmt.Errorf("font does not permit editable embedding")
}
return nil
}
// SFNTName 获取字体中的名称
// 入参: sfnt 字体, names 名称类型,按顺序查找
// 返回: string 字体名称
func SFNTName(sfnt *SFNT, names ...uint16) string {
records, values := fontNameValues(sfnt.Tables["name"])
for _, name := range names {
for _, record := range records {
if record.Name != name {
continue
}
if value := values[[4]uint16{record.Platform, record.Encoding, record.Language, name}]; value != "" {
return value
}
}
}
return ""
}
// SubsetSFNT 按实际用字裁剪字体,保留字符映射、名称及度量
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0, mapped 是否保留显式字形编号
// 返回: []byte 字体子集, error 错误信息
func SubsetSFNT(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
data, err := DefaultCFF2Font(data)
if err != nil {
return nil, err
}
sfnt, err := ParseSFNT(data)
if err != nil {
return nil, err
}
if fontEmbeddingFlags(sfnt)&0x0100 != 0 {
return bytes.Clone(data), nil
}
if sfnt.IsCFF {
count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
if err != nil {
return nil, err
}
if count != int(sfnt.NumGlyphs()) {
return nil, fmt.Errorf("CFF glyph count does not match maxp")
}
return subsetCFFFont(sfnt, glyphs, mapped)
}
if sfnt.Tables["fvar"] != nil {
return subsetVariableSFNTFont(sfnt, glyphs)
}
if mapped {
usage := newUsage()
for _, id := range glyphs {
usage.Glyphs[id] = true
for _, char := range sfnt.glyphCharacters(id) {
usage.Characters[char] = true
}
}
tables := make(map[string][]byte)
for _, tag := range []string{"cmap", "head", "hhea", "hmtx", "vhea", "vmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp"} {
if table := sfnt.Tables[tag]; table != nil {
tables[tag] = table
}
}
fixed, err := SerializeOTF(tables)
if err != nil {
return nil, err
}
if subset := SubsetSourceSFNT(fixed, usage); subset != nil {
return subset, nil
}
return fixed, nil
}
return subsetCompactSFNTFont(sfnt, glyphs)
}
// SubsetSourceSFNT 裁剪静态TrueType或CFF原有字体,保留字形编号、组合依赖、度量和提示指令
// 字形替换结果一并保留,集合、可变、彩色和未知表保持原样
// 入参: data 字体数据, usage 全包用字记录
// 返回: []byte 更小的字体子集,无确定收益时为空
func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
if usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
return nil
}
sfnt, err := ParseSFNT(data)
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", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "GSUB", "meta", "DSIG", "FFTM", "PCLT":
default:
return nil
}
}
var unicodeTables []sfntCmap
for _, record := range sfnt.cmaps {
if record.format == 14 {
continue
}
if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) {
unicodeTables = append(unicodeTables, record)
}
}
if len(unicodeTables) == 0 {
return nil
}
glyphs := maps.Clone(usage.Glyphs)
glyphs[0] = true
mapping := make(map[rune]uint16, len(usage.Characters))
for char := range usage.Characters {
id := sfnt.GlyphIndex(char)
for _, cmap := range unicodeTables {
if alternate, ok := cmap.mapping[char]; ok && alternate != id {
return nil
}
}
glyphs[id] = true
if id != 0 {
mapping[char] = id
}
}
variations, ok := subsetCmapVariations(sfnt.Tables["cmap"], usage.Characters, glyphs, sfnt.NumGlyphs())
if !ok || !retainSFNTSubstitutions(sfnt.Tables["GSUB"], glyphs, sfnt.NumGlyphs()) {
return nil
}
if sfnt.IsCFF {
count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
if err != nil || count != 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"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations)
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
}
}
dependencies, err := sfnt.glyphDependencies(slices.Collect(maps.Keys(glyphs))...)
if err != nil {
return nil
}
for _, dependency := range dependencies {
glyphs[dependency] = true
}
var glyf, loca []byte
for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
if binary.BigEndian.Uint16(sfnt.Tables["head"][50:]) == 0 {
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
} else {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
}
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
data, _ := trueTypeGlyphData(sfnt.Tables, uint16(id))
glyf = append(glyf, data...)
if len(glyf)%2 != 0 {
glyf = append(glyf, 0)
}
}
}
tables := maps.Clone(sfnt.Tables)
tables["glyf"], tables["loca"] = glyf, loca
tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations)
tables["head"] = bytes.Clone(tables["head"])
clear(tables["head"][8:12])
delete(tables, "DSIG")
result, err := SerializeOTF(tables)
if err != nil || len(result) >= len(data) {
return nil
}
return result
}
// retainSFNTSubstitutions 保留全部替换结果,使原字形替换表在裁剪后仍可使用
// 入参: data 替换表, glyphs 待保留字形, count 字形总数
// 返回: bool 替换表及结果编号是否有效
func retainSFNTSubstitutions(data []byte, glyphs map[uint16]bool, count uint16) bool {
if len(data) == 0 {
return true
}
if len(data) < 10 || binary.BigEndian.Uint16(data) != 1 || binary.BigEndian.Uint16(data[2:]) > 1 {
return false
}
if binary.BigEndian.Uint16(data[2:]) == 1 && (len(data) < 14 || binary.BigEndian.Uint32(data[10:]) != 0) {
return false
}
layout, _, err := tables.ParseLayout(data)
if err != nil {
return false
}
for _, lookup := range layout.LookupList.Lookups {
subtables, err := lookup.AsGSUBLookups()
if err != nil {
return false
}
for _, subtable := range subtables {
if extension, ok := subtable.(tables.ExtensionSubs); ok {
if extension.ExtensionOffset < 8 || uint64(extension.ExtensionOffset) > uint64(len(extension.RawData)) {
return false
}
subtable, err = extension.Resolve()
if err != nil {
return false
}
}
if !retainSFNTSubstitution(subtable, glyphs, count) {
return false
}
}
}
return true
}
// retainSFNTSubstitution 收集替换子表的输出字形,上下文替换的结果由被引用的查找表提供
// 入参: subtable 替换子表, glyphs 待保留字形, count 字形总数
// 返回: bool 子表类型及输出字形是否有效
func retainSFNTSubstitution(subtable tables.GSUBLookup, glyphs map[uint16]bool, count uint16) bool {
var outputs []tables.GlyphID
switch subtable := subtable.(type) {
case tables.SingleSubs:
switch data := subtable.Data.(type) {
case tables.SingleSubstData1:
for id := uint16(0); id < count; id++ {
if _, ok := data.Coverage.Index(tables.GlyphID(id)); ok {
outputs = append(outputs, tables.GlyphID(id+uint16(data.DeltaGlyphID)))
}
}
case tables.SingleSubstData2:
outputs = data.SubstituteGlyphIDs
default:
return false
}
case tables.MultipleSubs:
for _, sequence := range subtable.Sequences {
outputs = append(outputs, sequence.SubstituteGlyphIDs...)
}
case tables.AlternateSubs:
for _, set := range subtable.AlternateSets {
outputs = append(outputs, set.AlternateGlyphIDs...)
}
case tables.LigatureSubs:
for _, set := range subtable.LigatureSets {
for _, ligature := range set.Ligatures {
outputs = append(outputs, ligature.LigatureGlyph)
}
}
case tables.ReverseChainSingleSubs:
outputs = subtable.SubstituteGlyphIDs
case tables.ContextualSubs, tables.ChainedContextualSubs:
default:
return false
}
for _, id := range outputs {
if uint16(id) >= count {
return false
}
glyphs[uint16(id)] = true
}
return true
}
// subsetCompactSFNTFont 重排新建静态字体的字形,同步复合引用、字符映射和度量
// 入参: sfnt 字体, glyphs 使用字形
// 返回: []byte 紧凑字体子集, error 字形数据错误
func subsetCompactSFNTFont(sfnt *SFNT, glyphs []uint16) ([]byte, error) {
ids := []uint16{0}
remap := map[uint16]uint16{0: 0}
for _, id := range glyphs {
if id >= sfnt.NumGlyphs() {
return nil, fmt.Errorf("font glyph index out of range")
}
if _, ok := remap[id]; !ok {
remap[id] = uint16(len(ids))
ids = append(ids, id)
}
}
dependencies, err := sfnt.glyphDependencies(ids...)
if err != nil {
return nil, err
}
for _, id := range dependencies {
if _, ok := remap[id]; !ok {
remap[id] = uint16(len(ids))
ids = append(ids, id)
}
}
table := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "cvt ", "fpgm", "prep", "gasp", "vhea"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = bytes.Clone(data)
}
}
var glyf, loca, hmtx, vmtx []byte
mapping := make(map[rune]uint16)
for _, id := range ids {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
data, err := trueTypeGlyphData(sfnt.Tables, id)
if err != nil {
return nil, err
}
data = bytes.Clone(data)
if len(data) >= 10 && int16(binary.BigEndian.Uint16(data)) < 0 {
for pos := 10; ; {
flags := binary.BigEndian.Uint16(data[pos:])
child := binary.BigEndian.Uint16(data[pos+2:])
binary.BigEndian.PutUint16(data[pos+2:], remap[child])
pos += 6
if flags&1 != 0 {
pos += 2
}
switch flags & 0xc8 {
case 8:
pos += 2
case 0x40:
pos += 4
case 0x80:
pos += 8
}
if flags&0x20 == 0 {
break
}
}
}
glyf = append(glyf, data...)
for len(glyf)%4 != 0 {
glyf = append(glyf, 0)
}
hmtx = binary.BigEndian.AppendUint16(hmtx, sfnt.GlyphAdvance(id))
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(sfnt.sideBearing(id, false)))
if sfnt.vertical != 0 {
vmtx = binary.BigEndian.AppendUint16(vmtx, sfnt.GlyphVerticalAdvance(id))
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(sfnt.sideBearing(id, true)))
}
for _, char := range sfnt.glyphCharacters(id) {
mapping[char] = remap[id]
}
}
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
table["glyf"], table["loca"], table["hmtx"] = glyf, loca, hmtx
table["cmap"] = BuildCmapTable(uint16(len(ids)), mapping)
binary.BigEndian.PutUint16(table["head"][50:], 1)
binary.BigEndian.PutUint16(table["hhea"][34:], uint16(len(ids)))
binary.BigEndian.PutUint16(table["maxp"][4:], uint16(len(ids)))
if len(table["post"]) >= 32 {
table["post"] = table["post"][:32]
binary.BigEndian.PutUint32(table["post"], 0x30000)
}
if sfnt.vertical != 0 {
table["vmtx"] = vmtx
binary.BigEndian.PutUint16(table["vhea"][34:], uint16(len(ids)))
}
return SerializeOTF(table)
}
+134
View File
@@ -0,0 +1,134 @@
// 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 (
"context"
"encoding/binary"
"fmt"
"unicode/utf8"
"github.com/xiaoqidun/pdfgo"
)
// SymbolProgram 为标准符号字体补齐Unicode映射,保留字形编号和轮廓
// 入参: ctx 取消上下文, data 独立字体数据, name 标准字体名,为空时从字体名称表识别
// 返回: []byte 字体数据, map[string]uint16 标准字形编号,不适用时为空, error 映射或取消错误
func SymbolProgram(ctx context.Context, data []byte, name string) ([]byte, map[string]uint16, error) {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
tables, err := FileTables(data, 0)
if err != nil {
return data, nil, nil
}
legacy := symbolFontCmap(tables["cmap"])
if name == "" && !legacy {
return data, nil, nil
}
actual := symbolFontName(tables["name"])
if name == "" {
name = actual
}
if name != "Symbol" && name != "ZapfDingbats" || !legacy && actual != name {
return data, nil, nil
}
standard, err := new(pdfgo.Reader).ReadFontContext(ctx, pdfgo.Dictionary{"Type": pdfgo.Name("Font"), "Subtype": pdfgo.Name("Type1"), "BaseFont": pdfgo.Name(name)})
if err != nil {
return nil, nil, err
}
codes := make([]byte, 256)
for index := range codes {
codes[index] = byte(index)
}
glyphs, err := standard.DecodeContext(ctx, codes)
if err != nil {
return nil, nil, err
}
original := ParseCmapMappings(tables["cmap"])
mapping := make(map[rune]uint16)
names := make(map[string]uint16)
for _, glyph := range glyphs {
if glyph.Name == ".notdef" || glyph.Name == "" {
continue
}
char, size := utf8.DecodeRuneInString(glyph.Text)
var id uint16
if legacy {
id = original[rune(glyph.Code)]
for _, base := range [...]rune{0xf000, 0xf100, 0xf200} {
if candidate := original[base+rune(glyph.Code)]; candidate != 0 {
if id != 0 && id != candidate {
return nil, nil, fmt.Errorf("ambiguous symbol font mapping")
}
id = candidate
}
}
} else if size != 0 && size == len(glyph.Text) {
id = original[char]
}
if id == 0 {
continue
}
names[glyph.Name] = id
if size != 0 && size == len(glyph.Text) {
mapping[char] = id
}
}
if !legacy || len(mapping) == 0 {
return data, names, nil
}
tables["cmap"] = BuildCmapTable(0, mapping)
program, err := SerializeOTF(tables)
return program, names, err
}
// symbolFontName 从字体名称表识别标准符号字体,不依据文件名推断编码
// 入参: data 名称表数据
// 返回: string 标准字体名,无法识别时为空
func symbolFontName(data []byte) string {
for _, name := range NamesFromTable(data) {
switch NormalizeName(name) {
case "symbol", "symbolmt", "symbolregular":
return "Symbol"
case "zapfdingbats", "zapfdingbatsregular", "itczapfdingbats":
return "ZapfDingbats"
}
}
return ""
}
// symbolFontCmap 识别不含Unicode字符表的Windows符号字体
// 入参: data 字符映射表
// 返回: bool 是否为旧式符号字体
func symbolFontCmap(data []byte) bool {
if len(data) < 4 {
return false
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count > (len(data)-4)/8 {
return false
}
symbol := false
for i := range count {
pos := 4 + i*8
platform, encoding := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
if platform == 0 || platform == 3 && (encoding == 1 || encoding == 10) {
return false
}
symbol = symbol || platform == 3 && encoding == 0
}
return symbol
}
+296
View File
@@ -0,0 +1,296 @@
// 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"
"encoding/binary"
)
// fixTrueType 尝试修复TrueType/OpenType字体数据
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
original := data
data, err := DefaultCFF2Font(data)
if err != nil {
return false, original, nil, false, err
}
if len(data) < 12 {
return false, data, nil, false, nil
}
sfntTag := string(data[0:4])
isCFFSfnt := sfntTag == "OTTO"
numTables := binary.BigEndian.Uint16(data[4:6])
existingTables := make(map[string][]byte)
malformedDirectory := !bytes.Equal(original, data)
pos := 12
for i := 0; i < int(numTables); i++ {
if len(data) < pos+16 {
malformedDirectory = true
break
}
tag := string(data[pos : pos+4])
offset := binary.BigEndian.Uint32(data[pos+8 : pos+12])
length := binary.BigEndian.Uint32(data[pos+12 : pos+16])
if uint64(offset)+uint64(length) <= uint64(len(data)) {
existingTables[tag] = data[int(offset) : int(offset)+int(length)]
padding := (4 - (length & 3)) & 3
if offset%4 != 0 || len(data)-int(offset)-int(length) < int(padding) {
malformedDirectory = true
}
} else {
malformedDirectory = true
}
pos += 16
}
if existingTables["CFF "] != nil {
isCFFSfnt = true
sanitized, err := SanitizeCFF(existingTables["CFF "])
if err != nil {
return false, data, nil, false, err
}
var units uint16
if head := existingTables["head"]; len(head) >= 20 {
units = binary.BigEndian.Uint16(head[18:20])
}
normalized, err := NormalizeCFFCharstringsAt(sanitized, units)
if err != nil {
return false, data, nil, false, err
}
if !bytes.Equal(normalized, existingTables["CFF "]) {
existingTables["CFF "] = normalized
malformedDirectory = true
}
}
cmap, err := NormalizeCmap(existingTables["cmap"])
if err != nil {
return false, data, nil, false, err
}
if !bytes.Equal(cmap, existingTables["cmap"]) {
existingTables["cmap"] = cmap
malformedDirectory = true
}
missingHead := existingTables["head"] == nil
if !missingHead {
head := existingTables["head"]
if len(head) > 54 {
existingTables["head"] = head[:54]
malformedDirectory = true
}
if len(existingTables["head"]) != 54 || binary.BigEndian.Uint16(existingTables["head"][18:20]) == 0 {
missingHead = true
}
}
missingMaxp := existingTables["maxp"] == nil
missingHhea := existingTables["hhea"] == nil
missingHmtx := existingTables["hmtx"] == nil
missingOS2 := existingTables["OS/2"] == nil
missingCmap := existingTables["cmap"] == nil
if !missingCmap {
if !hasUsableCmap(existingTables["cmap"]) {
missingCmap = true
}
}
missingName := existingTables["name"] == nil
missingPost := existingTables["post"] == nil
var numGlyphs uint16 = 0
if !missingMaxp {
maxp := existingTables["maxp"]
if len(maxp) >= 6 {
numGlyphs = binary.BigEndian.Uint16(maxp[4:6])
}
}
if numGlyphs == 0 && isCFFSfnt {
if cff := existingTables["CFF "]; cff != nil {
if n, err := parseCFFAndCountGlyphs(cff); err == nil && n > 0 {
if n > 0xFFFF {
numGlyphs = 0xFFFF
} else {
numGlyphs = uint16(n)
}
}
}
}
if numGlyphs == 0 {
numGlyphs = 255
}
if !missingHhea && !missingHmtx {
hhea := existingTables["hhea"]
hmtx := existingTables["hmtx"]
if len(hhea) < 36 {
missingHhea = true
} else {
numMetrics := binary.BigEndian.Uint16(hhea[34:36])
if numMetrics == 0 || numMetrics > numGlyphs {
missingHhea = true
} else {
expectHmtx := int(numMetrics)*4 + int(numGlyphs-numMetrics)*2
if len(hmtx) < expectHmtx {
missingHmtx = true
} else if len(hmtx) > expectHmtx {
existingTables["hmtx"] = hmtx[:expectHmtx]
malformedDirectory = true
}
}
}
}
if !missingPost && hasBadPostTable(existingTables["post"], numGlyphs, !isCFFSfnt) {
missingPost = true
}
if !fixCmap && !missingHead && !missingMaxp && !missingHhea && !missingHmtx &&
!missingOS2 && !missingCmap && !missingName && !missingPost && !malformedDirectory {
return false, data, nil, false, nil
}
newTables := make(map[string][]byte)
for k, v := range existingTables {
newTables[k] = v
}
if missingHead {
newTables["head"] = buildHeadTable(1000)
}
if missingMaxp {
if isCFFSfnt {
newTables["maxp"] = buildCFFMaxpTable(numGlyphs)
} else {
newTables["maxp"] = buildTrueTypeMaxpTable(numGlyphs)
}
}
if missingHhea {
newTables["hhea"] = buildHheaTable(numGlyphs)
}
if missingHmtx {
var defWidths []uint16
if isCFFSfnt {
if widths, err := parseCFFWidths(newTables["CFF "], int(numGlyphs)); err == nil {
defWidths = widths
}
}
if len(defWidths) == 0 {
defWidths = make([]uint16, numGlyphs)
for i := range defWidths {
defWidths[i] = 500
}
}
newTables["hmtx"] = buildHmtxTable(defWidths)
}
ascender := int16(800)
descender := int16(-200)
if hhea, ok := newTables["hhea"]; ok && len(hhea) >= 10 {
ascender = int16(binary.BigEndian.Uint16(hhea[4:6]))
descender = int16(binary.BigEndian.Uint16(hhea[6:8]))
}
if missingOS2 {
newTables["OS/2"] = BuildOS2TableWithMetrics(ascender, descender)
}
var mapping map[rune]uint16
if fixCmap {
if !missingCmap {
mapping = ParseCmapMappings(newTables["cmap"])
}
if len(mapping) == 0 && isCFFSfnt {
mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs))
}
if len(mapping) == 0 {
mapping = make(map[rune]uint16)
}
AddPackedGlyphMapping(mapping, numGlyphs)
newTables["cmap"] = BuildCmapTable(numGlyphs, mapping)
}
if missingName && fixName {
newTables["name"] = buildNameTable()
}
if missingPost {
newTables["post"] = BuildPostTable()
}
finalData, err := SerializeOTF(newTables)
if err != nil {
return false, data, nil, missingCmap, err
}
return true, finalData, mapping, missingCmap, nil
}
// hasBadPostTable 检查post表的版本、长度及字形数量是否存在已知不兼容
// 入参: data post表数据, numGlyphs 字形总数, isTrueType 是否使用TrueType轮廓
// 返回: bool 是否需要替换post表
func hasBadPostTable(data []byte, numGlyphs uint16, isTrueType bool) bool {
if len(data) < 32 {
return true
}
version := binary.BigEndian.Uint32(data[0:4])
switch version {
case 0x00010000:
return !isTrueType || len(data) != 32
case 0x00020000:
if len(data) < 34 {
return true
}
postGlyphs := binary.BigEndian.Uint16(data[32:34])
if postGlyphs != numGlyphs {
return true
}
return len(data) < 34+int(postGlyphs)*2
case 0x00030000:
return len(data) != 32
default:
return true
}
}
// hasUsableCmap 检查是否存在可用的cmap子表
// 入参: data cmap表数据
// 返回: bool 是否可用
func hasUsableCmap(data []byte) bool {
if len(data) < 4 {
return false
}
numTables := binary.BigEndian.Uint16(data[2:4])
pos := 4
usable := false
for i := 0; i < int(numTables); i++ {
if len(data) < pos+8 {
break
}
platformID := binary.BigEndian.Uint16(data[pos : pos+2])
encodingID := binary.BigEndian.Uint16(data[pos+2 : pos+4])
offset := binary.BigEndian.Uint32(data[pos+4 : pos+8])
if uint32(len(data)) < offset+2 {
return false
}
format := binary.BigEndian.Uint16(data[offset : offset+2])
if !supportedCmapFormat(format, platformID, encodingID) {
return false
}
if platformID == 0 || platformID == 3 {
usable = true
}
pos += 8
}
return usable
}
// supportedCmapFormat 判断cmap子表格式是否可用
// 入参: format cmap子表格式, platformID 平台ID, encodingID 编码ID
// 返回: bool 是否可用
func supportedCmapFormat(format, platformID, encodingID uint16) bool {
switch format {
case 0, 4, 6, 12:
return true
case 14:
return platformID == 0 && encodingID == 5
default:
return false
}
}
+922
View File
@@ -0,0 +1,922 @@
// 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"
"encoding/binary"
"encoding/hex"
"fmt"
"math"
"strconv"
)
// type1StandardNames 保存Adobe标准编码的字形名称,用于复合字形引用
var type1StandardNames = func() [256]string {
var names [256]string
copy(names[32:127], cffStandardStrings[1:96])
for code, name := range map[byte]string{
161: "exclamdown", 162: "cent", 163: "sterling", 164: "fraction", 165: "yen", 166: "florin", 167: "section", 168: "currency",
169: "quotesingle", 170: "quotedblleft", 171: "guillemotleft", 172: "guilsinglleft", 173: "guilsinglright", 174: "fi", 175: "fl",
177: "endash", 178: "dagger", 179: "daggerdbl", 180: "periodcentered", 182: "paragraph", 183: "bullet", 184: "quotesinglbase",
185: "quotedblbase", 186: "quotedblright", 187: "guillemotright", 188: "ellipsis", 189: "perthousand", 191: "questiondown",
193: "grave", 194: "acute", 195: "circumflex", 196: "tilde", 197: "macron", 198: "breve", 199: "dotaccent", 200: "dieresis",
202: "ring", 203: "cedilla", 205: "hungarumlaut", 206: "ogonek", 207: "caron", 208: "emdash", 225: "AE", 227: "ordfeminine",
232: "Lslash", 233: "Oslash", 234: "OE", 235: "ordmasculine", 241: "ae", 245: "dotlessi", 248: "lslash", 249: "oslash", 250: "oe", 251: "germandbls",
} {
names[code] = name
}
return names
}()
// Type1Program 保存解密后的Type1字形与子程序
type Type1Program struct {
glyphs []type1Glyph
subrs [][]byte
lenIV int
matrix []float64
}
// type1Glyph 保存字形名称及指令
type type1Glyph struct {
name string
data []byte
}
// type1Scanner 按词项和显式长度读取Type1程序
type type1Scanner struct {
data []byte
pos int
}
// type1Outline 保存字形指令的操作数栈及轮廓位置
type type1Outline struct {
program *Type1Program
steps *int
active map[string]bool
args []float64
postscript []float64
flexPoints [][2]float64
width float64
sideX float64
sideY float64
originX float64
originY float64
x float64
y float64
pointX float64
pointY float64
open bool
defined bool
flex bool
path bytes.Buffer
}
// ParseType1Program 读取Type1字体的字形程序及子程序
// 入参: data PFA或PFB字体数据
// 返回: Type1Program 字形和子程序, error 错误信息
func ParseType1Program(data []byte) (Type1Program, error) {
var err error
data, err = flattenType1(data)
if err != nil {
return Type1Program{}, err
}
matrix, start, err := readType1Header(data)
if err != nil {
return Type1Program{}, err
}
for start < len(data) && type1Space(data[start]) {
start++
}
if start == len(data) {
return Type1Program{}, fmt.Errorf("empty Type1 eexec data")
}
encrypted := data[start:]
if type1HexEncoded(encrypted) {
end := bytes.Index(encrypted, []byte("cleartomark"))
if end >= 0 {
encrypted = encrypted[:end]
}
digits := make([]byte, 0, len(encrypted))
for _, c := range encrypted {
if type1Space(c) {
continue
}
if !type1HexDigit(c) {
break
}
digits = append(digits, c)
}
encrypted = make([]byte, hex.DecodedLen(len(digits)))
if _, err := hex.Decode(encrypted, digits); err != nil {
return Type1Program{}, fmt.Errorf("invalid Type1 eexec encoding: %w", err)
}
}
if len(encrypted) < 4 {
return Type1Program{}, fmt.Errorf("invalid Type1 eexec data")
}
decrypted := type1Decrypt(encrypted, 55665)[4:]
scanner := type1Scanner{data: decrypted}
program := Type1Program{lenIV: 4, matrix: matrix}
section := ""
for {
token := scanner.next()
if token == "" || token == "closefile" {
break
}
switch token {
case "/lenIV":
value, err := strconv.Atoi(scanner.next())
if err != nil || value < -1 {
return Type1Program{}, fmt.Errorf("invalid Type1 lenIV")
}
program.lenIV = value
case "/Subrs":
section = "subrs"
case "/CharStrings":
section = "glyphs"
case "def", "ND", "|-":
if section == "subrs" {
section = ""
}
case "dup":
if section != "subrs" {
continue
}
index, err := strconv.Atoi(scanner.next())
if err != nil || index < 0 || index > len(decrypted) {
return Type1Program{}, fmt.Errorf("invalid Type1 subroutine index")
}
length, err := strconv.Atoi(scanner.next())
if err != nil {
return Type1Program{}, fmt.Errorf("invalid Type1 subroutine length")
}
if !type1BinaryStart(scanner.next()) {
return Type1Program{}, fmt.Errorf("missing Type1 subroutine data")
}
raw, err := scanner.binary(length)
if err != nil {
return Type1Program{}, err
}
for len(program.subrs) <= index {
program.subrs = append(program.subrs, nil)
}
program.subrs[index] = raw
default:
if section != "glyphs" || len(token) < 2 || token[0] != '/' {
continue
}
name := token[1:]
next := scanner.next()
length, err := strconv.Atoi(next)
if err != nil {
continue
}
if !type1BinaryStart(scanner.next()) {
return Type1Program{}, fmt.Errorf("missing Type1 glyph data")
}
raw, err := scanner.binary(length)
if err != nil {
return Type1Program{}, err
}
program.glyphs = append(program.glyphs, type1Glyph{name: name, data: raw})
}
}
if len(program.glyphs) == 0 {
return Type1Program{}, fmt.Errorf("Type1 font has no glyphs")
}
for index := range program.subrs {
program.subrs[index], err = type1CharString(program.subrs[index], program.lenIV)
if err != nil {
return Type1Program{}, err
}
}
for index := range program.glyphs {
program.glyphs[index].data, err = type1CharString(program.glyphs[index].data, program.lenIV)
if err != nil {
return Type1Program{}, fmt.Errorf("Type1 glyph %s: %w", program.glyphs[index].name, err)
}
}
return program, nil
}
// readType1Header 读取明文字体矩阵及加密数据起点
// 入参: data Type1程序
// 返回: []float64 字体矩阵, int 加密起点, error 头部错误
func readType1Header(data []byte) ([]float64, int, error) {
scanner := type1Scanner{data: data}
var matrix []float64
for {
switch scanner.next() {
case "":
return nil, 0, fmt.Errorf("Type1 font lacks eexec data")
case "eexec":
return matrix, scanner.pos, nil
case "/FontMatrix":
closing := ""
switch scanner.next() {
case "[":
closing = "]"
case "{":
closing = "}"
default:
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix array")
}
matrix = make([]float64, 6)
for index := range matrix {
value, err := strconv.ParseFloat(scanner.next(), 64)
if err != nil || !finite(value) {
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix value")
}
matrix[index] = value
}
if scanner.next() != closing {
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix length")
}
if _, err := readCFFMatrix(cffDict{1207: matrix}, [6]float64{}); err != nil {
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix: %w", err)
}
}
}
}
// flattenType1 将PFB分段容器转换为连续的Type1程序
// 入参: data 字体数据
// 返回: []byte 连续程序, error 错误信息
func flattenType1(data []byte) ([]byte, error) {
if len(data) < 2 || data[0] != 0x80 {
return data, nil
}
var program []byte
for len(data) >= 2 && data[0] == 0x80 {
if data[1] == 3 {
return program, nil
}
if len(data) < 6 || data[1] != 1 && data[1] != 2 {
return nil, fmt.Errorf("invalid PFB segment")
}
length := binary.LittleEndian.Uint32(data[2:6])
if uint64(length) > uint64(len(data)-6) {
return nil, fmt.Errorf("truncated PFB segment")
}
program = append(program, data[6:6+length]...)
data = data[6+length:]
}
return nil, fmt.Errorf("missing PFB end marker")
}
// type1Decrypt 解密Type1的eexec或字形程序
// 入参: data 密文, seed 初始密钥
// 返回: []byte 明文
func type1Decrypt(data []byte, seed uint16) []byte {
out := make([]byte, len(data))
for index, value := range data {
out[index] = value ^ byte(seed>>8)
seed = uint16((uint32(value)+uint32(seed))*52845 + 22719)
}
return out
}
// type1CharString 解密字形程序并移除lenIV随机前缀
// 入参: data 字形密文, lenIV 前缀长度
// 返回: []byte 指令数据, error 错误信息
func type1CharString(data []byte, lenIV int) ([]byte, error) {
if lenIV < 0 {
return data, nil
}
if len(data) < lenIV {
return nil, fmt.Errorf("truncated Type1 charstring")
}
return type1Decrypt(data, 4330)[lenIV:], nil
}
// type1HexEncoded 区分eexec十六进制与二进制封装
// 入参: data eexec数据
// 返回: bool 是否为十六进制
func type1HexEncoded(data []byte) bool {
count := 0
for _, value := range data {
if type1Space(value) {
continue
}
if !type1HexDigit(value) {
return false
}
count++
if count == 8 {
return true
}
}
return false
}
// type1HexDigit 判断字节是否为十六进制数字
// 入参: value 待检查的字节
// 返回: bool 是否为十六进制数字
func type1HexDigit(value byte) bool {
return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'
}
// type1Space 判断PostScript空白字节
// 入参: value 待检查的字节
// 返回: bool 是否为空白字节
func type1Space(value byte) bool {
return value == ' ' || value == '\t' || value == '\r' || value == '\n' || value == '\f' || value == 0
}
// next 读取下一个PostScript词项,字形二进制块由binary处理
// 返回: string 词项
func (s *type1Scanner) next() string {
for s.pos < len(s.data) {
if type1Space(s.data[s.pos]) {
s.pos++
continue
}
if s.data[s.pos] == '%' {
for s.pos < len(s.data) && s.data[s.pos] != '\n' && s.data[s.pos] != '\r' {
s.pos++
}
continue
}
break
}
if s.pos == len(s.data) {
return ""
}
start := s.pos
if s.data[s.pos] == '(' {
depth := 1
s.pos++
for s.pos < len(s.data) && depth > 0 {
switch s.data[s.pos] {
case '\\':
if s.pos+1 < len(s.data) {
s.pos++
}
case '(':
depth++
case ')':
depth--
}
s.pos++
}
return string(s.data[start:s.pos])
}
if s.data[s.pos] == '<' && (s.pos+1 == len(s.data) || s.data[s.pos+1] != '<') {
for s.pos < len(s.data) && s.data[s.pos] != '>' {
s.pos++
}
if s.pos < len(s.data) {
s.pos++
}
return string(s.data[start:s.pos])
}
if bytes.IndexByte([]byte("[]{}()<>"), s.data[s.pos]) >= 0 {
s.pos++
return string(s.data[start:s.pos])
}
if s.data[s.pos] == '/' {
s.pos++
}
for s.pos < len(s.data) && !type1Space(s.data[s.pos]) && bytes.IndexByte([]byte("[]{}%()/<>"), s.data[s.pos]) < 0 {
s.pos++
}
return string(s.data[start:s.pos])
}
// binary 按PostScript长度读取RD后的加密字节
// 入参: length 数据长度
// 返回: []byte 二进制数据, error 错误信息
func (s *type1Scanner) binary(length int) ([]byte, error) {
if s.pos >= len(s.data) || !type1Space(s.data[s.pos]) || length < 0 {
return nil, fmt.Errorf("invalid Type1 binary data")
}
if s.data[s.pos] == '\r' && s.pos+1 < len(s.data) && s.data[s.pos+1] == '\n' {
s.pos += 2
} else {
s.pos++
}
if length > len(s.data)-s.pos {
return nil, fmt.Errorf("truncated Type1 binary data")
}
data := s.data[s.pos : s.pos+length]
s.pos += length
return data, nil
}
// type1BinaryStart 判断Type1二进制数据起始操作符
// 入参: token PostScript词项
// 返回: bool 是否为二进制数据起始操作符
func type1BinaryStart(token string) bool {
return token == "RD" || token == "-|"
}
// outline 将Type1字形指令转换为保留三次曲线的Type2程序
// 入参: glyph 字形数据
// 返回: []byte Type2字形程序, error 错误信息
func (p *Type1Program) outline(glyph type1Glyph) ([]byte, error) {
state := type1Outline{program: p, active: map[string]bool{glyph.name: true}}
ended, err := state.run(glyph.data, 0)
if err != nil {
return nil, fmt.Errorf("Type1 glyph %s: %w", glyph.name, err)
}
if !ended || !state.defined {
return nil, fmt.Errorf("Type1 glyph %s has no complete outline", glyph.name)
}
var result bytes.Buffer
if err := encodeType2Number(&result, state.width); err != nil {
return nil, err
}
result.Write(state.path.Bytes())
result.WriteByte(14)
return result.Bytes(), nil
}
// run 执行Type1字形或子程序,遇到endchar返回完成状态
// 入参: data 指令数据, depth 子程序深度
// 返回: bool 是否遇到endchar, error 错误信息
func (s *type1Outline) run(data []byte, depth int) (bool, error) {
if depth > 32 {
return false, fmt.Errorf("Type1 subroutine recursion")
}
if s.steps == nil {
s.steps = new(int)
}
for index := 0; index < len(data); index++ {
(*s.steps)++
if *s.steps > 1<<20 {
return false, fmt.Errorf("Type1 execution limit exceeded")
}
op := int(data[index])
if op >= 32 {
var value float64
switch {
case op <= 246:
value = float64(op - 139)
case op <= 250:
if index+1 >= len(data) {
return false, fmt.Errorf("truncated Type1 operand")
}
index++
value = float64((op-247)*256 + int(data[index]) + 108)
case op <= 254:
if index+1 >= len(data) {
return false, fmt.Errorf("truncated Type1 operand")
}
index++
value = float64(-(op-251)*256 - int(data[index]) - 108)
default:
if index+4 >= len(data) {
return false, fmt.Errorf("truncated Type1 operand")
}
value = float64(int32(binary.BigEndian.Uint32(data[index+1 : index+5])))
index += 4
}
if len(s.args) >= 24 {
return false, fmt.Errorf("Type1 operand limit exceeded")
}
s.args = append(s.args, value)
continue
}
if op == 12 {
if index+1 >= len(data) {
return false, fmt.Errorf("truncated Type1 operator")
}
index++
op = 1200 + int(data[index])
}
if op == 10 {
if len(s.args) == 0 {
return false, fmt.Errorf("Type1 callsubr has no index")
}
value := s.args[len(s.args)-1]
index := int(value)
s.args = s.args[:len(s.args)-1]
if value != float64(index) || index < 0 || index >= len(s.program.subrs) || s.program.subrs[index] == nil {
return false, fmt.Errorf("invalid Type1 subroutine index %d", index)
}
ended, err := s.run(s.program.subrs[index], depth+1)
if err != nil || ended {
return ended, err
}
continue
}
if op == 11 {
if depth == 0 {
return false, fmt.Errorf("Type1 return outside subroutine")
}
return false, nil
}
if op == 14 {
return true, nil
}
if err := s.operator(op); err != nil {
return false, err
}
if op == 1206 {
return true, nil
}
}
return false, fmt.Errorf("unterminated Type1 charstring")
}
// operator 执行单个Type1绘制指令并清空已消费的操作数
// 入参: op 指令编号
// 返回: error 错误信息
func (s *type1Outline) operator(op int) error {
a := s.args
s.args = a[:0]
switch op {
case 1200:
s.args = a
return nil
case 1, 3, 1201, 1202:
return nil
case 13:
if s.defined || len(a) != 2 {
return fmt.Errorf("invalid Type1 hsbw")
}
s.x, s.width, s.defined = a[0], a[1], true
s.sideX = s.x
s.x, s.y = s.x+s.originX, s.originY
case 1207:
if s.defined || len(a) != 4 || a[3] != 0 {
return fmt.Errorf("invalid Type1 sbw")
}
s.x, s.y, s.width, s.defined = a[0], a[1], a[2], true
s.sideX, s.sideY = s.x, s.y
s.x, s.y = s.x+s.originX, s.y+s.originY
case 4, 21, 22:
if !s.defined || op == 4 && len(a) != 1 || op == 21 && len(a) != 2 || op == 22 && len(a) != 1 {
return fmt.Errorf("invalid Type1 move")
}
if s.flex {
point := [2]float64{}
if op == 4 || op == 21 {
point[1] = a[len(a)-1]
}
if op == 21 || op == 22 {
point[0] = a[0]
}
s.flexPoints = append(s.flexPoints, point)
return nil
}
if op == 4 || op == 21 {
s.y += a[len(a)-1]
}
if op == 21 || op == 22 {
s.x += a[0]
}
return s.move()
case 5, 6, 7:
if !s.defined || op == 5 && (len(a) == 0 || len(a)%2 != 0) || op != 5 && len(a) == 0 {
return fmt.Errorf("invalid Type1 line")
}
if err := s.ensureMove(); err != nil {
return err
}
for n := 0; n < len(a); {
var dx, dy float64
switch op {
case 5:
dx, dy = a[n], a[n+1]
n += 2
case 6:
dx = a[n]
n++
case 7:
dy = a[n]
n++
}
if err := s.line(dx, dy); err != nil {
return err
}
}
case 8, 30, 31:
count := 6
if op != 8 {
count = 4
}
if !s.defined || len(a) == 0 || len(a)%count != 0 {
return fmt.Errorf("invalid Type1 curve")
}
if err := s.ensureMove(); err != nil {
return err
}
for n := 0; n < len(a); n += count {
var curve [6]float64
switch op {
case 8:
copy(curve[:], a[n:n+6])
case 30:
curve = [6]float64{0, a[n], a[n+1], a[n+2], a[n+3], 0}
case 31:
curve = [6]float64{a[n], 0, a[n+1], a[n+2], 0, a[n+3]}
}
if err := s.curve(curve); err != nil {
return err
}
}
case 9:
s.open = false
case 1212:
if len(a) < 2 || a[len(a)-1] == 0 {
return fmt.Errorf("invalid Type1 division")
}
a[len(a)-2] /= a[len(a)-1]
s.args = a[:len(a)-1]
case 1216:
if len(a) < 2 {
return fmt.Errorf("invalid Type1 callothersubr")
}
count, number := int(a[len(a)-2]), int(a[len(a)-1])
if float64(count) != a[len(a)-2] || float64(number) != a[len(a)-1] || count < 0 || count > len(a)-2 {
return fmt.Errorf("invalid Type1 callothersubr arguments")
}
s.args = a[:len(a)-count-2]
a = a[len(a)-count-2 : len(a)-2]
switch number {
case 0:
if !s.flex || count != 3 || len(s.flexPoints) != 7 {
return fmt.Errorf("invalid Type1 Flex end")
}
if err := s.ensureMove(); err != nil {
return err
}
first := s.flexPoints
if err := s.curve([6]float64{first[0][0] + first[1][0], first[0][1] + first[1][1], first[2][0], first[2][1], first[3][0], first[3][1]}); err != nil {
return err
}
if err := s.curve([6]float64{first[4][0], first[4][1], first[5][0], first[5][1], first[6][0], first[6][1]}); err != nil {
return err
}
if s.x != a[1]+s.originX || s.y != a[2]+s.originY {
return fmt.Errorf("Type1 Flex endpoint differs from outline")
}
s.postscript = []float64{s.x - s.originX, s.y - s.originY}
s.flexPoints = nil
s.flex = false
case 1:
if count != 0 || s.flex {
return fmt.Errorf("invalid Type1 Flex start")
}
s.flex = true
s.flexPoints = nil
case 2:
if count != 0 || !s.flex {
return fmt.Errorf("invalid Type1 Flex point")
}
case 3:
if count != 1 {
return fmt.Errorf("invalid Type1 hint replacement")
}
s.postscript = []float64{a[0]}
default:
return fmt.Errorf("unsupported Type1 OtherSubr %d", number)
}
case 1217:
if len(s.postscript) == 0 || len(a) >= 24 {
return fmt.Errorf("invalid Type1 pop")
}
s.args = append(a, s.postscript[0])
s.postscript = s.postscript[1:]
case 1233:
if len(a) != 2 {
return fmt.Errorf("invalid Type1 setcurrentpoint")
}
s.x, s.y = a[0]+s.originX, a[1]+s.originY
case 1206:
if !s.defined || len(a) != 5 || s.path.Len() != 0 {
return fmt.Errorf("invalid Type1 seac")
}
if err := s.component(a[3], s.originX, s.originY); err != nil {
return err
}
return s.component(a[4], s.originX+s.sideX+a[1]-a[0], s.originY+s.sideY+a[2])
default:
return fmt.Errorf("unsupported Type1 operator %d", op)
}
return nil
}
// component 将标准编码引用的字形展开为当前Type2轮廓,保留复合字形字宽
// 入参: code 标准编码, x 横向原点, y 纵向原点
// 返回: error 字形缺失、循环或指令错误
func (s *type1Outline) component(code, x, y float64) error {
if len(s.active) >= 32 {
return fmt.Errorf("Type1 component recursion")
}
if code < 0 || code > 255 || code != math.Trunc(code) {
return fmt.Errorf("invalid Type1 component code")
}
name := type1StandardNames[byte(code)]
if name == "" {
return fmt.Errorf("undefined Type1 component code %g", code)
}
if s.active[name] {
return fmt.Errorf("cyclic Type1 component %s", name)
}
for _, glyph := range s.program.glyphs {
if glyph.name != name {
continue
}
child := type1Outline{program: s.program, steps: s.steps, active: s.active, originX: x, originY: y, pointX: s.pointX, pointY: s.pointY}
s.active[name] = true
ended, err := child.run(glyph.data, 0)
delete(s.active, name)
if err != nil {
return err
}
if !ended || !child.defined {
return fmt.Errorf("incomplete Type1 component %s", name)
}
s.path.Write(child.path.Bytes())
s.pointX, s.pointY = child.pointX, child.pointY
return nil
}
return fmt.Errorf("missing Type1 component %s", name)
}
// move 在Type2字形程序中建立新轮廓
// 返回: error 错误信息
func (s *type1Outline) move() error {
if err := encodeType2Number(&s.path, s.x-s.pointX); err != nil {
return err
}
if err := encodeType2Number(&s.path, s.y-s.pointY); err != nil {
return err
}
s.path.WriteByte(21)
s.pointX, s.pointY, s.open = s.x, s.y, true
return nil
}
// ensureMove 在首次描绘或关闭轮廓后建立当前位置
// 返回: error 错误信息
func (s *type1Outline) ensureMove() error {
if !s.open || s.x != s.pointX || s.y != s.pointY {
return s.move()
}
return nil
}
// line 保留Type1线段的实际终点
// 入参: dx 横向增量, dy 纵向增量
// 返回: error 错误信息
func (s *type1Outline) line(dx, dy float64) error {
if err := encodeType2Number(&s.path, dx); err != nil {
return err
}
if err := encodeType2Number(&s.path, dy); err != nil {
return err
}
s.path.WriteByte(5)
s.x += dx
s.y += dy
s.pointX, s.pointY = s.x, s.y
return nil
}
// curve 保留Type1三次贝塞尔控制点
// 入参: values 六个相对坐标
// 返回: error 错误信息
func (s *type1Outline) curve(values [6]float64) error {
for _, value := range values {
if err := encodeType2Number(&s.path, value); err != nil {
return err
}
}
s.path.WriteByte(8)
s.x += values[0] + values[2] + values[4]
s.y += values[1] + values[3] + values[5]
s.pointX, s.pointY = s.x, s.y
return nil
}
// encodeType2Number 编码Type2字形指令的有符号定点操作数
// 入参: target 目标字节流, value 操作数
// 返回: error 错误信息
func encodeType2Number(target *bytes.Buffer, value float64) error {
if !finite(value) || value < -32768 || value >= 32768 {
return fmt.Errorf("Type2 operand out of range")
}
if value == math.Trunc(value) {
integer := int(value)
switch {
case integer >= -107 && integer <= 107:
target.WriteByte(byte(integer + 139))
case integer >= 108 && integer <= 1131:
integer -= 108
target.WriteByte(byte(247 + integer/256))
target.WriteByte(byte(integer % 256))
case integer <= -108 && integer >= -1131:
integer = -integer - 108
target.WriteByte(byte(251 + integer/256))
target.WriteByte(byte(integer % 256))
default:
target.WriteByte(28)
_ = binary.Write(target, binary.BigEndian, int16(integer))
}
return nil
}
fixed := math.Round(value * 65536)
if fixed < math.MinInt32 || fixed > math.MaxInt32 {
return fmt.Errorf("Type2 fixed operand out of range")
}
target.WriteByte(255)
_ = binary.Write(target, binary.BigEndian, int32(fixed))
return nil
}
// ToCFF 将Type1字形转换为保持三次轮廓的CFF字体
// 入参: name 字体名称
// 返回: []byte CFF数据, error 错误信息
func (p *Type1Program) ToCFF(name string) ([]byte, error) {
if len(p.glyphs) > math.MaxUint16 || name == "" {
return nil, fmt.Errorf("invalid Type1 font identity")
}
ordered := make([]type1Glyph, 0, len(p.glyphs))
for _, glyph := range p.glyphs {
if glyph.name == ".notdef" {
ordered = append(ordered, glyph)
break
}
}
if len(ordered) == 0 {
return nil, fmt.Errorf("Type1 font lacks .notdef glyph")
}
for _, glyph := range p.glyphs {
if glyph.name != ".notdef" {
ordered = append(ordered, glyph)
}
}
chars := make([][]byte, len(ordered))
strings := make([][]byte, 0, len(ordered)-1)
charset := make([]byte, 1+2*(len(ordered)-1))
for index, glyph := range ordered {
charstring, err := p.outline(glyph)
if err != nil {
return nil, err
}
chars[index] = charstring
if index == 0 {
continue
}
strings = append(strings, []byte(glyph.name))
sid := 391 + index - 1
if sid > math.MaxUint16 {
return nil, fmt.Errorf("Type1 font has too many glyph names")
}
binary.BigEndian.PutUint16(charset[1+(index-1)*2:], uint16(sid))
}
nameIndex := encodeCFFIndex([][]byte{[]byte(name)})
stringIndex := encodeCFFIndex(strings)
globalSubrs := encodeCFFIndex(nil)
charIndex := encodeCFFIndex(chars)
topDict := make([]byte, 12)
topDict[0], topDict[5], topDict[6], topDict[11] = 29, 15, 29, 17
if p.matrix != nil {
topDict = append(topDict, encodeCFFDict(cffDict{1207: p.matrix})...)
}
topIndex := encodeCFFIndex([][]byte{topDict})
charsetOffset := 4 + len(nameIndex) + len(topIndex) + len(stringIndex) + len(globalSubrs)
charOffset := charsetOffset + len(charset)
binary.BigEndian.PutUint32(topDict[1:5], uint32(charsetOffset))
binary.BigEndian.PutUint32(topDict[7:11], uint32(charOffset))
topIndex = encodeCFFIndex([][]byte{topDict})
data := make([]byte, 0, charOffset+len(charIndex))
data = append(data, 1, 0, 4, 4)
data = append(data, nameIndex...)
data = append(data, topIndex...)
data = append(data, stringIndex...)
data = append(data, globalSubrs...)
data = append(data, charset...)
data = append(data, charIndex...)
return data, nil
}
// GlyphIDs 返回Type1名称到转换后字形编号的对应关系
// 返回: map[string]uint16 字形编号
func (p *Type1Program) GlyphIDs() map[string]uint16 {
ids := map[string]uint16{".notdef": 0}
next := uint16(1)
for _, glyph := range p.glyphs {
if glyph.name != ".notdef" {
ids[glyph.name] = next
next++
}
}
return ids
}
+524
View File
@@ -0,0 +1,524 @@
// 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"
"math"
"slices"
)
// type2Font 保存Type2字形程序、子程序及私有字宽
type type2Font struct {
data []byte
chars [][]byte
locals [][]byte
globals [][]byte
names map[string]int
nominal, def float64
seed uint64
cid bool
}
// type2State 按Type2规范保存跨子程序共享的执行状态
type type2State struct {
font *type2Font
widthTarget *type2Font
matrix *[6]float64
args []float64
transient [32]float64
stored uint32
width float64
widthSet bool
widthOnly bool
hints int
x, y float64
outX, outY float64
originX, originY float64
component bool
stripHints bool
pathStarted bool
changed bool
output *bytes.Buffer
seed uint64
steps *int
dependencies map[uint16]bool
localUsage []bool
globalUsage []bool
}
// readType2Index 读取并校验CFF索引的全部偏移,不复制程序字节
// 入参: data 字体数据, offset 索引偏移
// 返回: [][]byte 索引项, int 索引结束位置, error 错误信息
func readType2Index(data []byte, offset int) ([][]byte, int, error) {
count, base, end, err := scanCFFIndex(data, offset)
if err != nil || count == 0 {
return nil, end, err
}
width := int(data[offset+2])
items := make([][]byte, count)
previous := 1
for index := 0; index <= count; index++ {
value := uint64(0)
for _, b := range data[offset+3+index*width : offset+3+(index+1)*width] {
value = value<<8 | uint64(b)
}
if index != 0 {
items[index-1] = data[base+previous-1 : base+int(value)-1]
}
previous = int(value)
}
return items, end, nil
}
// readType2Font 读取单FD字体的Type2执行资源,CID字体先经字体规范化
// 入参: data CFF字体数据
// 返回: *type2Font 执行资源, error 错误信息
func readType2Font(data []byte) (*type2Font, error) {
if !isBareCFFData(data) {
return nil, fmt.Errorf("invalid CFF header")
}
names, end, err := readType2Index(data, int(data[2]))
if err != nil {
return nil, fmt.Errorf("invalid CFF name index: %w", err)
}
if len(names) != 1 {
return nil, fmt.Errorf("invalid CFF font count")
}
tops, end, err := readType2Index(data, end)
if err != nil {
return nil, fmt.Errorf("invalid CFF top index: %w", err)
}
if len(tops) != 1 {
return nil, fmt.Errorf("invalid CFF top dictionary count")
}
_, end, err = readType2Index(data, end)
if err != nil {
return nil, err
}
globals, _, err := readType2Index(data, end)
if err != nil {
return nil, err
}
dict, err := readCFFDict(tops[0])
if err != nil {
return nil, err
}
if value, ok := dict[1206]; ok && (len(value) != 1 || value[0] != 2) {
return nil, fmt.Errorf("unsupported CFF charstring type")
}
if _, cid := dict[1230]; cid {
return nil, fmt.Errorf("CID CFF requires FD normalization")
}
offset := dict[17]
if len(offset) != 1 || !finite(offset[0]) || offset[0] < 0 || offset[0] > float64(len(data)) || offset[0] != math.Trunc(offset[0]) {
return nil, fmt.Errorf("invalid CFF charstrings offset")
}
chars, _, err := readType2Index(data, int(offset[0]))
if err != nil {
return nil, fmt.Errorf("invalid CFF charstrings: %w", err)
}
if len(chars) == 0 {
return nil, fmt.Errorf("empty CFF charstrings")
}
font := &type2Font{data: data, chars: chars, globals: globals, seed: 1}
if _, err := readType2Private(data, dict[18], font); err != nil {
return nil, err
}
return font, nil
}
// readType2Private 读取FD私有字宽、随机种子和局部子程序
// 入参: data 字体数据, values Private范围, font 执行资源
// 返回: cffDict 私有字典, error 范围或字段错误
func readType2Private(data []byte, values []float64, font *type2Font) (cffDict, error) {
private := cffDict{}
var err error
if values != nil {
if len(values) != 2 || !finite(values[0]) || !finite(values[1]) || values[0] < 0 || values[1] < 0 || values[0] > float64(len(data)) || values[1] > float64(len(data))-values[0] || values[0] != math.Trunc(values[0]) || values[1] != math.Trunc(values[1]) {
return nil, fmt.Errorf("invalid CFF private range")
}
start := int(values[1])
private, err = readCFFDict(data[start : start+int(values[0])])
if err != nil {
return nil, err
}
for op, target := range map[int]*float64{20: &font.def, 21: &font.nominal} {
if value, ok := private[op]; ok {
if len(value) != 1 || !finite(value[0]) {
return nil, fmt.Errorf("invalid CFF private width")
}
*target = value[0]
}
}
if value, ok := private[1219]; ok {
if len(value) != 1 || !finite(value[0]) || value[0] < math.MinInt32 || value[0] > math.MaxInt32 || value[0] != math.Trunc(value[0]) {
return nil, fmt.Errorf("invalid CFF random seed")
}
font.seed = uint64(int64(value[0])) | 1
}
if value, ok := private[19]; ok {
if len(value) != 1 || !finite(value[0]) || value[0] < 0 || value[0] > float64(len(data)-start) || value[0] != math.Trunc(value[0]) {
return nil, fmt.Errorf("invalid CFF local subroutine offset")
}
font.locals, _, err = readType2Index(data, start+int(value[0]))
if err != nil {
return nil, err
}
}
}
return private, nil
}
// normalizeType2ProgramsMatrix 规范化字形指令并按需变换轮廓与字宽
// 入参: data CFF字体数据, matrix 设计坐标变换,nil时保留原坐标
// 返回: [][]byte 字形程序, bool 是否变更, error 错误信息
func normalizeType2ProgramsMatrix(data []byte, matrix *[6]float64) ([][]byte, bool, error) {
font, err := readType2Font(data)
if err != nil {
return nil, false, err
}
result := slices.Clone(font.chars)
changed := false
var stack [48]float64
for gid, program := range font.chars {
if len(program) == 0 {
if matrix == nil {
continue
}
program = []byte{14}
}
steps := 0
state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps}
_, err := state.run(program, 0)
if err != nil {
continue
}
if !state.changed && matrix == nil {
continue
}
var output bytes.Buffer
steps = 0
state = type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output}
if matrix != nil {
state.matrix, state.widthTarget, state.stripHints = matrix, &type2Font{}, true
}
if _, err := state.run(program, 0); err != nil {
return nil, false, fmt.Errorf("CFF glyph %d: %w", gid, err)
}
result[gid] = output.Bytes()
changed = true
}
return result, changed, nil
}
// push 按Type2规范附录B校验并追加操作数
// 入参: value 操作数
// 返回: error 栈或数值错误
func (s *type2State) push(value float64) error {
if !finite(value) || len(s.args) >= 48 {
return fmt.Errorf("invalid Type2 operand stack")
}
s.args = append(s.args, value)
return nil
}
// emit 写入已经求值的操作数及指令,分析阶段不分配输出
// 入参: op 操作符, values 操作数
// 返回: error 数值编码或程序长度错误
func (s *type2State) emit(op int, values []float64) error {
if s.output == nil {
return nil
}
for _, value := range values {
if err := encodeType2Number(s.output, value); err != nil {
return err
}
}
if op >= 1200 {
s.output.WriteByte(12)
s.output.WriteByte(byte(op - 1200))
} else {
s.output.WriteByte(byte(op))
}
if s.output.Len() > 65535 {
return fmt.Errorf("expanded Type2 charstring exceeds 65535 bytes")
}
return nil
}
// takeWidth 在首次清栈指令前提取可选字宽,组件不写入自身字宽
// 入参: explicit 是否含字宽
// 返回: error 字宽编码错误
func (s *type2State) takeWidth(explicit bool) error {
if s.widthSet {
return nil
}
s.widthSet = true
if explicit {
s.width = s.font.nominal + s.args[0]
if !finite(s.width) {
return fmt.Errorf("invalid Type2 width")
}
if s.output != nil && !s.component && s.widthTarget == nil {
if err := encodeType2Number(s.output, s.args[0]); err != nil {
return err
}
}
copy(s.args, s.args[1:])
s.args = s.args[:len(s.args)-1]
}
if s.output != nil && !s.component && s.widthTarget != nil && s.width != s.widthTarget.def {
width := s.width
if s.matrix != nil {
width *= s.matrix[0]
}
return encodeType2Number(s.output, width-s.widthTarget.nominal)
}
return nil
}
// run 执行Type2程序,操作数和暂存数组在子程序间共享
// 入参: data 字形或子程序, depth 调用深度
// 返回: bool 是否结束字形, error 指令错误
func (s *type2State) run(data []byte, depth int) (bool, error) {
if depth > 10 || len(data) > 65535 {
return false, fmt.Errorf("Type2 implementation limit exceeded")
}
for index := 0; index < len(data); {
(*s.steps)++
if *s.steps > 1<<20 {
return false, fmt.Errorf("Type2 execution limit exceeded")
}
op := int(data[index])
start := index
index++
if op == 28 || op >= 32 {
length := 1
if op == 28 {
length = 3
} else if op == 255 {
length = 5
} else if op >= 247 {
length = 2
}
if length > len(data)-start {
return false, fmt.Errorf("truncated Type2 number")
}
index = start + length
if err := s.push(parseNumberType2(data, start)); err != nil {
return false, err
}
continue
}
if op == 12 {
if index == len(data) {
return false, fmt.Errorf("truncated Type2 operator")
}
op = 1200 + int(data[index])
index++
}
switch op {
case 10, 29:
if len(s.args) == 0 {
return false, fmt.Errorf("missing Type2 subroutine number")
}
value := s.args[len(s.args)-1]
s.args = s.args[:len(s.args)-1]
subrs := s.font.locals
usage := s.localUsage
if op == 29 {
subrs = s.font.globals
usage = s.globalUsage
}
value += float64(cffSubrBias(len(subrs)))
if value < 0 || value >= float64(len(subrs)) || value != math.Trunc(value) {
return false, fmt.Errorf("invalid Type2 subroutine number")
}
if usage != nil {
usage[int(value)] = true
}
ended, err := s.run(subrs[int(value)], depth+1)
if ended || err != nil {
return ended, err
}
case 11:
if depth == 0 {
return false, fmt.Errorf("Type2 return outside subroutine")
}
return false, nil
case 1, 3, 18, 23, 19, 20:
if err := s.takeWidth(len(s.args)%2 == 1); err != nil {
return false, err
}
if s.widthOnly {
return true, nil
}
if len(s.args)%2 != 0 || s.hints+len(s.args)/2 > 96 {
return false, fmt.Errorf("invalid Type2 stem hints")
}
s.hints += len(s.args) / 2
if op == 19 || op == 20 {
if s.hints == 0 {
return false, fmt.Errorf("Type2 mask has no stem hints")
}
length := (s.hints + 7) / 8
if length > len(data)-index {
return false, fmt.Errorf("truncated Type2 hint mask")
}
if !s.component && !s.stripHints && len(s.args) > 0 {
if err := s.emit(23, s.args); err != nil {
return false, err
}
if op == 20 {
s.changed = true
}
}
if !s.component && !s.stripHints {
if err := s.emit(op, nil); err != nil {
return false, err
}
if s.output != nil {
s.output.Write(data[index : index+length])
}
}
index += length
} else if !s.component && !s.stripHints {
if err := s.emit(op, s.args); err != nil {
return false, err
}
}
s.args = s.args[:0]
case 4, 21, 22:
count := 1
if op == 21 {
count = 2
}
if err := s.takeWidth(len(s.args) == count+1); err != nil {
return false, err
}
if len(s.args) != count {
return false, fmt.Errorf("invalid Type2 moveto operands")
}
if s.widthOnly {
return true, nil
}
if op != 4 {
s.x += s.args[0]
}
if op != 22 {
s.y += s.args[count-1]
}
if !finite(s.x) || !finite(s.y) {
return false, fmt.Errorf("invalid Type2 current point")
}
if s.matrix != nil {
x, y := s.transformedPoint()
if err := s.emit(21, []float64{x - s.outX, y - s.outY}); err != nil {
return false, err
}
} else if s.component {
if err := s.emit(21, []float64{s.x + s.originX - s.outX, s.y + s.originY - s.outY}); err != nil {
return false, err
}
} else if err := s.emit(op, s.args); err != nil {
return false, err
}
s.outX, s.outY = s.transformedPoint()
s.pathStarted = true
s.args = s.args[:0]
case 14:
if err := s.takeWidth(len(s.args) == 1 || len(s.args) == 5); err != nil {
return false, err
}
if s.widthOnly {
return true, nil
}
if len(s.args) == 4 {
if s.component {
return false, fmt.Errorf("nested Type2 composite glyph")
}
if err := s.appendComponent(s.args[2], 0, 0); err != nil {
return false, err
}
if err := s.appendComponent(s.args[3], s.args[0], s.args[1]); err != nil {
return false, err
}
s.changed = true
} else if len(s.args) != 0 {
return false, fmt.Errorf("invalid Type2 endchar operands")
}
if !s.component {
if err := s.emit(14, nil); err != nil {
return false, err
}
}
return true, nil
case 1200:
s.changed = true
case 5, 6, 7, 8, 24, 25, 26, 27, 30, 31, 1234, 1235, 1236, 1237:
if !s.pathStarted {
return false, fmt.Errorf("Type2 path has no moveto")
}
if err := s.draw(op); err != nil {
return false, err
}
default:
if err := s.calculate(op); err != nil {
return false, err
}
s.changed = true
}
}
return false, fmt.Errorf("Type2 program has no terminator")
}
// appendComponent 按StandardEncoding查找组合组件,保持源字宽与独立暂存状态
// 入参: code 标准字符码, x 横向偏移, y 纵向偏移
// 返回: error 组件或指令错误
func (s *type2State) appendComponent(code, x, y float64) error {
if s.font.cid {
return fmt.Errorf("CID CFF cannot use StandardEncoding components")
}
if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" {
return fmt.Errorf("invalid Type2 component code")
}
if s.font.names == nil {
sids, _, _, index, ok := getCFFCharsetInfo(s.font.data, len(s.font.chars))
if !ok {
return fmt.Errorf("invalid Type2 composite charset")
}
s.font.names = make(map[string]int, len(sids))
for gid, sid := range sids {
s.font.names[getCFFSIDString(s.font.data, index, sid)] = gid
}
}
name := type1StandardNames[int(code)]
gid, ok := s.font.names[name]
if !ok {
return fmt.Errorf("missing Type2 component %s", name)
}
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, localUsage: s.localUsage, globalUsage: s.globalUsage}
_, err := child.run(s.font.chars[gid], 0)
if err != nil {
return fmt.Errorf("invalid Type2 component %s: %w", name, err)
}
s.outX, s.outY = child.outX, child.outY
return nil
}
+71
View File
@@ -0,0 +1,71 @@
// 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"
)
// readType2FDSelect 校验CID字体的逐字形FD归属和范围终点
// 单FD子集缺少.notdef范围时使用唯一FD,不恢复其他缺失范围
// 入参: data 字体数据, offset FDSelect偏移, count 字形数, fonts FD数量
// 返回: []int FD索引, error 格式或范围错误
func readType2FDSelect(data []byte, offset, count, fonts int) ([]int, error) {
if offset < 0 || offset >= len(data) || count <= 0 || fonts <= 0 {
return nil, fmt.Errorf("invalid CFF FDSelect range")
}
result := make([]int, count)
switch data[offset] {
case 0:
if count > len(data)-offset-1 {
return nil, fmt.Errorf("truncated CFF FDSelect")
}
for gid, fd := range data[offset+1 : offset+1+count] {
if int(fd) >= fonts {
return nil, fmt.Errorf("invalid CFF font dictionary index")
}
result[gid] = int(fd)
}
case 3:
if offset > len(data)-3 {
return nil, fmt.Errorf("truncated CFF FDSelect header")
}
ranges := int(binary.BigEndian.Uint16(data[offset+1:]))
start := offset + 3
if ranges == 0 || ranges > (len(data)-start-2)/3 {
return nil, fmt.Errorf("invalid CFF FDSelect ranges")
}
first := int(binary.BigEndian.Uint16(data[start:]))
if first != 0 && !(first == 1 && fonts == 1) || int(binary.BigEndian.Uint16(data[start+3*ranges:])) != count {
return nil, fmt.Errorf("invalid CFF FDSelect endpoints")
}
for index := 0; index < ranges; index++ {
pos := start + 3*index
first := int(binary.BigEndian.Uint16(data[pos:]))
last := int(binary.BigEndian.Uint16(data[pos+3:]))
fd := int(data[pos+2])
if last <= first || last > count || fd >= fonts {
return nil, fmt.Errorf("invalid CFF FDSelect interval")
}
for gid := first; gid < last; gid++ {
result[gid] = fd
}
}
default:
return nil, fmt.Errorf("unsupported CFF FDSelect format %d", data[offset])
}
return result, nil
}
+211
View File
@@ -0,0 +1,211 @@
// 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 (
"fmt"
"math"
)
// readCFFMatrix 读取有限且可逆的字体矩阵,缺省时使用指定矩阵
// 入参: dict 字体字典, fallback 缺省矩阵
// 返回: [6]float64 字体矩阵, error 矩阵错误
func readCFFMatrix(dict cffDict, fallback [6]float64) ([6]float64, error) {
values, ok := dict[1207]
if !ok {
return fallback, nil
}
if len(values) != 6 {
return [6]float64{}, fmt.Errorf("invalid CFF FontMatrix length")
}
for _, value := range values {
if !finite(value) {
return [6]float64{}, fmt.Errorf("invalid CFF FontMatrix value")
}
}
determinant := values[0]*values[3] - values[1]*values[2]
if !finite(determinant) || determinant == 0 {
return [6]float64{}, fmt.Errorf("singular CFF FontMatrix")
}
return [6]float64(values), nil
}
// cffUnitsPerEm 保留有效的等比设计单位,其他矩阵使用1000单位封装
// 入参: matrix 字体矩阵
// 返回: uint16 OpenType设计单位
func cffUnitsPerEm(matrix [6]float64) uint16 {
if matrix[0] > 0 && matrix[0] == matrix[3] && matrix[1] == 0 && matrix[2] == 0 {
units := 1 / matrix[0]
if units >= 16 && units <= 16384 && math.Abs(units-math.Round(units)) < 1e-8 {
return uint16(math.Round(units))
}
}
return 1000
}
// transformCFFBounds 按变换后的四角更新字体整体边界
// 入参: dict 顶层字典, matrix 设计坐标变换
// 返回: error 边界或变换溢出错误
func transformCFFBounds(dict cffDict, matrix [6]float64) error {
box := dict[5]
if box == nil {
return nil
}
if len(box) != 4 {
return fmt.Errorf("invalid CFF font bounds")
}
left, bottom, right, top := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
for _, x := range []float64{box[0], box[2]} {
for _, y := range []float64{box[1], box[3]} {
tx, ty := matrix[0]*x+matrix[2]*y+matrix[4], matrix[1]*x+matrix[3]*y+matrix[5]
if !finite(tx) || !finite(ty) {
return fmt.Errorf("invalid CFF transformed bounds")
}
left, bottom, right, top = min(left, tx), min(bottom, ty), max(right, tx), max(top, ty)
}
}
dict[5] = []float64{left, bottom, right, top}
return nil
}
// transformedPoint 将字形当前位置与组件原点变换到输出坐标
// 返回: float64 横坐标, float64 纵坐标
func (s *type2State) transformedPoint() (float64, float64) {
x, y := s.x+s.originX, s.y+s.originY
if s.matrix == nil {
return x, y
}
m := s.matrix
return m[0]*x + m[2]*y + m[4], m[1]*x + m[3]*y + m[5]
}
// emitTransformedPath 展开紧凑路径指令并保留变换后的三次贝塞尔控制点
// 入参: op 已校验的路径指令, a 操作数
// 返回: error 输出编码错误
func (s *type2State) emitTransformedPath(op int, a []float64) error {
emit := func(op int, values ...float64) error {
m := s.matrix
var transformed [12]float64
for i := 0; i < len(values); i += 2 {
x, y := values[i], values[i+1]
transformed[i], transformed[i+1] = m[0]*x+m[2]*y, m[1]*x+m[3]*y
}
return s.emit(op, transformed[:len(values)])
}
switch op {
case 5:
for i := 0; i < len(a); i += 2 {
if err := emit(5, a[i], a[i+1]); err != nil {
return err
}
}
case 6, 7:
for i, value := range a {
x, y := 0.0, 0.0
if (i%2 == 0) == (op == 6) {
x = value
} else {
y = value
}
if err := emit(5, x, y); err != nil {
return err
}
}
case 8, 24, 25:
start, end := 0, len(a)
if op == 24 {
end -= 2
} else if op == 25 {
start = len(a) - 6
}
for i := 0; i < start; i += 2 {
if err := emit(5, a[i], a[i+1]); err != nil {
return err
}
}
for i := start; i < end; i += 6 {
if err := emit(8, a[i:i+6]...); err != nil {
return err
}
}
if end < len(a) {
return emit(5, a[end], a[end+1])
}
case 26, 27:
first := 0.0
if len(a)%4 == 1 {
first, a = a[0], a[1:]
}
for i := 0; i < len(a); i += 4 {
var err error
if op == 26 {
err = emit(8, first, a[i], a[i+1], a[i+2], 0, a[i+3])
} else {
err = emit(8, a[i], first, a[i+1], a[i+2], a[i+3], 0)
}
if err != nil {
return err
}
first = 0
}
case 30, 31:
horizontal := op == 31
for i := 0; i+4 <= len(a); i += 4 {
last := 0.0
if i+5 == len(a) {
last = a[i+4]
}
var err error
if horizontal {
err = emit(8, a[i], 0, a[i+1], a[i+2], last, a[i+3])
} else {
err = emit(8, 0, a[i], a[i+1], a[i+2], a[i+3], last)
}
if err != nil {
return err
}
horizontal = !horizontal
}
case 1234:
if err := emit(8, a[0], 0, a[1], a[2], a[3], 0); err != nil {
return err
}
return emit(8, a[4], 0, a[5], -a[2], a[6], 0)
case 1235:
if err := emit(8, a[:6]...); err != nil {
return err
}
return emit(8, a[6:12]...)
case 1236:
if err := emit(8, a[0], a[1], a[2], a[3], a[4], 0); err != nil {
return err
}
return emit(8, a[5], 0, a[6], a[7], a[8], -a[1]-a[3]-a[7])
case 1237:
x, y := 0.0, 0.0
for i := 0; i < 10; i += 2 {
x, y = x+a[i], y+a[i+1]
}
lastX, lastY := -x, a[10]
if math.Abs(x) > math.Abs(y) {
lastX, lastY = a[10], -y
}
if err := emit(8, a[:6]...); err != nil {
return err
}
return emit(8, a[6], a[7], a[8], a[9], lastX, lastY)
}
return nil
}
+277
View File
@@ -0,0 +1,277 @@
// 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 (
"fmt"
"math"
"slices"
)
// calculate 求值Type2算术、条件、栈及暂存指令,不写入输出程序
// 入参: op 操作符
// 返回: error 操作数或运算错误
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:
count = 1
case 1203, 1204, 1210, 1211, 1212, 1215, 1220, 1224, 1228, 1230:
count = 2
case 1222:
count = 4
default:
return fmt.Errorf("unsupported Type2 operator %d", op)
}
if len(s.args) < count {
return fmt.Errorf("missing Type2 operands for operator %d", op)
}
values := s.args[len(s.args)-count:]
value := values[0]
switch op {
case 1203:
value = 0
if values[0] != 0 && values[1] != 0 {
value = 1
}
case 1204:
value = 0
if values[0] != 0 || values[1] != 0 {
value = 1
}
case 1205:
value = 0
if values[0] == 0 {
value = 1
}
case 1209:
value = math.Abs(value)
case 1210:
value += values[1]
case 1211:
value -= values[1]
case 1212:
value /= values[1]
case 1214:
value = -value
case 1215:
value = 0
if values[0] == values[1] {
value = 1
}
case 1218:
s.args = s.args[:len(s.args)-1]
return nil
case 1220, 1221:
index := values[count-1]
if index < 0 || index >= float64(len(s.transient)) || index != math.Trunc(index) {
return fmt.Errorf("invalid Type2 transient index")
}
if op == 1220 {
s.transient[int(index)] = value
s.stored |= 1 << uint(index)
s.args = s.args[:len(s.args)-2]
return nil
}
if s.stored&(1<<uint(index)) == 0 {
return fmt.Errorf("uninitialized Type2 transient value")
}
value = s.transient[int(index)]
case 1222:
if values[2] > values[3] {
value = values[1]
}
case 1224:
value *= values[1]
case 1226:
value = math.Sqrt(value)
case 1227:
return s.push(value)
case 1228:
values[0], values[1] = values[1], values[0]
return nil
case 1229:
if value != math.Trunc(value) || value >= float64(len(s.args)-1) {
return fmt.Errorf("invalid Type2 stack index")
}
index := max(0, value)
if len(s.args) < 2 {
return fmt.Errorf("empty Type2 indexed stack")
}
value = s.args[len(s.args)-2-int(index)]
case 1230:
n, j := values[0], values[1]
if n < 0 || n > float64(len(s.args)-2) || n != math.Trunc(n) || j != math.Trunc(j) {
return fmt.Errorf("invalid Type2 stack roll")
}
s.args = s.args[:len(s.args)-2]
if n == 0 {
return nil
}
shift := int(math.Mod(j, n))
if shift < 0 {
shift += int(n)
}
tail := s.args[len(s.args)-int(n):]
slices.Reverse(tail)
slices.Reverse(tail[:shift])
slices.Reverse(tail[shift:])
return nil
}
s.args = s.args[:len(s.args)-count]
return s.push(value)
}
// draw 校验Type2路径指令并推进当前点,保留曲线及Flex编码
// 入参: op 操作符
// 返回: error 路径操作数或输出错误
func (s *type2State) draw(op int) error {
a := s.args
valid := true
dx, dy := 0.0, 0.0
switch op {
case 5:
valid = len(a) >= 2 && len(a)%2 == 0
if valid {
for i := 0; i < len(a); i += 2 {
dx, dy = dx+a[i], dy+a[i+1]
}
}
case 6, 7:
valid = len(a) > 0
for i, value := range a {
if (i%2 == 0) == (op == 6) {
dx += value
} else {
dy += value
}
}
case 8, 24, 25:
start, end := 0, len(a)
if op == 24 {
end -= 2
valid = len(a) >= 8 && end%6 == 0
} else if op == 25 {
start = len(a) - 6
valid = len(a) >= 8 && start%2 == 0
} else {
valid = len(a) >= 6 && len(a)%6 == 0
}
if valid {
for i := start; i < end; i += 2 {
dx, dy = dx+a[i], dy+a[i+1]
}
for i := 0; i < start; i += 2 {
dx, dy = dx+a[i], dy+a[i+1]
}
if end < len(a) {
dx, dy = dx+a[end], dy+a[end+1]
}
}
case 26, 27:
valid = len(a) >= 4 && (len(a)%4 == 0 || len(a)%4 == 1)
if valid {
i := 0
if len(a)%4 == 1 {
if op == 26 {
dx = a[0]
} else {
dy = a[0]
}
i++
}
for ; i < len(a); i += 4 {
if op == 26 {
dx, dy = dx+a[i+1], dy+a[i]+a[i+2]+a[i+3]
} else {
dx, dy = dx+a[i]+a[i+1]+a[i+3], dy+a[i+2]
}
}
}
case 30, 31:
valid = len(a) >= 4 && (len(a)%4 == 0 || len(a)%4 == 1)
if valid {
horizontal := op == 31
for i := 0; i+4 <= len(a); i += 4 {
if horizontal {
dx, dy = dx+a[i]+a[i+1], dy+a[i+2]+a[i+3]
} else {
dx, dy = dx+a[i+1]+a[i+3], dy+a[i]+a[i+2]
}
if i+5 == len(a) {
if horizontal {
dx += a[i+4]
} else {
dy += a[i+4]
}
}
horizontal = !horizontal
}
}
case 1234:
valid = len(a) == 7
if valid {
dx = a[0] + a[1] + a[3] + a[4] + a[5] + a[6]
}
case 1235:
valid = len(a) == 13
if valid {
for i := 0; i < 12; i += 2 {
dx, dy = dx+a[i], dy+a[i+1]
}
}
case 1236:
valid = len(a) == 9
if valid {
dx = a[0] + a[2] + a[4] + a[5] + a[6] + a[8]
}
case 1237:
valid = len(a) == 11
if valid {
for i := 0; i < 10; i += 2 {
dx, dy = dx+a[i], dy+a[i+1]
}
if math.Abs(dx) > math.Abs(dy) {
dx, dy = dx+a[10], 0
} else {
dx, dy = 0, dy+a[10]
}
}
default:
valid = false
}
if !valid || !finite(s.x+dx) || !finite(s.y+dy) {
return fmt.Errorf("invalid Type2 path operands for operator %d", op)
}
var err error
if s.matrix != nil {
err = s.emitTransformedPath(op, a)
} else {
err = s.emit(op, a)
}
if err != nil {
return err
}
s.x, s.y = s.x+dx, s.y+dy
s.outX, s.outY = s.transformedPoint()
s.args = s.args[:0]
return nil
}
+28
View File
@@ -0,0 +1,28 @@
// 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
// Usage 指定子集需要保留的字符、字形及引用是否存在不确定性
type Usage struct {
Characters map[rune]bool
Glyphs map[uint16]bool
Unsafe bool
}
// newUsage 创建具有可写字符和字形集合的子集输入
// 返回: *Usage 用字信息
func newUsage() *Usage {
return &Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool)}
}
+355
View File
@@ -0,0 +1,355 @@
// 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"
"encoding/binary"
"fmt"
"math"
"sort"
)
// otfTableRecord OTF表记录结构
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
type otfTableRecord struct {
tag string
checksum uint32
offset int
length int
data []byte
}
// finite 判断数值是否有限
// 入参: value 数值
// 返回: bool 是否有限
func finite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
// ExtractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和
// 入参: data 集合数据, index 字体索引
// 返回: []byte 单字体数据, error 错误信息
func ExtractCollectionFont(data []byte, index int) ([]byte, error) {
tables, err := FileTables(data, index)
if err != nil {
return nil, err
}
if len(tables["head"]) < 12 {
return nil, fmt.Errorf("invalid head font table")
}
tables["head"] = bytes.Clone(tables["head"])
clear(tables["head"][8:12])
return SerializeOTF(tables)
}
// FileTables 读取单字体或集合指定项的表目录,不复制表数据
// 入参: data 字体数据, index 字体索引
// 返回: map[string][]byte 字体表, error 错误信息
func FileTables(data []byte, index int) (map[string][]byte, error) {
count, err := FileCount(data)
if err != nil {
return nil, err
}
if index < 0 || index >= count {
return nil, fmt.Errorf("invalid font index %d", index)
}
var offset uint64
if string(data[:4]) == "ttcf" {
offset = uint64(binary.BigEndian.Uint32(data[12+index*4:]))
}
if offset > uint64(len(data)-12) {
return nil, fmt.Errorf("invalid font offset")
}
header := data[offset : offset+12]
if string(header[:4]) != "\x00\x01\x00\x00" && string(header[:4]) != "OTTO" && string(header[:4]) != "true" {
return nil, fmt.Errorf("invalid OpenType header")
}
tableCount := uint64(binary.BigEndian.Uint16(header[4:]))
offset += 12
if tableCount == 0 || tableCount > (uint64(len(data))-offset)/16 {
return nil, fmt.Errorf("invalid font table directory")
}
tables := make(map[string][]byte, tableCount)
for i := uint64(0); i < tableCount; i++ {
entry := data[offset+i*16 : offset+(i+1)*16]
tag := string(entry[:4])
start := uint64(binary.BigEndian.Uint32(entry[8:]))
length := uint64(binary.BigEndian.Uint32(entry[12:]))
if start > uint64(len(data)) || length > uint64(len(data))-start {
return nil, fmt.Errorf("invalid %s font table range", tag)
}
if _, exists := tables[tag]; exists {
return nil, fmt.Errorf("duplicate %s font table", tag)
}
tables[tag] = data[start : start+length]
}
return tables, nil
}
// calcTableChecksum 计算字体表校验和
// 入参: data 表数据
// 返回: uint32 校验和
func calcTableChecksum(data []byte) uint32 {
var sum uint32
length := len(data)
for i := 0; i < length; i += 4 {
if i+4 <= length {
sum += binary.BigEndian.Uint32(data[i : i+4])
} else {
var val uint32
rem := data[i:]
for j, b := range rem {
val |= uint32(b) << (24 - 8*j)
}
sum += val
}
}
return sum
}
// buildHeadTable 构建head表
// 入参: unitsPerEm 每em单位数
// 返回: []byte head表数据
func buildHeadTable(unitsPerEm uint16) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint16(1))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, uint32(0x5F0F3CF5))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, unitsPerEm)
binary.Write(buf, binary.BigEndian, int64(0))
binary.Write(buf, binary.BigEndian, int64(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(-500))
binary.Write(buf, binary.BigEndian, int16(1000))
binary.Write(buf, binary.BigEndian, int16(1000))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, int16(2))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
return buf.Bytes()
}
// buildHheaTable 构建hhea表
// 入参: numGlyphs 字形数量
// 返回: []byte hhea表数据
func buildHheaTable(numGlyphs uint16) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint16(1))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, int16(800))
binary.Write(buf, binary.BigEndian, int16(-200))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, uint16(1000))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(1000))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
return buf.Bytes()
}
// buildCFFMaxpTable 构建CFF轮廓使用的maxp 0.5表
// 入参: numGlyphs 字形数量
// 返回: []byte maxp表数据
func buildCFFMaxpTable(numGlyphs uint16) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint32(0x00005000))
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
return buf.Bytes()
}
// buildTrueTypeMaxpTable 构建TrueType轮廓使用的maxp 1.0表
// 入参: numGlyphs 字形数量
// 返回: []byte maxp表数据
func buildTrueTypeMaxpTable(numGlyphs uint16) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint32(0x00010000))
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
for i := 0; i < 13; i++ {
binary.Write(buf, binary.BigEndian, uint16(0))
}
return buf.Bytes()
}
// buildOS2Table 构建OS/2表 (使用默认Metrics)
// 返回: []byte OS/2表数据
func buildOS2Table() []byte {
return BuildOS2TableWithMetrics(800, -200)
}
// BuildOS2TableWithMetrics 构建OS/2表
// 入参: ascender 上升部, descender 下降部
// 返回: []byte OS/2表数据
func BuildOS2TableWithMetrics(ascender, descender int16) []byte {
os2 := new(bytes.Buffer)
binary.Write(os2, binary.BigEndian, uint16(3))
binary.Write(os2, binary.BigEndian, int16(500))
binary.Write(os2, binary.BigEndian, uint16(400))
binary.Write(os2, binary.BigEndian, uint16(5))
binary.Write(os2, binary.BigEndian, uint16(0))
binary.Write(os2, binary.BigEndian, int16(250))
binary.Write(os2, binary.BigEndian, int16(250))
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, int16(250))
binary.Write(os2, binary.BigEndian, int16(250))
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, int16(50))
binary.Write(os2, binary.BigEndian, int16(250))
binary.Write(os2, binary.BigEndian, int16(0))
os2.Write(make([]byte, 10))
binary.Write(os2, binary.BigEndian, uint32(0))
binary.Write(os2, binary.BigEndian, uint32(0))
binary.Write(os2, binary.BigEndian, uint32(0))
binary.Write(os2, binary.BigEndian, uint32(0))
os2.WriteString("PfEd")
binary.Write(os2, binary.BigEndian, uint16(0x0040))
binary.Write(os2, binary.BigEndian, uint16(0))
binary.Write(os2, binary.BigEndian, uint16(255))
binary.Write(os2, binary.BigEndian, ascender)
binary.Write(os2, binary.BigEndian, descender)
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, uint16(ascender))
if descender < 0 {
binary.Write(os2, binary.BigEndian, uint16(-descender))
} else {
binary.Write(os2, binary.BigEndian, uint16(descender))
}
binary.Write(os2, binary.BigEndian, uint32(0))
binary.Write(os2, binary.BigEndian, uint32(0))
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, int16(0))
binary.Write(os2, binary.BigEndian, uint16(0))
binary.Write(os2, binary.BigEndian, uint16(0))
binary.Write(os2, binary.BigEndian, uint16(0))
return os2.Bytes()
}
// buildNameTable 构建name表 (最小化)
// 返回: []byte name表数据
func buildNameTable() []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(0))
binary.Write(buf, binary.BigEndian, uint16(6))
return buf.Bytes()
}
// BuildPostTable 构建post表 (版本3.0, 无字形名称)
// 返回: []byte post表数据
func BuildPostTable() []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint32(0x00030000))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, int16(0))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, uint32(0))
binary.Write(buf, binary.BigEndian, uint32(0))
return buf.Bytes()
}
// buildHmtxTable 构建hmtx表
// 入参: widths 宽度列表
// 返回: []byte hmtx表数据
func buildHmtxTable(widths []uint16) []byte {
buf := new(bytes.Buffer)
for _, w := range widths {
binary.Write(buf, binary.BigEndian, uint16(w))
binary.Write(buf, binary.BigEndian, int16(0))
}
return buf.Bytes()
}
// SerializeOTF 序列化OpenType字体结构
// 入参: tables 表数据映射
// 返回: []byte 完整字体数据, error 错误信息
func SerializeOTF(tables map[string][]byte) ([]byte, error) {
numTables := uint16(len(tables))
entrySelector := 0
for 1<<(entrySelector+1) <= int(numTables) {
entrySelector++
}
searchRange := 1 << (entrySelector + 4)
rangeShift := int(numTables)*16 - searchRange
buf := new(bytes.Buffer)
if tables["CFF "] != nil || tables["CFF2"] != nil {
buf.WriteString("OTTO")
} else {
binary.Write(buf, binary.BigEndian, uint32(0x00010000))
}
binary.Write(buf, binary.BigEndian, numTables)
binary.Write(buf, binary.BigEndian, uint16(searchRange))
binary.Write(buf, binary.BigEndian, uint16(entrySelector))
binary.Write(buf, binary.BigEndian, uint16(rangeShift))
var tags []string
for t := range tables {
tags = append(tags, t)
}
sort.Strings(tags)
headerSize := 12 + 16*int(numTables)
offset := headerSize
var records []otfTableRecord
for _, tag := range tags {
data := tables[tag]
pad := (4 - (len(data) % 4)) % 4
padded := make([]byte, len(data)+pad)
copy(padded, data)
if tag == "head" && len(data) >= 12 {
clear(padded[8:12])
}
cs := calcTableChecksum(padded)
records = append(records, otfTableRecord{tag, cs, offset, len(data), padded})
offset += len(padded)
}
for _, r := range records {
buf.WriteString(r.tag)
binary.Write(buf, binary.BigEndian, r.checksum)
binary.Write(buf, binary.BigEndian, uint32(r.offset))
binary.Write(buf, binary.BigEndian, uint32(r.length))
}
for _, r := range records {
buf.Write(r.data)
}
fullData := buf.Bytes()
for _, r := range records {
if r.tag == "head" {
adjOffset := r.offset + 8
if adjOffset+4 <= len(fullData) {
binary.BigEndian.PutUint32(fullData[adjOffset:], 0)
cs := calcTableChecksum(fullData)
checksumAdj := 0xB1B0AFBA - cs
binary.BigEndian.PutUint32(fullData[adjOffset:], checksumAdj)
}
break
}
}
return fullData, nil
}
+146
View File
@@ -0,0 +1,146 @@
// 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"
)
// variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构
// 入参: sfnt 已解析字体
// 返回: bool 是否具备可裁剪的变化数据
func variableSFNTSubsettable(sfnt *SFNT) bool {
fvar, gvar := sfnt.Tables["fvar"], sfnt.Tables["gvar"]
if len(fvar) < 16 || len(gvar) < 20 || binary.BigEndian.Uint32(fvar) != 0x10000 || binary.BigEndian.Uint32(gvar) != 0x10000 {
return false
}
axes := int(binary.BigEndian.Uint16(fvar[8:]))
offset, size := int(binary.BigEndian.Uint16(fvar[4:])), int(binary.BigEndian.Uint16(fvar[10:]))
return axes > 0 && size >= 20 && offset >= 16 && offset <= len(fvar) && axes <= (len(fvar)-offset)/size &&
int(binary.BigEndian.Uint16(gvar[4:])) == axes && binary.BigEndian.Uint16(gvar[12:]) == sfnt.NumGlyphs()
}
// subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据
// 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 字形或变化表错误
func subsetVariableSFNTFont(sfnt *SFNT, glyphs []uint16) ([]byte, error) {
if !variableSFNTSubsettable(sfnt) {
return nil, fmt.Errorf("invalid TrueType variation tables")
}
usage := newUsage()
usage.Glyphs[0] = true
for _, gid := range glyphs {
if gid >= sfnt.NumGlyphs() {
return nil, fmt.Errorf("variable font glyph index out of range")
}
usage.Glyphs[gid] = true
for _, char := range sfnt.glyphCharacters(gid) {
usage.Characters[char] = true
}
}
dependencies, err := sfnt.glyphDependencies(slices.Collect(maps.Keys(usage.Glyphs))...)
if err != nil {
return nil, err
}
for _, dependency := range dependencies {
usage.Glyphs[dependency] = true
}
mapping := make(map[rune]uint16, len(usage.Characters))
for char := range usage.Characters {
mapping[char] = sfnt.GlyphIndex(char)
}
table := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp", "vhea", "vmtx"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = data
}
}
table["cmap"] = BuildCmapTable(sfnt.NumGlyphs(), mapping)
static, err := SerializeOTF(table)
if err != nil {
return nil, err
}
if subset := SubsetSourceSFNT(static, usage); subset != nil {
table, err = FileTables(subset, 0)
if err != nil {
return nil, err
}
}
table["gvar"], err = subsetGlyphVariations(sfnt.Tables["gvar"], usage.Glyphs)
if err != nil {
return nil, err
}
for _, tag := range []string{"fvar", "avar", "cvar", "HVAR", "VVAR", "MVAR", "STAT"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = data
}
}
return SerializeOTF(table)
}
// subsetGlyphVariations 按原字形编号重建gvar偏移表,保留共享元组及使用字形的完整变化数据
// 入参: data gvar表, glyphs 保留字形集合
// 返回: []byte 裁剪后的gvar表, error 表结构错误
func subsetGlyphVariations(data []byte, glyphs map[uint16]bool) ([]byte, error) {
if len(data) < 20 || binary.BigEndian.Uint32(data) != 0x10000 {
return nil, fmt.Errorf("invalid gvar header")
}
axes := uint64(binary.BigEndian.Uint16(data[4:]))
tuples := uint64(binary.BigEndian.Uint16(data[6:]))
shared := uint64(binary.BigEndian.Uint32(data[8:]))
count := int(binary.BigEndian.Uint16(data[12:]))
flags := binary.BigEndian.Uint16(data[14:])
start := uint64(binary.BigEndian.Uint32(data[16:]))
width := 2
if flags&1 != 0 {
width = 4
}
sharedSize := axes * tuples * 2
headerEnd := uint64(20 + (count+1)*width)
if flags > 1 || headerEnd > uint64(len(data)) || start < headerEnd || start > uint64(len(data)) || shared > uint64(len(data)) || sharedSize > uint64(len(data))-shared || sharedSize != 0 && shared < headerEnd {
return nil, fmt.Errorf("invalid gvar offsets")
}
readOffset := func(index int) uint64 {
if width == 2 {
return uint64(binary.BigEndian.Uint16(data[20+index*2:])) * 2
}
return uint64(binary.BigEndian.Uint32(data[20+index*4:]))
}
output := make([]byte, 20+(count+1)*4)
copy(output, data[:20])
binary.BigEndian.PutUint16(output[14:], 1)
binary.BigEndian.PutUint32(output[8:], uint32(len(output)))
output = append(output, data[shared:shared+sharedSize]...)
base := len(output)
binary.BigEndian.PutUint32(output[16:], uint32(base))
for gid := range count {
first, last := readOffset(gid), readOffset(gid+1)
if last < first || last > uint64(len(data))-start {
return nil, fmt.Errorf("invalid gvar glyph offsets")
}
if last > first && sharedSize != 0 && start+first < shared+sharedSize && shared < start+last {
return nil, fmt.Errorf("overlapping gvar glyph and shared tuple data")
}
binary.BigEndian.PutUint32(output[20+gid*4:], uint32(len(output)-base))
if glyphs[uint16(gid)] {
output = append(output, data[start+first:start+last]...)
}
}
binary.BigEndian.PutUint32(output[20+count*4:], uint32(len(output)-base))
return output, nil
}
+248
View File
@@ -0,0 +1,248 @@
// 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"
"compress/zlib"
"encoding/binary"
"fmt"
"io"
"sort"
"github.com/andybalholm/brotli"
)
// fontContainerLimit 限制压缩字体展开后的表数据总量
const fontContainerLimit = 256 << 20
// woff2Tags 保存WOFF2表目录的标准标签顺序
var woff2Tags = [...]string{
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill",
}
// DecodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理
// 入参: data WOFF2文件
// 返回: []byte OpenType数据, bool 是否已处理, error 封装错误
func DecodeWOFF2(data []byte) ([]byte, bool, error) {
if len(data) < 48 || string(data[:4]) != "wOF2" || uint64(binary.BigEndian.Uint32(data[8:])) != uint64(len(data)) {
return nil, true, fmt.Errorf("invalid WOFF2 header")
}
if string(data[4:8]) == "ttcf" {
return nil, false, nil
}
count := int(binary.BigEndian.Uint16(data[12:]))
if count == 0 || count > (len(data)-48)/2 {
return nil, true, fmt.Errorf("invalid WOFF2 table count")
}
tags := make([]string, 0, count)
sizes := make([]uint32, 0, count)
table := make(map[string][]byte, count)
pos, total, transformed := 48, uint64(0), false
for range count {
if pos >= len(data) {
return nil, true, fmt.Errorf("truncated WOFF2 directory")
}
flags := data[pos]
pos++
var tag string
if index := flags & 63; index == 63 {
if len(data)-pos < 4 {
return nil, true, fmt.Errorf("truncated WOFF2 tag")
}
tag = string(data[pos : pos+4])
pos += 4
} else {
tag = woff2Tags[index]
}
if _, exists := table[tag]; exists {
return nil, true, fmt.Errorf("duplicate WOFF2 table %q", tag)
}
table[tag] = nil
size, err := readWOFF2Base128(data, &pos)
if err != nil {
return nil, true, err
}
version := flags >> 6
changed := version != 0
if tag == "glyf" || tag == "loca" {
if version != 0 && version != 3 {
return nil, true, fmt.Errorf("invalid WOFF2 glyph transform")
}
changed = version == 0
} else if version != 0 && (tag != "hmtx" || version != 1) {
return nil, true, fmt.Errorf("invalid WOFF2 table transform %q", tag)
}
if changed {
transformed = true
size, err = readWOFF2Base128(data, &pos)
if err != nil {
return nil, true, err
}
if tag == "loca" && size != 0 {
return nil, true, fmt.Errorf("invalid WOFF2 loca length")
}
}
total += uint64(size)
if total+uint64(12+19*count) > fontContainerLimit {
return nil, true, fmt.Errorf("WOFF2 font exceeds size limit")
}
tags = append(tags, tag)
sizes = append(sizes, size)
}
end := uint64(pos) + uint64(binary.BigEndian.Uint32(data[20:]))
if end > uint64(len(data)) {
return nil, true, fmt.Errorf("invalid WOFF2 compressed length")
}
if err := validateWOFFBlocks(data, end, 28); err != nil {
return nil, true, err
}
if transformed {
return nil, false, nil
}
reader := brotli.NewReader(bytes.NewReader(data[pos:end]))
body, err := io.ReadAll(io.LimitReader(reader, int64(total)+1))
if err != nil {
return nil, true, fmt.Errorf("decode WOFF2 data: %w", err)
}
if uint64(len(body)) != total {
return nil, true, fmt.Errorf("invalid WOFF2 expanded length")
}
for i, tag := range tags {
size := int(sizes[i])
table[tag], body = body[:size], body[size:]
}
decoded, err := SerializeOTF(table)
return decoded, true, err
}
// readWOFF2Base128 读取WOFF2无符号变长整数,拒绝溢出和前导零
// 入参: data 编码数据, pos 当前读取位置
// 返回: uint32 整数值, error 编码错误
func readWOFF2Base128(data []byte, pos *int) (uint32, error) {
var value uint32
for i := range 5 {
if *pos >= len(data) {
return 0, fmt.Errorf("truncated WOFF2 integer")
}
b := data[*pos]
*pos++
if i == 0 && b == 0x80 || value > 0x1ffffff {
return 0, fmt.Errorf("invalid WOFF2 integer")
}
value = value<<7 | uint32(b&127)
if b&128 == 0 {
return value, nil
}
}
return 0, fmt.Errorf("invalid WOFF2 integer")
}
// DecodeWOFF 展开WOFF表数据,不解析或更改字体轮廓
// 入参: data WOFF文件
// 返回: []byte OpenType数据, error 封装错误
func DecodeWOFF(data []byte) ([]byte, error) {
if len(data) < 44 || uint64(binary.BigEndian.Uint32(data[8:])) != uint64(len(data)) || binary.BigEndian.Uint16(data[14:]) != 0 {
return nil, fmt.Errorf("invalid WOFF header")
}
count := int(binary.BigEndian.Uint16(data[12:]))
if count == 0 || count > (len(data)-44)/20 {
return nil, fmt.Errorf("invalid WOFF table count")
}
table := make(map[string][]byte, count)
ranges := make([][2]uint64, 0, count)
total := uint64(12 + 16*count)
for i := range count {
record := data[44+i*20:]
tag := string(record[:4])
offset, packed, size := uint64(binary.BigEndian.Uint32(record[4:])), uint64(binary.BigEndian.Uint32(record[8:])), uint64(binary.BigEndian.Uint32(record[12:]))
total += (size + 3) &^ 3
if _, exists := table[tag]; exists || offset < uint64(44+20*count) || offset%4 != 0 || packed > size || offset+packed > uint64(len(data)) || total > fontContainerLimit {
return nil, fmt.Errorf("invalid WOFF table %q", tag)
}
body := data[offset : offset+packed]
if packed < size {
reader, err := zlib.NewReader(bytes.NewReader(body))
if err != nil {
return nil, err
}
body, err = io.ReadAll(io.LimitReader(reader, int64(size)+1))
reader.Close()
if err != nil {
return nil, err
}
if uint64(len(body)) != size {
return nil, fmt.Errorf("invalid WOFF expanded length")
}
}
table[tag] = body
ranges = append(ranges, [2]uint64{offset, offset + packed})
}
if total != uint64(binary.BigEndian.Uint32(data[16:])) {
return nil, fmt.Errorf("invalid WOFF SFNT size")
}
sort.Slice(ranges, func(i, j int) bool { return ranges[i][0] < ranges[j][0] })
end := uint64(44 + 20*count)
for _, interval := range ranges {
if interval[0] != (end+3)&^3 {
return nil, fmt.Errorf("invalid WOFF table ranges")
}
end = interval[1]
}
if err := validateWOFFBlocks(data, end, 24); err != nil {
return nil, err
}
return SerializeOTF(table)
}
// validateWOFFBlocks 校验附加数据块的边界及对齐,字体元数据不写入输出
// 入参: data 字体文件, end 字体数据结束位置, fields 元数据偏移字段位置
// 返回: error 附加块错误
func validateWOFFBlocks(data []byte, end uint64, fields int) error {
for _, pos := range []int{fields, fields + 12} {
offset, length := uint64(binary.BigEndian.Uint32(data[pos:])), uint64(binary.BigEndian.Uint32(data[pos+4:]))
if offset == 0 {
if length != 0 || pos == fields && binary.BigEndian.Uint32(data[pos+8:]) != 0 {
return fmt.Errorf("invalid WOFF optional block")
}
continue
}
if length == 0 || offset != (end+3)&^3 || offset+length > uint64(len(data)) {
return fmt.Errorf("invalid WOFF block range")
}
for _, b := range data[end:offset] {
if b != 0 {
return fmt.Errorf("invalid WOFF padding")
}
}
end = offset + length
}
if end > uint64(len(data)) || uint64(len(data)) > (end+3)&^3 {
return fmt.Errorf("invalid WOFF trailing data")
}
for _, b := range data[end:] {
if b != 0 {
return fmt.Errorf("invalid WOFF padding")
}
}
return nil
}
+125
View File
@@ -0,0 +1,125 @@
// 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 image
import (
"bufio"
"encoding/binary"
"fmt"
"image"
"io"
"math/bits"
)
// bmpColorMask 保存一个BMP颜色分量的位域位置和取值范围
type bmpColorMask struct {
mask uint32
shift uint
Max uint32
}
// DecodeConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素
// 入参: input 图片输入流
// 返回: image.Config 图片配置, string 图片格式, error 头部错误
func DecodeConfig(input io.Reader) (image.Config, string, error) {
buffer := bufio.NewReader(input)
prefix, _ := buffer.Peek(34)
if IsTIFF(prefix) {
layout, err := readTIFFLayout(buffer)
if err != nil {
return image.Config{}, "tiff", err
}
size := layout.DisplaySize()
return image.Config{Width: size.X, Height: size.Y}, "tiff", nil
}
if len(prefix) == 34 && string(prefix[:2]) == "BM" {
header := binary.LittleEndian.Uint32(prefix[14:18])
bpp := binary.LittleEndian.Uint16(prefix[28:30])
compression := binary.LittleEndian.Uint32(prefix[30:34])
if (bpp == 16 || bpp == 32 && compression == 3) && (header == 40 || header == 52 || header == 56 || header == 108 || header == 124) {
size := int(header) + 14
if header == 40 && compression == 3 {
size += 12
}
data := make([]byte, size)
if _, err := io.ReadFull(buffer, data); err != nil {
return image.Config{}, "bmp", err
}
config, err := decodeBMPConfig(data)
return config, "bmp", err
}
}
return image.DecodeConfig(buffer)
}
// sample 将位域采样值映射到八位通道,缺省透明度为不透明
// 入参: value 编码像素
// 返回: uint8 通道值
func (m bmpColorMask) sample(value uint32) uint8 {
if m.mask == 0 {
return 255
}
return uint8(uint64((value&m.mask)>>m.shift) * 255 / uint64(m.Max))
}
// BMPColorMasks 读取BMP真彩色通道布局,校验位域连续性、重叠及像素范围
// 入参: data 已校验头部的BMP数据, offset 像素偏移, bpp 像素位数, compression 编码方式
// 返回: [4]bmpColorMask 红绿蓝和透明度通道, error 格式错误
func BMPColorMasks(data []byte, offset int, bpp uint16, compression uint32) ([4]bmpColorMask, error) {
var result [4]bmpColorMask
if bpp != 16 && bpp != 24 && bpp != 32 {
return result, fmt.Errorf("unsupported bmp depth")
}
masks := [4]uint32{0xff0000, 0xff00, 0xff}
if bpp == 16 {
masks = [4]uint32{0x7c00, 0x3e0, 0x1f}
}
switch compression {
case 0:
case 3:
if bpp == 24 {
return result, fmt.Errorf("invalid bmp bitfield depth")
}
header := binary.LittleEndian.Uint32(data[14:18])
if offset < 66 || len(data) < 66 || header != 40 && header < 52 {
return result, fmt.Errorf("truncated bmp color masks")
}
for i := range 3 {
masks[i] = binary.LittleEndian.Uint32(data[54+i*4:])
}
if header >= 56 {
masks[3] = binary.LittleEndian.Uint32(data[66:70])
}
default:
return result, fmt.Errorf("unsupported bmp compression")
}
var used uint32
for i, mask := range masks {
if mask == 0 {
if i < 3 {
return result, fmt.Errorf("empty bmp color mask")
}
continue
}
shift := uint(bits.TrailingZeros32(mask))
maximum := mask >> shift
if mask&used != 0 || maximum&(maximum+1) != 0 || uint64(mask)>>bpp != 0 {
return result, fmt.Errorf("invalid bmp color mask")
}
result[i] = bmpColorMask{mask: mask, shift: shift, Max: maximum}
used |= mask
}
return result, nil
}
+172
View File
@@ -0,0 +1,172 @@
// 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 image
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
"sync/atomic"
)
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
type EncodedImage struct {
data []byte
format string
config image.Config
result atomic.Pointer[encodedImageResult]
mu sync.Mutex
pending chan struct{}
}
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
type encodedImageResult struct {
image image.Image
err error
}
// NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func NewEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImage(bytes.Clone(data))
}
// Bytes 返回只读原始编码,调用方不得修改
// 返回: []byte 原始编码
func (img *EncodedImage) Bytes() []byte { return img.data }
// MIME 返回图片媒体类型
// 返回: string 媒体类型
func (img *EncodedImage) MIME() string { return "image/" + img.format }
// ColorModel 返回头信息中的颜色模型,不解码像素
// 返回: color.Model 颜色模型
func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel }
// Bounds 返回图片范围,不解码像素
// 返回: image.Rectangle 图片范围
func (img *EncodedImage) Bounds() image.Rectangle {
return image.Rect(0, 0, img.config.Width, img.config.Height)
}
// Image 首次解码图片并缓存结果或错误,支持并发读取
// 返回: image.Image 只读像素, error 解码错误
func (img *EncodedImage) Image() (image.Image, error) {
if result := img.result.Load(); result != nil {
return result.image, result.err
}
return img.ImageContext(context.Background())
}
// ImageContext 解码并共享只读像素,取消或超时后可重试,nil使用后台上下文
// 并发调用等待正在进行的解码,等待方取消不影响其他调用
// 入参: ctx 取消上下文
// 返回: image.Image 只读像素, error 解码或取消错误
func (img *EncodedImage) ImageContext(ctx context.Context) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
for {
if err := ctx.Err(); err != nil {
return nil, err
}
if result := img.result.Load(); result != nil {
return result.image, result.err
}
img.mu.Lock()
if img.result.Load() != nil {
img.mu.Unlock()
continue
}
pending := img.pending
if pending == nil {
pending = make(chan struct{})
img.pending = pending
img.mu.Unlock()
return img.decodeContext(ctx, pending)
}
img.mu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-pending:
}
}
}
// decodeContext 执行本次解码,退出时唤醒等待方,只缓存完整结果及稳定错误
// 入参: ctx 取消上下文, pending 本次解码的完成通知
// 返回: image.Image 只读像素, error 解码或取消错误
func (img *EncodedImage) decodeContext(ctx context.Context, pending chan struct{}) (image.Image, error) {
defer func() {
img.mu.Lock()
img.pending = nil
close(pending)
img.mu.Unlock()
}()
pixels, _, err := image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(img.data)), context: ctx})
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
img.result.Store(&encodedImageResult{image: pixels, err: err})
return pixels, err
}
// At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (img *EncodedImage) At(x, y int) color.Color {
decoded, err := img.Image()
if err != nil {
return color.Transparent
}
return decoded.At(x, y)
}
// newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImageContext(context.Background(), data)
}
// newEncodedImageContext 读取PNG或JPEG头信息,元数据扫描期间响应取消,不复制输入或解码像素
// 入参: ctx 取消上下文, data 只读图片编码
// 返回: *EncodedImage 惰性图片, error 头信息、格式或取消错误
func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, error) {
config, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
if err != nil {
return nil, err
}
if format != "jpeg" && format != "png" {
return nil, fmt.Errorf("unsupported encoded image %q", format)
}
return &EncodedImage{data: data, format: format, config: config}, nil
}
+31
View File
@@ -0,0 +1,31 @@
// 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 image
// NormalizeFormat 标准化印章媒体类型
// 入参: s 原始媒体类型
// 返回: string 标准媒体类型
func NormalizeFormat(s string) string {
switch s {
case "jpg":
return "jpeg"
case "bmp":
return "bmp"
case "jb2", "gbig2":
return "jbig2"
default:
return s
}
}
+714
View File
@@ -0,0 +1,714 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
_ "image/gif"
"io"
_ "github.com/xiaoqidun/j2kgo"
_ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
type transparentEdgeImage struct {
source image.Image
rect image.Rectangle
}
// transparentEdgeImage64 按需补齐十六位像素边缘,保留原始只读图像
type transparentEdgeImage64 struct {
source image.Image
rect image.Rectangle
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage) ColorModel() color.Model { return color.NRGBAModel }
// NRGBAAt 读取原始像素或按邻域补齐透明边缘,边界外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA 非预乘颜色
func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := NRGBAAt(s.source, sx, sy)
if c.A == 0 {
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
c = edge
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c = transparentPaddingColor(c)
}
return c
}
// At 返回非预乘像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage) At(x, y int) color.Color { return s.NRGBAAt(x, y) }
// RGBA64At 返回预乘十六位像素,供采样器无装箱读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBAAt(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回十六位非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model }
// NRGBA64At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *transparentEdgeImage64) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA64{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := NRGBA64At(s.source, sx, sy)
if c.A == 0 {
var best color.NRGBA64
for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ {
for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ {
v := NRGBA64At(s.source, nx, ny)
if v.A > best.A {
best = v
}
}
}
if best.A != 0 {
best.A = 1
c = best
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c.A = 1
}
return c
}
// At 返回非预乘十六位像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage64) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// RGBA64At 返回保留原采样精度的预乘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBA64At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Decode 解码图片数据
// 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
func Decode(data []byte) (image.Image, string, error) {
return DecodeContext(context.Background(), data)
}
// DecodeContext 解码图片并传递取消状态,PNG和JPEG仍保留惰性像素解码
// 入参: ctx 取消上下文, data 只读图片编码
// 返回: image.Image 图片对象, string 图片格式, error 解码或取消错误
func DecodeContext(ctx context.Context, data []byte) (img image.Image, format string, err error) {
if err := ctx.Err(); err != nil {
return nil, "", err
}
defer func() {
if cancelErr := ctx.Err(); cancelErr != nil {
img, err = nil, cancelErr
}
}()
if IsJPEG(data) || IsPNG(data) {
img, err := newEncodedImageContext(ctx, data)
if err != nil {
return nil, "", err
}
return img, img.format, nil
}
if IsTIFF(data) {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return nil, "tiff", err
}
img, err := DecodeTIFF(ctx, data, layout)
return img, "tiff", err
}
img, format, err = image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err == nil {
return img, NormalizeFormat(format), nil
}
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
if img, err := DecodeBMPContext(ctx, data); err == nil {
return img, "bmp", nil
}
return nil, "", err
}
// DecodePixelsContext 完整解码图片,确保像素可读取并传递取消状态
// 入参: ctx 取消上下文, data 图片编码数据
// 返回: image.Image 解码像素, string 图片格式, error 解码或取消错误
func DecodePixelsContext(ctx context.Context, data []byte) (image.Image, string, error) {
img, format, err := DecodeContext(ctx, data)
if err != nil {
return nil, format, err
}
pixels, err := PixelDataContext(ctx, img)
return pixels, format, err
}
// IsJPEG 判断是否为JPEG图片数据
// 入参: data 图片数据
// 返回: bool 是否为JPEG图片数据
func IsJPEG(data []byte) bool {
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
}
// IsPNG 判断是否为PNG图片数据
// 入参: data 图片数据
// 返回: bool 是否为PNG图片数据
func IsPNG(data []byte) bool {
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
}
// DecodeConfigData 解码图片尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
func DecodeConfigData(data []byte) (image.Config, string, error) {
return DecodeConfigDataContext(context.Background(), data)
}
// DecodeConfigDataContext 读取图片尺寸和颜色模型,传递布局解析中的取消状态
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Config 图片配置, string 图片格式, error 读取或取消错误
func DecodeConfigDataContext(ctx context.Context, data []byte) (config image.Config, format string, err error) {
if err := ctx.Err(); err != nil {
return image.Config{}, "", err
}
defer func() {
if cancelErr := ctx.Err(); cancelErr != nil {
config, err = image.Config{}, cancelErr
}
}()
if IsTIFF(data) {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return image.Config{}, "tiff", err
}
profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
var cfg image.Config
if layout.needsSampleView() {
img, _, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
cfg.ColorModel = img.ColorModel()
} else {
input := newTIFFDirectoryInput(data, layout.Offset)
input.ctx = ctx
cfg, err = tiff.DecodeConfig(input)
if err != nil {
return image.Config{}, "tiff", err
}
}
if profile != nil || layout.Transparency != nil {
cfg.ColorModel = color.NRGBA64Model
}
if layout.Transparency != nil {
if _, err := readTIFFColorProfile(ctx, data, *layout.Transparency); err != nil {
return image.Config{}, "tiff", err
}
if _, _, err := tiffPlaneDirectories(ctx, data, *layout.Transparency); err != nil {
return image.Config{}, "tiff", err
}
}
size := layout.DisplaySize()
cfg.Width, cfg.Height = size.X, size.Y
return cfg, "tiff", nil
}
cfg, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err == nil {
return cfg, NormalizeFormat(format), nil
}
if cfg, err := decodeBMPConfig(data); err == nil {
return cfg, "bmp", nil
}
return image.Config{}, "", err
}
// decodeBMPConfig 解码BMP尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, error 错误信息
func decodeBMPConfig(data []byte) (image.Config, error) {
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
if err != nil {
return image.Config{}, err
}
if offset <= 0 || width <= 0 || height == 0 {
return image.Config{}, fmt.Errorf("unsupported bmp")
}
if _, err := BMPColorMasks(data, offset, bpp, compression); err != nil {
return image.Config{}, err
}
if height < 0 {
height = -height
}
return image.Config{ColorModel: color.NRGBAModel, Width: width, Height: height}, nil
}
// decodeBMPImage 解码BMP图片
// 入参: data 图片数据
// 返回: image.Image 图片对象, error 错误信息
func decodeBMPImage(data []byte) (image.Image, error) {
return DecodeBMPContext(context.Background(), data)
}
// DecodeBMPContext 解码BMP真彩色像素,逐行检查取消
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Image 图片对象, error 格式或取消错误
func DecodeBMPContext(ctx context.Context, data []byte) (image.Image, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
if err != nil {
return nil, err
}
if offset > len(data) {
return nil, fmt.Errorf("invalid bmp pixel offset")
}
masks, err := BMPColorMasks(data, offset, bpp, compression)
if err != nil {
return nil, err
}
topDown := height < 0
if topDown {
height = -height
}
if width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid bmp size")
}
stride := ((int64(width)*int64(bpp) + 31) / 32) * 4
if stride > int64(len(data)-offset)/int64(height) {
return nil, fmt.Errorf("truncated bmp")
}
if width > int(^uint(0)>>1)/4/height {
return nil, fmt.Errorf("bmp dimensions exceed image capacity")
}
rowStride := int(stride)
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
if err := ctx.Err(); err != nil {
return nil, err
}
srcY := y
if !topDown {
srcY = height - 1 - y
}
row := data[offset+srcY*rowStride:]
for x := 0; x < width; x++ {
switch bpp {
case 16, 32:
var value uint32
if bpp == 16 {
value = uint32(binary.LittleEndian.Uint16(row[x*2:]))
} else {
value = binary.LittleEndian.Uint32(row[x*4:])
}
p := y*img.Stride + x*4
for i, mask := range masks {
img.Pix[p+i] = mask.sample(value)
}
case 24:
p := x * 3
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
}
}
}
return img, nil
}
// ParseBMPHeader 解析BMP文件头
// 入参: data 图片数据
// 返回: int 像素偏移, int 宽度, int 高度, uint16 位数, uint32 压缩方式, error 错误信息
func ParseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
if len(data) < 54 || string(data[:2]) != "BM" {
return 0, 0, 0, 0, 0, fmt.Errorf("not bmp")
}
offset := int(binary.LittleEndian.Uint32(data[10:14]))
dibSize := binary.LittleEndian.Uint32(data[14:18])
if dibSize < 40 {
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
}
if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
}
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
planes := binary.LittleEndian.Uint16(data[26:28])
bpp := binary.LittleEndian.Uint16(data[28:30])
compression := binary.LittleEndian.Uint32(data[30:34])
if planes != 1 || width <= 0 || height == 0 || int64(height) == -int64(^uint(0)>>1)-1 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
}
return offset, width, height, bpp, compression, nil
}
// PixelSource 获取图片像素源
// 入参: img 图片对象
// 返回: image.Image 图片像素源
func PixelSource(img image.Image) image.Image {
if decoded, err := PixelData(img); err == nil {
return decoded
}
return img
}
// PixelData 获取像素并保留惰性解码错误,供绘制和像素处理入口校验
// 入参: img 图片对象
// 返回: image.Image 解码像素, error 解码错误
func PixelData(img image.Image) (image.Image, error) {
if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
return src.Image()
}
return img, nil
}
// PixelDataContext 获取像素并传递取消状态,兼容不接收上下文的图像实现
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
// 返回: image.Image 解码像素, error 解码或取消错误
func PixelDataContext(ctx context.Context, img image.Image) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if src, ok := img.(interface {
ImageContext(context.Context) (image.Image, error)
}); ok {
return src.ImageContext(ctx)
}
pixels, err := PixelData(img)
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
return pixels, err
}
// GrayAt 获取图片灰度像素
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.Gray 灰度像素
func GrayAt(img image.Image, x, y int) color.Gray {
if src, ok := img.(*image.Gray); ok {
return src.GrayAt(x, y)
}
if src, ok := img.(*image.Paletted); ok {
return color.GrayModel.Convert(src.At(x, y)).(color.Gray)
}
if src, ok := img.(image.RGBA64Image); ok {
r, g, b, _ := src.RGBA64At(x, y).RGBA()
return color.Gray{Y: uint8((19595*r + 38470*g + 7471*b + 1<<15) >> 24)}
}
return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
}
// NRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA NRGBA像素
func NRGBAAt(img image.Image, x, y int) color.NRGBA {
switch src := img.(type) {
case interface{ NRGBAAt(int, int) color.NRGBA }:
return src.NRGBAAt(x, y)
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
c := src.NRGBA64At(x, y)
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
case *image.RGBA, *image.RGBA64, *image.Gray, *image.Gray16, *image.Alpha, *image.Alpha16, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA{}
}
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
if a != 0xffff {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
}
value := img.At(x, y)
if c, ok := value.(color.NRGBA64); ok {
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
}
return color.NRGBAModel.Convert(value).(color.NRGBA)
}
// AlphaAt 读取十六位透明度,原生图像避免颜色装箱
// 入参: img 图像, x 横坐标, y 纵坐标
// 返回: uint32 十六位透明度
func AlphaAt(img image.Image, x, y int) uint32 {
if source, ok := img.(image.RGBA64Image); ok {
return uint32(source.RGBA64At(x, y).A)
}
_, _, _, alpha := img.At(x, y).RGBA()
return alpha
}
// HighPrecision 判断颜色模型及调色板是否需要保留十六位采样
// 入参: img 图片对象
// 返回: bool 是否使用十六位颜色或透明度
func HighPrecision(img image.Image) bool {
model := img.ColorModel()
if palette, ok := model.(color.Palette); ok {
for _, value := range palette {
switch value.(type) {
case color.RGBA, color.NRGBA, color.Gray, color.Alpha, color.CMYK, color.YCbCr, color.NYCbCrA:
default:
pixel := color.NRGBA64Model.Convert(value).(color.NRGBA64)
if pixel.R%257 != 0 || pixel.G%257 != 0 || pixel.B%257 != 0 || pixel.A%257 != 0 {
return true
}
}
}
return false
}
return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model
}
// NRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA64 NRGBA64像素
func NRGBA64At(img image.Image, x, y int) color.NRGBA64 {
switch src := img.(type) {
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
return src.NRGBA64At(x, y)
case *image.NRGBA:
c := src.NRGBAAt(x, y)
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
case *image.Gray:
v := uint16(src.GrayAt(x, y).Y) * 257
return color.NRGBA64{R: v, G: v, B: v, A: 0xffff}
case *image.Gray16:
c := src.Gray16At(x, y)
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff}
case *image.Alpha, *image.Alpha16:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA64{}
}
return color.NRGBA64{R: 0xffff, G: 0xffff, B: 0xffff, A: c.A}
case *image.RGBA, *image.RGBA64, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA64{}
}
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
if a != 0xffff {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: c.A}
}
value := img.At(x, y)
if c, ok := value.(color.NRGBA); ok {
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
}
return color.NRGBA64Model.Convert(value).(color.NRGBA64)
}
// transparentEdgeColor 获取透明像素相邻的可见颜色
// 入参: source 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA 颜色, bool 是否存在
func transparentEdgeColor(source image.Image, x, y int) (color.NRGBA, bool) {
img, ok := source.(*image.NRGBA)
if !ok {
bounds := source.Bounds()
var best color.NRGBA
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
for nx := max(x-1, bounds.Min.X); nx < min(x+2, bounds.Max.X); nx++ {
if nx == x && ny == y {
continue
}
pixel := NRGBAAt(source, nx, ny)
if pixel.A > best.A {
best = pixel
if best.A == 255 {
best.A = 1
return best, true
}
}
}
}
if best.A == 0 {
return color.NRGBA{}, false
}
best.A = 1
return best, true
}
bounds := img.Bounds()
best, alpha := 0, uint8(0)
left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X)
scan:
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
offset := img.PixOffset(left, ny)
for nx := left; nx < right; nx++ {
if (nx != x || ny != y) && img.Pix[offset+3] > alpha {
best, alpha = offset, img.Pix[offset+3]
if alpha == 255 {
break scan
}
}
offset += 4
}
}
if alpha == 0 {
return color.NRGBA{}, false
}
return color.NRGBA{R: img.Pix[best], G: img.Pix[best+1], B: img.Pix[best+2], A: 1}, true
}
// transparentPaddingColor 获取透明补齐颜色
// 入参: c 边缘颜色
// 返回: color.NRGBA 补齐颜色
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
c.A = 1
return c
}
// WithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误
func WithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, 0, err
}
if img == nil {
return img, 0, nil
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w == 0 || h == 0 {
return img, 0, nil
}
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
return img, 0, nil
}
source, err := PixelDataContext(ctx, img)
if err != nil {
return nil, 0, err
}
switch source.(type) {
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
return img, 0, nil
}
hasZero, hasVisible := false, false
var pixels []byte
var stride, step int
switch src := source.(type) {
case *image.NRGBA:
pixels, stride, step = src.Pix, src.Stride, 4
case *image.RGBA:
pixels, stride, step = src.Pix, src.Stride, 4
case *image.Alpha:
pixels, stride, step = src.Pix, src.Stride, 1
}
if pixels != nil {
hasZero = pixels[step-1] == 0
hasVisible = !hasZero
scan:
for y := 0; y < h; y++ {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
offset := y*stride + step - 1
for x := 0; x < w; x++ {
if x&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
}
if (pixels[offset] == 0) != hasZero {
hasZero, hasVisible = true, true
break scan
}
offset += step
}
}
} else {
for y := bounds.Min.Y; y < bounds.Max.Y && !(hasZero && hasVisible); y++ {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
}
if AlphaAt(source, x, y) == 0 {
hasZero = true
} else {
hasVisible = true
}
if hasZero && hasVisible {
break
}
}
}
}
if err := ctx.Err(); err != nil {
return nil, 0, err
}
if !hasZero || !hasVisible {
return img, 0, nil
}
if HighPrecision(source) {
return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil
}
return &transparentEdgeImage{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil
}
+36
View File
@@ -0,0 +1,36 @@
// 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 image
import (
"context"
"io"
)
// contextInput 在读取图像编码时检查调用方取消状态
type contextInput struct {
io.ReadCloser
context context.Context
}
// Read 检查取消状态后读取原始数据
// 入参: data 数据缓冲区
// 返回: int 读取字节数, error 读取或取消错误
func (r contextInput) Read(data []byte) (int, error) {
if err := r.context.Err(); err != nil {
return 0, err
}
return r.ReadCloser.Read(data)
}
+234
View File
@@ -0,0 +1,234 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"hash/crc32"
)
// PageInfo 图片方向与像素密度,不改变原始编码
type PageInfo struct {
Orientation int
XDPI, YDPI float64
}
// PageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消
// TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF
// 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
// 返回: PageInfo 方向与横纵DPI, error 元数据或取消错误
func PageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (PageInfo, error) {
info := PageInfo{Orientation: 1, XDPI: dpi, YDPI: dpi}
if err := ctx.Err(); err != nil {
return info, err
}
var exif []byte
var directory uint32
scaleX, scaleY := 1.0, 1.0
if format == "tiff" {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return info, err
}
directory = layout.Offset
exif = data
original, display := layout.size(), layout.DisplaySize()
scaleX, scaleY = float64(display.X)/float64(original.X), float64(display.Y)/float64(original.Y)
}
if format == "bmp" && len(data) >= 54 && binary.LittleEndian.Uint32(data[14:18]) >= 40 {
x := int32(binary.LittleEndian.Uint32(data[38:42]))
y := int32(binary.LittleEndian.Uint32(data[42:46]))
if x > 0 && y > 0 {
info.XDPI, info.YDPI = float64(x)*0.0254, float64(y)*0.0254
}
}
if format == "png" {
for offset := 8; offset+12 <= len(data); {
if err := ctx.Err(); err != nil {
return info, err
}
n := uint64(binary.BigEndian.Uint32(data[offset:]))
if n > uint64(len(data)-offset-12) {
return info, fmt.Errorf("truncated PNG chunk")
}
end := offset + 8 + int(n)
kind, payload := string(data[offset+4:offset+8]), data[offset+8:end]
if kind == "pHYs" || kind == "eXIf" {
if crc32.ChecksumIEEE(data[offset+4:end]) != binary.BigEndian.Uint32(data[end:]) {
return info, fmt.Errorf("invalid PNG %s checksum", kind)
}
}
if kind == "pHYs" && len(payload) == 9 {
x, y := float64(binary.BigEndian.Uint32(payload)), float64(binary.BigEndian.Uint32(payload[4:]))
if x > 0 && y > 0 {
if payload[8] == 1 {
info.XDPI, info.YDPI = x*0.0254, y*0.0254
} else if payload[8] == 0 {
info.YDPI = dpi * y / x
}
}
} else if kind == "eXIf" {
exif = payload
}
offset = end + 4
if kind == "IEND" {
break
}
}
} else if format == "jpeg" {
for offset := 2; offset < len(data); {
if err := ctx.Err(); err != nil {
return info, err
}
if data[offset] != 0xff {
return info, fmt.Errorf("invalid JPEG marker")
}
for offset < len(data) && data[offset] == 0xff {
offset++
}
if offset >= len(data) {
break
}
marker := data[offset]
offset++
if marker == 0xda || marker == 0xd9 {
break
}
if marker == 0x01 || marker >= 0xd0 && marker <= 0xd7 {
continue
}
if offset+2 > len(data) {
return info, fmt.Errorf("truncated JPEG marker")
}
n := int(binary.BigEndian.Uint16(data[offset:]))
if n < 2 || n > len(data)-offset {
return info, fmt.Errorf("truncated JPEG segment")
}
payload := data[offset+2 : offset+n]
if marker == 0xe0 && len(payload) >= 14 && bytes.Equal(payload[:5], []byte("JFIF\x00")) {
x, y := float64(binary.BigEndian.Uint16(payload[8:])), float64(binary.BigEndian.Uint16(payload[10:]))
if x > 0 && y > 0 {
switch payload[7] {
case 0:
info.YDPI = dpi * y / x
case 1:
info.XDPI, info.YDPI = x, y
case 2:
info.XDPI, info.YDPI = x*2.54, y*2.54
}
}
} else if marker == 0xe1 && bytes.HasPrefix(payload, []byte("Exif\x00\x00")) {
exif = payload[6:]
}
offset += n
}
}
if len(exif) != 0 {
orientation, x, y, err := imageExifMetadata(exif, dpi, directory)
if err != nil {
return info, err
}
info.Orientation = orientation
if (format == "jpeg" || format == "tiff") && x > 0 && y > 0 {
info.XDPI, info.YDPI = x, y
}
if format == "tiff" {
if orientation >= 5 {
info.XDPI, info.YDPI = info.YDPI, info.XDPI
}
info.Orientation = 1
}
}
info.XDPI, info.YDPI = info.XDPI*scaleX, info.YDPI*scaleY
return info, ctx.Err()
}
// imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录
// 入参: data TIFF或EXIF数据, dpi 无绝对密度单位时的横向DPI,零值不换算, directory 目录偏移,零值使用首目录
// 返回: int EXIF方向值,未声明时为1, float64 横向DPI, float64 纵向DPI, error 元数据错误
func imageExifMetadata(data []byte, dpi float64, directory uint32) (int, float64, float64, error) {
invalid := fmt.Errorf("invalid EXIF image metadata")
if len(data) < 8 {
return 0, 0, 0, invalid
}
var order binary.ByteOrder
switch string(data[:2]) {
case "II":
order = binary.LittleEndian
case "MM":
order = binary.BigEndian
default:
return 0, 0, 0, invalid
}
if order.Uint16(data[2:]) != 42 {
return 0, 0, 0, invalid
}
offset := uint64(order.Uint32(data[4:]))
if directory != 0 {
offset = uint64(directory)
}
if offset < 8 || offset+2 > uint64(len(data)) {
return 0, 0, 0, invalid
}
count := uint64(order.Uint16(data[offset:]))
if count*12+offset+6 > uint64(len(data)) {
return 0, 0, 0, invalid
}
orientation, unit := 1, 2
var x, y float64
for i := uint64(0); i < count; i++ {
entry := data[offset+2+i*12 : offset+14+i*12]
tag, kind, count := order.Uint16(entry), order.Uint16(entry[2:]), order.Uint32(entry[4:])
switch tag {
case 0x112, 0x128:
if kind != 3 || count != 1 {
return 0, 0, 0, invalid
}
value := int(order.Uint16(entry[8:]))
if tag == 0x112 {
if value < 1 || value > 8 {
return 0, 0, 0, invalid
}
orientation = value
} else {
unit = value
}
case 0x11a, 0x11b:
at := uint64(order.Uint32(entry[8:]))
if kind != 5 || count != 1 || at+8 > uint64(len(data)) {
return 0, 0, 0, invalid
}
n, d := order.Uint32(data[at:]), order.Uint32(data[at+4:])
if n > 0 && d > 0 {
if tag == 0x11a {
x = float64(n) / float64(d)
} else {
y = float64(n) / float64(d)
}
}
}
}
if unit == 3 {
x, y = x*2.54, y*2.54
} else if unit == 1 && dpi > 0 && x > 0 && y > 0 {
x, y = dpi, dpi*y/x
} else if unit != 2 {
x, y = 0, 0
}
return orientation, x, y, nil
}
+119
View File
@@ -0,0 +1,119 @@
// 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 image
import "image"
// StorageCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量
// 包装图像可实现BackingImage() image.Image,使底层共享缓冲计入预算
// 入参: img 图片资源, limit 缓存预算
// 返回: int 估算字节数或超预算标记
func StorageCost(img image.Image, limit int) int {
cost := 256
w, h := img.Bounds().Dx(), img.Bounds().Dy()
if limit < cost || w <= 0 || h <= 0 || w > (limit-cost)/8/h {
return limit + 1
}
pixels := w * h * 8
if encoded, ok := img.(*EncodedImage); ok {
if cap(encoded.data) > limit-cost-pixels {
return limit + 1
}
return cost + pixels + cap(encoded.data)
}
var buffers [5]int
switch source := img.(type) {
case *image.RGBA:
buffers[0] = cap(source.Pix)
case *image.RGBA64:
buffers[0] = cap(source.Pix)
case *image.NRGBA:
buffers[0] = cap(source.Pix)
case *image.NRGBA64:
buffers[0] = cap(source.Pix)
case *image.Gray:
buffers[0] = cap(source.Pix)
case *image.Gray16:
buffers[0] = cap(source.Pix)
case *image.Alpha:
buffers[0] = cap(source.Pix)
case *image.Alpha16:
buffers[0] = cap(source.Pix)
case *image.CMYK:
buffers[0] = cap(source.Pix)
case *image.Paletted:
if cap(source.Palette) > (limit-cost)/16 {
return limit + 1
}
buffers[0], buffers[1] = cap(source.Pix), cap(source.Palette)*16
case *image.YCbCr:
buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr)}
case *image.NYCbCrA:
buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr), cap(source.A)}
case *planarTIFFImage:
if cap(source.palette) > (limit-cost)/8 {
return limit + 1
}
cost += cap(source.palette) * 8
if cap(source.planes) > (limit-cost)/16 || len(source.planes) > len(buffers) {
return limit + 1
}
cost += cap(source.planes) * 16
for i, plane := range source.planes {
switch plane := plane.(type) {
case *image.Gray:
buffers[i] = cap(plane.Pix)
case *image.Gray16:
buffers[i] = cap(plane.Pix)
default:
buffers[i] = StorageCost(plane, limit-cost)
}
}
case *tiffFaxImage:
if cap(source.blocks) > (limit-cost)/16 {
return limit + 1
}
cost += cap(source.blocks) * 16
for _, block := range source.blocks {
size := StorageCost(block, limit-cost)
if size > limit-cost {
return limit + 1
}
cost += size
}
case *orientedTIFFImage:
buffers[0] = StorageCost(source.source, limit-cost)
case *maskedTIFFImage:
buffers[0] = StorageCost(source.source, limit-cost)
buffers[1] = StorageCost(source.mask, limit-cost)
case *transparentEdgeImage:
buffers[0] = StorageCost(source.source, limit-cost)
case *transparentEdgeImage64:
buffers[0] = StorageCost(source.source, limit-cost)
case interface{ BackingImage() image.Image }:
buffers[0] = StorageCost(source.BackingImage(), limit-cost)
}
storage := 0
for _, size := range buffers {
if size > limit-cost-storage {
return limit + 1
}
storage += size
}
if pixels > limit-cost {
return limit + 1
}
return cost + max(pixels, storage)
}
+433
View File
@@ -0,0 +1,433 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"io"
)
// ErrTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
var ErrTIFFRandomAccess = errors.New("TIFF directory requires random access")
// TIFFLayout 保存TIFF目录的图像布局及关联透明蒙版
type TIFFLayout struct {
Width, Height int
TileWidth int
TileHeight int
Orientation int
Offset, Next uint32
Reduced, mask bool
Planar bool
auxiliary bool
Samples uint32
depth uint32
fillOrder uint32
compression uint32
predictor uint32
photometric uint32
profileOffset uint32
profileSize uint32
t4, t6 uint32
Transparency *TIFFLayout
}
// tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源
type tiffDirectoryInput struct {
reader *bytes.Reader
directory [4]byte
ctx context.Context
}
// orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度
type orientedTIFFImage struct {
source image.Image
TIFFLayout
}
// IsTIFF 识别标准TIFF字节序与版本标识
// 入参: data 文件头或完整编码
// 返回: bool 是否为TIFF 6.0编码
func IsTIFF(data []byte) bool {
return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a")
}
// readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图
// 入参: input 从文件头开始的输入流
// 返回: TIFFLayout 存储尺寸与方向, error 文件结构或方向错误
func readTIFFLayout(input io.Reader) (TIFFLayout, error) {
return ReadTIFFLayoutContext(context.Background(), input)
}
// ReadTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消
// 入参: ctx 取消上下文, input 从文件头开始的输入流
// 返回: TIFFLayout 存储尺寸与方向, error 结构、读取或取消错误
func ReadTIFFLayoutContext(ctx context.Context, input io.Reader) (TIFFLayout, error) {
var result TIFFLayout
if err := ctx.Err(); err != nil {
return result, err
}
var header [8]byte
if _, err := io.ReadFull(input, header[:]); err != nil {
return result, err
}
if !IsTIFF(header[:]) {
return result, fmt.Errorf("invalid TIFF header")
}
var order binary.ByteOrder = binary.LittleEndian
if header[0] == 'M' {
order = binary.BigEndian
}
offset := int64(order.Uint32(header[4:]))
position := int64(8)
var seen map[uint32]bool
var transparency *TIFFLayout
for offset != 0 {
if seen != nil && seen[uint32(offset)] {
return result, fmt.Errorf("cyclic TIFF directory chain")
}
layout, end, err := readTIFFDirectory(ctx, input, order, offset, position)
if err != nil {
return result, err
}
if layout.mask {
if !layout.Reduced {
if transparency != nil {
return result, fmt.Errorf("multiple TIFF transparency masks")
}
transparency = &layout
}
} else if !layout.Reduced || layout.Next == 0 && seen == nil {
if result.Offset != 0 {
return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources")
}
result = layout
}
if layout.Next != 0 {
if seen == nil {
seen = make(map[uint32]bool)
}
seen[uint32(offset)] = true
}
offset, position = int64(layout.Next), end
}
if err := ctx.Err(); err != nil {
return TIFFLayout{}, err
}
if result.Offset == 0 {
return result, fmt.Errorf("missing TIFF main image")
}
result.Transparency = transparency
if transparency != nil {
size := result.DisplaySize()
if size.X > int(^uint(0)>>1)/8/size.Y {
return TIFFLayout{}, fmt.Errorf("TIFF mask image dimensions exceed capacity")
}
}
return result, nil
}
// readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素
// 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置
// 返回: TIFFLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误
func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (TIFFLayout, int64, error) {
var result TIFFLayout
if err := ctx.Err(); err != nil {
return result, position, err
}
var header [4]byte
if offset < 8 {
return result, position, fmt.Errorf("invalid TIFF directory offset")
}
if seeker, ok := input.(io.Seeker); ok {
if _, err := seeker.Seek(offset, io.SeekStart); err != nil {
return result, position, err
}
} else {
if offset < position {
return result, position, ErrTIFFRandomAccess
}
reader := contextInput{ReadCloser: io.NopCloser(input), context: ctx}
if _, err := io.CopyN(io.Discard, reader, offset-position); err != nil {
return result, position, err
}
}
if _, err := io.ReadFull(input, header[:2]); err != nil {
return result, position, err
}
count := int(order.Uint16(header[:2]))
result.Orientation, result.Offset, result.Samples, result.fillOrder = 1, uint32(offset), 1, 1
result.depth = 1
result.compression = 1
result.predictor = 1
var entry [12]byte
var previous uint16
var subfile, legacy uint32
var hasSubfile bool
var invalidMaskDepth bool
for i := range count {
if err := ctx.Err(); err != nil {
return result, position, err
}
if _, err := io.ReadFull(input, entry[:]); err != nil {
return result, position, err
}
tag, kind, number := order.Uint16(entry[:2]), order.Uint16(entry[2:4]), order.Uint32(entry[4:8])
if i > 0 && tag <= previous {
return result, position, fmt.Errorf("invalid TIFF directory order")
}
previous = tag
if tag == 338 {
result.auxiliary = kind != 3 || number != 1 || order.Uint16(entry[8:]) == 0
}
if tag == 34675 {
if kind != 7 || number < 132 {
return result, position, fmt.Errorf("invalid TIFF color profile")
}
result.profileOffset, result.profileSize = order.Uint32(entry[8:]), number
continue
}
if tag == 258 {
invalidMaskDepth = kind != 3 || number != 1
if !invalidMaskDepth {
result.depth = uint32(order.Uint16(entry[8:10]))
}
}
if tag != 254 && tag != 255 && tag != 256 && tag != 257 && tag != 259 && tag != 262 && tag != 266 && tag != 274 && tag != 277 && tag != 284 && tag != 292 && tag != 293 && tag != 317 && tag != 322 && tag != 323 {
continue
}
if number != 1 || kind != 3 && kind != 4 || (tag == 255 || tag == 259 || tag == 262 || tag == 266 || tag == 274 || tag == 277 || tag == 284 || tag == 317) && kind != 3 || (tag == 254 || tag == 292 || tag == 293) && kind != 4 {
return result, position, fmt.Errorf("invalid TIFF image layout")
}
value := uint64(order.Uint32(entry[8:12]))
if kind == 3 {
value = uint64(order.Uint16(entry[8:10]))
}
if (tag == 256 || tag == 257 || tag == 322 || tag == 323) && (value == 0 || value > uint64(int(^uint(0)>>1))) {
return result, position, fmt.Errorf("invalid TIFF dimensions")
}
switch tag {
case 254:
subfile, hasSubfile = uint32(value), true
case 255:
legacy = uint32(value)
case 256:
result.Width = int(value)
case 257:
result.Height = int(value)
case 259:
result.compression = uint32(value)
case 262:
result.photometric = uint32(value)
result.mask = value == 4
case 266:
if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF fill order")
}
result.fillOrder = uint32(value)
case 274:
if value < 1 || value > 8 {
return result, position, fmt.Errorf("invalid TIFF orientation")
}
result.Orientation = int(value)
case 277:
if value == 0 {
return result, position, fmt.Errorf("invalid TIFF sample count")
}
result.Samples = uint32(value)
case 284:
if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF planar configuration")
}
result.Planar = value == 2
case 292:
result.t4 = uint32(value)
case 293:
result.t6 = uint32(value)
case 317:
result.predictor = uint32(value)
case 322:
result.TileWidth = int(value)
case 323:
result.TileHeight = int(value)
}
}
if _, err := io.ReadFull(input, header[:4]); err != nil {
return result, position, err
}
result.Next = order.Uint32(header[:4])
if hasSubfile {
result.Reduced, result.mask = subfile&1 != 0, result.mask || subfile&4 != 0
} else {
result.Reduced = legacy == 2
}
if result.Width == 0 || result.Height == 0 {
return result, position, fmt.Errorf("missing TIFF dimensions")
}
if (result.TileWidth == 0) != (result.TileHeight == 0) || result.TileWidth%16 != 0 || result.TileHeight%16 != 0 {
return result, position, fmt.Errorf("invalid TIFF tile dimensions")
}
if result.mask && (invalidMaskDepth || result.photometric != 4 || result.Samples != 1 || result.depth != 1) {
return result, position, fmt.Errorf("invalid TIFF transparency mask")
}
return result, offset + 6 + int64(count)*12, nil
}
// newTIFFDirectoryInput 创建以指定目录作为主图的只读解码视图
// 入参: data 已校验的TIFF编码, offset 主图目录偏移
// 返回: *tiffDirectoryInput 随机访问输入
func newTIFFDirectoryInput(data []byte, offset uint32) *tiffDirectoryInput {
input := &tiffDirectoryInput{reader: bytes.NewReader(data)}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
order.PutUint32(input.directory[:], offset)
return input
}
// patch 替换读取片段中与首目录指针重叠的部分
// 入参: data 读取结果, offset 文件偏移
func (r *tiffDirectoryInput) patch(data []byte, offset int64) {
for i := range r.directory {
at := int64(4+i) - offset
if at >= 0 && at < int64(len(data)) {
data[at] = r.directory[i]
}
}
}
// Read 读取图像编码并覆盖首目录指针
// 入参: data 接收缓冲
// 返回: int 读取字节数, error 读取错误
func (r *tiffDirectoryInput) Read(data []byte) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
offset := r.reader.Size() - int64(r.reader.Len())
n, err := r.reader.Read(data)
r.patch(data[:n], offset)
return n, err
}
// ReadAt 随机读取图像编码并覆盖首目录指针
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取错误
func (r *tiffDirectoryInput) ReadAt(data []byte, offset int64) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
n, err := r.reader.ReadAt(data, offset)
r.patch(data[:n], offset)
return n, err
}
// size 返回应用方向后的显示尺寸
// 返回: image.Point 像素宽高
func (l TIFFLayout) size() image.Point {
if l.Orientation >= 5 {
return image.Pt(l.Height, l.Width)
}
return image.Pt(l.Width, l.Height)
}
// DisplaySize 返回同时保留主图和透明蒙版分辨率的显示尺寸
// 返回: image.Point 保留高分辨率蒙版边缘的像素宽高
func (l TIFFLayout) DisplaySize() image.Point {
size := l.size()
if l.Transparency != nil {
mask := l.Transparency.size()
size.X, size.Y = max(size.X, mask.X), max(size.Y, mask.Y)
}
return size
}
// Bounds 返回从零开始的显示区域
// 返回: image.Rectangle 显示区域
func (s *orientedTIFFImage) Bounds() image.Rectangle {
return image.Rectangle{Max: s.size()}
}
// ColorModel 返回原始颜色模型
// 返回: color.Model 颜色模型
func (s *orientedTIFFImage) ColorModel() color.Model { return s.source.ColorModel() }
// sourcePoint 将显示坐标映射为存储坐标
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: image.Point 原始坐标, bool 是否位于显示区域内
func (s *orientedTIFFImage) sourcePoint(x, y int) (image.Point, bool) {
if !image.Pt(x, y).In(s.Bounds()) {
return image.Point{}, false
}
w, h := s.Width, s.Height
switch s.Orientation {
case 2:
x = w - 1 - x
case 3:
x, y = w-1-x, h-1-y
case 4:
y = h - 1 - y
case 5:
x, y = y, x
case 6:
x, y = y, h-1-x
case 7:
x, y = w-1-y, h-1-x
case 8:
x, y = w-1-y, x
}
b := s.source.Bounds()
return image.Pt(b.Min.X+x, b.Min.Y+y), true
}
// At 返回原图颜色,不重新采样或量化
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.Color 像素颜色,区域外透明
func (s *orientedTIFFImage) At(x, y int) color.Color {
p, ok := s.sourcePoint(x, y)
if !ok {
return color.NRGBA64{}
}
return s.source.At(p.X, p.Y)
}
// NRGBA64At 读取非预乘十六位颜色,保留透明像素中的原始颜色
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.NRGBA64 像素颜色
func (s *orientedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
p, ok := s.sourcePoint(x, y)
if !ok {
return color.NRGBA64{}
}
return NRGBA64At(s.source, p.X, p.Y)
}
// RGBA64At 返回预乘十六位像素,保留原始颜色精度
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.RGBA64 像素颜色
func (s *orientedTIFFImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+189
View File
@@ -0,0 +1,189 @@
// 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 image
import (
"cmp"
"context"
"encoding/binary"
"fmt"
"image"
"io"
"math/bits"
"slices"
"sort"
"golang.org/x/image/tiff"
)
// tiffBitOrderInput 仅在读取像素块时反转字节内位序,保留目录和原始数据
type tiffBitOrderInput struct {
source io.ReaderAt
ranges [][2]int64
position int64
}
// tiffBitOrderRanges 收集低位优先的未压缩一位图像块,压缩传真位序由解码器处理
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: [][2]int64 按偏移排序且不重叠的字节区间, error 字段或布局错误
func tiffBitOrderRanges(ctx context.Context, data []byte, layout TIFFLayout) ([][2]int64, error) {
if layout.fillOrder != 2 {
return nil, nil
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.Offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
compression := entries[259]
method := uint16(1)
if compression != nil {
if order.Uint16(compression[2:]) != 3 || order.Uint32(compression[4:]) != 1 {
return nil, fmt.Errorf("invalid TIFF compression field")
}
method = order.Uint16(compression[8:])
}
if method == 3 || method == 4 {
return nil, nil
}
if method != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for compression %d", method)
}
if entry := entries[258]; entry != nil {
if order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != layout.Samples {
return nil, fmt.Errorf("invalid TIFF sample depths")
}
for i := uint32(0); i < layout.Samples; i++ {
depth, err := tiffPlaneValue(data, order, entry, i)
if err != nil || depth != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for sample depth")
}
}
}
locations, lengths := entries[273], entries[279]
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
rows, err := value(278, ^uint32(0))
if err != nil || rows == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip")
}
expected := (uint64(layout.Height)-1)/uint64(rows) + 1
if entries[322] != nil {
locations, lengths = entries[324], entries[325]
width, err := value(322, 0)
if err != nil || width == 0 || width%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile width")
}
height, err := value(323, 0)
if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height")
}
expected = ((uint64(layout.Width)-1)/uint64(width) + 1) * ((uint64(layout.Height)-1)/uint64(height) + 1)
if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets")
}
}
if layout.Planar {
if expected > uint64(^uint32(0))/uint64(layout.Samples) {
return nil, fmt.Errorf("too many TIFF image blocks")
}
expected *= uint64(layout.Samples)
}
if locations == nil || lengths == nil {
return nil, fmt.Errorf("missing TIFF block arrays")
}
blocks := order.Uint32(locations[4:])
if blocks == 0 || uint64(blocks) != expected || order.Uint32(lengths[4:]) != blocks {
return nil, fmt.Errorf("invalid TIFF block count")
}
var ranges [][2]int64
for i := uint32(0); i < blocks; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
at, err := tiffPlaneValue(data, order, locations, i)
if err != nil {
return nil, err
}
size, err := tiffPlaneValue(data, order, lengths, i)
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF image block")
}
ranges = append(ranges, [2]int64{int64(at), int64(at) + int64(size)})
}
slices.SortFunc(ranges, func(a, b [2]int64) int { return cmp.Compare(a[0], b[0]) })
merged := ranges[:0]
for _, span := range ranges {
if len(merged) != 0 && span[0] <= merged[len(merged)-1][1] {
merged[len(merged)-1][1] = max(merged[len(merged)-1][1], span[1])
} else {
merged = append(merged, span)
}
}
return merged, nil
}
// decodeTIFFBitOrder 使用只读视图校正位序,不分配整幅编码的副本
// 入参: input 原始解码视图, ranges 需要反转位序的像素区间
// 返回: image.Image 解码图像, error 解码错误
func decodeTIFFBitOrder(input interface {
io.Reader
io.ReaderAt
}, ranges [][2]int64) (image.Image, error) {
if len(ranges) == 0 {
return tiff.Decode(input)
}
return tiff.Decode(&tiffBitOrderInput{source: input, ranges: ranges})
}
// Read 顺序读取并校正像素字节的位序
// 入参: data 接收缓冲
// 返回: int 读取字节数, error 读取错误
func (r *tiffBitOrderInput) Read(data []byte) (int, error) {
n, err := r.ReadAt(data, r.position)
r.position += int64(n)
return n, err
}
// ReadAt 按文件偏移读取,只反转实际读取到的像素字节
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取错误
func (r *tiffBitOrderInput) ReadAt(data []byte, offset int64) (int, error) {
n, err := r.source.ReadAt(data, offset)
index := sort.Search(len(r.ranges), func(i int) bool { return r.ranges[i][1] > offset })
for ; index < len(r.ranges) && r.ranges[index][0] < offset+int64(n); index++ {
span := r.ranges[index]
for i := max(int64(0), span[0]-offset); i < min(int64(n), span[1]-offset); i++ {
data[i] = bits.Reverse8(data[i])
}
}
return n, err
}
+185
View File
@@ -0,0 +1,185 @@
// 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 image
import (
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
"math/bits"
"github.com/xiaoqidun/pdfgo"
)
// tiffFaxImage 组合独立解码的传真条带或瓦片,保留紧凑的一位像素存储
type tiffFaxImage struct {
blocks []image.Image
bounds image.Rectangle
width int
height int
across int
}
// decodeTIFFFax 按TIFF传真选项解码各图像块,复用PDFGo的T.4与T.6解码器
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: image.Image 图像, error 字段、解码或取消错误
func decodeTIFFFax(ctx context.Context, data []byte, layout TIFFLayout) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if layout.Samples != 1 || layout.compression == 3 && layout.t4 & ^uint32(7) != 0 || layout.compression == 4 && layout.t6 & ^uint32(2) != 0 {
return nil, fmt.Errorf("invalid TIFF fax options")
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.Offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
for _, tag := range []uint16{258, 317, 339} {
v, err := value(tag, 1)
if err != nil || v != 1 || entries[tag] != nil && order.Uint16(entries[tag][2:]) != 3 {
return nil, fmt.Errorf("invalid TIFF fax sample field %d", tag)
}
}
photo, err := value(262, 2)
if err != nil || photo > 1 && photo != 4 || entries[338] != nil {
return nil, fmt.Errorf("invalid TIFF fax color model")
}
width := uint32(layout.Width)
height, err := value(278, ^uint32(0))
if err != nil || height == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip")
}
height = min(height, uint32(layout.Height))
locations, lengths := entries[273], entries[279]
tiled := entries[322] != nil
if tiled {
width, err = value(322, 0)
if err != nil || width == 0 || width%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile width")
}
height, err = value(323, 0)
if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height")
}
locations, lengths = entries[324], entries[325]
if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets")
}
}
if uint64(width) > uint64(int(^uint(0)>>1)) || uint64(height) > uint64(int(^uint(0)>>1)) || locations == nil || lengths == nil {
return nil, fmt.Errorf("invalid TIFF fax blocks")
}
across := (uint64(layout.Width)-1)/uint64(width) + 1
down := (uint64(layout.Height)-1)/uint64(height) + 1
blocks := across * down
if blocks != uint64(order.Uint32(locations[4:])) || blocks != uint64(order.Uint32(lengths[4:])) {
return nil, fmt.Errorf("invalid TIFF fax block count")
}
result := &tiffFaxImage{bounds: image.Rect(0, 0, layout.Width, layout.Height), width: int(width), height: int(height), across: int(across)}
for i := uint64(0); i < blocks; i++ {
if err := ctx.Err(); err != nil {
return nil, err
}
at, err := tiffPlaneValue(data, order, locations, uint32(i))
if err != nil {
return nil, err
}
size, err := tiffPlaneValue(data, order, lengths, uint32(i))
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF fax block")
}
encoded := data[uint64(at) : uint64(at)+uint64(size)]
if layout.fillOrder == 2 {
reversed := make([]byte, len(encoded))
for start := 0; start < len(encoded); {
if err := ctx.Err(); err != nil {
return nil, err
}
end := start + min(32768, len(encoded)-start)
for j := start; j < end; j++ {
reversed[j] = bits.Reverse8(encoded[j])
}
start = end
}
encoded = reversed
}
rows := height
if !tiled {
rows = min(height, uint32(uint64(layout.Height)-i*uint64(height)))
}
k := pdfgo.Integer(0)
if layout.compression == 4 {
k = -1
} else if layout.compression == 3 && layout.t4&1 != 0 {
k = 1
}
params := pdfgo.Dictionary{
"K": k, "Columns": pdfgo.Integer(width), "Rows": pdfgo.Integer(rows),
"EndOfBlock": pdfgo.Boolean(false), "EndOfLine": pdfgo.Boolean(layout.compression == 3),
"EncodedByteAlign": pdfgo.Boolean(layout.compression == 2), "BlackIs1": pdfgo.Boolean(photo == 1 || photo == 4),
}
object := &pdfgo.Image{Width: int(width), Height: int(rows), BitsPerComponent: 1, ColorSpace: pdfgo.Name("DeviceGray"), Stream: &pdfgo.Stream{Dictionary: pdfgo.Dictionary{"Filter": pdfgo.Name("CCITTFaxDecode"), "DecodeParms": params}, Data: encoded}}
pixels, err := object.DecodeSamplesContext(ctx)
if err != nil {
return nil, fmt.Errorf("TIFF fax block %d: %w", i, err)
}
result.blocks = append(result.blocks, pixels)
}
if err := ctx.Err(); err != nil {
return nil, err
}
return result, nil
}
// Bounds 返回主图显示区域,不包含边缘瓦片的填充像素
// 返回: image.Rectangle 图像区域
func (s *tiffFaxImage) Bounds() image.Rectangle { return s.bounds }
// ColorModel 返回二值图像使用的灰度颜色模型
// 返回: color.Model 灰度模型
func (s *tiffFaxImage) ColorModel() color.Model { return color.GrayModel }
// At 读取所属条带或瓦片中的像素,区域外返回黑色
// 入参: x 横向像素坐标, y 纵向像素坐标
// 返回: color.Color 像素颜色
func (s *tiffFaxImage) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.bounds) {
return color.Gray{}
}
block := s.blocks[(y/s.height)*s.across+x/s.width]
return block.At(x%s.width, y%s.height)
}
+85
View File
@@ -0,0 +1,85 @@
// 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 image
import (
"context"
"image"
"image/color"
)
// maskedTIFFImage 将独立透明蒙版叠加到主图,复用两个目录的只读像素
type maskedTIFFImage struct {
source, mask image.Image
bounds image.Rectangle
}
// DecodeTIFF 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: image.Image 图像, error 解码或取消错误
func DecodeTIFF(ctx context.Context, data []byte, layout TIFFLayout) (image.Image, error) {
source, err := decodeTIFFPixels(ctx, data, layout)
if err != nil || layout.Transparency == nil {
return source, err
}
mask, err := decodeTIFFPixels(ctx, data, *layout.Transparency)
if err != nil {
return nil, err
}
return &maskedTIFFImage{source: source, mask: mask, bounds: image.Rectangle{Max: layout.DisplaySize()}}, nil
}
// Bounds 返回主图和蒙版共同覆盖的像素区域
// 返回: image.Rectangle 显示区域
func (s *maskedTIFFImage) Bounds() image.Rectangle { return s.bounds }
// ColorModel 返回保留原图采样精度的非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *maskedTIFFImage) ColorModel() color.Model { return color.NRGBA64Model }
// NRGBA64At 按像素中心读取主图及蒙版,零位隐藏原图并保留隐藏颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 非预乘十六位颜色
func (s *maskedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.bounds) {
return color.NRGBA64{}
}
size := s.bounds.Size()
x, y = x-s.bounds.Min.X, y-s.bounds.Min.Y
b := s.source.Bounds()
sx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X))
sy := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y))
pixel := NRGBA64At(s.source, sx, sy)
b = s.mask.Bounds()
mx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X))
my := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y))
if NRGBA64At(s.mask, mx, my).R == 0 {
pixel.A = 0
}
return pixel
}
// At 返回非预乘颜色,区域外透明
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (s *maskedTIFFImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// RGBA64At 返回预乘十六位颜色,供采样器直接读取
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 预乘颜色
func (s *maskedTIFFImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBA64At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+797
View File
@@ -0,0 +1,797 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
"io"
"math"
)
// tiffPlaneInput 将单分量目录附加到只读视图,像素仍从原始编码读取
type tiffPlaneInput struct {
source, directory []byte
position int64
order binary.ByteOrder
ctx context.Context
}
// planarTIFFImage 组合颜色与Alpha分量,保留交错步长并按需映射调色板
type planarTIFFImage struct {
planes []image.Image
bounds image.Rectangle
depth uint32
packed [5]uint32
offsets [5]int
alpha uint32
gray bool
cmyk bool
dotRange [4][2]uint16
palette []color.NRGBA64
stride int
invert bool
predictorWidth int
}
// needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用
// 返回: bool 是否需要分量视图
func (layout TIFFLayout) needsSampleView() bool {
return layout.mask || layout.auxiliary || layout.photometric == 5 || layout.Planar && layout.Samples > 1 || layout.Samples == 2 || layout.depth < 8 && (layout.Samples > 1 || layout.depth > 1 || layout.predictor == 2)
}
// DecodedPixelBytes 估算每像素解码缓冲,低位深分量按逐位展开后的字节数计量
// 返回: uint64 每像素预算字节数
func (layout TIFFLayout) DecodedPixelBytes() uint64 {
samples := uint64(layout.Samples)
if layout.Planar {
if layout.photometric == 5 && samples > 4 {
return 20
}
return 16
}
if !layout.needsSampleView() {
return 8
}
bytes := uint64(1)
if layout.depth == 2 || layout.depth == 4 {
bytes = uint64(layout.depth)
} else if layout.depth > 8 {
bytes = 2
}
return max(uint64(8), samples*bytes)
}
// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局
// 返回: image.Image 图像, error 解码或取消错误
func decodeTIFFPixels(ctx context.Context, data []byte, layout TIFFLayout) (image.Image, error) {
profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil {
return nil, err
}
var ranges [][2]int64
if layout.compression < 2 || layout.compression > 4 {
ranges, err = tiffBitOrderRanges(ctx, data, layout)
if err != nil {
return nil, err
}
}
var pixels image.Image
if layout.compression >= 2 && layout.compression <= 4 {
pixels, err = decodeTIFFFax(ctx, data, layout)
if err != nil {
return nil, err
}
} else if layout.needsSampleView() {
result, directories, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return nil, err
}
for _, directory := range directories {
input := &tiffPlaneInput{source: data, directory: directory, order: binary.LittleEndian, ctx: ctx}
if data[0] == 'M' {
input.order = binary.BigEndian
}
plane, err := decodeTIFFBitOrder(input, ranges)
if err != nil {
return nil, err
}
if result.predictorWidth != 0 && (result.stride > 1 || result.packed[len(result.planes)] != 0) {
if err := restoreTIFFPrediction(ctx, plane, max(result.stride, 1), result.predictorWidth, result.packed[len(result.planes)]); err != nil {
return nil, err
}
}
result.planes = append(result.planes, plane)
}
pixels = result
} else {
input := newTIFFDirectoryInput(data, layout.Offset)
input.ctx = ctx
var err error
pixels, err = decodeTIFFBitOrder(input, ranges)
if err != nil {
return nil, err
}
}
if profile != nil {
pixels, err = applyTIFFColorProfile(ctx, pixels, profile)
if err != nil {
return nil, err
}
}
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
if layout.Orientation != 1 {
pixels = &orientedTIFFImage{source: pixels, TIFFLayout: layout}
}
return pixels, nil
}
// tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误
func tiffPlaneDirectories(ctx context.Context, data []byte, layout TIFFLayout) (*planarTIFFImage, [][]byte, error) {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
if uint64(len(data))+65536*12+8 > math.MaxUint32 {
return nil, nil, fmt.Errorf("TIFF plane directory offset exceeds format limit")
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.Offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
samples, err := value(277, 1)
if err != nil || samples < 1 || samples > math.MaxUint16 {
return nil, nil, fmt.Errorf("invalid TIFF plane count")
}
photo, err := value(262, math.MaxUint32)
if err != nil || photo > 5 {
return nil, nil, fmt.Errorf("unsupported TIFF planar color model")
}
compression, err := value(259, 1)
if err != nil {
return nil, nil, err
}
fill, err := value(266, 1)
if err != nil || fill != 1 && (fill != 2 || compression != 1 && (compression < 2 || compression > 4)) {
return nil, nil, fmt.Errorf("unsupported TIFF plane fill order")
}
predictor, err := value(317, 1)
if err != nil || predictor != 1 && predictor != 2 {
return nil, nil, fmt.Errorf("unsupported TIFF plane predictor")
}
result := &planarTIFFImage{bounds: image.Rect(0, 0, layout.Width, layout.Height), gray: photo < 2 || photo == 4, invert: photo == 0, cmyk: photo == 5}
if result.cmyk {
for _, tag := range []uint16{332, 334} {
want := uint32(1)
if tag == 334 {
want = 4
}
got, err := value(tag, want)
if err != nil || got != want || entries[tag] != nil && order.Uint16(entries[tag][2:]) != 3 {
return nil, nil, fmt.Errorf("unsupported TIFF ink configuration")
}
}
}
planes := samples
if !layout.Planar && samples > 1 {
result.stride = int(samples)
planes = 1
}
colors := uint32(3)
if result.cmyk {
colors = 4
} else if photo != 2 {
colors = 1
}
if samples != colors {
entry := entries[338]
if samples < colors || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples-colors {
return nil, nil, fmt.Errorf("invalid TIFF extra samples")
}
for i := uint32(0); i < samples-colors; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
alpha, err := tiffPlaneValue(data, order, entry, i)
if err != nil || alpha > 2 || alpha != 0 && result.alpha != 0 {
return nil, nil, fmt.Errorf("unsupported TIFF extra samples")
}
if alpha != 0 {
result.alpha, result.offsets[colors] = alpha, int(i)
}
}
} else if entries[338] != nil {
return nil, nil, fmt.Errorf("unexpected TIFF extra samples")
}
width, err := value(322, 0)
if err != nil {
return nil, nil, err
}
blocks := uint64(0)
locations, lengths := uint16(273), uint16(279)
if width != 0 {
height, err := value(323, 0)
if err != nil || height == 0 || width%16 != 0 || height%16 != 0 {
return nil, nil, fmt.Errorf("invalid TIFF tile dimensions")
}
blocks = ((uint64(layout.Width)-1)/uint64(width) + 1) * ((uint64(layout.Height)-1)/uint64(height) + 1)
locations, lengths = 324, 325
} else {
rows, err := value(278, math.MaxUint32)
if err != nil || rows == 0 {
return nil, nil, fmt.Errorf("invalid TIFF rows per strip")
}
blocks = (uint64(layout.Height)-1)/uint64(rows) + 1
}
if blocks > math.MaxUint32/uint64(planes) {
return nil, nil, fmt.Errorf("too many TIFF plane blocks")
}
for _, tag := range []uint16{locations, lengths} {
entry := entries[tag]
if entry == nil || uint64(order.Uint32(entry[4:])) != blocks*uint64(planes) || tag == 324 && order.Uint16(entry[2:]) != 4 {
return nil, nil, fmt.Errorf("invalid TIFF plane block count")
}
}
for i := uint32(0); i < uint32(blocks)*planes; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
at, err := tiffPlaneValue(data, order, entries[locations], i)
if err != nil {
return nil, nil, err
}
size, err := tiffPlaneValue(data, order, entries[lengths], i)
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, nil, fmt.Errorf("truncated TIFF plane block")
}
}
for _, tag := range []uint16{258, 339} {
if entry := entries[tag]; entry != nil && (order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples) {
return nil, nil, fmt.Errorf("invalid TIFF sample array")
}
}
var directories [][]byte
var firstDepth uint32
for plane := uint32(0); plane < samples; plane++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
slot := plane
visible := plane < colors || result.alpha != 0 && plane == colors+uint32(result.offsets[colors])
if !visible && result.stride == 0 {
continue
}
if plane >= colors {
slot = colors
}
depth := uint32(1)
if entry := entries[258]; entry != nil {
depth, err = tiffPlaneValue(data, order, entry, plane)
}
if err != nil || depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 16 {
return nil, nil, fmt.Errorf("unsupported TIFF plane depth")
}
if plane == 0 {
firstDepth = depth
} else if result.stride > 1 && depth != firstDepth {
return nil, nil, fmt.Errorf("unsupported TIFF mixed interleaved sample depths")
}
if !visible {
continue
}
result.depth = max(result.depth, depth)
if result.cmyk && plane < 4 {
low, high := uint32(0), uint32(1<<depth-1)
if entry := entries[336]; entry != nil {
n := order.Uint32(entry[4:])
if kind := order.Uint16(entry[2:]); kind != 1 && kind != 3 || n != 2 && n != 2*samples {
return nil, nil, fmt.Errorf("invalid TIFF dot range")
}
i := plane * 2
if n == 2 {
i = 0
}
if order.Uint16(entry[2:]) == 1 {
values := entry[8:]
if n > 4 {
at := uint64(order.Uint32(values))
if at+uint64(n) > uint64(len(data)) {
return nil, nil, fmt.Errorf("truncated TIFF dot range")
}
values = data[at : at+uint64(n)]
}
low, high = uint32(values[i]), uint32(values[i+1])
} else {
low, err = tiffPlaneValue(data, order, entry, i)
if err != nil {
return nil, nil, err
}
high, err = tiffPlaneValue(data, order, entry, i+1)
}
if err != nil || low == high || low > 1<<depth-1 || high > 1<<depth-1 {
return nil, nil, fmt.Errorf("invalid TIFF dot range")
}
}
result.dotRange[plane] = [2]uint16{uint16(low * 65535 / (1<<depth - 1)), uint16(high * 65535 / (1<<depth - 1))}
}
factor := uint64(max(result.stride, 1))
if depth == 1 && predictor == 2 {
result.packed[slot] = 1
}
if depth == 2 || depth == 4 {
result.packed[slot] = depth
factor *= uint64(depth)
if compression == 2 || compression == 3 || compression == 4 {
return nil, nil, fmt.Errorf("unsupported TIFF packed plane compression")
}
}
pixelBytes := factor
if depth > 8 {
pixelBytes *= 2
}
if uint64(layout.Width)*pixelBytes > uint64(math.MaxInt)/uint64(layout.Height) || uint64(layout.Width)*factor > math.MaxUint32 || uint64(width)*factor > math.MaxUint32 {
return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit")
}
if predictor == 2 && (result.stride > 1 || depth < 8) {
result.predictorWidth = layout.Width
if width != 0 {
result.predictorWidth = int(width)
}
}
if entry := entries[339]; entry != nil {
format, err := tiffPlaneValue(data, order, entry, plane)
if err != nil || format != 1 {
return nil, nil, fmt.Errorf("unsupported TIFF sample format")
}
}
if photo == 3 && plane == 0 {
entry := entries[320]
if depth > 8 || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != 3<<depth {
return nil, nil, fmt.Errorf("invalid TIFF color map")
}
n := uint32(1) << depth
result.palette = make([]color.NRGBA64, n)
for i := range n {
var components [3]uint16
for c := range components {
v, err := tiffPlaneValue(data, order, entry, uint32(c)*n+i)
if err != nil {
return nil, nil, err
}
components[c] = uint16(v)
if v%257 != 0 {
result.depth = 16
}
}
result.palette[i] = color.NRGBA64{R: components[0], G: components[1], B: components[2], A: 65535}
}
}
if result.stride > 1 && plane != 0 {
continue
}
directory := make([]byte, 2, 6+count*12)
for i := range count {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
entry := bytes.Clone(data[offset+2+i*12 : offset+14+i*12])
tag := order.Uint16(entry)
if tag == 338 || tag == 320 || tag >= 332 && tag <= 337 || tag == 34675 {
continue
}
scalar := uint32(0)
switch tag {
case 258:
scalar = depth
if result.packed[slot] != 0 {
scalar = 1
}
case 256, 322:
if factor != 1 {
original, err := tiffPlaneValue(data, order, entry, 0)
if err != nil {
return nil, nil, err
}
order.PutUint16(entry[2:], 4)
order.PutUint32(entry[4:], 1)
order.PutUint32(entry[8:], original*uint32(factor))
}
case 262:
scalar = 1
case 277, 284, 339:
scalar = 1
case 317:
if predictor == 2 && (result.stride > 1 || depth < 8) {
order.PutUint16(entry[8:], 1)
}
case locations, lengths:
entry, err = tiffPlaneArray(data, order, entry, uint32(blocks)*plane, uint32(blocks))
if err != nil {
return nil, nil, err
}
}
if tag == 258 || tag == 262 || tag == 277 || tag == 284 || tag == 339 {
order.PutUint16(entry[2:], 3)
order.PutUint32(entry[4:], 1)
clear(entry[8:])
order.PutUint16(entry[8:], uint16(scalar))
}
directory = append(directory, entry...)
}
order.PutUint16(directory, uint16((len(directory)-2)/12))
directories = append(directories, append(directory, 0, 0, 0, 0))
}
return result, directories, nil
}
// restoreTIFFPrediction 在解码缓冲内恢复水平差分,各行及瓦片独立计算
// 入参: ctx 取消上下文,可为nil, pixels 单通道解码视图, stride 每像素分量数, width 原始条带或瓦片宽度, packed 展开为一位采样的原位深,零表示未展开
// 返回: error 图像类型或取消错误
func restoreTIFFPrediction(ctx context.Context, pixels image.Image, stride, width int, packed uint32) error {
if ctx == nil {
ctx = context.Background()
}
var data []byte
var pitch, step int
switch pixels := pixels.(type) {
case *image.Gray:
data, pitch, step = pixels.Pix, pixels.Stride, max(int(packed), 1)
case *image.Gray16:
data, pitch, step = pixels.Pix, pixels.Stride, 2
default:
return fmt.Errorf("unsupported TIFF predictor image")
}
bounds := pixels.Bounds()
columns := bounds.Dx() / max(int(packed), 1)
block := width * stride
for y := range bounds.Dy() {
if err := ctx.Err(); err != nil {
return err
}
row := data[y*pitch : y*pitch+columns*step]
for start := 0; start < columns; {
end := start + min(block, columns-start)
for x := start + stride; x < end; {
if err := ctx.Err(); err != nil {
return err
}
limit := x + min(4096, end-x)
for ; x < limit; x++ {
at, previous := x*step, (x-stride)*step
if packed != 0 {
var current, prior uint16
for bit := range step {
current = current<<1 | uint16(row[at+bit]>>7)
prior = prior<<1 | uint16(row[previous+bit]>>7)
}
value := current + prior
for bit := range step {
row[at+bit] = byte(value>>uint(step-1-bit)&1) * 255
}
} else if step == 2 {
value := binary.BigEndian.Uint16(row[at:]) + binary.BigEndian.Uint16(row[previous:])
binary.BigEndian.PutUint16(row[at:], value)
} else {
row[at] += row[previous]
}
}
}
start = end
}
}
return ctx.Err()
}
// tiffPlaneValue 读取SHORT或LONG数组中的一项,校验完整数组范围
// 入参: data 原编码, order 字节序, entry 目录项, index 元素序号
// 返回: uint32 元素值, error 类型或越界错误
func tiffPlaneValue(data []byte, order binary.ByteOrder, entry []byte, index uint32) (uint32, error) {
kind, count := order.Uint16(entry[2:]), order.Uint32(entry[4:])
if kind != 3 && kind != 4 || index >= count {
return 0, fmt.Errorf("invalid TIFF plane array")
}
size := uint64(2)
if kind == 4 {
size = 4
}
values := entry[8:]
if size*uint64(count) > 4 {
at := uint64(order.Uint32(values))
if at+size*uint64(count) > uint64(len(data)) {
return 0, fmt.Errorf("truncated TIFF plane array")
}
values = data[at:]
}
if size == 2 {
return uint32(order.Uint16(values[uint64(index)*size:])), nil
}
return order.Uint32(values[uint64(index)*size:]), nil
}
// tiffPlaneArray 将块索引缩减为指定分量的范围,较长数组继续借用原编码
// 入参: data 原编码, order 字节序, entry 目录项副本, first 起始序号, count 元素数
// 返回: []byte 修改后的目录项, error 类型或越界错误
func tiffPlaneArray(data []byte, order binary.ByteOrder, entry []byte, first, count uint32) ([]byte, error) {
if _, err := tiffPlaneValue(data, order, entry, first+count-1); err != nil {
return nil, err
}
size := uint32(2)
if order.Uint16(entry[2:]) == 4 {
size = 4
}
if uint64(count)*uint64(size) <= 4 {
var values [4]byte
for i := uint32(0); i < count; i++ {
value, _ := tiffPlaneValue(data, order, entry, first+i)
if size == 2 {
order.PutUint16(values[i*size:], uint16(value))
} else {
order.PutUint32(values[i*size:], value)
}
}
copy(entry[8:], values[:])
} else {
order.PutUint32(entry[8:], order.Uint32(entry[8:])+first*size)
}
order.PutUint32(entry[4:], count)
return entry, nil
}
// Read 从组合视图顺序读取
// 入参: data 接收缓冲
// 返回: int 字节数, error 读取或取消错误
func (r *tiffPlaneInput) Read(data []byte) (int, error) {
n, err := r.ReadAt(data, r.position)
r.position += int64(n)
return n, err
}
// ReadAt 从原始编码或附加目录读取并替换首目录指针
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 字节数, error 读取或取消错误
func (r *tiffPlaneInput) ReadAt(data []byte, offset int64) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
if offset < 0 {
return 0, fmt.Errorf("negative TIFF read offset")
}
start := int64(len(r.source))
directory := start + start%2
written := 0
if offset < start {
written = copy(data, r.source[offset:])
}
if offset+int64(written) == start && start != directory && written < len(data) {
data[written] = 0
written++
}
if at := offset + int64(written) - directory; at >= 0 && at < int64(len(r.directory)) {
written += copy(data[written:], r.directory[at:])
}
var pointer [4]byte
r.order.PutUint32(pointer[:], uint32(directory))
for i, value := range pointer {
if at := int64(4+i) - offset; at >= 0 && at < int64(written) {
data[at] = value
}
}
if written != len(data) {
return written, io.EOF
}
return written, nil
}
// Bounds 返回存储坐标范围
// 返回: image.Rectangle 图像范围
func (p *planarTIFFImage) Bounds() image.Rectangle { return p.bounds }
// ColorModel 按透明度类型和最高分量精度返回颜色模型
// 返回: color.Model 颜色模型
func (p *planarTIFFImage) ColorModel() color.Model {
if p.gray && p.alpha == 0 {
if p.depth > 8 {
return color.Gray16Model
}
return color.GrayModel
}
if p.alpha == 1 {
if p.depth > 8 {
return color.RGBA64Model
}
return color.RGBAModel
}
if p.depth > 8 {
return color.NRGBA64Model
}
return color.NRGBAModel
}
// components 直接读取灰度分量,保留原始透明度语义
// 入参: x 横坐标, y 纵坐标
// 返回: [4]uint16 红、绿、蓝和透明度分量
func (p *planarTIFFImage) components(x, y int) [4]uint16 {
if !image.Pt(x, y).In(p.bounds) {
return [4]uint16{}
}
if p.cmyk {
ink := p.inksAt(x, y)
maximum := uint32(65535)
if p.alpha == 1 {
maximum = uint32(ink[4])
if maximum == 0 {
return [4]uint16{}
}
}
black := maximum - min(uint32(ink[3]), maximum)
var values [4]uint16
for i := range 3 {
values[i] = uint16((maximum - min(uint32(ink[i]), maximum)) * black / maximum)
}
values[3] = ink[4]
return values
}
values := [4]uint16{0, 0, 0, 65535}
count := 3
if p.gray || len(p.palette) != 0 {
count = 1
}
if p.alpha != 0 {
count++
}
for i := range count {
value := p.sampleAt(i, x, y)
if i == 0 && p.invert {
value = 65535 - value
}
if len(p.palette) != 0 && i == 0 {
c := p.palette[uint32(value)*uint32(len(p.palette)-1)/65535]
values = [4]uint16{c.R, c.G, c.B, c.A}
} else if p.gray && i == 0 {
values[0], values[1], values[2] = value, value, value
} else if p.gray || len(p.palette) != 0 {
values[3] = value
} else {
values[i] = value
}
}
if p.alpha == 1 && values[3] == 0 {
return [4]uint16{}
}
return values
}
// sampleAt 读取一个分量并扩展为十六位,交错布局复用同一像素缓冲
// 入参: i 分量序号, x 横坐标, y 纵坐标
// 返回: uint16 归一化采样值
func (p *planarTIFFImage) sampleAt(i, x, y int) uint16 {
plane, sx := p.planes[0], x
if p.stride > 1 {
sx = x*p.stride + i + p.offsets[i]
} else {
plane = p.planes[i]
}
switch plane := plane.(type) {
case *image.Gray:
if bits := p.packed[i]; bits != 0 {
var value uint16
for bit := range int(bits) {
value = value<<1 | uint16(plane.GrayAt(sx*int(bits)+bit, y).Y>>7)
}
return value * (65535 / uint16(1<<bits-1))
}
return uint16(plane.GrayAt(sx, y).Y) * 257
case *image.Gray16:
return plane.Gray16At(sx, y).Y
default:
v, _, _, _ := plane.At(sx, y).RGBA()
return uint16(v)
}
}
// inksAt 按网点范围读取CMYK覆盖量,范围外采样按端点截断,透明度独立归一化
// 入参: x 横坐标, y 纵坐标
// 返回: [5]uint16 青、品红、黄、黑及透明度
func (p *planarTIFFImage) inksAt(x, y int) [5]uint16 {
values := [5]uint16{0, 0, 0, 0, 65535}
for i := range 4 {
value := int64(p.sampleAt(i, x, y))
low, high := int64(p.dotRange[i][0]), int64(p.dotRange[i][1])
values[i] = uint16(max(0, min(65535, (value-low)*65535/(high-low))))
}
if p.alpha != 0 {
values[4] = p.sampleAt(4, x, y)
}
return values
}
// At 返回组合后的颜色,关联透明度保留预乘语义
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (p *planarTIFFImage) At(x, y int) color.Color {
values := p.components(x, y)
if p.alpha == 1 {
return color.RGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
return color.NRGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
// NRGBA64At 读取独立透明度颜色,保留非预乘分量中的隐藏颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 像素颜色
func (p *planarTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
values := p.components(x, y)
if p.alpha == 1 && values[3] != 65535 {
if values[3] == 0 {
return color.NRGBA64{}
}
for i := range 3 {
values[i] = uint16(uint32(values[i]) * 65535 / uint32(values[3]))
}
}
return color.NRGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
// RGBA64At 返回预乘十六位颜色,不分配临时颜色对象
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 像素颜色
func (p *planarTIFFImage) RGBA64At(x, y int) color.RGBA64 {
values := p.components(x, y)
if p.alpha != 1 && values[3] != 65535 {
for i := range 3 {
values[i] = uint16(uint32(values[i]) * uint32(values[3]) / 65535)
}
}
return color.RGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
+221
View File
@@ -0,0 +1,221 @@
// 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 image
import (
"context"
"fmt"
"image"
"image/color"
"math"
"strings"
"github.com/xiaoqidun/pdfgo"
)
// parseTIFFColorProfile 校验ICC源模型,保留原有模型错误并复用浮点颜色变换
// 入参: kind 源颜色模型, data ICC编码
// 返回: *pdfgo.ColorSpace 颜色变换, error 模型或配置错误
func parseTIFFColorProfile(kind string, data []byte) (*pdfgo.ColorSpace, error) {
model := map[string]string{"GRAY": "GRAY", "RGB": "RGB ", "CMYK": "CMYK"}[strings.ToUpper(kind)]
if len(data) < 132 || model == "" || string(data[16:20]) != model {
return nil, fmt.Errorf("ICC source model does not match OFD color space %q", kind)
}
return pdfgo.NewICCColorSpace(data)
}
// readTIFFColorProfile 读取主图的ICC配置,校验颜色模型和数据范围
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: *pdfgo.ColorSpace 颜色配置,未声明时为nil, error 配置或取消错误
func readTIFFColorProfile(ctx context.Context, data []byte, layout TIFFLayout) (*pdfgo.ColorSpace, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if layout.profileSize == 0 {
return nil, nil
}
at, size := uint64(layout.profileOffset), uint64(layout.profileSize)
if at+size > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF color profile")
}
kind := ""
switch layout.photometric {
case 0, 1:
kind = "GRAY"
case 2, 3:
kind = "RGB"
case 5:
kind = "CMYK"
default:
return nil, fmt.Errorf("unsupported TIFF profile color model")
}
profile, err := parseTIFFColorProfile(kind, data[at:at+size])
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
return profile, err
}
// applyTIFFColorProfile 将源颜色转换为十六位sRGB,保留独立透明度和隐藏颜色
// 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置
// 返回: image.Image 独立透明度图像, error 颜色变换或取消错误
func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
bounds := source.Bounds()
if bounds.Dx() <= 0 || bounds.Dy() <= 0 || bounds.Dx() > math.MaxInt/8/bounds.Dy() {
return nil, fmt.Errorf("TIFF profile image dimensions exceed capacity")
}
cmyk, _ := source.(*planarTIFFImage)
if cmyk != nil && !cmyk.cmyk {
cmyk = nil
}
model := source.ColorModel()
premultiplied := model == color.RGBAModel || model == color.RGBA64Model
table := newTIFFProfileTable(source)
result := image.NewNRGBA64(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
slot := -1
if len(table) != 0 {
index, alpha := tiffProfileSample(source, x, y, len(table))
slot = index
if pixel := table[index]; pixel.A != 0 {
pixel.A = alpha
result.SetNRGBA64(x, y, pixel)
continue
}
}
var values [4]float64
var alpha uint16
if cmyk != nil {
ink := cmyk.inksAt(x, y)
alpha = ink[4]
if cmyk.alpha == 1 && alpha == 0 {
continue
}
maximum := float64(65535)
if cmyk.alpha == 1 {
maximum = float64(alpha)
}
for i := range values {
values[i] = min(1, float64(ink[i])/maximum)
}
} else if premultiplied {
var pixel color.RGBA64
if native, ok := source.(image.RGBA64Image); ok {
pixel = native.RGBA64At(x, y)
} else {
pixel = color.RGBA64Model.Convert(source.At(x, y)).(color.RGBA64)
}
alpha = pixel.A
if alpha == 0 {
continue
}
for i, value := range [3]uint16{pixel.R, pixel.G, pixel.B} {
values[i] = min(1, float64(value)/float64(alpha))
}
} else {
pixel := NRGBA64At(source, x, y)
alpha = pixel.A
values = [4]float64{float64(pixel.R) / 65535, float64(pixel.G) / 65535, float64(pixel.B) / 65535}
}
rgb, err := profile.RGB(values[:profile.Components()], profile.DefaultIntent())
if err != nil {
return nil, err
}
var channels [3]uint16
for i, value := range rgb {
channels[i] = uint16(math.Round(max(0, min(1, value)) * 65535))
}
if slot >= 0 {
table[slot] = color.NRGBA64{R: channels[0], G: channels[1], B: channels[2], A: 65535}
}
result.SetNRGBA64(x, y, color.NRGBA64{R: channels[0], G: channels[1], B: channels[2], A: alpha})
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return result, nil
}
// newTIFFProfileTable 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换
// 入参: source 已解码且尺寸有效的图像
// 返回: []color.NRGBA64 颜色表,Alpha非零表示已有结果,不适用时为nil
func newTIFFProfileTable(source image.Image) []color.NRGBA64 {
count := 0
switch source := source.(type) {
case *image.Gray:
count = 256
case *image.Gray16:
count = 65536
case *image.Paletted:
count = len(source.Palette)
case *planarTIFFImage:
if source.alpha != 1 {
if len(source.palette) != 0 {
count = len(source.palette)
} else if source.gray {
count = 256
if source.packed[0] != 0 {
count = 1 << source.packed[0]
} else if _, ok := source.planes[0].(*image.Gray16); ok {
count = 65536
}
}
}
}
if count == 0 || count >= source.Bounds().Dx()*source.Bounds().Dy() {
return nil
}
return make([]color.NRGBA64, count)
}
// tiffProfileSample 读取查找表索引与独立透明度,颜色键不包含透明度
// 入参: source 适用查找表的图像, x 横坐标, y 纵坐标, count 颜色表长度
// 返回: int 颜色索引, uint16 透明度
func tiffProfileSample(source image.Image, x, y, count int) (int, uint16) {
switch source := source.(type) {
case *image.Gray:
return int(source.GrayAt(x, y).Y), 65535
case *image.Gray16:
return int(source.Gray16At(x, y).Y), 65535
case *image.Paletted:
index := source.ColorIndexAt(x, y)
_, _, _, alpha := source.Palette[index].RGBA()
return int(index), uint16(alpha)
case *planarTIFFImage:
index := int(uint32(source.sampleAt(0, x, y)) * uint32(count-1) / 65535)
alpha := uint16(65535)
if source.alpha != 0 {
alpha = source.sampleAt(1, x, y)
}
return index, alpha
}
return 0, 0
}