mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
870c29a83f
commit
9fd9ba7a28
20 files changed
+862
-97
No files matched your search
@@ -57,6 +57,7 @@
|
||||
<option value="300" selected>300 dpi</option>
|
||||
<option value="600">600 dpi</option>
|
||||
</select><span id="dpiValue" aria-hidden="true">300</span></label>
|
||||
<label class="toolbar-select"><select id="outputCompression" class="format-select" title="压缩" aria-label="压缩" disabled><option value="0">不变</option><option value="1">无损</option><option value="2">有损</option></select><span id="compressionValue" data-short="原" aria-hidden="true">不变</span></label>
|
||||
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
||||
<button id="exportButton" class="button" type="button" data-short="多" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" aria-controls="exportPanel" disabled>文档</button>
|
||||
</div>
|
||||
@@ -183,6 +184,7 @@
|
||||
<button id="convertAdd" class="button" type="button">添加</button><button id="convertClear" class="button" type="button">清空</button>
|
||||
<div class="convert-options">
|
||||
<div class="form-row"><label for="convertFormat">格式</label><select id="convertFormat" required></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></select></div>
|
||||
<div id="convertDPIRow" class="form-row" hidden><label for="convertDPI">精度</label><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></div>
|
||||
<div class="form-row"><label for="convertDestination">保存</label><select id="convertDestination"><option value="directory">目录</option><option value="archive">打包</option></select></div>
|
||||
</div>
|
||||
|
||||
+19
-12
@@ -723,13 +723,16 @@ button:not(:disabled):hover {
|
||||
.convert-options {
|
||||
flex: none;
|
||||
display: grid;
|
||||
grid-template-columns: calc(2em + 8px + var(--convert-control-width)) 132px calc(2em + 8px + var(--convert-control-width));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.convert-options:has(#convertDPIRow[hidden]) {
|
||||
grid-template-columns: repeat(2, calc(2em + 8px + var(--convert-control-width)));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.convert-options .form-row {
|
||||
@@ -906,7 +909,8 @@ button:not(:disabled):hover {
|
||||
}
|
||||
|
||||
.convert-dialog .form-actions {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
@@ -914,8 +918,14 @@ button:not(:disabled):hover {
|
||||
|
||||
.convert-dialog .form-actions .button {
|
||||
min-width: 3.5em;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
#convertAdd { grid-column: 1; }
|
||||
#convertClear { grid-column: 2; }
|
||||
#convertClose { grid-column: 4; justify-self: end; }
|
||||
#convertCancel, #convertStart { grid-column: 5; justify-self: end; }
|
||||
|
||||
.convert-progress progress {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -951,19 +961,18 @@ button:not(:disabled):hover {
|
||||
}
|
||||
.convert-dialog .form-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
row-gap: 12px;
|
||||
}
|
||||
.convert-dialog .form-actions > .button { grid-row: 2; }
|
||||
.convert-options {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.convert-options:has(#convertDPIRow[hidden]) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.convert-options .form-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
@@ -971,15 +980,10 @@ button:not(:disabled):hover {
|
||||
text-align: left;
|
||||
}
|
||||
.convert-options:has(#convertDPIRow[hidden]) .form-row {
|
||||
grid-template-columns: 2em minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.convert-options label { width: auto; }
|
||||
#convertAdd { grid-column: 1; }
|
||||
#convertClear { grid-column: 2; }
|
||||
#convertClose { grid-column: 3; }
|
||||
#convertCancel,
|
||||
#convertStart { grid-column: 4; }
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
@@ -3089,6 +3093,9 @@ button.outline-link:hover {
|
||||
|
||||
.toolbar-select > span {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
#compressionValue { font-size: 0; }
|
||||
#compressionValue::after { content: attr(data-short); font-size: 12px; }
|
||||
}
|
||||
+27
-13
@@ -20,7 +20,7 @@ const STATUS = {
|
||||
const wasmRequests = new Map();
|
||||
const metaContents = new WeakMap();
|
||||
const batch = { items: [], formats: [], running: false, canceled: false, worker: null, requests: new Map(), sequence: 0, activeID: 0, progressTime: 0 };
|
||||
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
|
||||
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
|
||||
|
||||
let wasmPromise = null;
|
||||
let remoteDownload = null;
|
||||
@@ -403,6 +403,8 @@ const el = {
|
||||
continuousButton: document.querySelector("#continuousButton"),
|
||||
annotationButton: document.querySelector("#annotationButton"),
|
||||
imageDPI: document.querySelector("#imageDPI"),
|
||||
outputCompression: document.querySelector("#outputCompression"),
|
||||
compressionValue: document.querySelector("#compressionValue"),
|
||||
dpiValue: document.querySelector("#dpiValue"),
|
||||
formatValue: document.querySelector("#formatValue"),
|
||||
exportFormat: document.querySelector("#exportFormat"),
|
||||
@@ -1250,6 +1252,10 @@ document.addEventListener("cut", copySelection);
|
||||
el.viewerPanel.addEventListener("paste", pasteEditorContent);
|
||||
document.addEventListener("selectionchange", syncSelection);
|
||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||
el.outputCompression.addEventListener("change", () => {
|
||||
el.compressionValue.textContent = el.outputCompression.selectedOptions[0].textContent;
|
||||
el.compressionValue.dataset.short = ["原", "无", "有"][Number(el.outputCompression.value)];
|
||||
});
|
||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||
el.exportButton.addEventListener("click", () => openExportPanel());
|
||||
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
|
||||
@@ -4640,6 +4646,7 @@ async function signDocument(event) {
|
||||
pages:el.signPages.value.trim(), placement:el.signPlacement.value,
|
||||
x:Number(el.signX.value), y:Number(el.signY.value), width:Number(el.signWidth.value), height:Number(el.signHeight.value),
|
||||
mode:el.signMode.value, lock:el.signLock.checked,
|
||||
compression: { mode: Number(el.outputCompression.value) || 0 },
|
||||
};
|
||||
el.signKey.value = "";
|
||||
if (openSeq !== state.openSeq || state.signCanceled || !el.signPanel.open) return;
|
||||
@@ -4842,7 +4849,8 @@ function renderBatchList() {
|
||||
}
|
||||
|
||||
function updateBatchControls() {
|
||||
for (const name of ["Add", "Clear", "Format", "Destination", "DPI"]) batchElements[name].disabled = batch.running;
|
||||
for (const name of ["Add", "Clear", "Format", "Compression", "Destination", "DPI"]) batchElements[name].disabled = batch.running;
|
||||
batchElements.Compression.disabled ||= (batch.items.length ? batch.items : [null]).every(item => (item?.format || batchElements.Format.value) === "txt");
|
||||
batchElements.Clear.disabled ||= !batch.items.length;
|
||||
batchElements.Start.disabled = batch.running || !batch.items.length || !batch.formats.length;
|
||||
batchElements.Start.hidden = batch.running;
|
||||
@@ -4957,12 +4965,13 @@ async function runBatch() {
|
||||
if (!defaultFormat) return;
|
||||
const pageRange = item => item.pages.trim() === "全部" ? "" : item.pages.trim();
|
||||
const dpi = Number(batchElements.DPI.value);
|
||||
const compression = { mode: Number(batchElements.Compression.value) || 0 };
|
||||
const archive = batchElements.Destination.value === "archive";
|
||||
const backend = state.renderBackend;
|
||||
const fontKey = [fontManager.permission, fontManager.records().map(font => [font.id, font.name, font.enabled, font.checksum])];
|
||||
for (const item of batch.items) {
|
||||
const format = batch.formats.find(format => format.value === (item.format || defaultFormat.value));
|
||||
const key = JSON.stringify([format.value, pageRange(item), batchUsesDPI(item) ? dpi : 0, archive, backend, fontKey]);
|
||||
const key = JSON.stringify([format.value, pageRange(item), batchUsesDPI(item) ? dpi : 0, archive, backend, fontKey, compression]);
|
||||
if (item.settingsKey !== key) {
|
||||
item.status = "pending";
|
||||
item.text = "待转";
|
||||
@@ -5015,7 +5024,7 @@ async function runBatch() {
|
||||
const outputName = () => (format.paged ? unique : `${unique}.${format.extension}`).toLowerCase();
|
||||
for (let suffix = 2; archive && names.has(outputName()); suffix++) unique = `${base} (${suffix})`;
|
||||
names.add(outputName());
|
||||
const options = { format: format.value, pages: pageRange(item), dpi, backend, archive, base: unique, credentials: null,
|
||||
const options = { format: format.value, pages: pageRange(item), dpi, backend, archive, compression, base: unique, credentials: null,
|
||||
fontNames: [...batch.localFonts.keys()] };
|
||||
try {
|
||||
for (;;) {
|
||||
@@ -5023,19 +5032,21 @@ async function runBatch() {
|
||||
const result = await batchCall("ofdgoConvertFile", [item.file, options, fonts, destination], progress => batchProgress(item, position, jobs.length, progress));
|
||||
for (const file of result.files) entries.push(file);
|
||||
item.details = (result.pdf?.Warnings || []).map(warning => `${warning.Page ? `第${warning.Page}页:` : ""}${warning.Message}`);
|
||||
if (result.unchanged) {
|
||||
if (result.unchanged && !result.bytes) {
|
||||
batchStatus(item, "unchanged", "原样");
|
||||
item.details = ["无需转换,源文件未改动"];
|
||||
} else batchStatus(item, archive ? "ready" : "done", archive ? "待存" : "完成");
|
||||
break;
|
||||
} catch (err) {
|
||||
if (batch.canceled || !["credentialsRequired", "invalidCredentials"].includes(err.code)) throw err;
|
||||
if (batch.canceled || !["credentialsRequired", "invalidCredentials", "pdfPassword"].includes(err.code)) throw err;
|
||||
const code = err.code === "pdfPassword" ? options.credentials ? "invalidCredentials" : "credentialsRequired" : err.code;
|
||||
options.credentials?.password?.fill(0);
|
||||
options.credentials?.key?.fill(0);
|
||||
options.credentials?.keyPassword?.fill(0);
|
||||
options.credentials = await requestCredentials(err.code, state.openSeq);
|
||||
options.credentials = await requestCredentials(code, state.openSeq, err.code === "pdfPassword");
|
||||
if (!options.credentials) throw new DOMException("已取消解锁", "AbortError");
|
||||
if (!window.confirm("转换结果将不保留原加密,是否继续?")) throw new DOMException("已取消转换", "AbortError");
|
||||
const preservesEncryption = err.code === "pdfPassword" && format.value === "pdf" && compression.mode !== 0 && !options.pages;
|
||||
if (!preservesEncryption && !window.confirm("转换结果将不保留原加密,是否继续?")) throw new DOMException("已取消转换", "AbortError");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -5051,7 +5062,7 @@ async function runBatch() {
|
||||
batchElements.Progress.value = position + 1;
|
||||
}
|
||||
if (archive && entries.length && !batch.canceled) {
|
||||
const result = await batchCall("ofdgoPackFiles", [entries, zipFile], progress => batchProgress(null, 0, 1, progress));
|
||||
const result = await batchCall("ofdgoPackFiles", [entries, zipFile, compression], progress => batchProgress(null, 0, 1, progress));
|
||||
if (result.blob) downloadBytes(result.blob, "application/zip", "转换结果.zip");
|
||||
packed = true;
|
||||
for (const item of jobs) if (item.status === "ready") batchStatus(item, "done", "完成");
|
||||
@@ -5085,6 +5096,8 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
|
||||
return;
|
||||
}
|
||||
const saving = value === "ofd";
|
||||
const compression = { mode: Number(el.outputCompression.value) || 0 };
|
||||
const outputOptions = compression.mode ? [compression] : [];
|
||||
if (saving && !state.editorInfo && !state.ofdBytes && !encryption) {
|
||||
return;
|
||||
}
|
||||
@@ -5126,12 +5139,12 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
|
||||
delete encryption.recipientFiles;
|
||||
if (openSeq !== state.openSeq) return;
|
||||
}
|
||||
const readingSave = saving && !state.editorInfo && !encryption && indices === null;
|
||||
const readingSave = saving && !state.editorInfo && !encryption && indices === null && compression.mode === 0;
|
||||
const result = readingSave
|
||||
? { label: "OFD", size: state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
||||
: encryption ? await callWASM("ofdgoSaveEncrypted", indices, encryption, file) : saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
|
||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, file)
|
||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
|
||||
: encryption ? await callWASM("ofdgoSaveEncrypted", indices, encryption, ...outputOptions, file) : saving ? await callWASM("ofdgoSaveDocument", indices, ...outputOptions, file) : whole
|
||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, ...outputOptions, file)
|
||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, ...outputOptions, file);
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
@@ -7925,6 +7938,7 @@ function updateControls() {
|
||||
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
||||
updateAnnotationButton();
|
||||
el.exportFormat.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||
el.outputCompression.disabled = !hasDoc || state.exporting;
|
||||
el.exportPageButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||
el.exportButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||
updateDPIControl();
|
||||
|
||||
@@ -120,12 +120,12 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
(action, pageName, done) => boundary(action, pageName).then(() => finish(done), err => finish(done, err)),
|
||||
done => finish(done), fonts, (name, done) => requestFont(id, signal, name, done));
|
||||
} else {
|
||||
const [entries, handle] = args;
|
||||
const [entries, handle, compression] = args;
|
||||
if (handle) output = await handle.createWritable();
|
||||
payload = await globalThis.ofdgoPackFiles(entries.map(entry => ({ name: entry.name, size: entry.file.size })),
|
||||
index => read(entries[index].file), write,
|
||||
(completed, total, done) => { self.postMessage({ id, type: "conversion", phase: "pack", completed, total }); finish(done); },
|
||||
done => finish(done));
|
||||
done => finish(done), compression);
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||
@@ -134,7 +134,7 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
if (output) { await output.close(); output = null; }
|
||||
else result.data.blob = new Blob(chunks, { type: "application/zip" });
|
||||
} else {
|
||||
if (result.data.unchanged) {
|
||||
if (result.data.unchanged && !result.data.bytes) {
|
||||
if (output) { await output.abort(); output = null; }
|
||||
if (root && created) { await root.removeEntry(created, { recursive: true }); created = null; }
|
||||
files = [];
|
||||
@@ -200,6 +200,7 @@ async function handleMessage({ id, name, args }) {
|
||||
if (converting) args.push((name, done) => requestFont(id, signal, name, done));
|
||||
} else {
|
||||
const file = args.pop();
|
||||
const compression = saving && name !== "ofdgoSaveSigned" && args.at(-1)?.mode !== undefined ? args.pop() : null;
|
||||
const indices = saving ? args.shift() : null;
|
||||
if (file) output = await file.createWritable();
|
||||
signal.throwIfAborted();
|
||||
@@ -226,8 +227,10 @@ async function handleMessage({ id, name, args }) {
|
||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||
finish(done);
|
||||
});
|
||||
if (indices != null) args.push(indices);
|
||||
if (compression) args.push(indices, compression, done => finish(done));
|
||||
else if (indices != null) args.push(indices);
|
||||
}
|
||||
if (!saving && args.some(value => value?.mode !== undefined)) args.push(done => finish(done));
|
||||
}
|
||||
}
|
||||
const payload = await globalThis[name](...args);
|
||||
|
||||
Reference in new issue
Block a user