mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +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
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
`)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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]}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user