mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
376 lines
13 KiB
Go
376 lines
13 KiB
Go
// 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 pdf
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"maps"
|
|
"math"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/xiaoqidun/pdfgo"
|
|
)
|
|
|
|
// PrintPlacement 保存源PDF页索引及毫米纸面上的变换和裁剪
|
|
type PrintPlacement struct {
|
|
Page int
|
|
Matrix [6]float64
|
|
Clip [4]float64
|
|
}
|
|
|
|
// PrintSheet 保存一个纸面的物理尺寸、放置内容和辅助线
|
|
type PrintSheet struct {
|
|
Width, Height float64
|
|
Placements []PrintPlacement
|
|
Lines [][4]float64
|
|
Label string
|
|
LabelX, LabelY float64
|
|
}
|
|
|
|
// printPDFObjects 写出自有打印对象,原PDF资源仅复制实际引用的部分
|
|
type printPDFObjects struct {
|
|
ctx context.Context
|
|
objects []pdfgo.Object
|
|
copied map[pdfgo.Reference]pdfgo.Reference
|
|
source *pdfgo.Reader
|
|
}
|
|
|
|
// printPDFNumbers 编码内容流数值,不使用PDF语法不接受的科学记数法
|
|
// 入参: out 内容流缓冲区, values 待编码数值
|
|
func printPDFNumbers(out *bytes.Buffer, values ...float64) {
|
|
for _, value := range values {
|
|
out.WriteString(strconv.FormatFloat(value, 'f', -1, 64))
|
|
out.WriteByte(' ')
|
|
}
|
|
}
|
|
|
|
// add 登记一个独立输出对象
|
|
// 入参: object 待登记对象
|
|
// 返回: pdfgo.Reference 输出对象引用
|
|
func (p *printPDFObjects) add(object pdfgo.Object) pdfgo.Reference {
|
|
p.objects = append(p.objects, object)
|
|
return pdfgo.Reference{Number: int64(len(p.objects))}
|
|
}
|
|
|
|
// copy 递归复制资源并改写引用,保留流编码及资源共享
|
|
// 入参: object 待复制对象, depth 当前递归深度
|
|
// 返回: pdfgo.Object 改写引用后的对象, error 资源、嵌套或取消错误
|
|
func (p *printPDFObjects) copy(object pdfgo.Object, depth int) (pdfgo.Object, error) {
|
|
if err := p.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if depth > 256 {
|
|
return nil, fmt.Errorf("print PDF resource nesting too deep")
|
|
}
|
|
switch value := object.(type) {
|
|
case pdfgo.Reference:
|
|
if next, ok := p.copied[value]; ok {
|
|
return next, nil
|
|
}
|
|
next := p.add(nil)
|
|
p.copied[value] = next
|
|
resolved, err := p.source.Object(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copied, err := p.copy(resolved, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.objects[next.Number-1] = copied
|
|
return next, nil
|
|
case pdfgo.Dictionary:
|
|
result := make(pdfgo.Dictionary, len(value))
|
|
for key, item := range value {
|
|
copied, err := p.copy(item, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[key] = copied
|
|
}
|
|
return result, nil
|
|
case pdfgo.Array:
|
|
result := make(pdfgo.Array, len(value))
|
|
for i, item := range value {
|
|
copied, err := p.copy(item, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[i] = copied
|
|
}
|
|
return result, nil
|
|
case *pdfgo.Stream:
|
|
dict := maps.Clone(value.Dictionary)
|
|
delete(dict, "Length")
|
|
copied, err := p.copy(dict, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &pdfgo.Stream{Dictionary: copied.(pdfgo.Dictionary), Data: value.Data}, nil
|
|
default:
|
|
return object, nil
|
|
}
|
|
}
|
|
|
|
// WritePrintPDF 将源页面作为独立表单放置,保持矢量、文字、资源及透明组
|
|
// 入参: ctx 输出上下文, writer 输出流, source 源PDF数据, sheets 有序纸面
|
|
// 返回: error 资源、编码、写入或取消错误
|
|
func WritePrintPDF(ctx context.Context, writer io.Writer, source []byte, sheets []PrintSheet) error {
|
|
var reader *pdfgo.Reader
|
|
if len(source) > 0 {
|
|
var err error
|
|
reader, err = pdfgo.NewReader(bytes.NewReader(source), int64(len(source)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
}
|
|
p := &printPDFObjects{ctx: ctx, objects: make([]pdfgo.Object, 2), copied: make(map[pdfgo.Reference]pdfgo.Reference), source: reader}
|
|
var pages []*pdfgo.Page
|
|
if reader != nil {
|
|
if err := reader.WalkPages(ctx, func(_ int, page *pdfgo.Page) error { pages = append(pages, page); return nil }); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
forms := make(map[int]pdfgo.Reference)
|
|
var children pdfgo.Array
|
|
const pt = 72.0 / 25.4
|
|
for _, sheet := range sheets {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
var content bytes.Buffer
|
|
objects := make(pdfgo.Dictionary)
|
|
for _, placement := range sheet.Placements {
|
|
if placement.Page < 0 || placement.Page >= len(pages) {
|
|
return fmt.Errorf("print PDF page outside source")
|
|
}
|
|
page := pages[placement.Page]
|
|
if page.Rotation != 0 || page.UserUnit != 1 {
|
|
return fmt.Errorf("print PDF backend must normalize page rotation and units")
|
|
}
|
|
form, ok := forms[placement.Page]
|
|
if !ok {
|
|
resources, err := p.copy(page.Resources, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var data bytes.Buffer
|
|
if _, err := page.WriteContent(ctx, &data); err != nil {
|
|
return err
|
|
}
|
|
box := page.MediaBox
|
|
dict := pdfgo.Dictionary{"Type": pdfgo.Name("XObject"), "Subtype": pdfgo.Name("Form"), "FormType": pdfgo.Integer(1), "BBox": pdfgo.Array{pdfgo.Real(box.XMin), pdfgo.Real(box.YMin), pdfgo.Real(box.XMax), pdfgo.Real(box.YMax)}, "Resources": resources}
|
|
if group := page.Dictionary["Group"]; group != nil {
|
|
copied, err := p.copy(group, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dict["Group"] = copied
|
|
}
|
|
form = p.add(&pdfgo.Stream{Dictionary: dict, Data: data.Bytes()})
|
|
forms[placement.Page] = form
|
|
}
|
|
name := pdfgo.Name(fmt.Sprintf("P%d", placement.Page))
|
|
objects[name] = form
|
|
c, m := placement.Clip, placement.Matrix
|
|
hs := (page.MediaBox.YMax - page.MediaBox.YMin) / pt
|
|
x0, y0 := page.MediaBox.XMin, page.MediaBox.YMin
|
|
content.WriteString("q ")
|
|
printPDFNumbers(&content, c[0]*pt, (sheet.Height-c[1]-c[3])*pt, c[2]*pt, c[3]*pt)
|
|
content.WriteString("re W n ")
|
|
printPDFNumbers(&content, m[0], -m[1], -m[2], m[3], (m[2]*hs+m[4])*pt-m[0]*x0+m[2]*y0, (sheet.Height-m[3]*hs-m[5])*pt+m[1]*x0-m[3]*y0)
|
|
fmt.Fprintf(&content, "cm /%s Do Q\n", name)
|
|
}
|
|
for _, line := range sheet.Lines {
|
|
content.WriteString("q 0 G ")
|
|
printPDFNumbers(&content, 0.15*pt)
|
|
content.WriteString("w ")
|
|
printPDFNumbers(&content, line[0]*pt, (sheet.Height-line[1])*pt)
|
|
content.WriteString("m ")
|
|
printPDFNumbers(&content, line[2]*pt, (sheet.Height-line[3])*pt)
|
|
content.WriteString("l S Q\n")
|
|
}
|
|
resources := pdfgo.Dictionary{"XObject": objects}
|
|
if sheet.Label != "" {
|
|
content.WriteString("q 0 g BT /Label 7.94 Tf 1 0 0 1 ")
|
|
printPDFNumbers(&content, sheet.LabelX*pt, (sheet.Height-sheet.LabelY)*pt)
|
|
content.WriteString("Tm ")
|
|
if err := printPDFObject(&content, pdfgo.String(sheet.Label), 0); err != nil {
|
|
return err
|
|
}
|
|
content.WriteString(" Tj ET Q\n")
|
|
resources["Font"] = pdfgo.Dictionary{"Label": pdfgo.Dictionary{"Type": pdfgo.Name("Font"), "Subtype": pdfgo.Name("Type1"), "BaseFont": pdfgo.Name("Helvetica")}}
|
|
}
|
|
stream := p.add(&pdfgo.Stream{Dictionary: pdfgo.Dictionary{}, Data: content.Bytes()})
|
|
children = append(children, p.add(pdfgo.Dictionary{"Type": pdfgo.Name("Page"), "Parent": pdfgo.Reference{Number: 2}, "MediaBox": pdfgo.Array{pdfgo.Integer(0), pdfgo.Integer(0), pdfgo.Real(sheet.Width * pt), pdfgo.Real(sheet.Height * pt)}, "Resources": resources, "Contents": stream}))
|
|
}
|
|
return p.write(writer, children)
|
|
}
|
|
|
|
// WritePrintImages 逐纸面消费光栅结果,不在输出对象中保留未压缩像素
|
|
// 入参: ctx 输出上下文, writer 输出流, sheets 有序纸面, render 按索引渲染纸面
|
|
// 返回: error 渲染、编码、写入或取消错误
|
|
func WritePrintImages(ctx context.Context, writer io.Writer, sheets []PrintSheet, render func(int) (image.Image, error)) error {
|
|
p := &printPDFObjects{ctx: ctx, objects: make([]pdfgo.Object, 2)}
|
|
var children pdfgo.Array
|
|
const pt = 72.0 / 25.4
|
|
for i, sheet := range sheets {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
img, err := render(i)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var data bytes.Buffer
|
|
compressed := zlib.NewWriter(&data)
|
|
b := img.Bounds()
|
|
row := make([]byte, b.Dx()*3)
|
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
for x := b.Min.X; x < b.Max.X; x++ {
|
|
r, g, blue, _ := img.At(x, y).RGBA()
|
|
offset := (x - b.Min.X) * 3
|
|
row[offset], row[offset+1], row[offset+2] = byte(r>>8), byte(g>>8), byte(blue>>8)
|
|
}
|
|
if _, err := compressed.Write(row); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := compressed.Close(); err != nil {
|
|
return err
|
|
}
|
|
picture := p.add(&pdfgo.Stream{Dictionary: pdfgo.Dictionary{"Type": pdfgo.Name("XObject"), "Subtype": pdfgo.Name("Image"), "Width": pdfgo.Integer(b.Dx()), "Height": pdfgo.Integer(b.Dy()), "ColorSpace": pdfgo.Name("DeviceRGB"), "BitsPerComponent": pdfgo.Integer(8), "Filter": pdfgo.Name("FlateDecode")}, Data: data.Bytes()})
|
|
var content bytes.Buffer
|
|
content.WriteString("q ")
|
|
printPDFNumbers(&content, sheet.Width*pt, 0, 0, sheet.Height*pt, 0, 0)
|
|
content.WriteString("cm /I Do Q\n")
|
|
stream := p.add(&pdfgo.Stream{Dictionary: pdfgo.Dictionary{}, Data: content.Bytes()})
|
|
children = append(children, p.add(pdfgo.Dictionary{"Type": pdfgo.Name("Page"), "Parent": pdfgo.Reference{Number: 2}, "MediaBox": pdfgo.Array{pdfgo.Integer(0), pdfgo.Integer(0), pdfgo.Real(sheet.Width * pt), pdfgo.Real(sheet.Height * pt)}, "Resources": pdfgo.Dictionary{"XObject": pdfgo.Dictionary{"I": picture}}, "Contents": stream}))
|
|
}
|
|
return p.write(writer, children)
|
|
}
|
|
|
|
// write 写出完整对象表和交叉引用,失败时由调用方丢弃输出
|
|
// 入参: writer 输出流, children 页面对象引用
|
|
// 返回: error 编码、写入或取消错误
|
|
func (p *printPDFObjects) write(writer io.Writer, children pdfgo.Array) error {
|
|
p.objects[0] = pdfgo.Dictionary{"Type": pdfgo.Name("Catalog"), "Pages": pdfgo.Reference{Number: 2}}
|
|
p.objects[1] = pdfgo.Dictionary{"Type": pdfgo.Name("Pages"), "Count": pdfgo.Integer(len(children)), "Kids": children}
|
|
var out bytes.Buffer
|
|
out.WriteString("%PDF-1.7\n%\xe2\xe3\xcf\xd3\n")
|
|
offsets := make([]int, len(p.objects))
|
|
for i, object := range p.objects {
|
|
if err := p.ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
offsets[i] = out.Len()
|
|
fmt.Fprintf(&out, "%d 0 obj\n", i+1)
|
|
if err := printPDFObject(&out, object, 0); err != nil {
|
|
return err
|
|
}
|
|
out.WriteString("\nendobj\n")
|
|
}
|
|
xref := out.Len()
|
|
fmt.Fprintf(&out, "xref\n0 %d\n0000000000 65535 f \n", len(offsets)+1)
|
|
for _, offset := range offsets {
|
|
fmt.Fprintf(&out, "%010d 00000 n \n", offset)
|
|
}
|
|
fmt.Fprintf(&out, "trailer\n<</Size %d/Root 1 0 R>>\nstartxref\n%d\n%%%%EOF\n", len(offsets)+1, xref)
|
|
return Write(p.ctx, out.Bytes(), writer, WriteOptions{})
|
|
}
|
|
|
|
// printPDFObject 编码已复制的标准PDF对象,名称和字符串按字节转义
|
|
// 入参: out 对象缓冲区, object 待编码对象, depth 当前递归深度
|
|
// 返回: error 类型或嵌套错误
|
|
func printPDFObject(out *bytes.Buffer, object pdfgo.Object, depth int) error {
|
|
if depth > 256 {
|
|
return fmt.Errorf("print PDF object nesting too deep")
|
|
}
|
|
switch value := object.(type) {
|
|
case nil:
|
|
out.WriteString("null")
|
|
case pdfgo.Boolean:
|
|
fmt.Fprintf(out, "%t", value)
|
|
case pdfgo.Integer:
|
|
fmt.Fprintf(out, "%d", value)
|
|
case pdfgo.Real:
|
|
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
|
return fmt.Errorf("invalid print PDF number")
|
|
}
|
|
out.WriteString(strconv.FormatFloat(float64(value), 'f', -1, 64))
|
|
case pdfgo.Reference:
|
|
fmt.Fprintf(out, "%d %d R", value.Number, value.Generation)
|
|
case pdfgo.Name:
|
|
out.WriteByte('/')
|
|
for _, b := range []byte(value) {
|
|
if b <= 32 || b >= 127 || strings.ContainsRune("()<>[]{}/%#", rune(b)) {
|
|
fmt.Fprintf(out, "#%02X", b)
|
|
} else {
|
|
out.WriteByte(b)
|
|
}
|
|
}
|
|
case pdfgo.String:
|
|
out.WriteByte('<')
|
|
out.WriteString(hex.EncodeToString(value))
|
|
out.WriteByte('>')
|
|
case pdfgo.Array:
|
|
out.WriteByte('[')
|
|
for _, item := range value {
|
|
if err := printPDFObject(out, item, depth+1); err != nil {
|
|
return err
|
|
}
|
|
out.WriteByte(' ')
|
|
}
|
|
out.WriteByte(']')
|
|
case pdfgo.Dictionary:
|
|
out.WriteString("<<")
|
|
for _, key := range slices.Sorted(maps.Keys(value)) {
|
|
if err := printPDFObject(out, key, depth+1); err != nil {
|
|
return err
|
|
}
|
|
out.WriteByte(' ')
|
|
if err := printPDFObject(out, value[key], depth+1); err != nil {
|
|
return err
|
|
}
|
|
out.WriteByte('\n')
|
|
}
|
|
out.WriteString(">>")
|
|
case *pdfgo.Stream:
|
|
dict := maps.Clone(value.Dictionary)
|
|
dict["Length"] = pdfgo.Integer(len(value.Data))
|
|
if err := printPDFObject(out, dict, depth+1); err != nil {
|
|
return err
|
|
}
|
|
out.WriteString("\nstream\n")
|
|
out.Write(value.Data)
|
|
out.WriteString("\nendstream")
|
|
default:
|
|
return fmt.Errorf("unsupported print PDF object %T", object)
|
|
}
|
|
return nil
|
|
}
|