mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:31:12 +08:00
656 lines
25 KiB
Go
656 lines
25 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 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
|
||
}
|