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