* Fix: #746

* formatting
This commit is contained in:
Ludy 2024-02-18 08:40:30 +01:00 committed by GitHub
parent 673f005fe6
commit 51ad741744
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 265 additions and 283 deletions

View file

@ -218,7 +218,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections"); addEndpointToGroup("Java", "split-pdf-by-sections");
addEndpointToGroup("Java", "remove-blanks"); addEndpointToGroup("Java", "remove-blanks");
// Javascript // Javascript
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Javascript", "sign");

View file

@ -25,16 +25,15 @@ $(document).ready(function () {
const originalButtonText = $("#submitBtn").text(); const originalButtonText = $("#submitBtn").text();
$("#submitBtn").text("Processing..."); $("#submitBtn").text("Processing...");
console.log(override); console.log(override);
// Set a timeout to show the game button if operation takes more than 5 seconds // Set a timeout to show the game button if operation takes more than 5 seconds
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
var boredWaiting = localStorage.getItem("boredWaiting") || "disabled"; var boredWaiting = localStorage.getItem("boredWaiting") || "disabled";
const showGameBtn = document.getElementById('show-game-btn'); const showGameBtn = document.getElementById("show-game-btn");
if(boredWaiting === "enabled" && showGameBtn){ if (boredWaiting === "enabled" && showGameBtn) {
showGameBtn.style.display = 'block'; showGameBtn.style.display = "block";
} }
}, 5000); }, 5000);
try { try {
if (remoteCall === true) { if (remoteCall === true) {
@ -46,9 +45,8 @@ $(document).ready(function () {
} }
clearTimeout(timeoutId); clearTimeout(timeoutId);
$("#submitBtn").text(originalButtonText); $("#submitBtn").text(originalButtonText);
} catch (error) { } catch (error) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
handleDownloadError(error); handleDownloadError(error);
$("#submitBtn").text(originalButtonText); $("#submitBtn").text(originalButtonText);
console.error(error); console.error(error);

View file

@ -3323,13 +3323,13 @@
kind: OptionKind.WORKER kind: OptionKind.WORKER
}, },
workerSrc: { workerSrc: {
value: "/pdfjs/pdf.worker.js", value: "./pdfjs/pdf.worker.js",
kind: OptionKind.WORKER kind: OptionKind.WORKER
} }
}; };
{ {
defaultOptions.defaultUrl = { defaultOptions.defaultUrl = {
value: "/pdfjs/example/Welcome.pdf", value: "./pdfjs/example/Welcome.pdf",
kind: OptionKind.VIEWER kind: OptionKind.VIEWER
}; };
defaultOptions.disablePreferences = { defaultOptions.disablePreferences = {

View file

@ -34,7 +34,7 @@
<link rel="stylesheet" href="css/bootstrap-icons.min.css"> <link rel="stylesheet" href="css/bootstrap-icons.min.css">
<!-- PDF.js --> <!-- PDF.js -->
<script src="pdfjs/pdf.js"></script> <script th:src="@{pdfjs/pdf.js}"></script>
<!-- PDF-Lib --> <!-- PDF-Lib -->
<script src="js/thirdParty/pdf-lib.min.js"></script> <script src="js/thirdParty/pdf-lib.min.js"></script>
@ -70,24 +70,22 @@
<script> <script>
console.log("loaded game"); console.log("loaded game");
$(document).ready(function() { $(document).ready(function() {
// Find the file input within the form
// Find the file input within the form var fileInput = $('input[type="file"]');
var fileInput = $('input[type="file"]');
// Find the closest enclosing form of the file input
// Find the closest enclosing form of the file input var form = fileInput.closest('form');
var form = fileInput.closest('form');
// Find the submit button within the form
// Find the submit button within the form var submitButton = form.find('button[type="submit"], input[type="submit"]');
var submitButton = form.find('button[type="submit"], input[type="submit"]');
// Create the 'show-game-btn' button
// Create the 'show-game-btn' button var gameButton = $('<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">Bored waiting?</button><br /><br />');
var gameButton = $('<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">Bored waiting?</button><br /><br />');
// Insert the 'show-game-btn' just above the submit button
// Insert the 'show-game-btn' just above the submit button submitButton.before(gameButton);
submitButton.before(gameButton);
function loadGameScript(callback) { function loadGameScript(callback) {
console.log('loadGameScript called'); console.log('loadGameScript called');
const script = document.createElement('script'); const script = document.createElement('script');
@ -122,8 +120,6 @@
gameDialog.close(); gameDialog.close();
} }
}) })
}) })
</script> </script>
<div id="game-container"> <div id="game-container">

View file

@ -4,272 +4,260 @@
<th:block th:insert="~{fragments/common :: head(title=#{adjustContrast.title}, header=#{adjustContrast.header})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{adjustContrast.title}, header=#{adjustContrast.header})}"></th:block>
</head> </head>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br /><br /> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-12"> <div class="col-md-12">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-3"> <div class="col-md-3">
<div id="sliders-container" style="display:none;"> <div id="sliders-container" style="display:none;">
<h4> <h4><span th:text="#{adjustContrast.contrast}"></span> <span id="contrast-val">100</span>%</h4>
<span th:text="#{adjustContrast.contrast}"></span> <span id="contrast-val">100</span>% <input type="range" min="0" max="200" value="100" id="contrast-slider" />
</h4>
<input type="range" min="0" max="200" value="100" id="contrast-slider" />
<h4> <h4><span th:text="#{adjustContrast.brightness}"></span> <span id="brightness-val">100</span>%</h4>
<span th:text="#{adjustContrast.brightness}"></span> <span id="brightness-val">100</span>% <input type="range" min="0" max="200" value="100" id="brightness-slider" />
</h4>
<input type="range" min="0" max="200" value="100" id="brightness-slider" />
<h4> <h4><span th:text="#{adjustContrast.saturation}"></span> <span id="saturation-val">100</span>%</h4>
<span th:text="#{adjustContrast.saturation}"></span> <span id="saturation-val">100</span>% <input type="range" min="0" max="200" value="100" id="saturation-slider" />
</h4> </div>
<input type="range" min="0" max="200" value="100" id="saturation-slider" /> </div>
</div> <div class="col-md-7">
</div> <h2 th:text="#{adjustContrast.header}"></h2>
<div class="col-md-7"> <div class="col-md-8">
<h2 th:text="#{adjustContrast.header}"></h2> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
<div class="col-md-8"> </div>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div> <br />
</div> <canvas id="contrast-pdf-canvas"></canvas>
<br> <button id="download-button" class="btn btn-primary" th:text="#{adjustContrast.download}"></button>
<canvas id="contrast-pdf-canvas"></canvas> </div>
<button id="download-button" class="btn btn-primary" th:text="#{adjustContrast.download}"></button> </div>
</div> <style>
</div> #flex-container {
<style> display: flex;
#flex-container { align-items: center;
display: flex; }
align-items: center; #sliders-container {
} padding: 0 20px; /* Add some padding to separate sliders from canvas */
#sliders-container { }
padding: 0 20px; /* Add some padding to separate sliders from canvas */ </style>
}
</style>
<script>
var canvas = document.getElementById('contrast-pdf-canvas');
var context = canvas.getContext('2d');
var originalImageData = null;
var allPages = [];
var pdfDoc = null;
var pdf = null; // This is the current PDF document
<script src="pdfjs/pdf.js"></script> async function renderPDFAndSaveOriginalImageData(file) {
<script> var fileReader = new FileReader();
var canvas = document.getElementById('contrast-pdf-canvas'); fileReader.onload = async function() {
var context = canvas.getContext('2d'); var data = new Uint8Array(this.result);
var originalImageData = null; pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
var allPages = []; pdf = await pdfjsLib.getDocument({data: data}).promise;
var pdfDoc = null;
var pdf = null; // This is the current PDF document
async function renderPDFAndSaveOriginalImageData(file) { // Get the number of pages in the PDF
var fileReader = new FileReader(); var numPages = pdf.numPages;
fileReader.onload = async function() { allPages = Array.from({length: numPages}, (_, i) => i + 1);
var data = new Uint8Array(this.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdf = await pdfjsLib.getDocument({data: data}).promise;
// Get the number of pages in the PDF // Create a new PDF document
var numPages = pdf.numPages; pdfDoc = await PDFLib.PDFDocument.create();
allPages = Array.from({length: numPages}, (_, i) => i + 1); // Render the first page in the viewer
await renderPageAndAdjustImageProperties(1);
document.getElementById("sliders-container").style.display = "block";
// Create a new PDF document };
pdfDoc = await PDFLib.PDFDocument.create(); fileReader.readAsArrayBuffer(file);
// Render the first page in the viewer }
await renderPageAndAdjustImageProperties(1);
document.getElementById("sliders-container").style.display = "block";
}; // This function is now async and returns a promise
fileReader.readAsArrayBuffer(file); function renderPageAndAdjustImageProperties(pageNum) {
}
// This function is now async and returns a promise
function renderPageAndAdjustImageProperties(pageNum) {
return new Promise(async function(resolve, reject) { return new Promise(async function(resolve, reject) {
var page = await pdf.getPage(pageNum); var page = await pdf.getPage(pageNum);
var scale = 1.5; var scale = 1.5;
var viewport = page.getViewport({ scale: scale }); var viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
var renderContext = { var renderContext = {
canvasContext: context, canvasContext: context,
viewport: viewport viewport: viewport
}; };
var renderTask = page.render(renderContext); var renderTask = page.render(renderContext);
renderTask.promise.then(function () { renderTask.promise.then(function () {
originalImageData = context.getImageData(0, 0, canvas.width, canvas.height); originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
adjustImageProperties(); adjustImageProperties();
resolve(); resolve();
}); });
canvas.classList.add("fixed-shadow-canvas"); canvas.classList.add("fixed-shadow-canvas");
}); });
} }
function adjustImageProperties() { function adjustImageProperties() {
var contrast = parseFloat(document.getElementById('contrast-slider').value); var contrast = parseFloat(document.getElementById('contrast-slider').value);
var brightness = parseFloat(document.getElementById('brightness-slider').value); var brightness = parseFloat(document.getElementById('brightness-slider').value);
var saturation = parseFloat(document.getElementById('saturation-slider').value); var saturation = parseFloat(document.getElementById('saturation-slider').value);
contrast /= 100; // normalize to range [0, 2] contrast /= 100; // normalize to range [0, 2]
brightness /= 100; // normalize to range [0, 2] brightness /= 100; // normalize to range [0, 2]
saturation /= 100; // normalize to range [0, 2] saturation /= 100; // normalize to range [0, 2]
if (originalImageData) { if (originalImageData) {
var newImageData = context.createImageData(originalImageData.width, originalImageData.height); var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
newImageData.data.set(originalImageData.data); newImageData.data.set(originalImageData.data);
for(var i=0; i<newImageData.data.length; i+=4) for(var i=0; i<newImageData.data.length; i+=4) {
{ var r = newImageData.data[i];
var r = newImageData.data[i]; var g = newImageData.data[i+1];
var g = newImageData.data[i+1]; var b = newImageData.data[i+2];
var b = newImageData.data[i+2]; // Adjust contrast
// Adjust contrast r = adjustContrastForPixel(r, contrast);
r = adjustContrastForPixel(r, contrast); g = adjustContrastForPixel(g, contrast);
g = adjustContrastForPixel(g, contrast); b = adjustContrastForPixel(b, contrast);
b = adjustContrastForPixel(b, contrast); // Adjust brightness
// Adjust brightness r = adjustBrightnessForPixel(r, brightness);
r = adjustBrightnessForPixel(r, brightness); g = adjustBrightnessForPixel(g, brightness);
g = adjustBrightnessForPixel(g, brightness); b = adjustBrightnessForPixel(b, brightness);
b = adjustBrightnessForPixel(b, brightness); // Adjust saturation
// Adjust saturation var rgb = adjustSaturationForPixel(r, g, b, saturation);
var rgb = adjustSaturationForPixel(r, g, b, saturation); newImageData.data[i] = rgb[0];
newImageData.data[i] = rgb[0]; newImageData.data[i+1] = rgb[1];
newImageData.data[i+1] = rgb[1]; newImageData.data[i+2] = rgb[2];
newImageData.data[i+2] = rgb[2];
}
context.putImageData(newImageData, 0, 0);
} }
context.putImageData(newImageData, 0, 0);
} }
}
function rgbToHsl(r, g, b) { function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255; r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b); var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2; var h, s, l = (max + min) / 2;
if (max === min) { if (max === min) {
h = s = 0; // achromatic h = s = 0; // achromatic
} else { } else {
var d = max - min; var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break; case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break; case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break; case b: h = (r - g) / d + 4; break;
}
h /= 6;
} }
return [h, s, l]; h /= 6;
} }
function hslToRgb(h, s, l) { return [h, s, l];
var r, g, b; }
if (s === 0) { function hslToRgb(h, s, l) {
r = g = b = l; // achromatic var r, g, b;
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s; if (s === 0) {
var p = 2 * l - q; r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
r = hue2rgb(p, q, h + 1 / 3); var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
g = hue2rgb(p, q, h); var p = 2 * l - q;
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255]; r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
} }
function adjustContrastForPixel(pixel, contrast) { return [r * 255, g * 255, b * 255];
// Normalize to range [-0.5, 0.5] }
var normalized = pixel / 255 - 0.5;
// Apply contrast function adjustContrastForPixel(pixel, contrast) {
normalized *= contrast; // Normalize to range [-0.5, 0.5]
var normalized = pixel / 255 - 0.5;
// Denormalize back to [0, 255] // Apply contrast
return (normalized + 0.5) * 255; normalized *= contrast;
}
function clamp(value, min, max) { // Denormalize back to [0, 255]
return Math.min(Math.max(value, min), max); return (normalized + 0.5) * 255;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function adjustSaturationForPixel(r, g, b, saturation) {
var hsl = rgbToHsl(r, g, b);
// Adjust saturation
hsl[1] = clamp(hsl[1] * saturation, 0, 1);
// Convert back to RGB
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Return adjusted RGB values
return rgb;
}
function adjustBrightnessForPixel(pixel, brightness) {
return Math.max(0, Math.min(255, pixel * brightness));
}
async function downloadPDF() {
for (var i = 0; i < allPages.length; i++) {
await renderPageAndAdjustImageProperties(allPages[i]);
const pngImageBytes = canvas.toDataURL('image/png');
const pngImage = await pdfDoc.embedPng(pngImageBytes);
const pngDims = pngImage.scale(1);
// Create a blank page matching the dimensions of the image
const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
// Draw the PNG image
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngDims.width,
height: pngDims.height
});
} }
function adjustSaturationForPixel(r, g, b, saturation) { // Serialize the PDFDocument to bytes (a Uint8Array)
var hsl = rgbToHsl(r, g, b); const pdfBytes = await pdfDoc.save();
// Adjust saturation // Create a Blob
hsl[1] = clamp(hsl[1] * saturation, 0, 1); const blob = new Blob([pdfBytes.buffer], {type: "application/pdf"});
// Convert back to RGB // Create download link
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = "download.pdf";
downloadLink.click();
// Return adjusted RGB values // After download, reset the viewer and clear stored data
return rgb; allPages = []; // Clear the pages
} originalImageData = null; // Clear the image data
function adjustBrightnessForPixel(pixel, brightness) {
return Math.max(0, Math.min(255, pixel * brightness));
}
async function downloadPDF() {
for (var i = 0; i < allPages.length; i++) {
await renderPageAndAdjustImageProperties(allPages[i]);
const pngImageBytes = canvas.toDataURL('image/png');
const pngImage = await pdfDoc.embedPng(pngImageBytes);
const pngDims = pngImage.scale(1);
// Create a blank page matching the dimensions of the image
const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
// Draw the PNG image
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngDims.width,
height: pngDims.height
});
}
// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();
// Create a Blob
const blob = new Blob([pdfBytes.buffer], {type: "application/pdf"});
// Create download link
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = "download.pdf";
downloadLink.click();
// After download, reset the viewer and clear stored data
allPages = []; // Clear the pages
originalImageData = null; // Clear the image data
// Go back to page 1 and render it in the viewer
if (pdf !== null) {
renderPageAndAdjustImageProperties(1);
}
// Go back to page 1 and render it in the viewer
if (pdf !== null) {
renderPageAndAdjustImageProperties(1);
} }
}
// Event listeners // Event listeners
document.getElementById('fileInput-input').addEventListener('change', function(e) { document.getElementById('fileInput-input').addEventListener('change', function(e) {
@ -294,14 +282,14 @@
}); });
document.getElementById('download-button').addEventListener('click', function() { document.getElementById('download-button').addEventListener('click', function() {
downloadPDF(); downloadPDF();
}); });
</script> </script>
</div>
</div> </div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block> </body>
</div>
</body>
</html> </html>

View file

@ -4,11 +4,11 @@
<th:block th:insert="~{fragments/common :: head(title=#{compare.title}, header=#{compare.header})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{compare.title}, header=#{compare.header})}"></th:block>
<style> <style>
.result-column { .result-column {
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 15px; padding: 15px;
overflow-y: auto; overflow-y: auto;
height: calc(100vh - 400px); height: calc(100vh - 400px);
white-space: pre-wrap; white-space: pre-wrap;
} }
</style> </style>
</head> </head>

View file

@ -5,7 +5,7 @@
</head> </head>
<body> <body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>

View file

@ -21,26 +21,26 @@
<script src="js/local-pdf-input-download.js"></script> <script src="js/local-pdf-input-download.js"></script>
<script> <script>
document.getElementById('pdfForm').addEventListener('submit', async (e) => { document.getElementById('pdfForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const { PDFDocument } = PDFLib; const { PDFDocument } = PDFLib;
const processFile = async (file) => { const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file); const origFileUrl = URL.createObjectURL(file);
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer()); const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true }); const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
const form = pdfDoc.getForm(); const form = pdfDoc.getForm();
form.flatten(); form.flatten();
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_flattened.pdf'; const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_flattened.pdf';
return { processedData: pdfBlob, fileName }; return { processedData: pdfBlob, fileName };
}; };
await downloadFilesWithCallback(processFile); await downloadFilesWithCallback(processFile);
}); });
</script> </script>
</form> </form>

View file

@ -32,11 +32,11 @@ See https://github.com/adobe-type-tools/cmap-resources
<script src="js/thirdParty/bootstrap.min.js"></script> <script src="js/thirdParty/bootstrap.min.js"></script>
<!-- This snippet is used in production (included from view-pdf.html) --> <!-- This snippet is used in production (included from view-pdf.html) -->
<link rel="resource" type="application/l10n" href="pdfjs/locale/locale.properties"> <link rel="resource" type="application/l10n" th:href="@{pdfjs/locale/locale.properties}">
<script src="pdfjs/pdf.js"></script> <script th:src="@{pdfjs/pdf.js}" type="module"></script>
<link rel="stylesheet" href="pdfjs/css/viewer.css"> <link rel="stylesheet" th:href="@{/pdfjs/css/viewer.css}">
<script src="pdfjs/js/viewer.js"></script> <script th:src="@{pdfjs/js/viewer.js}" type="module"></script>
</head> </head>
<body tabindex="1"> <body tabindex="1">