mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
54db6afc31
commit
accd0a2973
11 files changed
+358
-51
No files matched your search
@@ -51,13 +51,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tool-group" role="group" aria-label="导出">
|
<div class="tool-group" role="group" aria-label="导出">
|
||||||
<label class="toolbar-select"><select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select><span id="formatValue" aria-hidden="true"></span></label>
|
<label class="toolbar-select"><select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select><span id="formatValue" aria-hidden="true"></span></label>
|
||||||
<label class="toolbar-select"><select id="imageDPI" class="format-select" title="DPI" aria-label="DPI" disabled>
|
<label class="toolbar-select"><select id="imageDPI" class="format-select" title="输出精度" aria-label="输出精度" disabled>
|
||||||
<option value="96">96 dpi</option>
|
<option value="96">96 dpi</option>
|
||||||
<option value="150">150 dpi</option>
|
<option value="150">150 dpi</option>
|
||||||
<option value="300" selected>300 dpi</option>
|
<option value="300" selected>300 dpi</option>
|
||||||
<option value="600">600 dpi</option>
|
<option value="600">600 dpi</option>
|
||||||
</select><span id="dpiValue" aria-hidden="true">300</span></label>
|
</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><option value="3">均衡</option><option value="4">强压</option></select><span id="compressionValue" data-short="默" aria-hidden="true">默认</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><option value="3">均衡</option><option value="4">强压</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="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>
|
<button id="exportButton" class="button" type="button" data-short="多" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" aria-controls="exportPanel" disabled>文档</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+24
-24
@@ -2806,7 +2806,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages";
|
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages";
|
||||||
state.imageImporting = name === "ofdgoImportImages";
|
state.imageImporting = name === "ofdgoImportImages";
|
||||||
if (importing) state.importing = true;
|
if (importing) state.importing = true;
|
||||||
setBusy(true, importing ? "正在导入" : "", null);
|
setBusy(true, importing ? "正在导入页面" : "", null);
|
||||||
try {
|
try {
|
||||||
args = await Promise.all(args);
|
args = await Promise.all(args);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
@@ -3352,11 +3352,11 @@ async function loadWASM() {
|
|||||||
setProgress(data.text, data.percent);
|
setProgress(data.text, data.percent);
|
||||||
} else if (data.type === "conversion") {
|
} else if (data.type === "conversion") {
|
||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) {
|
||||||
if (data.phase === "open") setProgress("正在读取 PDF", null);
|
if (data.phase === "open") setProgress("正在读取文档", null);
|
||||||
else if (data.phase === "pages") setProgress(data.completed ? `正在整理 ${data.completed} 页` : "正在整理页面", null);
|
else if (data.phase === "pages") setProgress(data.completed ? `正在整理页面 · ${data.completed} 页` : "正在整理页面", null);
|
||||||
else if (data.phase === "convert") setProgress(`正在转换 ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null);
|
else if (data.phase === "convert") setProgress(`正在转换页面 · ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null);
|
||||||
else if (data.phase === "write.fonts") setProgress("正在处理字体", null);
|
else if (data.phase === "write.fonts") setProgress("正在处理字体", null);
|
||||||
else setProgress("正在生成 OFD", null);
|
else setProgress("正在生成文档", null);
|
||||||
}
|
}
|
||||||
} else if (data.type === "export") {
|
} else if (data.type === "export") {
|
||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
||||||
@@ -3377,15 +3377,15 @@ async function loadWASM() {
|
|||||||
}
|
}
|
||||||
} else if (data.type === "import") {
|
} else if (data.type === "import") {
|
||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.imageImporting) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.imageImporting) {
|
||||||
const text = data.phase === "commit" ? "正在完成" : `正在导入 ${data.completed} / ${data.total} 张`;
|
const text = data.phase === "commit" ? "正在完成导入" : `正在导入图片 · ${data.completed} / ${data.total} 张`;
|
||||||
el.createStatus.textContent = text;
|
el.createStatus.textContent = text;
|
||||||
setProgress(text, data.total ? data.completed / data.total * 100 : null);
|
setProgress(text, data.total ? data.completed / data.total * 100 : null);
|
||||||
if (data.phase === "commit") el.createCancel.disabled = el.cancelExportButton.disabled = true;
|
if (data.phase === "commit") el.createCancel.disabled = el.cancelExportButton.disabled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.importing && !el.importCancel.disabled) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.importing && !el.importCancel.disabled) {
|
||||||
const label = { ids: "正在检查", pages: "正在导入", resources: "正在读取", commit: "正在完成" }[data.phase];
|
const label = { ids: "正在检查标识", pages: "正在导入页面", resources: "正在读取资源", commit: "正在完成导入" }[data.phase];
|
||||||
el.importStatus.textContent = data.total ? `${label} ${data.completed} / ${data.total} 页` : label;
|
el.importStatus.textContent = data.total ? `${label} · ${data.completed} / ${data.total} 页` : label;
|
||||||
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
||||||
if (data.phase === "commit") el.importCancel.disabled = el.cancelExportButton.disabled = true;
|
if (data.phase === "commit") el.importCancel.disabled = el.cancelExportButton.disabled = true;
|
||||||
}
|
}
|
||||||
@@ -3508,7 +3508,7 @@ async function downloadRemoteDocument(options) {
|
|||||||
if (done) break;
|
if (done) break;
|
||||||
chunks.push(value);
|
chunks.push(value);
|
||||||
received += value.byteLength;
|
received += value.byteLength;
|
||||||
setProgress(`正在下载 ${(received / 1048576).toFixed(1)} MB`, total > 0 && received <= total ? received / total * 20 : null);
|
setProgress(`正在下载文档 · ${(received / 1048576).toFixed(1)} MB`, total > 0 && received <= total ? received / total * 20 : null);
|
||||||
}
|
}
|
||||||
} finally { reader.releaseLock(); }
|
} finally { reader.releaseLock(); }
|
||||||
controller.signal.throwIfAborted();
|
controller.signal.throwIfAborted();
|
||||||
@@ -3562,7 +3562,7 @@ async function openOFD(file, remote = null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (downloaded ? downloaded.pdf : /\.pdf$/i.test(file.name || "")) {
|
if (downloaded ? downloaded.pdf : /\.pdf$/i.test(file.name || "")) {
|
||||||
setProgress("正在转换 PDF", 25);
|
setProgress("正在转换文档", 25);
|
||||||
await ensureWASM();
|
await ensureWASM();
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
let converted;
|
let converted;
|
||||||
@@ -3646,7 +3646,7 @@ async function openSelectedFonts(event) {
|
|||||||
try {
|
try {
|
||||||
const fonts = [];
|
const fonts = [];
|
||||||
for (let i = 0; i < files.length; i += 1) {
|
for (let i = 0; i < files.length; i += 1) {
|
||||||
setProgress(`正在读取字体 ${i + 1} / ${files.length}`, 10 + Math.round(i / files.length * 60));
|
setProgress(`正在读取字体 · ${i + 1} / ${files.length}`, 10 + Math.round(i / files.length * 60));
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
fonts.push(fontManager.record(file.name, new Uint8Array(await file.arrayBuffer()), "upload"));
|
fonts.push(fontManager.record(file.name, new Uint8Array(await file.arrayBuffer()), "upload"));
|
||||||
}
|
}
|
||||||
@@ -3755,7 +3755,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
|||||||
const emptyStatus = available.length === 0 ? "暂无系统字体" : "暂无匹配字体";
|
const emptyStatus = available.length === 0 ? "暂无系统字体" : "暂无匹配字体";
|
||||||
const fonts = [];
|
const fonts = [];
|
||||||
for (let i = 0; i < selected.length; i += 1) {
|
for (let i = 0; i < selected.length; i += 1) {
|
||||||
setProgress(`正在读取字体 ${i + 1} / ${selected.length}`, 20 + Math.round(i / selected.length * 60));
|
setProgress(`正在读取字体 · ${i + 1} / ${selected.length}`, 20 + Math.round(i / selected.length * 60));
|
||||||
const item = selected[i];
|
const item = selected[i];
|
||||||
const data = await fontManager.read(item);
|
const data = await fontManager.read(item);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
@@ -4103,7 +4103,7 @@ async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, import
|
|||||||
canceled.name = "AbortError";
|
canceled.name = "AbortError";
|
||||||
throw canceled;
|
throw canceled;
|
||||||
}
|
}
|
||||||
setProgress(pdf ? "正在读取 PDF" : "正在解析文档", null);
|
setProgress(pdf ? "正在读取文档" : "正在解析文档", null);
|
||||||
el.progressPanel.hidden = false;
|
el.progressPanel.hidden = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4469,7 +4469,7 @@ function renderSecurity() {
|
|||||||
function cancelSigning() {
|
function cancelSigning() {
|
||||||
if (el.signCancel.disabled) return;
|
if (el.signCancel.disabled) return;
|
||||||
state.signCanceled = true;
|
state.signCanceled = true;
|
||||||
el.signStatus.textContent = "正在取消";
|
el.signStatus.textContent = "正在取消操作";
|
||||||
cancelExport();
|
cancelExport();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4681,7 +4681,7 @@ async function verifyDocument(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setBusy(true, "正在验签", null, "正在验签");
|
setBusy(true, "正在验证签名", null, "正在验证签名");
|
||||||
el.verifyForm.inert = true;
|
el.verifyForm.inert = true;
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -4733,7 +4733,7 @@ async function updateExportRange() {
|
|||||||
el.exportRangeRow.hidden = !specified;
|
el.exportRangeRow.hidden = !specified;
|
||||||
el.exportRange.removeAttribute("aria-invalid");
|
el.exportRange.removeAttribute("aria-invalid");
|
||||||
el.exportSubmit.disabled = specified;
|
el.exportSubmit.disabled = specified;
|
||||||
el.exportRangeStatus.textContent = specified && value ? "正在校验" : "";
|
el.exportRangeStatus.textContent = specified && value ? "正在校验页码" : "";
|
||||||
if (!specified || !value) {
|
if (!specified || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4952,7 +4952,7 @@ function startBatchWorker() {
|
|||||||
function cancelBatch() {
|
function cancelBatch() {
|
||||||
batch.canceled = true;
|
batch.canceled = true;
|
||||||
batchElements.Cancel.disabled = true;
|
batchElements.Cancel.disabled = true;
|
||||||
batchElements.Status.textContent = "正在取消";
|
batchElements.Status.textContent = "正在取消操作";
|
||||||
if (batch.activeID) batch.worker.postMessage({ type: "cancel", id: batch.activeID });
|
if (batch.activeID) batch.worker.postMessage({ type: "cancel", id: batch.activeID });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5201,12 +5201,12 @@ function cancelExport() {
|
|||||||
el.cancelExportButton.disabled = true;
|
el.cancelExportButton.disabled = true;
|
||||||
if (state.imageImporting) {
|
if (state.imageImporting) {
|
||||||
el.createCancel.disabled = true;
|
el.createCancel.disabled = true;
|
||||||
el.createStatus.textContent = "正在取消";
|
el.createStatus.textContent = "正在取消操作";
|
||||||
} else if (state.importing) {
|
} else if (state.importing) {
|
||||||
el.importCancel.disabled = true;
|
el.importCancel.disabled = true;
|
||||||
el.importStatus.textContent = "正在取消";
|
el.importStatus.textContent = "正在取消操作";
|
||||||
}
|
}
|
||||||
setProgress("正在取消", null);
|
setProgress("正在取消操作", null);
|
||||||
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5872,7 +5872,7 @@ async function selectDocumentText() {
|
|||||||
const count = state.doc.pageCount;
|
const count = state.doc.pageCount;
|
||||||
try {
|
try {
|
||||||
for (let index = 0; index < count; index += 1) {
|
for (let index = 0; index < count; index += 1) {
|
||||||
setStatus(`正在全选 ${index + 1} / ${count} 页`);
|
setStatus(`正在全选文字 · ${index + 1} / ${count} 页`);
|
||||||
const text = await callWASM("ofdgoPageTextString", index);
|
const text = await callWASM("ofdgoPageTextString", index);
|
||||||
if (!active()) {
|
if (!active()) {
|
||||||
return;
|
return;
|
||||||
@@ -6415,7 +6415,7 @@ async function searchDocument() {
|
|||||||
if (seq !== state.searchSeq || openSeq !== state.openSeq) {
|
if (seq !== state.searchSeq || openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.searchStatus.textContent = `正在搜索 ${page + 1} / ${pageCount} 页`;
|
el.searchStatus.textContent = `正在搜索文字 · ${page + 1} / ${pageCount} 页`;
|
||||||
if (!matches?.length) {
|
if (!matches?.length) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -7158,7 +7158,7 @@ function renderMeta(keepDetails = false) {
|
|||||||
el.metaFonts.textContent = String(doc.fontCount || 0);
|
el.metaFonts.textContent = String(doc.fontCount || 0);
|
||||||
el.pageTotal.textContent = String(doc.pageCount || 0);
|
el.pageTotal.textContent = String(doc.pageCount || 0);
|
||||||
if (keepDetails && doc.detailsPending) return;
|
if (keepDetails && doc.detailsPending) return;
|
||||||
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
||||||
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
|
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
|
||||||
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
||||||
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
||||||
@@ -8228,7 +8228,7 @@ function exportFormatUsesDPI(value) {
|
|||||||
|
|
||||||
function updateDPIControl() {
|
function updateDPIControl() {
|
||||||
el.imageDPI.disabled = state.exporting || !state.doc || document.body.hasAttribute("aria-busy") || (displayMode() !== "raster" && !exportFormatUsesDPI(el.exportFormat.value));
|
el.imageDPI.disabled = state.exporting || !state.doc || document.body.hasAttribute("aria-busy") || (displayMode() !== "raster" && !exportFormatUsesDPI(el.exportFormat.value));
|
||||||
el.imageDPI.title = `DPI:${currentImageDPI()}`;
|
el.imageDPI.title = `输出精度:${currentImageDPI()} DPI`;
|
||||||
el.dpiValue.textContent = String(currentImageDPI());
|
el.dpiValue.textContent = String(currentImageDPI());
|
||||||
const format = exportFormatInfo(el.exportFormat.value);
|
const format = exportFormatInfo(el.exportFormat.value);
|
||||||
el.formatValue.textContent = format?.label || "";
|
el.formatValue.textContent = format?.label || "";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929133432-1988ce19768e
|
github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929133432-1988ce19768e h1:mO1e6Z6RgCrTdPOO7zH1w1oUmSLFRBMijkOCQAlqit4=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771 h1:qddSlHkgZhRMyawpOS3TlW5nu8+IurZFjxS4Kwg8YLA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929133432-1988ce19768e/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type outputOptimization struct {
|
|||||||
protected bool
|
protected bool
|
||||||
completed int
|
completed int
|
||||||
deflater *flate.Writer
|
deflater *flate.Writer
|
||||||
|
optimized map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToWithOptions 按压缩策略写出OFD,签名文档不额外改写资源内容
|
// WriteToWithOptions 按压缩策略写出OFD,签名文档不额外改写资源内容
|
||||||
@@ -275,7 +276,7 @@ func (e *Editor) writeOutputEntry(archive *zip.Writer, header zip.FileHeader, da
|
|||||||
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil {
|
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if lossy, ok := e.output.images[strings.ToLower(cleanPackagePath(header.Name))]; ok {
|
if lossy, ok := e.output.images[strings.ToLower(cleanPackagePath(header.Name))]; ok && !e.output.optimized[strings.ToLower(cleanPackagePath(header.Name))] {
|
||||||
options := e.output.options
|
options := e.output.options
|
||||||
if !lossy {
|
if !lossy {
|
||||||
options.Mode = CompressionLossless
|
options.Mode = CompressionLossless
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"maps"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// compressResourceParts 优化输出图片并合并相同资源文件,保留资源标识及各自属性
|
||||||
|
// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目
|
||||||
|
// 返回: error 读取或取消错误
|
||||||
|
func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, removed map[string]bool) error {
|
||||||
|
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := make(map[string]bool)
|
||||||
|
if reader != nil {
|
||||||
|
for name := range reader.fileIndex {
|
||||||
|
names[name] = true
|
||||||
|
}
|
||||||
|
for name := range reader.files {
|
||||||
|
names[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name := range parts {
|
||||||
|
names[name] = true
|
||||||
|
}
|
||||||
|
read := func(name string) ([]byte, error) {
|
||||||
|
if data, ok := parts[name]; ok {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return reader.readFile(name)
|
||||||
|
}
|
||||||
|
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
|
||||||
|
var resources []string
|
||||||
|
actual := make(map[string]string)
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(names)) {
|
||||||
|
if err := e.output.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := strings.ToLower(cleanPackagePath(name))
|
||||||
|
if removed[key] || strings.HasSuffix(name, "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prior, ok := actual[key]; ok && prior != name {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
actual[key] = name
|
||||||
|
var input io.ReadCloser
|
||||||
|
if data, ok := parts[name]; ok {
|
||||||
|
input = io.NopCloser(bytes.NewReader(data))
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
input, err = reader.openFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource, safe := refs.scan(input, name)
|
||||||
|
input.Close()
|
||||||
|
if !safe {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resource {
|
||||||
|
resources = append(resources, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type resourceFile struct{ key, role string }
|
||||||
|
files := make(map[resourceFile]bool)
|
||||||
|
for _, name := range resources {
|
||||||
|
data, err := read(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, group := range root.children {
|
||||||
|
for _, node := range group.children {
|
||||||
|
field := ""
|
||||||
|
if group.name.Local == "Fonts" && node.name.Local == "Font" {
|
||||||
|
field = "FontFile"
|
||||||
|
}
|
||||||
|
if group.name.Local == "MultiMedias" && node.name.Local == "MultiMedia" {
|
||||||
|
field = "MediaFile"
|
||||||
|
}
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file := node.child(field)
|
||||||
|
if file == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var value string
|
||||||
|
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := editorResourceLocation(name, root.attr("BaseLoc"), value)
|
||||||
|
if field == "MediaFile" && node.attr("Type") != "Image" {
|
||||||
|
refs.files[key] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key != strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if actual[key] != "" && !refs.files[key] {
|
||||||
|
files[resourceFile{key, field}] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aliases := make(map[string]string)
|
||||||
|
locations := maps.Clone(actual)
|
||||||
|
reserved := make(map[string]bool, len(actual))
|
||||||
|
for key := range actual {
|
||||||
|
reserved[key] = true
|
||||||
|
}
|
||||||
|
formats := make(map[string]string)
|
||||||
|
seen := make(map[string]map[[32]byte]string)
|
||||||
|
e.output.optimized = make(map[string]bool)
|
||||||
|
for _, key := range slices.Sorted(maps.Keys(actual)) {
|
||||||
|
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, role := range []string{"FontFile", "MediaFile"} {
|
||||||
|
if !files[resourceFile{key, role}] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := read(actual[key])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if role == "MediaFile" {
|
||||||
|
options := e.output.options
|
||||||
|
if !e.output.images[key] {
|
||||||
|
options.Mode = CompressionLossless
|
||||||
|
}
|
||||||
|
candidate, err := pdfgo.OptimizeImageResource(e.output.ctx, data, options, e.output.sizes[key])
|
||||||
|
if err != nil {
|
||||||
|
if e.output.ctx.Err() != nil {
|
||||||
|
return e.output.ctx.Err()
|
||||||
|
}
|
||||||
|
} else if len(candidate) < len(data) {
|
||||||
|
data = candidate
|
||||||
|
parts[actual[key]] = data
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(data, []byte{255, 216}) {
|
||||||
|
formats[key] = "JPEG"
|
||||||
|
ext := path.Ext(actual[key])
|
||||||
|
if !strings.EqualFold(ext, ".jpg") && !strings.EqualFold(ext, ".jpeg") {
|
||||||
|
stem := strings.TrimSuffix(actual[key], ext)
|
||||||
|
name := stem + ".jpg"
|
||||||
|
for n := 1; reserved[strings.ToLower(name)]; n++ {
|
||||||
|
name = fmt.Sprintf("%s-%d.jpg", stem, n)
|
||||||
|
}
|
||||||
|
reserved[strings.ToLower(name)] = true
|
||||||
|
locations[key] = name
|
||||||
|
parts[name] = data
|
||||||
|
delete(parts, actual[key])
|
||||||
|
removed[key] = true
|
||||||
|
e.output.optimized[strings.ToLower(name)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.output.optimized[key] = true
|
||||||
|
}
|
||||||
|
if seen[role] == nil {
|
||||||
|
seen[role] = make(map[[32]byte]string)
|
||||||
|
}
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
if first, ok := seen[role][hash]; ok {
|
||||||
|
aliases[key] = first
|
||||||
|
removed[key] = true
|
||||||
|
if locations[key] != actual[key] {
|
||||||
|
delete(parts, locations[key])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seen[role][hash] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range resources {
|
||||||
|
data, err := read(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
for _, group := range root.children {
|
||||||
|
for _, node := range group.children {
|
||||||
|
for _, field := range []string{"FontFile", "MediaFile"} {
|
||||||
|
file := node.child(field)
|
||||||
|
if file == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var value string
|
||||||
|
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := editorResourceLocation(name, root.attr("BaseLoc"), value)
|
||||||
|
fragment := data[node.start:node.end]
|
||||||
|
location := locations[key]
|
||||||
|
if first, ok := aliases[key]; ok {
|
||||||
|
location = locations[first]
|
||||||
|
}
|
||||||
|
if location != "" && location != actual[key] {
|
||||||
|
var escaped bytes.Buffer
|
||||||
|
xml.EscapeText(&escaped, []byte("/"+location))
|
||||||
|
patch := editorXMLContent(data, file, escaped.Bytes())
|
||||||
|
patch.start -= node.start
|
||||||
|
patch.end -= node.start
|
||||||
|
fragment = editorPatchXML(fragment, []editorXMLPatch{patch})
|
||||||
|
}
|
||||||
|
if field == "MediaFile" && formats[key] != "" && node.attr("Format") != formats[key] {
|
||||||
|
parsed, err := parseEditorXML(fragment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fragment, err = editorXMLAttribute(fragment, parsed, "Format", formats[key])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !bytes.Equal(fragment, data[node.start:node.end]) {
|
||||||
|
patches = append(patches, editorXMLPatch{start: node.start, end: node.end, data: fragment})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(patches) > 0 {
|
||||||
|
parts[name] = editorPatchXML(data, patches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.output.ctx.Err()
|
||||||
|
}
|
||||||
@@ -450,6 +450,12 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
|
if removed == nil {
|
||||||
|
removed = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if err := e.compressResourceParts(parts, reader, removed); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected {
|
if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected {
|
||||||
name := "OFD.xml"
|
name := "OFD.xml"
|
||||||
if file, ok := reader.packageFile(name); ok {
|
if file, ok := reader.packageFile(name); ok {
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ type editorResourceRefs struct {
|
|||||||
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调
|
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调
|
||||||
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
||||||
func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) {
|
func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) {
|
||||||
if len(parts) == 0 && e.revision == 0 {
|
if e.output != nil && e.output.protected {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
|
|||||||
@@ -89,13 +89,16 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
|||||||
resource.Data = buffer.Bytes()
|
resource.Data = buffer.Bytes()
|
||||||
}
|
}
|
||||||
if s.renderer != nil && s.renderer.Compression.Mode != CompressionUnchanged {
|
if s.renderer != nil && s.renderer.Compression.Mode != CompressionUnchanged {
|
||||||
data, err := pdfgo.OptimizeImageSize(s.renderer.outputContext(), resource.Data, s.renderer.Compression, target)
|
data, err := pdfgo.OptimizeImageResource(s.renderer.outputContext(), resource.Data, s.renderer.Compression, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s.err = s.renderer.outputContext().Err(); s.err != nil {
|
if s.err = s.renderer.outputContext().Err(); s.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resource.Data = data
|
resource.Data = data
|
||||||
|
if bytes.HasPrefix(data, []byte{255, 216}) {
|
||||||
|
resource.MIME = "image/jpeg"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.imageNames != nil {
|
if s.imageNames != nil {
|
||||||
|
|||||||
+23
-19
@@ -28,8 +28,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -256,25 +254,31 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
seen := make(map[string]bool)
|
optimizer := &Editor{output: &outputOptimization{ctx: context.Background(), options: options.Compression, sizes: sizes, images: make(map[string]bool)}}
|
||||||
for i, img := range images {
|
optimizer.OnWriteProgress = func(stage string, completed, total int) error {
|
||||||
if err := options.progress.report("compress", i, len(images)); err != nil {
|
return options.progress.report(stage, completed, total)
|
||||||
return nil, err
|
}
|
||||||
}
|
for _, img := range images {
|
||||||
if seen[img.Location] {
|
key := strings.ToLower(cleanPackagePath(img.Location))
|
||||||
continue
|
optimizer.output.images[key] = !masks[key]
|
||||||
}
|
}
|
||||||
seen[img.Location] = true
|
removed := make(map[string]bool)
|
||||||
if data, ok := parts[img.Location]; ok {
|
if err := optimizer.compressResourceParts(parts, nil, removed); err != nil {
|
||||||
compression := options.Compression
|
return nil, err
|
||||||
if masks[strings.ToLower(cleanPackagePath(img.Location))] {
|
}
|
||||||
compression.Mode = CompressionLossless
|
for name := range parts {
|
||||||
}
|
if removed[strings.ToLower(cleanPackagePath(name))] {
|
||||||
if optimized, err := pdfgo.OptimizeImageSize(context.Background(), data, compression, sizes[strings.ToLower(cleanPackagePath(img.Location))]); err == nil {
|
delete(parts, name)
|
||||||
parts[img.Location] = optimized
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r = &Reader{files: parts}
|
||||||
|
if err := r.initRoot(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
doc, err = r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.Time.IsZero() {
|
if options.Time.IsZero() {
|
||||||
options.Time = time.Now()
|
options.Time = time.Now()
|
||||||
|
|||||||
@@ -169,6 +169,33 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
|||||||
}
|
}
|
||||||
output := &ofdCountingWriter{writer: writer}
|
output := &ofdCountingWriter{writer: writer}
|
||||||
archive := zip.NewWriter(output)
|
archive := zip.NewWriter(output)
|
||||||
|
if e.output != nil && e.output.options.Mode != CompressionUnchanged {
|
||||||
|
parts := make(map[string][]byte)
|
||||||
|
err := e.writeParts(func(name string, data []byte, _ bool) error {
|
||||||
|
if subset, ok := fonts[name]; ok {
|
||||||
|
data = subset
|
||||||
|
}
|
||||||
|
parts[name] = data
|
||||||
|
return nil
|
||||||
|
}, progress)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
removed := make(map[string]bool)
|
||||||
|
if err := e.compressResourceParts(parts, nil, removed); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(parts)) {
|
||||||
|
if removed[strings.ToLower(cleanPackagePath(name))] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: zip.Deflate}, parts[name]); err != nil {
|
||||||
|
return output.count, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = archive.Close()
|
||||||
|
return output.count, err
|
||||||
|
}
|
||||||
err = e.writeParts(func(name string, data []byte, compressed bool) error {
|
err = e.writeParts(func(name string, data []byte, compressed bool) error {
|
||||||
if subset, ok := fonts[name]; ok {
|
if subset, ok := fonts[name]; ok {
|
||||||
data = subset
|
data = subset
|
||||||
|
|||||||
Reference in new issue
Block a user