Files
ofdgo/internal/webui/wasm.go
T
2026-09-14 16:15:47 +08:00

489 lines
14 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 (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"syscall/js"
"github.com/xiaoqidun/ofdgo"
)
// exportWriter 分块传递导出数据
type exportWriter struct {
write js.Value
}
// Write 将数据块交给浏览器保存
// 入参: data 导出数据
// 返回: int 写入长度, error 错误信息
func (w exportWriter) Write(data []byte) (int, error) {
size := len(data)
for len(data) > 0 {
n := min(len(data), 1<<20)
if err := awaitExport(w.write, bytesToJS(data[:n])); err != nil {
return size - len(data), err
}
data = data[n:]
}
return size, nil
}
// awaitExport 等待浏览器处理导出检查点
// 入参: fn 浏览器回调, args 回调参数
// 返回: error 写入错误或取消原因
func awaitExport(fn js.Value, args ...any) error {
done := make(chan error)
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
var err error
if args[1].Bool() {
err = context.Canceled
} else if message := args[0].String(); message != "" {
err = fmt.Errorf("%s", message)
}
done <- err
return nil
})
defer callback.Release()
fn.Invoke(append(args, callback)...)
return <-done
}
// currentSession 当前WebUI文档会话
var currentSession *Session
// apiResult 浏览器接口返回结果
type apiResult struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Data any `json:"data,omitempty"`
}
// RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoConfigure", configureDocument)
registerCallback("ofdgoDocumentInfo", documentInfo)
registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoSVGFontData", svgFontData)
registerCallback("ofdgoSearchPage", searchPage)
registerCallback("ofdgoPageTextString", pageTextString)
registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoParsePageRange", parsePageRange)
registerExportCallback("ofdgoExportPage", exportPage)
registerExportCallback("ofdgoExportDocument", exportDocument)
registerExportCallback("ofdgoExportAttachment", exportAttachment)
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
select {}
}
// registerCallback 注册浏览器回调函数
// 入参: name 回调名称, fn 回调函数
func registerCallback(name string, fn func([]js.Value) (any, error)) {
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
return callbackResult(fn, args)
})
js.Global().Set(name, cb)
}
// registerExportCallback 异步导出,为浏览器处理数据块让出事件循环
// 入参: name 回调名称, fn 回调函数
func registerExportCallback(name string, fn func([]js.Value) (any, error)) {
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
executor := js.FuncOf(func(this js.Value, callbacks []js.Value) any {
resolve := callbacks[0]
go func() {
resolve.Invoke(callbackResult(fn, args))
}()
return nil
})
defer executor.Release()
return js.Global().Get("Promise").New(executor)
})
js.Global().Set(name, cb)
}
// callbackResult 转换浏览器接口结果
// 入参: fn 回调函数, args 回调参数
// 返回: any 浏览器结果
func callbackResult(fn func([]js.Value) (any, error), args []js.Value) any {
data, err := safeCall(fn, args)
if err != nil {
return encodeResult(apiResult{OK: false, Error: err.Error()})
}
if result, ok := data.(js.Value); ok {
return result
}
return encodeResult(apiResult{OK: true, Data: data})
}
// safeCall 调用浏览器回调并转换异常
// 入参: fn 回调函数, args 回调参数
// 返回: any 回调结果, error 错误信息
func safeCall(fn func([]js.Value) (any, error), args []js.Value) (data any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
return fn(args)
}
// openDocument 打开OFD文档
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
func openDocument(args []js.Value) (any, error) {
if len(args) < 3 {
return nil, fmt.Errorf("missing open arguments")
}
data, err := bytesFromJS(args[0])
if err != nil {
return nil, err
}
fonts, err := fontsFromJS(args[1])
if err != nil {
return nil, err
}
renderAnnotations := args[2].Bool()
if currentSession != nil {
_ = currentSession.Close()
currentSession = nil
}
session, err := Open(data, OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations})
if err != nil {
return nil, err
}
currentSession = session
return currentSession.Summary(), nil
}
// configureDocument 更新当前会话的字体和注解设置
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
func configureDocument(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if !args[0].IsNull() && !args[0].IsUndefined() {
fonts, err := fontsFromJS(args[0])
if err != nil {
return nil, err
}
if err := currentSession.SetFonts(fonts); err != nil {
return nil, err
}
}
if currentSession.Renderer.RenderAnnotations != args[1].Bool() {
currentSession.Renderer.RenderAnnotations = args[1].Bool()
clear(currentSession.textCache)
}
return currentSession.Summary(), nil
}
// documentInfo 获取首屏之后加载的字体统计和验签结果
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
func documentInfo(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return currentSession.Info(), nil
}
// exportAttachment 分块导出附件
// 入参: args 浏览器参数
// 返回: any 附件数据, error 错误信息
func exportAttachment(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
stream, err := currentSession.Reader.OpenAttachment(args[0].String())
if err != nil {
return nil, err
}
defer stream.Close()
writer := bufio.NewWriterSize(exportWriter{write: args[1]}, 1<<20)
if _, err := io.Copy(writer, stream); err != nil {
return nil, err
}
if err := writer.Flush(); err != nil {
return nil, err
}
return successResult(map[string]any{
"mime": "application/octet-stream",
}), nil
}
// svgFontData 读取SVG字体资源
// 入参: args 浏览器参数
// 返回: any 字体数据, error 错误信息
func svgFontData(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
data, err := currentSession.SVGFontData(args[0].String())
if err != nil {
return nil, err
}
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
}
// renderPage 渲染OFD页面
// 入参: args 浏览器参数
// 返回: any 页面SVG结果, error 错误信息
func renderPage(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if len(args) == 0 {
return nil, fmt.Errorf("missing page index")
}
page, err := currentSession.RenderPageSVG(args[0].Int())
if err != nil {
return nil, err
}
text, err := currentSession.PageText(args[0].Int())
if err != nil {
return nil, err
}
textData, err := json.Marshal(text)
if err != nil {
return nil, err
}
links := make([]any, len(page.Links))
for i, link := range page.Links {
links[i] = map[string]any{
"uri": link.URI,
"x": link.Box.X,
"y": link.Box.Y,
"width": link.Box.W,
"height": link.Box.H,
}
}
fonts := make([]any, len(page.Fonts))
for i, font := range page.Fonts {
fonts[i] = map[string]any{"name": font.Name, "weight": font.Weight, "style": font.Style}
}
return successResult(map[string]any{
"index": page.Index,
"number": page.Number,
"id": page.ID,
"width": page.Width,
"height": page.Height,
"svg": page.SVG,
"links": links,
"fonts": fonts,
"text": string(textData),
}), nil
}
// pageTextString 提取OFD页面原文
// 入参: args 浏览器参数
// 返回: any 页面原文, error 错误信息
func pageTextString(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return currentSession.PageTextString(args[0].Int())
}
// searchPage 搜索OFD页面文字
// 入参: args 浏览器参数
// 返回: any 搜索结果, error 错误信息
func searchPage(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return currentSession.SearchPage(args[0].Int(), args[1].String())
}
// exportFormats 获取导出格式
// 入参: args 浏览器参数
// 返回: any 导出格式, error 错误信息
func exportFormats(args []js.Value) (any, error) {
return ExportFormats(), nil
}
// parsePageRange 解析当前文档的导出页码
// 入参: args 浏览器参数
// 返回: any 页面索引, error 错误信息
func parsePageRange(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
count, err := currentSession.Reader.PageCount()
if err != nil {
return nil, err
}
return ofdgo.ParsePageRange(args[0].String(), count)
}
// exportPage 导出OFD单页
// 入参: args 浏览器参数
// 返回: any 导出结果, error 错误信息
func exportPage(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if len(args) < 4 {
return nil, fmt.Errorf("missing export page arguments")
}
writer := bufio.NewWriterSize(exportWriter{write: args[3]}, 1<<20)
format, err := currentSession.ExportPage(args[0].Int(), args[1].String(), args[2].Float(), writer)
if err != nil {
return nil, err
}
if err := writer.Flush(); err != nil {
return nil, err
}
return successResult(map[string]any{
"label": format.Label,
"mime": format.MIME,
}), nil
}
// exportDocument 导出OFD文档
// 入参: args 浏览器参数
// 返回: any 导出结果, error 错误信息
func exportDocument(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
renderer := currentSession.Renderer
renderer.OnExportProgress = func(completed, total int) error {
return awaitExport(args[4], completed, total)
}
defer func() { renderer.OnExportProgress = nil }()
var indices []int
if !args[2].IsNull() {
indices = make([]int, args[2].Length())
for i := range indices {
indices[i] = args[2].Index(i).Int()
}
}
writer := bufio.NewWriterSize(exportWriter{write: args[3]}, 1<<20)
format, err := currentSession.ExportDocument(args[0].String(), args[1].Float(), writer, indices...)
if err != nil {
return nil, err
}
if err := writer.Flush(); err != nil {
return nil, err
}
return successResult(map[string]any{
"label": format.Label,
"mime": format.MIME,
}), nil
}
// successResult 创建成功接口结果
// 入参: data 返回数据
// 返回: js.Value 接口结果
func successResult(data any) js.Value {
result := js.Global().Get("Object").New()
result.Set("ok", true)
result.Set("data", data)
return result
}
// bytesToJS 将字节数据转换为Uint8Array
// 入参: data 字节数据
// 返回: js.Value Uint8Array对象
func bytesToJS(data []byte) js.Value {
value := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(value, data)
return value
}
// matchFontFiles 匹配文档所需的字体文件
// 入参: args 浏览器参数
// 返回: any 匹配的字体文件名称, error 错误信息
func matchFontFiles(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0]))
}
// bytesFromJS 从浏览器值读取二进制数据
// 入参: value 浏览器值
// 返回: []byte 二进制数据, error 错误信息
func bytesFromJS(value js.Value) ([]byte, error) {
if value.IsUndefined() || value.IsNull() {
return nil, fmt.Errorf("missing binary data")
}
uint8Array := js.Global().Get("Uint8Array")
arrayBuffer := js.Global().Get("ArrayBuffer")
if value.InstanceOf(arrayBuffer) {
value = uint8Array.New(value)
}
if !value.InstanceOf(uint8Array) {
return nil, fmt.Errorf("binary data must be Uint8Array or ArrayBuffer")
}
data := make([]byte, value.Get("byteLength").Int())
if n := js.CopyBytesToGo(data, value); n != len(data) {
return nil, fmt.Errorf("copied %d of %d bytes", n, len(data))
}
return data, nil
}
// fontsFromJS 从浏览器值读取字体文件
// 入参: value 浏览器值
// 返回: []FontFile 字体文件, error 错误信息
func fontsFromJS(value js.Value) ([]FontFile, error) {
if value.IsUndefined() || value.IsNull() {
return nil, nil
}
length := value.Get("length").Int()
fonts := make([]FontFile, 0, length)
for i := 0; i < length; i++ {
item := value.Index(i)
data, err := bytesFromJS(item.Get("data"))
if err != nil {
return nil, err
}
fonts = append(fonts, FontFile{Name: item.Get("name").String(), Data: data})
}
return fonts, nil
}
// stringsFromJS 从浏览器值读取字符串列表
// 入参: value 浏览器值
// 返回: []string 字符串列表
func stringsFromJS(value js.Value) []string {
items := make([]string, value.Length())
for i := range items {
items[i] = value.Index(i).String()
}
return items
}
// encodeResult 编码浏览器接口返回结果
// 入参: result 返回结果
// 返回: string JSON字符串
func encodeResult(result apiResult) string {
data, err := json.Marshal(result)
if err != nil {
fallback, _ := json.Marshal(apiResult{OK: false, Error: err.Error()})
return string(fallback)
}
return string(data)
}