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开始