diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index fc99fe6..842521a 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4828,6 +4828,9 @@ async function changeDocument(name, item, ...args) { openSeq = advanceDocumentSequence(); if (changingFont) { await refreshFontBinding(doc, openSeq); + } else if (restoring && !doc.changes?.all && doc.changes?.pages?.length === 1 && doc.changes.pages[0] === state.pageIndex && (restoredIndex < 0 || restoredIndex === state.pageIndex)) { + if (location) canvasEditor.pendingSelection = {index:state.pageIndex, ids:location.ids}; + await refreshEditorPage(doc, state.pageIndex, openSeq, clearSelection); } else if (item || name === "ofdgoPasteObjects" || inserting || editingAnnotation) { await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection); } else { @@ -4960,7 +4963,7 @@ async function refreshFontBinding(doc, openSeq) { async function refreshEditorPage(doc, index, openSeq, clearSelection = false) { state.doc = doc; - resetSearch(); + const search = refreshSearchPages([index], openSeq); state.documentSelection = null; pageView.resetPageLoading(); try { @@ -4997,6 +5000,7 @@ async function refreshEditorPage(doc, index, openSeq, clearSelection = false) { renderMeta(true); updateControls(); loadDocumentDetails(openSeq); + await search; for (const shell of el.svgHost.children) pageView.observeFlowPage(shell); for (const button of el.pageList.children) pageView.observeThumbnail(button, Number(button.dataset.pageIndex), openSeq); } @@ -10771,6 +10775,39 @@ function resetSearch(clearInput = true) { clearSearchHighlights(); } +async function refreshSearchPages(pages, openSeq) { + const tab = documentTab(), target = tab.state, query = target.searchQuery; + if (!query || target.searchRunning) { resetSearch(false); return; } + const seq = ++target.searchSeq; + target.searchRunning = true; + const selected = target.searchMatches[target.searchIndex]; + try { + const matches = target.searchMatches.filter(match => !pages.includes(match.page)); + for (const page of pages) { + const result = await callTabWASM(tab, "ofdgoSearchPage", page, query); + if (tab.closed || target.openSeq !== openSeq || target.searchSeq !== seq) return; + for (const match of result || []) matches.push({...match, page}); + } + matches.sort((a,b) => a.page - b.page); + target.searchMatches = matches; + target.searchIndex = selected ? matches.indexOf(selected) : -1; + if (isForegroundDocument(tab)) { + el.searchResults.replaceChildren(); + clearSearchHighlights(); + el.searchStatus.textContent = matches.length ? "搜索完成" : "暂无结果"; + renderSearchResults(); + if (target.searchIndex >= 0) renderSearchHighlights(matches[target.searchIndex].page); + } + } catch (err) { + if (target.openSeq === openSeq && target.searchSeq === seq) { + target.searchQuery = ""; + if (isForegroundDocument(tab)) { resetSearch(false); showError(err, false); } + } + } finally { + if (target.searchSeq === seq) target.searchRunning = false; + } +} + async function searchDocument() { if (!state.doc || document.body.hasAttribute("data-busy")) { return; diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 5715456..fc6e26d 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -365,11 +365,12 @@ type editorClipboard struct { // editorInfo 编辑文档信息与操作状态 type editorInfo struct { DocumentInfo - Revision uint64 `json:"revision"` - CanUndo bool `json:"canUndo"` - CanRedo bool `json:"canRedo"` - PageCapabilities []uint `json:"pageCapabilities"` - EditWarnings []string `json:"editWarnings,omitempty"` + Revision uint64 `json:"revision"` + CanUndo bool `json:"canUndo"` + CanRedo bool `json:"canRedo"` + PageCapabilities []uint `json:"pageCapabilities"` + EditWarnings []string `json:"editWarnings,omitempty"` + Changes ofdgo.EditorChanges `json:"changes"` } // editorPageInfo 页面操作结果与目标页面 @@ -3203,7 +3204,7 @@ func (e *businessEngine) encodeResult(result apiResult) string { // editorSummary 获取当前编辑文档状态 // 返回: editorInfo 文档信息 func (e *businessEngine) editorSummary() editorInfo { - info := editorInfo{DocumentInfo: e.currentSession.Summary(), Revision: e.currentEditor.Revision(), CanUndo: e.currentEditor.CanUndo(), CanRedo: e.currentEditor.CanRedo()} + info := editorInfo{DocumentInfo: e.currentSession.Summary(), Revision: e.currentEditor.Revision(), CanUndo: e.currentEditor.CanUndo(), CanRedo: e.currentEditor.CanRedo(), Changes: e.currentEditor.Changes()} if !e.currentSession.doc.Permissions.Edit { info.EditWarnings = append(info.EditWarnings, "原文件声明不允许编辑") } @@ -6741,6 +6742,25 @@ func (e *businessEngine) previewEditor(editor *ofdgo.Editor, annotations bool) ( session.Renderer.SetFontFS(session.fontFS) editor.SetFontFS(session.fontFS) } + changes := editor.Changes() + if editor == e.currentEditor && !changes.All && len(session.boxCache) == len(e.currentSession.boxCache) { + changed := changes.Pages + for index, page := range e.currentSession.pageCache { + if !slices.Contains(changed, index) { + session.pageCache[index] = page + } + } + for _, index := range e.currentSession.textOrder { + if !slices.Contains(changed, index) { + session.cachePageText(index, e.currentSession.textCache[index]) + } + } + for index, box := range e.currentSession.boxCache { + if !slices.Contains(changed, index) { + session.boxCache[index] = box + } + } + } if editor == e.currentEditor { session.input, e.currentSession.input = e.currentSession.input, nil } diff --git a/ofdgo_compression.go b/ofdgo_compression.go index 60c3bd6..fb3d344 100644 --- a/ofdgo_compression.go +++ b/ofdgo_compression.go @@ -70,6 +70,7 @@ type outputOptimization struct { protected bool completed int deflater *flate.Writer + resources *editorResourceSelection } // outputCompressionBuffer 限制候选ZIP编码,不能缩小时终止本次压缩 diff --git a/ofdgo_editor.go b/ofdgo_editor.go index 35d3c59..a6f4133 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -46,38 +46,41 @@ const editorPathValidationLimit = 1024 // 阶段可重复,准备完成不代表保存成功 // 回调不可重入修改编辑器,可返回context.Canceled等调用方停止原因 type Editor struct { - OnWriteProgress func(stage string, completed, total int) error - Info DocInfo - pages []PageContent - resources []editorResource - runtimeResources *Resources - fonts map[string]*FontResource - fontRegistration editorFontRegistration - fontDirs []string - fontFS []fs.FS - fontRenderer *Renderer - fontSourcesCache *fontSourceCache - fontMetrics map[string]FontMetrics - backends RenderBackends - images map[string]image.Point - resourceID map[editorResourceKey]string - maxID int - history []editorChange - historyIndex int - historyLimit int - revision uint64 - serial uint64 - source *editorSource - origins map[string]*editorObjectOrigin - removedPages map[string]bool - structureRemaps map[string][]map[string]string - outlines []byte - encryption *encryptionState - output *outputOptimization - saveTarget Output - snapshotArchive *editorSnapshotArchive - validatedPaths *renderCache[string, bool] - richIndex *richTextIndex + OnWriteProgress func(stage string, completed, total int) error + Info DocInfo + pages []PageContent + resources []editorResource + runtimeResources *Resources + fonts map[string]*FontResource + fontRegistration editorFontRegistration + fontDirs []string + fontFS []fs.FS + fontRenderer *Renderer + fontSourcesCache *fontSourceCache + fontMetrics map[string]FontMetrics + backends RenderBackends + images map[string]image.Point + resourceID map[editorResourceKey]string + maxID int + history []editorChange + historyIndex int + historyLimit int + revision uint64 + serial uint64 + source *editorSource + origins map[string]*editorObjectOrigin + removedPages map[string]bool + structureRemaps map[string][]map[string]string + outlines []byte + encryption *encryptionState + output *outputOptimization + saveTarget Output + snapshotArchive *editorSnapshotArchive + validatedPaths *renderCache[string, bool] + changes EditorChanges + transactionChanges *EditorChanges + objectIndex *editorObjectIndex + richIndex *richTextIndex } // editorResource 文档内嵌资源 @@ -892,13 +895,8 @@ func (e *Editor) findObject(page int, id string) (*Layer, int, error) { if err != nil { return nil, 0, err } - for i := range content.Content.Layer { - layer := &content.Content.Layer[i] - for index, object := range layer.Objects { - if editorObjectID(object) == id && id != "" { - return layer, index, nil - } - } + if position, ok := e.objectPositions(content)[id]; ok { + return &content.Content.Layer[position.layer], position.index, nil } return nil, 0, fmt.Errorf("object %q not found on page %d", id, page) } diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index fd144f7..f087cdc 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -30,6 +30,36 @@ type editorObjectPosition struct { index int } +// editorObjectIndex 复用最近页面的只读对象位置,图层容器替换后重新建立 +type editorObjectIndex struct { + layers []Layer + positions map[string]editorObjectPosition +} + +// objectPositions 获取页面的对象位置索引,重复编号保留第一个匹配 +// 入参: page 内部只读页面 +// 返回: map[string]editorObjectPosition 只读索引 +func (e *Editor) objectPositions(page *PageContent) map[string]editorObjectPosition { + layers := page.Content.Layer + if len(layers) == 0 { + return nil + } + if cached := e.objectIndex; cached != nil && len(cached.layers) == len(layers) && &cached.layers[0] == &layers[0] { + return cached.positions + } + positions := make(map[string]editorObjectPosition) + for i, layer := range layers { + for j, object := range layer.Objects { + id := editorObjectID(object) + if _, exists := positions[id]; id != "" && !exists { + positions[id] = editorObjectPosition{i, j} + } + } + } + e.objectIndex = &editorObjectIndex{layers: layers, positions: positions} + return positions +} + // ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象 type ObjectPosition struct { Layer string @@ -709,7 +739,7 @@ func (e *Editor) replaceLayers(page int, layers []Layer) { func (e *Editor) replacePreparedLayers(page int, after []Layer) { if e.historyLimit == 0 { e.pages[page].Content.Layer = after - e.recordChange() + e.recordChange(page) return } before := copyEditorPage(e.pages[page]) @@ -738,7 +768,7 @@ func (e *Editor) replacePreparedLayers(page int, after []Layer) { e.pages[page].Content.Layer = copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer } apply(e, after) - if change := e.recordChange(); change != nil { + if change := e.recordChange(page); change != nil { change.undo = func(e *Editor) { apply(e, before.Content.Layer) } change.redo = func(e *Editor) { apply(e, after) } } @@ -835,6 +865,15 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve return nil } apply := func(e *Editor, objects []GraphicObject, origins map[string]*editorObjectOrigin) { + layers := slices.Clone(e.pages[page].Content.Layer) + cloned := make(map[int]bool) + for _, index := range indexes { + if !cloned[index.layer] { + layers[index.layer].Objects = slices.Clone(layers[index.layer].Objects) + cloned[index.layer] = true + } + } + e.pages[page].Content.Layer = layers for i, index := range indexes { e.pages[page].Content.Layer[index.layer].Objects[index.index] = objects[i] } @@ -843,7 +882,7 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve } } apply(e, after, afterOrigins) - if change := e.recordChange(); change != nil { + if change := e.recordChange(page); change != nil { change.undo = func(e *Editor) { apply(e, before, beforeOrigins) } change.redo = func(e *Editor) { apply(e, after, afterOrigins) } } @@ -891,30 +930,19 @@ func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []edi if len(ids) == 0 { return objects, indexes, nil } - selected := make(map[string]int, len(ids)) + positions := e.objectPositions(content) + selected := make(map[string]bool, len(ids)) for i, id := range ids { - if _, exists := selected[id]; exists { + if selected[id] { return nil, nil, fmt.Errorf("duplicate object %q", id) } - selected[id] = i - } - for i, layer := range content.Content.Layer { - for j, object := range layer.Objects { - id := editorObjectID(object) - if index, ok := selected[id]; ok && id != "" { - objects[index] = object - indexes[index] = editorObjectPosition{i, j} - delete(selected, id) - if len(selected) == 0 { - return objects, indexes, nil - } - } - } - } - for _, id := range ids { - if _, missing := selected[id]; missing { + selected[id] = true + position, ok := positions[id] + if !ok { return nil, nil, fmt.Errorf("object %q not found on page %d", id, page) } + indexes[i] = position + objects[i] = content.Content.Layer[position.layer].Objects[position.index] } return objects, indexes, nil } diff --git a/ofdgo_editor_copy.go b/ofdgo_editor_copy.go index 174d51f..515e73f 100644 --- a/ofdgo_editor_copy.go +++ b/ofdgo_editor_copy.go @@ -19,6 +19,7 @@ import ( "encoding/xml" "fmt" "reflect" + "slices" "strconv" "sync" ) @@ -241,6 +242,22 @@ func (e *Editor) copiedLayerStyle(object GraphicObject) string { // 入参: value 原值 // 返回: T 独立副本 func cloneEditorData[T any](value T) T { + switch source := any(value).(type) { + case GraphicObject: + return any(copyEditorGraphicObject(source)).(T) + case TextObject: + return any(copyEditorTextObject(source)).(T) + case PathObject: + return any(copyEditorPathObject(source)).(T) + case ImageObject: + return any(copyEditorImageObject(source)).(T) + case CompositeGraphicUnit: + return any(copyEditorCompositeGraphicUnit(source)).(T) + case PageContent: + return any(copyEditorPageContent(source)).(T) + case Layer: + return any(copyEditorLayer(source)).(T) + } source := reflect.ValueOf(value) if !source.IsValid() { return value @@ -336,3 +353,174 @@ func editorCloneFields(kind reflect.Type) []int { actual, _ := editorClonePlans.LoadOrStore(kind, fields) return actual.([]int) } + +// copyEditorGraphicObject 复制GraphicObject的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: GraphicObject 独立副本 +func copyEditorGraphicObject(value GraphicObject) GraphicObject { + value.TextObject = copyEditorTextObject(value.TextObject) + value.PathObject = copyEditorPathObject(value.PathObject) + value.ImageObject = copyEditorImageObject(value.ImageObject) + value.CompositeGraphicUnit = copyEditorCompositeGraphicUnit(value.CompositeGraphicUnit) + return value +} + +// copyEditorTextObject 复制TextObject的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: TextObject 独立副本 +func copyEditorTextObject(value TextObject) TextObject { + if value.DashOffset != nil { + copied := *value.DashOffset + value.DashOffset = &copied + } + if value.Alpha != nil { + copied := *value.Alpha + value.Alpha = &copied + } + if value.Visible != nil { + copied := *value.Visible + value.Visible = &copied + } + if value.Fill != nil { + copied := *value.Fill + value.Fill = &copied + } + if value.Stroke != nil { + copied := *value.Stroke + value.Stroke = &copied + } + value.StrokeColor = cloneEditorData(value.StrokeColor) + value.FillColor = cloneEditorData(value.FillColor) + value.CGTransform = slices.Clone(value.CGTransform) + value.TextCode = slices.Clone(value.TextCode) + value.Clips = cloneEditorData(value.Clips) + value.Actions = cloneEditorData(value.Actions) + return value +} + +// copyEditorPathObject 复制PathObject的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: PathObject 独立副本 +func copyEditorPathObject(value PathObject) PathObject { + if value.DashOffset != nil { + copied := *value.DashOffset + value.DashOffset = &copied + } + if value.Alpha != nil { + copied := *value.Alpha + value.Alpha = &copied + } + if value.Visible != nil { + copied := *value.Visible + value.Visible = &copied + } + if value.Stroke != nil { + copied := *value.Stroke + value.Stroke = &copied + } + if value.Fill != nil { + copied := *value.Fill + value.Fill = &copied + } + value.StrokeColor = cloneEditorData(value.StrokeColor) + value.FillColor = cloneEditorData(value.FillColor) + value.Clips = cloneEditorData(value.Clips) + value.Actions = cloneEditorData(value.Actions) + return value +} + +// copyEditorImageObject 复制ImageObject的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: ImageObject 独立副本 +func copyEditorImageObject(value ImageObject) ImageObject { + if value.Alpha != nil { + copied := *value.Alpha + value.Alpha = &copied + } + if value.Visible != nil { + copied := *value.Visible + value.Visible = &copied + } + value.Border = cloneEditorData(value.Border) + value.Clips = cloneEditorData(value.Clips) + value.Actions = cloneEditorData(value.Actions) + return value +} + +// copyEditorCompositeGraphicUnit 复制CompositeGraphicUnit的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: CompositeGraphicUnit 独立副本 +func copyEditorCompositeGraphicUnit(value CompositeGraphicUnit) CompositeGraphicUnit { + if value.Alpha != nil { + copied := *value.Alpha + value.Alpha = &copied + } + if value.Visible != nil { + copied := *value.Visible + value.Visible = &copied + } + value.Objects = slices.Clone(value.Objects) + for i := range value.Objects { + value.Objects[i] = copyEditorGraphicObject(value.Objects[i]) + } + value.TextObject = slices.Clone(value.TextObject) + for i := range value.TextObject { + value.TextObject[i] = copyEditorTextObject(value.TextObject[i]) + } + value.PathObject = slices.Clone(value.PathObject) + for i := range value.PathObject { + value.PathObject[i] = copyEditorPathObject(value.PathObject[i]) + } + value.ImageObject = slices.Clone(value.ImageObject) + for i := range value.ImageObject { + value.ImageObject[i] = copyEditorImageObject(value.ImageObject[i]) + } + value.CompositeGraphicUnit = slices.Clone(value.CompositeGraphicUnit) + for i := range value.CompositeGraphicUnit { + value.CompositeGraphicUnit[i] = copyEditorCompositeGraphicUnit(value.CompositeGraphicUnit[i]) + } + value.Clips = cloneEditorData(value.Clips) + value.Actions = cloneEditorData(value.Actions) + return value +} + +// copyEditorPageContent 复制PageContent的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: PageContent 独立副本 +func copyEditorPageContent(value PageContent) PageContent { + value.Template = slices.Clone(value.Template) + value.PageRes = slices.Clone(value.PageRes) + value.Content.Layer = slices.Clone(value.Content.Layer) + for i := range value.Content.Layer { + value.Content.Layer[i] = copyEditorLayer(value.Content.Layer[i]) + } + value.Actions = cloneEditorData(value.Actions) + return value +} + +// copyEditorLayer 复制Layer的可变字段,保留内部只读状态 +// 入参: value 原值 +// 返回: Layer 独立副本 +func copyEditorLayer(value Layer) Layer { + value.Objects = slices.Clone(value.Objects) + for i := range value.Objects { + value.Objects[i] = copyEditorGraphicObject(value.Objects[i]) + } + value.TextObject = slices.Clone(value.TextObject) + for i := range value.TextObject { + value.TextObject[i] = copyEditorTextObject(value.TextObject[i]) + } + value.PathObject = slices.Clone(value.PathObject) + for i := range value.PathObject { + value.PathObject[i] = copyEditorPathObject(value.PathObject[i]) + } + value.ImageObject = slices.Clone(value.ImageObject) + for i := range value.ImageObject { + value.ImageObject[i] = copyEditorImageObject(value.ImageObject[i]) + } + value.CompositeGraphicUnit = slices.Clone(value.CompositeGraphicUnit) + for i := range value.CompositeGraphicUnit { + value.CompositeGraphicUnit[i] = copyEditorCompositeGraphicUnit(value.CompositeGraphicUnit[i]) + } + return value +} diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index 5fc3025..a1e834f 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -34,8 +34,9 @@ type editorFontUsage struct { unsafe bool } -// editorFontSubset 保存单个新增字体最近一次裁剪结果,不保留历史字形集合 +// editorFontSubset 保存单个新增字体及其快照最近一次裁剪结果,不保留历史字形集合 type editorFontSubset struct { + source [32]byte glyphs []uint16 data []byte mapped bool @@ -317,13 +318,13 @@ func (e *Editor) subsetSnapshotFonts(ctx context.Context, progress editorProgres if e.source == nil || e.backends.FontResources == nil { return nil, ctx.Err() } - candidates := make(map[string]bool) - for _, resource := range e.resources { + candidates := make(map[string]int) + for i, resource := range e.resources { if err := ctx.Err(); err != nil { return nil, err } if resource.font != nil && resource.font.FontFile != "" && e.source.reader.fileNames[resource.name] { - candidates[resource.name] = true + candidates[resource.name] = i } } if len(candidates) == 0 { @@ -399,15 +400,24 @@ func (e *Editor) subsetSnapshotFonts(ctx context.Context, progress editorProgres if font.unsafe { continue } - subset, err := e.backends.FontResources.SubsetFont(data, slices.Sorted(maps.Keys(glyphs)), preserve) - if err != nil { - return nil, err + ids := slices.Sorted(maps.Keys(glyphs)) + checksum := sha256.Sum256(data) + resource := &e.resources[candidates[name]] + if resource.subset == nil || resource.subset.source != checksum || resource.subset.mapped != preserve || !slices.Equal(resource.subset.glyphs, ids) { + subset, err := e.backends.FontResources.SubsetFont(data, ids, preserve) + if err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + if len(subset) >= len(data) { + subset = nil + } + resource.subset = &editorFontSubset{source: checksum, glyphs: ids, data: subset, mapped: preserve} } - if err := ctx.Err(); err != nil { - return nil, err - } - if len(subset) != 0 && len(subset) < len(data) { - result[name] = subset + if len(resource.subset.data) != 0 { + result[name] = resource.subset.data } } return result, progress.report("fonts", len(candidates), len(candidates)) @@ -593,7 +603,7 @@ func (e *Editor) subsetNewFonts(progress editorProgress) (map[string][]byte, err return nil, err } resource := &e.resources[i] - if resource.font == nil { + if resource.font == nil || e.source != nil && e.source.reader.fileNames[resource.name] { continue } usage := used[resource.font.ID] diff --git a/ofdgo_editor_group.go b/ofdgo_editor_group.go index 7db3dbd..363f8e9 100644 --- a/ofdgo_editor_group.go +++ b/ofdgo_editor_group.go @@ -253,7 +253,7 @@ func (e *Editor) replaceGroup(page int, indexes []editorObjectPosition, objects } } apply(e, after, origins) - if change := e.recordChange(); change != nil { + if change := e.recordChange(page); change != nil { change.undo = func(e *Editor) { apply(e, before, previous) } change.redo = func(e *Editor) { apply(e, after, origins) } } diff --git a/ofdgo_editor_history.go b/ofdgo_editor_history.go index 7167000..272c533 100644 --- a/ofdgo_editor_history.go +++ b/ofdgo_editor_history.go @@ -21,10 +21,43 @@ import ( // editorChange 页面或对象操作及其修订标识 type editorChange struct { - undo func(*Editor) - redo func(*Editor) - before uint64 - after uint64 + undo func(*Editor) + redo func(*Editor) + before uint64 + after uint64 + changes EditorChanges +} + +// EditorChanges 描述最近一次修改或历史回放的刷新范围 +// All表示文档结构或共享资源变化;否则Pages为受影响的零基页码 +// 返回值只覆盖最近一次操作,调用方应在每次操作后读取 +type EditorChanges struct { + All bool `json:"all"` + Pages []int `json:"pages,omitempty"` +} + +// Changes 获取最近一次有效修改的独立刷新范围 +// 返回: EditorChanges 变化范围;未发生修改时为空 +func (e *Editor) Changes() EditorChanges { + return EditorChanges{All: e.changes.All, Pages: slices.Clone(e.changes.Pages)} +} + +// merge 合并同一事务中的变化范围 +// 入参: change 待合并的变化 +func (c *EditorChanges) merge(change EditorChanges) { + if c.All { + return + } + if change.All { + c.All = true + c.Pages = nil + return + } + for _, page := range change.Pages { + if !slices.Contains(c.Pages, page) { + c.Pages = append(c.Pages, page) + } + } } // Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史 @@ -37,6 +70,8 @@ func (e *Editor) Transaction(edit func(*Editor) error) error { before.Info = cloneEditorData(e.Info) next := e.transactionSnapshot() next.history, next.historyIndex, next.historyLimit = nil, 0, 0 + pending := &EditorChanges{} + next.transactionChanges = pending *e = next committed := false defer func() { @@ -48,6 +83,11 @@ func (e *Editor) Transaction(edit func(*Editor) error) error { return err } changed := e.revision != before.revision + changes := *pending + if e.transactionChanges != pending { + changes = EditorChanges{All: true} + } + e.transactionChanges = before.transactionChanges e.history, e.historyIndex, e.historyLimit = before.history, before.historyIndex, before.historyLimit if !sameBackend(e.backends.FontResources, before.backends.FontResources) { e.clearFontResourceCache() @@ -62,7 +102,7 @@ func (e *Editor) Transaction(edit func(*Editor) error) error { } if changed { e.serial, e.revision = before.serial, before.revision - e.recordTransaction(before) + e.recordTransaction(before, changes) } committed = true return nil @@ -108,6 +148,7 @@ func (e *Editor) Undo() bool { e.historyIndex-- change := e.history[e.historyIndex] change.undo(e) + e.changes, e.objectIndex = change.changes, nil e.revision = change.before return true } @@ -120,6 +161,7 @@ func (e *Editor) Redo() bool { } change := e.history[e.historyIndex] change.redo(e) + e.changes, e.objectIndex = change.changes, nil e.historyIndex++ e.revision = change.after return true @@ -132,15 +174,12 @@ func (e *Editor) Revision() uint64 { return e.revision } -// transactionSnapshot 复制可变容器,保留只读XML、图元及二进制资源的共享 +// transactionSnapshot 复制顶层容器,共享按替换方式更新的页面图层、图元和二进制资源 // 返回: Editor 独立事务状态 func (e *Editor) transactionSnapshot() Editor { next := *e next.Info = cloneEditorData(e.Info) - next.pages = make([]PageContent, len(e.pages)) - for i, page := range e.pages { - next.pages[i] = copyEditorPage(page) - } + next.pages = slices.Clone(e.pages) next.resources = slices.Clone(e.resources) next.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID) next.origins = maps.Clone(e.origins) @@ -213,9 +252,13 @@ func (e *Editor) retainDocumentResources(source *Editor) { } // recordTransaction 记录事务快照,在各自资源视图中保留后续注册的资源 -// 入参: before 修改前的文档状态 -func (e *Editor) recordTransaction(before Editor) { - if change := e.recordChange(); change != nil { +// 入参: before 修改前的文档状态, changes 可选的事务变化范围 +func (e *Editor) recordTransaction(before Editor, changes ...EditorChanges) { + scope := EditorChanges{All: true} + if len(changes) != 0 { + scope = changes[0] + } + if change := e.recordChangeScope(scope); change != nil { after := e.transactionSnapshot() before.history, after.history = nil, nil change.undo = func(e *Editor) { @@ -230,10 +273,23 @@ func (e *Editor) recordTransaction(before Editor) { } // recordChange 分配修订标识并为已完成的有效修改预留记录 +// 入参: pages 受影响页码,省略表示全局变化 // 返回: *editorChange 记录,未启用历史时为nil -func (e *Editor) recordChange() *editorChange { +func (e *Editor) recordChange(pages ...int) *editorChange { + return e.recordChangeScope(EditorChanges{All: len(pages) == 0, Pages: slices.Clone(pages)}) +} + +// recordChangeScope 分配修订并记录变更范围,事务中同步合并 +// 入参: scope 变更范围 +// 返回: *editorChange 可补充回放操作的记录 +func (e *Editor) recordChangeScope(scope EditorChanges) *editorChange { + e.objectIndex = nil + e.changes = scope + if e.transactionChanges != nil { + e.transactionChanges.merge(scope) + } e.serial++ - change := editorChange{before: e.revision, after: e.serial} + change := editorChange{before: e.revision, after: e.serial, changes: scope} e.revision = e.serial if e.historyLimit == 0 { return nil diff --git a/ofdgo_writer.go b/ofdgo_writer.go index a8182d8..854e6d2 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -791,6 +791,14 @@ func (e *Editor) writePartsWithPages(ctx context.Context, write func(string, []b return nil } +// editorResourceSelection 保存单次输出中同一编辑快照的资源筛选结果 +type editorResourceSelection struct { + owner *Editor + fonts, images []editorResource + spaces []ColorSpace + files map[string]bool +} + // usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池 // 入参: ctx 取消上下文 // 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, map[string]bool 独立资源文件集合, error 取消错误 @@ -798,6 +806,10 @@ func (e *Editor) usedResources(ctx context.Context) (fonts, images []editorResou if err := ctx.Err(); err != nil { return nil, nil, nil, nil, err } + if e.output != nil && e.output.resources != nil && e.output.resources.owner == e { + cached := e.output.resources + return slices.Clone(cached.fonts), slices.Clone(cached.images), slices.Clone(cached.spaces), maps.Clone(cached.files), nil + } used := make(map[string]bool) promoted := make(map[string]bool) files := make(map[string]bool) @@ -945,6 +957,9 @@ func (e *Editor) usedResources(ctx context.Context) (fonts, images []editorResou if err := ctx.Err(); err != nil { return nil, nil, nil, nil, err } + if e.output != nil { + e.output.resources = &editorResourceSelection{owner: e, fonts: slices.Clone(fonts), images: slices.Clone(images), spaces: slices.Clone(spaces), files: maps.Clone(files)} + } return fonts, images, spaces, files, nil }