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