flatten pdf
This commit is contained in:
parent
a342def43f
commit
acda1e4dd8
4 changed files with 243 additions and 97 deletions
47
src/main/resources/static/js/local-pdf-input-download.js
Normal file
47
src/main/resources/static/js/local-pdf-input-download.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
async function downloadFilesWithCallback(processFileCallback) {
|
||||||
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
|
const files = fileInput.files;
|
||||||
|
|
||||||
|
const zipThreshold = 4;
|
||||||
|
const zipFiles = files.length > zipThreshold;
|
||||||
|
|
||||||
|
let jszip = null;
|
||||||
|
if (zipFiles) {
|
||||||
|
jszip = new JSZip();
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = Array.from(files).map(async file => {
|
||||||
|
const { processedData, fileName } = await processFileCallback(file);
|
||||||
|
|
||||||
|
if (zipFiles) {
|
||||||
|
jszip.file(fileName, processedData);
|
||||||
|
} else {
|
||||||
|
const url = URL.createObjectURL(processedData);
|
||||||
|
const downloadOption = localStorage.getItem('downloadOption');
|
||||||
|
|
||||||
|
if (downloadOption === 'sameWindow') {
|
||||||
|
window.location.href = url;
|
||||||
|
} else if (downloadOption === 'newWindow') {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.href = url;
|
||||||
|
downloadLink.download = fileName;
|
||||||
|
downloadLink.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
if (zipFiles) {
|
||||||
|
const content = await jszip.generateAsync({ type: "blob" });
|
||||||
|
const url = URL.createObjectURL(content);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = "files.zip";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
}
|
|
@ -205,7 +205,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
</dialog>
|
</dialog>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}">
|
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: 'true'">
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('form').submit(async function(event) {
|
$('form').submit(async function(event) {
|
||||||
|
@ -227,102 +227,108 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
var multiple = [[${multiple}]] || false;
|
var multiple = [[${multiple}]] || false;
|
||||||
var override = $('#override').val() || '';
|
var override = $('#override').val() || '';
|
||||||
console.log("override=" + override)
|
console.log("override=" + override)
|
||||||
if (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) {
|
|
||||||
console.log("multi parallel download")
|
|
||||||
await submitMultiPdfForm(event,url);
|
|
||||||
} else {
|
|
||||||
console.log("start single download")
|
|
||||||
|
|
||||||
// Get the selected download option from localStorage
|
if([[${remoteCall}]] === true) {
|
||||||
const downloadOption = localStorage.getItem('downloadOption');
|
if (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) {
|
||||||
|
console.log("multi parallel download")
|
||||||
|
await submitMultiPdfForm(event,url);
|
||||||
|
} else {
|
||||||
|
console.log("start single download")
|
||||||
|
|
||||||
var formData = new FormData($('form')[0]);
|
// Get the selected download option from localStorage
|
||||||
|
const downloadOption = localStorage.getItem('downloadOption');
|
||||||
|
|
||||||
// Send the request to the server using the fetch() API
|
var formData = new FormData($('form')[0]);
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
// Send the request to the server using the fetch() API
|
||||||
body: formData
|
const response = await fetch(url, {
|
||||||
});
|
method: 'POST',
|
||||||
try {
|
body: formData
|
||||||
if (!response) {
|
});
|
||||||
throw new Error('Received null response for file ' + i);
|
try {
|
||||||
}
|
if (!response) {
|
||||||
console.log("load single download")
|
throw new Error('Received null response for file ' + i);
|
||||||
|
}
|
||||||
|
console.log("load single download")
|
||||||
|
|
||||||
|
|
||||||
// Extract the filename from the Content-Disposition header, if present
|
// Extract the filename from the Content-Disposition header, if present
|
||||||
let filename = null;
|
let filename = null;
|
||||||
const contentDispositionHeader = response.headers.get('Content-Disposition');
|
const contentDispositionHeader = response.headers.get('Content-Disposition');
|
||||||
console.log(contentDispositionHeader)
|
console.log(contentDispositionHeader)
|
||||||
if (contentDispositionHeader && contentDispositionHeader.indexOf('attachment') !== -1) {
|
if (contentDispositionHeader && contentDispositionHeader.indexOf('attachment') !== -1) {
|
||||||
filename = decodeURIComponent(contentDispositionHeader.split('filename=')[1].replace(/"/g, ''));
|
filename = decodeURIComponent(contentDispositionHeader.split('filename=')[1].replace(/"/g, ''));
|
||||||
} else {
|
|
||||||
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
|
|
||||||
filename = 'download';
|
|
||||||
}
|
|
||||||
console.log("filename=" + filename)
|
|
||||||
|
|
||||||
|
|
||||||
const contentType = response.headers.get('Content-Type');
|
|
||||||
console.log("contentType=" + contentType)
|
|
||||||
// Check if the response is a PDF or an image
|
|
||||||
if (contentType.includes('pdf') || contentType.includes('image')) {
|
|
||||||
const blob = await response.blob();
|
|
||||||
console.log("pdf/image")
|
|
||||||
|
|
||||||
// Perform the appropriate action based on the download option
|
|
||||||
if (downloadOption === 'sameWindow') {
|
|
||||||
console.log("same window")
|
|
||||||
|
|
||||||
// Open the file in the same window
|
|
||||||
window.location.href = URL.createObjectURL(blob);
|
|
||||||
} else if (downloadOption === 'newWindow') {
|
|
||||||
console.log("new window")
|
|
||||||
|
|
||||||
// Open the file in a new window
|
|
||||||
window.open(URL.createObjectURL(blob), '_blank');
|
|
||||||
} else {
|
} else {
|
||||||
console.log("else save")
|
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
|
||||||
|
filename = 'download';
|
||||||
|
}
|
||||||
|
console.log("filename=" + filename)
|
||||||
|
|
||||||
// Download the file
|
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
console.log("contentType=" + contentType)
|
||||||
|
// Check if the response is a PDF or an image
|
||||||
|
if (contentType.includes('pdf') || contentType.includes('image')) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
console.log("pdf/image")
|
||||||
|
|
||||||
|
// Perform the appropriate action based on the download option
|
||||||
|
if (downloadOption === 'sameWindow') {
|
||||||
|
console.log("same window")
|
||||||
|
|
||||||
|
// Open the file in the same window
|
||||||
|
window.location.href = URL.createObjectURL(blob);
|
||||||
|
} else if (downloadOption === 'newWindow') {
|
||||||
|
console.log("new window")
|
||||||
|
|
||||||
|
// Open the file in a new window
|
||||||
|
window.open(URL.createObjectURL(blob), '_blank');
|
||||||
|
} else {
|
||||||
|
console.log("else save")
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
} else if (contentType.includes('json')) {
|
||||||
|
// Handle the JSON response
|
||||||
|
const json = await response.json();
|
||||||
|
// Format the error message
|
||||||
|
const errorMessage = JSON.stringify(json, null, 2);
|
||||||
|
// Display the error message in an alert
|
||||||
|
alert(`An error occurred: ${errorMessage}`);
|
||||||
|
} else {
|
||||||
|
const blob = await response.blob()
|
||||||
|
console.log("else save 2 zip")
|
||||||
|
|
||||||
|
// For ZIP files or other file types, just download the file
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(blob);
|
link.href = URL.createObjectURL(blob);
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
} else if (contentType.includes('json')) {
|
} catch(error) {
|
||||||
// Handle the JSON response
|
console.log("error listener")
|
||||||
const json = await response.json();
|
|
||||||
// Format the error message
|
|
||||||
const errorMessage = JSON.stringify(json, null, 2);
|
|
||||||
// Display the error message in an alert
|
|
||||||
alert(`An error occurred: ${errorMessage}`);
|
|
||||||
} else {
|
|
||||||
const blob = await response.blob()
|
|
||||||
console.log("else save 2 zip")
|
|
||||||
|
|
||||||
// For ZIP files or other file types, just download the file
|
// Extract the error message and stack trace from the response
|
||||||
const link = document.createElement('a');
|
const errorMessage = error.message;
|
||||||
link.href = URL.createObjectURL(blob);
|
const stackTrace = error.stack;
|
||||||
link.download = filename;
|
|
||||||
link.click();
|
|
||||||
}
|
|
||||||
} catch(error) {
|
|
||||||
console.log("error listener")
|
|
||||||
|
|
||||||
// Extract the error message and stack trace from the response
|
// Create an error message to display to the user
|
||||||
const errorMessage = error.message;
|
const message = `${errorMessage}\n\n${stackTrace}`;
|
||||||
const stackTrace = error.stack;
|
|
||||||
|
|
||||||
// Create an error message to display to the user
|
$('#submitBtn').text(submitButtonText);
|
||||||
const message = `${errorMessage}\n\n${stackTrace}`;
|
|
||||||
|
|
||||||
$('#submitBtn').text(submitButtonText);
|
// Display the error message to the user
|
||||||
|
alert(message);
|
||||||
|
|
||||||
// Display the error message to the user
|
};
|
||||||
alert(message);
|
|
||||||
|
|
||||||
};
|
}
|
||||||
|
} else {
|
||||||
|
//Offline do nothing and let other scripts handle everything
|
||||||
|
|
||||||
}
|
}
|
||||||
$('#submitBtn').text(submitButtonText);
|
$('#submitBtn').text(submitButtonText);
|
||||||
|
@ -467,7 +473,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
<div class="custom-file-chooser">
|
<div class="custom-file-chooser">
|
||||||
<div class="custom-file">
|
<div class="custom-file">
|
||||||
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple>
|
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple required>
|
||||||
<label class="custom-file-label" th:for="${name}+'-input'" th:text="${inputText}"></label>
|
<label class="custom-file-label" th:for="${name}+'-input'" th:text="${inputText}"></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="selected-files"></div>
|
<div class="selected-files"></div>
|
||||||
|
|
57
src/main/resources/templates/other/flatten.html
Normal file
57
src/main/resources/templates/other/flatten.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{flatten.title})}"></th:block>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||||
|
<br> <br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2 th:text="#{flatten.header}"></h2>
|
||||||
|
<form id="pdfForm" class="mb-3">
|
||||||
|
<div class="custom-file">
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
|
||||||
|
<script src="js/local-pdf-input-download.js"></script>
|
||||||
|
<script>
|
||||||
|
document.getElementById('pdfForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { PDFDocument } = PDFLib;
|
||||||
|
|
||||||
|
const processFile = async (file) => {
|
||||||
|
const origFileUrl = URL.createObjectURL(file);
|
||||||
|
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
|
||||||
|
const pdfDoc = await PDFDocument.load(formPdfBytes);
|
||||||
|
|
||||||
|
const form = pdfDoc.getForm();
|
||||||
|
form.flatten();
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||||
|
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_flattened.pdf';
|
||||||
|
|
||||||
|
return { processedData: pdfBlob, fileName };
|
||||||
|
};
|
||||||
|
|
||||||
|
await downloadFilesWithCallback(processFile);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -60,8 +60,11 @@
|
||||||
<div class = "btn-group">
|
<div class = "btn-group">
|
||||||
<input type="radio" class="btn-check" name="signature-type" id="draw-signature" autocomplete="off" checked>
|
<input type="radio" class="btn-check" name="signature-type" id="draw-signature" autocomplete="off" checked>
|
||||||
<label class="btn btn-outline-secondary" for="draw-signature">Draw signature</label>
|
<label class="btn btn-outline-secondary" for="draw-signature">Draw signature</label>
|
||||||
|
<input type="radio" class="btn-check" name="signature-type" id="generate-signature" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-secondary" for="generate-signature">Generate Signature</label>
|
||||||
<input type="radio" class="btn-check" name="signature-type" id="import-image" autocomplete="off">
|
<input type="radio" class="btn-check" name="signature-type" id="import-image" autocomplete="off">
|
||||||
<label class="btn btn-outline-secondary" for="import-image">Import image</label>
|
<label class="btn btn-outline-secondary" for="import-image">Import image</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='signature-upload', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div>
|
<div th:replace="~{fragments/common :: fileSelector(name='signature-upload', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div>
|
||||||
|
@ -72,6 +75,10 @@
|
||||||
<button id="clear-signature" class="btn btn-outline-danger mt-2">Clear</button>
|
<button id="clear-signature" class="btn btn-outline-danger mt-2">Clear</button>
|
||||||
<button id="save-signature" class="btn btn-outline-success mt-2">Save</button>
|
<button id="save-signature" class="btn btn-outline-success mt-2">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="signature-text-input-container" style="display: none;">
|
||||||
|
<input type="text" id="signature-text-input" class="form-control mt-2" placeholder="Type your signature here...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="pdf-container">
|
<div id="pdf-container">
|
||||||
<canvas id="pdf-canvas"></canvas>
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
@ -89,6 +96,7 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const pdfUpload = document.querySelector('input[name=pdf-upload]');
|
const pdfUpload = document.querySelector('input[name=pdf-upload]');
|
||||||
const signatureUpload = document.querySelector('input[name=signature-upload]');
|
const signatureUpload = document.querySelector('input[name=signature-upload]');
|
||||||
|
const signatureTextInput = document.querySelector('input[name=signatureTextInput]');
|
||||||
const pdfCanvas = document.getElementById('pdf-canvas');
|
const pdfCanvas = document.getElementById('pdf-canvas');
|
||||||
const signatureCanvas = document.getElementById('signature-canvas');
|
const signatureCanvas = document.getElementById('signature-canvas');
|
||||||
const downloadPdfBtn = document.getElementById('download-pdf');
|
const downloadPdfBtn = document.getElementById('download-pdf');
|
||||||
|
@ -124,18 +132,45 @@
|
||||||
signaturePad.clear();
|
signaturePad.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("input[name=signature-type]").change(function() {
|
$("input[name=signature-type]").change(function () {
|
||||||
const drawSignatureInput = document.getElementById('draw-signature');
|
const drawSignatureInput = document.getElementById("draw-signature");
|
||||||
signaturePadContainer.style.display = drawSignatureInput.checked ? 'block' : 'none';
|
const generateSignatureInput = document.getElementById("generate-signature");
|
||||||
document.querySelector('input[name=signature-upload]').closest(".custom-file-chooser").style.display = drawSignatureInput.checked ? 'none' : 'block';
|
const importImageInputContainer = document.querySelector("input[name=signature-upload]").closest(".custom-file-chooser");
|
||||||
|
signaturePadContainer.style.display = drawSignatureInput.checked ? "block" : "none";
|
||||||
|
importImageInputContainer.style.display = drawSignatureInput.checked ? "none" : (generateSignatureInput.checked ? "none" : "block");
|
||||||
|
document.getElementById("signature-text-input-container").style.display = generateSignatureInput.checked ? "block" : "none";
|
||||||
|
|
||||||
if (drawSignatureInput.checked) {
|
if (drawSignatureInput.checked) {
|
||||||
populateSignatureFromPad();
|
populateSignatureFromPad();
|
||||||
|
} else if (generateSignatureInput.checked) {
|
||||||
|
populateSignatureFromText();
|
||||||
} else {
|
} else {
|
||||||
populateSignatureFromFileUpload();
|
populateSignatureFromFileUpload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function populateSignatureFromText() {
|
||||||
|
const signatureText = document.getElementById("signature-text-input").value;
|
||||||
|
if (!signatureText) return;
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.font = "32px Arial";
|
||||||
|
const textMetrics = ctx.measureText(signatureText);
|
||||||
|
|
||||||
|
canvas.width = textMetrics.width;
|
||||||
|
canvas.height = parseInt(ctx.font, 10);
|
||||||
|
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.font = "32px Arial";
|
||||||
|
ctx.fillText(signatureText, 0, canvas.height * 0.8);
|
||||||
|
|
||||||
|
const dataURL = canvas.toDataURL();
|
||||||
|
populateSignature(dataURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function populateSignature(imgUrl) {
|
function populateSignature(imgUrl) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
@ -184,6 +219,7 @@
|
||||||
populateSignature(dataURL);
|
populateSignature(dataURL);
|
||||||
}
|
}
|
||||||
signatureUpload.addEventListener('change', populateSignatureFromFileUpload);
|
signatureUpload.addEventListener('change', populateSignatureFromFileUpload);
|
||||||
|
signatureTextInput.addEventListener('change', populateSignatureFromText);
|
||||||
saveSignatureBtn.addEventListener('click', populateSignatureFromPad);
|
saveSignatureBtn.addEventListener('click', populateSignatureFromPad);
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue