feat(转换能力): 支持原生与网页批量并行
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-18 01:48:36 +08:00
1 parent 0eae7c72d0
commit a0899fa150
15 files changed
+549 -119

No files matched your search

+2
View File
@@ -424,6 +424,7 @@ type businessEngine struct {
copiedStyle *ofdgo.GraphicObject
ctx context.Context
resolveSource func(context.Context, any) (io.ReaderAt, int64, error)
conversionPool *ofdgo.ConversionPool
}
// Open 打开浏览器内存中的OFD文档
@@ -1391,6 +1392,7 @@ func (e *businessEngine) convertFile(args []platformValue) (any, error) {
}
options := ofdgo.ConvertOptions{
Format: settings.Get("format").String(), PageRange: settings.Get("pages").String(),
Pool: e.conversionPool,
SkipUnchanged: true,
Backends: &backends,
RendererOptions: []ofdgo.RendererOption{ofdgo.WithDPI(settings.Get("dpi").Float())},
+7 -1
View File
@@ -67,6 +67,8 @@ type NativeOutputTarget struct {
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
type NativeEngine struct {
businessEngine
// ConversionPool 协调不同会话的批量转换资源
ConversionPool *ofdgo.ConversionPool
// Resolve 按授权标识提供资源读取源
Resolve NativeResolver
// LoadFont 按名称读取系统字体数据
@@ -134,7 +136,11 @@ func NewNativeEngine(resolve NativeResolver) *NativeEngine { return &NativeEngin
// 批量输出到非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 })
e.once.Do(func() {
e.gate = make(chan struct{}, 1)
e.resolveSource = e.source
e.conversionPool = e.ConversionPool
})
if ctx == nil {
ctx = context.Background()
}
+92 -13
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows && amd64
//go:build !js || !wasm
package webview
@@ -44,17 +44,19 @@ const applicationID = "me.aite.ofdgo"
// Host 连接桌面系统能力和共用文档业务
type Host struct {
mu sync.Mutex
window *guigo.Window
files *fileStore
engines map[string]*webui.NativeEngine
engineStops map[string]func() bool
cancels map[string]context.CancelFunc
fontOnce sync.Once
fonts []map[string]any
fontFiles map[string]string
fontError error
pendingPaths []string
mu sync.Mutex
window *guigo.Window
files *fileStore
engines map[string]*webui.NativeEngine
engineStops map[string]func() bool
conversionPool *ofdgo.ConversionPool
cancels map[string]context.CancelFunc
batchResults map[string]any
fontOnce sync.Once
fonts []map[string]any
fontFiles map[string]string
fontError error
pendingPaths []string
}
// resourceReader 保存资源读取位置及关闭操作
@@ -76,9 +78,14 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
return err
}
h := &Host{files: files, engines: make(map[string]*webui.NativeEngine), cancels: make(map[string]context.CancelFunc), fontFiles: make(map[string]string), pendingPaths: append([]string(nil), paths...)}
h.conversionPool, err = ofdgo.NewConversionPool(ofdgo.ConversionPoolOptions{AvailableMemory: conversionMemory})
if err != nil {
files.close()
return err
}
defer h.close()
bindings := make(map[string]guigo.HandlerFunc)
for _, name := range []string{"engine.open", "engine.close", "engine.call", "open", "save", "directory", "file", "release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
}
static := http.FileServerFS(webuiassets.NativeFS)
@@ -260,6 +267,7 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
switch name {
case "engine.open":
engine := webui.NewNativeEngine(h.files.resolve)
engine.ConversionPool = h.conversionPool
engine.OpenWriter = h.openWriter
engine.ReserveOutput = h.reserveOutput
engine.CleanupOutput = h.cleanupOutput
@@ -273,8 +281,69 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
h.engineStops[id] = context.AfterFunc(guigo.PageContext(ctx), func() { _ = h.closeEngine(id) })
h.mu.Unlock()
return guigo.Result{Value: map[string]any{"engine": id}, Discard: func() { _ = h.closeEngine(id) }}, nil
case "batch":
return map[string]any{"parallelism": h.conversionPool.Parallelism()}, nil
case "engine.close":
return nil, h.closeEngine(text("engine"))
case "engine.finish":
key := text("engine") + "/" + fmt.Sprint(args["id"])
h.mu.Lock()
delete(h.batchResults, key)
h.mu.Unlock()
return nil, nil
case "engine.cancel":
key := text("engine") + "/" + fmt.Sprint(args["id"])
h.mu.Lock()
cancel := h.cancels[key]
h.mu.Unlock()
if cancel != nil {
cancel()
}
return nil, nil
case "engine.start":
operationName := text("name")
if operationName != "ofdgoConvertFile" && operationName != "ofdgoPackFiles" {
return nil, fmt.Errorf("unsupported batch operation")
}
id := text("engine")
key := id + "/" + fmt.Sprint(args["id"])
page := guigo.PageContext(ctx)
operation, cancel := context.WithCancel(page)
h.mu.Lock()
engine := h.engines[id]
_, duplicate := h.cancels[key]
if engine == nil || duplicate {
h.mu.Unlock()
cancel()
return nil, fmt.Errorf("invalid batch operation")
}
h.cancels[key] = cancel
h.mu.Unlock()
values, _ := args["args"].([]any)
go func() {
defer cancel()
defer func() { h.mu.Lock(); delete(h.cancels, key); h.mu.Unlock() }()
result := engine.Call(operation, operationName, values, func(event any) {
if value, ok := event.(map[string]any); ok {
value["id"] = args["id"]
h.emit("ofdgo.engine."+id, value)
}
})
encoded := h.encode(page, result)
h.mu.Lock()
if page.Err() != nil || h.engines[id] != engine {
h.mu.Unlock()
h.discard(encoded)
return
}
if h.batchResults == nil {
h.batchResults = make(map[string]any)
}
h.batchResults[key] = encoded
h.mu.Unlock()
h.emit("ofdgo.engine."+id, map[string]any{"id": args["id"], "type": "result", "result": encoded})
}()
return guigo.Result{Value: true, Discard: cancel}, nil
case "engine.call":
id := text("engine")
key := id + "/" + fmt.Sprint(args["id"])
@@ -670,6 +739,7 @@ func (h *Host) loadFont(ctx context.Context, name string) ([]byte, error) {
// 入参: id 会话标识
// 返回: error 关闭错误
func (h *Host) closeEngine(id string) error {
var results []any
h.mu.Lock()
engine := h.engines[id]
delete(h.engines, id)
@@ -682,7 +752,16 @@ func (h *Host) closeEngine(id string) error {
cancel()
}
}
for key, result := range h.batchResults {
if strings.HasPrefix(key, id+"/") {
results = append(results, result)
delete(h.batchResults, key)
}
}
h.mu.Unlock()
for _, result := range results {
h.discard(result)
}
if engine != nil {
return engine.Close()
}
+20
View File
@@ -0,0 +1,20 @@
// 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 !windows
package webview
// conversionMemory 未提供系统内存探测时仅按并行数调度
var conversionMemory func() int64
@@ -0,0 +1,45 @@
// 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 webview
import (
"syscall"
"unsafe"
)
// memoryStatusProc 查询系统物理内存和提交额度
var memoryStatusProc = syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
// conversionMemory 预留系统内存后返回可用于新增转换的额度
// 返回: int64 可用字节数,查询失败时暂停扩容
func conversionMemory() int64 {
status := struct {
length uint32
load uint32
total uint64
available uint64
totalPage uint64
availablePage uint64
totalVirtual uint64
availableVirtual uint64
extended uint64
}{}
status.length = uint32(unsafe.Sizeof(status))
ok, _, _ := memoryStatusProc.Call(uintptr(unsafe.Pointer(&status)))
if ok == 0 {
return 0
}
return max(0, int64(min(status.available, status.availablePage))-int64(status.total/8))
}