mirror of
https://github.com/24eme/signaturepdf.git
synced 2023-08-25 09:33:08 +02:00
Merge branch 'metadata'
This commit is contained in:
commit
c89c8d53dd
6
app.php
6
app.php
@ -353,6 +353,12 @@ $f3->route('POST /organize',
|
||||
);
|
||||
}
|
||||
|
||||
$f3->route('GET /metadata',
|
||||
function($f3) {
|
||||
echo View::instance()->render('metadata.html.php');
|
||||
}
|
||||
);
|
||||
|
||||
function convertPHPSizeToBytes($sSize)
|
||||
{
|
||||
$sSuffix = strtoupper(substr($sSize, -1));
|
||||
|
@ -9,3 +9,6 @@ PDF_STORAGE_PATH=/path/to/folder
|
||||
|
||||
; Manage demo link pdf : true (by default, show), false (hide), or custom link
|
||||
;PDF_DEMO_LINK=true
|
||||
|
||||
; Metadata default fields
|
||||
;METADATA_DEFAULT_FIELDS[metadata_key].type = "text"
|
||||
|
@ -63,6 +63,20 @@
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.delete-metadata {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
font-size: 1.2rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.input-metadata:hover > .delete-metadata {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.subtitle {
|
||||
font-size: .875em
|
||||
|
292
public/js/metadata.js
Normal file
292
public/js/metadata.js
Normal file
@ -0,0 +1,292 @@
|
||||
var windowWidth = window.innerWidth;
|
||||
var menu = null;
|
||||
var menuOffcanvas = null;
|
||||
var is_mobile = function() {
|
||||
return !(window.getComputedStyle(document.getElementById('is_mobile')).display === "none");
|
||||
};
|
||||
|
||||
var responsiveDisplay = function() {
|
||||
if(is_mobile()) {
|
||||
menu.classList.remove('show');
|
||||
menuOffcanvas.hide();
|
||||
} else {
|
||||
menuOffcanvas.show();
|
||||
}
|
||||
menu.classList.remove('d-md-block');
|
||||
menu.classList.remove('d-none');
|
||||
};
|
||||
|
||||
var pdfjsLib = window['pdfjs-dist/build/pdf'];
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = '/vendor/pdf.worker.js?legacy';
|
||||
var nbPDF = 0;
|
||||
var pages = [];
|
||||
var pdfRenderTasks = [];
|
||||
let filename = null
|
||||
let pdffile = null
|
||||
let deletedMetadata = [];
|
||||
|
||||
var loadPDF = async function(pdfBlob, filename, pdfIndex) {
|
||||
let url = await URL.createObjectURL(pdfBlob);
|
||||
|
||||
pdffile = pdfBlob
|
||||
|
||||
let loadingTask = pdfjsLib.getDocument(url);
|
||||
document.querySelector('#text_document_name span').innerText = filename;
|
||||
await loadingTask.promise.then(function(pdf) {
|
||||
pdf.getMetadata().then(function(metadata) {
|
||||
console.log(metadata);
|
||||
for(fieldKey in defaultFields) {
|
||||
addMetadata(fieldKey, null, defaultFields[fieldKey]['type'], false);
|
||||
}
|
||||
|
||||
for(metaKey in metadata.info) {
|
||||
if(metaKey == "Custom" || metaKey == "PDFFormatVersion" || metaKey.match(/^Is/) || metaKey == "Trapped") {
|
||||
continue;
|
||||
}
|
||||
addMetadata(metaKey, metadata.info[metaKey], "text", false);
|
||||
}
|
||||
|
||||
for(metaKey in metadata.info.Custom) {
|
||||
if(metaKey == "sha256") {
|
||||
continue;
|
||||
}
|
||||
|
||||
addMetadata(metaKey, metadata.info.Custom[metaKey], "text", false);
|
||||
}
|
||||
|
||||
for(let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++ ) {
|
||||
pdf.getPage(pageNumber).then(function(page) {
|
||||
let pageIndex = (page.pageNumber - 1);
|
||||
pages[pageIndex] = page;
|
||||
pageRender(pageIndex);
|
||||
});
|
||||
}
|
||||
if(document.querySelector('.input-metadata input')) {
|
||||
document.querySelector('.input-metadata input').focus();
|
||||
} else {
|
||||
document.getElementById('input_metadata_key').focus();
|
||||
}
|
||||
});
|
||||
}, function (reason) {
|
||||
console.error(reason);
|
||||
});
|
||||
|
||||
return loadingTask;
|
||||
}
|
||||
|
||||
var pageRender = async function(pageIndex) {
|
||||
|
||||
let page = pages[pageIndex];
|
||||
|
||||
let viewport = page.getViewport({scale: 1});
|
||||
let sizeWidth = document.getElementById('container-pages').offsetWidth;
|
||||
let scaleWidth = sizeWidth / viewport.width;
|
||||
let viewportWidth = page.getViewport({scale: scaleWidth });
|
||||
|
||||
viewport = viewportWidth;
|
||||
|
||||
let canvasPDF = document.createElement('canvas');
|
||||
canvasPDF.classList.add('shadow-sm');
|
||||
document.getElementById('container-pages').appendChild(canvasPDF);
|
||||
let context = canvasPDF.getContext('2d');
|
||||
canvasPDF.height = viewport.height;
|
||||
canvasPDF.width = viewport.width;
|
||||
|
||||
if(pdfRenderTasks[pageIndex]) {
|
||||
pdfRenderTasks[pageIndex].cancel();
|
||||
}
|
||||
pdfRenderTasks[pageIndex] = await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
});
|
||||
}
|
||||
|
||||
var addMetadata = function(key, value, type, focus) {
|
||||
let input = document.querySelector('.input-metadata input[name="'+key+'"]');
|
||||
|
||||
if(input && input.value === null) {
|
||||
input.value = value;
|
||||
}
|
||||
if(input && focus) {
|
||||
input.focus();
|
||||
}
|
||||
if(input) {
|
||||
return;
|
||||
}
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.classList.add('form-floating', 'mt-3', 'input-metadata');
|
||||
|
||||
input = document.createElement('input');
|
||||
input.value = value;
|
||||
input.type = type;
|
||||
input.name = key;
|
||||
input.classList.add('form-control');
|
||||
|
||||
let label = document.createElement('label');
|
||||
label.innerText = key;
|
||||
|
||||
let deleteButton = document.createElement('div')
|
||||
deleteButton.innerHTML = "×"
|
||||
deleteButton.classList.add('delete-metadata')
|
||||
|
||||
div.appendChild(input);
|
||||
div.appendChild(label);
|
||||
div.appendChild(deleteButton);
|
||||
document.getElementById('form-metadata-container').appendChild(div);
|
||||
|
||||
if(focus) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMetadata = function(el) {
|
||||
if (confirm("Souhaitez-vous supprimer ce champ ?") === false) return;
|
||||
|
||||
const input = el.closest('.input-metadata')
|
||||
const label = input.querySelector('label').innerText
|
||||
deletedMetadata.push(label)
|
||||
input.remove()
|
||||
}
|
||||
|
||||
const DL = function (d,f) {
|
||||
let a = document.createElement("a"),
|
||||
u = URL.createObjectURL(d);
|
||||
a.download = f,
|
||||
a.href = u,
|
||||
a.click(),
|
||||
setTimeout(() => URL.revokeObjectURL(u))
|
||||
}
|
||||
|
||||
const save = async function () {
|
||||
const PDFDocument = window['PDFLib'].PDFDocument
|
||||
const PDFHexString = window['PDFLib'].PDFHexString
|
||||
const PDFName = window['PDFLib'].PDFName
|
||||
|
||||
const arrayBuffer = await pdffile.arrayBuffer();
|
||||
const pdf = await PDFDocument.load(arrayBuffer);
|
||||
|
||||
deletedMetadata.forEach(function (el) {
|
||||
pdf.getInfoDict().delete(PDFName.of(el))
|
||||
});
|
||||
|
||||
([...document.getElementsByClassName('input-metadata')] || []).forEach(function (el) {
|
||||
const label = el.querySelector('label').innerText
|
||||
const input = el.querySelector('input').value
|
||||
|
||||
pdf.getInfoDict().set(PDFName.of(label), PDFHexString.fromText(input));
|
||||
});
|
||||
|
||||
const newPDF = new Blob([await pdf.save()], {type: "application/pdf"});
|
||||
DL(newPDF, filename)
|
||||
}
|
||||
|
||||
var createEventsListener = function() {
|
||||
document.getElementById('form_metadata_add').addEventListener('submit', function(e) {
|
||||
let formData = new FormData(this);
|
||||
addMetadata(formData.get('metadata_key'), "", "text", true);
|
||||
this.classList.add('invisible');
|
||||
setTimeout(function() { document.getElementById('form_metadata_add').classList.remove('invisible'); }, 400);
|
||||
this.reset();
|
||||
e.preventDefault();
|
||||
});
|
||||
document.getElementById('input_metadata_value').addEventListener('focus', function(e) {
|
||||
if(document.getElementById('input_metadata_key').value) {
|
||||
document.querySelector('#form_metadata_add button').click();
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', function (event) {
|
||||
if (event.target.closest(".delete-metadata")) {
|
||||
deleteMetadata(event.target)
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('save').addEventListener('click', function (e) {
|
||||
save()
|
||||
})
|
||||
}
|
||||
|
||||
async function getPDFBlobFromCache(cacheUrl) {
|
||||
const cache = await caches.open('pdf');
|
||||
let responsePdf = await cache.match(cacheUrl);
|
||||
|
||||
if(!responsePdf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pdfBlob = await responsePdf.blob();
|
||||
|
||||
return pdfBlob;
|
||||
}
|
||||
|
||||
async function uploadFromUrl(url) {
|
||||
history.replaceState({}, '', '/metadata');
|
||||
var response = await fetch(url);
|
||||
if(response.status != 200) {
|
||||
return;
|
||||
}
|
||||
var pdfBlob = await response.blob();
|
||||
|
||||
if(pdfBlob.type != 'application/pdf' && pdfBlob.type != 'application/octet-stream') {
|
||||
return;
|
||||
}
|
||||
let dataTransfer = new DataTransfer();
|
||||
let filename = url.replace(/^.*\//, '');
|
||||
dataTransfer.items.add(new File([pdfBlob], filename, {
|
||||
type: 'application/pdf'
|
||||
}));
|
||||
document.getElementById('input_pdf_upload').files = dataTransfer.files;
|
||||
document.getElementById('input_pdf_upload').dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
var pageUpload = async function() {
|
||||
document.querySelector('body').classList.remove('bg-light');
|
||||
document.getElementById('input_pdf_upload').value = '';
|
||||
document.getElementById('page-upload').classList.remove('d-none');
|
||||
document.getElementById('page-metadata').classList.add('d-none');
|
||||
document.getElementById('input_pdf_upload').focus();
|
||||
const cache = await caches.open('pdf');
|
||||
document.getElementById('input_pdf_upload').addEventListener('change', async function(event) {
|
||||
let filename = document.getElementById('input_pdf_upload').files[0].name;
|
||||
let response = new Response(document.getElementById('input_pdf_upload').files[0], { "status" : 200, "statusText" : "OK" });
|
||||
let urlPdf = '/pdf/'+filename;
|
||||
await cache.put(urlPdf, response);
|
||||
history.pushState({}, '', '/metadata#'+filename);
|
||||
pageMetadata(urlPdf)
|
||||
});
|
||||
}
|
||||
|
||||
var pageMetadata = async function(url) {
|
||||
filename = url.replace('/pdf/', '');
|
||||
document.title = filename + ' - ' + document.title;
|
||||
document.querySelector('body').classList.add('bg-light');
|
||||
document.getElementById('page-upload').classList.add('d-none');
|
||||
document.getElementById('page-metadata').classList.remove('d-none');
|
||||
menu = document.getElementById('sidebarTools');
|
||||
menuOffcanvas = new bootstrap.Offcanvas(menu);
|
||||
responsiveDisplay();
|
||||
|
||||
let pdfBlob = await getPDFBlobFromCache(url);
|
||||
if(!pdfBlob) {
|
||||
document.location = '/metadata';
|
||||
return;
|
||||
}
|
||||
|
||||
createEventsListener();
|
||||
loadPDF(pdfBlob, filename, nbPDF);
|
||||
};
|
||||
|
||||
(function () {
|
||||
if(window.location.hash && window.location.hash.match(/^\#http/)) {
|
||||
let hashUrl = window.location.hash.replace(/^\#/, '');
|
||||
pageUpload();
|
||||
uploadFromUrl(hashUrl);
|
||||
} else if(window.location.hash) {
|
||||
pageMetadata('/pdf/'+window.location.hash.replace(/^\#/, ''));
|
||||
} else {
|
||||
pageUpload();
|
||||
}
|
||||
window.addEventListener('hashchange', function() {
|
||||
window.location.reload();
|
||||
})
|
||||
})();
|
16
public/vendor/pdf-lib.min.js
vendored
Normal file
16
public/vendor/pdf-lib.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
107
templates/metadata.html.php
Normal file
107
templates/metadata.html.php
Normal file
@ -0,0 +1,107 @@
|
||||
<!doctype html>
|
||||
<html lang="fr_FR">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/vendor/bootstrap.min.css?5.1.1" rel="stylesheet">
|
||||
<link href="/vendor/bootstrap-icons.css?1.5.0" rel="stylesheet">
|
||||
<link href="/css/app.css?202210080134" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon-organization.ico">
|
||||
|
||||
<title>Édition des métadonnées d'un PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="alert alert-danger text-center" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> Site non fonctionnel sans JavaScript activé
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="page-upload">
|
||||
<ul class="nav justify-content-center nav-tabs mt-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/signature"><i class="bi bi-vector-pen"></i> Signer</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/organization"><i class="bi bi-ui-checks-grid"></i> Organiser</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/metadata"><i class="bi bi-tags"></i> Metadonnées</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-4 py-4 text-center">
|
||||
<h1 class="display-5 fw-bold mb-0 mt-3"><i class="bi bi-tags"></i> Éditer les métadonnées</h1>
|
||||
<p class="fw-light mb-3 subtitle text-dark text-nowrap" style="overflow: hidden; text-overflow: ellipsis;">Ajouter, modifier ou supprimer les métadonnées d'un PDF</p>
|
||||
<div class="col-md-6 col-lg-5 col-xl-4 col-xxl-3 mx-auto">
|
||||
<div class="col-12">
|
||||
<label class="form-label mt-3" for="input_pdf_upload">Choisir un PDF <small class="opacity-75" style="cursor: help" title="Le PDF ne doit pas dépasser <?php echo round($maxSize / 1024 / 1024) ?> Mo"><i class="bi bi-info-circle"></i></small></label>
|
||||
<input id="input_pdf_upload" placeholder="Choisir un PDF" class="form-control form-control-lg" type="file" accept=".pdf,application/pdf" />
|
||||
<p class="mt-2 small fw-light text-dark"> </p>
|
||||
<?php if($PDF_DEMO_LINK): ?>
|
||||
<a class="btn btn-sm btn-link opacity-75" href="#<?php echo $PDF_DEMO_LINK ?>">Tester avec un PDF de démo</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="text-center text-muted mb-2 fixed-bottom opacity-75">
|
||||
<small>Logiciel libre <span class="d-none d-md-inline">sous license AGPL-3.0 </span>: <a href="https://github.com/24eme/signaturepdf">voir le code source</a></small>
|
||||
</footer>
|
||||
</div>
|
||||
<div id="page-metadata" class="d-none">
|
||||
<div id="div-margin-top" style="height: 88px;" class="d-md-none"></div>
|
||||
<div style="width: 60%; overflow: auto;" class="vh-100" id="container-main">
|
||||
<div id="form-metadata" class="mx-auto w-75 pt-3 pb-5">
|
||||
<h3>Liste des métadonnées du PDF</h3>
|
||||
<div id="form-metadata-container">
|
||||
</div>
|
||||
<form id="form_metadata_add" class="position-relative">
|
||||
<hr class="text-muted mt-4 mb-3" />
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted" for="input_metadata_key">Ajouter une nouvelle métadonnée</label>
|
||||
<div class="form-floating">
|
||||
<input id="input_metadata_key" name="metadata_key" type="text" class="form-control" required value="" style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;">
|
||||
<label>Clé</label>
|
||||
</div>
|
||||
<input id="input_metadata_value" readonly="readonly" style="border-top: 0; border-top-right-radius: 0; border-top-left-radius: 0;" name="metadata_value" type="text" class="form-control bg-light opacity-50" value="" placeholder="Valeur" style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;">
|
||||
</div>
|
||||
<button type="submit" type="button" class="btn btn-outline-secondary float-end"><i class="bi bi-plus-circle"></i> Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="div-margin-bottom" style="height: 55px;" class="d-md-none"></div>
|
||||
<div style="width: 40%;" 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">
|
||||
<a class="btn btn-close btn-sm position-absolute opacity-25 d-none d-sm-none d-md-block" title="Fermer ce PDF et retourner à l'accueil" style="position: absolute; top: 2px; right: 2px; font-size: 10px;" href="/metadata"></a>
|
||||
<div class="offcanvas-header d-block mb-0 pb-0 border-bottom">
|
||||
<h5 class="mb-1 d-block w-100" id="sidebarToolsLabel">Édition des métadonnées<span class="float-end me-2" title="Ce PDF est stocké sur votre ordinateur pour être signé par vous uniquement"><i class="bi bi-tags"></i></span></h5>
|
||||
<button type="button" class="btn-close text-reset d-md-none" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
<p id="text_document_name" class="text-muted mb-2" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden;" title=""><i class="bi bi-files"></i> <span></span></p>
|
||||
</div>
|
||||
<div class="offcanvas-body bg-light" style="padding-bottom: 60px;">
|
||||
<div id="container-pages">
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-absolute bg-white bottom-0 pb-2 ps-2 pe-2 w-100 border-top shadow-lg">
|
||||
<div id="btn_container" class="d-grid gap-2 mt-2">
|
||||
<button class="btn btn-primary" type="submit" id="save"><i class="bi bi-download"></i> Enregistrer et télécharger le PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bottom_bar" class="position-fixed bottom-0 start-0 bg-white w-100 p-2 shadow-sm d-md-none">
|
||||
<div id="bottom_bar_action" class="d-grid gap-2">
|
||||
<button class="btn btn-primary" id="save_mobile"><i class="bi bi-download"></i> Télécharger le PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="is_mobile" class="d-md-none"></span>
|
||||
|
||||
<script src="/vendor/bootstrap.min.js?5.1.3"></script>
|
||||
<script src="/vendor/pdf.js?legacy"></script>
|
||||
<script src="/vendor/pdf-lib.min.js?1.17.1"></script>
|
||||
<script>
|
||||
var defaultFields = <?php echo json_encode(isset($METADATA_DEFAULT_FIELDS) ? $METADATA_DEFAULT_FIELDS : array()); ?>;
|
||||
</script>
|
||||
<script src="/js/metadata.js?202212070154"></script>
|
||||
</body>
|
||||
</html>
|
@ -26,6 +26,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/organization"><i class="bi bi-ui-checks-grid"></i> Organiser</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/metadata"><i class="bi bi-tags"></i> Metadonnées</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-4 py-4 text-center">
|
||||
<h1 class="display-5 fw-bold mb-0 mt-3"><i class="bi bi-ui-checks-grid"></i> Organiser des PDF</h1>
|
||||
|
@ -25,6 +25,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/organization"><i class="bi bi-ui-checks-grid"></i> Organiser</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/metadata"><i class="bi bi-tags"></i> Metadonnées</a>
|
||||
</li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<div class="px-4 py-4 text-center">
|
||||
|
Loading…
Reference in New Issue
Block a user