mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
799041171b
commit
0ae9ad6562
22 files changed
+2220
-21
No files matched your search
@@ -0,0 +1,375 @@
|
||||
// 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
|
||||
}
|
||||
@@ -423,6 +423,7 @@ type businessEngine struct {
|
||||
currentSession *Session
|
||||
currentEditor *ofdgo.Editor
|
||||
saveSnapshot *ofdgo.Editor
|
||||
printDocument *ofdgo.PrintDocument
|
||||
pendingImport *ofdgo.Reader
|
||||
copiedObjects *editorClipboard
|
||||
copiedStyle *ofdgo.GraphicObject
|
||||
@@ -7445,6 +7446,11 @@ func (c *outputContext) Err() error {
|
||||
// businessCommands 共用业务命令表
|
||||
func (e *businessEngine) businessCommands() map[string]func([]platformValue) (any, error) {
|
||||
return map[string]func([]platformValue) (any, error){
|
||||
"ofdgoPreparePrint": e.preparePrint,
|
||||
"ofdgoLayoutPrint": e.layoutPrint,
|
||||
"ofdgoPrintSide": e.printSide,
|
||||
"ofdgoPrintPDF": e.printPDF,
|
||||
"ofdgoDiscardPrint": e.discardPrint,
|
||||
"ofdgoDetectFormat": e.detectDocumentFormat,
|
||||
"ofdgoOpen": e.openDocument,
|
||||
"ofdgoSelectDocument": e.selectDocument,
|
||||
@@ -7594,6 +7600,8 @@ func (e *businessEngine) operationContext() context.Context {
|
||||
func failureResult(err error) apiResult {
|
||||
result := apiResult{Error: err.Error()}
|
||||
switch {
|
||||
case errors.Is(err, ofdgo.ErrPrintDenied):
|
||||
result.Code, result.Error = "printDenied", "文档禁止打印"
|
||||
case errors.Is(err, ofdgo.ErrPDFPassword):
|
||||
result.Code, result.Error = "pdfPassword", "PDF密码缺失或错误"
|
||||
case errors.Is(err, ofdgo.ErrCredentialsRequired):
|
||||
|
||||
@@ -357,6 +357,7 @@ func (e *NativeEngine) Close() error {
|
||||
e.gate <- struct{}{}
|
||||
defer func() { <-e.gate }()
|
||||
e.closed = true
|
||||
_, _ = e.discardPrint(nil)
|
||||
_ = e.waitRenders(context.Background())
|
||||
e.renderIdle = nil
|
||||
e.saveSnapshot = nil
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 webui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/xiaoqidun/ofdgo"
|
||||
)
|
||||
|
||||
// preparePrint 从当前编辑入口创建独立打印快照,业务选项由公开库统一校验
|
||||
// 入参: args 排版选项及可选输出任务标识
|
||||
// 返回: any 纸面布局, error 快照、权限或排版错误
|
||||
func (e *businessEngine) preparePrint(args []platformValue) (any, error) {
|
||||
if e.currentSession == nil || len(args) == 0 {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
var options ofdgo.PrintOptions
|
||||
if err := json.Unmarshal([]byte(platformJSON(args[0])), &options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := e.operationContext()
|
||||
if len(args) > 1 {
|
||||
ctx = e.newOutputContext(args[1])
|
||||
}
|
||||
editor := e.currentEditor
|
||||
if editor == nil {
|
||||
var err error
|
||||
editor, err = e.currentSession.Reader.EditorContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
source := e.currentSession.Renderer
|
||||
dirs, fonts := source.FontSources()
|
||||
document, err := editor.PreparePrint(ctx, options, ofdgo.WithRenderBackends(source.Backends()), ofdgo.WithFontDirs(dirs...), ofdgo.WithFontFS(fonts...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.printDocument != nil {
|
||||
_ = e.printDocument.Close()
|
||||
}
|
||||
e.printDocument = document
|
||||
data, err := json.Marshal(document.Layout())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return successResult(platformParseJSON(data)), nil
|
||||
}
|
||||
|
||||
// layoutPrint 在既有快照上调整排版,预览期间不重复复制源包
|
||||
// 入参: args 新排版选项及可选输出任务标识
|
||||
// 返回: any 新纸面布局, error 状态、权限或排版错误
|
||||
func (e *businessEngine) layoutPrint(args []platformValue) (any, error) {
|
||||
if e.printDocument == nil || len(args) == 0 {
|
||||
return nil, fmt.Errorf("print document is not prepared")
|
||||
}
|
||||
var options ofdgo.PrintOptions
|
||||
if err := json.Unmarshal([]byte(platformJSON(args[0])), &options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := e.operationContext()
|
||||
if len(args) > 1 {
|
||||
ctx = e.newOutputContext(args[1])
|
||||
}
|
||||
next, err := e.printDocument.Reconfigure(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = e.printDocument.Close()
|
||||
e.printDocument = next
|
||||
data, err := json.Marshal(next.Layout())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return successResult(platformParseJSON(data)), nil
|
||||
}
|
||||
|
||||
// printSide 按需输出一个纸面,不使用阅读区缓存或缩略图
|
||||
// 入参: args 零基纸面索引及可选输出任务标识
|
||||
// 返回: any 内嵌资源的SVG纸面, error 状态或渲染错误
|
||||
func (e *businessEngine) printSide(args []platformValue) (any, error) {
|
||||
if e.printDocument == nil || len(args) == 0 {
|
||||
return nil, fmt.Errorf("print document is not prepared")
|
||||
}
|
||||
ctx := e.operationContext()
|
||||
if len(args) > 1 {
|
||||
ctx = e.newOutputContext(args[1])
|
||||
}
|
||||
var output bytes.Buffer
|
||||
if err := e.printDocument.RenderSVG(ctx, args[0].Int(), &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return successResult(map[string]any{"svg": output.String()}), nil
|
||||
}
|
||||
|
||||
// printPDF 输出和预览相同的打印布局,便于另存或交给第三方打印系统
|
||||
// 入参: args 可选输出任务标识
|
||||
// 返回: any PDF数据及媒体类型, error 状态或输出错误
|
||||
func (e *businessEngine) printPDF(args []platformValue) (any, error) {
|
||||
if e.printDocument == nil {
|
||||
return nil, fmt.Errorf("print document is not prepared")
|
||||
}
|
||||
ctx := e.operationContext()
|
||||
if len(args) > 0 {
|
||||
ctx = e.newOutputContext(args[0])
|
||||
}
|
||||
var output bytes.Buffer
|
||||
if err := e.printDocument.WritePDF(ctx, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return successResult(map[string]any{"bytes": bytesToJS(output.Bytes()), "mime": "application/pdf"}), nil
|
||||
}
|
||||
|
||||
// discardPrint 释放当前打印快照,可重复调用
|
||||
// 入参: _ 保留的调用参数
|
||||
// 返回: any 空结果, error 资源释放错误
|
||||
func (e *businessEngine) discardPrint(_ []platformValue) (any, error) {
|
||||
if e.printDocument != nil {
|
||||
err := e.printDocument.Close()
|
||||
e.printDocument = nil
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return successResult(nil), nil
|
||||
}
|
||||
@@ -99,6 +99,7 @@ func (w *wasmEngines) close(args []js.Value) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
delete(w.engines, id)
|
||||
_, _ = e.discardPrint(nil)
|
||||
e.clearImport()
|
||||
err := e.currentSession.Close()
|
||||
*e = businessEngine{}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -106,7 +107,7 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
|
||||
h.conversionPool = h.resources.ConversionPool()
|
||||
defer h.close()
|
||||
bindings := make(map[string]guigo.HandlerFunc)
|
||||
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close", "update.check", "update.open"} {
|
||||
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close", "update.check", "update.open", "print.capabilities", "print.dialog", "print.printers", "print.query", "print.properties", "print"} {
|
||||
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
|
||||
}
|
||||
static := http.FileServerFS(webuiassets.NativeFS)
|
||||
@@ -305,6 +306,44 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
||||
text := func(key string) string { value, _ := args[key].(string); return value }
|
||||
flag := func(key string) bool { value, _ := args[key].(bool); return value }
|
||||
switch name {
|
||||
case "print.capabilities", "print.dialog", "print.printers", "print.query", "print.properties", "print":
|
||||
h.mu.Lock()
|
||||
window := h.window
|
||||
h.mu.Unlock()
|
||||
if window == nil {
|
||||
return nil, fmt.Errorf("窗口尚未就绪")
|
||||
}
|
||||
if name == "print.capabilities" {
|
||||
return window.PrintCapabilities(ctx)
|
||||
}
|
||||
if name == "print.dialog" {
|
||||
return nil, window.ShowPrintUI(ctx, text("kind"))
|
||||
}
|
||||
if name == "print.printers" {
|
||||
return window.ListPrinters(ctx)
|
||||
}
|
||||
if name == "print.properties" {
|
||||
return nil, window.ShowPrinterProperties(ctx, text("printer"))
|
||||
}
|
||||
data, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var options guigo.PrintOptions
|
||||
if err := json.Unmarshal(data, &options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile, err := window.QueryPrinter(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "print.query" {
|
||||
return profile, nil
|
||||
}
|
||||
if text("fingerprint") == "" || text("fingerprint") != profile.Fingerprint {
|
||||
return nil, fmt.Errorf("打印机设置已变化,请重新预览")
|
||||
}
|
||||
return window.Print(ctx, options)
|
||||
case "engine.open":
|
||||
engine := webui.NewNativeEngine(h.files.resolve)
|
||||
parallelism, cacheBytes := runtime.GOMAXPROCS(0), int64(128<<20)
|
||||
|
||||
Reference in new issue
Block a user