Merge branch 'main' of git@github.com:Frooodle/Stirling-PDF.git into
main
This commit is contained in:
commit
a34c2863bd
7 changed files with 553 additions and 86 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
|
|
@ -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/)
|
||||
[![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)
|
||||
[![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.
|
||||
|
||||
|
@ -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)
|
||||
- HTML, CSS, JavaScript
|
||||
- Docker
|
||||
- PDF.js
|
||||
- PDF-LIB.js
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
|
@ -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.desc=Convertir formatos PDF a Word (DOC, DOCX y ODT)
|
||||
|
||||
home.PDFToPresentation.title=PDF a presentación
|
||||
home.PDFToPresentation.desc=Convertir PDF a formatos de presentación (PPT, PPTX y ODP)
|
||||
home.PDFToPresentation.title=PDF a presentación
|
||||
home.PDFToPresentation.desc=Convertir PDF a formatos de presentación (PPT, PPTX y ODP)
|
||||
|
||||
home.PDFToText.title=PDF a texto/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.title=OCR / Escaneo de limpieza
|
||||
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.SeleccionaText.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.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.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.SeleccionaText.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.SeleccionaText.8=Normal (Se producirá un error si el PDF contiene texto)
|
||||
ocr.SeleccionaText.9=Ajustes Adicionales
|
||||
ocr.SeleccionaText.10=Modo OCR
|
||||
ocr.selectText.1=Selecciona los idiomas que se detectarán en el PDF (Los enumerados son los detectados actualmente):
|
||||
ocr.selectText.2=Produzca un archivo de texto que contenga texto OCR junto con el PDF editado con OCR
|
||||
ocr.selectText.3=Corrija las páginas que se escanearon en un ángulo torcido girándolas nuevamente a su lugar
|
||||
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.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.selectText.6=Ignora las páginas que tienen texto interactivo, solo las páginas OCR que son imágenes
|
||||
ocr.selectText.7=Fuerza OCR, OCR eliminará en cada página todo el texto original
|
||||
ocr.selectText.8=Normal (Se producirá un error si el PDF contiene texto)
|
||||
ocr.selectText.9=Ajustes Adicionales
|
||||
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.credit=Este servicio utiliza OCRmyPDF y Tesseract para OCR.
|
||||
ocr.submit=Procesa PDF con OCR
|
||||
|
@ -135,7 +135,7 @@ ocr.submit=Procesa PDF con OCR
|
|||
|
||||
extractImages.title=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
|
||||
|
||||
|
||||
|
@ -151,13 +151,13 @@ fileToPDF.submit=Convertir a PDF
|
|||
compress.title=Comprimir
|
||||
compress.header=Comprimir PDF
|
||||
compress.credit=Este servicio usa OCRmyPDF para la Compresión/Optimizatión del PDF.
|
||||
compress.SeleccionaText.1=Nivel de Optimización:
|
||||
compress.SeleccionaText.2=0 (Sin optimización)
|
||||
compress.SeleccionaText.3=1 (Por defecto, optimización sin pérdidas)
|
||||
compress.SeleccionaText.4=2 (Optimización con pérdida)
|
||||
compress.SeleccionaText.5=3 (Optimización con pérdida, más agresiva)
|
||||
compress.SeleccionaText.6=Habilita la vista web rápida (linealizar PDF)
|
||||
compress.SeleccionaText.7=Habilita la codificación JBIG2 con pérdida
|
||||
compress.selectText.1=Nivel de Optimización:
|
||||
compress.selectText.2=0 (Sin optimización)
|
||||
compress.selectText.3=1 (Por defecto, optimización sin pérdidas)
|
||||
compress.selectText.4=2 (Optimización con pérdida)
|
||||
compress.selectText.5=3 (Optimización con pérdida, más agresiva)
|
||||
compress.selectText.6=Habilita la vista web rápida (linealizar PDF)
|
||||
compress.selectText.7=Habilita la codificación JBIG2 con pérdida
|
||||
compress.submit=Comprimir
|
||||
|
||||
|
||||
|
@ -169,9 +169,9 @@ addImage.submit=Añade imagen
|
|||
|
||||
|
||||
#merge
|
||||
merge.title=Mezcla
|
||||
merge.header=Mezcla múltiples PDFs (2+)
|
||||
merge.submit=Mezcla
|
||||
merge.title=Une
|
||||
merge.header=Une múltiples PDFs (2+)
|
||||
merge.submit=Une
|
||||
|
||||
#pdfOrganiser
|
||||
pdfOrganiser.title=Organizador de páginas
|
||||
|
@ -199,13 +199,13 @@ split.title=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.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.4=Documento #2: Page 2 and 3
|
||||
split.desc.5=Documento #3: Page 4, 5 and 6
|
||||
split.desc.6=Documento #4: Page 7
|
||||
split.desc.7=Documento #5: Page 8
|
||||
split.desc.8=Documento #6: Page 9 and 10
|
||||
split.splitPages=Introduzca las páginas para dividir en:
|
||||
split.desc.3=Documento #1: Página 1
|
||||
split.desc.4=Documento #2: Páginas 2 y 3
|
||||
split.desc.5=Documento #3: Páginas 4, 5 y 6
|
||||
split.desc.6=Documento #4: Página 7
|
||||
split.desc.7=Documento #5: Página 8
|
||||
split.desc.8=Documento #6: Páginas 9 y 10
|
||||
split.splitPages=Introduzca las páginas para dividir:
|
||||
split.submit=Dividir
|
||||
|
||||
|
||||
|
@ -213,16 +213,16 @@ split.submit=Dividir
|
|||
imageToPDF.title=Imagen a PDF
|
||||
imageToPDF.header=Imagen a PDF
|
||||
imageToPDF.submit=Convertir
|
||||
imageToPDF.SeleccionaText.1=Estirar para ajustar
|
||||
imageToPDF.SeleccionaText.2=Auto rotación PDF
|
||||
imageToPDF.SeleccionaText.3=Lógica de archivos múltiples (Únicamente activado si funciona con multiples imágenes)
|
||||
imageToPDF.SeleccionaText.4=Une en un único PDF
|
||||
imageToPDF.SeleccionaText.5=Convertir a PDFs separados
|
||||
imageToPDF.selectText.1=Estirar para ajustar
|
||||
imageToPDF.selectText.2=Auto rotación PDF
|
||||
imageToPDF.selectText.3=Lógica de archivos múltiples (Únicamente activado si funciona con multiples imágenes)
|
||||
imageToPDF.selectText.4=Une en un único PDF
|
||||
imageToPDF.selectText.5=Convertir a PDFs separados
|
||||
|
||||
#pdfToImage
|
||||
pdfToImage.title=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.single=Imagen Grande Única
|
||||
pdfToImage.multi=Múltiples Imágenes
|
||||
|
@ -235,68 +235,68 @@ pdfToImage.submit=Convertir
|
|||
#addPassword
|
||||
addPassword.title=Añade Contraseña
|
||||
addPassword.header=Añade contraseña (Encripta)
|
||||
addPassword.SeleccionaText.1=Selecciona PDF para encriptar
|
||||
addPassword.SeleccionaText.2=Contraseña
|
||||
addPassword.SeleccionaText.3=Longitud de la clave de cifrado
|
||||
addPassword.SeleccionaText.4=Valores altos son más fuertes, pero valores bajos tienen mejor compatibilidad.
|
||||
addPassword.SeleccionaText.5=Permisos para establecer
|
||||
addPassword.SeleccionaText.6=Impedir el ensamblaje del documento
|
||||
addPassword.SeleccionaText.7=Impedir la extracción de contenido
|
||||
addPassword.SeleccionaText.8=Impedir la extracción para la accesibilidad
|
||||
addPassword.SeleccionaText.9=Impedir rellenar formulario
|
||||
addPassword.SeleccionaText.10=Impedir modificación
|
||||
addPassword.SeleccionaText.11=Impedir modificación de anotaciones
|
||||
addPassword.SeleccionaText.12=Impedir imprimir
|
||||
addPassword.SeleccionaText.13=Impedir imprimir diferentes formatos
|
||||
addPassword.selectText.1=Selecciona PDF para encriptar
|
||||
addPassword.selectText.2=Contraseña
|
||||
addPassword.selectText.3=Longitud de la clave de cifrado
|
||||
addPassword.selectText.4=Valores altos son más fuertes, pero valores bajos tienen mejor compatibilidad.
|
||||
addPassword.selectText.5=Permisos para establecer
|
||||
addPassword.selectText.6=Impedir el ensamblaje del documento
|
||||
addPassword.selectText.7=Impedir la extracción de contenido
|
||||
addPassword.selectText.8=Impedir la extracción para la accesibilidad
|
||||
addPassword.selectText.9=Impedir rellenar formulario
|
||||
addPassword.selectText.10=Impedir modificación
|
||||
addPassword.selectText.11=Impedir modificación de anotaciones
|
||||
addPassword.selectText.12=Impedir imprimir
|
||||
addPassword.selectText.13=Impedir imprimir diferentes formatos
|
||||
addPassword.submit=Encripta
|
||||
|
||||
#watermark
|
||||
watermark.title=Añade marca de agua
|
||||
watermark.header=Añade marca de agua
|
||||
watermark.SeleccionaText.1=Selecciona PDF para añadir marca de agua:
|
||||
watermark.SeleccionaText.2=Texto de la marca de agua:
|
||||
watermark.SeleccionaText.3=Tamaño de la Fuente:
|
||||
watermark.SeleccionaText.4=Rotación (0-360):
|
||||
watermark.SeleccionaText.5=Ancho (Espacio entre cada marca de agua horizontalmente):
|
||||
watermark.SeleccionaText.6=Alto (Espacio entre cada marca de agua verticalmente):
|
||||
watermark.SeleccionaText.7=Opacidad (0% - 100%):
|
||||
watermark.selectText.1=Selecciona PDF para añadir marca de agua:
|
||||
watermark.selectText.2=Texto de la marca de agua:
|
||||
watermark.selectText.3=Tamaño de la Fuente:
|
||||
watermark.selectText.4=Rotación (0-360):
|
||||
watermark.selectText.5=Ancho (Espacio entre cada marca de agua horizontalmente):
|
||||
watermark.selectText.6=Alto (Espacio entre cada marca de agua verticalmente):
|
||||
watermark.selectText.7=Opacidad (0% - 100%):
|
||||
watermark.submit=Añade marca de agua
|
||||
|
||||
#remove-watermark
|
||||
remove-watermark.title=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.SeleccionaText.2=Texto de la marca de agua:
|
||||
remove-watermark.selectText.1=Selecciona PDF para eliminar la marca de agua:
|
||||
remove-watermark.selectText.2=Texto de la marca de agua:
|
||||
remove-watermark.submit=Elimina marca de agua
|
||||
|
||||
#Change permissions
|
||||
permissions.title=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.SeleccionaText.1=Selecciona PDF para cambiar los permisos
|
||||
permissions.SeleccionaText.2=Permisos a establecer
|
||||
permissions.SeleccionaText.3=Impedir el ensamblaje del documento
|
||||
permissions.SeleccionaText.4=Impedir la extracción de contenido
|
||||
permissions.SeleccionaText.5=Impedir la extracción para la accesibilidad
|
||||
permissions.SeleccionaText.6=Impedir rellenar formulario
|
||||
permissions.SeleccionaText.7=Impedir modificación
|
||||
permissions.SeleccionaText.8=Impedir modificación de anotaciones
|
||||
permissions.SeleccionaText.9=Impedir imprimir
|
||||
permissions.SeleccionaText.10=Impedir imprimir diferentes formatos
|
||||
permissions.selectText.1=Selecciona PDF para cambiar los permisos
|
||||
permissions.selectText.2=Permisos a establecer
|
||||
permissions.selectText.3=Impedir el ensamblaje del documento
|
||||
permissions.selectText.4=Impedir la extracción de contenido
|
||||
permissions.selectText.5=Impedir la extracción para la accesibilidad
|
||||
permissions.selectText.6=Impedir rellenar formulario
|
||||
permissions.selectText.7=Impedir modificación
|
||||
permissions.selectText.8=Impedir modificación de anotaciones
|
||||
permissions.selectText.9=Impedir imprimir
|
||||
permissions.selectText.10=Impedir imprimir diferentes formatos
|
||||
permissions.submit=Cambiar
|
||||
|
||||
#remove password
|
||||
removePassword.title=Elimina contraseña
|
||||
removePassword.header=Elimina contraseña (Desencripta)
|
||||
removePassword.SeleccionaText.1=Selecciona PDF para Desencriptar
|
||||
removePassword.SeleccionaText.2=Contraseña
|
||||
removePassword.selectText.1=Selecciona PDF para Desencriptar
|
||||
removePassword.selectText.2=Contraseña
|
||||
removePassword.submit=Elimina
|
||||
|
||||
changeMetadata.title=Cambia Metadatos
|
||||
changeMetadata.header=Cambia Metadatos
|
||||
changeMetadata.SeleccionaText.1=Edite las variables que desea cambiar
|
||||
changeMetadata.SeleccionaText.2=Elimina todos los metadatos
|
||||
changeMetadata.SeleccionaText.3=Mostrar metadatos personalizados:
|
||||
changeMetadata.selectText.1=Edite las variables que desea cambiar
|
||||
changeMetadata.selectText.2=Elimina todos los metadatos
|
||||
changeMetadata.selectText.3=Mostrar metadatos personalizados:
|
||||
changeMetadata.author=Autor:
|
||||
changeMetadata.creationDate=Fecha de Creación (yyyy/MM/dd HH:mm:ss):
|
||||
changeMetadata.creator=Creador:
|
||||
|
@ -306,13 +306,13 @@ changeMetadata.producer=Productor:
|
|||
changeMetadata.subject=Asunto:
|
||||
changeMetadata.title=Título:
|
||||
changeMetadata.trapped=Trapped:
|
||||
changeMetadata.SeleccionaText.4=Otros Metadatos:
|
||||
changeMetadata.SeleccionaText.5=Agregar entrada de metadatos personalizados
|
||||
changeMetadata.selectText.4=Otros Metadatos:
|
||||
changeMetadata.selectText.5=Agregar entrada de metadatos personalizados
|
||||
changeMetadata.submit=Cambia
|
||||
|
||||
xlsToPdf.title=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
|
||||
|
||||
|
||||
|
@ -328,29 +328,29 @@ pdfToPDFA.submit=Convertir
|
|||
PDFToWord.title=PDF a Word
|
||||
PDFToWord.header=PDF a Word
|
||||
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
|
||||
|
||||
PDFToPresentation.title=PDF a presentación
|
||||
PDFToPresentation.header=PDF a presentación
|
||||
PDFToPresentation.title=PDF a presentación
|
||||
PDFToPresentation.header=PDF a presentación
|
||||
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
|
||||
|
||||
|
||||
PDFToText.title=PDF a texto/RTF
|
||||
PDFToText.header=PDF a texto/RTF
|
||||
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
|
||||
|
||||
|
||||
PDFToHTML.title=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
|
||||
|
||||
PDFToXML.title=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
|
||||
|
|
|
@ -21,3 +21,17 @@ html[lang-direction=rtl] * {
|
|||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-top {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.align-center-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
}
|
||||
.align-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<script src="pdfjs/pdf.js"></script>
|
||||
|
||||
<!-- PDF-Lib -->
|
||||
<script src="pdf-lib.min.js"></script>
|
||||
<script src="js/pdf-lib.min.js"></script>
|
||||
|
||||
<!-- Custom -->
|
||||
<link rel="stylesheet" href="css/general.css">
|
||||
|
|
450
src/main/resources/templates/page-manager.html
Normal file
450
src/main/resources/templates/page-manager.html
Normal 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>
|
Loading…
Reference in a new issue