diff --git a/go.mod b/go.mod index 2a99bd0..97683f6 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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-20261003024624-bf2ee2f33c58 + github.com/xiaoqidun/pdfgo v0.0.0-20261003042717-ffb073d5634d 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 917f762..3546da7 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20261003024624-bf2ee2f33c58 h1:n2RFxdeo5fmtvgkRKeMSgJHqmVEGMoS45SppUk4laaE= -github.com/xiaoqidun/pdfgo v0.0.0-20261003024624-bf2ee2f33c58/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20261003042717-ffb073d5634d h1:WiG6LgYU1hZBAELi72qP6UnCyA8cPe7Ap6nto0ofGnA= +github.com/xiaoqidun/pdfgo v0.0.0-20261003042717-ffb073d5634d/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index 6a31160..822fc8e 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -108,11 +108,14 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float return } m = m.Scale(imgW/float64(img.Bounds().Dx()), imgH/float64(img.Bounds().Dy())) - img, pad := imageWithTransparentEdge(img) - if pad > 0 { - p := float64(pad) - m[0][2] -= m[0][0]*p + m[0][1]*p - m[1][2] -= m[1][0]*p + m[1][1]*p + if _, pdf := ctx.Renderer.(*pdfRenderer); !pdf { + var pad int + img, pad = imageWithTransparentEdge(img) + if pad > 0 { + p := float64(pad) + m[0][2] -= m[0][0]*p + m[0][1]*p + m[1][2] -= m[1][0]*p + m[1][1]*p + } } ctx.RenderImage(r.canvasEncodedImage(img), ctx.CoordSystemView().Mul(ctx.View()).Mul(m)) if compiled && imageClip != nil && compiler.err == nil { diff --git a/ofdgo_renderer_output_canvas.go b/ofdgo_renderer_output_canvas.go index 5dc6992..b82b01d 100644 --- a/ofdgo_renderer_output_canvas.go +++ b/ofdgo_renderer_output_canvas.go @@ -18,6 +18,7 @@ import ( "bufio" "bytes" "fmt" + "image" "io" "github.com/tdewolff/canvas" @@ -126,7 +127,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i } start := buf.Len() p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil) - renderer := &pdfRenderer{PDF: p, glyphPaths: make(map[*canvas.Path]*canvas.Path)} + renderer := &pdfRenderer{PDF: p, glyphPaths: make(map[*canvas.Path]*canvas.Path), images: make([][]image.Image, 1)} var info DocInfo if docInfo, err := r.Reader.DocInfo(); err == nil { info = *docInfo @@ -135,6 +136,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i for i, page := range pages { if i > 0 { p.NewPage(page.Box.W, page.Box.H) + renderer.images = append(renderer.images, nil) } navigation.apply(p, i) if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), page.Content, !r.TransparentBackground); err != nil { @@ -154,6 +156,16 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i return err } data := replacePDFProducer(buf.Bytes()[start:]) + data, err = renderer.preserveImages(r.outputContext(), data) + if err != nil { + buf.Truncate(start) + return err + } + if direct && renderer.exactImages { + buf.Truncate(start) + _, err = buf.Write(data) + return err + } if r.Compression.Mode != CompressionUnchanged { reader, err := pdfgo.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { @@ -171,7 +183,10 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i if direct { return nil } - _, err = writer.Write(data) + n, err := writer.Write(data) + if err == nil && n != len(data) { + err = io.ErrShortWrite + } return err } diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index 0b6477f..3080059 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -17,15 +17,19 @@ package ofdgo import ( "fmt" "image" + "image/color" "github.com/tdewolff/canvas" + cimage "github.com/tdewolff/canvas/image" "github.com/tdewolff/canvas/renderers/pdf" ) // pdfRenderer PDF渲染器 type pdfRenderer struct { *pdf.PDF - glyphPaths map[*canvas.Path]*canvas.Path + glyphPaths map[*canvas.Path]*canvas.Path + images [][]image.Image + exactImages bool } // pdfNavigation PDF导航信息 @@ -58,6 +62,27 @@ type pdfOutline struct { // RenderImage 在图像局部保存状态前同步不透明画笔,避免恢复后的透明度与后端缓存不一致 // 入参: img 图像, matrix 图像变换 func (r *pdfRenderer) RenderImage(img image.Image, matrix canvas.Matrix) { + if len(r.images) != 0 { + var original image.Image + opaque := false + if source, ok := img.(interface{ Opaque() bool }); ok { + opaque = source.Opaque() + } + switch img.ColorModel() { + case color.GrayModel, color.CMYKModel: + case color.RGBAModel, color.NRGBAModel, color.AlphaModel: + if !opaque { + original = img + } + default: + if source, ok := img.(*cimage.Image); !ok || source.Mimetype != "image/jpeg" || source.Mask != nil { + original = img + } + } + r.exactImages = r.exactImages || original != nil + last := len(r.images) - 1 + r.images[last] = append(r.images[last], original) + } style := canvas.DefaultStyle style.Fill = canvas.Paint{Color: canvas.White} style.Stroke = canvas.Paint{Color: canvas.Black} diff --git a/ofdgo_renderer_pdf_images.go b/ofdgo_renderer_pdf_images.go new file mode 100644 index 0000000..f78aa6a --- /dev/null +++ b/ofdgo_renderer_pdf_images.go @@ -0,0 +1,106 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "bytes" + "context" + "fmt" + "image" + + "github.com/xiaoqidun/pdfgo" +) + +// preserveImages 按实际绘制顺序恢复颜色及透明度精度,不依赖后端资源命名或对象编号 +// 入参: ctx 取消上下文, data 后端PDF数据 +// 返回: []byte 保留图像精度的PDF数据, error 资源或写入错误 +func (r *pdfRenderer) preserveImages(ctx context.Context, data []byte) ([]byte, error) { + if !r.exactImages { + return data, nil + } + reader, err := pdfgo.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return nil, err + } + defer reader.Close() + images := make(map[pdfgo.Reference]image.Image) + pages := 0 + err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { + if index >= len(r.images) { + return fmt.Errorf("unexpected PDF output page") + } + pages++ + value, err := reader.Resolve(page.Resources["XObject"]) + if err != nil { + return err + } + resources, _ := value.(pdfgo.Dictionary) + var content bytes.Buffer + if _, err := page.WriteContent(ctx, &content); err != nil { + return err + } + position := 0 + if err := pdfgo.WalkOperations(ctx, content.Bytes(), func(operation pdfgo.Operation) error { + if operation.Operator != "Do" { + return nil + } + if len(operation.Operands) != 1 || position >= len(r.images[index]) { + return fmt.Errorf("unexpected PDF output image") + } + name, ok := operation.Operands[0].(pdfgo.Name) + if !ok { + return fmt.Errorf("invalid PDF output image name") + } + ref, ok := resources[name].(pdfgo.Reference) + if !ok { + return fmt.Errorf("invalid PDF output image reference") + } + original := r.images[index][position] + position++ + if original == nil { + return nil + } + image, err := reader.ReadImage(ref) + if err != nil { + return err + } + if image.Width != original.Bounds().Dx() || image.Height != original.Bounds().Dy() { + return fmt.Errorf("PDF output image dimensions differ") + } + if previous := images[ref]; previous != nil && previous != original { + return fmt.Errorf("PDF output image reference conflicts") + } + images[ref] = original + return nil + }); err != nil { + return err + } + if position != len(r.images[index]) { + return fmt.Errorf("PDF output image missing") + } + return nil + }) + if err != nil { + return nil, err + } + if pages != len(r.images) { + return nil, fmt.Errorf("PDF output page missing") + } + var result bytes.Buffer + if _, err := reader.ReplaceImagesTo(ctx, &result, images); err != nil { + return nil, err + } + return result.Bytes(), nil +} diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index ba2b3b1..5169153 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -18,6 +18,7 @@ import ( "strings" "github.com/tdewolff/canvas" + "github.com/tdewolff/font" ) const ptPerMM = 72.0 / 25.4 @@ -181,7 +182,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 ctx.Pop() return } - face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) + face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal, font.NoHinting) recoverBaseline := len(obj.TextCode) > 0 && obj.TextCode[0].Y == "" var metrics canvas.FontMetrics if textRun != nil || recoverBaseline {