mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
916 lines
26 KiB
Go
916 lines
26 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.
|
||
|
||
//go:build !js || !wasm
|
||
|
||
package webui
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"reflect"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/xiaoqidun/ofdgo"
|
||
)
|
||
|
||
const (
|
||
platformTypeUndefined = iota
|
||
platformTypeNumber
|
||
platformTypeString
|
||
platformTypeBoolean
|
||
platformTypeObject
|
||
platformTypeFunction
|
||
)
|
||
|
||
// NativeResolver 将授权标识解析为随机读取源,生命周期由宿主管理
|
||
// 入参: token 资源授权标识
|
||
// 返回: io.ReaderAt 随机读取器, int64 资源大小, error 解析错误
|
||
type NativeResolver func(token string) (io.ReaderAt, int64, error)
|
||
|
||
// NativeOutputRequest 请求为整次批量转换预留唯一输出入口
|
||
type NativeOutputRequest struct {
|
||
Destination any
|
||
Base, Extension, MIME string
|
||
Paged bool
|
||
}
|
||
|
||
// NativeOutputTarget 保存预留目标,Name为根目录下可整体删除的入口名
|
||
// Destination交给OpenWriter,分页写入名为Base/页文件名,单文件写入名为Base.Extension
|
||
type NativeOutputTarget struct {
|
||
Destination any
|
||
Base, Name string
|
||
}
|
||
|
||
// NativeEngine 保存独立会话,不同实例可并行,同一实例的回调不可重入Call或Close
|
||
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
|
||
type NativeEngine struct {
|
||
businessEngine
|
||
// Resolve 按授权标识提供资源读取源
|
||
Resolve NativeResolver
|
||
// LoadFont 按名称读取系统字体数据
|
||
LoadFont func(context.Context, string) ([]byte, error)
|
||
// OpenWriter 必须防止覆盖已有文件,成功时Close提交,失败时仅调用可选Abort
|
||
// 不支持Abort的输出由宿主负责回收,宿主也负责回滚已完成的批量输出
|
||
OpenWriter func(context.Context, any, string) (io.WriteCloser, error)
|
||
// ReserveOutput 为单次批量预留整体入口,返回错误时由宿主释放未交付的预留
|
||
ReserveOutput func(context.Context, NativeOutputRequest) (NativeOutputTarget, error)
|
||
// CleanupOutput 必须幂等,以兼容前端收到output事件后重复removeEntry
|
||
CleanupOutput func(context.Context, NativeOutputTarget) error
|
||
gate chan struct{}
|
||
once sync.Once
|
||
closed bool
|
||
}
|
||
|
||
// nativeReader 在会话当前操作上下文中分块读取,不捕获已结束请求的上下文
|
||
type nativeReader struct {
|
||
engine *NativeEngine
|
||
source io.ReaderAt
|
||
}
|
||
|
||
// nativeCall 保存单次worker适配状态
|
||
type nativeCall struct {
|
||
engine *NativeEngine
|
||
ctx context.Context
|
||
emit func(any)
|
||
output io.WriteCloser
|
||
buffer bytes.Buffer
|
||
destination any
|
||
files []any
|
||
currentName string
|
||
mime string
|
||
size int64
|
||
exporting bool
|
||
batch bool
|
||
base string
|
||
extension string
|
||
paged bool
|
||
reserved *NativeOutputTarget
|
||
committed bool
|
||
}
|
||
|
||
// platformValue 仅承载JSON参数、字节与宿主回调,不执行脚本
|
||
type platformValue struct{ value any }
|
||
|
||
// nativeCallback 同步宿主回调
|
||
type nativeCallback func(...any) (any, error)
|
||
|
||
// NewNativeEngine 创建独立业务会话,resolver不接收未经授权的文件路径
|
||
func NewNativeEngine(resolve NativeResolver) *NativeEngine { return &NativeEngine{Resolve: resolve} }
|
||
|
||
// Call 执行worker输入协议,返回ok/data/error,字节保持[]byte,emit不含请求id
|
||
// 入参: ctx 取消上下文, name 原操作名, args worker参数, emit 同步进度通知
|
||
// 返回: map[string]any 操作结果,宿主负责将二进制转换为资源句柄
|
||
// nativeFile只接受授权标识,size以Resolve结果为准,不接受路径字符串或nativeData输出标识
|
||
// 输出目标为nil时导出返回data.blob,批量返回data.files中的name、mime及file字节
|
||
// 批量输出到非nil目标时必须同时提供ReserveOutput和CleanupOutput,避免误清理既有文件
|
||
// 取消为协作式检查,不能抢占宿主阻塞的ReadAt、字体读取或加密库内部计算
|
||
func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit func(any)) (result map[string]any) {
|
||
e.once.Do(func() { e.gate = make(chan struct{}, 1); e.resolveSource = e.source })
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
select {
|
||
case e.gate <- struct{}{}:
|
||
case <-ctx.Done():
|
||
return nativeFailure(ctx.Err())
|
||
}
|
||
defer func() { <-e.gate }()
|
||
if err := ctx.Err(); err != nil {
|
||
return nativeFailure(err)
|
||
}
|
||
if e.closed {
|
||
return nativeFailure(fmt.Errorf("native engine is closed"))
|
||
}
|
||
e.ctx = ctx
|
||
if e.currentSession != nil {
|
||
e.currentSession.Renderer.OutputContext = ctx
|
||
}
|
||
defer func() {
|
||
if e.currentSession != nil {
|
||
e.currentSession.Renderer.OutputContext = context.Background()
|
||
}
|
||
e.ctx = nil
|
||
if failure := recover(); failure != nil {
|
||
if err, ok := failure.(error); ok {
|
||
result = nativeFailure(err)
|
||
} else {
|
||
result = nativeFailure(fmt.Errorf("%v", failure))
|
||
}
|
||
}
|
||
}()
|
||
fn := e.businessCommands()[name]
|
||
if fn == nil {
|
||
return nativeFailure(fmt.Errorf("unknown operation: %s", name))
|
||
}
|
||
call := &nativeCall{engine: e, ctx: ctx, emit: emit}
|
||
defer func() {
|
||
if err := call.abort(); err != nil {
|
||
if result == nil || result["ok"] == true {
|
||
result = nativeFailure(err)
|
||
} else {
|
||
result["error"] = fmt.Sprintf("%v; cleanup: %v", result["error"], err)
|
||
}
|
||
}
|
||
}()
|
||
prepared, err := call.prepare(name, append([]any(nil), args...))
|
||
if err != nil {
|
||
return nativeFailure(err)
|
||
}
|
||
values := make([]platformValue, len(prepared))
|
||
for i, arg := range prepared {
|
||
values[i] = platformValueOf(arg)
|
||
}
|
||
data, err := e.safeCall(fn, values)
|
||
if err != nil {
|
||
return nativeFailure(err)
|
||
}
|
||
if err := ctx.Err(); err != nil {
|
||
return nativeFailure(err)
|
||
}
|
||
if wrapped, ok := data.(platformValue); ok {
|
||
result = nativePlain(wrapped).(map[string]any)
|
||
} else {
|
||
result = map[string]any{"ok": true, "data": nativePlain(data)}
|
||
}
|
||
if err := call.finish(name, result); err != nil {
|
||
return nativeFailure(err)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// Close 释放会话,不关闭宿主持有的ReaderAt资源
|
||
func (e *NativeEngine) Close() error {
|
||
e.once.Do(func() { e.gate = make(chan struct{}, 1); e.resolveSource = e.source })
|
||
e.gate <- struct{}{}
|
||
defer func() { <-e.gate }()
|
||
e.closed = true
|
||
var err error
|
||
if e.currentSession != nil {
|
||
err = e.currentSession.Close()
|
||
}
|
||
if e.pendingImport != nil {
|
||
err = errors.Join(err, e.pendingImport.Close())
|
||
}
|
||
e.currentSession, e.currentEditor, e.pendingImport, e.copiedObjects, e.copiedStyle = nil, nil, nil, nil, nil
|
||
return err
|
||
}
|
||
|
||
// source 读取授权源并以宿主报告的大小为准
|
||
func (e *NativeEngine) source(ctx context.Context, value any) (io.ReaderAt, int64, error) {
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if p, ok := value.(platformValue); ok {
|
||
value = p.value
|
||
}
|
||
if data, ok := value.([]byte); ok {
|
||
return nativeReader{engine: e, source: bytes.NewReader(data)}, int64(len(data)), nil
|
||
}
|
||
descriptor, ok := value.(map[string]any)
|
||
if !ok {
|
||
return nil, 0, fmt.Errorf("expected bytes or nativeFile descriptor")
|
||
}
|
||
token, ok := descriptor["nativeFile"].(string)
|
||
if !ok || token == "" || e.Resolve == nil {
|
||
return nil, 0, fmt.Errorf("unauthorized native file")
|
||
}
|
||
source, size, err := e.Resolve(token)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if source == nil || size < 0 {
|
||
return nil, 0, fmt.Errorf("invalid native file source")
|
||
}
|
||
return nativeReader{engine: e, source: source}, size, nil
|
||
}
|
||
|
||
// ReadAt 分块传递取消检查,宿主ReadAt自身阻塞时由宿主控制超时
|
||
func (r nativeReader) ReadAt(data []byte, offset int64) (int, error) {
|
||
total := 0
|
||
for len(data) > 0 {
|
||
if err := r.engine.operationContext().Err(); err != nil {
|
||
return total, err
|
||
}
|
||
n := min(len(data), 256<<10)
|
||
got, err := r.source.ReadAt(data[:n], offset)
|
||
total += got
|
||
if err != nil {
|
||
return total, err
|
||
}
|
||
if got != n {
|
||
return total, io.ErrUnexpectedEOF
|
||
}
|
||
data, offset = data[n:], offset+int64(n)
|
||
}
|
||
return total, nil
|
||
}
|
||
|
||
// readBytes 分块读取完整资源并检查取消状态
|
||
// 入参: ctx 调用上下文, value 资源参数
|
||
// 返回: []byte 资源数据, error 读取错误
|
||
func (e *NativeEngine) readBytes(ctx context.Context, value any) ([]byte, error) {
|
||
source, size, err := e.source(ctx, value)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if size > int64(int(^uint(0)>>1)) {
|
||
return nil, fmt.Errorf("resource exceeds address space")
|
||
}
|
||
output := make([]byte, int(size))
|
||
for offset := int64(0); offset < size; {
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
n := int(min(int64(256<<10), size-offset))
|
||
got, err := source.ReadAt(output[int(offset):int(offset)+n], offset)
|
||
if got != n {
|
||
return nil, io.ErrUnexpectedEOF
|
||
}
|
||
if err != nil && err != io.EOF {
|
||
return nil, err
|
||
}
|
||
offset += int64(got)
|
||
}
|
||
return output, nil
|
||
}
|
||
|
||
// nativePlain 保留字节并转换JSON结构
|
||
func nativePlain(value any) any {
|
||
switch v := value.(type) {
|
||
case platformValue:
|
||
return nativePlain(v.value)
|
||
case []byte:
|
||
return v
|
||
case map[string]any:
|
||
result := make(map[string]any, len(v))
|
||
for k, item := range v {
|
||
result[k] = nativePlain(item)
|
||
}
|
||
return result
|
||
case nil:
|
||
return nil
|
||
}
|
||
v := reflect.ValueOf(value)
|
||
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
|
||
result := make([]any, v.Len())
|
||
for i := range result {
|
||
result[i] = nativePlain(v.Index(i).Interface())
|
||
}
|
||
return result
|
||
}
|
||
if v.Kind() == reflect.Pointer {
|
||
if v.IsNil() {
|
||
return nil
|
||
}
|
||
return nativePlain(v.Elem().Interface())
|
||
}
|
||
if v.Kind() == reflect.Struct {
|
||
data, err := json.Marshal(value)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
var result any
|
||
if err := json.Unmarshal(data, &result); err != nil {
|
||
panic(err)
|
||
}
|
||
if fields, ok := result.(map[string]any); ok {
|
||
for i := 0; i < v.NumField(); i++ {
|
||
field := v.Type().Field(i)
|
||
if !field.IsExported() {
|
||
continue
|
||
}
|
||
name := strings.Split(field.Tag.Get("json"), ",")[0]
|
||
if name == "-" {
|
||
continue
|
||
}
|
||
if field.Anonymous && name == "" {
|
||
if embedded, ok := nativePlain(v.Field(i).Interface()).(map[string]any); ok {
|
||
for key, value := range embedded {
|
||
if _, exists := fields[key]; exists {
|
||
fields[key] = value
|
||
}
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
if name == "" {
|
||
name = field.Name
|
||
}
|
||
if _, exists := fields[name]; exists {
|
||
fields[name] = nativePlain(v.Field(i).Interface())
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
return value
|
||
}
|
||
|
||
// nativeFailure 转换结构化业务错误
|
||
func nativeFailure(err error) map[string]any {
|
||
result := nativePlain(failureResult(err)).(map[string]any)
|
||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||
result["canceled"] = true
|
||
}
|
||
return result
|
||
}
|
||
|
||
// event 发送标准worker进度
|
||
func (c *nativeCall) event(value map[string]any) {
|
||
if c.emit != nil {
|
||
c.emit(value)
|
||
}
|
||
}
|
||
|
||
// checkpoint 检查操作取消
|
||
func (c *nativeCall) checkpoint(...any) (any, error) { return nil, c.ctx.Err() }
|
||
|
||
// progress 创建标准进度回调
|
||
func (c *nativeCall) progress(kind, stage string, phased bool) nativeCallback {
|
||
return func(args ...any) (any, error) {
|
||
if err := c.ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
event := map[string]any{"type": kind}
|
||
if stage != "" {
|
||
event["stage"] = stage
|
||
}
|
||
if phased {
|
||
event["phase"], event["completed"], event["total"] = args[0], args[1], args[2]
|
||
} else {
|
||
event["completed"], event["total"] = args[0], args[1]
|
||
}
|
||
c.event(event)
|
||
return nil, c.ctx.Err()
|
||
}
|
||
}
|
||
|
||
// readCallback 创建有界随机读取回调
|
||
func (c *nativeCall) readCallback(value any) (nativeCallback, int64, error) {
|
||
source, size, err := c.engine.source(c.ctx, value)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return func(args ...any) (any, error) {
|
||
if err := c.ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
offset, n := int64(platformValueOf(args[0]).Float()), platformValueOf(args[1]).Int()
|
||
if offset < 0 || n < 0 || n > 256<<10 || offset > size || int64(n) > size-offset {
|
||
return nil, fmt.Errorf("invalid native read range")
|
||
}
|
||
data := make([]byte, n)
|
||
got, err := source.ReadAt(data, offset)
|
||
if got != n {
|
||
return nil, io.ErrUnexpectedEOF
|
||
}
|
||
if err != nil && err != io.EOF {
|
||
return nil, err
|
||
}
|
||
return data, nil
|
||
}, size, nil
|
||
}
|
||
|
||
// write 写出数据块
|
||
func (c *nativeCall) write(args ...any) (any, error) {
|
||
if err := c.ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
value := platformValueOf(args[0])
|
||
data, ok := value.value.([]byte)
|
||
if !ok {
|
||
return nil, fmt.Errorf("expected output bytes")
|
||
}
|
||
var writer io.Writer = &c.buffer
|
||
if c.output != nil {
|
||
writer = c.output
|
||
}
|
||
n, err := writer.Write(data)
|
||
c.size += int64(n)
|
||
if err == nil && n != len(data) {
|
||
err = io.ErrShortWrite
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
// open 创建宿主输出,目标只交给授权hook解释
|
||
func (c *nativeCall) open(name string) error {
|
||
c.currentName = name
|
||
c.buffer.Reset()
|
||
if c.destination == nil {
|
||
return nil
|
||
}
|
||
if c.engine.OpenWriter == nil {
|
||
return fmt.Errorf("native output resolver is not configured")
|
||
}
|
||
var err error
|
||
c.output, err = c.engine.OpenWriter(c.ctx, c.destination, name)
|
||
if err == nil && c.output == nil {
|
||
return fmt.Errorf("native output resolver returned nil")
|
||
}
|
||
return err
|
||
}
|
||
|
||
// boundary 处理批量输出文件边界
|
||
func (c *nativeCall) boundary(args ...any) (any, error) {
|
||
if err := c.ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
action, name := args[0].(string), args[1].(string)
|
||
if action == "open" {
|
||
if c.paged {
|
||
name = c.base + "/" + name
|
||
} else {
|
||
name = c.base + "." + c.extension
|
||
}
|
||
return nil, c.open(name)
|
||
}
|
||
file := map[string]any{"name": c.currentName, "mime": c.mime}
|
||
if c.output != nil {
|
||
if err := c.output.Close(); err != nil {
|
||
return nil, err
|
||
}
|
||
c.output = nil
|
||
} else {
|
||
file["file"] = append([]byte(nil), c.buffer.Bytes()...)
|
||
}
|
||
c.files = append(c.files, file)
|
||
c.buffer.Reset()
|
||
return nil, nil
|
||
}
|
||
|
||
// font 按需请求授权字体
|
||
func (c *nativeCall) font(args ...any) (any, error) {
|
||
if err := c.ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
if c.engine.LoadFont == nil {
|
||
return nil, fmt.Errorf("native font resolver is not configured")
|
||
}
|
||
return c.engine.LoadFont(c.ctx, args[0].(string))
|
||
}
|
||
|
||
// fileCallback 逐个读取图片或附件
|
||
func (c *nativeCall) fileCallback(files platformValue) nativeCallback {
|
||
return func(args ...any) (any, error) {
|
||
data, err := c.engine.readBytes(c.ctx, files.Index(platformValueOf(args[0]).Int()).value)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]any{"bytes": data}, nil
|
||
}
|
||
}
|
||
|
||
// prepare 将worker输入转换为共用业务参数
|
||
func (c *nativeCall) prepare(name string, args []any) ([]any, error) {
|
||
checkpoint := nativeCallback(c.checkpoint)
|
||
write := nativeCallback(c.write)
|
||
switch name {
|
||
case "ofdgoDetectFormat":
|
||
if len(args) != 1 {
|
||
return nil, fmt.Errorf("missing detection input")
|
||
}
|
||
read, size, err := c.readCallback(args[0])
|
||
return []any{read, size, checkpoint}, err
|
||
case "ofdgoConvertFile":
|
||
if len(args) != 4 {
|
||
return nil, fmt.Errorf("missing conversion input")
|
||
}
|
||
read, size, err := c.readCallback(args[0])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
c.destination, c.batch = args[3], true
|
||
options := platformValueOf(args[1])
|
||
c.base = nativeOutputBase(options.Get("base"))
|
||
for _, f := range ofdgo.OutputFormats() {
|
||
if f.Value == options.Get("format").String() {
|
||
c.mime = f.MIME
|
||
c.extension, c.paged = f.Extension, f.Paged
|
||
}
|
||
}
|
||
if c.extension == "" {
|
||
return nil, fmt.Errorf("unsupported conversion format")
|
||
}
|
||
if c.destination != nil {
|
||
if c.engine.ReserveOutput == nil || c.engine.CleanupOutput == nil {
|
||
return nil, fmt.Errorf("batch destination requires reservation and cleanup hooks")
|
||
}
|
||
target, err := c.engine.ReserveOutput(c.ctx, NativeOutputRequest{Destination: c.destination, Base: c.base, Extension: c.extension, MIME: c.mime, Paged: c.paged})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
c.reserved = &target
|
||
if target.Destination == nil || target.Base == "" || target.Name == "" || nativeOutputBase(platformValueOf(target.Base)) != target.Base {
|
||
return nil, fmt.Errorf("invalid reserved output target")
|
||
}
|
||
c.destination, c.base = target.Destination, target.Base
|
||
c.event(map[string]any{"type": "output", "name": target.Name})
|
||
}
|
||
return []any{read, size, args[1], write, c.progress("conversion", "", true), nativeCallback(c.boundary), checkpoint, args[2], nativeCallback(c.font)}, nil
|
||
case "ofdgoPackFiles":
|
||
if len(args) != 3 {
|
||
return nil, fmt.Errorf("missing archive input")
|
||
}
|
||
entries := platformValueOf(args[0])
|
||
metadata := make([]any, entries.Length())
|
||
readers := make([]nativeCallback, entries.Length())
|
||
for i := range readers {
|
||
entry := entries.Index(i)
|
||
read, size, err := c.readCallback(entry.Get("file").value)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
readers[i] = read
|
||
metadata[i] = map[string]any{"name": entry.Get("name").String(), "size": size}
|
||
}
|
||
c.destination, c.exporting = args[1], true
|
||
if err := c.open(""); err != nil {
|
||
return nil, err
|
||
}
|
||
progress := nativeCallback(func(values ...any) (any, error) {
|
||
c.event(map[string]any{"type": "conversion", "phase": "pack", "completed": values[0], "total": values[1]})
|
||
return nil, c.ctx.Err()
|
||
})
|
||
return []any{metadata, nativeCallback(func(values ...any) (any, error) { return readers[platformValueOf(values[0]).Int()], nil }), write, progress, checkpoint, args[2]}, nil
|
||
case "ofdgoChangeAttachment":
|
||
if len(args) > 3 && (args[0] == "add" || args[0] == "replace") {
|
||
args[3] = c.fileCallback(platformValueOf(args[3]))
|
||
}
|
||
case "ofdgoChangeResource":
|
||
if len(args) > 4 && (args[0] == "add" || args[0] == "replace") {
|
||
data, err := c.engine.readBytes(c.ctx, args[4])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
args[4] = data
|
||
}
|
||
case "ofdgoImportImages":
|
||
if len(args) != 5 {
|
||
return nil, fmt.Errorf("missing image import input")
|
||
}
|
||
files := platformValueOf(args[0])
|
||
names := make([]any, files.Length())
|
||
for i := range names {
|
||
names[i] = files.Index(i).Get("name").String()
|
||
}
|
||
args[0] = names
|
||
args = append(args, c.fileCallback(files))
|
||
}
|
||
switch name {
|
||
case "ofdgoResources":
|
||
return append(args, checkpoint), nil
|
||
case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages":
|
||
return append(args, c.progress("import", "", true)), nil
|
||
case "ofdgoConvertPDF":
|
||
return append(args, c.progress("conversion", "", true), nativeCallback(c.font)), nil
|
||
case "ofdgoExportImages", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned":
|
||
if len(args) == 0 {
|
||
return nil, fmt.Errorf("missing output arguments")
|
||
}
|
||
c.destination, c.exporting = args[len(args)-1], true
|
||
args = args[:len(args)-1]
|
||
if err := c.open(""); err != nil {
|
||
return nil, err
|
||
}
|
||
saving := name == "ofdgoSaveDocument" || name == "ofdgoSaveSigned"
|
||
var indices, compression any
|
||
if saving {
|
||
if name != "ofdgoSaveSigned" && len(args) > 0 {
|
||
if m, ok := args[len(args)-1].(map[string]any); ok && m["mode"] != nil {
|
||
compression = m
|
||
args = args[:len(args)-1]
|
||
}
|
||
}
|
||
if len(args) == 0 {
|
||
return nil, fmt.Errorf("missing save page selection")
|
||
}
|
||
indices, args = args[0], args[1:]
|
||
}
|
||
args = append(args, write)
|
||
if name == "ofdgoExportDocument" {
|
||
args = append(args, c.progress("export", "pages", false))
|
||
}
|
||
if name == "ofdgoExportVersion" {
|
||
args = append(args, c.progress("export", "prepare", true), checkpoint)
|
||
}
|
||
if saving {
|
||
args = append(args, c.progress("export", "prepare", true))
|
||
if compression != nil {
|
||
args = append(args, indices, compression, checkpoint)
|
||
} else if indices != nil {
|
||
args = append(args, indices)
|
||
}
|
||
} else {
|
||
for _, arg := range args {
|
||
if m, ok := arg.(map[string]any); ok && m["mode"] != nil {
|
||
args = append(args, checkpoint)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return args, nil
|
||
}
|
||
|
||
// finish 完成输出并保留原生字节
|
||
func (c *nativeCall) finish(name string, result map[string]any) error {
|
||
if !c.exporting && !c.batch {
|
||
return nil
|
||
}
|
||
data, ok := result["data"].(map[string]any)
|
||
if !ok {
|
||
return fmt.Errorf("invalid output result")
|
||
}
|
||
if c.batch {
|
||
if data["unchanged"] == true && c.size == 0 {
|
||
c.files = []any{}
|
||
if err := c.abort(); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
data["files"] = c.files
|
||
} else if c.output != nil {
|
||
if err := c.output.Close(); err != nil {
|
||
return err
|
||
}
|
||
c.output = nil
|
||
} else {
|
||
data["blob"] = append([]byte(nil), c.buffer.Bytes()...)
|
||
}
|
||
data["size"] = c.size
|
||
c.committed = true
|
||
if c.exporting && name != "ofdgoExportMedia" && name != "ofdgoPackFiles" {
|
||
c.event(map[string]any{"type": "export", "stage": "save"})
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// abort 失败时释放尚未提交的宿主输出,支持可选Abort回滚
|
||
func (c *nativeCall) abort() error {
|
||
var err error
|
||
if c.output != nil {
|
||
if output, ok := c.output.(interface{ Abort() error }); ok {
|
||
err = output.Abort()
|
||
}
|
||
c.output = nil
|
||
}
|
||
if c.reserved != nil && !c.committed {
|
||
target := *c.reserved
|
||
c.reserved = nil
|
||
err = errors.Join(err, c.engine.CleanupOutput(context.WithoutCancel(c.ctx), target))
|
||
}
|
||
return err
|
||
}
|
||
|
||
// nativeOutputBase 清理worker输出文件名,冲突后缀由宿主排他创建决定
|
||
func nativeOutputBase(value platformValue) string {
|
||
base := "document"
|
||
if value.Type() == platformTypeString {
|
||
base = value.String()
|
||
}
|
||
base = strings.Map(func(r rune) rune {
|
||
if r < 32 || strings.ContainsRune("<>:\"/\\|?*", r) {
|
||
return '_'
|
||
}
|
||
return r
|
||
}, base)
|
||
base = strings.TrimRight(base, ". ")
|
||
if base == "" {
|
||
return "document"
|
||
}
|
||
return base
|
||
}
|
||
|
||
// sourceFromValue 读取宿主授权的随机读取源
|
||
func (e *businessEngine) sourceFromValue(value platformValue) (io.ReaderAt, int64, error) {
|
||
return e.resolveSource(e.operationContext(), value.value)
|
||
}
|
||
|
||
// platformValueOf 包装参数
|
||
func platformValueOf(v any) platformValue {
|
||
if p, ok := v.(platformValue); ok {
|
||
return p
|
||
}
|
||
return platformValue{v}
|
||
}
|
||
|
||
// platformNull 返回空参数
|
||
func platformNull() platformValue { return platformValue{} }
|
||
|
||
// Type 返回协议参数类型
|
||
func (v platformValue) Type() int {
|
||
switch v.value.(type) {
|
||
case nil:
|
||
return platformTypeUndefined
|
||
case string:
|
||
return platformTypeString
|
||
case bool:
|
||
return platformTypeBoolean
|
||
case nativeCallback:
|
||
return platformTypeFunction
|
||
case float64, float32, int, int64, uint64, json.Number:
|
||
return platformTypeNumber
|
||
default:
|
||
return platformTypeObject
|
||
}
|
||
}
|
||
|
||
// IsNull 检查空参数
|
||
func (v platformValue) IsNull() bool { return v.value == nil }
|
||
|
||
// IsUndefined 检查缺省参数
|
||
func (v platformValue) IsUndefined() bool { return v.value == nil }
|
||
|
||
// String 读取字符串
|
||
func (v platformValue) String() string {
|
||
if v.value == nil {
|
||
return "<undefined>"
|
||
}
|
||
return fmt.Sprint(v.value)
|
||
}
|
||
|
||
// Bool 读取布尔参数
|
||
func (v platformValue) Bool() bool {
|
||
b, ok := v.value.(bool)
|
||
if !ok {
|
||
panic("expected boolean")
|
||
}
|
||
return b
|
||
}
|
||
|
||
// Float 读取数值参数
|
||
func (v platformValue) Float() float64 {
|
||
switch n := v.value.(type) {
|
||
case float64:
|
||
return n
|
||
case float32:
|
||
return float64(n)
|
||
case int:
|
||
return float64(n)
|
||
case int64:
|
||
return float64(n)
|
||
case uint64:
|
||
return float64(n)
|
||
case json.Number:
|
||
f, err := n.Float64()
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return f
|
||
default:
|
||
panic("expected number")
|
||
}
|
||
}
|
||
|
||
// Int 读取整数参数
|
||
func (v platformValue) Int() int { return int(v.Float()) }
|
||
|
||
// Get 读取对象字段
|
||
func (v platformValue) Get(key string) platformValue {
|
||
if m, ok := v.value.(map[string]any); ok {
|
||
return platformValueOf(m[key])
|
||
}
|
||
if key == "length" || key == "byteLength" {
|
||
return platformValueOf(v.Length())
|
||
}
|
||
return platformNull()
|
||
}
|
||
|
||
// Length 读取数组长度
|
||
func (v platformValue) Length() int {
|
||
if v.value == nil {
|
||
return 0
|
||
}
|
||
return reflect.ValueOf(v.value).Len()
|
||
}
|
||
|
||
// Index 读取数组元素
|
||
func (v platformValue) Index(i int) platformValue {
|
||
return platformValueOf(reflect.ValueOf(v.value).Index(i).Interface())
|
||
}
|
||
|
||
// Invoke 调用宿主函数
|
||
func (v platformValue) Invoke(args ...any) platformValue {
|
||
result, err := v.value.(nativeCallback)(args...)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return platformValueOf(result)
|
||
}
|
||
|
||
// platformCopyBytes 复制读取块
|
||
func platformCopyBytes(dst []byte, src platformValue) int {
|
||
return copy(dst, src.value.([]byte))
|
||
}
|
||
|
||
// platformJSON 编码小参数
|
||
func platformJSON(v platformValue) string {
|
||
data, err := json.Marshal(v.value)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return string(data)
|
||
}
|
||
|
||
// platformParseJSON 解析小参数
|
||
func platformParseJSON(data []byte) map[string]any {
|
||
var v map[string]any
|
||
if err := json.Unmarshal(data, &v); err != nil {
|
||
panic(err)
|
||
}
|
||
return v
|
||
}
|
||
|
||
// bytesToJS 保留原生二进制数据
|
||
func bytesToJS(data []byte) platformValue { return platformValueOf(data) }
|
||
|
||
// bytesFromJS 读取字节或授权资源
|
||
func (e *businessEngine) bytesFromJS(v platformValue) ([]byte, error) {
|
||
if data, ok := v.value.([]byte); ok {
|
||
return append([]byte(nil), data...), nil
|
||
}
|
||
source, size, err := e.sourceFromValue(v)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return io.ReadAll(io.NewSectionReader(source, 0, size))
|
||
}
|
||
|
||
// successResult 保留二进制返回结构
|
||
func successResult(data any) platformValue {
|
||
return platformValueOf(map[string]any{"ok": true, "data": data})
|
||
}
|
||
|
||
// awaitExport 执行可取消宿主回调
|
||
func awaitExport(fn platformValue, args ...any) error {
|
||
_, err := fn.value.(nativeCallback)(args...)
|
||
return err
|
||
}
|
||
|
||
// loadConversionFont 读取宿主授权字体
|
||
func (e *businessEngine) loadConversionFont(fn platformValue, name string) ([]byte, error) {
|
||
result, err := fn.value.(nativeCallback)(name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return e.bytesFromJS(platformValueOf(result))
|
||
}
|