Stirling-PDF/src/main/resources/templates/multi-tool.html
jordy e048fc6653 fix RTL language directions
The positioning of the insert pdf buttons and the direction icon of the move page buttons are tied to the language direction, to fix this we retrieve the language direction from the document and use this to reverse some logic/elements for RTL languages.
2023-04-22 19:29:21 +02:00

659 lines
27 KiB
HTML

<!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=#{multiTool.title})}"></th:block>
<body>
<div id="image-highlighter"></div>
<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">
<h2 th:text="#{multiTool.header}"></h2>
<div class="col-md-12" id="pages-container-wrapper">
<div id="pages-container"></div>
</div>
<div class="col-md-6" style="text-align: center">
<div id="global-buttons-container">
<button class="btn btn-primary" onclick="addPdfs()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16">
<path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>
</button>
<button class="btn btn-secondary enable-on-file" onclick="rotateAll(-90)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>
</button>
<button class="btn btn-secondary enable-on-file" onclick="rotateAll(90)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>
</button>
<button id="export-button" class="btn btn-primary enable-on-file" onclick="exportPdf()" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
<script>
var fileName = null;
/**
*
* Manage Page container scroll
*
*/
const pagesContainer = document.getElementById("pages-container");
const pagesContainerWrapper = document.getElementById("pages-container-wrapper")
const pageDirection = getComputedStyle(document.body).direction;
var scrollDelta = 0; // variable to store the accumulated scroll delta
var isScrolling = false; // variable to track if scroll is already in progress
pagesContainerWrapper.addEventListener("wheel", function(e) {
e.preventDefault(); // prevent default mousewheel behavior
// Accumulate the horizontal scroll delta
scrollDelta -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY;
// If scroll is not already in progress, start the scroll loop
if (!isScrolling) {
isScrolling = true;
requestAnimationFrame(scrollLoop);
}
});
function scrollLoop() {
// Scroll the div horizontally by a fraction of the accumulated scroll delta
pagesContainerWrapper.scrollLeft += scrollDelta * 0.1;
// Reduce the accumulated scroll delta by a fraction
scrollDelta *= 0.9;
// If scroll delta is still significant, continue the scroll loop
if (Math.abs(scrollDelta) > 0.1) {
requestAnimationFrame(scrollLoop);
} else {
isScrolling = false; // Reset scroll in progress flag
}
}
/**
*
* Manage image highlighter clearing
*
*/
const imageHighlighter = document.getElementById('image-highlighter');
imageHighlighter.onclick = (e) => {
imageHighlighter.childNodes.forEach((child) => {
child.classList.add('remove');
setTimeout(() => {
imageHighlighter.removeChild(child);
}, 100)
})
}
/**
*
* Methods for managing PDFs
*
*/
function addPdfs(nextSiblingElement) {
var input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.setAttribute("accept", "application/pdf");
input.onchange = async(e) => {
const files = e.target.files;
fileName = files[0].name;
for (var i=0; i < files.length; i++) {
addPdfFile(files[i], nextSiblingElement);
}
document.querySelectorAll(".enable-on-file").forEach(element => {
element.disabled = false;
});
}
input.click();
}
async function addPdfFile(file, nextSiblingElement) {
const { renderer, pdfDocument } = await loadFile(file);
const moveUpButtonCallback = e => {
var imgContainer = e.target;
while (!imgContainer.classList.contains("page-container")) {
imgContainer = imgContainer.parentNode;
}
const sibling = imgContainer.previousSibling;
if (sibling) {
pagesContainer.removeChild(imgContainer);
pagesContainer.insertBefore(imgContainer, sibling);
imgContainer.scrollIntoView({
behavior: "instant",
block: "center",
})
}
};
const moveDownButtonCallback = e => {
var imgContainer = e.target;
while (!imgContainer.classList.contains("page-container")) {
imgContainer = imgContainer.parentNode;
}
const sibling = imgContainer.nextSibling;
if (sibling) {
pagesContainer.removeChild(imgContainer);
if (sibling.nextSibling) {
pagesContainer.insertBefore(imgContainer, sibling.nextSibling);
} else {
pagesContainer.appendChild(imgContainer)
}
imgContainer.scrollIntoView({
behavior: "instant",
block: "center",
})
}
};
const rotateCCWButtonCallback = e => {
var imgContainer = e.target;
while (!imgContainer.classList.contains("page-container")) {
imgContainer = imgContainer.parentNode;
}
const img = imgContainer.querySelector("img");
rotateElement(img, -90)
};
const rotateCWButtonCallback = e => {
var imgContainer = e.target;
while (!imgContainer.classList.contains("page-container")) {
imgContainer = imgContainer.parentNode;
}
const img = imgContainer.querySelector("img");
rotateElement(img, 90)
};
const deletePageButtonCallback = e => {
var imgContainer = e.target;
while (!imgContainer.classList.contains("page-container")) {
imgContainer = imgContainer.parentNode;
}
pagesContainer.removeChild(imgContainer);
};
const insertFileButtonCallback = e => {
var imgContainer = e.target;
while (!imgContainer.classList.contains("page-container")) {
imgContainer = imgContainer.parentNode;
}
addPdfs(imgContainer)
};
for (var i=0; i < renderer.pageCount; i++) {
const div = document.createElement('div');
div.classList.add("page-container");
var img = document.createElement('img');
img.classList.add('page-image')
const imageSrc = await renderer.renderPage(i)
img.src = imageSrc;
img.pageIdx = i;
img.rend = renderer;
img.doc = pdfDocument;
div.appendChild(img);
/**
* Making pages larger when clicking on them
*/
img.addEventListener('mousedown', (x) => {
var bigImg = document.createElement('img');
bigImg.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
};
bigImg.src = imageSrc;
imageHighlighter.appendChild(bigImg);
})
/**
* Rendering the various buttons to manipulate and move pdf pages
*/
const leftDirection = pageDirection === 'rtl' ? 'right' : 'left'
const rightDirection = pageDirection === 'rtl' ? 'left' : 'right'
const buttonContainer = document.createElement('div');
buttonContainer.classList.add("button-container");
const moveUp = document.createElement('button');
moveUp.classList.add("move-left-button","btn", "btn-secondary");
moveUp.innerHTML = `<i class="bi bi-arrow-${leftDirection}-short"></i>`;
moveUp.onclick = moveUpButtonCallback;
buttonContainer.appendChild(moveUp);
const moveDown = document.createElement('button');
moveDown.classList.add("move-right-button","btn", "btn-secondary");
moveDown.innerHTML = `<i class="bi bi-arrow-${rightDirection}-short"></i>`;
moveDown.onclick = moveDownButtonCallback;
buttonContainer.appendChild(moveDown);
const rotateCCW = document.createElement('button');
rotateCCW.classList.add("btn", "btn-secondary");
rotateCCW.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>`;
rotateCCW.onclick = rotateCCWButtonCallback;
buttonContainer.appendChild(rotateCCW);
const rotateCW = document.createElement('button');
rotateCW.classList.add("btn", "btn-secondary");
rotateCW.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>`;
rotateCW.onclick = rotateCWButtonCallback;
buttonContainer.appendChild(rotateCW);
const deletePage = document.createElement('button');
deletePage.classList.add("btn", "btn-danger");
deletePage.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
</svg>`;
deletePage.onclick = deletePageButtonCallback;
buttonContainer.appendChild(deletePage);
div.appendChild(buttonContainer);
const insertFileButtonContainer = document.createElement('div');
insertFileButtonContainer.classList.add(
"insert-file-button-container",
leftDirection,
`align-center-${leftDirection}`);
const insertFileButton = document.createElement('button');
insertFileButton.classList.add("btn", "btn-primary", "insert-file-button");
insertFileButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16">
<path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>`;
insertFileButton.onclick = insertFileButtonCallback;
insertFileButtonContainer.appendChild(insertFileButton);
div.appendChild(insertFileButtonContainer);
// add this button to every element, but only show it on the last one :D
const insertFileButtonRightContainer = document.createElement('div');
insertFileButtonRightContainer.classList.add(
"insert-file-button-container",
rightDirection,
`align-center-${rightDirection}`);
const insertFileButtonRight = document.createElement('button');
insertFileButtonRight.classList.add("btn", "btn-primary", "insert-file-button");
insertFileButtonRight.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16">
<path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
insertFileButtonRight</svg>`;
insertFileButtonRight.onclick = () => addPdfs();
insertFileButtonRightContainer.appendChild(insertFileButtonRight);
div.appendChild(insertFileButtonRightContainer);
if (nextSiblingElement) {
pagesContainer.insertBefore(div, nextSiblingElement);
} else {
pagesContainer.appendChild(div);
}
}
}
function rotateElement(element, deg) {
var lastTransform = element.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ''));
const newAngle = lastAngle + deg;
element.style.rotate = newAngle + "deg";
}
function rotateAll(deg) {
for (var i=0; i<pagesContainer.childNodes.length; i++) {
const img = pagesContainer.childNodes[i].querySelector("img");
if (!img) continue;
rotateElement(img, deg)
}
}
async function exportPdf() {
const pdfDoc = await PDFLib.PDFDocument.create();
for (var i=0; i<pagesContainer.childNodes.length; i++) {
const img = pagesContainer.childNodes[i].querySelector("img");
if (!img) continue;
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx])
const page = pages[0];
const rotation = img.style.rotate;
if (rotation) {
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle))
}
pdfDoc.addPage(page);
}
const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(pdfBlob);
const downloadOption = localStorage.getItem('downloadOption');
if (downloadOption === 'sameWindow') {
// Open the file in the same window
window.location.href = url;
} else if (downloadOption === 'newWindow') {
// Open the file in a new window
window.open(url, '_blank');
} else {
// Download the file
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = fileName ? fileName : 'managed.pdf';
downloadLink.click();
}
}
async function loadFile(file) {
var objectUrl = URL.createObjectURL(file);
var pdfDocument = await toPdfLib(objectUrl);
var renderer = await toRenderer(objectUrl);
return { renderer, pdfDocument };
}
async function toPdfLib(objectUrl) {
const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes);
return pdfDoc;
}
async function toRenderer(objectUrl) {
const pdf = await pdfjsLib.getDocument(objectUrl).promise;
return {
document: pdf,
pageCount: pdf.numPages,
renderPage: async function(pageIdx) {
const page = await this.document.getPage(pageIdx+1);
const canvas = document.createElement("canvas");
// set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) {
canvas.width = page.view[3];
canvas.height = page.view[2];
} else {
canvas.width = page.view[2];
canvas.height = page.view[3];
}
// render the page onto the canvas
var renderContext = {
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 })
};
await page.render(renderContext).promise;
return canvas.toDataURL();
}
};
}
</script>
<style>
#global-buttons-container {
display: flex;
gap: 10px;
align-items: start;
background: rgba(13, 110, 253, 0.1);
border: 1px solid rgba(0, 0, 0, .25);
backdrop-filter: blur(2px);
top: 10px;
z-index: 10;
padding: 10px;
border-radius: 8px;
}
#global-buttons-container > * {
padding: 0.6rem 0.75rem;
}
#global-buttons-container svg {
width: 20px;
height: 20px;
}
#export-button {
margin-left: auto;
}
#pages-container-wrapper {
--background-color: rgba(0, 0, 0, 0.025);
--scroll-bar-color: #f1f1f1;
--scroll-bar-thumb: #888;
--scroll-bar-thumb-hover: #555;
background-color: var(--background-color);
width: 100%;
display: flex;
flex-direction: column;
padding: 10px 25px;
border-radius: 10px;
overflow-y: hidden;
overflow-x: auto;
min-height: 275px;
margin: 0 0 30px 0;
}
#pages-container {
margin: auto;
gap: 0px;
display:flex;
align-items: center;
justify-content: center;
}
/* width */
#pages-container-wrapper::-webkit-scrollbar {
width: 10px;
height: 10px;
}
/* Track */
#pages-container-wrapper::-webkit-scrollbar-track {
background: var(--scroll-bar-color);
}
/* Handle */
#pages-container-wrapper::-webkit-scrollbar-thumb {
border-radius: 10px;
background: var(--scroll-bar-thumb);
}
/* Handle on hover */
#pages-container-wrapper::-webkit-scrollbar-thumb:hover {
background: var(--scroll-bar-thumb-hover);
}
.page-container {
height:250px;
display: flex;
align-items: center;
flex-direction: column-reverse;
aspect-ratio: 1;
text-align: center;
position: relative;
user-select: none;
}
.page-container img {
max-width: calc(100% - 15px);
max-height: calc(100% - 15px);
display: block;
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
border-radius: 4px;
transition: rotate 0.3s;
}
.page-container .button-container {
z-index: 2;
display:flex;
}
.page-container .button-container > * {
padding: 0.25rem 0.5rem;
margin: 3px;
display: block;
}
.page-container svg {
width: 16px;
height: 16px;
}
.page-container:nth-child(1) .move-left-button {
display: none;
}
.page-container:last-child .move-right-button {
display: none;
}
.page-image {
cursor:pointer;
}
/* "insert pdf" buttons that appear on the right when hover */
.insert-file-button-container {
translate: 0 -50%;
width: 80px;
height: 100%;
z-index: 1;
opacity: 0;
transition: opacity 0.2s;
}
.insert-file-button-container.left {
left: -20px;
}
.insert-file-button-container.right {
right: -20px;
}
html[lang-direction=ltr] .insert-file-button-container.right {
display:none;
}
html[lang-direction=rtl] .insert-file-button-container.left {
display:none;
}
.insert-file-button-container.left .insert-file-button {
left: 0;
translate: 0 -50%;
}
.insert-file-button-container.right .insert-file-button {
right: 0;
translate: 0 -50%;
}
html[lang-direction=ltr] .page-container:last-child > .insert-file-button-container.right {
display: block;
}
html[lang-direction=rtl] .page-container:last-child > .insert-file-button-container.left {
display: block;
}
.insert-file-button-container:hover {
opacity: 1;
transition: opacity 0.05s;
}
.insert-file-button {
position: absolute;
top: 50%;
right: 50%;
translate: 50% -50%;
aspect-ratio: 1;
border-radius: 100px;
}
#image-highlighter {
position: fixed;
display:flex;
inset: 0;
z-index: 10000;
background-color: rgba(0, 0, 0, 0);
visibility: hidden;
align-items: center;
justify-content: center;
transition: visbility 0.1s linear, background-color 0.1s linear;
}
#image-highlighter > * {
max-width: 80vw;
max-height: 80vh;
animation: image-highlight .1s linear;
transition: transform .1s linear, opacity .1s linear;
}
#image-highlighter > *.remove {
transform: scale(0.8) !important;
opacity: 0 !important;
}
#image-highlighter:not(:empty) {
background-color: rgba(0, 0, 0, 0.37);
visibility: visible;
}
@keyframes image-highlight {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
#add-pdf-button {
margin: 0 auto;
}
</style>
</body>
</html>