mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
725ea0eceb
commit
7624d2dc91
9 files changed
+378
-33
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+65
-12
@@ -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 判断不受底层分色影响的全黑设备色
|
||||
|
||||
+220
-5
@@ -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)
|
||||
}
|
||||
+25
-2
@@ -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)
|
||||
|
||||
+23
-7
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user