Files
ofdgo/ofdgo_print.go
2026-10-24 20:53:46 +08:00

656 lines
25 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"context"
"errors"
"fmt"
"math"
"slices"
"sync"
)
// ErrPrintDenied 表示文档不允许打印或请求份数超出声明上限
var ErrPrintDenied = errors.New("document printing is not permitted")
// PrintOptions 设置内容排版,长度以毫米为单位,比例1表示100%
// Pages为零基有序页码,nil表示全部,非nil空列表表示不选择页面;与PageRange互斥
// Mode支持single、multiple、booklet、poster;Scaling支持fit、shrink、actual、custom
// PaperWidth和PaperHeight为实际纸面尺寸,显式提供时不自动交换方向
// 两者同时为零时,单页沿用源尺寸,其他模式采用A4,小册子默认横向
// Parity支持all、odd、even,按源页码过滤;Sides支持all、front、back,按输出纸面过滤
// BookletSheets为每册纸张数,0表示整份一册;Binding支持left、right
// Rows和Columns仅用于多页,Gap为单元间隔,Overlap为海报相邻纸面的重叠
// Duplex支持simplex、longEdge、shortEdge;小册子默认shortEdge,其他模式默认simplex
// PrintableArea为已选设备当前方向下的可打印区域,Margins为该区域内的额外留白
// Reverse反转纸张输出顺序,保留每张纸内部的正背面与拼版顺序
// Copies和Collate描述设备作业,不展开重复内容;SheetFrom/SheetTo为一基纸张范围
// Contents支持documentAndAnnotations、document、annotations、forms,document保留表单外观
// PageOrder支持horizontal、horizontalReverse、vertical、verticalReverse
// BackRotate将背面绕纸张中心旋转180度;海报Labels保留底部5毫米绘制拼接标签
// Scale用于自定义缩放及海报分块,零值采用1;Rotate允许自动旋转页面以适应单元
// Raster将纸面光栅化,Grayscale以光栅方式输出灰度;DPI默认300,上限2400
// Borders绘制单元边框,CropMarks绘制裁切标记,TileOnlyLarge仅分块超大页面
type PrintOptions struct {
Pages []int `json:"pages,omitempty"`
PageRange string `json:"pageRange,omitempty"`
Mode string `json:"mode,omitempty"`
Scaling string `json:"scaling,omitempty"`
PaperWidth float64 `json:"paperWidth,omitempty"`
PaperHeight float64 `json:"paperHeight,omitempty"`
Margins PrintMargins `json:"margins"`
PrintableArea *Box `json:"printableArea,omitempty"`
Duplex string `json:"duplex,omitempty"`
BackRotate bool `json:"backRotate,omitempty"`
Copies int `json:"copies,omitempty"`
Collate *bool `json:"collate,omitempty"`
SheetFrom int `json:"sheetFrom,omitempty"`
SheetTo int `json:"sheetTo,omitempty"`
Scale float64 `json:"scale,omitempty"`
Rotate bool `json:"rotate,omitempty"`
Reverse bool `json:"reverse,omitempty"`
Parity string `json:"parity,omitempty"`
Sides string `json:"sides,omitempty"`
Rows int `json:"rows,omitempty"`
Columns int `json:"columns,omitempty"`
// ColumnOrder 兼容纵向填充设置,仅在PageOrder为空时生效
ColumnOrder bool `json:"columnOrder,omitempty"`
PageOrder string `json:"pageOrder,omitempty"`
Gap float64 `json:"gap,omitempty"`
Overlap float64 `json:"overlap,omitempty"`
BookletSheets int `json:"bookletSheets,omitempty"`
Binding string `json:"binding,omitempty"`
// Annotations 兼容注解开关,仅在Contents为空时生效
Annotations *bool `json:"annotations,omitempty"`
Contents string `json:"contents,omitempty"`
Raster bool `json:"raster,omitempty"`
DPI float64 `json:"dpi,omitempty"`
Grayscale bool `json:"grayscale,omitempty"`
Borders bool `json:"borders,omitempty"`
CropMarks bool `json:"cropMarks,omitempty"`
TileOnlyLarge bool `json:"tileOnlyLarge,omitempty"`
Labels bool `json:"labels,omitempty"`
}
// PrintMargins 保存纸面四边的非负页边距
type PrintMargins struct {
Top float64 `json:"top"`
Right float64 `json:"right"`
Bottom float64 `json:"bottom"`
Left float64 `json:"left"`
}
// PrintPlacement 将归一化源页面坐标映射到纸面,Transform按a、b、c、d、e、f排列
// Clip为纸面坐标裁剪;Page从零开始,同一页面可以多次放置
// Width和Height为归一化源页尺寸;Clipped表示变换后的内容超出裁剪区域
type PrintPlacement struct {
Page int `json:"page"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Transform [6]float64 `json:"transform"`
Clip Box `json:"clip"`
Clipped bool `json:"clipped"`
}
// PrintSide 保存一个输出纸面,Number是过滤前的一基纸面序号,Sheet为一基纸张编号
// Back仅在双面作业中表示背面,TileRow/TileColumn为海报的一基拼接位置
type PrintSide struct {
Number int `json:"number"`
Sheet int `json:"sheet"`
Back bool `json:"back"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Placements []PrintPlacement `json:"placements"`
TileRow int `json:"tileRow,omitempty"`
TileColumn int `json:"tileColumn,omitempty"`
TileRows int `json:"tileRows,omitempty"`
TileColumns int `json:"tileColumns,omitempty"`
Label string `json:"label,omitempty"`
LabelX float64 `json:"labelX,omitempty"`
LabelY float64 `json:"labelY,omitempty"`
}
// PrintLayout 保存独立排版结果,SourcePages为选中源页数,CopiesLimit为文档声明的份数上限
// CopiesLimit为-1时无限制;纯布局计算不绑定文档,默认同样为-1
// Sheets为筛选后的纸张数;BackContent表示正背面筛选前存在含页面内容的背面
type PrintLayout struct {
SourcePages int `json:"sourcePages"`
CopiesLimit int `json:"copiesLimit"`
Copies int `json:"copies"`
Collate bool `json:"collate"`
Duplex string `json:"duplex"`
Sheets int `json:"sheets"`
BackContent bool `json:"backContent"`
Sides []PrintSide `json:"sides"`
}
// PrintDocument 固定排版和渲染配置,可串行输出多个纸面,Close后不可再渲染
// Renderer.PreparePrint借用输入,使用结束前不得关闭或修改源输入及外部字体
// Editor.PreparePrint创建独立编辑快照,不要求保留原编辑器;外部字体仍须可读
// 各方法以互斥锁串行执行,Layout返回独立副本;取消只中断当前输出,不销毁任务
type PrintDocument struct {
mu sync.Mutex
renderer *Renderer
layout PrintLayout
options PrintOptions
closed bool
}
// PlanPrint 仅根据源页面区域和选项计算打印布局,不读取文档或调用设备
// 入参: pages 按文档顺序排列的页面区域, options 打印内容选项
// 返回: PrintLayout 有序纸面, error 尺寸、页码或选项错误
func PlanPrint(pages []Box, options PrintOptions) (PrintLayout, error) {
o, err := normalizePrintOptions(options)
if err != nil {
return PrintLayout{CopiesLimit: -1}, err
}
return planPrint(pages, o)
}
// planPrint 按已校验的选项计算纸面,供独立布局和打印任务共用
// 入参: pages 按文档顺序排列的页面区域, o 规范排版选项
// 返回: PrintLayout 有序纸面, error 尺寸、页码或排版错误
func planPrint(pages []Box, o PrintOptions) (PrintLayout, error) {
result := PrintLayout{CopiesLimit: -1}
if len(pages) == 0 {
return result, fmt.Errorf("no pages to print")
}
for _, page := range pages {
if !printPositive(page.W) || !printPositive(page.H) {
return result, fmt.Errorf("invalid print page size")
}
}
indices := o.Pages
if o.PageRange != "" {
var err error
indices, err = ParsePageRange(o.PageRange, len(pages))
if err != nil {
return result, err
}
} else if o.Pages == nil {
indices = make([]int, len(pages))
for i := range indices {
indices[i] = i
}
}
selected := make([]int, 0, len(indices))
for _, index := range indices {
if index < 0 || index >= len(pages) {
return result, fmt.Errorf("print page index %d outside document", index)
}
if o.Parity == "odd" && index%2 != 0 || o.Parity == "even" && index%2 == 0 {
continue
}
selected = append(selected, index)
}
if len(selected) == 0 {
return result, fmt.Errorf("no pages selected for printing")
}
result.SourcePages = len(selected)
result.Copies, result.Collate, result.Duplex = o.Copies, *o.Collate, o.Duplex
addSide := func(width, height float64, order []int, rows, columns int) error {
area := printPageArea(width, height, o)
cw, ch := (area.W-float64(columns-1)*o.Gap)/float64(columns), (area.H-float64(rows-1)*o.Gap)/float64(rows)
if !printPositive(cw) || !printPositive(ch) {
return fmt.Errorf("print margins or gap exceed paper size")
}
side := PrintSide{Width: width, Height: height, Placements: []PrintPlacement{}}
for cell, index := range order {
if index < 0 {
continue
}
x, y := cell%columns, cell/columns
if o.PageOrder == "vertical" || o.PageOrder == "verticalReverse" {
x, y = cell/rows, cell%rows
}
if o.PageOrder == "horizontalReverse" || o.PageOrder == "verticalReverse" {
x = columns - 1 - x
}
box := Box{X: area.X + float64(x)*(cw+o.Gap), Y: area.Y + float64(y)*(ch+o.Gap), W: cw, H: ch}
placement := printPlacement(index, pages[index], box, o)
for _, value := range placement.Transform {
if math.IsNaN(value) || math.IsInf(value, 0) {
return fmt.Errorf("invalid print placement")
}
}
side.Placements = append(side.Placements, placement)
}
if o.Mode == "poster" && o.Labels && len(order) > 0 {
side.Label = fmt.Sprintf("Page %d Row 1/1 Column 1/1", order[0]+1)
side.LabelX, side.LabelY = area.X+1, area.Y+area.H+3.5
}
result.Sides = append(result.Sides, side)
return nil
}
w, h := o.PaperWidth, o.PaperHeight
if w == 0 && o.Mode != "single" {
w, h = 210, 297
if o.Mode == "booklet" {
w, h = h, w
}
}
switch o.Mode {
case "single":
for _, index := range selected {
pw, ph := w, h
if pw == 0 {
pw, ph = pages[index].W, pages[index].H
}
if err := addSide(pw, ph, []int{index}, 1, 1); err != nil {
return PrintLayout{}, err
}
}
case "multiple":
count := o.Rows * o.Columns
for start := 0; start < len(selected); start += count {
if err := addSide(w, h, selected[start:min(start+count, len(selected))], o.Rows, o.Columns); err != nil {
return PrintLayout{}, err
}
}
case "booklet":
chunk := ((len(selected) + 3) / 4) * 4
if o.BookletSheets > 0 {
chunk = o.BookletSheets * 4
}
for start := 0; start < len(selected); start += chunk {
end := min(start+chunk, len(selected))
order := make([]int, ((end-start+3)/4)*4)
for i := range order {
order[i] = -1
}
copy(order, selected[start:end])
for i := 0; i < len(order)/4; i++ {
for _, pair := range [][]int{{order[len(order)-1-i*2], order[i*2]}, {order[i*2+1], order[len(order)-2-i*2]}} {
if o.Binding == "right" {
slices.Reverse(pair)
}
if err := addSide(w, h, pair, 1, 2); err != nil {
return PrintLayout{}, err
}
}
}
}
case "poster":
area := printPageArea(w, h, o)
if area.W <= o.Overlap || area.H <= o.Overlap {
return result, fmt.Errorf("poster overlap or margins exceed paper size")
}
for _, index := range selected {
pw, ph := pages[index].W*o.Scale, pages[index].H*o.Scale
if o.TileOnlyLarge && pw <= area.W && ph <= area.H {
if err := addSide(w, h, []int{index}, 1, 1); err != nil {
return PrintLayout{}, err
}
continue
}
columns, rows := max(1, math.Ceil((pw-o.Overlap)/(area.W-o.Overlap))), max(1, math.Ceil((ph-o.Overlap)/(area.H-o.Overlap)))
if !printPositive(columns*rows) || columns*rows > 10000 || len(result.Sides)+int(columns*rows) > 10000 {
return result, fmt.Errorf("too many poster sides")
}
for row := 0; row < int(rows); row++ {
for column := 0; column < int(columns); column++ {
m := [6]float64{o.Scale, 0, 0, o.Scale, area.X - float64(column)*(area.W-o.Overlap), area.Y - float64(row)*(area.H-o.Overlap)}
side := PrintSide{Width: w, Height: h, TileRow: row + 1, TileColumn: column + 1, TileRows: int(rows), TileColumns: int(columns), Placements: []PrintPlacement{{Page: index, Width: pages[index].W, Height: pages[index].H, Transform: m, Clip: area}}}
if o.Labels {
side.Label = fmt.Sprintf("Page %d Row %d/%d Column %d/%d", index+1, row+1, int(rows), column+1, int(columns))
side.LabelX, side.LabelY = area.X+1, area.Y+area.H+3.5
}
result.Sides = append(result.Sides, side)
}
}
}
}
if o.Duplex != "simplex" {
paired := make([]PrintSide, 0, len(result.Sides)+1)
for _, side := range result.Sides {
if len(paired)%2 == 1 {
previous := paired[len(paired)-1]
if previous.Width != side.Width || previous.Height != side.Height {
paired = append(paired, PrintSide{Width: previous.Width, Height: previous.Height, Placements: []PrintPlacement{}})
}
}
paired = append(paired, side)
}
if len(paired)%2 == 1 {
previous := paired[len(paired)-1]
paired = append(paired, PrintSide{Width: previous.Width, Height: previous.Height, Placements: []PrintPlacement{}})
}
result.Sides = paired
}
for i := range result.Sides {
result.Sides[i].Number = i + 1
result.Sides[i].Sheet = i + 1
if o.Duplex != "simplex" {
result.Sides[i].Sheet = i/2 + 1
result.Sides[i].Back = i%2 == 1
}
if o.BackRotate && result.Sides[i].Back {
for j := range result.Sides[i].Placements {
placement := &result.Sides[i].Placements[j]
m := placement.Transform
placement.Transform = [6]float64{-m[0], -m[1], -m[2], -m[3], result.Sides[i].Width - m[4], result.Sides[i].Height - m[5]}
placement.Clip.X = result.Sides[i].Width - placement.Clip.X - placement.Clip.W
placement.Clip.Y = result.Sides[i].Height - placement.Clip.Y - placement.Clip.H
area := printPageArea(result.Sides[i].Width, result.Sides[i].Height, o)
x, y := math.Max(area.X, placement.Clip.X), math.Max(area.Y, placement.Clip.Y)
right, bottom := math.Min(area.X+area.W, placement.Clip.X+placement.Clip.W), math.Min(area.Y+area.H, placement.Clip.Y+placement.Clip.H)
if right-x < placement.Clip.W-1e-6 || bottom-y < placement.Clip.H-1e-6 {
placement.Clipped = true
}
placement.Clip = Box{X: x, Y: y, W: math.Max(0, right-x), H: math.Max(0, bottom-y)}
}
}
}
lastSheet := result.Sides[len(result.Sides)-1].Sheet
if o.SheetFrom > lastSheet || o.SheetTo > lastSheet {
return result, fmt.Errorf("print sheet outside layout")
}
for _, side := range result.Sides {
if side.Back && len(side.Placements) > 0 && (o.SheetFrom == 0 || side.Sheet >= o.SheetFrom) && (o.SheetTo == 0 || side.Sheet <= o.SheetTo) {
result.BackContent = true
break
}
}
if o.Sides != "all" || o.SheetFrom != 0 || o.SheetTo != 0 {
filtered := make([]PrintSide, 0, len(result.Sides))
for _, side := range result.Sides {
if o.SheetFrom > 0 && side.Sheet < o.SheetFrom || o.SheetTo > 0 && side.Sheet > o.SheetTo {
continue
}
if o.Sides == "all" || o.Sides == "front" && !side.Back || o.Sides == "back" && side.Back {
filtered = append(filtered, side)
}
}
result.Sides = filtered
}
if len(result.Sides) == 0 {
return result, fmt.Errorf("no print sides selected")
}
if o.Reverse {
slices.Reverse(result.Sides)
for i := 0; i+1 < len(result.Sides); i++ {
if result.Sides[i].Sheet == result.Sides[i+1].Sheet {
result.Sides[i], result.Sides[i+1] = result.Sides[i+1], result.Sides[i]
i++
}
}
}
previous := 0
for _, side := range result.Sides {
if side.Sheet != previous {
result.Sheets++
previous = side.Sheet
}
}
return result, nil
}
// normalizePrintOptions 校验有限尺寸并填充公共默认值
// 入参: o 原始排版选项
// 返回: PrintOptions 独立规范选项, error 配置错误
func normalizePrintOptions(o PrintOptions) (PrintOptions, error) {
if o.Mode == "" {
o.Mode = "single"
}
if o.Scaling == "" {
o.Scaling = "shrink"
}
if o.Parity == "" {
o.Parity = "all"
}
if o.Sides == "" {
o.Sides = "all"
}
if o.Binding == "" {
o.Binding = "left"
}
if o.Duplex == "" {
o.Duplex = "simplex"
if o.Mode == "booklet" {
o.Duplex = "shortEdge"
}
}
if o.Copies == 0 {
o.Copies = 1
}
if o.Collate == nil {
collate := true
o.Collate = &collate
} else {
collate := *o.Collate
o.Collate = &collate
}
if o.PageOrder == "" {
o.PageOrder = "horizontal"
if o.ColumnOrder {
o.PageOrder = "vertical"
}
}
if o.Contents == "" {
o.Contents = "documentAndAnnotations"
if o.Annotations != nil && !*o.Annotations {
o.Contents = "document"
}
}
o.ColumnOrder, o.Annotations = false, nil
if !slices.Contains([]string{"document", "documentAndAnnotations", "annotations", "forms"}, o.Contents) {
return o, fmt.Errorf("invalid print contents")
}
if o.Scale == 0 {
o.Scale = 1
}
if o.DPI == 0 {
o.DPI = 300
}
if o.Rows == 0 {
o.Rows = 1
}
if o.Columns == 0 {
o.Columns = 2
}
if !slices.Contains([]string{"single", "multiple", "booklet", "poster"}, o.Mode) || !slices.Contains([]string{"fit", "shrink", "actual", "custom"}, o.Scaling) || !slices.Contains([]string{"all", "odd", "even"}, o.Parity) || !slices.Contains([]string{"all", "front", "back"}, o.Sides) || !slices.Contains([]string{"left", "right"}, o.Binding) || !slices.Contains([]string{"simplex", "longEdge", "shortEdge"}, o.Duplex) || !slices.Contains([]string{"horizontal", "horizontalReverse", "vertical", "verticalReverse"}, o.PageOrder) {
return o, fmt.Errorf("invalid print mode")
}
if o.Copies < 1 || o.Copies > 999 || o.SheetFrom < 0 || o.SheetTo < 0 || o.SheetTo > 0 && o.SheetTo < o.SheetFrom || o.Duplex == "simplex" && (o.Mode == "booklet" || o.Sides != "all") {
return o, fmt.Errorf("invalid print job options")
}
if o.PrintableArea != nil {
area := *o.PrintableArea
if !printPositive(area.W) || !printPositive(area.H) || !printPositive(area.X+1) || !printPositive(area.Y+1) || area.X < 0 || area.Y < 0 || o.PaperWidth <= 0 || area.X+area.W > o.PaperWidth+1e-6 || area.Y+area.H > o.PaperHeight+1e-6 {
return o, fmt.Errorf("invalid printer printable area")
}
o.PrintableArea = &area
}
for _, v := range []float64{o.PaperWidth, o.PaperHeight, o.Margins.Top, o.Margins.Right, o.Margins.Bottom, o.Margins.Left, o.Gap, o.Overlap} {
if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
return o, fmt.Errorf("invalid print dimensions")
}
}
if (o.PaperWidth == 0) != (o.PaperHeight == 0) || !printPositive(o.Scale) || o.Scale > 100 || !printPositive(o.DPI) || o.DPI > 2400 || o.Rows < 1 || o.Columns < 1 || o.Rows > 16 || o.Columns > 16 || o.BookletSheets < 0 || o.BookletSheets > 2500 || o.Pages != nil && o.PageRange != "" {
return o, fmt.Errorf("invalid print options")
}
o.Pages = slices.Clone(o.Pages)
return o, nil
}
// printPositive 检查正有限数
// 入参: v 待校验数值
// 返回: bool 是否为正有限数
func printPositive(v float64) bool {
return v > 0 && !math.IsInf(v, 0) && !math.IsNaN(v)
}
// printPageArea 在设备可打印区域内应用额外留白,未知设备时使用整张纸
// 入参: w 纸面宽度, h 纸面高度, o 排版选项
// 返回: Box 应用额外边距后的毫米区域
func printPageArea(w, h float64, o PrintOptions) Box {
area := Box{W: w, H: h}
if o.PrintableArea != nil {
area = *o.PrintableArea
}
area.X += o.Margins.Left
area.Y += o.Margins.Top
area.W -= o.Margins.Left + o.Margins.Right
area.H -= o.Margins.Top + o.Margins.Bottom
if o.Mode == "poster" && o.Labels {
area.H -= 5
}
return area
}
// printPlacement 计算居中及自动旋转后的单元变换
// 入参: index 零基源页码, source 源页面区域, cell 纸面单元区域, o 排版选项
// 返回: PrintPlacement 页面放置及裁剪信息
func printPlacement(index int, source, cell Box, o PrintOptions) PrintPlacement {
w, h := source.W, source.H
rotated := o.Rotate && math.Min(cell.W/h, cell.H/w) > math.Min(cell.W/w, cell.H/h)
if rotated {
w, h = h, w
}
scale := 1.0
switch o.Scaling {
case "fit":
scale = math.Min(cell.W/w, cell.H/h)
case "shrink":
scale = math.Min(1, math.Min(cell.W/w, cell.H/h))
case "custom":
scale = o.Scale
}
x, y := cell.X+(cell.W-w*scale)/2, cell.Y+(cell.H-h*scale)/2
m := [6]float64{scale, 0, 0, scale, x, y}
if rotated {
m = [6]float64{0, scale, -scale, 0, x + source.H*scale, y}
}
clipped := w*scale > cell.W+1e-6 || h*scale > cell.H+1e-6
return PrintPlacement{Page: index, Width: source.W, Height: source.H, Transform: m, Clip: cell, Clipped: clipped}
}
// PreparePrint 固定当前阅读入口、版本及配置,底层输入由调用方保持存活
// 入参: ctx 准备上下文, options 内容排版选项
// 返回: *PrintDocument 独立打印任务, error 权限、布局或读取错误
func (r *Renderer) PreparePrint(ctx context.Context, options PrintOptions) (*PrintDocument, error) {
if ctx == nil {
return nil, fmt.Errorf("nil print context")
}
if r == nil || r.Reader == nil {
return nil, fmt.Errorf("missing print renderer")
}
o, err := normalizePrintOptions(options)
if err != nil {
return nil, err
}
view, err := r.Fork(ctx)
if err != nil {
return nil, err
}
doc, err := view.Reader.DocContext(ctx)
if err != nil {
return nil, err
}
if !doc.Permissions.Print || doc.Permissions.Copies == 0 {
return nil, ErrPrintDenied
}
if doc.Permissions.Copies > 0 && o.Copies > doc.Permissions.Copies {
return nil, ErrPrintDenied
}
boxes := make([]Box, len(doc.Pages.Page))
for i, page := range doc.Pages.Page {
if err := ctx.Err(); err != nil {
return nil, err
}
area, err := view.Reader.readPageHeaderContext(ctx, page)
if err != nil {
return nil, err
}
boxes[i], err = view.GetPageBox(&area)
if err != nil {
return nil, err
}
}
layout, err := planPrint(boxes, o)
if err != nil {
return nil, err
}
layout.CopiesLimit = doc.Permissions.Copies
view.printing = true
view.printContents = o.Contents
view.RenderAnnotations = true
return &PrintDocument{renderer: view, layout: layout, options: o}, nil
}
// PreparePrint 从独立编辑快照创建打印任务,不修改源编辑器或撤销记录
// 入参: ctx 准备上下文, options 排版选项, rendererOptions 字体及后端配置
// 返回: *PrintDocument 打印任务, error 快照或准备错误
func (e *Editor) PreparePrint(ctx context.Context, options PrintOptions, rendererOptions ...RendererOption) (*PrintDocument, error) {
if ctx == nil || e == nil {
return nil, fmt.Errorf("missing print editor or context")
}
snapshot, err := e.Snapshot(ctx)
if err != nil {
return nil, err
}
reader, err := snapshot.Reader(ctx)
if err != nil {
return nil, err
}
defer reader.Close()
renderer := NewRenderer(reader, rendererOptions...)
return renderer.PreparePrint(ctx, options)
}
// Layout 返回可供第三方预览和检查的独立布局副本
// 返回: PrintLayout 独立布局副本
func (p *PrintDocument) Layout() PrintLayout {
p.mu.Lock()
defer p.mu.Unlock()
result := p.layout
result.Sides = slices.Clone(result.Sides)
for i := range result.Sides {
result.Sides[i].Placements = slices.Clone(result.Sides[i].Placements)
}
return result
}
// Reconfigure 在相同文档快照上创建新的打印任务,不改变原任务的布局
// 入参: ctx 准备上下文, options 新排版选项
// 返回: *PrintDocument 独立打印任务, error 权限、布局或读取错误
func (p *PrintDocument) Reconfigure(ctx context.Context, options PrintOptions) (*PrintDocument, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil, fmt.Errorf("print document is closed")
}
return p.renderer.PreparePrint(ctx, options)
}
// Close 释放任务持有的派生缓存和快照,不关闭调用方借入的输入
// 返回: error 资源释放错误
func (p *PrintDocument) Close() error {
if p == nil {
return nil
}
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
p.renderer.ClearCache()
err := p.renderer.Reader.Close()
p.renderer = nil
return err
}