mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
feat(图形界面): 将OFDGo编译为WASM提供WebUI
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
// 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 (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/xiaoqidun/ofdgo"
|
||||
)
|
||||
|
||||
// currentSession 当前WebUI文档会话
|
||||
var currentSession *Session
|
||||
|
||||
// callbacks 浏览器回调函数引用
|
||||
var callbacks []js.Func
|
||||
|
||||
// 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("ofdgoRenderPage", renderPage)
|
||||
registerCallback("ofdgoExportPDF", exportPDF)
|
||||
registerCallback("ofdgoFontCandidates", fontCandidates)
|
||||
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 {
|
||||
data, err := safeCall(fn, args)
|
||||
if err != nil {
|
||||
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
||||
}
|
||||
return encodeResult(apiResult{OK: true, Data: data})
|
||||
})
|
||||
js.Global().Set(name, cb)
|
||||
callbacks = append(callbacks, cb)
|
||||
}
|
||||
|
||||
// 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) == 0 {
|
||||
return nil, fmt.Errorf("missing ofd data")
|
||||
}
|
||||
data, err := bytesFromJS(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fonts, err := fontsFromJS(jsArg(args, 1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if currentSession != nil {
|
||||
_ = currentSession.Close()
|
||||
currentSession = nil
|
||||
}
|
||||
session, err := Open(data, OpenOptions{Fonts: fonts})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentSession = session
|
||||
return currentSession.Info(), 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")
|
||||
}
|
||||
return currentSession.RenderPageSVG(args[0].Int())
|
||||
}
|
||||
|
||||
// exportPDF 导出OFD为PDF
|
||||
// 入参: args 浏览器参数
|
||||
// 返回: any PDF结果, error 错误信息
|
||||
func exportPDF(args []js.Value) (any, error) {
|
||||
if currentSession == nil {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
data, err := currentSession.ExportPDF()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"base64": base64.StdEncoding.EncodeToString(data), "size": len(data)}, nil
|
||||
}
|
||||
|
||||
// fontCandidates 获取字体候选名称
|
||||
// 入参: args 浏览器参数
|
||||
// 返回: any 字体候选名称, error 错误信息
|
||||
func fontCandidates(args []js.Value) (any, error) {
|
||||
names := stringsFromJS(jsArg(args, 0))
|
||||
return ofdgo.FontCandidateNames(names...), nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if value.IsUndefined() || value.IsNull() {
|
||||
return nil
|
||||
}
|
||||
if js.Global().Get("Array").Call("isArray", value).Bool() {
|
||||
length := value.Get("length").Int()
|
||||
items := make([]string, 0, length)
|
||||
for i := 0; i < length; i++ {
|
||||
item := value.Index(i)
|
||||
if !item.IsUndefined() && !item.IsNull() {
|
||||
items = append(items, item.String())
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
return []string{value.String()}
|
||||
}
|
||||
|
||||
// jsArg 获取浏览器参数
|
||||
// 入参: args 浏览器参数列表, index 参数索引
|
||||
// 返回: js.Value 浏览器值
|
||||
func jsArg(args []js.Value, index int) js.Value {
|
||||
if index < 0 || index >= len(args) {
|
||||
return js.Undefined()
|
||||
}
|
||||
return args[index]
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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 webui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/xiaoqidun/ofdgo"
|
||||
)
|
||||
|
||||
// OpenOptions 打开OFD文档选项
|
||||
type OpenOptions struct {
|
||||
Fonts []FontFile
|
||||
}
|
||||
|
||||
// FontFile 字体文件
|
||||
type FontFile = ofdgo.FontFile
|
||||
|
||||
// FontInfo OFD字体信息
|
||||
type FontInfo = ofdgo.FontInfo
|
||||
|
||||
// Session WebUI文档会话
|
||||
type Session struct {
|
||||
Reader *ofdgo.Reader
|
||||
Renderer *ofdgo.Renderer
|
||||
doc *ofdgo.Document
|
||||
}
|
||||
|
||||
// DocumentInfo 文档信息
|
||||
type DocumentInfo struct {
|
||||
Version string `json:"version"`
|
||||
DocType string `json:"docType"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Subject string `json:"subject"`
|
||||
CreationDate string `json:"creationDate"`
|
||||
ModDate string `json:"modDate"`
|
||||
PageCount int `json:"pageCount"`
|
||||
FontCount int `json:"fontCount"`
|
||||
Fonts []FontInfo `json:"fonts"`
|
||||
Pages []PageInfo `json:"pages"`
|
||||
}
|
||||
|
||||
// PageInfo 页面信息
|
||||
type PageInfo struct {
|
||||
Index int `json:"index"`
|
||||
ID string `json:"id"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
}
|
||||
|
||||
// PageSVG 页面SVG结果
|
||||
type PageSVG struct {
|
||||
Index int `json:"index"`
|
||||
Number int `json:"number"`
|
||||
ID string `json:"id"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
SVG string `json:"svg"`
|
||||
}
|
||||
|
||||
// Open 打开浏览器内存中的OFD文档
|
||||
// 入参: data OFD文件数据, opts 打开选项
|
||||
// 返回: *Session 文档会话, error 错误信息
|
||||
func Open(data []byte, opts OpenOptions) (*Session, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("empty ofd data")
|
||||
}
|
||||
reader, err := ofdgo.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, err := reader.Doc()
|
||||
if err != nil {
|
||||
reader.Close()
|
||||
return nil, err
|
||||
}
|
||||
var rendererOptions []ofdgo.RendererOption
|
||||
if len(opts.Fonts) > 0 {
|
||||
fontFS := ofdgo.NewFontFS(opts.Fonts)
|
||||
if fontFS.Len() == 0 {
|
||||
reader.Close()
|
||||
return nil, fmt.Errorf("invalid font file")
|
||||
}
|
||||
rendererOptions = append(rendererOptions, ofdgo.WithFontFS(fontFS))
|
||||
}
|
||||
return &Session{Reader: reader, Renderer: ofdgo.NewRenderer(reader, rendererOptions...), doc: doc}, nil
|
||||
}
|
||||
|
||||
// Close 关闭文档会话
|
||||
// 返回: error 错误信息
|
||||
func (s *Session) Close() error {
|
||||
if s == nil || s.Reader == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Reader.Close()
|
||||
}
|
||||
|
||||
// Info 获取文档信息
|
||||
// 返回: DocumentInfo 文档信息
|
||||
func (s *Session) Info() DocumentInfo {
|
||||
info := DocumentInfo{
|
||||
Version: s.Reader.Version(),
|
||||
DocType: s.Reader.DocType(),
|
||||
PageCount: len(s.doc.Pages.Page),
|
||||
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||
}
|
||||
if fonts, err := s.Renderer.FontInfos(); err == nil {
|
||||
info.Fonts = fonts
|
||||
}
|
||||
info.FontCount = len(info.Fonts)
|
||||
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
||||
info.Title = docInfo.Title
|
||||
info.Author = docInfo.Author
|
||||
info.Subject = docInfo.Subject
|
||||
info.CreationDate = docInfo.CreationDate
|
||||
info.ModDate = docInfo.ModDate
|
||||
}
|
||||
for index, pageRef := range s.doc.Pages.Page {
|
||||
pageInfo := PageInfo{Index: index, ID: pageRef.ID}
|
||||
if page, err := s.Reader.PageContent(pageRef); err == nil {
|
||||
if box, err := s.Renderer.GetPageBox(page); err == nil {
|
||||
pageInfo.Width = box.W
|
||||
pageInfo.Height = box.H
|
||||
}
|
||||
}
|
||||
info.Pages = append(info.Pages, pageInfo)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// RenderPageSVG 渲染页面为SVG
|
||||
// 入参: index 页面索引
|
||||
// 返回: PageSVG 页面SVG结果, error 错误信息
|
||||
func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||
return PageSVG{}, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
if index < 0 || index >= len(s.doc.Pages.Page) {
|
||||
return PageSVG{}, fmt.Errorf("page index %d out of range", index)
|
||||
}
|
||||
pageRef := s.doc.Pages.Page[index]
|
||||
page, err := s.Reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
return PageSVG{}, err
|
||||
}
|
||||
box, err := s.Renderer.GetPageBox(page)
|
||||
if err != nil {
|
||||
return PageSVG{}, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := s.Renderer.RenderToSVG(page, &buf); err != nil {
|
||||
return PageSVG{}, err
|
||||
}
|
||||
return PageSVG{Index: index, Number: index + 1, ID: pageRef.ID, Width: box.W, Height: box.H, SVG: buf.String()}, nil
|
||||
}
|
||||
|
||||
// ExportPDF 导出整个文档为PDF
|
||||
// 返回: []byte PDF文件数据, error 错误信息
|
||||
func (s *Session) ExportPDF() ([]byte, error) {
|
||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := s.Renderer.RenderToMultiPagePDF(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user