From 201082daa0164fd21de9748e655fcbc7dfb3eeac Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 21 Oct 2026 13:03:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BAPDF=E8=BD=AC=E6=8D=A2=E4=B8=8E=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- internal/base/font/type1.go | 58 +++++++++++ internal/base/font/util.go | 102 +++++++++++++++++++ ofdgo_pdf_action.go | 2 +- ofdgo_pdf_annot.go | 47 +++++++++ ofdgo_pdf_group.go | 14 ++- ofdgo_pdf_pattern.go | 191 ++++++++++++++++++++++++++++++++++-- ofdgo_pdf_text.go | 30 +++++- 9 files changed, 434 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 3d9251e..01a8165 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72 github.com/xiaoqidun/j2kgo v1.0.2 github.com/xiaoqidun/jbig2 v1.1.8 - github.com/xiaoqidun/pdfgo v0.0.0-20261021023132-6452ac6abaef + github.com/xiaoqidun/pdfgo v0.0.0-20261021045903-4d52abc4d1c4 golang.org/x/crypto v0.58.0 golang.org/x/image v0.47.0 golang.org/x/text v0.43.0 diff --git a/go.sum b/go.sum index dbcdddc..115f950 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo= github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= -github.com/xiaoqidun/pdfgo v0.0.0-20261021023132-6452ac6abaef h1:q0QmZrpkiTtTtNghOY7t+G30YrYu7V3/FgOi14NbdGc= -github.com/xiaoqidun/pdfgo v0.0.0-20261021023132-6452ac6abaef/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4= +github.com/xiaoqidun/pdfgo v0.0.0-20261021045903-4d52abc4d1c4 h1:RXZ4mH75yoSxdNuXdgvRsoz6DHJGYz71+r1A14qLoj8= +github.com/xiaoqidun/pdfgo v0.0.0-20261021045903-4d52abc4d1c4/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4= 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= diff --git a/internal/base/font/type1.go b/internal/base/font/type1.go index 363fcd1..f4bdb62 100644 --- a/internal/base/font/type1.go +++ b/internal/base/font/type1.go @@ -198,6 +198,9 @@ func ParseType1Program(data []byte) (Type1Program, error) { return Type1Program{}, fmt.Errorf("Type1 font has no glyphs") } for index := range program.subrs { + if program.subrs[index] == nil { + continue + } program.subrs[index], err = type1CharString(program.subrs[index], program.lenIV) if err != nil { return Type1Program{}, err @@ -212,6 +215,61 @@ func ParseType1Program(data []byte) (Type1Program, error) { return program, nil } +// NormalizePDFType1 修复完整加密分段之后的PFB明文尾部,不补齐加密或字形数据 +// 入参: data 原始PFB程序 +// 返回: []byte 原程序或独立修复容器, bool 是否修复, error 容器或程序错误 +func NormalizePDFType1(data []byte) ([]byte, bool, error) { + if len(data) < 6 || data[0] != 0x80 { + return data, false, nil + } + binarySeen := false + for offset := 0; offset+2 <= len(data) && data[offset] == 0x80; { + kind := data[offset+1] + if kind == 3 { + return data, false, nil + } + if offset+6 > len(data) || kind != 1 && kind != 2 { + return data, false, fmt.Errorf("incomplete PFB segment header") + } + length := uint64(binary.LittleEndian.Uint32(data[offset+2:])) + remaining := len(data) - offset - 6 + if binarySeen && kind == 1 && remaining == 0 { + result := append(bytes.Clone(data[:offset]), 0x80, 3) + if _, err := ParseType1Program(result); err != nil { + return nil, false, err + } + return result, true, nil + } + if length > uint64(remaining) { + return nil, false, fmt.Errorf("incomplete PFB encrypted or text segment") + } + if binarySeen && kind == 1 && length == 0 { + tail := data[offset+6:] + if len(tail) < 2 || !bytes.Equal(tail[len(tail)-2:], []byte{0x80, 3}) { + return data, false, fmt.Errorf("unbounded PFB text trailer") + } + text := bytes.TrimSpace(tail[:len(tail)-2]) + if !bytes.HasSuffix(text, []byte("cleartomark")) { + return data, false, fmt.Errorf("PFB trailer lacks cleartomark") + } + for _, b := range bytes.TrimSuffix(text, []byte("cleartomark")) { + if b != '0' && !type1Space(b) { + return data, false, fmt.Errorf("unexpected PFB text trailer") + } + } + result := bytes.Clone(data) + binary.LittleEndian.PutUint32(result[offset+2:], uint32(len(tail)-2)) + if _, err := ParseType1Program(result); err != nil { + return nil, false, err + } + return result, true, nil + } + binarySeen = binarySeen || kind == 2 + offset += 6 + int(length) + } + return data, false, nil +} + // readType1Header 读取明文字体矩阵及加密数据起点 // 入参: data Type1程序 // 返回: []float64 字体矩阵, int 加密起点, error 头部错误 diff --git a/internal/base/font/util.go b/internal/base/font/util.go index a5bb820..7337794 100644 --- a/internal/base/font/util.go +++ b/internal/base/font/util.go @@ -87,6 +87,108 @@ func FileTables(data []byte, index int) (map[string][]byte, error) { return tables, nil } +// NormalizePDFProgram 修复可确定的PDF内嵌SFNT封装,不补造TrueType轮廓或字形编号 +// 入参: data 原始字体程序 +// 返回: []byte 原程序或独立修复程序, bool 是否修复, error 无法确认的目录或度量错误 +func NormalizePDFProgram(data []byte) ([]byte, bool, error) { + if len(data) < 12 || string(data[:4]) != "OTTO" && string(data[:4]) != "\x00\x01\x00\x00" && string(data[:4]) != "true" { + return data, false, nil + } + count := int(binary.BigEndian.Uint16(data[4:6])) + if count == 0 || count > (len(data)-12)/16 { + return nil, false, fmt.Errorf("unrecoverable PDF font directory size") + } + first := len(data) + for n := 0; n < count; n++ { + record := data[12+n*16:] + start, size := uint64(binary.BigEndian.Uint32(record[8:])), uint64(binary.BigEndian.Uint32(record[12:])) + if size != 0 && start >= 12 && start+size <= uint64(len(data)) { + first = min(first, int(start)) + } + } + changed := false + if first < 12+count*16 { + if (first-12)%16 != 0 { + return nil, false, fmt.Errorf("ambiguous PDF font directory overlap") + } + actual := (first - 12) / 16 + for n := 0; n < count; n++ { + record := data[12+n*16:] + printable := true + for _, b := range record[:4] { + printable = printable && b >= 32 && b <= 126 + } + if printable != (n < actual) { + return nil, false, fmt.Errorf("ambiguous PDF font table count") + } + if n < actual { + start, size := uint64(binary.BigEndian.Uint32(record[8:])), uint64(binary.BigEndian.Uint32(record[12:])) + if size != 0 && (start < uint64(first) || start+size > uint64(len(data))) { + return nil, false, fmt.Errorf("incomplete PDF font table") + } + } + } + data = bytes.Clone(data) + binary.BigEndian.PutUint16(data[4:6], uint16(actual)) + changed = true + } + tables, err := FileTables(data, 0) + if err != nil { + return nil, false, err + } + if head := tables["head"]; len(head) > 54 && len(head) <= 57 { + tables["head"] = head[:54] + changed = true + } + if hhea := tables["hhea"]; len(hhea) > 36 && len(hhea) <= 39 && binary.BigEndian.Uint32(hhea) == 0x00010000 { + tables["hhea"] = hhea[:36] + changed = true + } + if maxp := tables["maxp"]; tables["glyf"] != nil && len(maxp) > 32 && len(maxp) <= 35 && binary.BigEndian.Uint32(maxp) == 0x00010000 { + tables["maxp"] = maxp[:32] + changed = true + } + if post := tables["post"]; len(post) > 32 && binary.BigEndian.Uint32(post) == 0x00030000 { + tables["post"] = post[:32] + changed = true + } + if cvt := tables["cvt "]; len(cvt)%2 != 0 && cvt[len(cvt)-1] == 0 { + tables["cvt "] = cvt[:len(cvt)-1] + changed = true + } + if len(tables["CFF "]) != 0 && len(tables["head"]) == 0 && len(tables["maxp"]) == 0 && len(tables["hhea"]) == 0 && len(tables["hmtx"]) == 0 { + program, _, err := WrapCFFToOTF(tables["CFF "]) + return program, true, err + } + if maxp := tables["maxp"]; len(tables["CFF "]) != 0 && tables["glyf"] == nil && len(maxp) >= 6 && (binary.BigEndian.Uint32(maxp) == 0x00010000 || binary.BigEndian.Uint32(maxp) == 0x00005000 && len(maxp) != 6) { + count, err := parseCFFAndCountGlyphs(tables["CFF "]) + if err != nil { + return nil, false, err + } + if count != int(binary.BigEndian.Uint16(maxp[4:6])) { + program, _, err := WrapCFFToOTF(tables["CFF "]) + if err != nil { + return nil, false, err + } + rebuilt, err := FileTables(program, 0) + if err != nil { + return nil, false, err + } + if len(tables["head"]) < 20 || !bytes.Equal(tables["head"][18:20], rebuilt["head"][18:20]) { + return nil, false, fmt.Errorf("CFF and SFNT design units differ") + } + return program, true, nil + } + tables["maxp"] = buildCFFMaxpTable(uint16(count)) + changed = true + } + if !changed { + return data, false, nil + } + program, err := SerializeOTF(tables) + return program, true, err +} + // calcTableChecksum 计算字体表校验和 // 入参: data 表数据 // 返回: uint32 校验和 diff --git a/ofdgo_pdf_action.go b/ofdgo_pdf_action.go index 0ba3e00..5b072a9 100644 --- a/ofdgo_pdf_action.go +++ b/ofdgo_pdf_action.go @@ -315,7 +315,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) { if err := local.commitObjects(); err != nil { return Box{}, err } - reader, err := editor.resourceReader() + reader, err := local.temporaryReader() if err != nil { return Box{}, err } diff --git a/ofdgo_pdf_annot.go b/ofdgo_pdf_annot.go index 9d27596..1469054 100644 --- a/ofdgo_pdf_annot.go +++ b/ofdgo_pdf_annot.go @@ -19,6 +19,7 @@ import ( "context" "encoding/json" "encoding/xml" + "errors" "fmt" "maps" "math" @@ -120,6 +121,52 @@ func (p *pdfImporter) annotationOpen(ctx context.Context, dict pdfgo.Dictionary) // 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作 // 返回: error 外观或转换错误 func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error { + if annotation.Rect.XMin == annotation.Rect.XMax || annotation.Rect.YMin == annotation.Rect.YMax { + return p.paintedAnnotation(ctx, page, annotation, nil, actions...) + } + if !annotation.IsStandard() { + flags, err := p.reader.ReadAnnotationFlags(annotation) + if err != nil { + return err + } + if flags&1 != 0 { + return p.paintedAnnotation(ctx, page, annotation, nil, actions...) + } + } + if p.warning != nil && annotation.Dictionary["AP"] == nil && annotation.Dictionary["BS"] == nil && annotation.Dictionary["Border"] != nil { + border, err := p.reader.Resolve(annotation.Dictionary["Border"]) + if err != nil { + return err + } + if array, ok := border.(pdfgo.Array); ok && len(array) == 0 { + if err := p.preservePDFObject(ctx, "Annotation.Border", annotation.Dictionary["Border"]); err != nil { + return err + } + } + } + if annotation.Dictionary["AP"] != nil { + if _, err := p.reader.ReadAnnotationAppearance(annotation); err != nil { + if !errors.Is(err, pdfgo.ErrInvalidAnnotationAppearance) { + return err + } + if p.warning == nil { + return err + } + if err := p.preservePDFObject(ctx, "Annotation.Appearance", annotation.Dictionary); err != nil { + return err + } + } + } + switch annotation.Subtype { + case "PrinterMark", "TrapNet", "3D", "RichMedia", "Watermark": + appearance, err := p.reader.Resolve(annotation.Dictionary["AP"]) + if err != nil { + return err + } + if appearance == nil { + return p.recoverPDFObject(ctx, "Annotation.Appearance", annotation.Dictionary, &pdfgo.UnsupportedError{Feature: "annotation without static appearance"}) + } + } return p.paintedAnnotation(ctx, page, annotation, nil, actions...) } diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 6913118..42812d9 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -414,7 +414,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, if err := local.commitObjects(); err != nil { return nil, box, err } - reader, err := editor.resourceReader() + reader, err := local.temporaryReader() if err != nil { return nil, box, err } @@ -435,6 +435,18 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, return p.groupSceneBounds(scene) } +// temporaryReader 为局部编译准备资源,带有保留信息时使用完整快照 +// 返回: *Reader 独立阅读器, error 准备或取消错误 +func (p *pdfImporter) temporaryReader() (*Reader, error) { + if err := p.ctx.Err(); err != nil { + return nil, err + } + if p.editor.source != nil || len(p.editor.origins) != 0 { + return p.editor.readerContext(p.ctx, nil) + } + return p.editor.resourceReader() +} + // groupSceneBounds 计算已编译场景在页面内的像素对齐区域 // 入参: scene 中立绘制场景 // 返回: *RasterPage 非空场景, Box 页面区域, error 度量错误 diff --git a/ofdgo_pdf_pattern.go b/ofdgo_pdf_pattern.go index 3f8219b..3efa31d 100644 --- a/ofdgo_pdf_pattern.go +++ b/ofdgo_pdf_pattern.go @@ -27,12 +27,13 @@ const pdfPatternCellLimit = 8 << 20 // pdfPatternCellKey 隔离图案来源、基色、坐标、精度、混合空间与目标文档资源 type pdfPatternCellKey struct { - source *pdfgo.TilingPattern - base pdfgo.Paint - matrix pdfgo.Matrix - dpi float64 - editor *Editor - space *pdfgo.ColorSpace + source *pdfgo.TilingPattern + base pdfgo.Paint + matrix pdfgo.Matrix + dpi float64 + editor *Editor + space *pdfgo.ColorSpace + pageWidth, pageHeight float64 } // pdfPatternCell 保存图案快照及单元内部的来源区段 @@ -71,7 +72,7 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain if p.patternCells == nil { p.patternCells = resourceCache[pdfPatternCellKey, pdfPatternCell](p.resources, pdfPatternCellLimit) } - key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace} + key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace, pageWidth: p.pageWidth, pageHeight: p.pageHeight} if source.PaintType == 2 { base.Tiling, base.Axial, base.Radial, base.Mesh, base.Function, base.Shading = nil, nil, nil, nil, nil, nil base.Alpha = 1 @@ -140,6 +141,146 @@ func pdfPatternCellCost(value reflect.Value, budget, depth int) int { return cost } +// pdfPatternRectangleBounds 识别无文字裁剪的单个轴对齐矩形,不把边界框当作路径覆盖范围 +// 入参: path 矩形路径 +// 返回: pdfgo.Rectangle 精确覆盖范围, bool 是否单矩形 +func pdfPatternRectangleBounds(path pdfgo.Path) (pdfgo.Rectangle, bool) { + if len(path.Text) != 0 || len(path.Segments) != 5 || path.Segments[4].Operator != "C" || len(path.Segments[4].Points) != 0 { + return pdfgo.Rectangle{}, false + } + var points [4]pdfgo.Point + box := pdfgo.Rectangle{XMin: math.Inf(1), YMin: math.Inf(1), XMax: math.Inf(-1), YMax: math.Inf(-1)} + for i, segment := range path.Segments[:4] { + if len(segment.Points) != 1 || i == 0 && segment.Operator != "M" || i != 0 && segment.Operator != "L" { + return pdfgo.Rectangle{}, false + } + point := segment.Points[0] + if !finite(point.X) || !finite(point.Y) { + return pdfgo.Rectangle{}, false + } + points[i] = point + box.XMin, box.YMin = math.Min(box.XMin, point.X), math.Min(box.YMin, point.Y) + box.XMax, box.YMax = math.Max(box.XMax, point.X), math.Max(box.YMax, point.Y) + } + if box.XMin == box.XMax || box.YMin == box.YMax { + return pdfgo.Rectangle{}, false + } + corners := 0 + for i, point := range points { + next := points[(i+1)%len(points)] + if (point.X == next.X) == (point.Y == next.Y) || point.X != box.XMin && point.X != box.XMax || point.Y != box.YMin && point.Y != box.YMax { + return pdfgo.Rectangle{}, false + } + corner := 0 + if point.X == box.XMax { + corner++ + } + if point.Y == box.YMax { + corner += 2 + } + corners |= 1 << corner + } + return box, corners == 15 +} + +// pdfPatternRectanglePath 构造精确闭合矩形,用于连续覆盖的绘制及裁剪 +// 入参: box 矩形范围 +// 返回: pdfgo.Path 闭合矩形 +func pdfPatternRectanglePath(box pdfgo.Rectangle) pdfgo.Path { + return pdfgo.Path{Segments: []pdfgo.Segment{{Operator: "M", Points: []pdfgo.Point{{X: box.XMin, Y: box.YMin}}}, {Operator: "L", Points: []pdfgo.Point{{X: box.XMax, Y: box.YMin}}}, {Operator: "L", Points: []pdfgo.Point{{X: box.XMax, Y: box.YMax}}}, {Operator: "L", Points: []pdfgo.Point{{X: box.XMin, Y: box.YMax}}}, {Operator: "C"}}} +} + +// densePatternRectangle 对单一不透明矩形的连续重叠轴求精确并集,不修改源步距或近似采样 +// 入参: source 原图案, base 基色, width 单元宽, height 单元高, xstep 横向步距, ystep 纵向步距 +// 返回: *pdfgo.PathMark 并集矩形, bool 横轴已连续覆盖, bool 纵轴已连续覆盖, error 解析或取消错误 +func (p *pdfImporter) densePatternRectangle(source *pdfgo.TilingPattern, base pdfgo.Paint, width, height, xstep, ystep float64) (*pdfgo.PathMark, bool, bool, error) { + limit := float64(int(^uint(0) >> 1)) + continuousX, continuousY := width/xstep >= limit, height/ystep >= limit + if !continuousX && !continuousY { + return nil, false, false, nil + } + var mark pdfgo.PathMark + paths, other := 0, false + visitor := pdfgo.Visitor{ + Warning: p.warning, + Path: func(value pdfgo.PathMark) error { + paths++ + if paths == 1 { + mark = value + } + return nil + }, + Text: func(pdfgo.TextMark) error { other = true; return nil }, Image: func(pdfgo.ImageMark) error { other = true; return nil }, + Form: func(pdfgo.FormMark, func(pdfgo.Visitor) error) error { other = true; return nil }, + Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { other = true; return nil }, + PostScript: func(pdfgo.PostScriptMark) error { other = true; return nil }, + } + if err := source.Walk(p.ctx, base, visitor); err != nil { + return nil, false, false, err + } + if err := p.ctx.Err(); err != nil { + return nil, false, false, err + } + style, paint := mark.Style, mark.Style.Fill + if other || paths != 1 || !mark.Fill || mark.Stroke || paint.None || paint.Alpha != 1 || paint.Colorant != nil || paint.Process != nil || paint.Tiling != nil || paint.Shading != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Function != nil || style.SoftMask != nil || style.Transfer != nil || style.Halftone != nil || style.FillOverprint || style.AlphaIsShape || style.BlendMode != "" && style.BlendMode != "Normal" || len(style.Clips) != 1 { + return nil, false, false, nil + } + clip, ok := pdfPatternRectangleBounds(style.Clips[0]) + if !ok || clip != source.BBox { + return nil, false, false, nil + } + box, ok := pdfPatternRectangleBounds(mark.Path) + if !ok { + return nil, false, false, nil + } + box.XMin, box.YMin = math.Max(box.XMin, clip.XMin), math.Max(box.YMin, clip.YMin) + box.XMax, box.YMax = math.Min(box.XMax, clip.XMax), math.Min(box.YMax, clip.YMax) + if box.XMax <= box.XMin || box.YMax <= box.YMin || continuousX && box.XMax-box.XMin < xstep || continuousY && box.YMax-box.YMin < ystep { + return nil, false, false, nil + } + if continuousX { + box.XMin, box.XMax = clip.XMin, clip.XMax + } + if continuousY { + box.YMin, box.YMax = clip.YMin, clip.YMax + } + mark.Path = pdfPatternRectanglePath(box) + return &mark, continuousX, continuousY, nil +} + +// densePatternPageBounds 将连续覆盖轴的等价单元边界置于页面外,避免子像素单元接缝 +// 入参: source 原图案, box 原单元, x 横轴连续覆盖, y 纵轴连续覆盖 +// 返回: pdfgo.Rectangle 等价单元边界, error 非有限范围或退化变换错误 +func (p *pdfImporter) densePatternPageBounds(source *pdfgo.TilingPattern, box pdfgo.Rectangle, x, y bool) (pdfgo.Rectangle, error) { + inverse, ok := p.matrix.Mul(source.Matrix).Inverse() + if !ok { + return box, fmt.Errorf("invalid PDF dense pattern matrix") + } + if !finite(p.pageWidth) || !finite(p.pageHeight) || p.pageWidth <= 0 || p.pageHeight <= 0 { + return box, fmt.Errorf("invalid PDF dense pattern page bounds") + } + area := pdfgo.Rectangle{XMin: math.Inf(1), YMin: math.Inf(1), XMax: math.Inf(-1), YMax: math.Inf(-1)} + for _, point := range []pdfgo.Point{{}, {X: p.pageWidth}, {Y: p.pageHeight}, {X: p.pageWidth, Y: p.pageHeight}} { + point = inverse.Apply(point) + area.XMin, area.YMin = math.Min(area.XMin, point.X), math.Min(area.YMin, point.Y) + area.XMax, area.YMax = math.Max(area.XMax, point.X), math.Max(area.YMax, point.Y) + } + if x { + span := box.XMax - box.XMin + box.XMin, box.XMax = math.Min(box.XMin, area.XMin-span), math.Max(box.XMax, area.XMax+span) + } + if y { + span := box.YMax - box.YMin + box.YMin, box.YMax = math.Min(box.YMin, area.YMin-span), math.Max(box.YMax, area.YMax+span) + } + for _, value := range []float64{box.XMin, box.YMin, box.XMax, box.YMax, box.XMax - box.XMin, box.YMax - box.YMin} { + if !finite(value) { + return box, fmt.Errorf("nonfinite PDF dense pattern page bounds") + } + } + return box, nil +} + // buildTilingPattern 保留PDF图案单元与页面坐标之间的仿射关系 // 入参: source 平铺图案, base 无色图案基色 // 返回: *Pattern OFD图案, error 转换错误 @@ -148,12 +289,41 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo box := source.BBox width, height := box.XMax-box.XMin, box.YMax-box.YMin xstep, ystep := math.Abs(source.XStep), math.Abs(source.YStep) + if (width == 0 || height == 0) && finite(width) && finite(height) && width >= 0 && height >= 0 && finite(xstep) && finite(ystep) && xstep > 0 && ystep > 0 { + return &Pattern{Width: unit, Height: unit, XStep: unit, YStep: unit, RelativeTo: "Page"}, nil + } if width <= 0 || height <= 0 || !finite(xstep) || !finite(ystep) || xstep == 0 || ystep == 0 { return nil, fmt.Errorf("invalid PDF tiling pattern dimensions") } + dense, continuousX, continuousY, err := p.densePatternRectangle(source, base, width, height, xstep, ystep) + if err != nil { + return nil, err + } + if dense != nil { + box, err = p.densePatternPageBounds(source, box, continuousX, continuousY) + if err != nil { + return nil, err + } + bounds, _ := pdfPatternRectangleBounds(dense.Path) + if continuousX { + bounds.XMin, bounds.XMax = box.XMin, box.XMax + } + if continuousY { + bounds.YMin, bounds.YMax = box.YMin, box.YMax + } + dense.Path, dense.Style.Clips = pdfPatternRectanglePath(bounds), []pdfgo.Path{pdfPatternRectanglePath(box)} + width, height = box.XMax-box.XMin, box.YMax-box.YMin + } localToPDF := pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, box.XMin, box.YMax} m := p.matrix.Mul(source.Matrix).Mul(localToPDF) pattern := &Pattern{Width: math.Min(width, xstep) * unit, Height: math.Min(height, ystep) * unit, XStep: xstep * unit, YStep: ystep * unit, RelativeTo: "Page", CTM: pdfNumbers(m[:]...)} + repeatWidth, repeatHeight := width, height + if continuousX { + pattern.Width, repeatWidth = width*unit, xstep + } + if continuousY { + pattern.Height, repeatHeight = height*unit, ystep + } if p.patternOrigins == nil { p.patternOrigins = make(map[*Pattern]pdfObjectOrigin) } @@ -166,6 +336,9 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo } cell.objects = nil nodes, err := cell.collectCompositeNodes(func(visitor pdfgo.Visitor) error { + if dense != nil { + return visitor.Path(*dense) + } return source.Walk(p.ctx, base, visitor) }, false) if err != nil { @@ -185,8 +358,8 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo return nil, err } pattern.CellContent.Objects = objects - if len(objects) != 0 && (width > xstep || height > ystep) { - columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep) + if len(objects) != 0 && (repeatWidth > xstep || repeatHeight > ystep) { + columns, rows := math.Ceil(repeatWidth/xstep), math.Ceil(repeatHeight/ystep) if !finite(columns*rows) || columns*rows >= float64(uint(^uint(0)>>1)+1) { return nil, fmt.Errorf("PDF tiling pattern instance count overflow") } diff --git a/ofdgo_pdf_text.go b/ofdgo_pdf_text.go index b14431d..f3f51a7 100644 --- a/ofdgo_pdf_text.go +++ b/ofdgo_pdf_text.go @@ -166,6 +166,11 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { p.editor.fontMetrics[id] = imported.metrics } } else { + if font.Recovered { + if err := p.preservePDFObject(p.ctx, "Font", font.Dictionary); err != nil { + return err + } + } id, err = p.editor.AddExternalFont(font.Name) } if err != nil { @@ -625,15 +630,28 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) } var type1 *basefont.Type1Program result := &pdfImportedFont{} + programSource := *source + programRecovered := false + if p.warning != nil { + var err error + if source.ProgramType == "FontFile" { + programSource.Program, programRecovered, err = basefont.NormalizePDFType1(source.Program) + } else { + programSource.Program, programRecovered, err = basefont.NormalizePDFProgram(source.Program) + } + if err != nil { + return nil, fmt.Errorf("PDF font %s container: %w", source.Name, err) + } + } if source.ProgramType == "FontFile" { - parsed, err := basefont.ParseType1Program(source.Program) + parsed, err := basefont.ParseType1Program(programSource.Program) if err != nil { return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) } type1 = &parsed result.type1Glyphs = parsed.GlyphIDs() } - program, repair, err := codecpdf.FontProgram(source, type1) + program, repair, err := codecpdf.FontProgram(&programSource, type1) if err != nil { return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) } @@ -658,6 +676,14 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) return nil, fmt.Errorf("PDF font backend does not provide glyph outlines") } result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.MetricsLimit + if source.Recovered || programRecovered { + if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil { + return nil, err + } + } + if programRecovered { + p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF font %s container and metrics normalized from embedded program; original glyph IDs retained", source.Name)}) + } if repair.Locations { p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF font %s has invalid glyph locations; index rebuilt with original glyph IDs", source.Name)}) }