Compare commits

...

2 Commits

Author SHA1 Message Date
nobohan
bf61324c1d Signature: multi page viewer - WIP 2024-09-04 11:26:08 +02:00
nobohan
abf20b0cf2 Signature: add a signature zone manually 2024-09-04 10:14:08 +02:00
3 changed files with 156 additions and 49 deletions

View File

@@ -68,7 +68,7 @@ export interface PDFPage {
height: number,
}
export interface SignatureZone {
index: number,
index: number | null,
x: number,
y: number,
width: number,
@@ -82,4 +82,6 @@ export interface Signature {
zones: SignatureZone[],
}
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
export type CanvasEvent = 'select' | 'add';

View File

@@ -75,13 +75,44 @@
</div>
</div>
</div>
<div class="col-12 text-center">
<canvas class="m-auto" id="canvas"></canvas>
<div v-if="multiPage" class="col-12 text-center">
<canvas
v-for="p in pageCount"
:key="p"
class="m-auto"
:id="`canvas-${p}`"
></canvas>
</div>
<div v-else class="col-12 text-center">
<canvas class="m-auto" :id="canvas"></canvas>
</div>
<div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
<div class="row mb-3">
<div class="col-12 d-flex justify-content-end">
<div class="col-4 col-xl-3 gap-2 d-grid">
<button
v-if="adding"
class="btn btn-misc btn-cancel me-2 btn-sm"
@click="removeNewZone()"
>
{{ $t("remove_sign_zone") }}
</button>
</div>
<div class="col-4 gap-2 d-grid">
<button
class="btn btn-create btn-sm"
:class="{ active: canvasEvent === 'add' }"
@click="toggleAddZone()"
>
{{ $t("add_sign_zone") }}
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="col-4">
<button
class="btn btn-action me-2"
:disabled="!userSignatureZone"
@@ -90,7 +121,7 @@
{{ $t("sign") }}
</button>
</div>
<div class="col-6 d-flex justify-content-end">
<div class="col-8 d-flex justify-content-end">
<button
class="btn btn-misc me-2"
:hidden="!userSignatureZone"
@@ -119,7 +150,12 @@
import { ref, Ref, reactive } from "vue";
import { useToast } from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
import { Signature, SignatureZone, SignedState } from "../../types";
import {
CanvasEvent,
Signature,
SignatureZone,
SignedState,
} from "../../types";
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import * as pdfjsLib from "pdfjs-dist";
import {
@@ -142,13 +178,15 @@ import {
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
const multiPage: Ref<boolean> = ref(true);
const modalOpen: Ref<boolean> = ref(false);
const loading: Ref<boolean> = ref(false);
const adding: Ref<boolean> = ref(false);
const canvasEvent: Ref<CanvasEvent> = ref("select");
const signedState: Ref<SignedState> = ref("pending");
const page: Ref<number> = ref(1);
const pageCount: Ref<number> = ref(0);
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
let pdfSource: Ref<string> = ref("");
let pdf = {} as PDFDocumentProxy;
declare global {
@@ -162,17 +200,30 @@ const $toast = useToast();
const signature = window.signature;
const urlInfo = build_download_info_link(signature.storedObject.filename);
console.log(signature);
const mountPdf = async (url: string) => {
const loadingTask = pdfjsLib.getDocument(url);
pdf = await loadingTask.promise;
pageCount.value = pdf.numPages;
await setPage(1);
if (multiPage.value) {
await setAllPages();
} else {
await setPage(1);
}
};
const getRenderContext = (pdfPage: PDFPageProxy) => {
const scale = 1;
const viewport = pdfPage.getViewport({ scale });
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
let canvas;
if (multiPage.value) {
canvas = document.getElementById(
`canvas-${pdfPage.pageNumber}`
) as HTMLCanvasElement;
} else {
canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
}
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
canvas.height = viewport.height;
canvas.width = viewport.width;
@@ -183,6 +234,9 @@ const getRenderContext = (pdfPage: PDFPageProxy) => {
};
};
const setAllPages = async () =>
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
const setPage = async (page: number) => {
const pdfPage = await pdf.getPage(page);
const renderContext = getRenderContext(pdfPage);
@@ -208,33 +262,31 @@ async function downloadAndOpen(): Promise<Blob> {
const initPdf = () => {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvas.addEventListener(
"pointerup",
(e: PointerEvent) => canvasClick(e, canvas),
false
);
canvas.addEventListener("pointerup", canvasClick, false);
setTimeout(() => addZones(page.value), 800);
};
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
Math.round((x * canvasWidth) / PDFWidth);
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
Math.round((h * canvasHeight) / PDFHeight);
const hitSignature = (
zone: SignatureZone,
xy: number[],
canvasWidth: number,
canvasHeight: number
) => {
const scaleXToCanvas = (x: number) =>
Math.round((x * canvasWidth) / zone.PDFPage.width);
const scaleHeightToCanvas = (h: number) =>
Math.round((h * canvasHeight) / zone.PDFPage.height);
const scaleYToCanvas = (y: number) =>
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
return (
scaleXToCanvas(zone.x) < xy[0] &&
xy[0] < scaleXToCanvas(zone.x + zone.width) &&
scaleYToCanvas(zone.y) < xy[1] &&
xy[1] < scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height)
);
};
) =>
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
xy[0] <
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
zone.PDFPage.height -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
xy[1] &&
xy[1] <
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
zone.PDFPage.height;
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
userSignatureZone.value = z;
@@ -245,7 +297,7 @@ const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
}
};
const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
const selectZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones
.filter((z) => z.PDFPage.index + 1 === page.value)
.map((z) => {
@@ -262,6 +314,13 @@ const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
}
});
const canvasClick = (e: PointerEvent) => {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvasEvent.value === "select"
? selectZoneOnCanvas(e, canvas)
: addZoneOnCanvas(e, canvas);
};
const turnPage = async (upOrDown: number) => {
userSignatureZone.value = null;
page.value = page.value + upOrDown;
@@ -296,12 +355,6 @@ const drawZone = (
) => {
const unselectedBlue = "#007bff";
const selectedBlue = "#034286";
const scaleXToCanvas = (x: number) =>
Math.round((x * canvasWidth) / zone.PDFPage.width);
const scaleHeightToCanvas = (h: number) =>
Math.round((h * canvasHeight) / zone.PDFPage.height);
const scaleYToCanvas = (y: number) =>
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
ctx.strokeStyle =
userSignatureZone.value?.index === zone.index
? selectedBlue
@@ -309,16 +362,22 @@ const drawZone = (
ctx.lineWidth = 2;
ctx.lineJoin = "bevel";
ctx.strokeRect(
scaleXToCanvas(zone.x),
scaleYToCanvas(zone.y),
scaleXToCanvas(zone.width),
scaleHeightToCanvas(zone.height)
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
zone.PDFPage.height -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
);
ctx.font = "bold 16px serif";
ctx.textAlign = "center";
ctx.fillStyle = "black";
const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2;
const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2;
const xText =
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
const yText =
zone.PDFPage.height -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
if (userSignatureZone.value?.index === zone.index) {
ctx.fillStyle = selectedBlue;
ctx.fillText("Signer ici", xText, yText);
@@ -420,21 +479,65 @@ const confirmSign = () => {
};
const undoSign = async () => {
// const canvas = document.querySelectorAll("canvas")[0];
// const ctx = canvas.getContext("2d");
// if (ctx && userSignatureZone.value) {
// //drawZone(userSignatureZone.value, ctx, canvas.width, canvas.height);
// }
signature.zones = signature.zones.filter((z) => z.index !== null);
await setPage(page.value);
setTimeout(() => addZones(page.value), 200);
userSignatureZone.value = null;
adding.value = false;
};
const toggleAddZone = () => {
canvasEvent.value === "select"
? (canvasEvent.value = "add")
: (canvasEvent.value = "select");
};
const addZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) => {
const BOX_WIDTH = 180;
const BOX_HEIGHT = 90;
const PDFPageHeight = canvas.height;
const PDFPageWidth = canvas.width;
const x = e.offsetX;
const y = e.offsetY;
const newZone: SignatureZone = {
index: null,
x:
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
y:
PDFPageHeight -
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
width: BOX_WIDTH,
height: BOX_HEIGHT,
PDFPage: {
index: page.value - 1,
width: PDFPageWidth,
height: PDFPageHeight,
},
};
signature.zones.push(newZone);
setTimeout(() => addZones(page.value), 200);
canvasEvent.value = "select";
adding.value = true;
};
const removeNewZone = async () => {
signature.zones = signature.zones.filter((z) => z.index !== null);
userSignatureZone.value = null;
await setPage(page.value);
setTimeout(() => addZones(page.value), 200);
canvasEvent.value = "select";
adding.value = false;
};
downloadAndOpen();
</script>
<style scoped lang="scss">
#canvas {
canvas {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
}
div#action-buttons {

View File

@@ -16,7 +16,9 @@ const appMessages = {
last_sign_zone: 'Zone de signature précédente',
next_sign_zone: 'Zone de signature suivante',
electronic_signature_in_progress: 'Signature électronique en cours...',
loading: 'Chargement...'
loading: 'Chargement...',
add_sign_zone: 'Ajouter une zone de signature',
remove_sign_zone: 'Enlever la zone',
}
}