mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
865 lines
26 KiB
Go
865 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 (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"math"
|
|
"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)
|
|
registerCallback("ofdgoCreateDocument", createDocument)
|
|
registerCallback("ofdgoChangePage", changePage)
|
|
registerCallback("ofdgoInsertText", insertText)
|
|
registerCallback("ofdgoUpdateText", updateText)
|
|
registerCallback("ofdgoInsertImage", insertImage)
|
|
registerCallback("ofdgoTransformObject", transformObject)
|
|
registerCallback("ofdgoDeleteObject", deleteObject)
|
|
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
|
|
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
|
|
registerExportCallback("ofdgoSaveDocument", saveDocument)
|
|
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()
|
|
currentEditor = nil
|
|
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
|
|
}
|
|
objects, err := editorObjects(args[0].Int(), 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}
|
|
}
|
|
images := make([]any, len(page.Images))
|
|
for i, image := range page.Images {
|
|
images[i] = map[string]any{"name": image.Name, "mime": image.MIME, "bytes": bytesToJS(image.Data)}
|
|
}
|
|
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,
|
|
"images": images,
|
|
"text": string(textData),
|
|
"objects": objects,
|
|
}), 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)
|
|
}
|
|
|
|
// currentEditor 当前新建文档
|
|
var currentEditor *ofdgo.Editor
|
|
|
|
// editorInfo 创作文档信息与操作状态
|
|
type editorInfo struct {
|
|
DocumentInfo
|
|
Revision uint64 `json:"revision"`
|
|
CanUndo bool `json:"canUndo"`
|
|
CanRedo bool `json:"canRedo"`
|
|
}
|
|
|
|
// editorPageInfo 页面操作结果与目标页面
|
|
type editorPageInfo struct {
|
|
editorInfo
|
|
PageIndex int `json:"pageIndex"`
|
|
}
|
|
|
|
// editorSummary 获取当前创作文档状态
|
|
// 返回: editorInfo 文档信息
|
|
func editorSummary() editorInfo {
|
|
return editorInfo{currentSession.Summary(), currentEditor.Revision(), currentEditor.CanUndo(), currentEditor.CanRedo()}
|
|
}
|
|
|
|
// restoreEditor 撤销或重做并更新预览
|
|
// 入参: redo 是否重做
|
|
// 返回: any 文档信息, error 错误信息
|
|
func restoreEditor(redo bool) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
apply := currentEditor.Undo
|
|
if redo {
|
|
apply = currentEditor.Redo
|
|
}
|
|
if !apply() {
|
|
return editorSummary(), nil
|
|
}
|
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
}
|
|
|
|
// editorObjects 提供创作画布的对象命中区域
|
|
// 入参: index 页面索引, text 页面文字
|
|
// 返回: []any 对象区域, error 错误信息
|
|
func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|
objects := []any{}
|
|
if currentEditor == nil {
|
|
return objects, nil
|
|
}
|
|
page, err := currentSession.pageContent(index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
textBoxes := make(map[string]ofdgo.Box)
|
|
textValues := make(map[string]string)
|
|
for _, run := range text.Runs {
|
|
textValues[run.ID] += run.Text
|
|
for _, box := range run.Boxes {
|
|
if box.W <= 0 || box.H <= 0 {
|
|
continue
|
|
}
|
|
prev, exists := textBoxes[run.ID]
|
|
if !exists {
|
|
textBoxes[run.ID] = box
|
|
continue
|
|
}
|
|
x, y := math.Min(prev.X, box.X), math.Min(prev.Y, box.Y)
|
|
textBoxes[run.ID] = ofdgo.Box{X: x, Y: y, W: math.Max(prev.X+prev.W, box.X+box.W) - x, H: math.Max(prev.Y+prev.H, box.Y+box.H) - y}
|
|
}
|
|
}
|
|
fonts, err := currentSession.Reader.Fonts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fontNames := make(map[string]string, len(fonts))
|
|
for _, font := range fonts {
|
|
fontNames[font.ID] = font.FontName
|
|
}
|
|
for _, layer := range page.Content.Layer {
|
|
for _, object := range layer.Objects {
|
|
var id string
|
|
var box ofdgo.Box
|
|
switch object.Type {
|
|
case "TextObject":
|
|
id = object.TextObject.ID
|
|
box = textBoxes[id]
|
|
case "ImageObject":
|
|
id = object.ImageObject.ID
|
|
box, err = ofdgo.ParseBox(object.ImageObject.Boundary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
continue
|
|
}
|
|
if box.W > 0 && box.H > 0 {
|
|
item := map[string]any{"id": id, "image": object.Type == "ImageObject", "x": box.X, "y": box.Y, "width": box.W, "height": box.H}
|
|
if object.Type == "TextObject" {
|
|
item["text"], item["font"], item["fontName"], item["size"] = textValues[id], object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size
|
|
}
|
|
objects = append(objects, item)
|
|
}
|
|
}
|
|
}
|
|
return objects, nil
|
|
}
|
|
|
|
// transformObject 移动文字或等比缩放图片
|
|
// 入参: args 页码、对象标识、位移和缩放比例
|
|
// 返回: any 文档信息, error 错误信息
|
|
func transformObject(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
page, id := args[0].Int(), args[1].String()
|
|
object, err := currentEditor.Object(page, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scale := args[4].Float()
|
|
var boundary *string
|
|
switch object.Type {
|
|
case "TextObject":
|
|
if scale != 1 {
|
|
return nil, fmt.Errorf("text resizing is not supported")
|
|
}
|
|
boundary = &object.TextObject.Boundary
|
|
case "ImageObject":
|
|
boundary = &object.ImageObject.Boundary
|
|
object.ImageObject.CTM = ""
|
|
default:
|
|
return nil, fmt.Errorf("unsupported object type %q", object.Type)
|
|
}
|
|
box, err := ofdgo.ParseBox(*boundary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
*boundary = fmt.Sprintf("%g %g %g %g", box.X+args[2].Float(), box.Y+args[3].Float(), box.W*scale, box.H*scale)
|
|
if err := currentEditor.UpdateObject(page, id, object); err != nil {
|
|
return nil, err
|
|
}
|
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
}
|
|
|
|
// deleteObject 删除创作画布中选中的对象
|
|
// 入参: args 页码和对象标识
|
|
// 返回: any 文档信息, error 错误信息
|
|
func deleteObject(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
if err := currentEditor.DeleteObject(args[0].Int(), args[1].String()); err != nil {
|
|
return nil, err
|
|
}
|
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
}
|
|
|
|
// changePage 管理创作文档的页面
|
|
// 入参: args 操作、页面索引及尺寸或目标索引
|
|
// 返回: any 文档信息和目标页面, error 错误信息
|
|
func changePage(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
index := args[1].Int()
|
|
revision := currentEditor.Revision()
|
|
var err error
|
|
switch action := args[0].String(); action {
|
|
case "add":
|
|
index, err = currentEditor.AddPage(args[2].Float(), args[3].Float())
|
|
case "copy":
|
|
index, err = currentEditor.CopyPage(index)
|
|
case "delete":
|
|
if currentEditor.PageCount() == 1 {
|
|
return nil, fmt.Errorf("the document must contain at least one page")
|
|
}
|
|
err = currentEditor.DeletePage(index)
|
|
index = min(index, currentEditor.PageCount()-1)
|
|
case "move":
|
|
target := args[2].Int()
|
|
err = currentEditor.MovePage(index, target)
|
|
index = target
|
|
case "resize":
|
|
err = currentEditor.ResizePage(index, args[2].Float(), args[3].Float())
|
|
default:
|
|
return nil, fmt.Errorf("unsupported page action %q", action)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if currentEditor.Revision() == revision {
|
|
return editorPageInfo{editorSummary(), index}, nil
|
|
}
|
|
info, err := previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return editorPageInfo{info, index}, nil
|
|
}
|
|
|
|
// createDocument 新建单页文档
|
|
// 入参: args 标题、纸张宽高和注解设置
|
|
// 返回: any 文档信息, error 错误信息
|
|
func createDocument(args []js.Value) (any, error) {
|
|
editor := ofdgo.NewEditor()
|
|
editor.Info.Title = args[0].String()
|
|
if _, err := editor.AddPage(args[1].Float(), args[2].Float()); err != nil {
|
|
return nil, err
|
|
}
|
|
editor.SetHistoryLimit(100)
|
|
return previewEditor(editor, args[3].Bool())
|
|
}
|
|
|
|
// previewEditor 通过内存快照更新预览,不生成中间压缩包
|
|
// 入参: editor 新建文档, annotations 是否显示注解
|
|
// 返回: editorInfo 文档信息, error 错误信息
|
|
func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
|
reader, err := editor.Reader()
|
|
if err != nil {
|
|
return editorInfo{}, err
|
|
}
|
|
session, err := newSession(reader, OpenOptions{RenderAnnotations: annotations})
|
|
if err != nil {
|
|
return editorInfo{}, err
|
|
}
|
|
if currentSession != nil {
|
|
_ = currentSession.Close()
|
|
}
|
|
currentSession = session
|
|
currentEditor = editor
|
|
return editorSummary(), nil
|
|
}
|
|
|
|
// updateText 修改文字内容、字体和字号,空字体数据沿用对象字体
|
|
// 入参: args 页码、对象标识、内容、字体数据和字号
|
|
// 返回: any 文档信息, error 错误信息
|
|
func updateText(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
page, id := args[0].Int(), args[1].String()
|
|
object, err := currentEditor.Object(page, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fontID := object.TextObject.Font
|
|
if !args[3].IsNull() {
|
|
data, err := bytesFromJS(args[3])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fontID, err = currentEditor.AddFont(FontFile{Data: data}, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := currentEditor.UpdateText(page, id, args[2].String(), fontID, args[4].Float()); err != nil {
|
|
return nil, err
|
|
}
|
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
}
|
|
|
|
// insertText 使用选定字体创建文字对象
|
|
// 入参: args 页码、内容、字体数据、横纵坐标和字号
|
|
// 返回: any 文档信息, error 错误信息
|
|
func insertText(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
data, err := bytesFromJS(args[2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fontID, err := currentEditor.AddFont(FontFile{Data: data}, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
page := args[0].Int()
|
|
content, err := currentSession.pageContent(page)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
box, err := currentSession.pageBox(page, content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
x, y := args[3].Float(), args[4].Float()
|
|
box = ofdgo.Box{X: x, Y: y, W: box.W - x, H: box.H - y}
|
|
if _, err := currentEditor.AddText(page, box, args[1].String(), fontID, args[5].Float()); err != nil {
|
|
return nil, err
|
|
}
|
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
}
|
|
|
|
// insertImage 按原始比例创建图片对象
|
|
// 入参: args 页码、图片数据、横纵坐标和宽度
|
|
// 返回: any 文档信息, error 错误信息
|
|
func insertImage(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
data, err := bytesFromJS(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resourceID, err := currentEditor.AddImage(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config, _, err := image.DecodeConfig(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
width := args[4].Float()
|
|
box := ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: width, H: width * float64(config.Height) / float64(config.Width)}
|
|
content, err := currentSession.pageContent(args[0].Int())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pageBox, err := currentSession.pageBox(args[0].Int(), content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if height := pageBox.H - box.Y*2; box.H > height {
|
|
box.W *= height / box.H
|
|
box.H = height
|
|
}
|
|
object := ofdgo.GraphicObject{Type: "ImageObject", ImageObject: ofdgo.ImageObject{
|
|
ResourceID: resourceID, Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
|
|
}}
|
|
if _, err := currentEditor.AddObject(args[0].Int(), object); err != nil {
|
|
return nil, err
|
|
}
|
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
}
|
|
|
|
// saveDocument 分块写出新建文档
|
|
// 入参: args 数据写出回调
|
|
// 返回: any 保存结果, error 错误信息
|
|
func saveDocument(args []js.Value) (any, error) {
|
|
if currentEditor == nil {
|
|
return nil, fmt.Errorf("no document is being created")
|
|
}
|
|
writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20)
|
|
if _, err := currentEditor.WriteTo(writer); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := writer.Flush(); err != nil {
|
|
return nil, err
|
|
}
|
|
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
|
|
}
|