feat(打印能力): 新增打印并优化界面布局
build.yaml / build (push) Successful in 46s

This commit is contained in:
xiaoqidun committed 2026-10-24 20:53:46 +08:00
1 parent 799041171b
commit 0ae9ad6562
22 files changed
+2220 -21

No files matched your search

+655
View File
@@ -0,0 +1,655 @@
// 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
}