mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
fe164fb60b
commit
f472ebd931
5 files changed
+135
-33
No files matched your search
+49
-1
@@ -3,6 +3,8 @@ const FONT_DATABASE = "ofdgo";
|
||||
const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null;
|
||||
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
||||
const DEFAULT_IMAGE_DPI = 300;
|
||||
const PAGE_CACHE_LIMIT = 16;
|
||||
const PAGE_CACHE_BYTES = 32 * 1024 * 1024;
|
||||
const STATUS = {
|
||||
ready: "选择 OFD 文件",
|
||||
opening: "正在打开文档",
|
||||
@@ -1760,6 +1762,7 @@ function flowPageObserver() {
|
||||
state.selectedPages.delete(index);
|
||||
renderFlowPage(index, { openSeq, priority: 4 });
|
||||
}
|
||||
trimPageCache();
|
||||
}, {
|
||||
root: el.viewerPanel,
|
||||
rootMargin: "600px 0px",
|
||||
@@ -1798,6 +1801,7 @@ async function renderFlowPage(index, options = {}) {
|
||||
}
|
||||
updateThumbnail(index, openSeq);
|
||||
}
|
||||
trimPageCache();
|
||||
return page;
|
||||
} catch (err) {
|
||||
if (openSeq === state.openSeq) {
|
||||
@@ -1816,7 +1820,10 @@ function loadPageData(index, options = {}) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
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 priority = options.priority ?? 3;
|
||||
@@ -1869,7 +1876,9 @@ async function processPageRenderQueue() {
|
||||
state.svgImages.set(image.name, image.url);
|
||||
}
|
||||
}
|
||||
page.imageNames = page.images.map((image) => image.name);
|
||||
delete page.images;
|
||||
page.cacheBytes = (page.svg.length + page.text.length) * 2;
|
||||
page.text = JSON.parse(page.text);
|
||||
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) {
|
||||
for (const font of fonts) {
|
||||
if (openSeq !== state.openSeq) {
|
||||
@@ -1992,6 +2037,7 @@ function syncSelection() {
|
||||
for (const index of state.selectedPages) {
|
||||
unmountPage(index);
|
||||
}
|
||||
trimPageCache();
|
||||
}
|
||||
|
||||
function isDocumentSelection(range) {
|
||||
@@ -2652,6 +2698,7 @@ function thumbnailObserver() {
|
||||
state.visibleThumbnails.add(index);
|
||||
renderThumbnail(index, openSeq);
|
||||
}
|
||||
trimPageCache();
|
||||
}, {
|
||||
root: el.pageListPanel,
|
||||
rootMargin: "180px 0px",
|
||||
@@ -2682,6 +2729,7 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
|
||||
return;
|
||||
}
|
||||
updateThumbnail(index, openSeq);
|
||||
trimPageCache();
|
||||
} catch {
|
||||
if (openSeq === state.openSeq) {
|
||||
markThumbnailError(index);
|
||||
|
||||
+16
-26
@@ -278,15 +278,8 @@ func (s *Session) SetFonts(fonts []FontFile) error {
|
||||
func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
|
||||
text := s.textCache[index]
|
||||
if text == nil {
|
||||
page := s.pageCache[index]
|
||||
var err error
|
||||
if page == nil {
|
||||
page, err = s.Reader.PageContentByIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
text, err = s.Renderer.PageText(page)
|
||||
text, err = s.readPageText(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -301,11 +294,8 @@ func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
|
||||
func (s *Session) PageTextString(index int) (string, error) {
|
||||
text := s.textCache[index]
|
||||
if text == nil {
|
||||
page, err := s.Reader.PageContentByIndex(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text, err = s.Renderer.PageText(page)
|
||||
var err error
|
||||
text, err = s.readPageText(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -313,6 +303,16 @@ func (s *Session) PageTextString(index int) (string, error) {
|
||||
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 搜索指定页面,复用当前会话的文字索引
|
||||
// 入参: index 页面索引, query 搜索文字
|
||||
// 返回: []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 {
|
||||
return fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
if len(indices) > 0 {
|
||||
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)
|
||||
return s.Renderer.RenderToMultiPagePDF(writer, indices...)
|
||||
}
|
||||
|
||||
// pageContent 获取页面内容
|
||||
// pageContent 读取页面,复用最近一次解析结果
|
||||
// 入参: index 页面索引
|
||||
// 返回: *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 {
|
||||
return page, nil
|
||||
}
|
||||
clear(s.pageCache)
|
||||
page, err := s.Reader.PageContentByIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+15
-1
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -254,13 +255,26 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
|
||||
// 入参: 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)
|
||||
data, err := r.readFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
content.ID = page.ID
|
||||
|
||||
@@ -146,11 +146,7 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error
|
||||
}
|
||||
written := false
|
||||
for i, index := range indices {
|
||||
page, err := r.Reader.PageContentByIndex(index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read page %d: %w", index+1, err)
|
||||
}
|
||||
text, err := r.PageText(page)
|
||||
text, err := r.PageTextByIndex(index)
|
||||
if err != nil {
|
||||
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)
|
||||
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
||||
}
|
||||
pages[i].Content = nil
|
||||
if progress != nil {
|
||||
if err := progress(i+1, len(pages)); err != nil {
|
||||
buf.Truncate(start)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -23,6 +25,39 @@ import (
|
||||
"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 页面文字,按绘制顺序保留文本对象,不包含图像和签名外观
|
||||
type PageText struct {
|
||||
Runs []TextRun `json:"runs"`
|
||||
@@ -74,6 +109,24 @@ func (r *Renderer) PageText(page *PageContent) (*PageText, error) {
|
||||
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 页面原文
|
||||
func (p *PageText) String() string {
|
||||
|
||||
Reference in new issue
Block a user