mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
ee3fd02a0a
commit
64f9ec4241
87 files changed
+2693
-2289
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package font
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fontNameReplacer 移除字体名称匹配时忽略的分隔符
|
||||
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
||||
|
||||
// NormalizeName 规范化字体名称
|
||||
// 入参: name 字体名称
|
||||
// 返回: string 规范化后的字体名称
|
||||
func NormalizeName(name string) string {
|
||||
name = strings.ReplaceAll(name, "\\", "/")
|
||||
name = path.Base(strings.TrimSpace(name))
|
||||
if IsFontFileName(name) {
|
||||
name = strings.TrimSuffix(name, path.Ext(name))
|
||||
}
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
return fontNameReplacer.Replace(name)
|
||||
}
|
||||
|
||||
// IsFontFileName 判断是否为字体文件名
|
||||
// 入参: name 文件名
|
||||
// 返回: bool 是否为字体文件
|
||||
func IsFontFileName(name string) bool {
|
||||
switch strings.ToLower(path.Ext(name)) {
|
||||
case ".ttf", ".otf", ".ttc":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AppendFontName 追加字体名称
|
||||
// 入参: names 字体名称列表, seen 去重映射, name 字体名称
|
||||
// 返回: []string 字体名称列表
|
||||
func AppendFontName(names []string, seen map[string]bool, name string) []string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return names
|
||||
}
|
||||
key := NormalizeName(name)
|
||||
if key == "" || seen[key] {
|
||||
return names
|
||||
}
|
||||
seen[key] = true
|
||||
return append(names, name)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package font
|
||||
|
||||
// glyphPather 接收字体设计坐标中的轮廓指令
|
||||
type glyphPather interface {
|
||||
MoveTo(float64, float64)
|
||||
LineTo(float64, float64)
|
||||
QuadTo(float64, float64, float64, float64)
|
||||
CubeTo(float64, float64, float64, float64, float64, float64)
|
||||
Close()
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package font
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"unicode/utf16"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
)
|
||||
|
||||
// fontNameRecord 字体名称表记录
|
||||
type fontNameRecord struct {
|
||||
Platform uint16
|
||||
Encoding uint16
|
||||
Language uint16
|
||||
Name uint16
|
||||
Length uint16
|
||||
Offset uint16
|
||||
}
|
||||
|
||||
// FileNames 按字体索引读取字体或集合中各项的名称
|
||||
// 入参: file 字体数据
|
||||
// 返回: [][]string 各字体名称及带样式的字体族名称
|
||||
func FileNames(file io.ReaderAt) [][]string {
|
||||
var header [12]byte
|
||||
if _, err := file.ReadAt(header[:], 0); err != nil {
|
||||
return nil
|
||||
}
|
||||
if string(header[:4]) != "ttcf" {
|
||||
return [][]string{fontFaceNames(file, 0)}
|
||||
}
|
||||
version := binary.BigEndian.Uint32(header[4:])
|
||||
if version != 0x00010000 && version != 0x00020000 {
|
||||
return nil
|
||||
}
|
||||
var faces [][]string
|
||||
for i, count := uint32(0), binary.BigEndian.Uint32(header[8:]); i < count; i++ {
|
||||
var entry [4]byte
|
||||
if _, err := file.ReadAt(entry[:], 12+int64(i)*4); err != nil {
|
||||
return nil
|
||||
}
|
||||
faces = append(faces, fontFaceNames(file, int64(binary.BigEndian.Uint32(entry[:]))))
|
||||
}
|
||||
return faces
|
||||
}
|
||||
|
||||
// fontFaceNames 读取指定字体表目录中的名称
|
||||
// 入参: file 字体数据, offset 字体表目录偏移
|
||||
// 返回: []string 字体名称及带样式的字体族名称
|
||||
func fontFaceNames(file io.ReaderAt, offset int64) []string {
|
||||
var header [12]byte
|
||||
if _, err := file.ReadAt(header[:], offset); err != nil {
|
||||
return nil
|
||||
}
|
||||
switch string(header[:4]) {
|
||||
case "\x00\x01\x00\x00", "OTTO", "true":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
entries := make([]byte, int(binary.BigEndian.Uint16(header[4:]))*16)
|
||||
if _, err := file.ReadAt(entries, offset+12); err != nil {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(entries); i += 16 {
|
||||
if string(entries[i:i+4]) != "name" {
|
||||
continue
|
||||
}
|
||||
offset := int64(binary.BigEndian.Uint32(entries[i+8:]))
|
||||
length := int64(binary.BigEndian.Uint32(entries[i+12:]))
|
||||
data, err := io.ReadAll(io.NewSectionReader(file, offset, length))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return NamesFromTable(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamesFromTable 解析字体名称表
|
||||
// 入参: data 名称表数据
|
||||
// 返回: []string 字体名称列表
|
||||
func NamesFromTable(data []byte) []string {
|
||||
records, values := fontNameValues(data)
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, record := range records {
|
||||
key := [4]uint16{record.Platform, record.Encoding, record.Language, record.Name}
|
||||
name := values[key]
|
||||
switch record.Name {
|
||||
case 1, 16:
|
||||
key[3]++
|
||||
if style := values[key]; style != "" && name != "" {
|
||||
name += " " + style
|
||||
}
|
||||
case 4, 6:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
names = AppendFontName(names, seen, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// fontNameValues 解码字体名称表的多语言记录
|
||||
// 入参: data 名称表数据
|
||||
// 返回: []fontNameRecord 名称记录, map[[4]uint16]string 平台、编码、语言和名称对应的文本
|
||||
func fontNameValues(data []byte) ([]fontNameRecord, map[[4]uint16]string) {
|
||||
if len(data) < 6 || binary.BigEndian.Uint16(data) > 1 {
|
||||
return nil, nil
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||
storage := int(binary.BigEndian.Uint16(data[4:]))
|
||||
if 6+count*12 > storage || storage > len(data) {
|
||||
return nil, nil
|
||||
}
|
||||
records := make([]fontNameRecord, count)
|
||||
if err := binary.Read(bytes.NewReader(data[6:]), binary.BigEndian, records); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
values := make(map[[4]uint16]string)
|
||||
for _, record := range records {
|
||||
switch record.Name {
|
||||
case 1, 2, 4, 6, 16, 17:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
start := storage + int(record.Offset)
|
||||
end := start + int(record.Length)
|
||||
if end > len(data) {
|
||||
continue
|
||||
}
|
||||
var name string
|
||||
switch {
|
||||
case record.Platform == 0 || record.Platform == 3 && (record.Encoding == 0 || record.Encoding == 1 || record.Encoding == 10):
|
||||
if record.Length%2 != 0 {
|
||||
continue
|
||||
}
|
||||
chars := make([]uint16, int(record.Length)/2)
|
||||
for i := range chars {
|
||||
chars[i] = binary.BigEndian.Uint16(data[start+i*2:])
|
||||
}
|
||||
name = string(utf16.Decode(chars))
|
||||
case record.Platform == 1 && record.Encoding == 0:
|
||||
name, _ = charmap.Macintosh.NewDecoder().String(string(data[start:end]))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
values[[4]uint16{record.Platform, record.Encoding, record.Language, record.Name}] = name
|
||||
}
|
||||
return records, values
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package font
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// FixDataAggressive 激进修复字体数据
|
||||
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
|
||||
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染
|
||||
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
|
||||
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
|
||||
func FixDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
|
||||
if len(data) < 4 {
|
||||
return false, data, nil, false, nil
|
||||
}
|
||||
tag := string(data[0:4])
|
||||
u32Tag := binary.BigEndian.Uint32(data[0:4])
|
||||
isTrueType := u32Tag == 0x00010000 || tag == "true"
|
||||
isOpenType := tag == "OTTO"
|
||||
isTTC := tag == "ttcf"
|
||||
if isTTC {
|
||||
return false, data, nil, false, nil
|
||||
}
|
||||
if isBareCFFData(data) {
|
||||
otf, mapping, err := WrapCFFToOTF(data)
|
||||
if err != nil {
|
||||
return false, data, nil, false, err
|
||||
}
|
||||
return true, otf, mapping, false, nil
|
||||
}
|
||||
if !isTrueType && !isOpenType {
|
||||
return false, data, nil, false, nil
|
||||
}
|
||||
fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName)
|
||||
return fixed, newData, mapping, mc, err
|
||||
}
|
||||
|
||||
// SFNTData 保留字体原始表数据和时间,重新计算目录与校验和
|
||||
// 入参: tables 原始字体表
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func SFNTData(tables map[string][]byte) []byte {
|
||||
data, _ := SerializeOTF(tables)
|
||||
return data
|
||||
}
|
||||
|
||||
// isBareCFFData 检查是否为CFF裸字体数据
|
||||
// 入参: data 字体数据
|
||||
// 返回: bool 是否为CFF裸数据
|
||||
func isBareCFFData(data []byte) bool {
|
||||
if len(data) < 4 {
|
||||
return false
|
||||
}
|
||||
if data[0] != 1 || data[1] != 0 {
|
||||
return false
|
||||
}
|
||||
hdrSize := int(data[2])
|
||||
offSize := int(data[3])
|
||||
return 4 <= hdrSize && hdrSize <= len(data) && 1 <= offSize && offSize <= 4
|
||||
}
|
||||
@@ -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
|
||||
@@ -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":
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
|
||||
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
|
||||
type EncodedImage struct {
|
||||
data []byte
|
||||
format string
|
||||
config image.Config
|
||||
result atomic.Pointer[encodedImageResult]
|
||||
mu sync.Mutex
|
||||
pending chan struct{}
|
||||
}
|
||||
|
||||
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
|
||||
type encodedImageResult struct {
|
||||
image image.Image
|
||||
err error
|
||||
}
|
||||
|
||||
// NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据
|
||||
// 入参: data 原始编码
|
||||
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
|
||||
func NewEncodedImage(data []byte) (*EncodedImage, error) {
|
||||
return newEncodedImage(bytes.Clone(data))
|
||||
}
|
||||
|
||||
// Bytes 返回只读原始编码,调用方不得修改
|
||||
// 返回: []byte 原始编码
|
||||
func (img *EncodedImage) Bytes() []byte { return img.data }
|
||||
|
||||
// MIME 返回图片媒体类型
|
||||
// 返回: string 媒体类型
|
||||
func (img *EncodedImage) MIME() string { return "image/" + img.format }
|
||||
|
||||
// ColorModel 返回头信息中的颜色模型,不解码像素
|
||||
// 返回: color.Model 颜色模型
|
||||
func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel }
|
||||
|
||||
// Bounds 返回图片范围,不解码像素
|
||||
// 返回: image.Rectangle 图片范围
|
||||
func (img *EncodedImage) Bounds() image.Rectangle {
|
||||
return image.Rect(0, 0, img.config.Width, img.config.Height)
|
||||
}
|
||||
|
||||
// Image 首次解码图片并缓存结果或错误,支持并发读取
|
||||
// 返回: image.Image 只读像素, error 解码错误
|
||||
func (img *EncodedImage) Image() (image.Image, error) {
|
||||
if result := img.result.Load(); result != nil {
|
||||
return result.image, result.err
|
||||
}
|
||||
return img.ImageContext(context.Background())
|
||||
}
|
||||
|
||||
// ImageContext 解码并共享只读像素,取消或超时后可重试,nil使用后台上下文
|
||||
// 并发调用等待正在进行的解码,等待方取消不影响其他调用
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: image.Image 只读像素, error 解码或取消错误
|
||||
func (img *EncodedImage) ImageContext(ctx context.Context) (image.Image, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result := img.result.Load(); result != nil {
|
||||
return result.image, result.err
|
||||
}
|
||||
img.mu.Lock()
|
||||
if img.result.Load() != nil {
|
||||
img.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
pending := img.pending
|
||||
if pending == nil {
|
||||
pending = make(chan struct{})
|
||||
img.pending = pending
|
||||
img.mu.Unlock()
|
||||
return img.decodeContext(ctx, pending)
|
||||
}
|
||||
img.mu.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-pending:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decodeContext 执行本次解码,退出时唤醒等待方,只缓存完整结果及稳定错误
|
||||
// 入参: ctx 取消上下文, pending 本次解码的完成通知
|
||||
// 返回: image.Image 只读像素, error 解码或取消错误
|
||||
func (img *EncodedImage) decodeContext(ctx context.Context, pending chan struct{}) (image.Image, error) {
|
||||
defer func() {
|
||||
img.mu.Lock()
|
||||
img.pending = nil
|
||||
close(pending)
|
||||
img.mu.Unlock()
|
||||
}()
|
||||
pixels, _, err := image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(img.data)), context: ctx})
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
return nil, cancelErr
|
||||
}
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
img.result.Store(&encodedImageResult{image: pixels, err: err})
|
||||
return pixels, err
|
||||
}
|
||||
|
||||
// At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误
|
||||
// 入参: x 横坐标, y 纵坐标
|
||||
// 返回: color.Color 像素颜色
|
||||
func (img *EncodedImage) At(x, y int) color.Color {
|
||||
decoded, err := img.Image()
|
||||
if err != nil {
|
||||
return color.Transparent
|
||||
}
|
||||
return decoded.At(x, y)
|
||||
}
|
||||
|
||||
// newEncodedImage 接管编码数据并读取头信息
|
||||
// 入参: data 原始编码
|
||||
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
|
||||
func newEncodedImage(data []byte) (*EncodedImage, error) {
|
||||
return newEncodedImageContext(context.Background(), data)
|
||||
}
|
||||
|
||||
// newEncodedImageContext 读取PNG或JPEG头信息,元数据扫描期间响应取消,不复制输入或解码像素
|
||||
// 入参: ctx 取消上下文, data 只读图片编码
|
||||
// 返回: *EncodedImage 惰性图片, error 头信息、格式或取消错误
|
||||
func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, error) {
|
||||
config, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
return nil, cancelErr
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if format != "jpeg" && format != "png" {
|
||||
return nil, fmt.Errorf("unsupported encoded image %q", format)
|
||||
}
|
||||
return &EncodedImage{data: data, format: format, config: config}, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package image
|
||||
|
||||
// NormalizeFormat 标准化印章媒体类型
|
||||
// 入参: s 原始媒体类型
|
||||
// 返回: string 标准媒体类型
|
||||
func NormalizeFormat(s string) string {
|
||||
switch s {
|
||||
case "jpg":
|
||||
return "jpeg"
|
||||
case "bmp":
|
||||
return "bmp"
|
||||
case "jb2", "gbig2":
|
||||
return "jbig2"
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,714 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/gif"
|
||||
"io"
|
||||
|
||||
_ "github.com/xiaoqidun/j2kgo"
|
||||
_ "github.com/xiaoqidun/jbig2"
|
||||
_ "golang.org/x/image/bmp"
|
||||
"golang.org/x/image/tiff"
|
||||
)
|
||||
|
||||
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
|
||||
type transparentEdgeImage struct {
|
||||
source image.Image
|
||||
rect image.Rectangle
|
||||
}
|
||||
|
||||
// transparentEdgeImage64 按需补齐十六位像素边缘,保留原始只读图像
|
||||
type transparentEdgeImage64 struct {
|
||||
source image.Image
|
||||
rect image.Rectangle
|
||||
}
|
||||
|
||||
// Bounds 返回含采样边距的图像边界
|
||||
// 返回: image.Rectangle 图像边界
|
||||
func (s *transparentEdgeImage) Bounds() image.Rectangle { return s.rect }
|
||||
|
||||
// ColorModel 返回非预乘颜色模型
|
||||
// 返回: color.Model 颜色模型
|
||||
func (s *transparentEdgeImage) ColorModel() color.Model { return color.NRGBAModel }
|
||||
|
||||
// NRGBAAt 读取原始像素或按邻域补齐透明边缘,边界外透明
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.NRGBA 非预乘颜色
|
||||
func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
|
||||
if !image.Pt(x, y).In(s.rect) {
|
||||
return color.NRGBA{}
|
||||
}
|
||||
b := s.source.Bounds()
|
||||
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
|
||||
c := NRGBAAt(s.source, sx, sy)
|
||||
if c.A == 0 {
|
||||
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
|
||||
c = edge
|
||||
}
|
||||
}
|
||||
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
|
||||
c = transparentPaddingColor(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// At 返回非预乘像素颜色
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.Color 像素颜色
|
||||
func (s *transparentEdgeImage) At(x, y int) color.Color { return s.NRGBAAt(x, y) }
|
||||
|
||||
// RGBA64At 返回预乘十六位像素,供采样器无装箱读取
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.RGBA64 预乘颜色
|
||||
func (s *transparentEdgeImage) RGBA64At(x, y int) color.RGBA64 {
|
||||
r, g, b, a := s.NRGBAAt(x, y).RGBA()
|
||||
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
|
||||
}
|
||||
|
||||
// Bounds 返回含采样边距的图像边界
|
||||
// 返回: image.Rectangle 图像边界
|
||||
func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect }
|
||||
|
||||
// ColorModel 返回十六位非预乘颜色模型
|
||||
// 返回: color.Model 颜色模型
|
||||
func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model }
|
||||
|
||||
// NRGBA64At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.NRGBA64 非预乘颜色
|
||||
func (s *transparentEdgeImage64) NRGBA64At(x, y int) color.NRGBA64 {
|
||||
if !image.Pt(x, y).In(s.rect) {
|
||||
return color.NRGBA64{}
|
||||
}
|
||||
b := s.source.Bounds()
|
||||
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
|
||||
c := NRGBA64At(s.source, sx, sy)
|
||||
if c.A == 0 {
|
||||
var best color.NRGBA64
|
||||
for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ {
|
||||
for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ {
|
||||
v := NRGBA64At(s.source, nx, ny)
|
||||
if v.A > best.A {
|
||||
best = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.A != 0 {
|
||||
best.A = 1
|
||||
c = best
|
||||
}
|
||||
}
|
||||
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
|
||||
c.A = 1
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// At 返回非预乘十六位像素颜色
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.Color 像素颜色
|
||||
func (s *transparentEdgeImage64) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
|
||||
|
||||
// RGBA64At 返回保留原采样精度的预乘颜色
|
||||
// 入参: x 横向坐标, y 纵向坐标
|
||||
// 返回: color.RGBA64 预乘颜色
|
||||
func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 {
|
||||
r, g, b, a := s.NRGBA64At(x, y).RGBA()
|
||||
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
|
||||
}
|
||||
|
||||
// Decode 解码图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
|
||||
func Decode(data []byte) (image.Image, string, error) {
|
||||
return DecodeContext(context.Background(), data)
|
||||
}
|
||||
|
||||
// DecodeContext 解码图片并传递取消状态,PNG和JPEG仍保留惰性像素解码
|
||||
// 入参: ctx 取消上下文, data 只读图片编码
|
||||
// 返回: image.Image 图片对象, string 图片格式, error 解码或取消错误
|
||||
func DecodeContext(ctx context.Context, data []byte) (img image.Image, format string, err error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer func() {
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
img, err = nil, cancelErr
|
||||
}
|
||||
}()
|
||||
if IsJPEG(data) || IsPNG(data) {
|
||||
img, err := newEncodedImageContext(ctx, data)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return img, img.format, nil
|
||||
}
|
||||
if IsTIFF(data) {
|
||||
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, "tiff", err
|
||||
}
|
||||
img, err := DecodeTIFF(ctx, data, layout)
|
||||
return img, "tiff", err
|
||||
}
|
||||
img, format, err = image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||
if err == nil {
|
||||
return img, NormalizeFormat(format), nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil, "", ctx.Err()
|
||||
}
|
||||
if img, err := DecodeBMPContext(ctx, data); err == nil {
|
||||
return img, "bmp", nil
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// DecodePixelsContext 完整解码图片,确保像素可读取并传递取消状态
|
||||
// 入参: ctx 取消上下文, data 图片编码数据
|
||||
// 返回: image.Image 解码像素, string 图片格式, error 解码或取消错误
|
||||
func DecodePixelsContext(ctx context.Context, data []byte) (image.Image, string, error) {
|
||||
img, format, err := DecodeContext(ctx, data)
|
||||
if err != nil {
|
||||
return nil, format, err
|
||||
}
|
||||
pixels, err := PixelDataContext(ctx, img)
|
||||
return pixels, format, err
|
||||
}
|
||||
|
||||
// IsJPEG 判断是否为JPEG图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: bool 是否为JPEG图片数据
|
||||
func IsJPEG(data []byte) bool {
|
||||
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
|
||||
}
|
||||
|
||||
// IsPNG 判断是否为PNG图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: bool 是否为PNG图片数据
|
||||
func IsPNG(data []byte) bool {
|
||||
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
|
||||
}
|
||||
|
||||
// DecodeConfigData 解码图片尺寸
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
|
||||
func DecodeConfigData(data []byte) (image.Config, string, error) {
|
||||
return DecodeConfigDataContext(context.Background(), data)
|
||||
}
|
||||
|
||||
// DecodeConfigDataContext 读取图片尺寸和颜色模型,传递布局解析中的取消状态
|
||||
// 入参: ctx 取消上下文, data 图片数据
|
||||
// 返回: image.Config 图片配置, string 图片格式, error 读取或取消错误
|
||||
func DecodeConfigDataContext(ctx context.Context, data []byte) (config image.Config, format string, err error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return image.Config{}, "", err
|
||||
}
|
||||
defer func() {
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
config, err = image.Config{}, cancelErr
|
||||
}
|
||||
}()
|
||||
if IsTIFF(data) {
|
||||
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return image.Config{}, "tiff", err
|
||||
}
|
||||
profile, err := readTIFFColorProfile(ctx, data, layout)
|
||||
if err != nil {
|
||||
return image.Config{}, "tiff", err
|
||||
}
|
||||
var cfg image.Config
|
||||
if layout.needsSampleView() {
|
||||
img, _, err := tiffPlaneDirectories(ctx, data, layout)
|
||||
if err != nil {
|
||||
return image.Config{}, "tiff", err
|
||||
}
|
||||
cfg.ColorModel = img.ColorModel()
|
||||
} else {
|
||||
input := newTIFFDirectoryInput(data, layout.Offset)
|
||||
input.ctx = ctx
|
||||
cfg, err = tiff.DecodeConfig(input)
|
||||
if err != nil {
|
||||
return image.Config{}, "tiff", err
|
||||
}
|
||||
}
|
||||
if profile != nil || layout.Transparency != nil {
|
||||
cfg.ColorModel = color.NRGBA64Model
|
||||
}
|
||||
if layout.Transparency != nil {
|
||||
if _, err := readTIFFColorProfile(ctx, data, *layout.Transparency); err != nil {
|
||||
return image.Config{}, "tiff", err
|
||||
}
|
||||
if _, _, err := tiffPlaneDirectories(ctx, data, *layout.Transparency); err != nil {
|
||||
return image.Config{}, "tiff", err
|
||||
}
|
||||
}
|
||||
size := layout.DisplaySize()
|
||||
cfg.Width, cfg.Height = size.X, size.Y
|
||||
return cfg, "tiff", nil
|
||||
}
|
||||
cfg, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||
if err == nil {
|
||||
return cfg, NormalizeFormat(format), nil
|
||||
}
|
||||
if cfg, err := decodeBMPConfig(data); err == nil {
|
||||
return cfg, "bmp", nil
|
||||
}
|
||||
return image.Config{}, "", err
|
||||
}
|
||||
|
||||
// decodeBMPConfig 解码BMP尺寸
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Config 图片尺寸, error 错误信息
|
||||
func decodeBMPConfig(data []byte) (image.Config, error) {
|
||||
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
|
||||
if err != nil {
|
||||
return image.Config{}, err
|
||||
}
|
||||
if offset <= 0 || width <= 0 || height == 0 {
|
||||
return image.Config{}, fmt.Errorf("unsupported bmp")
|
||||
}
|
||||
if _, err := BMPColorMasks(data, offset, bpp, compression); err != nil {
|
||||
return image.Config{}, err
|
||||
}
|
||||
if height < 0 {
|
||||
height = -height
|
||||
}
|
||||
return image.Config{ColorModel: color.NRGBAModel, Width: width, Height: height}, nil
|
||||
}
|
||||
|
||||
// decodeBMPImage 解码BMP图片
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Image 图片对象, error 错误信息
|
||||
func decodeBMPImage(data []byte) (image.Image, error) {
|
||||
return DecodeBMPContext(context.Background(), data)
|
||||
}
|
||||
|
||||
// DecodeBMPContext 解码BMP真彩色像素,逐行检查取消
|
||||
// 入参: ctx 取消上下文, data 图片数据
|
||||
// 返回: image.Image 图片对象, error 格式或取消错误
|
||||
func DecodeBMPContext(ctx context.Context, data []byte) (image.Image, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if offset > len(data) {
|
||||
return nil, fmt.Errorf("invalid bmp pixel offset")
|
||||
}
|
||||
masks, err := BMPColorMasks(data, offset, bpp, compression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
topDown := height < 0
|
||||
if topDown {
|
||||
height = -height
|
||||
}
|
||||
if width <= 0 || height <= 0 {
|
||||
return nil, fmt.Errorf("invalid bmp size")
|
||||
}
|
||||
stride := ((int64(width)*int64(bpp) + 31) / 32) * 4
|
||||
if stride > int64(len(data)-offset)/int64(height) {
|
||||
return nil, fmt.Errorf("truncated bmp")
|
||||
}
|
||||
if width > int(^uint(0)>>1)/4/height {
|
||||
return nil, fmt.Errorf("bmp dimensions exceed image capacity")
|
||||
}
|
||||
rowStride := int(stride)
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srcY := y
|
||||
if !topDown {
|
||||
srcY = height - 1 - y
|
||||
}
|
||||
row := data[offset+srcY*rowStride:]
|
||||
for x := 0; x < width; x++ {
|
||||
switch bpp {
|
||||
case 16, 32:
|
||||
var value uint32
|
||||
if bpp == 16 {
|
||||
value = uint32(binary.LittleEndian.Uint16(row[x*2:]))
|
||||
} else {
|
||||
value = binary.LittleEndian.Uint32(row[x*4:])
|
||||
}
|
||||
p := y*img.Stride + x*4
|
||||
for i, mask := range masks {
|
||||
img.Pix[p+i] = mask.sample(value)
|
||||
}
|
||||
case 24:
|
||||
p := x * 3
|
||||
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// ParseBMPHeader 解析BMP文件头
|
||||
// 入参: data 图片数据
|
||||
// 返回: int 像素偏移, int 宽度, int 高度, uint16 位数, uint32 压缩方式, error 错误信息
|
||||
func ParseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
|
||||
if len(data) < 54 || string(data[:2]) != "BM" {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("not bmp")
|
||||
}
|
||||
offset := int(binary.LittleEndian.Uint32(data[10:14]))
|
||||
dibSize := binary.LittleEndian.Uint32(data[14:18])
|
||||
if dibSize < 40 {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
|
||||
}
|
||||
if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
|
||||
}
|
||||
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
|
||||
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
|
||||
planes := binary.LittleEndian.Uint16(data[26:28])
|
||||
bpp := binary.LittleEndian.Uint16(data[28:30])
|
||||
compression := binary.LittleEndian.Uint32(data[30:34])
|
||||
if planes != 1 || width <= 0 || height == 0 || int64(height) == -int64(^uint(0)>>1)-1 {
|
||||
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
|
||||
}
|
||||
return offset, width, height, bpp, compression, nil
|
||||
}
|
||||
|
||||
// PixelSource 获取图片像素源
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 图片像素源
|
||||
func PixelSource(img image.Image) image.Image {
|
||||
if decoded, err := PixelData(img); err == nil {
|
||||
return decoded
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// PixelData 获取像素并保留惰性解码错误,供绘制和像素处理入口校验
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 解码像素, error 解码错误
|
||||
func PixelData(img image.Image) (image.Image, error) {
|
||||
if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
|
||||
return src.Image()
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// PixelDataContext 获取像素并传递取消状态,兼容不接收上下文的图像实现
|
||||
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
|
||||
// 返回: image.Image 解码像素, error 解码或取消错误
|
||||
func PixelDataContext(ctx context.Context, img image.Image) (image.Image, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if src, ok := img.(interface {
|
||||
ImageContext(context.Context) (image.Image, error)
|
||||
}); ok {
|
||||
return src.ImageContext(ctx)
|
||||
}
|
||||
pixels, err := PixelData(img)
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
return nil, cancelErr
|
||||
}
|
||||
return pixels, err
|
||||
}
|
||||
|
||||
// GrayAt 获取图片灰度像素
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.Gray 灰度像素
|
||||
func GrayAt(img image.Image, x, y int) color.Gray {
|
||||
if src, ok := img.(*image.Gray); ok {
|
||||
return src.GrayAt(x, y)
|
||||
}
|
||||
if src, ok := img.(*image.Paletted); ok {
|
||||
return color.GrayModel.Convert(src.At(x, y)).(color.Gray)
|
||||
}
|
||||
if src, ok := img.(image.RGBA64Image); ok {
|
||||
r, g, b, _ := src.RGBA64At(x, y).RGBA()
|
||||
return color.Gray{Y: uint8((19595*r + 38470*g + 7471*b + 1<<15) >> 24)}
|
||||
}
|
||||
return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
|
||||
}
|
||||
|
||||
// NRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA NRGBA像素
|
||||
func NRGBAAt(img image.Image, x, y int) color.NRGBA {
|
||||
switch src := img.(type) {
|
||||
case interface{ NRGBAAt(int, int) color.NRGBA }:
|
||||
return src.NRGBAAt(x, y)
|
||||
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
|
||||
c := src.NRGBA64At(x, y)
|
||||
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
|
||||
case *image.RGBA, *image.RGBA64, *image.Gray, *image.Gray16, *image.Alpha, *image.Alpha16, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
|
||||
c := src.(image.RGBA64Image).RGBA64At(x, y)
|
||||
if c.A == 0 {
|
||||
return color.NRGBA{}
|
||||
}
|
||||
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
|
||||
if a != 0xffff {
|
||||
r = r * 0xffff / a
|
||||
g = g * 0xffff / a
|
||||
b = b * 0xffff / a
|
||||
}
|
||||
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
|
||||
}
|
||||
value := img.At(x, y)
|
||||
if c, ok := value.(color.NRGBA64); ok {
|
||||
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
|
||||
}
|
||||
return color.NRGBAModel.Convert(value).(color.NRGBA)
|
||||
}
|
||||
|
||||
// AlphaAt 读取十六位透明度,原生图像避免颜色装箱
|
||||
// 入参: img 图像, x 横坐标, y 纵坐标
|
||||
// 返回: uint32 十六位透明度
|
||||
func AlphaAt(img image.Image, x, y int) uint32 {
|
||||
if source, ok := img.(image.RGBA64Image); ok {
|
||||
return uint32(source.RGBA64At(x, y).A)
|
||||
}
|
||||
_, _, _, alpha := img.At(x, y).RGBA()
|
||||
return alpha
|
||||
}
|
||||
|
||||
// HighPrecision 判断颜色模型及调色板是否需要保留十六位采样
|
||||
// 入参: img 图片对象
|
||||
// 返回: bool 是否使用十六位颜色或透明度
|
||||
func HighPrecision(img image.Image) bool {
|
||||
model := img.ColorModel()
|
||||
if palette, ok := model.(color.Palette); ok {
|
||||
for _, value := range palette {
|
||||
switch value.(type) {
|
||||
case color.RGBA, color.NRGBA, color.Gray, color.Alpha, color.CMYK, color.YCbCr, color.NYCbCrA:
|
||||
default:
|
||||
pixel := color.NRGBA64Model.Convert(value).(color.NRGBA64)
|
||||
if pixel.R%257 != 0 || pixel.G%257 != 0 || pixel.B%257 != 0 || pixel.A%257 != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model
|
||||
}
|
||||
|
||||
// NRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA64 NRGBA64像素
|
||||
func NRGBA64At(img image.Image, x, y int) color.NRGBA64 {
|
||||
switch src := img.(type) {
|
||||
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
|
||||
return src.NRGBA64At(x, y)
|
||||
case *image.NRGBA:
|
||||
c := src.NRGBAAt(x, y)
|
||||
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
|
||||
case *image.Gray:
|
||||
v := uint16(src.GrayAt(x, y).Y) * 257
|
||||
return color.NRGBA64{R: v, G: v, B: v, A: 0xffff}
|
||||
case *image.Gray16:
|
||||
c := src.Gray16At(x, y)
|
||||
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff}
|
||||
case *image.Alpha, *image.Alpha16:
|
||||
c := src.(image.RGBA64Image).RGBA64At(x, y)
|
||||
if c.A == 0 {
|
||||
return color.NRGBA64{}
|
||||
}
|
||||
return color.NRGBA64{R: 0xffff, G: 0xffff, B: 0xffff, A: c.A}
|
||||
case *image.RGBA, *image.RGBA64, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
|
||||
c := src.(image.RGBA64Image).RGBA64At(x, y)
|
||||
if c.A == 0 {
|
||||
return color.NRGBA64{}
|
||||
}
|
||||
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
|
||||
if a != 0xffff {
|
||||
r = r * 0xffff / a
|
||||
g = g * 0xffff / a
|
||||
b = b * 0xffff / a
|
||||
}
|
||||
return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: c.A}
|
||||
}
|
||||
value := img.At(x, y)
|
||||
if c, ok := value.(color.NRGBA); ok {
|
||||
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
|
||||
}
|
||||
return color.NRGBA64Model.Convert(value).(color.NRGBA64)
|
||||
}
|
||||
|
||||
// transparentEdgeColor 获取透明像素相邻的可见颜色
|
||||
// 入参: source 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA 颜色, bool 是否存在
|
||||
func transparentEdgeColor(source image.Image, x, y int) (color.NRGBA, bool) {
|
||||
img, ok := source.(*image.NRGBA)
|
||||
if !ok {
|
||||
bounds := source.Bounds()
|
||||
var best color.NRGBA
|
||||
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
|
||||
for nx := max(x-1, bounds.Min.X); nx < min(x+2, bounds.Max.X); nx++ {
|
||||
if nx == x && ny == y {
|
||||
continue
|
||||
}
|
||||
pixel := NRGBAAt(source, nx, ny)
|
||||
if pixel.A > best.A {
|
||||
best = pixel
|
||||
if best.A == 255 {
|
||||
best.A = 1
|
||||
return best, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.A == 0 {
|
||||
return color.NRGBA{}, false
|
||||
}
|
||||
best.A = 1
|
||||
return best, true
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
best, alpha := 0, uint8(0)
|
||||
left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X)
|
||||
scan:
|
||||
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
|
||||
offset := img.PixOffset(left, ny)
|
||||
for nx := left; nx < right; nx++ {
|
||||
if (nx != x || ny != y) && img.Pix[offset+3] > alpha {
|
||||
best, alpha = offset, img.Pix[offset+3]
|
||||
if alpha == 255 {
|
||||
break scan
|
||||
}
|
||||
}
|
||||
offset += 4
|
||||
}
|
||||
}
|
||||
if alpha == 0 {
|
||||
return color.NRGBA{}, false
|
||||
}
|
||||
return color.NRGBA{R: img.Pix[best], G: img.Pix[best+1], B: img.Pix[best+2], A: 1}, true
|
||||
}
|
||||
|
||||
// transparentPaddingColor 获取透明补齐颜色
|
||||
// 入参: c 边缘颜色
|
||||
// 返回: color.NRGBA 补齐颜色
|
||||
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
|
||||
c.A = 1
|
||||
return c
|
||||
}
|
||||
|
||||
// WithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度
|
||||
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
|
||||
// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误
|
||||
func WithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if img == nil {
|
||||
return img, 0, nil
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
if w == 0 || h == 0 {
|
||||
return img, 0, nil
|
||||
}
|
||||
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
|
||||
return img, 0, nil
|
||||
}
|
||||
source, err := PixelDataContext(ctx, img)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
switch source.(type) {
|
||||
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
|
||||
return img, 0, nil
|
||||
}
|
||||
hasZero, hasVisible := false, false
|
||||
var pixels []byte
|
||||
var stride, step int
|
||||
switch src := source.(type) {
|
||||
case *image.NRGBA:
|
||||
pixels, stride, step = src.Pix, src.Stride, 4
|
||||
case *image.RGBA:
|
||||
pixels, stride, step = src.Pix, src.Stride, 4
|
||||
case *image.Alpha:
|
||||
pixels, stride, step = src.Pix, src.Stride, 1
|
||||
}
|
||||
if pixels != nil {
|
||||
hasZero = pixels[step-1] == 0
|
||||
hasVisible = !hasZero
|
||||
scan:
|
||||
for y := 0; y < h; y++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
offset := y*stride + step - 1
|
||||
for x := 0; x < w; x++ {
|
||||
if x&255 == 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
if (pixels[offset] == 0) != hasZero {
|
||||
hasZero, hasVisible = true, true
|
||||
break scan
|
||||
}
|
||||
offset += step
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y && !(hasZero && hasVisible); y++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
if (x-bounds.Min.X)&255 == 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
if AlphaAt(source, x, y) == 0 {
|
||||
hasZero = true
|
||||
} else {
|
||||
hasVisible = true
|
||||
}
|
||||
if hasZero && hasVisible {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !hasZero || !hasVisible {
|
||||
return img, 0, nil
|
||||
}
|
||||
if HighPrecision(source) {
|
||||
return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil
|
||||
}
|
||||
return &transparentEdgeImage{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
// contextInput 在读取图像编码时检查调用方取消状态
|
||||
type contextInput struct {
|
||||
io.ReadCloser
|
||||
context context.Context
|
||||
}
|
||||
|
||||
// Read 检查取消状态后读取原始数据
|
||||
// 入参: data 数据缓冲区
|
||||
// 返回: int 读取字节数, error 读取或取消错误
|
||||
func (r contextInput) Read(data []byte) (int, error) {
|
||||
if err := r.context.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ReadCloser.Read(data)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() }
|
||||
@@ -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
@@ -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
@@ -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) }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user