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

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

No files matched your search

+21 -21
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -30,16 +30,16 @@ import (
// cffDict 使用float64存储所有数值,以统一处理整数和实数 // cffDict 使用float64存储所有数值,以统一处理整数和实数
type cffDict map[int][]float64 type cffDict map[int][]float64
// wrapCFFToOTF 将CFF裸数据包装为OpenType字体格式 // WrapCFFToOTF 将CFF裸数据包装为OpenType字体格式
// 入参: cffData CFF字体数据 // 入参: cffData CFF字体数据
// 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息 // 返回: []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) numGlyphs, err := parseCFFAndCountGlyphs(cffData)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
mapping := getCmapFromCFF(cffData, int(numGlyphs)) mapping := getCmapFromCFF(cffData, int(numGlyphs))
sanitized, err := sanitizeCFF(cffData) sanitized, err := SanitizeCFF(cffData)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -70,10 +70,10 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
tables["maxp"] = buildCFFMaxpTable(uint16(numGlyphs)) tables["maxp"] = buildCFFMaxpTable(uint16(numGlyphs))
tables["OS/2"] = buildOS2Table() tables["OS/2"] = buildOS2Table()
tables["name"] = buildNameTable() tables["name"] = buildNameTable()
tables["post"] = buildPostTable() tables["post"] = BuildPostTable()
tables["hmtx"] = buildHmtxTable(widths) tables["hmtx"] = buildHmtxTable(widths)
tables["cmap"] = buildCmapTable(uint16(numGlyphs), mapping) tables["cmap"] = BuildCmapTable(uint16(numGlyphs), mapping)
data, err := serializeOTF(tables) data, err := SerializeOTF(tables)
return data, mapping, err return data, mapping, err
} }
@@ -81,13 +81,13 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
// 入参: data CFF字体数据 // 入参: data CFF字体数据
// 返回: []byte 标准化后的CFF数据, error 错误信息 // 返回: []byte 标准化后的CFF数据, error 错误信息
func normalizeCFFCharstrings(data []byte) ([]byte, 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时由字体矩阵确定 // 入参: data CFF字体数据, unitsPerEm 设计单位,0时由字体矩阵确定
// 返回: []byte 标准化后的CFF数据, error 错误信息 // 返回: []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) { if len(data) < 4 || int(data[2]) >= len(data) {
return nil, fmt.Errorf("invalid CFF header") return nil, fmt.Errorf("invalid CFF header")
} }
@@ -214,10 +214,10 @@ func replaceCFFCharstrings(data []byte, dict cffDict, topStart, topSize, charSta
return result return result
} }
// sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix // SanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix
// 入参: data 原始CFF数据 // 入参: data 原始CFF数据
// 返回: []byte 清洗后的CFF数据, error 错误信息 // 返回: []byte 清洗后的CFF数据, error 错误信息
func sanitizeCFF(data []byte) ([]byte, error) { func SanitizeCFF(data []byte) ([]byte, error) {
if !isBareCFFData(data) { if !isBareCFFData(data) {
return nil, fmt.Errorf("invalid CFF header") 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") return nil, fmt.Errorf("invalid CFF FDArray count")
} }
fonts := make([]type2Font, fdCount) fonts := make([]type2Font, fdCount)
var type1Fonts []*type1Program var type1Fonts []*Type1Program
if kind == 1 { if kind == 1 {
type1Fonts = make([]*type1Program, fdCount) type1Fonts = make([]*Type1Program, fdCount)
} }
privateDicts := make([]cffDict, fdCount) privateDicts := make([]cffDict, fdCount)
matrices := make([][6]float64, fdCount) matrices := make([][6]float64, fdCount)
@@ -1079,7 +1079,7 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
if gid == 0 { if gid == 0 {
continue continue
} }
mapping[packedGlyphRune(uint16(gid))] = uint16(gid) mapping[PackedGlyphRune(uint16(gid))] = uint16(gid)
if r, ok := adobeGB1CIDToUnicode(cid); ok { if r, ok := adobeGB1CIDToUnicode(cid); ok {
mapping[r] = uint16(gid) mapping[r] = uint16(gid)
} }
@@ -1091,7 +1091,7 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
if gid == 0 { if gid == 0 {
continue continue
} }
mapping[packedGlyphRune(uint16(gid))] = uint16(gid) mapping[PackedGlyphRune(uint16(gid))] = uint16(gid)
} }
return mapping return mapping
} }
@@ -1113,18 +1113,18 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
r = getUnicodeFromName(name) r = getUnicodeFromName(name)
} }
if r == 0 { 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) mapping[r] = uint16(gid)
} }
return mapping return mapping
} }
// getCFFCIDRuneMap 获取CID到包装字体字符的映射 // GetCFFCIDRuneMap 获取CID到包装字体字符的映射
// 入参: data CFF或OpenType字体数据 // 入参: data CFF或OpenType字体数据
// 返回: map[uint16]rune CID映射 // 返回: map[uint16]rune CID映射
func getCFFCIDRuneMap(data []byte) map[uint16]rune { func GetCFFCIDRuneMap(data []byte) map[uint16]rune {
cffData := getCFFData(data) cffData := getCFFData(data)
if cffData == nil { if cffData == nil {
return nil return nil
@@ -1143,7 +1143,7 @@ func getCFFCIDRuneMap(data []byte) map[uint16]rune {
} }
gidRunes := make(map[uint16]rune) gidRunes := make(map[uint16]rune)
for run, gid := range mapping { for run, gid := range mapping {
if run != packedGlyphRune(gid) { if run != PackedGlyphRune(gid) {
gidRunes[gid] = run gidRunes[gid] = run
} }
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -25,10 +25,10 @@ import (
"github.com/go-text/typesetting/font/opentype/tables" "github.com/go-text/typesetting/font/opentype/tables"
) )
// defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓 // DefaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓
// 入参: data 独立字体数据 // 入参: data 独立字体数据
// 返回: []byte 字体数据,非CFF2原样返回, error 解析或编码错误 // 返回: []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" { if len(data) < 12 || string(data[:4]) != "OTTO" {
return data, nil return data, nil
} }
@@ -42,7 +42,7 @@ func defaultCFF2Font(data []byte) (result []byte, err error) {
if !found { if !found {
return data, nil return data, nil
} }
table, err := fontFileTables(data, 0) table, err := FileTables(data, 0)
if err != nil || table["CFF2"] == nil { if err != nil || table["CFF2"] == nil {
return data, err 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"} { for _, tag := range []string{"CFF2", "fvar", "avar", "STAT", "HVAR", "VVAR", "MVAR", "DSIG"} {
delete(table, tag) delete(table, tag)
} }
return serializeOTF(table) return SerializeOTF(table)
} }
// cffOutlineProgram 将默认设计轮廓编码为无子程序的Type2字形 // cffOutlineProgram 将默认设计轮廓编码为无子程序的Type2字形
@@ -165,7 +165,7 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
if units < 16 || units > 16384 { if units < 16 || units > 16384 {
return nil, fmt.Errorf("invalid CFF design units") return nil, fmt.Errorf("invalid CFF design units")
} }
name := fontFaceInfo(table["name"], 0).PostScriptName name := FaceInfo(table["name"], 0).PostScriptName
if name == "" { if name == "" {
name = "OpenTypeInstance" name = "OpenTypeInstance"
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -112,7 +112,7 @@ func compactCFFGlyphs(data []byte, keep map[uint16]bool) ([]byte, []uint16, erro
// compactCFFTables 重建按字形编号索引的度量和字符映射,不保留旧编号的布局表 // compactCFFTables 重建按字形编号索引的度量和字符映射,不保留旧编号的布局表
// 入参: font 原字体, cff 紧凑CFF数据, ids 新编号对应的原编号 // 入参: font 原字体, cff 紧凑CFF数据, ids 新编号对应的原编号
// 返回: []byte 独立OpenType字体, error 封装错误 // 返回: []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) tables := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "vhea", "gasp"} { for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "vhea", "gasp"} {
if data := font.Tables[tag]; data != nil { 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["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["hhea"][34:], uint16(len(ids)))
binary.BigEndian.PutUint16(tables["maxp"][4:], uint16(len(ids))) binary.BigEndian.PutUint16(tables["maxp"][4:], uint16(len(ids)))
if len(tables["post"]) >= 32 { if len(tables["post"]) >= 32 {
@@ -151,7 +151,7 @@ func compactCFFTables(font *sfntFont, cff []byte, ids []uint16) ([]byte, error)
return nil, err return nil, err
} }
} }
return serializeOTF(tables) return SerializeOTF(tables)
} }
// compactCFFVerticalOrigins 重排CFF字体的逐字形纵向原点,保留默认原点 // compactCFFVerticalOrigins 重排CFF字体的逐字形纵向原点,保留默认原点
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import "strings" import (
"strings"
)
// cffExpertCharset 按CFF规范附录C保存Expert字符集的SID顺序 // cffExpertCharset 按CFF规范附录C保存Expert字符集的SID顺序
var cffExpertCharset = [...]uint16{ var cffExpertCharset = [...]uint16{
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -23,7 +23,7 @@ import (
// subsetCFFFont 裁剪CFF字体,保留组合组件和度量,按需重排字形 // subsetCFFFont 裁剪CFF字体,保留组合组件和度量,按需重排字形
// 入参: sfnt 已解析字体, glyphs 使用的字形, preserve 是否保留原字形编号 // 入参: sfnt 已解析字体, glyphs 使用的字形, preserve 是否保留原字形编号
// 返回: []byte 字体子集, error 裁剪错误 // 返回: []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} keep := map[uint16]bool{0: true}
for _, gid := range glyphs { for _, gid := range glyphs {
if gid >= sfnt.NumGlyphs() { if gid >= sfnt.NumGlyphs() {
@@ -48,11 +48,11 @@ func subsetCFFFont(sfnt *sfntFont, glyphs []uint16, preserve bool) ([]byte, erro
mapping[char] = gid mapping[char] = gid
} }
} }
tables["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) tables["cmap"] = BuildCmapTable(sfnt.NumGlyphs(), mapping)
for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} { for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} {
delete(tables, tag) delete(tables, tag)
} }
return serializeOTF(tables) return SerializeOTF(tables)
} }
// subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号 // subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import "fmt" import (
"fmt"
)
// cffCharstringType 读取CFF字形程序类型,缺省为Type2 // cffCharstringType 读取CFF字形程序类型,缺省为Type2
// 入参: dict 顶层字典 // 入参: dict 顶层字典
@@ -32,13 +34,13 @@ func cffCharstringType(dict cffDict) (int, error) {
// readCFFType1Program 读取Type1字形及无偏置局部子程序,保留源字形顺序 // readCFFType1Program 读取Type1字形及无偏置局部子程序,保留源字形顺序
// 入参: data CFF字体, chars 字形程序, private 私有字典范围, cid 是否为CID字体 // 入参: data CFF字体, chars 字形程序, private 私有字典范围, cid 是否为CID字体
// 返回: *type1Program 执行资源, error 字符集或私有字典错误 // 返回: *Type1Program 执行资源, error 字符集或私有字典错误
func readCFFType1Program(data []byte, chars [][]byte, private []float64, cid bool) (*type1Program, error) { func readCFFType1Program(data []byte, chars [][]byte, private []float64, cid bool) (*Type1Program, error) {
resources := &type2Font{} resources := &type2Font{}
if _, err := readType2Private(data, private, resources); err != nil { if _, err := readType2Private(data, private, resources); err != nil {
return nil, err return nil, err
} }
program := &type1Program{subrs: resources.locals, lenIV: -1} program := &Type1Program{subrs: resources.locals, lenIV: -1}
if cid { if cid {
return program, nil return program, nil
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -31,10 +31,10 @@ type cmapSegment struct {
offset uint16 offset uint16
} }
// normalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号 // NormalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号
// 入参: data 字符映射表 // 入参: data 字符映射表
// 返回: []byte 等价字符映射表, error 子表错误 // 返回: []byte 等价字符映射表, error 子表错误
func normalizeCmap(data []byte) ([]byte, error) { func NormalizeCmap(data []byte) ([]byte, error) {
if len(data) < 4 { if len(data) < 4 {
return data, nil return data, nil
} }
@@ -76,7 +76,7 @@ func normalizeCmap(data []byte) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
table := buildCmapTable(0, mapping) table := BuildCmapTable(0, mapping)
if language > 65535 { if language > 65535 {
table = buildCmapTableFormat12(0, mapping) table = buildCmapTableFormat12(0, mapping)
} }
@@ -255,17 +255,17 @@ func parseCmapFormat2(data []byte) (map[rune]uint16, error) {
return mapping, nil return mapping, nil
} }
// packedGlyphRune 获取包装字体字符 // PackedGlyphRune 获取包装字体字符
// 入参: gid 字形ID // 入参: gid 字形ID
// 返回: rune 包装字体字符 // 返回: rune 包装字体字符
func packedGlyphRune(gid uint16) rune { func PackedGlyphRune(gid uint16) rune {
return 0xF0000 + rune(gid) return 0xF0000 + rune(gid)
} }
// parseCmapMappings 解析cmap字符映射 // ParseCmapMappings 解析cmap字符映射
// 入参: data cmap表数据 // 入参: data cmap表数据
// 返回: map[rune]uint16 字符到字形映射 // 返回: map[rune]uint16 字符到字形映射
func parseCmapMappings(data []byte) map[rune]uint16 { func ParseCmapMappings(data []byte) map[rune]uint16 {
if len(data) < 4 { if len(data) < 4 {
return nil return nil
} }
@@ -427,18 +427,18 @@ func parseCmapFormat12(data []byte, result map[rune]uint16) {
} }
} }
// addPackedGlyphMapping 添加字形ID私有映射 // AddPackedGlyphMapping 添加字形ID私有映射
// 入参: mapping 字符映射, numGlyphs 字形数量 // 入参: 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++ { for i := uint16(0); i < numGlyphs; i++ {
mapping[packedGlyphRune(i)] = i mapping[PackedGlyphRune(i)] = i
} }
} }
// buildCmapTable 构建格式4或12字符映射,省略缺字映射 // BuildCmapTable 构建格式4或12字符映射,省略缺字映射
// 入参: numGlyphs 字形数量, mapping 字符映射 // 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据 // 返回: []byte cmap表数据
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte { func BuildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
if shouldBuildCmapFormat12(mapping) { if shouldBuildCmapFormat12(mapping) {
return buildCmapTableFormat12(numGlyphs, mapping) return buildCmapTableFormat12(numGlyphs, mapping)
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"encoding/binary" "encoding/binary"
@@ -211,7 +211,7 @@ func cmapVariationRune(value [3]byte) rune {
// 入参: count 字形总数, mapping 字符映射, variations 格式14子表 // 入参: count 字形总数, mapping 字符映射, variations 格式14子表
// 返回: []byte 字符映射表 // 返回: []byte 字符映射表
func buildCmapWithVariations(count uint16, mapping map[rune]uint16, variations []byte) []byte { func buildCmapWithVariations(count uint16, mapping map[rune]uint16, variations []byte) []byte {
base := buildCmapTable(count, mapping) base := BuildCmapTable(count, mapping)
if len(variations) == 0 { if len(variations) == 0 {
return base return base
} }
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"unicode/utf8"
)
// Face 字体文件中的字体名称与零起始索引,不依赖文件名
type Face struct {
Index int `json:"index"`
FullName string `json:"fullName"`
Family string `json:"family"`
Style string `json:"style"`
PostScriptName string `json:"postscriptName"`
Names []string `json:"names"`
}
// FaceInfo 从名称表读取字体信息
// 入参: data 名称表数据, index 零起始字体索引
// 返回: Face 字体信息
func FaceInfo(data []byte, index int) Face {
records, values := fontNameValues(data)
name := func(ids ...uint16) string {
for _, id := range ids {
for _, record := range records {
if record.Name == id {
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
return value
}
}
}
}
return ""
}
return Face{Index: index, FullName: name(4, 6, 16, 1), Family: name(16, 1), Style: name(17, 2), PostScriptName: name(6), Names: NamesFromTable(data)}
}
// FileCount 校验字体或集合头并读取字体数量
// 入参: data OpenType数据
// 返回: int 字体数量, error 格式错误
func FileCount(data []byte) (int, error) {
if len(data) < 12 {
return 0, fmt.Errorf("invalid font header")
}
switch string(data[:4]) {
case "\x00\x01\x00\x00", "OTTO", "true":
return 1, nil
case "ttcf":
version := binary.BigEndian.Uint32(data[4:])
count := uint64(binary.BigEndian.Uint32(data[8:]))
if (version != 0x00010000 && version != 0x00020000) || count == 0 || count > uint64(len(data)-12)/4 {
return 0, fmt.Errorf("invalid font collection header")
}
return int(count), nil
default:
return 0, fmt.Errorf("invalid OpenType header")
}
}
// ReadFaces 读取已解封装字体或集合的名称信息
// 入参: data OpenType数据
// 返回: []Face 字体列表, error 格式或名称表错误
func ReadFaces(data []byte) ([]Face, error) {
count, err := FileCount(data)
if err != nil {
return nil, err
}
faces := make([]Face, 0, count)
for index := 0; index < count; index++ {
tables, err := FileTables(data, index)
if err != nil {
return nil, err
}
face := FaceInfo(tables["name"], index)
if face.FullName == "" {
return nil, fmt.Errorf("font %d has no usable name", index)
}
faces = append(faces, face)
}
return faces, nil
}
// ExtractFace 提取已解封装字体中的集合项,规范字符映射并保留原始表
// 入参: data OpenType数据, index 零起始字体索引
// 返回: []byte 独立字体数据, error 格式或索引错误
func ExtractFace(data []byte, index int) ([]byte, error) {
var err error
if bytes.HasPrefix(data, []byte("ttcf")) {
data, err = ExtractCollectionFont(data, index)
if err != nil {
return nil, err
}
index = 0
}
tables, err := FileTables(data, index)
if err != nil {
return nil, err
}
cmap, err := NormalizeCmap(tables["cmap"])
if err != nil {
return nil, err
}
if !bytes.Equal(cmap, tables["cmap"]) {
tables["cmap"] = cmap
return SerializeOTF(tables)
}
return bytes.Clone(data), nil
}
// MissingGlyphs 检查独立字体的字符覆盖,忽略段落换行并按首次出现顺序去重
// 入参: data 独立OpenType数据, text 待检查文字
// 返回: string 缺失字符, error 字体或文字格式错误
func MissingGlyphs(data []byte, text string) (string, error) {
if !utf8.ValidString(text) {
return "", fmt.Errorf("text must be UTF-8")
}
data, err := DefaultCFF2Font(data)
if err != nil {
return "", err
}
sfnt, err := ParseSFNT(data)
if err != nil {
return "", err
}
seen := make(map[rune]bool)
var missing strings.Builder
for _, char := range text {
if char == '\r' || char == '\n' || seen[char] {
continue
}
seen[char] = true
if sfnt.GlyphIndex(char) == 0 {
missing.WriteRune(char)
}
}
return missing.String(), nil
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"encoding/binary" "encoding/binary"
@@ -25,9 +25,9 @@ import (
"github.com/go-text/typesetting/font/opentype" "github.com/go-text/typesetting/font/opentype"
) )
// sfntOutliner 缓存字体程序并提取独立字形轮廓 // Outliner 缓存字体程序并提取独立字形轮廓
type sfntOutliner struct { type Outliner struct {
font *sfntFont Font *SFNT
cffOnce sync.Once cffOnce sync.Once
cff *cff.CFF cff *cff.CFF
cffErr error cffErr error
@@ -46,15 +46,16 @@ type ttGlyph struct {
warning error warning error
} }
// path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理 // Path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理
// 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放 // 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放
// 返回: error 字形解析错误 // 返回: error 字形解析错误
func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error { func (f *Outliner) Path(path glyphPather, glyph uint16, scale float64) error {
if !f.font.IsTrueType { type point struct{ X, Y float64 }
if !f.Font.IsTrueType {
f.cffOnce.Do(func() { f.cffOnce.Do(func() {
data, err := sanitizeCFF(f.font.Tables["CFF "]) data, err := SanitizeCFF(f.Font.Tables["CFF "])
if err == nil { if err == nil {
data, err = normalizeCFFCharstringsAt(data, f.font.UnitsPerEm()) data, err = NormalizeCFFCharstringsAt(data, f.Font.UnitsPerEm())
} }
if err != nil { if err != nil {
f.cffErr = err f.cffErr = err
@@ -107,23 +108,23 @@ func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error
first = last + 1 first = last + 1
continue continue
} }
point := func(i int) Point { coordinate := func(i int) point {
return Point{X: float64(points[i].x) * scale / 64, Y: float64(points[i].y) * scale / 64} 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 begin, stop := first, last
if points[first].on { if points[first].on {
begin++ begin++
} else if points[last].on { } else if points[last].on {
start, stop = end, last-1 start, stop = end, last-1
} else { } 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) path.MoveTo(start.X, start.Y)
var control Point var control point
pending := false pending := false
for i := begin; i <= stop; i++ { for i := begin; i <= stop; i++ {
p := point(i) p := coordinate(i)
if points[i].on { if points[i].on {
if pending { if pending {
path.QuadTo(control.X, control.Y, p.X, p.Y) 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 解码简单或复合字形并执行对应字节码,不改写字体资源 // glyph 解码简单或复合字形并执行对应字节码,不改写字体资源
// 入参: id 字形编号, active 递归访问集合 // 入参: id 字形编号, active 递归访问集合
// 返回: ttGlyph 字形轮廓, error 字形错误 // 返回: 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 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) return result, fmt.Errorf("invalid TrueType component reference %d", id)
} }
active[id] = true active[id] = true
defer delete(active, id) defer delete(active, id)
data, err := trueTypeGlyphData(f.font.Tables, id) data, err := trueTypeGlyphData(f.Font.Tables, id)
if err != nil { if err != nil {
return result, err return result, err
} }
@@ -189,14 +190,14 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
if len(program) == 0 { if len(program) == 0 {
return result, nil return result, nil
} }
outlineOnly, err := ttOutlineOnly(f.font.Tables, program) outlineOnly, err := ttOutlineOnly(f.Font.Tables, program)
if err != nil { if err != nil {
return result, err return result, err
} }
if outlineOnly { if outlineOnly {
return result, nil 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 f.err != nil {
if ttRecoverableHint(f.err) { if ttRecoverableHint(f.err) {
result.warning = f.err result.warning = f.err
@@ -225,13 +226,13 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
// phantomPoints 按水平和垂直度量生成字形末尾的四个虚拟点 // phantomPoints 按水平和垂直度量生成字形末尾的四个虚拟点
// 入参: id 字形编号, data 完整字形头 // 入参: id 字形编号, data 完整字形头
// 返回: [4]ttPoint 左右边距与上下原点 // 返回: [4]ttPoint 左右边距与上下原点
func (f *sfntOutliner) phantomPoints(id uint16, data []byte) [4]ttPoint { func (f *Outliner) phantomPoints(id uint16, data []byte) [4]ttPoint {
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.sideBearing(id, false)) left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.Font.sideBearing(id, false))
top := int32(int16(binary.BigEndian.Uint16(data[8:]))) top := int32(int16(binary.BigEndian.Uint16(data[8:])))
if f.font.vertical != 0 { if f.Font.vertical != 0 {
top += int32(f.font.sideBearing(id, true)) 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 判断栈参数、指令引用或控制流错误是否允许保留执行前轮廓 // ttRecoverableHint 判断栈参数、指令引用或控制流错误是否允许保留执行前轮廓
@@ -274,7 +275,7 @@ func trueTypeGlyphData(tables map[string][]byte, id uint16) ([]byte, error) {
// recordGlyphWarning 记录字形恢复提示,允许并发提取轮廓 // recordGlyphWarning 记录字形恢复提示,允许并发提取轮廓
// 入参: id 字形编号, warning 恢复原因 // 入参: id 字形编号, warning 恢复原因
func (f *sfntOutliner) recordGlyphWarning(id uint16, warning error) { func (f *Outliner) recordGlyphWarning(id uint16, warning error) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
if f.warnings == nil { if f.warnings == nil {
@@ -283,10 +284,10 @@ func (f *sfntOutliner) recordGlyphWarning(id uint16, warning error) {
f.warnings[id] = warning f.warnings[id] = warning
} }
// glyphWarning 返回字形恢复为执行前轮廓的原因 // GlyphWarning 返回字形恢复为执行前轮廓的原因
// 入参: id 字形编号 // 入参: id 字形编号
// 返回: error 恢复原因,无恢复时为空 // 返回: error 恢复原因,无恢复时为空
func (f *sfntOutliner) glyphWarning(id uint16) error { func (f *Outliner) GlyphWarning(id uint16) error {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
return f.warnings[id] return f.warnings[id]
@@ -382,7 +383,7 @@ func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) {
// composite 合成分量变换和点匹配,保留分量顺序及父级字节码 // composite 合成分量变换和点匹配,保留分量顺序及父级字节码
// 入参: glyph 父字形编号, data 复合字形数据, phantoms 父字形虚拟点, active 递归访问集合 // 入参: glyph 父字形编号, data 复合字形数据, phantoms 父字形虚拟点, active 递归访问集合
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误 // 返回: 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 var result ttGlyph
pos := 10 pos := 10
total := -1 total := -1
@@ -475,7 +476,7 @@ func (f *sfntOutliner) composite(glyph uint16, data []byte, phantoms [4]ttPoint,
} else { } else {
if total < 0 { if total < 0 {
var err error 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 { if err != nil {
return result, nil, err return result, nil, err
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"encoding/binary" "encoding/binary"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -22,19 +22,19 @@ import (
"slices" "slices"
) )
// errTTInvalidLocations 表示字形索引不满足递增偏移要求 // ErrTTInvalidLocations 表示字形索引不满足递增偏移要求
var errTTInvalidLocations = errors.New("invalid TrueType glyph locations") var ErrTTInvalidLocations = errors.New("invalid TrueType glyph locations")
// normalizeTrueTypeLocations 验证物理字形边界后按原编号重排,不改变空字形和分量引用 // NormalizeTrueTypeLocations 验证物理字形边界后按原编号重排,不改变空字形和分量引用
// 入参: tables 独占字体表目录,表内容只读 // 入参: tables 独占字体表目录,表内容只读
// 返回: bool 是否重建索引, error 无法可靠恢复的索引或轮廓错误 // 返回: bool 是否重建索引, error 无法可靠恢复的索引或轮廓错误
func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) { func NormalizeTrueTypeLocations(tables map[string][]byte) (bool, error) {
if _, ok := tables["glyf"]; !ok { if _, ok := tables["glyf"]; !ok {
return false, nil return false, nil
} }
head, maxp, loca, glyf := tables["head"], tables["maxp"], tables["loca"], tables["glyf"] head, maxp, loca, glyf := tables["head"], tables["maxp"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(maxp) < 6 { 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:])) count := int(binary.BigEndian.Uint16(maxp[4:]))
width := 2 width := 2
@@ -43,10 +43,10 @@ func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) {
case 1: case 1:
width = 4 width = 4
default: default:
return false, fmt.Errorf("%w: format", errTTInvalidLocations) return false, fmt.Errorf("%w: format", ErrTTInvalidLocations)
} }
if count == 0 || len(loca) < (count+1)*width { 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 { location := func(i int) uint64 {
if width == 2 { if width == 2 {
@@ -58,7 +58,7 @@ func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) {
for i := range count + 1 { for i := range count + 1 {
offset := location(i) offset := location(i)
if offset > uint64(len(glyf)) { 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 ordered = ordered && previous <= offset
previous = offset previous = offset
@@ -83,31 +83,31 @@ func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) {
continue continue
} }
if _, duplicate := bodies[start]; duplicate { 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) index, _ := slices.BinarySearch(physical, start)
if index+1 >= len(physical) { 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]] data := glyf[start:physical[index+1]]
size, refs, err := trueTypeGlyphSize(data, count) size, refs, err := trueTypeGlyphSize(data, count)
if err != nil { 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 { 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 bodies[start], components[id] = data[:size], refs
total += (uint64(size) + 3) &^ 3 total += (uint64(size) + 3) &^ 3
if total > uint64(^uint(0)>>1) || total > uint64(^uint32(0)) { 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) state := make([]uint8, count)
var visit func(int) error var visit func(int) error
visit = func(id int) error { visit = func(id int) error {
if state[id] == 1 { 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 { if state[id] == 2 {
return nil return nil
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
import (
"path"
"strings"
)
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
// NormalizeName 规范化字体名称
// 入参: name 字体名称
// 返回: string 规范化后的字体名称
func NormalizeName(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = path.Base(strings.TrimSpace(name))
if IsFontFileName(name) {
name = strings.TrimSuffix(name, path.Ext(name))
}
name = strings.ToLower(strings.TrimSpace(name))
return fontNameReplacer.Replace(name)
}
// IsFontFileName 判断是否为字体文件名
// 入参: name 文件名
// 返回: bool 是否为字体文件
func IsFontFileName(name string) bool {
switch strings.ToLower(path.Ext(name)) {
case ".ttf", ".otf", ".ttc":
return true
default:
return false
}
}
// AppendFontName 追加字体名称
// 入参: names 字体名称列表, seen 去重映射, name 字体名称
// 返回: []string 字体名称列表
func AppendFontName(names []string, seen map[string]bool, name string) []string {
name = strings.TrimSpace(name)
if name == "" {
return names
}
key := NormalizeName(name)
if key == "" || seen[key] {
return names
}
seen[key] = true
return append(names, name)
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
// glyphPather 接收字体设计坐标中的轮廓指令
type glyphPather interface {
MoveTo(float64, float64)
LineTo(float64, float64)
QuadTo(float64, float64, float64, float64)
CubeTo(float64, float64, float64, float64, float64, float64)
Close()
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
import (
"bytes"
"encoding/binary"
"io"
"unicode/utf16"
"golang.org/x/text/encoding/charmap"
)
// fontNameRecord 字体名称表记录
type fontNameRecord struct {
Platform uint16
Encoding uint16
Language uint16
Name uint16
Length uint16
Offset uint16
}
// FileNames 按字体索引读取字体或集合中各项的名称
// 入参: file 字体数据
// 返回: [][]string 各字体名称及带样式的字体族名称
func FileNames(file io.ReaderAt) [][]string {
var header [12]byte
if _, err := file.ReadAt(header[:], 0); err != nil {
return nil
}
if string(header[:4]) != "ttcf" {
return [][]string{fontFaceNames(file, 0)}
}
version := binary.BigEndian.Uint32(header[4:])
if version != 0x00010000 && version != 0x00020000 {
return nil
}
var faces [][]string
for i, count := uint32(0), binary.BigEndian.Uint32(header[8:]); i < count; i++ {
var entry [4]byte
if _, err := file.ReadAt(entry[:], 12+int64(i)*4); err != nil {
return nil
}
faces = append(faces, fontFaceNames(file, int64(binary.BigEndian.Uint32(entry[:]))))
}
return faces
}
// fontFaceNames 读取指定字体表目录中的名称
// 入参: file 字体数据, offset 字体表目录偏移
// 返回: []string 字体名称及带样式的字体族名称
func fontFaceNames(file io.ReaderAt, offset int64) []string {
var header [12]byte
if _, err := file.ReadAt(header[:], offset); err != nil {
return nil
}
switch string(header[:4]) {
case "\x00\x01\x00\x00", "OTTO", "true":
default:
return nil
}
entries := make([]byte, int(binary.BigEndian.Uint16(header[4:]))*16)
if _, err := file.ReadAt(entries, offset+12); err != nil {
return nil
}
for i := 0; i < len(entries); i += 16 {
if string(entries[i:i+4]) != "name" {
continue
}
offset := int64(binary.BigEndian.Uint32(entries[i+8:]))
length := int64(binary.BigEndian.Uint32(entries[i+12:]))
data, err := io.ReadAll(io.NewSectionReader(file, offset, length))
if err != nil {
return nil
}
return NamesFromTable(data)
}
return nil
}
// NamesFromTable 解析字体名称表
// 入参: data 名称表数据
// 返回: []string 字体名称列表
func NamesFromTable(data []byte) []string {
records, values := fontNameValues(data)
var names []string
seen := make(map[string]bool)
for _, record := range records {
key := [4]uint16{record.Platform, record.Encoding, record.Language, record.Name}
name := values[key]
switch record.Name {
case 1, 16:
key[3]++
if style := values[key]; style != "" && name != "" {
name += " " + style
}
case 4, 6:
default:
continue
}
names = AppendFontName(names, seen, name)
}
return names
}
// fontNameValues 解码字体名称表的多语言记录
// 入参: data 名称表数据
// 返回: []fontNameRecord 名称记录, map[[4]uint16]string 平台、编码、语言和名称对应的文本
func fontNameValues(data []byte) ([]fontNameRecord, map[[4]uint16]string) {
if len(data) < 6 || binary.BigEndian.Uint16(data) > 1 {
return nil, nil
}
count := int(binary.BigEndian.Uint16(data[2:]))
storage := int(binary.BigEndian.Uint16(data[4:]))
if 6+count*12 > storage || storage > len(data) {
return nil, nil
}
records := make([]fontNameRecord, count)
if err := binary.Read(bytes.NewReader(data[6:]), binary.BigEndian, records); err != nil {
return nil, nil
}
values := make(map[[4]uint16]string)
for _, record := range records {
switch record.Name {
case 1, 2, 4, 6, 16, 17:
default:
continue
}
start := storage + int(record.Offset)
end := start + int(record.Length)
if end > len(data) {
continue
}
var name string
switch {
case record.Platform == 0 || record.Platform == 3 && (record.Encoding == 0 || record.Encoding == 1 || record.Encoding == 10):
if record.Length%2 != 0 {
continue
}
chars := make([]uint16, int(record.Length)/2)
for i := range chars {
chars[i] = binary.BigEndian.Uint16(data[start+i*2:])
}
name = string(utf16.Decode(chars))
case record.Platform == 1 && record.Encoding == 0:
name, _ = charmap.Macintosh.NewDecoder().String(string(data[start:end]))
default:
continue
}
values[[4]uint16{record.Platform, record.Encoding, record.Language, record.Name}] = name
}
return records, values
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
import (
"encoding/binary"
)
// FixDataAggressive 激进修复字体数据
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func FixDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
if len(data) < 4 {
return false, data, nil, false, nil
}
tag := string(data[0:4])
u32Tag := binary.BigEndian.Uint32(data[0:4])
isTrueType := u32Tag == 0x00010000 || tag == "true"
isOpenType := tag == "OTTO"
isTTC := tag == "ttcf"
if isTTC {
return false, data, nil, false, nil
}
if isBareCFFData(data) {
otf, mapping, err := WrapCFFToOTF(data)
if err != nil {
return false, data, nil, false, err
}
return true, otf, mapping, false, nil
}
if !isTrueType && !isOpenType {
return false, data, nil, false, nil
}
fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName)
return fixed, newData, mapping, mc, err
}
// SFNTData 保留字体原始表数据和时间,重新计算目录与校验和
// 入参: tables 原始字体表
// 返回: []byte 独立SFNT数据
func SFNTData(tables map[string][]byte) []byte {
data, _ := SerializeOTF(tables)
return data
}
// isBareCFFData 检查是否为CFF裸字体数据
// 入参: data 字体数据
// 返回: bool 是否为CFF裸数据
func isBareCFFData(data []byte) bool {
if len(data) < 4 {
return false
}
if data[0] != 1 || data[1] != 0 {
return false
}
hdrSize := int(data[2])
offSize := int(data[3])
return 4 <= hdrSize && hdrSize <= len(data) && 1 <= offSize && offSize <= 4
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"encoding/binary" "encoding/binary"
@@ -24,8 +24,8 @@ import (
"github.com/go-text/typesetting/font/opentype/tables" "github.com/go-text/typesetting/font/opentype/tables"
) )
// sfntFont 保存独立字体表和基础度量,不包含绘图库对象 // SFNT 保存独立字体表和基础度量,不包含绘图库对象
type sfntFont struct { type SFNT struct {
Tables map[string][]byte Tables map[string][]byte
IsTrueType, IsCFF bool IsTrueType, IsCFF bool
cmaps []sfntCmap cmaps []sfntCmap
@@ -40,26 +40,26 @@ type sfntCmap struct {
mapping map[rune]uint16 mapping map[rune]uint16
} }
// parseSFNTFont 校验独立字体的必需表、度量范围及字符映射 // ParseSFNT 校验独立字体的必需表、度量范围及字符映射
// 入参: data 独立SFNT数据 // 入参: data 独立SFNT数据
// 返回: *sfntFont 字体, error 格式错误 // 返回: *SFNT 字体, error 格式错误
func parseSFNTFont(data []byte) (*sfntFont, error) { func ParseSFNT(data []byte) (*SFNT, error) {
table, err := fontFileTables(data, 0) table, err := FileTables(data, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return parseSFNTTables(table) return ParseSFNTTables(table)
} }
// parseSFNTTables 校验字体表并解析度量和字符映射,借用表数据且不修改其内容 // ParseSFNTTables 校验字体表并解析度量和字符映射,借用表数据且不修改其内容
// 入参: table 字体表 // 入参: table 字体表
// 返回: *sfntFont 字体, error 格式错误 // 返回: *SFNT 字体, error 格式错误
func parseSFNTTables(table map[string][]byte) (*sfntFont, error) { func ParseSFNTTables(table map[string][]byte) (*SFNT, error) {
head, maxp, hhea := table["head"], table["maxp"], table["hhea"] head, maxp, hhea := table["head"], table["maxp"], table["hhea"]
if len(head) != 54 || len(maxp) < 6 || len(hhea) < 36 { if len(head) != 54 || len(maxp) < 6 || len(hhea) < 36 {
return nil, fmt.Errorf("missing or truncated font metrics") 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.units, f.count = binary.BigEndian.Uint16(head[18:]), binary.BigEndian.Uint16(maxp[4:])
f.horizontal = binary.BigEndian.Uint16(hhea[34:]) 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) { 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表 // 入参: data cmap表
// 返回: []sfntCmap 编码记录, error 表结构错误 // 返回: []sfntCmap 编码记录, error 表结构错误
func readSFNTCmaps(data []byte) ([]sfntCmap, error) { func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
data, err := normalizeCmap(data) data, err := NormalizeCmap(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -157,16 +157,26 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
// UnitsPerEm 返回字体设计单位 // UnitsPerEm 返回字体设计单位
// 返回: uint16 每em单位数 // 返回: uint16 每em单位数
func (f *sfntFont) UnitsPerEm() uint16 { return f.units } func (f *SFNT) UnitsPerEm() uint16 { return f.units }
// NumGlyphs 返回字形总数 // NumGlyphs 返回字形总数
// 返回: uint16 字形数 // 返回: 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 按编码表顺序查找字符,不替换缺失字形 // GlyphIndex 按编码表顺序查找字符,不替换缺失字形
// 入参: char 字符 // 入参: char 字符
// 返回: uint16 字形编号 // 返回: uint16 字形编号
func (f *sfntFont) GlyphIndex(char rune) uint16 { func (f *SFNT) GlyphIndex(char rune) uint16 {
for _, cmap := range f.cmaps { for _, cmap := range f.cmaps {
if gid := cmap.mapping[char]; gid != 0 { if gid := cmap.mapping[char]; gid != 0 {
return gid return gid
@@ -178,7 +188,7 @@ func (f *sfntFont) GlyphIndex(char rune) uint16 {
// glyphCharacters 返回指向指定字形的全部字符 // glyphCharacters 返回指向指定字形的全部字符
// 入参: gid 字形编号 // 入参: gid 字形编号
// 返回: []rune 排序后的字符 // 返回: []rune 排序后的字符
func (f *sfntFont) glyphCharacters(gid uint16) []rune { func (f *SFNT) glyphCharacters(gid uint16) []rune {
f.reverseOnce.Do(func() { f.reverseOnce.Do(func() {
f.reverse = make(map[uint16][]rune) f.reverse = make(map[uint16][]rune)
seen := make(map[rune]bool) seen := make(map[rune]bool)
@@ -200,7 +210,7 @@ func (f *sfntFont) glyphCharacters(gid uint16) []rune {
// GlyphAdvance 返回水平字宽 // GlyphAdvance 返回水平字宽
// 入参: gid 字形编号 // 入参: gid 字形编号
// 返回: uint16 字宽 // 返回: uint16 字宽
func (f *sfntFont) GlyphAdvance(gid uint16) uint16 { func (f *SFNT) GlyphAdvance(gid uint16) uint16 {
if gid >= f.count { if gid >= f.count {
return 0 return 0
} }
@@ -210,7 +220,7 @@ func (f *sfntFont) GlyphAdvance(gid uint16) uint16 {
// GlyphVerticalAdvance 返回纵向字距,无垂直度量时使用em高度 // GlyphVerticalAdvance 返回纵向字距,无垂直度量时使用em高度
// 入参: gid 字形编号 // 入参: gid 字形编号
// 返回: uint16 纵向字距 // 返回: uint16 纵向字距
func (f *sfntFont) GlyphVerticalAdvance(gid uint16) uint16 { func (f *SFNT) GlyphVerticalAdvance(gid uint16) uint16 {
if f.vertical == 0 { if f.vertical == 0 {
return f.units return f.units
} }
@@ -223,7 +233,7 @@ func (f *sfntFont) GlyphVerticalAdvance(gid uint16) uint16 {
// sideBearing 返回指定方向的字形边距 // sideBearing 返回指定方向的字形边距
// 入参: gid 字形编号, vertical 是否读取顶部边距 // 入参: gid 字形编号, vertical 是否读取顶部边距
// 返回: int16 边距 // 返回: 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 data, count := f.Tables["hmtx"], f.horizontal
if vertical { if vertical {
data, count = f.Tables["vmtx"], f.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 // VerticalMetrics 依据OS/2排版标志选择行度量,缺失时使用hhea
// 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙 // 返回: 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"] 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:]))) a, d, gap := int(int16(binary.BigEndian.Uint16(hhea[4:]))), int(int16(binary.BigEndian.Uint16(hhea[6:]))), int(int16(binary.BigEndian.Uint16(hhea[8:])))
if len(os2) >= 78 { if len(os2) >= 78 {
@@ -261,12 +271,12 @@ func (f *sfntFont) VerticalMetrics() (uint16, uint16, uint16) {
// Write 保留原始表数据并重新计算字体校验和 // Write 保留原始表数据并重新计算字体校验和
// 返回: []byte 独立字体 // 返回: []byte 独立字体
func (f *sfntFont) Write() []byte { data, _ := serializeOTF(f.Tables); return data } func (f *SFNT) Write() []byte { data, _ := SerializeOTF(f.Tables); return data }
// glyphDependencies 批量收集复合字形依赖,校验引用、记录长度和嵌套深度 // glyphDependencies 批量收集复合字形依赖,校验引用、记录长度和嵌套深度
// 入参: glyphs 字形编号 // 入参: glyphs 字形编号
// 返回: []uint16 依赖字形, error 字形错误 // 返回: []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) heights, active := make(map[uint16]int), make(map[uint16]bool)
var result []uint16 var result []uint16
var visit func(uint16) error var visit func(uint16) error
+433
View File
@@ -0,0 +1,433 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
import (
"bytes"
"encoding/binary"
"fmt"
"maps"
"slices"
"github.com/go-text/typesetting/font/opentype/tables"
)
// Subsettable 判断默认资源后端能否无损裁剪该字体
// 入参: sfnt 已解析字体
// 返回: bool 是否支持裁剪
func Subsettable(sfnt *SFNT) bool {
if fontEmbeddingFlags(sfnt)&0x0100 != 0 || slices.ContainsFunc([]string{"COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) {
return false
}
if sfnt.IsCFF {
if sfnt.Tables["fvar"] != nil {
return false
}
font, err := readType2Font(sfnt.Tables["CFF "])
if err == nil {
return len(font.chars) == int(sfnt.NumGlyphs())
}
cid, err := readCIDSubsetFont(sfnt.Tables["CFF "])
return err == nil && len(cid.fonts[0].chars) == int(sfnt.NumGlyphs())
}
return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt))
}
// fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零
// 入参: sfnt 已解析字体
// 返回: uint16 嵌入标志
func fontEmbeddingFlags(sfnt *SFNT) uint16 {
if table := sfnt.Tables["OS/2"]; len(table) >= 10 {
return binary.BigEndian.Uint16(table[8:10])
}
return 0
}
// ValidateEmbedding 校验字体是否允许作为可编辑轮廓字体嵌入
// 入参: data 独立OpenType字体
// 返回: error 字体解析或嵌入限制
func ValidateEmbedding(data []byte) error {
sfnt, err := ParseSFNT(data)
if err != nil {
return err
}
flags := fontEmbeddingFlags(sfnt)
if flags&0x0200 != 0 || flags&0x000e != 0 && flags&0x0008 == 0 {
return fmt.Errorf("font does not permit editable embedding")
}
return nil
}
// SFNTName 获取字体中的名称
// 入参: sfnt 字体, names 名称类型,按顺序查找
// 返回: string 字体名称
func SFNTName(sfnt *SFNT, names ...uint16) string {
records, values := fontNameValues(sfnt.Tables["name"])
for _, name := range names {
for _, record := range records {
if record.Name != name {
continue
}
if value := values[[4]uint16{record.Platform, record.Encoding, record.Language, name}]; value != "" {
return value
}
}
}
return ""
}
// SubsetSFNT 按实际用字裁剪字体,保留字符映射、名称及度量
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0, mapped 是否保留显式字形编号
// 返回: []byte 字体子集, error 错误信息
func SubsetSFNT(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
data, err := DefaultCFF2Font(data)
if err != nil {
return nil, err
}
sfnt, err := ParseSFNT(data)
if err != nil {
return nil, err
}
if fontEmbeddingFlags(sfnt)&0x0100 != 0 {
return bytes.Clone(data), nil
}
if sfnt.IsCFF {
count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
if err != nil {
return nil, err
}
if count != int(sfnt.NumGlyphs()) {
return nil, fmt.Errorf("CFF glyph count does not match maxp")
}
return subsetCFFFont(sfnt, glyphs, mapped)
}
if sfnt.Tables["fvar"] != nil {
return subsetVariableSFNTFont(sfnt, glyphs)
}
if mapped {
usage := newUsage()
for _, id := range glyphs {
usage.Glyphs[id] = true
for _, char := range sfnt.glyphCharacters(id) {
usage.Characters[char] = true
}
}
tables := make(map[string][]byte)
for _, tag := range []string{"cmap", "head", "hhea", "hmtx", "vhea", "vmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp"} {
if table := sfnt.Tables[tag]; table != nil {
tables[tag] = table
}
}
fixed, err := SerializeOTF(tables)
if err != nil {
return nil, err
}
if subset := SubsetSourceSFNT(fixed, usage); subset != nil {
return subset, nil
}
return fixed, nil
}
return subsetCompactSFNTFont(sfnt, glyphs)
}
// SubsetSourceSFNT 裁剪静态TrueType或CFF原有字体,保留字形编号、组合依赖、度量和提示指令
// 字形替换结果一并保留,集合、可变、彩色和未知表保持原样
// 入参: data 字体数据, usage 全包用字记录
// 返回: []byte 更小的字体子集,无确定收益时为空
func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
if usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
return nil
}
sfnt, err := ParseSFNT(data)
if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
return nil
}
for tag := range sfnt.Tables {
switch tag {
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "GSUB", "meta", "DSIG", "FFTM", "PCLT":
default:
return nil
}
}
var unicodeTables []sfntCmap
for _, record := range sfnt.cmaps {
if record.format == 14 {
continue
}
if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) {
unicodeTables = append(unicodeTables, record)
}
}
if len(unicodeTables) == 0 {
return nil
}
glyphs := maps.Clone(usage.Glyphs)
glyphs[0] = true
mapping := make(map[rune]uint16, len(usage.Characters))
for char := range usage.Characters {
id := sfnt.GlyphIndex(char)
for _, cmap := range unicodeTables {
if alternate, ok := cmap.mapping[char]; ok && alternate != id {
return nil
}
}
glyphs[id] = true
if id != 0 {
mapping[char] = id
}
}
variations, ok := subsetCmapVariations(sfnt.Tables["cmap"], usage.Characters, glyphs, sfnt.NumGlyphs())
if !ok || !retainSFNTSubstitutions(sfnt.Tables["GSUB"], glyphs, sfnt.NumGlyphs()) {
return nil
}
if sfnt.IsCFF {
count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
if err != nil || count != int(sfnt.NumGlyphs()) {
return nil
}
cff, err := subsetCFFCharstrings(sfnt.Tables["CFF "], glyphs)
if err != nil {
return nil
}
tables := maps.Clone(sfnt.Tables)
tables["CFF "] = cff
tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations)
delete(tables, "DSIG")
result, err := SerializeOTF(tables)
if err != nil || len(result) >= len(data) {
return nil
}
return result
}
for id := 0; id < int(sfnt.NumGlyphs()); id++ {
if _, err := trueTypeGlyphData(sfnt.Tables, uint16(id)); err != nil {
return nil
}
}
dependencies, err := sfnt.glyphDependencies(slices.Collect(maps.Keys(glyphs))...)
if err != nil {
return nil
}
for _, dependency := range dependencies {
glyphs[dependency] = true
}
var glyf, loca []byte
for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
if binary.BigEndian.Uint16(sfnt.Tables["head"][50:]) == 0 {
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
} else {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
}
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
data, _ := trueTypeGlyphData(sfnt.Tables, uint16(id))
glyf = append(glyf, data...)
if len(glyf)%2 != 0 {
glyf = append(glyf, 0)
}
}
}
tables := maps.Clone(sfnt.Tables)
tables["glyf"], tables["loca"] = glyf, loca
tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations)
tables["head"] = bytes.Clone(tables["head"])
clear(tables["head"][8:12])
delete(tables, "DSIG")
result, err := SerializeOTF(tables)
if err != nil || len(result) >= len(data) {
return nil
}
return result
}
// retainSFNTSubstitutions 保留全部替换结果,使原字形替换表在裁剪后仍可使用
// 入参: data 替换表, glyphs 待保留字形, count 字形总数
// 返回: bool 替换表及结果编号是否有效
func retainSFNTSubstitutions(data []byte, glyphs map[uint16]bool, count uint16) bool {
if len(data) == 0 {
return true
}
if len(data) < 10 || binary.BigEndian.Uint16(data) != 1 || binary.BigEndian.Uint16(data[2:]) > 1 {
return false
}
if binary.BigEndian.Uint16(data[2:]) == 1 && (len(data) < 14 || binary.BigEndian.Uint32(data[10:]) != 0) {
return false
}
layout, _, err := tables.ParseLayout(data)
if err != nil {
return false
}
for _, lookup := range layout.LookupList.Lookups {
subtables, err := lookup.AsGSUBLookups()
if err != nil {
return false
}
for _, subtable := range subtables {
if extension, ok := subtable.(tables.ExtensionSubs); ok {
if extension.ExtensionOffset < 8 || uint64(extension.ExtensionOffset) > uint64(len(extension.RawData)) {
return false
}
subtable, err = extension.Resolve()
if err != nil {
return false
}
}
if !retainSFNTSubstitution(subtable, glyphs, count) {
return false
}
}
}
return true
}
// retainSFNTSubstitution 收集替换子表的输出字形,上下文替换的结果由被引用的查找表提供
// 入参: subtable 替换子表, glyphs 待保留字形, count 字形总数
// 返回: bool 子表类型及输出字形是否有效
func retainSFNTSubstitution(subtable tables.GSUBLookup, glyphs map[uint16]bool, count uint16) bool {
var outputs []tables.GlyphID
switch subtable := subtable.(type) {
case tables.SingleSubs:
switch data := subtable.Data.(type) {
case tables.SingleSubstData1:
for id := uint16(0); id < count; id++ {
if _, ok := data.Coverage.Index(tables.GlyphID(id)); ok {
outputs = append(outputs, tables.GlyphID(id+uint16(data.DeltaGlyphID)))
}
}
case tables.SingleSubstData2:
outputs = data.SubstituteGlyphIDs
default:
return false
}
case tables.MultipleSubs:
for _, sequence := range subtable.Sequences {
outputs = append(outputs, sequence.SubstituteGlyphIDs...)
}
case tables.AlternateSubs:
for _, set := range subtable.AlternateSets {
outputs = append(outputs, set.AlternateGlyphIDs...)
}
case tables.LigatureSubs:
for _, set := range subtable.LigatureSets {
for _, ligature := range set.Ligatures {
outputs = append(outputs, ligature.LigatureGlyph)
}
}
case tables.ReverseChainSingleSubs:
outputs = subtable.SubstituteGlyphIDs
case tables.ContextualSubs, tables.ChainedContextualSubs:
default:
return false
}
for _, id := range outputs {
if uint16(id) >= count {
return false
}
glyphs[uint16(id)] = true
}
return true
}
// subsetCompactSFNTFont 重排新建静态字体的字形,同步复合引用、字符映射和度量
// 入参: sfnt 字体, glyphs 使用字形
// 返回: []byte 紧凑字体子集, error 字形数据错误
func subsetCompactSFNTFont(sfnt *SFNT, glyphs []uint16) ([]byte, error) {
ids := []uint16{0}
remap := map[uint16]uint16{0: 0}
for _, id := range glyphs {
if id >= sfnt.NumGlyphs() {
return nil, fmt.Errorf("font glyph index out of range")
}
if _, ok := remap[id]; !ok {
remap[id] = uint16(len(ids))
ids = append(ids, id)
}
}
dependencies, err := sfnt.glyphDependencies(ids...)
if err != nil {
return nil, err
}
for _, id := range dependencies {
if _, ok := remap[id]; !ok {
remap[id] = uint16(len(ids))
ids = append(ids, id)
}
}
table := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "cvt ", "fpgm", "prep", "gasp", "vhea"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = bytes.Clone(data)
}
}
var glyf, loca, hmtx, vmtx []byte
mapping := make(map[rune]uint16)
for _, id := range ids {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
data, err := trueTypeGlyphData(sfnt.Tables, id)
if err != nil {
return nil, err
}
data = bytes.Clone(data)
if len(data) >= 10 && int16(binary.BigEndian.Uint16(data)) < 0 {
for pos := 10; ; {
flags := binary.BigEndian.Uint16(data[pos:])
child := binary.BigEndian.Uint16(data[pos+2:])
binary.BigEndian.PutUint16(data[pos+2:], remap[child])
pos += 6
if flags&1 != 0 {
pos += 2
}
switch flags & 0xc8 {
case 8:
pos += 2
case 0x40:
pos += 4
case 0x80:
pos += 8
}
if flags&0x20 == 0 {
break
}
}
}
glyf = append(glyf, data...)
for len(glyf)%4 != 0 {
glyf = append(glyf, 0)
}
hmtx = binary.BigEndian.AppendUint16(hmtx, sfnt.GlyphAdvance(id))
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(sfnt.sideBearing(id, false)))
if sfnt.vertical != 0 {
vmtx = binary.BigEndian.AppendUint16(vmtx, sfnt.GlyphVerticalAdvance(id))
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(sfnt.sideBearing(id, true)))
}
for _, char := range sfnt.glyphCharacters(id) {
mapping[char] = remap[id]
}
}
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
table["glyf"], table["loca"], table["hmtx"] = glyf, loca, hmtx
table["cmap"] = BuildCmapTable(uint16(len(ids)), mapping)
binary.BigEndian.PutUint16(table["head"][50:], 1)
binary.BigEndian.PutUint16(table["hhea"][34:], uint16(len(ids)))
binary.BigEndian.PutUint16(table["maxp"][4:], uint16(len(ids)))
if len(table["post"]) >= 32 {
table["post"] = table["post"][:32]
binary.BigEndian.PutUint32(table["post"], 0x30000)
}
if sfnt.vertical != 0 {
table["vmtx"] = vmtx
binary.BigEndian.PutUint16(table["vhea"][34:], uint16(len(ids)))
}
return SerializeOTF(table)
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"context" "context"
@@ -23,14 +23,14 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// symbolFontProgram 为标准符号字体补齐Unicode映射,保留字形编号和轮廓 // SymbolProgram 为标准符号字体补齐Unicode映射,保留字形编号和轮廓
// 入参: ctx 取消上下文, data 独立字体数据, name 标准字体名,为空时从字体名称表识别 // 入参: ctx 取消上下文, data 独立字体数据, name 标准字体名,为空时从字体名称表识别
// 返回: []byte 字体数据, map[string]uint16 标准字形编号,不适用时为空, error 映射或取消错误 // 返回: []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 { if err := ctx.Err(); err != nil {
return nil, nil, err return nil, nil, err
} }
tables, err := fontFileTables(data, 0) tables, err := FileTables(data, 0)
if err != nil { if err != nil {
return data, nil, nil return data, nil, nil
} }
@@ -57,7 +57,7 @@ func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, m
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
original := parseCmapMappings(tables["cmap"]) original := ParseCmapMappings(tables["cmap"])
mapping := make(map[rune]uint16) mapping := make(map[rune]uint16)
names := make(map[string]uint16) names := make(map[string]uint16)
for _, glyph := range glyphs { for _, glyph := range glyphs {
@@ -90,8 +90,8 @@ func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, m
if !legacy || len(mapping) == 0 { if !legacy || len(mapping) == 0 {
return data, names, nil return data, names, nil
} }
tables["cmap"] = buildCmapTable(0, mapping) tables["cmap"] = BuildCmapTable(0, mapping)
program, err := serializeOTF(tables) program, err := SerializeOTF(tables)
return program, names, err return program, names, err
} }
@@ -99,8 +99,8 @@ func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, m
// 入参: data 名称表数据 // 入参: data 名称表数据
// 返回: string 标准字体名,无法识别时为空 // 返回: string 标准字体名,无法识别时为空
func symbolFontName(data []byte) string { func symbolFontName(data []byte) string {
for _, name := range fontNamesFromTable(data) { for _, name := range NamesFromTable(data) {
switch fontNormalizeName(name) { switch NormalizeName(name) {
case "symbol", "symbolmt", "symbolregular": case "symbol", "symbolmt", "symbolregular":
return "Symbol" return "Symbol"
case "zapfdingbats", "zapfdingbatsregular", "itczapfdingbats": case "zapfdingbats", "zapfdingbatsregular", "itczapfdingbats":
+11 -11
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -24,7 +24,7 @@ import (
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息 // 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) { func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
original := data original := data
data, err := defaultCFF2Font(data) data, err := DefaultCFF2Font(data)
if err != nil { if err != nil {
return false, original, nil, false, err 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 { if existingTables["CFF "] != nil {
isCFFSfnt = true isCFFSfnt = true
sanitized, err := sanitizeCFF(existingTables["CFF "]) sanitized, err := SanitizeCFF(existingTables["CFF "])
if err != nil { if err != nil {
return false, data, nil, false, err 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 { if head := existingTables["head"]; len(head) >= 20 {
units = binary.BigEndian.Uint16(head[18:20]) units = binary.BigEndian.Uint16(head[18:20])
} }
normalized, err := normalizeCFFCharstringsAt(sanitized, units) normalized, err := NormalizeCFFCharstringsAt(sanitized, units)
if err != nil { if err != nil {
return false, data, nil, false, err return false, data, nil, false, err
} }
@@ -75,7 +75,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
malformedDirectory = true malformedDirectory = true
} }
} }
cmap, err := normalizeCmap(existingTables["cmap"]) cmap, err := NormalizeCmap(existingTables["cmap"])
if err != nil { if err != nil {
return false, data, nil, false, err 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])) descender = int16(binary.BigEndian.Uint16(hhea[6:8]))
} }
if missingOS2 { if missingOS2 {
newTables["OS/2"] = buildOS2TableWithMetrics(ascender, descender) newTables["OS/2"] = BuildOS2TableWithMetrics(ascender, descender)
} }
var mapping map[rune]uint16 var mapping map[rune]uint16
if fixCmap { if fixCmap {
if !missingCmap { if !missingCmap {
mapping = parseCmapMappings(newTables["cmap"]) mapping = ParseCmapMappings(newTables["cmap"])
} }
if len(mapping) == 0 && isCFFSfnt { if len(mapping) == 0 && isCFFSfnt {
mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs)) 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 { if len(mapping) == 0 {
mapping = make(map[rune]uint16) mapping = make(map[rune]uint16)
} }
addPackedGlyphMapping(mapping, numGlyphs) AddPackedGlyphMapping(mapping, numGlyphs)
newTables["cmap"] = buildCmapTable(numGlyphs, mapping) newTables["cmap"] = BuildCmapTable(numGlyphs, mapping)
} }
if missingName && fixName { if missingName && fixName {
newTables["name"] = buildNameTable() newTables["name"] = buildNameTable()
} }
if missingPost { if missingPost {
newTables["post"] = buildPostTable() newTables["post"] = BuildPostTable()
} }
finalData, err := serializeOTF(newTables) finalData, err := SerializeOTF(newTables)
if err != nil { if err != nil {
return false, data, nil, missingCmap, err return false, data, nil, missingCmap, err
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -41,8 +41,8 @@ var type1StandardNames = func() [256]string {
return names return names
}() }()
// type1Program 保存解密后的Type1字形与子程序 // Type1Program 保存解密后的Type1字形与子程序
type type1Program struct { type Type1Program struct {
glyphs []type1Glyph glyphs []type1Glyph
subrs [][]byte subrs [][]byte
lenIV int lenIV int
@@ -63,7 +63,7 @@ type type1Scanner struct {
// type1Outline 保存字形指令的操作数栈及轮廓位置 // type1Outline 保存字形指令的操作数栈及轮廓位置
type type1Outline struct { type type1Outline struct {
program *type1Program program *Type1Program
steps *int steps *int
active map[string]bool active map[string]bool
args []float64 args []float64
@@ -84,24 +84,24 @@ type type1Outline struct {
path bytes.Buffer path bytes.Buffer
} }
// parseType1Program 读取Type1字体的字形程序及子程序 // ParseType1Program 读取Type1字体的字形程序及子程序
// 入参: data PFA或PFB字体数据 // 入参: data PFA或PFB字体数据
// 返回: type1Program 字形和子程序, error 错误信息 // 返回: Type1Program 字形和子程序, error 错误信息
func parseType1Program(data []byte) (type1Program, error) { func ParseType1Program(data []byte) (Type1Program, error) {
var err error var err error
data, err = flattenType1(data) data, err = flattenType1(data)
if err != nil { if err != nil {
return type1Program{}, err return Type1Program{}, err
} }
matrix, start, err := readType1Header(data) matrix, start, err := readType1Header(data)
if err != nil { if err != nil {
return type1Program{}, err return Type1Program{}, err
} }
for start < len(data) && type1Space(data[start]) { for start < len(data) && type1Space(data[start]) {
start++ start++
} }
if start == len(data) { if start == len(data) {
return type1Program{}, fmt.Errorf("empty Type1 eexec data") return Type1Program{}, fmt.Errorf("empty Type1 eexec data")
} }
encrypted := data[start:] encrypted := data[start:]
if type1HexEncoded(encrypted) { if type1HexEncoded(encrypted) {
@@ -121,15 +121,15 @@ func parseType1Program(data []byte) (type1Program, error) {
} }
encrypted = make([]byte, hex.DecodedLen(len(digits))) encrypted = make([]byte, hex.DecodedLen(len(digits)))
if _, err := hex.Decode(encrypted, digits); err != nil { 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 { 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:] decrypted := type1Decrypt(encrypted, 55665)[4:]
scanner := type1Scanner{data: decrypted} scanner := type1Scanner{data: decrypted}
program := type1Program{lenIV: 4, matrix: matrix} program := Type1Program{lenIV: 4, matrix: matrix}
section := "" section := ""
for { for {
token := scanner.next() token := scanner.next()
@@ -140,7 +140,7 @@ func parseType1Program(data []byte) (type1Program, error) {
case "/lenIV": case "/lenIV":
value, err := strconv.Atoi(scanner.next()) value, err := strconv.Atoi(scanner.next())
if err != nil || value < -1 { if err != nil || value < -1 {
return type1Program{}, fmt.Errorf("invalid Type1 lenIV") return Type1Program{}, fmt.Errorf("invalid Type1 lenIV")
} }
program.lenIV = value program.lenIV = value
case "/Subrs": case "/Subrs":
@@ -157,18 +157,18 @@ func parseType1Program(data []byte) (type1Program, error) {
} }
index, err := strconv.Atoi(scanner.next()) index, err := strconv.Atoi(scanner.next())
if err != nil || index < 0 || index > len(decrypted) { 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()) length, err := strconv.Atoi(scanner.next())
if err != nil { if err != nil {
return type1Program{}, fmt.Errorf("invalid Type1 subroutine length") return Type1Program{}, fmt.Errorf("invalid Type1 subroutine length")
} }
if !type1BinaryStart(scanner.next()) { 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) raw, err := scanner.binary(length)
if err != nil { if err != nil {
return type1Program{}, err return Type1Program{}, err
} }
for len(program.subrs) <= index { for len(program.subrs) <= index {
program.subrs = append(program.subrs, nil) program.subrs = append(program.subrs, nil)
@@ -185,28 +185,28 @@ func parseType1Program(data []byte) (type1Program, error) {
continue continue
} }
if !type1BinaryStart(scanner.next()) { 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) raw, err := scanner.binary(length)
if err != nil { if err != nil {
return type1Program{}, err return Type1Program{}, err
} }
program.glyphs = append(program.glyphs, type1Glyph{name: name, data: raw}) program.glyphs = append(program.glyphs, type1Glyph{name: name, data: raw})
} }
} }
if len(program.glyphs) == 0 { 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 { for index := range program.subrs {
program.subrs[index], err = type1CharString(program.subrs[index], program.lenIV) program.subrs[index], err = type1CharString(program.subrs[index], program.lenIV)
if err != nil { if err != nil {
return type1Program{}, err return Type1Program{}, err
} }
} }
for index := range program.glyphs { for index := range program.glyphs {
program.glyphs[index].data, err = type1CharString(program.glyphs[index].data, program.lenIV) program.glyphs[index].data, err = type1CharString(program.glyphs[index].data, program.lenIV)
if err != nil { 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 return program, nil
@@ -426,7 +426,7 @@ func type1BinaryStart(token string) bool {
// outline 将Type1字形指令转换为保留三次曲线的Type2程序 // outline 将Type1字形指令转换为保留三次曲线的Type2程序
// 入参: glyph 字形数据 // 入参: glyph 字形数据
// 返回: []byte Type2字形程序, error 错误信息 // 返回: []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}} state := type1Outline{program: p, active: map[string]bool{glyph.name: true}}
ended, err := state.run(glyph.data, 0) ended, err := state.run(glyph.data, 0)
if err != nil { if err != nil {
@@ -840,10 +840,10 @@ func encodeType2Number(target *bytes.Buffer, value float64) error {
return nil return nil
} }
// toCFF 将Type1字形转换为保持三次轮廓的CFF字体 // ToCFF 将Type1字形转换为保持三次轮廓的CFF字体
// 入参: name 字体名称 // 入参: name 字体名称
// 返回: []byte CFF数据, error 错误信息 // 返回: []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 == "" { if len(p.glyphs) > math.MaxUint16 || name == "" {
return nil, fmt.Errorf("invalid Type1 font identity") return nil, fmt.Errorf("invalid Type1 font identity")
} }
@@ -907,9 +907,9 @@ func (p *type1Program) toCFF(name string) ([]byte, error) {
return data, nil return data, nil
} }
// glyphIDs 返回Type1名称到转换后字形编号的对应关系 // GlyphIDs 返回Type1名称到转换后字形编号的对应关系
// 返回: map[string]uint16 字形编号 // 返回: map[string]uint16 字形编号
func (p *type1Program) glyphIDs() map[string]uint16 { func (p *Type1Program) GlyphIDs() map[string]uint16 {
ids := map[string]uint16{".notdef": 0} ids := map[string]uint16{".notdef": 0}
next := uint16(1) next := uint16(1)
for _, glyph := range p.glyphs { for _, glyph := range p.glyphs {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"encoding/binary" "encoding/binary"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"fmt" "fmt"
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package font
// Usage 指定子集需要保留的字符、字形及引用是否存在不确定性
type Usage struct {
Characters map[rune]bool
Glyphs map[uint16]bool
Unsafe bool
}
// newUsage 创建具有可写字符和字形集合的子集输入
// 返回: *Usage 用字信息
func newUsage() *Usage {
return &Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool)}
}
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"math"
"sort" "sort"
) )
@@ -31,11 +32,18 @@ type otfTableRecord struct {
data []byte data []byte
} }
// extractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和 // finite 判断数值是否有限
// 入参: value 数值
// 返回: bool 是否有限
func finite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
// ExtractCollectionFont 提取集合中的单个OpenType字体,保留表内容并重新计算校验和
// 入参: data 集合数据, index 字体索引 // 入参: data 集合数据, index 字体索引
// 返回: []byte 单字体数据, error 错误信息 // 返回: []byte 单字体数据, error 错误信息
func extractCollectionFont(data []byte, index int) ([]byte, error) { func ExtractCollectionFont(data []byte, index int) ([]byte, error) {
tables, err := fontFileTables(data, index) tables, err := FileTables(data, index)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -44,14 +52,14 @@ func extractCollectionFont(data []byte, index int) ([]byte, error) {
} }
tables["head"] = bytes.Clone(tables["head"]) tables["head"] = bytes.Clone(tables["head"])
clear(tables["head"][8:12]) clear(tables["head"][8:12])
return serializeOTF(tables) return SerializeOTF(tables)
} }
// fontFileTables 读取单字体或集合指定项的表目录,不复制表数据 // FileTables 读取单字体或集合指定项的表目录,不复制表数据
// 入参: data 字体数据, index 字体索引 // 入参: data 字体数据, index 字体索引
// 返回: map[string][]byte 字体表, error 错误信息 // 返回: map[string][]byte 字体表, error 错误信息
func fontFileTables(data []byte, index int) (map[string][]byte, error) { func FileTables(data []byte, index int) (map[string][]byte, error) {
count, err := fontFileCount(data) count, err := FileCount(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -190,13 +198,13 @@ func buildTrueTypeMaxpTable(numGlyphs uint16) []byte {
// buildOS2Table 构建OS/2表 (使用默认Metrics) // buildOS2Table 构建OS/2表 (使用默认Metrics)
// 返回: []byte OS/2表数据 // 返回: []byte OS/2表数据
func buildOS2Table() []byte { func buildOS2Table() []byte {
return buildOS2TableWithMetrics(800, -200) return BuildOS2TableWithMetrics(800, -200)
} }
// buildOS2TableWithMetrics 构建OS/2表 // BuildOS2TableWithMetrics 构建OS/2表
// 入参: ascender 上升部, descender 下降部 // 入参: ascender 上升部, descender 下降部
// 返回: []byte OS/2表数据 // 返回: []byte OS/2表数据
func buildOS2TableWithMetrics(ascender, descender int16) []byte { func BuildOS2TableWithMetrics(ascender, descender int16) []byte {
os2 := new(bytes.Buffer) os2 := new(bytes.Buffer)
binary.Write(os2, binary.BigEndian, uint16(3)) binary.Write(os2, binary.BigEndian, uint16(3))
binary.Write(os2, binary.BigEndian, int16(500)) binary.Write(os2, binary.BigEndian, int16(500))
@@ -252,9 +260,9 @@ func buildNameTable() []byte {
return buf.Bytes() return buf.Bytes()
} }
// buildPostTable 构建post表 (版本3.0, 无字形名称) // BuildPostTable 构建post表 (版本3.0, 无字形名称)
// 返回: []byte post表数据 // 返回: []byte post表数据
func buildPostTable() []byte { func BuildPostTable() []byte {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint32(0x00030000)) binary.Write(buf, binary.BigEndian, uint32(0x00030000))
binary.Write(buf, binary.BigEndian, uint32(0)) binary.Write(buf, binary.BigEndian, uint32(0))
@@ -280,10 +288,10 @@ func buildHmtxTable(widths []uint16) []byte {
return buf.Bytes() return buf.Bytes()
} }
// serializeOTF 序列化OpenType字体结构 // SerializeOTF 序列化OpenType字体结构
// 入参: tables 表数据映射 // 入参: tables 表数据映射
// 返回: []byte 完整字体数据, error 错误信息 // 返回: []byte 完整字体数据, error 错误信息
func serializeOTF(tables map[string][]byte) ([]byte, error) { func SerializeOTF(tables map[string][]byte) ([]byte, error) {
numTables := uint16(len(tables)) numTables := uint16(len(tables))
entrySelector := 0 entrySelector := 0
for 1<<(entrySelector+1) <= int(numTables) { for 1<<(entrySelector+1) <= int(numTables) {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"encoding/binary" "encoding/binary"
@@ -24,7 +24,7 @@ import (
// variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构 // variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构
// 入参: sfnt 已解析字体 // 入参: sfnt 已解析字体
// 返回: bool 是否具备可裁剪的变化数据 // 返回: bool 是否具备可裁剪的变化数据
func variableSFNTSubsettable(sfnt *sfntFont) bool { func variableSFNTSubsettable(sfnt *SFNT) bool {
fvar, gvar := sfnt.Tables["fvar"], sfnt.Tables["gvar"] 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 { if len(fvar) < 16 || len(gvar) < 20 || binary.BigEndian.Uint32(fvar) != 0x10000 || binary.BigEndian.Uint32(gvar) != 0x10000 {
return false return false
@@ -38,30 +38,30 @@ func variableSFNTSubsettable(sfnt *sfntFont) bool {
// subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据 // subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据
// 入参: sfnt 已解析字体, glyphs 使用的字形 // 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 字形或变化表错误 // 返回: []byte 字体子集, error 字形或变化表错误
func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { func subsetVariableSFNTFont(sfnt *SFNT, glyphs []uint16) ([]byte, error) {
if !variableSFNTSubsettable(sfnt) { if !variableSFNTSubsettable(sfnt) {
return nil, fmt.Errorf("invalid TrueType variation tables") return nil, fmt.Errorf("invalid TrueType variation tables")
} }
usage := newEditorFontUsage() usage := newUsage()
usage.glyphs[0] = true usage.Glyphs[0] = true
for _, gid := range glyphs { for _, gid := range glyphs {
if gid >= sfnt.NumGlyphs() { if gid >= sfnt.NumGlyphs() {
return nil, fmt.Errorf("variable font glyph index out of range") 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) { 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 { if err != nil {
return nil, err return nil, err
} }
for _, dependency := range dependencies { for _, dependency := range dependencies {
usage.glyphs[dependency] = true usage.Glyphs[dependency] = true
} }
mapping := make(map[rune]uint16, len(usage.chars)) mapping := make(map[rune]uint16, len(usage.Characters))
for char := range usage.chars { for char := range usage.Characters {
mapping[char] = sfnt.GlyphIndex(char) mapping[char] = sfnt.GlyphIndex(char)
} }
table := make(map[string][]byte) table := make(map[string][]byte)
@@ -70,18 +70,18 @@ func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
table[tag] = data table[tag] = data
} }
} }
table["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) table["cmap"] = BuildCmapTable(sfnt.NumGlyphs(), mapping)
static, err := serializeOTF(table) static, err := SerializeOTF(table)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if subset := subsetSourceSFNTFont(static, usage); subset != nil { if subset := SubsetSourceSFNT(static, usage); subset != nil {
table, err = fontFileTables(subset, 0) table, err = FileTables(subset, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
table["gvar"], err = subsetGlyphVariations(sfnt.Tables["gvar"], usage.glyphs) table["gvar"], err = subsetGlyphVariations(sfnt.Tables["gvar"], usage.Glyphs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -90,7 +90,7 @@ func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
table[tag] = data table[tag] = data
} }
} }
return serializeOTF(table) return SerializeOTF(table)
} }
// subsetGlyphVariations 按原字形编号重建gvar偏移表,保留共享元组及使用字形的完整变化数据 // subsetGlyphVariations 按原字形编号重建gvar偏移表,保留共享元组及使用字形的完整变化数据
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package font
import ( import (
"bytes" "bytes"
@@ -40,10 +40,10 @@ var woff2Tags = [...]string{
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill",
} }
// decodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理 // DecodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理
// 入参: data WOFF2文件 // 入参: data WOFF2文件
// 返回: []byte OpenType数据, bool 是否已处理, error 封装错误 // 返回: []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)) { 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") return nil, true, fmt.Errorf("invalid WOFF2 header")
} }
@@ -131,7 +131,7 @@ func decodeWOFF2(data []byte) ([]byte, bool, error) {
size := int(sizes[i]) size := int(sizes[i])
table[tag], body = body[:size], body[size:] table[tag], body = body[:size], body[size:]
} }
decoded, err := serializeOTF(table) decoded, err := SerializeOTF(table)
return decoded, true, err return decoded, true, err
} }
@@ -157,10 +157,10 @@ func readWOFF2Base128(data []byte, pos *int) (uint32, error) {
return 0, fmt.Errorf("invalid WOFF2 integer") return 0, fmt.Errorf("invalid WOFF2 integer")
} }
// decodeWOFF 展开WOFF表数据,不解析或更改字体轮廓 // DecodeWOFF 展开WOFF表数据,不解析或更改字体轮廓
// 入参: data WOFF文件 // 入参: data WOFF文件
// 返回: []byte OpenType数据, error 封装错误 // 返回: []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 { 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") 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 { if err := validateWOFFBlocks(data, end, 24); err != nil {
return nil, err return nil, err
} }
return serializeOTF(table) return SerializeOTF(table)
} }
// validateWOFFBlocks 校验附加数据块的边界及对齐,字体元数据不写入输出 // validateWOFFBlocks 校验附加数据块的边界及对齐,字体元数据不写入输出
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"bufio" "bufio"
@@ -27,21 +27,21 @@ import (
type bmpColorMask struct { type bmpColorMask struct {
mask uint32 mask uint32
shift uint shift uint
max uint32 Max uint32
} }
// decodeImageConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素 // DecodeConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素
// 入参: input 图片输入流 // 入参: input 图片输入流
// 返回: image.Config 图片配置, string 图片格式, error 头部错误 // 返回: 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) buffer := bufio.NewReader(input)
prefix, _ := buffer.Peek(34) prefix, _ := buffer.Peek(34)
if isTIFFData(prefix) { if IsTIFF(prefix) {
layout, err := readTIFFLayout(buffer) layout, err := readTIFFLayout(buffer)
if err != nil { if err != nil {
return image.Config{}, "tiff", err return image.Config{}, "tiff", err
} }
size := layout.displaySize() size := layout.DisplaySize()
return image.Config{Width: size.X, Height: size.Y}, "tiff", nil return image.Config{Width: size.X, Height: size.Y}, "tiff", nil
} }
if len(prefix) == 34 && string(prefix[:2]) == "BM" { if len(prefix) == 34 && string(prefix[:2]) == "BM" {
@@ -71,13 +71,13 @@ func (m bmpColorMask) sample(value uint32) uint8 {
if m.mask == 0 { if m.mask == 0 {
return 255 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 编码方式 // 入参: data 已校验头部的BMP数据, offset 像素偏移, bpp 像素位数, compression 编码方式
// 返回: [4]bmpColorMask 红绿蓝和透明度通道, error 格式错误 // 返回: [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 var result [4]bmpColorMask
if bpp != 16 && bpp != 24 && bpp != 32 { if bpp != 16 && bpp != 24 && bpp != 32 {
return result, fmt.Errorf("unsupported bmp depth") 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 { if mask&used != 0 || maximum&(maximum+1) != 0 || uint64(mask)>>bpp != 0 {
return result, fmt.Errorf("invalid bmp color mask") 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 used |= mask
} }
return result, nil return result, nil
+172
View File
@@ -0,0 +1,172 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package image
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
"sync/atomic"
)
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
type EncodedImage struct {
data []byte
format string
config image.Config
result atomic.Pointer[encodedImageResult]
mu sync.Mutex
pending chan struct{}
}
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
type encodedImageResult struct {
image image.Image
err error
}
// NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func NewEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImage(bytes.Clone(data))
}
// Bytes 返回只读原始编码,调用方不得修改
// 返回: []byte 原始编码
func (img *EncodedImage) Bytes() []byte { return img.data }
// MIME 返回图片媒体类型
// 返回: string 媒体类型
func (img *EncodedImage) MIME() string { return "image/" + img.format }
// ColorModel 返回头信息中的颜色模型,不解码像素
// 返回: color.Model 颜色模型
func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel }
// Bounds 返回图片范围,不解码像素
// 返回: image.Rectangle 图片范围
func (img *EncodedImage) Bounds() image.Rectangle {
return image.Rect(0, 0, img.config.Width, img.config.Height)
}
// Image 首次解码图片并缓存结果或错误,支持并发读取
// 返回: image.Image 只读像素, error 解码错误
func (img *EncodedImage) Image() (image.Image, error) {
if result := img.result.Load(); result != nil {
return result.image, result.err
}
return img.ImageContext(context.Background())
}
// ImageContext 解码并共享只读像素,取消或超时后可重试,nil使用后台上下文
// 并发调用等待正在进行的解码,等待方取消不影响其他调用
// 入参: ctx 取消上下文
// 返回: image.Image 只读像素, error 解码或取消错误
func (img *EncodedImage) ImageContext(ctx context.Context) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
for {
if err := ctx.Err(); err != nil {
return nil, err
}
if result := img.result.Load(); result != nil {
return result.image, result.err
}
img.mu.Lock()
if img.result.Load() != nil {
img.mu.Unlock()
continue
}
pending := img.pending
if pending == nil {
pending = make(chan struct{})
img.pending = pending
img.mu.Unlock()
return img.decodeContext(ctx, pending)
}
img.mu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-pending:
}
}
}
// decodeContext 执行本次解码,退出时唤醒等待方,只缓存完整结果及稳定错误
// 入参: ctx 取消上下文, pending 本次解码的完成通知
// 返回: image.Image 只读像素, error 解码或取消错误
func (img *EncodedImage) decodeContext(ctx context.Context, pending chan struct{}) (image.Image, error) {
defer func() {
img.mu.Lock()
img.pending = nil
close(pending)
img.mu.Unlock()
}()
pixels, _, err := image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(img.data)), context: ctx})
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
img.result.Store(&encodedImageResult{image: pixels, err: err})
return pixels, err
}
// At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (img *EncodedImage) At(x, y int) color.Color {
decoded, err := img.Image()
if err != nil {
return color.Transparent
}
return decoded.At(x, y)
}
// newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImageContext(context.Background(), data)
}
// newEncodedImageContext 读取PNG或JPEG头信息,元数据扫描期间响应取消,不复制输入或解码像素
// 入参: ctx 取消上下文, data 只读图片编码
// 返回: *EncodedImage 惰性图片, error 头信息、格式或取消错误
func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, error) {
config, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
if err != nil {
return nil, err
}
if format != "jpeg" && format != "png" {
return nil, fmt.Errorf("unsupported encoded image %q", format)
}
return &EncodedImage{data: data, format: format, config: config}, nil
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package image
// NormalizeFormat 标准化印章媒体类型
// 入参: s 原始媒体类型
// 返回: string 标准媒体类型
func NormalizeFormat(s string) string {
switch s {
case "jpg":
return "jpeg"
case "bmp":
return "bmp"
case "jb2", "gbig2":
return "jbig2"
default:
return s
}
}
+714
View File
@@ -0,0 +1,714 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
_ "image/gif"
"io"
_ "github.com/xiaoqidun/j2kgo"
_ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
type transparentEdgeImage struct {
source image.Image
rect image.Rectangle
}
// transparentEdgeImage64 按需补齐十六位像素边缘,保留原始只读图像
type transparentEdgeImage64 struct {
source image.Image
rect image.Rectangle
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage) ColorModel() color.Model { return color.NRGBAModel }
// NRGBAAt 读取原始像素或按邻域补齐透明边缘,边界外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA 非预乘颜色
func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := NRGBAAt(s.source, sx, sy)
if c.A == 0 {
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
c = edge
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c = transparentPaddingColor(c)
}
return c
}
// At 返回非预乘像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage) At(x, y int) color.Color { return s.NRGBAAt(x, y) }
// RGBA64At 返回预乘十六位像素,供采样器无装箱读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBAAt(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回十六位非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model }
// NRGBA64At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *transparentEdgeImage64) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA64{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := NRGBA64At(s.source, sx, sy)
if c.A == 0 {
var best color.NRGBA64
for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ {
for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ {
v := NRGBA64At(s.source, nx, ny)
if v.A > best.A {
best = v
}
}
}
if best.A != 0 {
best.A = 1
c = best
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c.A = 1
}
return c
}
// At 返回非预乘十六位像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage64) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// RGBA64At 返回保留原采样精度的预乘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBA64At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Decode 解码图片数据
// 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
func Decode(data []byte) (image.Image, string, error) {
return DecodeContext(context.Background(), data)
}
// DecodeContext 解码图片并传递取消状态,PNG和JPEG仍保留惰性像素解码
// 入参: ctx 取消上下文, data 只读图片编码
// 返回: image.Image 图片对象, string 图片格式, error 解码或取消错误
func DecodeContext(ctx context.Context, data []byte) (img image.Image, format string, err error) {
if err := ctx.Err(); err != nil {
return nil, "", err
}
defer func() {
if cancelErr := ctx.Err(); cancelErr != nil {
img, err = nil, cancelErr
}
}()
if IsJPEG(data) || IsPNG(data) {
img, err := newEncodedImageContext(ctx, data)
if err != nil {
return nil, "", err
}
return img, img.format, nil
}
if IsTIFF(data) {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return nil, "tiff", err
}
img, err := DecodeTIFF(ctx, data, layout)
return img, "tiff", err
}
img, format, err = image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err == nil {
return img, NormalizeFormat(format), nil
}
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
if img, err := DecodeBMPContext(ctx, data); err == nil {
return img, "bmp", nil
}
return nil, "", err
}
// DecodePixelsContext 完整解码图片,确保像素可读取并传递取消状态
// 入参: ctx 取消上下文, data 图片编码数据
// 返回: image.Image 解码像素, string 图片格式, error 解码或取消错误
func DecodePixelsContext(ctx context.Context, data []byte) (image.Image, string, error) {
img, format, err := DecodeContext(ctx, data)
if err != nil {
return nil, format, err
}
pixels, err := PixelDataContext(ctx, img)
return pixels, format, err
}
// IsJPEG 判断是否为JPEG图片数据
// 入参: data 图片数据
// 返回: bool 是否为JPEG图片数据
func IsJPEG(data []byte) bool {
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
}
// IsPNG 判断是否为PNG图片数据
// 入参: data 图片数据
// 返回: bool 是否为PNG图片数据
func IsPNG(data []byte) bool {
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
}
// DecodeConfigData 解码图片尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
func DecodeConfigData(data []byte) (image.Config, string, error) {
return DecodeConfigDataContext(context.Background(), data)
}
// DecodeConfigDataContext 读取图片尺寸和颜色模型,传递布局解析中的取消状态
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Config 图片配置, string 图片格式, error 读取或取消错误
func DecodeConfigDataContext(ctx context.Context, data []byte) (config image.Config, format string, err error) {
if err := ctx.Err(); err != nil {
return image.Config{}, "", err
}
defer func() {
if cancelErr := ctx.Err(); cancelErr != nil {
config, err = image.Config{}, cancelErr
}
}()
if IsTIFF(data) {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return image.Config{}, "tiff", err
}
profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
var cfg image.Config
if layout.needsSampleView() {
img, _, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
cfg.ColorModel = img.ColorModel()
} else {
input := newTIFFDirectoryInput(data, layout.Offset)
input.ctx = ctx
cfg, err = tiff.DecodeConfig(input)
if err != nil {
return image.Config{}, "tiff", err
}
}
if profile != nil || layout.Transparency != nil {
cfg.ColorModel = color.NRGBA64Model
}
if layout.Transparency != nil {
if _, err := readTIFFColorProfile(ctx, data, *layout.Transparency); err != nil {
return image.Config{}, "tiff", err
}
if _, _, err := tiffPlaneDirectories(ctx, data, *layout.Transparency); err != nil {
return image.Config{}, "tiff", err
}
}
size := layout.DisplaySize()
cfg.Width, cfg.Height = size.X, size.Y
return cfg, "tiff", nil
}
cfg, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err == nil {
return cfg, NormalizeFormat(format), nil
}
if cfg, err := decodeBMPConfig(data); err == nil {
return cfg, "bmp", nil
}
return image.Config{}, "", err
}
// decodeBMPConfig 解码BMP尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, error 错误信息
func decodeBMPConfig(data []byte) (image.Config, error) {
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
if err != nil {
return image.Config{}, err
}
if offset <= 0 || width <= 0 || height == 0 {
return image.Config{}, fmt.Errorf("unsupported bmp")
}
if _, err := BMPColorMasks(data, offset, bpp, compression); err != nil {
return image.Config{}, err
}
if height < 0 {
height = -height
}
return image.Config{ColorModel: color.NRGBAModel, Width: width, Height: height}, nil
}
// decodeBMPImage 解码BMP图片
// 入参: data 图片数据
// 返回: image.Image 图片对象, error 错误信息
func decodeBMPImage(data []byte) (image.Image, error) {
return DecodeBMPContext(context.Background(), data)
}
// DecodeBMPContext 解码BMP真彩色像素,逐行检查取消
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Image 图片对象, error 格式或取消错误
func DecodeBMPContext(ctx context.Context, data []byte) (image.Image, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
if err != nil {
return nil, err
}
if offset > len(data) {
return nil, fmt.Errorf("invalid bmp pixel offset")
}
masks, err := BMPColorMasks(data, offset, bpp, compression)
if err != nil {
return nil, err
}
topDown := height < 0
if topDown {
height = -height
}
if width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid bmp size")
}
stride := ((int64(width)*int64(bpp) + 31) / 32) * 4
if stride > int64(len(data)-offset)/int64(height) {
return nil, fmt.Errorf("truncated bmp")
}
if width > int(^uint(0)>>1)/4/height {
return nil, fmt.Errorf("bmp dimensions exceed image capacity")
}
rowStride := int(stride)
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
if err := ctx.Err(); err != nil {
return nil, err
}
srcY := y
if !topDown {
srcY = height - 1 - y
}
row := data[offset+srcY*rowStride:]
for x := 0; x < width; x++ {
switch bpp {
case 16, 32:
var value uint32
if bpp == 16 {
value = uint32(binary.LittleEndian.Uint16(row[x*2:]))
} else {
value = binary.LittleEndian.Uint32(row[x*4:])
}
p := y*img.Stride + x*4
for i, mask := range masks {
img.Pix[p+i] = mask.sample(value)
}
case 24:
p := x * 3
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
}
}
}
return img, nil
}
// ParseBMPHeader 解析BMP文件头
// 入参: data 图片数据
// 返回: int 像素偏移, int 宽度, int 高度, uint16 位数, uint32 压缩方式, error 错误信息
func ParseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
if len(data) < 54 || string(data[:2]) != "BM" {
return 0, 0, 0, 0, 0, fmt.Errorf("not bmp")
}
offset := int(binary.LittleEndian.Uint32(data[10:14]))
dibSize := binary.LittleEndian.Uint32(data[14:18])
if dibSize < 40 {
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
}
if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
}
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
planes := binary.LittleEndian.Uint16(data[26:28])
bpp := binary.LittleEndian.Uint16(data[28:30])
compression := binary.LittleEndian.Uint32(data[30:34])
if planes != 1 || width <= 0 || height == 0 || int64(height) == -int64(^uint(0)>>1)-1 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
}
return offset, width, height, bpp, compression, nil
}
// PixelSource 获取图片像素源
// 入参: img 图片对象
// 返回: image.Image 图片像素源
func PixelSource(img image.Image) image.Image {
if decoded, err := PixelData(img); err == nil {
return decoded
}
return img
}
// PixelData 获取像素并保留惰性解码错误,供绘制和像素处理入口校验
// 入参: img 图片对象
// 返回: image.Image 解码像素, error 解码错误
func PixelData(img image.Image) (image.Image, error) {
if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
return src.Image()
}
return img, nil
}
// PixelDataContext 获取像素并传递取消状态,兼容不接收上下文的图像实现
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
// 返回: image.Image 解码像素, error 解码或取消错误
func PixelDataContext(ctx context.Context, img image.Image) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if src, ok := img.(interface {
ImageContext(context.Context) (image.Image, error)
}); ok {
return src.ImageContext(ctx)
}
pixels, err := PixelData(img)
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
return pixels, err
}
// GrayAt 获取图片灰度像素
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.Gray 灰度像素
func GrayAt(img image.Image, x, y int) color.Gray {
if src, ok := img.(*image.Gray); ok {
return src.GrayAt(x, y)
}
if src, ok := img.(*image.Paletted); ok {
return color.GrayModel.Convert(src.At(x, y)).(color.Gray)
}
if src, ok := img.(image.RGBA64Image); ok {
r, g, b, _ := src.RGBA64At(x, y).RGBA()
return color.Gray{Y: uint8((19595*r + 38470*g + 7471*b + 1<<15) >> 24)}
}
return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
}
// NRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA NRGBA像素
func NRGBAAt(img image.Image, x, y int) color.NRGBA {
switch src := img.(type) {
case interface{ NRGBAAt(int, int) color.NRGBA }:
return src.NRGBAAt(x, y)
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
c := src.NRGBA64At(x, y)
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
case *image.RGBA, *image.RGBA64, *image.Gray, *image.Gray16, *image.Alpha, *image.Alpha16, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA{}
}
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
if a != 0xffff {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
}
value := img.At(x, y)
if c, ok := value.(color.NRGBA64); ok {
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
}
return color.NRGBAModel.Convert(value).(color.NRGBA)
}
// AlphaAt 读取十六位透明度,原生图像避免颜色装箱
// 入参: img 图像, x 横坐标, y 纵坐标
// 返回: uint32 十六位透明度
func AlphaAt(img image.Image, x, y int) uint32 {
if source, ok := img.(image.RGBA64Image); ok {
return uint32(source.RGBA64At(x, y).A)
}
_, _, _, alpha := img.At(x, y).RGBA()
return alpha
}
// HighPrecision 判断颜色模型及调色板是否需要保留十六位采样
// 入参: img 图片对象
// 返回: bool 是否使用十六位颜色或透明度
func HighPrecision(img image.Image) bool {
model := img.ColorModel()
if palette, ok := model.(color.Palette); ok {
for _, value := range palette {
switch value.(type) {
case color.RGBA, color.NRGBA, color.Gray, color.Alpha, color.CMYK, color.YCbCr, color.NYCbCrA:
default:
pixel := color.NRGBA64Model.Convert(value).(color.NRGBA64)
if pixel.R%257 != 0 || pixel.G%257 != 0 || pixel.B%257 != 0 || pixel.A%257 != 0 {
return true
}
}
}
return false
}
return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model
}
// NRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA64 NRGBA64像素
func NRGBA64At(img image.Image, x, y int) color.NRGBA64 {
switch src := img.(type) {
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
return src.NRGBA64At(x, y)
case *image.NRGBA:
c := src.NRGBAAt(x, y)
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
case *image.Gray:
v := uint16(src.GrayAt(x, y).Y) * 257
return color.NRGBA64{R: v, G: v, B: v, A: 0xffff}
case *image.Gray16:
c := src.Gray16At(x, y)
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff}
case *image.Alpha, *image.Alpha16:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA64{}
}
return color.NRGBA64{R: 0xffff, G: 0xffff, B: 0xffff, A: c.A}
case *image.RGBA, *image.RGBA64, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA64{}
}
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
if a != 0xffff {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: c.A}
}
value := img.At(x, y)
if c, ok := value.(color.NRGBA); ok {
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
}
return color.NRGBA64Model.Convert(value).(color.NRGBA64)
}
// transparentEdgeColor 获取透明像素相邻的可见颜色
// 入参: source 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA 颜色, bool 是否存在
func transparentEdgeColor(source image.Image, x, y int) (color.NRGBA, bool) {
img, ok := source.(*image.NRGBA)
if !ok {
bounds := source.Bounds()
var best color.NRGBA
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
for nx := max(x-1, bounds.Min.X); nx < min(x+2, bounds.Max.X); nx++ {
if nx == x && ny == y {
continue
}
pixel := NRGBAAt(source, nx, ny)
if pixel.A > best.A {
best = pixel
if best.A == 255 {
best.A = 1
return best, true
}
}
}
}
if best.A == 0 {
return color.NRGBA{}, false
}
best.A = 1
return best, true
}
bounds := img.Bounds()
best, alpha := 0, uint8(0)
left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X)
scan:
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
offset := img.PixOffset(left, ny)
for nx := left; nx < right; nx++ {
if (nx != x || ny != y) && img.Pix[offset+3] > alpha {
best, alpha = offset, img.Pix[offset+3]
if alpha == 255 {
break scan
}
}
offset += 4
}
}
if alpha == 0 {
return color.NRGBA{}, false
}
return color.NRGBA{R: img.Pix[best], G: img.Pix[best+1], B: img.Pix[best+2], A: 1}, true
}
// transparentPaddingColor 获取透明补齐颜色
// 入参: c 边缘颜色
// 返回: color.NRGBA 补齐颜色
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
c.A = 1
return c
}
// WithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误
func WithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, 0, err
}
if img == nil {
return img, 0, nil
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w == 0 || h == 0 {
return img, 0, nil
}
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
return img, 0, nil
}
source, err := PixelDataContext(ctx, img)
if err != nil {
return nil, 0, err
}
switch source.(type) {
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
return img, 0, nil
}
hasZero, hasVisible := false, false
var pixels []byte
var stride, step int
switch src := source.(type) {
case *image.NRGBA:
pixels, stride, step = src.Pix, src.Stride, 4
case *image.RGBA:
pixels, stride, step = src.Pix, src.Stride, 4
case *image.Alpha:
pixels, stride, step = src.Pix, src.Stride, 1
}
if pixels != nil {
hasZero = pixels[step-1] == 0
hasVisible = !hasZero
scan:
for y := 0; y < h; y++ {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
offset := y*stride + step - 1
for x := 0; x < w; x++ {
if x&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
}
if (pixels[offset] == 0) != hasZero {
hasZero, hasVisible = true, true
break scan
}
offset += step
}
}
} else {
for y := bounds.Min.Y; y < bounds.Max.Y && !(hasZero && hasVisible); y++ {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
}
if AlphaAt(source, x, y) == 0 {
hasZero = true
} else {
hasVisible = true
}
if hasZero && hasVisible {
break
}
}
}
}
if err := ctx.Err(); err != nil {
return nil, 0, err
}
if !hasZero || !hasVisible {
return img, 0, nil
}
if HighPrecision(source) {
return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil
}
return &transparentEdgeImage{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package image
import (
"context"
"io"
)
// contextInput 在读取图像编码时检查调用方取消状态
type contextInput struct {
io.ReadCloser
context context.Context
}
// Read 检查取消状态后读取原始数据
// 入参: data 数据缓冲区
// 返回: int 读取字节数, error 读取或取消错误
func (r contextInput) Read(data []byte) (int, error) {
if err := r.context.Err(); err != nil {
return 0, err
}
return r.ReadCloser.Read(data)
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"bytes" "bytes"
@@ -22,18 +22,18 @@ import (
"hash/crc32" "hash/crc32"
) )
// imagePageInfo 图片方向与像素密度,不改变原始编码 // PageInfo 图片方向与像素密度,不改变原始编码
type imagePageInfo struct { type PageInfo struct {
orientation int Orientation int
xdpi, ydpi float64 XDPI, YDPI float64
} }
// imagePageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消 // PageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消
// TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF // TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF
// 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI // 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
// 返回: imagePageInfo 方向与横纵DPI, error 元数据或取消错误 // 返回: PageInfo 方向与横纵DPI, error 元数据或取消错误
func imagePageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (imagePageInfo, error) { func PageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (PageInfo, error) {
info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi} info := PageInfo{Orientation: 1, XDPI: dpi, YDPI: dpi}
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return info, err return info, err
} }
@@ -41,20 +41,20 @@ func imagePageMetadataContext(ctx context.Context, data []byte, format string, d
var directory uint32 var directory uint32
scaleX, scaleY := 1.0, 1.0 scaleX, scaleY := 1.0, 1.0
if format == "tiff" { if format == "tiff" {
layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil { if err != nil {
return info, err return info, err
} }
directory = layout.offset directory = layout.Offset
exif = data 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) 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 { if format == "bmp" && len(data) >= 54 && binary.LittleEndian.Uint32(data[14:18]) >= 40 {
x := int32(binary.LittleEndian.Uint32(data[38:42])) x := int32(binary.LittleEndian.Uint32(data[38:42]))
y := int32(binary.LittleEndian.Uint32(data[42:46])) y := int32(binary.LittleEndian.Uint32(data[42:46]))
if x > 0 && y > 0 { 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" { 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:])) x, y := float64(binary.BigEndian.Uint32(payload)), float64(binary.BigEndian.Uint32(payload[4:]))
if x > 0 && y > 0 { if x > 0 && y > 0 {
if payload[8] == 1 { 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 { } else if payload[8] == 0 {
info.ydpi = dpi * y / x info.YDPI = dpi * y / x
} }
} }
} else if kind == "eXIf" { } else if kind == "eXIf" {
@@ -125,11 +125,11 @@ func imagePageMetadataContext(ctx context.Context, data []byte, format string, d
if x > 0 && y > 0 { if x > 0 && y > 0 {
switch payload[7] { switch payload[7] {
case 0: case 0:
info.ydpi = dpi * y / x info.YDPI = dpi * y / x
case 1: case 1:
info.xdpi, info.ydpi = x, y info.XDPI, info.YDPI = x, y
case 2: 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")) { } 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 { if err != nil {
return info, err return info, err
} }
info.orientation = orientation info.Orientation = orientation
if (format == "jpeg" || format == "tiff") && x > 0 && y > 0 { 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 format == "tiff" {
if orientation >= 5 { 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() return info, ctx.Err()
} }
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package image
import "image"
// StorageCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量
// 包装图像可实现BackingImage() image.Image,使底层共享缓冲计入预算
// 入参: img 图片资源, limit 缓存预算
// 返回: int 估算字节数或超预算标记
func StorageCost(img image.Image, limit int) int {
cost := 256
w, h := img.Bounds().Dx(), img.Bounds().Dy()
if limit < cost || w <= 0 || h <= 0 || w > (limit-cost)/8/h {
return limit + 1
}
pixels := w * h * 8
if encoded, ok := img.(*EncodedImage); ok {
if cap(encoded.data) > limit-cost-pixels {
return limit + 1
}
return cost + pixels + cap(encoded.data)
}
var buffers [5]int
switch source := img.(type) {
case *image.RGBA:
buffers[0] = cap(source.Pix)
case *image.RGBA64:
buffers[0] = cap(source.Pix)
case *image.NRGBA:
buffers[0] = cap(source.Pix)
case *image.NRGBA64:
buffers[0] = cap(source.Pix)
case *image.Gray:
buffers[0] = cap(source.Pix)
case *image.Gray16:
buffers[0] = cap(source.Pix)
case *image.Alpha:
buffers[0] = cap(source.Pix)
case *image.Alpha16:
buffers[0] = cap(source.Pix)
case *image.CMYK:
buffers[0] = cap(source.Pix)
case *image.Paletted:
if cap(source.Palette) > (limit-cost)/16 {
return limit + 1
}
buffers[0], buffers[1] = cap(source.Pix), cap(source.Palette)*16
case *image.YCbCr:
buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr)}
case *image.NYCbCrA:
buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr), cap(source.A)}
case *planarTIFFImage:
if cap(source.palette) > (limit-cost)/8 {
return limit + 1
}
cost += cap(source.palette) * 8
if cap(source.planes) > (limit-cost)/16 || len(source.planes) > len(buffers) {
return limit + 1
}
cost += cap(source.planes) * 16
for i, plane := range source.planes {
switch plane := plane.(type) {
case *image.Gray:
buffers[i] = cap(plane.Pix)
case *image.Gray16:
buffers[i] = cap(plane.Pix)
default:
buffers[i] = StorageCost(plane, limit-cost)
}
}
case *tiffFaxImage:
if cap(source.blocks) > (limit-cost)/16 {
return limit + 1
}
cost += cap(source.blocks) * 16
for _, block := range source.blocks {
size := StorageCost(block, limit-cost)
if size > limit-cost {
return limit + 1
}
cost += size
}
case *orientedTIFFImage:
buffers[0] = StorageCost(source.source, limit-cost)
case *maskedTIFFImage:
buffers[0] = StorageCost(source.source, limit-cost)
buffers[1] = StorageCost(source.mask, limit-cost)
case *transparentEdgeImage:
buffers[0] = StorageCost(source.source, limit-cost)
case *transparentEdgeImage64:
buffers[0] = StorageCost(source.source, limit-cost)
case interface{ BackingImage() image.Image }:
buffers[0] = StorageCost(source.BackingImage(), limit-cost)
}
storage := 0
for _, size := range buffers {
if size > limit-cost-storage {
return limit + 1
}
storage += size
}
if pixels > limit-cost {
return limit + 1
}
return cost + max(pixels, storage)
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"bytes" "bytes"
@@ -25,20 +25,20 @@ import (
"io" "io"
) )
// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 // ErrTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
var errTIFFRandomAccess = errors.New("TIFF directory requires random access") var ErrTIFFRandomAccess = errors.New("TIFF directory requires random access")
// tiffImageLayout 保存TIFF目录的图像布局及关联透明蒙版 // TIFFLayout 保存TIFF目录的图像布局及关联透明蒙版
type tiffImageLayout struct { type TIFFLayout struct {
width, height int Width, Height int
tileWidth int TileWidth int
tileHeight int TileHeight int
orientation int Orientation int
offset, next uint32 Offset, Next uint32
reduced, mask bool Reduced, mask bool
planar bool Planar bool
auxiliary bool auxiliary bool
samples uint32 Samples uint32
depth uint32 depth uint32
fillOrder uint32 fillOrder uint32
compression uint32 compression uint32
@@ -47,7 +47,7 @@ type tiffImageLayout struct {
profileOffset uint32 profileOffset uint32
profileSize uint32 profileSize uint32
t4, t6 uint32 t4, t6 uint32
transparency *tiffImageLayout Transparency *TIFFLayout
} }
// tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源 // tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源
@@ -60,28 +60,28 @@ type tiffDirectoryInput struct {
// orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度 // orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度
type orientedTIFFImage struct { type orientedTIFFImage struct {
source image.Image source image.Image
tiffImageLayout TIFFLayout
} }
// isTIFFData 识别标准TIFF字节序与版本标识 // IsTIFF 识别标准TIFF字节序与版本标识
// 入参: data 文件头或完整编码 // 入参: data 文件头或完整编码
// 返回: bool 是否为TIFF 6.0编码 // 返回: 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") return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a")
} }
// readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图 // readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图
// 入参: input 从文件头开始的输入流 // 入参: input 从文件头开始的输入流
// 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误 // 返回: TIFFLayout 存储尺寸与方向, error 文件结构或方向错误
func readTIFFLayout(input io.Reader) (tiffImageLayout, error) { func readTIFFLayout(input io.Reader) (TIFFLayout, error) {
return readTIFFLayoutContext(context.Background(), input) return ReadTIFFLayoutContext(context.Background(), input)
} }
// readTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消 // ReadTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消
// 入参: ctx 取消上下文, input 从文件头开始的输入流 // 入参: ctx 取消上下文, input 从文件头开始的输入流
// 返回: tiffImageLayout 存储尺寸与方向, error 结构、读取或取消错误 // 返回: TIFFLayout 存储尺寸与方向, error 结构、读取或取消错误
func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayout, error) { func ReadTIFFLayoutContext(ctx context.Context, input io.Reader) (TIFFLayout, error) {
var result tiffImageLayout var result TIFFLayout
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return result, err return result, err
} }
@@ -89,7 +89,7 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou
if _, err := io.ReadFull(input, header[:]); err != nil { if _, err := io.ReadFull(input, header[:]); err != nil {
return result, err return result, err
} }
if !isTIFFData(header[:]) { if !IsTIFF(header[:]) {
return result, fmt.Errorf("invalid TIFF header") return result, fmt.Errorf("invalid TIFF header")
} }
var order binary.ByteOrder = binary.LittleEndian 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:])) offset := int64(order.Uint32(header[4:]))
position := int64(8) position := int64(8)
var seen map[uint32]bool var seen map[uint32]bool
var transparency *tiffImageLayout var transparency *TIFFLayout
for offset != 0 { for offset != 0 {
if seen != nil && seen[uint32(offset)] { if seen != nil && seen[uint32(offset)] {
return result, fmt.Errorf("cyclic TIFF directory chain") return result, fmt.Errorf("cyclic TIFF directory chain")
@@ -109,37 +109,37 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou
return result, err return result, err
} }
if layout.mask { if layout.mask {
if !layout.reduced { if !layout.Reduced {
if transparency != nil { if transparency != nil {
return result, fmt.Errorf("multiple TIFF transparency masks") return result, fmt.Errorf("multiple TIFF transparency masks")
} }
transparency = &layout transparency = &layout
} }
} else if !layout.reduced || layout.next == 0 && seen == nil { } else if !layout.Reduced || layout.Next == 0 && seen == nil {
if result.offset != 0 { if result.Offset != 0 {
return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources") return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources")
} }
result = layout result = layout
} }
if layout.next != 0 { if layout.Next != 0 {
if seen == nil { if seen == nil {
seen = make(map[uint32]bool) seen = make(map[uint32]bool)
} }
seen[uint32(offset)] = true seen[uint32(offset)] = true
} }
offset, position = int64(layout.next), end offset, position = int64(layout.Next), end
} }
if err := ctx.Err(); err != nil { 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") return result, fmt.Errorf("missing TIFF main image")
} }
result.transparency = transparency result.Transparency = transparency
if transparency != nil { if transparency != nil {
size := result.displaySize() size := result.DisplaySize()
if size.X > int(^uint(0)>>1)/8/size.Y { 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 return result, nil
@@ -147,9 +147,9 @@ func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayou
// readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素 // readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素
// 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置 // 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置
// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误 // 返回: TIFFLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误
func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) { func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (TIFFLayout, int64, error) {
var result tiffImageLayout var result TIFFLayout
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return result, position, err return result, position, err
} }
@@ -163,9 +163,9 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr
} }
} else { } else {
if offset < position { 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 { if _, err := io.CopyN(io.Discard, reader, offset-position); err != nil {
return result, position, err return result, position, err
} }
@@ -174,7 +174,7 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr
return result, position, err return result, position, err
} }
count := int(order.Uint16(header[:2])) 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.depth = 1
result.compression = 1 result.compression = 1
result.predictor = 1 result.predictor = 1
@@ -230,9 +230,9 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr
case 255: case 255:
legacy = uint32(value) legacy = uint32(value)
case 256: case 256:
result.width = int(value) result.Width = int(value)
case 257: case 257:
result.height = int(value) result.Height = int(value)
case 259: case 259:
result.compression = uint32(value) result.compression = uint32(value)
case 262: case 262:
@@ -247,17 +247,17 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr
if value < 1 || value > 8 { if value < 1 || value > 8 {
return result, position, fmt.Errorf("invalid TIFF orientation") return result, position, fmt.Errorf("invalid TIFF orientation")
} }
result.orientation = int(value) result.Orientation = int(value)
case 277: case 277:
if value == 0 { if value == 0 {
return result, position, fmt.Errorf("invalid TIFF sample count") return result, position, fmt.Errorf("invalid TIFF sample count")
} }
result.samples = uint32(value) result.Samples = uint32(value)
case 284: case 284:
if value != 1 && value != 2 { if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF planar configuration") return result, position, fmt.Errorf("invalid TIFF planar configuration")
} }
result.planar = value == 2 result.Planar = value == 2
case 292: case 292:
result.t4 = uint32(value) result.t4 = uint32(value)
case 293: case 293:
@@ -265,27 +265,27 @@ func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOr
case 317: case 317:
result.predictor = uint32(value) result.predictor = uint32(value)
case 322: case 322:
result.tileWidth = int(value) result.TileWidth = int(value)
case 323: case 323:
result.tileHeight = int(value) result.TileHeight = int(value)
} }
} }
if _, err := io.ReadFull(input, header[:4]); err != nil { if _, err := io.ReadFull(input, header[:4]); err != nil {
return result, position, err return result, position, err
} }
result.next = order.Uint32(header[:4]) result.Next = order.Uint32(header[:4])
if hasSubfile { 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 { } 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") 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") 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, position, fmt.Errorf("invalid TIFF transparency mask")
} }
return result, offset + 6 + int64(count)*12, nil return result, offset + 6 + int64(count)*12, nil
@@ -346,19 +346,19 @@ func (r *tiffDirectoryInput) ReadAt(data []byte, offset int64) (int, error) {
// size 返回应用方向后的显示尺寸 // size 返回应用方向后的显示尺寸
// 返回: image.Point 像素宽高 // 返回: image.Point 像素宽高
func (l tiffImageLayout) size() image.Point { func (l TIFFLayout) size() image.Point {
if l.orientation >= 5 { if l.Orientation >= 5 {
return image.Pt(l.height, l.width) 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 保留高分辨率蒙版边缘的像素宽高 // 返回: image.Point 保留高分辨率蒙版边缘的像素宽高
func (l tiffImageLayout) displaySize() image.Point { func (l TIFFLayout) DisplaySize() image.Point {
size := l.size() size := l.size()
if l.transparency != nil { if l.Transparency != nil {
mask := l.transparency.size() mask := l.Transparency.size()
size.X, size.Y = max(size.X, mask.X), max(size.Y, mask.Y) size.X, size.Y = max(size.X, mask.X), max(size.Y, mask.Y)
} }
return size return size
@@ -381,8 +381,8 @@ func (s *orientedTIFFImage) sourcePoint(x, y int) (image.Point, bool) {
if !image.Pt(x, y).In(s.Bounds()) { if !image.Pt(x, y).In(s.Bounds()) {
return image.Point{}, false return image.Point{}, false
} }
w, h := s.width, s.height w, h := s.Width, s.Height
switch s.orientation { switch s.Orientation {
case 2: case 2:
x = w - 1 - x x = w - 1 - x
case 3: case 3:
@@ -421,7 +421,7 @@ func (s *orientedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
if !ok { if !ok {
return color.NRGBA64{} return color.NRGBA64{}
} }
return imageNRGBA64At(s.source, p.X, p.Y) return NRGBA64At(s.source, p.X, p.Y)
} }
// RGBA64At 返回预乘十六位像素,保留原始颜色精度 // RGBA64At 返回预乘十六位像素,保留原始颜色精度
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"cmp" "cmp"
@@ -38,7 +38,7 @@ type tiffBitOrderInput struct {
// tiffBitOrderRanges 收集低位优先的未压缩一位图像块,压缩传真位序由解码器处理 // tiffBitOrderRanges 收集低位优先的未压缩一位图像块,压缩传真位序由解码器处理
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: [][2]int64 按偏移排序且不重叠的字节区间, error 字段或布局错误 // 返回: [][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 { if layout.fillOrder != 2 {
return nil, nil return nil, nil
} }
@@ -46,7 +46,7 @@ func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout
if data[0] == 'M' { if data[0] == 'M' {
order = binary.BigEndian order = binary.BigEndian
} }
offset := int(layout.offset) offset := int(layout.Offset)
count := int(order.Uint16(data[offset:])) count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count) entries := make(map[uint16][]byte, count)
for i := range 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) return nil, fmt.Errorf("unsupported TIFF fill order for compression %d", method)
} }
if entry := entries[258]; entry != nil { 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") 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) depth, err := tiffPlaneValue(data, order, entry, i)
if err != nil || depth != 1 { if err != nil || depth != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for sample depth") 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 { if err != nil || rows == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip") 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 { if entries[322] != nil {
locations, lengths = entries[324], entries[325] locations, lengths = entries[324], entries[325]
width, err := value(322, 0) 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 { if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height") 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 { if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets") return nil, fmt.Errorf("invalid TIFF tile offsets")
} }
} }
if layout.planar { if layout.Planar {
if expected > uint64(^uint32(0))/uint64(layout.samples) { if expected > uint64(^uint32(0))/uint64(layout.Samples) {
return nil, fmt.Errorf("too many TIFF image blocks") return nil, fmt.Errorf("too many TIFF image blocks")
} }
expected *= uint64(layout.samples) expected *= uint64(layout.Samples)
} }
if locations == nil || lengths == nil { if locations == nil || lengths == nil {
return nil, fmt.Errorf("missing TIFF block arrays") return nil, fmt.Errorf("missing TIFF block arrays")
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"context" "context"
@@ -37,21 +37,21 @@ type tiffFaxImage struct {
// decodeTIFFFax 按TIFF传真选项解码各图像块,复用PDFGo的T.4与T.6解码器 // decodeTIFFFax 按TIFF传真选项解码各图像块,复用PDFGo的T.4与T.6解码器
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: image.Image 图像, error 字段、解码或取消错误 // 返回: 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 { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err 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") return nil, fmt.Errorf("invalid TIFF fax options")
} }
var order binary.ByteOrder = binary.LittleEndian var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' { if data[0] == 'M' {
order = binary.BigEndian order = binary.BigEndian
} }
offset := int(layout.offset) offset := int(layout.Offset)
count := int(order.Uint16(data[offset:])) count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count) entries := make(map[uint16][]byte, count)
for i := range 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 { if err != nil || photo > 1 && photo != 4 || entries[338] != nil {
return nil, fmt.Errorf("invalid TIFF fax color model") return nil, fmt.Errorf("invalid TIFF fax color model")
} }
width := uint32(layout.width) width := uint32(layout.Width)
height, err := value(278, ^uint32(0)) height, err := value(278, ^uint32(0))
if err != nil || height == 0 { if err != nil || height == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip") 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] locations, lengths := entries[273], entries[279]
tiled := entries[322] != nil tiled := entries[322] != nil
if tiled { 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 { 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") return nil, fmt.Errorf("invalid TIFF fax blocks")
} }
across := (uint64(layout.width)-1)/uint64(width) + 1 across := (uint64(layout.Width)-1)/uint64(width) + 1
down := (uint64(layout.height)-1)/uint64(height) + 1 down := (uint64(layout.Height)-1)/uint64(height) + 1
blocks := across * down blocks := across * down
if blocks != uint64(order.Uint32(locations[4:])) || blocks != uint64(order.Uint32(lengths[4:])) { if blocks != uint64(order.Uint32(locations[4:])) || blocks != uint64(order.Uint32(lengths[4:])) {
return nil, fmt.Errorf("invalid TIFF fax block count") 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++ { for i := uint64(0); i < blocks; i++ {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
@@ -139,7 +139,7 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im
} }
rows := height rows := height
if !tiled { 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) k := pdfgo.Integer(0)
if layout.compression == 4 { if layout.compression == 4 {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"context" "context"
@@ -26,19 +26,19 @@ type maskedTIFFImage struct {
bounds image.Rectangle bounds image.Rectangle
} }
// decodeTIFFImage 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版 // DecodeTIFF 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: image.Image 图像, error 解码或取消错误 // 返回: 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) source, err := decodeTIFFPixels(ctx, data, layout)
if err != nil || layout.transparency == nil { if err != nil || layout.Transparency == nil {
return source, err return source, err
} }
mask, err := decodeTIFFPixels(ctx, data, *layout.transparency) mask, err := decodeTIFFPixels(ctx, data, *layout.Transparency)
if err != nil { if err != nil {
return nil, err 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 返回主图和蒙版共同覆盖的像素区域 // Bounds 返回主图和蒙版共同覆盖的像素区域
@@ -61,11 +61,11 @@ func (s *maskedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
b := s.source.Bounds() b := s.source.Bounds()
sx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X)) 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)) 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() b = s.mask.Bounds()
mx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X)) 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)) 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 pixel.A = 0
} }
return pixel return pixel
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"bytes" "bytes"
@@ -52,15 +52,15 @@ type planarTIFFImage struct {
// needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用 // needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用
// 返回: bool 是否需要分量视图 // 返回: bool 是否需要分量视图
func (layout tiffImageLayout) needsSampleView() bool { func (layout TIFFLayout) needsSampleView() bool {
return layout.mask || layout.auxiliary || layout.photometric == 5 || layout.planar && layout.samples > 1 || layout.samples == 2 || layout.depth < 8 && (layout.samples > 1 || layout.depth > 1 || layout.predictor == 2) 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 每像素预算字节数 // 返回: uint64 每像素预算字节数
func (layout tiffImageLayout) decodedPixelBytes() uint64 { func (layout TIFFLayout) DecodedPixelBytes() uint64 {
samples := uint64(layout.samples) samples := uint64(layout.Samples)
if layout.planar { if layout.Planar {
if layout.photometric == 5 && samples > 4 { if layout.photometric == 5 && samples > 4 {
return 20 return 20
} }
@@ -81,7 +81,7 @@ func (layout tiffImageLayout) decodedPixelBytes() uint64 {
// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向 // decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局
// 返回: image.Image 图像, error 解码或取消错误 // 返回: 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) profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -122,7 +122,7 @@ func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout)
} }
pixels = result pixels = result
} else { } else {
input := newTIFFDirectoryInput(data, layout.offset) input := newTIFFDirectoryInput(data, layout.Offset)
input.ctx = ctx input.ctx = ctx
var err error var err error
pixels, err = decodeTIFFBitOrder(input, ranges) pixels, err = decodeTIFFBitOrder(input, ranges)
@@ -141,8 +141,8 @@ func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout)
return nil, err return nil, err
} }
} }
if layout.orientation != 1 { if layout.Orientation != 1 {
pixels = &orientedTIFFImage{source: pixels, tiffImageLayout: layout} pixels = &orientedTIFFImage{source: pixels, TIFFLayout: layout}
} }
return pixels, nil return pixels, nil
} }
@@ -150,7 +150,7 @@ func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout)
// tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面 // tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误 // 返回: *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 ctx != nil {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, nil, err return nil, nil, err
@@ -163,7 +163,7 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo
if data[0] == 'M' { if data[0] == 'M' {
order = binary.BigEndian order = binary.BigEndian
} }
offset := int(layout.offset) offset := int(layout.Offset)
count := int(order.Uint16(data[offset:])) count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count) entries := make(map[uint16][]byte, count)
for i := range 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 { if err != nil || predictor != 1 && predictor != 2 {
return nil, nil, fmt.Errorf("unsupported TIFF plane predictor") 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 { if result.cmyk {
for _, tag := range []uint16{332, 334} { for _, tag := range []uint16{332, 334} {
want := uint32(1) want := uint32(1)
@@ -214,7 +214,7 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo
} }
} }
planes := samples planes := samples
if !layout.planar && samples > 1 { if !layout.Planar && samples > 1 {
result.stride = int(samples) result.stride = int(samples)
planes = 1 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 { if err != nil || height == 0 || width%16 != 0 || height%16 != 0 {
return nil, nil, fmt.Errorf("invalid TIFF tile dimensions") 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 locations, lengths = 324, 325
} else { } else {
rows, err := value(278, math.MaxUint32) rows, err := value(278, math.MaxUint32)
if err != nil || rows == 0 { if err != nil || rows == 0 {
return nil, nil, fmt.Errorf("invalid TIFF rows per strip") 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) { if blocks > math.MaxUint32/uint64(planes) {
return nil, nil, fmt.Errorf("too many TIFF plane blocks") 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 { if depth > 8 {
pixelBytes *= 2 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") return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit")
} }
if predictor == 2 && (result.stride > 1 || depth < 8) { if predictor == 2 && (result.stride > 1 || depth < 8) {
result.predictorWidth = layout.width result.predictorWidth = layout.Width
if width != 0 { if width != 0 {
result.predictorWidth = int(width) result.predictorWidth = int(width)
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package image
import ( import (
"context" "context"
@@ -20,12 +20,26 @@ import (
"image" "image"
"image/color" "image/color"
"math" "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配置,校验颜色模型和数据范围 // readTIFFColorProfile 读取主图的ICC配置,校验颜色模型和数据范围
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: *colorProfile 颜色配置,未声明时为nil, error 配置或取消错误 // 返回: *pdfgo.ColorSpace 颜色配置,未声明时为nil, error 配置或取消错误
func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayout) (*colorProfile, error) { func readTIFFColorProfile(ctx context.Context, data []byte, layout TIFFLayout) (*pdfgo.ColorSpace, error) {
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
@@ -50,7 +64,7 @@ func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayo
default: default:
return nil, fmt.Errorf("unsupported TIFF profile color model") 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 { if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr return nil, cancelErr
} }
@@ -60,7 +74,7 @@ func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayo
// applyTIFFColorProfile 将源颜色转换为十六位sRGB,保留独立透明度和隐藏颜色 // applyTIFFColorProfile 将源颜色转换为十六位sRGB,保留独立透明度和隐藏颜色
// 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置 // 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置
// 返回: image.Image 独立透明度图像, error 颜色变换或取消错误 // 返回: 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 { if ctx == nil {
ctx = context.Background() 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)) values[i] = min(1, float64(value)/float64(alpha))
} }
} else { } else {
pixel := imageNRGBA64At(source, x, y) pixel := NRGBA64At(source, x, y)
alpha = pixel.A alpha = pixel.A
values = [4]float64{float64(pixel.R) / 65535, float64(pixel.G) / 65535, float64(pixel.B) / 65535} 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 { if err != nil {
return nil, err return nil, err
} }
+14 -12
View File
@@ -22,6 +22,8 @@ import (
"io" "io"
"runtime" "runtime"
"sync" "sync"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理 // outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
@@ -135,14 +137,14 @@ func outputImageWorkingSet(ctx context.Context, data []byte) int {
} }
input := bytes.NewReader(data) input := bytes.NewReader(data)
available := outputImageWorkingSetLimit - len(data) available := outputImageWorkingSetLimit - len(data)
if isTIFFData(data) { if baseimage.IsTIFF(data) {
layout, err := readTIFFLayoutContext(ctx, input) layout, err := baseimage.ReadTIFFLayoutContext(ctx, input)
if err != nil { if err != nil {
return outputImageWorkingSetLimit return outputImageWorkingSetLimit
} }
return len(data) + tiffImageWorkingSet(layout, available) 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 { if err != nil || config.Width <= 0 || config.Height <= 0 {
return outputImageWorkingSetLimit return outputImageWorkingSetLimit
} }
@@ -155,21 +157,21 @@ func outputImageWorkingSet(ctx context.Context, data []byte) int {
// tiffImageWorkingSet 分别估算显示缓冲及主图、蒙版的完整瓦片存储,超预算时返回上限 // tiffImageWorkingSet 分别估算显示缓冲及主图、蒙版的完整瓦片存储,超预算时返回上限
// 入参: layout 已校验的主图布局, limit 可用字节数 // 入参: layout 已校验的主图布局, limit 可用字节数
// 返回: int 不超过limit的估算值 // 返回: int 不超过limit的估算值
func tiffImageWorkingSet(layout tiffImageLayout, limit int) int { func tiffImageWorkingSet(layout baseimage.TIFFLayout, limit int) int {
size := layout.displaySize() size := layout.DisplaySize()
if size.X <= 0 || size.Y <= 0 || size.X > limit/16/size.Y { if size.X <= 0 || size.Y <= 0 || size.X > limit/16/size.Y {
return limit return limit
} }
cost := size.X * size.Y * 16 cost := size.X * size.Y * 16
for source := &layout; source != nil; source = source.transparency { for source := &layout; source != nil; source = source.Transparency {
width, height := uint64(source.width), uint64(source.height) width, height := uint64(source.Width), uint64(source.Height)
if source.tileWidth != 0 { if source.TileWidth != 0 {
x, y := uint64(source.tileWidth), uint64(source.tileHeight) x, y := uint64(source.TileWidth), uint64(source.TileHeight)
width, height = ((width-1)/x+1)*x, ((height-1)/y+1)*y width, height = ((width-1)/x+1)*x, ((height-1)/y+1)*y
} }
pixelSize := max(uint64(16), source.decodedPixelBytes()) pixelSize := max(uint64(16), source.DecodedPixelBytes())
if !source.planar { if !source.Planar {
pixelSize = max(pixelSize, uint64(source.samples)*4) pixelSize = max(pixelSize, uint64(source.Samples)*4)
} }
if width > uint64(limit-cost)/pixelSize/height { if width > uint64(limit-cost)/pixelSize/height {
return limit return limit
+8 -7
View File
@@ -23,6 +23,7 @@ import (
"io" "io"
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -36,10 +37,10 @@ type rasterDensity struct {
// 入参: data 图片编码 // 入参: data 图片编码
// 返回: string 标准资源格式,未识别时为空 // 返回: string 标准资源格式,未识别时为空
func outputImageFormat(data []byte) string { func outputImageFormat(data []byte) string {
if isJPEGData(data) { if baseimage.IsJPEG(data) {
return "JPEG" return "JPEG"
} }
if isPNGData(data) { if baseimage.IsPNG(data) {
return "PNG" return "PNG"
} }
return "" return ""
@@ -56,7 +57,7 @@ func optimizeImageResource(ctx context.Context, data []byte, options Compression
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
if options.Mode != CompressionUnchanged && isTIFFData(data) { if options.Mode != CompressionUnchanged && baseimage.IsTIFF(data) {
return optimizeTIFFResource(ctx, data, options, size) return optimizeTIFFResource(ctx, data, options, size)
} }
if options.Mode == CompressionUnchanged || !bytes.HasPrefix(data, []byte("BM")) { 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 像素需求 // 入参: ctx 取消上下文, data BMP编码, options 压缩选项, size 像素需求
// 返回: []byte 图片编码, error 解码、编码或取消错误 // 返回: []byte 图片编码, error 解码、编码或取消错误
func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]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 { if err != nil {
return data, nil return data, nil
} }
@@ -98,13 +99,13 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
return data, nil return data, nil
} }
if depth == 16 || depth == 32 { if depth == 16 || depth == 32 {
masks, err := bmpColorMasks(data, offset, depth, compression) masks, err := baseimage.BMPColorMasks(data, offset, depth, compression)
if err != nil { if err != nil {
return data, nil return data, nil
} }
if options.Mode == CompressionLossless { if options.Mode == CompressionLossless {
for _, mask := range masks { for _, mask := range masks {
if mask.max > 255 { if mask.Max > 255 {
return data, nil return data, nil
} }
} }
@@ -112,7 +113,7 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
} }
var pixels image.Image var pixels image.Image
if depth == 16 || compression == 3 { if depth == 16 || compression == 3 {
pixels, err = decodeBMPImageContext(ctx, data) pixels, err = baseimage.DecodeBMPContext(ctx, data)
} else { } else {
pixels, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) pixels, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
} }
+11 -9
View File
@@ -20,6 +20,8 @@ import (
"encoding/binary" "encoding/binary"
"image" "image"
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度 // optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度
@@ -27,22 +29,22 @@ import (
// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求 // 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求
// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误 // 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误
func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]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 { if err != nil {
return data, ctx.Err() return data, ctx.Err()
} }
pixelSize := int64(layout.decodedPixelBytes()) pixelSize := int64(layout.DecodedPixelBytes())
if int64(layout.width) > outputOptimizationBufferLimit/pixelSize/int64(layout.height) { if int64(layout.Width) > outputOptimizationBufferLimit/pixelSize/int64(layout.Height) {
return data, ctx.Err() 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() return data, ctx.Err()
} }
density, ok := tiffCompressionDensity(data, layout) density, ok := tiffCompressionDensity(data, layout)
if !ok { if !ok {
return data, ctx.Err() return data, ctx.Err()
} }
pixels, err := decodeTIFFImage(ctx, data, layout) pixels, err := baseimage.DecodeTIFF(ctx, data, layout)
if err != nil { if err != nil {
return data, ctx.Err() return data, ctx.Err()
} }
@@ -53,16 +55,16 @@ func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionO
// 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码 // 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码
// 入参: data TIFF编码, layout 已校验的主图布局 // 入参: data TIFF编码, layout 已校验的主图布局
// 返回: rasterDensity PNG像素密度, bool 是否可完整转换 // 返回: rasterDensity PNG像素密度, bool 是否可完整转换
func tiffCompressionDensity(data []byte, layout tiffImageLayout) (rasterDensity, bool) { func tiffCompressionDensity(data []byte, layout baseimage.TIFFLayout) (rasterDensity, bool) {
var density rasterDensity var density rasterDensity
var order binary.ByteOrder = binary.LittleEndian var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' { if data[0] == 'M' {
order = binary.BigEndian 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 return density, false
} }
offset := int(layout.offset) offset := int(layout.Offset)
count := int(order.Uint16(data[offset:])) count := int(order.Uint16(data[offset:]))
unit := uint32(2) unit := uint32(2)
var resolutions [2][2]uint32 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 { 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 return density, false
} }
if layout.orientation >= 5 { if layout.Orientation >= 5 {
density.x, density.y = density.y, density.x density.x, density.y = density.y, density.x
} }
return density, true return density, true
+4 -2
View File
@@ -32,6 +32,8 @@ import (
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// editorPathValidationLimit 限制同次对象校验保留的路径数量 // editorPathValidationLimit 限制同次对象校验保留的路径数量
@@ -483,7 +485,7 @@ func (e *Editor) ImageSize(id string) (image.Point, error) {
// 入参: data 图片数据, decoded 已解码像素,为nil时按需解码 // 入参: data 图片数据, decoded 已解码像素,为nil时按需解码
// 返回: string 资源标识, error 注册错误 // 返回: string 资源标识, error 注册错误
func (e *Editor) addDecodedImage(data []byte, decoded image.Image) (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 { if err != nil {
return "", err return "", err
} }
@@ -676,7 +678,7 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro
if object.Type != "ImageObject" { if object.Type != "ImageObject" {
return fmt.Errorf("object %q is not an image", id) 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 { if err != nil {
return err return err
} }
+3 -1
View File
@@ -20,6 +20,8 @@ import (
"fmt" "fmt"
"maps" "maps"
"slices" "slices"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// DeleteCompositeObjects 删除内部成员,保留容器、未选对象及其他实例,一次撤销恢复全部 // 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") return fmt.Errorf("composite member is not an image")
} }
object := node.object object := node.object
decoded, _, err := decodeImagePixelsContext(context.Background(), data) decoded, _, err := baseimage.DecodePixelsContext(context.Background(), data)
if err != nil { if err != nil {
return err return err
} }
+5 -3
View File
@@ -27,6 +27,8 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
const ( const (
@@ -285,7 +287,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
return nil, err return nil, err
} }
if mode == FontEmbed { if mode == FontEmbed {
if err := validateFontEmbedding(parsed.Data); err != nil { if err := basefont.ValidateEmbedding(parsed.Data); err != nil {
return nil, err return nil, err
} }
} }
@@ -490,11 +492,11 @@ func fontBindingFile(ctx context.Context, reader *Reader, pending map[string][]b
// 入参: source 原字体, target 待嵌入字体 // 入参: source 原字体, target 待嵌入字体
// 返回: bool 是否保留原字体内容及字形编号 // 返回: bool 是否保留原字体内容及字形编号
func sameBindingFont(source, target []byte) bool { func sameBindingFont(source, target []byte) bool {
a, err := fontFileTables(source, 0) a, err := basefont.FileTables(source, 0)
if err != nil { if err != nil {
return false return false
} }
b, err := fontFileTables(target, 0) b, err := basefont.FileTables(target, 0)
if err != nil || len(a) != len(b) { if err != nil || len(a) != len(b) {
return false return false
} }
+8 -6
View File
@@ -20,6 +20,8 @@ import (
"fmt" "fmt"
"image" "image"
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// ImagePageSource 图片页面来源,按列表顺序逐张读取,每张图片处理完成后不再引用返回数据 // ImagePageSource 图片页面来源,按列表顺序逐张读取,每张图片处理完成后不再引用返回数据
@@ -115,7 +117,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return 0, err return 0, err
} }
config, format, err := decodeImageConfigDataContext(ctx, data) config, format, err := baseimage.DecodeConfigDataContext(ctx, data)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -125,18 +127,18 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
var decoded image.Image var decoded image.Image
checksum := sha256.Sum256(data) checksum := sha256.Sum256(data)
if !verified[checksum] { if !verified[checksum] {
decoded, _, err = decodeImagePixelsContext(ctx, data) decoded, _, err = baseimage.DecodePixelsContext(ctx, data)
if err != nil { if err != nil {
return 0, err return 0, err
} }
verified[checksum] = true verified[checksum] = true
} }
metadata, err := imagePageMetadataContext(ctx, data, format, options.DPI) metadata, err := baseimage.PageMetadataContext(ctx, data, format, options.DPI)
if err != nil { if err != nil {
return 0, err return 0, err
} }
w, h := float64(config.Width)*25.4/metadata.xdpi, float64(config.Height)*25.4/metadata.ydpi w, h := float64(config.Width)*25.4/metadata.XDPI, float64(config.Height)*25.4/metadata.YDPI
if metadata.orientation >= 5 { if metadata.Orientation >= 5 {
w, h = h, w w, h = h, w
} }
pw, ph := options.Width, options.Height pw, ph := options.Width, options.Height
@@ -160,7 +162,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
if err != nil { if err != nil {
return 0, err return 0, err
} }
matrix := imagePageOrientation(metadata.orientation, w, h) matrix := imagePageOrientation(metadata.Orientation, w, h)
_, err = e.AddObject(page, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{ _, err = e.AddObject(page, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{
ResourceID: resource, ResourceID: resource,
Boundary: fmt.Sprintf("%s %s %s %s", ofdNumber((pw-w)/2), ofdNumber((ph-h)/2), ofdNumber(w), ofdNumber(h)), Boundary: fmt.Sprintf("%s %s %s %s", ofdNumber((pw-w)/2), ofdNumber((ph-h)/2), ofdNumber(w), ofdNumber(h)),
+6 -4
View File
@@ -29,6 +29,8 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// editorSource 保留输入包及按需加载的原始页面 // editorSource 保留输入包及按需加载的原始页面
@@ -945,13 +947,13 @@ func (e *Editor) editorImage(id string) (image.Point, error) {
return image.Point{}, err return image.Point{}, err
} }
defer input.Close() defer input.Close()
config, _, err := decodeImageConfig(input) config, _, err := baseimage.DecodeConfig(input)
if errors.Is(err, errTIFFRandomAccess) { if errors.Is(err, baseimage.ErrTIFFRandomAccess) {
data, readErr := reader.readFileView(reader.ResPath(name)) data, readErr := reader.readFileView(reader.ResPath(name))
if readErr != nil { if readErr != nil {
return image.Point{}, readErr return image.Point{}, readErr
} }
config, _, err = decodeImageConfigData(data) config, _, err = baseimage.DecodeConfigData(data)
} }
if err != nil { if err != nil {
return image.Point{}, err return image.Point{}, err
@@ -984,7 +986,7 @@ func (e *Editor) validateImageMask(id string, size image.Point) error {
return err return err
} }
} }
mask, _, err := decodeImageData(data) mask, _, err := baseimage.Decode(data)
if err != nil { if err != nil {
return err return err
} }
+5 -3
View File
@@ -23,6 +23,8 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// ResourceTarget 描述当前页可绑定资源动作的图元,Bound表示已有该资源的点击动作 // ResourceTarget 描述当前页可绑定资源动作的图元,Bound表示已有该资源的点击动作
@@ -175,7 +177,7 @@ func (e *Editor) ReplaceResource(ctx context.Context, kind, id, format string, d
return fmt.Errorf("empty media data") return fmt.Errorf("empty media data")
} }
if kind == "Image" { if kind == "Image" {
img, actual, err := decodeImagePixelsContext(ctx, data) img, actual, err := baseimage.DecodePixelsContext(ctx, data)
if err != nil { if err != nil {
return err return err
} }
@@ -322,7 +324,7 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id
if err != nil { if err != nil {
return err return err
} }
config, _, err := decodeImageConfigDataContext(ctx, data) config, _, err := baseimage.DecodeConfigDataContext(ctx, data)
if err != nil { if err != nil {
return err return err
} }
@@ -341,7 +343,7 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id
if err != nil { if err != nil {
return err return err
} }
img, _, err = decodeImageDataContext(ctx, data) img, _, err = baseimage.DecodeContext(ctx, data)
if err != nil { if err != nil {
return err return err
} }
+2 -47
View File
@@ -15,7 +15,7 @@
package ofdgo package ofdgo
import ( import (
"encoding/binary" basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// FixFontDataAggressive 激进修复字体数据 // FixFontDataAggressive 激进修复字体数据
@@ -24,50 +24,5 @@ import (
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name // 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息 // 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) { func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
if len(data) < 4 { return basefont.FixDataAggressive(data, fixCmap, fixName)
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
} }
+4 -2
View File
@@ -19,6 +19,8 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位 // FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位
@@ -219,13 +221,13 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
r.preparedFonts[id] = prepared r.preparedFonts[id] = prepared
return prepared, nil 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 { if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil {
prepared.Data = data prepared.Data = data
if mapping != nil { if mapping != nil {
prepared.Glyphs = make(map[uint16]rune) prepared.Glyphs = make(map[uint16]rune)
for character, glyph := range mapping { for character, glyph := range mapping {
if character == packedGlyphRune(glyph) { if character == basefont.PackedGlyphRune(glyph) {
prepared.Glyphs[glyph] = character prepared.Glyphs[glyph] = character
} }
} }
+6 -110
View File
@@ -15,22 +15,14 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"encoding/binary"
"fmt" "fmt"
"strings"
"unicode/utf8" "unicode/utf8"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 // FontFace 字体文件中的字体名称与零起始索引,不依赖文件名
type FontFace struct { type FontFace = basefont.Face
Index int `json:"index"`
FullName string `json:"fullName"`
Family string `json:"family"`
Style string `json:"style"`
PostScriptName string `json:"postscriptName"`
Names []string `json:"names"`
}
// Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓 // Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓
// 返回: []FontFace 字体列表, error 格式或名称表错误 // 返回: []FontFace 字体列表, error 格式或名称表错误
@@ -39,23 +31,7 @@ func (f FontFile) Faces() ([]FontFace, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
count, err := fontFileCount(data) return basefont.ReadFaces(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
} }
// Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件 // Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件
@@ -66,26 +42,7 @@ func (f FontFile) Face(index int) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if bytes.HasPrefix(data, []byte("ttcf")) { return basefont.ExtractFace(data, index)
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
} }
// MissingGlyphs 检查指定字体的字符覆盖,不注册资源或修改文档,忽略段落换行 // MissingGlyphs 检查指定字体的字符覆盖,不注册资源或修改文档,忽略段落换行
@@ -99,66 +56,5 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
data, err = defaultCFF2Font(data) return basefont.MissingGlyphs(data, text)
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")
}
} }
+10 -8
View File
@@ -23,6 +23,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// 字体文件系统及目录条目的接口检查 // 字体文件系统及目录条目的接口检查
@@ -130,7 +132,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
sort.Strings(fsys.names) sort.Strings(fsys.names)
fsys.candidates = fontFileCandidates(fsys.names, path.Base) fsys.candidates = fontFileCandidates(fsys.names, path.Base)
for i, file := range fsys.candidates { 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 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))} fsys := &FontFS{files: make(map[string][]byte), loaders: make(map[string]func() ([]byte, error))}
for _, original := range names { for _, original := range names {
name := cleanFontName(original) name := cleanFontName(original)
if name == "." || !isFontFileName(name) { if name == "." || !basefont.IsFontFileName(name) {
continue continue
} }
if _, exists := fsys.files[name]; exists { if _, exists := fsys.files[name]; exists {
@@ -438,7 +440,7 @@ func fontFileMatches(candidates []fontFileCandidate, names []string, bold, itali
continue continue
} }
for j, alias := range append([]string{name}, fontQualifiedNames(name)...) { 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 == "" { if matcher.stem == "" {
continue continue
} }
@@ -507,9 +509,9 @@ func cleanFontName(name string) string {
func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate { func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate {
files := make([]fontFileCandidate, 0, len(names)) files := make([]fontFileCandidate, 0, len(names))
for _, name := range names { for _, name := range names {
if isFontFileName(name) { if basefont.IsFontFileName(name) {
baseName := base(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{ files = append(files, fontFileCandidate{
name: name, name: name,
base: baseName, base: baseName,
@@ -534,7 +536,7 @@ func newFontPatternMatcher(pattern string) fontPatternMatcher {
} }
if matcher.stem != "" { if matcher.stem != "" {
for _, alias := range fontExactCandidateNames(matcher.stem) { for _, alias := range fontExactCandidateNames(matcher.stem) {
alias = fontNormalizeName(alias) alias = basefont.NormalizeName(alias)
if alias != "" { if alias != "" {
matcher.aliases = append(matcher.aliases, alias) matcher.aliases = append(matcher.aliases, alias)
} }
@@ -563,7 +565,7 @@ func (m fontPatternMatcher) rank(name string) int {
return m.rankCandidate(fontFileCandidate{ return m.rankCandidate(fontFileCandidate{
base: name, base: name,
lowerBase: strings.ToLower(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)) suffix := matcher.styleSuffixNormalized(matcher.candidateName(file))
if rank == fontMatchFuzzy { if rank == fontMatchFuzzy {
suffix = matcher.styleSuffixNormalized(fontNormalizeName(file.base)) suffix = matcher.styleSuffixNormalized(basefont.NormalizeName(file.base))
} }
next := fontFileMatch{ next := fontFileMatch{
name: file.name, name: file.name,
+6 -4
View File
@@ -22,6 +22,8 @@ import (
"path" "path"
"slices" "slices"
"sort" "sort"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
const ( const (
@@ -128,7 +130,7 @@ func (r *Reader) fontData(id string, readonly bool) ([]byte, error) {
} }
if bytes.HasPrefix(data, []byte("ttcf")) { if bytes.HasPrefix(data, []byte("ttcf")) {
index := fontCollectionIndex(data, []string{f.FontName, f.FamilyName}, f.Bold, f.Italic) index := fontCollectionIndex(data, []string{f.FontName, f.FamilyName}, f.Bold, f.Italic)
return extractCollectionFont(data, index) return basefont.ExtractCollectionFont(data, index)
} }
return data, nil return data, nil
} }
@@ -245,11 +247,11 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
return nil, err return nil, err
} }
if data, err = decodeFontContainer(data); err == nil { 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) faces = make([]*FontFace, count)
for index := range faces { for index := range faces {
if tables, err := fontFileTables(data, index); err == nil { if tables, err := basefont.FileTables(data, index); err == nil {
info := fontFaceInfo(tables["name"], index) info := basefont.FaceInfo(tables["name"], index)
faces[index] = &info faces[index] = &info
} }
} }
+18 -60
View File
@@ -18,6 +18,8 @@ import (
"path" "path"
"slices" "slices"
"strings" "strings"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// 字体匹配按精确、部分和模糊匹配排列,零值表示未匹配 // 字体匹配按精确、部分和模糊匹配排列,零值表示未匹配
@@ -105,9 +107,6 @@ var fontMatchRules = normalizeFontMatchRules([]fontMatchRule{
}, },
}) })
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
// fontMatchRule 字体匹配规则 // fontMatchRule 字体匹配规则
type fontMatchRule struct { type fontMatchRule struct {
Keys []string Keys []string
@@ -121,7 +120,7 @@ type fontMatchRule struct {
// 入参: name 字体名称 // 入参: name 字体名称
// 返回: string 规范化后的字体名称 // 返回: string 规范化后的字体名称
func FontNormalizeName(name string) string { func FontNormalizeName(name string) string {
return fontNormalizeName(name) return basefont.NormalizeName(name)
} }
// FontCandidateNames 获取字体候选名称 // FontCandidateNames 获取字体候选名称
@@ -154,7 +153,7 @@ func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule {
keys := append(append(rule.Keys, rule.Names...), rule.System) keys := append(append(rule.Keys, rule.Names...), rule.System)
rule.Keys = nil rule.Keys = nil
for _, key := range keys { for _, key := range keys {
key = fontNormalizeName(key) key = basefont.NormalizeName(key)
if key != "" && !slices.Contains(rule.Keys, key) { if key != "" && !slices.Contains(rule.Keys, key) {
rule.Keys = append(rule.Keys, key) rule.Keys = append(rule.Keys, key)
} }
@@ -163,19 +162,6 @@ func normalizeFontMatchRules(rules []fontMatchRule) []fontMatchRule {
return rules 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 获取字体候选名称 // fontCandidateNames 获取字体候选名称
// 入参: names 字体名称列表 // 入参: names 字体名称列表
// 返回: []string 字体候选名称 // 返回: []string 字体候选名称
@@ -194,8 +180,8 @@ func fontExactCandidateNames(names ...string) []string {
// 入参: name 字体名称 // 入参: name 字体名称
// 返回: []string 保留后缀的候选名称 // 返回: []string 保留后缀的候选名称
func fontQualifiedNames(name string) []string { func fontQualifiedNames(name string) []string {
name = fontNormalizeName(name) name = basefont.NormalizeName(name)
key := fontNormalizeName(name) key := basefont.NormalizeName(name)
var names []string var names []string
seen := make(map[string]bool) seen := make(map[string]bool)
for _, rule := range fontMatchRules { for _, rule := range fontMatchRules {
@@ -210,7 +196,7 @@ func fontQualifiedNames(name string) []string {
} }
suffix := strings.TrimPrefix(name, prefix) suffix := strings.TrimPrefix(name, prefix)
for _, alias := range append([]string{rule.System}, rule.Names...) { 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 return names
@@ -228,7 +214,7 @@ func fontCandidateNamesByLevel(maxLevel int, names ...string) []string {
if name == "" { if name == "" {
continue continue
} }
result = appendFontName(result, seen, name) result = basefont.AppendFontName(result, seen, name)
bases = append(bases, name) bases = append(bases, name)
} }
levels := fontMatchLevels(bases) levels := fontMatchLevels(bases)
@@ -236,9 +222,9 @@ func fontCandidateNamesByLevel(maxLevel int, names ...string) []string {
for i := range bases { for i := range bases {
for j, rule := range fontMatchRules { for j, rule := range fontMatchRules {
if levels[i*len(fontMatchRules)+j] == level { 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 { 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 字体匹配模式主干 // 返回: string 字体匹配模式主干
func fontPatternStem(pattern string) string { func fontPatternStem(pattern string) string {
stem := strings.TrimSuffix(pattern, "*") stem := strings.TrimSuffix(pattern, "*")
if isFontFileName(stem) { if basefont.IsFontFileName(stem) {
stem = strings.TrimSuffix(stem, path.Ext(stem)) stem = strings.TrimSuffix(stem, path.Ext(stem))
} }
if index := strings.IndexAny(stem, "*?["); index >= 0 { if index := strings.IndexAny(stem, "*?["); index >= 0 {
stem = stem[:index] stem = stem[:index]
} }
return fontNormalizeName(stem) return basefont.NormalizeName(stem)
} }
// fontFilePatterns 获取字体文件匹配模式 // fontFilePatterns 获取字体文件匹配模式
@@ -293,7 +279,7 @@ func fontSystemNames(names ...string) []string {
seen := make(map[string]bool) seen := make(map[string]bool)
for _, name := range names { for _, name := range names {
for _, item := range fontCandidateNamesByLevel(fontMatchPartial, name) { for _, item := range fontCandidateNamesByLevel(fontMatchPartial, name) {
result = appendFontName(result, seen, item) result = basefont.AppendFontName(result, seen, item)
} }
} }
return result return result
@@ -311,13 +297,13 @@ func fontCandidateNamesAtLevel(level int, names []string, levels []int) []string
continue continue
} }
if level == fontMatchExact { if level == fontMatchExact {
result = appendFontName(result, seen, name) result = basefont.AppendFontName(result, seen, name)
} }
for j, rule := range fontMatchRules { for j, rule := range fontMatchRules {
if levels[i*len(fontMatchRules)+j] == level { 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 { for _, item := range rule.Names {
result = appendFontName(result, seen, item) result = basefont.AppendFontName(result, seen, item)
} }
} }
} }
@@ -339,7 +325,7 @@ func fontDefaultSystemNames() []string {
// 返回: bool 是否禁用合成粗体 // 返回: bool 是否禁用合成粗体
func fontNoSyntheticBold(names ...string) bool { func fontNoSyntheticBold(names ...string) bool {
for _, name := range names { for _, name := range names {
name = fontNormalizeName(name) name = basefont.NormalizeName(name)
for _, rule := range fontMatchRules { for _, rule := range fontMatchRules {
if !rule.NoSyntheticBold { if !rule.NoSyntheticBold {
continue continue
@@ -353,25 +339,13 @@ func fontNoSyntheticBold(names ...string) bool {
return false 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 按名称和规则顺序计算匹配等级 // fontMatchLevels 按名称和规则顺序计算匹配等级
// 入参: names 字体名称列表 // 入参: names 字体名称列表
// 返回: []int 名称与规则的匹配等级 // 返回: []int 名称与规则的匹配等级
func fontMatchLevels(names []string) []int { func fontMatchLevels(names []string) []int {
levels := make([]int, len(names)*len(fontMatchRules)) levels := make([]int, len(names)*len(fontMatchRules))
for i, name := range names { for i, name := range names {
name = fontNormalizeName(name) name = basefont.NormalizeName(name)
for j, rule := range fontMatchRules { for j, rule := range fontMatchRules {
levels[i*len(fontMatchRules)+j] = fontRuleMatchLevel(rule, name) levels[i*len(fontMatchRules)+j] = fontRuleMatchLevel(rule, name)
} }
@@ -400,22 +374,6 @@ func fontRuleMatchLevel(rule fontMatchRule, name string) int {
return level 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 追加字体文件匹配模式 // appendFontPattern 追加字体文件匹配模式
// 入参: patterns 匹配模式列表, seen 去重映射, pattern 匹配模式 // 入参: patterns 匹配模式列表, seen 去重映射, pattern 匹配模式
// 返回: []string 匹配模式列表 // 返回: []string 匹配模式列表
+10 -17
View File
@@ -17,6 +17,8 @@ package ofdgo
import ( import (
"fmt" "fmt"
"sync" "sync"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// sfntGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量 // sfntGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量
@@ -24,9 +26,9 @@ const sfntGlyphBoundsLimit = 4096
// sfntFontMetrics 保留默认字体度量,写出时不更新原始时间 // sfntFontMetrics 保留默认字体度量,写出时不更新原始时间
type sfntFontMetrics struct { type sfntFontMetrics struct {
*sfntFont *basefont.SFNT
*fontShaper *fontShaper
outlines *sfntOutliner outlines *basefont.Outliner
bounds *sfntFontBounds bounds *sfntFontBounds
} }
@@ -42,15 +44,6 @@ type sfntGlyphSize struct {
size float64 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 将字体轮廓转换为纵轴向下的几何路径 // glyphGeometry 将字体轮廓转换为纵轴向下的几何路径
type glyphGeometry struct{ path GeometryPath } type glyphGeometry struct{ path GeometryPath }
@@ -92,15 +85,15 @@ func (b SFNTBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo
// 入参: data 字体数据 // 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误 // 返回: FontMetrics 字体度量, error 解析错误
func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) { func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) {
data, err := defaultCFF2Font(data) data, err := basefont.DefaultCFF2Font(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sfnt, err := parseSFNTFont(data) sfnt, err := basefont.ParseSFNT(data)
if err != nil { if err != nil {
return nil, err 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} metrics.fontShaper = &fontShaper{metrics: metrics}
return metrics, nil return metrics, nil
} }
@@ -108,7 +101,7 @@ func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) {
// Write 返回字体数据,不修改字体元信息 // Write 返回字体数据,不修改字体元信息
// 返回: []byte 独立SFNT数据 // 返回: []byte 独立SFNT数据
func (f sfntFontMetrics) Write() []byte { func (f sfntFontMetrics) Write() []byte {
return f.sfntFont.Write() return f.SFNT.Write()
} }
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓 // GlyphOutline 返回基线原点、纵轴向下的字形轮廓
@@ -119,7 +112,7 @@ func (f sfntFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath,
return nil, fmt.Errorf("invalid glyph or size") return nil, fmt.Errorf("invalid glyph or size")
} }
path := &glyphGeometry{} 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 nil, err
} }
return path.path, nil return path.path, nil
@@ -161,7 +154,7 @@ func (f sfntFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error) {
// 入参: glyph 字形编号 // 入参: glyph 字形编号
// 返回: error 恢复原因,无恢复时为空 // 返回: error 恢复原因,无恢复时为空
func (f sfntFontMetrics) GlyphWarning(glyph uint16) error { func (f sfntFontMetrics) GlyphWarning(glyph uint16) error {
return f.outlines.glyphWarning(glyph) return f.outlines.GlyphWarning(glyph)
} }
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次 // GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
+3 -148
View File
@@ -16,23 +16,10 @@ package ofdgo
import ( import (
"bytes" "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 建立各字体的内部名称与文件别名匹配候选 // appendFontFileNames 建立各字体的内部名称与文件别名匹配候选
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称 // 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
// 返回: []fontFileCandidate 字体候选列表 // 返回: []fontFileCandidate 字体候选列表
@@ -43,7 +30,7 @@ func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]st
file.face = face file.face = face
file.names = make([]string, len(names)) file.names = make([]string, len(names))
for i, name := range names { for i, name := range names {
file.names[i] = fontNormalizeName(name) file.names[i] = basefont.NormalizeName(name)
} }
for _, name := range file.names { for _, name := range file.names {
file.normalized = name file.normalized = name
@@ -58,37 +45,11 @@ func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]st
return candidates 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 按内部名称与样式选择集合项,未匹配时保留首项 // fontCollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项
// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体 // 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: int 零起始字体索引 // 返回: int 零起始字体索引
func fontCollectionIndex(data []byte, names []string, bold, italic bool) 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 按名称与样式选择字体索引,未匹配时保留首项 // fontNameIndex 按名称与样式选择字体索引,未匹配时保留首项
@@ -101,109 +62,3 @@ func fontNameIndex(faces [][]string, names []string, bold, italic bool) int {
} }
return 0 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
}
+13 -425
View File
@@ -15,20 +15,17 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"maps"
"slices"
"github.com/go-text/typesetting/font/opentype/tables" basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理 // SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理
type SFNTBackend struct{} type SFNTBackend struct{}
// sfntResourceMetrics 隔离资源解析器的度量实现 // sfntResourceMetrics 隔离资源解析器的度量实现
type sfntResourceMetrics struct{ *sfntFont } type sfntResourceMetrics struct{ *basefont.SFNT }
// Name 返回资源后端标识 // Name 返回资源后端标识
// 返回: string 后端标识 // 返回: string 后端标识
@@ -43,15 +40,15 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
data, err = defaultCFF2Font(data) data, err = basefont.DefaultCFF2Font(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sfnt, err := parseSFNTFont(data) sfnt, err := basefont.ParseSFNT(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
name := sfntFontName(sfnt, 6, 4, 1) name := basefont.SFNTName(sfnt, 6, 4, 1)
if name == "" { if name == "" {
return nil, fmt.Errorf("font has no name") return nil, fmt.Errorf("font has no name")
} }
@@ -61,8 +58,8 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
} }
return &FontResource{ return &FontResource{
FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension, 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}, 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: sfntSubsettable(sfnt), CanSubset: basefont.Subsettable(sfnt),
}, nil }, nil
} }
@@ -70,432 +67,23 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号 // 入参: data 字体数据, glyphs 用字, preserve 是否保留编号
// 返回: []byte 子集, error 裁剪错误 // 返回: []byte 子集, error 裁剪错误
func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]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 按全包引用裁剪原字体,不确定时保留原资源 // SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源
// 入参: data 字体数据, usage 用字信息 // 入参: data 字体数据, usage 用字信息
// 返回: []byte 子集, error 裁剪错误 // 返回: []byte 子集, error 裁剪错误
func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) { func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) {
u := newEditorFontUsage() u := &basefont.Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool), Unsafe: usage.Unsafe}
u.unsafe = usage.Unsafe
for _, char := range usage.Characters { for _, char := range usage.Characters {
u.chars[char] = true u.Characters[char] = true
} }
for _, glyph := range usage.Glyphs { 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 返回未改动元信息的字体数据 // Write 返回未改动元信息的字体数据
// 返回: []byte 字体数据 // 返回: []byte 字体数据
func (f sfntResourceMetrics) Write() []byte { return f.sfntFont.Write() } func (f sfntResourceMetrics) Write() []byte { return f.SFNT.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)
}
+9 -7
View File
@@ -26,6 +26,8 @@ import (
"runtime" "runtime"
"sort" "sort"
"sync" "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) data, err = os.ReadFile(key.name)
} }
if err == nil && bytes.HasPrefix(data, []byte("ttcf")) { if err == nil && bytes.HasPrefix(data, []byte("ttcf")) {
data, err = extractCollectionFont(data, key.face) data, err = basefont.ExtractCollectionFont(data, key.face)
} }
if err == nil { if err == nil {
data, _, err = symbolFontProgram(context.Background(), data, "") data, _, err = basefont.SymbolProgram(context.Background(), data, "")
} }
r.fontSourcesCache.data[key] = fontSourceData{data, err} r.fontSourcesCache.data[key] = fontSourceData{data, err}
return data, err return data, err
@@ -229,7 +231,7 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
for _, name := range fontSystemNames(names...) { for _, name := range fontSystemNames(names...) {
source := fontSource{kind: fontSourceSystem, name: name, exact: true} source := fontSource{kind: fontSourceSystem, name: name, exact: true}
for i, requested := range names { for i, requested := range names {
if fontNormalizeName(name) == fontNormalizeName(requested) { if basefont.NormalizeName(name) == basefont.NormalizeName(requested) {
source.priority = (i - len(names)) * 2 source.priority = (i - len(names)) * 2
break break
} }
@@ -294,7 +296,7 @@ func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool)
if err != nil { if err != nil {
continue continue
} }
candidates = appendFontFileNames(candidates, i, fontFileNames(file)) candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
file.Close() file.Close()
} }
r.fontSourcesCache.directories[dir] = candidates 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) data, _ := io.ReadAll(file)
reader = bytes.NewReader(data) reader = bytes.NewReader(data)
} }
candidates = appendFontFileNames(candidates, i, fontFileNames(reader)) candidates = appendFontFileNames(candidates, i, basefont.FileNames(reader))
file.Close() file.Close()
} }
r.fontSourcesCache.filesystems[index] = candidates r.fontSourcesCache.filesystems[index] = candidates
@@ -373,7 +375,7 @@ func indexSystemFonts() []fontFileCandidate {
seen := make(map[string]bool) seen := make(map[string]bool)
for _, dir := range systemFontDirs() { for _, dir := range systemFontDirs() {
_ = filepath.WalkDir(dir, func(name string, entry fs.DirEntry, err error) error { _ = 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 seen[name] = true
files = append(files, name) files = append(files, name)
} }
@@ -386,7 +388,7 @@ func indexSystemFonts() []fontFileCandidate {
if err != nil { if err != nil {
continue continue
} }
candidates = appendFontFileNames(candidates, i, fontFileNames(file)) candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
file.Close() file.Close()
} }
return candidates return candidates
+7 -698
View File
@@ -15,386 +15,13 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"fmt" "fmt"
"image" "image"
"image/color"
_ "image/gif"
"io"
_ "github.com/xiaoqidun/j2kgo" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
_ "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 := 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 合并同尺寸二值蒙版和对象透明度,保留原图采样精度及只读像素 // imageWithEffects 合并同尺寸二值蒙版和对象透明度,保留原图采样精度及只读像素
// 入参: ctx 取消上下文,nil使用后台上下文, img 原图, mask 二值蒙版,可为空, alpha 对象透明度,可为空 // 入参: ctx 取消上下文,nil使用后台上下文, img 原图, mask 二值蒙版,可为空, alpha 对象透明度,可为空
// 返回: image.Image 处理后的图片, error 蒙版、解码或取消错误 // 返回: 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 { if img == nil || mask == nil && a == 255 {
return img, nil return img, nil
} }
source, err := imagePixelDataContext(ctx, img) source, err := baseimage.PixelDataContext(ctx, img)
if err != nil { if err != nil {
return nil, err return nil, err
} }
bounds := source.Bounds() bounds := source.Bounds()
var maskBounds image.Rectangle var maskBounds image.Rectangle
if mask != nil { if mask != nil {
mask, err = imagePixelDataContext(ctx, mask) mask, err = baseimage.PixelDataContext(ctx, mask)
if err != nil { if err != nil {
return nil, err 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") return nil, fmt.Errorf("image mask dimensions differ from source")
} }
} }
if imageHighPrecision(source) { if baseimage.HighPrecision(source) {
out := image.NewNRGBA64(bounds) out := image.NewNRGBA64(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ { 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 opacity *= int(value) / 255
} }
pixel := imageNRGBA64At(source, x, y) pixel := baseimage.NRGBA64At(source, x, y)
pixel.A = uint16(uint32(pixel.A) * uint32(opacity) / 255) pixel.A = uint16(uint32(pixel.A) * uint32(opacity) / 255)
out.SetNRGBA64(x, y, pixel) out.SetNRGBA64(x, y, pixel)
} }
@@ -471,7 +98,7 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i
return nil, err 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++ { 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 { if mask.Bounds().Size() != size || size.X <= 0 || size.Y <= 0 {
return fmt.Errorf("image mask dimensions differ from source") return fmt.Errorf("image mask dimensions differ from source")
} }
mask, err := imagePixelDataContext(ctx, mask) mask, err := baseimage.PixelDataContext(ctx, mask)
if err != nil { if err != nil {
return err return err
} }
@@ -540,321 +167,3 @@ func validateImageMask(ctx context.Context, mask image.Image, size image.Point)
} }
return ctx.Err() 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
}
+18 -256
View File
@@ -15,17 +15,12 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"errors"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
_ "image/jpeg"
_ "image/png" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"io"
"sync"
"sync/atomic"
) )
// imageCacheLimit 限制图片缓存的编码数据及预计解码内存 // imageCacheLimit 限制图片缓存的编码数据及预计解码内存
@@ -33,20 +28,7 @@ const imageCacheLimit = 64 << 20
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码 // EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回 // Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
type EncodedImage struct { type EncodedImage = baseimage.EncodedImage
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
}
// resampledImageMask 按替换图尺寸读取已校验的二值蒙版,不分配缩放像素 // resampledImageMask 按替换图尺寸读取已校验的二值蒙版,不分配缩放像素
type resampledImageMask struct { type resampledImageMask struct {
@@ -55,108 +37,6 @@ type resampledImageMask struct {
bounds image.Rectangle 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保留原始编码并惰性解码 // ImageResource 按资源标识读取只读图片,PNG和JPEG保留原始编码并惰性解码
// 应先读取源页面以加载页面资源,返回EncodedImage时通过Image检查像素解码错误 // 应先读取源页面以加载页面资源,返回EncodedImage时通过Image检查像素解码错误
// 入参: id 图片资源标识 // 入参: id 图片资源标识
@@ -176,7 +56,7 @@ func (r *Renderer) imageObjectResource(object ImageObject) (image.Image, error)
read := func(id string) (image.Image, error) { read := func(id string) (image.Image, error) {
img, err := r.ImageResource(id) img, err := r.ImageResource(id)
if err == nil && (r.decodeImages || object.Substitution != "") { if err == nil && (r.decodeImages || object.Substitution != "") {
pixels, decodeErr := imagePixelDataContext(r.pageContext, img) pixels, decodeErr := baseimage.PixelDataContext(r.pageContext, img)
err = decodeErr err = decodeErr
if r.decodeImages { if r.decodeImages {
img = pixels 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 { if err := validateImageMask(ctx, mask, original.Bounds().Size()); err != nil {
return nil, err return nil, err
} }
mask, err = imagePixelDataContext(ctx, mask) mask, err = baseimage.PixelDataContext(ctx, mask)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -264,7 +144,7 @@ func (s *resampledImageMask) GrayAt(x, y int) color.Gray {
bounds, size := s.original, s.bounds.Size() 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)) 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)) 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 返回采样后的二值灰度 // At 返回采样后的二值灰度
@@ -272,6 +152,10 @@ func (s *resampledImageMask) GrayAt(x, y int) color.Gray {
// 返回: color.Color 像素颜色 // 返回: color.Color 像素颜色
func (s *resampledImageMask) At(x, y int) color.Color { return s.GrayAt(x, y) } 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 返回不透明的十六位灰度颜色,供蒙版合成直接读取 // RGBA64At 返回不透明的十六位灰度颜色,供蒙版合成直接读取
// 入参: x 横坐标, y 纵坐标 // 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 像素颜色 // 返回: 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} 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 复用原始图片资源,不依赖输出后端 // cachedImageResource 复用原始图片资源,不依赖输出后端
// 入参: resPath 图片资源路径 // 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 读取错误 // 返回: image.Image 图片对象, error 读取错误
@@ -321,114 +181,11 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) {
if err != nil { if err != nil {
return nil, err 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 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 读取或复用图片资源,按输出需要解码像素 // decodeImageResource 读取或复用图片资源,按输出需要解码像素
// 入参: resPath 图片资源路径 // 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 读取或解码错误 // 返回: image.Image 图片对象, error 读取或解码错误
@@ -438,7 +195,7 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
return nil, err return nil, err
} }
if r.decodeImages { if r.decodeImages {
return imagePixelDataContext(r.pageContext, img) return baseimage.PixelDataContext(r.pageContext, img)
} }
return img, nil return img, nil
} }
@@ -455,6 +212,11 @@ func (r *Renderer) readImageResource(resPath string) (image.Image, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
img, _, err := decodeImageDataContext(ctx, data) img, _, err := baseimage.DecodeContext(ctx, data)
return img, err return img, err
} }
// NewEncodedImage 复制输入数据并读取PNG或JPEG头信息,不解码像素
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func NewEncodedImage(data []byte) (*EncodedImage, error) { return baseimage.NewEncodedImage(data) }
+4 -3
View File
@@ -21,6 +21,7 @@ import (
"math" "math"
"runtime" "runtime"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -417,9 +418,9 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg
externalNames = make(map[string]bool) externalNames = make(map[string]bool)
} }
name := pdfEmbeddedFontName(mark.Font.Name) name := pdfEmbeddedFontName(mark.Font.Name)
if !externalNames[fontNormalizeName(name)] { if !externalNames[basefont.NormalizeName(name)] {
for _, candidate := range fontExactCandidateNames(name) { for _, candidate := range fontExactCandidateNames(name) {
externalNames[fontNormalizeName(candidate)] = true externalNames[basefont.NormalizeName(candidate)] = true
} }
} }
} else { } else {
@@ -513,7 +514,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg
return nil, err return nil, err
} }
for font := range embeddedFonts { for font := range embeddedFonts {
if !externalNames[fontNormalizeName(pdfEmbeddedFontName(font.Name))] { if !externalNames[basefont.NormalizeName(pdfEmbeddedFontName(font.Name))] {
continue continue
} }
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
+2 -1
View File
@@ -20,6 +20,7 @@ import (
"image" "image"
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -248,7 +249,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
return err return err
} }
for x := region.Min.X; x < region.Max.X; x++ { for x := region.Min.X; x < region.Max.X; x++ {
a := imageAlphaAt(coverage, x, y) a := baseimage.AlphaAt(coverage, x, y)
if a == 0 { if a == 0 {
continue continue
} }
+2 -1
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -99,7 +100,7 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
} }
shape := 0.0 shape := 0.0
if coverage != nil { 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 shape = float64(a) / 65535
} }
if nodes[0].image != nil && shape != 0 { if nodes[0].image != nil && shape != 0 {
+22 -21
View File
@@ -23,6 +23,7 @@ import (
"unicode/utf16" "unicode/utf16"
"unicode/utf8" "unicode/utf8"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -137,22 +138,22 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
if cached := p.fonts[source]; cached != nil { if cached := p.fonts[source]; cached != nil {
return cached, nil return cached, nil
} }
var type1 *type1Program var type1 *basefont.Type1Program
result := &pdfImportedFont{} result := &pdfImportedFont{}
if source.ProgramType == "FontFile" { if source.ProgramType == "FontFile" {
parsed, err := parseType1Program(source.Program) parsed, err := basefont.ParseType1Program(source.Program)
if err != nil { if err != nil {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err)
} }
type1 = &parsed type1 = &parsed
result.type1Glyphs = parsed.glyphIDs() result.type1Glyphs = parsed.GlyphIDs()
} }
program, repair, err := pdfFontProgram(source, type1) program, repair, err := pdfFontProgram(source, type1)
if err != nil { if err != nil {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err)
} }
if repair.locations && p.warning == nil { 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 { if p.editor.backends.FontResources == nil {
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable) 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 p.symbolFonts[source] = nil
return nil, 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 { if err != nil {
return nil, err return nil, err
} }
@@ -235,19 +236,19 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) {
// 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号 // 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, pdfFontRepair 字体修复范围, error 错误信息 // 返回: []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 var repair pdfFontRepair
program := source.Program program := source.Program
if source.ProgramType == "FontFile" { if source.ProgramType == "FontFile" {
if type1 == nil { if type1 == nil {
parsed, err := parseType1Program(program) parsed, err := basefont.ParseType1Program(program)
if err != nil { if err != nil {
return nil, repair, err return nil, repair, err
} }
type1 = &parsed type1 = &parsed
} }
var err error var err error
program, err = type1.toCFF(pdfFontIdentity(source)) program, err = type1.ToCFF(pdfFontIdentity(source))
if err != nil { if err != nil {
return nil, repair, err 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 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 { if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
var err error var err error
program, _, err = wrapCFFToOTF(program) program, _, err = basefont.WrapCFFToOTF(program)
if err != nil { if err != nil {
return nil, repair, err return nil, repair, err
} }
} }
tables, err := fontFileTables(program, 0) tables, err := basefont.FileTables(program, 0)
if err != nil { if err != nil {
return nil, repair, err return nil, repair, err
} }
@@ -271,7 +272,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
if count == 0 { if count == 0 {
return nil, repair, fmt.Errorf("embedded PDF font has no glyphs") 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 { if err != nil {
return nil, repair, err return nil, repair, err
} }
@@ -281,11 +282,11 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
changed = true changed = true
} }
if cff := tables["CFF "]; len(cff) != 0 { if cff := tables["CFF "]; len(cff) != 0 {
sanitized, err := sanitizeCFF(cff) sanitized, err := basefont.SanitizeCFF(cff)
if err != nil { if err != nil {
return nil, repair, err 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 { if err != nil {
return nil, repair, err return nil, repair, err
} }
@@ -318,7 +319,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
} }
invalidCmap := false invalidCmap := false
if source.Subtype == "TrueType" || source.Subtype == "Type0" { if source.Subtype == "TrueType" || source.Subtype == "Type0" {
mapping := parseCmapMappings(tables["cmap"]) mapping := basefont.ParseCmapMappings(tables["cmap"])
for char, glyph := range mapping { for char, glyph := range mapping {
if glyph >= count { if glyph >= count {
delete(mapping, char) delete(mapping, char)
@@ -326,7 +327,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
} }
} }
if invalidCmap && source.Subtype == "TrueType" { if invalidCmap && source.Subtype == "TrueType" {
tables["cmap"] = buildCmapTable(count, mapping) tables["cmap"] = basefont.BuildCmapTable(count, mapping)
changed = true 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) return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count)
} }
} }
addPackedGlyphMapping(mapping, count) basefont.AddPackedGlyphMapping(mapping, count)
tables["cmap"] = buildCmapTable(count, mapping) tables["cmap"] = basefont.BuildCmapTable(count, mapping)
changed = true changed = true
} }
if len(fontNamesFromTable(tables["name"])) == 0 { if len(basefont.NamesFromTable(tables["name"])) == 0 {
fontName := pdfFontIdentity(source) fontName := pdfFontIdentity(source)
name := utf16.Encode([]rune(fontName)) name := utf16.Encode([]rune(fontName))
if len(name) > 32767 { if len(name) > 32767 {
@@ -368,12 +369,12 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
changed = true changed = true
} }
if len(tables["post"]) == 0 { if len(tables["post"]) == 0 {
tables["post"] = buildPostTable() tables["post"] = basefont.BuildPostTable()
changed = true changed = true
} }
if len(tables["OS/2"]) == 0 { if len(tables["OS/2"]) == 0 {
hhea := tables["hhea"] 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 changed = true
} }
for _, tag := range []string{"cvt ", "fpgm", "prep"} { for _, tag := range []string{"cvt ", "fpgm", "prep"} {
@@ -385,7 +386,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
if !changed && !pdfSFNTMissingPadding(program) { if !changed && !pdfSFNTMissingPadding(program) {
return program, repair, nil return program, repair, nil
} }
result, err := serializeOTF(tables) result, err := basefont.SerializeOTF(tables)
return result, repair, err return result, repair, err
} }
+2 -1
View File
@@ -20,6 +20,7 @@ import (
"image" "image"
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo" "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 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)) pixel.A = uint8(math.Round(float64(pixel.A) * opacity))
out.SetNRGBA(px, py, pixel) out.SetNRGBA(px, py, pixel)
} }
+2 -1
View File
@@ -25,6 +25,7 @@ import (
"strconv" "strconv"
"unicode/utf8" "unicode/utf8"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo" "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()) { if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{} return color.NRGBA64{}
} }
gray := imageNRGBA64At(s.source, x, y) gray := baseimage.NRGBA64At(s.source, x, y)
pixel := s.fill pixel := s.fill
pixel.A = uint16(65535 - gray.R) pixel.A = uint16(65535 - gray.R)
return pixel return pixel
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"image" "image"
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"golang.org/x/image/draw" "golang.org/x/image/draw"
"golang.org/x/image/math/f64" "golang.org/x/image/math/f64"
) )
@@ -101,7 +102,7 @@ func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Im
return nil return nil
} }
layer := image.NewRGBA64(bounds) 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) draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over)
return nil return nil
} }
+3 -1
View File
@@ -18,6 +18,8 @@ import (
"container/list" "container/list"
"image" "image"
"reflect" "reflect"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源 // renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
@@ -113,7 +115,7 @@ func rasterCommandsCost(commands []RasterCommand, limit int) int {
} }
images[command.Image] = true images[command.Image] = true
} }
part = imageResourceCost(command.Image, limit-cost) part = baseimage.StorageCost(command.Image, limit-cost)
if part > limit-cost { if part > limit-cost {
return limit + 1 return limit + 1
} }
+4 -2
View File
@@ -21,6 +21,8 @@ import (
"image" "image"
"math" "math"
"slices" "slices"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索 // DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索
@@ -225,7 +227,7 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error {
} }
return nil return nil
} }
img, _, err := decodeImageDataContext(ctx, stamp.Data) img, _, err := baseimage.DecodeContext(ctx, stamp.Data)
if err != nil { if err != nil {
return err return err
} }
@@ -236,7 +238,7 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error {
// 入参: img 印章图像, stamp 印章 // 入参: img 印章图像, stamp 印章
// 返回: error 编译错误 // 返回: error 编译错误
func (c *semanticCompiler) stampImage(img image.Image, stamp 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 { if err != nil {
return err return err
} }
+2 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer" "github.com/tdewolff/canvas/renderers/rasterizer"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// CanvasBackend canvas页面编译与绘制适配器,ColorSpace仅影响光栅输出 // 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} masks := renderCache[[32]byte, *image.Alpha]{limit: 16 << 20}
for i, cmd := range page.Commands { for i, cmd := range page.Commands {
if cmd.Image != nil { if cmd.Image != nil {
source, err := imagePixelData(cmd.Image) source, err := baseimage.PixelData(cmd.Image)
if err != nil { if err != nil {
return nil, fmt.Errorf("canvas image %d: %w", i, err) return nil, fmt.Errorf("canvas image %d: %w", i, err)
} }
+7 -2
View File
@@ -20,6 +20,7 @@ import (
"image/color" "image/color"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象 // canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象
@@ -105,7 +106,7 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) {
return return
} }
var err error var err error
img, err = imagePixelData(img) img, err = baseimage.PixelData(img)
if err != nil { if err != nil {
c.err = err c.err = err
return return
@@ -127,10 +128,14 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) {
// 返回: image.Rectangle 图像边界 // 返回: image.Rectangle 图像边界
func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect } func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect }
// BackingImage 返回共享的原图,用于计算底层像素存储
// 返回: image.Image 原图
func (s *rasterPaddedImage) BackingImage() image.Image { return s.source }
// ColorModel 按原图采样精度返回支持透明补边的预乘颜色模型 // ColorModel 按原图采样精度返回支持透明补边的预乘颜色模型
// 返回: color.Model 颜色模型 // 返回: color.Model 颜色模型
func (s *rasterPaddedImage) ColorModel() color.Model { func (s *rasterPaddedImage) ColorModel() color.Model {
if imageHighPrecision(s.source) { if baseimage.HighPrecision(s.source) {
return color.RGBA64Model return color.RGBA64Model
} }
return color.RGBAModel return color.RGBAModel
+4 -3
View File
@@ -19,6 +19,7 @@ import (
"sync" "sync"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态 // canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态
@@ -44,9 +45,9 @@ func decodeFontContainer(data []byte) ([]byte, error) {
case "\x00\x01\x00\x00", "OTTO", "true", "ttcf": case "\x00\x01\x00\x00", "OTTO", "true", "ttcf":
return data, nil return data, nil
case "wOFF": case "wOFF":
return decodeWOFF(data) return basefont.DecodeWOFF(data)
case "wOF2": case "wOF2":
decoded, handled, err := decodeWOFF2(data) decoded, handled, err := basefont.DecodeWOFF2(data)
if handled { if handled {
return decoded, err return decoded, err
} }
@@ -55,5 +56,5 @@ func decodeFontContainer(data []byte) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return serializeOTF(loaded.Tables) return basefont.SerializeOTF(loaded.Tables)
} }
+7 -8
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text" canvastext "github.com/tdewolff/canvas/text"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// textGlyphPathCacheKey 字形路径缓存键 // textGlyphPathCacheKey 字形路径缓存键
@@ -52,14 +53,14 @@ func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
// textGlyphPath 获取绘制字形路径 // textGlyphPath 获取绘制字形路径
// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器 // 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器
// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误 // 返回: *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{} p := &canvas.Path{}
width := 0.0 width := 0.0
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF { if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
x, y := face.XOffset, face.YOffset x, y := face.XOffset, face.YOffset
for _, shaped := range face.Glyphs(glyph.Text) { for _, shaped := range face.Glyphs(glyph.Text) {
part := &canvas.Path{} 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 return nil, 0, err
} }
p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset))) 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) width = face.MmPerEm * float64(x)
} else { } else {
glyphID := uint16(glyph.GlyphID) 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 return nil, 0, err
} }
width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
@@ -106,19 +107,17 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
cache := r.canvasFontOutlines() cache := r.canvasFontOutlines()
outlines, _ = cache.get(face.Font) outlines, _ = cache.get(face.Font)
if outlines == nil { if outlines == nil {
metrics, err := parseSFNTTables(face.Font.Tables) metrics, err := basefont.ParseSFNTTables(face.Font.Tables)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return &canvas.Path{}, 0 return &canvas.Path{}, 0
} }
outlines = &sfntOutliner{font: metrics} outlines = &basefont.Outliner{Font: metrics}
cost := uint64(512) cost := uint64(512)
for _, data := range metrics.Tables { for _, data := range metrics.Tables {
cost += uint64(len(data)) * 8 cost += uint64(len(data)) * 8
} }
for _, cmap := range metrics.cmaps { cost += uint64(metrics.CmapEntries()) * 48
cost += uint64(len(cmap.mapping)) * 48
}
cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1))) cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1)))
} }
r.canvasState().fontOutlines[face.Font] = outlines r.canvasState().fontOutlines[face.Font] = outlines
+11 -10
View File
@@ -23,6 +23,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvasimage "github.com/tdewolff/canvas/image" canvasimage "github.com/tdewolff/canvas/image"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"golang.org/x/image/draw" "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 { if _, pdf := ctx.Renderer.(*pdfRenderer); !pdf {
var pad int var pad int
var err error var err error
img, pad, err = imageWithTransparentEdge(r.pageContext, img) img, pad, err = baseimage.WithTransparentEdge(r.pageContext, img)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return return
@@ -147,13 +148,13 @@ func (r *Renderer) canvasEncodedImage(img image.Image) image.Image {
return encoded return encoded
} }
var encoded *canvasimage.Image var encoded *canvasimage.Image
if source.format == "jpeg" { if source.MIME() == "image/jpeg" {
encoded, _ = canvasimage.NewJPEGImage(bytes.NewReader(source.Bytes())) encoded, _ = canvasimage.NewJPEGImage(bytes.NewReader(source.Bytes()))
} else { } else {
encoded, _ = canvasimage.NewPNGImage(bytes.NewReader(source.Bytes())) encoded, _ = canvasimage.NewPNGImage(bytes.NewReader(source.Bytes()))
} }
encoded.Bytes = 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 return encoded
} }
@@ -207,7 +208,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
return img return img
} }
original := img original := img
source, err := imagePixelDataContext(r.pageContext, img) source, err := baseimage.PixelDataContext(r.pageContext, img)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return img return img
@@ -218,7 +219,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
if scale > 1 { if scale > 1 {
nw, nh := int(float64(w)*scale), int(float64(h)*scale) nw, nh := int(float64(w)*scale), int(float64(h)*scale)
var resized draw.Image var resized draw.Image
if imageHighPrecision(img) { if baseimage.HighPrecision(img) {
resized = image.NewNRGBA64(image.Rect(0, 0, nw, nh)) resized = image.NewNRGBA64(image.Rect(0, 0, nw, nh))
} else { } else {
resized = image.NewNRGBA(image.Rect(0, 0, nw, nh)) 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") r.renderError = fmt.Errorf("image clip raster dimensions differ from source")
return img return img
} }
result, err = imagePixelDataContext(work, result) result, err = baseimage.PixelDataContext(work, result)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return img return img
} }
if imageHighPrecision(img) || imageHighPrecision(result) { if baseimage.HighPrecision(img) || baseimage.HighPrecision(result) {
out := image.NewNRGBA64(bounds) out := image.NewNRGBA64(bounds)
maskBounds := result.Bounds() maskBounds := result.Bounds()
opaque := true opaque := true
@@ -279,8 +280,8 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
return img return img
} }
} }
pixel := imageNRGBA64At(img, x, y) pixel := baseimage.NRGBA64At(img, x, y)
alpha := imageAlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) alpha := baseimage.AlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y)
opaque = opaque && alpha == 65535 opaque = opaque && alpha == 65535
pixel.A = uint16(uint32(pixel.A) * alpha / 65535) pixel.A = uint16(uint32(pixel.A) * alpha / 65535)
out.SetNRGBA64(x, y, pixel) out.SetNRGBA64(x, y, pixel)
@@ -384,7 +385,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
return img 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 a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A
c.A = uint8(int(c.A) * int(a) / 255) c.A = uint8(int(c.A) * int(a) / 255)
out.SetNRGBA(x, y, c) out.SetNRGBA(x, y, c)
+2 -1
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text" canvastext "github.com/tdewolff/canvas/text"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
"github.com/xiaoqidun/pdfgo" "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) { func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas.Text, error) {
font := r.fonts[face.Font] font := r.fonts[face.Font]
if font == nil { 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 { if err != nil {
return nil, err return nil, err
} }
+2 -1
View File
@@ -21,6 +21,7 @@ import (
"image" "image"
"io" "io"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo" "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 { if err := ctx.Err(); err != nil {
return err return err
} }
pixels, err := imagePixelDataContext(ctx, original) pixels, err := baseimage.PixelDataContext(ctx, original)
if err != nil { if err != nil {
return err return err
} }
+3 -2
View File
@@ -20,6 +20,7 @@ import (
"image" "image"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// renderStamp 渲染印章 // renderStamp 渲染印章
@@ -87,7 +88,7 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
return return
} }
if len(s.Data) > 0 { if len(s.Data) > 0 {
img, _, err := decodeImageDataContext(pageContext, s.Data) img, _, err := baseimage.DecodeContext(pageContext, s.Data)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return return
@@ -149,7 +150,7 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
// renderStampImage 渲染印章图像 // renderStampImage 渲染印章图像
// 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度 // 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度
func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) { 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 { if err != nil {
r.renderError = err r.renderError = err
return return
+7 -6
View File
@@ -18,6 +18,7 @@ import (
"image" "image"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
) )
// canvasFontKey 按字体内容、名称及样式区分可复用的字体族,不依赖文档资源编号 // canvasFontKey 按字体内容、名称及样式区分可复用的字体族,不依赖文档资源编号
@@ -57,7 +58,7 @@ type canvasBackendState struct {
fontMap map[string]*canvas.FontFamily fontMap map[string]*canvas.FontFamily
svgFontCache map[*canvas.Font]SVGFont svgFontCache map[*canvas.Font]SVGFont
textGlyphPathCache renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue] textGlyphPathCache renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]
fontOutlines map[*canvas.Font]*sfntOutliner fontOutlines map[*canvas.Font]*basefont.Outliner
} }
// canvasClipIntersections 获取子渲染器共享的几何交集缓存,限制结果保留量 // canvasClipIntersections 获取子渲染器共享的几何交集缓存,限制结果保留量
@@ -118,13 +119,13 @@ func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.Font
} }
// canvasFontOutlines 获取子渲染器共享的字体轮廓解析缓存,限制字体及解析数据保留量 // canvasFontOutlines 获取子渲染器共享的字体轮廓解析缓存,限制字体及解析数据保留量
// 返回: *renderCache[*canvas.Font, *sfntOutliner] 字体轮廓缓存 // 返回: *renderCache[*canvas.Font, *basefont.Outliner] 字体轮廓缓存
func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *sfntOutliner] { func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *basefont.Outliner] {
key := canvasOutlineCacheKey{} key := canvasOutlineCacheKey{}
if state, ok := r.sharedBackendStates[key]; ok { 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 r.sharedBackendStates[key] = cache
return cache return cache
} }
@@ -149,5 +150,5 @@ func (s *canvasBackendState) resetFonts() {
s.fontMap = make(map[string]*canvas.FontFamily) s.fontMap = make(map[string]*canvas.FontFamily)
s.svgFontCache = make(map[*canvas.Font]SVGFont) s.svgFontCache = make(map[*canvas.Font]SVGFont)
s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20} s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20}
s.fontOutlines = make(map[*canvas.Font]*sfntOutliner) s.fontOutlines = make(map[*canvas.Font]*basefont.Outliner)
} }
+2 -1
View File
@@ -30,6 +30,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvasimage "github.com/tdewolff/canvas/image" canvasimage "github.com/tdewolff/canvas/image"
"github.com/tdewolff/canvas/renderers/svg" "github.com/tdewolff/canvas/renderers/svg"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -146,7 +147,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
font := text.MostCommonFontFace().Font font := text.MostCommonFontFace().Font
resource, ok := s.renderer.canvasState().svgFontCache[font] resource, ok := s.renderer.canvasState().svgFontCache[font]
if !ok { if !ok {
data := fontSFNTData(font.Tables) data := basefont.SFNTData(font.Tables)
resource = SVGFont{ resource = SVGFont{
Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()), Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()),
Weight: font.Style().CSS(), Weight: font.Style().CSS(),
+5 -19
View File
@@ -22,6 +22,8 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"strings" "strings"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
const ( const (
@@ -333,7 +335,7 @@ func sealMediaFromElements(elements []asn1.RawValue) (string, []byte, bool) {
if len(elements) < 2 || elements[1].Tag != asn1.TagOctetString { if len(elements) < 2 || elements[1].Tag != asn1.TagOctetString {
return "", nil, false return "", nil, false
} }
mediaType := normalizeSealType(sealString(elements[0])) mediaType := baseimage.NormalizeFormat(sealString(elements[0]))
if isSealMediaType(mediaType) { if isSealMediaType(mediaType) {
if mediaType == "ofd" { if mediaType == "ofd" {
if data := trimOFDPackage(elements[1].Bytes); len(data) > 0 { if data := trimOFDPackage(elements[1].Bytes); len(data) > 0 {
@@ -363,22 +365,6 @@ func sealString(raw asn1.RawValue) string {
return s 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 判断是否为可渲染印章媒体类型 // isSealMediaType 判断是否为可渲染印章媒体类型
// 入参: s 媒体类型 // 入参: s 媒体类型
// 返回: bool 是否可渲染 // 返回: bool 是否可渲染
@@ -438,11 +424,11 @@ func zipDataEnd(data []byte) int {
// 入参: data 原始数据 // 入参: data 原始数据
// 返回: string 媒体类型, []byte 图片数据 // 返回: string 媒体类型, []byte 图片数据
func probeImageMedia(data []byte) (string, []byte) { func probeImageMedia(data []byte) (string, []byte) {
_, format, err := decodeImageConfigData(data) _, format, err := baseimage.DecodeConfigData(data)
if err != nil { if err != nil {
return "", nil return "", nil
} }
return normalizeSealType(format), data return baseimage.NormalizeFormat(format), data
} }
// addStamp 添加印章到页面 // addStamp 添加印章到页面
+2 -1
View File
@@ -24,6 +24,7 @@ import (
"github.com/emmansun/gmsm/sm2" "github.com/emmansun/gmsm/sm2"
"github.com/emmansun/gmsm/sm3" "github.com/emmansun/gmsm/sm3"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// SES摘要及签名算法的对象标识和ASN.1序列标签 // SES摘要及签名算法的对象标识和ASN.1序列标签
@@ -593,7 +594,7 @@ func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
if !ok || len(items) < 4 { if !ok || len(items) < 4 {
return "", nil, fmt.Errorf("invalid ses picture") 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]) picData, err := asn1OctetString(items[1])
if err != nil { if err != nil {
return "", nil, err return "", nil, err
+2 -1
View File
@@ -35,6 +35,7 @@ import (
"github.com/emmansun/gmsm/sm2" "github.com/emmansun/gmsm/sm2"
"github.com/emmansun/gmsm/sm3" "github.com/emmansun/gmsm/sm3"
"github.com/emmansun/gmsm/smx509" "github.com/emmansun/gmsm/smx509"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// signatureWriteKey 已验证签署身份 // signatureWriteKey 已验证签署身份
@@ -157,7 +158,7 @@ func signatureWriteIdentity(options SignatureWriteOptions) (*signatureWriteKey,
return nil, fmt.Errorf("invalid seal maker signature") return nil, fmt.Errorf("invalid seal maker signature")
} }
mediaType, media := probeSealMedia(seal.PicData) 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") return nil, fmt.Errorf("invalid seal picture")
} }
} }
+2 -1
View File
@@ -25,6 +25,7 @@ import (
"image/png" "image/png"
"io" "io"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"golang.org/x/image/tiff" "golang.org/x/image/tiff"
) )
@@ -257,7 +258,7 @@ func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImag
return err return err
} }
} }
value := imageNRGBA64At(img, x, y) value := baseimage.NRGBA64At(img, x, y)
current := pixels.NRGBA64At(x, y) current := pixels.NRGBA64At(x, y)
if channels&8 != 0 { if channels&8 != 0 {
current.R = value.R current.R = value.R