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-07-03 08:01:49 +02:00
commit edd19703ae
No known key found for this signature in database
GPG key ID: A3FAAA06E6569B4C
5 changed files with 156 additions and 43 deletions

View file

@ -74,6 +74,10 @@
opacity: 1;
}
#toolbox i {
cursor: pointer;
}
.canvas-container .btn-drag, .canvas-container .btn-rotate, .canvas-container .btn-delete, .canvas-container .btn-select, .canvas-container .btn-download, .canvas-container .btn-restore, .canvas-container .btn-drag-here, .canvas-container .btn-drag-here_mobile, .canvas-container .btn-cancel {
font-size: 30px;
cursor: move;
@ -113,6 +117,10 @@
user-select: none;
}
.input-metadata > label {
width: 110%;
}
.input-metadata:hover > .delete-metadata {
display: block;
}

View file

@ -32,6 +32,7 @@ async function loadPDF(pdfBlob) {
const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise;
const metadata = await pdf.getMetadata()
const attachments = await pdf.getAttachments();
for(fieldKey in defaultFields) {
addMetadata(fieldKey, null, defaultFields[fieldKey]['type'], false);
@ -52,6 +53,40 @@ async function loadPDF(pdfBlob) {
addMetadata(metaKey, metadata.info.Custom[metaKey], "text", false);
}
if (attachments) {
Object.entries(attachments).forEach(([_key, value]) => {
if (value.filename.startsWith('factur-x') === false) {
return
}
const decodedAttachment = new TextDecoder().decode(value.content)
const parser = new DOMParser();
const xml = parser.parseFromString(decodedAttachment, "application/xml")
const error = xml.querySelector('parseerror')
if (error) {
console.log(error)
return
}
const walker = xml.createTreeWalker(xml.firstChild, NodeFilter.SHOW_TEXT)
while(walker.nextNode()) {
const node = walker.currentNode
const treeKey = []
treeKey.push(node.parentNode.localName)
let root = node.parentNode
while (! (root.parentNode instanceof XMLDocument)) {
root = root.parentNode
treeKey.push(root.localName) // nodeName si on veut le namespace
}
const newInput = addMetadata(treeKey.join(' « '), node.textContent.trim(), "text", false, true)
newInput.dataset.fromAttachment = value.filename
newInput.disabled = true
}
})
}
for(let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++ ) {
pdf.getPage(pageNumber).then(function(page) {
let pageIndex = (page.pageNumber - 1);
@ -130,17 +165,19 @@ async function pageRender(pageIndex) {
})
}
function addMetadata(key, value, type, focus) {
let input = document.querySelector('.input-metadata input[name="'+key+'"]');
function addMetadata(key, value, type, focus, forceCreation = false) {
if (! forceCreation) {
let input = document.querySelector('.input-metadata input[name="'+key+'"]');
if(input && !input.value) {
input.value = value;
}
if(input && focus) {
input.focus();
}
if(input) {
return;
if(input && !input.value) {
input.value = value;
}
if(input && focus) {
input.focus();
}
if(input) {
return input;
}
}
let div = document.createElement('div');
@ -168,6 +205,8 @@ function addMetadata(key, value, type, focus) {
if(focus) {
input.focus();
}
return input
}
function deleteMetadata(el) {
@ -204,6 +243,10 @@ async function save() {
const label = el.querySelector('label').innerText
const input = el.querySelector('input').value
if ('fromAttachment' in el.querySelector('input').dataset) {
return;
}
pdf.getInfoDict().set(PDFName.of(label), PDFHexString.fromText(input));
});

View file

@ -23,6 +23,7 @@ if(is_mobile()) {
let nbPDF = 0;
let pages = [];
let formats = [];
let pdfRenderTasks = [];
async function loadPDF(pdfBlob, filename, pdfIndex) {
@ -47,7 +48,11 @@ async function loadPDF(pdfBlob, filename, pdfIndex) {
pdf.getPage(pageNumber).then(function(page) {
let pageIndex = pdfLetter + "_" + (page.pageNumber - 1);
pages[pageIndex] = page;
const viewportFormat = page.getViewport({ scale: 1 });
const widthFormat = Math.round(viewportFormat.width * 25.4 / 72 * 10) / 10;
const heightFormat = Math.round(viewportFormat.height * 25.4 / 72 * 10) / 10;
formats[pageIndex] = widthFormat + " x " + heightFormat + " mm";
let pageTitle = trad['Page'] + ' ' + page.pageNumber + ' - '+formats[pageIndex]+' - ' + filename;
let pageHTML = '<div class="position-relative mt-0 ms-1 me-0 mb-1 canvas-container d-flex align-items-center justify-content-center bg-transparent bg-opacity-25 border border-2 border-transparent" id="canvas-container-' + pageIndex +'" draggable="true">';
pageHTML += '<canvas class="canvas-pdf shadow-sm"></canvas>';
pageHTML += '<div title="' + trad['Select this page'] + '" class="position-absolute top-0 start-50 translate-middle-x p-2 ps-3 pe-3 mt-2 rounded-circle btn-select d-none"><i class="bi bi-check-square"></i></div>';
@ -57,7 +62,7 @@ async function loadPDF(pdfBlob, filename, pdfIndex) {
pageHTML += '<div title="' + trad['Move here'] + '" class="position-absolute start-50 top-50 translate-middle p-2 ps-4 pe-4 container-resize btn-drag-here d-none"><i class="bi bi-arrows-expand-vertical"></i></div>';
pageHTML += '<div title="' + trad['Turn this page'] + '" class="position-absolute top-50 end-0 translate-middle-y p-2 ps-3 pe-3 me-2 rounded-circle container-rotate btn-rotate d-none"><i class="bi bi-arrow-clockwise"></i></div>';
pageHTML += '<div title="' + trad['Download this page'] + '" class="position-absolute bottom-0 start-50 translate-middle-x p-2 ps-3 pe-3 mb-3 rounded-circle btn-download d-none"><i class="bi bi-download"></i></div>';
pageHTML += '<p class="page-title position-absolute text-center w-100 ps-2 pe-2 pb-0 pt-0 mb-1 bg-white opacity-75 d-none" style="bottom: -4px; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">' + trad['Page'] + ' ' + page.pageNumber + ' - ' + filename + '</p>';
pageHTML += '<p class="page-title position-absolute text-center w-100 ps-2 pe-2 pb-0 pt-0 mb-1 bg-white opacity-75 d-none" style="bottom: -4px; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">' + pageTitle + '</p>';
pageHTML += '<input form="form_pdf" class="checkbox-page d-none" role="switch" type="checkbox" checked="checked" value="'+pdfLetter+page.pageNumber+'" />';
pageHTML += '<input type="hidden" class="input-rotate" value="0" id="input_rotate_'+pageIndex+'" />';
pageHTML += '<input type="checkbox" class="input-select d-none" value="'+pdfLetter+page.pageNumber+'" id="input_select_'+pageIndex+'" />';
@ -68,6 +73,7 @@ async function loadPDF(pdfBlob, filename, pdfIndex) {
document.getElementById('container-pages').insertAdjacentHTML('beforeend', pageHTML);
let canvasContainer = document.getElementById('canvas-container-' + pageIndex);
canvasContainer.title = pageTitle;
canvasContainer.addEventListener('click', function(e) {
canvasContainer.querySelector('.btn-select').click();
});
@ -214,6 +220,7 @@ async function pageRender(pageIndex) {
scrollWidth = -4;
}
let page = pages[pageIndex];
let rotation = parseInt(document.querySelector('#input_rotate_'+pageIndex).value);
let viewport = page.getViewport({scale: 1, rotation: rotation});
let sizeWidth = Math.floor((document.getElementById('container-pages').offsetWidth - (8*(nbPagePerLine+1)) - scrollWidth) / nbPagePerLine);

View file

@ -170,6 +170,20 @@ async function loadPDF(pdfBlob) {
toolBox.init(event.selected[0])
});
canvasEdition.on("selection:updated", function(event) {
toolBox.reset()
if (event.selected.length > 1 || event.selected.length === 0) {
return;
}
toolBox.init(event.selected[0])
});
canvasEdition.on("object:modified", function(event) {
toolBox.init(event.target)
});
canvasEdition.on("selection:cleared", function(event) {
toolBox.reset()
});
canvasEditions.push(canvasEdition);
});
}
@ -1248,30 +1262,40 @@ function storePenColor(color) {
}
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-droplet-fill" viewBox="0 0 16 16"><path d="M8 16a6 6 0 0 0 6-6c0-1.655-1.122-2.904-2.432-4.362C10.254 4.176 8.75 2.503 8 0c0 0-6 5.686-6 10a6 6 0 0 0 6 6M6.646 4.646l.708.708c-.29.29-1.128 1.311-1.907 2.87l-.894-.448c.82-1.641 1.717-2.753 2.093-3.13"/></svg>'
const _coloricon = document.createElement('i')
_coloricon.classList.add('bi', 'bi-droplet-fill', 'mx-1')
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();
}
}
const _trashicon = document.createElement('i')
_trashicon.classList.add('bi', 'bi-trash3', 'float-end', 'border-start', 'border-2', 'mx-1', 'ps-1')
function _changeColor(eventData, transform) {
const target = transform.target;
const _copyicon = document.createElement('i')
_copyicon.classList.add('bi', 'bi-copy', 'mx-1')
let _elSelected
const _elToolbox = document.createElement('div')
_elToolbox.id = 'toolbox'
_elToolbox.classList.add('position-absolute', 'border', 'p-1', 'bg-body-secondary', 'shadow-sm', 'ms-3', 'mt-3', 'd-none', 'd-md-block')
_elToolbox.style['z-index'] = 1030
_elToolbox.style.width = 'max-content'
_elToolbox.appendChild(_coloricon)
_elToolbox.appendChild(_trashicon)
_elToolbox.appendChild(_copyicon)
_coloricon.addEventListener('click', _changeColor)
_trashicon.addEventListener('click', _delete)
_copyicon.addEventListener('click', _copy)
function _changeColor() {
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") {
_elSelected.set({ fill: e.target.value })
_elSelected.canvas.requestRenderAll()
if(_elSelected.type != "rect") {
storePenColor(e.target.value)
}
})
@ -1280,22 +1304,53 @@ const toolBox = (function () {
_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
function _copy() {
canvasEditions.forEach(function (canvas) {
if (_elSelected.canvas === canvas) {
return
}
_elSelected.clone(function (clonedItem) {
addObjectInCanvas(canvas, clonedItem)
})
})
el.controls.color = colorControl
}
function _delete() {
deleteActiveObject()
reset()
}
function init(el) {
if (_elToolbox) {
reset()
}
_elSelected = el
const container = document.getElementById('container-pages')
container.appendChild(_elToolbox)
_elToolbox.style.left = (
_elSelected.getBoundingRect().left
+ _elSelected.getScaledWidth() / 2
- _elToolbox.offsetWidth / 2
+ +window.getComputedStyle(_elToolbox).getPropertyValue("margin-left").replace('px', '')
) + 'px'
_elToolbox.style.top = (
Math.max(..._elSelected.getCoords().map((c) => c.y)) // on sélectionne le coin le plus bas
+ _elSelected.canvas._offset.top // hauteur du canvas dans le viewport
+ container.scrollTop // haut du container
) + 'px'
}
function reset() {
_elSelected = null
_elToolbox.remove()
}
return {
init: init
init: init,
reset: reset
}
})()

View file

@ -41,7 +41,7 @@
</div>
<?php endif; ?>
<div style="height: 65px;" class="d-md-none"></div>
<div id="container-pages" class="col-12 pt-1 pb-1 text-center vh-100" dir="auto">
<div id="container-pages" class="col-12 pt-1 pb-1 text-center vh-100 position-relative" dir="auto">
</div>
<div style="height: 55px;" class="d-md-none"></div>
<div class="offcanvas offcanvas-end show d-none d-md-block shadow-sm" data-bs-backdrop="false" data-bs-scroll="true" data-bs-keyboard="false" tabindex="-1" id="sidebarTools" aria-labelledby="sidebarToolsLabel">