diff --git a/assets/webui/index.html b/assets/webui/index.html index 1aa4870..546f80f 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -332,6 +332,21 @@
+ +
+

文档管理

+
+
+ + + + + + +
+
+
+
@@ -594,19 +609,11 @@
文档
- +
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index a0dc6de..43bf632 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -1429,6 +1429,7 @@ body[data-hide-meta] .meta-panel { .viewer-panel, .search-results, .editor-toolbar, +.document-list, .meta-panel { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; @@ -1523,16 +1524,6 @@ body[data-hide-meta] .meta-panel { margin-bottom: 10px; } -.document-row .panel-actions { - flex-wrap: nowrap; -} - -.document-row .small-button { - width: 28px; - height: 28px; - padding: 0; -} - .document-select { flex: 1; width: 0; @@ -1542,6 +1533,81 @@ body[data-hide-meta] .meta-panel { font-size: 12px; } +.document-dialog { + width: 440px; +} + +.document-dialog .form-status:not(:empty) { + margin-top: 8px; +} + +.document-actions { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 6px; +} + +.document-actions > .button { + min-width: 0; + padding-inline: 6px; +} + +.document-list { + max-height: 45dvh; + min-height: 80px; + margin-bottom: 12px; + border: 1px solid var(--line); + overflow: auto; +} + +.document-entry { + display: grid; + grid-template-columns: minmax(0, 1fr) 4em; + align-items: center; + gap: 8px; + width: 100%; + min-height: 38px; + padding: 8px 10px; + border: 0; + border-bottom: 1px solid var(--line); + text-align: left; +} + +.document-entry:last-child { + border-bottom: 0; +} + +.document-entry:hover, +.document-entry[aria-selected="true"] { + background: var(--accent-soft); +} + +.document-entry[aria-selected="true"] { + color: var(--accent-strong); +} + +.document-entry:focus-visible { + outline-offset: -2px; +} + +.document-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.document-pages { + font-size: 12px; + color: var(--text-muted); + white-space: nowrap; +} + +.document-pages { + text-align: right; + font-variant-numeric: tabular-nums; +} + .app-panel { margin-bottom: 12px; padding-bottom: 12px; @@ -3127,6 +3193,10 @@ audio.page-media { grid-template-columns: 1fr; } + .document-actions { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .tool-group + .tool-group::before { display: none; } diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 272381c..8eb853e 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -60,6 +60,9 @@ const state = { imageImporting: false, createInsert: false, createPackage: false, + createDocumentAt: null, + documentManagement: null, + infoDocumentIndex: null, pageSelection: new Set(), pageSelectionAnchor: null, pageMultiSelect: false, @@ -235,8 +238,13 @@ const el = { infoButton: document.querySelector("#infoButton"), documentSelect: document.querySelector("#documentSelect"), documentActions: document.querySelector("#documentActions"), + documentManageButton: document.querySelector("#documentManageButton"), + documentPanel: document.querySelector("#documentPanel"), + documentForm: document.querySelector("#documentForm"), + documentList: document.querySelector("#documentList"), + documentStatus: document.querySelector("#documentStatus"), + documentClose: document.querySelector("#documentClose"), documentRow: document.querySelector("#documentRow"), - documentOrder: document.querySelector("#documentOrder"), documentAddButton: document.querySelector("#documentAddButton"), documentDeleteButton: document.querySelector("#documentDeleteButton"), documentUpButton: document.querySelector("#documentUpButton"), @@ -679,30 +687,54 @@ editorClick(el.paragraphButton, () => { }); el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close()); editorClick(el.documentAddButton, () => openCreatePanel(false, true)); +editorClick(el.documentManageButton, openDocumentManagement); +el.documentClose.addEventListener("click", () => el.documentPanel.close()); +el.documentPanel.addEventListener("cancel", event => { if (el.documentForm.inert) event.preventDefault(); }); +el.documentPanel.addEventListener("close", () => { + if (!el.documentPanel.open) { + state.documentManagement = null; + el.documentList.replaceChildren(); + el.documentStatus.textContent = ""; + } +}); +el.documentList.addEventListener("keydown", event => { + if (!state.documentManagement || !["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return; + event.preventDefault(); + const count = state.doc.documents.length, index = state.documentManagement.index; + selectManagedDocument(event.key === "Home" ? 0 : event.key === "End" ? count - 1 + : Math.max(0, Math.min(count - 1, index + (event.key === "ArrowUp" ? -1 : 1))), true); +}); editorClick(el.documentDeleteButton, () => { - if (window.confirm("删除当前文档?")) return changeDocument("ofdgoChangeDocument", null, "delete", state.doc.documentIndex || 0); + const index = managedDocumentIndex(), entry = state.doc.documents[index]; + if (window.confirm(`删除文档“${entry.title || "未命名"}”?`)) return changeDocument("ofdgoChangeDocument", null, "delete", index); }); editorClick(el.documentUpButton, () => { - const index = state.doc.documentIndex || 0; + const index = managedDocumentIndex(); return changeDocument("ofdgoChangeDocument", null, "move", index, index - 1); }); editorClick(el.documentDownButton, () => { - const index = state.doc.documentIndex || 0; + const index = managedDocumentIndex(); return changeDocument("ofdgoChangeDocument", null, "move", index, index + 1); }); editorClick(el.infoButton, () => { - for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = state.doc[key.toLowerCase()] || ""; + state.infoDocumentIndex = managedDocumentIndex(); + const info = state.documentManagement?.entries?.[state.infoDocumentIndex] || state.doc; + for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = info[key.toLowerCase()] || ""; el.infoCustomRows.replaceChildren(); - for (const [index, field] of (state.doc.customData || []).entries()) addCustomDataRow(field, index); + for (const [index, field] of (info.customData || []).entries()) addCustomDataRow(field, index); el.infoStatus.textContent = ""; el.infoPanel.showModal(); }); el.infoCancel.addEventListener("click", () => el.infoPanel.close()); +el.infoPanel.addEventListener("cancel", event => { if (el.infoForm.inert) event.preventDefault(); }); +el.infoPanel.addEventListener("close", () => { if (!el.infoPanel.open) state.infoDocumentIndex = null; }); el.infoCustomAdd.addEventListener("click", () => addCustomDataRow()); el.infoForm.addEventListener("submit", async event => { event.preventDefault(); const fields = [...el.infoCustomRows.children].map(row => ({Name: row.children[0].value, Value: row.children[1].value, Source: row.sourceIndex})); - if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields))) el.infoPanel.close(); + const args = [el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields)]; + if (state.infoDocumentIndex !== null) args.push(state.infoDocumentIndex); + if (await changeDocument("ofdgoUpdateInfo", null, ...args)) el.infoPanel.close(); }); function addCustomDataRow(field = {Name: "", Value: ""}, sourceIndex) { @@ -1529,7 +1561,7 @@ function handleKeyDown(event) { } function formDialogOpen() { - return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open + return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open || el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open; } @@ -1956,6 +1988,93 @@ function setEditorInfo(doc) { setDirty(state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision); } +function managedDocumentIndex() { + return state.documentManagement?.index ?? state.doc?.documentIndex ?? 0; +} + +function documentEntryKey(entry) { + return JSON.stringify([entry?.root, entry?.id]); +} + +async function openDocumentManagement() { + if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return; + const index = state.doc.documentIndex || 0; + state.documentManagement = { index, key: documentEntryKey(state.doc.documents[index]), entries: null, loading: false, failed: false }; + renderDocumentManagement(); + el.documentPanel.showModal(); + await refreshDocumentManagement(true); +} + +async function refreshDocumentManagement(focus = false) { + const manager = state.documentManagement, openSeq = state.openSeq; + if (!manager || !el.documentPanel.open) return; + manager.loading = true; + manager.failed = false; + el.documentStatus.textContent = "正在读取文档"; + updateDocumentControls(); + try { + const entries = await callWASM("ofdgoPackageDocuments"); + if (state.documentManagement !== manager || !el.documentPanel.open) return; + if (openSeq !== state.openSeq) return await refreshDocumentManagement(focus); + const index = entries.findIndex(entry => documentEntryKey(entry) === manager.key); + manager.index = index < 0 ? Math.min(manager.index, entries.length - 1) : index; + manager.entries = entries; + manager.key = documentEntryKey(entries[manager.index]); + el.documentStatus.textContent = ""; + renderDocumentManagement(); + if (focus) el.documentList.children[manager.index]?.focus(); + } catch (error) { + if (state.documentManagement === manager && openSeq === state.openSeq) { + manager.failed = true; + el.documentStatus.textContent = error.message; + } + } finally { + if (state.documentManagement === manager) { + manager.loading = false; + updateDocumentControls(); + } + } +} + +function renderDocumentManagement() { + const manager = state.documentManagement; + if (!manager) return; + const entries = manager.entries || state.doc.documents; + el.documentList.replaceChildren(...entries.map(entry => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "document-entry"; + button.setAttribute("role", "option"); + button.setAttribute("aria-selected", String(entry.index === manager.index)); + button.tabIndex = entry.index === manager.index ? 0 : -1; + const name = document.createElement("span"); + name.className = "document-name"; + name.textContent = `${entry.index + 1} · ${entry.title || "未命名"}`; + button.title = name.textContent; + const pages = document.createElement("span"); + pages.className = "document-pages"; + const count = entry.pageCount ?? (entry.index === state.doc.documentIndex ? state.doc.pageCount : null); + pages.textContent = count === null ? "-" : `${count}页`; + button.append(name, pages); + button.addEventListener("click", () => selectManagedDocument(entry.index)); + return button; + })); + updateDocumentControls(); +} + +function selectManagedDocument(index, focus = false) { + const manager = state.documentManagement; + if (!manager || manager.loading || document.body.hasAttribute("aria-busy") || index < 0 || index >= state.doc.documents.length) return; + manager.index = index; + manager.key = documentEntryKey(state.doc.documents[index]); + for (const [i, button] of [...el.documentList.children].entries()) { + button.setAttribute("aria-selected", String(i === index)); + button.tabIndex = i === index ? 0 : -1; + } + updateDocumentControls(); + if (focus) el.documentList.children[index]?.focus(); +} + async function switchDocument(index) { if (!state.doc || document.body.hasAttribute("aria-busy") || state.exporting || formDialogOpen()) return; const previous = state.doc.documentIndex || 0; @@ -2087,9 +2206,10 @@ function openCreatePanel(insert = false, packageDocument = false) { el.createForm.reset(); state.createInsert = insert === true; state.createPackage = packageDocument === true; + state.createDocumentAt = state.createPackage ? managedDocumentIndex() + 1 : null; el.createMode.value = state.createInsert ? "images" : "blank"; - el.createSubmit.textContent = state.createInsert ? "插入" : "新建"; - el.createPanel.setAttribute("aria-label", state.createInsert ? "插入图片" : "新建文档"); + el.createSubmit.textContent = state.createInsert ? "插入" : state.createPackage ? "新增" : "新建"; + el.createPanel.setAttribute("aria-label", state.createInsert ? "插入图片" : state.createPackage ? "新增文档" : "新建文档"); el.createCancel.disabled = false; updateCreateOptions(); el.createStatus.textContent = ""; @@ -2130,7 +2250,7 @@ async function createDocument(event) { event.preventDefault(); if (state.createPackage) { if (await changeDocument("ofdgoChangeDocument", null, "add", el.createName.value.trim() || "未命名", - Number(el.createWidth.value), Number(el.createHeight.value), (state.doc.documentIndex || 0) + 1)) el.createPanel.close(); + Number(el.createWidth.value), Number(el.createHeight.value), state.createDocumentAt)) el.createPanel.close(); return; } if (state.createInsert) { @@ -2884,6 +3004,7 @@ async function changeDocument(name, item, ...args) { const restoring = name === "ofdgoUndo" || name === "ofdgoRedo"; const { scrollLeft, scrollTop } = el.viewerPanel; const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages"; + let refreshManagement = false; state.imageImporting = name === "ofdgoImportImages"; if (importing) state.importing = true; setBusy(true, importing ? "正在导入页面" : "", null); @@ -2916,12 +3037,16 @@ async function changeDocument(name, item, ...args) { if (doc.revision === state.editorInfo?.revision) { return true; } + refreshManagement = el.documentPanel.open && (name === "ofdgoChangeDocument" || name === "ofdgoUpdateInfo" || restoring); const previousDocument = state.doc.documents?.[state.doc.documentIndex || 0]; const nextDocument = doc.documents?.[doc.documentIndex || 0]; const documentChanged = previousDocument?.root !== nextDocument?.root || previousDocument?.id !== nextDocument?.id; const packageChanged = documentChanged || name === "ofdgoChangeDocument" || restoring && JSON.stringify(doc.documents?.map(entry => [entry.root, entry.id])) !== JSON.stringify(state.doc.documents?.map(entry => [entry.root, entry.id])); if (packageChanged) { + if (state.documentManagement && name === "ofdgoChangeDocument" && args[0] === "add") { + state.documentManagement.key = documentEntryKey(nextDocument); + } resetCompositeScope(); state.objectClipboard = state.styleClipboard = null; state.pageSelection.clear(); @@ -3010,6 +3135,7 @@ async function changeDocument(name, item, ...args) { return true; } } catch (err) { + refreshManagement = false; if (openSeq === state.openSeq) { if (el.annotationCreate.open) { el.annotationCreateStatus.textContent = err.message; @@ -3037,6 +3163,8 @@ async function changeDocument(name, item, ...args) { el.createStatus.textContent = err.message; } else if (el.importPanel.open) { el.importStatus.textContent = err.message; + } else if (el.documentPanel.open) { + el.documentStatus.textContent = err.message; } else { showError(err, false); } @@ -3052,6 +3180,7 @@ async function changeDocument(name, item, ...args) { } if (openSeq === state.openSeq) { setBusy(false); + if (refreshManagement) await refreshDocumentManagement(); if (toolbarFocus?.isConnected && !toolbarFocus.disabled) toolbarFocus.focus({ preventScroll: true }); } } @@ -3722,6 +3851,9 @@ async function openOFD(file, remote = null) { el.pagePanel.close(); el.paragraphPanel.close(); el.infoPanel.close(); + el.documentPanel.close(); + state.documentManagement = null; + state.infoDocumentIndex = null; el.annotationNote.close(); el.annotationCreate.close(); el.objectPicker.close(); @@ -8305,15 +8437,20 @@ function updateFitSpace() { } function updateDocumentControls() { - const count = state.doc?.documents?.length || 0, index = state.doc?.documentIndex || 0; - const disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy"); - el.documentActions.hidden = el.documentOrder.hidden = !state.editing; + const count = state.doc?.documents?.length || 0, index = managedDocumentIndex(), manager = state.documentManagement; + const busy = document.body.hasAttribute("aria-busy"); + const disabled = !state.doc || !state.ready || state.exporting || busy; + el.documentManageButton.hidden = !state.editing; + el.documentManageButton.disabled = disabled; el.documentRow.hidden = !count || !state.editing && count < 2; el.documentSelect.disabled = disabled; - el.documentAddButton.disabled = el.infoButton.disabled = disabled || !state.editing; - el.documentDeleteButton.disabled = disabled || !state.editing || count <= 1; - el.documentUpButton.disabled = disabled || !state.editing || index === 0; - el.documentDownButton.disabled = disabled || !state.editing || index >= count - 1; + const actionsDisabled = disabled || !state.editing || Boolean(manager?.loading || manager?.failed); + el.documentList.inert = busy || Boolean(manager?.loading); + el.documentAddButton.disabled = actionsDisabled; + el.infoButton.disabled = actionsDisabled || Boolean(manager && !manager.entries); + el.documentDeleteButton.disabled = actionsDisabled || count <= 1; + el.documentUpButton.disabled = actionsDisabled || index === 0; + el.documentDownButton.disabled = actionsDisabled || index >= count - 1; } function updateControls() { @@ -8355,7 +8492,6 @@ function updateEditorTools() { el.editButton.title = state.editing ? "阅读" : "编辑"; el.editButton.setAttribute("aria-label", el.editButton.title); el.editNotice.textContent = state.editorInfo?.editWarnings?.join(";") || ""; - el.infoButton.hidden = !state.editing; el.editNotice.hidden = !state.editing || !el.editNotice.textContent; el.editorTools.hidden = !state.editing; const pagesDisabled = !state.editing || !state.ready || state.exporting; @@ -8535,6 +8671,7 @@ function setBusy(busy, text = "", percent = 0, status = "") { el.pageForm.inert = busy; el.paragraphForm.inert = busy; el.infoForm.inert = busy; + el.documentForm.inert = busy; el.attachmentForm.inert = busy; el.attachmentPanel.inert = busy; el.annotationForm.inert = busy; diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 343052b..d2297ef 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -186,6 +186,7 @@ func RunWASM() { registerCallback("ofdgoOpen", openDocument) registerCallback("ofdgoSelectDocument", selectDocument) registerCallback("ofdgoChangeDocument", changePackageDocument) + registerCallback("ofdgoPackageDocuments", packageDocuments) registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument) registerAsyncCallback("ofdgoConvertFile", convertFile) registerAsyncCallback("ofdgoPackFiles", packFiles) @@ -3285,11 +3286,21 @@ func createDocument(args []js.Value) (any, error) { } // updateInfo 修改文档信息和自定义字段,保留其他元数据及创建程序标识 -// 入参: args 标题、作者、主题、可选自定义字段JSON +// 入参: args 标题、作者、主题、可选自定义字段JSON及文档索引 // 返回: any 编辑状态, error 错误信息 func updateInfo(args []js.Value) (any, error) { return changeObjects(func() error { - info := currentEditor.Info + if len(args) < 3 || len(args) > 5 { + return fmt.Errorf("invalid document info arguments") + } + index := currentEditor.DocumentIndex() + if len(args) > 4 { + index = args[4].Int() + } + info, err := currentEditor.DocumentInfo(index) + if err != nil { + return err + } info.Title, info.Author, info.Subject = args[0].String(), args[1].String(), args[2].String() if len(args) > 3 { var submitted []struct { @@ -3317,11 +3328,40 @@ func updateInfo(args []js.Value) (any, error) { info.CustomDatas = &ofdgo.CustomDatas{CustomData: fields} } } - currentEditor.SetInfo(info) - return nil + return currentEditor.SetDocumentInfo(index, info) }) } +// packageDocuments 按需读取管理列表的元数据和页数,不切换当前文档或解析页面内容 +// 入参: args 保留参数 +// 返回: any 文档列表, error 编辑状态错误 +func packageDocuments(args []js.Value) (any, error) { + if currentSession == nil || currentEditor == nil { + return nil, fmt.Errorf("no document is being edited") + } + entries := make([]DocumentEntry, currentEditor.DocumentCount()) + for i := range entries { + info, err := currentEditor.DocumentInfo(i) + if err != nil { + return nil, err + } + entry := &entries[i] + body := currentSession.Reader.OFD.DocBody[i] + entry.Index, entry.Root, entry.ID = i, body.DocRoot, info.DocID + entry.Title, entry.Author, entry.Subject = info.Title, info.Author, info.Subject + if info.CustomDatas != nil { + entry.CustomData = info.CustomDatas.CustomData + } + if i == currentEditor.DocumentIndex() { + count := currentEditor.PageCount() + entry.PageCount = &count + } else if count, err := currentSession.Reader.DocumentPageCount(i); err == nil { + entry.PageCount = &count + } + } + return entries, nil +} + // changeAttachment 编辑附件,逐个读取文件并复用库层事务和历史 // 入参: args 操作、附件标识、名称列表、文件读取回调 // 返回: any 编辑状态, error 错误信息 diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 51056ba..dd7bfae 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -98,12 +98,16 @@ type DocumentInfo struct { DetailsPending bool `json:"detailsPending,omitempty"` } -// DocumentEntry 包内文档入口,不预先解析其他文档的页面和资源 +// DocumentEntry 包内文档入口,管理列表按需补充元数据和页数 type DocumentEntry struct { - Index int `json:"index"` - Title string `json:"title"` - Root string `json:"root"` - ID string `json:"id"` + Index int `json:"index"` + Title string `json:"title"` + Root string `json:"root"` + ID string `json:"id"` + Author string `json:"author,omitempty"` + Subject string `json:"subject,omitempty"` + CustomData []ofdgo.CustomData `json:"customData,omitempty"` + PageCount *int `json:"pageCount,omitempty"` } // DocumentDetails 不含页面数组的文档补充信息 diff --git a/ofdgo_editor_documents.go b/ofdgo_editor_documents.go index eec3353..2b86f43 100644 --- a/ofdgo_editor_documents.go +++ b/ofdgo_editor_documents.go @@ -23,6 +23,7 @@ import ( "io" "maps" "path" + "reflect" "slices" "strings" ) @@ -45,6 +46,50 @@ func (e *Editor) DocumentIndex() int { return e.source.reader.DocumentIndex() } +// DocumentInfo 获取指定文档信息的独立副本,不切换当前文档 +// 入参: index 文档索引,从0开始 +// 返回: DocInfo 文档信息, error 索引错误 +func (e *Editor) DocumentInfo(index int) (DocInfo, error) { + if index < 0 || index >= e.DocumentCount() { + return DocInfo{}, fmt.Errorf("document index %d out of range", index) + } + if index == e.DocumentIndex() { + return cloneEditorData(e.Info), nil + } + info := cloneEditorData(e.source.reader.OFD.DocBody[index].DocInfo) + if info.CustomDatas != nil { + for i := range info.CustomDatas.CustomData { + info.CustomDatas.CustomData[i].sourceIndex = i + 1 + } + } + return info, nil +} + +// SetDocumentInfo 修改指定文档信息,保留扩展内容,不切换当前文档,可通过Undo撤销 +// 入参: index 文档索引,从0开始, info 文档信息 +// 返回: error 修改错误 +func (e *Editor) SetDocumentInfo(index int, info DocInfo) error { + before, err := e.DocumentInfo(index) + if err != nil { + return err + } + if reflect.DeepEqual(before, info) { + return nil + } + if index == e.DocumentIndex() { + e.SetInfo(info) + return nil + } + return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) { + updated, err := editorUpdateInfoXML(data, NewEditor().Info.DocID, index, before, info) + if err != nil { + return 0, err + } + reader.files[reader.fileNamesFold["ofd.xml"]] = updated + return reader.DocumentIndex(), nil + }) +} + // AddDocument 在包内插入含一页的空白文档并选中,不修改输入包 // 当前文档的修改保留,新文档使用独立目录和文档标识;可通过Undo撤销 // 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量 @@ -116,26 +161,12 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int, // 入参: index 文档索引,从0开始, title 标题,空值移除标题 // 返回: error 修改错误 func (e *Editor) RenameDocument(index int, title string) error { - if index < 0 || index >= e.DocumentCount() { - return fmt.Errorf("document index %d out of range", index) + info, err := e.DocumentInfo(index) + if err != nil { + return err } - if index == e.DocumentIndex() { - info := e.Info - info.Title = title - e.SetInfo(info) - return nil - } - if e.source.reader.OFD.DocBody[index].DocInfo.Title == title { - return nil - } - return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) { - data, info, err := editorDocInfoXML(data, NewEditor().Info.DocID, index) - if err != nil { - return 0, err - } - reader.files[reader.fileNamesFold["ofd.xml"]] = editorXMLSetText(data, info, [][2]string{{"Title", title}}) - return reader.DocumentIndex(), nil - }) + info.Title = title + return e.SetDocumentInfo(index, info) } // MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销 diff --git a/ofdgo_editor_metadata.go b/ofdgo_editor_metadata.go index 9e3eb69..3c84edd 100644 --- a/ofdgo_editor_metadata.go +++ b/ofdgo_editor_metadata.go @@ -29,16 +29,23 @@ func (e *Editor) sourceInfoXML() ([]byte, error) { if err != nil { return nil, err } - data, info, err := editorDocInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex) + return editorUpdateInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex, e.source.info, e.Info) +} + +// editorUpdateInfoXML 更新指定文档的描述和自定义字段,保留未修改内容 +// 入参: data 根索引XML, docID 缺少描述时使用的标识, index 文档索引, before 原信息, after 新信息 +// 返回: []byte 修改后的XML, error 解析错误 +func editorUpdateInfoXML(data []byte, docID string, index int, before, after DocInfo) ([]byte, error) { + data, info, err := editorDocInfoXML(data, docID, index) if err != nil { return nil, err } - before, after := e.source.info, e.Info var values [][2]string for _, field := range [][3]string{ {"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author}, {"Subject", before.Subject, after.Subject}, {"Abstract", before.Abstract, after.Abstract}, {"CreationDate", before.CreationDate, after.CreationDate}, {"ModDate", before.ModDate, after.ModDate}, + {"Creator", before.Creator, after.Creator}, {"CreatorVersion", before.CreatorVersion, after.CreatorVersion}, } { if field[1] != field[2] { values = append(values, [2]string{field[0], field[2]}) @@ -50,7 +57,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) { if err != nil { return nil, err } - return editorCustomDatas(data, root.childAt("DocBody", e.source.reader.documentIndex).child("DocInfo"), after.CustomDatas) + return editorCustomDatas(data, root.childAt("DocBody", index).child("DocInfo"), after.CustomDatas) } return data, nil } diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 56626ac..37fdbba 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -83,6 +83,30 @@ func (r *Reader) DocumentIndex() int { return r.documentIndex } +// DocumentPageCount 获取指定文档页数,不切换当前文档或加载资源、注释和签名 +// 入参: index 文档索引,从0开始 +// 返回: int 页数, error 错误信息 +func (r *Reader) DocumentPageCount(index int) (int, error) { + if index < 0 || index >= r.DocumentCount() { + return 0, fmt.Errorf("document index %d out of range", index) + } + if index == r.documentIndex && r.doc != nil { + return len(r.doc.Pages.Page), nil + } + data, err := r.readFile(r.OFD.DocBody[index].DocRoot) + if err != nil { + return 0, err + } + var doc struct { + XMLName xml.Name `xml:"Document"` + Pages Pages `xml:"Pages"` + } + if err := xml.Unmarshal(data, &doc); err != nil { + return 0, fmt.Errorf("failed to unmarshal document.xml: %w", err) + } + return len(doc.Pages.Page), nil +} + // Document 创建指定文档的独立阅读器,不改变当前文档或混用资源缓存 // 返回的阅读器不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器 // 入参: index 文档索引,从0开始