// 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 将授权标识解析为随机读取源,生命周期由宿主管理 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 NativeResolver 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 读取需要完整数据的资源,每块检查取消 func (e *NativeEngine) readBytes(ctx context.Context, value any) ([]byte, error) { source, size, err := e.source(ctx, value) if err != nil { return nil, err } var output bytes.Buffer chunk := make([]byte, 256<<10) for offset := int64(0); offset < size; { if err := ctx.Err(); err != nil { return nil, err } n := int(min(int64(len(chunk)), size-offset)) got, err := source.ReadAt(chunk[:n], offset) if got != n { return nil, io.ErrUnexpectedEOF } if err != nil && err != io.EOF { return nil, err } output.Write(chunk[:got]) offset += int64(got) } return output.Bytes(), 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 "" } 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)) }