diff --git a/ofdgo_font_cff.go b/internal/base/font/cff.go similarity index 97% rename from ofdgo_font_cff.go rename to internal/base/font/cff.go index 7e404cd..97f0e43 100644 --- a/ofdgo_font_cff.go +++ b/internal/base/font/cff.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -30,16 +30,16 @@ import ( // cffDict 使用float64存储所有数值,以统一处理整数和实数 type cffDict map[int][]float64 -// wrapCFFToOTF 将CFF裸数据包装为OpenType字体格式 +// WrapCFFToOTF 将CFF裸数据包装为OpenType字体格式 // 入参: cffData CFF字体数据 // 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息 -func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { +func WrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { numGlyphs, err := parseCFFAndCountGlyphs(cffData) if err != nil { return nil, nil, err } mapping := getCmapFromCFF(cffData, int(numGlyphs)) - sanitized, err := sanitizeCFF(cffData) + sanitized, err := SanitizeCFF(cffData) if err != nil { return nil, nil, err } @@ -70,10 +70,10 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { tables["maxp"] = buildCFFMaxpTable(uint16(numGlyphs)) tables["OS/2"] = buildOS2Table() tables["name"] = buildNameTable() - tables["post"] = buildPostTable() + tables["post"] = BuildPostTable() tables["hmtx"] = buildHmtxTable(widths) - tables["cmap"] = buildCmapTable(uint16(numGlyphs), mapping) - data, err := serializeOTF(tables) + tables["cmap"] = BuildCmapTable(uint16(numGlyphs), mapping) + data, err := SerializeOTF(tables) return data, mapping, err } @@ -81,13 +81,13 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { // 入参: data CFF字体数据 // 返回: []byte 标准化后的CFF数据, error 错误信息 func normalizeCFFCharstrings(data []byte) ([]byte, error) { - return normalizeCFFCharstringsAt(data, 0) + return NormalizeCFFCharstringsAt(data, 0) } -// normalizeCFFCharstringsAt 统一CFF设计坐标,现有OpenType字体使用head表的设计单位 +// NormalizeCFFCharstringsAt 统一CFF设计坐标,现有OpenType字体使用head表的设计单位 // 入参: data CFF字体数据, unitsPerEm 设计单位,0时由字体矩阵确定 // 返回: []byte 标准化后的CFF数据, error 错误信息 -func normalizeCFFCharstringsAt(data []byte, unitsPerEm uint16) ([]byte, error) { +func NormalizeCFFCharstringsAt(data []byte, unitsPerEm uint16) ([]byte, error) { if len(data) < 4 || int(data[2]) >= len(data) { return nil, fmt.Errorf("invalid CFF header") } @@ -214,10 +214,10 @@ func replaceCFFCharstrings(data []byte, dict cffDict, topStart, topSize, charSta return result } -// sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix +// SanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix // 入参: data 原始CFF数据 // 返回: []byte 清洗后的CFF数据, error 错误信息 -func sanitizeCFF(data []byte) ([]byte, error) { +func SanitizeCFF(data []byte) ([]byte, error) { if !isBareCFFData(data) { return nil, fmt.Errorf("invalid CFF header") } @@ -397,9 +397,9 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict return nil, fmt.Errorf("invalid CFF FDArray count") } fonts := make([]type2Font, fdCount) - var type1Fonts []*type1Program + var type1Fonts []*Type1Program if kind == 1 { - type1Fonts = make([]*type1Program, fdCount) + type1Fonts = make([]*Type1Program, fdCount) } privateDicts := make([]cffDict, fdCount) matrices := make([][6]float64, fdCount) @@ -1079,7 +1079,7 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { if gid == 0 { continue } - mapping[packedGlyphRune(uint16(gid))] = uint16(gid) + mapping[PackedGlyphRune(uint16(gid))] = uint16(gid) if r, ok := adobeGB1CIDToUnicode(cid); ok { mapping[r] = uint16(gid) } @@ -1091,7 +1091,7 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { if gid == 0 { continue } - mapping[packedGlyphRune(uint16(gid))] = uint16(gid) + mapping[PackedGlyphRune(uint16(gid))] = uint16(gid) } return mapping } @@ -1113,18 +1113,18 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { r = getUnicodeFromName(name) } if r == 0 { - r = packedGlyphRune(uint16(gid)) + r = PackedGlyphRune(uint16(gid)) } - mapping[packedGlyphRune(uint16(gid))] = uint16(gid) + mapping[PackedGlyphRune(uint16(gid))] = uint16(gid) mapping[r] = uint16(gid) } return mapping } -// getCFFCIDRuneMap 获取CID到包装字体字符的映射 +// GetCFFCIDRuneMap 获取CID到包装字体字符的映射 // 入参: data CFF或OpenType字体数据 // 返回: map[uint16]rune CID映射 -func getCFFCIDRuneMap(data []byte) map[uint16]rune { +func GetCFFCIDRuneMap(data []byte) map[uint16]rune { cffData := getCFFData(data) if cffData == nil { return nil @@ -1143,7 +1143,7 @@ func getCFFCIDRuneMap(data []byte) map[uint16]rune { } gidRunes := make(map[uint16]rune) for run, gid := range mapping { - if run != packedGlyphRune(gid) { + if run != PackedGlyphRune(gid) { gidRunes[gid] = run } } diff --git a/ofdgo_font_cff2.go b/internal/base/font/cff2.go similarity index 96% rename from ofdgo_font_cff2.go rename to internal/base/font/cff2.go index 78bb04d..e0fd771 100644 --- a/ofdgo_font_cff2.go +++ b/internal/base/font/cff2.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -25,10 +25,10 @@ import ( "github.com/go-text/typesetting/font/opentype/tables" ) -// defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓 +// DefaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓 // 入参: data 独立字体数据 // 返回: []byte 字体数据,非CFF2原样返回, error 解析或编码错误 -func defaultCFF2Font(data []byte) (result []byte, err error) { +func DefaultCFF2Font(data []byte) (result []byte, err error) { if len(data) < 12 || string(data[:4]) != "OTTO" { return data, nil } @@ -42,7 +42,7 @@ func defaultCFF2Font(data []byte) (result []byte, err error) { if !found { return data, nil } - table, err := fontFileTables(data, 0) + table, err := FileTables(data, 0) if err != nil || table["CFF2"] == nil { return data, err } @@ -106,7 +106,7 @@ func defaultCFF2Font(data []byte) (result []byte, err error) { for _, tag := range []string{"CFF2", "fvar", "avar", "STAT", "HVAR", "VVAR", "MVAR", "DSIG"} { delete(table, tag) } - return serializeOTF(table) + return SerializeOTF(table) } // cffOutlineProgram 将默认设计轮廓编码为无子程序的Type2字形 @@ -165,7 +165,7 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) { if units < 16 || units > 16384 { return nil, fmt.Errorf("invalid CFF design units") } - name := fontFaceInfo(table["name"], 0).PostScriptName + name := FaceInfo(table["name"], 0).PostScriptName if name == "" { name = "OpenTypeInstance" } diff --git a/ofdgo_font_cff_cid_subset.go b/internal/base/font/cff_cid_subset.go similarity index 99% rename from ofdgo_font_cff_cid_subset.go rename to internal/base/font/cff_cid_subset.go index a181471..f28dcb8 100644 --- a/ofdgo_font_cff_cid_subset.go +++ b/internal/base/font/cff_cid_subset.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" diff --git a/ofdgo_font_cff_compact.go b/internal/base/font/cff_compact.go similarity index 98% rename from ofdgo_font_cff_compact.go rename to internal/base/font/cff_compact.go index 4f8ec4e..34d788c 100644 --- a/ofdgo_font_cff_compact.go +++ b/internal/base/font/cff_compact.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -112,7 +112,7 @@ func compactCFFGlyphs(data []byte, keep map[uint16]bool) ([]byte, []uint16, erro // compactCFFTables 重建按字形编号索引的度量和字符映射,不保留旧编号的布局表 // 入参: font 原字体, cff 紧凑CFF数据, ids 新编号对应的原编号 // 返回: []byte 独立OpenType字体, error 封装错误 -func compactCFFTables(font *sfntFont, cff []byte, ids []uint16) ([]byte, 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 { @@ -133,7 +133,7 @@ func compactCFFTables(font *sfntFont, cff []byte, ids []uint16) ([]byte, error) } } tables["CFF "], tables["hmtx"] = cff, hmtx - tables["cmap"] = buildCmapTable(uint16(len(ids)), mapping) + 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 { @@ -151,7 +151,7 @@ func compactCFFTables(font *sfntFont, cff []byte, ids []uint16) ([]byte, error) return nil, err } } - return serializeOTF(tables) + return SerializeOTF(tables) } // compactCFFVerticalOrigins 重排CFF字体的逐字形纵向原点,保留默认原点 diff --git a/ofdgo_font_cff_names.go b/internal/base/font/cff_names.go similarity index 99% rename from ofdgo_font_cff_names.go rename to internal/base/font/cff_names.go index af17850..1ef3765 100644 --- a/ofdgo_font_cff_names.go +++ b/internal/base/font/cff_names.go @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font -import "strings" +import ( + "strings" +) // cffExpertCharset 按CFF规范附录C保存Expert字符集的SID顺序 var cffExpertCharset = [...]uint16{ diff --git a/ofdgo_font_cff_subset.go b/internal/base/font/cff_subset.go similarity index 98% rename from ofdgo_font_cff_subset.go rename to internal/base/font/cff_subset.go index d840b0c..c9b96e3 100644 --- a/ofdgo_font_cff_subset.go +++ b/internal/base/font/cff_subset.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -23,7 +23,7 @@ import ( // subsetCFFFont 裁剪CFF字体,保留组合组件和度量,按需重排字形 // 入参: sfnt 已解析字体, glyphs 使用的字形, preserve 是否保留原字形编号 // 返回: []byte 字体子集, error 裁剪错误 -func subsetCFFFont(sfnt *sfntFont, glyphs []uint16, preserve bool) ([]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() { @@ -48,11 +48,11 @@ func subsetCFFFont(sfnt *sfntFont, glyphs []uint16, preserve bool) ([]byte, erro mapping[char] = gid } } - tables["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) + tables["cmap"] = BuildCmapTable(sfnt.NumGlyphs(), mapping) for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} { delete(tables, tag) } - return serializeOTF(tables) + return SerializeOTF(tables) } // subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号 diff --git a/ofdgo_font_cff_type1.go b/internal/base/font/cff_type1.go similarity index 94% rename from ofdgo_font_cff_type1.go rename to internal/base/font/cff_type1.go index c1a2599..eaea161 100644 --- a/ofdgo_font_cff_type1.go +++ b/internal/base/font/cff_type1.go @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font -import "fmt" +import ( + "fmt" +) // cffCharstringType 读取CFF字形程序类型,缺省为Type2 // 入参: dict 顶层字典 @@ -32,13 +34,13 @@ func cffCharstringType(dict cffDict) (int, error) { // readCFFType1Program 读取Type1字形及无偏置局部子程序,保留源字形顺序 // 入参: data CFF字体, chars 字形程序, private 私有字典范围, cid 是否为CID字体 -// 返回: *type1Program 执行资源, error 字符集或私有字典错误 -func readCFFType1Program(data []byte, chars [][]byte, private []float64, cid bool) (*type1Program, error) { +// 返回: *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} + program := &Type1Program{subrs: resources.locals, lenIV: -1} if cid { return program, nil } diff --git a/ofdgo_font_cmap.go b/internal/base/font/cmap.go similarity index 96% rename from ofdgo_font_cmap.go rename to internal/base/font/cmap.go index c6d3369..bb72ae2 100644 --- a/ofdgo_font_cmap.go +++ b/internal/base/font/cmap.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -31,10 +31,10 @@ type cmapSegment struct { offset uint16 } -// normalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号 +// NormalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号 // 入参: data 字符映射表 // 返回: []byte 等价字符映射表, error 子表错误 -func normalizeCmap(data []byte) ([]byte, error) { +func NormalizeCmap(data []byte) ([]byte, error) { if len(data) < 4 { return data, nil } @@ -76,7 +76,7 @@ func normalizeCmap(data []byte) ([]byte, error) { if err != nil { return nil, err } - table := buildCmapTable(0, mapping) + table := BuildCmapTable(0, mapping) if language > 65535 { table = buildCmapTableFormat12(0, mapping) } @@ -255,17 +255,17 @@ func parseCmapFormat2(data []byte) (map[rune]uint16, error) { return mapping, nil } -// packedGlyphRune 获取包装字体字符 +// PackedGlyphRune 获取包装字体字符 // 入参: gid 字形ID // 返回: rune 包装字体字符 -func packedGlyphRune(gid uint16) rune { +func PackedGlyphRune(gid uint16) rune { return 0xF0000 + rune(gid) } -// parseCmapMappings 解析cmap字符映射 +// ParseCmapMappings 解析cmap字符映射 // 入参: data cmap表数据 // 返回: map[rune]uint16 字符到字形映射 -func parseCmapMappings(data []byte) map[rune]uint16 { +func ParseCmapMappings(data []byte) map[rune]uint16 { if len(data) < 4 { return nil } @@ -427,18 +427,18 @@ func parseCmapFormat12(data []byte, result map[rune]uint16) { } } -// addPackedGlyphMapping 添加字形ID私有映射 +// AddPackedGlyphMapping 添加字形ID私有映射 // 入参: mapping 字符映射, numGlyphs 字形数量 -func addPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) { +func AddPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) { for i := uint16(0); i < numGlyphs; i++ { - mapping[packedGlyphRune(i)] = i + mapping[PackedGlyphRune(i)] = i } } -// buildCmapTable 构建格式4或12字符映射,省略缺字映射 +// BuildCmapTable 构建格式4或12字符映射,省略缺字映射 // 入参: numGlyphs 字形数量, mapping 字符映射 // 返回: []byte cmap表数据 -func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte { +func BuildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte { if shouldBuildCmapFormat12(mapping) { return buildCmapTableFormat12(numGlyphs, mapping) } diff --git a/ofdgo_font_cmap_variation.go b/internal/base/font/cmap_variation.go similarity index 99% rename from ofdgo_font_cmap_variation.go rename to internal/base/font/cmap_variation.go index e917b36..acaf9b6 100644 --- a/ofdgo_font_cmap_variation.go +++ b/internal/base/font/cmap_variation.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "encoding/binary" @@ -211,7 +211,7 @@ func cmapVariationRune(value [3]byte) rune { // 入参: count 字形总数, mapping 字符映射, variations 格式14子表 // 返回: []byte 字符映射表 func buildCmapWithVariations(count uint16, mapping map[rune]uint16, variations []byte) []byte { - base := buildCmapTable(count, mapping) + base := BuildCmapTable(count, mapping) if len(variations) == 0 { return base } diff --git a/internal/base/font/file.go b/internal/base/font/file.go new file mode 100644 index 0000000..511cfbd --- /dev/null +++ b/internal/base/font/file.go @@ -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 +} diff --git a/ofdgo_font_glyf.go b/internal/base/font/glyf.go similarity index 89% rename from ofdgo_font_glyf.go rename to internal/base/font/glyf.go index c933091..675b4a7 100644 --- a/ofdgo_font_glyf.go +++ b/internal/base/font/glyf.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "encoding/binary" @@ -25,9 +25,9 @@ import ( "github.com/go-text/typesetting/font/opentype" ) -// sfntOutliner 缓存字体程序并提取独立字形轮廓 -type sfntOutliner struct { - font *sfntFont +// Outliner 缓存字体程序并提取独立字形轮廓 +type Outliner struct { + Font *SFNT cffOnce sync.Once cff *cff.CFF cffErr error @@ -46,15 +46,16 @@ type ttGlyph struct { warning error } -// path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理 +// Path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理 // 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放 // 返回: error 字形解析错误 -func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error { - if !f.font.IsTrueType { +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 "]) + data, err := SanitizeCFF(f.Font.Tables["CFF "]) if err == nil { - data, err = normalizeCFFCharstringsAt(data, f.font.UnitsPerEm()) + data, err = NormalizeCFFCharstringsAt(data, f.Font.UnitsPerEm()) } if err != nil { f.cffErr = err @@ -107,23 +108,23 @@ func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error first = last + 1 continue } - point := func(i int) Point { - return Point{X: float64(points[i].x) * scale / 64, Y: float64(points[i].y) * scale / 64} + coordinate := func(i int) point { + return point{X: float64(points[i].x) * scale / 64, Y: float64(points[i].y) * scale / 64} } - start, end := point(first), point(last) + 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} + start = point{X: (start.X + end.X) / 2, Y: (start.Y + end.Y) / 2} } path.MoveTo(start.X, start.Y) - var control Point + var control point pending := false for i := begin; i <= stop; i++ { - p := point(i) + p := coordinate(i) if points[i].on { if pending { path.QuadTo(control.X, control.Y, p.X, p.Y) @@ -150,14 +151,14 @@ func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error // glyph 解码简单或复合字形并执行对应字节码,不改写字体资源 // 入参: id 字形编号, active 递归访问集合 // 返回: ttGlyph 字形轮廓, error 字形错误 -func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) { +func (f *Outliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) { var result ttGlyph - if id >= f.font.NumGlyphs() || active[id] { + 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) + data, err := trueTypeGlyphData(f.Font.Tables, id) if err != nil { return result, err } @@ -189,14 +190,14 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) if len(program) == 0 { return result, nil } - outlineOnly, err := ttOutlineOnly(f.font.Tables, program) + 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()) }) + 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 @@ -225,13 +226,13 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) // phantomPoints 按水平和垂直度量生成字形末尾的四个虚拟点 // 入参: id 字形编号, data 完整字形头 // 返回: [4]ttPoint 左右边距与上下原点 -func (f *sfntOutliner) phantomPoints(id uint16, data []byte) [4]ttPoint { - left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.sideBearing(id, false)) +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)) + 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}} + 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 判断栈参数、指令引用或控制流错误是否允许保留执行前轮廓 @@ -274,7 +275,7 @@ func trueTypeGlyphData(tables map[string][]byte, id uint16) ([]byte, error) { // recordGlyphWarning 记录字形恢复提示,允许并发提取轮廓 // 入参: id 字形编号, warning 恢复原因 -func (f *sfntOutliner) recordGlyphWarning(id uint16, warning error) { +func (f *Outliner) recordGlyphWarning(id uint16, warning error) { f.mu.Lock() defer f.mu.Unlock() if f.warnings == nil { @@ -283,10 +284,10 @@ func (f *sfntOutliner) recordGlyphWarning(id uint16, warning error) { f.warnings[id] = warning } -// glyphWarning 返回字形恢复为执行前轮廓的原因 +// GlyphWarning 返回字形恢复为执行前轮廓的原因 // 入参: id 字形编号 // 返回: error 恢复原因,无恢复时为空 -func (f *sfntOutliner) glyphWarning(id uint16) error { +func (f *Outliner) GlyphWarning(id uint16) error { f.mu.Lock() defer f.mu.Unlock() return f.warnings[id] @@ -382,7 +383,7 @@ func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) { // composite 合成分量变换和点匹配,保留分量顺序及父级字节码 // 入参: glyph 父字形编号, data 复合字形数据, phantoms 父字形虚拟点, active 递归访问集合 // 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误 -func (f *sfntOutliner) composite(glyph uint16, data []byte, phantoms [4]ttPoint, active map[uint16]bool) (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 @@ -475,7 +476,7 @@ func (f *sfntOutliner) composite(glyph uint16, data []byte, phantoms [4]ttPoint, } else { if total < 0 { var err error - total, err = trueTypePointCount(f.font.Tables, glyph, map[uint16]int{}, map[uint16]bool{}) + total, err = trueTypePointCount(f.Font.Tables, glyph, map[uint16]int{}, map[uint16]bool{}) if err != nil { return result, nil, err } diff --git a/ofdgo_font_hint.go b/internal/base/font/hint.go similarity index 99% rename from ofdgo_font_hint.go rename to internal/base/font/hint.go index ace3506..c5b301c 100644 --- a/ofdgo_font_hint.go +++ b/internal/base/font/hint.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "encoding/binary" diff --git a/ofdgo_font_loca.go b/internal/base/font/loca.go similarity index 90% rename from ofdgo_font_loca.go rename to internal/base/font/loca.go index 2bc5605..3a21902 100644 --- a/ofdgo_font_loca.go +++ b/internal/base/font/loca.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -22,19 +22,19 @@ import ( "slices" ) -// errTTInvalidLocations 表示字形索引不满足递增偏移要求 -var errTTInvalidLocations = errors.New("invalid TrueType glyph locations") +// ErrTTInvalidLocations 表示字形索引不满足递增偏移要求 +var ErrTTInvalidLocations = errors.New("invalid TrueType glyph locations") -// normalizeTrueTypeLocations 验证物理字形边界后按原编号重排,不改变空字形和分量引用 +// NormalizeTrueTypeLocations 验证物理字形边界后按原编号重排,不改变空字形和分量引用 // 入参: tables 独占字体表目录,表内容只读 // 返回: bool 是否重建索引, error 无法可靠恢复的索引或轮廓错误 -func normalizeTrueTypeLocations(tables map[string][]byte) (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) + return false, fmt.Errorf("%w: missing header", ErrTTInvalidLocations) } count := int(binary.BigEndian.Uint16(maxp[4:])) width := 2 @@ -43,10 +43,10 @@ func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) { case 1: width = 4 default: - return false, fmt.Errorf("%w: format", errTTInvalidLocations) + return false, fmt.Errorf("%w: format", ErrTTInvalidLocations) } if count == 0 || len(loca) < (count+1)*width { - return false, fmt.Errorf("%w: truncated index", errTTInvalidLocations) + return false, fmt.Errorf("%w: truncated index", ErrTTInvalidLocations) } location := func(i int) uint64 { if width == 2 { @@ -58,7 +58,7 @@ func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) { for i := range count + 1 { offset := location(i) if offset > uint64(len(glyf)) { - return false, fmt.Errorf("%w: offset %d outside table", errTTInvalidLocations, i) + return false, fmt.Errorf("%w: offset %d outside table", ErrTTInvalidLocations, i) } ordered = ordered && previous <= offset previous = offset @@ -83,31 +83,31 @@ func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) { continue } if _, duplicate := bodies[start]; duplicate { - return false, fmt.Errorf("%w: ambiguous shared offset %d", errTTInvalidLocations, start) + 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) + 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) + 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) + 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) + 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) + return fmt.Errorf("%w: cyclic component %d", ErrTTInvalidLocations, id) } if state[id] == 2 { return nil diff --git a/internal/base/font/match.go b/internal/base/font/match.go new file mode 100644 index 0000000..39cd1ea --- /dev/null +++ b/internal/base/font/match.go @@ -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) +} diff --git a/internal/base/font/metrics.go b/internal/base/font/metrics.go new file mode 100644 index 0000000..2d1d5fc --- /dev/null +++ b/internal/base/font/metrics.go @@ -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() +} diff --git a/internal/base/font/name.go b/internal/base/font/name.go new file mode 100644 index 0000000..a4ace62 --- /dev/null +++ b/internal/base/font/name.go @@ -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 +} diff --git a/internal/base/font/repair.go b/internal/base/font/repair.go new file mode 100644 index 0000000..b28e0c1 --- /dev/null +++ b/internal/base/font/repair.go @@ -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 +} diff --git a/ofdgo_font_sfnt.go b/internal/base/font/sfnt.go similarity index 85% rename from ofdgo_font_sfnt.go rename to internal/base/font/sfnt.go index 9c88a18..1cfb680 100644 --- a/ofdgo_font_sfnt.go +++ b/internal/base/font/sfnt.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "encoding/binary" @@ -24,8 +24,8 @@ import ( "github.com/go-text/typesetting/font/opentype/tables" ) -// sfntFont 保存独立字体表和基础度量,不包含绘图库对象 -type sfntFont struct { +// SFNT 保存独立字体表和基础度量,不包含绘图库对象 +type SFNT struct { Tables map[string][]byte IsTrueType, IsCFF bool cmaps []sfntCmap @@ -40,26 +40,26 @@ type sfntCmap struct { mapping map[rune]uint16 } -// parseSFNTFont 校验独立字体的必需表、度量范围及字符映射 +// ParseSFNT 校验独立字体的必需表、度量范围及字符映射 // 入参: data 独立SFNT数据 -// 返回: *sfntFont 字体, error 格式错误 -func parseSFNTFont(data []byte) (*sfntFont, error) { - table, err := fontFileTables(data, 0) +// 返回: *SFNT 字体, error 格式错误 +func ParseSFNT(data []byte) (*SFNT, error) { + table, err := FileTables(data, 0) if err != nil { return nil, err } - return parseSFNTTables(table) + return ParseSFNTTables(table) } -// parseSFNTTables 校验字体表并解析度量和字符映射,借用表数据且不修改其内容 +// ParseSFNTTables 校验字体表并解析度量和字符映射,借用表数据且不修改其内容 // 入参: table 字体表 -// 返回: *sfntFont 字体, error 格式错误 -func parseSFNTTables(table map[string][]byte) (*sfntFont, error) { +// 返回: *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 := &sfntFont{Tables: maps.Clone(table), IsTrueType: table["glyf"] != nil, IsCFF: table["CFF "] != nil} + 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) { @@ -106,7 +106,7 @@ func validSFNTMetrics(data []byte, count, metrics uint16) bool { // 入参: data cmap表 // 返回: []sfntCmap 编码记录, error 表结构错误 func readSFNTCmaps(data []byte) ([]sfntCmap, error) { - data, err := normalizeCmap(data) + data, err := NormalizeCmap(data) if err != nil { return nil, err } @@ -157,16 +157,26 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) { // UnitsPerEm 返回字体设计单位 // 返回: uint16 每em单位数 -func (f *sfntFont) UnitsPerEm() uint16 { return f.units } +func (f *SFNT) UnitsPerEm() uint16 { return f.units } // NumGlyphs 返回字形总数 // 返回: uint16 字形数 -func (f *sfntFont) NumGlyphs() uint16 { return f.count } +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 *sfntFont) GlyphIndex(char rune) uint16 { +func (f *SFNT) GlyphIndex(char rune) uint16 { for _, cmap := range f.cmaps { if gid := cmap.mapping[char]; gid != 0 { return gid @@ -178,7 +188,7 @@ func (f *sfntFont) GlyphIndex(char rune) uint16 { // glyphCharacters 返回指向指定字形的全部字符 // 入参: gid 字形编号 // 返回: []rune 排序后的字符 -func (f *sfntFont) glyphCharacters(gid uint16) []rune { +func (f *SFNT) glyphCharacters(gid uint16) []rune { f.reverseOnce.Do(func() { f.reverse = make(map[uint16][]rune) seen := make(map[rune]bool) @@ -200,7 +210,7 @@ func (f *sfntFont) glyphCharacters(gid uint16) []rune { // GlyphAdvance 返回水平字宽 // 入参: gid 字形编号 // 返回: uint16 字宽 -func (f *sfntFont) GlyphAdvance(gid uint16) uint16 { +func (f *SFNT) GlyphAdvance(gid uint16) uint16 { if gid >= f.count { return 0 } @@ -210,7 +220,7 @@ func (f *sfntFont) GlyphAdvance(gid uint16) uint16 { // GlyphVerticalAdvance 返回纵向字距,无垂直度量时使用em高度 // 入参: gid 字形编号 // 返回: uint16 纵向字距 -func (f *sfntFont) GlyphVerticalAdvance(gid uint16) uint16 { +func (f *SFNT) GlyphVerticalAdvance(gid uint16) uint16 { if f.vertical == 0 { return f.units } @@ -223,7 +233,7 @@ func (f *sfntFont) GlyphVerticalAdvance(gid uint16) uint16 { // sideBearing 返回指定方向的字形边距 // 入参: gid 字形编号, vertical 是否读取顶部边距 // 返回: int16 边距 -func (f *sfntFont) sideBearing(gid uint16, vertical bool) 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 @@ -240,7 +250,7 @@ func (f *sfntFont) sideBearing(gid uint16, vertical bool) int16 { // VerticalMetrics 依据OS/2排版标志选择行度量,缺失时使用hhea // 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙 -func (f *sfntFont) VerticalMetrics() (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 { @@ -261,12 +271,12 @@ func (f *sfntFont) VerticalMetrics() (uint16, uint16, uint16) { // Write 保留原始表数据并重新计算字体校验和 // 返回: []byte 独立字体 -func (f *sfntFont) Write() []byte { data, _ := serializeOTF(f.Tables); return data } +func (f *SFNT) Write() []byte { data, _ := SerializeOTF(f.Tables); return data } // glyphDependencies 批量收集复合字形依赖,校验引用、记录长度和嵌套深度 // 入参: glyphs 字形编号 // 返回: []uint16 依赖字形, error 字形错误 -func (f *sfntFont) glyphDependencies(glyphs ...uint16) ([]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 diff --git a/internal/base/font/subset.go b/internal/base/font/subset.go new file mode 100644 index 0000000..2b458fe --- /dev/null +++ b/internal/base/font/subset.go @@ -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) +} diff --git a/ofdgo_font_symbol.go b/internal/base/font/symbol.go similarity index 88% rename from ofdgo_font_symbol.go rename to internal/base/font/symbol.go index 3c64a71..eadb5e3 100644 --- a/ofdgo_font_symbol.go +++ b/internal/base/font/symbol.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "context" @@ -23,14 +23,14 @@ import ( "github.com/xiaoqidun/pdfgo" ) -// symbolFontProgram 为标准符号字体补齐Unicode映射,保留字形编号和轮廓 +// SymbolProgram 为标准符号字体补齐Unicode映射,保留字形编号和轮廓 // 入参: ctx 取消上下文, data 独立字体数据, name 标准字体名,为空时从字体名称表识别 // 返回: []byte 字体数据, map[string]uint16 标准字形编号,不适用时为空, error 映射或取消错误 -func symbolFontProgram(ctx context.Context, data []byte, name string) ([]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 := fontFileTables(data, 0) + tables, err := FileTables(data, 0) if err != nil { return data, nil, nil } @@ -57,7 +57,7 @@ func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, m if err != nil { return nil, nil, err } - original := parseCmapMappings(tables["cmap"]) + original := ParseCmapMappings(tables["cmap"]) mapping := make(map[rune]uint16) names := make(map[string]uint16) for _, glyph := range glyphs { @@ -90,8 +90,8 @@ func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, m if !legacy || len(mapping) == 0 { return data, names, nil } - tables["cmap"] = buildCmapTable(0, mapping) - program, err := serializeOTF(tables) + tables["cmap"] = BuildCmapTable(0, mapping) + program, err := SerializeOTF(tables) return program, names, err } @@ -99,8 +99,8 @@ func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, m // 入参: data 名称表数据 // 返回: string 标准字体名,无法识别时为空 func symbolFontName(data []byte) string { - for _, name := range fontNamesFromTable(data) { - switch fontNormalizeName(name) { + for _, name := range NamesFromTable(data) { + switch NormalizeName(name) { case "symbol", "symbolmt", "symbolregular": return "Symbol" case "zapfdingbats", "zapfdingbatsregular", "itczapfdingbats": diff --git a/ofdgo_font_ttf.go b/internal/base/font/ttf.go similarity index 93% rename from ofdgo_font_ttf.go rename to internal/base/font/ttf.go index 0e6b877..3d44d87 100644 --- a/ofdgo_font_ttf.go +++ b/internal/base/font/ttf.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -24,7 +24,7 @@ import ( // 返回: 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) + data, err := DefaultCFF2Font(data) if err != nil { return false, original, nil, false, err } @@ -58,7 +58,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin } if existingTables["CFF "] != nil { isCFFSfnt = true - sanitized, err := sanitizeCFF(existingTables["CFF "]) + sanitized, err := SanitizeCFF(existingTables["CFF "]) if err != nil { return false, data, nil, false, err } @@ -66,7 +66,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin if head := existingTables["head"]; len(head) >= 20 { units = binary.BigEndian.Uint16(head[18:20]) } - normalized, err := normalizeCFFCharstringsAt(sanitized, units) + normalized, err := NormalizeCFFCharstringsAt(sanitized, units) if err != nil { return false, data, nil, false, err } @@ -75,7 +75,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin malformedDirectory = true } } - cmap, err := normalizeCmap(existingTables["cmap"]) + cmap, err := NormalizeCmap(existingTables["cmap"]) if err != nil { return false, data, nil, false, err } @@ -193,12 +193,12 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin descender = int16(binary.BigEndian.Uint16(hhea[6:8])) } if missingOS2 { - newTables["OS/2"] = buildOS2TableWithMetrics(ascender, descender) + newTables["OS/2"] = BuildOS2TableWithMetrics(ascender, descender) } var mapping map[rune]uint16 if fixCmap { if !missingCmap { - mapping = parseCmapMappings(newTables["cmap"]) + mapping = ParseCmapMappings(newTables["cmap"]) } if len(mapping) == 0 && isCFFSfnt { mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs)) @@ -206,16 +206,16 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin if len(mapping) == 0 { mapping = make(map[rune]uint16) } - addPackedGlyphMapping(mapping, numGlyphs) - newTables["cmap"] = buildCmapTable(numGlyphs, mapping) + AddPackedGlyphMapping(mapping, numGlyphs) + newTables["cmap"] = BuildCmapTable(numGlyphs, mapping) } if missingName && fixName { newTables["name"] = buildNameTable() } if missingPost { - newTables["post"] = buildPostTable() + newTables["post"] = BuildPostTable() } - finalData, err := serializeOTF(newTables) + finalData, err := SerializeOTF(newTables) if err != nil { return false, data, nil, missingCmap, err } diff --git a/ofdgo_font_type1.go b/internal/base/font/type1.go similarity index 94% rename from ofdgo_font_type1.go rename to internal/base/font/type1.go index ec721d9..363fcd1 100644 --- a/ofdgo_font_type1.go +++ b/internal/base/font/type1.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -41,8 +41,8 @@ var type1StandardNames = func() [256]string { return names }() -// type1Program 保存解密后的Type1字形与子程序 -type type1Program struct { +// Type1Program 保存解密后的Type1字形与子程序 +type Type1Program struct { glyphs []type1Glyph subrs [][]byte lenIV int @@ -63,7 +63,7 @@ type type1Scanner struct { // type1Outline 保存字形指令的操作数栈及轮廓位置 type type1Outline struct { - program *type1Program + program *Type1Program steps *int active map[string]bool args []float64 @@ -84,24 +84,24 @@ type type1Outline struct { path bytes.Buffer } -// parseType1Program 读取Type1字体的字形程序及子程序 +// ParseType1Program 读取Type1字体的字形程序及子程序 // 入参: data PFA或PFB字体数据 -// 返回: type1Program 字形和子程序, error 错误信息 -func parseType1Program(data []byte) (type1Program, error) { +// 返回: Type1Program 字形和子程序, error 错误信息 +func ParseType1Program(data []byte) (Type1Program, error) { var err error data, err = flattenType1(data) if err != nil { - return type1Program{}, err + return Type1Program{}, err } matrix, start, err := readType1Header(data) if err != nil { - return type1Program{}, err + return Type1Program{}, err } for start < len(data) && type1Space(data[start]) { start++ } if start == len(data) { - return type1Program{}, fmt.Errorf("empty Type1 eexec data") + return Type1Program{}, fmt.Errorf("empty Type1 eexec data") } encrypted := data[start:] if type1HexEncoded(encrypted) { @@ -121,15 +121,15 @@ func parseType1Program(data []byte) (type1Program, error) { } 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) + return Type1Program{}, fmt.Errorf("invalid Type1 eexec encoding: %w", err) } } if len(encrypted) < 4 { - return type1Program{}, fmt.Errorf("invalid Type1 eexec data") + return Type1Program{}, fmt.Errorf("invalid Type1 eexec data") } decrypted := type1Decrypt(encrypted, 55665)[4:] scanner := type1Scanner{data: decrypted} - program := type1Program{lenIV: 4, matrix: matrix} + program := Type1Program{lenIV: 4, matrix: matrix} section := "" for { token := scanner.next() @@ -140,7 +140,7 @@ func parseType1Program(data []byte) (type1Program, error) { case "/lenIV": value, err := strconv.Atoi(scanner.next()) if err != nil || value < -1 { - return type1Program{}, fmt.Errorf("invalid Type1 lenIV") + return Type1Program{}, fmt.Errorf("invalid Type1 lenIV") } program.lenIV = value case "/Subrs": @@ -157,18 +157,18 @@ func parseType1Program(data []byte) (type1Program, error) { } index, err := strconv.Atoi(scanner.next()) if err != nil || index < 0 || index > len(decrypted) { - return type1Program{}, fmt.Errorf("invalid Type1 subroutine index") + 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") + return Type1Program{}, fmt.Errorf("invalid Type1 subroutine length") } if !type1BinaryStart(scanner.next()) { - return type1Program{}, fmt.Errorf("missing Type1 subroutine data") + return Type1Program{}, fmt.Errorf("missing Type1 subroutine data") } raw, err := scanner.binary(length) if err != nil { - return type1Program{}, err + return Type1Program{}, err } for len(program.subrs) <= index { program.subrs = append(program.subrs, nil) @@ -185,28 +185,28 @@ func parseType1Program(data []byte) (type1Program, error) { continue } if !type1BinaryStart(scanner.next()) { - return type1Program{}, fmt.Errorf("missing Type1 glyph data") + return Type1Program{}, fmt.Errorf("missing Type1 glyph data") } raw, err := scanner.binary(length) if err != nil { - return type1Program{}, err + 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") + 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 + 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 Type1Program{}, fmt.Errorf("Type1 glyph %s: %w", program.glyphs[index].name, err) } } return program, nil @@ -426,7 +426,7 @@ func type1BinaryStart(token string) bool { // outline 将Type1字形指令转换为保留三次曲线的Type2程序 // 入参: glyph 字形数据 // 返回: []byte Type2字形程序, error 错误信息 -func (p *type1Program) outline(glyph type1Glyph) ([]byte, 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 { @@ -840,10 +840,10 @@ func encodeType2Number(target *bytes.Buffer, value float64) error { return nil } -// toCFF 将Type1字形转换为保持三次轮廓的CFF字体 +// ToCFF 将Type1字形转换为保持三次轮廓的CFF字体 // 入参: name 字体名称 // 返回: []byte CFF数据, error 错误信息 -func (p *type1Program) toCFF(name string) ([]byte, error) { +func (p *Type1Program) ToCFF(name string) ([]byte, error) { if len(p.glyphs) > math.MaxUint16 || name == "" { return nil, fmt.Errorf("invalid Type1 font identity") } @@ -907,9 +907,9 @@ func (p *type1Program) toCFF(name string) ([]byte, error) { return data, nil } -// glyphIDs 返回Type1名称到转换后字形编号的对应关系 +// GlyphIDs 返回Type1名称到转换后字形编号的对应关系 // 返回: map[string]uint16 字形编号 -func (p *type1Program) glyphIDs() map[string]uint16 { +func (p *Type1Program) GlyphIDs() map[string]uint16 { ids := map[string]uint16{".notdef": 0} next := uint16(1) for _, glyph := range p.glyphs { diff --git a/ofdgo_font_type2.go b/internal/base/font/type2.go similarity index 99% rename from ofdgo_font_type2.go rename to internal/base/font/type2.go index 0912831..4fcbc82 100644 --- a/ofdgo_font_type2.go +++ b/internal/base/font/type2.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" diff --git a/ofdgo_font_type2_fd.go b/internal/base/font/type2_fd.go similarity index 99% rename from ofdgo_font_type2_fd.go rename to internal/base/font/type2_fd.go index 62d866c..a7183ad 100644 --- a/ofdgo_font_type2_fd.go +++ b/internal/base/font/type2_fd.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "encoding/binary" diff --git a/ofdgo_font_type2_matrix.go b/internal/base/font/type2_matrix.go similarity index 99% rename from ofdgo_font_type2_matrix.go rename to internal/base/font/type2_matrix.go index 4a81812..0377dc2 100644 --- a/ofdgo_font_type2_matrix.go +++ b/internal/base/font/type2_matrix.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "fmt" diff --git a/ofdgo_font_type2_stack.go b/internal/base/font/type2_stack.go similarity index 99% rename from ofdgo_font_type2_stack.go rename to internal/base/font/type2_stack.go index e113e3b..5a0fd36 100644 --- a/ofdgo_font_type2_stack.go +++ b/internal/base/font/type2_stack.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "fmt" diff --git a/internal/base/font/usage.go b/internal/base/font/usage.go new file mode 100644 index 0000000..4254e4c --- /dev/null +++ b/internal/base/font/usage.go @@ -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)} +} diff --git a/ofdgo_font_util.go b/internal/base/font/util.go similarity index 92% rename from ofdgo_font_util.go rename to internal/base/font/util.go index 6d70749..e77d62c 100644 --- a/ofdgo_font_util.go +++ b/internal/base/font/util.go @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" "encoding/binary" "fmt" + "math" "sort" ) @@ -31,11 +32,18 @@ type otfTableRecord struct { data []byte } -// extractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和 +// 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 := fontFileTables(data, index) +func ExtractCollectionFont(data []byte, index int) ([]byte, error) { + tables, err := FileTables(data, index) if err != nil { return nil, err } @@ -44,14 +52,14 @@ func extractCollectionFont(data []byte, index int) ([]byte, error) { } tables["head"] = bytes.Clone(tables["head"]) clear(tables["head"][8:12]) - return serializeOTF(tables) + return SerializeOTF(tables) } -// fontFileTables 读取单字体或集合指定项的表目录,不复制表数据 +// FileTables 读取单字体或集合指定项的表目录,不复制表数据 // 入参: data 字体数据, index 字体索引 // 返回: map[string][]byte 字体表, error 错误信息 -func fontFileTables(data []byte, index int) (map[string][]byte, error) { - count, err := fontFileCount(data) +func FileTables(data []byte, index int) (map[string][]byte, error) { + count, err := FileCount(data) if err != nil { return nil, err } @@ -190,13 +198,13 @@ func buildTrueTypeMaxpTable(numGlyphs uint16) []byte { // buildOS2Table 构建OS/2表 (使用默认Metrics) // 返回: []byte OS/2表数据 func buildOS2Table() []byte { - return buildOS2TableWithMetrics(800, -200) + return BuildOS2TableWithMetrics(800, -200) } -// buildOS2TableWithMetrics 构建OS/2表 +// BuildOS2TableWithMetrics 构建OS/2表 // 入参: ascender 上升部, descender 下降部 // 返回: []byte OS/2表数据 -func buildOS2TableWithMetrics(ascender, descender int16) []byte { +func BuildOS2TableWithMetrics(ascender, descender int16) []byte { os2 := new(bytes.Buffer) binary.Write(os2, binary.BigEndian, uint16(3)) binary.Write(os2, binary.BigEndian, int16(500)) @@ -252,9 +260,9 @@ func buildNameTable() []byte { return buf.Bytes() } -// buildPostTable 构建post表 (版本3.0, 无字形名称) +// BuildPostTable 构建post表 (版本3.0, 无字形名称) // 返回: []byte post表数据 -func buildPostTable() []byte { +func BuildPostTable() []byte { buf := new(bytes.Buffer) binary.Write(buf, binary.BigEndian, uint32(0x00030000)) binary.Write(buf, binary.BigEndian, uint32(0)) @@ -280,10 +288,10 @@ func buildHmtxTable(widths []uint16) []byte { return buf.Bytes() } -// serializeOTF 序列化OpenType字体结构 +// SerializeOTF 序列化OpenType字体结构 // 入参: tables 表数据映射 // 返回: []byte 完整字体数据, error 错误信息 -func serializeOTF(tables map[string][]byte) ([]byte, error) { +func SerializeOTF(tables map[string][]byte) ([]byte, error) { numTables := uint16(len(tables)) entrySelector := 0 for 1<<(entrySelector+1) <= int(numTables) { diff --git a/ofdgo_font_variable.go b/internal/base/font/variable.go similarity index 88% rename from ofdgo_font_variable.go rename to internal/base/font/variable.go index 1a54e87..e0bd511 100644 --- a/ofdgo_font_variable.go +++ b/internal/base/font/variable.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "encoding/binary" @@ -24,7 +24,7 @@ import ( // variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构 // 入参: sfnt 已解析字体 // 返回: bool 是否具备可裁剪的变化数据 -func variableSFNTSubsettable(sfnt *sfntFont) 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 @@ -38,30 +38,30 @@ func variableSFNTSubsettable(sfnt *sfntFont) bool { // subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据 // 入参: sfnt 已解析字体, glyphs 使用的字形 // 返回: []byte 字体子集, error 字形或变化表错误 -func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { +func subsetVariableSFNTFont(sfnt *SFNT, glyphs []uint16) ([]byte, error) { if !variableSFNTSubsettable(sfnt) { return nil, fmt.Errorf("invalid TrueType variation tables") } - usage := newEditorFontUsage() - usage.glyphs[0] = true + 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 + usage.Glyphs[gid] = true for _, char := range sfnt.glyphCharacters(gid) { - usage.chars[char] = true + usage.Characters[char] = true } } - dependencies, err := sfnt.glyphDependencies(slices.Collect(maps.Keys(usage.glyphs))...) + dependencies, err := sfnt.glyphDependencies(slices.Collect(maps.Keys(usage.Glyphs))...) if err != nil { return nil, err } for _, dependency := range dependencies { - usage.glyphs[dependency] = true + usage.Glyphs[dependency] = true } - mapping := make(map[rune]uint16, len(usage.chars)) - for char := range usage.chars { + mapping := make(map[rune]uint16, len(usage.Characters)) + for char := range usage.Characters { mapping[char] = sfnt.GlyphIndex(char) } table := make(map[string][]byte) @@ -70,18 +70,18 @@ func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { table[tag] = data } } - table["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) - static, err := serializeOTF(table) + table["cmap"] = BuildCmapTable(sfnt.NumGlyphs(), mapping) + static, err := SerializeOTF(table) if err != nil { return nil, err } - if subset := subsetSourceSFNTFont(static, usage); subset != nil { - table, err = fontFileTables(subset, 0) + 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) + table["gvar"], err = subsetGlyphVariations(sfnt.Tables["gvar"], usage.Glyphs) if err != nil { return nil, err } @@ -90,7 +90,7 @@ func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { table[tag] = data } } - return serializeOTF(table) + return SerializeOTF(table) } // subsetGlyphVariations 按原字形编号重建gvar偏移表,保留共享元组及使用字形的完整变化数据 diff --git a/ofdgo_font_woff.go b/internal/base/font/woff.go similarity index 96% rename from ofdgo_font_woff.go rename to internal/base/font/woff.go index c22427a..c7dc049 100644 --- a/ofdgo_font_woff.go +++ b/internal/base/font/woff.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package font import ( "bytes" @@ -40,10 +40,10 @@ var woff2Tags = [...]string{ "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill", } -// decodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理 +// DecodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理 // 入参: data WOFF2文件 // 返回: []byte OpenType数据, bool 是否已处理, error 封装错误 -func decodeWOFF2(data []byte) ([]byte, 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") } @@ -131,7 +131,7 @@ func decodeWOFF2(data []byte) ([]byte, bool, error) { size := int(sizes[i]) table[tag], body = body[:size], body[size:] } - decoded, err := serializeOTF(table) + decoded, err := SerializeOTF(table) return decoded, true, err } @@ -157,10 +157,10 @@ func readWOFF2Base128(data []byte, pos *int) (uint32, error) { return 0, fmt.Errorf("invalid WOFF2 integer") } -// decodeWOFF 展开WOFF表数据,不解析或更改字体轮廓 +// DecodeWOFF 展开WOFF表数据,不解析或更改字体轮廓 // 入参: data WOFF文件 // 返回: []byte OpenType数据, error 封装错误 -func decodeWOFF(data []byte) ([]byte, 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") } @@ -211,7 +211,7 @@ func decodeWOFF(data []byte) ([]byte, error) { if err := validateWOFFBlocks(data, end, 24); err != nil { return nil, err } - return serializeOTF(table) + return SerializeOTF(table) } // validateWOFFBlocks 校验附加数据块的边界及对齐,字体元数据不写入输出 diff --git a/ofdgo_image_bmp.go b/internal/base/image/bmp.go similarity index 87% rename from ofdgo_image_bmp.go rename to internal/base/image/bmp.go index 176ede1..62f17c5 100644 --- a/ofdgo_image_bmp.go +++ b/internal/base/image/bmp.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "bufio" @@ -27,21 +27,21 @@ import ( type bmpColorMask struct { mask uint32 shift uint - max uint32 + Max uint32 } -// decodeImageConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素 +// DecodeConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素 // 入参: input 图片输入流 // 返回: image.Config 图片配置, string 图片格式, error 头部错误 -func decodeImageConfig(input io.Reader) (image.Config, string, error) { +func DecodeConfig(input io.Reader) (image.Config, string, error) { buffer := bufio.NewReader(input) prefix, _ := buffer.Peek(34) - if isTIFFData(prefix) { + if IsTIFF(prefix) { layout, err := readTIFFLayout(buffer) if err != nil { return image.Config{}, "tiff", err } - size := layout.displaySize() + size := layout.DisplaySize() return image.Config{Width: size.X, Height: size.Y}, "tiff", nil } if len(prefix) == 34 && string(prefix[:2]) == "BM" { @@ -71,13 +71,13 @@ 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)) + return uint8(uint64((value&m.mask)>>m.shift) * 255 / uint64(m.Max)) } -// bmpColorMasks 读取BMP真彩色通道布局,校验位域连续性、重叠及像素范围 +// BMPColorMasks 读取BMP真彩色通道布局,校验位域连续性、重叠及像素范围 // 入参: data 已校验头部的BMP数据, offset 像素偏移, bpp 像素位数, compression 编码方式 // 返回: [4]bmpColorMask 红绿蓝和透明度通道, error 格式错误 -func bmpColorMasks(data []byte, offset int, bpp uint16, compression uint32) ([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") @@ -118,7 +118,7 @@ func bmpColorMasks(data []byte, offset int, bpp uint16, compression uint32) ([4] 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} + result[i] = bmpColorMask{mask: mask, shift: shift, Max: maximum} used |= mask } return result, nil diff --git a/internal/base/image/encoded.go b/internal/base/image/encoded.go new file mode 100644 index 0000000..e9d26f2 --- /dev/null +++ b/internal/base/image/encoded.go @@ -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 +} diff --git a/internal/base/image/format.go b/internal/base/image/format.go new file mode 100644 index 0000000..4c39914 --- /dev/null +++ b/internal/base/image/format.go @@ -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 + } +} diff --git a/internal/base/image/image.go b/internal/base/image/image.go new file mode 100644 index 0000000..de3f605 --- /dev/null +++ b/internal/base/image/image.go @@ -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 +} diff --git a/internal/base/image/input.go b/internal/base/image/input.go new file mode 100644 index 0000000..e55cba9 --- /dev/null +++ b/internal/base/image/input.go @@ -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) +} diff --git a/ofdgo_image_metadata.go b/internal/base/image/metadata.go similarity index 84% rename from ofdgo_image_metadata.go rename to internal/base/image/metadata.go index e13a1a4..6d04707 100644 --- a/ofdgo_image_metadata.go +++ b/internal/base/image/metadata.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "bytes" @@ -22,18 +22,18 @@ import ( "hash/crc32" ) -// imagePageInfo 图片方向与像素密度,不改变原始编码 -type imagePageInfo struct { - orientation int - xdpi, ydpi float64 +// PageInfo 图片方向与像素密度,不改变原始编码 +type PageInfo struct { + Orientation int + XDPI, YDPI float64 } -// imagePageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消 +// PageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消 // TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF // 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI -// 返回: imagePageInfo 方向与横纵DPI, error 元数据或取消错误 -func imagePageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (imagePageInfo, error) { - info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: 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 } @@ -41,20 +41,20 @@ func imagePageMetadataContext(ctx context.Context, data []byte, format string, d var directory uint32 scaleX, scaleY := 1.0, 1.0 if format == "tiff" { - layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) + layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data)) if err != nil { return info, err } - directory = layout.offset + directory = layout.Offset exif = data - original, display := layout.size(), layout.displaySize() + 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 + info.XDPI, info.YDPI = float64(x)*0.0254, float64(y)*0.0254 } } if format == "png" { @@ -77,9 +77,9 @@ func imagePageMetadataContext(ctx context.Context, data []byte, format string, d 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 + info.XDPI, info.YDPI = x*0.0254, y*0.0254 } else if payload[8] == 0 { - info.ydpi = dpi * y / x + info.YDPI = dpi * y / x } } } else if kind == "eXIf" { @@ -125,11 +125,11 @@ func imagePageMetadataContext(ctx context.Context, data []byte, format string, d if x > 0 && y > 0 { switch payload[7] { case 0: - info.ydpi = dpi * y / x + info.YDPI = dpi * y / x case 1: - info.xdpi, info.ydpi = x, y + info.XDPI, info.YDPI = x, y case 2: - info.xdpi, info.ydpi = x*2.54, y*2.54 + info.XDPI, info.YDPI = x*2.54, y*2.54 } } } else if marker == 0xe1 && bytes.HasPrefix(payload, []byte("Exif\x00\x00")) { @@ -143,18 +143,18 @@ func imagePageMetadataContext(ctx context.Context, data []byte, format string, d if err != nil { return info, err } - info.orientation = orientation + info.Orientation = orientation if (format == "jpeg" || format == "tiff") && x > 0 && y > 0 { - info.xdpi, info.ydpi = x, y + info.XDPI, info.YDPI = x, y } if format == "tiff" { if orientation >= 5 { - info.xdpi, info.ydpi = info.ydpi, info.xdpi + info.XDPI, info.YDPI = info.YDPI, info.XDPI } - info.orientation = 1 + info.Orientation = 1 } } - info.xdpi, info.ydpi = info.xdpi*scaleX, info.ydpi*scaleY + info.XDPI, info.YDPI = info.XDPI*scaleX, info.YDPI*scaleY return info, ctx.Err() } diff --git a/internal/base/image/storage.go b/internal/base/image/storage.go new file mode 100644 index 0000000..5223aa1 --- /dev/null +++ b/internal/base/image/storage.go @@ -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) +} diff --git a/ofdgo_image_tiff.go b/internal/base/image/tiff.go similarity index 79% rename from ofdgo_image_tiff.go rename to internal/base/image/tiff.go index f2ab463..94ff86d 100644 --- a/ofdgo_image_tiff.go +++ b/internal/base/image/tiff.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "bytes" @@ -25,20 +25,20 @@ import ( "io" ) -// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 -var errTIFFRandomAccess = errors.New("TIFF directory requires random access") +// ErrTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 +var ErrTIFFRandomAccess = errors.New("TIFF directory requires random access") -// tiffImageLayout 保存TIFF目录的图像布局及关联透明蒙版 -type tiffImageLayout struct { - width, height int - tileWidth int - tileHeight int - orientation int - offset, next uint32 - reduced, mask bool - planar bool +// 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 + Samples uint32 depth uint32 fillOrder uint32 compression uint32 @@ -47,7 +47,7 @@ type tiffImageLayout struct { profileOffset uint32 profileSize uint32 t4, t6 uint32 - transparency *tiffImageLayout + Transparency *TIFFLayout } // tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源 @@ -60,28 +60,28 @@ type tiffDirectoryInput struct { // orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度 type orientedTIFFImage struct { source image.Image - tiffImageLayout + TIFFLayout } -// isTIFFData 识别标准TIFF字节序与版本标识 +// IsTIFF 识别标准TIFF字节序与版本标识 // 入参: data 文件头或完整编码 // 返回: bool 是否为TIFF 6.0编码 -func isTIFFData(data []byte) bool { +func IsTIFF(data []byte) bool { return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a") } // readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图 // 入参: input 从文件头开始的输入流 -// 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误 -func readTIFFLayout(input io.Reader) (tiffImageLayout, error) { - return readTIFFLayoutContext(context.Background(), input) +// 返回: TIFFLayout 存储尺寸与方向, error 文件结构或方向错误 +func readTIFFLayout(input io.Reader) (TIFFLayout, error) { + return ReadTIFFLayoutContext(context.Background(), input) } -// readTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消 +// ReadTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消 // 入参: ctx 取消上下文, input 从文件头开始的输入流 -// 返回: tiffImageLayout 存储尺寸与方向, error 结构、读取或取消错误 -func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayout, error) { - var result tiffImageLayout +// 返回: TIFFLayout 存储尺寸与方向, error 结构、读取或取消错误 +func ReadTIFFLayoutContext(ctx context.Context, input io.Reader) (TIFFLayout, error) { + var result TIFFLayout if err := ctx.Err(); err != nil { return result, err } @@ -89,7 +89,7 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou if _, err := io.ReadFull(input, header[:]); err != nil { return result, err } - if !isTIFFData(header[:]) { + if !IsTIFF(header[:]) { return result, fmt.Errorf("invalid TIFF header") } var order binary.ByteOrder = binary.LittleEndian @@ -99,7 +99,7 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou offset := int64(order.Uint32(header[4:])) position := int64(8) var seen map[uint32]bool - var transparency *tiffImageLayout + var transparency *TIFFLayout for offset != 0 { if seen != nil && seen[uint32(offset)] { return result, fmt.Errorf("cyclic TIFF directory chain") @@ -109,37 +109,37 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou return result, err } if layout.mask { - if !layout.reduced { + 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 { + } 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 layout.Next != 0 { if seen == nil { seen = make(map[uint32]bool) } seen[uint32(offset)] = true } - offset, position = int64(layout.next), end + offset, position = int64(layout.Next), end } if err := ctx.Err(); err != nil { - return tiffImageLayout{}, err + return TIFFLayout{}, err } - if result.offset == 0 { + if result.Offset == 0 { return result, fmt.Errorf("missing TIFF main image") } - result.transparency = transparency + result.Transparency = transparency if transparency != nil { - size := result.displaySize() + size := result.DisplaySize() if size.X > int(^uint(0)>>1)/8/size.Y { - return tiffImageLayout{}, fmt.Errorf("TIFF mask image dimensions exceed capacity") + return TIFFLayout{}, fmt.Errorf("TIFF mask image dimensions exceed capacity") } } return result, nil @@ -147,9 +147,9 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou // readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素 // 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置 -// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误 -func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) { - var result tiffImageLayout +// 返回: 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 } @@ -163,9 +163,9 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr } } else { if offset < position { - return result, position, errTIFFRandomAccess + return result, position, ErrTIFFRandomAccess } - reader := imageInput{ReadCloser: io.NopCloser(input), context: ctx} + reader := contextInput{ReadCloser: io.NopCloser(input), context: ctx} if _, err := io.CopyN(io.Discard, reader, offset-position); err != nil { return result, position, err } @@ -174,7 +174,7 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr return result, position, err } count := int(order.Uint16(header[:2])) - result.orientation, result.offset, result.samples, result.fillOrder = 1, uint32(offset), 1, 1 + result.Orientation, result.Offset, result.Samples, result.fillOrder = 1, uint32(offset), 1, 1 result.depth = 1 result.compression = 1 result.predictor = 1 @@ -230,9 +230,9 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr case 255: legacy = uint32(value) case 256: - result.width = int(value) + result.Width = int(value) case 257: - result.height = int(value) + result.Height = int(value) case 259: result.compression = uint32(value) case 262: @@ -247,17 +247,17 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr if value < 1 || value > 8 { return result, position, fmt.Errorf("invalid TIFF orientation") } - result.orientation = int(value) + result.Orientation = int(value) case 277: if value == 0 { return result, position, fmt.Errorf("invalid TIFF sample count") } - result.samples = uint32(value) + result.Samples = uint32(value) case 284: if value != 1 && value != 2 { return result, position, fmt.Errorf("invalid TIFF planar configuration") } - result.planar = value == 2 + result.Planar = value == 2 case 292: result.t4 = uint32(value) case 293: @@ -265,27 +265,27 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr case 317: result.predictor = uint32(value) case 322: - result.tileWidth = int(value) + result.TileWidth = int(value) case 323: - result.tileHeight = int(value) + result.TileHeight = int(value) } } if _, err := io.ReadFull(input, header[:4]); err != nil { return result, position, err } - result.next = order.Uint32(header[:4]) + result.Next = order.Uint32(header[:4]) if hasSubfile { - result.reduced, result.mask = subfile&1 != 0, result.mask || subfile&4 != 0 + result.Reduced, result.mask = subfile&1 != 0, result.mask || subfile&4 != 0 } else { - result.reduced = legacy == 2 + result.Reduced = legacy == 2 } - if result.width == 0 || result.height == 0 { + 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 { + 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) { + 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 @@ -346,19 +346,19 @@ func (r *tiffDirectoryInput) ReadAt(data []byte, offset int64) (int, error) { // size 返回应用方向后的显示尺寸 // 返回: image.Point 像素宽高 -func (l tiffImageLayout) size() image.Point { - if l.orientation >= 5 { - return image.Pt(l.height, l.width) +func (l TIFFLayout) size() image.Point { + if l.Orientation >= 5 { + return image.Pt(l.Height, l.Width) } - return image.Pt(l.width, l.height) + return image.Pt(l.Width, l.Height) } -// displaySize 返回同时保留主图和透明蒙版分辨率的显示尺寸 +// DisplaySize 返回同时保留主图和透明蒙版分辨率的显示尺寸 // 返回: image.Point 保留高分辨率蒙版边缘的像素宽高 -func (l tiffImageLayout) displaySize() image.Point { +func (l TIFFLayout) DisplaySize() image.Point { size := l.size() - if l.transparency != nil { - mask := l.transparency.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 @@ -381,8 +381,8 @@ 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 { + w, h := s.Width, s.Height + switch s.Orientation { case 2: x = w - 1 - x case 3: @@ -421,7 +421,7 @@ func (s *orientedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 { if !ok { return color.NRGBA64{} } - return imageNRGBA64At(s.source, p.X, p.Y) + return NRGBA64At(s.source, p.X, p.Y) } // RGBA64At 返回预乘十六位像素,保留原始颜色精度 diff --git a/ofdgo_image_tiff_bits.go b/internal/base/image/tiff_bits.go similarity index 93% rename from ofdgo_image_tiff_bits.go rename to internal/base/image/tiff_bits.go index 1bea5ab..a2cc601 100644 --- a/ofdgo_image_tiff_bits.go +++ b/internal/base/image/tiff_bits.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "cmp" @@ -38,7 +38,7 @@ type tiffBitOrderInput struct { // tiffBitOrderRanges 收集低位优先的未压缩一位图像块,压缩传真位序由解码器处理 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 返回: [][2]int64 按偏移排序且不重叠的字节区间, error 字段或布局错误 -func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout) ([][2]int64, error) { +func tiffBitOrderRanges(ctx context.Context, data []byte, layout TIFFLayout) ([][2]int64, error) { if layout.fillOrder != 2 { return nil, nil } @@ -46,7 +46,7 @@ func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout if data[0] == 'M' { order = binary.BigEndian } - offset := int(layout.offset) + offset := int(layout.Offset) count := int(order.Uint16(data[offset:])) entries := make(map[uint16][]byte, count) for i := range count { @@ -68,10 +68,10 @@ func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout 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 { + 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++ { + 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") @@ -93,7 +93,7 @@ func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout if err != nil || rows == 0 { return nil, fmt.Errorf("invalid TIFF rows per strip") } - expected := (uint64(layout.height)-1)/uint64(rows) + 1 + expected := (uint64(layout.Height)-1)/uint64(rows) + 1 if entries[322] != nil { locations, lengths = entries[324], entries[325] width, err := value(322, 0) @@ -104,16 +104,16 @@ func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout 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) + 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) { + if layout.Planar { + if expected > uint64(^uint32(0))/uint64(layout.Samples) { return nil, fmt.Errorf("too many TIFF image blocks") } - expected *= uint64(layout.samples) + expected *= uint64(layout.Samples) } if locations == nil || lengths == nil { return nil, fmt.Errorf("missing TIFF block arrays") diff --git a/ofdgo_image_tiff_fax.go b/internal/base/image/tiff_fax.go similarity index 91% rename from ofdgo_image_tiff_fax.go rename to internal/base/image/tiff_fax.go index a817aa8..8ca7531 100644 --- a/ofdgo_image_tiff_fax.go +++ b/internal/base/image/tiff_fax.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "context" @@ -37,21 +37,21 @@ type tiffFaxImage struct { // decodeTIFFFax 按TIFF传真选项解码各图像块,复用PDFGo的T.4与T.6解码器 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 返回: image.Image 图像, error 字段、解码或取消错误 -func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (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 { + 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) + offset := int(layout.Offset) count := int(order.Uint16(data[offset:])) entries := make(map[uint16][]byte, count) for i := range count { @@ -78,12 +78,12 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im if err != nil || photo > 1 && photo != 4 || entries[338] != nil { return nil, fmt.Errorf("invalid TIFF fax color model") } - width := uint32(layout.width) + 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)) + height = min(height, uint32(layout.Height)) locations, lengths := entries[273], entries[279] tiled := entries[322] != nil if tiled { @@ -103,13 +103,13 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im 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 + 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)} + 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 @@ -139,7 +139,7 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im } rows := height if !tiled { - rows = min(height, uint32(uint64(layout.height)-i*uint64(height))) + rows = min(height, uint32(uint64(layout.Height)-i*uint64(height))) } k := pdfgo.Integer(0) if layout.compression == 4 { diff --git a/ofdgo_image_tiff_mask.go b/internal/base/image/tiff_mask.go similarity index 86% rename from ofdgo_image_tiff_mask.go rename to internal/base/image/tiff_mask.go index dca3aaf..8573912 100644 --- a/ofdgo_image_tiff_mask.go +++ b/internal/base/image/tiff_mask.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "context" @@ -26,19 +26,19 @@ type maskedTIFFImage struct { bounds image.Rectangle } -// decodeTIFFImage 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版 +// DecodeTIFF 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局 // 返回: image.Image 图像, error 解码或取消错误 -func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (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 { + if err != nil || layout.Transparency == nil { return source, err } - mask, err := decodeTIFFPixels(ctx, data, *layout.transparency) + 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 + return &maskedTIFFImage{source: source, mask: mask, bounds: image.Rectangle{Max: layout.DisplaySize()}}, nil } // Bounds 返回主图和蒙版共同覆盖的像素区域 @@ -61,11 +61,11 @@ func (s *maskedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 { 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 := imageNRGBA64At(s.source, sx, sy) + 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 imageNRGBA64At(s.mask, mx, my).R == 0 { + if NRGBA64At(s.mask, mx, my).R == 0 { pixel.A = 0 } return pixel diff --git a/ofdgo_image_tiff_planar.go b/internal/base/image/tiff_planar.go similarity index 95% rename from ofdgo_image_tiff_planar.go rename to internal/base/image/tiff_planar.go index cbb7f07..6946c63 100644 --- a/ofdgo_image_tiff_planar.go +++ b/internal/base/image/tiff_planar.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "bytes" @@ -52,15 +52,15 @@ type planarTIFFImage struct { // needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用 // 返回: bool 是否需要分量视图 -func (layout tiffImageLayout) 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) +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 估算每像素解码缓冲,低位深分量按逐位展开后的字节数计量 +// DecodedPixelBytes 估算每像素解码缓冲,低位深分量按逐位展开后的字节数计量 // 返回: uint64 每像素预算字节数 -func (layout tiffImageLayout) decodedPixelBytes() uint64 { - samples := uint64(layout.samples) - if layout.planar { +func (layout TIFFLayout) DecodedPixelBytes() uint64 { + samples := uint64(layout.Samples) + if layout.Planar { if layout.photometric == 5 && samples > 4 { return 20 } @@ -81,7 +81,7 @@ func (layout tiffImageLayout) decodedPixelBytes() uint64 { // decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局 // 返回: image.Image 图像, error 解码或取消错误 -func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout) (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 @@ -122,7 +122,7 @@ func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout) } pixels = result } else { - input := newTIFFDirectoryInput(data, layout.offset) + input := newTIFFDirectoryInput(data, layout.Offset) input.ctx = ctx var err error pixels, err = decodeTIFFBitOrder(input, ranges) @@ -141,8 +141,8 @@ func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout) return nil, err } } - if layout.orientation != 1 { - pixels = &orientedTIFFImage{source: pixels, tiffImageLayout: layout} + if layout.Orientation != 1 { + pixels = &orientedTIFFImage{source: pixels, TIFFLayout: layout} } return pixels, nil } @@ -150,7 +150,7 @@ func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout) // tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误 -func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayout) (*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 @@ -163,7 +163,7 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo if data[0] == 'M' { order = binary.BigEndian } - offset := int(layout.offset) + offset := int(layout.Offset) count := int(order.Uint16(data[offset:])) entries := make(map[uint16][]byte, count) for i := range count { @@ -200,7 +200,7 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo 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} + 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) @@ -214,7 +214,7 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo } } planes := samples - if !layout.planar && samples > 1 { + if !layout.Planar && samples > 1 { result.stride = int(samples) planes = 1 } @@ -257,14 +257,14 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo 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) + 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 + blocks = (uint64(layout.Height)-1)/uint64(rows) + 1 } if blocks > math.MaxUint32/uint64(planes) { return nil, nil, fmt.Errorf("too many TIFF plane blocks") @@ -376,11 +376,11 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo 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 { + 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 + result.predictorWidth = layout.Width if width != 0 { result.predictorWidth = int(width) } diff --git a/ofdgo_image_tiff_profile.go b/internal/base/image/tiff_profile.go similarity index 85% rename from ofdgo_image_tiff_profile.go rename to internal/base/image/tiff_profile.go index 5d07da5..8078266 100644 --- a/ofdgo_image_tiff_profile.go +++ b/internal/base/image/tiff_profile.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package image import ( "context" @@ -20,12 +20,26 @@ import ( "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 主图布局 -// 返回: *colorProfile 颜色配置,未声明时为nil, error 配置或取消错误 -func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayout) (*colorProfile, error) { +// 返回: *pdfgo.ColorSpace 颜色配置,未声明时为nil, error 配置或取消错误 +func readTIFFColorProfile(ctx context.Context, data []byte, layout TIFFLayout) (*pdfgo.ColorSpace, error) { if ctx == nil { ctx = context.Background() } @@ -50,7 +64,7 @@ func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayo default: return nil, fmt.Errorf("unsupported TIFF profile color model") } - profile, err := parseColorProfile(kind, data[at:at+size]) + profile, err := parseTIFFColorProfile(kind, data[at:at+size]) if cancelErr := ctx.Err(); cancelErr != nil { return nil, cancelErr } @@ -60,7 +74,7 @@ func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayo // applyTIFFColorProfile 将源颜色转换为十六位sRGB,保留独立透明度和隐藏颜色 // 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置 // 返回: image.Image 独立透明度图像, error 颜色变换或取消错误 -func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *colorProfile) (image.Image, error) { +func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace) (image.Image, error) { if ctx == nil { ctx = context.Background() } @@ -126,11 +140,11 @@ func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *col values[i] = min(1, float64(value)/float64(alpha)) } } else { - pixel := imageNRGBA64At(source, x, y) + 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) + rgb, err := profile.RGB(values[:profile.Components()], profile.DefaultIntent()) if err != nil { return nil, err } diff --git a/ofdgo_compression_parallel.go b/ofdgo_compression_parallel.go index ca929fb..facd617 100644 --- a/ofdgo_compression_parallel.go +++ b/ofdgo_compression_parallel.go @@ -22,6 +22,8 @@ import ( "io" "runtime" "sync" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理 @@ -135,14 +137,14 @@ func outputImageWorkingSet(ctx context.Context, data []byte) int { } input := bytes.NewReader(data) available := outputImageWorkingSetLimit - len(data) - if isTIFFData(data) { - layout, err := readTIFFLayoutContext(ctx, input) + if baseimage.IsTIFF(data) { + layout, err := baseimage.ReadTIFFLayoutContext(ctx, input) if err != nil { return outputImageWorkingSetLimit } return len(data) + tiffImageWorkingSet(layout, available) } - config, _, err := decodeImageConfig(imageInput{ReadCloser: io.NopCloser(input), context: ctx}) + config, _, err := baseimage.DecodeConfig(imageInput{ReadCloser: io.NopCloser(input), context: ctx}) if err != nil || config.Width <= 0 || config.Height <= 0 { return outputImageWorkingSetLimit } @@ -155,21 +157,21 @@ func outputImageWorkingSet(ctx context.Context, data []byte) int { // tiffImageWorkingSet 分别估算显示缓冲及主图、蒙版的完整瓦片存储,超预算时返回上限 // 入参: layout 已校验的主图布局, limit 可用字节数 // 返回: int 不超过limit的估算值 -func tiffImageWorkingSet(layout tiffImageLayout, limit int) int { - size := layout.displaySize() +func tiffImageWorkingSet(layout baseimage.TIFFLayout, limit int) int { + size := layout.DisplaySize() if size.X <= 0 || size.Y <= 0 || size.X > limit/16/size.Y { return limit } cost := size.X * size.Y * 16 - for source := &layout; source != nil; source = source.transparency { - width, height := uint64(source.width), uint64(source.height) - if source.tileWidth != 0 { - x, y := uint64(source.tileWidth), uint64(source.tileHeight) + for source := &layout; source != nil; source = source.Transparency { + width, height := uint64(source.Width), uint64(source.Height) + if source.TileWidth != 0 { + x, y := uint64(source.TileWidth), uint64(source.TileHeight) width, height = ((width-1)/x+1)*x, ((height-1)/y+1)*y } - pixelSize := max(uint64(16), source.decodedPixelBytes()) - if !source.planar { - pixelSize = max(pixelSize, uint64(source.samples)*4) + pixelSize := max(uint64(16), source.DecodedPixelBytes()) + if !source.Planar { + pixelSize = max(pixelSize, uint64(source.Samples)*4) } if width > uint64(limit-cost)/pixelSize/height { return limit diff --git a/ofdgo_compression_raster.go b/ofdgo_compression_raster.go index cbbd5f0..a54d5d8 100644 --- a/ofdgo_compression_raster.go +++ b/ofdgo_compression_raster.go @@ -23,6 +23,7 @@ import ( "io" "math" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" ) @@ -36,10 +37,10 @@ type rasterDensity struct { // 入参: data 图片编码 // 返回: string 标准资源格式,未识别时为空 func outputImageFormat(data []byte) string { - if isJPEGData(data) { + if baseimage.IsJPEG(data) { return "JPEG" } - if isPNGData(data) { + if baseimage.IsPNG(data) { return "PNG" } return "" @@ -56,7 +57,7 @@ func optimizeImageResource(ctx context.Context, data []byte, options Compression if err := ctx.Err(); err != nil { return nil, err } - if options.Mode != CompressionUnchanged && isTIFFData(data) { + if options.Mode != CompressionUnchanged && baseimage.IsTIFF(data) { return optimizeTIFFResource(ctx, data, options, size) } if options.Mode == CompressionUnchanged || !bytes.HasPrefix(data, []byte("BM")) { @@ -69,7 +70,7 @@ func optimizeImageResource(ctx context.Context, data []byte, options Compression // 入参: ctx 取消上下文, data BMP编码, options 压缩选项, size 像素需求 // 返回: []byte 图片编码, error 解码、编码或取消错误 func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { - offset, width, height, depth, compression, err := parseBMPHeader(data) + offset, width, height, depth, compression, err := baseimage.ParseBMPHeader(data) if err != nil { return data, nil } @@ -98,13 +99,13 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp return data, nil } if depth == 16 || depth == 32 { - masks, err := bmpColorMasks(data, offset, depth, compression) + masks, err := baseimage.BMPColorMasks(data, offset, depth, compression) if err != nil { return data, nil } if options.Mode == CompressionLossless { for _, mask := range masks { - if mask.max > 255 { + if mask.Max > 255 { return data, nil } } @@ -112,7 +113,7 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp } var pixels image.Image if depth == 16 || compression == 3 { - pixels, err = decodeBMPImageContext(ctx, data) + pixels, err = baseimage.DecodeBMPContext(ctx, data) } else { pixels, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) } diff --git a/ofdgo_compression_tiff.go b/ofdgo_compression_tiff.go index f473f34..8ad509e 100644 --- a/ofdgo_compression_tiff.go +++ b/ofdgo_compression_tiff.go @@ -20,6 +20,8 @@ import ( "encoding/binary" "image" "math" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度 @@ -27,22 +29,22 @@ import ( // 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求 // 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误 func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { - layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) + layout, err := baseimage.ReadTIFFLayoutContext(ctx, bytes.NewReader(data)) if err != nil { return data, ctx.Err() } - pixelSize := int64(layout.decodedPixelBytes()) - if int64(layout.width) > outputOptimizationBufferLimit/pixelSize/int64(layout.height) { + pixelSize := int64(layout.DecodedPixelBytes()) + if int64(layout.Width) > outputOptimizationBufferLimit/pixelSize/int64(layout.Height) { return data, ctx.Err() } - if layout.tileWidth != 0 && int64(layout.tileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.tileHeight) { + if layout.TileWidth != 0 && int64(layout.TileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.TileHeight) { return data, ctx.Err() } density, ok := tiffCompressionDensity(data, layout) if !ok { return data, ctx.Err() } - pixels, err := decodeTIFFImage(ctx, data, layout) + pixels, err := baseimage.DecodeTIFF(ctx, data, layout) if err != nil { return data, ctx.Err() } @@ -53,16 +55,16 @@ func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionO // 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码 // 入参: data TIFF编码, layout 已校验的主图布局 // 返回: rasterDensity PNG像素密度, bool 是否可完整转换 -func tiffCompressionDensity(data []byte, layout tiffImageLayout) (rasterDensity, bool) { +func tiffCompressionDensity(data []byte, layout baseimage.TIFFLayout) (rasterDensity, bool) { var density rasterDensity var order binary.ByteOrder = binary.LittleEndian if data[0] == 'M' { order = binary.BigEndian } - if layout.next != 0 || layout.offset != order.Uint32(data[4:]) || layout.reduced { + if layout.Next != 0 || layout.Offset != order.Uint32(data[4:]) || layout.Reduced { return density, false } - offset := int(layout.offset) + offset := int(layout.Offset) count := int(order.Uint16(data[offset:])) unit := uint32(2) var resolutions [2][2]uint32 @@ -165,7 +167,7 @@ func tiffCompressionDensity(data []byte, layout tiffImageLayout) (rasterDensity, if math.Round(density.x) < 1 || math.Round(density.y) < 1 || math.Round(density.x) > math.MaxInt32 || math.Round(density.y) > math.MaxInt32 { return density, false } - if layout.orientation >= 5 { + if layout.Orientation >= 5 { density.x, density.y = density.y, density.x } return density, true diff --git a/ofdgo_editor.go b/ofdgo_editor.go index 706f81d..20e25ea 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -32,6 +32,8 @@ import ( "strings" "time" "unicode/utf8" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // editorPathValidationLimit 限制同次对象校验保留的路径数量 @@ -483,7 +485,7 @@ func (e *Editor) ImageSize(id string) (image.Point, error) { // 入参: data 图片数据, decoded 已解码像素,为nil时按需解码 // 返回: string 资源标识, error 注册错误 func (e *Editor) addDecodedImage(data []byte, decoded image.Image) (string, error) { - config, format, err := decodeImageConfigData(data) + config, format, err := baseimage.DecodeConfigData(data) if err != nil { return "", err } @@ -676,7 +678,7 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro if object.Type != "ImageObject" { return fmt.Errorf("object %q is not an image", id) } - decoded, _, err := decodeImagePixelsContext(context.Background(), data) + decoded, _, err := baseimage.DecodePixelsContext(context.Background(), data) if err != nil { return err } diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index 5b3020b..efbfed8 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -20,6 +20,8 @@ import ( "fmt" "maps" "slices" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部 @@ -121,7 +123,7 @@ func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, dat return fmt.Errorf("composite member is not an image") } object := node.object - decoded, _, err := decodeImagePixelsContext(context.Background(), data) + decoded, _, err := baseimage.DecodePixelsContext(context.Background(), data) if err != nil { return err } diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 6f80ce6..2d5c639 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -27,6 +27,8 @@ import ( "slices" "strconv" "strings" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) const ( @@ -285,7 +287,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen return nil, err } if mode == FontEmbed { - if err := validateFontEmbedding(parsed.Data); err != nil { + if err := basefont.ValidateEmbedding(parsed.Data); err != nil { return nil, err } } @@ -490,11 +492,11 @@ func fontBindingFile(ctx context.Context, reader *Reader, pending map[string][]b // 入参: source 原字体, target 待嵌入字体 // 返回: bool 是否保留原字体内容及字形编号 func sameBindingFont(source, target []byte) bool { - a, err := fontFileTables(source, 0) + a, err := basefont.FileTables(source, 0) if err != nil { return false } - b, err := fontFileTables(target, 0) + b, err := basefont.FileTables(target, 0) if err != nil || len(a) != len(b) { return false } diff --git a/ofdgo_editor_images.go b/ofdgo_editor_images.go index 363db96..a800e4a 100644 --- a/ofdgo_editor_images.go +++ b/ofdgo_editor_images.go @@ -20,6 +20,8 @@ import ( "fmt" "image" "math" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // ImagePageSource 图片页面来源,按列表顺序逐张读取,每张图片处理完成后不再引用返回数据 @@ -115,7 +117,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag if err := ctx.Err(); err != nil { return 0, err } - config, format, err := decodeImageConfigDataContext(ctx, data) + config, format, err := baseimage.DecodeConfigDataContext(ctx, data) if err != nil { return 0, err } @@ -125,18 +127,18 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag var decoded image.Image checksum := sha256.Sum256(data) if !verified[checksum] { - decoded, _, err = decodeImagePixelsContext(ctx, data) + decoded, _, err = baseimage.DecodePixelsContext(ctx, data) if err != nil { return 0, err } verified[checksum] = true } - metadata, err := imagePageMetadataContext(ctx, data, format, options.DPI) + metadata, err := baseimage.PageMetadataContext(ctx, data, format, options.DPI) if err != nil { return 0, err } - w, h := float64(config.Width)*25.4/metadata.xdpi, float64(config.Height)*25.4/metadata.ydpi - if metadata.orientation >= 5 { + w, h := float64(config.Width)*25.4/metadata.XDPI, float64(config.Height)*25.4/metadata.YDPI + if metadata.Orientation >= 5 { w, h = h, w } pw, ph := options.Width, options.Height @@ -160,7 +162,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag if err != nil { return 0, err } - matrix := imagePageOrientation(metadata.orientation, w, h) + matrix := imagePageOrientation(metadata.Orientation, w, h) _, err = e.AddObject(page, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{ ResourceID: resource, Boundary: fmt.Sprintf("%s %s %s %s", ofdNumber((pw-w)/2), ofdNumber((ph-h)/2), ofdNumber(w), ofdNumber(h)), diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 5cc3a20..afd74f6 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -29,6 +29,8 @@ import ( "slices" "strconv" "strings" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // editorSource 保留输入包及按需加载的原始页面 @@ -945,13 +947,13 @@ func (e *Editor) editorImage(id string) (image.Point, error) { return image.Point{}, err } defer input.Close() - config, _, err := decodeImageConfig(input) - if errors.Is(err, errTIFFRandomAccess) { + config, _, err := baseimage.DecodeConfig(input) + if errors.Is(err, baseimage.ErrTIFFRandomAccess) { data, readErr := reader.readFileView(reader.ResPath(name)) if readErr != nil { return image.Point{}, readErr } - config, _, err = decodeImageConfigData(data) + config, _, err = baseimage.DecodeConfigData(data) } if err != nil { return image.Point{}, err @@ -984,7 +986,7 @@ func (e *Editor) validateImageMask(id string, size image.Point) error { return err } } - mask, _, err := decodeImageData(data) + mask, _, err := baseimage.Decode(data) if err != nil { return err } diff --git a/ofdgo_editor_resource.go b/ofdgo_editor_resource.go index a9709da..2247d7e 100644 --- a/ofdgo_editor_resource.go +++ b/ofdgo_editor_resource.go @@ -23,6 +23,8 @@ import ( "slices" "strconv" "strings" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // ResourceTarget 描述当前页可绑定资源动作的图元,Bound表示已有该资源的点击动作 @@ -175,7 +177,7 @@ func (e *Editor) ReplaceResource(ctx context.Context, kind, id, format string, d return fmt.Errorf("empty media data") } if kind == "Image" { - img, actual, err := decodeImagePixelsContext(ctx, data) + img, actual, err := baseimage.DecodePixelsContext(ctx, data) if err != nil { return err } @@ -322,7 +324,7 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id if err != nil { return err } - config, _, err := decodeImageConfigDataContext(ctx, data) + config, _, err := baseimage.DecodeConfigDataContext(ctx, data) if err != nil { return err } @@ -341,7 +343,7 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id if err != nil { return err } - img, _, err = decodeImageDataContext(ctx, data) + img, _, err = baseimage.DecodeContext(ctx, data) if err != nil { return err } diff --git a/ofdgo_font.go b/ofdgo_font.go index a0b7547..4d3d385 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -15,7 +15,7 @@ package ofdgo import ( - "encoding/binary" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // FixFontDataAggressive 激进修复字体数据 @@ -24,50 +24,5 @@ import ( // 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name // 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息 func FixFontDataAggressive(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 -} - -// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和 -// 入参: tables 原始字体表 -// 返回: []byte 独立SFNT数据 -func fontSFNTData(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 + return basefont.FixDataAggressive(data, fixCmap, fixName) } diff --git a/ofdgo_font_backend.go b/ofdgo_font_backend.go index a29cc2f..0787cb4 100644 --- a/ofdgo_font_backend.go +++ b/ofdgo_font_backend.go @@ -19,6 +19,8 @@ import ( "crypto/sha256" "errors" "fmt" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位 @@ -219,13 +221,13 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { r.preparedFonts[id] = prepared return prepared, nil } - prepared.CIDs = getCFFCIDRuneMap(resolved.Data) + prepared.CIDs = basefont.GetCFFCIDRuneMap(resolved.Data) if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil { prepared.Data = data if mapping != nil { prepared.Glyphs = make(map[uint16]rune) for character, glyph := range mapping { - if character == packedGlyphRune(glyph) { + if character == basefont.PackedGlyphRune(glyph) { prepared.Glyphs[glyph] = character } } diff --git a/ofdgo_font_file.go b/ofdgo_font_file.go index e4bc38c..a79ed14 100644 --- a/ofdgo_font_file.go +++ b/ofdgo_font_file.go @@ -15,22 +15,14 @@ package ofdgo import ( - "bytes" - "encoding/binary" "fmt" - "strings" "unicode/utf8" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 -type FontFace 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"` -} +type FontFace = basefont.Face // Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓 // 返回: []FontFace 字体列表, error 格式或名称表错误 @@ -39,23 +31,7 @@ func (f FontFile) Faces() ([]FontFace, error) { if err != nil { return nil, err } - count, err := fontFileCount(data) - if err != nil { - return nil, err - } - faces := make([]FontFace, 0, count) - for index := 0; index < count; index++ { - tables, err := fontFileTables(data, index) - if err != nil { - return nil, err - } - face := fontFaceInfo(tables["name"], index) - if face.FullName == "" { - return nil, fmt.Errorf("font %d has no usable name", index) - } - faces = append(faces, face) - } - return faces, nil + return basefont.ReadFaces(data) } // Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件 @@ -66,26 +42,7 @@ func (f FontFile) Face(index int) ([]byte, error) { if err != nil { return nil, err } - if bytes.HasPrefix(data, []byte("ttcf")) { - data, err = extractCollectionFont(data, index) - if err != nil { - return nil, err - } - index = 0 - } - tables, err := fontFileTables(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 + return basefont.ExtractFace(data, index) } // MissingGlyphs 检查指定字体的字符覆盖,不注册资源或修改文档,忽略段落换行 @@ -99,66 +56,5 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) { if err != nil { return "", err } - data, err = defaultCFF2Font(data) - if err != nil { - return "", err - } - sfnt, err := parseSFNTFont(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 -} - -// fontFaceInfo 从名称表读取字体信息 -// 入参: data 名称表数据, index 零起始字体索引 -// 返回: FontFace 字体信息 -func fontFaceInfo(data []byte, index int) FontFace { - 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 FontFace{Index: index, FullName: name(4, 6, 16, 1), Family: name(16, 1), Style: name(17, 2), PostScriptName: name(6), Names: fontNamesFromTable(data)} -} - -// fontFileCount 校验字体或集合头并读取字体数量 -// 入参: data OpenType数据 -// 返回: int 字体数量, error 格式错误 -func fontFileCount(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") - } + return basefont.MissingGlyphs(data, text) } diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index 7263787..02b955f 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -23,6 +23,8 @@ import ( "strings" "sync" "time" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // 字体文件系统及目录条目的接口检查 @@ -130,7 +132,7 @@ func NewFontFS(fonts []FontFile) *FontFS { sort.Strings(fsys.names) fsys.candidates = fontFileCandidates(fsys.names, path.Base) for i, file := range fsys.candidates { - fsys.candidates = appendFontFileNames(fsys.candidates, i, fontFileNames(bytes.NewReader(fsys.files[file.name]))) + fsys.candidates = appendFontFileNames(fsys.candidates, i, basefont.FileNames(bytes.NewReader(fsys.files[file.name]))) } return fsys } @@ -143,7 +145,7 @@ func NewLazyFontFS(names []string, load func(string) ([]byte, error)) *FontFS { fsys := &FontFS{files: make(map[string][]byte), loaders: make(map[string]func() ([]byte, error))} for _, original := range names { name := cleanFontName(original) - if name == "." || !isFontFileName(name) { + if name == "." || !basefont.IsFontFileName(name) { continue } if _, exists := fsys.files[name]; exists { @@ -438,7 +440,7 @@ func fontFileMatches(candidates []fontFileCandidate, names []string, bold, itali continue } for j, alias := range append([]string{name}, fontQualifiedNames(name)...) { - matcher := fontPatternMatcher{stem: fontNormalizeName(alias), priority: (i-len(names))*2 + min(j, 1)} + matcher := fontPatternMatcher{stem: basefont.NormalizeName(alias), priority: (i-len(names))*2 + min(j, 1)} if matcher.stem == "" { continue } @@ -507,9 +509,9 @@ func cleanFontName(name string) string { func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate { files := make([]fontFileCandidate, 0, len(names)) for _, name := range names { - if isFontFileName(name) { + if basefont.IsFontFileName(name) { baseName := base(name) - normalized := fontNormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName))) + normalized := basefont.NormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName))) files = append(files, fontFileCandidate{ name: name, base: baseName, @@ -534,7 +536,7 @@ func newFontPatternMatcher(pattern string) fontPatternMatcher { } if matcher.stem != "" { for _, alias := range fontExactCandidateNames(matcher.stem) { - alias = fontNormalizeName(alias) + alias = basefont.NormalizeName(alias) if alias != "" { matcher.aliases = append(matcher.aliases, alias) } @@ -563,7 +565,7 @@ func (m fontPatternMatcher) rank(name string) int { return m.rankCandidate(fontFileCandidate{ base: name, lowerBase: strings.ToLower(name), - normalized: fontNormalizeName(name), + normalized: basefont.NormalizeName(name), }) } @@ -656,7 +658,7 @@ func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, mat } suffix := matcher.styleSuffixNormalized(matcher.candidateName(file)) if rank == fontMatchFuzzy { - suffix = matcher.styleSuffixNormalized(fontNormalizeName(file.base)) + suffix = matcher.styleSuffixNormalized(basefont.NormalizeName(file.base)) } next := fontFileMatch{ name: file.name, diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 021a940..527b79e 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -22,6 +22,8 @@ import ( "path" "slices" "sort" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) const ( @@ -128,7 +130,7 @@ func (r *Reader) fontData(id string, readonly bool) ([]byte, error) { } if bytes.HasPrefix(data, []byte("ttcf")) { index := fontCollectionIndex(data, []string{f.FontName, f.FamilyName}, f.Bold, f.Italic) - return extractCollectionFont(data, index) + return basefont.ExtractCollectionFont(data, index) } return data, nil } @@ -245,11 +247,11 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) { return nil, err } if data, err = decodeFontContainer(data); err == nil { - if count, err := fontFileCount(data); err == nil { + if count, err := basefont.FileCount(data); err == nil { faces = make([]*FontFace, count) for index := range faces { - if tables, err := fontFileTables(data, index); err == nil { - info := fontFaceInfo(tables["name"], index) + if tables, err := basefont.FileTables(data, index); err == nil { + info := basefont.FaceInfo(tables["name"], index) faces[index] = &info } } diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go index 547de22..62d8738 100644 --- a/ofdgo_font_match.go +++ b/ofdgo_font_match.go @@ -18,6 +18,8 @@ import ( "path" "slices" "strings" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // 字体匹配按精确、部分和模糊匹配排列,零值表示未匹配 @@ -105,9 +107,6 @@ var fontMatchRules = normalizeFontMatchRules([]fontMatchRule{ }, }) -// fontNameReplacer 移除字体名称匹配时忽略的分隔符 -var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "") - // fontMatchRule 字体匹配规则 type fontMatchRule struct { Keys []string @@ -121,7 +120,7 @@ type fontMatchRule struct { // 入参: name 字体名称 // 返回: string 规范化后的字体名称 func FontNormalizeName(name string) string { - return fontNormalizeName(name) + return basefont.NormalizeName(name) } // FontCandidateNames 获取字体候选名称 @@ -154,7 +153,7 @@ func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule { keys := append(append(rule.Keys, rule.Names...), rule.System) rule.Keys = nil for _, key := range keys { - key = fontNormalizeName(key) + key = basefont.NormalizeName(key) if key != "" && !slices.Contains(rule.Keys, key) { rule.Keys = append(rule.Keys, key) } @@ -163,19 +162,6 @@ func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule { return rules } -// fontNormalizeName 规范化字体名称 -// 入参: name 字体名称 -// 返回: string 规范化后的字体名称 -func fontNormalizeName(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) -} - // fontCandidateNames 获取字体候选名称 // 入参: names 字体名称列表 // 返回: []string 字体候选名称 @@ -194,8 +180,8 @@ func fontExactCandidateNames(names ...string) []string { // 入参: name 字体名称 // 返回: []string 保留后缀的候选名称 func fontQualifiedNames(name string) []string { - name = fontNormalizeName(name) - key := fontNormalizeName(name) + name = basefont.NormalizeName(name) + key := basefont.NormalizeName(name) var names []string seen := make(map[string]bool) for _, rule := range fontMatchRules { @@ -210,7 +196,7 @@ func fontQualifiedNames(name string) []string { } suffix := strings.TrimPrefix(name, prefix) for _, alias := range append([]string{rule.System}, rule.Names...) { - names = appendFontName(names, seen, fontNormalizeName(alias)+suffix) + names = basefont.AppendFontName(names, seen, basefont.NormalizeName(alias)+suffix) } } return names @@ -228,7 +214,7 @@ func fontCandidateNamesByLevel(maxLevel int, names ...string) []string { if name == "" { continue } - result = appendFontName(result, seen, name) + result = basefont.AppendFontName(result, seen, name) bases = append(bases, name) } levels := fontMatchLevels(bases) @@ -236,9 +222,9 @@ func fontCandidateNamesByLevel(maxLevel int, names ...string) []string { for i := range bases { for j, rule := range fontMatchRules { if levels[i*len(fontMatchRules)+j] == level { - result = appendFontName(result, seen, rule.System) + result = basefont.AppendFontName(result, seen, rule.System) for _, item := range rule.Names { - result = appendFontName(result, seen, item) + result = basefont.AppendFontName(result, seen, item) } } } @@ -252,13 +238,13 @@ func fontCandidateNamesByLevel(maxLevel int, names ...string) []string { // 返回: string 字体匹配模式主干 func fontPatternStem(pattern string) string { stem := strings.TrimSuffix(pattern, "*") - if isFontFileName(stem) { + if basefont.IsFontFileName(stem) { stem = strings.TrimSuffix(stem, path.Ext(stem)) } if index := strings.IndexAny(stem, "*?["); index >= 0 { stem = stem[:index] } - return fontNormalizeName(stem) + return basefont.NormalizeName(stem) } // fontFilePatterns 获取字体文件匹配模式 @@ -293,7 +279,7 @@ func fontSystemNames(names ...string) []string { seen := make(map[string]bool) for _, name := range names { for _, item := range fontCandidateNamesByLevel(fontMatchPartial, name) { - result = appendFontName(result, seen, item) + result = basefont.AppendFontName(result, seen, item) } } return result @@ -311,13 +297,13 @@ func fontCandidateNamesAtLevel(level int, names []string, levels []int) []string continue } if level == fontMatchExact { - result = appendFontName(result, seen, name) + result = basefont.AppendFontName(result, seen, name) } for j, rule := range fontMatchRules { if levels[i*len(fontMatchRules)+j] == level { - result = appendFontName(result, seen, rule.System) + result = basefont.AppendFontName(result, seen, rule.System) for _, item := range rule.Names { - result = appendFontName(result, seen, item) + result = basefont.AppendFontName(result, seen, item) } } } @@ -339,7 +325,7 @@ func fontDefaultSystemNames() []string { // 返回: bool 是否禁用合成粗体 func fontNoSyntheticBold(names ...string) bool { for _, name := range names { - name = fontNormalizeName(name) + name = basefont.NormalizeName(name) for _, rule := range fontMatchRules { if !rule.NoSyntheticBold { continue @@ -353,25 +339,13 @@ func fontNoSyntheticBold(names ...string) bool { return false } -// isFontFileName 判断是否为字体文件名 -// 入参: name 文件名 -// 返回: bool 是否为字体文件 -func isFontFileName(name string) bool { - switch strings.ToLower(path.Ext(name)) { - case ".ttf", ".otf", ".ttc": - return true - default: - return false - } -} - // fontMatchLevels 按名称和规则顺序计算匹配等级 // 入参: names 字体名称列表 // 返回: []int 名称与规则的匹配等级 func fontMatchLevels(names []string) []int { levels := make([]int, len(names)*len(fontMatchRules)) for i, name := range names { - name = fontNormalizeName(name) + name = basefont.NormalizeName(name) for j, rule := range fontMatchRules { levels[i*len(fontMatchRules)+j] = fontRuleMatchLevel(rule, name) } @@ -400,22 +374,6 @@ func fontRuleMatchLevel(rule fontMatchRule, name string) int { return level } -// 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 := fontNormalizeName(name) - if key == "" || seen[key] { - return names - } - seen[key] = true - return append(names, name) -} - // appendFontPattern 追加字体文件匹配模式 // 入参: patterns 匹配模式列表, seen 去重映射, pattern 匹配模式 // 返回: []string 匹配模式列表 diff --git a/ofdgo_font_metrics.go b/ofdgo_font_metrics.go index 5b402ed..caf46b5 100644 --- a/ofdgo_font_metrics.go +++ b/ofdgo_font_metrics.go @@ -17,6 +17,8 @@ package ofdgo import ( "fmt" "sync" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // sfntGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量 @@ -24,9 +26,9 @@ const sfntGlyphBoundsLimit = 4096 // sfntFontMetrics 保留默认字体度量,写出时不更新原始时间 type sfntFontMetrics struct { - *sfntFont + *basefont.SFNT *fontShaper - outlines *sfntOutliner + outlines *basefont.Outliner bounds *sfntFontBounds } @@ -42,15 +44,6 @@ type sfntGlyphSize struct { size float64 } -// glyphPather 接收字体设计坐标中的轮廓指令 -type glyphPather interface { - MoveTo(float64, float64) - LineTo(float64, float64) - QuadTo(float64, float64, float64, float64) - CubeTo(float64, float64, float64, float64, float64, float64) - Close() -} - // glyphGeometry 将字体轮廓转换为纵轴向下的几何路径 type glyphGeometry struct{ path GeometryPath } @@ -92,15 +85,15 @@ func (b SFNTBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo // 入参: data 字体数据 // 返回: FontMetrics 字体度量, error 解析错误 func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) { - data, err := defaultCFF2Font(data) + data, err := basefont.DefaultCFF2Font(data) if err != nil { return nil, err } - sfnt, err := parseSFNTFont(data) + sfnt, err := basefont.ParseSFNT(data) if err != nil { return nil, err } - metrics := &sfntFontMetrics{sfntFont: sfnt, outlines: &sfntOutliner{font: sfnt}, bounds: &sfntFontBounds{}} + metrics := &sfntFontMetrics{SFNT: sfnt, outlines: &basefont.Outliner{Font: sfnt}, bounds: &sfntFontBounds{}} metrics.fontShaper = &fontShaper{metrics: metrics} return metrics, nil } @@ -108,7 +101,7 @@ func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) { // Write 返回字体数据,不修改字体元信息 // 返回: []byte 独立SFNT数据 func (f sfntFontMetrics) Write() []byte { - return f.sfntFont.Write() + return f.SFNT.Write() } // GlyphOutline 返回基线原点、纵轴向下的字形轮廓 @@ -119,7 +112,7 @@ func (f sfntFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, return nil, fmt.Errorf("invalid glyph or size") } path := &glyphGeometry{} - if err := f.outlines.path(path, glyph, size/float64(f.UnitsPerEm())); err != nil { + if err := f.outlines.Path(path, glyph, size/float64(f.UnitsPerEm())); err != nil { return nil, err } return path.path, nil @@ -161,7 +154,7 @@ func (f sfntFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error) { // 入参: glyph 字形编号 // 返回: error 恢复原因,无恢复时为空 func (f sfntFontMetrics) GlyphWarning(glyph uint16) error { - return f.outlines.glyphWarning(glyph) + return f.outlines.GlyphWarning(glyph) } // GlyphOutlines 批量提取字形轮廓,重复编号只解析一次 diff --git a/ofdgo_font_name.go b/ofdgo_font_name.go index 30b88bf..89c3dc0 100644 --- a/ofdgo_font_name.go +++ b/ofdgo_font_name.go @@ -16,23 +16,10 @@ package ofdgo import ( "bytes" - "encoding/binary" - "io" - "unicode/utf16" - "golang.org/x/text/encoding/charmap" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) -// fontNameRecord 字体名称表记录 -type fontNameRecord struct { - Platform uint16 - Encoding uint16 - Language uint16 - Name uint16 - Length uint16 - Offset uint16 -} - // appendFontFileNames 建立各字体的内部名称与文件别名匹配候选 // 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称 // 返回: []fontFileCandidate 字体候选列表 @@ -43,7 +30,7 @@ func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]st file.face = face file.names = make([]string, len(names)) for i, name := range names { - file.names[i] = fontNormalizeName(name) + file.names[i] = basefont.NormalizeName(name) } for _, name := range file.names { file.normalized = name @@ -58,37 +45,11 @@ func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]st return candidates } -// fontFileNames 按字体索引读取字体或集合中各项的名称 -// 入参: file 字体数据 -// 返回: [][]string 各字体名称及带样式的字体族名称 -func fontFileNames(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 -} - // fontCollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项 // 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体 // 返回: int 零起始字体索引 func fontCollectionIndex(data []byte, names []string, bold, italic bool) int { - return fontNameIndex(fontFileNames(bytes.NewReader(data)), names, bold, italic) + return fontNameIndex(basefont.FileNames(bytes.NewReader(data)), names, bold, italic) } // fontNameIndex 按名称与样式选择字体索引,未匹配时保留首项 @@ -101,109 +62,3 @@ func fontNameIndex(faces [][]string, names []string, bold, italic bool) int { } return 0 } - -// 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 fontNamesFromTable(data) - } - return nil -} - -// fontNamesFromTable 解析字体名称表 -// 入参: data 名称表数据 -// 返回: []string 字体名称列表 -func fontNamesFromTable(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 -} diff --git a/ofdgo_font_resource_sfnt.go b/ofdgo_font_resource_sfnt.go index 26fa06f..97115ef 100644 --- a/ofdgo_font_resource_sfnt.go +++ b/ofdgo_font_resource_sfnt.go @@ -15,20 +15,17 @@ package ofdgo import ( - "bytes" "encoding/binary" "fmt" - "maps" - "slices" - "github.com/go-text/typesetting/font/opentype/tables" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理 type SFNTBackend struct{} // sfntResourceMetrics 隔离资源解析器的度量实现 -type sfntResourceMetrics struct{ *sfntFont } +type sfntResourceMetrics struct{ *basefont.SFNT } // Name 返回资源后端标识 // 返回: string 后端标识 @@ -43,15 +40,15 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er if err != nil { return nil, err } - data, err = defaultCFF2Font(data) + data, err = basefont.DefaultCFF2Font(data) if err != nil { return nil, err } - sfnt, err := parseSFNTFont(data) + sfnt, err := basefont.ParseSFNT(data) if err != nil { return nil, err } - name := sfntFontName(sfnt, 6, 4, 1) + name := basefont.SFNTName(sfnt, 6, 4, 1) if name == "" { return nil, fmt.Errorf("font has no name") } @@ -61,8 +58,8 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er } return &FontResource{ FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension, - Font: Font{FontName: name, FamilyName: sfntFontName(sfnt, 16, 1), Charset: "unicode", Bold: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&1 != 0, Italic: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&2 != 0, FixedWidth: len(sfnt.Tables["post"]) >= 16 && binary.BigEndian.Uint32(sfnt.Tables["post"][12:]) != 0}, - CanSubset: sfntSubsettable(sfnt), + Font: Font{FontName: name, FamilyName: basefont.SFNTName(sfnt, 16, 1), Charset: "unicode", Bold: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&1 != 0, Italic: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&2 != 0, FixedWidth: len(sfnt.Tables["post"]) >= 16 && binary.BigEndian.Uint32(sfnt.Tables["post"][12:]) != 0}, + CanSubset: basefont.Subsettable(sfnt), }, nil } @@ -70,432 +67,23 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er // 入参: data 字体数据, glyphs 用字, preserve 是否保留编号 // 返回: []byte 子集, error 裁剪错误 func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) { - return subsetSFNTFont(data, glyphs, preserve) + return basefont.SubsetSFNT(data, glyphs, preserve) } // SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源 // 入参: data 字体数据, usage 用字信息 // 返回: []byte 子集, error 裁剪错误 func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) { - u := newEditorFontUsage() - u.unsafe = usage.Unsafe + u := &basefont.Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool), Unsafe: usage.Unsafe} for _, char := range usage.Characters { - u.chars[char] = true + u.Characters[char] = true } for _, glyph := range usage.Glyphs { - u.glyphs[glyph] = true + u.Glyphs[glyph] = true } - return subsetSourceSFNTFont(data, u), nil + return basefont.SubsetSourceSFNT(data, u), nil } // Write 返回未改动元信息的字体数据 // 返回: []byte 字体数据 -func (f sfntResourceMetrics) Write() []byte { return f.sfntFont.Write() } - -// sfntSubsettable 判断默认资源后端能否无损裁剪该字体 -// 入参: sfnt 已解析字体 -// 返回: bool 是否支持裁剪 -func sfntSubsettable(sfnt *sfntFont) 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 *sfntFont) uint16 { - if table := sfnt.Tables["OS/2"]; len(table) >= 10 { - return binary.BigEndian.Uint16(table[8:10]) - } - return 0 -} - -// validateFontEmbedding 校验字体是否允许作为可编辑轮廓字体嵌入 -// 入参: data 独立OpenType字体 -// 返回: error 字体解析或嵌入限制 -func validateFontEmbedding(data []byte) error { - sfnt, err := parseSFNTFont(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 -} - -// sfntFontName 获取字体中的名称 -// 入参: sfnt 字体, names 名称类型,按顺序查找 -// 返回: string 字体名称 -func sfntFontName(sfnt *sfntFont, 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 "" -} - -// subsetSFNTFont 按实际用字裁剪字体,保留字符映射、名称及度量 -// 入参: data 字体数据, glyphs 已排序的字形编号,包含0, mapped 是否保留显式字形编号 -// 返回: []byte 字体子集, error 错误信息 -func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) { - data, err := defaultCFF2Font(data) - if err != nil { - return nil, err - } - sfnt, err := parseSFNTFont(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 := newEditorFontUsage() - for _, id := range glyphs { - usage.glyphs[id] = true - for _, char := range sfnt.glyphCharacters(id) { - usage.chars[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 := subsetSourceSFNTFont(fixed, usage); subset != nil { - return subset, nil - } - return fixed, nil - } - return subsetCompactSFNTFont(sfnt, glyphs) -} - -// subsetSourceSFNTFont 裁剪静态TrueType或CFF原有字体,保留字形编号、组合依赖、度量和提示指令 -// 字形替换结果一并保留,集合、可变、彩色和未知表保持原样 -// 入参: data 字体数据, usage 全包用字记录 -// 返回: []byte 更小的字体子集,无确定收益时为空 -func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { - if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) { - return nil - } - sfnt, err := parseSFNTFont(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.chars)) - for char := range usage.chars { - 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.chars, 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 *sfntFont, 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) -} +func (f sfntResourceMetrics) Write() []byte { return f.SFNT.Write() } diff --git a/ofdgo_font_source.go b/ofdgo_font_source.go index da79a59..cf53f7b 100644 --- a/ofdgo_font_source.go +++ b/ofdgo_font_source.go @@ -26,6 +26,8 @@ import ( "runtime" "sort" "sync" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // 字体来源:文件、文件系统和系统字体 @@ -112,10 +114,10 @@ func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, data, err = os.ReadFile(key.name) } if err == nil && bytes.HasPrefix(data, []byte("ttcf")) { - data, err = extractCollectionFont(data, key.face) + data, err = basefont.ExtractCollectionFont(data, key.face) } if err == nil { - data, _, err = symbolFontProgram(context.Background(), data, "") + data, _, err = basefont.SymbolProgram(context.Background(), data, "") } r.fontSourcesCache.data[key] = fontSourceData{data, err} return data, err @@ -229,7 +231,7 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource { for _, name := range fontSystemNames(names...) { source := fontSource{kind: fontSourceSystem, name: name, exact: true} for i, requested := range names { - if fontNormalizeName(name) == fontNormalizeName(requested) { + if basefont.NormalizeName(name) == basefont.NormalizeName(requested) { source.priority = (i - len(names)) * 2 break } @@ -294,7 +296,7 @@ func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) if err != nil { continue } - candidates = appendFontFileNames(candidates, i, fontFileNames(file)) + candidates = appendFontFileNames(candidates, i, basefont.FileNames(file)) file.Close() } r.fontSourcesCache.directories[dir] = candidates @@ -324,7 +326,7 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f data, _ := io.ReadAll(file) reader = bytes.NewReader(data) } - candidates = appendFontFileNames(candidates, i, fontFileNames(reader)) + candidates = appendFontFileNames(candidates, i, basefont.FileNames(reader)) file.Close() } r.fontSourcesCache.filesystems[index] = candidates @@ -373,7 +375,7 @@ func indexSystemFonts() []fontFileCandidate { seen := make(map[string]bool) for _, dir := range systemFontDirs() { _ = filepath.WalkDir(dir, func(name string, entry fs.DirEntry, err error) error { - if err == nil && !entry.IsDir() && isFontFileName(name) && !seen[name] { + if err == nil && !entry.IsDir() && basefont.IsFontFileName(name) && !seen[name] { seen[name] = true files = append(files, name) } @@ -386,7 +388,7 @@ func indexSystemFonts() []fontFileCandidate { if err != nil { continue } - candidates = appendFontFileNames(candidates, i, fontFileNames(file)) + candidates = appendFontFileNames(candidates, i, basefont.FileNames(file)) file.Close() } return candidates diff --git a/ofdgo_image.go b/ofdgo_image.go index c2e8ed2..fdfdb13 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -15,386 +15,13 @@ package ofdgo 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" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) -// 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 := imageNRGBAAt(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 := imageNRGBA64At(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 := imageNRGBA64At(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)} -} - -// decodeImageData 解码图片数据 -// 入参: data 图片数据 -// 返回: image.Image 图片对象, string 图片格式, error 错误信息 -func decodeImageData(data []byte) (image.Image, string, error) { - return decodeImageDataContext(context.Background(), data) -} - -// decodeImageDataContext 解码图片并传递取消状态,PNG和JPEG仍保留惰性像素解码 -// 入参: ctx 取消上下文, data 只读图片编码 -// 返回: image.Image 图片对象, string 图片格式, error 解码或取消错误 -func decodeImageDataContext(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 isJPEGData(data) || isPNGData(data) { - img, err := newEncodedImageContext(ctx, data) - if err != nil { - return nil, "", err - } - return img, img.format, nil - } - if isTIFFData(data) { - layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) - if err != nil { - return nil, "tiff", err - } - img, err := decodeTIFFImage(ctx, data, layout) - return img, "tiff", err - } - img, format, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) - if err == nil { - return img, normalizeSealType(format), nil - } - if ctx.Err() != nil { - return nil, "", ctx.Err() - } - if img, err := decodeBMPImageContext(ctx, data); err == nil { - return img, "bmp", nil - } - return nil, "", err -} - -// decodeImagePixelsContext 完整解码图片,确保像素可读取并传递取消状态 -// 入参: ctx 取消上下文, data 图片编码数据 -// 返回: image.Image 解码像素, string 图片格式, error 解码或取消错误 -func decodeImagePixelsContext(ctx context.Context, data []byte) (image.Image, string, error) { - img, format, err := decodeImageDataContext(ctx, data) - if err != nil { - return nil, format, err - } - pixels, err := imagePixelDataContext(ctx, img) - return pixels, format, err -} - -// isJPEGData 判断是否为JPEG图片数据 -// 入参: data 图片数据 -// 返回: bool 是否为JPEG图片数据 -func isJPEGData(data []byte) bool { - return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8 -} - -// isPNGData 判断是否为PNG图片数据 -// 入参: data 图片数据 -// 返回: bool 是否为PNG图片数据 -func isPNGData(data []byte) bool { - return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n")) -} - -// decodeImageConfigData 解码图片尺寸 -// 入参: data 图片数据 -// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息 -func decodeImageConfigData(data []byte) (image.Config, string, error) { - return decodeImageConfigDataContext(context.Background(), data) -} - -// decodeImageConfigDataContext 读取图片尺寸和颜色模型,传递布局解析中的取消状态 -// 入参: ctx 取消上下文, data 图片数据 -// 返回: image.Config 图片配置, string 图片格式, error 读取或取消错误 -func decodeImageConfigDataContext(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 isTIFFData(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(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) - if err == nil { - return cfg, normalizeSealType(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 decodeBMPImageContext(context.Background(), data) -} - -// decodeBMPImageContext 解码BMP真彩色像素,逐行检查取消 -// 入参: ctx 取消上下文, data 图片数据 -// 返回: image.Image 图片对象, error 格式或取消错误 -func decodeBMPImageContext(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 -} - // imageWithEffects 合并同尺寸二值蒙版和对象透明度,保留原图采样精度及只读像素 // 入参: ctx 取消上下文,nil使用后台上下文, img 原图, mask 二值蒙版,可为空, alpha 对象透明度,可为空 // 返回: image.Image 处理后的图片, error 蒙版、解码或取消错误 @@ -412,14 +39,14 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i if img == nil || mask == nil && a == 255 { return img, nil } - source, err := imagePixelDataContext(ctx, img) + source, err := baseimage.PixelDataContext(ctx, img) if err != nil { return nil, err } bounds := source.Bounds() var maskBounds image.Rectangle if mask != nil { - mask, err = imagePixelDataContext(ctx, mask) + mask, err = baseimage.PixelDataContext(ctx, mask) if err != nil { return nil, err } @@ -428,7 +55,7 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i return nil, fmt.Errorf("image mask dimensions differ from source") } } - if imageHighPrecision(source) { + if baseimage.HighPrecision(source) { out := image.NewNRGBA64(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { @@ -445,7 +72,7 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i } opacity *= int(value) / 255 } - pixel := imageNRGBA64At(source, x, y) + pixel := baseimage.NRGBA64At(source, x, y) pixel.A = uint16(uint32(pixel.A) * uint32(opacity) / 255) out.SetNRGBA64(x, y, pixel) } @@ -471,7 +98,7 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i return nil, err } } - out.SetNRGBA(x, y, imageNRGBAAt(source, x, y)) + out.SetNRGBA(x, y, baseimage.NRGBAAt(source, x, y)) } } for x := 0; x < bounds.Dx(); x++ { @@ -521,7 +148,7 @@ func validateImageMask(ctx context.Context, mask image.Image, size image.Point) if mask.Bounds().Size() != size || size.X <= 0 || size.Y <= 0 { return fmt.Errorf("image mask dimensions differ from source") } - mask, err := imagePixelDataContext(ctx, mask) + mask, err := baseimage.PixelDataContext(ctx, mask) if err != nil { return err } @@ -540,321 +167,3 @@ func validateImageMask(ctx context.Context, mask image.Image, size image.Point) } return ctx.Err() } - -// imagePixelSource 获取图片像素源 -// 入参: img 图片对象 -// 返回: image.Image 图片像素源 -func imagePixelSource(img image.Image) image.Image { - if decoded, err := imagePixelData(img); err == nil { - return decoded - } - return img -} - -// imagePixelData 获取像素并保留惰性解码错误,供绘制和像素处理入口校验 -// 入参: img 图片对象 -// 返回: image.Image 解码像素, error 解码错误 -func imagePixelData(img image.Image) (image.Image, error) { - if src, ok := img.(interface{ Image() (image.Image, error) }); ok { - return src.Image() - } - return img, nil -} - -// imagePixelDataContext 获取像素并传递取消状态,兼容不接收上下文的图像实现 -// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象 -// 返回: image.Image 解码像素, error 解码或取消错误 -func imagePixelDataContext(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 := imagePixelData(img) - if cancelErr := ctx.Err(); cancelErr != nil { - return nil, cancelErr - } - return pixels, err -} - -// imageGrayAt 获取图片灰度像素 -// 入参: img 图片对象, x X坐标, y Y坐标 -// 返回: color.Gray 灰度像素 -func imageGrayAt(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) -} - -// imageNRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量 -// 入参: img 图片对象, x X坐标, y Y坐标 -// 返回: color.NRGBA NRGBA像素 -func imageNRGBAAt(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) -} - -// imageAlphaAt 读取十六位透明度,原生图像避免颜色装箱 -// 入参: img 图像, x 横坐标, y 纵坐标 -// 返回: uint32 十六位透明度 -func imageAlphaAt(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 -} - -// imageHighPrecision 判断颜色模型及调色板是否需要保留十六位采样 -// 入参: img 图片对象 -// 返回: bool 是否使用十六位颜色或透明度 -func imageHighPrecision(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 -} - -// imageNRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱 -// 入参: img 图片对象, x X坐标, y Y坐标 -// 返回: color.NRGBA64 NRGBA64像素 -func imageNRGBA64At(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 := imageNRGBAAt(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 -} - -// imageWithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度 -// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象 -// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误 -func imageWithTransparentEdge(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 := imagePixelDataContext(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 imageAlphaAt(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 imageHighPrecision(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 -} diff --git a/ofdgo_image_resource.go b/ofdgo_image_resource.go index 2e87651..a910632 100644 --- a/ofdgo_image_resource.go +++ b/ofdgo_image_resource.go @@ -15,17 +15,12 @@ package ofdgo import ( - "bytes" "context" - "errors" "fmt" "image" "image/color" - _ "image/jpeg" - _ "image/png" - "io" - "sync" - "sync/atomic" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // imageCacheLimit 限制图片缓存的编码数据及预计解码内存 @@ -33,20 +28,7 @@ const imageCacheLimit = 64 << 20 // 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 -} +type EncodedImage = baseimage.EncodedImage // resampledImageMask 按替换图尺寸读取已校验的二值蒙版,不分配缩放像素 type resampledImageMask struct { @@ -55,108 +37,6 @@ type resampledImageMask struct { bounds image.Rectangle } -// 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(imageInput{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) -} - // ImageResource 按资源标识读取只读图片,PNG和JPEG保留原始编码并惰性解码 // 应先读取源页面以加载页面资源,返回EncodedImage时通过Image检查像素解码错误 // 入参: id 图片资源标识 @@ -176,7 +56,7 @@ func (r *Renderer) imageObjectResource(object ImageObject) (image.Image, error) read := func(id string) (image.Image, error) { img, err := r.ImageResource(id) if err == nil && (r.decodeImages || object.Substitution != "") { - pixels, decodeErr := imagePixelDataContext(r.pageContext, img) + pixels, decodeErr := baseimage.PixelDataContext(r.pageContext, img) err = decodeErr if r.decodeImages { img = pixels @@ -239,7 +119,7 @@ func (r *Renderer) imageObjectMask(object ImageObject, target image.Image) (imag if err := validateImageMask(ctx, mask, original.Bounds().Size()); err != nil { return nil, err } - mask, err = imagePixelDataContext(ctx, mask) + mask, err = baseimage.PixelDataContext(ctx, mask) if err != nil { return nil, err } @@ -264,7 +144,7 @@ func (s *resampledImageMask) GrayAt(x, y int) color.Gray { bounds, size := s.original, s.bounds.Size() sx := bounds.Min.X + int((uint64(x-s.bounds.Min.X)*uint64(bounds.Dx())+uint64(bounds.Dx()/2))/uint64(size.X)) sy := bounds.Min.Y + int((uint64(y-s.bounds.Min.Y)*uint64(bounds.Dy())+uint64(bounds.Dy()/2))/uint64(size.Y)) - return imageGrayAt(s.source, sx, sy) + return baseimage.GrayAt(s.source, sx, sy) } // At 返回采样后的二值灰度 @@ -272,6 +152,10 @@ func (s *resampledImageMask) GrayAt(x, y int) color.Gray { // 返回: color.Color 像素颜色 func (s *resampledImageMask) At(x, y int) color.Color { return s.GrayAt(x, y) } +// BackingImage 返回共享只读蒙版,供图像缓存计算底层存储 +// 返回: image.Image 共享只读蒙版 +func (s *resampledImageMask) BackingImage() image.Image { return s.source } + // RGBA64At 返回不透明的十六位灰度颜色,供蒙版合成直接读取 // 入参: x 横坐标, y 纵坐标 // 返回: color.RGBA64 像素颜色 @@ -280,30 +164,6 @@ func (s *resampledImageMask) RGBA64At(x, y int) color.RGBA64 { return color.RGBA64{R: value, G: value, B: value, A: 65535} } -// 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(imageInput{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 -} - // cachedImageResource 复用原始图片资源,不依赖输出后端 // 入参: resPath 图片资源路径 // 返回: image.Image 图片对象, error 读取错误 @@ -321,114 +181,11 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) { if err != nil { return nil, err } - r.imageCache.put(resPath, img, imageResourceCost(img, r.imageCache.limit)) + r.imageCache.put(resPath, img, baseimage.StorageCost(img, r.imageCache.limit)) } return img, nil } -// imageResourceCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量 -// 入参: img 图片资源, limit 缓存预算 -// 返回: int 估算字节数或超预算标记 -func imageResourceCost(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] = imageResourceCost(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 := imageResourceCost(block, limit-cost) - if size > limit-cost { - return limit + 1 - } - cost += size - } - case *orientedTIFFImage: - buffers[0] = imageResourceCost(source.source, limit-cost) - case *maskedTIFFImage: - buffers[0] = imageResourceCost(source.source, limit-cost) - buffers[1] = imageResourceCost(source.mask, limit-cost) - case *resampledImageMask: - buffers[0] = imageResourceCost(source.source, limit-cost) - case *transparentEdgeImage: - buffers[0] = imageResourceCost(source.source, limit-cost) - case *transparentEdgeImage64: - buffers[0] = imageResourceCost(source.source, limit-cost) - case *rasterPaddedImage: - buffers[0] = imageResourceCost(source.source, 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) -} - // decodeImageResource 读取或复用图片资源,按输出需要解码像素 // 入参: resPath 图片资源路径 // 返回: image.Image 图片对象, error 读取或解码错误 @@ -438,7 +195,7 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) { return nil, err } if r.decodeImages { - return imagePixelDataContext(r.pageContext, img) + return baseimage.PixelDataContext(r.pageContext, img) } return img, nil } @@ -455,6 +212,11 @@ func (r *Renderer) readImageResource(resPath string) (image.Image, error) { if err != nil { return nil, err } - img, _, err := decodeImageDataContext(ctx, data) + img, _, err := baseimage.DecodeContext(ctx, data) return img, err } + +// NewEncodedImage 复制输入数据并读取PNG或JPEG头信息,不解码像素 +// 入参: data 原始编码 +// 返回: *EncodedImage 惰性图片, error 头信息或格式错误 +func NewEncodedImage(data []byte) (*EncodedImage, error) { return baseimage.NewEncodedImage(data) } diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index 80870a5..eeeff6d 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -21,6 +21,7 @@ import ( "math" "runtime" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" "github.com/xiaoqidun/pdfgo" ) @@ -417,9 +418,9 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg externalNames = make(map[string]bool) } name := pdfEmbeddedFontName(mark.Font.Name) - if !externalNames[fontNormalizeName(name)] { + if !externalNames[basefont.NormalizeName(name)] { for _, candidate := range fontExactCandidateNames(name) { - externalNames[fontNormalizeName(candidate)] = true + externalNames[basefont.NormalizeName(candidate)] = true } } } else { @@ -513,7 +514,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg return nil, err } for font := range embeddedFonts { - if !externalNames[fontNormalizeName(pdfEmbeddedFontName(font.Name))] { + if !externalNames[basefont.NormalizeName(pdfEmbeddedFontName(font.Name))] { continue } if err := p.ctx.Err(); err != nil { diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index f8e88f2..d0065df 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -20,6 +20,7 @@ import ( "image" "math" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" ) @@ -248,7 +249,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf return err } for x := region.Min.X; x < region.Max.X; x++ { - a := imageAlphaAt(coverage, x, y) + a := baseimage.AlphaAt(coverage, x, y) if a == 0 { continue } diff --git a/ofdgo_pdf_composite_shading.go b/ofdgo_pdf_composite_shading.go index 956455c..1eab560 100644 --- a/ofdgo_pdf_composite_shading.go +++ b/ofdgo_pdf_composite_shading.go @@ -17,6 +17,7 @@ package ofdgo import ( "math" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" ) @@ -99,7 +100,7 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo } shape := 0.0 if coverage != nil { - a := imageAlphaAt(coverage, i%c.width, i/c.width) + a := baseimage.AlphaAt(coverage, i%c.width, i/c.width) shape = float64(a) / 65535 } if nodes[0].image != nil && shape != 0 { diff --git a/ofdgo_pdf_font.go b/ofdgo_pdf_font.go index 5cc95e9..7fa0945 100644 --- a/ofdgo_pdf_font.go +++ b/ofdgo_pdf_font.go @@ -23,6 +23,7 @@ import ( "unicode/utf16" "unicode/utf8" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" "github.com/xiaoqidun/pdfgo" ) @@ -137,22 +138,22 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) if cached := p.fonts[source]; cached != nil { return cached, nil } - var type1 *type1Program + var type1 *basefont.Type1Program result := &pdfImportedFont{} if source.ProgramType == "FontFile" { - parsed, err := parseType1Program(source.Program) + parsed, err := basefont.ParseType1Program(source.Program) if err != nil { return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) } type1 = &parsed - result.type1Glyphs = parsed.glyphIDs() + result.type1Glyphs = parsed.GlyphIDs() } program, repair, err := pdfFontProgram(source, type1) if err != nil { return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) } if repair.locations && p.warning == nil { - return nil, fmt.Errorf("PDF font %s program: %w", source.Name, errTTInvalidLocations) + return nil, fmt.Errorf("PDF font %s program: %w", source.Name, basefont.ErrTTInvalidLocations) } if p.editor.backends.FontResources == nil { return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable) @@ -206,7 +207,7 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { p.symbolFonts[source] = nil return nil, nil } - program, names, err := symbolFontProgram(p.ctx, resolved.Data, name) + program, names, err := basefont.SymbolProgram(p.ctx, resolved.Data, name) if err != nil { return nil, err } @@ -235,19 +236,19 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { // 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 // 返回: []byte 封装后的字体数据, pdfFontRepair 字体修复范围, error 错误信息 -func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRepair, error) { +func pdfFontProgram(source *pdfgo.Font, type1 *basefont.Type1Program) ([]byte, pdfFontRepair, error) { var repair pdfFontRepair program := source.Program if source.ProgramType == "FontFile" { if type1 == nil { - parsed, err := parseType1Program(program) + parsed, err := basefont.ParseType1Program(program) if err != nil { return nil, repair, err } type1 = &parsed } var err error - program, err = type1.toCFF(pdfFontIdentity(source)) + program, err = type1.ToCFF(pdfFontIdentity(source)) if err != nil { return nil, repair, err } @@ -255,12 +256,12 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4 if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF { var err error - program, _, err = wrapCFFToOTF(program) + program, _, err = basefont.WrapCFFToOTF(program) if err != nil { return nil, repair, err } } - tables, err := fontFileTables(program, 0) + tables, err := basefont.FileTables(program, 0) if err != nil { return nil, repair, err } @@ -271,7 +272,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep if count == 0 { return nil, repair, fmt.Errorf("embedded PDF font has no glyphs") } - repair.locations, err = normalizeTrueTypeLocations(tables) + repair.locations, err = basefont.NormalizeTrueTypeLocations(tables) if err != nil { return nil, repair, err } @@ -281,11 +282,11 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep changed = true } if cff := tables["CFF "]; len(cff) != 0 { - sanitized, err := sanitizeCFF(cff) + sanitized, err := basefont.SanitizeCFF(cff) if err != nil { return nil, repair, err } - normalized, err := normalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20])) + normalized, err := basefont.NormalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20])) if err != nil { return nil, repair, err } @@ -318,7 +319,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep } invalidCmap := false if source.Subtype == "TrueType" || source.Subtype == "Type0" { - mapping := parseCmapMappings(tables["cmap"]) + mapping := basefont.ParseCmapMappings(tables["cmap"]) for char, glyph := range mapping { if glyph >= count { delete(mapping, char) @@ -326,7 +327,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep } } if invalidCmap && source.Subtype == "TrueType" { - tables["cmap"] = buildCmapTable(count, mapping) + tables["cmap"] = basefont.BuildCmapTable(count, mapping) changed = true } } @@ -340,11 +341,11 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count) } } - addPackedGlyphMapping(mapping, count) - tables["cmap"] = buildCmapTable(count, mapping) + basefont.AddPackedGlyphMapping(mapping, count) + tables["cmap"] = basefont.BuildCmapTable(count, mapping) changed = true } - if len(fontNamesFromTable(tables["name"])) == 0 { + if len(basefont.NamesFromTable(tables["name"])) == 0 { fontName := pdfFontIdentity(source) name := utf16.Encode([]rune(fontName)) if len(name) > 32767 { @@ -368,12 +369,12 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep changed = true } if len(tables["post"]) == 0 { - tables["post"] = buildPostTable() + tables["post"] = basefont.BuildPostTable() changed = true } if len(tables["OS/2"]) == 0 { hhea := tables["hhea"] - tables["OS/2"] = buildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8]))) + tables["OS/2"] = basefont.BuildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8]))) changed = true } for _, tag := range []string{"cvt ", "fpgm", "prep"} { @@ -385,7 +386,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep if !changed && !pdfSFNTMissingPadding(program) { return program, repair, nil } - result, err := serializeOTF(tables) + result, err := basefont.SerializeOTF(tables) return result, repair, err } diff --git a/ofdgo_pdf_mask.go b/ofdgo_pdf_mask.go index 88e6b60..f99afac 100644 --- a/ofdgo_pdf_mask.go +++ b/ofdgo_pdf_mask.go @@ -20,6 +20,7 @@ import ( "image" "math" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" ) @@ -66,7 +67,7 @@ func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.Sof } } px, py := out.Rect.Min.X+x+i%width, out.Rect.Min.Y+y+i/width - pixel := imageNRGBAAt(source, px, py) + pixel := baseimage.NRGBAAt(source, px, py) pixel.A = uint8(math.Round(float64(pixel.A) * opacity)) out.SetNRGBA(px, py, pixel) } diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index dab7c32..6882407 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -25,6 +25,7 @@ import ( "strconv" "unicode/utf8" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" ) @@ -77,7 +78,7 @@ func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 { if !image.Pt(x, y).In(s.Bounds()) { return color.NRGBA64{} } - gray := imageNRGBA64At(s.source, x, y) + gray := baseimage.NRGBA64At(s.source, x, y) pixel := s.fill pixel.A = uint16(65535 - gray.R) return pixel diff --git a/ofdgo_raster_image.go b/ofdgo_raster_image.go index 064037b..09def0a 100644 --- a/ofdgo_raster_image.go +++ b/ofdgo_raster_image.go @@ -18,6 +18,7 @@ import ( "image" "math" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "golang.org/x/image/draw" "golang.org/x/image/math/f64" ) @@ -101,7 +102,7 @@ func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Im return nil } layer := image.NewRGBA64(bounds) - drawRasterImagePixels(layer, imagePixelSource(src), m, draw.Src) + drawRasterImagePixels(layer, baseimage.PixelSource(src), m, draw.Src) draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over) return nil } diff --git a/ofdgo_render_cache.go b/ofdgo_render_cache.go index d20f80e..c080257 100644 --- a/ofdgo_render_cache.go +++ b/ofdgo_render_cache.go @@ -18,6 +18,8 @@ import ( "container/list" "image" "reflect" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源 @@ -113,7 +115,7 @@ func rasterCommandsCost(commands []RasterCommand, limit int) int { } images[command.Image] = true } - part = imageResourceCost(command.Image, limit-cost) + part = baseimage.StorageCost(command.Image, limit-cost) if part > limit-cost { return limit + 1 } diff --git a/ofdgo_render_compile_image.go b/ofdgo_render_compile_image.go index 5a74b50..b6434cc 100644 --- a/ofdgo_render_compile_image.go +++ b/ofdgo_render_compile_image.go @@ -21,6 +21,8 @@ import ( "image" "math" "slices" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索 @@ -225,7 +227,7 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error { } return nil } - img, _, err := decodeImageDataContext(ctx, stamp.Data) + img, _, err := baseimage.DecodeContext(ctx, stamp.Data) if err != nil { return err } @@ -236,7 +238,7 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error { // 入参: img 印章图像, stamp 印章 // 返回: error 编译错误 func (c *semanticCompiler) stampImage(img image.Image, stamp Stamp) error { - pixels, err := imagePixelDataContext(c.renderer.pageContext, img) + pixels, err := baseimage.PixelDataContext(c.renderer.pageContext, img) if err != nil { return err } diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index 425b2fe..d169065 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -23,6 +23,7 @@ import ( "github.com/tdewolff/canvas" "github.com/tdewolff/canvas/renderers/rasterizer" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // CanvasBackend canvas页面编译与绘制适配器,ColorSpace仅影响光栅输出 @@ -99,7 +100,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima masks := renderCache[[32]byte, *image.Alpha]{limit: 16 << 20} for i, cmd := range page.Commands { if cmd.Image != nil { - source, err := imagePixelData(cmd.Image) + source, err := baseimage.PixelData(cmd.Image) if err != nil { return nil, fmt.Errorf("canvas image %d: %w", i, err) } diff --git a/ofdgo_renderer_compile_canvas.go b/ofdgo_renderer_compile_canvas.go index 2e6058c..3d74105 100644 --- a/ofdgo_renderer_compile_canvas.go +++ b/ofdgo_renderer_compile_canvas.go @@ -20,6 +20,7 @@ import ( "image/color" "github.com/tdewolff/canvas" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象 @@ -105,7 +106,7 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) { return } var err error - img, err = imagePixelData(img) + img, err = baseimage.PixelData(img) if err != nil { c.err = err return @@ -127,10 +128,14 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) { // 返回: image.Rectangle 图像边界 func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect } +// BackingImage 返回共享的原图,用于计算底层像素存储 +// 返回: image.Image 原图 +func (s *rasterPaddedImage) BackingImage() image.Image { return s.source } + // ColorModel 按原图采样精度返回支持透明补边的预乘颜色模型 // 返回: color.Model 颜色模型 func (s *rasterPaddedImage) ColorModel() color.Model { - if imageHighPrecision(s.source) { + if baseimage.HighPrecision(s.source) { return color.RGBA64Model } return color.RGBAModel diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index 25dd387..b5b92f1 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -19,6 +19,7 @@ import ( "sync" "github.com/tdewolff/canvas" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态 @@ -44,9 +45,9 @@ func decodeFontContainer(data []byte) ([]byte, error) { case "\x00\x01\x00\x00", "OTTO", "true", "ttcf": return data, nil case "wOFF": - return decodeWOFF(data) + return basefont.DecodeWOFF(data) case "wOF2": - decoded, handled, err := decodeWOFF2(data) + decoded, handled, err := basefont.DecodeWOFF2(data) if handled { return decoded, err } @@ -55,5 +56,5 @@ func decodeFontContainer(data []byte) ([]byte, error) { if err != nil { return nil, err } - return serializeOTF(loaded.Tables) + return basefont.SerializeOTF(loaded.Tables) } diff --git a/ofdgo_renderer_glyph_canvas.go b/ofdgo_renderer_glyph_canvas.go index b3c8e4d..7b24222 100644 --- a/ofdgo_renderer_glyph_canvas.go +++ b/ofdgo_renderer_glyph_canvas.go @@ -17,6 +17,7 @@ package ofdgo import ( "github.com/tdewolff/canvas" canvastext "github.com/tdewolff/canvas/text" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // textGlyphPathCacheKey 字形路径缓存键 @@ -52,14 +53,14 @@ func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 { // textGlyphPath 获取绘制字形路径 // 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器 // 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误 -func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *sfntOutliner) (*canvas.Path, float64, error) { +func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *basefont.Outliner) (*canvas.Path, float64, error) { p := &canvas.Path{} width := 0.0 if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF { x, y := face.XOffset, face.YOffset for _, shaped := range face.Glyphs(glyph.Text) { part := &canvas.Path{} - if err := outlines.path(part, shaped.ID, face.MmPerEm); err != nil { + if err := outlines.Path(part, shaped.ID, face.MmPerEm); err != nil { return nil, 0, err } p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset))) @@ -68,7 +69,7 @@ func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *sfntOutline width = face.MmPerEm * float64(x) } else { glyphID := uint16(glyph.GlyphID) - if err := outlines.path(p, glyphID, face.MmPerEm); err != nil { + if err := outlines.Path(p, glyphID, face.MmPerEm); err != nil { return nil, 0, err } width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) @@ -106,19 +107,17 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) ( cache := r.canvasFontOutlines() outlines, _ = cache.get(face.Font) if outlines == nil { - metrics, err := parseSFNTTables(face.Font.Tables) + metrics, err := basefont.ParseSFNTTables(face.Font.Tables) if err != nil { r.renderError = err return &canvas.Path{}, 0 } - outlines = &sfntOutliner{font: metrics} + outlines = &basefont.Outliner{Font: metrics} cost := uint64(512) for _, data := range metrics.Tables { cost += uint64(len(data)) * 8 } - for _, cmap := range metrics.cmaps { - cost += uint64(len(cmap.mapping)) * 48 - } + cost += uint64(metrics.CmapEntries()) * 48 cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1))) } r.canvasState().fontOutlines[face.Font] = outlines diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index 273a879..4766d04 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -23,6 +23,7 @@ import ( "github.com/tdewolff/canvas" canvasimage "github.com/tdewolff/canvas/image" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "golang.org/x/image/draw" ) @@ -111,7 +112,7 @@ func (r *Renderer) drawClippedImage(ctx *canvas.Context, img image.Image, clipPa if _, pdf := ctx.Renderer.(*pdfRenderer); !pdf { var pad int var err error - img, pad, err = imageWithTransparentEdge(r.pageContext, img) + img, pad, err = baseimage.WithTransparentEdge(r.pageContext, img) if err != nil { r.renderError = err return @@ -147,13 +148,13 @@ func (r *Renderer) canvasEncodedImage(img image.Image) image.Image { return encoded } var encoded *canvasimage.Image - if source.format == "jpeg" { + if source.MIME() == "image/jpeg" { encoded, _ = canvasimage.NewJPEGImage(bytes.NewReader(source.Bytes())) } else { encoded, _ = canvasimage.NewPNGImage(bytes.NewReader(source.Bytes())) } encoded.Bytes = source.Bytes() - state.images.put(source, encoded, 2*imageResourceCost(source, state.images.limit/2)) + state.images.put(source, encoded, 2*baseimage.StorageCost(source, state.images.limit/2)) return encoded } @@ -207,7 +208,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } original := img - source, err := imagePixelDataContext(r.pageContext, img) + source, err := baseimage.PixelDataContext(r.pageContext, img) if err != nil { r.renderError = err return img @@ -218,7 +219,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva if scale > 1 { nw, nh := int(float64(w)*scale), int(float64(h)*scale) var resized draw.Image - if imageHighPrecision(img) { + if baseimage.HighPrecision(img) { resized = image.NewNRGBA64(image.Rect(0, 0, nw, nh)) } else { resized = image.NewNRGBA(image.Rect(0, 0, nw, nh)) @@ -258,12 +259,12 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva r.renderError = fmt.Errorf("image clip raster dimensions differ from source") return img } - result, err = imagePixelDataContext(work, result) + result, err = baseimage.PixelDataContext(work, result) if err != nil { r.renderError = err return img } - if imageHighPrecision(img) || imageHighPrecision(result) { + if baseimage.HighPrecision(img) || baseimage.HighPrecision(result) { out := image.NewNRGBA64(bounds) maskBounds := result.Bounds() opaque := true @@ -279,8 +280,8 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } } - pixel := imageNRGBA64At(img, x, y) - alpha := imageAlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) + pixel := baseimage.NRGBA64At(img, x, y) + alpha := baseimage.AlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) opaque = opaque && alpha == 65535 pixel.A = uint16(uint32(pixel.A) * alpha / 65535) out.SetNRGBA64(x, y, pixel) @@ -384,7 +385,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } } - c := imageNRGBAAt(source, x, y) + c := baseimage.NRGBAAt(source, x, y) a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A c.A = uint8(int(c.A) * int(a) / 255) out.SetNRGBA(x, y, c) diff --git a/ofdgo_renderer_pdf_text.go b/ofdgo_renderer_pdf_text.go index fb889ea..d1406fe 100644 --- a/ofdgo_renderer_pdf_text.go +++ b/ofdgo_renderer_pdf_text.go @@ -17,6 +17,7 @@ package ofdgo import ( "github.com/tdewolff/canvas" canvastext "github.com/tdewolff/canvas/text" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" "github.com/xiaoqidun/pdfgo" ) @@ -51,7 +52,7 @@ func (r *pdfRenderer) preserveText(first int, text string) { func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas.Text, error) { font := r.fonts[face.Font] if font == nil { - loaded, err := loadCanvasFont(fontSFNTData(face.Font.Tables), 0, face.Font.Style()) + loaded, err := loadCanvasFont(basefont.SFNTData(face.Font.Tables), 0, face.Font.Style()) if err != nil { return nil, err } diff --git a/ofdgo_renderer_pdf_write.go b/ofdgo_renderer_pdf_write.go index a5f4897..00121b7 100644 --- a/ofdgo_renderer_pdf_write.go +++ b/ofdgo_renderer_pdf_write.go @@ -21,6 +21,7 @@ import ( "image" "io" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" ) @@ -164,7 +165,7 @@ func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Write if err := ctx.Err(); err != nil { return err } - pixels, err := imagePixelDataContext(ctx, original) + pixels, err := baseimage.PixelDataContext(ctx, original) if err != nil { return err } diff --git a/ofdgo_renderer_stamp_canvas.go b/ofdgo_renderer_stamp_canvas.go index 59d859d..9ad9ebe 100644 --- a/ofdgo_renderer_stamp_canvas.go +++ b/ofdgo_renderer_stamp_canvas.go @@ -20,6 +20,7 @@ import ( "image" "github.com/tdewolff/canvas" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // renderStamp 渲染印章 @@ -87,7 +88,7 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { return } if len(s.Data) > 0 { - img, _, err := decodeImageDataContext(pageContext, s.Data) + img, _, err := baseimage.DecodeContext(pageContext, s.Data) if err != nil { r.renderError = err return @@ -149,7 +150,7 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image { // renderStampImage 渲染印章图像 // 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度 func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) { - pixels, err := imagePixelDataContext(r.pageContext, img) + pixels, err := baseimage.PixelDataContext(r.pageContext, img) if err != nil { r.renderError = err return diff --git a/ofdgo_renderer_state_canvas.go b/ofdgo_renderer_state_canvas.go index bfa67ab..6d57d6b 100644 --- a/ofdgo_renderer_state_canvas.go +++ b/ofdgo_renderer_state_canvas.go @@ -18,6 +18,7 @@ import ( "image" "github.com/tdewolff/canvas" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // canvasFontKey 按字体内容、名称及样式区分可复用的字体族,不依赖文档资源编号 @@ -57,7 +58,7 @@ type canvasBackendState struct { fontMap map[string]*canvas.FontFamily svgFontCache map[*canvas.Font]SVGFont textGlyphPathCache renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue] - fontOutlines map[*canvas.Font]*sfntOutliner + fontOutlines map[*canvas.Font]*basefont.Outliner } // canvasClipIntersections 获取子渲染器共享的几何交集缓存,限制结果保留量 @@ -118,13 +119,13 @@ func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.Font } // canvasFontOutlines 获取子渲染器共享的字体轮廓解析缓存,限制字体及解析数据保留量 -// 返回: *renderCache[*canvas.Font, *sfntOutliner] 字体轮廓缓存 -func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *sfntOutliner] { +// 返回: *renderCache[*canvas.Font, *basefont.Outliner] 字体轮廓缓存 +func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *basefont.Outliner] { key := canvasOutlineCacheKey{} if state, ok := r.sharedBackendStates[key]; ok { - return state.(*renderCache[*canvas.Font, *sfntOutliner]) + return state.(*renderCache[*canvas.Font, *basefont.Outliner]) } - cache := &renderCache[*canvas.Font, *sfntOutliner]{limit: 32 << 20} + cache := &renderCache[*canvas.Font, *basefont.Outliner]{limit: 32 << 20} r.sharedBackendStates[key] = cache return cache } @@ -149,5 +150,5 @@ func (s *canvasBackendState) resetFonts() { s.fontMap = make(map[string]*canvas.FontFamily) s.svgFontCache = make(map[*canvas.Font]SVGFont) s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20} - s.fontOutlines = make(map[*canvas.Font]*sfntOutliner) + s.fontOutlines = make(map[*canvas.Font]*basefont.Outliner) } diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index c83577d..fe686a0 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -30,6 +30,7 @@ import ( "github.com/tdewolff/canvas" canvasimage "github.com/tdewolff/canvas/image" "github.com/tdewolff/canvas/renderers/svg" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" "github.com/xiaoqidun/pdfgo" ) @@ -146,7 +147,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { font := text.MostCommonFontFace().Font resource, ok := s.renderer.canvasState().svgFontCache[font] if !ok { - data := fontSFNTData(font.Tables) + data := basefont.SFNTData(font.Tables) resource = SVGFont{ Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()), Weight: font.Style().CSS(), diff --git a/ofdgo_sign.go b/ofdgo_sign.go index fce2ca6..4facfa7 100644 --- a/ofdgo_sign.go +++ b/ofdgo_sign.go @@ -22,6 +22,8 @@ import ( "encoding/xml" "fmt" "strings" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) const ( @@ -333,7 +335,7 @@ func sealMediaFromElements(elements []asn1.RawValue) (string, []byte, bool) { if len(elements) < 2 || elements[1].Tag != asn1.TagOctetString { return "", nil, false } - mediaType := normalizeSealType(sealString(elements[0])) + mediaType := baseimage.NormalizeFormat(sealString(elements[0])) if isSealMediaType(mediaType) { if mediaType == "ofd" { if data := trimOFDPackage(elements[1].Bytes); len(data) > 0 { @@ -363,22 +365,6 @@ func sealString(raw asn1.RawValue) string { return s } -// normalizeSealType 标准化印章媒体类型 -// 入参: s 原始媒体类型 -// 返回: string 标准媒体类型 -func normalizeSealType(s string) string { - switch s { - case "jpg": - return "jpeg" - case "bmp": - return "bmp" - case "jb2", "gbig2": - return "jbig2" - default: - return s - } -} - // isSealMediaType 判断是否为可渲染印章媒体类型 // 入参: s 媒体类型 // 返回: bool 是否可渲染 @@ -438,11 +424,11 @@ func zipDataEnd(data []byte) int { // 入参: data 原始数据 // 返回: string 媒体类型, []byte 图片数据 func probeImageMedia(data []byte) (string, []byte) { - _, format, err := decodeImageConfigData(data) + _, format, err := baseimage.DecodeConfigData(data) if err != nil { return "", nil } - return normalizeSealType(format), data + return baseimage.NormalizeFormat(format), data } // addStamp 添加印章到页面 diff --git a/ofdgo_sign_ses.go b/ofdgo_sign_ses.go index 9656a60..94c2a57 100644 --- a/ofdgo_sign_ses.go +++ b/ofdgo_sign_ses.go @@ -24,6 +24,7 @@ import ( "github.com/emmansun/gmsm/sm2" "github.com/emmansun/gmsm/sm3" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // SES摘要及签名算法的对象标识和ASN.1序列标签 @@ -593,7 +594,7 @@ func parseSESPicture(raw asn1.RawValue) (string, []byte, error) { if !ok || len(items) < 4 { return "", nil, fmt.Errorf("invalid ses picture") } - picType := normalizeSealType(strings.ToLower(strings.TrimSpace(asn1String(items[0])))) + picType := baseimage.NormalizeFormat(strings.ToLower(strings.TrimSpace(asn1String(items[0])))) picData, err := asn1OctetString(items[1]) if err != nil { return "", nil, err diff --git a/ofdgo_sign_write_crypto.go b/ofdgo_sign_write_crypto.go index 7601bbf..6e3e3b7 100644 --- a/ofdgo_sign_write_crypto.go +++ b/ofdgo_sign_write_crypto.go @@ -35,6 +35,7 @@ import ( "github.com/emmansun/gmsm/sm2" "github.com/emmansun/gmsm/sm3" "github.com/emmansun/gmsm/smx509" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // signatureWriteKey 已验证签署身份 @@ -157,7 +158,7 @@ func signatureWriteIdentity(options SignatureWriteOptions) (*signatureWriteKey, return nil, fmt.Errorf("invalid seal maker signature") } mediaType, media := probeSealMedia(seal.PicData) - if len(media) == 0 || mediaType != normalizeSealType(seal.PicType) { + if len(media) == 0 || mediaType != baseimage.NormalizeFormat(seal.PicType) { return nil, fmt.Errorf("invalid seal picture") } } diff --git a/ofdgo_u3d_texture_decode.go b/ofdgo_u3d_texture_decode.go index bb927ad..0b6541e 100644 --- a/ofdgo_u3d_texture_decode.go +++ b/ofdgo_u3d_texture_decode.go @@ -25,6 +25,7 @@ import ( "image/png" "io" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "golang.org/x/image/tiff" ) @@ -257,7 +258,7 @@ func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImag return err } } - value := imageNRGBA64At(img, x, y) + value := baseimage.NRGBA64At(img, x, y) current := pixels.NRGBA64At(x, y) if channels&8 != 0 { current.R = value.R