feat(转换服务): 完善PDF印章并提速
build.yaml / build (push) Successful in 40s

This commit is contained in:
xiaoqidun committed 2026-09-25 23:21:24 +08:00
1 parent 725ea0eceb
commit 7624d2dc91
9 files changed
+378 -33

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20260925101147-ee88b22d9f03 github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -60,8 +60,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= 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 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20260925101147-ee88b22d9f03 h1:aYnf/5Wz2sS68w+Tr7G/cfiw9p70RBE/dmgxwGbz21k= github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b h1:4j0AxYMyWpmZLcISwFG4wbNbyRxfb8wDi+j9KlGLybQ=
github.com/xiaoqidun/pdfgo v0.0.0-20260925101147-ee88b22d9f03/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+11 -3
View File
@@ -27,7 +27,7 @@ import (
// AddAnnotation 新增标准注解并分配独立标识,外观中的基本对象使用注解局部毫米坐标 // AddAnnotation 新增标准注解并分配独立标识,外观中的基本对象使用注解局部毫米坐标
// Appearance.Objects为空时使用分类对象集合,输入对象及资源不被修改 // Appearance.Objects为空时使用分类对象集合,输入对象及资源不被修改
// 入参: index 页面索引, annotation 注解内容,ID和LastModDate由编辑器生成 // 入参: index 页面索引, annotation 注解内容,ID由编辑器生成,空LastModDate使用当前日期
// 返回: string 注解标识, error 错误信息 // 返回: string 注解标识, error 错误信息
func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) { func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) {
var id string var id string
@@ -76,8 +76,14 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error)
if err != nil { if err != nil {
return err return err
} }
date := annotation.LastModDate
if date == "" {
date = time.Now().Format("2006-01-02")
} else if _, err := time.Parse("2006-01-02", date); err != nil {
return fmt.Errorf("invalid annotation date %q: %w", date, err)
}
attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: annotation.Type}, attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: annotation.Type},
{Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: time.Now().Format("2006-01-02")}} {Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: date}}
attrs.add("Subtype", annotation.Subtype) attrs.add("Subtype", annotation.Subtype)
attrs.flag("Visible", annotation.Visible) attrs.flag("Visible", annotation.Visible)
if annotation.NoZoom { if annotation.NoZoom {
@@ -367,7 +373,9 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by
resource := &e.resources[i] resource := &e.resources[i]
if resource.font != nil { if resource.font != nil {
resource.font = cloneEditorData(resource.font) resource.font = cloneEditorData(resource.font)
resource.font.FontFile = "/" + resource.name if resource.name != "" {
resource.font.FontFile = "/" + resource.name
}
} }
if resource.image != nil { if resource.image != nil {
resource.image = cloneEditorData(resource.image) resource.image = cloneEditorData(resource.image)
+28
View File
@@ -325,6 +325,34 @@ func collectObjectReferences(object GraphicObject, used map[string]bool) {
// 入参: value 颜色 // 入参: value 颜色
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) editorColor(value *FillColor) error { func (e *Editor) editorColor(value *FillColor) error {
if value != nil && value.Pattern != nil {
pattern := value.Pattern
if value.unsupported || value.AxialShd != nil || value.RadialShd != nil || !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 || !finite(pattern.XStep) || !finite(pattern.YStep) || pattern.XStep < pattern.Width || pattern.YStep < pattern.Height {
return fmt.Errorf("invalid pattern paint")
}
if pattern.RelativeTo != "" && pattern.RelativeTo != "Page" && pattern.RelativeTo != "Object" {
return fmt.Errorf("invalid pattern reference")
}
if pattern.CTM != "" {
if _, err := creationNumbers(pattern.CTM, 6); err != nil {
return err
}
}
base := *value
base.Pattern = nil
if _, err := e.Color(&base); err != nil {
return err
}
for _, object := range pattern.CellContent.Objects {
if object.Type == "PathObject" && (object.PathObject.FillColor != nil && object.PathObject.FillColor.Pattern != nil || object.PathObject.StrokeColor != nil && object.PathObject.StrokeColor.Pattern != nil) || object.Type == "TextObject" && (object.TextObject.FillColor != nil && object.TextObject.FillColor.Pattern != nil || object.TextObject.StrokeColor != nil && object.TextObject.StrokeColor.Pattern != nil) {
return fmt.Errorf("nested pattern paint is not supported")
}
if _, err := e.prepareObject("", object); err != nil {
return err
}
}
return nil
}
if value == nil || value.AxialShd == nil && value.RadialShd == nil { if value == nil || value.AxialShd == nil && value.RadialShd == nil {
_, err := e.Color(value) _, err := e.Color(value)
return err return err
+3 -1
View File
@@ -397,7 +397,9 @@ func (e *Editor) bindImportResources() {
resource := &e.resources[i] resource := &e.resources[i]
if resource.font != nil { if resource.font != nil {
resource.font = cloneEditorData(resource.font) resource.font = cloneEditorData(resource.font)
resource.font.FontFile = "/" + resource.name if resource.name != "" {
resource.font.FontFile = "/" + resource.name
}
} }
if resource.image != nil { if resource.image != nil {
resource.image = cloneEditorData(resource.image) resource.image = cloneEditorData(resource.image)
+65 -12
View File
@@ -274,15 +274,21 @@ func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor {
// 入参: path PDF路径, origin 对象边界 // 入参: path PDF路径, origin 对象边界
// 返回: string OFD路径数据 // 返回: string OFD路径数据
func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string { func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string {
var parts []string data := make([]byte, 0, len(path.Segments)*48)
for _, segment := range path.Segments { for index, segment := range path.Segments {
parts = append(parts, segment.Operator) if index != 0 {
data = append(data, ' ')
}
data = append(data, segment.Operator...)
for _, point := range segment.Points { for _, point := range segment.Points {
point = p.matrix.Apply(point) point = p.matrix.Apply(point)
parts = append(parts, pdfNumbers(point.X-origin.X, point.Y-origin.Y)) data = append(data, ' ')
data = strconv.AppendFloat(data, point.X-origin.X, 'g', -1, 64)
data = append(data, ' ')
data = strconv.AppendFloat(data, point.Y-origin.Y, 'g', -1, 64)
} }
} }
return strings.Join(parts, " ") return string(data)
} }
// clips 保持裁剪路径的交集,不随对象CTM重复变换 // clips 保持裁剪路径的交集,不随对象CTM重复变换
@@ -378,8 +384,16 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
margin := mark.Style.LineWidth * scale / 2 * math.Max(1, mark.Style.MiterLimit) margin := mark.Style.LineWidth * scale / 2 * math.Max(1, mark.Style.MiterLimit)
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin} box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
} }
strokeColor := StrokeColor(*p.paintColor(mark.Style.Stroke, box)) fillColor, err := p.paintColor(mark.Style.Fill, box)
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: p.paintColor(mark.Style.Fill, box), 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: p.clips(mark.Style.Clips, box)} if err != nil {
return err
}
strokePaint, err := p.paintColor(mark.Style.Stroke, box)
if err != nil {
return err
}
strokeColor := StrokeColor(*strokePaint)
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), 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: p.clips(mark.Style.Clips, box)}
object.LineWidthSet = object.LineWidth == 0 object.LineWidthSet = object.LineWidth == 0
if mark.Path.EvenOdd { if mark.Path.EvenOdd {
object.Rule = "Even-Odd" object.Rule = "Even-Odd"
@@ -400,9 +414,17 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
// paintColor 将页面渐变转换为对象局部坐标 // paintColor 将页面渐变转换为对象局部坐标
// 入参: paint PDF画刷, box 对象边界 // 入参: paint PDF画刷, box 对象边界
// 返回: *FillColor OFD颜色或渐变 // 返回: *FillColor OFD颜色或渐变, error 图案转换错误
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) *FillColor { func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
color := p.color(paint) color := p.color(paint)
if paint.Tiling != nil {
pattern, err := p.tilingPattern(paint.Tiling, paint)
if err != nil {
return nil, err
}
color.Pattern = pattern
return color, nil
}
if paint.Radial != nil { if paint.Radial != nil {
gradient := paint.Radial gradient := paint.Radial
start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End) start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End)
@@ -420,10 +442,10 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) *FillColor {
} }
color.Value = "" color.Value = ""
color.RadialShd = shading color.RadialShd = shading
return color return color, nil
} }
if paint.Axial == nil { if paint.Axial == nil {
return color return color, nil
} }
gradient := paint.Axial gradient := paint.Axial
start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End) start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End)
@@ -440,7 +462,38 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) *FillColor {
} }
color.Value = "" color.Value = ""
color.AxialShd = shading color.AxialShd = shading
return color return color, nil
}
// tilingPattern 保留PDF图案单元与页面坐标之间的仿射关系
// 入参: source 平铺图案, base 无色图案基色
// 返回: *Pattern OFD图案, error 转换错误
func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Paint) (*Pattern, error) {
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"}
}
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[:]...)}
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.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 {
return cell.group(mark, walk, visitor)
}
if err := source.Walk(p.ctx, base, visitor); err != nil {
return nil, err
}
if err := cell.flushPath(); err != nil {
return nil, err
}
pattern.CellContent.Objects = cell.objects
return pattern, nil
} }
// pdfOpaqueBlack 判断不受底层分色影响的全黑设备色 // pdfOpaqueBlack 判断不受底层分色影响的全黑设备色
+220 -5
View File
@@ -22,10 +22,223 @@ import (
"image/color" "image/color"
"image/png" "image/png"
"math" "math"
"strconv"
"strings"
"time"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// stampAnnotation 将PDF印章外观转换为OFD注解
// 入参: ctx取消上下文, page PDF页面, annotation PDF印章
// 返回: error 外观或转换错误
func (p *pdfImporter) stampAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error {
flags := int64(0)
if value := annotation.Dictionary["F"]; value != nil {
resolved, err := p.reader.Resolve(value)
if err != nil {
return err
}
number, ok := resolved.(pdfgo.Integer)
if !ok {
return fmt.Errorf("invalid PDF annotation flags")
}
flags = int64(number)
}
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}),
p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMax, Y: annotation.Rect.YMax}),
p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMin, Y: annotation.Rect.YMax}),
})
stamp := *p
stamp.matrix = (pdfgo.Matrix{1, 0, 0, 1, -box.X, -box.Y}).Mul(p.matrix)
stamp.objects = nil
stamp.pendingPath = nil
stamp.report = PDFImportReport{}
var darkenImages []pdfgo.ImageMark
visitor := pdfgo.Visitor{
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("stamp 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 {
return err
}
if err := stamp.flushPath(); 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"})
}
converted := Annotation{Type: "Stamp", Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}}
if flags&(1|2|32) != 0 {
visible := false
converted.Visible = &visible
}
converted.NoZoom = flags&8 != 0
converted.NoRotate = flags&16 != 0
if value := annotation.Dictionary["T"]; value != nil {
resolved, err := p.reader.Resolve(value)
if err != nil {
return err
}
creator, ok := resolved.(pdfgo.String)
if !ok {
return fmt.Errorf("invalid PDF stamp creator")
}
converted.Creator, err = pdfgo.DecodeTextString(creator)
if err != nil {
return err
}
}
if value := annotation.Dictionary["M"]; value != nil {
resolved, err := p.reader.Resolve(value)
if err != nil {
return err
}
encoded, ok := resolved.(pdfgo.String)
if !ok {
return fmt.Errorf("invalid PDF stamp modification date")
}
date, err := pdfgo.DecodeTextString(encoded)
if err != nil {
return err
}
date = strings.TrimPrefix(date, "D:")
if len(date) < 4 {
return fmt.Errorf("invalid PDF stamp modification date")
}
year, err := strconv.Atoi(date[:4])
if err != nil {
return fmt.Errorf("invalid PDF stamp 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)
}
}
if len(date) >= 8 {
day, err = strconv.Atoi(date[6:8])
if err != nil {
return fmt.Errorf("invalid PDF stamp 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")
}
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"})
}
}
if _, err := p.editor.AddAnnotation(p.page, converted); err != nil {
return fmt.Errorf("stamp 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"})
}
return nil
}
// 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
}
// signatureWidget 转换可见签名外观,不将PDF签名误写成OFD签名 // signatureWidget 转换可见签名外观,不将PDF签名误写成OFD签名
// 入参: ctx 取消上下文, page PDF页面, annotation 签名控件 // 入参: ctx 取消上下文, page PDF页面, annotation 签名控件
// 返回: error 外观或转换错误 // 返回: error 外观或转换错误
@@ -90,7 +303,7 @@ func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation p
if err := p.commitObjects(); err != nil { if err := p.commitObjects(); err != nil {
return err return err
} }
base, err := renderImportedPage(p.editor, p.page) base, err := renderImportedPage(p.editor, p.page, false)
if err != nil { if err != nil {
return err return err
} }
@@ -108,7 +321,7 @@ func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation p
if _, err := appearance.CopyObjects(0, stamp.objects, 0, 0); err != nil { if _, err := appearance.CopyObjects(0, stamp.objects, 0, 0); err != nil {
return err return err
} }
overlay, err := renderImportedPage(appearance, 0) overlay, err := renderImportedPage(appearance, 0, false)
if err != nil { if err != nil {
return err return err
} }
@@ -156,9 +369,9 @@ func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation p
} }
// renderImportedPage 在固定印刷分辨率绘制待合成页面 // renderImportedPage 在固定印刷分辨率绘制待合成页面
// 入参: editor 编辑文档, page 页序号 // 入参: editor 编辑文档, page 页序号, transparent 是否保留透明背景
// 返回: image.Image 页面像素, error 渲染错误 // 返回: image.Image 页面像素, error 渲染错误
func renderImportedPage(editor *Editor, page int) (image.Image, error) { func renderImportedPage(editor *Editor, page int, transparent bool) (image.Image, error) {
reader, err := editor.Reader() reader, err := editor.Reader()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -168,5 +381,7 @@ func renderImportedPage(editor *Editor, page int) (image.Image, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewRenderer(reader, WithDPI(300), WithRenderBackends(editor.backends)).RenderToImage(content) renderer := NewRenderer(reader, WithDPI(300), WithRenderBackends(editor.backends), WithAnnotations(!transparent))
renderer.TransparentBackground = transparent
return renderer.RenderToImage(content)
} }
+25 -2
View File
@@ -22,7 +22,7 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// annotations 将链接和签名外观按页面顺序转换为OFD对象 // annotations 将链接、印章和签名外观按页面顺序转换为OFD对象
// 入参: ctx 取消上下文, page PDF页面, strict 严格检查开关 // 入参: ctx 取消上下文, page PDF页面, strict 严格检查开关
// 返回: error 错误信息 // 返回: error 错误信息
func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error { func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error {
@@ -38,10 +38,29 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
} }
continue continue
} }
if annotation.Subtype == "Stamp" {
if err := p.stampAnnotation(ctx, page, annotation); err != nil {
return err
}
continue
}
if annotation.Subtype != "Link" { if annotation.Subtype != "Link" {
return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)} return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)}
} }
for _, key := range []pdfgo.Name{"AP", "AA", "OC"} { if dict["AP"] != nil {
visitor := pdfgo.Visitor{
Path: func(pdfgo.PathMark) error { return &pdfgo.UnsupportedError{Feature: "visible link appearance"} },
Text: func(pdfgo.TextMark) error { return &pdfgo.UnsupportedError{Feature: "visible link appearance"} },
Image: func(pdfgo.ImageMark) error { return &pdfgo.UnsupportedError{Feature: "visible link appearance"} },
}
visitor.Group = func(_ pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return walk(visitor)
}
if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil {
return err
}
}
for _, key := range []pdfgo.Name{"AA", "OC"} {
if dict[key] != nil { if dict[key] != nil {
return &pdfgo.UnsupportedError{Feature: fmt.Sprintf("link annotation field %q", key)} return &pdfgo.UnsupportedError{Feature: fmt.Sprintf("link annotation field %q", key)}
} }
@@ -139,6 +158,10 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
} }
targetPage := p.pages[destination.Page] targetPage := p.pages[destination.Page]
if targetPage == nil { if targetPage == nil {
if !strict {
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF link targets a page outside the document page tree; link omitted"})
continue
}
return fmt.Errorf("PDF destination page missing") return fmt.Errorf("PDF destination page missing")
} }
matrix, _, _ := pdfPageMatrix(targetPage) matrix, _, _ := pdfPageMatrix(targetPage)
+23 -7
View File
@@ -74,11 +74,14 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
} }
if source.ImageMask { if source.ImageMask {
fill := mark.Style.Fill.RGB
if mark.Style.Fill.CMYK != nil { if mark.Style.Fill.CMYK != nil {
return &pdfgo.UnsupportedError{Feature: "CMYK stencil image"} if *mark.Style.Fill.CMYK != [4]float64{} {
return &pdfgo.UnsupportedError{Feature: "CMYK stencil image"}
}
fill = [3]float64{1, 1, 1}
} }
stencil := image.NewNRGBA64(image.Rect(0, 0, source.Width, source.Height)) stencil := image.NewNRGBA64(image.Rect(0, 0, source.Width, source.Height))
fill := mark.Style.Fill.RGB
for y := 0; y < source.Height; y++ { for y := 0; y < source.Height; y++ {
for x := 0; x < source.Width; x++ { for x := 0; x < source.Width; x++ {
gray, _, _, _ := decoded.At(x, y).RGBA() gray, _, _, _ := decoded.At(x, y).RGBA()
@@ -167,14 +170,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
var program []byte var program []byte
program, err = pdfFontProgram(font) program, err = pdfFontProgram(font)
if err != nil { if err != nil {
return err return fmt.Errorf("PDF font %s program: %w", font.Name, err)
} }
id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0) id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0)
} else { } else {
id, err = p.editor.AddExternalFont(font.Name) id, err = p.editor.AddExternalFont(font.Name)
} }
if err != nil { if err != nil {
return err return fmt.Errorf("PDF font %s resource: %w", font.Name, err)
} }
p.fontIDs[font] = id p.fontIDs[font] = id
} }
@@ -188,7 +191,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data) metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data)
if err != nil { if err != nil {
return err return fmt.Errorf("PDF font %s metrics: %w", font.Name, err)
} }
p.fontMetrics[id] = metrics p.fontMetrics[id] = metrics
} }
@@ -271,7 +274,11 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
object.Boundary = pdfBoundary(box) object.Boundary = pdfBoundary(box)
if stroke { if stroke {
color := StrokeColor(*p.paintColor(mark.Style.Stroke, box)) strokePaint, err := p.paintColor(mark.Style.Stroke, box)
if err != nil {
return err
}
color := StrokeColor(*strokePaint)
object.StrokeColor = &color object.StrokeColor = &color
} }
if box.X >= p.pageWidth || box.Y >= p.pageHeight || box.X+box.W <= 0 || box.Y+box.H <= 0 { if box.X >= p.pageWidth || box.Y >= p.pageHeight || box.X+box.W <= 0 || box.Y+box.H <= 0 {
@@ -281,7 +288,10 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
object.Fill = &fill object.Fill = &fill
object.Stroke = &stroke object.Stroke = &stroke
object.Visible = &visible object.Visible = &visible
object.FillColor = p.paintColor(mark.Style.Fill, box) object.FillColor, err = p.paintColor(mark.Style.Fill, box)
if err != nil {
return err
}
object.Clips = p.clips(mark.Style.Clips, box) object.Clips = p.clips(mark.Style.Clips, box)
p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object}) p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object})
p.report.TextObjects++ p.report.TextObjects++
@@ -378,6 +388,12 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
tables["OS/2"] = buildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8]))) tables["OS/2"] = buildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8])))
changed = true changed = true
} }
for _, tag := range []string{"cvt ", "fpgm", "prep"} {
if data, ok := tables[tag]; ok && len(data) == 0 {
delete(tables, tag)
changed = true
}
}
if !changed && len(program)%4 == 0 { if !changed && len(program)%4 == 0 {
return program, nil return program, nil
} }