mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
be8465884a
commit
87334eb84c
5 files changed
+462
-23
No files matched your search
@@ -344,9 +344,10 @@
|
||||
<form id="importForm">
|
||||
<div class="form-row"><label for="importFile">文档</label><input id="importFile" type="file" accept=".ofd" required autofocus></div>
|
||||
<div class="form-row"><label for="importRange">范围</label><select id="importRange"><option value="all">全部</option><option value="custom">指定</option></select></div>
|
||||
<div id="importPagesRow" class="form-row" hidden><label for="importPages">页码</label><input id="importPages" type="text" autocomplete="off" spellcheck="false" disabled></div>
|
||||
<div class="form-row"><label for="importPosition">位置</label><select id="importPosition"><option value="before">页前</option><option value="after" selected>页后</option><option value="first">开头</option><option value="last">末尾</option></select></div>
|
||||
<div class="form-row"><label for="importOutlines">目录</label><input id="importOutlines" type="checkbox" title="导入对应目录"></div>
|
||||
<div id="importDocuments" class="document-list" hidden></div>
|
||||
<div id="importPagesRow" class="form-row" hidden><label id="importPagesLabel" for="importPages">页码</label><input id="importPages" type="text" autocomplete="off" spellcheck="false" disabled></div>
|
||||
<div id="importPositionRow" class="form-row"><label for="importPosition">位置</label><select id="importPosition"><option value="before">页前</option><option value="after" selected>页后</option><option value="first">开头</option><option value="last">末尾</option></select></div>
|
||||
<div id="importOutlinesRow" class="form-row"><label for="importOutlines">目录</label><input id="importOutlines" type="checkbox" title="导入对应目录"></div>
|
||||
<div class="form-actions"><div id="importStatus" class="form-status"></div><button id="importCancel" class="button" type="button">取消</button><button id="importSubmit" class="button" type="submit" disabled>插入</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -397,6 +398,7 @@
|
||||
<div id="documentList" class="document-list"></div>
|
||||
<div id="documentActions" class="document-actions">
|
||||
<button id="documentAddButton" class="button" type="button">新增</button>
|
||||
<button id="documentImportButton" class="button" type="button">导入</button>
|
||||
<button id="infoButton" class="button" type="button">修改</button>
|
||||
<button id="documentDeleteButton" class="button" type="button">删除</button>
|
||||
<button id="documentUpButton" class="button" type="button">前移</button>
|
||||
|
||||
+50
-14
@@ -64,6 +64,8 @@ const state = {
|
||||
encryptionDirty: false,
|
||||
insertObject: null,
|
||||
importPageCount: 0,
|
||||
importDocumentCount: 0,
|
||||
importDocumentMode: false,
|
||||
importEncrypted: false,
|
||||
importing: false,
|
||||
imageImporting: false,
|
||||
@@ -263,6 +265,7 @@ const el = {
|
||||
documentClose: document.querySelector("#documentClose"),
|
||||
documentRow: document.querySelector("#documentRow"),
|
||||
documentAddButton: document.querySelector("#documentAddButton"),
|
||||
documentImportButton: document.querySelector("#documentImportButton"),
|
||||
documentDeleteButton: document.querySelector("#documentDeleteButton"),
|
||||
documentUpButton: document.querySelector("#documentUpButton"),
|
||||
documentDownButton: document.querySelector("#documentDownButton"),
|
||||
@@ -305,7 +308,11 @@ const el = {
|
||||
importRange: document.querySelector("#importRange"),
|
||||
importPagesRow: document.querySelector("#importPagesRow"),
|
||||
importPages: document.querySelector("#importPages"),
|
||||
importPagesLabel: document.querySelector("#importPagesLabel"),
|
||||
importDocuments: document.querySelector("#importDocuments"),
|
||||
importPosition: document.querySelector("#importPosition"),
|
||||
importPositionRow: document.querySelector("#importPositionRow"),
|
||||
importOutlinesRow: document.querySelector("#importOutlinesRow"),
|
||||
importOutlines: document.querySelector("#importOutlines"),
|
||||
importStatus: document.querySelector("#importStatus"),
|
||||
importCancel: document.querySelector("#importCancel"),
|
||||
@@ -757,6 +764,7 @@ editorClick(el.paragraphButton, () => {
|
||||
});
|
||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||
editorClick(el.documentAddButton, () => openCreatePanel(false, true));
|
||||
editorClick(el.documentImportButton, () => openImportPanel(true));
|
||||
editorClick(el.documentManageButton, openDocumentManagement);
|
||||
resourceElements.ManageButton.addEventListener("click", openResourceManagement);
|
||||
resourceElements.Close.addEventListener("click", () => resourceElements.Panel.close());
|
||||
@@ -1149,6 +1157,8 @@ el.importPanel.addEventListener("cancel", event => {
|
||||
});
|
||||
el.importPanel.addEventListener("close", () => {
|
||||
state.importPageCount = 0;
|
||||
state.importDocumentCount = 0;
|
||||
state.importDocumentMode = false;
|
||||
state.importEncrypted = false;
|
||||
el.importForm.reset();
|
||||
if (state.ready) callWASM("ofdgoLoadImport", null).catch(err => showError(err, false));
|
||||
@@ -1163,14 +1173,17 @@ el.importRange.addEventListener("change", () => {
|
||||
});
|
||||
el.importForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (document.body.hasAttribute("data-busy") || !state.importPageCount || !el.importForm.reportValidity()) return;
|
||||
const position = { before:state.pageIndex, after:state.pageIndex+1, first:0, last:state.doc.pageCount }[el.importPosition.value];
|
||||
if (document.body.hasAttribute("data-busy") || !(state.importDocumentMode ? state.importDocumentCount : state.importPageCount) || !el.importForm.reportValidity()) return;
|
||||
const position = state.importDocumentMode ? managedDocumentIndex() + 1 : { before:state.pageIndex, after:state.pageIndex+1, first:0, last:state.doc.pageCount }[el.importPosition.value];
|
||||
const options = [];
|
||||
if (state.importEncrypted) {
|
||||
if (!window.confirm(state.doc.encryption?.encrypted ? "按目标文档的加密策略导入,是否继续?" : "目标未加密,导入内容将按明文保存,是否继续?")) return;
|
||||
options.push(true);
|
||||
}
|
||||
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked, ...options);
|
||||
const range = el.importRange.value === "custom" ? el.importPages.value : "";
|
||||
const saved = state.importDocumentMode
|
||||
? await changeDocument("ofdgoImportDocuments", null, range, position, state.importEncrypted)
|
||||
: await changeDocument("ofdgoImportPages", null, range, position, el.importOutlines.checked, ...options);
|
||||
if (saved) el.importPanel.close();
|
||||
});
|
||||
editorClick(el.copyPageButton, () => changeSelectedPages("copy"));
|
||||
@@ -2149,11 +2162,21 @@ function remapOutlineExpansion(name, args, next) {
|
||||
walk(roots, (item, key) => state.outlineExpanded.set(key, Boolean(expanded.get(item))));
|
||||
}
|
||||
|
||||
function openImportPanel() {
|
||||
function openImportPanel(documents = false) {
|
||||
if (!state.editing || document.body.hasAttribute("data-busy")) return;
|
||||
state.importPageCount = 0;
|
||||
state.importDocumentCount = 0;
|
||||
state.importDocumentMode = documents;
|
||||
state.importEncrypted = false;
|
||||
el.importForm.reset();
|
||||
el.importStatus.textContent = "";
|
||||
el.importDocuments.replaceChildren();
|
||||
el.importDocuments.hidden = !documents;
|
||||
el.importPagesLabel.textContent = documents ? "文档" : "页码";
|
||||
el.importPages.removeAttribute("title");
|
||||
el.importPositionRow.hidden = documents;
|
||||
el.importOutlinesRow.hidden = documents;
|
||||
el.importSubmit.textContent = documents ? "导入" : "插入";
|
||||
el.importSubmit.disabled = true;
|
||||
el.importPagesRow.hidden = true;
|
||||
el.importPages.disabled = true;
|
||||
@@ -2165,6 +2188,8 @@ async function loadImportFile() {
|
||||
const file = el.importFile.files[0];
|
||||
const openSeq = state.openSeq;
|
||||
state.importPageCount = 0;
|
||||
state.importDocumentCount = 0;
|
||||
el.importDocuments.replaceChildren();
|
||||
state.importEncrypted = false;
|
||||
el.importSubmit.disabled = true;
|
||||
el.importStatus.textContent = "";
|
||||
@@ -2178,10 +2203,20 @@ async function loadImportFile() {
|
||||
return;
|
||||
}
|
||||
state.importPageCount = info?.pageCount || 0;
|
||||
state.importDocumentCount = info?.documentCount || 0;
|
||||
state.importEncrypted = Boolean(info?.encrypted);
|
||||
if (state.importDocumentMode) {
|
||||
el.importDocuments.replaceChildren(...(info?.documents || []).map(entry => {
|
||||
const row = document.createElement("div");
|
||||
row.textContent = `${entry.index + 1} · ${entry.title || "未命名"}`;
|
||||
return row;
|
||||
}));
|
||||
el.importStatus.textContent = `共${state.importDocumentCount}个文档${info?.documentsSigned || state.doc.signatureCount ? " · 合并后需验签" : ""}`;
|
||||
} else {
|
||||
el.importPages.title = state.importPageCount ? `页码 1-${state.importPageCount}` : "页码";
|
||||
el.importStatus.textContent = info?.signed ? "保留签章外观,原签名不再有效" : "";
|
||||
el.importSubmit.disabled = !state.importPageCount;
|
||||
}
|
||||
el.importSubmit.disabled = !(state.importDocumentMode ? state.importDocumentCount : state.importPageCount);
|
||||
} catch (err) {
|
||||
if (openSeq === state.openSeq && el.importPanel.open) el.importStatus.textContent = err.message;
|
||||
} finally {
|
||||
@@ -3590,12 +3625,12 @@ async function changeDocument(name, item, ...args) {
|
||||
const restoring = name === "ofdgoUndo" || name === "ofdgoRedo";
|
||||
if (restoring) canvasEditor.richClipboard = null;
|
||||
const { scrollLeft, scrollTop } = el.viewerPanel;
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages";
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
|
||||
const changingFont = name === "ofdgoChangeFontBinding";
|
||||
let refreshManagement = false;
|
||||
state.imageImporting = name === "ofdgoImportImages";
|
||||
if (importing) state.importing = true;
|
||||
setBusy(true, importing ? "正在导入页面" : "", null);
|
||||
setBusy(true, importing ? name === "ofdgoImportDocuments" ? "正在导入文档" : "正在导入页面" : "", null);
|
||||
const progressTimer = changingFont ? window.setTimeout(() => {
|
||||
if (openSeq === state.openSeq && document.body.hasAttribute("data-busy")) {
|
||||
el.progressPanel.hidden = false;
|
||||
@@ -3647,17 +3682,17 @@ async function changeDocument(name, item, ...args) {
|
||||
}
|
||||
const restoredView = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null;
|
||||
if (restoredView?.reading && state.savedRevision === revision) state.savedRevision = doc.revision;
|
||||
refreshManagement = el.documentPanel.open && (name === "ofdgoChangeDocument" || name === "ofdgoUpdateInfo" || restoring);
|
||||
refreshManagement = el.documentPanel.open && (name === "ofdgoChangeDocument" || name === "ofdgoImportDocuments" || name === "ofdgoUpdateInfo" || restoring);
|
||||
const previousDocument = state.doc.documents?.[state.doc.documentIndex || 0];
|
||||
const nextDocument = doc.documents?.[doc.documentIndex || 0];
|
||||
const sameDocument = previousDocument?.id && previousDocument.id === nextDocument?.id && state.doc.documentIndex === doc.documentIndex;
|
||||
const documentChanged = (state.doc.documentVersion || "") !== (doc.documentVersion || "")
|
||||
|| previousDocument?.id !== nextDocument?.id || previousDocument?.root !== nextDocument?.root
|
||||
&& !(sameDocument && (name === "ofdgoChangeFontBinding" || restoring));
|
||||
const packageChanged = documentChanged || name === "ofdgoChangeDocument" || restoring
|
||||
const packageChanged = documentChanged || name === "ofdgoChangeDocument" || name === "ofdgoImportDocuments" || 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") {
|
||||
if (state.documentManagement && (name === "ofdgoImportDocuments" || name === "ofdgoChangeDocument" && args[0] === "add")) {
|
||||
state.documentManagement.key = documentEntryKey(nextDocument);
|
||||
}
|
||||
resetCompositeScope();
|
||||
@@ -4326,8 +4361,8 @@ async function loadWASM() {
|
||||
return;
|
||||
}
|
||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.importing && !el.importCancel.disabled) {
|
||||
const label = { ids: "正在检查标识", pages: "正在导入页面", resources: "正在读取资源", commit: "正在完成导入" }[data.phase];
|
||||
el.importStatus.textContent = data.total ? `${label} · ${data.completed} / ${data.total} 页` : label;
|
||||
const label = { ids: "正在检查标识", documents: "正在导入文档", pages: "正在导入页面", resources: "正在读取资源", commit: "正在完成导入" }[data.phase] || "正在导入文档";
|
||||
el.importStatus.textContent = data.total ? `${label} · ${data.completed} / ${data.total}${state.importDocumentMode ? "" : " 页"}` : label;
|
||||
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
||||
if (data.phase === "commit") el.importCancel.disabled = el.cancelExportButton.disabled = true;
|
||||
}
|
||||
@@ -11717,6 +11752,7 @@ function updateDocumentControls() {
|
||||
const actionsDisabled = disabled || !state.editing || Boolean(manager?.loading || manager?.failed);
|
||||
el.documentList.inert = busy || Boolean(manager?.loading);
|
||||
el.documentAddButton.disabled = actionsDisabled;
|
||||
el.documentImportButton.disabled = actionsDisabled;
|
||||
el.infoButton.disabled = actionsDisabled || Boolean(manager && !manager.entries);
|
||||
el.documentDeleteButton.disabled = actionsDisabled || count <= 1;
|
||||
el.documentUpButton.disabled = actionsDisabled || index === 0;
|
||||
@@ -11916,7 +11952,7 @@ async function callWASM(name, ...args) {
|
||||
request.fontData = new Map();
|
||||
args.push([...request.localFonts.keys()]);
|
||||
}
|
||||
if (name === "ofdgoImportImages" || name === "ofdgoExportImages" || name === "ofdgoConvertPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoExportResource" || name === "ofdgoExportVersion" || name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
|
||||
if (name === "ofdgoImportDocuments" || name === "ofdgoImportImages" || name === "ofdgoExportImages" || name === "ofdgoConvertPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoExportResource" || name === "ofdgoExportVersion" || name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
|
||||
state.exportRequestID = id;
|
||||
el.cancelExportButton.hidden = false;
|
||||
el.cancelExportButton.disabled = false;
|
||||
@@ -11964,7 +12000,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
el.importForm.inert = busy && !state.importing;
|
||||
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
|
||||
el.importPages.disabled = busy || el.importRange.value !== "custom";
|
||||
el.importSubmit.disabled = busy || !state.importPageCount;
|
||||
el.importSubmit.disabled = busy || !(state.importDocumentMode ? state.importDocumentCount : state.importPageCount);
|
||||
el.batchPagesForm.inert = busy;
|
||||
el.objectStyleForm.inert = busy;
|
||||
el.objectBoundsForm.inert = busy;
|
||||
|
||||
@@ -30,7 +30,7 @@ self.onmessage = ({ data }) => {
|
||||
return;
|
||||
}
|
||||
if (data.name === "ofdgoResources") operations.set(data.id, new AbortController());
|
||||
if (data.name === "ofdgoImportImages" || data.name === "ofdgoExportImages" || data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" || data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoExportResource" || data.name === "ofdgoExportMedia" || data.name === "ofdgoExportVersion" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
|
||||
if (data.name === "ofdgoImportDocuments" || data.name === "ofdgoImportImages" || data.name === "ofdgoExportImages" || data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" || data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoExportResource" || data.name === "ofdgoExportMedia" || data.name === "ofdgoExportVersion" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
|
||||
operations.set(data.id, new AbortController());
|
||||
}
|
||||
pending = pending.then(() => data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" ? handleBatchMessage(data) : handleMessage(data));
|
||||
@@ -166,7 +166,7 @@ async function handleMessage({ id, name, args }) {
|
||||
let output;
|
||||
let channel;
|
||||
const signal = operations.get(id)?.signal;
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages";
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
|
||||
const converting = name === "ofdgoConvertPDF";
|
||||
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned";
|
||||
const resources = name === "ofdgoResources";
|
||||
|
||||
+42
-4
@@ -302,6 +302,7 @@ func RunWASM() {
|
||||
registerCallback("ofdgoResizeObjects", resizeObjects)
|
||||
registerCallback("ofdgoLoadImport", loadImport)
|
||||
registerAsyncCallback("ofdgoImportPages", importPages)
|
||||
registerAsyncCallback("ofdgoImportDocuments", importDocuments)
|
||||
registerCallback("ofdgoInsertText", insertText)
|
||||
registerCallback("ofdgoUpdateText", updateText)
|
||||
registerCallback("ofdgoPreviewText", previewText)
|
||||
@@ -4640,7 +4641,7 @@ func objectStyle(value js.Value) ofdgo.ObjectStyle {
|
||||
return style
|
||||
}
|
||||
|
||||
// clearImport 释放待插页文档
|
||||
// clearImport 释放待导入文档
|
||||
func clearImport() {
|
||||
if pendingImport != nil {
|
||||
_ = pendingImport.Close()
|
||||
@@ -4648,9 +4649,9 @@ func clearImport() {
|
||||
}
|
||||
}
|
||||
|
||||
// loadImport 加载待插页文件的索引,null参数清除待处理文件
|
||||
// loadImport 加载待导入文件的索引,null参数清除待处理文件
|
||||
// 入参: args OFD字节数组或null
|
||||
// 返回: any 页数及签名提示, error 错误信息
|
||||
// 返回: any 文档列表、页数及签名提示, error 错误信息
|
||||
func loadImport(args []js.Value) (any, error) {
|
||||
clearImport()
|
||||
if args[0].IsNull() {
|
||||
@@ -4683,7 +4684,13 @@ func loadImport(args []js.Value) (any, error) {
|
||||
return nil, fmt.Errorf("document has no pages")
|
||||
}
|
||||
pendingImport = reader
|
||||
return map[string]any{"pageCount": len(doc.Pages.Page), "signed": doc.Signatures != "", "encrypted": reader.Encryption().Encrypted}, nil
|
||||
entries := make([]DocumentEntry, reader.DocumentCount())
|
||||
signed := doc.Signatures != ""
|
||||
for index, body := range reader.OFD.DocBody {
|
||||
entries[index] = DocumentEntry{Index: index, Title: body.DocInfo.Title, Root: body.DocRoot, ID: body.DocInfo.DocID}
|
||||
signed = signed || body.Signatures != ""
|
||||
}
|
||||
return map[string]any{"pageCount": len(doc.Pages.Page), "signed": doc.Signatures != "", "documentsSigned": signed, "encrypted": reader.Encryption().Encrypted, "documents": entries, "documentCount": len(entries)}, nil
|
||||
}
|
||||
|
||||
// importPages 插入指定来源页面并保持目标文档元数据
|
||||
@@ -4727,6 +4734,37 @@ func importPages(args []js.Value) (any, error) {
|
||||
return editorPageInfo{info, at}, err
|
||||
}
|
||||
|
||||
// importDocuments 导入指定文档及历史版本并选中首个导入文档
|
||||
// 入参: args 文档序号表达式、插入位置、解密确认和可选进度回调
|
||||
// 返回: any 文档信息及当前页, error 导入错误
|
||||
func importDocuments(args []js.Value) (any, error) {
|
||||
if currentEditor == nil || pendingImport == nil {
|
||||
return nil, fmt.Errorf("no document is ready for document import")
|
||||
}
|
||||
if len(args) < 3 {
|
||||
return nil, fmt.Errorf("invalid document import arguments")
|
||||
}
|
||||
var indexes []int
|
||||
var err error
|
||||
if value := strings.TrimSpace(args[0].String()); value != "" {
|
||||
indexes, err = ofdgo.ParsePageRange(value, pendingImport.DocumentCount())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
options := ofdgo.DocumentImportOptions{AllowDecrypted: args[2].Bool()}
|
||||
ctx := context.Background()
|
||||
if len(args) > 3 {
|
||||
ctx, options.OnProgress = newEditorOperationContext(args[3])
|
||||
}
|
||||
if _, err := currentEditor.ImportDocumentsContext(ctx, pendingImport, indexes, args[1].Int(), options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearImport()
|
||||
info, err := previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||
return editorPageInfo{info, 0}, err
|
||||
}
|
||||
|
||||
// importImages 逐张读取图片并建页,负数位置表示新建文档
|
||||
// 入参: args 文件名、选项、插入位置、标题、注解设置、读取与进度回调
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DocumentImportOptions 整文档导入选项
|
||||
// AllowDecrypted显式允许解密来源导入未加密目标
|
||||
// OnProgress报告documents、resources、commit阶段,total为0表示总量未知
|
||||
// 回调返回错误则放弃导入,回调不可重入修改编辑器
|
||||
type DocumentImportOptions struct {
|
||||
AllowDecrypted bool
|
||||
OnProgress func(stage string, completed, total int) error
|
||||
}
|
||||
|
||||
// editorDocumentImport 保存文档依赖及原始引用位置
|
||||
type editorDocumentImport struct {
|
||||
ctx context.Context
|
||||
source *Reader
|
||||
files map[string]*versionEditFile
|
||||
kinds map[string]int
|
||||
queue []string
|
||||
progress editorProgress
|
||||
}
|
||||
|
||||
// ImportDocuments 导入整文档及历史版本并选中首项
|
||||
// 入参: source 来源阅读器, indexes 从0开始的文档索引,nil表示全部,空切片不导入, at 从0开始的插入位置,可等于文档数
|
||||
// 返回: []int 新文档索引, error 导入错误
|
||||
func (e *Editor) ImportDocuments(source *Reader, indexes []int, at int) ([]int, error) {
|
||||
return e.ImportDocumentsContext(context.Background(), source, indexes, at, DocumentImportOptions{})
|
||||
}
|
||||
|
||||
// ImportDocumentsWithOptions 按指定选项导入整文档
|
||||
// 入参: source 来源阅读器, indexes 从0开始的文档索引,nil表示全部,空切片不导入, at 从0开始的插入位置,可等于文档数, options 导入选项
|
||||
// 返回: []int 新文档索引, error 导入错误
|
||||
func (e *Editor) ImportDocumentsWithOptions(source *Reader, indexes []int, at int, options DocumentImportOptions) ([]int, error) {
|
||||
return e.ImportDocumentsContext(context.Background(), source, indexes, at, options)
|
||||
}
|
||||
|
||||
// ImportDocumentsContext 原子导入文档并保留一次撤销记录
|
||||
// 保持输入索引顺序,拒绝重复索引,返回后可关闭来源阅读器
|
||||
// 保留签名数据和证书,入口或引用迁移后应重新验签
|
||||
// 入参: ctx 取消上下文, source 来源阅读器, indexes 从0开始的文档索引,nil表示全部,空切片不导入, at 从0开始的插入位置,可等于文档数, options 导入选项
|
||||
// 返回: []int 新文档索引, error 校验、导入或取消错误
|
||||
func (e *Editor) ImportDocumentsContext(ctx context.Context, source *Reader, indexes []int, at int, options DocumentImportOptions) ([]int, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if source == nil {
|
||||
return nil, fmt.Errorf("document import requires a source")
|
||||
}
|
||||
if at < 0 || at > e.DocumentCount() {
|
||||
return nil, fmt.Errorf("document index %d out of range", at)
|
||||
}
|
||||
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
|
||||
return nil, ErrEncryptionPolicyRequired
|
||||
}
|
||||
if indexes == nil {
|
||||
indexes = make([]int, source.DocumentCount())
|
||||
for index := range indexes {
|
||||
indexes[index] = index
|
||||
}
|
||||
} else {
|
||||
indexes = slices.Clone(indexes)
|
||||
}
|
||||
seen := make(map[int]bool, len(indexes))
|
||||
for _, index := range indexes {
|
||||
if index < 0 || index >= source.DocumentCount() || seen[index] {
|
||||
return nil, fmt.Errorf("invalid or duplicate document index %d", index)
|
||||
}
|
||||
seen[index] = true
|
||||
}
|
||||
if len(indexes) == 0 {
|
||||
return []int{}, ctx.Err()
|
||||
}
|
||||
migration := &editorDocumentImport{ctx: ctx, source: source, files: make(map[string]*versionEditFile),
|
||||
kinds: make(map[string]int), progress: editorProgress(options.OnProgress).withContext(ctx)}
|
||||
if err := migration.progress.report("documents", 0, len(indexes)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := source.readFileViewContext(ctx, "OFD.xml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := &versionEditFile{reader: source, name: "OFD.xml", data: data}
|
||||
for position, index := range indexes {
|
||||
body := root.childAt("DocBody", index)
|
||||
if body == nil {
|
||||
return nil, fmt.Errorf("document body %d not found", index)
|
||||
}
|
||||
if err := migration.collect(entry, root, body, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := migration.progress.report("documents", position+1, len(indexes)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := migration.load(false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = e.editDocumentsContext(ctx, func(reader *Reader, target []byte, targetRoot *editorXML) (int, error) {
|
||||
version, err := documentImportVersion(targetRoot.attr("Version"), root.attr("Version"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if version != targetRoot.attr("Version") {
|
||||
target, err = editorXMLAttribute(target, targetRoot, "Version", version)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
targetRoot, err = parseEditorXMLContext(ctx, target)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
paths := make(map[string]string)
|
||||
occupied, err := documentImportOccupied(ctx, reader, target, targetRoot)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reserved := maps.Clone(occupied)
|
||||
for name := range migration.kinds {
|
||||
reserved[name] = nil
|
||||
}
|
||||
for _, name := range slices.Sorted(maps.Keys(migration.kinds)) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
candidate := packageAvailableName(reader, occupied, name)
|
||||
if candidate != name {
|
||||
candidate = packageAvailableName(reader, reserved, name)
|
||||
paths[name] = candidate
|
||||
reserved[candidate] = nil
|
||||
}
|
||||
}
|
||||
for _, name := range slices.Sorted(maps.Keys(migration.files)) {
|
||||
file := migration.files[name]
|
||||
content, err := file.rewrite(ctx, paths)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
location := name
|
||||
if paths[name] != "" {
|
||||
location = paths[name]
|
||||
}
|
||||
reader.files[location] = bytes.Clone(content)
|
||||
}
|
||||
updated, err := entry.rewrite(ctx, paths)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
updatedRoot, err := parseEditorXMLContext(ctx, updated)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var inserted bytes.Buffer
|
||||
for _, index := range indexes {
|
||||
body := updatedRoot.childAt("DocBody", index)
|
||||
content, err := editorXMLStandaloneContext(ctx, updated[body.start:body.end], body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
inserted.Write(content)
|
||||
}
|
||||
position := targetRoot.close
|
||||
if body := targetRoot.childAt("DocBody", at); body != nil {
|
||||
position = body.start
|
||||
}
|
||||
reader.files["OFD.xml"] = editorPatchXML(target, []editorXMLPatch{{start: position, end: position, data: inserted.Bytes()}})
|
||||
if err := migration.progress.report("commit", 0, 0); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return at, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]int, len(indexes))
|
||||
for index := range result {
|
||||
result[index] = at + index
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// documentImportVersion 合并OFD格式版本
|
||||
// 入参: target 目标版本, source 来源版本
|
||||
// 返回: string 合并版本, error 版本错误
|
||||
func documentImportVersion(target, source string) (string, error) {
|
||||
if source == "" || source == target {
|
||||
return target, nil
|
||||
}
|
||||
if target == "" {
|
||||
return source, nil
|
||||
}
|
||||
if (source == "1.0" || source == "1.1") && (target == "1.0" || target == "1.1") {
|
||||
return "1.1", nil
|
||||
}
|
||||
return "", fmt.Errorf("cannot merge OFD versions %q and %q", target, source)
|
||||
}
|
||||
|
||||
// documentImportOccupied 保留目标全部文档及版本的已声明路径
|
||||
// 入参: ctx 取消上下文, reader 目标阅读器, data 根索引原文, root 根索引节点
|
||||
// 返回: map[string][]byte 占用路径, error 扫描错误
|
||||
func documentImportOccupied(ctx context.Context, reader *Reader, data []byte, root *editorXML) (map[string][]byte, error) {
|
||||
migration := &editorDocumentImport{ctx: ctx, source: reader, files: make(map[string]*versionEditFile),
|
||||
kinds: make(map[string]int), progress: editorProgress(nil).withContext(ctx)}
|
||||
file := &versionEditFile{reader: reader, name: "OFD.xml", data: data}
|
||||
for _, body := range root.children {
|
||||
if body.matchesOFD("DocBody") {
|
||||
if err := migration.collect(file, root, body, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := migration.load(true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
occupied := make(map[string][]byte, len(migration.kinds))
|
||||
for name := range migration.kinds {
|
||||
occupied[name] = nil
|
||||
}
|
||||
return occupied, nil
|
||||
}
|
||||
|
||||
// reference 记录已解析的包内引用并调度结构文件
|
||||
// 入参: file 来源文件, node 引用节点, attribute 属性名, base 资源目录, value 引用值, kind 引用类别
|
||||
// 返回: error 路径错误
|
||||
func (m *editorDocumentImport) reference(file *versionEditFile, node *editorXML, attribute, base, value string, kind int) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
target := resolveResourcePath(file.name, base, value)
|
||||
if !validPackagePath(target) {
|
||||
return fmt.Errorf("invalid document import path %q", value)
|
||||
}
|
||||
if target == "OFD.xml" && attribute == "FileRef" && node.matchesOFD("Reference") {
|
||||
return nil
|
||||
}
|
||||
file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target})
|
||||
if previous := m.kinds[target]; previous < kind {
|
||||
m.kinds[target] = kind
|
||||
m.queue = append(m.queue, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// collect 收集标准引用并保持未知扩展载荷原文
|
||||
// 入参: file 来源文件, root 文件根节点, node 当前节点, base 资源目录
|
||||
// 返回: error 引用或取消错误
|
||||
func (m *editorDocumentImport) collect(file *versionEditFile, root, node *editorXML, base string) error {
|
||||
if err := m.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !node.matchesOFD(node.name.Local) {
|
||||
return nil
|
||||
}
|
||||
parent := ""
|
||||
if node.parent != nil {
|
||||
parent = node.parent.name.Local
|
||||
}
|
||||
if parent == "Extension" && node.name.Local == "Data" {
|
||||
return nil
|
||||
}
|
||||
if root.matchesOFD("Res") {
|
||||
base = root.attr("BaseLoc")
|
||||
}
|
||||
for _, attribute := range node.attrs {
|
||||
if attribute.Name.Space != "" {
|
||||
continue
|
||||
}
|
||||
kind := packageReference(root.name.Local, parent, node.name.Local, attribute.Name.Local)
|
||||
if root.matchesOFD("Signature") && parent == "References" && node.matchesOFD("Reference") && attribute.Name.Local == "FileRef" {
|
||||
kind = packageReferenceData
|
||||
}
|
||||
if attribute.Name.Local == "FileLoc" && (parent == "Action" || parent == "Actions" || node.matchesOFD("GotoA") || node.matchesOFD("URI") || node.matchesOFD("Sound") || node.matchesOFD("Movie")) {
|
||||
kind = packageReferenceData
|
||||
}
|
||||
if kind != packageReferenceNone {
|
||||
if err := m.reference(file, node, attribute.Name.Local, base, attribute.Value, kind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
kind := packageReference(root.name.Local, parent, node.name.Local, "")
|
||||
if node.matchesOFD("FileLoc") && (parent == "GotoA" || parent == "URI" || parent == "Sound" || parent == "Movie" || parent == "Action") {
|
||||
kind = packageReferenceData
|
||||
}
|
||||
if kind != packageReferenceNone {
|
||||
if err := m.reference(file, node, "", base, editorImportText(file.data, node), kind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, child := range node.children {
|
||||
if err := m.collect(file, root, child, base); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// load 读取依赖并仅解析标准结构文件
|
||||
// 入参: structuresOnly 是否只读取结构文件
|
||||
// 返回: error 读取、解析或取消错误
|
||||
func (m *editorDocumentImport) load(structuresOnly bool) error {
|
||||
parsed := make(map[string]bool)
|
||||
for position := 0; position < len(m.queue); position++ {
|
||||
if err := m.progress.report("resources", position, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
name := m.queue[position]
|
||||
if structuresOnly && m.kinds[name] != packageReferenceXML {
|
||||
continue
|
||||
}
|
||||
file := m.files[name]
|
||||
if file == nil {
|
||||
if _, cached := m.source.files[name]; !cached {
|
||||
if _, exists := m.source.packageFile(name); !exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
data, err := m.source.readFileViewContext(m.ctx, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import document dependency %s: %w", name, err)
|
||||
}
|
||||
file = &versionEditFile{reader: m.source, name: name, data: data}
|
||||
m.files[name] = file
|
||||
}
|
||||
if m.kinds[name] == packageReferenceXML && !parsed[name] {
|
||||
parsed[name] = true
|
||||
root, err := parseEditorXMLContext(m.ctx, file.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.collect(file, root, root, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return m.progress.report("resources", len(m.files), len(m.files))
|
||||
}
|
||||
Reference in new issue
Block a user