mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
31e67b2721
commit
5842522edf
12 files changed
+456
-33
No files matched your search
@@ -438,6 +438,7 @@
|
|||||||
<div class="form-row"><label for="signIntermediates">中间证书</label><input id="signIntermediates" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
|
<div class="form-row"><label for="signIntermediates">中间证书</label><input id="signIntermediates" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
|
||||||
<div class="form-row"><label for="signSeal">电子印章</label><input id="signSeal" type="file" accept=".esl,.ses,.der" aria-label="SES电子印章"></div>
|
<div class="form-row"><label for="signSeal">电子印章</label><input id="signSeal" type="file" accept=".esl,.ses,.der" aria-label="SES电子印章"></div>
|
||||||
<div id="signPlacementFields" hidden>
|
<div id="signPlacementFields" hidden>
|
||||||
|
<div class="form-row"><span>预览</span><div class="seal-position-options"><button id="signPick" class="button" type="button">定位</button><label><input id="signKeepRatio" type="checkbox" checked>等比</label></div></div>
|
||||||
<div class="form-row"><label for="signPlacement">位置</label><select id="signPlacement"><option value="normal">页面</option><option value="seam">骑缝</option></select></div>
|
<div class="form-row"><label for="signPlacement">位置</label><select id="signPlacement"><option value="normal">页面</option><option value="seam">骑缝</option></select></div>
|
||||||
<div class="form-row"><label for="signPages">页码</label><input id="signPages" type="text" autocomplete="off" spellcheck="false"></div>
|
<div class="form-row"><label for="signPages">页码</label><input id="signPages" type="text" autocomplete="off" spellcheck="false"></div>
|
||||||
<div class="form-row"><label for="signX">横坐标</label><div class="form-unit"><input id="signX" type="number" min="0" step="any" value="10" aria-label="横坐标(毫米)"><span>mm</span></div></div>
|
<div class="form-row"><label for="signX">横坐标</label><div class="form-unit"><input id="signX" type="number" min="0" step="any" value="10" aria-label="横坐标(毫米)"><span>mm</span></div></div>
|
||||||
@@ -450,6 +451,11 @@
|
|||||||
<div class="form-actions"><div id="signStatus" class="form-status" role="status" aria-live="polite"></div><button id="signCancel" class="button" type="button">取消</button><button class="button" type="submit" title="签署另存">签署</button></div>
|
<div class="form-actions"><div id="signStatus" class="form-status" role="status" aria-live="polite"></div><button id="signCancel" class="button" type="button">取消</button><button class="button" type="submit" title="签署另存">签署</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<div hidden>
|
||||||
|
<div id="signPositionLayer" class="seal-position-layer" aria-hidden="true">
|
||||||
|
<div id="signPositionStamp" class="seal-position-stamp" hidden><img id="signPositionImage" alt="" draggable="false"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-labelledby="verifyTitle">
|
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-labelledby="verifyTitle">
|
||||||
<form id="verifyForm">
|
<form id="verifyForm">
|
||||||
<h2 id="verifyTitle" class="form-title">验证签名</h2>
|
<h2 id="verifyTitle" class="form-title">验证签名</h2>
|
||||||
|
|||||||
@@ -1041,6 +1041,43 @@ button:not(:disabled):hover {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seal-position-options,
|
||||||
|
.seal-position-options label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-picking .page-surface {
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-picking .edit-layer {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-position-layer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-position-stamp {
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
outline: 1px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-position-stamp img {
|
||||||
|
position: absolute;
|
||||||
|
max-width: none;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.security-dialog {
|
.security-dialog {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-7
@@ -1129,8 +1129,15 @@ el.signCancel.addEventListener("click", () => state.signing ? cancelSigning() :
|
|||||||
el.signPanel.addEventListener("cancel", event => {
|
el.signPanel.addEventListener("cancel", event => {
|
||||||
if (state.signing) { event.preventDefault(); cancelSigning(); }
|
if (state.signing) { event.preventDefault(); cancelSigning(); }
|
||||||
});
|
});
|
||||||
el.signPanel.addEventListener("close", () => { if (!el.signPanel.open) el.signForm.reset(); });
|
el.signPanel.addEventListener("close", () => { if (!el.signPanel.open && !sealPreview) el.signForm.reset(); });
|
||||||
el.signForm.addEventListener("submit", signDocument);
|
el.signForm.addEventListener("submit", signDocument);
|
||||||
|
const sealPosition = Object.fromEntries(["Layer", "Stamp", "Image"].map(name => [name, document.querySelector(`#signPosition${name}`)]));
|
||||||
|
let sealPreview = null;
|
||||||
|
document.querySelector("#signPick").addEventListener("click", openSealPosition);
|
||||||
|
el.viewerPanel.addEventListener("pointerdown", beginSealDrag, true);
|
||||||
|
el.viewerPanel.addEventListener("pointermove", moveSealDrag, true);
|
||||||
|
el.viewerPanel.addEventListener("pointerup", confirmSealPosition, true);
|
||||||
|
el.viewerPanel.addEventListener("pointercancel", () => { if (sealPreview) finishSealPosition(); }, true);
|
||||||
el.verifyButton.addEventListener("click", () => {
|
el.verifyButton.addEventListener("click", () => {
|
||||||
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||||
el.verifyForm.reset();
|
el.verifyForm.reset();
|
||||||
@@ -1343,6 +1350,10 @@ updateSidebarState();
|
|||||||
boot();
|
boot();
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
|
if (sealPreview) {
|
||||||
|
if (event.key === "Escape") { event.preventDefault(); finishSealPosition(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.defaultPrevented || event.isComposing || event.altKey) {
|
if (event.defaultPrevented || event.isComposing || event.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1457,7 +1468,7 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formDialogOpen() {
|
function formDialogOpen() {
|
||||||
return batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3831,6 +3842,7 @@ function updateFontPermissionHint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearSecurityForms() {
|
function clearSecurityForms() {
|
||||||
|
if (sealPreview) finishSealPosition(false);
|
||||||
finishCredentials(null);
|
finishCredentials(null);
|
||||||
clearEncryptionForm();
|
clearEncryptionForm();
|
||||||
el.signKey.value = "";
|
el.signKey.value = "";
|
||||||
@@ -4248,11 +4260,134 @@ function cancelSigning() {
|
|||||||
cancelExport();
|
cancelExport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finishSealPosition(returnToForm = true) {
|
||||||
|
const preview = sealPreview;
|
||||||
|
if (!preview) return;
|
||||||
|
sealPreview = null;
|
||||||
|
if (preview.pointer != null && el.viewerPanel.hasPointerCapture(preview.pointer)) el.viewerPanel.releasePointerCapture(preview.pointer);
|
||||||
|
URL.revokeObjectURL(preview.url);
|
||||||
|
sealPosition.Layer.remove();
|
||||||
|
sealPosition.Image.removeAttribute("src");
|
||||||
|
el.viewerPanel.classList.remove("seal-picking");
|
||||||
|
canvasEditor.enabled = preview.enabled;
|
||||||
|
if (preview.sidebar && COMPACT_LAYOUT.matches) {
|
||||||
|
Object.assign(state, preview.sidebar);
|
||||||
|
updateSidebarState();
|
||||||
|
}
|
||||||
|
setBusy(document.body.hasAttribute("aria-busy"));
|
||||||
|
if (returnToForm && preview.sequence === state.openSeq && !el.signPanel.open) el.signPanel.showModal();
|
||||||
|
if (preview.sequence === state.openSeq) setStatus(preview.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSealPosition() {
|
||||||
|
const file = el.signSeal.files?.[0];
|
||||||
|
if (!file || !state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
const sequence = state.openSeq;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const range = el.signPages.value.trim(), mode = el.signPlacement.value;
|
||||||
|
const selected = await callWASM("ofdgoParsePageRange", range);
|
||||||
|
if (mode === "seam" && selected.length < 2) throw new Error("骑缝签章至少选择两页");
|
||||||
|
const result = await callWASM("ofdgoPreviewSignatureSeal", new Uint8Array(await file.arrayBuffer()), `1-${state.doc.pageCount}`);
|
||||||
|
if (sequence !== state.openSeq || !el.signPanel.open || file !== el.signSeal.files?.[0]) return;
|
||||||
|
sealPreview = {pages:result.pages, selected, range, mode, ratio:document.querySelector("#signKeepRatio").checked ? result.width / result.height : 0, sequence, enabled:canvasEditor.enabled, status:el.statusText.textContent, url:URL.createObjectURL(new Blob([result.svg], {type:"image/svg+xml"}))};
|
||||||
|
sealPosition.Image.src = sealPreview.url;
|
||||||
|
await sealPosition.Image.decode();
|
||||||
|
if (sequence !== state.openSeq || !el.signPanel.open) { finishSealPosition(false); return; }
|
||||||
|
canvasEditor.enabled = false;
|
||||||
|
if (COMPACT_LAYOUT.matches) {
|
||||||
|
sealPreview.sidebar = {showPages:state.showPages, showMeta:state.showMeta};
|
||||||
|
state.showPages = state.showMeta = false;
|
||||||
|
updateSidebarState();
|
||||||
|
}
|
||||||
|
el.viewerPanel.classList.add("seal-picking");
|
||||||
|
el.signPanel.close();
|
||||||
|
el.signStatus.textContent = "";
|
||||||
|
setStatus("拖动选取签章区域,Esc 取消");
|
||||||
|
el.viewerPanel.focus({preventScroll:true});
|
||||||
|
} catch (error) { finishSealPosition(); el.signStatus.textContent = error.message; }
|
||||||
|
finally { if (sequence === state.openSeq) setBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sealPointer(event) {
|
||||||
|
const preview = sealPreview;
|
||||||
|
const point = pagePoint(event.clientX, event.clientY, preview.surface.getBoundingClientRect(), preview.page, state.rotation);
|
||||||
|
return {x:preview.page.x + point.x, y:preview.page.y + point.y};
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginSealDrag(event) {
|
||||||
|
const preview = sealPreview;
|
||||||
|
if (!preview || preview.confirming || event.button !== 0) return;
|
||||||
|
const surface = event.target.closest(".page-surface");
|
||||||
|
if (!surface) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
const index = Number(surface.closest(".page-shell").dataset.pageIndex);
|
||||||
|
if ((preview.mode === "seam" || preview.selected.length > 1) && !preview.selected.includes(index)) {
|
||||||
|
setStatus("请在所选页码范围内定位");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
preview.page = preview.pages[index];
|
||||||
|
preview.surface = surface;
|
||||||
|
preview.start = sealPointer(event);
|
||||||
|
preview.pointer = event.pointerId;
|
||||||
|
preview.box = null;
|
||||||
|
sealPosition.Stamp.hidden = true;
|
||||||
|
surface.append(sealPosition.Layer);
|
||||||
|
el.viewerPanel.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSealDrag(event) {
|
||||||
|
const preview = sealPreview;
|
||||||
|
if (!preview?.start || preview.pointer !== event.pointerId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
const point = sealPointer(event), start = preview.start, page = preview.page;
|
||||||
|
const dx = point.x - start.x, dy = point.y - start.y;
|
||||||
|
let width = Math.abs(dx), height = Math.abs(dy);
|
||||||
|
const count = preview.mode === "seam" ? preview.selected.length : 1;
|
||||||
|
if (preview.ratio) {
|
||||||
|
width = Math.min(width, height * preview.ratio / count);
|
||||||
|
height = width * count / preview.ratio;
|
||||||
|
}
|
||||||
|
const x = dx < 0 ? start.x - width : start.x, y = dy < 0 ? start.y - height : start.y;
|
||||||
|
preview.box = {x:preview.mode === "seam" ? page.x + page.width - x - width : x, y, width:width * count, height};
|
||||||
|
sealPosition.Stamp.hidden = false;
|
||||||
|
Object.assign(sealPosition.Stamp.style, {left:`${(x-page.x)/page.width*100}%`, top:`${(y-page.y)/page.height*100}%`, width:`${width/page.width*100}%`, height:`${height/page.height*100}%`});
|
||||||
|
Object.assign(sealPosition.Image.style, {width:`${count*100}%`, left:`${preview.mode === "seam" ? -preview.selected.indexOf(page.index)*100 : 0}%`});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmSealPosition(event) {
|
||||||
|
const preview = sealPreview;
|
||||||
|
if (!preview?.start || preview.pointer !== event.pointerId || preview.confirming) return;
|
||||||
|
moveSealDrag(event);
|
||||||
|
preview.start = null;
|
||||||
|
el.viewerPanel.releasePointerCapture(event.pointerId);
|
||||||
|
const box = preview.box;
|
||||||
|
if (!box || box.width < 0.01 || box.height < 0.01) { sealPosition.Layer.remove(); return; }
|
||||||
|
preview.confirming = true;
|
||||||
|
const range = preview.mode === "normal" && preview.selected.length === 1 ? String(preview.page.index + 1) : preview.range;
|
||||||
|
try {
|
||||||
|
await callWASM("ofdgoSignaturePlacement", {...box, pages:range, placement:preview.mode});
|
||||||
|
if (sealPreview !== preview || preview.sequence !== state.openSeq) return;
|
||||||
|
el.signPages.value = range;
|
||||||
|
for (const [name, key] of [["X", "x"], ["Y", "y"], ["Width", "width"], ["Height", "height"]]) el[`sign${name}`].value = String(box[key]);
|
||||||
|
finishSealPosition();
|
||||||
|
} catch (error) {
|
||||||
|
if (sealPreview === preview) { finishSealPosition(); el.signStatus.textContent = error.message; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateSignPlacement() {
|
function updateSignPlacement() {
|
||||||
const stamp = Boolean(el.signSeal.files?.length);
|
const stamp = Boolean(el.signSeal.files?.length);
|
||||||
el.signPlacementFields.hidden = !stamp;
|
el.signPlacementFields.hidden = !stamp;
|
||||||
el.signPages.required = stamp;
|
el.signPlacement.disabled = !stamp;
|
||||||
el.signX.disabled = !stamp || el.signPlacement.value === "seam";
|
for (const input of [el.signPages, el.signX, el.signY, el.signWidth, el.signHeight]) {
|
||||||
|
input.disabled = !stamp;
|
||||||
|
input.required = stamp;
|
||||||
|
}
|
||||||
|
document.querySelector('label[for="signX"]').textContent = el.signPlacement.value === "seam" ? "右侧内缩" : "横坐标";
|
||||||
|
el.signX.setAttribute("aria-label", el.signPlacement.value === "seam" ? "右侧内缩(毫米)" : "横坐标(毫米)");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signDocument(event) {
|
async function signDocument(event) {
|
||||||
@@ -5637,6 +5772,7 @@ async function pasteEditorContent(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copySelection(event) {
|
function copySelection(event) {
|
||||||
|
if (sealPreview) return;
|
||||||
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -7580,7 +7716,7 @@ function updateEditorTools() {
|
|||||||
const moveDisabled = selectionDisabled || selectedPages.some(index => !pageCan("move", index));
|
const moveDisabled = selectionDisabled || selectedPages.some(index => !pageCan("move", index));
|
||||||
el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0;
|
el.movePagePrevButton.disabled = moveDisabled || selectedPages[0] === 0;
|
||||||
el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1;
|
el.movePageNextButton.disabled = moveDisabled || selectedPages.at(-1) === state.doc?.pageCount - 1;
|
||||||
const enabled = state.editing && state.selectObjects && !state.panMode;
|
const enabled = state.editing && state.selectObjects && !state.panMode && !sealPreview;
|
||||||
canvasEditor.setEnabled(enabled);
|
canvasEditor.setEnabled(enabled);
|
||||||
updateObjectControls(canvasEditor.selected);
|
updateObjectControls(canvasEditor.selected);
|
||||||
el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting;
|
el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting;
|
||||||
@@ -7728,7 +7864,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
document.body.toggleAttribute("aria-busy", busy);
|
document.body.toggleAttribute("aria-busy", busy);
|
||||||
renderSecurity();
|
renderSecurity();
|
||||||
updateDisplayControls();
|
updateDisplayControls();
|
||||||
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
|
el.editButton.disabled = busy || Boolean(sealPreview) || !state.doc || !state.ready || state.exporting;
|
||||||
el.createForm.inert = busy;
|
el.createForm.inert = busy;
|
||||||
el.insertForm.inert = busy;
|
el.insertForm.inert = busy;
|
||||||
el.pageForm.inert = busy;
|
el.pageForm.inert = busy;
|
||||||
@@ -7746,7 +7882,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
el.outlineList.inert = busy;
|
el.outlineList.inert = busy;
|
||||||
el.navigationTabs.inert = busy;
|
el.navigationTabs.inert = busy;
|
||||||
el.pageList.inert = busy;
|
el.pageList.inert = busy;
|
||||||
el.editorTools.inert = busy;
|
el.editorTools.inert = busy || Boolean(sealPreview);
|
||||||
el.fontList.inert = busy;
|
el.fontList.inert = busy;
|
||||||
if (!busy) {
|
if (!busy) {
|
||||||
el.progressPanel.hidden = true;
|
el.progressPanel.hidden = true;
|
||||||
|
|||||||
@@ -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-20260927142759-12ed20917f60
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927150509-ae8390858589
|
||||||
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-20260927142759-12ed20917f60 h1:0JC6RK3DMb11RIVoaSSlprHo9oAZb+YyfUThEsTs0Bk=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927150509-ae8390858589 h1:dHH5wnrIyOKRpx3Dw4IiMgMEPMrNFdatfkoMKpX3Khk=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927142759-12ed20917f60/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927150509-ae8390858589/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=
|
||||||
|
|||||||
+102
-21
@@ -286,6 +286,8 @@ func RunWASM() {
|
|||||||
registerAsyncCallback("ofdgoSaveDocument", saveDocument)
|
registerAsyncCallback("ofdgoSaveDocument", saveDocument)
|
||||||
registerAsyncCallback("ofdgoSaveEncrypted", saveEncrypted)
|
registerAsyncCallback("ofdgoSaveEncrypted", saveEncrypted)
|
||||||
registerAsyncCallback("ofdgoSaveSigned", saveSigned)
|
registerAsyncCallback("ofdgoSaveSigned", saveSigned)
|
||||||
|
registerCallback("ofdgoPreviewSignatureSeal", previewSignatureSeal)
|
||||||
|
registerCallback("ofdgoSignaturePlacement", signaturePlacement)
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4148,6 +4150,105 @@ func saveEncrypted(args []js.Value) (any, error) {
|
|||||||
return saveEditor(editor, args[1:])
|
return saveEditor(editor, args[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// previewSignatureSeal 渲染独立印章预览并读取所选页面区域,不接触私钥或修改文档
|
||||||
|
// 入参: args 印章数据和页码范围
|
||||||
|
// 返回: any 印章图像、声明尺寸和页面区域, error 错误信息
|
||||||
|
func previewSignatureSeal(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil || len(args) < 2 {
|
||||||
|
return nil, fmt.Errorf("missing seal preview arguments")
|
||||||
|
}
|
||||||
|
data, err := bytesFromJS(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
seal, err := ofdgo.ParseSignatureSeal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
indices, err := ofdgo.ParsePageRange(args[1].String(), len(currentSession.doc.Pages.Page))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pages := make([]map[string]any, 0, len(indices))
|
||||||
|
for _, index := range indices {
|
||||||
|
area, err := currentSession.Reader.PageArea(currentSession.doc.Pages.Page[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
box, err := ofdgo.ParseBox(area.PhysicalBox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pages = append(pages, map[string]any{"index": index, "x": box.X, "y": box.Y, "width": box.W, "height": box.H})
|
||||||
|
}
|
||||||
|
editor := ofdgo.NewEditor()
|
||||||
|
if _, err := editor.AddPage(seal.Width, seal.Height); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader, err := editor.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
doc, err := reader.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader.Stamps = map[string][]ofdgo.Stamp{doc.Pages.Page[0].ID: {{Box: ofdgo.Box{W: seal.Width, H: seal.Height}, Type: seal.Type, Data: seal.Data}}}
|
||||||
|
renderer := ofdgo.NewRenderer(reader, ofdgo.WithRenderBackends(currentSession.Renderer.Backends()))
|
||||||
|
renderer.TransparentBackground = true
|
||||||
|
if currentSession.fontFS != nil {
|
||||||
|
renderer.SetFontFS(currentSession.fontFS)
|
||||||
|
}
|
||||||
|
page, err := reader.PageContentByIndex(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var svg bytes.Buffer
|
||||||
|
if err := renderer.RenderToSVG(page, &svg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{"svg": svg.String(), "width": seal.Width, "height": seal.Height, "pages": pages}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureStampsFromJS 复用库层坐标校验生成页面或骑缝印章位置
|
||||||
|
// 入参: input 页面和毫米坐标参数
|
||||||
|
// 返回: []ofdgo.SignatureStamp 印章位置, error 校验错误
|
||||||
|
func signatureStampsFromJS(input js.Value) ([]ofdgo.SignatureStamp, error) {
|
||||||
|
count, err := currentSession.Reader.PageCount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
indices, err := ofdgo.ParsePageRange(input.Get("pages").String(), count)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pages := make([]int, len(indices))
|
||||||
|
for i, index := range indices {
|
||||||
|
pages[i] = index + 1
|
||||||
|
}
|
||||||
|
box := ofdgo.Box{X: input.Get("x").Float(), Y: input.Get("y").Float(), W: input.Get("width").Float(), H: input.Get("height").Float()}
|
||||||
|
switch input.Get("placement").String() {
|
||||||
|
case "normal":
|
||||||
|
return currentSession.Reader.SignaturePageStamps(pages, box)
|
||||||
|
case "seam":
|
||||||
|
return currentSession.Reader.SignatureSeamStamps(pages, box)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid stamp placement")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// signaturePlacement 校验预览确认的印章范围,不生成签名
|
||||||
|
// 入参: args 页面和毫米坐标参数
|
||||||
|
// 返回: any 校验结果, error 错误信息
|
||||||
|
func signaturePlacement(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil || len(args) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing stamp placement arguments")
|
||||||
|
}
|
||||||
|
_, err := signatureStampsFromJS(args[0])
|
||||||
|
return successResult(nil), err
|
||||||
|
}
|
||||||
|
|
||||||
// saveSigned 调用库层最终包签署并在回验成功后交付浏览器
|
// saveSigned 调用库层最终包签署并在回验成功后交付浏览器
|
||||||
// 入参: args 本机证书私钥、信任根、印章位置、写出回调及进度回调
|
// 入参: args 本机证书私钥、信任根、印章位置、写出回调及进度回调
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: any 保存结果, error 错误信息
|
||||||
@@ -4200,30 +4301,10 @@ func saveSigned(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
count, err := currentSession.Reader.PageCount()
|
options.Stamps, err = signatureStampsFromJS(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
indices, err := ofdgo.ParsePageRange(input.Get("pages").String(), count)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
box := ofdgo.Box{X: input.Get("x").Float(), Y: input.Get("y").Float(), W: input.Get("width").Float(), H: input.Get("height").Float()}
|
|
||||||
if input.Get("placement").String() == "seam" {
|
|
||||||
pages := make([]int, len(indices))
|
|
||||||
for i, index := range indices {
|
|
||||||
pages[i] = index + 1
|
|
||||||
}
|
|
||||||
options.Stamps, err = currentSession.Reader.SignatureSeamStamps(pages, box)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, index := range indices {
|
|
||||||
options.Stamps = append(options.Stamps, ofdgo.SignatureStamp{PageRef: currentSession.doc.Pages.Page[index].ID,
|
|
||||||
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
progress := editorOperationProgress(args[2])
|
progress := editorOperationProgress(args[2])
|
||||||
if err := progress("sign", 0, 1); err != nil {
|
if err := progress("sign", 0, 1); err != nil {
|
||||||
|
|||||||
@@ -48,9 +48,21 @@ type Annotation struct {
|
|||||||
NoZoom bool `xml:"NoZoom,attr"`
|
NoZoom bool `xml:"NoZoom,attr"`
|
||||||
NoRotate bool `xml:"NoRotate,attr"`
|
NoRotate bool `xml:"NoRotate,attr"`
|
||||||
Remark string `xml:"Remark"`
|
Remark string `xml:"Remark"`
|
||||||
|
Parameters *AnnotationParameters `xml:"Parameters,omitempty"`
|
||||||
Appearance Appearance
|
Appearance Appearance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnnotationParameters 保存注释参数集合
|
||||||
|
type AnnotationParameters struct {
|
||||||
|
Parameter []AnnotationParameter `xml:"Parameter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnotationParameter 保存标准注释参数的名称和值
|
||||||
|
type AnnotationParameter struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
Value string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
// AnnotationInfo 注释信息,Page从1开始,位置和尺寸使用页面毫米坐标
|
// AnnotationInfo 注释信息,Page从1开始,位置和尺寸使用页面毫米坐标
|
||||||
type AnnotationInfo struct {
|
type AnnotationInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
@@ -96,6 +96,28 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error)
|
|||||||
if annotation.Remark != "" {
|
if annotation.Remark != "" {
|
||||||
content = editorXMLText("Remark", annotation.Remark)
|
content = editorXMLText("Remark", annotation.Remark)
|
||||||
}
|
}
|
||||||
|
if annotation.Parameters != nil && len(annotation.Parameters.Parameter) != 0 {
|
||||||
|
var parameters []byte
|
||||||
|
for _, parameter := range annotation.Parameters.Parameter {
|
||||||
|
if parameter.Name == "" {
|
||||||
|
return fmt.Errorf("empty annotation parameter name")
|
||||||
|
}
|
||||||
|
var value bytes.Buffer
|
||||||
|
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||||
|
}
|
||||||
|
data, err := editorXMLContainer("Parameters", nil, parameters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||||
|
}
|
||||||
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
|
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
|
||||||
data, err := editorXMLContainer("Annot", attrs, content)
|
data, err := editorXMLContainer("Annot", attrs, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+15
-1
@@ -112,6 +112,20 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
|
|||||||
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF darken stamp appearance flattened over page content"})
|
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF darken stamp appearance flattened over page content"})
|
||||||
}
|
}
|
||||||
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}}
|
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}}
|
||||||
|
popupObject := annotation.Dictionary["Popup"]
|
||||||
|
if annotation.Subtype == "Popup" {
|
||||||
|
popupObject = annotation.Dictionary
|
||||||
|
}
|
||||||
|
if popupObject != nil {
|
||||||
|
popup, err := p.reader.ReadPopupAnnotation(popupObject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{
|
||||||
|
{Name: "PDF.Popup.Open", Value: strconv.FormatBool(popup.Open)},
|
||||||
|
{Name: "PDF.Popup.Rect", Value: fmt.Sprintf("%g %g %g %g", popup.Rect.XMin, popup.Rect.YMin, popup.Rect.XMax, popup.Rect.YMax)},
|
||||||
|
}}
|
||||||
|
}
|
||||||
if len(actions) != 0 {
|
if len(actions) != 0 {
|
||||||
region, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
|
region, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -220,7 +234,7 @@ func pdfAnnotationType(subtype pdfgo.Name) string {
|
|||||||
return "Watermark"
|
return "Watermark"
|
||||||
case "Highlight", "Underline", "Squiggly", "StrikeOut":
|
case "Highlight", "Underline", "Squiggly", "StrikeOut":
|
||||||
return "Highlight"
|
return "Highlight"
|
||||||
case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link":
|
case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup":
|
||||||
return "Path"
|
return "Path"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
+53
-1
@@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
@@ -30,8 +31,59 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, annotation := range annotations {
|
parents := make(map[pdfgo.Reference]int)
|
||||||
|
for i, annotation := range annotations {
|
||||||
|
if annotation.Reference != (pdfgo.Reference{}) && annotation.Subtype != "Popup" {
|
||||||
|
parents[annotation.Reference] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attached := make(map[int]bool)
|
||||||
|
for i, annotation := range annotations {
|
||||||
|
if annotation.Subtype != "Popup" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
popup, err := p.reader.ReadPopupAnnotation(annotation.Dictionary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if parent, ok := parents[popup.Parent]; ok {
|
||||||
|
dict := maps.Clone(annotations[parent].Dictionary)
|
||||||
|
if previous := dict["Popup"]; previous != nil {
|
||||||
|
if reference, ok := previous.(pdfgo.Reference); !ok || reference != annotation.Reference {
|
||||||
|
return fmt.Errorf("inconsistent PDF popup parent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dict["Popup"] = annotation.Dictionary
|
||||||
|
annotations[parent].Dictionary = dict
|
||||||
|
attached[i] = true
|
||||||
|
} else {
|
||||||
|
annotations[i].Dictionary = popup.Dictionary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, annotation := range annotations {
|
||||||
|
if attached[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dict := annotation.Dictionary
|
dict := annotation.Dictionary
|
||||||
|
popupObject := dict["Popup"]
|
||||||
|
if annotation.Subtype == "Popup" {
|
||||||
|
popupObject = dict
|
||||||
|
}
|
||||||
|
if popupObject != nil {
|
||||||
|
popup, err := p.reader.ReadPopupAnnotation(popupObject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if annotation.Subtype != "Popup" && popup.Parent != (pdfgo.Reference{}) && popup.Parent != annotation.Reference {
|
||||||
|
return fmt.Errorf("inconsistent PDF popup parent")
|
||||||
|
}
|
||||||
|
if popup.Open {
|
||||||
|
if strict {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "initially open annotation popup"}
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF popup open state retained as annotation parameter"})
|
||||||
|
}
|
||||||
|
}
|
||||||
if annotation.Subtype == "Sound" {
|
if annotation.Subtype == "Sound" {
|
||||||
sound, err := p.reader.ReadSound(dict["Sound"])
|
sound, err := p.reader.ReadSound(dict["Sound"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -36,6 +36,46 @@ const (
|
|||||||
signASN1Sequence = 16
|
signASN1Sequence = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ParsedSignatureSeal 保存电子印章声明信息、图像和毫米尺寸,不代表验签或信任结果
|
||||||
|
type ParsedSignatureSeal struct {
|
||||||
|
Info SignatureSealInfo
|
||||||
|
Type string
|
||||||
|
Data []byte
|
||||||
|
Width float64
|
||||||
|
Height float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSignatureSeal 解析独立SES印章供展示使用,不验证授权、有效期或制章者信任
|
||||||
|
// 入参: data DER编码的电子印章
|
||||||
|
// 返回: *ParsedSignatureSeal 印章信息及独立图像副本, error 解析错误
|
||||||
|
func ParseSignatureSeal(data []byte) (*ParsedSignatureSeal, error) {
|
||||||
|
var raw asn1.RawValue
|
||||||
|
rest, err := asn1.Unmarshal(data, &raw)
|
||||||
|
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || raw.Class != asn1.ClassUniversal {
|
||||||
|
return nil, fmt.Errorf("invalid SES seal DER")
|
||||||
|
}
|
||||||
|
seal, err := parseSESSeal(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items, _ := asn1Children(raw.Bytes)
|
||||||
|
info, _ := asn1Children(items[0].Bytes)
|
||||||
|
picture, _ := asn1Children(info[3].Bytes)
|
||||||
|
width, err := asn1Integer(picture[2])
|
||||||
|
if err != nil || width <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid seal picture width")
|
||||||
|
}
|
||||||
|
height, err := asn1Integer(picture[3])
|
||||||
|
if err != nil || height <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid seal picture height")
|
||||||
|
}
|
||||||
|
format, image := probeSealMedia(seal.PicData)
|
||||||
|
if len(image) == 0 || format != seal.PicType {
|
||||||
|
return nil, fmt.Errorf("invalid seal picture")
|
||||||
|
}
|
||||||
|
return &ParsedSignatureSeal{Info: seal.Info, Type: format, Data: append([]byte(nil), image...), Width: float64(width), Height: float64(height)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// sesSignature SES签章值
|
// sesSignature SES签章值
|
||||||
type sesSignature struct {
|
type sesSignature struct {
|
||||||
Timestamp []byte
|
Timestamp []byte
|
||||||
|
|||||||
@@ -21,6 +21,29 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SignaturePageStamps 生成所选页面相同毫米坐标的印章位置并检查页内范围
|
||||||
|
// 入参: pages 从1开始且不重复的页码, box 完整印章区域
|
||||||
|
// 返回: []SignatureStamp 可传入签署选项的位置, error 错误信息
|
||||||
|
func (r *Reader) SignaturePageStamps(pages []int, box Box) ([]SignatureStamp, error) {
|
||||||
|
if r == nil || len(pages) == 0 || !signatureWriteBoxValid(box) {
|
||||||
|
return nil, fmt.Errorf("invalid page stamp parameters")
|
||||||
|
}
|
||||||
|
doc, err := r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
seen := make(map[int]bool)
|
||||||
|
stamps := make([]SignatureStamp, 0, len(pages))
|
||||||
|
for _, page := range pages {
|
||||||
|
if page < 1 || page > len(doc.Pages.Page) || seen[page] {
|
||||||
|
return nil, fmt.Errorf("invalid stamp page: %d", page)
|
||||||
|
}
|
||||||
|
seen[page] = true
|
||||||
|
stamps = append(stamps, SignatureStamp{PageRef: doc.Pages.Page[page-1].ID, Boundary: signatureWriteBox(box)})
|
||||||
|
}
|
||||||
|
return signatureWriteStamps(r, stamps, true)
|
||||||
|
}
|
||||||
|
|
||||||
// SignatureSeamStamps 生成页面右侧的等宽骑缝印章位置
|
// SignatureSeamStamps 生成页面右侧的等宽骑缝印章位置
|
||||||
// 页码从1开始,按传入顺序分割同一印章;box.X为右侧内缩,Y为纵坐标,W/H为完整印章尺寸
|
// 页码从1开始,按传入顺序分割同一印章;box.X为右侧内缩,Y为纵坐标,W/H为完整印章尺寸
|
||||||
// 入参: pages 至少两页且不重复, box 印章参数,单位毫米
|
// 入参: pages 至少两页且不重复, box 印章参数,单位毫米
|
||||||
|
|||||||
Reference in new issue
Block a user