feat(转换能力): 修复水印并优化字体与导入
build.yaml / build (push) Successful in 52s

This commit is contained in:
xiaoqidun committed 2026-10-04 16:29:30 +08:00
1 parent 290fc57c34
commit 60eb5ac6a1
12 files changed
+504 -87

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261004052854-254c40ac6764
github.com/xiaoqidun/pdfgo v0.0.0-20261004082616-4fa2e8738fca
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0
golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004052854-254c40ac6764 h1:YKsnM25IZ2xqdcm6Gl30boDID7ctPxNa/QPxUoCjmKQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004052854-254c40ac6764/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004082616-4fa2e8738fca h1:9Q47dgBr5UXsOcXZ9X5xYO4xzxaJYldslT07WRiFCak=
github.com/xiaoqidun/pdfgo v0.0.0-20261004082616-4fa2e8738fca/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+20
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"cmp"
"context"
"fmt"
"reflect"
"slices"
@@ -73,6 +74,16 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
// 入参: page 目标页面索引, objects 对象快照, dx、dy 毫米位移
// 返回: []string 按绘制顺序排列的新对象标识, error 错误信息
func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64) ([]string, error) {
return e.CopyObjectsContext(context.Background(), page, objects, dx, dy)
}
// CopyObjectsContext 复制对象快照,准备期间取消不提交对象或撤销记录
// 入参: ctx 取消上下文, page 目标页, objects 对象快照, dx、dy 毫米位移
// 返回: []string 新对象标识, error 校验或取消错误
func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []GraphicObject, dx, dy float64) ([]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if !finite(dx) || !finite(dy) {
return nil, fmt.Errorf("copy requires finite offsets")
}
@@ -86,12 +97,18 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
if err := e.prepareSourceIDs(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
prepared := make([]GraphicObject, len(objects))
origins := make([]*editorObjectOrigin, len(objects))
result := make([]string, len(objects))
maximum := e.maxID + len(objects)
validation := &editorValidation{Editor: e}
for i, object := range objects {
if err := ctx.Err(); err != nil {
return nil, err
}
result[i] = strconv.Itoa(e.maxID + i + 1)
object, err := validation.prepareCopiedObject(result[i], object)
if err != nil {
@@ -111,6 +128,9 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
return nil, err
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
e.maxID = maximum
for i, origin := range origins {
if origin != nil {
+85 -29
View File
@@ -99,7 +99,10 @@ func normalizeCFFCharstrings(data []byte) ([]byte, error) {
if topData == nil || topStart+topSize > len(data) {
return nil, fmt.Errorf("invalid CFF top dictionary")
}
dict := parseCFFDict(topData)
dict, err := readCFFDict(topData)
if err != nil {
return nil, err
}
charOffset := dict[17]
if len(charOffset) != 1 {
return nil, fmt.Errorf("missing CFF charstrings")
@@ -216,7 +219,10 @@ func sanitizeCFF(data []byte) ([]byte, error) {
}
_, glbSz := getCFFIndexCount(data, offset)
globalSubrIndexData := data[offset : offset+glbSz]
topDict := parseCFFDict(topDictData)
topDict, err := readCFFDict(topDictData)
if err != nil {
return nil, err
}
if _, isCID := topDict[1230]; !isCID {
return data, nil
}
@@ -233,7 +239,10 @@ func sanitizeCFF(data []byte) ([]byte, error) {
return sanitizeMultiFDCFF(data, hdrSize, nameIndexData, topDict, stringIndexData, globalSubrIndexData, fdArrOff, fdCount)
}
fontDictData, _ := getCFFIndexData(data, fdArrOff)
fontDict := parseCFFDict(fontDictData)
fontDict, err := readCFFDict(fontDictData)
if err != nil {
return nil, err
}
if fdMat, ok := fontDict[1207]; ok && len(fdMat) == 6 {
topMat, hasTop := topDict[1207]
if !hasTop || len(topMat) != 6 {
@@ -255,7 +264,10 @@ func sanitizeCFF(data []byte) ([]byte, error) {
}
var subrsOffRel int
if len(privDictData) > 0 {
pDict := parseCFFDict(privDictData)
pDict, err := readCFFDict(privDictData)
if err != nil {
return nil, err
}
if sVals, ok := pDict[19]; ok && len(sVals) > 0 {
subrsOffRel = int(sVals[0])
}
@@ -284,7 +296,10 @@ func sanitizeCFF(data []byte) ([]byte, error) {
delete(topDict, 16)
var finalPrivData []byte
if len(privDictData) > 0 {
pDict := parseCFFDict(privDictData)
pDict, err := readCFFDict(privDictData)
if err != nil {
return nil, err
}
if err := normalizeCFFPrivate(pDict); err != nil {
return nil, err
}
@@ -358,7 +373,11 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict
privateDicts := make([]cffDict, fdCount)
for fd, item := range fdItems {
fonts[fd] = type2Font{data: data, chars: charStrings, globals: globalSubrs, seed: 1}
privateDicts[fd], err = readType2Private(data, parseCFFDict(item)[18], &fonts[fd])
fontDict, dictErr := readCFFDict(item)
if dictErr != nil {
return nil, dictErr
}
privateDicts[fd], err = readType2Private(data, fontDict[18], &fonts[fd])
if err != nil {
return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err)
}
@@ -452,7 +471,10 @@ func parseCFFAndCountGlyphs(data []byte) (int, error) {
}
topDictData, _ := getCFFIndexData(data, offset)
if topDictData != nil {
dict := parseCFFDict(topDictData)
dict, err := readCFFDict(topDictData)
if err != nil {
return 0, err
}
if offsetVals, ok := dict[17]; ok && len(offsetVals) > 0 {
charStrOff := int(offsetVals[0])
if charStrOff > 0 && charStrOff < len(data) {
@@ -478,62 +500,87 @@ func multiplyAffine(a, b []float64) []float64 {
}
}
// parseCFFDict 解析CFF字典数据
// parseCFFDict 读取可选CFF字典,编码无效时返回nil
// 入参: data 字典数据
// 返回: cffDict 解析后的字典映射
func parseCFFDict(data []byte) cffDict {
dict, _ := readCFFDict(data)
return dict
}
// readCFFDict 校验CFF字典编码、数值和48项操作数上限
// 入参: data 字典数据
// 返回: cffDict 字典映射, error 编码或操作数错误
func readCFFDict(data []byte) (cffDict, error) {
dict := make(cffDict)
var operands []float64
i := 0
for i < len(data) {
b := data[i]
i++
if b <= 27 {
if b <= 21 {
op := int(b)
if b == 12 {
if i >= len(data) {
break
return nil, fmt.Errorf("truncated CFF operator")
}
op = 1200 + int(data[i])
i++
}
if len(operands) == 0 {
return nil, fmt.Errorf("missing CFF dictionary operands")
}
dict[op] = operands
operands = nil
} else if b == 28 {
if i+1 < len(data) {
val := int(int16(binary.BigEndian.Uint16(data[i:])))
operands = append(operands, float64(val))
i += 2
if len(data)-i < 2 {
return nil, fmt.Errorf("truncated CFF integer")
}
val := int(int16(binary.BigEndian.Uint16(data[i:])))
operands = append(operands, float64(val))
i += 2
} else if b == 29 {
if i+3 < len(data) {
val := int(int32(binary.BigEndian.Uint32(data[i:])))
operands = append(operands, float64(val))
i += 4
if len(data)-i < 4 {
return nil, fmt.Errorf("truncated CFF integer")
}
val := int(int32(binary.BigEndian.Uint32(data[i:])))
operands = append(operands, float64(val))
i += 4
} else if b == 30 {
s, n := parseCFFReal(data[i:])
if f, err := strconv.ParseFloat(s, 64); err == nil {
operands = append(operands, f)
f, err := strconv.ParseFloat(s, 64)
if n == 0 || err != nil || !finite(f) {
return nil, fmt.Errorf("invalid CFF real")
}
operands = append(operands, f)
i += n
} else if b >= 32 && b <= 246 {
operands = append(operands, float64(int(b)-139))
} else if b >= 247 && b <= 250 {
if i < len(data) {
b1 := int(data[i])
i++
operands = append(operands, float64((int(b)-247)*256+b1+108))
if i == len(data) {
return nil, fmt.Errorf("truncated CFF integer")
}
b1 := int(data[i])
i++
operands = append(operands, float64((int(b)-247)*256+b1+108))
} else if b >= 251 && b <= 254 {
if i < len(data) {
b1 := int(data[i])
i++
operands = append(operands, float64(-(int(b)-251)*256-b1-108))
if i == len(data) {
return nil, fmt.Errorf("truncated CFF integer")
}
b1 := int(data[i])
i++
operands = append(operands, float64(-(int(b)-251)*256-b1-108))
} else {
return nil, fmt.Errorf("invalid CFF dictionary byte")
}
if len(operands) > 48 {
return nil, fmt.Errorf("CFF dictionary operand limit exceeded")
}
}
return dict
if len(operands) != 0 {
return nil, fmt.Errorf("unused CFF dictionary operands")
}
return dict, nil
}
// parseCFFReal 解析CFF实数编码
@@ -549,6 +596,9 @@ func parseCFFReal(data []byte) (string, int) {
nibbles := []byte{b >> 4, b & 0x0F}
for _, n := range nibbles {
if n == 0xF {
if b>>4 == 15 && b&15 != 15 {
return "", 0
}
done = true
break
}
@@ -567,8 +617,14 @@ func parseCFFReal(data []byte) (string, int) {
if n == 0xE {
sb.WriteString("-")
}
if n == 0xD {
return "", 0
}
}
}
if !done {
return "", 0
}
return sb.String(), i
}
+8 -2
View File
@@ -122,7 +122,10 @@ func readType2Font(data []byte) (*type2Font, error) {
if err != nil {
return nil, err
}
dict := parseCFFDict(tops[0])
dict, err := readCFFDict(tops[0])
if err != nil {
return nil, err
}
if value, ok := dict[1206]; ok && (len(value) != 1 || value[0] != 2) {
return nil, fmt.Errorf("unsupported CFF charstring type")
}
@@ -158,7 +161,10 @@ func readType2Private(data []byte, values []float64, font *type2Font) (cffDict,
return nil, fmt.Errorf("invalid CFF private range")
}
start := int(values[1])
private = parseCFFDict(data[start : start+int(values[0])])
private, err = readCFFDict(data[start : start+int(values[0])])
if err != nil {
return nil, err
}
for op, target := range map[int]*float64{20: &font.def, 21: &font.nominal} {
if value, ok := private[op]; ok {
if len(value) != 1 || !finite(value[0]) {
+73 -18
View File
@@ -75,6 +75,7 @@ type pdfImporter struct {
fonts map[*pdfgo.Font]*pdfImportedFont
fontWarnings map[string]bool
clipTexts map[*pdfgo.TextClip][]TextObject
clipPaths *renderCache[[32]byte, pdfClipPath]
cmykSpace string
objects []GraphicObject
pages map[pdfgo.Reference]*pdfgo.Page
@@ -204,6 +205,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.compositeNodes = nil
importer.compositeSpace = nil
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
importer.clipPaths = nil
importer.annotationData = nil
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
@@ -279,7 +281,7 @@ func (p *pdfImporter) commitObjects() error {
if len(p.objects) == 0 {
return nil
}
if _, err := p.editor.CopyObjects(p.page, p.objects, 0, 0); err != nil {
if _, err := p.editor.CopyObjectsContext(p.ctx, p.page, p.objects, 0, 0); err != nil {
return err
}
p.objects = nil
@@ -340,6 +342,40 @@ func pdfBounds(points []pdfgo.Point) Box {
return Box{minX, minY, math.Max(maxX-minX, 1e-6), math.Max(maxY-minY, 1e-6)}
}
// pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合
// 入参: ctx 取消上下文, path PDF路径
// 返回: Box 控制点边界,空路径返回零值, error 取消错误
func (p *pdfImporter) pathBoundsContext(ctx context.Context, path pdfgo.Path) (Box, error) {
if err := ctx.Err(); err != nil {
return Box{}, err
}
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
found := false
for _, segment := range path.Segments {
if err := ctx.Err(); err != nil {
return Box{}, err
}
for index, point := range segment.Points {
if index&255 == 0 {
if err := ctx.Err(); err != nil {
return Box{}, err
}
}
point = p.matrix.Apply(point)
minX, minY = math.Min(minX, point.X), math.Min(minY, point.Y)
maxX, maxY = math.Max(maxX, point.X), math.Max(maxY, point.Y)
found = true
}
}
if err := ctx.Err(); err != nil {
return Box{}, err
}
if !found {
return Box{}, nil
}
return Box{minX, minY, math.Max(maxX-minX, 1e-6), math.Max(maxY-minY, 1e-6)}, nil
}
// color 保留设备色彩分量并复用文档颜色空间
// 入参: paint PDF颜色与不透明度
// 返回: *FillColor OFD颜色
@@ -360,8 +396,22 @@ func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor {
// 入参: path PDF路径, origin 对象边界
// 返回: string OFD路径数据
func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string {
data, _ := p.pathDataContext(context.Background(), path, origin)
return data
}
// pathDataContext 转换路径并在分段处理时检查取消
// 入参: ctx 取消上下文, path PDF路径, origin 对象边界
// 返回: string OFD路径数据, error 取消错误
func (p *pdfImporter) pathDataContext(ctx context.Context, path pdfgo.Path, origin Box) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
data := make([]byte, 0, len(path.Segments)*48)
for index, segment := range path.Segments {
if err := ctx.Err(); err != nil {
return "", err
}
if index != 0 {
data = append(data, ' ')
}
@@ -374,7 +424,10 @@ func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string {
data = strconv.AppendFloat(data, point.Y-origin.Y, 'g', -1, 64)
}
}
return string(data)
if err := ctx.Err(); err != nil {
return "", err
}
return string(data), nil
}
// clips 保持裁剪路径的交集,不随对象CTM重复变换
@@ -387,20 +440,21 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
flag := false
clips := &Clips{TransFlag: &flag}
for _, path := range paths {
area := ClipArea{}
var points []pdfgo.Point
for _, segment := range path.Segments {
for _, point := range segment.Points {
points = append(points, p.matrix.Apply(point))
}
if err := p.ctx.Err(); err != nil {
return nil, err
}
area := ClipArea{}
if len(path.Segments) != 0 {
box := pdfBounds(points)
encoded, err := p.clipPath(path)
if err != nil {
return nil, err
}
box := encoded.box
rule := "NonZero"
if path.EvenOdd {
rule = "Even-Odd"
}
area.Path = append(area.Path, PathObject{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: p.pathData(path, box), Rule: rule})
area.Path = append(area.Path, PathObject{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: encoded.data, Rule: rule})
}
for _, mark := range path.Text {
objects, ok := p.clipTexts[mark]
@@ -491,16 +545,13 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
if p.warning != nil && (mark.Fill && pdfGradientError(mark.Style.Fill) != nil || mark.Stroke && pdfGradientError(mark.Style.Stroke) != nil) {
return p.gradientPath(mark)
}
var points []pdfgo.Point
for _, segment := range mark.Path.Segments {
for _, point := range segment.Points {
points = append(points, p.matrix.Apply(point))
}
box, err := p.pathBoundsContext(p.ctx, mark.Path)
if err != nil {
return err
}
if len(points) == 0 {
if box == (Box{}) {
return fmt.Errorf("empty PDF painted path")
}
box := pdfBounds(points)
scale := math.Hypot(p.matrix[0], p.matrix[1])
drawing, origin, ctm := *p, box, ""
marginScale := scale
@@ -548,7 +599,11 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
if err != nil {
return err
}
object := PathObject{Boundary: pdfBoundary(box), CTM: ctm, AbbreviatedData: drawing.pathData(mark.Path, origin), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: clips}
data, err := drawing.pathDataContext(p.ctx, mark.Path, origin)
if err != nil {
return err
}
object := PathObject{Boundary: pdfBoundary(box), CTM: ctm, AbbreviatedData: data, Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: clips}
object.LineWidthSet = object.LineWidth == 0
if mark.Path.EvenOdd {
object.Rule = "Even-Odd"
+100
View File
@@ -0,0 +1,100 @@
// 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 ofdgo
import (
"context"
"crypto/sha256"
"encoding/binary"
"math"
"github.com/xiaoqidun/pdfgo"
)
// pdfClipPath 保存不可变的裁剪路径编码,不缓存对象位移或文字裁剪
type pdfClipPath struct {
box Box
data string
}
// pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址
// 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换
// 返回: [32]byte 内容摘要, error 取消错误
func pdfClipPathKey(ctx context.Context, path pdfgo.Path, matrix pdfgo.Matrix) ([32]byte, error) {
hash := sha256.New()
var value [16]byte
for _, number := range matrix {
binary.LittleEndian.PutUint64(value[:8], math.Float64bits(number))
hash.Write(value[:8])
}
binary.LittleEndian.PutUint64(value[:8], uint64(len(path.Segments)))
value[8] = 0
if path.EvenOdd {
value[8] = 1
}
hash.Write(value[:9])
for _, segment := range path.Segments {
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
binary.LittleEndian.PutUint64(value[:8], uint64(len(segment.Operator)))
binary.LittleEndian.PutUint64(value[8:], uint64(len(segment.Points)))
hash.Write(value[:])
hash.Write([]byte(segment.Operator))
for index, point := range segment.Points {
if index&255 == 0 {
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
}
binary.LittleEndian.PutUint64(value[:8], math.Float64bits(point.X))
binary.LittleEndian.PutUint64(value[8:], math.Float64bits(point.Y))
hash.Write(value[:])
}
}
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
var key [32]byte
hash.Sum(key[:0])
return key, nil
}
// clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误
// 入参: path 裁剪路径
// 返回: pdfClipPath 变换后的边界及编码, error 取消错误
func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
key, err := pdfClipPathKey(p.ctx, path, p.matrix)
if err != nil {
return pdfClipPath{}, err
}
if p.clipPaths == nil {
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20}
}
if cached, ok := p.clipPaths.get(key); ok {
return cached, nil
}
box, err := p.pathBoundsContext(p.ctx, path)
if err != nil {
return pdfClipPath{}, err
}
data, err := p.pathDataContext(p.ctx, path, box)
if err != nil {
return pdfClipPath{}, err
}
result := pdfClipPath{box: box, data: data}
p.clipPaths.put(key, result, len(data)+256)
return result, nil
}
+36 -1
View File
@@ -242,6 +242,8 @@ func (p *pdfImporter) compositeTextBounds(mark pdfgo.TextMark, stroke bool) Box
// 返回: []pdfCompositeNode 原始图元, error 解析错误
func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) {
active := make(map[*pdfgo.Font]map[string]bool)
var externalNames map[string]bool
var embeddedFonts map[*pdfgo.Font]bool
var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error)
collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) {
var nodes []pdfCompositeNode
@@ -257,6 +259,24 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
return nil
}
v.Text = func(mark pdfgo.TextMark) error {
if mark.Font.Subtype != "Type3" {
if len(mark.Font.Program) == 0 {
if externalNames == nil {
externalNames = make(map[string]bool)
}
name := pdfEmbeddedFontName(mark.Font.Name)
if !externalNames[fontNormalizeName(name)] {
for _, candidate := range fontExactCandidateNames(name) {
externalNames[fontNormalizeName(candidate)] = true
}
}
} else {
if embeddedFonts == nil {
embeddedFonts = make(map[*pdfgo.Font]bool)
}
embeddedFonts[mark.Font] = true
}
}
var text []pdfCompositeNode
if mark.Font.Subtype != "Type3" {
if err := p.halftone(mark.Style); err != nil {
@@ -327,7 +347,22 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
err := walk(v)
return nodes, err
}
return collect(walk)
nodes, err := collect(walk)
if err != nil {
return nil, err
}
for font := range embeddedFonts {
if !externalNames[fontNormalizeName(pdfEmbeddedFontName(font.Name))] {
continue
}
if err := p.ctx.Err(); err != nil {
return nil, err
}
if _, err := p.importedFont(font); err != nil {
return nil, err
}
}
return nodes, nil
}
// compositePage 保留独立不透明对象,仅将需要背景参与的效果在局部区域合成
+49
View File
@@ -36,6 +36,55 @@ type pdfImportedFont struct {
repairLimit uint16
}
// embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序
// 入参: source 外部字体, glyphs 当前文字字形
// 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空
func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph) *pdfImportedFont {
name := pdfEmbeddedFontName(source.Name)
if name == "" || len(glyphs) == 0 {
return nil
}
var matched *pdfImportedFont
for _, imported := range p.fonts {
if pdfEmbeddedFontName(imported.resource.Font.FontName) != name {
continue
}
if matched != nil && matched.checksum != imported.checksum {
return nil
}
matched = imported
}
if matched == nil {
return nil
}
for _, glyph := range glyphs {
if utf8.RuneCountInString(glyph.Text) != 1 {
return nil
}
char, _ := utf8.DecodeRuneInString(glyph.Text)
id := matched.metrics.GlyphIndex(char)
if id == 0 || id >= matched.metrics.NumGlyphs() || matched.repairLimit != 0 && id >= matched.repairLimit {
return nil
}
}
return matched
}
// pdfEmbeddedFontName 去除标准六位大写子集前缀,保留字体名称和样式后缀
// 入参: name PDF或内嵌字体名称
// 返回: string 原字体名称
func pdfEmbeddedFontName(name string) string {
if len(name) <= 7 || name[6] != '+' {
return name
}
for i := range 6 {
if name[i] < 'A' || name[i] > 'Z' {
return name
}
}
return name[7:]
}
// importedFont 按原PDF字体复用包装和解析结果,失败结果不进入缓存
// 入参: source 源字体
// 返回: *pdfImportedFont 只读导入字体, error 字体程序或后端错误
+56 -10
View File
@@ -146,10 +146,17 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { return errPDFGroupRaster },
}
collect.Path = func(path pdfgo.PathMark) error {
if err := p.ctx.Err(); err != nil {
return err
}
if path.Style.SoftMask != nil || path.Style.BlendMode != "" && path.Style.BlendMode != "Normal" && path.Style.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "masked or blended transparent group content"}
}
shape, err := ParseGeometryPath(p.pathData(path.Path, Box{}))
data, err := p.pathDataContext(p.ctx, path.Path, Box{})
if err != nil {
return err
}
shape, err := ParseGeometryPath(data)
if err != nil {
return err
}
@@ -191,7 +198,11 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
return fmt.Errorf("invalid PDF stroke matrix")
}
local := pdfImporter{matrix: inverse}
strokeShape, err = ParseGeometryPath(local.pathData(path.Path, Box{}))
data, err = local.pathDataContext(p.ctx, path.Path, Box{})
if err != nil {
return err
}
strokeShape, err = ParseGeometryPath(data)
if err != nil {
return err
}
@@ -219,6 +230,9 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
if err != nil {
return err
}
if err := p.ctx.Err(); err != nil {
return err
}
combined = append(combined, outline...)
}
return nil
@@ -226,6 +240,9 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
if err := walk(collect); err != nil {
return err
}
if err := p.ctx.Err(); err != nil {
return err
}
if len(combined) == 0 {
return nil
}
@@ -238,6 +255,9 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
}
var covered GeometryPath
for i := len(regions) - 1; i >= 0; i-- {
if err := p.ctx.Err(); err != nil {
return err
}
region := regions[i]
visible := region.path
if opaque && len(covered) > 0 {
@@ -344,10 +364,16 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if p.fonts == nil {
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
}
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts}
if p.clipPaths == nil {
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20}
}
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, clipPaths: p.clipPaths}
if err := build(&local); err != nil {
return nil, box, err
}
if err := p.ctx.Err(); err != nil {
return nil, box, err
}
if err := local.flushPath(); err != nil {
return nil, box, err
}
@@ -357,19 +383,19 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if err := local.commitObjects(); err != nil {
return nil, box, err
}
reader, err := editor.Reader()
reader, err := editor.resourceReader()
if err != nil {
return nil, box, err
}
defer reader.Close()
page, err := reader.PageContentByIndex(0)
if err != nil {
return nil, box, err
}
page := &editor.pages[0]
renderer := p.renderer.childRenderer(reader)
renderer.TransparentBackground = true
for _, layer := range page.Content.Layer {
for _, object := range layer.Objects {
if err := p.ctx.Err(); err != nil {
return nil, box, err
}
bounds, err := renderer.ObjectBounds(object, layer.DrawParam)
if err != nil {
return nil, box, err
@@ -388,6 +414,9 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
return nil, box, nil
}
scene, err := renderer.CompilePage(page)
if canceled := p.ctx.Err(); canceled != nil {
return nil, box, canceled
}
return scene, box, err
}
@@ -521,6 +550,9 @@ func pdfClippedVisitor(visitor pdfgo.Visitor, clip pdfgo.Path) pdfgo.Visitor {
// 入参: mask PDF蒙版
// 返回: pdfgo.Path 页面坐标裁剪路径, error 不可精确转换或几何错误
func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
if err := p.ctx.Err(); err != nil {
return pdfgo.Path{}, err
}
if mask.Subtype != "Luminosity" || mask.ColorSpace == nil || mask.ColorSpace.Model != "DeviceGray" || mask.ColorSpace.Calibrated() {
return pdfgo.Path{}, &pdfgo.UnsupportedError{Feature: "nonbinary luminosity mask"}
}
@@ -576,6 +608,9 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
return walk(visitor)
}
visitor.Path = func(mark pdfgo.PathMark) error {
if err := p.ctx.Err(); err != nil {
return err
}
paint := mark.Style.Fill
if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Function != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) {
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
@@ -584,7 +619,11 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
if err != nil {
return err
}
shape, err := ParseGeometryPath(identity.pathData(mark.Path, Box{}))
data, err := identity.pathDataContext(p.ctx, mark.Path, Box{})
if err != nil {
return err
}
shape, err := ParseGeometryPath(data)
if err != nil {
return err
}
@@ -596,7 +635,11 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
if len(clip.Text) != 0 {
return &pdfgo.UnsupportedError{Feature: "text-clipped binary mask"}
}
c, err := ParseGeometryPath(identity.pathData(clip, Box{}))
data, err := identity.pathDataContext(p.ctx, clip, Box{})
if err != nil {
return err
}
c, err := ParseGeometryPath(data)
if err != nil {
return err
}
@@ -621,6 +664,9 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
}
path := pdfgo.Path{}
for _, segment := range region {
if err := p.ctx.Err(); err != nil {
return pdfgo.Path{}, err
}
point := func(p Point) pdfgo.Point { return pdfgo.Point{X: p.X, Y: p.Y} }
switch segment.Verb {
case GeometryMove:
+21 -5
View File
@@ -21,6 +21,7 @@ import (
"image/color"
"math"
"reflect"
"slices"
"strconv"
"unicode/utf8"
@@ -120,11 +121,11 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
}
}
if id == "" {
jbig2Original, err := source.JBIG2File()
jbig2Original, err := source.JBIG2FileContext(p.ctx)
if err != nil {
return err
}
jpegOriginal, err := source.JPEGFile()
jpegOriginal, err := source.JPEGFileContext(p.ctx)
if err != nil {
return err
}
@@ -192,7 +193,7 @@ func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
return nil
}
// text 保留内嵌字形及逐字基线,外部字体保留名称引用
// text 保留字形及逐字基线,外部字体优先复用同名内嵌资源
// 入参: mark PDF文字绘制信息
// 返回: error 错误信息
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
@@ -256,14 +257,29 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
return &pdfgo.UnsupportedError{Feature: "font program " + string(font.ProgramType)}
}
var imported *pdfImportedFont
reused := false
if embedded {
imported, err = p.importedFont(font)
if err != nil {
return err
}
} else if imported = p.embeddedTextFont(font, mark.Glyphs); imported != nil {
reused, embedded = true, true
mark.Glyphs = slices.Clone(mark.Glyphs)
for index := range mark.Glyphs {
char, _ := utf8.DecodeRuneInString(mark.Glyphs[index].Text)
mark.Glyphs[index].ID = imported.metrics.GlyphIndex(char)
mark.Glyphs[index].HasID = true
}
}
id := p.fontIDs[font]
if id == "" {
if reused {
id, err = p.editor.addFontResource(imported.resource, imported.checksum)
if err != nil {
return fmt.Errorf("PDF font %s resource: %w", font.Name, err)
}
p.editor.fontMetrics[id] = imported.metrics
} else if id == "" {
var err error
if embedded {
if imported.repairLimit != 0 {
@@ -324,7 +340,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
return err
}
gid := glyph.ID
if embedded && font.ProgramType == "FontFile" {
if embedded && !reused && font.ProgramType == "FontFile" {
gid = imported.type1Glyphs[glyph.Name]
glyph.HasID = true
}
+53 -19
View File
@@ -241,6 +241,31 @@ func (e *Editor) reader(progress editorProgress) (*Reader, error) {
return r, nil
}
// resourceReader 为临时图元编译准备资源索引,不序列化页面或作为公共快照
// 返回: *Reader 资源阅读器, error 资源错误
func (e *Editor) resourceReader() (*Reader, error) {
if e.source != nil || len(e.origins) != 0 {
return nil, fmt.Errorf("resource reader requires created objects")
}
if err := e.validate(); err != nil {
return nil, err
}
r := &Reader{files: make(map[string][]byte)}
if err := e.writePartsWithPages(func(name string, data []byte, _ bool) error {
r.files[name] = data
return nil
}, nil, false); err != nil {
return nil, err
}
if err := r.initRoot(); err != nil {
return nil, err
}
if _, err := r.Doc(); err != nil {
return nil, err
}
return r, nil
}
// validate 校验文档保存条件
// 返回: error 错误信息
func (e *Editor) validate() error {
@@ -269,6 +294,13 @@ func (e *Editor) validate() error {
// 入参: write 条目写入方法, progress 保存进度回调,预览时为nil
// 返回: error 错误信息
func (e *Editor) writeParts(write func(string, []byte, bool) error, progress editorProgress) error {
return e.writePartsWithPages(write, progress, true)
}
// writePartsWithPages 统一生成包条目,临时编译可省去页面XML
// 入参: write 条目写入方法, progress 保存进度回调, includePages 是否写出页面
// 返回: error 写入错误
func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, progress editorProgress, includePages bool) error {
if len(e.removedPages) != 0 {
output := write
write = func(name string, data []byte, binary bool) error {
@@ -359,27 +391,29 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
return err
}
}
for i, page := range e.pages {
if err := progress.report("pages", i, len(e.pages)); err != nil {
if includePages {
for i, page := range e.pages {
if err := progress.report("pages", i, len(e.pages)); err != nil {
return err
}
name := packagePagePath(e.packageDirectory(), page.ID)
if len(e.origins) == 0 {
if err := writeXML(name, func(x *ofdXML) { x.page(page) }); err != nil {
return err
}
} else {
data, err := e.sourceNewPageXML(page)
if err != nil {
return err
}
if err := write(name, data, false); err != nil {
return err
}
}
}
if err := progress.report("pages", len(e.pages), len(e.pages)); err != nil {
return err
}
name := packagePagePath(e.packageDirectory(), page.ID)
if len(e.origins) == 0 {
if err := writeXML(name, func(x *ofdXML) { x.page(page) }); err != nil {
return err
}
} else {
data, err := e.sourceNewPageXML(page)
if err != nil {
return err
}
if err := write(name, data, false); err != nil {
return err
}
}
}
if err := progress.report("pages", len(e.pages), len(e.pages)); err != nil {
return err
}
for _, resources := range [][]editorResource{fonts, images} {
for _, resource := range resources {