mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
66aa87304b
commit
4f530eb5b8
5 files changed
+142
-29
No files matched your search
+44
-8
@@ -7,6 +7,7 @@ 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 svgFontBytes = new WeakMap();
|
||||
const PAGE_LAYOUT_GAP = 16;
|
||||
const SCROLL_SEGMENT_SIZE = 1000000;
|
||||
const OBJECT_CLIPBOARD_TYPE = "application/x-ofdgo-objects";
|
||||
@@ -139,6 +140,8 @@ const state = {
|
||||
documentSelection: null,
|
||||
pageInFlight: new Map(),
|
||||
pageRenderQueue: [],
|
||||
pagePrefetchIndex: null,
|
||||
pagePrefetchDirection: 1,
|
||||
pageRenderRunning: false,
|
||||
pageRenderWorkers: 0,
|
||||
pageObserver: null,
|
||||
@@ -8982,6 +8985,8 @@ function resetPageLoading() {
|
||||
task.resolve(null);
|
||||
}
|
||||
state.pageRenderQueue = [];
|
||||
state.pagePrefetchIndex = null;
|
||||
state.pagePrefetchDirection = 1;
|
||||
state.pageInFlight.clear();
|
||||
resetThumbnails();
|
||||
if (state.pageObserver) {
|
||||
@@ -9209,17 +9214,21 @@ function trimPageCache() {
|
||||
for (const task of state.pageInFlight.values()) {
|
||||
protectedPages.add(task.index);
|
||||
}
|
||||
const references = new Map();
|
||||
const references = new Map(), fontReferences = 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 font of page.fonts || []) {
|
||||
fontReferences.set(font.name, (fontReferences.get(font.name) || 0) + 1);
|
||||
}
|
||||
}
|
||||
for (const image of state.svgImages.values()) {
|
||||
bytes += image.size;
|
||||
}
|
||||
for (const face of state.svgFonts.values()) bytes += svgFontBytes.get(face) || 0;
|
||||
const budget = nativeHost?.cacheBytes || PAGE_CACHE_BYTES;
|
||||
for (const [index, page] of state.pageCache) {
|
||||
if (state.pageCache.size <= PAGE_CACHE_LIMIT * Math.max(1, budget / PAGE_CACHE_BYTES) && bytes <= budget) {
|
||||
@@ -9235,9 +9244,23 @@ function trimPageCache() {
|
||||
references.set(name, count);
|
||||
if (count === 0 && !state.pageInFlight.size) {
|
||||
const image = state.svgImages.get(name);
|
||||
bytes -= image.size;
|
||||
URL.revokeObjectURL(image.url);
|
||||
state.svgImages.delete(name);
|
||||
if (image) {
|
||||
bytes -= image.size;
|
||||
URL.revokeObjectURL(image.url);
|
||||
state.svgImages.delete(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const font of page.fonts || []) {
|
||||
const count = fontReferences.get(font.name) - 1;
|
||||
fontReferences.set(font.name, count);
|
||||
if (count === 0 && !state.pageInFlight.size) {
|
||||
const face = state.svgFonts.get(font.name);
|
||||
if (face) {
|
||||
bytes -= svgFontBytes.get(face) || 0;
|
||||
document.fonts.delete(face);
|
||||
state.svgFonts.delete(font.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9274,6 +9297,7 @@ async function loadSVGFont(font, openSeq) {
|
||||
return;
|
||||
}
|
||||
document.fonts.add(face);
|
||||
svgFontBytes.set(face, data.bytes.byteLength);
|
||||
state.svgFonts.set(font.name, face);
|
||||
}
|
||||
|
||||
@@ -9889,10 +9913,22 @@ function markFlowPageError(index, err) {
|
||||
|
||||
function queueNearbyPages(index, openSeq = state.openSeq) {
|
||||
const start = pageGroupStart(index);
|
||||
const before = nativeHost ? state.pageLayout : 1;
|
||||
const after = nativeHost ? Math.max(1, Math.min(nativeHost.parallelism, state.pageLayout * 2)) : 1;
|
||||
for (let i = Math.max(0, start - before); i <= Math.min((state.doc?.pageCount || 1) - 1, start + state.pageLayout + after); i += 1) {
|
||||
renderFlowPage(i, { openSeq, priority: 2 });
|
||||
if (state.pagePrefetchIndex !== null && start !== state.pagePrefetchIndex) state.pagePrefetchDirection = Math.sign(start - state.pagePrefetchIndex);
|
||||
const direction = state.pagePrefetchDirection;
|
||||
state.pagePrefetchIndex = start;
|
||||
let bytes = 0;
|
||||
for (const page of state.pageCache.values()) bytes += page.cacheBytes || 0;
|
||||
for (const image of state.svgImages.values()) bytes += image.size;
|
||||
for (const face of state.svgFonts.values()) bytes += svgFontBytes.get(face) || 0;
|
||||
const budget = nativeHost?.cacheBytes || PAGE_CACHE_BYTES;
|
||||
const cost = Math.max(1 << 20, bytes / Math.max(1, state.pageCache.size));
|
||||
const count = Math.max(0, Math.min(nativeHost?.parallelism || 1, state.pageLayout * 2, Math.floor((budget - bytes) / cost)));
|
||||
for (let i = start; i < Math.min(state.doc?.pageCount || 1, start + state.pageLayout); i += 1) {
|
||||
renderFlowPage(i, { openSeq, priority: 1 });
|
||||
}
|
||||
for (let offset = 1; offset <= count; offset += 1) {
|
||||
const next = direction > 0 ? start + state.pageLayout + offset - 1 : start - offset;
|
||||
if (next >= 0 && next < (state.doc?.pageCount || 1)) renderFlowPage(next, { openSeq, priority: 6 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ type Editor struct {
|
||||
encryption *encryptionState
|
||||
output *outputOptimization
|
||||
saveTarget Output
|
||||
snapshotArchive *editorSnapshotArchive
|
||||
validatedPaths *renderCache[string, bool]
|
||||
richIndex *richTextIndex
|
||||
}
|
||||
|
||||
@@ -817,7 +817,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
|
||||
files = make(map[string][]byte)
|
||||
}
|
||||
maps.Copy(files, parts)
|
||||
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex, selectedVersion: e.source.reader.selectedVersion}
|
||||
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex, selectedVersion: e.source.reader.selectedVersion, pageContents: e.source.reader.pageContents}
|
||||
if err := reader.initRootContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+42
-4
@@ -18,6 +18,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -48,6 +49,7 @@ type Reader struct {
|
||||
drawParamCache map[string]*DrawParam
|
||||
compositeGraphicUnitCache map[string]*CompositeGraphicUnit
|
||||
pageHeaderCache map[string]PageContent
|
||||
pageContents *renderCache[pageContentKey, PageContent]
|
||||
doc *Document
|
||||
relatedPending bool
|
||||
documentIndex int
|
||||
@@ -63,6 +65,12 @@ type Reader struct {
|
||||
encryption *encryptionState
|
||||
}
|
||||
|
||||
// pageContentKey 按页面原文及解析模式区分缓存,不包含文档资源和区域继承
|
||||
type pageContentKey struct {
|
||||
digest [32]byte
|
||||
textOnly bool
|
||||
}
|
||||
|
||||
// documentPageList 仅读取文档中的页面索引,统计页数时不构建其他结构
|
||||
type documentPageList struct {
|
||||
Pages Pages
|
||||
@@ -104,6 +112,9 @@ func (r *Reader) Fork(ctx context.Context) (*Reader, error) {
|
||||
if _, err := r.DocContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.pageContents == nil {
|
||||
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20}
|
||||
}
|
||||
next := *r
|
||||
next.Closer = nil
|
||||
next.resourcesRead = maps.Clone(r.resourcesRead)
|
||||
@@ -768,6 +779,9 @@ func (r *Reader) initRootContext(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
r.fileIndex, r.fileNames = view.fileIndex, view.fileNames
|
||||
if r.pageContents == nil {
|
||||
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20}
|
||||
}
|
||||
r.OFD = &ofd
|
||||
r.doc, r.versionInfo = nil, nil
|
||||
r.documentRoot, r.RootDir = "", ""
|
||||
@@ -1085,12 +1099,21 @@ func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, templ
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.pageContents == nil {
|
||||
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20}
|
||||
}
|
||||
var content PageContent
|
||||
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), ctx: ctx})
|
||||
if textOnly {
|
||||
err = xml.NewTokenDecoder(&textPageTokens{Decoder: decoder}).Decode(&content)
|
||||
if len(data) > (r.pageContents.limit/4-4096)/16 {
|
||||
content, err = decodePageContent(ctx, data, textOnly)
|
||||
} else {
|
||||
err = decoder.Decode(&content)
|
||||
var parsed PageContent
|
||||
parsed, err = r.pageContents.load(ctx, pageContentKey{sha256.Sum256(data), textOnly}, func() (PageContent, int, error) {
|
||||
content, err := decodePageContent(ctx, data, textOnly)
|
||||
return content, len(data)*16 + 4096, err
|
||||
})
|
||||
if err == nil {
|
||||
content = cloneEditorData(parsed)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
||||
@@ -1114,6 +1137,21 @@ func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, templ
|
||||
return &content, nil
|
||||
}
|
||||
|
||||
// decodePageContent 解析未应用页面资源和区域继承的原始内容
|
||||
// 入参: ctx 取消上下文, data 页面XML, textOnly 是否仅读取文字
|
||||
// 返回: PageContent 页面内容, error 解析错误
|
||||
func decodePageContent(ctx context.Context, data []byte, textOnly bool) (PageContent, error) {
|
||||
var content PageContent
|
||||
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), ctx: ctx})
|
||||
var err error
|
||||
if textOnly {
|
||||
err = xml.NewTokenDecoder(&textPageTokens{Decoder: decoder}).Decode(&content)
|
||||
} else {
|
||||
err = decoder.Decode(&content)
|
||||
}
|
||||
return content, err
|
||||
}
|
||||
|
||||
// loadPageResources 加载页面资源,不解析图元
|
||||
// 入参: page 页面引用
|
||||
func (r *Reader) loadPageResources(page Page) {
|
||||
|
||||
+54
-16
@@ -30,10 +30,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"weak"
|
||||
|
||||
basefile "github.com/xiaoqidun/ofdgo/internal/base/file"
|
||||
)
|
||||
|
||||
// editorSnapshotArchiveLimit 限制编辑器常驻的快照压缩包字节数
|
||||
const editorSnapshotArchiveLimit = 32 << 20
|
||||
|
||||
// ErrSaveTarget 表示编辑器尚未绑定保存目标
|
||||
var ErrSaveTarget = errors.New("save target is not set")
|
||||
|
||||
@@ -65,6 +69,13 @@ type fileOutput struct {
|
||||
target *basefile.Target
|
||||
}
|
||||
|
||||
// editorSnapshotArchive 复用独立压缩包,超预算时仅在已有快照存活期间共享
|
||||
type editorSnapshotArchive struct {
|
||||
files []*zip.File
|
||||
archive weak.Pointer[zip.Reader]
|
||||
retained *zip.Reader
|
||||
}
|
||||
|
||||
// editorProgress 编辑与保存阶段的进度回调,nil表示不回报进度
|
||||
type editorProgress func(stage string, completed, total int) error
|
||||
|
||||
@@ -91,7 +102,7 @@ type ofdAttrs []xml.Attr
|
||||
// Snapshot 创建独立编辑快照,完成后可与原编辑器并行使用
|
||||
// 创建过程需与原实例串行,保留修订及保存目标,不复制撤销记录和进度回调
|
||||
// 保存目标与原实例共享,由输出实现协调并发事务及提交版本
|
||||
// 原包压缩数据直接复制,快照不依赖原阅读器及文件句柄,内存随快照释放
|
||||
// 原包压缩数据独立复制并在快照间复用,超预算数据随最后一个快照释放
|
||||
// 自定义后端及字体文件系统仍由调用方提供,需支持并行读取
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: *Editor 独立快照, error 准备或取消错误
|
||||
@@ -106,21 +117,7 @@ func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
|
||||
next.encryption = &encryptionState{info: cloneEditorData(reader.encryption.info), options: cloneEncryptionOptions(reader.encryption.options)}
|
||||
}
|
||||
if reader.Zip != nil {
|
||||
var data bytes.Buffer
|
||||
var count int64
|
||||
archive := zip.NewWriter(convertWriter{ctx: ctx, writer: &data, count: &count})
|
||||
for _, file := range reader.Zip.File {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := archive.Copy(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := archive.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.Zip, err = zip.NewReader(bytes.NewReader(data.Bytes()), int64(data.Len()))
|
||||
next.Zip, err = e.copySnapshotArchive(ctx, reader.Zip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -143,6 +140,47 @@ func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// copySnapshotArchive 复用条目未变的独立压缩包,取消或失败不发布缓存
|
||||
// 入参: ctx 取消上下文, source 原压缩包
|
||||
// 返回: *zip.Reader 独立压缩包, error 复制错误
|
||||
func (e *Editor) copySnapshotArchive(ctx context.Context, source *zip.Reader) (*zip.Reader, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cached := e.snapshotArchive; cached != nil && slices.Equal(cached.files, source.File) {
|
||||
if archive := cached.archive.Value(); archive != nil {
|
||||
return archive, nil
|
||||
}
|
||||
}
|
||||
var data bytes.Buffer
|
||||
var count int64
|
||||
archive := zip.NewWriter(convertWriter{ctx: ctx, writer: &data, count: &count})
|
||||
for _, file := range source.File {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := archive.Copy(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := archive.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader, err := zip.NewReader(bytes.NewReader(data.Bytes()), int64(data.Len()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cached := &editorSnapshotArchive{files: slices.Clone(source.File), archive: weak.Make(reader)}
|
||||
if data.Cap() <= editorSnapshotArchiveLimit {
|
||||
cached.retained = reader
|
||||
}
|
||||
e.snapshotArchive = cached
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// NewFileOutput 创建本地保存目标并记录当前文件状态,不创建或修改文件
|
||||
// 同一目标应复用返回值,以便后续保存检测外部修改
|
||||
// 替换前比较文件标识、大小和修改时间,不提供跨进程排他锁
|
||||
|
||||
Reference in new issue
Block a user