From 052e34a4e6b4bcff2f773cde07028ce22c36ba53 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 27 Sep 2026 16:28:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E6=B8=B2=E6=9F=93):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=BE=E6=A1=88=E6=B3=A8=E9=87=8A=E4=B8=8E?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +-- ofdgo_pdf.go | 45 ++++++++++++++++++++++++++--- ofdgo_pdf_annot.go | 69 ++++++++++++++++++++++++++++++++++---------- ofdgo_pdf_link.go | 24 +++++++++++++-- ofdgo_render_walk.go | 2 +- 6 files changed, 119 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 38a2f36..b1c0e17 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,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-20260927070002-9eedb7bf8e13 + github.com/xiaoqidun/pdfgo v0.0.0-20260927082417-6c78229518ef golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index dccf3a0..f74f6bc 100644 --- a/go.sum +++ b/go.sum @@ -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-20260927070002-9eedb7bf8e13 h1:P3UEKffubpK6Cqh1ulQysufcw3c+zhEsUv4z+EhqCDA= -github.com/xiaoqidun/pdfgo v0.0.0-20260927070002-9eedb7bf8e13/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260927082417-6c78229518ef h1:GW25sDMcACPksCOx58dKcbV2qWhbhClj6m7nuu+rZbo= +github.com/xiaoqidun/pdfgo v0.0.0-20260927082417-6c78229518ef/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= diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index b1ea53c..de90795 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -608,15 +608,16 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain const unit = 25.4 / 72 box := source.BBox width, height := box.XMax-box.XMin, box.YMax-box.YMin - if width <= 0 || height <= 0 || source.XStep < width || source.YStep < height { - return nil, &pdfgo.UnsupportedError{Feature: "overlapping or empty tiling pattern cell"} + xstep, ystep := math.Abs(source.XStep), math.Abs(source.YStep) + if width <= 0 || height <= 0 || !finite(xstep) || !finite(ystep) || xstep == 0 || ystep == 0 { + return nil, fmt.Errorf("invalid PDF tiling pattern dimensions") } localToPDF := pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, box.XMin, box.YMax} m := p.matrix.Mul(source.Matrix).Mul(localToPDF) - pattern := &Pattern{Width: width * unit, Height: height * unit, XStep: source.XStep * unit, YStep: source.YStep * unit, RelativeTo: "Page", CTM: pdfNumbers(m[:]...)} + 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[:]...)} cell := *p cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit} - cell.pageWidth, cell.pageHeight = pattern.Width, pattern.Height + cell.pageWidth, cell.pageHeight = width*unit, height*unit cell.objects, cell.pendingPath = nil, nil visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image} visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { @@ -629,6 +630,42 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain return nil, err } pattern.CellContent.Objects = cell.objects + if width > xstep || height > ystep { + columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep) + if !finite(columns*rows) || columns*rows > float64(int(^uint(0)>>1)) { + return nil, fmt.Errorf("PDF tiling pattern instance count overflow") + } + bounds := make([]Box, len(cell.objects)) + for index, object := range cell.objects { + if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke { + var err error + bounds[index], err = ParseBox(object.PathObject.Boundary) + if err != nil { + return nil, err + } + } + } + pattern.CellContent.Objects = nil + for row := 1 - int(rows); row <= 0; row++ { + for column := 1 - int(columns); column <= 0; column++ { + if err := p.ctx.Err(); err != nil { + return nil, err + } + dx, dy := float64(column)*xstep*unit, float64(row)*ystep*unit + for index, object := range cell.objects { + b := bounds[index] + if b.W > 0 && b.H > 0 && (b.X+dx+b.W < 0 || b.Y+dy+b.H < 0 || b.X+dx > pattern.Width || b.Y+dy > pattern.Height) { + continue + } + translated, err := transformEditorObject(object, dx, dy, 1) + if err != nil { + return nil, err + } + pattern.CellContent.Objects = append(pattern.CellContent.Objects, translated) + } + } + } + } return pattern, nil } diff --git a/ofdgo_pdf_annot.go b/ofdgo_pdf_annot.go index c96a8bd..a219661 100644 --- a/ofdgo_pdf_annot.go +++ b/ofdgo_pdf_annot.go @@ -29,10 +29,15 @@ import ( "github.com/xiaoqidun/pdfgo" ) -// stampAnnotation 将PDF印章外观转换为OFD注解 -// 入参: ctx取消上下文, page PDF页面, annotation PDF印章 +// appearanceAnnotation 将PDF静态外观转换为对应类型的OFD注解 +// 入参: ctx取消上下文, page PDF页面, annotation PDF注解 // 返回: error 外观或转换错误 -func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error { +func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error { + for _, key := range []pdfgo.Name{"AA", "OC"} { + if annotation.Dictionary[key] != nil { + return &pdfgo.UnsupportedError{Feature: fmt.Sprintf("annotation field %q", key)} + } + } flags := int64(0) if value := annotation.Dictionary["F"]; value != nil { resolved, err := p.reader.Resolve(value) @@ -45,6 +50,9 @@ func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, ann } flags = int64(number) } + if flags&256 != 0 { + return &pdfgo.UnsupportedError{Feature: "annotation ToggleNoView flag"} + } box := pdfBounds([]pdfgo.Point{ p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMin, Y: annotation.Rect.YMin}), p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMax, Y: annotation.Rect.YMin}), @@ -69,7 +77,7 @@ func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, ann return &pdfgo.UnsupportedError{Feature: "darken stamp text"} } if err := stamp.text(mark); err != nil { - return fmt.Errorf("stamp text: %w", err) + return fmt.Errorf("annotation text: %w", err) } return nil }, @@ -99,8 +107,20 @@ func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, ann } p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF darken stamp appearance flattened over page content"}) } - converted := Annotation{Type: "Stamp", Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}} - if flags&(1|2|32) != 0 { + converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}} + if value, err := p.reader.Resolve(annotation.Dictionary["Contents"]); err != nil { + return err + } else if value != nil { + contents, ok := value.(pdfgo.String) + if !ok { + return fmt.Errorf("invalid PDF annotation contents") + } + converted.Remark, err = pdfgo.DecodeTextString(contents) + if err != nil { + return err + } + } + if flags&(2|32) != 0 { visible := false converted.Visible = &visible } @@ -113,7 +133,7 @@ func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, ann } creator, ok := resolved.(pdfgo.String) if !ok { - return fmt.Errorf("invalid PDF stamp creator") + return fmt.Errorf("invalid PDF annotation creator") } converted.Creator, err = pdfgo.DecodeTextString(creator) if err != nil { @@ -127,7 +147,7 @@ func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, ann } encoded, ok := resolved.(pdfgo.String) if !ok { - return fmt.Errorf("invalid PDF stamp modification date") + return fmt.Errorf("invalid PDF annotation modification date") } date, err := pdfgo.DecodeTextString(encoded) if err != nil { @@ -135,46 +155,63 @@ func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, ann } date = strings.TrimPrefix(date, "D:") if len(date) < 4 { - return fmt.Errorf("invalid PDF stamp modification date") + return fmt.Errorf("invalid PDF annotation modification date") } year, err := strconv.Atoi(date[:4]) if err != nil { - return fmt.Errorf("invalid PDF stamp modification date: %w", err) + return fmt.Errorf("invalid PDF annotation modification date: %w", err) } month, day := 1, 1 if len(date) >= 6 { month, err = strconv.Atoi(date[4:6]) if err != nil { - return fmt.Errorf("invalid PDF stamp modification month: %w", err) + return fmt.Errorf("invalid PDF annotation modification month: %w", err) } } if len(date) >= 8 { day, err = strconv.Atoi(date[6:8]) if err != nil { - return fmt.Errorf("invalid PDF stamp modification day: %w", err) + return fmt.Errorf("invalid PDF annotation modification day: %w", err) } } parsed := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) if parsed.Year() != year || int(parsed.Month()) != month || parsed.Day() != day { - return fmt.Errorf("invalid PDF stamp modification date") + return fmt.Errorf("invalid PDF annotation modification date") } converted.LastModDate = parsed.Format("2006-01-02") if len(date) > 8 { - p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF stamp modification time cannot fit OFD annotation date"}) + p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF annotation modification time cannot fit OFD annotation date"}) } } if _, err := p.editor.AddAnnotation(p.page, converted); err != nil { - return fmt.Errorf("stamp annotation: %w", err) + return fmt.Errorf("PDF annotation: %w", err) } p.report.TextObjects += stamp.report.TextObjects p.report.PathObjects += stamp.report.PathObjects p.report.ImageObjects += stamp.report.ImageObjects if flags&64 != 0 { - p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF stamp read-only flag not transferred"}) + p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF annotation read-only flag not transferred"}) } return nil } +// pdfAnnotationType 将具有静态外观的PDF注解类别对应到OFD注解类别 +// 入参: subtype PDF注解类别 +// 返回: string OFD注解类别,空字符串表示需单独处理 +func pdfAnnotationType(subtype pdfgo.Name) string { + switch subtype { + case "Stamp": + return "Stamp" + case "Watermark": + return "Watermark" + case "Highlight", "Underline", "Squiggly", "StrikeOut": + return "Highlight" + case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink": + return "Path" + } + return "" +} + // darkenStampAppearance 按PDF的Darken公式合成印章范围 // 入参: page PDF页面, box印章范围, mark图像, stamp局部转换器 // 返回: error 转换错误 diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index b6ab00f..b2a4a7e 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -22,7 +22,7 @@ import ( "github.com/xiaoqidun/pdfgo" ) -// annotations 将链接、印章和表单外观按页面顺序转换为OFD对象 +// annotations 将链接、静态注解和表单外观按页面顺序转换为OFD对象 // 入参: ctx 取消上下文, page PDF页面, strict 严格检查开关 // 返回: error 错误信息 func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error { @@ -48,8 +48,8 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict } continue } - if annotation.Subtype == "Stamp" { - if err := p.stampAnnotation(ctx, page, annotation); err != nil { + if pdfAnnotationType(annotation.Subtype) != "" { + if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { return err } continue @@ -196,6 +196,24 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict dest.OmitLeft, dest.OmitTop = retained[1], retained[0] } dest.OmitZoom = retained[2] || values[2] == 0 + } else if destination.Mode == "FitR" { + values := [4]float64{} + for n, v := range destination.Parameters { + switch v := v.(type) { + case pdfgo.Integer: + values[n] = float64(v) + case pdfgo.Real: + values[n] = float64(v) + default: + return fmt.Errorf("invalid PDF FitR destination coordinate") + } + } + bounds := pdfBounds([]pdfgo.Point{ + matrix.Apply(pdfgo.Point{X: values[0], Y: values[1]}), + matrix.Apply(pdfgo.Point{X: values[2], Y: values[3]}), + }) + dest.Left, dest.Top = bounds.X, bounds.Y + dest.Right, dest.Bottom = bounds.X+bounds.W, bounds.Y+bounds.H } else if destination.Mode != "Fit" { return &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)} } diff --git a/ofdgo_render_walk.go b/ofdgo_render_walk.go index bd27b80..76025cd 100644 --- a/ofdgo_render_walk.go +++ b/ofdgo_render_walk.go @@ -99,7 +99,7 @@ func (r *Renderer) walkAnnotation(annotation Annotation, visitor PageVisitor) er } box, _ := ParseBox(annotation.Appearance.Boundary) matrix := TranslationMatrix(box.X, box.Y) - state := RenderState{Parent: &matrix} + state := RenderState{Parent: &matrix, BoundaryInCTM: true} for i := range annotation.Appearance.Objects { if err := r.WalkObject(&annotation.Appearance.Objects[i], state, visitor); err != nil { return err