From 135d094a6a21c122d4006880f536265c548af6cd Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Wed, 18 Jun 2025 15:00:20 +0200 Subject: [PATCH 01/17] sign color: "toolbox" with colorpicker --- public/js/signature.js | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/public/js/signature.js b/public/js/signature.js index 5c42393..753fcd8 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -157,6 +157,13 @@ async function loadPDF(pdfBlob) { const textLinesMaxWidth = event.target.textLines.reduce((max, _, i) => Math.max(max, event.target.getLineWidth(i)), 0); event.target.set({width: textLinesMaxWidth}); }); + canvasEdition.on("selection:created", function(event) { + if (event.selected.length > 1 || event.selected.length === 0) { + return; + } + + toolBox.init(event.selected[0]) + }); canvasEditions.push(canvasEdition); }); } @@ -1118,3 +1125,52 @@ document.addEventListener('DOMContentLoaded', function () { window.location.reload(); }) }); + +const toolBox = (function () { + const _coloricon = document.createElement('img') + _coloricon.src = 'data:image/svg+xml,' + + function _renderIcon(icon) { + return function renderIcon(ctx, left, top, styleOverride, fabricObject) { + const size = this.cornerSize; + ctx.save(); + ctx.translate(left, top); + ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); + ctx.drawImage(icon, -size/2, -size/2, size, size); + ctx.restore(); + } + } + + function _changeColor(eventData, transform) { + const target = transform.target; + const _colorpicker = document.createElement('input') + _colorpicker.setAttribute('type', 'color') + + _colorpicker.addEventListener('input', function (e) { + target.set({ fill: e.target.value }) + target.canvas.requestRenderAll() + }) + + _colorpicker.click() + _colorpicker.remove() + } + + function init(el) { + colorControl = new fabric.Control({ + x: 0.5, + y: -0.5, + offsetY: -16, + offsetX: 16, + cursorStyle: 'pointer', + mouseUpHandler: _changeColor, + render: _renderIcon(_coloricon), + cornerSize: 24 + }) + + el.controls.color = colorControl + } + + return { + init: init + } +})() From 48323911b3a80600ec5b49a9c98089afa2ae1ddb Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Wed, 18 Jun 2025 18:17:44 +0200 Subject: [PATCH 02/17] add input type=color to modify signature color --- public/js/signature.js | 16 +++++++++++++--- templates/signature.html.php | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/public/js/signature.js b/public/js/signature.js index 753fcd8..6ebf538 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -16,6 +16,8 @@ let menu = null; let menuOffcanvas = null; let currentCursor = null; let signaturePad = null; +const penColorPicker = document.getElementById('penColorPicker'); +let penColor = '#000000' let nblayers = null; let hasModifications = false; let currentTextScale = 1; @@ -501,7 +503,8 @@ function createAndAddSvgInCanvas(canvas, item, x, y, height = null) { top: y - 20, fontSize: 20, direction: direction, - fontFamily: 'Monospace' + fontFamily: 'Monospace', + fill: penColor }); addObjectInCanvas(canvas, textbox).setActiveObject(textbox); @@ -518,8 +521,8 @@ function createAndAddSvgInCanvas(canvas, item, x, y, height = null) { if(item == 'strikethrough') { let line = new fabric.Line([x, y, x + 250, y], { - fill: 'black', - stroke: 'black', + fill: penColor, + stroke: penColor, lockScalingFlip: true, strokeWidth: 2, padding: 10, @@ -546,6 +549,8 @@ function createAndAddSvgInCanvas(canvas, item, x, y, height = null) { svg.top = y - (svg.getScaledHeight() / 2); svg.left = x - (svg.getScaledWidth() / 2); + svg.fill = penColor + addObjectInCanvas(canvas, svg); }); }; @@ -938,6 +943,11 @@ function createEventsListener() { zoomChange(1) }); + penColorPicker.addEventListener('input', function (e) { + e.preventDefault() + penColor = penColorPicker.value + }) + window.addEventListener('beforeunload', function(event) { if(!hasModifications) { return; diff --git a/templates/signature.html.php b/templates/signature.html.php index 54a78c4..705efdb 100644 --- a/templates/signature.html.php +++ b/templates/signature.html.php @@ -181,8 +181,9 @@ - From 76e2082a72b9bc2d16083b42e1728e38b34cc6f7 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Wed, 18 Jun 2025 18:30:05 +0200 Subject: [PATCH 03/17] sign color: store custom color in localstorage --- public/js/signature.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/js/signature.js b/public/js/signature.js index 6ebf538..8e3d122 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -17,7 +17,8 @@ let menuOffcanvas = null; let currentCursor = null; let signaturePad = null; const penColorPicker = document.getElementById('penColorPicker'); -let penColor = '#000000' +let penColor = localStorage.getItem('penColor') ?? '#000000' +penColorPicker.value = penColor let nblayers = null; let hasModifications = false; let currentTextScale = 1; @@ -946,6 +947,7 @@ function createEventsListener() { penColorPicker.addEventListener('input', function (e) { e.preventDefault() penColor = penColorPicker.value + localStorage.setItem('penColor', penColor) }) window.addEventListener('beforeunload', function(event) { From 83d95ed2e90720b11696eacb21c330f81bd5dfb4 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Thu, 19 Jun 2025 16:55:45 +0200 Subject: [PATCH 04/17] debounce function on endStroke event limit api call --- public/js/common.js | 12 ++++++++++++ public/js/signature.js | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/js/common.js b/public/js/common.js index 9f6926e..b1f51fd 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -259,3 +259,15 @@ async function imageToPdf(file) { function convertOctet2MegoOctet(nbOctet) { return (Math.round(nbOctet/1000/1000*100)/100).toFixed(2); } + +function debounce(callback, delay){ + let timer; + return function(){ + const args = arguments; + const context = this; + clearTimeout(timer); + timer = setTimeout(function(){ + callback.apply(context, args); + }, delay) + } +} diff --git a/public/js/signature.js b/public/js/signature.js index 8e3d122..2362387 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -973,14 +973,14 @@ function createSignaturePad() { minWidth: 1, maxWidth: 2 }); - signaturePad.addEventListener('endStroke', function(){ + signaturePad.addEventListener('endStroke', debounce(function(){ const file = new File([dataURLtoBlob(signaturePad.toDataURL())], "draw.png", { type: 'image/png' }); let data = new FormData(); data.append('file', file); uploadSVG(data); - }); + }), 500); }; async function getPDFBlobFromCache(cacheUrl) { From b8696b05c8aab8aee49ad4d54782dd7fad643cea Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Fri, 20 Jun 2025 18:32:26 +0200 Subject: [PATCH 05/17] =?UTF-8?q?function=20globale=20enregistrement=20cou?= =?UTF-8?q?leur=20trac=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/signature.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/js/signature.js b/public/js/signature.js index 2362387..4af99b3 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -18,7 +18,6 @@ let currentCursor = null; let signaturePad = null; const penColorPicker = document.getElementById('penColorPicker'); let penColor = localStorage.getItem('penColor') ?? '#000000' -penColorPicker.value = penColor let nblayers = null; let hasModifications = false; let currentTextScale = 1; @@ -946,8 +945,7 @@ function createEventsListener() { penColorPicker.addEventListener('input', function (e) { e.preventDefault() - penColor = penColorPicker.value - localStorage.setItem('penColor', penColor) + storePenColor(penColorPicker.value) }) window.addEventListener('beforeunload', function(event) { @@ -1105,6 +1103,7 @@ async function pageSignature(url) { return; } + storePenColor(penColor) createSignaturePad(); responsiveDisplay(); displaysSVG(); @@ -1138,6 +1137,12 @@ document.addEventListener('DOMContentLoaded', function () { }) }); +function storePenColor(color) { + penColor = color + penColorPicker.value = color + localStorage.setItem('penColor', penColor) +} + const toolBox = (function () { const _coloricon = document.createElement('img') _coloricon.src = 'data:image/svg+xml,' @@ -1161,6 +1166,7 @@ const toolBox = (function () { _colorpicker.addEventListener('input', function (e) { target.set({ fill: e.target.value }) target.canvas.requestRenderAll() + storePenColor(e.target.value) }) _colorpicker.click() From f3fdb969f9f87ef04b31758a87e93a9ba9e528b1 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Fri, 20 Jun 2025 18:45:59 +0200 Subject: [PATCH 06/17] color picker fix black color not selectable --- public/js/signature.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/js/signature.js b/public/js/signature.js index 4af99b3..e5c8154 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -1162,6 +1162,7 @@ const toolBox = (function () { const target = transform.target; const _colorpicker = document.createElement('input') _colorpicker.setAttribute('type', 'color') + _colorpicker.value = penColor _colorpicker.addEventListener('input', function (e) { target.set({ fill: e.target.value }) From e19832adb00d58ff2aba6a0639cc328528d59e35 Mon Sep 17 00:00:00 2001 From: Vincent LAURENT Date: Mon, 23 Jun 2025 09:29:33 +0200 Subject: [PATCH 07/17] sign color: Flattened the signature svg so that color change is possible with fill --- app.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.php b/app.php index c23680a..a1b86b8 100644 --- a/app.php +++ b/app.php @@ -174,7 +174,7 @@ $f3->route('POST /image2svg', shell_exec(sprintf("convert -background white -flatten %s %s", $imageFile, $imageFile.".bmp")); shell_exec(sprintf("mkbitmap -x -f 8 %s -o %s", $imageFile.".bmp", $imageFile.".bpm")); - shell_exec(sprintf("potrace --svg %s -o %s", $imageFile.".bpm", $imageFile.".svg")); + shell_exec(sprintf("potrace --flat --svg %s -o %s", $imageFile.".bpm", $imageFile.".svg")); header('Content-Type: image/svg+xml'); echo file_get_contents($imageFile.".svg"); From 7a7285a16ece2bb89f256665cc43b87d9ee7626e Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Mon, 23 Jun 2025 12:53:06 +0200 Subject: [PATCH 08/17] fix debounce function --- public/js/signature.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/signature.js b/public/js/signature.js index e5c8154..0f77933 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -978,7 +978,7 @@ function createSignaturePad() { let data = new FormData(); data.append('file', file); uploadSVG(data); - }), 500); + }, 500)); }; async function getPDFBlobFromCache(cacheUrl) { From 2ff34a94b413216023d5eeb926db66d6e9ba35a7 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Mon, 23 Jun 2025 16:22:36 +0200 Subject: [PATCH 09/17] watermark: preview + rework frontend Switch to frontend management since the preview uses overlay and send the generated watermark Backend switched from merging the watermark into the pdf to just flattening the pdf (avoid duplicated watermark + no text selectable) --- app.php | 2 +- lib/PDFSignature.class.php | 20 ++++---------------- public/js/signature.js | 15 +++++++++++++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/app.php b/app.php index a1b86b8..5b90e7f 100644 --- a/app.php +++ b/app.php @@ -228,7 +228,7 @@ $f3->route('POST /sign', PDFSignature::createPDFFromSvg($svgFiles, $tmpfile.'.svg.pdf'); PDFSignature::addSvgToPDF($tmpfile.'.pdf', $tmpfile.'.svg.pdf', $tmpfile.'_signe.pdf'); if ($filigrane) { - PDFSignature::addFiligrane($filigrane, $tmpfile); + PDFSignature::flatten($tmpfile); } Web::instance()->send($tmpfile.'_signe.pdf', null, 0, TRUE, $filename); diff --git a/lib/PDFSignature.class.php b/lib/PDFSignature.class.php index 9bdc70a..f9ee2c8 100644 --- a/lib/PDFSignature.class.php +++ b/lib/PDFSignature.class.php @@ -196,26 +196,14 @@ class PDFSignature } } - public static function addFiligrane($text, $pdf) + public static function flatten($pdf) { // check version of imagick $command = (null === shell_exec("command -v magick")) ? 'convert' : 'magick'; - // Création texte watermark - $watermarkCommand = sprintf( - '%s -density 144 -units PixelsPerInch pdf:%s_signe.pdf -write mpr:base \ - \( -density 144 -units PixelsPerInch -background None -fill "#0007" -pointsize 20 label:%s -rotate -40 +repage -write mpr:TILE +delete \) \ - \( -clone 0 -tile mpr:TILE -draw "color 0,0 reset" -write mpr:TILES -delete 0 \) \ - -delete 0--1 \ - mpr:TILES null: mpr:base \ - -compose Overlay -layers composite \ - -density 144 -units PixelsPerInch \ - -sharpen 0x1.0 \ - -compress zip \ - pdf:%s_signe.pdf' - , $command, escapeshellarg($pdf), escapeshellarg($text), escapeshellarg($pdf)); - - shell_exec($watermarkCommand); + shell_exec(sprintf( + '%s -density 200 -units PixelsPerInch %s_signe.pdf -compress zip %s_signe.pdf' + , $command, escapeshellarg($pdf), escapeshellarg($pdf))); } public function clean() { diff --git a/public/js/signature.js b/public/js/signature.js index 0f77933..cd14caa 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -777,9 +777,20 @@ function createEventsListener() { div.classList.add('d-none') input.querySelector('input').focus() }) - document.querySelector('input[name=watermark]')?.addEventListener('keyup', function (e) { + document.querySelector('input[name=watermark]')?.addEventListener('keyup', debounce(function (e) { setIsChanged(hasModifications || !!e.target.value) - }) + + canvasEditions.forEach(function (canvas) { + const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0007"}) + const overlay = new fabric.Rect({ + fill: new fabric.Pattern({source: text.toCanvasElement(), repeat: 'repeat'}), + height: canvas.height, + width: canvas.width + }) + + canvas.setOverlayImage(overlay, canvas.renderAll.bind(canvas)) + }) + }, 750)) if(document.querySelector('#alert-signature-help')) { document.getElementById('btn-signature-help').addEventListener('click', function(event) { From 8ce7fda9b4d4743db4131a32ba67bdca0ffae5d0 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Mon, 23 Jun 2025 16:59:27 +0200 Subject: [PATCH 10/17] opacity watermark more visible --- public/js/signature.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/signature.js b/public/js/signature.js index cd14caa..99460c6 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -781,7 +781,7 @@ function createEventsListener() { setIsChanged(hasModifications || !!e.target.value) canvasEditions.forEach(function (canvas) { - const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0007"}) + const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0009"}) const overlay = new fabric.Rect({ fill: new fabric.Pattern({source: text.toCanvasElement(), repeat: 'repeat'}), height: canvas.height, From d4093080757dfa2fdee04cdde67a9301f258eaa9 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Mon, 23 Jun 2025 18:09:49 +0200 Subject: [PATCH 11/17] watermark: la taille du texte de l'overlay ne bouge plus au zoom --- public/js/signature.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/js/signature.js b/public/js/signature.js index 99460c6..0c6aa04 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -781,7 +781,10 @@ function createEventsListener() { setIsChanged(hasModifications || !!e.target.value) canvasEditions.forEach(function (canvas) { - const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0009"}) + // Pourquoi 27 : 40 / 1.5 = 26.6666 + // fontSize ^ ^ currentScale par défaut + // Comme ça le texte de l'overlay ne bouge pas au zoom + const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0009", fontSize: 27 * currentScale}) const overlay = new fabric.Rect({ fill: new fabric.Pattern({source: text.toCanvasElement(), repeat: 'repeat'}), height: canvas.height, From 16e2540fd60fa36eb0df43bdce9e721498c26f21 Mon Sep 17 00:00:00 2001 From: Vincent LAURENT Date: Mon, 23 Jun 2025 19:02:00 +0200 Subject: [PATCH 12/17] redact: redaction of an area --- public/js/signature.js | 45 ++++++++++++++++++++++++++++++++++-- templates/signature.html.php | 10 +++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/public/js/signature.js b/public/js/signature.js index e5c8154..a9bf166 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -133,6 +133,9 @@ async function loadPDF(pdfBlob) { if (event.target instanceof fabric.Line) { return; } + if (event.target instanceof fabric.Rect) { + return; + } if(event.transform.action == "scaleX") { event.target.scaleY = event.target.scaleX; } @@ -273,7 +276,7 @@ function svgChange(input, event) { let input_selected = document.querySelector('input[name="svg_2_add"]:checked'); - if(input_selected && !input_selected.value.match(/^data:/) && input_selected.value != "text" && input_selected.value != "strikethrough") { + if(input_selected && !input_selected.value.match(/^data:/) && input_selected.value != "text" && input_selected.value != "strikethrough" && input_selected.value != "rectangle") { input_selected = null; } @@ -439,6 +442,9 @@ function deleteActiveObject() { canvasEditions.forEach(function(canvasEdition, index) { canvasEdition.getActiveObjects().forEach(function(activeObject) { canvasEdition.remove(activeObject); + if(activeObject.type == "rect") { + updateFlatten(); + } }); }) }; @@ -468,6 +474,21 @@ function addObjectInCanvas(canvas, item) { return canvas.add(item); }; +function updateFlatten() { + let flatten = Boolean(document.querySelector('input[name=watermark]').value); + canvasEditions.forEach(function(canvasEdition, index) { + canvasEdition.getObjects().forEach(function(object) { + if(object.type == "rect") { + flatten = true; + } + }); + }) + document.querySelector('input[name=flatten]').checked = flatten; + if(document.getElementById('save_flatten_indicator')) { + document.getElementById('save_flatten_indicator').classList.toggle('invisible', !flatten); + } +} + function setIsChanged(changed) { hasModifications = changed @@ -534,6 +555,23 @@ function createAndAddSvgInCanvas(canvas, item, x, y, height = null) { return; } + if(item == 'rectangle') { + let rect = new fabric.Rect({ + left: x, + top: y, + width: 200, + height: 100, + fill: '#000', + lockScalingFlip: true + }); + rect.setControlsVisibility({ tl: false, tr: false, bl: false, br: false,}) + + addObjectInCanvas(canvas, rect).setActiveObject(rect); + + updateFlatten(); + return; + } + fabric.loadSVGFromURL(item, function(objects, options) { let svg = fabric.util.groupSVGElements(objects, options); svg.svgOrigin = item; @@ -779,6 +817,7 @@ function createEventsListener() { }) document.querySelector('input[name=watermark]')?.addEventListener('keyup', function (e) { setIsChanged(hasModifications || !!e.target.value) + updateFlatten(); }) if(document.querySelector('#alert-signature-help')) { @@ -1167,7 +1206,9 @@ const toolBox = (function () { _colorpicker.addEventListener('input', function (e) { target.set({ fill: e.target.value }) target.canvas.requestRenderAll() - storePenColor(e.target.value) + if(target.type != "rect") { + storePenColor(e.target.value) + } }) _colorpicker.click() diff --git a/templates/signature.html.php b/templates/signature.html.php index 705efdb..9d6e233 100644 --- a/templates/signature.html.php +++ b/templates/signature.html.php @@ -83,7 +83,10 @@ - +
+ + +
@@ -107,8 +110,9 @@
- -
+ + +
From 8234926d1e7ab3a0198b825a9a91d76864a88463 Mon Sep 17 00:00:00 2001 From: Vincent LAURENT Date: Mon, 23 Jun 2025 23:26:24 +0200 Subject: [PATCH 13/17] redact: wording and disabled in sharing mode --- app.php | 3 +-- templates/index.html.php | 1 + templates/signature.html.php | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app.php b/app.php index 5b90e7f..4e98468 100644 --- a/app.php +++ b/app.php @@ -189,7 +189,6 @@ $f3->route('POST /image2svg', $f3->route('POST /sign', function($f3) { $filename = null; - $filigrane = $f3->get('POST.watermark'); $tmpfile = tempnam($f3->get('UPLOADS'), 'pdfsignature_sign_'.uniqid("", true)); unlink($tmpfile); $svgFiles = []; @@ -227,7 +226,7 @@ $f3->route('POST /sign', PDFSignature::createPDFFromSvg($svgFiles, $tmpfile.'.svg.pdf'); PDFSignature::addSvgToPDF($tmpfile.'.pdf', $tmpfile.'.svg.pdf', $tmpfile.'_signe.pdf'); - if ($filigrane) { + if ($f3->get('POST.flatten')) { PDFSignature::flatten($tmpfile); } diff --git a/templates/index.html.php b/templates/index.html.php index bb640bf..14b7de7 100644 --- a/templates/index.html.php +++ b/templates/index.html.php @@ -29,6 +29,7 @@ +
diff --git a/templates/signature.html.php b/templates/signature.html.php index 9d6e233..3b8c76c 100644 --- a/templates/signature.html.php +++ b/templates/signature.html.php @@ -83,11 +83,11 @@ -
- - -
+
+ + +
@@ -111,7 +111,7 @@ - +
From f577c68755e0f1ea983d23480150852565b94ede Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Tue, 24 Jun 2025 11:03:07 +0200 Subject: [PATCH 14/17] flatten: return true early when rect found We don't need to iterate on all objects if one rect is found --- public/js/signature.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/js/signature.js b/public/js/signature.js index 9c9e371..63464bf 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -476,13 +476,13 @@ function addObjectInCanvas(canvas, item) { function updateFlatten() { let flatten = Boolean(document.querySelector('input[name=watermark]').value); - canvasEditions.forEach(function(canvasEdition, index) { - canvasEdition.getObjects().forEach(function(object) { - if(object.type == "rect") { - flatten = true; - } - }); + + flatten = flatten || canvasEditions.some(function (canvas) { + return canvas.getObjects().some(function (object) { + return object.type === "rect" + }) }) + document.querySelector('input[name=flatten]').checked = flatten; if(document.getElementById('save_flatten_indicator')) { document.getElementById('save_flatten_indicator').classList.toggle('invisible', !flatten); From 228b9a6839e9d729bd7694de8e9d147ee98783f5 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Tue, 24 Jun 2025 15:59:27 +0200 Subject: [PATCH 15/17] watermark opti --- public/js/signature.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/public/js/signature.js b/public/js/signature.js index 63464bf..ec02f56 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -819,18 +819,25 @@ function createEventsListener() { document.querySelector('input[name=watermark]')?.addEventListener('keyup', debounce(function (e) { setIsChanged(hasModifications || !!e.target.value) updateFlatten(); - canvasEditions.forEach(function (canvas) { - // Pourquoi 27 : 40 / 1.5 = 26.6666 - // fontSize ^ ^ currentScale par défaut - // Comme ça le texte de l'overlay ne bouge pas au zoom - const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0009", fontSize: 27 * currentScale}) - const overlay = new fabric.Rect({ - fill: new fabric.Pattern({source: text.toCanvasElement(), repeat: 'repeat'}), - height: canvas.height, - width: canvas.width - }) - canvas.setOverlayImage(overlay, canvas.renderAll.bind(canvas)) + // Pourquoi 27 : 40 / 1.5 = 26.6666 + // fontSize ^ ^ currentScale par défaut + // Comme ça le texte de l'overlay ne bouge pas au zoom + const text = new fabric.Text(e.target.value, {angle: -40, fill: "#0009", fontSize: 27 * currentScale}) + const overlay = new fabric.Rect({ + fill: new fabric.Pattern({ + source: text.toCanvasElement(), + }), + }) + + canvasEditions.forEach(function (canvas) { + overlay.height = canvas.height + overlay.width = canvas.width + + canvas.objectCaching = false + canvas.setOverlayImage(overlay, canvas.renderAll.bind(canvas), { + objectCaching: false + }) }) }, 750)) From c78725fb2dc7ad41f7c9472b03da91a17fc250cc Mon Sep 17 00:00:00 2001 From: Vincent LAURENT Date: Wed, 25 Jun 2025 16:43:36 +0200 Subject: [PATCH 16/17] watermark: update on resize and no text limit --- public/js/signature.js | 24 ++++++++++++++++++++++++ templates/signature.html.php | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/public/js/signature.js b/public/js/signature.js index ec02f56..194ed2f 100644 --- a/public/js/signature.js +++ b/public/js/signature.js @@ -474,6 +474,27 @@ function addObjectInCanvas(canvas, item) { return canvas.add(item); }; +function updateWatermark() { + const text = new fabric.Text(document.querySelector('input[name=watermark]').value, {angle: -40, fill: "#0009", fontSize: 27 * currentScale}) + text.scale = 0. + const overlay = new fabric.Rect({ + fill: new fabric.Pattern({ + source: text.toCanvasElement(), + }), + }) + + canvasEditions.forEach(function (canvas) { + overlay.height = canvas.height + overlay.width = canvas.width + + canvas.objectCaching = false + canvas.setOverlayImage(overlay, canvas.renderAll.bind(canvas), { + objectCaching: false + }) + }) +} + + function updateFlatten() { let flatten = Boolean(document.querySelector('input[name=watermark]').value); @@ -596,6 +617,7 @@ function createAndAddSvgInCanvas(canvas, item, x, y, height = null) { function autoZoom() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(resizePDF, 100); + updateWatermark(); }; function zoomChange(inOrOut) { @@ -681,6 +703,8 @@ function resizePDF(scale = 'auto') { resizeTimeout = null; }); }); + + updateWatermark(); }; function createEventsListener() { diff --git a/templates/signature.html.php b/templates/signature.html.php index 3b8c76c..dd88aff 100644 --- a/templates/signature.html.php +++ b/templates/signature.html.php @@ -93,7 +93,7 @@
- " aria-label="Watermark" aria-describedby="watermark-addon" maxlength="30"> + " aria-label="Watermark" aria-describedby="watermark-addon">
From 0e708ee6feb048357fde11981c1360cb16fe4ac3 Mon Sep 17 00:00:00 2001 From: Gabriel Poma Date: Wed, 25 Jun 2025 17:25:35 +0200 Subject: [PATCH 17/17] metadata: pdf text is selectable (can copy/paste from pdf to metadata input) --- public/css/pdf_viewer.css | 120 ++++++++++++++++++++++++++++++++++++ public/js/metadata.js | 40 ++++++++++-- templates/metadata.html.php | 1 + 3 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 public/css/pdf_viewer.css diff --git a/public/css/pdf_viewer.css b/public/css/pdf_viewer.css new file mode 100644 index 0000000..bacab24 --- /dev/null +++ b/public/css/pdf_viewer.css @@ -0,0 +1,120 @@ +.textLayer{ + position:absolute; + text-align:initial; + inset:0; + overflow:clip; + opacity:1; + line-height:1; + -webkit-text-size-adjust:none; + -moz-text-size-adjust:none; + text-size-adjust:none; + forced-color-adjust:none; + transform-origin:0 0; + caret-color:CanvasText; + z-index:0; +} + +.textLayer.highlighting{ + touch-action:none; +} + +.textLayer :is(span,br){ + color:transparent; + position:absolute; + white-space:pre; + cursor:text; + transform-origin:0% 0%; +} + +.textLayer > :not(.markedContent),.textLayer .markedContent span:not(.markedContent){ + z-index:1; +} + +.textLayer span.markedContent{ + top:0; + height:0; +} + +.textLayer .highlight{ + --highlight-bg-color:rgb(180 0 170 / 0.25); + --highlight-selected-bg-color:rgb(0 100 0 / 0.25); + --highlight-backdrop-filter:none; + --highlight-selected-backdrop-filter:none; +} + +@media screen and (forced-colors: active){ + + .textLayer .highlight{ + --highlight-bg-color:transparent; + --highlight-selected-bg-color:transparent; + --highlight-backdrop-filter:var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter:var( + --hcm-highlight-selected-filter + ); + } +} + +.textLayer .highlight{ + + margin:-1px; + padding:1px; + background-color:var(--highlight-bg-color); + -webkit-backdrop-filter:var(--highlight-backdrop-filter); + backdrop-filter:var(--highlight-backdrop-filter); + border-radius:4px; +} + +.appended:is(.textLayer .highlight){ + position:initial; +} + +.begin:is(.textLayer .highlight){ + border-radius:4px 0 0 4px; +} + +.end:is(.textLayer .highlight){ + border-radius:0 4px 4px 0; +} + +.middle:is(.textLayer .highlight){ + border-radius:0; +} + +.selected:is(.textLayer .highlight){ + background-color:var(--highlight-selected-bg-color); + -webkit-backdrop-filter:var(--highlight-selected-backdrop-filter); + backdrop-filter:var(--highlight-selected-backdrop-filter); +} + +.textLayer ::-moz-selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); +} + +.textLayer ::selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); +} + +.textLayer br::-moz-selection{ + background:transparent; +} + +.textLayer br::selection{ + background:transparent; +} + +.textLayer .endOfContent{ + display:block; + position:absolute; + inset:100% 0 0; + z-index:0; + cursor:default; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.textLayer.selecting .endOfContent{ + top:0; +} diff --git a/public/js/metadata.js b/public/js/metadata.js index 862c2c1..e2c47ce 100644 --- a/public/js/metadata.js +++ b/public/js/metadata.js @@ -81,14 +81,34 @@ async function pageRender(pageIndex) { let scaleWidth = sizeWidth / viewport.width; let viewportWidth = page.getViewport({scale: scaleWidth }); + document.documentElement.style.setProperty('--scale-factor', scaleWidth) // needed to scale the textLayer + // to the canvas size (var used in style attribute) + viewport = viewportWidth; - let canvasPDF = document.createElement('canvas'); - canvasPDF.classList.add('shadow-sm'); - document.getElementById('container-pages').appendChild(canvasPDF); - let context = canvasPDF.getContext('2d'); + const containerPagePDF = document.createElement('div') + const canvasPDF = document.createElement('canvas') + const wrapperPDF = document.createElement('div') + const textPDF = document.createElement('div') + + document.getElementById('container-pages').appendChild(containerPagePDF) + containerPagePDF.appendChild(wrapperPDF) + wrapperPDF.appendChild(canvasPDF) + wrapperPDF.appendChild(textPDF) + + const context = canvasPDF.getContext('2d') + canvasPDF.height = viewport.height; canvasPDF.width = viewport.width; + canvasPDF.classList.add('shadow-sm'); + + containerPagePDF.classList.add('page') + containerPagePDF.setAttribute('id', 'container-page-'+pageIndex) + + wrapperPDF.classList.add('canvasWrapper'); + wrapperPDF.style.position = 'relative' + + textPDF.classList.add('textLayer') if(pdfRenderTasks[pageIndex]) { pdfRenderTasks[pageIndex].cancel(); @@ -97,6 +117,18 @@ async function pageRender(pageIndex) { canvasContext: context, viewport: viewport, }); + + pdfRenderTasks[pageIndex].promise.then(function () { + return page.getTextContent() + }).then(function (textContent) { + const textLayer = new pdfjsLib.TextLayer({ + textContentSource: textContent, + viewport: viewport, + container: textPDF, + }); + + textLayer.render() + }) } function addMetadata(key, value, type, focus) { diff --git a/templates/metadata.html.php b/templates/metadata.html.php index 09fbcd6..2747061 100644 --- a/templates/metadata.html.php +++ b/templates/metadata.html.php @@ -2,6 +2,7 @@ + Signature PDF - Éditer les métadonnées