feat(注解转换): 完善注解合成并优化转换
build.yaml / build (push) Successful in 41s

This commit is contained in:
xiaoqidun committed 2026-09-28 16:16:05 +08:00
1 parent ee6c9f6a16
commit 1d365d29db
9 files changed
+132 -271

No files matched your search

+18 -234
View File
@@ -15,13 +15,8 @@
package ofdgo
import (
"bytes"
"context"
"fmt"
"image"
"image/color"
"image/png"
"math"
"strconv"
"strings"
"time"
@@ -34,6 +29,9 @@ import (
// 返回: error 外观或转换错误
func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error {
for _, key := range []pdfgo.Name{"AA", "OC", "A"} {
if annotation.Subtype == "Widget" && (key == "A" || key == "AA") {
continue
}
if key == "A" && (annotation.Subtype == "Link" || annotation.Subtype == "Movie") {
continue
}
@@ -67,49 +65,20 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
stamp.objects = nil
stamp.pendingPath = nil
stamp.report = PDFImportReport{}
var darkenImages []pdfgo.ImageMark
visitor := pdfgo.Visitor{
Warning: p.warning,
Path: func(mark pdfgo.PathMark) error {
if mark.Style.BlendMode == "Darken" {
return &pdfgo.UnsupportedError{Feature: "darken stamp path"}
}
return stamp.path(mark)
},
Text: func(mark pdfgo.TextMark) error {
if mark.Style.BlendMode == "Darken" {
return &pdfgo.UnsupportedError{Feature: "darken stamp text"}
}
if err := stamp.text(mark); err != nil {
return fmt.Errorf("annotation text: %w", err)
}
return nil
},
Image: func(mark pdfgo.ImageMark) error {
if mark.Style.BlendMode == "Darken" {
darkenImages = append(darkenImages, mark)
return nil
}
return stamp.image(mark)
},
}
visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return stamp.group(group, walk, visitor)
}
if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil {
stamp.compositeCache = nil
stamp.clipTexts = make(map[*pdfgo.TextClip]TextObject)
stamp.pageWidth, stamp.pageHeight = box.W, box.H
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
return p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor)
})
if err != nil {
return err
}
if err := stamp.flushPath(); err != nil {
if err := stamp.compositeObjects(nodes); err != nil {
return err
}
if len(darkenImages) != 0 {
if len(darkenImages) != 1 || len(stamp.objects) != 0 {
return &pdfgo.UnsupportedError{Feature: "mixed darken stamp appearance"}
}
if err := p.darkenStampAppearance(page, box, darkenImages[0], &stamp); err != nil {
return err
}
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF darken stamp appearance flattened over page content"})
if flags&(2|32) == 0 {
p.compositeNodes = stamp.compositeNodes
}
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}}
popupObject := annotation.Dictionary["Popup"]
@@ -234,77 +203,13 @@ func pdfAnnotationType(subtype pdfgo.Name) string {
return "Watermark"
case "Highlight", "Underline", "Squiggly", "StrikeOut":
return "Highlight"
case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup":
case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup", "Widget":
return "Path"
}
return ""
}
// darkenStampAppearance 按PDF的Darken公式合成印章范围
// 入参: page PDF页面, box印章范围, mark图像, stamp局部转换器
// 返回: error 转换错误
func (p *pdfImporter) darkenStampAppearance(page *pdfgo.Page, box Box, mark pdfgo.ImageMark, stamp *pdfImporter) error {
if err := p.commitObjects(); err != nil {
return err
}
base, err := renderImportedPage(p.editor, p.page, false)
if err != nil {
return err
}
_, width, height := pdfPageMatrix(page)
appearance := NewEditor()
appearance.SetRenderBackends(p.editor.backends)
if _, err := appearance.AddPage(width, height); err != nil {
return err
}
imageImporter := pdfImporter{reader: p.reader, editor: appearance, matrix: p.matrix, pageBox: page.CropBox}
mark.Style.BlendMode = "Normal"
if err := imageImporter.image(mark); err != nil {
return err
}
if _, err := appearance.CopyObjects(0, imageImporter.objects, 0, 0); err != nil {
return err
}
overlay, err := renderImportedPage(appearance, 0, true)
if err != nil {
return err
}
if base.Bounds() != overlay.Bounds() {
return fmt.Errorf("stamp appearance page size mismatch")
}
const dpi = 300.0
pixelsPerMM := dpi / 25.4
region := image.Rect(int(math.Floor(box.X*pixelsPerMM)), int(math.Floor(box.Y*pixelsPerMM)), int(math.Ceil((box.X+box.W)*pixelsPerMM)), int(math.Ceil((box.Y+box.H)*pixelsPerMM))).Intersect(base.Bounds())
if region.Empty() {
return fmt.Errorf("stamp appearance outside page")
}
composite := image.NewRGBA(image.Rect(0, 0, region.Dx(), region.Dy()))
for y := region.Min.Y; y < region.Max.Y; y++ {
for x := region.Min.X; x < region.Max.X; x++ {
background := color.NRGBAModel.Convert(base.At(x, y)).(color.NRGBA)
foreground := color.NRGBAModel.Convert(overlay.At(x, y)).(color.NRGBA)
alpha := float64(foreground.A) / 255
blend := func(back, front uint8) uint8 {
return uint8(math.Round((1-alpha)*float64(back) + alpha*math.Min(float64(back), float64(front))))
}
composite.SetRGBA(x-region.Min.X, y-region.Min.Y, color.RGBA{R: blend(background.R, foreground.R), G: blend(background.G, foreground.G), B: blend(background.B, foreground.B), A: 255})
}
}
var encoded bytes.Buffer
if err := png.Encode(&encoded, composite); err != nil {
return err
}
id, err := p.editor.AddImage(encoded.Bytes())
if err != nil {
return err
}
regionBox := Box{X: float64(region.Min.X)/pixelsPerMM - box.X, Y: float64(region.Min.Y)/pixelsPerMM - box.Y, W: float64(region.Dx()) / pixelsPerMM, H: float64(region.Dy()) / pixelsPerMM}
stamp.objects = append(stamp.objects, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{Boundary: pdfBoundary(regionBox), ResourceID: id}})
stamp.report.ImageObjects++
return nil
}
// formWidget 将表单当前外观转换为页面对象并报告交互语义差异
// formWidget 将表单当前外观转换为注解并报告交互语义差异
// 入参: ctx 取消上下文, page PDF页面, annotation 表单控件, strict 严格检查开关
// 返回: error 外观或转换错误
func (p *pdfImporter) formWidget(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error {
@@ -326,12 +231,7 @@ func (p *pdfImporter) formWidget(ctx context.Context, page *pdfgo.Page, annotati
if flags&(8|16) != 0 || annotation.Dictionary["OC"] != nil {
return &pdfgo.UnsupportedError{Feature: "widget viewing behavior"}
}
visitor := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning}
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, visitor) }
if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil {
return err
}
if err := p.flushPath(); err != nil {
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF form field appearance imported; interactive behavior not transferred"})
@@ -362,125 +262,9 @@ func (p *pdfImporter) signatureWidget(ctx context.Context, page *pdfgo.Page, ann
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "hidden PDF signature not transferred"})
return nil
}
var images []pdfgo.ImageMark
visitor := pdfgo.Visitor{
Path: func(pdfgo.PathMark) error { return &pdfgo.UnsupportedError{Feature: "signature appearance path"} },
Text: func(pdfgo.TextMark) error { return &pdfgo.UnsupportedError{Feature: "signature appearance text"} },
Image: func(mark pdfgo.ImageMark) error {
images = append(images, mark)
return nil
},
}
if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil {
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
if len(images) != 1 {
return &pdfgo.UnsupportedError{Feature: "signature appearance composition"}
}
mark := images[0]
message := "PDF signature appearance imported; digital signature not transferred"
if mark.Style.BlendMode == "Multiply" {
if err := p.multiplySignatureAppearance(page, annotation, mark); err != nil {
return err
}
message = "PDF signature appearance flattened over page content; digital signature not transferred"
} else if mark.Style.BlendMode == "" || mark.Style.BlendMode == "Normal" || mark.Style.BlendMode == "Compatible" {
if err := p.image(mark); err != nil {
return err
}
} else {
return &pdfgo.UnsupportedError{Feature: "signature appearance blend mode"}
}
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: message})
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF signature appearance imported; digital signature not transferred"})
return nil
}
// multiplySignatureAppearance 合成签名覆盖区域,正文及其他页面对象保持独立
// 入参: page PDF页面, annotation 签名控件, mark 印章图像
// 返回: error 图像或渲染错误
func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation pdfgo.Annotation, mark pdfgo.ImageMark) error {
if err := p.commitObjects(); err != nil {
return err
}
base, err := renderImportedPage(p.editor, p.page, false)
if err != nil {
return err
}
_, width, height := pdfPageMatrix(page)
appearance := NewEditor()
appearance.SetRenderBackends(p.editor.backends)
if _, err := appearance.AddPage(width, height); err != nil {
return err
}
stamp := pdfImporter{reader: p.reader, editor: appearance, matrix: p.matrix, pageBox: page.CropBox}
mark.Style.BlendMode = "Normal"
if err := stamp.image(mark); err != nil {
return err
}
if _, err := appearance.CopyObjects(0, stamp.objects, 0, 0); err != nil {
return err
}
overlay, err := renderImportedPage(appearance, 0, false)
if err != nil {
return err
}
if base.Bounds() != overlay.Bounds() {
return fmt.Errorf("signature appearance page size mismatch")
}
annotationBox := annotation.Rect
box := pdfBounds([]pdfgo.Point{
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMin, Y: annotationBox.YMin}),
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMax, Y: annotationBox.YMin}),
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMax, Y: annotationBox.YMax}),
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMin, Y: annotationBox.YMax}),
})
const dpi = 300.0
pixelsPerMM := dpi / 25.4
region := image.Rect(int(math.Floor(box.X*pixelsPerMM)), int(math.Floor(box.Y*pixelsPerMM)), int(math.Ceil((box.X+box.W)*pixelsPerMM)), int(math.Ceil((box.Y+box.H)*pixelsPerMM))).Intersect(base.Bounds())
if region.Empty() {
return fmt.Errorf("signature appearance outside page")
}
composite := image.NewRGBA(image.Rect(0, 0, region.Dx(), region.Dy()))
for y := region.Min.Y; y < region.Max.Y; y++ {
for x := region.Min.X; x < region.Max.X; x++ {
background := color.RGBAModel.Convert(base.At(x, y)).(color.RGBA)
foreground := color.RGBAModel.Convert(overlay.At(x, y)).(color.RGBA)
composite.SetRGBA(x-region.Min.X, y-region.Min.Y, color.RGBA{
R: uint8((int(background.R)*int(foreground.R) + 127) / 255),
G: uint8((int(background.G)*int(foreground.G) + 127) / 255),
B: uint8((int(background.B)*int(foreground.B) + 127) / 255),
A: 255,
})
}
}
var encoded bytes.Buffer
if err := png.Encode(&encoded, composite); err != nil {
return err
}
id, err := p.editor.AddImage(encoded.Bytes())
if err != nil {
return err
}
regionBox := Box{X: float64(region.Min.X) / pixelsPerMM, Y: float64(region.Min.Y) / pixelsPerMM, W: float64(region.Dx()) / pixelsPerMM, H: float64(region.Dy()) / pixelsPerMM}
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{Boundary: pdfBoundary(regionBox), ResourceID: id}})
p.report.ImageObjects++
return nil
}
// renderImportedPage 在固定印刷分辨率绘制待合成页面
// 入参: editor 编辑文档, page 页序号, transparent 是否保留透明背景
// 返回: image.Image 页面像素, error 渲染错误
func renderImportedPage(editor *Editor, page int, transparent bool) (image.Image, error) {
reader, err := editor.Reader()
if err != nil {
return nil, err
}
defer reader.Close()
content, err := reader.PageContentByIndex(page)
if err != nil {
return nil, err
}
renderer := NewRenderer(reader, WithDPI(300), WithRenderBackends(editor.backends), WithAnnotations(!transparent))
renderer.TransparentBackground = transparent
return renderer.RenderToImage(content)
}