From 23849aee868d4a31b2f36c1261debd84d93b2d1d Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 13 Jul 2026 16:51:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96):=20?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 15 ++-------- internal/webui/wasm.go | 50 ++++++++++++++++++++++++++++------ ofdgo.go | 1 + ofdgo_font_fs.go | 13 +++++---- ofdgo_font_loader.go | 59 ++++++++++++++++++++++++++++++---------- ofdgo_renderer.go | 1 + ofdgo_renderer_output.go | 4 +-- ofdgo_renderer_text.go | 25 +++++++++++------ 8 files changed, 116 insertions(+), 52 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 53721bb..50907d6 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1003,7 +1003,7 @@ async function exportPDF() { return; } setProgress("正在保存文档 PDF", 86); - const bytes = base64ToBytes(result.base64); + const bytes = result.bytes; downloadBytes(bytes, "application/pdf", pdfFileName()); setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`); } catch (err) { @@ -1047,7 +1047,7 @@ async function exportCurrentPage() { return; } setProgress(`正在保存 ${result.label || label}`, 86); - const bytes = base64ToBytes(result.base64); + const bytes = result.bytes; downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format)); setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`); } catch (err) { @@ -2159,7 +2159,7 @@ function callWASM(name, ...args) { } throw err; } - const result = JSON.parse(payload); + const result = typeof payload === "string" ? JSON.parse(payload) : payload; if (!result.ok) { throw new Error(result.error || "WASM 调用失败"); } @@ -2215,15 +2215,6 @@ function formatSize(value) { return value.toFixed(1); } -function base64ToBytes(base64) { - const binary = atob(base64 || ""); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i += 1) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; -} - function downloadBytes(bytes, mime, name) { const blob = new Blob([bytes], { type: mime }); const link = document.createElement("a"); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 237dbfb..00cee99 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -17,7 +17,6 @@ package webui import ( - "encoding/base64" "encoding/json" "fmt" "syscall/js" @@ -57,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) { 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}) }) js.Global().Set(name, cb) @@ -113,7 +115,18 @@ func renderPage(args []js.Value) (any, error) { if len(args) == 0 { return nil, fmt.Errorf("missing page index") } - return currentSession.RenderPageSVG(args[0].Int()) + page, err := currentSession.RenderPageSVG(args[0].Int()) + if err != nil { + return nil, err + } + return successResult(map[string]any{ + "index": page.Index, + "number": page.Number, + "id": page.ID, + "width": page.Width, + "height": page.Height, + "svg": page.SVG, + }), nil } // exportFormats 获取导出格式 @@ -137,14 +150,14 @@ func exportPage(args []js.Value) (any, error) { if err != nil { return nil, err } - return map[string]any{ - "base64": base64.StdEncoding.EncodeToString(data), + return successResult(map[string]any{ + "bytes": bytesToJS(data), "size": len(data), "format": format.Value, "label": format.Label, "extension": format.Extension, "mime": format.MIME, - }, nil + }), nil } // exportPDF 导出OFD文档为PDF @@ -158,10 +171,29 @@ func exportPDF(args []js.Value) (any, error) { if err != nil { return nil, err } - return map[string]any{ - "base64": base64.StdEncoding.EncodeToString(data), - "size": len(data), - }, nil + return successResult(map[string]any{ + "bytes": bytesToJS(data), + "size": len(data), + }), 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 } // fontSystemNames 获取系统字体名称 diff --git a/ofdgo.go b/ofdgo.go index fe84cb0..cebba4d 100644 --- a/ofdgo.go +++ b/ofdgo.go @@ -71,6 +71,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer { DrawParams: reader.drawParamCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache, FontMap: make(map[string]*canvas.FontFamily), + fontFSCache: make(map[fontFSKey]*canvas.FontFamily), } for _, opt := range opts { opt(r) diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index 58d1603..1de4a8b 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -32,8 +32,9 @@ type FontFile struct { // FontFS 内存字体文件系统 type FontFS struct { - files map[string][]byte - names []string + files map[string][]byte + names []string + candidates []fontFileCandidate } // NewFontFS 创建内存字体文件系统 @@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS { fsys.files[name] = append([]byte(nil), font.Data...) } sort.Strings(fsys.names) + fsys.candidates = fontFileCandidates(fsys.names, path.Base) return fsys } @@ -146,11 +148,10 @@ func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string { // 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 // 返回: []string 字体文件列表 func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string { - files := fontFileCandidates(fsys.names, path.Base) - matches := make([]fontFileMatch, 0, len(files)) - seen := make(map[string]int, len(files)) + matches := make([]fontFileMatch, 0, len(fsys.candidates)) + seen := make(map[string]int, len(fsys.candidates)) for _, matcher := range newFontPatternMatchers(patterns) { - for _, file := range files { + for _, file := range fsys.candidates { rank := matcher.rankCandidate(file) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) } diff --git a/ofdgo_font_loader.go b/ofdgo_font_loader.go index a5a506c..ce56d65 100644 --- a/ofdgo_font_loader.go +++ b/ofdgo_font_loader.go @@ -22,6 +22,13 @@ import ( "github.com/tdewolff/canvas" ) +// fontFSKey 字体文件系统缓存键 +type fontFSKey struct { + index int + name string + style canvas.FontStyle +} + // loadFont 加载字体 // 入参: fontID 字体ID // 返回: *canvas.FontFamily 字体族 @@ -99,27 +106,19 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { } } } - for _, fsys := range r.fontFS { + for index, fsys := range r.fontFS { for _, m := range fontFSMatchesStyle(fsys, patterns, boldStyle, italicStyle) { - resData, err := fs.ReadFile(fsys, m) - if err == nil { - if err := ff.LoadFont(resData, 0, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff - } + if loaded := r.loadFontFromFS(fontID, ff, index, fsys, m, fontStyle); loaded != nil { + return loaded } } } if !canLoadSystemFonts() { - for _, fsys := range r.fontFS { + for index, fsys := range r.fontFS { if matches, err := fs.Glob(fsys, "*"); err == nil { for _, m := range matches { - resData, err := fs.ReadFile(fsys, m) - if err == nil { - if err := ff.LoadFont(resData, 0, fontStyle); err == nil { - r.FontMap[fontID] = ff - return ff - } + if loaded := r.loadFontFromFS(fontID, ff, index, fsys, m, fontStyle); loaded != nil { + return loaded } } } @@ -155,6 +154,38 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { return defaultFont } +// loadFontFromFS 从字体文件系统加载字体 +// 入参: fontID 字体ID, family 字体族, index 文件系统索引, fsys 字体文件系统, name 字体文件名, style 字体样式 +// 返回: *canvas.FontFamily 字体族 +func (r *Renderer) loadFontFromFS(fontID string, family *canvas.FontFamily, index int, fsys fs.FS, name string, style canvas.FontStyle) *canvas.FontFamily { + key := fontFSKey{index: index, name: name, style: style} + if cached := r.fontFSCache[key]; cached != nil { + r.FontMap[fontID] = cached + return cached + } + data, err := readFontData(fsys, name) + if err != nil || family.LoadFont(data, 0, style) != nil { + return nil + } + r.fontFSCache[key] = family + r.FontMap[fontID] = family + return family +} + +// readFontData 读取字体文件数据 +// 入参: fsys 字体文件系统, name 字体文件名 +// 返回: []byte 字体文件数据, error 错误信息 +func readFontData(fsys fs.FS, name string) ([]byte, error) { + if fontFS, ok := fsys.(*FontFS); ok { + data, ok := fontFS.files[cleanFontName(name)] + if !ok { + return nil, fs.ErrNotExist + } + return data, nil + } + return fs.ReadFile(fsys, name) +} + // matchFontFiles 查找字体文件 // 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体 // 返回: []string 文件列表 diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index ecb5354..7ae4efa 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -33,6 +33,7 @@ type Renderer struct { FontMap map[string]*canvas.FontFamily FontGIDMap map[string]map[uint16]rune FontCIDMap map[string]map[uint16]rune + fontFSCache map[fontFSKey]*canvas.FontFamily fontDirs []string fontFS []fs.FS } diff --git a/ofdgo_renderer_output.go b/ofdgo_renderer_output.go index aab3f6e..5b5f49f 100644 --- a/ofdgo_renderer_output.go +++ b/ofdgo_renderer_output.go @@ -59,8 +59,8 @@ func replacePDFProducer(data []byte) []byte { return data } dst := []byte("/Producer(xiaoqidun/ofdgo)") - result := make([]byte, 0, len(data)-len(old)+len(dst)) - return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...) + copy(data[idx:idx+len(old)], dst) + return data } // RenderToPDF 渲染为PDF diff --git a/ofdgo_renderer_text.go b/ofdgo_renderer_text.go index 1f1dd4d..49deb1b 100644 --- a/ofdgo_renderer_text.go +++ b/ofdgo_renderer_text.go @@ -144,13 +144,21 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } for i, glyph := range glyphs { str := glyph.Text + drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 + var glyphPath *canvas.Path + var glyphWidth float64 + if drawAsGlyphPath { + glyphPath, glyphWidth = textGlyphPath(face, glyph) + } else { + glyphWidth = textGlyphWidth(face, glyph) + } if i < len(xs) { cx = xs[i] } else if i > 0 { if dx, ok := textDelta(dxs, i-1); ok { cx += dx } else if len(dys) == 0 { - cx += textGlyphWidth(face, glyph) * hScale + cx += glyphWidth * hScale } } if i < len(ys) { @@ -169,7 +177,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 tx, ty := ctm.Transform(cx, cy) canvasX, canvasY = tx+bx, pageH-(ty+by) } - textWidth := textGlyphWidth(face, glyph) * hScale + textWidth := glyphWidth * hScale glyphFillPaint := fillPaint if useGlyphFillPaint { glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY) @@ -178,21 +186,20 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if glyphFillPaint != nil { ctx.SetFill(glyphFillPaint) drawGlyph := func(x, y float64) { - if drawAsPath || glyph.GlyphID >= 0 { - path, width := textGlyphPath(face, glyph) + if drawAsGlyphPath { scaleX := hScale - if advanceLimit > 0 && width*scaleX > advanceLimit { - scaleX = advanceLimit / width + if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { + scaleX = advanceLimit / glyphWidth } - textWidth = width * scaleX + textWidth = glyphWidth * scaleX if scaleX != 1 { ctx.Push() ctx.Translate(x, y) ctx.Scale(scaleX, 1) - ctx.DrawPath(0, 0, path) + ctx.DrawPath(0, 0, glyphPath) ctx.Pop() } else { - ctx.DrawPath(x, y, path) + ctx.DrawPath(x, y, glyphPath) } } else { scaled := hScale != 1