mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
30d9699385
commit
4d0a909de5
14 files changed
+438
-50
No files matched your search
@@ -140,6 +140,7 @@
|
|||||||
<option value="contain">适应</option>
|
<option value="contain">适应</option>
|
||||||
<option value="cover">填充</option>
|
<option value="cover">填充</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="saveImageButton" class="button" type="button" disabled>另存</button>
|
||||||
<button id="cropImageButton" class="button" type="button" aria-label="裁剪图片" aria-pressed="false" disabled>裁剪</button>
|
<button id="cropImageButton" class="button" type="button" aria-label="裁剪图片" aria-pressed="false" disabled>裁剪</button>
|
||||||
<button id="resetCropButton" class="button" type="button" aria-label="还原图片" disabled>还原</button>
|
<button id="resetCropButton" class="button" type="button" aria-label="还原图片" disabled>还原</button>
|
||||||
<span>图片</span>
|
<span>图片</span>
|
||||||
|
|||||||
+27
-10
@@ -86,6 +86,7 @@ a:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar {
|
.editor-toolbar {
|
||||||
|
--editor-control-width: 58px;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: 40dvh;
|
max-height: 40dvh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -103,8 +104,8 @@ a:focus-visible {
|
|||||||
display: grid;
|
display: grid;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
grid-template-columns: repeat(3, max-content);
|
grid-template-columns: repeat(3, var(--editor-control-width));
|
||||||
grid-template-rows: repeat(2, 28px) 16px;
|
grid-template-rows: repeat(2, var(--control-height)) 16px;
|
||||||
column-gap: 4px;
|
column-gap: 4px;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@@ -112,17 +113,29 @@ a:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-image {
|
.editor-image {
|
||||||
grid-template-columns: repeat(2, max-content);
|
grid-template-columns: repeat(2, var(--editor-control-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-actions,
|
.editor-actions,
|
||||||
.editor-transform,
|
.editor-transform,
|
||||||
.editor-erase {
|
.editor-erase {
|
||||||
grid-template-columns: max-content;
|
grid-template-columns: var(--editor-control-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-image > select {
|
.editor-group > .button,
|
||||||
grid-column: 1 / -1;
|
.editor-group > select {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-group > select {
|
||||||
|
height: var(--control-height);
|
||||||
|
padding: 0 4px;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #1f2428;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-paragraph > .editor-field,
|
.editor-paragraph > .editor-field,
|
||||||
@@ -131,11 +144,11 @@ a:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-objects {
|
.editor-objects {
|
||||||
grid-template-columns: repeat(4, max-content);
|
grid-template-columns: repeat(4, var(--editor-control-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-group:has(#penTool) {
|
.editor-group:has(#penTool) {
|
||||||
grid-template-columns: repeat(4, max-content);
|
grid-template-columns: repeat(4, var(--editor-control-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-picker-list {
|
.object-picker-list {
|
||||||
@@ -208,7 +221,7 @@ a:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-paragraph {
|
.editor-paragraph {
|
||||||
grid-template-columns: 54px 120px;
|
grid-template-columns: var(--editor-control-width) 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-group:has(#shapeFill) {
|
.editor-group:has(#shapeFill) {
|
||||||
@@ -2917,6 +2930,10 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 901px) and (max-width: 1600px) and (pointer: fine) {
|
@media (min-width: 901px) and (max-width: 1600px) and (pointer: fine) {
|
||||||
|
.editor-toolbar {
|
||||||
|
--editor-control-width: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-group {
|
.editor-group {
|
||||||
padding-inline: 3px;
|
padding-inline: 3px;
|
||||||
column-gap: 3px;
|
column-gap: 3px;
|
||||||
@@ -2931,7 +2948,7 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-paragraph {
|
.editor-paragraph {
|
||||||
grid-template-columns: 54px 108px;
|
grid-template-columns: var(--editor-control-width) 108px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|||||||
+46
-14
@@ -204,6 +204,7 @@ const el = {
|
|||||||
objectFlip: document.querySelector("#objectFlip"),
|
objectFlip: document.querySelector("#objectFlip"),
|
||||||
imageFit: document.querySelector("#imageFit"),
|
imageFit: document.querySelector("#imageFit"),
|
||||||
cropImageButton: document.querySelector("#cropImageButton"),
|
cropImageButton: document.querySelector("#cropImageButton"),
|
||||||
|
saveImageButton: document.querySelector("#saveImageButton"),
|
||||||
resetCropButton: document.querySelector("#resetCropButton"),
|
resetCropButton: document.querySelector("#resetCropButton"),
|
||||||
multiSelectButton: document.querySelector("#multiSelectButton"),
|
multiSelectButton: document.querySelector("#multiSelectButton"),
|
||||||
textAlign: document.querySelector("#textAlign"),
|
textAlign: document.querySelector("#textAlign"),
|
||||||
@@ -1523,7 +1524,7 @@ function openObjectStyle() {
|
|||||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||||
el.objectLinkFields.hidden = items.length !== 1 || items[0].scoped;
|
el.objectLinkFields.hidden = items.length !== 1 || items[0].scoped;
|
||||||
const link = items[0].link ? JSON.parse(items[0].link) : null;
|
const link = items[0].link ? JSON.parse(items[0].link) : null;
|
||||||
const targetPage = link?.Goto?.Dest ? state.doc.pages.findIndex(page => page.id === link.Goto.Dest.PageID) : -1;
|
const targetPage = link?.Goto?.Dest ? pageIndexByID(state.doc.pages, link.Goto.Dest.PageID) : -1;
|
||||||
el.objectLinkKind.value = items[0].linkCount > 1 ? "keep" : link?.URI ? "uri" : targetPage >= 0 ? "page" : link ? "keep" : "none";
|
el.objectLinkKind.value = items[0].linkCount > 1 ? "keep" : link?.URI ? "uri" : targetPage >= 0 ? "page" : link ? "keep" : "none";
|
||||||
el.objectLinkKind.disabled = items[0].linkCount > 1;
|
el.objectLinkKind.disabled = items[0].linkCount > 1;
|
||||||
el.objectLinkAddress.value = link?.URI?.URI || "";
|
el.objectLinkAddress.value = link?.URI?.URI || "";
|
||||||
@@ -2769,7 +2770,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const clipboard = state.objectClipboard;
|
const clipboard = state.objectClipboard;
|
||||||
if (reindexed && clipboard?.scope?.startsWith(reindexed.scope + "/") && clipboard.page === reindexed.page) state.objectClipboard = null;
|
if (reindexed && clipboard?.scope?.startsWith(reindexed.scope + "/") && clipboard.page === reindexed.page) state.objectClipboard = null;
|
||||||
const location = name === "ofdgoUndo" ? view?.before : view?.after;
|
const location = name === "ofdgoUndo" ? view?.before : view?.after;
|
||||||
const restoredIndex = location ? doc.pages.findIndex(page => page.id === location.page) : -1;
|
const restoredIndex = location ? pageIndexByID(doc.pages, location.page) : -1;
|
||||||
if (restoredIndex >= 0) {
|
if (restoredIndex >= 0) {
|
||||||
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
||||||
} else if (!item && !inserting && !editingAnnotation && !(name === "ofdgoPasteObjects" && args[4])) {
|
} else if (!item && !inserting && !editingAnnotation && !(name === "ofdgoPasteObjects" && args[4])) {
|
||||||
@@ -2810,7 +2811,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (item || name === "ofdgoPasteObjects" || inserting || editingAnnotation) {
|
if (item || name === "ofdgoPasteObjects" || inserting || editingAnnotation) {
|
||||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
||||||
} else {
|
} else {
|
||||||
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
const samePage = pageIndexByID(doc.pages, previous.id);
|
||||||
const pageIndex = restoredIndex >= 0 ? restoredIndex : doc.pageIndex ?? (samePage < 0 ? Math.min(state.pageIndex, doc.pageCount - 1) : samePage);
|
const pageIndex = restoredIndex >= 0 ? restoredIndex : doc.pageIndex ?? (samePage < 0 ? Math.min(state.pageIndex, doc.pageCount - 1) : samePage);
|
||||||
const page = doc.pages[pageIndex];
|
const page = doc.pages[pageIndex];
|
||||||
const keepScroll = doc.pageIndex === undefined && page.id === previous.id && pageIndex === state.pageIndex
|
const keepScroll = doc.pageIndex === undefined && page.id === previous.id && pageIndex === state.pageIndex
|
||||||
@@ -4120,7 +4121,7 @@ async function openDocument(options = {}) {
|
|||||||
if (preview) state.pageCache.set(page.index, { ...preview, previewOnly: true });
|
if (preview) state.pageCache.set(page.index, { ...preview, previewOnly: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const indices = new Set([pageIndex, ...doc.pages.filter((page) => previewIDs.includes(page.id)).map((page) => page.index)]);
|
const indices = new Set([pageIndex, ...previewIDs.map(id => pageIndexByID(doc.pages, id)).filter(index => index >= 0)]);
|
||||||
for (const index of indices) {
|
for (const index of indices) {
|
||||||
await loadPageData(index, { openSeq, priority: 0, refresh: true });
|
await loadPageData(index, { openSeq, priority: 0, refresh: true });
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
@@ -4282,28 +4283,46 @@ async function renderPage(index, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAttachment(attachment) {
|
function downloadAttachment(attachment) {
|
||||||
|
return downloadResource(attachment, "ofdgoExportAttachment", "附件");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDocumentImage(resource) {
|
||||||
|
if (!resource || !state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
const seq = state.openSeq;
|
||||||
|
try {
|
||||||
|
const image = await callWASM("ofdgoImages", resource);
|
||||||
|
if (seq !== state.openSeq) return;
|
||||||
|
await downloadResource({id: image.id, fileName: image.name}, "ofdgoExportImages", "图片");
|
||||||
|
} catch (err) {
|
||||||
|
if (seq === state.openSeq) showError(err, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
el.saveImageButton.addEventListener("click", () => saveDocumentImage(canvasEditor.selected?.imageResource));
|
||||||
|
|
||||||
|
async function downloadResource(attachment, api, label) {
|
||||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.doc.encryption?.encrypted && !window.confirm("附件将按明文下载,是否继续?")) return;
|
if (state.doc.encryption?.encrypted && !window.confirm(`${label}将按明文下载,是否继续?`)) return;
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
state.exporting = true;
|
state.exporting = true;
|
||||||
updateControls();
|
updateControls();
|
||||||
setBusy(true, "正在读取附件", null, "正在读取附件");
|
setBusy(true, `正在读取${label}`, null, `正在读取${label}`);
|
||||||
try {
|
try {
|
||||||
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({ suggestedName: attachment.fileName }) : null;
|
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({ suggestedName: attachment.fileName }) : null;
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await callWASM("ofdgoExportAttachment", attachment.id, file);
|
const result = await callWASM(api, attachment.id, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.blob) {
|
if (result.blob) {
|
||||||
downloadBytes(result.blob, result.mime, attachment.fileName);
|
downloadBytes(result.blob, result.mime, attachment.fileName);
|
||||||
}
|
}
|
||||||
setStatus(`附件已下载(${formatBytes(result.size)})`);
|
setStatus(`${label}已下载(${formatBytes(result.size)})`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
if (err.name === "AbortError") {
|
if (err.name === "AbortError") {
|
||||||
@@ -5531,7 +5550,7 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
|
|||||||
|
|
||||||
function pageLinkTarget(link) {
|
function pageLinkTarget(link) {
|
||||||
if (link.dest) {
|
if (link.dest) {
|
||||||
const target = state.doc.pages.findIndex(page => page.id === link.dest.pageID);
|
const target = pageIndexByID(state.doc.pages, link.dest.pageID);
|
||||||
return { href: target < 0 ? "#" : `#page-${target + 1}`, title: target < 0 ? "文档链接" : `第${target + 1}页`, activate: () => navigateDestination(link.dest) };
|
return { href: target < 0 ? "#" : `#page-${target + 1}`, title: target < 0 ? "文档链接" : `第${target + 1}页`, activate: () => navigateDestination(link.dest) };
|
||||||
}
|
}
|
||||||
if (link.attachment) {
|
if (link.attachment) {
|
||||||
@@ -5621,7 +5640,7 @@ function sourcePoint(page, x, y) {
|
|||||||
|
|
||||||
async function navigateDestination(dest) {
|
async function navigateDestination(dest) {
|
||||||
if (document.body.hasAttribute("aria-busy")) return;
|
if (document.body.hasAttribute("aria-busy")) return;
|
||||||
const index = state.doc.pages.findIndex(page => page.id === dest.pageID);
|
const index = pageIndexByID(state.doc.pages, dest.pageID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
setStatus("目标页面不存在");
|
setStatus("目标页面不存在");
|
||||||
return;
|
return;
|
||||||
@@ -6498,9 +6517,21 @@ function createOutlineList(outlines, path = []) {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pageIndexes = new WeakMap();
|
||||||
|
|
||||||
|
function pageIndexByID(pages, id) {
|
||||||
|
let indexes = pageIndexes.get(pages);
|
||||||
|
if (!indexes) {
|
||||||
|
indexes = new Map(pages.map((page, position) => [page.id, {page, position}]));
|
||||||
|
pageIndexes.set(pages, indexes);
|
||||||
|
}
|
||||||
|
const entry = indexes.get(id);
|
||||||
|
return entry ? entry.page.index ?? entry.position : -1;
|
||||||
|
}
|
||||||
|
|
||||||
function selectedPageIndexes() {
|
function selectedPageIndexes() {
|
||||||
if (!state.pageSelection.size) return state.pageMultiSelect ? [] : [state.pageIndex];
|
if (!state.pageSelection.size) return state.pageMultiSelect ? [] : [state.pageIndex];
|
||||||
return state.doc.pages.filter(page => state.pageSelection.has(page.id)).map(page => page.index);
|
return [...state.pageSelection].map(id => pageIndexByID(state.doc.pages, id)).filter(index => index >= 0).sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSelectedPages(action, direction = 0) {
|
function changeSelectedPages(action, direction = 0) {
|
||||||
@@ -6546,7 +6577,7 @@ function selectThumbnailPage(event, index) {
|
|||||||
const page = state.doc.pages[index], additive = event.ctrlKey || event.metaKey || state.pageMultiSelect;
|
const page = state.doc.pages[index], additive = event.ctrlKey || event.metaKey || state.pageMultiSelect;
|
||||||
if (!additive) state.pageSelection.clear();
|
if (!additive) state.pageSelection.clear();
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
let anchor = state.doc.pages.findIndex(page => page.id === state.pageSelectionAnchor);
|
let anchor = pageIndexByID(state.doc.pages, state.pageSelectionAnchor);
|
||||||
if (anchor < 0) anchor = state.pageIndex;
|
if (anchor < 0) anchor = state.pageIndex;
|
||||||
for (let i = Math.min(anchor, index); i <= Math.max(anchor, index); i++) state.pageSelection.add(state.doc.pages[i].id);
|
for (let i = Math.min(anchor, index); i <= Math.max(anchor, index); i++) state.pageSelection.add(state.doc.pages[i].id);
|
||||||
} else {
|
} else {
|
||||||
@@ -7859,6 +7890,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
||||||
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping || !canEditObject(item, state.composite ? "transform" : "arrange");
|
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping || !canEditObject(item, state.composite ? "transform" : "arrange");
|
||||||
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "cropImage");
|
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "cropImage");
|
||||||
|
el.saveImageButton.disabled = disabled || !item.imageResource;
|
||||||
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped && !canEditObject(item, "fitImage"));
|
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped && !canEditObject(item, "fitImage"));
|
||||||
el.cropImageButton.textContent = cropping ? "完成" : "裁剪";
|
el.cropImageButton.textContent = cropping ? "完成" : "裁剪";
|
||||||
el.cropImageButton.setAttribute("aria-label", cropping ? "完成裁剪" : "裁剪图片");
|
el.cropImageButton.setAttribute("aria-label", cropping ? "完成裁剪" : "裁剪图片");
|
||||||
@@ -7958,7 +7990,7 @@ async function callWASM(name, ...args) {
|
|||||||
request.fontData = new Map();
|
request.fontData = new Map();
|
||||||
args.push([...request.localFonts.keys()]);
|
args.push([...request.localFonts.keys()]);
|
||||||
}
|
}
|
||||||
if (name === "ofdgoConvertPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
|
if (name === "ofdgoExportImages" || name === "ofdgoConvertPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
|
||||||
state.exportRequestID = id;
|
state.exportRequestID = id;
|
||||||
el.cancelExportButton.hidden = false;
|
el.cancelExportButton.hidden = false;
|
||||||
el.cancelExportButton.disabled = false;
|
el.cancelExportButton.disabled = false;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ self.onmessage = ({ data }) => {
|
|||||||
operations.get(data.id)?.abort();
|
operations.get(data.id)?.abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" || data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveEncrypted" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
|
if (data.name === "ofdgoExportImages" || data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" || data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveEncrypted" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
|
||||||
operations.set(data.id, new AbortController());
|
operations.set(data.id, new AbortController());
|
||||||
}
|
}
|
||||||
pending = pending.then(() => data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" ? handleBatchMessage(data) : handleMessage(data));
|
pending = pending.then(() => data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" ? handleBatchMessage(data) : handleMessage(data));
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ async function isBundleComplete(bundle) {
|
|||||||
}
|
}
|
||||||
const cache = await caches.open(bundle.name);
|
const cache = await caches.open(bundle.name);
|
||||||
const keys = await cache.keys();
|
const keys = await cache.keys();
|
||||||
return bundle.assets.every((url) => keys.some((key) => key.url === url));
|
const urls = new Set(keys.map(key => key.url));
|
||||||
|
return bundle.assets.every(url => urls.has(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function currentBundle(meta) {
|
async function currentBundle(meta) {
|
||||||
|
|||||||
@@ -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-20260928121523-d13a51030150
|
github.com/xiaoqidun/pdfgo v0.0.0-20260928145637-87d467fc8ef1
|
||||||
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-20260928121523-d13a51030150 h1:N4JwW+p/KtgiNeNGVEdqswfLvefqZXGeS6tPpYKpIJQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260928145637-87d467fc8ef1 h1:meZvn/eb/AAbr6qfsP41UZZsW/g1v67a2CUYGzoJpS0=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260928121523-d13a51030150/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260928145637-87d467fc8ef1/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=
|
||||||
|
|||||||
+66
-7
@@ -204,6 +204,8 @@ func RunWASM() {
|
|||||||
registerAsyncCallback("ofdgoExportPage", exportPage)
|
registerAsyncCallback("ofdgoExportPage", exportPage)
|
||||||
registerAsyncCallback("ofdgoExportDocument", exportDocument)
|
registerAsyncCallback("ofdgoExportDocument", exportDocument)
|
||||||
registerAsyncCallback("ofdgoExportAttachment", exportAttachment)
|
registerAsyncCallback("ofdgoExportAttachment", exportAttachment)
|
||||||
|
registerCallback("ofdgoImages", documentImages)
|
||||||
|
registerAsyncCallback("ofdgoExportImages", exportImages)
|
||||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||||
registerCallback("ofdgoFontFaces", fontFaces)
|
registerCallback("ofdgoFontFaces", fontFaces)
|
||||||
registerCallback("ofdgoFontFace", fontFace)
|
registerCallback("ofdgoFontFace", fontFace)
|
||||||
@@ -651,6 +653,49 @@ func documentInfo(args []js.Value) (any, error) {
|
|||||||
return currentSession.Details(), nil
|
return currentSession.Details(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// documentImages 读取当前文档的原始图片目录
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 图片目录, error 读取错误
|
||||||
|
func documentImages(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
return currentSession.Reader.Image(context.Background(), args[0].String())
|
||||||
|
}
|
||||||
|
return currentSession.Reader.Images(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportImages 分块保存单幅原始图片或图片归档
|
||||||
|
// 入参: args 图片标识和写出回调,空标识保存全部
|
||||||
|
// 返回: any 文件类型, error 读写错误
|
||||||
|
func exportImages(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
writer := bufio.NewWriterSize(exportWriter{write: args[1]}, 1<<20)
|
||||||
|
mime := "application/octet-stream"
|
||||||
|
if id := args[0].String(); id != "" {
|
||||||
|
input, err := currentSession.Reader.OpenImage(context.Background(), id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
if _, err := io.Copy(writer, input); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mime = "application/zip"
|
||||||
|
if err := currentSession.Reader.WriteImages(context.Background(), writer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return successResult(map[string]any{"mime": mime}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// exportAttachment 分块导出附件
|
// exportAttachment 分块导出附件
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 附件数据, error 错误信息
|
// 返回: any 附件数据, error 错误信息
|
||||||
@@ -736,6 +781,20 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
links := make([]any, len(page.Links))
|
links := make([]any, len(page.Links))
|
||||||
|
attachmentNames := make(map[string]string)
|
||||||
|
for _, link := range page.Links {
|
||||||
|
if link.Attachment == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attachments, err := currentSession.Reader.Attachments()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
attachmentNames[attachment.ID] = path.Base(currentSession.Reader.ResPath(attachment.FileLoc))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
for i, link := range page.Links {
|
for i, link := range page.Links {
|
||||||
item := map[string]any{
|
item := map[string]any{
|
||||||
"uri": link.URI,
|
"uri": link.URI,
|
||||||
@@ -748,13 +807,7 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
if link.Attachment != "" {
|
if link.Attachment != "" {
|
||||||
item["attachment"] = link.Attachment
|
item["attachment"] = link.Attachment
|
||||||
attachments, _ := currentSession.Reader.Attachments()
|
item["fileName"] = attachmentNames[link.Attachment]
|
||||||
for _, attachment := range attachments {
|
|
||||||
if attachment.ID == link.Attachment {
|
|
||||||
item["fileName"] = path.Base(currentSession.Reader.ResPath(attachment.FileLoc))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if dest := link.Dest; dest != nil {
|
if dest := link.Dest; dest != nil {
|
||||||
item["dest"] = map[string]any{"type": dest.Type, "pageID": dest.PageID, "left": dest.Left, "top": dest.Top,
|
item["dest"] = map[string]any{"type": dest.Type, "pageID": dest.PageID, "left": dest.Left, "top": dest.Top,
|
||||||
@@ -1184,6 +1237,9 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
}
|
}
|
||||||
item["imageBounds"] = editorBox(full)
|
item["imageBounds"] = editorBox(full)
|
||||||
}
|
}
|
||||||
|
if object.Type == "ImageObject" {
|
||||||
|
item["imageResource"] = object.ImageObject.ResourceID
|
||||||
|
}
|
||||||
if object.Type == "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
item["bounds"] = editorBox(box)
|
item["bounds"] = editorBox(box)
|
||||||
value, layout := object.TextObject.TextLayout()
|
value, layout := object.TextObject.TextLayout()
|
||||||
@@ -2114,6 +2170,9 @@ func compositeObjects(args []js.Value) (any, error) {
|
|||||||
item := map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": member.Position.Index, "count": member.Position.Count, "container": key + ":" + member.Position.Container, "scoped": true, "contours": member.Contours,
|
item := map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": member.Position.Index, "count": member.Position.Count, "container": key + ":" + member.Position.Container, "scoped": true, "contours": member.Contours,
|
||||||
"capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
|
"capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
|
||||||
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||||
|
if member.Object.Type == "ImageObject" {
|
||||||
|
item["imageResource"] = member.Object.ImageObject.ResourceID
|
||||||
|
}
|
||||||
if kind, geometry := member.Shape(); kind != "" {
|
if kind, geometry := member.Shape(); kind != "" {
|
||||||
item["shape"], item["geometry"] = string(kind), editorBox(geometry)
|
item["shape"], item["geometry"] = string(kind), editorBox(geometry)
|
||||||
} else if kind, box, matrix := member.ShapeFrame(); kind != "" {
|
} else if kind, box, matrix := member.ShapeFrame(); kind != "" {
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
// 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 (
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageInfo 描述文档内的原始图片资源,独立蒙版保留为独立资源
|
||||||
|
// 不包含对象裁剪、旋转、透明度及显示尺寸,不代表页面合成后的外观
|
||||||
|
type ImageInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Location string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageOutput 在分块写出图片时检查调用方取消状态
|
||||||
|
type imageOutput struct {
|
||||||
|
context context.Context
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageInput 在读取原始图片时检查调用方取消状态
|
||||||
|
type imageInput struct {
|
||||||
|
io.ReadCloser
|
||||||
|
context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read 检查取消状态后读取原始数据
|
||||||
|
// 入参: data 数据缓冲区
|
||||||
|
// 返回: int 读取字节数, error 读取或取消错误
|
||||||
|
func (r imageInput) Read(data []byte) (int, error) {
|
||||||
|
if err := r.context.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.ReadCloser.Read(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write 检查取消状态后写出原始数据
|
||||||
|
// 入参: data 数据块
|
||||||
|
// 返回: int 写入字节数, error 写入或取消错误
|
||||||
|
func (w imageOutput) Write(data []byte) (int, error) {
|
||||||
|
if err := w.context.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return w.writer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Images 列出文档、页面和模板声明的图片资源,不解码图片像素
|
||||||
|
// 包含未使用及重复引用的资源声明,同一资源标识只返回一次
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: []ImageInfo 图片资源, error 读取或取消错误
|
||||||
|
func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.imageCatalog != nil {
|
||||||
|
return slices.Clone(r.imageCatalog), nil
|
||||||
|
}
|
||||||
|
doc, err := r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resources := make([]string, 0)
|
||||||
|
for _, name := range append(slices.Clone(doc.CommonData.DocumentRes), doc.CommonData.PublicRes...) {
|
||||||
|
resources = append(resources, r.ResPath(name))
|
||||||
|
}
|
||||||
|
pages := slices.Clone(doc.Pages.Page)
|
||||||
|
for _, template := range doc.CommonData.TemplatePage {
|
||||||
|
pages = append(pages, Page{BaseLoc: template.BaseLoc})
|
||||||
|
}
|
||||||
|
for _, page := range pages {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header, err := r.readPageHeader(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, name := range header.PageRes {
|
||||||
|
resources = append(resources, resolveResourcePath(r.ResPath(page.BaseLoc), "", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
ids := make(map[string]ImageInfo)
|
||||||
|
items := make([]ImageInfo, 0)
|
||||||
|
for _, name := range resources {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if seen[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = true
|
||||||
|
data, err := r.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var res Res
|
||||||
|
if err := xml.Unmarshal(data, &res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, media := range res.MultiMedias.MultiMedia {
|
||||||
|
if media.Type != "Image" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
|
||||||
|
if media.ID == "" || location == "" {
|
||||||
|
return nil, fmt.Errorf("invalid image resource")
|
||||||
|
}
|
||||||
|
item := ImageInfo{ID: media.ID, Name: path.Base(location), Format: media.Format, Location: location}
|
||||||
|
if previous, ok := ids[item.ID]; ok {
|
||||||
|
if previous != item {
|
||||||
|
return nil, fmt.Errorf("conflicting image resource %q", item.ID)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids[item.ID] = item
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.imageCatalog = items
|
||||||
|
return slices.Clone(items), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image 查找指定图片,已加载资源直接读取所在资源文件,不扫描其他页面
|
||||||
|
// 入参: ctx 取消上下文, id 图片资源标识
|
||||||
|
// 返回: ImageInfo 图片资源, error 读取或取消错误
|
||||||
|
func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
|
if name := r.resourceFiles[id]; name != "" {
|
||||||
|
data, err := r.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
|
var res Res
|
||||||
|
if err := xml.Unmarshal(data, &res); err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
|
for _, media := range res.MultiMedias.MultiMedia {
|
||||||
|
if media.ID == id && media.Type == "Image" && media.MediaFile != "" {
|
||||||
|
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
|
||||||
|
return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ImageInfo{}, fmt.Errorf("image resource %q not found", id)
|
||||||
|
}
|
||||||
|
items, err := r.Images(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == id {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ImageInfo{}, fmt.Errorf("image resource %q not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenImage 打开原始图片资源流,不解码、不重采样或重新压缩
|
||||||
|
// 加密文档返回解密后的资源,调用方负责关闭并完整读取以校验ZIP数据
|
||||||
|
// 入参: ctx 取消上下文, id 图片资源标识
|
||||||
|
// 返回: io.ReadCloser 原始图片流, error 读取错误
|
||||||
|
func (r *Reader) OpenImage(ctx context.Context, id string) (io.ReadCloser, error) {
|
||||||
|
item, err := r.Image(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
input, err := r.openFile(item.Location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return imageInput{ReadCloser: input, context: ctx}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteImages 将原始图片逐个写入ZIP,不改变图片编码,文件序号避免同名覆盖
|
||||||
|
// 入参: ctx 取消上下文, writer 输出流, ids 资源标识,省略则导出全部
|
||||||
|
// 返回: error 读取、写出或取消错误
|
||||||
|
func (r *Reader) WriteImages(ctx context.Context, writer io.Writer, ids ...string) error {
|
||||||
|
var items []ImageInfo
|
||||||
|
if len(ids) == 0 {
|
||||||
|
var err error
|
||||||
|
items, err = r.Images(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, id := range ids {
|
||||||
|
if seen[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item, err := r.Image(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
seen[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
archive := zip.NewWriter(imageOutput{ctx, writer})
|
||||||
|
for i, item := range items {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name := strings.Map(func(c rune) rune {
|
||||||
|
if c < 32 || strings.ContainsRune(`<>:"/\|?*`, c) {
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}, item.Name)
|
||||||
|
header := &zip.FileHeader{Name: fmt.Sprintf("%04d-%s", i+1, name), Method: zip.Store}
|
||||||
|
output, err := archive.CreateHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
input, err := r.openFile(item.Location)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(imageOutput{ctx, output}, input)
|
||||||
|
closeErr := input.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return archive.Close()
|
||||||
|
}
|
||||||
+12
-1
@@ -153,8 +153,19 @@ func objectGeometryPath(geometry GeometryBackend, object PathObject) (GeometryPa
|
|||||||
// 入参: path 填充路径
|
// 入参: path 填充路径
|
||||||
// 返回: GeometryPath 显式闭合路径
|
// 返回: GeometryPath 显式闭合路径
|
||||||
func closedGeometry(path GeometryPath) GeometryPath {
|
func closedGeometry(path GeometryPath) GeometryPath {
|
||||||
|
open, missing := false, false
|
||||||
|
for _, segment := range path {
|
||||||
|
if segment.Verb == GeometryMove && open {
|
||||||
|
missing = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
open = segment.Verb != GeometryClose
|
||||||
|
}
|
||||||
|
if !missing && !open {
|
||||||
|
return path
|
||||||
|
}
|
||||||
result := make(GeometryPath, 0, len(path)+1)
|
result := make(GeometryPath, 0, len(path)+1)
|
||||||
open := false
|
open = false
|
||||||
for _, segment := range path {
|
for _, segment := range path {
|
||||||
if segment.Verb == GeometryMove && open {
|
if segment.Verb == GeometryMove && open {
|
||||||
result = append(result, GeometrySegment{Verb: GeometryClose})
|
result = append(result, GeometrySegment{Verb: GeometryClose})
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type Reader struct {
|
|||||||
ResMap map[string]string
|
ResMap map[string]string
|
||||||
resourcesRead map[string]bool
|
resourcesRead map[string]bool
|
||||||
resourceFiles map[string]string
|
resourceFiles map[string]string
|
||||||
|
imageCatalog []ImageInfo
|
||||||
fontCache map[string]*Font
|
fontCache map[string]*Font
|
||||||
fontResourcesRead bool
|
fontResourcesRead bool
|
||||||
fontFaces map[string][]*FontFace
|
fontFaces map[string][]*FontFace
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
// Renderer 渲染器实现
|
// Renderer 渲染器实现
|
||||||
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
||||||
|
// 外部字体缺失且无可用回退时不绘制对应文字,其余内容继续;字体状态可通过FontInfos查询
|
||||||
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
||||||
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
||||||
// TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景
|
// TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景
|
||||||
|
|||||||
@@ -14,11 +14,7 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import "github.com/tdewolff/canvas"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadFont 加载字体
|
// loadFont 加载字体
|
||||||
// 入参: fontID 字体ID
|
// 入参: fontID 字体ID
|
||||||
@@ -34,13 +30,6 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
}
|
}
|
||||||
of := r.Reader.fontCache[fontID]
|
of := r.Reader.fontCache[fontID]
|
||||||
if len(resolved.Data) == 0 {
|
if len(resolved.Data) == 0 {
|
||||||
if !r.textOnly {
|
|
||||||
name := fontID
|
|
||||||
if of != nil && of.FontName != "" {
|
|
||||||
name = of.FontName
|
|
||||||
}
|
|
||||||
r.renderError = fmt.Errorf("font %q is unavailable", name)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if of == nil {
|
if of == nil {
|
||||||
|
|||||||
+24
-1
@@ -883,13 +883,36 @@ func verifySignatureCertificateChain(cert *smx509.Certificate, pool, trusts [][]
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
group.pool.AddCert(ca)
|
addSignatureCertificate(group.pool, ca)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err := cert.Verify(smx509.VerifyOptions{Roots: roots, Intermediates: intermediates, CurrentTime: at, KeyUsages: []smx509.ExtKeyUsage{usage}})
|
_, err := cert.Verify(smx509.VerifyOptions{Roots: roots, Intermediates: intermediates, CurrentTime: at, KeyUsages: []smx509.ExtKeyUsage{usage}})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addSignatureCertificate 添加证书并按RFC5280排除自颁发中间证书的路径长度
|
||||||
|
// 入参: pool 证书池, cert 证书
|
||||||
|
func addSignatureCertificate(pool *smx509.CertPool, cert *smx509.Certificate) {
|
||||||
|
if !cert.BasicConstraintsValid || cert.MaxPathLen < 0 {
|
||||||
|
pool.AddCert(cert)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy := *cert
|
||||||
|
copy.MaxPathLen, copy.MaxPathLenZero = -1, false
|
||||||
|
pool.AddCertWithConstraint(©, func(chain []*smx509.Certificate) error {
|
||||||
|
length := 0
|
||||||
|
for i, intermediate := range chain {
|
||||||
|
if i > 0 && !bytes.Equal(intermediate.RawSubject, intermediate.RawIssuer) {
|
||||||
|
length++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if length > cert.MaxPathLen {
|
||||||
|
return smx509.CertificateInvalidError{Cert: cert, Reason: smx509.TooManyIntermediates}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// verifyCertificateSignature 验证证书签名
|
// verifyCertificateSignature 验证证书签名
|
||||||
// 入参: cert 证书信息, issuerCert 颁发者证书
|
// 入参: cert 证书信息, issuerCert 颁发者证书
|
||||||
// 返回: bool 是否验证通过, error 错误信息
|
// 返回: bool 是否验证通过, error 错误信息
|
||||||
|
|||||||
Reference in new issue
Block a user