1
0
Fork 0
mirror of https://github.com/24eme/signaturepdf synced 2026-03-14 13:55:44 +01:00

Merge remote-tracking branch 'origin/master'

This commit is contained in:
Hosted Weblate 2025-06-26 01:03:03 +02:00
commit e54e83e896
No known key found for this signature in database
GPG key ID: A3FAAA06E6569B4C
9 changed files with 356 additions and 37 deletions

View file

@ -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");
@ -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,8 +226,8 @@ $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);
if ($f3->get('POST.flatten')) {
PDFSignature::flatten($tmpfile);
}
Web::instance()->send($tmpfile.'_signe.pdf', null, 0, TRUE, $filename);

View file

@ -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() {

120
public/css/pdf_viewer.css Normal file
View file

@ -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;
}

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -16,6 +16,8 @@ let menu = null;
let menuOffcanvas = null;
let currentCursor = null;
let signaturePad = null;
const penColorPicker = document.getElementById('penColorPicker');
let penColor = localStorage.getItem('penColor') ?? '#000000'
let nblayers = null;
let hasModifications = false;
let currentTextScale = 1;
@ -131,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;
}
@ -157,6 +162,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);
});
}
@ -264,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;
}
@ -430,6 +442,9 @@ function deleteActiveObject() {
canvasEditions.forEach(function(canvasEdition, index) {
canvasEdition.getActiveObjects().forEach(function(activeObject) {
canvasEdition.remove(activeObject);
if(activeObject.type == "rect") {
updateFlatten();
}
});
})
};
@ -459,6 +474,42 @@ 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);
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);
}
}
function setIsChanged(changed) {
hasModifications = changed
@ -494,7 +545,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);
@ -511,8 +563,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,
@ -524,6 +576,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;
@ -539,6 +608,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);
});
};
@ -546,6 +617,7 @@ function createAndAddSvgInCanvas(canvas, item, x, y, height = null) {
function autoZoom() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizePDF, 100);
updateWatermark();
};
function zoomChange(inOrOut) {
@ -631,6 +703,8 @@ function resizePDF(scale = 'auto') {
resizeTimeout = null;
});
});
updateWatermark();
};
function createEventsListener() {
@ -765,9 +839,31 @@ 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)
})
updateFlatten();
// 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))
if(document.querySelector('#alert-signature-help')) {
document.getElementById('btn-signature-help').addEventListener('click', function(event) {
@ -931,6 +1027,11 @@ function createEventsListener() {
zoomChange(1)
});
penColorPicker.addEventListener('input', function (e) {
e.preventDefault()
storePenColor(penColorPicker.value)
})
window.addEventListener('beforeunload', function(event) {
if(!hasModifications) {
return;
@ -954,14 +1055,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) {
@ -1086,6 +1187,7 @@ async function pageSignature(url) {
return;
}
storePenColor(penColor)
createSignaturePad();
responsiveDisplay();
displaysSVG();
@ -1118,3 +1220,62 @@ document.addEventListener('DOMContentLoaded', function () {
window.location.reload();
})
});
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,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-palette" viewBox="0 0 16 16"><path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/><path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/></svg>'
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.value = penColor
_colorpicker.addEventListener('input', function (e) {
target.set({ fill: e.target.value })
target.canvas.requestRenderAll()
if(target.type != "rect") {
storePenColor(e.target.value)
}
})
_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
}
})()

View file

@ -29,6 +29,7 @@
<a href="<?php echo $REVERSE_PROXY_URL; ?>/signature" class="list-group-item list-group-item-action"><i class="bi bi-people-fill"></i> <?php echo _("Allow multiple people to sign via a shared link") ?></a>
<a href="<?php echo $REVERSE_PROXY_URL; ?>/signature" class="list-group-item list-group-item-action"><i class="bi bi-patch-check"></i> <?php echo _("Sign with the server certificate") ?></a>
<a href="<?php echo $REVERSE_PROXY_URL; ?>/signature" class="list-group-item list-group-item-action"><i class="bi bi-droplet-half"></i> <?php echo _("Add a watermark") ?></a>
<a href="<?php echo $REVERSE_PROXY_URL; ?>/signature" class="list-group-item list-group-item-action"><i class="bi bi-bounding-box"></i> <?php echo _("Redact areas") ?></a>
</div>
</div>
</div>

View file

@ -2,6 +2,7 @@
<html lang="<?php echo $TRANSLATION_LANGUAGE ?>" dir="<?php echo $DIRECTION_LANGUAGE ?>" style="direction: <?php echo $DIRECTION_LANGUAGE ?>;">
<head>
<?php include('components/header.html.php'); ?>
<link href="<?php echo $REVERSE_PROXY_URL; ?>/css/pdf_viewer.css" rel="stylesheet">
<title>Signature PDF - Éditer les métadonnées</title>
<meta name="description" content="Logiciel libre en ligne qui permet d'jouter, modifier ou supprimer les métadonnées d'un PDF">
</head>

View file

@ -83,14 +83,17 @@
<input type="radio" class="btn-check" id="radio_svg_check" data-height="18" name="svg_2_add" autocomplete="off" value="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktY2hlY2stbGciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZD0iTTEyLjczNiAzLjk3YS43MzMuNzMzIDAgMCAxIDEuMDQ3IDBjLjI4Ni4yODkuMjkuNzU2LjAxIDEuMDVMNy44OCAxMi4wMWEuNzMzLjczMyAwIDAgMS0xLjA2NS4wMkwzLjIxNyA4LjM4NGEuNzU3Ljc1NyAwIDAgMSAwLTEuMDYuNzMzLjczMyAwIDAgMSAxLjA0NyAwbDMuMDUyIDMuMDkzIDUuNC02LjQyNWEuMjQ3LjI0NyAwIDAgMSAuMDItLjAyMloiLz4KPC9zdmc+Cg==">
<label draggable="true" id="label_svg_check" class="btn btn-outline-secondary text-black text-start btn-svg" for="radio_svg_check"><?php echo sprintf(_("%s Check box"), '<i class="bi bi-check-square"></i>'); ?></label>
</div>
<?php if (! isset($hash)): ?>
<div class="d-grid gap-2 mb-2 list-item-add">
<input type="radio" class="btn-check" id="radio_svg_rectangle" name="svg_2_add" autocomplete="off" value="rectangle" data-svg="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktYm91bmRpbmctYm94IiB2aWV3Qm94PSIwIDAgMTYgMTYiPg0KICA8cGF0aCBkPSJNNSAyVjBIMHY1aDJ2NkgwdjVoNXYtMmg2djJoNXYtNWgtMlY1aDJWMGgtNXYyem02IDF2MmgydjZoLTJ2Mkg1di0ySDNWNWgyVjN6bTEtMmgzdjNoLTN6bTMgMTF2M2gtM3YtM3pNNCAxNUgxdi0zaDN6TTEgNFYxaDN2M3oiLz4NCjwvc3ZnPg==">
<label draggable="true" id="label_svg_rectangle" class="btn btn-outline-secondary text-black text-start btn-svg" for="radio_svg_rectangle"><i class="bi bi-bounding-box"></i> <?php echo _("Redaction area"); ?></label>
</div>
<div class="input-watermark-placeholder btn btn-outline-secondary w-100 text-start text-dark">
<span class="bi bi-droplet-half" id="watermark-addon"></span> <?php echo _("Watermark") ?>
</div>
<div class="input-group d-none">
<span class="input-group-text border-secondary"><i class="bi bi-droplet-half"></i></span>
<input form="form_pdf" type="text" class="form-control border-secondary" name="watermark" placeholder="<?php echo _("Watermark") ?>" aria-label="Watermark" aria-describedby="watermark-addon" maxlength="30">
<input form="form_pdf" type="text" class="form-control border-secondary" name="watermark" placeholder="<?php echo _("Watermark") ?>" aria-label="Watermark" aria-describedby="watermark-addon">
</div>
<?php endif ?>
@ -107,8 +110,9 @@
<form id="form_pdf" action="<?php echo $REVERSE_PROXY_URL; ?>/sign" method="post" enctype="multipart/form-data" class="d-none d-sm-none d-md-block">
<input id="input_pdf" name="pdf" type="file" class="d-none" />
<input id="input_svg" name="svg[]" type="file" class="d-none" />
<button class="btn btn-primary w-100 mt-2" disabled="disabled" type="submit" id="save"><i class="bi bi-download"></i> <?php echo _("Download the signed PDF"); ?></button>
</form>
<input name="flatten" type="checkbox" value="1" class="d-none" />
<button class="btn btn-primary w-100 mt-2 w-100" disabled="disabled" type="submit" id="save"><i class="bi bi-download"></i> <?php echo _("Download the signed PDF"); ?> <i id="save_flatten_indicator" class="bi bi-layers-half opacity-50 float-end invisible" title="<?php echo _("The PDF will be flattened") ?>"></i></button>
</form>
<?php elseif(!isset($noSharingMode)): ?>
<div class="d-none d-sm-none d-md-block position-relative">
<a id="btn-signature-help" class="position-absolute top-0 end-0 text-dark" href="" style="z-index: 5;"><i class="bi bi-question-circle"></i></a>
@ -181,8 +185,9 @@
</div>
<input id="input-svg-type" type="hidden" />
</div>
<div class="modal-footer d-block">
<div class="modal-footer justify-content-between">
<button tabindex="-1" type="button" class="btn btn-light col-4" data-bs-dismiss="modal"><?php echo _("Cancel"); ?></button>
<input type="color" id="penColorPicker" class="form-control form-control-color">
<button id="btn_modal_ajouter" type="button" disabled="disabled" data-bs-dismiss="modal" class="btn btn-primary float-end col-4"><span id="btn_modal_ajouter_spinner" class="spinner-border spinner-border-sm d-none"></span><span id="btn_modal_ajouter_check" class="bi bi-check-circle"></span> <?php echo _("Create"); ?></button>
</div>
</div>