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;
}
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");
+41 -9
View File
@@ -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 获取系统字体名称
+1
View File
@@ -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)
+7 -6
View File
@@ -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)
}
+45 -14
View File
@@ -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 文件列表
+1
View File
@@ -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
}
+2 -2
View File
@@ -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
+16 -9
View File
@@ -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