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

+8
View File
@@ -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):
+1
View File
@@ -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
+141
View File
@@ -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
}
+1
View File
@@ -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{}