mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
6a4b322e2c
commit
151ddb0a20
21 files changed
+819
-195
No files matched your search
@@ -22,6 +22,7 @@
|
|||||||
<button id="closeButton" class="icon-button" type="button" title="关闭" disabled><span class="close-icon"></span></button>
|
<button id="closeButton" class="icon-button" type="button" title="关闭" disabled><span class="close-icon"></span></button>
|
||||||
<button id="newButton" class="icon-button" type="button" title="新建"><span class="new-icon"></span></button>
|
<button id="newButton" class="icon-button" type="button" title="新建"><span class="new-icon"></span></button>
|
||||||
<button id="saveButton" class="icon-button" type="button" title="保存" disabled><span class="save-icon"></span></button>
|
<button id="saveButton" class="icon-button" type="button" title="保存" disabled><span class="save-icon"></span></button>
|
||||||
|
<button id="saveAsButton" class="icon-button" type="button" title="另存" disabled><span class="save-as-icon"></span></button>
|
||||||
<button id="editButton" class="icon-button" type="button" title="编辑" data-pressed="false" disabled><span class="edit-icon"></span></button>
|
<button id="editButton" class="icon-button" type="button" title="编辑" data-pressed="false" disabled><span class="edit-icon"></span></button>
|
||||||
<button id="convertButton" class="icon-button" type="button" title="转换"><span class="convert-icon"></span></button>
|
<button id="convertButton" class="icon-button" type="button" title="转换"><span class="convert-icon"></span></button>
|
||||||
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" data-pressed="true"><span class="pages-icon"></span></button>
|
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" data-pressed="true"><span class="pages-icon"></span></button>
|
||||||
@@ -206,7 +207,7 @@
|
|||||||
<div class="form-row"><label for="convertCompression">压缩</label><select id="convertCompression"><option value="0">原始</option><option value="1">无损</option><option value="2">轻压</option><option value="3">均衡</option><option value="4">强压</option></select></div>
|
<div class="form-row"><label for="convertCompression">压缩</label><select id="convertCompression"><option value="0">原始</option><option value="1">无损</option><option value="2">轻压</option><option value="3">均衡</option><option value="4">强压</option></select></div>
|
||||||
<div id="convertFontRow" class="form-row" hidden><label for="convertFontMode">字体</label><select id="convertFontMode"><option value="0">保持</option><option value="1">内嵌</option><option value="2">外部</option></select></div>
|
<div id="convertFontRow" class="form-row" hidden><label for="convertFontMode">字体</label><select id="convertFontMode"><option value="0">保持</option><option value="1">内嵌</option><option value="2">外部</option></select></div>
|
||||||
<div id="convertDPIRow" class="form-row" hidden><label for="convertDPI">精度</label><div class="form-select"><select id="convertDPI"><option value="96">96 DPI</option><option value="150">150 DPI</option><option value="300" selected>300 DPI</option><option value="600">600 DPI</option></select><span id="convertDPIValue">300</span></div></div>
|
<div id="convertDPIRow" class="form-row" hidden><label for="convertDPI">精度</label><div class="form-select"><select id="convertDPI"><option value="96">96 DPI</option><option value="150">150 DPI</option><option value="300" selected>300 DPI</option><option value="600">600 DPI</option></select><span id="convertDPIValue">300</span></div></div>
|
||||||
<div class="form-row"><label for="convertDestination">保存</label><select id="convertDestination"><option value="directory">目录</option><option value="archive">打包</option></select></div>
|
<div class="form-row"><label for="convertDestination">输出</label><select id="convertDestination"><option value="directory">目录</option><option value="archive">打包</option></select></div>
|
||||||
<div class="form-row"><label for="convertParallel">并行</label><input id="convertParallel" type="number" min="1" step="1" placeholder="自动"></div>
|
<div class="form-row"><label for="convertParallel">并行</label><input id="convertParallel" type="number" min="1" step="1" placeholder="自动"></div>
|
||||||
</div>
|
</div>
|
||||||
<button id="convertClose" class="button" type="button">关闭</button><button id="convertCancel" class="button" type="button" hidden>取消</button><button id="convertStart" class="button" type="submit" disabled>转换</button>
|
<button id="convertClose" class="button" type="button">关闭</button><button id="convertCancel" class="button" type="button" hidden>取消</button><button id="convertStart" class="button" type="submit" disabled>转换</button>
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ button:not(:disabled):hover {
|
|||||||
.close-icon,
|
.close-icon,
|
||||||
.new-icon,
|
.new-icon,
|
||||||
.save-icon,
|
.save-icon,
|
||||||
|
.save-as-icon,
|
||||||
.edit-icon,
|
.edit-icon,
|
||||||
.convert-icon,
|
.convert-icon,
|
||||||
.pages-icon,
|
.pages-icon,
|
||||||
@@ -593,6 +594,10 @@ button:not(:disabled):hover {
|
|||||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 4 5 5M3 21l2-7L16 3a2 2 0 0 1 5 5L10 19Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 4 5 5M3 21l2-7L16 3a2 2 0 0 1 5 5L10 19Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-as-icon {
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v3M7 3v5h8M7 21v-8h4m3 8 7-7m-7 0h7v7'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.convert-icon {
|
.convert-icon {
|
||||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 7h18l-4-4M21 17H3l4 4'/%3E%3C/svg%3E") center / contain no-repeat;
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 7h18l-4-4M21 17H3l4 4'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-44
@@ -52,6 +52,8 @@ const state = {
|
|||||||
wasmRecovering: false,
|
wasmRecovering: false,
|
||||||
wasmRecoveries: 0,
|
wasmRecoveries: 0,
|
||||||
exporting: false,
|
exporting: false,
|
||||||
|
saving: false,
|
||||||
|
saveRequestID: 0,
|
||||||
signing: false,
|
signing: false,
|
||||||
signCanceled: false,
|
signCanceled: false,
|
||||||
exportRequestID: 0,
|
exportRequestID: 0,
|
||||||
@@ -61,6 +63,8 @@ const state = {
|
|||||||
editorInfo: null,
|
editorInfo: null,
|
||||||
editorViews: new Map(),
|
editorViews: new Map(),
|
||||||
savedRevision: null,
|
savedRevision: null,
|
||||||
|
saveHandle: null,
|
||||||
|
saveStamp: null,
|
||||||
packageDirty: false,
|
packageDirty: false,
|
||||||
encryptionDirty: false,
|
encryptionDirty: false,
|
||||||
insertObject: null,
|
insertObject: null,
|
||||||
@@ -428,6 +432,7 @@ const el = {
|
|||||||
pageStatus: document.querySelector("#pageStatus"),
|
pageStatus: document.querySelector("#pageStatus"),
|
||||||
pageCancel: document.querySelector("#pageCancel"),
|
pageCancel: document.querySelector("#pageCancel"),
|
||||||
saveButton: document.querySelector("#saveButton"),
|
saveButton: document.querySelector("#saveButton"),
|
||||||
|
saveAsButton: document.querySelector("#saveAsButton"),
|
||||||
createPanel: document.querySelector("#createPanel"),
|
createPanel: document.querySelector("#createPanel"),
|
||||||
createMode: document.querySelector("#createMode"),
|
createMode: document.querySelector("#createMode"),
|
||||||
createImages: document.querySelector("#createImages"),
|
createImages: document.querySelector("#createImages"),
|
||||||
@@ -607,7 +612,7 @@ const annotationFields = Object.fromEntries(["CreateForm", "CreateCancel", "Cont
|
|||||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||||
|
|
||||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||||
textControls: [el.textFont, el.textFontToggle, el.textFontAdd, el.textFontList, el.textSize, el.textColor, textBackground, ...textFormatButtons, el.undoButton, el.redoButton, el.saveButton, el.fontInput, document.querySelector("#textReflow")],
|
textControls: [el.textFont, el.textFontToggle, el.textFontAdd, el.textFontList, el.textSize, el.textColor, textBackground, ...textFormatButtons, el.undoButton, el.redoButton, el.saveButton, el.saveAsButton, el.fontInput, document.querySelector("#textReflow")],
|
||||||
keepTextFocus: () => fontPicker.open || el.closePanel.open || canvasEditor.input?.fontDialog,
|
keepTextFocus: () => fontPicker.open || el.closePanel.open || canvasEditor.input?.fontDialog,
|
||||||
textClipboardScope: () => `${state.wasmSeq}:${state.fileName}:${state.doc?.documents?.[state.doc.documentIndex || 0]?.root}:${state.doc?.documentIndex || 0}:${state.doc?.documentVersion || ""}`,
|
textClipboardScope: () => `${state.wasmSeq}:${state.fileName}:${state.doc?.documents?.[state.doc.documentIndex || 0]?.root}:${state.doc?.documentIndex || 0}:${state.doc?.documentVersion || ""}`,
|
||||||
busy: () => document.body.hasAttribute("data-busy"),
|
busy: () => document.body.hasAttribute("data-busy"),
|
||||||
@@ -1475,7 +1480,8 @@ el.insertCancel.addEventListener("click", () => el.insertPanel.close());
|
|||||||
el.insertPanel.addEventListener("close", () => { state.insertObject = null; });
|
el.insertPanel.addEventListener("close", () => { state.insertObject = null; });
|
||||||
el.insertForm.addEventListener("submit", insertObject);
|
el.insertForm.addEventListener("submit", insertObject);
|
||||||
el.textFontAdd.addEventListener("click", () => openFontFile(el.fontInput));
|
el.textFontAdd.addEventListener("click", () => openFontFile(el.fontInput));
|
||||||
el.saveButton.addEventListener("click", () => exportFile(true, null, "ofd"));
|
el.saveButton.addEventListener("click", () => exportFile(true, null, "ofd", false));
|
||||||
|
el.saveAsButton.addEventListener("click", () => exportFile(true, null, "ofd", true));
|
||||||
editorClick(el.encryptSaveButton, openEncryptionPanel);
|
editorClick(el.encryptSaveButton, openEncryptionPanel);
|
||||||
el.signButton.addEventListener("click", () => {
|
el.signButton.addEventListener("click", () => {
|
||||||
if (!state.doc || document.body.hasAttribute("data-busy")) return;
|
if (!state.doc || document.body.hasAttribute("data-busy")) return;
|
||||||
@@ -1744,10 +1750,10 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "s" && state.doc
|
if ((event.ctrlKey || event.metaKey) && key.toLowerCase() === "s" && state.doc
|
||||||
&& (state.editorInfo || state.ofdBytes)) {
|
&& (state.editorInfo || state.ofdBytes)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!document.body.hasAttribute("data-busy") && !formDialogOpen()) exportFile(true, null, "ofd");
|
if (!document.body.hasAttribute("data-busy") && !formDialogOpen()) exportFile(true, null, "ofd", event.shiftKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
|
||||||
@@ -2320,20 +2326,24 @@ function setDirty(dirty) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function discardChanges() {
|
function discardChanges() {
|
||||||
|
if (state.saving) return false;
|
||||||
return !state.dirty || window.confirm("文档尚未保存,放弃更改?");
|
return !state.dirty || window.confirm("文档尚未保存,放弃更改?");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeFile() {
|
async function closeFile() {
|
||||||
|
if (state.saving) return;
|
||||||
if (!state.doc || state.exporting || batch.unlocking || document.body.hasAttribute("data-busy")) return;
|
if (!state.doc || state.exporting || batch.unlocking || document.body.hasAttribute("data-busy")) return;
|
||||||
let openSeq = state.openSeq;
|
let openSeq = state.openSeq;
|
||||||
if (state.dirty || canvasEditor.input || canvasEditor.nudge || canvasEditor.nudgeCommit || canvasEditor.crop) {
|
if (state.dirty || canvasEditor.input || canvasEditor.nudge || canvasEditor.nudgeCommit || canvasEditor.crop) {
|
||||||
|
el.closePanel.querySelector('[value="save"]').textContent = state.saveHandle ? "保存" : "另存";
|
||||||
el.closePanel.returnValue = "cancel";
|
el.closePanel.returnValue = "cancel";
|
||||||
const choice = new Promise(resolve => el.closePanel.addEventListener("close", () => resolve(el.closePanel.returnValue), { once: true }));
|
const choice = new Promise(resolve => el.closePanel.addEventListener("close", () => resolve(el.closePanel.returnValue), { once: true }));
|
||||||
el.closePanel.showModal();
|
el.closePanel.showModal();
|
||||||
const action = await choice;
|
const action = await choice;
|
||||||
if (openSeq !== state.openSeq || action === "cancel") return;
|
if (openSeq !== state.openSeq || action === "cancel") return;
|
||||||
if (action === "save") {
|
if (action === "save") {
|
||||||
if (!await exportFile(true, null, "ofd")) return;
|
if (!await exportFile(true, null, "ofd", false)) return;
|
||||||
|
if (state.dirty) return;
|
||||||
openSeq = state.openSeq;
|
openSeq = state.openSeq;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2345,6 +2355,8 @@ async function closeFile() {
|
|||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.doc = null;
|
state.doc = null;
|
||||||
state.fileName = "ofdgo.ofd";
|
state.fileName = "ofdgo.ofd";
|
||||||
|
state.saveHandle = null;
|
||||||
|
state.saveStamp = null;
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
state.editorInfo = null;
|
state.editorInfo = null;
|
||||||
state.savedRevision = null;
|
state.savedRevision = null;
|
||||||
@@ -2497,6 +2509,7 @@ function selectManagedVersion(id, focus = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function changeVersion(action) {
|
async function changeVersion(action) {
|
||||||
|
if (state.saving) return;
|
||||||
const manager = state.versionManagement;
|
const manager = state.versionManagement;
|
||||||
if (!state.ready || state.exporting || !state.editing && action !== "select" || !manager?.entries || manager.loading || manager.failed || document.body.hasAttribute("data-busy")) return;
|
if (!state.ready || state.exporting || !state.editing && action !== "select" || !manager?.entries || manager.loading || manager.failed || document.body.hasAttribute("data-busy")) return;
|
||||||
const entry = manager.entries.find(entry => entry.id === manager.id);
|
const entry = manager.entries.find(entry => entry.id === manager.id);
|
||||||
@@ -2518,6 +2531,7 @@ async function changeVersion(action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchReadingVersion(id) {
|
async function switchReadingVersion(id) {
|
||||||
|
if (state.saving) return;
|
||||||
if (!state.doc || state.editing || !state.ready || state.exporting || document.body.hasAttribute("data-busy")) return;
|
if (!state.doc || state.editing || !state.ready || state.exporting || document.body.hasAttribute("data-busy")) return;
|
||||||
if (id === (state.doc.documentVersion || "")) return true;
|
if (id === (state.doc.documentVersion || "")) return true;
|
||||||
const dirty = state.dirty;
|
const dirty = state.dirty;
|
||||||
@@ -2556,7 +2570,7 @@ async function switchReadingVersion(id) {
|
|||||||
|
|
||||||
function updateVersionControls() {
|
function updateVersionControls() {
|
||||||
const manager = state.versionManagement, busy = document.body.hasAttribute("data-busy");
|
const manager = state.versionManagement, busy = document.body.hasAttribute("data-busy");
|
||||||
const disabled = !state.doc || !state.ready || state.exporting || busy;
|
const disabled = !state.doc || !state.ready || state.exporting || state.saving || busy;
|
||||||
const title = state.editing ? "版本管理" : "版本浏览";
|
const title = state.editing ? "版本管理" : "版本浏览";
|
||||||
el.versionManageButton.hidden = !state.editing && !(state.doc?.versionCount > 0);
|
el.versionManageButton.hidden = !state.editing && !(state.doc?.versionCount > 0);
|
||||||
el.versionTitle.textContent = title;
|
el.versionTitle.textContent = title;
|
||||||
@@ -2691,6 +2705,7 @@ function selectManagedDocument(index, focus = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchDocument(index) {
|
async function switchDocument(index) {
|
||||||
|
if (state.saving) return;
|
||||||
if (!state.doc || document.body.hasAttribute("data-busy") || state.exporting || formDialogOpen()) return;
|
if (!state.doc || document.body.hasAttribute("data-busy") || state.exporting || formDialogOpen()) return;
|
||||||
const previous = state.doc.documentIndex || 0;
|
const previous = state.doc.documentIndex || 0;
|
||||||
if (index === previous) return;
|
if (index === previous) return;
|
||||||
@@ -2819,6 +2834,7 @@ function confirmTextReflow(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreatePanel(insert = false, packageDocument = false) {
|
function openCreatePanel(insert = false, packageDocument = false) {
|
||||||
|
if (state.saving && insert !== true) return;
|
||||||
if (document.body.hasAttribute("data-busy")) {
|
if (document.body.hasAttribute("data-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2912,6 +2928,8 @@ async function createDocument(event) {
|
|||||||
setPan(false);
|
setPan(false);
|
||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.fileName = `${title.replace(/[\\/:*?"<>|]+/g, "_").replace(/\.ofd$/i, "")}.ofd`;
|
state.fileName = `${title.replace(/[\\/:*?"<>|]+/g, "_").replace(/\.ofd$/i, "")}.ofd`;
|
||||||
|
state.saveHandle = null;
|
||||||
|
state.saveStamp = null;
|
||||||
state.conversionWarnings = [];
|
state.conversionWarnings = [];
|
||||||
state.savedRevision = null;
|
state.savedRevision = null;
|
||||||
state.packageDirty = false;
|
state.packageDirty = false;
|
||||||
@@ -4112,9 +4130,19 @@ function releasePageResources() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openOFDFile() {
|
async function openOFDFile() {
|
||||||
|
if (state.saving) return;
|
||||||
if (document.body.hasAttribute("data-busy")) {
|
if (document.body.hasAttribute("data-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!nativeHost && window.showOpenFilePicker) {
|
||||||
|
try {
|
||||||
|
const [handle] = await window.showOpenFilePicker({types: [{description: "OFD、PDF", accept: {"application/ofd": [".ofd"], "application/pdf": [".pdf"]}}]});
|
||||||
|
const file = await handle.getFile();
|
||||||
|
await requestLocalFontsBeforeOpen();
|
||||||
|
await openOFD(file, null, handle);
|
||||||
|
} catch (error) { if (error.name !== "AbortError") showError(error, !state.doc); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
await requestLocalFontsBeforeOpen();
|
await requestLocalFontsBeforeOpen();
|
||||||
el.ofdInput.value = "";
|
el.ofdInput.value = "";
|
||||||
el.ofdInput.click();
|
el.ofdInput.click();
|
||||||
@@ -4139,11 +4167,12 @@ async function openOFDFromDrop(event) {
|
|||||||
setStatus("一次拖入一个文件");
|
setStatus("一次拖入一个文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const handlePromise = event.dataTransfer.items[0]?.getAsFileSystemHandle?.();
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
if (file && isDocumentFile(file)) {
|
if (file && isDocumentFile(file)) {
|
||||||
await requestLocalFontsBeforeOpen();
|
await requestLocalFontsBeforeOpen();
|
||||||
}
|
}
|
||||||
await openOFD(file);
|
await openOFD(file, null, await handlePromise?.catch(() => null));
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFontFile(input) {
|
function openFontFile(input) {
|
||||||
@@ -4249,7 +4278,7 @@ async function boot() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
opening = opening.then(async () => {
|
opening = opening.then(async () => {
|
||||||
await openOFD(await files[0].getFile());
|
await openOFD(await files[0].getFile(), null, files[0]);
|
||||||
}).catch((err) => showError(err, !state.doc));
|
}).catch((err) => showError(err, !state.doc));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4557,11 +4586,15 @@ async function loadWASM() {
|
|||||||
else setProgress("正在生成文档", null);
|
else setProgress("正在生成文档", null);
|
||||||
}
|
}
|
||||||
} else if (data.type === "export") {
|
} else if (data.type === "export") {
|
||||||
|
if (data.id === state.saveRequestID && state.saving && !state.exporting) {
|
||||||
|
if (data.stage === "save") el.cancelExportButton.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
||||||
if (data.stage === "save") {
|
if (data.stage === "save") {
|
||||||
el.cancelExportButton.disabled = true;
|
el.cancelExportButton.disabled = true;
|
||||||
if (state.signing) el.signCancel.disabled = true;
|
if (state.signing) el.signCancel.disabled = true;
|
||||||
setProgress("正在保存文件", null);
|
setProgress("正在写入文件", null);
|
||||||
} else if (!el.cancelExportButton.disabled) {
|
} else if (!el.cancelExportButton.disabled) {
|
||||||
if (data.stage === "prepare") {
|
if (data.stage === "prepare") {
|
||||||
const label = { snapshot: "正在准备文档", ids: "正在检查标识", commit: "正在整理文档", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", compress: "正在压缩文件", write: "正在写入文件", sign: "正在签署文档", encrypt: "正在加密文档" }[data.phase];
|
const label = { snapshot: "正在准备文档", ids: "正在检查标识", commit: "正在整理文档", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", compress: "正在压缩文件", write: "正在写入文件", sign: "正在签署文档", encrypt: "正在加密文档" }[data.phase];
|
||||||
@@ -4600,6 +4633,7 @@ async function loadWASM() {
|
|||||||
} else {
|
} else {
|
||||||
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "跨段或换行修改需先设置段落排版" : data.error));
|
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "跨段或换行修改需先设置段落排版" : data.error));
|
||||||
err.code = data.code;
|
err.code = data.code;
|
||||||
|
if (data.errorName) err.name = data.errorName;
|
||||||
err.missingGlyphs = data.missingGlyphs;
|
err.missingGlyphs = data.missingGlyphs;
|
||||||
if (data.canceled) {
|
if (data.canceled) {
|
||||||
err.name = "AbortError";
|
err.name = "AbortError";
|
||||||
@@ -4729,7 +4763,8 @@ async function downloadRemoteDocument(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openOFD(file, remote = null) {
|
async function openOFD(file, remote = null, handle = file?.handle) {
|
||||||
|
if (state.saving) return;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4785,6 +4820,8 @@ async function openOFD(file, remote = null) {
|
|||||||
const doc = convertedDoc || await openWithCredentials(await fontManager.files(fontManager.userFonts), openSeq, bytes);
|
const doc = convertedDoc || await openWithCredentials(await fontManager.files(fontManager.userFonts), openSeq, bytes);
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
state.ofdBytes = bytes;
|
state.ofdBytes = bytes;
|
||||||
|
state.saveHandle = format === "ofd" && !remote ? handle || null : null;
|
||||||
|
state.saveStamp = state.saveHandle && !nativeHost ? {size: file.size, modified: file.lastModified} : null;
|
||||||
state.conversionWarnings = warnings;
|
state.conversionWarnings = warnings;
|
||||||
state.fileName = (name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd");
|
state.fileName = (name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd");
|
||||||
if (remote && !/\.ofd$/i.test(state.fileName)) state.fileName += ".ofd";
|
if (remote && !/\.ofd$/i.test(state.fileName)) state.fileName += ".ofd";
|
||||||
@@ -6712,11 +6749,11 @@ async function commitInvoice(draft, request) {
|
|||||||
const key = invoicePanel.key;
|
const key = invoicePanel.key;
|
||||||
settings.busy = true;
|
settings.busy = true;
|
||||||
settingsElements.Form.inert = true;
|
settingsElements.Form.inert = true;
|
||||||
settingsElements.Status.textContent = "正在保存修改";
|
settingsElements.Status.textContent = "正在应用修改";
|
||||||
try {
|
try {
|
||||||
invoicePanel.result = null;
|
invoicePanel.result = null;
|
||||||
if (!await changeDocument("ofdgoChangeInvoice", null, JSON.stringify({ ...draft.scope, source: draft.entry?.source, index: draft.entry?.index || 0, digest: draft.detail.digest, ...request }))) {
|
if (!await changeDocument("ofdgoChangeInvoice", null, JSON.stringify({ ...draft.scope, source: draft.entry?.source, index: draft.entry?.index || 0, digest: draft.detail.digest, ...request }))) {
|
||||||
if (settingsElements.Status.textContent === "正在保存修改") settingsElements.Status.textContent = el.statusText.textContent;
|
if (settingsElements.Status.textContent === "正在应用修改") settingsElements.Status.textContent = el.statusText.textContent;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = invoicePanel.result;
|
const result = invoicePanel.result;
|
||||||
@@ -6742,7 +6779,7 @@ async function commitInvoice(draft, request) {
|
|||||||
await loadDocumentSettings();
|
await loadDocumentSettings();
|
||||||
const warning = invoiceIssues(result?.issues).replaceAll("\n", " · ");
|
const warning = invoiceIssues(result?.issues).replaceAll("\n", " · ");
|
||||||
if (warning) {
|
if (warning) {
|
||||||
const status = "已保存 · " + warning;
|
const status = "已应用 · " + warning;
|
||||||
if (invoiceCurrent()) invoiceCurrent().status = status;
|
if (invoiceCurrent()) invoiceCurrent().status = status;
|
||||||
settingsElements.Status.textContent = status;
|
settingsElements.Status.textContent = status;
|
||||||
}
|
}
|
||||||
@@ -6861,11 +6898,11 @@ async function commitSettings(name, ...args) {
|
|||||||
const pending = settings.drafts.get(settings.view), baseline = settings.baseline;
|
const pending = settings.drafts.get(settings.view), baseline = settings.baseline;
|
||||||
settings.busy = true;
|
settings.busy = true;
|
||||||
settingsElements.Form.inert = true;
|
settingsElements.Form.inert = true;
|
||||||
settingsElements.Status.textContent = "正在保存修改";
|
settingsElements.Status.textContent = "正在应用修改";
|
||||||
const view = settings.view, focus = document.activeElement?.id, body = document.querySelector("#settingsBody"), scroll = body?.scrollTop || 0;
|
const view = settings.view, focus = document.activeElement?.id, body = document.querySelector("#settingsBody"), scroll = body?.scrollTop || 0;
|
||||||
try {
|
try {
|
||||||
if (!await changeDocument(name, null, ...args)) {
|
if (!await changeDocument(name, null, ...args)) {
|
||||||
if (settingsElements.Status.textContent === "正在保存修改") settingsElements.Status.textContent = el.statusText.textContent;
|
if (settingsElements.Status.textContent === "正在应用修改") settingsElements.Status.textContent = el.statusText.textContent;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
settings.drafts.delete(view);
|
settings.drafts.delete(view);
|
||||||
@@ -7803,6 +7840,7 @@ function updateEncryptionType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function applyEncryption(event) {
|
async function applyEncryption(event) {
|
||||||
|
if (state.saving) { event.preventDefault(); return; }
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.doc || document.body.hasAttribute("data-busy") || el.encryptionSubmit.disabled) return;
|
if (!state.doc || document.body.hasAttribute("data-busy") || el.encryptionSubmit.disabled) return;
|
||||||
const mode = el.encryptionType.value;
|
const mode = el.encryptionType.value;
|
||||||
@@ -7884,7 +7922,7 @@ function renderSecurity() {
|
|||||||
el.securitySchemeRow.hidden = !encryption?.encrypted;
|
el.securitySchemeRow.hidden = !encryption?.encrypted;
|
||||||
el.securityMethod.textContent = scheme ? "SM4-CBC" : "";
|
el.securityMethod.textContent = scheme ? "SM4-CBC" : "";
|
||||||
el.securityMethodRow.hidden = !encryption?.encrypted || !scheme;
|
el.securityMethodRow.hidden = !encryption?.encrypted || !scheme;
|
||||||
el.encryptSaveButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("data-busy");
|
el.encryptSaveButton.disabled = !state.doc || !state.ready || state.exporting || state.saving || document.body.hasAttribute("data-busy");
|
||||||
el.verifyButton.disabled = el.encryptSaveButton.disabled;
|
el.verifyButton.disabled = el.encryptSaveButton.disabled;
|
||||||
el.signButton.disabled = el.encryptSaveButton.disabled;
|
el.signButton.disabled = el.encryptSaveButton.disabled;
|
||||||
}
|
}
|
||||||
@@ -8034,6 +8072,7 @@ function updateSignPlacement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function signDocument(event) {
|
async function signDocument(event) {
|
||||||
|
if (state.saving) { event.preventDefault(); return; }
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.doc || document.body.hasAttribute("data-busy")) return;
|
if (!state.doc || document.body.hasAttribute("data-busy")) return;
|
||||||
if (!el.signCertificate.files?.length || !el.signKey.files?.length || !el.signRoots.files?.length) {
|
if (!el.signCertificate.files?.length || !el.signKey.files?.length || !el.signRoots.files?.length) {
|
||||||
@@ -8587,8 +8626,8 @@ function compressionOptions(value) {
|
|||||||
return preset > 2 ? { mode: 2, level: preset - 2 } : { mode: preset };
|
return preset > 2 ? { mode: 2, level: preset - 2 } : { mode: preset };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportFile(whole, indices = null, value = el.exportFormat.value) {
|
async function exportFile(whole, indices = null, value = el.exportFormat.value, saveAs = true) {
|
||||||
if (!state.doc || document.body.hasAttribute("data-busy")) {
|
if (!state.doc || state.saving || state.exporting || document.body.hasAttribute("data-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const saving = value === "ofd";
|
const saving = value === "ofd";
|
||||||
@@ -8608,14 +8647,36 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
const mime = archive ? "application/zip" : format.mime;
|
const mime = archive ? "application/zip" : format.mime;
|
||||||
let openSeq = state.openSeq;
|
let openSeq = state.openSeq;
|
||||||
const fileName = saving && indices !== null ? `${baseFileName()}_选页.ofd` : whole ? `${baseFileName()}.${extension}` : pageFileName(extension);
|
const fileName = saving && indices !== null ? `${baseFileName()}_选页.ofd` : whole ? `${baseFileName()}.${extension}` : pageFileName(extension);
|
||||||
|
const wasmSeq = state.wasmSeq;
|
||||||
const pageIndex = state.pageIndex;
|
const pageIndex = state.pageIndex;
|
||||||
const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
|
const dpi = exportFormatUsesDPI(format.value) ? currentImageDPI() : 0;
|
||||||
const status = saving ? "正在保存文档" : whole ? STATUS.exporting : STATUS.pageExporting;
|
let action = saving ? saveAs || !state.saveHandle || indices !== null ? "另存" : "保存" : "导出";
|
||||||
|
const status = saving ? `正在${action}文档` : whole ? STATUS.exporting : STATUS.pageExporting;
|
||||||
|
let retrySaveAs = false;
|
||||||
|
let background = false;
|
||||||
|
let savedRevision = null;
|
||||||
|
let snapshotPrepared = false;
|
||||||
state.exporting = true;
|
state.exporting = true;
|
||||||
|
state.saving = saving && indices === null;
|
||||||
updateControls();
|
updateControls();
|
||||||
setBusy(true, "正在生成文件", null, status);
|
setBusy(true, "正在生成文件", null, status);
|
||||||
try {
|
try {
|
||||||
const file = nativeHost || window.showSaveFilePicker ? await saveFilePicker({
|
let file = saving && indices === null && !saveAs ? state.saveHandle : null;
|
||||||
|
if (file && !nativeHost) {
|
||||||
|
try {
|
||||||
|
const permission = await file.queryPermission({mode: "readwrite"});
|
||||||
|
if (permission !== "granted" && await file.requestPermission({mode: "readwrite"}) !== "granted") file = null;
|
||||||
|
} catch (error) {
|
||||||
|
if (!["NotAllowedError", "SecurityError"].includes(error.name)) throw error;
|
||||||
|
file = null;
|
||||||
|
}
|
||||||
|
if (file && state.saveStamp) {
|
||||||
|
const current = await file.getFile();
|
||||||
|
if (current.size !== state.saveStamp.size || current.lastModified !== state.saveStamp.modified) throw new Error("文件已被修改,请另存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (saving && !file) action = "另存";
|
||||||
|
file ||= nativeHost || window.showSaveFilePicker ? await saveFilePicker({
|
||||||
suggestedName: fileName,
|
suggestedName: fileName,
|
||||||
types: [{ description: label, accept: { [mime]: [`.${extension}`] } }],
|
types: [{ description: label, accept: { [mime]: [`.${extension}`] } }],
|
||||||
}) : null;
|
}) : null;
|
||||||
@@ -8631,34 +8692,63 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
setBusy(true, "正在生成文件", null, status);
|
setBusy(true, "正在生成文件", null, status);
|
||||||
}
|
}
|
||||||
const readingSave = saving && !state.editorInfo && indices === null && compression.mode === 0;
|
const readingSave = saving && !state.editorInfo && indices === null && compression.mode === 0;
|
||||||
|
const destination = !nativeHost && file && file === state.saveHandle && state.saveStamp ? {fileHandle: file, saveStamp: state.saveStamp} : file;
|
||||||
|
if (saving && indices === null && !readingSave) {
|
||||||
|
snapshotPrepared = true;
|
||||||
|
await callWASM("ofdgoPrepareSave");
|
||||||
|
savedRevision = state.editorInfo?.revision;
|
||||||
|
background = true;
|
||||||
|
state.exporting = false;
|
||||||
|
setBusy(false);
|
||||||
|
updateControls();
|
||||||
|
}
|
||||||
const result = readingSave
|
const result = readingSave
|
||||||
? { label: "OFD", size: state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
? { label: "OFD", size: state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
||||||
: saving ? await callWASM("ofdgoSaveDocument", indices, ...outputOptions, file) : whole
|
: saving ? await callWASM(background ? "ofdgoSaveSnapshot" : "ofdgoSaveDocument", indices, ...outputOptions, destination) : whole
|
||||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, ...outputOptions, file)
|
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, ...outputOptions, file)
|
||||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, ...outputOptions, file);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, ...outputOptions, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if ((!background && openSeq !== state.openSeq) || wasmSeq !== state.wasmSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (readingSave && file) {
|
if (readingSave && file) {
|
||||||
const writer = await file.createWritable();
|
const writer = await file.createWritable();
|
||||||
await writer.write(result.blob);
|
try {
|
||||||
await writer.close();
|
await writer.write(result.blob);
|
||||||
|
await writer.close();
|
||||||
|
} catch (error) {
|
||||||
|
await writer.abort().catch(() => {});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
delete result.blob;
|
delete result.blob;
|
||||||
}
|
}
|
||||||
if (result.blob) {
|
if (result.blob) {
|
||||||
downloadBytes(result.blob, result.mime, fileName);
|
downloadBytes(result.blob, result.mime, fileName);
|
||||||
}
|
}
|
||||||
if (saving && indices === null && state.editorInfo) {
|
if (saving && indices === null && state.editorInfo && file) {
|
||||||
state.savedRevision = state.editorInfo.revision;
|
state.savedRevision = background ? savedRevision : state.editorInfo.revision;
|
||||||
state.packageDirty = false;
|
state.packageDirty = false;
|
||||||
state.encryptionDirty = false;
|
state.encryptionDirty = false;
|
||||||
setDirty(false);
|
updatePendingChanges();
|
||||||
}
|
}
|
||||||
setStatus(`${result.label}已${saving ? "保存" : "导出"}(${formatBytes(result.size)})`);
|
if (saving && indices === null && file) {
|
||||||
|
state.saveHandle = file;
|
||||||
|
state.fileName = file.name;
|
||||||
|
state.saveStamp = null;
|
||||||
|
if (!nativeHost) {
|
||||||
|
try {
|
||||||
|
const current = await file.getFile();
|
||||||
|
state.saveStamp = {size: current.size, modified: current.lastModified};
|
||||||
|
} catch { state.saveHandle = null; }
|
||||||
|
}
|
||||||
|
document.title = `${nativeHost ? "OFDGo" : "OFDGo WebUI"} - ${state.fileName}`;
|
||||||
|
el.metaFile.textContent = state.fileName;
|
||||||
|
}
|
||||||
|
setStatus(`${result.label}已${action}(${formatBytes(result.size)})`);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
retrySaveAs = saving && !saveAs && !nativeHost && !!state.saveHandle && ["NotAllowedError", "SecurityError"].includes(err.name);
|
||||||
const canceled = saving ? "保存已取消" : "导出已取消";
|
if ((background || openSeq === state.openSeq) && wasmSeq === state.wasmSeq && !retrySaveAs) {
|
||||||
|
const canceled = `${action}已取消`;
|
||||||
if (el.batchPagesPanel.open) el.batchPagesStatus.textContent = err.name === "AbortError" ? canceled : err.message;
|
if (el.batchPagesPanel.open) el.batchPagesStatus.textContent = err.name === "AbortError" ? canceled : err.message;
|
||||||
if (err.name === "AbortError") {
|
if (err.name === "AbortError") {
|
||||||
setStatus(canceled);
|
setStatus(canceled);
|
||||||
@@ -8667,14 +8757,21 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.exporting = false;
|
if (snapshotPrepared) await callWASM("ofdgoDiscardSave").catch(() => {});
|
||||||
state.exportRequestID = 0;
|
state.saving = false;
|
||||||
el.cancelExportButton.hidden = true;
|
state.saveRequestID = 0;
|
||||||
if (openSeq === state.openSeq) {
|
if (!background) {
|
||||||
setBusy(false);
|
state.exporting = false;
|
||||||
|
state.exportRequestID = 0;
|
||||||
|
}
|
||||||
|
el.cancelExportButton.hidden = !state.exportRequestID;
|
||||||
|
if (!document.body.hasAttribute("data-busy")) el.progressPanel.hidden = true;
|
||||||
|
if ((background || openSeq === state.openSeq) && wasmSeq === state.wasmSeq) {
|
||||||
|
if (!background) setBusy(false);
|
||||||
updateControls();
|
updateControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (retrySaveAs && (background || openSeq === state.openSeq) && wasmSeq === state.wasmSeq) return exportFile(whole, indices, value, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelExport() {
|
function cancelExport() {
|
||||||
@@ -8682,7 +8779,8 @@ function cancelExport() {
|
|||||||
remoteDownload.abort();
|
remoteDownload.abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!state.exportRequestID || el.cancelExportButton.disabled) {
|
const requestID = state.exportRequestID || state.saveRequestID;
|
||||||
|
if (!requestID || el.cancelExportButton.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.cancelExportButton.disabled = true;
|
el.cancelExportButton.disabled = true;
|
||||||
@@ -8694,7 +8792,7 @@ function cancelExport() {
|
|||||||
el.importStatus.textContent = "正在取消操作";
|
el.importStatus.textContent = "正在取消操作";
|
||||||
}
|
}
|
||||||
setProgress("正在取消操作", null);
|
setProgress("正在取消操作", null);
|
||||||
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
wasmWorker.postMessage({ type: "cancel", id: requestID });
|
||||||
}
|
}
|
||||||
|
|
||||||
function virtualIndex(offsets, top) {
|
function virtualIndex(offsets, top) {
|
||||||
@@ -12023,14 +12121,14 @@ function updateDocumentControls() {
|
|||||||
const busy = document.body.hasAttribute("data-busy");
|
const busy = document.body.hasAttribute("data-busy");
|
||||||
const disabled = !state.doc || !state.ready || state.exporting || busy;
|
const disabled = !state.doc || !state.ready || state.exporting || busy;
|
||||||
el.documentManageButton.hidden = !state.editing;
|
el.documentManageButton.hidden = !state.editing;
|
||||||
el.documentManageButton.disabled = disabled;
|
el.documentManageButton.disabled = disabled || state.saving;
|
||||||
resourceElements.ManageButton.disabled = disabled;
|
resourceElements.ManageButton.disabled = disabled;
|
||||||
settingsElements.Button.disabled = disabled;
|
settingsElements.Button.disabled = disabled;
|
||||||
updateInvoiceControl(disabled);
|
updateInvoiceControl(disabled);
|
||||||
updateResourceControls();
|
updateResourceControls();
|
||||||
el.documentRow.hidden = !count || !state.editing && count < 2;
|
el.documentRow.hidden = !count || !state.editing && count < 2;
|
||||||
el.documentSelect.disabled = disabled;
|
el.documentSelect.disabled = disabled || state.saving;
|
||||||
const actionsDisabled = disabled || !state.editing || Boolean(manager?.loading || manager?.failed);
|
const actionsDisabled = disabled || state.saving || !state.editing || Boolean(manager?.loading || manager?.failed);
|
||||||
el.documentList.inert = busy || Boolean(manager?.loading);
|
el.documentList.inert = busy || Boolean(manager?.loading);
|
||||||
el.documentAddButton.disabled = actionsDisabled;
|
el.documentAddButton.disabled = actionsDisabled;
|
||||||
el.documentImportButton.disabled = actionsDisabled;
|
el.documentImportButton.disabled = actionsDisabled;
|
||||||
@@ -12045,7 +12143,8 @@ function updateControls() {
|
|||||||
renderSecurity();
|
renderSecurity();
|
||||||
updateStructureControls();
|
updateStructureControls();
|
||||||
const hasDoc = Boolean(state.doc);
|
const hasDoc = Boolean(state.doc);
|
||||||
el.closeButton.disabled = !hasDoc || state.exporting || document.body.hasAttribute("data-busy");
|
el.newButton.disabled = state.saving;
|
||||||
|
el.closeButton.disabled = !hasDoc || state.exporting || state.saving || document.body.hasAttribute("data-busy");
|
||||||
updateDocumentControls();
|
updateDocumentControls();
|
||||||
updateDisplayControls();
|
updateDisplayControls();
|
||||||
updateEditorTools();
|
updateEditorTools();
|
||||||
@@ -12083,7 +12182,8 @@ function updateEditorTools() {
|
|||||||
el.editorTools.hidden = !state.editing;
|
el.editorTools.hidden = !state.editing;
|
||||||
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
||||||
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
||||||
el.saveButton.disabled = !state.doc || !state.ready || state.exporting || (!state.editorInfo && !state.ofdBytes);
|
el.saveButton.disabled = !state.doc || !state.ready || state.exporting || state.saving || (!state.editorInfo && !state.ofdBytes);
|
||||||
|
el.saveAsButton.disabled = el.saveButton.disabled;
|
||||||
el.addPageButton.disabled = pagesDisabled;
|
el.addPageButton.disabled = pagesDisabled;
|
||||||
const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex];
|
const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex];
|
||||||
const selectionDisabled = pagesDisabled || !selectedPages.length;
|
const selectionDisabled = pagesDisabled || !selectedPages.length;
|
||||||
@@ -12225,6 +12325,11 @@ async function callWASM(name, ...args) {
|
|||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = ++wasmRequestID;
|
const id = ++wasmRequestID;
|
||||||
|
if (name === "ofdgoPrepareSave" || name === "ofdgoSaveSnapshot") {
|
||||||
|
state.saveRequestID = id;
|
||||||
|
el.cancelExportButton.hidden = false;
|
||||||
|
el.cancelExportButton.disabled = false;
|
||||||
|
}
|
||||||
if (name === "ofdgoResources" && resourceElements.Panel.open) resourceManager.requestID = id;
|
if (name === "ofdgoResources" && resourceElements.Panel.open) resourceManager.requestID = id;
|
||||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
||||||
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
|
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
|
||||||
@@ -12264,7 +12369,7 @@ function showError(err, empty = !state.doc) {
|
|||||||
|
|
||||||
function setBusy(busy, text = "", percent = 0, status = "") {
|
function setBusy(busy, text = "", percent = 0, status = "") {
|
||||||
document.body.toggleAttribute("data-busy", busy);
|
document.body.toggleAttribute("data-busy", busy);
|
||||||
el.closeButton.disabled = busy || !state.doc || state.exporting;
|
el.closeButton.disabled = busy || !state.doc || state.exporting || state.saving;
|
||||||
el.refreshAppButton.disabled = busy;
|
el.refreshAppButton.disabled = busy;
|
||||||
updateStructureControls();
|
updateStructureControls();
|
||||||
updateDocumentControls();
|
updateDocumentControls();
|
||||||
@@ -12296,7 +12401,11 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
el.fontList.inert = busy;
|
el.fontList.inert = busy;
|
||||||
el.docFontList.inert = busy;
|
el.docFontList.inert = busy;
|
||||||
if (!busy) {
|
if (!busy) {
|
||||||
el.progressPanel.hidden = true;
|
el.progressPanel.hidden = !state.saving;
|
||||||
|
if (state.saving) {
|
||||||
|
el.cancelExportButton.hidden = false;
|
||||||
|
setProgress("正在保存文档", null);
|
||||||
|
}
|
||||||
if (state.fontSyncPending) {
|
if (state.fontSyncPending) {
|
||||||
window.setTimeout(syncUserFonts, 0);
|
window.setTimeout(syncUserFonts, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function directoryPicker() {
|
|||||||
class NativeFile {
|
class NativeFile {
|
||||||
constructor(value) {
|
constructor(value) {
|
||||||
Object.assign(this, value);
|
Object.assign(this, value);
|
||||||
|
if (value.handle) this.handle = new NativeHandle(nativeHost, value.handle);
|
||||||
this.byteLength = this.size;
|
this.byteLength = this.size;
|
||||||
this.type ||= "application/octet-stream";
|
this.type ||= "application/octet-stream";
|
||||||
if (value.temporary) nativeHost?.resources?.register(this, value.nativeFile);
|
if (value.temporary) nativeHost?.resources?.register(this, value.nativeFile);
|
||||||
@@ -157,7 +158,7 @@ class NativeHost {
|
|||||||
if (data.type === "font") return;
|
if (data.type === "font") return;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
requests.set(data.id, controller);
|
requests.set(data.id, controller);
|
||||||
pending = pending.then(async () => {
|
const operation = pending.then(async () => {
|
||||||
const engine = await ready;
|
const engine = await ready;
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
controller.signal.throwIfAborted();
|
controller.signal.throwIfAborted();
|
||||||
@@ -188,6 +189,7 @@ class NativeHost {
|
|||||||
if (closed) { await host.release(result); return; }
|
if (closed) { await host.release(result); return; }
|
||||||
emit({id:data.id, ...decoded});
|
emit({id:data.id, ...decoded});
|
||||||
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => requests.delete(data.id));
|
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => requests.delete(data.id));
|
||||||
|
if (data.name !== "ofdgoSaveSnapshot") pending = operation;
|
||||||
},
|
},
|
||||||
terminate() {
|
terminate() {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ self.onmessage = ({ data }) => {
|
|||||||
operations.get(data.id)?.abort();
|
operations.get(data.id)?.abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.name === "ofdgoResources") operations.set(data.id, new AbortController());
|
if (["ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot"].includes(data.name)) operations.set(data.id, new AbortController());
|
||||||
|
if (data.name === "ofdgoSaveSnapshot") {
|
||||||
|
pending = pending.then(() => { void handleMessage(data); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
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") {
|
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());
|
operations.set(data.id, new AbortController());
|
||||||
}
|
}
|
||||||
@@ -173,12 +177,14 @@ async function handleBatchMessage({ id, name, args }) {
|
|||||||
}
|
}
|
||||||
async function handleMessage({ id, name, args }) {
|
async function handleMessage({ id, name, args }) {
|
||||||
let output;
|
let output;
|
||||||
|
let saveFile;
|
||||||
|
let saveStamp;
|
||||||
let channel;
|
let channel;
|
||||||
const signal = operations.get(id)?.signal;
|
const signal = operations.get(id)?.signal;
|
||||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
|
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
|
||||||
const converting = name === "ofdgoConvertPDF";
|
const converting = name === "ofdgoConvertPDF";
|
||||||
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned";
|
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned";
|
||||||
const resources = name === "ofdgoResources";
|
const resources = name === "ofdgoResources" || name === "ofdgoPrepareSave";
|
||||||
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
|
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
|
||||||
const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null;
|
const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null;
|
||||||
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
|
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
|
||||||
@@ -230,7 +236,10 @@ async function handleMessage({ id, name, args }) {
|
|||||||
});
|
});
|
||||||
if (converting) args.push((name, done) => requestFont(id, signal, name, done));
|
if (converting) args.push((name, done) => requestFont(id, signal, name, done));
|
||||||
} else {
|
} else {
|
||||||
const file = args.pop();
|
const destination = args.pop();
|
||||||
|
const file = destination?.fileHandle || destination;
|
||||||
|
saveFile = file;
|
||||||
|
saveStamp = destination?.saveStamp;
|
||||||
const compression = saving && name !== "ofdgoSaveSigned" && args.at(-1)?.mode !== undefined ? args.pop() : null;
|
const compression = saving && name !== "ofdgoSaveSigned" && args.at(-1)?.mode !== undefined ? args.pop() : null;
|
||||||
const indices = saving ? args.shift() : null;
|
const indices = saving ? args.shift() : null;
|
||||||
if (file) output = await file.createWritable();
|
if (file) output = await file.createWritable();
|
||||||
@@ -263,7 +272,7 @@ async function handleMessage({ id, name, args }) {
|
|||||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||||
finish(done);
|
finish(done);
|
||||||
});
|
});
|
||||||
if (compression) args.push(indices, compression, done => finish(done));
|
if (compression || name === "ofdgoSaveSnapshot") args.push(indices, compression || {mode: 0}, done => finish(done));
|
||||||
else if (indices != null) args.push(indices);
|
else if (indices != null) args.push(indices);
|
||||||
}
|
}
|
||||||
if (!saving && args.some(value => value?.mode !== undefined)) args.push(done => finish(done));
|
if (!saving && args.some(value => value?.mode !== undefined)) args.push(done => finish(done));
|
||||||
@@ -277,6 +286,10 @@ async function handleMessage({ id, name, args }) {
|
|||||||
result.data.size = size;
|
result.data.size = size;
|
||||||
if (name !== "ofdgoExportMedia") self.postMessage({ id, type: "export", stage: "save" });
|
if (name !== "ofdgoExportMedia") self.postMessage({ id, type: "export", stage: "save" });
|
||||||
if (output) {
|
if (output) {
|
||||||
|
if (saveStamp) {
|
||||||
|
const current = await saveFile.getFile();
|
||||||
|
if (current.size !== saveStamp.size || current.lastModified !== saveStamp.modified) throw new Error("文件已被修改,请另存");
|
||||||
|
}
|
||||||
await output.close();
|
await output.close();
|
||||||
} else {
|
} else {
|
||||||
result.data.blob = new Blob(chunks, { type: result.data.mime });
|
result.data.blob = new Blob(chunks, { type: result.data.mime });
|
||||||
@@ -289,7 +302,7 @@ async function handleMessage({ id, name, args }) {
|
|||||||
self.postMessage({ id, ...result }, transfer);
|
self.postMessage({ id, ...result }, transfer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await output?.abort().catch(() => {});
|
await output?.abort().catch(() => {});
|
||||||
const result = { id, ok: false, error: err.message };
|
const result = { id, ok: false, error: err.message, errorName: err.name };
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
result.error = resources ? "查询已取消" : importing ? "导入已取消" : converting ? "转换已取消" : "导出已取消";
|
result.error = resources ? "查询已取消" : importing ? "导入已取消" : converting ? "转换已取消" : "导出已取消";
|
||||||
result.canceled = true;
|
result.canceled = true;
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// 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 file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrChanged 表示目标文件已被其他操作修改
|
||||||
|
var ErrChanged = errors.New("file changed since opening")
|
||||||
|
|
||||||
|
// ErrBusy 表示目标文件正在写入
|
||||||
|
var ErrBusy = errors.New("file save is in progress")
|
||||||
|
|
||||||
|
// activeFiles 协调当前进程内的目标写入
|
||||||
|
var activeFiles sync.Map
|
||||||
|
|
||||||
|
// Target 保存文件路径及最近确认的文件状态,可跨次写入复用
|
||||||
|
type Target struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
name string
|
||||||
|
info os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer 写入同目录临时文件,提交前检查取消及目标冲突
|
||||||
|
type Writer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
target *Target
|
||||||
|
file *os.File
|
||||||
|
ctx context.Context
|
||||||
|
err error
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTarget 记录目标当前状态,允许目标尚不存在
|
||||||
|
// 入参: name 目标路径
|
||||||
|
// 返回: *Target 保存目标, error 路径或文件错误
|
||||||
|
func NewTarget(name string) (*Target, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
name, err := filepath.Abs(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := os.Lstat(name)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if info != nil && !info.Mode().IsRegular() {
|
||||||
|
return nil, fmt.Errorf("save target is not a regular file")
|
||||||
|
}
|
||||||
|
return &Target{name: name, info: info}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name 返回目标绝对路径
|
||||||
|
// 返回: string 文件路径
|
||||||
|
func (t *Target) Name() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// check 检查目标是否保持最近确认的状态
|
||||||
|
// 返回: error 文件状态错误
|
||||||
|
func (t *Target) check() error {
|
||||||
|
info, err := os.Lstat(t.name)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if t.info == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ErrChanged
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.info == nil || !os.SameFile(t.info, info) || t.info.Size() != info.Size() || !t.info.ModTime().Equal(info.ModTime()) {
|
||||||
|
return ErrChanged
|
||||||
|
}
|
||||||
|
if info.Mode().Perm()&0222 == 0 {
|
||||||
|
return os.ErrPermission
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin 创建独占写入事务,调用方必须提交或终止
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Writer 事务, error 创建错误
|
||||||
|
func (t *Target) Begin(ctx context.Context) (*Writer, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if _, loaded := activeFiles.LoadOrStore(t.name, true); loaded {
|
||||||
|
return nil, ErrBusy
|
||||||
|
}
|
||||||
|
if err := t.check(); err != nil {
|
||||||
|
activeFiles.Delete(t.name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for range 100 {
|
||||||
|
n, err := rand.Int(rand.Reader, big.NewInt(10000000))
|
||||||
|
if err != nil {
|
||||||
|
activeFiles.Delete(t.name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := fmt.Sprintf("%s.%07d.tmp", t.name, n.Int64())
|
||||||
|
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||||
|
if errors.Is(err, os.ErrExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
activeFiles.Delete(t.name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Writer{target: t, file: f, ctx: ctx}, nil
|
||||||
|
}
|
||||||
|
activeFiles.Delete(t.name)
|
||||||
|
return nil, os.ErrExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write 追加数据并记录写入错误
|
||||||
|
// 入参: data 数据
|
||||||
|
// 返回: int 写入长度, error 写入错误
|
||||||
|
func (w *Writer) Write(data []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.closed {
|
||||||
|
return 0, os.ErrClosed
|
||||||
|
}
|
||||||
|
if w.err == nil {
|
||||||
|
w.err = w.ctx.Err()
|
||||||
|
}
|
||||||
|
if w.err != nil {
|
||||||
|
return 0, w.err
|
||||||
|
}
|
||||||
|
n, err := w.file.Write(data)
|
||||||
|
if err == nil && n != len(data) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
w.err = err
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 完成写入并替换目标,成功后刷新目标状态
|
||||||
|
// 返回: error 提交错误
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.closed {
|
||||||
|
return os.ErrClosed
|
||||||
|
}
|
||||||
|
w.closed = true
|
||||||
|
defer activeFiles.Delete(w.target.name)
|
||||||
|
defer os.Remove(w.file.Name())
|
||||||
|
w.target.mu.Lock()
|
||||||
|
defer w.target.mu.Unlock()
|
||||||
|
if err := errors.Join(w.err, w.ctx.Err(), w.target.check()); err != nil {
|
||||||
|
return errors.Join(err, w.file.Close())
|
||||||
|
}
|
||||||
|
if w.target.info != nil {
|
||||||
|
if err := w.file.Chmod(w.target.info.Mode().Perm()); err != nil {
|
||||||
|
return errors.Join(err, w.file.Close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := w.file.Sync(); err != nil {
|
||||||
|
return errors.Join(err, w.file.Close())
|
||||||
|
}
|
||||||
|
info, err := w.file.Stat()
|
||||||
|
if err = errors.Join(err, w.file.Close(), w.ctx.Err()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := w.target.check(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := Replace(w.file.Name(), w.target.name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.target.info = info
|
||||||
|
if current, err := os.Stat(w.target.name); err == nil {
|
||||||
|
w.target.info = current
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort 丢弃临时输出,不改变目标
|
||||||
|
// 返回: error 清理错误
|
||||||
|
func (w *Writer) Abort() error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.closed = true
|
||||||
|
defer activeFiles.Delete(w.target.name)
|
||||||
|
return errors.Join(w.file.Close(), os.Remove(w.file.Name()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package file
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// Open 打开文件,路径替换后已有读取句柄仍有效
|
||||||
|
// 入参: name 文件路径
|
||||||
|
// 返回: *os.File 文件, error 打开错误
|
||||||
|
func Open(name string) (*os.File, error) {
|
||||||
|
return os.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace 替换目标路径
|
||||||
|
// 入参: source 临时路径, target 目标路径
|
||||||
|
// 返回: error 替换错误
|
||||||
|
func Replace(source, target string) error {
|
||||||
|
return os.Rename(source, target)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// 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 file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// replaceFileProc 保留目标文件属性并替换文件内容
|
||||||
|
var replaceFileProc = syscall.NewLazyDLL("kernel32.dll").NewProc("ReplaceFileW")
|
||||||
|
|
||||||
|
// Open 保留替换共享权限,路径替换后已有读取句柄仍有效
|
||||||
|
// 入参: name 文件路径
|
||||||
|
// 返回: *os.File 文件, error 打开错误
|
||||||
|
func Open(name string) (*os.File, error) {
|
||||||
|
path, err := nativePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handle, err := syscall.CreateFile(path, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(handle), name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace 替换目标,目标不存在时重命名
|
||||||
|
// 入参: source 临时路径, target 目标路径
|
||||||
|
// 返回: error 替换错误
|
||||||
|
func Replace(source, target string) error {
|
||||||
|
from, err := nativePath(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
to, err := nativePath(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, _, err := replaceFileProc.Call(uintptr(unsafe.Pointer(to)), uintptr(unsafe.Pointer(from)), 0, 0, 0, 0)
|
||||||
|
if result != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err == syscall.ERROR_FILE_NOT_FOUND {
|
||||||
|
return os.Rename(source, target)
|
||||||
|
}
|
||||||
|
return &os.LinkError{Op: "replace", Old: source, New: target, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativePath 转换绝对长路径
|
||||||
|
// 入参: name 路径
|
||||||
|
// 返回: *uint16 原生路径, error 转换错误
|
||||||
|
func nativePath(name string) (*uint16, error) {
|
||||||
|
path, err := filepath.Abs(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, `\\?\`) {
|
||||||
|
if strings.HasPrefix(path, `\\`) {
|
||||||
|
path = `\\?\UNC\` + path[2:]
|
||||||
|
} else {
|
||||||
|
path = `\\?\` + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return syscall.UTF16PtrFromString(path)
|
||||||
|
}
|
||||||
@@ -419,6 +419,7 @@ type outputContext struct {
|
|||||||
type businessEngine struct {
|
type businessEngine struct {
|
||||||
currentSession *Session
|
currentSession *Session
|
||||||
currentEditor *ofdgo.Editor
|
currentEditor *ofdgo.Editor
|
||||||
|
saveSnapshot *ofdgo.Editor
|
||||||
pendingImport *ofdgo.Reader
|
pendingImport *ofdgo.Reader
|
||||||
copiedObjects *editorClipboard
|
copiedObjects *editorClipboard
|
||||||
copiedStyle *ofdgo.GraphicObject
|
copiedStyle *ofdgo.GraphicObject
|
||||||
@@ -7162,6 +7163,54 @@ func (e *businessEngine) saveSigned(args []platformValue) (any, error) {
|
|||||||
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
|
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareSave 创建独立保存快照,后续写出不占用当前编辑文档
|
||||||
|
// 入参: args 取消检查回调
|
||||||
|
// 返回: any 快照修订, error 准备错误
|
||||||
|
func (e *businessEngine) prepareSave(args []platformValue) (any, error) {
|
||||||
|
e.saveSnapshot = nil
|
||||||
|
editor := e.currentEditor
|
||||||
|
if editor == nil {
|
||||||
|
if e.currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
editor, err = e.currentSession.Reader.Editor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := e.operationContext()
|
||||||
|
if len(args) > 0 {
|
||||||
|
ctx = e.newOutputContext(args[0])
|
||||||
|
}
|
||||||
|
snapshot, err := editor.Snapshot(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.saveSnapshot = snapshot
|
||||||
|
return successResult(map[string]any{"revision": editor.Revision()}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// savePrepared 写出并释放已准备的快照,不修改当前编辑状态
|
||||||
|
// 入参: args 写出回调、进度回调及输出配置
|
||||||
|
// 返回: any 保存结果, error 保存错误
|
||||||
|
func (e *businessEngine) savePrepared(args []platformValue) (any, error) {
|
||||||
|
snapshot := e.saveSnapshot
|
||||||
|
e.saveSnapshot = nil
|
||||||
|
if snapshot == nil {
|
||||||
|
return nil, fmt.Errorf("save snapshot is not prepared")
|
||||||
|
}
|
||||||
|
return e.saveEditor(snapshot, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// discardSave 释放尚未写出的保存快照
|
||||||
|
// 入参: args 空参数
|
||||||
|
// 返回: any 操作结果, error 错误信息
|
||||||
|
func (e *businessEngine) discardSave(args []platformValue) (any, error) {
|
||||||
|
e.saveSnapshot = nil
|
||||||
|
return successResult(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
// saveDocument 分块写出当前编辑文档或指定页面
|
// saveDocument 分块写出当前编辑文档或指定页面
|
||||||
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
|
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: any 保存结果, error 错误信息
|
||||||
@@ -7403,6 +7452,9 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
|
|||||||
"ofdgoUndo": func([]platformValue) (any, error) { return e.restoreEditor(false) },
|
"ofdgoUndo": func([]platformValue) (any, error) { return e.restoreEditor(false) },
|
||||||
"ofdgoRedo": func([]platformValue) (any, error) { return e.restoreEditor(true) },
|
"ofdgoRedo": func([]platformValue) (any, error) { return e.restoreEditor(true) },
|
||||||
"ofdgoSaveDocument": e.saveDocument,
|
"ofdgoSaveDocument": e.saveDocument,
|
||||||
|
"ofdgoPrepareSave": e.prepareSave,
|
||||||
|
"ofdgoSaveSnapshot": e.savePrepared,
|
||||||
|
"ofdgoDiscardSave": e.discardSave,
|
||||||
"ofdgoSetEncryption": e.setEncryption,
|
"ofdgoSetEncryption": e.setEncryption,
|
||||||
"ofdgoSaveSigned": e.saveSigned,
|
"ofdgoSaveSigned": e.saveSigned,
|
||||||
"ofdgoPreviewSignatureSeal": e.previewSignatureSeal,
|
"ofdgoPreviewSignatureSeal": e.previewSignatureSeal,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type NativeEngine struct {
|
|||||||
CleanupOutput func(context.Context, NativeOutputTarget) error
|
CleanupOutput func(context.Context, NativeOutputTarget) error
|
||||||
gate chan struct{}
|
gate chan struct{}
|
||||||
once sync.Once
|
once sync.Once
|
||||||
|
saves sync.WaitGroup
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,13 +150,33 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nativeFailure(ctx.Err())
|
return nativeFailure(ctx.Err())
|
||||||
}
|
}
|
||||||
defer func() { <-e.gate }()
|
locked := true
|
||||||
|
defer func() {
|
||||||
|
if locked {
|
||||||
|
<-e.gate
|
||||||
|
}
|
||||||
|
}()
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nativeFailure(err)
|
return nativeFailure(err)
|
||||||
}
|
}
|
||||||
if e.closed {
|
if e.closed {
|
||||||
return nativeFailure(fmt.Errorf("native engine is closed"))
|
return nativeFailure(fmt.Errorf("native engine is closed"))
|
||||||
}
|
}
|
||||||
|
if name == "ofdgoSaveSnapshot" {
|
||||||
|
snapshot := e.saveSnapshot
|
||||||
|
e.saveSnapshot = nil
|
||||||
|
if snapshot == nil {
|
||||||
|
return nativeFailure(fmt.Errorf("save snapshot is not prepared"))
|
||||||
|
}
|
||||||
|
output := &NativeEngine{OpenWriter: e.OpenWriter}
|
||||||
|
output.currentEditor = snapshot
|
||||||
|
e.saves.Add(1)
|
||||||
|
defer e.saves.Done()
|
||||||
|
<-e.gate
|
||||||
|
locked = false
|
||||||
|
defer output.Close()
|
||||||
|
return output.Call(ctx, "ofdgoSaveDocument", args, emit)
|
||||||
|
}
|
||||||
e.ctx = ctx
|
e.ctx = ctx
|
||||||
if e.currentSession != nil {
|
if e.currentSession != nil {
|
||||||
e.currentSession.Renderer.OutputContext = ctx
|
e.currentSession.Renderer.OutputContext = ctx
|
||||||
@@ -219,6 +240,8 @@ func (e *NativeEngine) Close() error {
|
|||||||
e.gate <- struct{}{}
|
e.gate <- struct{}{}
|
||||||
defer func() { <-e.gate }()
|
defer func() { <-e.gate }()
|
||||||
e.closed = true
|
e.closed = true
|
||||||
|
e.saveSnapshot = nil
|
||||||
|
e.saves.Wait()
|
||||||
var err error
|
var err error
|
||||||
if e.currentSession != nil {
|
if e.currentSession != nil {
|
||||||
err = e.currentSession.Close()
|
err = e.currentSession.Close()
|
||||||
@@ -728,7 +751,7 @@ func (c *nativeCall) prepare(name string, args []any) ([]any, error) {
|
|||||||
args = append(args, c.fileCallback(files))
|
args = append(args, c.fileCallback(files))
|
||||||
}
|
}
|
||||||
switch name {
|
switch name {
|
||||||
case "ofdgoResources":
|
case "ofdgoResources", "ofdgoPrepareSave":
|
||||||
return append(args, checkpoint), nil
|
return append(args, checkpoint), nil
|
||||||
case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages":
|
case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages":
|
||||||
return append(args, c.progress("import", "", true)), nil
|
return append(args, c.progress("import", "", true)), nil
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ func RunWASM() {
|
|||||||
"ofdgoImportPages": true,
|
"ofdgoImportPages": true,
|
||||||
"ofdgoImportDocuments": true,
|
"ofdgoImportDocuments": true,
|
||||||
"ofdgoSaveDocument": true,
|
"ofdgoSaveDocument": true,
|
||||||
|
"ofdgoPrepareSave": true,
|
||||||
|
"ofdgoSaveSnapshot": true,
|
||||||
"ofdgoSetEncryption": true,
|
"ofdgoSetEncryption": true,
|
||||||
"ofdgoSaveSigned": true,
|
"ofdgoSaveSigned": true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
|||||||
case "remove":
|
case "remove":
|
||||||
return nil, h.files.remove(text("handle"), text("name"), flag("recursive"))
|
return nil, h.files.remove(text("handle"), text("name"), flag("recursive"))
|
||||||
case "write.begin":
|
case "write.begin":
|
||||||
writer, err := h.files.begin(text("handle"))
|
writer, err := h.files.begin(guigo.PageContext(ctx), text("handle"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -660,7 +660,7 @@ func (h *Host) openWriter(ctx context.Context, value any, name string) (io.Write
|
|||||||
return nil, fmt.Errorf("输出文件已存在")
|
return nil, fmt.Errorf("输出文件已存在")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return h.files.begin(id)
|
return h.files.begin(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reserveOutput 为一次转换预留独立输出文件或目录
|
// reserveOutput 为一次转换预留独立输出文件或目录
|
||||||
|
|||||||
@@ -29,18 +29,23 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
basefile "github.com/xiaoqidun/ofdgo/internal/base/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fileEntry 保存窗口授权的文件或目录
|
// fileEntry 保存窗口授权的文件或目录
|
||||||
type fileEntry struct {
|
type fileEntry struct {
|
||||||
path string
|
path string
|
||||||
name string
|
name string
|
||||||
data []byte
|
data []byte
|
||||||
file *os.File
|
file *os.File
|
||||||
stop func() bool
|
stop func() bool
|
||||||
directory bool
|
directory bool
|
||||||
owned bool
|
owned bool
|
||||||
temporary bool
|
temporary bool
|
||||||
|
saveTarget *basefile.Target
|
||||||
|
readers int
|
||||||
|
retired bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileStore 管理窗口范围内的文件授权和临时数据
|
// fileStore 管理窗口范围内的文件授权和临时数据
|
||||||
@@ -55,12 +60,8 @@ type fileStore struct {
|
|||||||
// fileWriter 在同目录临时文件中保存输出
|
// fileWriter 在同目录临时文件中保存输出
|
||||||
type fileWriter struct {
|
type fileWriter struct {
|
||||||
id string
|
id string
|
||||||
mu sync.Mutex
|
|
||||||
store *fileStore
|
store *fileStore
|
||||||
file *os.File
|
output *basefile.Writer
|
||||||
target string
|
|
||||||
err error
|
|
||||||
closed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newFileStore 创建独立文件授权表
|
// newFileStore 创建独立文件授权表
|
||||||
@@ -124,7 +125,14 @@ func (s *fileStore) add(name string, directory bool) (map[string]any, error) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
id := rand.Text()
|
id := rand.Text()
|
||||||
s.entries[id] = &fileEntry{path: name, name: filepath.Base(name), directory: directory}
|
entry := &fileEntry{path: name, name: filepath.Base(name), directory: directory}
|
||||||
|
if !directory {
|
||||||
|
entry.saveTarget, err = basefile.NewTarget(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.entries[id] = entry
|
||||||
return s.descriptor(id), nil
|
return s.descriptor(id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,9 +169,10 @@ func (s *fileStore) file(id string) (map[string]any, error) {
|
|||||||
if entry == nil || entry.directory {
|
if entry == nil || entry.directory {
|
||||||
return nil, fmt.Errorf("文件授权无效")
|
return nil, fmt.Errorf("文件授权无效")
|
||||||
}
|
}
|
||||||
|
handle := s.descriptor(id)
|
||||||
size := int64(len(entry.data))
|
size := int64(len(entry.data))
|
||||||
if entry.data == nil {
|
if entry.data == nil {
|
||||||
file, err := openReadFile(entry.path)
|
file, err := basefile.Open(entry.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -177,7 +186,7 @@ func (s *fileStore) file(id string) (map[string]any, error) {
|
|||||||
entry = &fileEntry{path: entry.path, name: entry.name, file: file}
|
entry = &fileEntry{path: entry.path, name: entry.name, file: file}
|
||||||
s.entries[id] = entry
|
s.entries[id] = entry
|
||||||
}
|
}
|
||||||
return map[string]any{"nativeFile": id, "name": entry.name, "size": size, "url": "/__native/data/" + id}, nil
|
return map[string]any{"nativeFile": id, "name": entry.name, "size": size, "url": "/__native/data/" + id, "handle": handle}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve 按授权标识读取文件并保持随机读取句柄
|
// resolve 按授权标识读取文件并保持随机读取句柄
|
||||||
@@ -194,7 +203,7 @@ func (s *fileStore) resolve(id string) (io.ReaderAt, int64, error) {
|
|||||||
return bytes.NewReader(entry.data), int64(len(entry.data)), nil
|
return bytes.NewReader(entry.data), int64(len(entry.data)), nil
|
||||||
}
|
}
|
||||||
if entry.file == nil {
|
if entry.file == nil {
|
||||||
file, err := openReadFile(entry.path)
|
file, err := basefile.Open(entry.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -237,7 +246,27 @@ func (s *fileStore) resource(id string) (io.ReaderAt, int64, func() error, error
|
|||||||
if entry.data != nil {
|
if entry.data != nil {
|
||||||
return bytes.NewReader(entry.data), int64(len(entry.data)), nil, nil
|
return bytes.NewReader(entry.data), int64(len(entry.data)), nil, nil
|
||||||
}
|
}
|
||||||
file, err := openReadFile(entry.path)
|
if entry.file != nil && !entry.temporary {
|
||||||
|
info, err := entry.file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, nil, err
|
||||||
|
}
|
||||||
|
entry.readers++
|
||||||
|
var once sync.Once
|
||||||
|
var closeErr error
|
||||||
|
return entry.file, info.Size(), func() error {
|
||||||
|
once.Do(func() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
entry.readers--
|
||||||
|
if entry.retired && entry.readers == 0 {
|
||||||
|
closeErr = entry.file.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return closeErr
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
file, err := basefile.Open(entry.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, nil, err
|
return nil, 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -374,9 +403,9 @@ func (s *fileStore) reserve(id, base, extension string, directory bool) (string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// begin 创建文件写入事务
|
// begin 创建文件写入事务
|
||||||
// 入参: id 输出授权标识
|
// 入参: ctx 取消上下文, id 输出授权标识
|
||||||
// 返回: *fileWriter 事务写入器, error 创建错误
|
// 返回: *fileWriter 事务写入器, error 创建错误
|
||||||
func (s *fileStore) begin(id string) (*fileWriter, error) {
|
func (s *fileStore) begin(ctx context.Context, id string) (*fileWriter, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if s.closed {
|
if s.closed {
|
||||||
@@ -386,11 +415,18 @@ func (s *fileStore) begin(id string) (*fileWriter, error) {
|
|||||||
if entry == nil || entry.directory || entry.path == "" {
|
if entry == nil || entry.directory || entry.path == "" {
|
||||||
return nil, fmt.Errorf("输出授权无效")
|
return nil, fmt.Errorf("输出授权无效")
|
||||||
}
|
}
|
||||||
file, err := temporaryFile(filepath.Dir(entry.path), filepath.Base(entry.path)+".")
|
if entry.saveTarget == nil {
|
||||||
|
var err error
|
||||||
|
entry.saveTarget, err = basefile.NewTarget(entry.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output, err := entry.saveTarget.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
writer := &fileWriter{id: rand.Text(), store: s, file: file, target: entry.path}
|
writer := &fileWriter{id: rand.Text(), store: s, output: output}
|
||||||
s.writers[writer.id] = writer
|
s.writers[writer.id] = writer
|
||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
@@ -429,7 +465,8 @@ func (s *fileStore) remove(id, name string, recursive bool) error {
|
|||||||
defer root.Close()
|
defer root.Close()
|
||||||
for key, entry := range s.entries {
|
for key, entry := range s.entries {
|
||||||
if entry.path == target || recursive && strings.HasPrefix(entry.path, target+string(filepath.Separator)) {
|
if entry.path == target || recursive && strings.HasPrefix(entry.path, target+string(filepath.Separator)) {
|
||||||
if entry.file != nil {
|
entry.retired = true
|
||||||
|
if entry.file != nil && entry.readers == 0 {
|
||||||
entry.file.Close()
|
entry.file.Close()
|
||||||
}
|
}
|
||||||
delete(s.entries, key)
|
delete(s.entries, key)
|
||||||
@@ -445,62 +482,21 @@ func (s *fileStore) remove(id, name string, recursive bool) error {
|
|||||||
// 入参: data 输出数据
|
// 入参: data 输出数据
|
||||||
// 返回: int 写入字节数, error 写入错误
|
// 返回: int 写入字节数, error 写入错误
|
||||||
func (w *fileWriter) Write(data []byte) (int, error) {
|
func (w *fileWriter) Write(data []byte) (int, error) {
|
||||||
w.mu.Lock()
|
return w.output.Write(data)
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.closed {
|
|
||||||
return 0, os.ErrClosed
|
|
||||||
}
|
|
||||||
if w.err != nil {
|
|
||||||
return 0, w.err
|
|
||||||
}
|
|
||||||
n, err := w.file.Write(data)
|
|
||||||
if err == nil && n != len(data) {
|
|
||||||
err = io.ErrShortWrite
|
|
||||||
}
|
|
||||||
w.err = err
|
|
||||||
return n, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 同步并替换目标文件
|
// Close 同步并替换目标文件
|
||||||
// 返回: error 提交错误
|
// 返回: error 提交错误
|
||||||
func (w *fileWriter) Close() error {
|
func (w *fileWriter) Close() error {
|
||||||
defer w.release()
|
defer w.release()
|
||||||
w.mu.Lock()
|
return w.output.Close()
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.closed {
|
|
||||||
return os.ErrClosed
|
|
||||||
}
|
|
||||||
w.closed = true
|
|
||||||
if w.err != nil {
|
|
||||||
return errors.Join(w.err, w.file.Close(), os.Remove(w.file.Name()))
|
|
||||||
}
|
|
||||||
if err := w.file.Sync(); err != nil {
|
|
||||||
w.file.Close()
|
|
||||||
os.Remove(w.file.Name())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := w.file.Close(); err != nil {
|
|
||||||
os.Remove(w.file.Name())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := replaceFile(w.file.Name(), w.target); err != nil {
|
|
||||||
os.Remove(w.file.Name())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abort 删除未提交的临时输出
|
// Abort 删除未提交的临时输出
|
||||||
// 返回: error 清理错误
|
// 返回: error 清理错误
|
||||||
func (w *fileWriter) Abort() error {
|
func (w *fileWriter) Abort() error {
|
||||||
defer w.release()
|
defer w.release()
|
||||||
w.mu.Lock()
|
return w.output.Abort()
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
w.closed = true
|
|
||||||
return errors.Join(w.file.Close(), os.Remove(w.file.Name()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// release 移除已结束的写入事务
|
// release 移除已结束的写入事务
|
||||||
@@ -556,9 +552,12 @@ func (s *fileStore) close() error {
|
|||||||
if entry.stop != nil {
|
if entry.stop != nil {
|
||||||
entry.stop()
|
entry.stop()
|
||||||
}
|
}
|
||||||
if entry.file != nil {
|
s.mu.Lock()
|
||||||
|
entry.retired = true
|
||||||
|
if entry.file != nil && entry.readers == 0 {
|
||||||
err = errors.Join(err, entry.file.Close())
|
err = errors.Join(err, entry.file.Close())
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
return errors.Join(err, os.RemoveAll(s.temporary))
|
return errors.Join(err, os.RemoveAll(s.temporary))
|
||||||
}
|
}
|
||||||
@@ -16,21 +16,9 @@
|
|||||||
|
|
||||||
package webview
|
package webview
|
||||||
|
|
||||||
import "os"
|
|
||||||
|
|
||||||
// conversionMemory 未提供系统内存探测时仅按并行数调度
|
// conversionMemory 未提供系统内存探测时仅按并行数调度
|
||||||
var conversionMemory func() int64
|
var conversionMemory func() int64
|
||||||
|
|
||||||
// openReadFile 打开独立读取句柄
|
|
||||||
func openReadFile(name string) (*os.File, error) {
|
|
||||||
return os.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// replaceFile 替换目标文件并保留已有读取句柄
|
|
||||||
func replaceFile(source, target string) error {
|
|
||||||
return os.Rename(source, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontNameLanguage 未指定系统显示语言时沿用字体名称表顺序
|
// fontNameLanguage 未指定系统显示语言时沿用字体名称表顺序
|
||||||
// 返回: uint16 未指定平台标识, uint16 未指定语言标识
|
// 返回: uint16 未指定平台标识, uint16 未指定语言标识
|
||||||
func fontNameLanguage() (uint16, uint16) {
|
func fontNameLanguage() (uint16, uint16) {
|
||||||
|
|||||||
@@ -15,68 +15,13 @@
|
|||||||
package webview
|
package webview
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// replaceFileProc 保留目标文件属性并替换文件内容
|
|
||||||
var replaceFileProc = syscall.NewLazyDLL("kernel32.dll").NewProc("ReplaceFileW")
|
|
||||||
|
|
||||||
// memoryStatusProc 查询系统物理内存和提交额度
|
// memoryStatusProc 查询系统物理内存和提交额度
|
||||||
var memoryStatusProc = syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
|
var memoryStatusProc = syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
|
||||||
|
|
||||||
// openReadFile 允许原子替换路径,同时保持已有读取句柄有效
|
|
||||||
func openReadFile(name string) (*os.File, error) {
|
|
||||||
path, err := nativeFilePath(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
|
||||||
}
|
|
||||||
handle, err := syscall.CreateFile(path, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
|
||||||
}
|
|
||||||
return os.NewFile(uintptr(handle), name), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// replaceFile 替换已有文件,目标不存在时直接重命名
|
|
||||||
func replaceFile(source, target string) error {
|
|
||||||
from, err := nativeFilePath(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
to, err := nativeFilePath(target)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result, _, err := replaceFileProc.Call(uintptr(unsafe.Pointer(to)), uintptr(unsafe.Pointer(from)), 0, 0, 0, 0)
|
|
||||||
if result != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err == syscall.ERROR_FILE_NOT_FOUND {
|
|
||||||
return os.Rename(source, target)
|
|
||||||
}
|
|
||||||
return &os.LinkError{Op: "replace", Old: source, New: target, Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nativeFilePath 转换绝对路径并支持长路径与网络共享
|
|
||||||
func nativeFilePath(name string) (*uint16, error) {
|
|
||||||
path, err := filepath.Abs(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(path, `\\?\`) {
|
|
||||||
if strings.HasPrefix(path, `\\`) {
|
|
||||||
path = `\\?\UNC\` + path[2:]
|
|
||||||
} else {
|
|
||||||
path = `\\?\` + path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return syscall.UTF16PtrFromString(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontNameLanguage 获取系统显示语言对应的OpenType名称表标识
|
// fontNameLanguage 获取系统显示语言对应的OpenType名称表标识
|
||||||
// 返回: uint16 名称表平台标识, uint16 系统显示语言标识
|
// 返回: uint16 名称表平台标识, uint16 系统显示语言标识
|
||||||
func fontNameLanguage() (uint16, uint16) {
|
func fontNameLanguage() (uint16, uint16) {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
|
basefile "github.com/xiaoqidun/ofdgo/internal/base/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open 打开OFD文件
|
// Open 打开OFD文件
|
||||||
@@ -27,14 +29,24 @@ import (
|
|||||||
// 入参: path 文件路径, options 阅读选项
|
// 入参: path 文件路径, options 阅读选项
|
||||||
// 返回: *Reader 阅读器实例, error 错误信息
|
// 返回: *Reader 阅读器实例, error 错误信息
|
||||||
func Open(path string, options ...ReaderOption) (*Reader, error) {
|
func Open(path string, options ...ReaderOption) (*Reader, error) {
|
||||||
r, err := zip.OpenReader(path)
|
f, err := basefile.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r, err := zip.NewReader(f, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
reader := &Reader{
|
reader := &Reader{
|
||||||
Path: path,
|
Path: path,
|
||||||
Zip: &r.Reader,
|
Zip: r,
|
||||||
Closer: r,
|
Closer: f,
|
||||||
}
|
}
|
||||||
if err := reader.open(options); err != nil {
|
if err := reader.open(options); err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
|
|||||||
+3
-1
@@ -33,6 +33,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
basefile "github.com/xiaoqidun/ofdgo/internal/base/file"
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ import (
|
|||||||
const editorPathValidationLimit = 1024
|
const editorPathValidationLimit = 1024
|
||||||
|
|
||||||
// Editor 编辑OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
|
// Editor 编辑OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
|
||||||
// Info可修改文档元数据,通过方法管理页面、对象和资源,WriteTo另存结果,不覆盖输入
|
// Info可修改文档元数据,通过方法管理页面、对象和资源,WriteTo输出副本,Save和SaveAs通过绑定路径保存
|
||||||
// OnWriteProgress可选,按fonts、pages、references、resources、compress、write阶段同步回报进度,返回错误则停止保存
|
// OnWriteProgress可选,按fonts、pages、references、resources、compress、write阶段同步回报进度,返回错误则停止保存
|
||||||
// completed和total为当前阶段已处理及总工作项,compress累计完成条目数且total为0,write阶段不计数
|
// completed和total为当前阶段已处理及总工作项,compress累计完成条目数且total为0,write阶段不计数
|
||||||
// 阶段可重复,准备完成不代表保存成功
|
// 阶段可重复,准备完成不代表保存成功
|
||||||
@@ -73,6 +74,7 @@ type Editor struct {
|
|||||||
outlines []byte
|
outlines []byte
|
||||||
encryption *encryptionState
|
encryption *encryptionState
|
||||||
output *outputOptimization
|
output *outputOptimization
|
||||||
|
saveTarget *basefile.Target
|
||||||
validatedPaths *renderCache[string, bool]
|
validatedPaths *renderCache[string, bool]
|
||||||
richIndex *richTextIndex
|
richIndex *richTextIndex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ func (e *Editor) adoptDocumentReader(ctx context.Context, reader *Reader) error
|
|||||||
next.revision, next.serial = e.revision, e.serial
|
next.revision, next.serial = e.revision, e.serial
|
||||||
next.OnWriteProgress, next.output = e.OnWriteProgress, e.output
|
next.OnWriteProgress, next.output = e.OnWriteProgress, e.output
|
||||||
next.encryption = e.encryption
|
next.encryption = e.encryption
|
||||||
|
next.saveTarget = e.saveTarget
|
||||||
next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS
|
next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS
|
||||||
next.validatedPaths = e.validatedPaths
|
next.validatedPaths = e.validatedPaths
|
||||||
*e = *next
|
*e = *next
|
||||||
|
|||||||
@@ -908,6 +908,7 @@ func (e *Editor) restoreResourceSnapshot(state Editor) {
|
|||||||
}
|
}
|
||||||
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
|
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
|
||||||
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
|
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
|
||||||
|
state.saveTarget = e.saveTarget
|
||||||
state.fontRenderer, state.fontSourcesCache = nil, nil
|
state.fontRenderer, state.fontSourcesCache = nil, nil
|
||||||
state.fontMetrics = make(map[string]FontMetrics)
|
state.fontMetrics = make(map[string]FontMetrics)
|
||||||
*e = state
|
*e = state
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ func (e *Editor) restoreTransaction(state Editor) {
|
|||||||
state.fontSourcesCache = e.fontSourcesCache
|
state.fontSourcesCache = e.fontSourcesCache
|
||||||
state.validatedPaths = e.validatedPaths
|
state.validatedPaths = e.validatedPaths
|
||||||
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
|
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
|
||||||
|
state.saveTarget = e.saveTarget
|
||||||
*e = state
|
*e = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+138
@@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -29,6 +30,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
basefile "github.com/xiaoqidun/ofdgo/internal/base/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorProgress 编辑与保存阶段的进度回调,nil表示不回报进度
|
// editorProgress 编辑与保存阶段的进度回调,nil表示不回报进度
|
||||||
@@ -54,6 +57,141 @@ type ofdXML struct {
|
|||||||
// ofdAttrs OFD节点属性
|
// ofdAttrs OFD节点属性
|
||||||
type ofdAttrs []xml.Attr
|
type ofdAttrs []xml.Attr
|
||||||
|
|
||||||
|
// ErrSaveTarget 表示编辑器尚未绑定保存目标
|
||||||
|
var ErrSaveTarget = errors.New("save target is not set")
|
||||||
|
|
||||||
|
// ErrSaveConflict 表示保存目标在打开或最近保存后被其他操作修改
|
||||||
|
var ErrSaveConflict = basefile.ErrChanged
|
||||||
|
|
||||||
|
// ErrSaveBusy 表示目标文件正在提交其他写入事务
|
||||||
|
var ErrSaveBusy = basefile.ErrBusy
|
||||||
|
|
||||||
|
// Snapshot 创建独立编辑快照,完成后可与原编辑器并行使用
|
||||||
|
// 创建过程需与原实例串行,保留修订及保存目标,不复制撤销记录和进度回调
|
||||||
|
// 原包压缩数据直接复制,快照不依赖原阅读器及文件句柄,内存随快照释放
|
||||||
|
// 自定义后端及字体文件系统仍由调用方提供,需支持并行读取
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Editor 独立快照, error 准备或取消错误
|
||||||
|
func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
|
||||||
|
reader, err := e.readerContext(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
next := &Reader{files: make(map[string][]byte, len(reader.files)), encryption: reader.encryption, documentIndex: reader.documentIndex, selectedVersion: cloneEditorData(reader.selectedVersion)}
|
||||||
|
if reader.encryption != nil {
|
||||||
|
next.encryption = &encryptionState{info: cloneEditorData(reader.encryption.info), options: cloneEncryptionOptions(reader.encryption.options)}
|
||||||
|
}
|
||||||
|
if reader.Zip != nil {
|
||||||
|
var data bytes.Buffer
|
||||||
|
var count int64
|
||||||
|
archive := zip.NewWriter(convertWriter{context: ctx, writer: &data, count: &count})
|
||||||
|
for _, file := range reader.Zip.File {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := archive.Copy(file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := archive.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
next.Zip, err = zip.NewReader(bytes.NewReader(data.Bytes()), int64(data.Len()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, data := range reader.files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
next.files[name] = bytes.Clone(data)
|
||||||
|
}
|
||||||
|
if err := next.initRootContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot, err := next.EditorContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot.revision, snapshot.serial, snapshot.saveTarget = e.revision, e.serial, e.saveTarget
|
||||||
|
snapshot.backends, snapshot.fontDirs, snapshot.fontFS = e.backends, slices.Clone(e.fontDirs), slices.Clone(e.fontFS)
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSavePath 绑定保存目标并记录当前文件状态,不读取或修改编辑内容
|
||||||
|
// 绑定已有文件时应在打开后立即调用;空路径解除绑定,调用需与编辑操作串行
|
||||||
|
// 入参: name 目标路径
|
||||||
|
// 返回: error 路径或文件状态错误
|
||||||
|
func (e *Editor) SetSavePath(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
e.saveTarget = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
target, err := basefile.NewTarget(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.saveTarget = target
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePath 返回当前保存目标,未绑定时返回空字符串
|
||||||
|
// 返回: string 绝对文件路径
|
||||||
|
func (e *Editor) SavePath() string {
|
||||||
|
if e.saveTarget == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.saveTarget.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 将当前完整文档写回已绑定目标,不改变编辑状态及撤销记录
|
||||||
|
// 调用需与编辑操作串行,支持取消;失败保留原文件,成功后可继续编辑和保存
|
||||||
|
// 入参: ctx 取消上下文, options 输出配置
|
||||||
|
// 返回: int64 输出字节数, error 保存错误
|
||||||
|
func (e *Editor) Save(ctx context.Context, options WriteOptions) (int64, error) {
|
||||||
|
if e.saveTarget == nil {
|
||||||
|
return 0, ErrSaveTarget
|
||||||
|
}
|
||||||
|
return e.saveFile(ctx, e.saveTarget, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAs 将完整文档另存,成功后切换保存目标,不改变撤销记录
|
||||||
|
// 调用需与编辑操作串行,取消或失败时保持原目标
|
||||||
|
// 入参: ctx 取消上下文, name 目标路径, options 输出配置
|
||||||
|
// 返回: int64 输出字节数, error 保存错误
|
||||||
|
func (e *Editor) SaveAs(ctx context.Context, name string, options WriteOptions) (int64, error) {
|
||||||
|
target, err := basefile.NewTarget(name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if e.saveTarget != nil && target.Name() == e.saveTarget.Name() {
|
||||||
|
target = e.saveTarget
|
||||||
|
}
|
||||||
|
n, err := e.saveFile(ctx, target, options)
|
||||||
|
if err == nil {
|
||||||
|
e.saveTarget = target
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveFile 使用共用文件事务提交文档
|
||||||
|
// 入参: ctx 取消上下文, target 保存目标, options 输出配置
|
||||||
|
// 返回: int64 输出字节数, error 保存错误
|
||||||
|
func (e *Editor) saveFile(ctx context.Context, target *basefile.Target, options WriteOptions) (int64, error) {
|
||||||
|
writer, err := target.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer writer.Abort()
|
||||||
|
n, err := e.WriteToWithOptions(ctx, writer, options)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
return n, writer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
||||||
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
||||||
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
||||||
|
|||||||
Reference in new issue
Block a user