Merge branch 'main' of git@github.com:Frooodle/Stirling-PDF.git into

main
This commit is contained in:
Anthony Stirling 2023-04-21 13:15:46 +01:00
commit a34c2863bd
7 changed files with 553 additions and 86 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: Frooodle # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

View file

@ -6,6 +6,7 @@
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf)
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex) [![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex)
[![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs. This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
@ -42,6 +43,8 @@ Feel free to request any features of bug fixes either in github issues or our [D
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF) - [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
- Docker - Docker
- PDF.js
- PDF-LIB.js
## How to use ## How to use
@ -95,4 +98,4 @@ Stirling PDF allows easy customization of the visible application name.
Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAVBAR_NAME with Docker or Java. Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAVBAR_NAME with Docker or Java.
If running Java directly, you can also pass these as properties using -D arguments. If running Java directly, you can also pass these as properties using -D arguments.
Using the same method you can also change the default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR to select your default language (Will always default to English on invalid locale) Using the same method you can also change the default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR to select your default language (Will always default to English on invalid locale)

View file

@ -88,8 +88,8 @@ home.pdfToPDFA.desc=Convierte PDF to PDF/A para almacenamiento a largo plazo
home.PDFToWord.title=PDF a Word home.PDFToWord.title=PDF a Word
home.PDFToWord.desc=Convertir formatos PDF a Word (DOC, DOCX y ODT) home.PDFToWord.desc=Convertir formatos PDF a Word (DOC, DOCX y ODT)
home.PDFToPresentation.title=PDF a presentación home.PDFToPresentation.title=PDF a presentación
home.PDFToPresentation.desc=Convertir PDF a formatos de presentación (PPT, PPTX y ODP) home.PDFToPresentation.desc=Convertir PDF a formatos de presentación (PPT, PPTX y ODP)
home.PDFToText.title=PDF a texto/RTF home.PDFToText.title=PDF a texto/RTF
home.PDFToText.desc=Convertir PDF a texto o formato RTF home.PDFToText.desc=Convertir PDF a texto o formato RTF
@ -117,16 +117,16 @@ settings.zipThreshold=Ficheros Zip cuando excede el número de ficheros descarga
#OCR #OCR
ocr.title=OCR / Escaneo de limpieza ocr.title=OCR / Escaneo de limpieza
ocr.header=Escaneos de limpieza / OCR (Reconocimiento óptico de caracteres) ocr.header=Escaneos de limpieza / OCR (Reconocimiento óptico de caracteres)
ocr.SeleccionaText.1=Selecciona los idiomas que se detectarán en el PDF (Los enumerados son los detectados actualmente): ocr.selectText.1=Selecciona los idiomas que se detectarán en el PDF (Los enumerados son los detectados actualmente):
ocr.SeleccionaText.2=Produzca un archivo de texto que contenga texto OCR junto con el PDF editado con OCR ocr.selectText.2=Produzca un archivo de texto que contenga texto OCR junto con el PDF editado con OCR
ocr.SeleccionaText.3=Corrija las páginas que se escanearon en un ángulo torcido girándolas nuevamente a su lugar ocr.selectText.3=Corrija las páginas que se escanearon en un ángulo torcido girándolas nuevamente a su lugar
ocr.SeleccionaText.4=Limpie la página para que sea menos probable que el OCR encuentre texto en el ruido de fondo. (Sin cambio de salida) ocr.selectText.4=Limpie la página para que sea menos probable que el OCR encuentre texto en el ruido de fondo. (Sin cambio de salida)
ocr.SeleccionaText.5=Limpie la página para que sea menos probable que el OCR encuentre texto en el ruido de fondo, mantiene la limpieza en la salida. ocr.selectText.5=Limpie la página para que sea menos probable que el OCR encuentre texto en el ruido de fondo, mantiene la limpieza en la salida.
ocr.SeleccionaText.6=Ignora las páginas que tienen texto interactivo, solo las páginas OCR que son imágenes ocr.selectText.6=Ignora las páginas que tienen texto interactivo, solo las páginas OCR que son imágenes
ocr.SeleccionaText.7=Fuerza OCR, OCR eliminará en cada página todo el texto original ocr.selectText.7=Fuerza OCR, OCR eliminará en cada página todo el texto original
ocr.SeleccionaText.8=Normal (Se producirá un error si el PDF contiene texto) ocr.selectText.8=Normal (Se producirá un error si el PDF contiene texto)
ocr.SeleccionaText.9=Ajustes Adicionales ocr.selectText.9=Ajustes Adicionales
ocr.SeleccionaText.10=Modo OCR ocr.selectText.10=Modo OCR
ocr.help=Lea esta documentación sobre cómo usar esto para otros idiomas y/o no usarlo en docker ocr.help=Lea esta documentación sobre cómo usar esto para otros idiomas y/o no usarlo en docker
ocr.credit=Este servicio utiliza OCRmyPDF y Tesseract para OCR. ocr.credit=Este servicio utiliza OCRmyPDF y Tesseract para OCR.
ocr.submit=Procesa PDF con OCR ocr.submit=Procesa PDF con OCR
@ -135,7 +135,7 @@ ocr.submit=Procesa PDF con OCR
extractImages.title=Extraer imágenes extractImages.title=Extraer imágenes
extractImages.header=Extraer imágenes extractImages.header=Extraer imágenes
extractImages.SeleccionaText=Selecciona el formato de imagen para convertir las imágenes extraídas extractImages.selectText=Selecciona el formato de imagen para convertir las imágenes extraídas
extractImages.submit=Extraer extractImages.submit=Extraer
@ -151,13 +151,13 @@ fileToPDF.submit=Convertir a PDF
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir PDF compress.header=Comprimir PDF
compress.credit=Este servicio usa OCRmyPDF para la Compresión/Optimizatión del PDF. compress.credit=Este servicio usa OCRmyPDF para la Compresión/Optimizatión del PDF.
compress.SeleccionaText.1=Nivel de Optimización: compress.selectText.1=Nivel de Optimización:
compress.SeleccionaText.2=0 (Sin optimización) compress.selectText.2=0 (Sin optimización)
compress.SeleccionaText.3=1 (Por defecto, optimización sin pérdidas) compress.selectText.3=1 (Por defecto, optimización sin pérdidas)
compress.SeleccionaText.4=2 (Optimización con pérdida) compress.selectText.4=2 (Optimización con pérdida)
compress.SeleccionaText.5=3 (Optimización con pérdida, más agresiva) compress.selectText.5=3 (Optimización con pérdida, más agresiva)
compress.SeleccionaText.6=Habilita la vista web rápida (linealizar PDF) compress.selectText.6=Habilita la vista web rápida (linealizar PDF)
compress.SeleccionaText.7=Habilita la codificación JBIG2 con pérdida compress.selectText.7=Habilita la codificación JBIG2 con pérdida
compress.submit=Comprimir compress.submit=Comprimir
@ -169,9 +169,9 @@ addImage.submit=Añade imagen
#merge #merge
merge.title=Mezcla merge.title=Une
merge.header=Mezcla múltiples PDFs (2+) merge.header=Une múltiples PDFs (2+)
merge.submit=Mezcla merge.submit=Une
#pdfOrganiser #pdfOrganiser
pdfOrganiser.title=Organizador de páginas pdfOrganiser.title=Organizador de páginas
@ -199,13 +199,13 @@ split.title=Dividir PDF
split.header=Dividir PDF split.header=Dividir PDF
split.desc.1=Los números que selecciona son el número de página en el que desea hacer una división split.desc.1=Los números que selecciona son el número de página en el que desea hacer una división
split.desc.2=Como tal, seleccionar 1,3,7-8 dividiría un documento de 10 páginas en 6 archivos PDF separados con: split.desc.2=Como tal, seleccionar 1,3,7-8 dividiría un documento de 10 páginas en 6 archivos PDF separados con:
split.desc.3=Documento #1: Page 1 split.desc.3=Documento #1: Página 1
split.desc.4=Documento #2: Page 2 and 3 split.desc.4=Documento #2: Páginas 2 y 3
split.desc.5=Documento #3: Page 4, 5 and 6 split.desc.5=Documento #3: Páginas 4, 5 y 6
split.desc.6=Documento #4: Page 7 split.desc.6=Documento #4: Página 7
split.desc.7=Documento #5: Page 8 split.desc.7=Documento #5: Página 8
split.desc.8=Documento #6: Page 9 and 10 split.desc.8=Documento #6: Páginas 9 y 10
split.splitPages=Introduzca las páginas para dividir en: split.splitPages=Introduzca las páginas para dividir:
split.submit=Dividir split.submit=Dividir
@ -213,16 +213,16 @@ split.submit=Dividir
imageToPDF.title=Imagen a PDF imageToPDF.title=Imagen a PDF
imageToPDF.header=Imagen a PDF imageToPDF.header=Imagen a PDF
imageToPDF.submit=Convertir imageToPDF.submit=Convertir
imageToPDF.SeleccionaText.1=Estirar para ajustar imageToPDF.selectText.1=Estirar para ajustar
imageToPDF.SeleccionaText.2=Auto rotación PDF imageToPDF.selectText.2=Auto rotación PDF
imageToPDF.SeleccionaText.3=Lógica de archivos múltiples (Únicamente activado si funciona con multiples imágenes) imageToPDF.selectText.3=Lógica de archivos múltiples (Únicamente activado si funciona con multiples imágenes)
imageToPDF.SeleccionaText.4=Une en un único PDF imageToPDF.selectText.4=Une en un único PDF
imageToPDF.SeleccionaText.5=Convertir a PDFs separados imageToPDF.selectText.5=Convertir a PDFs separados
#pdfToImage #pdfToImage
pdfToImage.title=PDF a Imagen pdfToImage.title=PDF a Imagen
pdfToImage.header=PDF a Imagen pdfToImage.header=PDF a Imagen
pdfToImage.SeleccionaText=Formato de Imagen pdfToImage.selectText=Formato de Imagen
pdfToImage.singleOrMultiple=Tipo resultante de imagen pdfToImage.singleOrMultiple=Tipo resultante de imagen
pdfToImage.single=Imagen Grande Única pdfToImage.single=Imagen Grande Única
pdfToImage.multi=Múltiples Imágenes pdfToImage.multi=Múltiples Imágenes
@ -235,68 +235,68 @@ pdfToImage.submit=Convertir
#addPassword #addPassword
addPassword.title=Añade Contraseña addPassword.title=Añade Contraseña
addPassword.header=Añade contraseña (Encripta) addPassword.header=Añade contraseña (Encripta)
addPassword.SeleccionaText.1=Selecciona PDF para encriptar addPassword.selectText.1=Selecciona PDF para encriptar
addPassword.SeleccionaText.2=Contraseña addPassword.selectText.2=Contraseña
addPassword.SeleccionaText.3=Longitud de la clave de cifrado addPassword.selectText.3=Longitud de la clave de cifrado
addPassword.SeleccionaText.4=Valores altos son más fuertes, pero valores bajos tienen mejor compatibilidad. addPassword.selectText.4=Valores altos son más fuertes, pero valores bajos tienen mejor compatibilidad.
addPassword.SeleccionaText.5=Permisos para establecer addPassword.selectText.5=Permisos para establecer
addPassword.SeleccionaText.6=Impedir el ensamblaje del documento addPassword.selectText.6=Impedir el ensamblaje del documento
addPassword.SeleccionaText.7=Impedir la extracción de contenido addPassword.selectText.7=Impedir la extracción de contenido
addPassword.SeleccionaText.8=Impedir la extracción para la accesibilidad addPassword.selectText.8=Impedir la extracción para la accesibilidad
addPassword.SeleccionaText.9=Impedir rellenar formulario addPassword.selectText.9=Impedir rellenar formulario
addPassword.SeleccionaText.10=Impedir modificación addPassword.selectText.10=Impedir modificación
addPassword.SeleccionaText.11=Impedir modificación de anotaciones addPassword.selectText.11=Impedir modificación de anotaciones
addPassword.SeleccionaText.12=Impedir imprimir addPassword.selectText.12=Impedir imprimir
addPassword.SeleccionaText.13=Impedir imprimir diferentes formatos addPassword.selectText.13=Impedir imprimir diferentes formatos
addPassword.submit=Encripta addPassword.submit=Encripta
#watermark #watermark
watermark.title=Añade marca de agua watermark.title=Añade marca de agua
watermark.header=Añade marca de agua watermark.header=Añade marca de agua
watermark.SeleccionaText.1=Selecciona PDF para añadir marca de agua: watermark.selectText.1=Selecciona PDF para añadir marca de agua:
watermark.SeleccionaText.2=Texto de la marca de agua: watermark.selectText.2=Texto de la marca de agua:
watermark.SeleccionaText.3=Tamaño de la Fuente: watermark.selectText.3=Tamaño de la Fuente:
watermark.SeleccionaText.4=Rotación (0-360): watermark.selectText.4=Rotación (0-360):
watermark.SeleccionaText.5=Ancho (Espacio entre cada marca de agua horizontalmente): watermark.selectText.5=Ancho (Espacio entre cada marca de agua horizontalmente):
watermark.SeleccionaText.6=Alto (Espacio entre cada marca de agua verticalmente): watermark.selectText.6=Alto (Espacio entre cada marca de agua verticalmente):
watermark.SeleccionaText.7=Opacidad (0% - 100%): watermark.selectText.7=Opacidad (0% - 100%):
watermark.submit=Añade marca de agua watermark.submit=Añade marca de agua
#remove-watermark #remove-watermark
remove-watermark.title=Elimina marca de agua remove-watermark.title=Elimina marca de agua
remove-watermark.header=Elimina marca de agua remove-watermark.header=Elimina marca de agua
remove-watermark.SeleccionaText.1=Selecciona PDF para eliminar la marca de agua: remove-watermark.selectText.1=Selecciona PDF para eliminar la marca de agua:
remove-watermark.SeleccionaText.2=Texto de la marca de agua: remove-watermark.selectText.2=Texto de la marca de agua:
remove-watermark.submit=Elimina marca de agua remove-watermark.submit=Elimina marca de agua
#Change permissions #Change permissions
permissions.title=Cambiar Permisos permissions.title=Cambiar Permisos
permissions.header=Cambiar Permisos permissions.header=Cambiar Permisos
permissions.warning=Advertencia para que estos permisos no se puedan cambiar, se recomienda configurarlos con una contraseña a través de la página de cambio de contraseña permissions.warning=Advertencia para que estos permisos no se puedan cambiar, se recomienda configurarlos con una contraseña a través de la página de cambio de contraseña
permissions.SeleccionaText.1=Selecciona PDF para cambiar los permisos permissions.selectText.1=Selecciona PDF para cambiar los permisos
permissions.SeleccionaText.2=Permisos a establecer permissions.selectText.2=Permisos a establecer
permissions.SeleccionaText.3=Impedir el ensamblaje del documento permissions.selectText.3=Impedir el ensamblaje del documento
permissions.SeleccionaText.4=Impedir la extracción de contenido permissions.selectText.4=Impedir la extracción de contenido
permissions.SeleccionaText.5=Impedir la extracción para la accesibilidad permissions.selectText.5=Impedir la extracción para la accesibilidad
permissions.SeleccionaText.6=Impedir rellenar formulario permissions.selectText.6=Impedir rellenar formulario
permissions.SeleccionaText.7=Impedir modificación permissions.selectText.7=Impedir modificación
permissions.SeleccionaText.8=Impedir modificación de anotaciones permissions.selectText.8=Impedir modificación de anotaciones
permissions.SeleccionaText.9=Impedir imprimir permissions.selectText.9=Impedir imprimir
permissions.SeleccionaText.10=Impedir imprimir diferentes formatos permissions.selectText.10=Impedir imprimir diferentes formatos
permissions.submit=Cambiar permissions.submit=Cambiar
#remove password #remove password
removePassword.title=Elimina contraseña removePassword.title=Elimina contraseña
removePassword.header=Elimina contraseña (Desencripta) removePassword.header=Elimina contraseña (Desencripta)
removePassword.SeleccionaText.1=Selecciona PDF para Desencriptar removePassword.selectText.1=Selecciona PDF para Desencriptar
removePassword.SeleccionaText.2=Contraseña removePassword.selectText.2=Contraseña
removePassword.submit=Elimina removePassword.submit=Elimina
changeMetadata.title=Cambia Metadatos changeMetadata.title=Cambia Metadatos
changeMetadata.header=Cambia Metadatos changeMetadata.header=Cambia Metadatos
changeMetadata.SeleccionaText.1=Edite las variables que desea cambiar changeMetadata.selectText.1=Edite las variables que desea cambiar
changeMetadata.SeleccionaText.2=Elimina todos los metadatos changeMetadata.selectText.2=Elimina todos los metadatos
changeMetadata.SeleccionaText.3=Mostrar metadatos personalizados: changeMetadata.selectText.3=Mostrar metadatos personalizados:
changeMetadata.author=Autor: changeMetadata.author=Autor:
changeMetadata.creationDate=Fecha de Creación (yyyy/MM/dd HH:mm:ss): changeMetadata.creationDate=Fecha de Creación (yyyy/MM/dd HH:mm:ss):
changeMetadata.creator=Creador: changeMetadata.creator=Creador:
@ -306,13 +306,13 @@ changeMetadata.producer=Productor:
changeMetadata.subject=Asunto: changeMetadata.subject=Asunto:
changeMetadata.title=Título: changeMetadata.title=Título:
changeMetadata.trapped=Trapped: changeMetadata.trapped=Trapped:
changeMetadata.SeleccionaText.4=Otros Metadatos: changeMetadata.selectText.4=Otros Metadatos:
changeMetadata.SeleccionaText.5=Agregar entrada de metadatos personalizados changeMetadata.selectText.5=Agregar entrada de metadatos personalizados
changeMetadata.submit=Cambia changeMetadata.submit=Cambia
xlsToPdf.title=Excel a PDF xlsToPdf.title=Excel a PDF
xlsToPdf.header=Excel a PDF xlsToPdf.header=Excel a PDF
xlsToPdf.SeleccionaText.1=Selecciona hoja de cálculo de Excel XLS o XLSX para convertir xlsToPdf.selectText.1=Selecciona hoja de cálculo de Excel XLS o XLSX para convertir
xlsToPdf.convert=convertir xlsToPdf.convert=convertir
@ -328,29 +328,29 @@ pdfToPDFA.submit=Convertir
PDFToWord.title=PDF a Word PDFToWord.title=PDF a Word
PDFToWord.header=PDF a Word PDFToWord.header=PDF a Word
PDFToWord.selectText.1=Formato de archivo de salida PDFToWord.selectText.1=Formato de archivo de salida
PDFToWord.credit=Este servicio utiliza LibreOffice para la conversión de archivos. PDFToWord.credit=Este servicio utiliza LibreOffice para la conversión de archivos.
PDFToWord.submit=Convertir PDFToWord.submit=Convertir
PDFToPresentation.title=PDF a presentación PDFToPresentation.title=PDF a presentación
PDFToPresentation.header=PDF a presentación PDFToPresentation.header=PDF a presentación
PDFToPresentation.selectText.1=Formato de archivo de salida PDFToPresentation.selectText.1=Formato de archivo de salida
PDFToPresentation.credit=Este servicio utiliza LibreOffice para la conversión de archivos. PDFToPresentation.credit=Este servicio utiliza LibreOffice para la conversión de archivos.
PDFToPresentation.submit=Convertir PDFToPresentation.submit=Convertir
PDFToText.title=PDF a texto/RTF PDFToText.title=PDF a texto/RTF
PDFToText.header=PDF a texto/RTF PDFToText.header=PDF a texto/RTF
PDFToText.selectText.1=Formato de archivo de salida PDFToText.selectText.1=Formato de archivo de salida
PDFToText.credit=Este servicio utiliza LibreOffice para la conversión de archivos. PDFToText.credit=Este servicio utiliza LibreOffice para la conversión de archivos.
PDFToText.submit=Convertir PDFToText.submit=Convertir
PDFToHTML.title=PDF a HTML PDFToHTML.title=PDF a HTML
PDFToHTML.header=PDF a HTML PDFToHTML.header=PDF a HTML
PDFToHTML.credit=Este servicio utiliza LibreOffice para la conversión de archivos. PDFToHTML.credit=Este servicio utiliza LibreOffice para la conversión de archivos.
PDFToHTML.submit=Convertir PDFToHTML.submit=Convertir
PDFToXML.title=PDF a XML PDFToXML.title=PDF a XML
PDFToXML.header=PDF a XML PDFToXML.header=PDF a XML
PDFToXML.credit=Este servicio utiliza LibreOffice para la conversión de archivos. PDFToXML.credit=Este servicio utiliza LibreOffice para la conversión de archivos.
PDFToXML.submit=Convertir PDFToXML.submit=Convertir

View file

@ -21,3 +21,17 @@ html[lang-direction=rtl] * {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
.align-top {
position: absolute;
top: 0;
}
.align-center-right {
position: absolute;
right: 0;
top: 50%;
}
.align-bottom {
position: absolute;
bottom: 0;
}

View file

@ -23,7 +23,7 @@
<script src="pdfjs/pdf.js"></script> <script src="pdfjs/pdf.js"></script>
<!-- PDF-Lib --> <!-- PDF-Lib -->
<script src="pdf-lib.min.js"></script> <script src="js/pdf-lib.min.js"></script>
<!-- Custom --> <!-- Custom -->
<link rel="stylesheet" href="css/general.css"> <link rel="stylesheet" href="css/general.css">

View file

@ -0,0 +1,450 @@
<!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=#{pageManager.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" style="text-align: center">
<h2 th:text="#{pageManager.header}"></h2>
<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 id="pages-container"></div>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
<script>
var fileName = null;
const pagesContainer = document.getElementById("pages-container");
// add the bottom "insert pdf" button that appears on the right when hovered over
const bottomInsertFileButtonContainer = document.createElement('div');
bottomInsertFileButtonContainer.classList.add("insert-file-button-container", "align-bottom");
bottomInsertFileButtonContainer.style.transform = "translate(0, 50%)";
const bottomInsertFileButton = document.createElement('button');
bottomInsertFileButton.classList.add("btn", "btn-primary", "insert-file-button", "align-center-right");
bottomInsertFileButton.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>`;
bottomInsertFileButton.onclick = e => addPdfs();
bottomInsertFileButtonContainer.appendChild(bottomInsertFileButton);
pagesContainer.appendChild(bottomInsertFileButtonContainer);
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;
}
console.log("imgContainer", imgContainer);
const sibling = imgContainer.previousSibling;
console.log("sibling", sibling);
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;
}
console.log("imgContainer", imgContainer);
const sibling = imgContainer.nextSibling;
console.log("sibling", sibling);
if (sibling) {
pagesContainer.removeChild(imgContainer);
if (sibling.nextSibling) {
pagesContainer.insertBefore(imgContainer, sibling.nextSibling);
console.log("insert sibling.nextSibling", sibling.nextSibling);
} else {
pagesContainer.appendChild(imgContainer)
console.log("append");
}
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.src = await renderer.renderPage(i);
img.pageIdx = i;
img.rend = renderer;
img.doc = pdfDocument;
div.appendChild(img);
const buttonContainer = document.createElement('div');
buttonContainer.classList.add("button-container");
const moveUp = document.createElement('button');
moveUp.classList.add("move-up-button","btn", "btn-secondary");
moveUp.innerHTML = '<i class="bi bi-arrow-up-short"></i>';
moveUp.onclick = moveUpButtonCallback;
buttonContainer.appendChild(moveUp);
const moveDown = document.createElement('button');
moveDown.classList.add("move-down-button","btn", "btn-secondary");
moveDown.innerHTML = '<i class="bi bi-arrow-down-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-secondary");
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", "align-top");
const insertFileButton = document.createElement('button');
insertFileButton.classList.add("btn", "btn-primary", "insert-file-button", "align-center-right");
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);
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, ''));
console.log(img.style.rotate, img.style.rotate.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 downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = fileName ? fileName : 'managed.pdf';
downloadLink.click(); // Simulate a click on the download link to start the download
}
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);
position: sticky;
top: 10px;
z-index: 10;
padding: 10px;
margin-top: 30px;
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 {
width: 100%;
display: flex;
gap: 0px;
flex-direction: column;
align-items: center;
margin: 30px 0;
}
.page-container {
width: 500px;
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: 0 0 10px rgba(0,0,0);
transition: rotate 0.3s;
}
.page-container .button-container {
position: absolute;
top: 50%;
translate: 0 -50%;
right: 6px;
}
.page-container .button-container > * {
padding: 0.25rem 0.5rem;
margin: 3px;
display: block;
}
.page-container svg {
width: 16px;
height: 16px;
}
.page-container:nth-child(2) .move-up-button {
visibility: hidden;
}
.page-container:last-child .move-down-button {
visibility: hidden;
}
/* "insert pdf" buttons that appear on the right when hover */
.insert-file-button-container {
translate: 0 -50%;
width: 100%;
height: 60px;
z-index: 1;
opacity: 0;
transition: opacity 0.2s;
}
.insert-file-button-container:hover {
opacity: 1;
transition: opacity 0.05s;
}
.insert-file-button {
translate: 100% -50%;
rotate: 45deg;
aspect-ratio: 1;
border-radius: 100px 100px 100px 0;
}
.insert-file-button > * {
rotate: -45deg;
}
#add-pdf-button {
margin: 0 auto;
}
</style>
</body>
</html>