feat(性能优化): 优化阅读缓存、全文搜索及PDF导出的内存占用
build.yaml / build (push) Successful in 35s

This commit is contained in:
xiaoqidun committed 2026-09-14 22:15:03 +08:00
1 parent fe164fb60b
commit f472ebd931
5 files changed
+135 -33

No files matched your search

+49 -1
View File
@@ -3,6 +3,8 @@ const FONT_DATABASE = "ofdgo";
const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null; const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null;
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)"); const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
const DEFAULT_IMAGE_DPI = 300; const DEFAULT_IMAGE_DPI = 300;
const PAGE_CACHE_LIMIT = 16;
const PAGE_CACHE_BYTES = 32 * 1024 * 1024;
const STATUS = { const STATUS = {
ready: "选择 OFD 文件", ready: "选择 OFD 文件",
opening: "正在打开文档", opening: "正在打开文档",
@@ -1760,6 +1762,7 @@ function flowPageObserver() {
state.selectedPages.delete(index); state.selectedPages.delete(index);
renderFlowPage(index, { openSeq, priority: 4 }); renderFlowPage(index, { openSeq, priority: 4 });
} }
trimPageCache();
}, { }, {
root: el.viewerPanel, root: el.viewerPanel,
rootMargin: "600px 0px", rootMargin: "600px 0px",
@@ -1798,6 +1801,7 @@ async function renderFlowPage(index, options = {}) {
} }
updateThumbnail(index, openSeq); updateThumbnail(index, openSeq);
} }
trimPageCache();
return page; return page;
} catch (err) { } catch (err) {
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
@@ -1816,7 +1820,10 @@ function loadPageData(index, options = {}) {
return Promise.resolve(null); return Promise.resolve(null);
} }
if (state.pageCache.has(index)) { if (state.pageCache.has(index)) {
return Promise.resolve(state.pageCache.get(index)); const page = state.pageCache.get(index);
state.pageCache.delete(index);
state.pageCache.set(index, page);
return Promise.resolve(page);
} }
const key = `${openSeq}:${index}`; const key = `${openSeq}:${index}`;
const priority = options.priority ?? 3; const priority = options.priority ?? 3;
@@ -1869,7 +1876,9 @@ async function processPageRenderQueue() {
state.svgImages.set(image.name, image.url); state.svgImages.set(image.name, image.url);
} }
} }
page.imageNames = page.images.map((image) => image.name);
delete page.images; delete page.images;
page.cacheBytes = (page.svg.length + page.text.length) * 2;
page.text = JSON.parse(page.text); page.text = JSON.parse(page.text);
state.pageCache.set(task.index, page); state.pageCache.set(task.index, page);
} }
@@ -1885,6 +1894,42 @@ async function processPageRenderQueue() {
} }
} }
function trimPageCache() {
const protectedPages = new Set([state.pageIndex, ...state.visiblePages, ...state.visibleThumbnails, ...state.selectedPages]);
for (const task of state.pageInFlight.values()) {
protectedPages.add(task.index);
}
const references = new Map();
let bytes = 0;
for (const page of state.pageCache.values()) {
bytes += page.cacheBytes;
for (const name of page.imageNames) {
references.set(name, (references.get(name) || 0) + 1);
}
}
for (const url of state.svgImages.values()) {
bytes += url.length * 2;
}
for (const [index, page] of state.pageCache) {
if (state.pageCache.size <= PAGE_CACHE_LIMIT && bytes <= PAGE_CACHE_BYTES) {
break;
}
if (protectedPages.has(index)) {
continue;
}
state.pageCache.delete(index);
bytes -= page.cacheBytes;
for (const name of page.imageNames) {
const count = references.get(name) - 1;
references.set(name, count);
if (count === 0) {
bytes -= state.svgImages.get(name).length * 2;
state.svgImages.delete(name);
}
}
}
}
async function loadSVGFonts(fonts, openSeq) { async function loadSVGFonts(fonts, openSeq) {
for (const font of fonts) { for (const font of fonts) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
@@ -1992,6 +2037,7 @@ function syncSelection() {
for (const index of state.selectedPages) { for (const index of state.selectedPages) {
unmountPage(index); unmountPage(index);
} }
trimPageCache();
} }
function isDocumentSelection(range) { function isDocumentSelection(range) {
@@ -2652,6 +2698,7 @@ function thumbnailObserver() {
state.visibleThumbnails.add(index); state.visibleThumbnails.add(index);
renderThumbnail(index, openSeq); renderThumbnail(index, openSeq);
} }
trimPageCache();
}, { }, {
root: el.pageListPanel, root: el.pageListPanel,
rootMargin: "180px 0px", rootMargin: "180px 0px",
@@ -2682,6 +2729,7 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
return; return;
} }
updateThumbnail(index, openSeq); updateThumbnail(index, openSeq);
trimPageCache();
} catch { } catch {
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
markThumbnailError(index); markThumbnailError(index);
+16 -26
View File
@@ -278,15 +278,8 @@ func (s *Session) SetFonts(fonts []FontFile) error {
func (s *Session) PageText(index int) (*ofdgo.PageText, error) { func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
text := s.textCache[index] text := s.textCache[index]
if text == nil { if text == nil {
page := s.pageCache[index]
var err error var err error
if page == nil { text, err = s.readPageText(index)
page, err = s.Reader.PageContentByIndex(index)
if err != nil {
return nil, err
}
}
text, err = s.Renderer.PageText(page)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -301,11 +294,8 @@ func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
func (s *Session) PageTextString(index int) (string, error) { func (s *Session) PageTextString(index int) (string, error) {
text := s.textCache[index] text := s.textCache[index]
if text == nil { if text == nil {
page, err := s.Reader.PageContentByIndex(index) var err error
if err != nil { text, err = s.readPageText(index)
return "", err
}
text, err = s.Renderer.PageText(page)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -313,6 +303,16 @@ func (s *Session) PageTextString(index int) (string, error) {
return text.String(), nil return text.String(), nil
} }
// readPageText 复用已加载页面,否则只解析文字所需图元
// 入参: index 页面索引
// 返回: *ofdgo.PageText 页面文字, error 错误信息
func (s *Session) readPageText(index int) (*ofdgo.PageText, error) {
if page := s.pageCache[index]; page != nil {
return s.Renderer.PageText(page)
}
return s.Renderer.PageTextByIndex(index)
}
// SearchPage 搜索指定页面,复用当前会话的文字索引 // SearchPage 搜索指定页面,复用当前会话的文字索引
// 入参: index 页面索引, query 搜索文字 // 入参: index 页面索引, query 搜索文字
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息 // 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
@@ -667,21 +667,10 @@ func (s *Session) ExportPDF(writer io.Writer, indices ...int) error {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil { if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return fmt.Errorf("ofd document is not opened") return fmt.Errorf("ofd document is not opened")
} }
if len(indices) > 0 { return s.Renderer.RenderToMultiPagePDF(writer, indices...)
return s.Renderer.RenderToMultiPagePDF(writer, indices...)
}
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
for i := range pages {
page, err := s.pageContent(i)
if err != nil {
return err
}
pages[i] = page
}
return s.Renderer.RenderPagesToPDF(pages, writer)
} }
// pageContent 获取页面内容 // pageContent 读取页面,复用最近一次解析结果
// 入参: index 页面索引 // 入参: index 页面索引
// 返回: *ofdgo.PageContent 页面内容, error 错误信息 // 返回: *ofdgo.PageContent 页面内容, error 错误信息
func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) { func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
@@ -691,6 +680,7 @@ func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
if page, ok := s.pageCache[index]; ok { if page, ok := s.pageCache[index]; ok {
return page, nil return page, nil
} }
clear(s.pageCache)
page, err := s.Reader.PageContentByIndex(index) page, err := s.Reader.PageContentByIndex(index)
if err != nil { if err != nil {
return nil, err return nil, err
+15 -1
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"archive/zip" "archive/zip"
"bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
@@ -254,13 +255,26 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
// 入参: page 页面对象 // 入参: page 页面对象
// 返回: *PageContent 页面内容, error 错误信息 // 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) PageContent(page Page) (*PageContent, error) { func (r *Reader) PageContent(page Page) (*PageContent, error) {
return r.readPageContent(page, false)
}
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
// 入参: page 页面对象, textOnly 是否仅用于文字提取
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) readPageContent(page Page, textOnly bool) (*PageContent, error) {
fullPath := r.ResPath(page.BaseLoc) fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFile(fullPath) data, err := r.readFile(fullPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var content PageContent var content PageContent
if err := xml.Unmarshal(data, &content); err != nil { if textOnly {
decoder := &textPageTokens{Decoder: xml.NewDecoder(bytes.NewReader(data))}
err = xml.NewTokenDecoder(decoder).Decode(&content)
} else {
err = xml.Unmarshal(data, &content)
}
if err != nil {
return nil, fmt.Errorf("failed to unmarshal page content: %w", err) return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
} }
content.ID = page.ID content.ID = page.ID
+2 -5
View File
@@ -146,11 +146,7 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error
} }
written := false written := false
for i, index := range indices { for i, index := range indices {
page, err := r.Reader.PageContentByIndex(index) text, err := r.PageTextByIndex(index)
if err != nil {
return fmt.Errorf("failed to read page %d: %w", index+1, err)
}
text, err := r.PageText(page)
if err != nil { if err != nil {
return fmt.Errorf("failed to extract page %d text: %w", index+1, err) return fmt.Errorf("failed to extract page %d text: %w", index+1, err)
} }
@@ -420,6 +416,7 @@ func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer, progress fu
buf.Truncate(start) buf.Truncate(start)
return fmt.Errorf("failed to render page %d: %w", i+1, err) return fmt.Errorf("failed to render page %d: %w", i+1, err)
} }
pages[i].Content = nil
if progress != nil { if progress != nil {
if err := progress(i+1, len(pages)); err != nil { if err := progress(i+1, len(pages)); err != nil {
buf.Truncate(start) buf.Truncate(start)
+53
View File
@@ -15,6 +15,8 @@
package ofdgo package ofdgo
import ( import (
"encoding/xml"
"fmt"
"math" "math"
"strings" "strings"
"unicode" "unicode"
@@ -23,6 +25,39 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// textPageTokens 保留文字及其复合图元和裁剪,跳过独立路径与图片
type textPageTokens struct {
*xml.Decoder
parents []string
}
// Token 读取文字页面所需的XML节点
// 返回: xml.Token XML节点, error 错误信息
func (d *textPageTokens) Token() (xml.Token, error) {
for {
token, err := d.Decoder.Token()
if err != nil {
return nil, err
}
switch node := token.(type) {
case xml.StartElement:
if len(d.parents) > 0 && (node.Name.Local == "PathObject" || node.Name.Local == "ImageObject") {
switch d.parents[len(d.parents)-1] {
case "Layer", "PageBlock", "CompositeGraphicUnit", "CompositeObject":
if err := d.Decoder.Skip(); err != nil {
return nil, err
}
continue
}
}
d.parents = append(d.parents, node.Name.Local)
case xml.EndElement:
d.parents = d.parents[:len(d.parents)-1]
}
return token, nil
}
}
// PageText 页面文字,按绘制顺序保留文本对象,不包含图像和签名外观 // PageText 页面文字,按绘制顺序保留文本对象,不包含图像和签名外观
type PageText struct { type PageText struct {
Runs []TextRun `json:"runs"` Runs []TextRun `json:"runs"`
@@ -74,6 +109,24 @@ func (r *Renderer) PageText(page *PageContent) (*PageText, error) {
return renderer.pageText, nil return renderer.pageText, nil
} }
// PageTextByIndex 按页面索引提取文字,避免解析独立路径和图片图元
// 入参: index 页面索引,从0开始
// 返回: *PageText 页面文字, error 错误信息
func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
doc, err := r.Reader.Doc()
if err != nil {
return nil, err
}
if index < 0 || index >= len(doc.Pages.Page) {
return nil, fmt.Errorf("page index %d out of range", index)
}
page, err := r.Reader.readPageContent(doc.Pages.Page[index], true)
if err != nil {
return nil, err
}
return r.PageText(page)
}
// String 按文本对象顺序合并原文,以换行分隔非空对象 // String 按文本对象顺序合并原文,以换行分隔非空对象
// 返回: string 页面原文 // 返回: string 页面原文
func (p *PageText) String() string { func (p *PageText) String() string {