diff --git a/go.mod b/go.mod index 904a87d..1f89e29 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-20260925101147-ee88b22d9f03 + github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b 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 825c1d7..d57b640 100644 --- a/go.sum +++ b/go.sum @@ -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/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-20260925101147-ee88b22d9f03 h1:aYnf/5Wz2sS68w+Tr7G/cfiw9p70RBE/dmgxwGbz21k= -github.com/xiaoqidun/pdfgo v0.0.0-20260925101147-ee88b22d9f03/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= +github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b h1:4j0AxYMyWpmZLcISwFG4wbNbyRxfb8wDi+j9KlGLybQ= +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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo_editor_annot_content.go b/ofdgo_editor_annot_content.go index fe9b432..f44bd34 100644 --- a/ofdgo_editor_annot_content.go +++ b/ofdgo_editor_annot_content.go @@ -27,7 +27,7 @@ import ( // AddAnnotation 新增标准注解并分配独立标识,外观中的基本对象使用注解局部毫米坐标 // Appearance.Objects为空时使用分类对象集合,输入对象及资源不被修改 -// 入参: index 页面索引, annotation 注解内容,ID和LastModDate由编辑器生成 +// 入参: index 页面索引, annotation 注解内容,ID由编辑器生成,空LastModDate使用当前日期 // 返回: string 注解标识, error 错误信息 func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) { var id string @@ -76,8 +76,14 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) if err != nil { 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}, - {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.flag("Visible", annotation.Visible) if annotation.NoZoom { @@ -367,7 +373,9 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by resource := &e.resources[i] if resource.font != nil { resource.font = cloneEditorData(resource.font) - resource.font.FontFile = "/" + resource.name + if resource.name != "" { + resource.font.FontFile = "/" + resource.name + } } if resource.image != nil { resource.image = cloneEditorData(resource.image) diff --git a/ofdgo_editor_color.go b/ofdgo_editor_color.go index 047e9d1..131ebd6 100644 --- a/ofdgo_editor_color.go +++ b/ofdgo_editor_color.go @@ -325,6 +325,34 @@ func collectObjectReferences(object GraphicObject, used map[string]bool) { // 入参: value 颜色 // 返回: 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 { _, err := e.Color(value) return err diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index 7108b05..0c77914 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -397,7 +397,9 @@ func (e *Editor) bindImportResources() { resource := &e.resources[i] if resource.font != nil { resource.font = cloneEditorData(resource.font) - resource.font.FontFile = "/" + resource.name + if resource.name != "" { + resource.font.FontFile = "/" + resource.name + } } if resource.image != nil { resource.image = cloneEditorData(resource.image) diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index a12c6a4..22f8db9 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -274,15 +274,21 @@ func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor { // 入参: path PDF路径, origin 对象边界 // 返回: string OFD路径数据 func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string { - var parts []string - for _, segment := range path.Segments { - parts = append(parts, segment.Operator) + data := make([]byte, 0, len(path.Segments)*48) + for index, segment := range path.Segments { + if index != 0 { + data = append(data, ' ') + } + data = append(data, segment.Operator...) for _, point := range segment.Points { 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重复变换 @@ -378,8 +384,16 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { 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} } - strokeColor := StrokeColor(*p.paintColor(mark.Style.Stroke, 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)} + fillColor, err := p.paintColor(mark.Style.Fill, 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 if mark.Path.EvenOdd { object.Rule = "Even-Odd" @@ -400,9 +414,17 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { // paintColor 将页面渐变转换为对象局部坐标 // 入参: paint PDF画刷, box 对象边界 -// 返回: *FillColor OFD颜色或渐变 -func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) *FillColor { +// 返回: *FillColor OFD颜色或渐变, error 图案转换错误 +func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) { 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 { gradient := paint.Radial 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.RadialShd = shading - return color + return color, nil } if paint.Axial == nil { - return color + return color, nil } gradient := paint.Axial 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.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 判断不受底层分色影响的全黑设备色 diff --git a/ofdgo_pdf_annot.go b/ofdgo_pdf_annot.go index d04caed..09b087f 100644 --- a/ofdgo_pdf_annot.go +++ b/ofdgo_pdf_annot.go @@ -22,10 +22,223 @@ import ( "image/color" "image/png" "math" + "strconv" + "strings" + "time" "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签名 // 入参: ctx 取消上下文, page PDF页面, annotation 签名控件 // 返回: error 外观或转换错误 @@ -90,7 +303,7 @@ func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation p if err := p.commitObjects(); err != nil { return err } - base, err := renderImportedPage(p.editor, p.page) + base, err := renderImportedPage(p.editor, p.page, false) if err != nil { 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 { return err } - overlay, err := renderImportedPage(appearance, 0) + overlay, err := renderImportedPage(appearance, 0, false) if err != nil { return err } @@ -156,9 +369,9 @@ func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation p } // renderImportedPage 在固定印刷分辨率绘制待合成页面 -// 入参: editor 编辑文档, page 页序号 +// 入参: editor 编辑文档, page 页序号, transparent 是否保留透明背景 // 返回: 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() if err != nil { return nil, err @@ -168,5 +381,7 @@ func renderImportedPage(editor *Editor, page int) (image.Image, error) { if err != nil { 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) } diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index c823985..bb76965 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 { @@ -38,10 +38,29 @@ 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 { + return err + } + continue + } if annotation.Subtype != "Link" { 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 { 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] 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") } matrix, _, _ := pdfPageMatrix(targetPage) diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index 27fb815..f9a5501 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -74,11 +74,14 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { } } if source.ImageMask { + fill := mark.Style.Fill.RGB 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)) - fill := mark.Style.Fill.RGB for y := 0; y < source.Height; y++ { for x := 0; x < source.Width; x++ { gray, _, _, _ := decoded.At(x, y).RGBA() @@ -167,14 +170,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { var program []byte program, err = pdfFontProgram(font) 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) } else { id, err = p.editor.AddExternalFont(font.Name) } if err != nil { - return err + return fmt.Errorf("PDF font %s resource: %w", font.Name, err) } 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) if err != nil { - return err + return fmt.Errorf("PDF font %s metrics: %w", font.Name, err) } p.fontMetrics[id] = metrics } @@ -271,7 +274,11 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } object.Boundary = pdfBoundary(box) 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 } 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.Stroke = &stroke 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) p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object}) 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]))) 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 { return program, nil }