feat(性能优化): 提升速度
build.yaml / build (push) Successful in 34s

This commit is contained in:
2026-07-13 16:51:25 +08:00
parent 4e651619f8
commit 23849aee86
8 changed files with 116 additions and 52 deletions
+3 -12
View File
@@ -1003,7 +1003,7 @@ async function exportPDF() {
return; return;
} }
setProgress("正在保存文档 PDF", 86); setProgress("正在保存文档 PDF", 86);
const bytes = base64ToBytes(result.base64); const bytes = result.bytes;
downloadBytes(bytes, "application/pdf", pdfFileName()); downloadBytes(bytes, "application/pdf", pdfFileName());
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`); setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
} catch (err) { } catch (err) {
@@ -1047,7 +1047,7 @@ async function exportCurrentPage() {
return; return;
} }
setProgress(`正在保存 ${result.label || label}`, 86); 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)); 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)}`); setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
} catch (err) { } catch (err) {
@@ -2159,7 +2159,7 @@ function callWASM(name, ...args) {
} }
throw err; throw err;
} }
const result = JSON.parse(payload); const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (!result.ok) { if (!result.ok) {
throw new Error(result.error || "WASM 调用失败"); throw new Error(result.error || "WASM 调用失败");
} }
@@ -2215,15 +2215,6 @@ function formatSize(value) {
return value.toFixed(1); 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) { function downloadBytes(bytes, mime, name) {
const blob = new Blob([bytes], { type: mime }); const blob = new Blob([bytes], { type: mime });
const link = document.createElement("a"); const link = document.createElement("a");
+40 -8
View File
@@ -17,7 +17,6 @@
package webui package webui
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"syscall/js" "syscall/js"
@@ -57,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
if err != nil { if err != nil {
return encodeResult(apiResult{OK: false, Error: err.Error()}) return encodeResult(apiResult{OK: false, Error: err.Error()})
} }
if result, ok := data.(js.Value); ok {
return result
}
return encodeResult(apiResult{OK: true, Data: data}) return encodeResult(apiResult{OK: true, Data: data})
}) })
js.Global().Set(name, cb) js.Global().Set(name, cb)
@@ -113,7 +115,18 @@ func renderPage(args []js.Value) (any, error) {
if len(args) == 0 { if len(args) == 0 {
return nil, fmt.Errorf("missing page index") 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 获取导出格式 // exportFormats 获取导出格式
@@ -137,14 +150,14 @@ func exportPage(args []js.Value) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return map[string]any{ return successResult(map[string]any{
"base64": base64.StdEncoding.EncodeToString(data), "bytes": bytesToJS(data),
"size": len(data), "size": len(data),
"format": format.Value, "format": format.Value,
"label": format.Label, "label": format.Label,
"extension": format.Extension, "extension": format.Extension,
"mime": format.MIME, "mime": format.MIME,
}, nil }), nil
} }
// exportPDF 导出OFD文档为PDF // exportPDF 导出OFD文档为PDF
@@ -158,10 +171,29 @@ func exportPDF(args []js.Value) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return map[string]any{ return successResult(map[string]any{
"base64": base64.StdEncoding.EncodeToString(data), "bytes": bytesToJS(data),
"size": len(data), "size": len(data),
}, nil }), 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 获取系统字体名称 // fontSystemNames 获取系统字体名称
+1
View File
@@ -71,6 +71,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache, DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache,
FontMap: make(map[string]*canvas.FontFamily), FontMap: make(map[string]*canvas.FontFamily),
fontFSCache: make(map[fontFSKey]*canvas.FontFamily),
} }
for _, opt := range opts { for _, opt := range opts {
opt(r) opt(r)
+5 -4
View File
@@ -34,6 +34,7 @@ type FontFile struct {
type FontFS struct { type FontFS struct {
files map[string][]byte files map[string][]byte
names []string names []string
candidates []fontFileCandidate
} }
// NewFontFS 创建内存字体文件系统 // NewFontFS 创建内存字体文件系统
@@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
fsys.files[name] = append([]byte(nil), font.Data...) fsys.files[name] = append([]byte(nil), font.Data...)
} }
sort.Strings(fsys.names) sort.Strings(fsys.names)
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
return fsys return fsys
} }
@@ -146,11 +148,10 @@ func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 // 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表 // 返回: []string 字体文件列表
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string { func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
files := fontFileCandidates(fsys.names, path.Base) matches := make([]fontFileMatch, 0, len(fsys.candidates))
matches := make([]fontFileMatch, 0, len(files)) seen := make(map[string]int, len(fsys.candidates))
seen := make(map[string]int, len(files))
for _, matcher := range newFontPatternMatchers(patterns) { for _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range files { for _, file := range fsys.candidates {
rank := matcher.rankCandidate(file) rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
} }
+45 -14
View File
@@ -22,6 +22,13 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// fontFSKey 字体文件系统缓存键
type fontFSKey struct {
index int
name string
style canvas.FontStyle
}
// loadFont 加载字体 // loadFont 加载字体
// 入参: fontID 字体ID // 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族 // 返回: *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) { for _, m := range fontFSMatchesStyle(fsys, patterns, boldStyle, italicStyle) {
resData, err := fs.ReadFile(fsys, m) if loaded := r.loadFontFromFS(fontID, ff, index, fsys, m, fontStyle); loaded != nil {
if err == nil { return loaded
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
} }
} }
} }
if !canLoadSystemFonts() { if !canLoadSystemFonts() {
for _, fsys := range r.fontFS { for index, fsys := range r.fontFS {
if matches, err := fs.Glob(fsys, "*"); err == nil { if matches, err := fs.Glob(fsys, "*"); err == nil {
for _, m := range matches { for _, m := range matches {
resData, err := fs.ReadFile(fsys, m) if loaded := r.loadFontFromFS(fontID, ff, index, fsys, m, fontStyle); loaded != nil {
if err == nil { return loaded
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
} }
} }
} }
@@ -155,6 +154,38 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
return defaultFont 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 查找字体文件 // matchFontFiles 查找字体文件
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体 // 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 文件列表 // 返回: []string 文件列表
+1
View File
@@ -33,6 +33,7 @@ type Renderer struct {
FontMap map[string]*canvas.FontFamily FontMap map[string]*canvas.FontFamily
FontGIDMap map[string]map[uint16]rune FontGIDMap map[string]map[uint16]rune
FontCIDMap map[string]map[uint16]rune FontCIDMap map[string]map[uint16]rune
fontFSCache map[fontFSKey]*canvas.FontFamily
fontDirs []string fontDirs []string
fontFS []fs.FS fontFS []fs.FS
} }
+2 -2
View File
@@ -59,8 +59,8 @@ func replacePDFProducer(data []byte) []byte {
return data return data
} }
dst := []byte("/Producer(xiaoqidun/ofdgo)") dst := []byte("/Producer(xiaoqidun/ofdgo)")
result := make([]byte, 0, len(data)-len(old)+len(dst)) copy(data[idx:idx+len(old)], dst)
return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...) return data
} }
// RenderToPDF 渲染为PDF // RenderToPDF 渲染为PDF
+16 -9
View File
@@ -144,13 +144,21 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
for i, glyph := range glyphs { for i, glyph := range glyphs {
str := glyph.Text 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) { if i < len(xs) {
cx = xs[i] cx = xs[i]
} else if i > 0 { } else if i > 0 {
if dx, ok := textDelta(dxs, i-1); ok { if dx, ok := textDelta(dxs, i-1); ok {
cx += dx cx += dx
} else if len(dys) == 0 { } else if len(dys) == 0 {
cx += textGlyphWidth(face, glyph) * hScale cx += glyphWidth * hScale
} }
} }
if i < len(ys) { 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) tx, ty := ctm.Transform(cx, cy)
canvasX, canvasY = tx+bx, pageH-(ty+by) canvasX, canvasY = tx+bx, pageH-(ty+by)
} }
textWidth := textGlyphWidth(face, glyph) * hScale textWidth := glyphWidth * hScale
glyphFillPaint := fillPaint glyphFillPaint := fillPaint
if useGlyphFillPaint { if useGlyphFillPaint {
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY) 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 { if glyphFillPaint != nil {
ctx.SetFill(glyphFillPaint) ctx.SetFill(glyphFillPaint)
drawGlyph := func(x, y float64) { drawGlyph := func(x, y float64) {
if drawAsPath || glyph.GlyphID >= 0 { if drawAsGlyphPath {
path, width := textGlyphPath(face, glyph)
scaleX := hScale scaleX := hScale
if advanceLimit > 0 && width*scaleX > advanceLimit { if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / width scaleX = advanceLimit / glyphWidth
} }
textWidth = width * scaleX textWidth = glyphWidth * scaleX
if scaleX != 1 { if scaleX != 1 {
ctx.Push() ctx.Push()
ctx.Translate(x, y) ctx.Translate(x, y)
ctx.Scale(scaleX, 1) ctx.Scale(scaleX, 1)
ctx.DrawPath(0, 0, path) ctx.DrawPath(0, 0, glyphPath)
ctx.Pop() ctx.Pop()
} else { } else {
ctx.DrawPath(x, y, path) ctx.DrawPath(x, y, glyphPath)
} }
} else { } else {
scaled := hScale != 1 scaled := hScale != 1