Compare commits

...

46 Commits

Author SHA1 Message Date
Anthony Stirling
bf995f989c fixes 2023-07-26 13:00:06 +01:00
systo
c14aa6851e Merge branch 'main' of git@github.com:Frooodle/Stirling-PDF.git into main 2023-07-25 23:36:32 +01:00
Anthony Stirling
8260eced2d auto split cleanup 2023-07-25 23:36:19 +01:00
Anthony Stirling
d028465dc5 Update README.md 2023-07-25 21:21:43 +01:00
Anthony Stirling
29dab5e47d numbers name 2023-07-25 00:01:20 +01:00
Anthony Stirling
9e655631b4 lang cleanup 2023-07-24 23:53:52 +01:00
Anthony Stirling
179c7b80bb languages 2023-07-24 23:12:33 +01:00
Anthony Stirling
349bf29122 fix navbar not supporting other languages nicely size wise 2023-07-23 23:20:30 +01:00
Anthony Stirling
295357f12b tags and searching 2023-07-23 23:05:02 +01:00
Anthony Stirling
940f8d999e Update messages_en_GB.properties 2023-07-23 15:13:33 +01:00
Anthony Stirling
5605d53a5f Update messages_en_GB.properties 2023-07-23 13:05:47 +01:00
Anthony Stirling
116d103119 html to pdf 2023-07-23 00:03:25 +01:00
Anthony Stirling
2fd8c643af UI for html/url 2023-07-22 17:27:08 +01:00
Anthony Stirling
4367ae7934 html and url to pdf init 2023-07-22 16:57:40 +01:00
Anthony Stirling
749461334d pdfjs worker changes and crop fix 2023-07-22 13:17:24 +01:00
Anthony Stirling
e83a027023 Merge pull request #266 from webysther/patch-1
Update HowToUseOCR.md
2023-07-19 23:35:20 +01:00
Anthony Stirling
1883b477a3 Merge branch 'main' into patch-1 2023-07-19 23:33:46 +01:00
Anthony Stirling
37e2cd40da Merge pull request #276 from Frooodle/pipelineStuff
Start of v0.11
2023-07-19 23:26:57 +01:00
Anthony Stirling
81a9329975 border to contrast 2023-07-19 23:23:08 +01:00
Anthony Stirling
0eb019fc3c searchbar cleanups 2023-07-19 23:15:16 +01:00
Anthony Stirling
4129c75475 crop fix, auto split docs and UI and message 2023-07-19 22:11:59 +01:00
Anthony Stirling
3d66f03f58 hide pipeline 2023-07-18 22:09:48 +01:00
Anthony Stirling
7b83104fd6 fix for #275 2023-07-18 22:04:18 +01:00
Anthony Stirling
794aede27f docs 2023-07-17 21:59:34 +01:00
Anthony Stirling
08eb39b206 divider examples 2023-07-16 23:52:09 +01:00
Anthony Stirling
2566c7f3d7 translations 2023-07-16 19:42:13 +01:00
Anthony Stirling
a8522bb3b5 GB pretty 2023-07-16 19:34:01 +01:00
Anthony Stirling
92b9142902 language cleanups and sanitize 2023-07-16 18:57:21 +01:00
Anthony Stirling
d07e3e6522 change add numbers grid and remove files from pipelines 2023-07-16 16:07:08 +01:00
Anthony Stirling
29aabdfba8 filter 2023-07-16 00:36:58 +01:00
Anthony Stirling
9af1b0cfdc some more changes also broke pipeline a bit 2023-07-15 16:06:33 +01:00
Anthony Stirling
6e32c7fe85 auto split init 2023-07-15 11:39:10 +01:00
Anthony Stirling
ddf5915c6a drag drop niceness 2023-07-13 22:03:23 +01:00
Anthony Stirling
cdbf1fa73a watermark features 2023-07-12 23:27:36 +01:00
Anthony Stirling
5d926b022b gitingore 2023-07-12 00:17:55 +01:00
Anthony Stirling
50bcca10e2 pipeline stuff 2023-07-12 00:17:44 +01:00
Webysther Sperandio
a5528c06ee Update HowToUseOCR.md 2023-07-10 13:49:58 +02:00
Anthony Stirling
94526de04b sign fix 2023-07-09 20:34:07 +01:00
Anthony Stirling
1ddf7abe6f extra fonts plus dynamic fonts 2023-07-09 19:36:41 +01:00
Anthony Stirling
a742c1b034 stuff 2023-07-09 18:10:10 +01:00
Anthony Stirling
6e726ac2a6 lots of stuff and garbage code for automate to cleanup lots 2023-07-09 00:05:33 +01:00
Anthony Stirling
5877b40be5 adjust contrast! 2023-07-06 22:52:22 +01:00
Anthony Stirling
a3c7f5aa46 Search bar and adjust contrast 2023-07-05 22:21:43 +01:00
Anthony Stirling
4e28bf03bd auto rename 2023-07-04 23:25:21 +01:00
Anthony Stirling
f92482d89e page numbers and custom images 2023-07-04 21:45:35 +01:00
Anthony Stirling
3c54429fe0 Update README.md 2023-07-02 19:19:49 +01:00
94 changed files with 17369 additions and 9625 deletions

235
.gitignore vendored
View File

@@ -1,115 +1,122 @@
### Eclipse ### ### Eclipse ###
.metadata .metadata
bin/ bin/
tmp/ tmp/
*.tmp *.tmp
*.bak *.bak
*.swp *.swp
*~.nib *~.nib
local.properties local.properties
.settings/ .settings/
.loadpath .loadpath
.recommenders .recommenders
.classpath .classpath
.project .project
version.properties version.properties
pipeline/
# Gradle
.gradle #### Stirling-PDF Files ###
.lock customFiles/
config/
# External tool builders watchedFolders/
.externalToolBuilders/
# Locally stored "Eclipse launch configurations" # Gradle
*.launch .gradle
.lock
# PyDev specific (Python IDE for Eclipse)
*.pydevproject # External tool builders
.externalToolBuilders/
# CDT-specific (C/C++ Development Tooling)
.cproject # Locally stored "Eclipse launch configurations"
*.launch
# CDT- autotools
.autotools # PyDev specific (Python IDE for Eclipse)
*.pydevproject
# Java annotation processor (APT)
.factorypath # CDT-specific (C/C++ Development Tooling)
.cproject
# PDT-specific (PHP Development Tools)
.buildpath # CDT- autotools
.autotools
# sbteclipse plugin
.target # Java annotation processor (APT)
.factorypath
# Tern plugin
.tern-project # PDT-specific (PHP Development Tools)
.buildpath
# TeXlipse plugin
.texlipse # sbteclipse plugin
.target
# STS (Spring Tool Suite)
.springBeans # Tern plugin
.tern-project
# Code Recommenders
.recommenders/ # TeXlipse plugin
.texlipse
# Annotation Processing
.apt_generated/ # STS (Spring Tool Suite)
.apt_generated_test/ .springBeans
# Scala IDE specific (Scala & Java development for Eclipse) # Code Recommenders
.cache-main .recommenders/
.scala_dependencies
.worksheet # Annotation Processing
.apt_generated/
# Uncomment this line if you wish to ignore the project description file. .apt_generated_test/
# Typically, this file would be tracked if it contains build/dependency configurations:
#.project # Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
### Eclipse Patch ### .scala_dependencies
# Spring Boot Tooling .worksheet
.sts4-cache/
# Uncomment this line if you wish to ignore the project description file.
### Git ### # Typically, this file would be tracked if it contains build/dependency configurations:
# Created by git for backups. To disable backups in Git: #.project
# $ git config --global mergetool.keepBackup false
*.orig ### Eclipse Patch ###
# Spring Boot Tooling
# Created by git when using merge tools for conflicts .sts4-cache/
*.BACKUP.*
*.BASE.* ### Git ###
*.LOCAL.* # Created by git for backups. To disable backups in Git:
*.REMOTE.* # $ git config --global mergetool.keepBackup false
*_BACKUP_*.txt *.orig
*_BASE_*.txt
*_LOCAL_*.txt # Created by git when using merge tools for conflicts
*_REMOTE_*.txt *.BACKUP.*
*.BASE.*
### Java ### *.LOCAL.*
# Compiled class file *.REMOTE.*
*.class *_BACKUP_*.txt
*_BASE_*.txt
# Log file *_LOCAL_*.txt
*.log *_REMOTE_*.txt
# BlueJ files ### Java ###
*.ctxt # Compiled class file
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/ # Log file
*.log
# Package Files #
*.jar # BlueJ files
*.war *.ctxt
*.nar
*.ear # Mobile Tools for Java (J2ME)
*.zip .mtj.tmp/
*.tar.gz
*.rar # Package Files #
*.jar
/build *.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
/build
/.vscode /.vscode

View File

@@ -1,5 +1,5 @@
# Build jbig2enc in a separate stage # Build jbig2enc in a separate stage
FROM frooodle/stirling-pdf-base:latest FROM frooodle/stirling-pdf-base:beta4
# Create scripts folder and copy local scripts # Create scripts folder and copy local scripts
RUN mkdir /scripts RUN mkdir /scripts

View File

@@ -10,6 +10,12 @@ RUN apt-get update && \
unoconv && \ unoconv && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
#Install fonts
RUN mkdir /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
RUN fc-cache -f -v
# Copy the application JAR file # Copy the application JAR file
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar

View File

@@ -29,7 +29,7 @@ RUN apt-get update && \
libjpeg-dev && \ libjpeg-dev && \
pip install --upgrade pip && \ pip install --upgrade pip && \
pip install --no-cache-dir \ pip install --no-cache-dir \
opencv-python-headless && \ opencv-python-headless WeasyPrint && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Final stage: Copy necessary files from the previous stage # Final stage: Copy necessary files from the previous stage

View File

@@ -1,35 +1,41 @@
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript | | Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------| |---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | | | adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | | | auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ | | crop | ✔️ | | | | | | | | | ✔️ | |
| remove-pages | ✔️ | | | | | | | | | ✔️ | | | merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | | | multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
| scale-pages | ✔️ | | | | | | | | | ✔️ | | | pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
| split-pdfs | ✔️ | | | | | | | | | ✔️ | | | remove-pages | ✔️ | | | | | | | | | ✔️ | |
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | | | rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | scale-pages | ✔️ | | | | | | | | | ✔️ | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | split-pdfs | ✔️ | | | | | | | | | ✔️ | |
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | | | file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
| add-password | | | ✔️ | | | | | | | ✔️ | | | pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
| add-watermark | | | ✔️ | | | | | | | ✔️ | | | pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
| cert-sign | | | ✔️ | | | | | | | ✔️ | | | xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| change-permissions | | | ✔️ | | | | | | | ✔️ | | | add-password | | | ✔️ | | | | | | | ✔️ | |
| remove-password | | | ✔️ | | | | | | | ✔️ | | | add-watermark | | | ✔️ | | | | | | | ✔️ | |
| add-image | | | | ✔️ | | | | | | ✔️ | | | cert-sign | | | ✔️ | | | | | | | ✔️ | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | change-permissions | | | ✔️ | | | | | | | ✔️ | |
| compare | | | | ✔️ | | | | | | | ✔️ | | remove-password | | | ✔️ | | | | | | | ✔️ | |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | add-image | | | | ✔️ | | | | | | ✔️ | |
| extract-images | | | | ✔️ | | | | | | ✔️ | | | add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
| flatten | | | | ✔️ | | | | | | | | | auto-rename | | | | ✔️ | | | | | | ✔️ | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | change-metadata | | | | ✔️ | | | | | | ✔️ | |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | compare | | | | ✔️ | | | | | | | ✔️ |
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
| sign | | | | ✔️ | | | | | | | ✔️ | | extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | |
| flatten | | | | ✔️ | | | | | | | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
| sign | | | | ✔️ | | | | | | | ✔️ |

View File

@@ -3,7 +3,7 @@
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker. This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
## How does the OCR Work ## How does the OCR Work
Stirling-PDF uses OCRmyPDF which in turn uses tesseract for its text recognition. Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
All credit goes to them for this awesome work! All credit goes to them for this awesome work!
## Language Packs ## Language Packs

View File

@@ -27,6 +27,11 @@ Feel free to request any features or bug fixes either in github issues or our [D
- Convert PDFs to and from images - Convert PDFs to and from images
- Reorganize PDF pages into different orders. - Reorganize PDF pages into different orders.
- Add/Generate signatures - Add/Generate signatures
- Format PDFs into a multi-paged page
- Scale page contents size by set %
- Adjust Contrast
- Crop PDF
- Auto Split PDF (With physically scanned page dividers)
- Flatten PDFs - Flatten PDFs
- Repair PDFs - Repair PDFs
- Detect and remove blank pages - Detect and remove blank pages
@@ -39,8 +44,14 @@ Feel free to request any features or bug fixes either in github issues or our [D
- Add watermark(s) - Add watermark(s)
- Convert Any common file to PDF (using LibreOffice) - Convert Any common file to PDF (using LibreOffice)
- Convert PDF to Word/Powerpoint/Others (using LibreOffice) - Convert PDF to Word/Powerpoint/Others (using LibreOffice)
- Convert HTML to PDF
- URL to PDF
- Extract images from PDF - Extract images from PDF
- Extract images from Scans
- Add page numbers
- Auto rename file by detecting PDF header text
- OCR on PDF (Using OCRMyPDF) - OCR on PDF (Using OCRMyPDF)
- PDF/A conversion (Using OCRMyPDF)
- Edit metadata - Edit metadata
- Dark mode support. - Dark mode support.
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example) - Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example)
@@ -86,6 +97,8 @@ docker run -d \
Can also add these for customisation but are not required Can also add these for customisation but are not required
-v /location/of/extraConfigs:/configs \
-v /location/of/customFiles:/customFiles \
-e APP_HOME_NAME="Stirling PDF" \ -e APP_HOME_NAME="Stirling PDF" \
-e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \ -e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \
-e APP_NAVBAR_NAME="Stirling PDF" \ -e APP_NAVBAR_NAME="Stirling PDF" \
@@ -104,6 +117,7 @@ services:
volumes: volumes:
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages - /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages
# - /location/of/extraConfigs:/configs # - /location/of/extraConfigs:/configs
# - /location/of/customFiles:/customFiles/
# environment: # environment:
# APP_LOCALE: en_GB # APP_LOCALE: en_GB
# APP_HOME_NAME: Stirling PDF # APP_HOME_NAME: Stirling PDF
@@ -160,11 +174,12 @@ Using the same method you can also change
- Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visiblility. - Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visiblility.
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app - Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md) - Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md)
- Change the max file size allowed through the server with the environment variable MAX_FILE_SIZE. default 2000MB
- Customise static files such as app logo by placing files in the /customFiles/static/ directory. Example to customise app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
## API ## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation [here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## FAQ ## FAQ

View File

@@ -1,48 +1,54 @@
|Technology | Ultra-Lite | Lite | Full | |Technology | Ultra-Lite | Lite | Full |
|----------------|:----------:|:----:|:----:| |----------------|:----------:|:----:|:----:|
| Java | ✔️ | ✔️ | ✔️ | | Java | ✔️ | ✔️ | ✔️ |
| JavaScript | ✔️ | ✔️ | ✔️ | | JavaScript | ✔️ | ✔️ | ✔️ |
| Libre | | ✔️ | ✔️ | | Libre | | ✔️ | ✔️ |
| Python | | | ✔️ | | Python | | | ✔️ |
| OpenCV | | | ✔️ | | OpenCV | | | ✔️ |
| OCRmyPDF | | | ✔️ | | OCRmyPDF | | | ✔️ |
Operation | Ultra-Lite | Lite | Full Operation | Ultra-Lite | Lite | Full
--------------------|------------|------|----- --------------------|------------|------|-----
add-password | ✔️ | ✔️ | ✔️ add-page-numbers | ✔️ | ✔️ | ✔️
add-watermark | ✔️ | ✔️ | ✔️ add-password | ✔️ | ✔️ | ✔️
cert-sign | ✔️ | ✔️ | ✔️ add-watermark | ✔️ | ✔️ | ✔️
change-metadata | ✔️ | ✔️ | ✔️ adjust-contrast | ✔️ | ✔️ | ✔️
change-permissions | ✔️ | ✔️ | ✔️ auto-split-pdf | ✔️ | ✔️ | ✔️
compare | ✔️ | ✔️ | ✔️ auto-rename | ✔️ | ✔️ | ✔️
extract-images | ✔️ | ✔️ | ✔️ cert-sign | ✔️ | ✔️ | ✔️
flatten | ✔️ | ✔️ | ✔️ crop | ✔️ | ✔️ | ✔️
img-to-pdf | ✔️ | ✔️ | ✔️ change-metadata | ✔️ | ✔️ | ✔️
merge-pdfs | ✔️ | ✔️ | ✔️ change-permissions | ✔️ | ✔️ | ✔️
multi-page-layout | ✔️ | ✔️ | ✔️ compare | ✔️ | ✔️ | ✔️
pdf-organizer | ✔️ | ✔️ | ✔️ extract-images | ✔️ | ✔️ | ✔️
pdf-to-img | ✔️ | ✔️ | ✔️ flatten | ✔️ | ✔️ | ✔️
remove-pages | ✔️ | ✔️ | ✔️ img-to-pdf | ✔️ | ✔️ | ✔️
remove-password | ✔️ | ✔️ | ✔️ merge-pdfs | ✔️ | ✔️ | ✔️
rotate-pdf | ✔️ | ✔️ | ✔️ multi-page-layout | ✔️ | ✔️ | ✔️
scale-pages | ✔️ | ✔️ | ✔️ pdf-organizer | ✔️ | ✔️ | ✔️
sign | ✔️ | ✔️ | ✔️ pdf-to-img | ✔️ | ✔️ | ✔️
split-pdfs | ✔️ | ✔️ | ✔️ remove-pages | ✔️ | ✔️ | ✔️
add-image | ✔️ | ✔️ | ✔️ remove-password | ✔️ | ✔️ | ✔️
file-to-pdf | | ✔️ | ✔️ rotate-pdf | ✔️ | ✔️ | ✔️
pdf-to-html | | ✔️ | ✔️ sanitize-pdf | ✔️ | ✔️ | ✔️
pdf-to-presentation | | ✔️ | ✔️ scale-pages | ✔️ | ✔️ | ✔️
pdf-to-text | | ✔️ | ✔️ sign | ✔️ | ✔️ | ✔️
pdf-to-word | | ✔️ | ✔️ split-pdfs | ✔️ | ✔️ | ✔️
pdf-to-xml | | ✔️ | ✔️ add-image | ✔️ | ✔️ | ✔️
repair | | ✔️ | ✔️ file-to-pdf | | ✔️ | ✔️
xlsx-to-pdf | | ✔️ | ✔️ pdf-to-html | | ✔️ | ✔️
compress-pdf | | | ✔️ pdf-to-presentation | | ✔️ | ✔️
extract-image-scans | | | ✔️ pdf-to-text | | ✔️ | ✔️
ocr-pdf | | | ✔️ pdf-to-word | | ✔️ | ✔️
pdf-to-pdfa | | | ✔️ pdf-to-xml | | ✔️ | ✔️
remove-blanks | | | ✔️ repair | | ✔️ | ✔️
xlsx-to-pdf | | ✔️ | ✔️
compress-pdf | | | ✔️
extract-image-scans | | | ✔️
ocr-pdf | | | ✔️
pdf-to-pdfa | | | ✔️
remove-blanks | | | ✔️

View File

@@ -8,7 +8,7 @@ plugins {
} }
group = 'stirling.software' group = 'stirling.software'
version = '0.10.3' version = '0.11.1'
sourceCompatibility = '17' sourceCompatibility = '17'
repositories { repositories {
@@ -62,6 +62,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core' implementation 'io.micrometer:micrometer-core'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
} }

80
scripts/PropSync.java Normal file
View File

@@ -0,0 +1,80 @@
package stirling.software.Stirling.Stats;
import java.nio.file.*;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.io.*;
import java.util.*;
public class PropSync {
public static void main(String[] args) throws IOException {
File folder = new File("C:\\Users\\systo\\git\\Stirling-PDF\\src\\main\\resources");
File[] files = folder.listFiles((dir, name) -> name.matches("messages_.*\\.properties"));
List<String> enLines = Files.readAllLines(Paths.get(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
Map<String, String> enProps = linesToProps(enLines);
for (File file : files) {
if (!file.getName().equals("messages_en_GB.properties")) {
System.out.println("Processing file: " + file.getName());
List<String> lines;
try {
lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
} catch (MalformedInputException e) {
System.out.println("Skipping due to not UTF8 format for file: " + file.getName());
continue;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Map<String, String> currentProps = linesToProps(lines);
List<String> newLines = syncPropsWithLines(enProps, currentProps, enLines);
Files.write(file.toPath(), newLines, StandardCharsets.UTF_8);
System.out.println("Finished processing file: " + file.getName());
}
}
}
private static Map<String, String> linesToProps(List<String> lines) {
Map<String, String> props = new LinkedHashMap<>();
for (String line : lines) {
if (!line.trim().isEmpty() && line.contains("=")) {
String[] parts = line.split("=", 2);
props.put(parts[0].trim(), parts[1].trim());
}
}
return props;
}
private static List<String> syncPropsWithLines(Map<String, String> enProps, Map<String, String> currentProps, List<String> enLines) {
List<String> newLines = new ArrayList<>();
boolean needsTranslateComment = false; // flag to check if we need to add "TODO: Translate"
for (String line : enLines) {
if (line.contains("=")) {
String key = line.split("=", 2)[0].trim();
if (currentProps.containsKey(key)) {
newLines.add(key + "=" + currentProps.get(key));
needsTranslateComment = false;
} else {
if (!needsTranslateComment) {
newLines.add("##########################");
newLines.add("### TODO: Translate ###");
newLines.add("##########################");
needsTranslateComment = true;
}
newLines.add(line);
}
} else {
// handle comments and other non-property lines
newLines.add(line);
needsTranslateComment = false; // reset the flag when we encounter comments or empty lines
}
}
return newLines;
}
}

View File

@@ -1,63 +1,76 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException;
import org.springframework.boot.SpringApplication; import java.nio.file.Files;
import org.springframework.boot.autoconfigure.SpringBootApplication; import java.nio.file.Path;
import org.springframework.core.env.Environment; import java.nio.file.Paths;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.annotation.PostConstruct; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication import org.springframework.core.env.Environment;
//@EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling;
public class SPdfApplication {
import jakarta.annotation.PostConstruct;
@Autowired import stirling.software.SPDF.utils.GeneralUtils;
private Environment env;
@SpringBootApplication
@PostConstruct //@EnableScheduling
public void init() { public class SPdfApplication {
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); @Autowired
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true"); private Environment env;
if (browserOpen) { @PostConstruct
try { public void init() {
String port = env.getProperty("local.server.port"); // Check if the BROWSER_OPEN environment variable is set to true
if(port == null || port.length() == 0) { String browserOpenEnv = env.getProperty("BROWSER_OPEN");
port="8080"; boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
}
String url = "http://localhost:" + port; if (browserOpen) {
try {
String os = System.getProperty("os.name").toLowerCase(); String port = env.getProperty("local.server.port");
Runtime rt = Runtime.getRuntime(); if(port == null || port.length() == 0) {
if (os.contains("win")) { port="8080";
// For Windows }
rt.exec("rundll32 url.dll,FileProtocolHandler " + url); String url = "http://localhost:" + port;
}
} catch (Exception e) { String os = System.getProperty("os.name").toLowerCase();
e.printStackTrace(); Runtime rt = Runtime.getRuntime();
} if (os.contains("win")) {
} // For Windows
} rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
}
public static void main(String[] args) { } catch (Exception e) {
SpringApplication.run(SPdfApplication.class, args); e.printStackTrace();
try { }
Thread.sleep(1000); }
} catch (InterruptedException e) { }
// TODO Auto-generated catch block
e.printStackTrace(); public static void main(String[] args) {
} SpringApplication.run(SPdfApplication.class, args);
System.out.println("Stirling-PDF Started."); try {
Thread.sleep(1000);
String port = System.getProperty("local.server.port"); } catch (InterruptedException e) {
if(port == null || port.length() == 0) { // TODO Auto-generated catch block
port="8080"; e.printStackTrace();
} }
String url = "http://localhost:" + port;
System.out.println("Navigate to " + url); GeneralUtils.createDir("customFiles/static/");
} GeneralUtils.createDir("customFiles/templates/");
GeneralUtils.createDir("config");
System.out.println("Stirling-PDF Started.");
String port = System.getProperty("local.server.port");
if(port == null || port.length() == 0) {
port="8080";
}
String url = "http://localhost:" + port;
System.out.println("Navigate to " + url);
}
} }

View File

@@ -1,60 +1,60 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Locale; import java.util.Locale;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver; import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(new CleanUrlInterceptor()); registry.addInterceptor(new CleanUrlInterceptor());
} }
@Bean @Bean
public LocaleChangeInterceptor localeChangeInterceptor() { public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang"); lci.setParamName("lang");
return lci; return lci;
} }
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = System.getProperty("APP_LOCALE"); String appLocaleEnv = System.getProperty("APP_LOCALE");
if (appLocaleEnv == null) if (appLocaleEnv == null)
appLocaleEnv = System.getenv("APP_LOCALE"); appLocaleEnv = System.getenv("APP_LOCALE");
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
String tempLanguageTag = tempLocale.toLanguageTag(); String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
} }
} }
} }
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }
} }

View File

@@ -1,200 +1,213 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>(); private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
public EndpointConfiguration() { public EndpointConfiguration() {
init(); init();
processEnvironmentConfigs(); processEnvironmentConfigs();
} }
public void enableEndpoint(String endpoint) { public void enableEndpoint(String endpoint) {
endpointStatuses.put(endpoint, true); endpointStatuses.put(endpoint, true);
} }
public void disableEndpoint(String endpoint) { public void disableEndpoint(String endpoint) {
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
logger.info("Disabling {}", endpoint); logger.info("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false); endpointStatuses.put(endpoint, false);
} }
} }
public boolean isEndpointEnabled(String endpoint) { public boolean isEndpointEnabled(String endpoint) {
if (endpoint.startsWith("/")) { if (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1); endpoint = endpoint.substring(1);
} }
return endpointStatuses.getOrDefault(endpoint, true); return endpointStatuses.getOrDefault(endpoint, true);
} }
public void addEndpointToGroup(String group, String endpoint) { public void addEndpointToGroup(String group, String endpoint) {
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
} }
public void enableGroup(String group) { public void enableGroup(String group) {
Set<String> endpoints = endpointGroups.get(group); Set<String> endpoints = endpointGroups.get(group);
if (endpoints != null) { if (endpoints != null) {
for (String endpoint : endpoints) { for (String endpoint : endpoints) {
enableEndpoint(endpoint); enableEndpoint(endpoint);
} }
} }
} }
public void disableGroup(String group) { public void disableGroup(String group) {
Set<String> endpoints = endpointGroups.get(group); Set<String> endpoints = endpointGroups.get(group);
if (endpoints != null) { if (endpoints != null) {
for (String endpoint : endpoints) { for (String endpoint : endpoints) {
disableEndpoint(endpoint); disableEndpoint(endpoint);
} }
} }
} }
public void init() { public void init() {
// Adding endpoints to "PageOps" group // Adding endpoints to "PageOps" group
addEndpointToGroup("PageOps", "remove-pages"); addEndpointToGroup("PageOps", "remove-pages");
addEndpointToGroup("PageOps", "merge-pdfs"); addEndpointToGroup("PageOps", "merge-pdfs");
addEndpointToGroup("PageOps", "split-pdfs"); addEndpointToGroup("PageOps", "split-pdfs");
addEndpointToGroup("PageOps", "pdf-organizer"); addEndpointToGroup("PageOps", "pdf-organizer");
addEndpointToGroup("PageOps", "rotate-pdf"); addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout"); addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "scale-pages"); addEndpointToGroup("PageOps", "scale-pages");
addEndpointToGroup("PageOps", "adjust-contrast");
// Adding endpoints to "Convert" group addEndpointToGroup("PageOps", "crop");
addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("PageOps", "auto-split-pdf");
addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("Convert", "pdf-to-pdfa"); // Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "file-to-pdf"); addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("Convert", "xlsx-to-pdf"); addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("Convert", "pdf-to-word"); addEndpointToGroup("Convert", "pdf-to-pdfa");
addEndpointToGroup("Convert", "pdf-to-presentation"); addEndpointToGroup("Convert", "file-to-pdf");
addEndpointToGroup("Convert", "pdf-to-text"); addEndpointToGroup("Convert", "xlsx-to-pdf");
addEndpointToGroup("Convert", "pdf-to-html"); addEndpointToGroup("Convert", "pdf-to-word");
addEndpointToGroup("Convert", "pdf-to-xml"); addEndpointToGroup("Convert", "pdf-to-presentation");
addEndpointToGroup("Convert", "pdf-to-text");
// Adding endpoints to "Security" group addEndpointToGroup("Convert", "pdf-to-html");
addEndpointToGroup("Security", "add-password"); addEndpointToGroup("Convert", "pdf-to-xml");
addEndpointToGroup("Security", "remove-password"); addEndpointToGroup("Convert", "html-to-pdf");
addEndpointToGroup("Security", "change-permissions"); addEndpointToGroup("Convert", "url-to-pdf");
addEndpointToGroup("Security", "add-watermark");
addEndpointToGroup("Security", "cert-sign"); // Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
addEndpointToGroup("Security", "remove-password");
addEndpointToGroup("Security", "change-permissions");
// Adding endpoints to "Other" group addEndpointToGroup("Security", "add-watermark");
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Other", "add-image"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Other", "compress-pdf");
addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Other", "change-metadata"); // Adding endpoints to "Other" group
addEndpointToGroup("Other", "extract-image-scans"); addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "sign"); addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "flatten"); addEndpointToGroup("Other", "compress-pdf");
addEndpointToGroup("Other", "repair"); addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Other", "remove-blanks"); addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Other", "compare"); addEndpointToGroup("Other", "extract-image-scans");
addEndpointToGroup("Other", "sign");
addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "repair");
addEndpointToGroup("Other", "remove-blanks");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "add-page-numbers");
addEndpointToGroup("Other", "auto-rename");
//CLI
addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("CLI", "remove-blanks");
addEndpointToGroup("CLI", "repair"); //CLI
addEndpointToGroup("CLI", "pdf-to-pdfa"); addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("CLI", "file-to-pdf"); addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("CLI", "xlsx-to-pdf"); addEndpointToGroup("CLI", "remove-blanks");
addEndpointToGroup("CLI", "pdf-to-word"); addEndpointToGroup("CLI", "repair");
addEndpointToGroup("CLI", "pdf-to-presentation"); addEndpointToGroup("CLI", "pdf-to-pdfa");
addEndpointToGroup("CLI", "pdf-to-text"); addEndpointToGroup("CLI", "file-to-pdf");
addEndpointToGroup("CLI", "pdf-to-html"); addEndpointToGroup("CLI", "xlsx-to-pdf");
addEndpointToGroup("CLI", "pdf-to-xml"); addEndpointToGroup("CLI", "pdf-to-word");
addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "pdf-to-presentation");
addEndpointToGroup("CLI", "pdf-to-text");
//python addEndpointToGroup("CLI", "pdf-to-html");
addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("CLI", "pdf-to-xml");
addEndpointToGroup("Python", "remove-blanks"); addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf");
//openCV
addEndpointToGroup("OpenCV", "extract-image-scans"); //python
addEndpointToGroup("OpenCV", "remove-blanks"); addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "remove-blanks");
//LibreOffice addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("LibreOffice", "repair"); addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); //openCV
addEndpointToGroup("LibreOffice", "pdf-to-word"); addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("LibreOffice", "pdf-to-presentation"); addEndpointToGroup("OpenCV", "remove-blanks");
addEndpointToGroup("LibreOffice", "pdf-to-text");
addEndpointToGroup("LibreOffice", "pdf-to-html"); //LibreOffice
addEndpointToGroup("LibreOffice", "pdf-to-xml"); addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
//OCRmyPDF addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("LibreOffice", "pdf-to-text");
addEndpointToGroup("OCRmyPDF", "ocr-pdf"); addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml");
//Java
addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-pages"); //OCRmyPDF
addEndpointToGroup("Java", "split-pdfs"); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("Java", "pdf-organizer"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
addEndpointToGroup("Java", "rotate-pdf"); addEndpointToGroup("OCRmyPDF", "ocr-pdf");
addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "img-to-pdf"); //Java
addEndpointToGroup("Java", "add-password"); addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-password"); addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "change-permissions"); addEndpointToGroup("Java", "split-pdfs");
addEndpointToGroup("Java", "add-watermark"); addEndpointToGroup("Java", "pdf-organizer");
addEndpointToGroup("Java", "add-image"); addEndpointToGroup("Java", "rotate-pdf");
addEndpointToGroup("Java", "extract-images"); addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "change-metadata"); addEndpointToGroup("Java", "img-to-pdf");
addEndpointToGroup("Java", "cert-sign"); addEndpointToGroup("Java", "add-password");
addEndpointToGroup("Java", "multi-page-layout"); addEndpointToGroup("Java", "remove-password");
addEndpointToGroup("Java", "scale-pages"); addEndpointToGroup("Java", "change-permissions");
addEndpointToGroup("Java", "add-watermark");
addEndpointToGroup("Java", "add-image");
//Javascript addEndpointToGroup("Java", "extract-images");
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Java", "multi-page-layout");
addEndpointToGroup("Java", "scale-pages");
} addEndpointToGroup("Java", "add-page-numbers");
addEndpointToGroup("Java", "auto-rename");
private void processEnvironmentConfigs() { addEndpointToGroup("Java", "auto-split-pdf");
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE"); addEndpointToGroup("Java", "sanitize-pdf");
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE"); addEndpointToGroup("Java", "crop");
if (endpointsToRemove != null) { //Javascript
String[] endpoints = endpointsToRemove.split(","); addEndpointToGroup("Javascript", "pdf-organizer");
for (String endpoint : endpoints) { addEndpointToGroup("Javascript", "sign");
disableEndpoint(endpoint.trim()); addEndpointToGroup("Javascript", "compare");
} addEndpointToGroup("Javascript", "adjust-contrast");
}
if (groupsToRemove != null) { }
String[] groups = groupsToRemove.split(",");
for (String group : groups) { private void processEnvironmentConfigs() {
disableGroup(group.trim()); String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
} String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
}
} if (endpointsToRemove != null) {
String[] endpoints = endpointsToRemove.split(",");
} for (String endpoint : endpoints) {
disableEndpoint(endpoint.trim());
}
}
if (groupsToRemove != null) {
String[] groups = groupsToRemove.split(",");
for (String group : groups) {
disableGroup(group.trim());
}
}
}
}

View File

@@ -3,6 +3,7 @@ package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
@@ -15,4 +16,12 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor); registry.addInterceptor(endpointInterceptor);
} }
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/")
.setCachePeriod(0); // Optional: disable caching
}
} }

View File

@@ -0,0 +1,132 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "General", description = "General APIs")
public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
byte[] bytes = file.getBytes();
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
PdfCanvas pdfCanvas = new PdfCanvas(page);
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.rectangle(x, y, width, height);
pdfCanvas.clip();
pdfCanvas.addXObject(formXObject, -x, -y);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
}
}

View File

@@ -52,11 +52,11 @@ public class ScalePagesController {
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") @Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> scalePages( public ResponseEntity<byte[]> scalePages(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "String", allowableValues = { @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = {
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4",
"B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL", "B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL",
"EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize, "EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize,
@Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "float")) @RequestParam("scaleFactor") float scaleFactor) @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor)
throws IOException { throws IOException {
Map<String, PageSize> sizeMap = new HashMap<>(); Map<String, PageSize> sizeMap = new HashMap<>();

View File

@@ -0,0 +1,129 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertHtmlToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/html-to-pdf")
@Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format."
)
public ResponseEntity<byte[]> HtmlToPdf(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput) throws IOException, InterruptedException {
if (fileInput == null) {
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion.");
}
String originalFilename = fileInput.getOriginalFilename();
if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
throw new IllegalArgumentException("File must be either .html or .zip format.");
}
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
Path tempInputFile = null;
byte[] pdfBytes;
try {
if (originalFilename.endsWith(".html")) {
tempInputFile = Files.createTempFile("input_", ".html");
Files.write(tempInputFile, fileInput.getBytes());
} else {
tempInputFile = unzipAndGetMainHtml(fileInput);
}
List<String> command = new ArrayList<>();
command.add("weasyprint");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
int returnCode = 0;
if (originalFilename.endsWith(".zip")) {
returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command, tempInputFile.getParent().toFile());
} else {
returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command);
}
pdfBytes = Files.readAllBytes(tempOutputFile);
} finally {
// Clean up temporary files
Files.delete(tempOutputFile);
Files.delete(tempInputFile);
if (originalFilename.endsWith(".zip")) {
GeneralUtils.deleteDirectory(tempInputFile.getParent());
}
}
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
private Path unzipAndGetMainHtml(MultipartFile zipFile) throws IOException {
Path tempDirectory = Files.createTempDirectory("unzipped_");
try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempDirectory.resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(filePath); // Explicitly create the directory structure
} else {
Files.createDirectories(filePath.getParent()); // Create parent directories if they don't exist
Files.copy(zipIn, filePath);
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
}
//search for the main HTML file.
try (Stream<Path> walk = Files.walk(tempDirectory)) {
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html"))
.collect(Collectors.toList());
if (htmlFiles.isEmpty()) {
throw new IOException("No HTML files found in the unzipped directory.");
}
// Prioritize 'index.html' if it exists, otherwise use the first .html file
for (Path htmlFile : htmlFiles) {
if (htmlFile.getFileName().toString().equals("index.html")) {
return htmlFile;
}
}
return htmlFiles.get(0);
}
}
}

View File

@@ -0,0 +1,76 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertWebsiteToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/url-to-pdf")
@Operation(
summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format."
)
public ResponseEntity<byte[]> urlToPdf(
@RequestPart(required = true, value = "urlInput")
@Parameter(description = "The input URL to be converted to a PDF file", required = true)
String URL) throws IOException, InterruptedException {
// Validate the URL format
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided.");
}
Path tempOutputFile = null;
byte[] pdfBytes;
try {
// Prepare the output file path
tempOutputFile = Files.createTempFile("output_", ".pdf");
// Prepare the OCRmyPDF command
List<String> command = new ArrayList<>();
command.add("weasyprint");
command.add(URL);
command.add(tempOutputFile.toString());
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command);
// Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile);
}
finally {
// Clean up the temporary files
Files.delete(tempOutputFile);
}
// Convert URL to a safe filename
String outputFilename = convertURLToFileName(URL);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
private String convertURLToFileName(String url) {
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
if(safeName.length() > 50) {
safeName = safeName.substring(0, 50); // restrict to 50 characters
}
return safeName + ".pdf";
}
}

View File

@@ -1,162 +1,202 @@
package stirling.software.SPDF.controller.api.filters; package stirling.software.SPDF.controller.api.filters;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController
@Tag(name = "Filter", description = "Filter APIs") @RestController
public class FilterController { @Tag(name = "Filter", description = "Filter APIs")
public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/contains-text")
@Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
public Boolean containsText( @Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO")
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, public ResponseEntity<byte[]> containsText(
@Parameter(description = "The text to check for", required = true) String text, @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
@Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber) @Parameter(description = "The text to check for", required = true) String text,
throws IOException, InterruptedException { @Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); throws IOException, InterruptedException {
return PdfUtils.hasText(pdfDocument, pageNumber); PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
} if (PdfUtils.hasText(pdfDocument, pageNumber, text))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
@PostMapping(consumes = "multipart/form-data", value = "/contains-image") return null;
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO") }
public Boolean containsImage(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, // TODO
@Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber) @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
throws IOException, InterruptedException { @Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); public ResponseEntity<byte[]> containsImage(
return PdfUtils.hasImagesOnPage(null); @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
} @Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
throws IOException, InterruptedException {
@PostMapping(consumes = "multipart/form-data", value = "/page-count") PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") if (PdfUtils.hasImages(pdfDocument, pageNumber))
public Boolean pageCount( return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, return null;
@Parameter(description = "Page Count", required = true) String pageCount, }
@Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator)
throws IOException, InterruptedException { @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
// Load the PDF @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
PDDocument document = PDDocument.load(inputFile.getInputStream()); public ResponseEntity<byte[]> pageCount(
int actualPageCount = document.getNumberOfPages(); @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Page Count", required = true) String pageCount,
// Perform the comparison @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
switch (comparator) { "Greater", "Equal", "Less" })) String comparator)
case "Greater": throws IOException, InterruptedException {
return actualPageCount > Integer.parseInt(pageCount); // Load the PDF
case "Equal": PDDocument document = PDDocument.load(inputFile.getInputStream());
return actualPageCount == Integer.parseInt(pageCount); int actualPageCount = document.getNumberOfPages();
case "Less":
return actualPageCount < Integer.parseInt(pageCount); boolean valid = false;
default: // Perform the comparison
throw new IllegalArgumentException("Invalid comparator: " + comparator); switch (comparator) {
} case "Greater":
} valid = actualPageCount > Integer.parseInt(pageCount);
break;
@PostMapping(consumes = "multipart/form-data", value = "/page-size") case "Equal":
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") valid = actualPageCount == Integer.parseInt(pageCount);
public Boolean pageSize( break;
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, case "Less":
@Parameter(description = "Standard Page Size", required = true) String standardPageSize, valid = actualPageCount < Integer.parseInt(pageCount);
@Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) break;
throws IOException, InterruptedException { default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
// Load the PDF }
PDDocument document = PDDocument.load(inputFile.getInputStream());
if (valid)
PDPage firstPage = document.getPage(0); return WebResponseUtils.multiPartFileToWebResponse(inputFile);
PDRectangle actualPageSize = firstPage.getMediaBox(); return null;
}
// Calculate the area of the actual page size
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
// Get the standard size and calculate its area public ResponseEntity<byte[]> pageSize(
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
float standardArea = standardSize.getWidth() * standardSize.getHeight(); @Parameter(description = "Standard Page Size", required = true) String standardPageSize,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
// Perform the comparison "Greater", "Equal", "Less" })) String comparator)
switch (comparator) { throws IOException, InterruptedException {
case "Greater":
return actualArea > standardArea; // Load the PDF
case "Equal": PDDocument document = PDDocument.load(inputFile.getInputStream());
return actualArea == standardArea;
case "Less": PDPage firstPage = document.getPage(0);
return actualArea < standardArea; PDRectangle actualPageSize = firstPage.getMediaBox();
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator); // Calculate the area of the actual page size
} float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
}
// Get the standard size and calculate its area
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
@PostMapping(consumes = "multipart/form-data", value = "/file-size") float standardArea = standardSize.getWidth() * standardSize.getHeight();
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
public Boolean fileSize( boolean valid = false;
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, // Perform the comparison
@Parameter(description = "File Size", required = true) String fileSize, switch (comparator) {
@Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) case "Greater":
throws IOException, InterruptedException { valid = actualArea > standardArea;
break;
// Get the file size case "Equal":
long actualFileSize = inputFile.getSize(); valid = actualArea == standardArea;
break;
// Perform the comparison case "Less":
switch (comparator) { valid = actualArea < standardArea;
case "Greater": break;
return actualFileSize > Long.parseLong(fileSize); default:
case "Equal": throw new IllegalArgumentException("Invalid comparator: " + comparator);
return actualFileSize == Long.parseLong(fileSize); }
case "Less":
return actualFileSize < Long.parseLong(fileSize); if (valid)
default: return WebResponseUtils.multiPartFileToWebResponse(inputFile);
throw new IllegalArgumentException("Invalid comparator: " + comparator); return null;
} }
}
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
@PostMapping(consumes = "multipart/form-data", value = "/page-rotation") public ResponseEntity<byte[]> fileSize(
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
public Boolean pageRotation( @Parameter(description = "File Size", required = true) String fileSize,
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
@Parameter(description = "Rotation in degrees", required = true) int rotation, "Greater", "Equal", "Less" })) String comparator)
@Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) throws IOException, InterruptedException {
throws IOException, InterruptedException {
// Get the file size
// Load the PDF long actualFileSize = inputFile.getSize();
PDDocument document = PDDocument.load(inputFile.getInputStream());
boolean valid = false;
// Get the rotation of the first page // Perform the comparison
PDPage firstPage = document.getPage(0); switch (comparator) {
int actualRotation = firstPage.getRotation(); case "Greater":
valid = actualFileSize > Long.parseLong(fileSize);
// Perform the comparison break;
switch (comparator) { case "Equal":
case "Greater": valid = actualFileSize == Long.parseLong(fileSize);
return actualRotation > rotation; break;
case "Equal": case "Less":
return actualRotation == rotation; valid = actualFileSize < Long.parseLong(fileSize);
case "Less": break;
return actualRotation < rotation; default:
default: throw new IllegalArgumentException("Invalid comparator: " + comparator);
throw new IllegalArgumentException("Invalid comparator: " + comparator); }
}
} if (valid)
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
} return null;
}
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageRotation(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Rotation in degrees", required = true) int rotation,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());
// Get the rotation of the first page
PDPage firstPage = document.getPage(0);
int actualRotation = firstPage.getRotation();
boolean valid = false;
// Perform the comparison
switch (comparator) {
case "Greater":
valid = actualRotation > rotation;
break;
case "Equal":
valid = actualRotation == rotation;
break;
case "Less":
valid = actualRotation < rotation;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid)
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
}
}

View File

@@ -0,0 +1,177 @@
package stirling.software.SPDF.controller.api.other;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.text.TextPosition;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.util.ArrayList;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.text.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.ResponseEntity;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class AutoRenameController {
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
private static final int LINE_LIMIT = 11;
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the header is to be extracted.", required = true) MultipartFile file,
@RequestParam(required = false, defaultValue = "false") @Parameter(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false) Boolean useFirstTextAsFallback)
throws Exception {
PDDocument document = PDDocument.load(file.getInputStream());
PDFTextStripper reader = new PDFTextStripper() {
class LineInfo {
String text;
float fontSize;
LineInfo(String text, float fontSize) {
this.text = text;
this.fontSize = fontSize;
}
}
List<LineInfo> lineInfos = new ArrayList<>();
StringBuilder lineBuilder = new StringBuilder();
float lastY = -1;
float maxFontSizeInLine = 0.0f;
int lineCount = 0;
@Override
protected void processTextPosition(TextPosition text) {
if (lastY != text.getY() && lineCount < LINE_LIMIT) {
processLine();
lineBuilder = new StringBuilder(text.getUnicode());
maxFontSizeInLine = text.getFontSizeInPt();
lastY = text.getY();
lineCount++;
} else if (lineCount < LINE_LIMIT) {
lineBuilder.append(text.getUnicode());
if (text.getFontSizeInPt() > maxFontSizeInLine) {
maxFontSizeInLine = text.getFontSizeInPt();
}
}
}
private void processLine() {
if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
}
}
@Override
public String getText(PDDocument doc) throws IOException {
this.lineInfos.clear();
this.lineBuilder = new StringBuilder();
this.lastY = -1;
this.maxFontSizeInLine = 0.0f;
this.lineCount = 0;
super.getText(doc);
processLine(); // Process the last line
// Merge lines with same font size
List<LineInfo> mergedLineInfos = new ArrayList<>();
for (int i = 0; i < lineInfos.size(); i++) {
String mergedText = lineInfos.get(i).text;
float fontSize = lineInfos.get(i).fontSize;
while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) {
mergedText += " " + lineInfos.get(i + 1).text;
i++;
}
mergedLineInfos.add(new LineInfo(mergedText, fontSize));
}
// Sort lines by font size in descending order and get the first one
mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null);
}
};
String header = reader.getText(document);
// Sanitize the header string by removing characters not allowed in a filename.
if (header != null && header.length() < 255) {
header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
} else {
logger.info("File has no good title to be found");
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
}
}
}

View File

@@ -0,0 +1,141 @@
package stirling.software.SPDF.controller.api.other;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@RestController
public class AutoSplitPdfController {
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(
@RequestParam("fileInput") @Parameter(description = "The input PDF file which needs to be split into separate documents based on QR code boundaries.", required = true) MultipartFile file,
@RequestParam(value ="duplexMode",defaultValue = "false") @Parameter(description = "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", required = false) boolean duplexMode)
throws IOException {
InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream);
PDFRenderer pdfRenderer = new PDFRenderer(document);
List<PDDocument> splitDocuments = new ArrayList<>();
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150);
String result = decodeQRCode(bim);
if (QR_CONTENT.equals(result) && page != 0) {
splitDocuments.add(new PDDocument());
}
if (!splitDocuments.isEmpty() && !QR_CONTENT.equals(result)) {
splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page));
} else if (page == 0) {
PDDocument firstDocument = new PDDocument();
firstDocument.addPage(document.getPage(page));
splitDocuments.add(firstDocument);
}
// If duplexMode is true and current page is a divider, then skip next page
if (duplexMode && QR_CONTENT.equals(result)) {
page++;
}
}
// Remove split documents that have no pages
splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0);
for (PDDocument splitDocument : splitDocuments) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
splitDocument.save(baos);
splitDocumentsBoas.add(baos);
splitDocument.close();
}
document.close();
Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
data = Files.readAllBytes(zipFile);
Files.delete(zipFile);
}
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
}
private static String decodeQRCode(BufferedImage bufferedImage) {
LuminanceSource source;
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false);
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
byte[] newPixels = new byte[pixels.length];
for (int i = 0; i < pixels.length; i++) {
newPixels[i] = (byte) (pixels[i] & 0xff);
}
source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false);
} else {
throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
}
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = new MultiFormatReader().decode(bitmap);
return result.getText();
} catch (NotFoundException e) {
return null; // there is no QR code in the image
}
}
}

View File

@@ -221,6 +221,15 @@ public class CompressController {
// Read the optimized PDF file // Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
// Check if optimized file is larger than the original
if(pdfBytes.length > inputFileSize) {
// Log the occurrence
logger.warn("Optimized file is larger than the original. Returning the original file instead.");
// Read the original file again
pdfBytes = Files.readAllBytes(tempInputFile);
}
// Clean up the temporary files // Clean up the temporary files
Files.delete(tempInputFile); Files.delete(tempInputFile);
Files.delete(tempOutputFile); Files.delete(tempOutputFile);

View File

@@ -0,0 +1,174 @@
package stirling.software.SPDF.controller.api.other;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addPageNumbers(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin,
@Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position,
@Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber,
@Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
@Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
throws IOException {
byte[] fileBytes = file.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
int pageNumber = startingNumber;
float marginFactor;
switch (customMargin.toLowerCase()) {
case "small":
marginFactor = 0.02f;
break;
case "medium":
marginFactor = 0.035f;
break;
case "large":
marginFactor = 0.05f;
break;
case "x-large":
marginFactor = 0.1f;
break;
default:
marginFactor = 0.035f;
break;
}
float fontSize = 12.0f;
PdfReader reader = new PdfReader(bais);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(reader, writer);
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
for (int i : pagesToNumberList) {
PdfPage page = pdfDoc.getPage(i+1);
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
float textWidth = font.getWidth(text, fontSize);
float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
float x, y;
TextAlignment alignment;
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3;
switch (xGroup) {
case 0: // left
x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
alignment = TextAlignment.LEFT;
break;
case 1: // center
x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
alignment = TextAlignment.CENTER;
break;
default: // right
x = pageSize.getRight() - marginFactor * pageSize.getWidth();
alignment = TextAlignment.RIGHT;
break;
}
switch (yGroup) {
case 0: // bottom
y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
break;
default: // top
y = pageSize.getTop() - marginFactor * pageSize.getHeight();
break;
}
new Canvas(pdfCanvas, page.getPageSize())
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
pageNumber++;
}
pdfDoc.close();
byte[] resultBytes = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
}
}

View File

@@ -1,399 +0,0 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Pipeline", description = "Pipeline APIs")
public class Controller {
@Autowired
private ObjectMapper objectMapper;
final String jsonFileName = "pipelineCofig.json";
final String watchedFoldersDir = "watchedFolders/";
@Scheduled(fixedRate = 5000)
public void scanFolders() {
Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
} catch (IOException e) {
e.printStackTrace();
return;
}
}
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> {
try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t);
}
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleDirectory(Path dir) throws Exception {
Path jsonFile = dir.resolve(jsonFileName);
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir);
}
if (Files.exists(jsonFile)) {
// Read JSON file
String jsonString;
try {
jsonString = new String(Files.readAllBytes(jsonFile));
} catch (IOException e) {
e.printStackTrace();
return;
}
// Decode JSON to PipelineConfig
PipelineConfig config;
try {
config = objectMapper.readValue(jsonString, PipelineConfig.class);
// Assuming your PipelineConfig class has getters for all necessary fields, you can perform checks here
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
throw new IOException("Invalid JSON format");
}
} catch (IOException e) {
e.printStackTrace();
return;
}
// For each operation in the pipeline
for (PipelineOperation operation : config.getOperations()) {
// Collect all files based on fileInput
File[] files;
String fileInput = (String) operation.getParameters().get("fileInput");
if ("automated".equals(fileInput)) {
// If fileInput is "automated", process all files in the directory
try (Stream<Path> paths = Files.list(dir)) {
files = paths.filter(path -> !path.equals(jsonFile))
.map(Path::toFile)
.toArray(File[]::new);
} catch (IOException e) {
e.printStackTrace();
return;
}
} else {
// If fileInput contains a path, process only this file
files = new File[]{new File(fileInput)};
}
// Prepare the files for processing
File[] filesToProcess = files.clone();
for (File file : filesToProcess) {
Files.move(file.toPath(), processingDir.resolve(file.getName()));
}
// Process the files
try {
List<Resource> resources = handleFiles(filesToProcess, jsonString);
// Move resultant files and rename them as per config in JSON file
for (Resource resource : resources) {
String outputFileName = config.getOutputPattern().replace("{filename}", resource.getFile().getName());
outputFileName = outputFileName.replace("{pipelineName}", config.getName());
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
// {filename} {folder} {date} {tmime} {pipeline}
Files.move(resource.getFile().toPath(), Paths.get(config.getOutputDir(), outputFileName));
}
// If successful, delete the original files
for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName()));
}
} catch (Exception e) {
// If an error occurs, move the original files back
for (File file : filesToProcess) {
Files.move(processingDir.resolve(file.getName()), file.toPath());
}
throw e;
}
}
}
}
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception{
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
for (JsonNode operationNode : pipelineNode) {
String operation = operationNode.get("operation").asText();
JsonNode parametersNode = operationNode.get("parameters");
String inputFileExtension = "";
if(operationNode.has("inputFileType")) {
inputFileExtension = operationNode.get("inputFileType").asText();
} else {
inputFileExtension=".pdf";
}
List<Resource> newOutputFiles = new ArrayList<>();
boolean hasInputFileType = false;
for (Resource file : outputFiles) {
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields();
while (parameters.hasNext()) {
Map.Entry<String, JsonNode> parameter = parameters.next();
body.add(parameter.getKey(), parameter.getValue().asText());
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/" + operation;
ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody());
hasErrors = true;
continue;
}
// Check if the response body is a zip file
if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource = new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return file.getFilename(); // Preserving original filename
}
};
newOutputFiles.add(outputResource);
}
}
if (!hasInputFileType) {
logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation);
hasErrors = true;
}
outputFiles = newOutputFiles;
}
logPrintStream.close();
}
return outputFiles;
}
List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (File file : files) {
Path path = Paths.get(file.getAbsolutePath());
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
@Override
public String getFilename() {
return file.getName();
}
};
outputFiles.add(fileResource);
}
return processFiles(outputFiles, jsonString);
}
List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception{
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
return processFiles(outputFiles, jsonString);
}
@PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) {
try {
List<Resource> outputFiles = handleFiles(files, jsonString);
if (outputFiles.size() == 1) {
// If there is only one file, return it directly
Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream();
byte[] bytes = new byte[(int)singleFile.contentLength()];
is.read(bytes);
is.close();
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
}
// Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos);
// Loop through each file and add it to the zip
for (Resource file : outputFiles) {
ZipEntry zipEntry = new ZipEntry(file.getFilename());
zipOut.putNextEntry(zipEntry);
// Read the file into a byte array
InputStream is = file.getInputStream();
byte[] bytes = new byte[(int)file.contentLength()];
is.read(bytes);
// Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry();
is.close();
}
zipOut.close();
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private boolean isZip(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException {
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
return unzippedFiles;
}
}

View File

@@ -0,0 +1,516 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController {
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
@Autowired
private ObjectMapper objectMapper;
final String jsonFileName = "pipelineConfig.json";
final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Scheduled(fixedRate = 25000)
public void scanFolders() {
logger.info("Scanning folders...");
Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e);
return;
}
}
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> {
try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t);
}
} catch (Exception e) {
logger.error("Error handling directory: {}", t, e);
}
});
} catch (Exception e) {
logger.error("Error walking through directory: {}", watchedFolderPath, e);
}
}
private void handleDirectory(Path dir) throws Exception {
logger.info("Handling directory: {}", dir);
Path jsonFile = dir.resolve(jsonFileName);
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir);
logger.info("Created processing directory: {}", processingDir);
}
if (Files.exists(jsonFile)) {
// Read JSON file
String jsonString;
try {
jsonString = new String(Files.readAllBytes(jsonFile));
logger.info("Read JSON file: {}", jsonFile);
} catch (IOException e) {
logger.error("Error reading JSON file: {}", jsonFile, e);
return;
}
// Decode JSON to PipelineConfig
PipelineConfig config;
try {
config = objectMapper.readValue(jsonString, PipelineConfig.class);
// Assuming your PipelineConfig class has getters for all necessary fields, you
// can perform checks here
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
throw new IOException("Invalid JSON format");
}
} catch (IOException e) {
logger.error("Error parsing PipelineConfig: {}", jsonString, e);
return;
}
// For each operation in the pipeline
for (PipelineOperation operation : config.getOperations()) {
// Collect all files based on fileInput
File[] files;
String fileInput = (String) operation.getParameters().get("fileInput");
if ("automated".equals(fileInput)) {
// If fileInput is "automated", process all files in the directory
try (Stream<Path> paths = Files.list(dir)) {
files = paths
.filter(path -> !Files.isDirectory(path)) // exclude directories
.filter(path -> !path.equals(jsonFile)) // exclude jsonFile
.map(Path::toFile)
.toArray(File[]::new);
} catch (IOException e) {
e.printStackTrace();
return;
}
} else {
// If fileInput contains a path, process only this file
files = new File[] { new File(fileInput) };
}
// Prepare the files for processing
List<File> filesToProcess = new ArrayList<>();
for (File file : files) {
logger.info(file.getName());
logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName()));
Files.move(file.toPath(), processingDir.resolve(file.getName()));
filesToProcess.add(processingDir.resolve(file.getName()).toFile());
}
// Process the files
try {
List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
if(resources == null) {
return;
}
// Move resultant files and rename them as per config in JSON file
for (Resource resource : resources) {
String resourceName = resource.getFilename();
String baseName = resourceName.substring(0, resourceName.lastIndexOf("."));
String extension = resourceName.substring(resourceName.lastIndexOf(".")+1);
String outputFileName = config.getOutputPattern().replace("{filename}", baseName);
outputFileName = outputFileName.replace("{pipelineName}", config.getName());
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
outputFileName += "." + extension;
// {filename} {folder} {date} {tmime} {pipeline}
String outputDir = config.getOutputDir();
// Check if the environment variable 'automatedOutputFolder' is set
String outputFolder = System.getenv("automatedOutputFolder");
if (outputFolder == null || outputFolder.isEmpty()) {
// If the environment variable is not set, use the default value
outputFolder = finishedFoldersDir;
}
logger.info("outputDir 0={}", outputDir);
// Replace the placeholders in the outputDir string
outputDir = outputDir.replace("{outputFolder}", outputFolder);
outputDir = outputDir.replace("{folderName}", dir.toString());
logger.info("outputDir 1={}", outputDir);
outputDir = outputDir.replace("\\watchedFolders", "");
outputDir = outputDir.replace("//watchedFolders", "");
outputDir = outputDir.replace("\\\\watchedFolders", "");
outputDir = outputDir.replace("/watchedFolders", "");
Path outputPath;
logger.info("outputDir 2={}", outputDir);
if (Paths.get(outputDir).isAbsolute()) {
// If it's an absolute path, use it directly
outputPath = Paths.get(outputDir);
} else {
// If it's a relative path, make it relative to the current working directory
outputPath = Paths.get(".", outputDir);
}
logger.info("outputPath={}", outputPath);
if (!Files.exists(outputPath)) {
try {
Files.createDirectories(outputPath);
logger.info("Created directory: {}", outputPath);
} catch (IOException e) {
logger.error("Error creating directory: {}", outputPath, e);
return;
}
}
logger.info("outputPath {}", outputPath);
logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString());
File newFile = new File(outputPath.resolve(outputFileName).toString());
OutputStream os = new FileOutputStream(newFile);
os.write(((ByteArrayResource)resource).getByteArray());
os.close();
logger.info("made {}", outputPath.resolve(outputFileName));
}
// If successful, delete the original files
for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName()));
}
} catch (Exception e) {
// If an error occurs, move the original files back
for (File file : filesToProcess) {
Files.move(processingDir.resolve(file.getName()), file.toPath());
}
throw e;
}
}
}
}
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
logger.info("Running pipelineNode: {}", pipelineNode);
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
for (JsonNode operationNode : pipelineNode) {
String operation = operationNode.get("operation").asText();
logger.info("Running operation: {}", operation);
JsonNode parametersNode = operationNode.get("parameters");
String inputFileExtension = "";
if (operationNode.has("inputFileType")) {
inputFileExtension = operationNode.get("inputFileType").asText();
} else {
inputFileExtension = ".pdf";
}
List<Resource> newOutputFiles = new ArrayList<>();
boolean hasInputFileType = false;
for (Resource file : outputFiles) {
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields();
while (parameters.hasNext()) {
Map.Entry<String, JsonNode> parameter = parameters.next();
body.add(parameter.getKey(), parameter.getValue().asText());
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/" + operation;
ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
// If the operation is filter and the response body is null or empty, skip this file
if (operation.startsWith("filter-") && (response.getBody() == null || response.getBody().length == 0)) {
logger.info("Skipping file due to failing {}", operation);
continue;
}
if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody());
hasErrors = true;
continue;
}
// Define filename
String filename;
if ("auto-rename".equals(operation)) {
// If the operation is "auto-rename", generate a new filename.
// This is a simple example of generating a filename using current timestamp.
// Modify as per your needs.
filename = "file_" + System.currentTimeMillis();
} else {
// Otherwise, keep the original filename.
filename = file.getFilename();
}
// Check if the response body is a zip file
if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource = new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return filename;
}
};
newOutputFiles.add(outputResource);
}
}
if (!hasInputFileType) {
logPrintStream.println(
"No files with extension " + inputFileExtension + " found for operation " + operation);
hasErrors = true;
}
outputFiles = newOutputFiles;
}
logPrintStream.close();
}
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
return outputFiles;
}
List<Resource> handleFiles(File[] files, String jsonString) throws Exception {
if(files == null || files.length == 0) {
logger.info("No files");
return null;
}
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (File file : files) {
Path path = Paths.get(file.getAbsolutePath());
System.out.println("Reading file: " + path); // debug statement
if (Files.exists(path)) {
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
@Override
public String getFilename() {
return file.getName();
}
};
outputFiles.add(fileResource);
} else {
System.out.println("File not found: " + path); // debug statement
}
}
logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString);
}
List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception {
if(files == null || files.length == 0) {
logger.info("No files");
return null;
}
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString);
}
@PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) {
logger.info("Received POST request to /handleData with {} files", files.length);
try {
List<Resource> outputFiles = handleFiles(files, jsonString);
if (outputFiles != null && outputFiles.size() == 1) {
// If there is only one file, return it directly
Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream();
byte[] bytes = new byte[(int) singleFile.contentLength()];
is.read(bytes);
is.close();
logger.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(),
MediaType.APPLICATION_OCTET_STREAM);
} else if (outputFiles == null) {
return null;
}
// Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos);
// Loop through each file and add it to the zip
for (Resource file : outputFiles) {
ZipEntry zipEntry = new ZipEntry(file.getFilename());
zipOut.putNextEntry(zipEntry);
// Read the file into a byte array
InputStream is = file.getInputStream();
byte[] bytes = new byte[(int) file.contentLength()];
is.read(bytes);
// Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry();
is.close();
}
zipOut.close();
logger.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
logger.error("Error handling data: ", e);
return null;
}
}
private boolean isZip(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
}

View File

@@ -0,0 +1,140 @@
package stirling.software.SPDF.controller.api.security;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import stirling.software.SPDF.utils.WebResponseUtils;
import java.io.IOException;
import java.io.InputStream;
@RestController
public class SanitizeController {
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
@Operation(summary = "Sanitize a PDF file",
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> sanitizePDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be sanitized")
MultipartFile inputFile,
@RequestParam(name = "removeJavaScript", required = false, defaultValue = "true")
@Parameter(description = "Remove JavaScript actions from the PDF if set to true")
Boolean removeJavaScript,
@RequestParam(name = "removeEmbeddedFiles", required = false, defaultValue = "true")
@Parameter(description = "Remove embedded files from the PDF if set to true")
Boolean removeEmbeddedFiles,
@RequestParam(name = "removeMetadata", required = false, defaultValue = "true")
@Parameter(description = "Remove metadata from the PDF if set to true")
Boolean removeMetadata,
@RequestParam(name = "removeLinks", required = false, defaultValue = "true")
@Parameter(description = "Remove links from the PDF if set to true")
Boolean removeLinks,
@RequestParam(name = "removeFonts", required = false, defaultValue = "true")
@Parameter(description = "Remove fonts from the PDF if set to true")
Boolean removeFonts) throws IOException {
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if (removeJavaScript) {
sanitizeJavaScript(document);
}
if (removeEmbeddedFiles) {
sanitizeEmbeddedFiles(document);
}
if (removeMetadata) {
sanitizeMetadata(document);
}
if (removeLinks) {
sanitizeLinks(document);
}
if (removeFonts) {
sanitizeFonts(document);
}
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf");
}
}
private void sanitizeJavaScript(PDDocument document) throws IOException {
for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationWidget) {
PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
PDAction action = widget.getAction();
if (action instanceof PDActionJavaScript) {
widget.setAction(null);
}
}
}
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm != null) {
for (PDField field : acroForm.getFields()) {
if (field.getActions().getF() instanceof PDActionJavaScript) {
field.getActions().setF(null);
}
}
}
}
}
private void sanitizeEmbeddedFiles(PDDocument document) {
PDPageTree allPages = document.getPages();
for (PDPage page : allPages) {
PDResources res = page.getResources();
// Remove embedded files from the PDF
res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
}
}
private void sanitizeMetadata(PDDocument document) {
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
if (metadata != null) {
document.getDocumentCatalog().setMetadata(null);
}
}
private void sanitizeLinks(PDDocument document) throws IOException {
for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationLink) {
PDAction action = ((PDAnnotationLink) annotation).getAction();
if (action instanceof PDActionLaunch || action instanceof PDActionURI) {
((PDAnnotationLink) annotation).setAction(null);
}
}
}
}
}
private void sanitizeFonts(PDDocument document) {
for (PDPage page : document.getPages()) {
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
}
}
}

View File

@@ -1,12 +1,15 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
import java.util.List; import javax.imageio.ImageIO;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@@ -15,6 +18,8 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
@@ -30,124 +35,164 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@RestController @RestController
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class WatermarkController { public class WatermarkController {
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark") @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(summary = "Add watermark to a PDF file", @Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark text, font size, rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") public ResponseEntity<byte[]> addWatermark(
public ResponseEntity<byte[]> addWatermark( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to add a watermark") MultipartFile pdfFile,
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType,
@Parameter(description = "The input PDF file to add a watermark") @RequestPart(required = false) @Parameter(description = "The watermark text") String watermarkText,
MultipartFile pdfFile, @RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage,
@RequestParam(defaultValue = "roman", name = "alphabet")
@Parameter(description = "The selected alphabet", @RequestParam(defaultValue = "roman", name = "alphabet") @Parameter(description = "The selected alphabet",
schema = @Schema(type = "string", schema = @Schema(type = "string",
allowableValues = {"roman","arabic","japanese","korean","chinese"}, allowableValues = {"roman","arabic","japanese","korean","chinese"},
defaultValue = "roman")) defaultValue = "roman")) String alphabet,
String alphabet, @RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize,
@RequestParam("watermarkText") @RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation,
@Parameter(description = "The watermark text to add to the PDF file") @RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity,
String watermarkText, @RequestParam(defaultValue = "50", name = "widthSpacer") @Parameter(description = "The width spacer between watermark elements", example = "50") int widthSpacer,
@RequestParam(defaultValue = "30", name = "fontSize") @RequestParam(defaultValue = "50", name = "heightSpacer") @Parameter(description = "The height spacer between watermark elements", example = "50") int heightSpacer)
@Parameter(description = "The font size of the watermark text", example = "30") throws IOException, Exception {
float fontSize,
@RequestParam(defaultValue = "0", name = "rotation")
@Parameter(description = "The rotation of the watermark text in degrees", example = "0")
float rotation,
@RequestParam(defaultValue = "0.5", name = "opacity")
@Parameter(description = "The opacity of the watermark text (0.0 - 1.0)", example = "0.5")
float opacity,
@RequestParam(defaultValue = "50", name = "widthSpacer")
@Parameter(description = "The width spacer between watermark texts", example = "50")
int widthSpacer,
@RequestParam(defaultValue = "50", name = "heightSpacer")
@Parameter(description = "The height spacer between watermark texts", example = "50")
int heightSpacer) throws IOException, Exception {
// Load the input PDF // Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream()); PDDocument document = PDDocument.load(pdfFile.getInputStream());
String producer = document.getDocumentInformation().getProducer();
// Create a page in the document
for (PDPage page : document.getPages()) {
// Get the page's content stream // Create a page in the document
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); for (PDPage page : document.getPages()) {
// Set transparency // Get the page's content stream
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); PDPageContentStream contentStream = new PDPageContentStream(document, page,
graphicsState.setNonStrokingAlphaConstant(opacity); PDPageContentStream.AppendMode.APPEND, true);
contentStream.setGraphicsStateParameters(graphicsState);
// Set transparency
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream.setGraphicsStateParameters(graphicsState);
String resourceDir = ""; if (watermarkType.equalsIgnoreCase("text")) {
PDFont font = PDType1Font.HELVETICA_BOLD; addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer,
switch (alphabet) { fontSize, alphabet);
case "arabic": } else if (watermarkType.equalsIgnoreCase("image")) {
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer,
break; fontSize);
case "japanese": }
resourceDir = "static/fonts/Meiryo.ttf";
break;
case "korean":
resourceDir = "static/fonts/malgun.ttf";
break;
case "chinese":
resourceDir = "static/fonts/SimSun.ttf";
break;
case "roman":
default:
resourceDir = "static/fonts/NotoSans-Regular.ttf";
break;
}
// Close the content stream
contentStream.close();
}
return WebResponseUtils.pdfDocToWebResponse(document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
}
private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document,
PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException {
String resourceDir = "";
PDFont font = PDType1Font.HELVETICA_BOLD;
switch (alphabet) {
case "arabic":
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
break;
case "japanese":
resourceDir = "static/fonts/Meiryo.ttf";
break;
case "korean":
resourceDir = "static/fonts/malgun.ttf";
break;
case "chinese":
resourceDir = "static/fonts/SimSun.ttf";
break;
case "roman":
default:
resourceDir = "static/fonts/NotoSans-Regular.ttf";
break;
}
if(!resourceDir.equals("")) {
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os);
}
if(!resourceDir.equals("")) { font = PDType0Font.load(document, tempFile);
ClassPathResource classPathResource = new ClassPathResource(resourceDir); tempFile.deleteOnExit();
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os);
}
font = PDType0Font.load(document, tempFile);
tempFile.deleteOnExit();
}
contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
// Set size and location of watermark
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
float watermarkHeight = heightSpacer + fontSize;
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
// Add the watermark text
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
if(producer.contains("Google Docs")) {
//This fixes weird unknown google docs y axis rotation/flip issue
//TODO: Long term fix one day
//contentStream.setTextMatrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight);
Matrix matrix = new Matrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight);
contentStream.setTextMatrix(matrix);
} else {
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight));
}
contentStream.showTextWithPositioning(new Object[] { watermarkText });
}
}
contentStream.endText();
// Close the content stream
contentStream.close();
} }
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
} contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
// Set size and location of text watermark
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
float watermarkHeight = heightSpacer + fontSize;
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
// Add the text watermark
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
contentStream.beginText();
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation),
j * watermarkWidth, i * watermarkHeight));
contentStream.showText(watermarkText);
contentStream.endText();
}
}
}
private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation,
int widthSpacer, int heightSpacer, float fontSize) throws IOException {
// Load the watermark image
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
// Compute width based on original aspect ratio
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
// Desired physical height (in PDF points)
float desiredPhysicalHeight = fontSize ;
// Desired physical width based on the aspect ratio
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
// Convert the BufferedImage to PDImageXObject
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
// Calculate the number of rows and columns for watermarks
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
float x = j * (desiredPhysicalWidth + widthSpacer);
float y = i * (desiredPhysicalHeight + heightSpacer);
// Save the graphics state
contentStream.saveGraphicsState();
// Create rotation matrix and rotate
contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
// Draw the image and restore the graphics state
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
contentStream.restoreGraphicsState();
}
}
}
} }

View File

@@ -1,88 +1,102 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController { public class ConverterWebController {
@GetMapping("/img-to-pdf") @GetMapping("/img-to-pdf")
@Hidden @Hidden
public String convertImgToPdfForm(Model model) { public String convertImgToPdfForm(Model model) {
model.addAttribute("currentPage", "img-to-pdf"); model.addAttribute("currentPage", "img-to-pdf");
return "convert/img-to-pdf"; return "convert/img-to-pdf";
} }
@GetMapping("/html-to-pdf")
@GetMapping("/pdf-to-img") @Hidden
@Hidden public String convertHTMLToPdfForm(Model model) {
public String pdfToimgForm(Model model) { model.addAttribute("currentPage", "html-to-pdf");
model.addAttribute("currentPage", "pdf-to-img"); return "convert/html-to-pdf";
return "convert/pdf-to-img"; }
}
@GetMapping("/url-to-pdf")
@GetMapping("/file-to-pdf") @Hidden
@Hidden public String convertURLToPdfForm(Model model) {
public String convertToPdfForm(Model model) { model.addAttribute("currentPage", "url-to-pdf");
model.addAttribute("currentPage", "file-to-pdf"); return "convert/url-to-pdf";
return "convert/file-to-pdf"; }
}
@GetMapping("/pdf-to-img")
@Hidden
//PDF TO...... public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img");
@GetMapping("/pdf-to-html") return "convert/pdf-to-img";
@Hidden }
public ModelAndView pdfToHTML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html"); @GetMapping("/file-to-pdf")
modelAndView.addObject("currentPage", "pdf-to-html"); @Hidden
return modelAndView; public String convertToPdfForm(Model model) {
} model.addAttribute("currentPage", "file-to-pdf");
return "convert/file-to-pdf";
@GetMapping("/pdf-to-presentation") }
@Hidden
public ModelAndView pdfToPresentation() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
modelAndView.addObject("currentPage", "pdf-to-presentation"); //PDF TO......
return modelAndView;
} @GetMapping("/pdf-to-html")
@Hidden
@GetMapping("/pdf-to-text") public ModelAndView pdfToHTML() {
@Hidden ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
public ModelAndView pdfToText() { modelAndView.addObject("currentPage", "pdf-to-html");
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text"); return modelAndView;
modelAndView.addObject("currentPage", "pdf-to-text"); }
return modelAndView;
} @GetMapping("/pdf-to-presentation")
@Hidden
@GetMapping("/pdf-to-word") public ModelAndView pdfToPresentation() {
@Hidden ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
public ModelAndView pdfToWord() { modelAndView.addObject("currentPage", "pdf-to-presentation");
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word"); return modelAndView;
modelAndView.addObject("currentPage", "pdf-to-word"); }
return modelAndView;
} @GetMapping("/pdf-to-text")
@Hidden
@GetMapping("/pdf-to-xml") public ModelAndView pdfToText() {
@Hidden ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
public ModelAndView pdfToXML() { modelAndView.addObject("currentPage", "pdf-to-text");
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml"); return modelAndView;
modelAndView.addObject("currentPage", "pdf-to-xml"); }
return modelAndView;
} @GetMapping("/pdf-to-word")
@Hidden
public ModelAndView pdfToWord() {
@GetMapping("/pdf-to-pdfa") ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
@Hidden modelAndView.addObject("currentPage", "pdf-to-word");
public String pdfToPdfAForm(Model model) { return modelAndView;
model.addAttribute("currentPage", "pdf-to-pdfa"); }
return "convert/pdf-to-pdfa";
} @GetMapping("/pdf-to-xml")
} @Hidden
public ModelAndView pdfToXML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
modelAndView.addObject("currentPage", "pdf-to-xml");
return modelAndView;
}
@GetMapping("/pdf-to-pdfa")
@Hidden
public String pdfToPdfAForm(Model model) {
model.addAttribute("currentPage", "pdf-to-pdfa");
return "convert/pdf-to-pdfa";
}
}

View File

@@ -1,21 +1,81 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class GeneralWebController { public class GeneralWebController {
@GetMapping("/pipeline")
@Hidden @GetMapping("/pipeline")
public String pipelineForm(Model model) { @Hidden
model.addAttribute("currentPage", "pipeline"); public String pipelineForm(Model model) {
return "pipeline"; model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>();
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
List<Path> jsonFiles = paths
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.collect(Collectors.toList());
for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content);
}
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, Map.class);
String name = (String) jsonContent.get("name");
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", config);
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
} catch (IOException e) {
e.printStackTrace();
} }
model.addAttribute("pipelineConfigs", pipelineConfigs);
return "pipeline";
}
@GetMapping("/merge-pdfs") @GetMapping("/merge-pdfs")
@Hidden @Hidden
@@ -65,7 +125,64 @@ public class GeneralWebController {
@Hidden @Hidden
public String signForm(Model model) { public String signForm(Model model) {
model.addAttribute("currentPage", "sign"); model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames());
return "sign"; return "sign";
} }
private List<String> getFontNames() {
List<String> fontNames = new ArrayList<>();
try {
// Get the directory URL from classpath
URL dirURL = getClass().getClassLoader().getResource("static/fonts");
if (dirURL != null && dirURL.getProtocol().equals("file")) {
// If running from the file system (e.g., IDE)
fontNames.addAll(
Files.list(Paths.get(dirURL.toURI()))
.map(java.nio.file.Path::getFileName)
.map(java.nio.file.Path::toString)
.filter(name -> name.endsWith(".woff2"))
.map(name -> name.substring(0, name.length() - 6)) // Remove .woff2 extension
.collect(Collectors.toList())
);
} else {
// If running from a JAR file
// Resources in JAR go through a different URL protocol.
// In this case, we'll use a different approach to list them.
// Create a link to the resource. This assumes resources are at the root of the JAR.
URI uri = getClass().getResource("/").toURI();
FileSystem fileSystem = FileSystems.newFileSystem(uri, new HashMap<>());
Path myPath = fileSystem.getPath("/static/fonts/");
Files.walk(myPath, 1)
.filter(path -> !Files.isDirectory(path))
.map(path -> path.getFileName().toString())
.filter(name -> name.endsWith(".woff2"))
.map(name -> name.substring(0, name.length() - 6)) // Remove .woff2 extension
.forEach(fontNames::add);
fileSystem.close();
}
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("Failed to read font directory", e);
}
return fontNames;
}
@GetMapping("/crop")
@Hidden
public String cropForm(Model model) {
model.addAttribute("currentPage", "crop");
return "crop";
}
@GetMapping("/auto-split-pdf")
@Hidden
public String autoSPlitPDFForm(Model model) {
model.addAttribute("currentPage", "auto-split-pdf");
return "auto-split-pdf";
}
} }

View File

@@ -32,6 +32,13 @@ public class OtherWebController {
return modelAndView; return modelAndView;
} }
@GetMapping("/add-page-numbers")
@Hidden
public String addPageNumbersForm(Model model) {
model.addAttribute("currentPage", "add-page-numbers");
return "other/add-page-numbers";
}
@GetMapping("/extract-images") @GetMapping("/extract-images")
@Hidden @Hidden
public String extractImagesForm(Model model) { public String extractImagesForm(Model model) {
@@ -133,4 +140,13 @@ public class OtherWebController {
return "other/auto-crop"; return "other/auto-crop";
} }
@GetMapping("/auto-rename")
@Hidden
public String autoRenameForm(Model model) {
model.addAttribute("currentPage", "auto-rename");
return "other/auto-rename";
}
} }

View File

@@ -1,46 +1,53 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class SecurityWebController { public class SecurityWebController {
@GetMapping("/add-password") @GetMapping("/add-password")
@Hidden @Hidden
public String addPasswordForm(Model model) { public String addPasswordForm(Model model) {
model.addAttribute("currentPage", "add-password"); model.addAttribute("currentPage", "add-password");
return "security/add-password"; return "security/add-password";
} }
@GetMapping("/change-permissions") @GetMapping("/change-permissions")
@Hidden @Hidden
public String permissionsForm(Model model) { public String permissionsForm(Model model) {
model.addAttribute("currentPage", "change-permissions"); model.addAttribute("currentPage", "change-permissions");
return "security/change-permissions"; return "security/change-permissions";
} }
@GetMapping("/remove-password") @GetMapping("/remove-password")
@Hidden @Hidden
public String removePasswordForm(Model model) { public String removePasswordForm(Model model) {
model.addAttribute("currentPage", "remove-password"); model.addAttribute("currentPage", "remove-password");
return "security/remove-password"; return "security/remove-password";
} }
@GetMapping("/add-watermark") @GetMapping("/add-watermark")
@Hidden @Hidden
public String addWatermarkForm(Model model) { public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "add-watermark"); model.addAttribute("currentPage", "add-watermark");
return "security/add-watermark"; return "security/add-watermark";
} }
@GetMapping("/cert-sign") @GetMapping("/cert-sign")
@Hidden @Hidden
public String certSignForm(Model model) { public String certSignForm(Model model) {
model.addAttribute("currentPage", "cert-sign"); model.addAttribute("currentPage", "cert-sign");
return "security/cert-sign"; return "security/cert-sign";
} }
}
@GetMapping("/sanitize-pdf")
@Hidden
public String sanitizeForm(Model model) {
model.addAttribute("currentPage", "sanitize-pdf");
return "security/sanitize-pdf";
}
}

View File

@@ -22,4 +22,11 @@ public class PipelineOperation {
public void setParameters(Map<String, Object> parameters) { public void setParameters(Map<String, Object> parameters) {
this.parameters = parameters; this.parameters = parameters;
} }
@Override
public String toString() {
return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
}
} }

View File

@@ -1,91 +1,153 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.util.ArrayList; import java.io.IOException;
import java.util.List; import java.net.MalformedURLException;
import java.net.URL;
public class GeneralUtils { import java.nio.file.FileVisitResult;
import java.nio.file.Files;
public static Long convertSizeToBytes(String sizeStr) { import java.nio.file.Path;
if (sizeStr == null) { import java.nio.file.Paths;
return null; import java.nio.file.SimpleFileVisitor;
} import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
sizeStr = sizeStr.trim().toUpperCase(); import java.util.List;
try {
if (sizeStr.endsWith("KB")) { public class GeneralUtils {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
} else if (sizeStr.endsWith("MB")) { public static void deleteDirectory(Path path) throws IOException {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024); Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
} else if (sizeStr.endsWith("GB")) { @Override
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024); public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
} else if (sizeStr.endsWith("B")) { Files.delete(file);
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); return FileVisitResult.CONTINUE;
} else { }
// Input string does not have a valid format, handle this case
} @Override
} catch (NumberFormatException e) { public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// The numeric part of the input string cannot be parsed, handle this case Files.delete(dir);
} return FileVisitResult.CONTINUE;
}
return null; });
} }
public static List<Integer> parsePageList(String[] pageOrderArr, int totalPages) { public static String convertToFileName(String name) {
List<Integer> newPageOrder = new ArrayList<>(); String safeName = name.replaceAll("[^a-zA-Z0-9]", "_");
if (safeName.length() > 50) {
// loop through the page order array safeName = safeName.substring(0, 50);
for (String element : pageOrderArr) { }
// check if the element contains a range of pages return safeName;
if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { }
// Handle page order as a function
int coefficient = 0;
int constant = 0; public static boolean isValidURL(String urlStr) {
boolean coefficientExists = false; try {
boolean constantExists = false; new URL(urlStr);
return true;
if (element.contains("n")) { } catch (MalformedURLException e) {
String[] parts = element.split("n"); return false;
if (!parts[0].equals("") && parts[0] != null) { }
coefficient = Integer.parseInt(parts[0]); }
coefficientExists = true;
}
if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { public static Long convertSizeToBytes(String sizeStr) {
constant = Integer.parseInt(parts[1]); if (sizeStr == null) {
constantExists = true; return null;
} }
} else if (element.contains("+")) {
constant = Integer.parseInt(element.replace("+", "")); sizeStr = sizeStr.trim().toUpperCase();
constantExists = true; try {
} if (sizeStr.endsWith("KB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
for (int i = 1; i <= totalPages; i++) { } else if (sizeStr.endsWith("MB")) {
int pageNum = coefficientExists ? coefficient * i : i; return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024);
pageNum += constantExists ? constant : 0; } else if (sizeStr.endsWith("GB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024);
if (pageNum <= totalPages && pageNum > 0) { } else if (sizeStr.endsWith("B")) {
newPageOrder.add(pageNum - 1); return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} } else {
} // Input string does not have a valid format, handle this case
} else if (element.contains("-")) { }
// split the range into start and end page } catch (NumberFormatException e) {
String[] range = element.split("-"); // The numeric part of the input string cannot be parsed, handle this case
int start = Integer.parseInt(range[0]); }
int end = Integer.parseInt(range[1]);
// check if the end page is greater than total pages return null;
if (end > totalPages) { }
end = totalPages;
} public static List<Integer> parsePageList(String[] pageOrderArr, int totalPages) {
// loop through the range of pages List<Integer> newPageOrder = new ArrayList<>();
for (int j = start; j <= end; j++) {
// print the current index // loop through the page order array
newPageOrder.add(j - 1); for (String element : pageOrderArr) {
} if (element.equalsIgnoreCase("all")) {
} else { for (int i = 0; i < totalPages; i++) {
// if the element is a single page newPageOrder.add(i);
newPageOrder.add(Integer.parseInt(element) - 1); }
} // As all pages are already added, no need to check further
} break;
}
return newPageOrder; else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) {
} // Handle page order as a function
} int coefficient = 0;
int constant = 0;
boolean coefficientExists = false;
boolean constantExists = false;
if (element.contains("n")) {
String[] parts = element.split("n");
if (!parts[0].equals("") && parts[0] != null) {
coefficient = Integer.parseInt(parts[0]);
coefficientExists = true;
}
if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) {
constant = Integer.parseInt(parts[1]);
constantExists = true;
}
} else if (element.contains("+")) {
constant = Integer.parseInt(element.replace("+", ""));
constantExists = true;
}
for (int i = 1; i <= totalPages; i++) {
int pageNum = coefficientExists ? coefficient * i : i;
pageNum += constantExists ? constant : 0;
if (pageNum <= totalPages && pageNum > 0) {
newPageOrder.add(pageNum - 1);
}
}
} else if (element.contains("-")) {
// split the range into start and end page
String[] range = element.split("-");
int start = Integer.parseInt(range[0]);
int end = Integer.parseInt(range[1]);
// check if the end page is greater than total pages
if (end > totalPages) {
end = totalPages;
}
// loop through the range of pages
for (int j = start; j <= end; j++) {
// print the current index
newPageOrder.add(j - 1);
}
} else {
// if the element is a single page
newPageOrder.add(Integer.parseInt(element) - 1);
}
}
return newPageOrder;
}
public static boolean createDir(String path) {
Path folder = Paths.get(path);
if (!Files.exists(folder)) {
try {
Files.createDirectories(folder);
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
return true;
}
}

View File

@@ -44,7 +44,7 @@ public class PdfUtils {
public static PDRectangle textToPageSize(String size) { public static PDRectangle textToPageSize(String size) {
switch (size) { switch (size.toUpperCase()) {
case "A0": case "A0":
return PDRectangle.A0; return PDRectangle.A0;
case "A1": case "A1":
@@ -68,43 +68,37 @@ public class PdfUtils {
} }
} }
public boolean hasImageInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException {
PDFTextStripper textStripper = new PDFTextStripper();
String pdfText = "";
if(pagesToCheck == null || pagesToCheck.equals("all")) {
pdfText = textStripper.getText(pdfDocument);
} else { public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException {
// remove whitespaces String[] pageOrderArr = pagesToCheck.split(",");
pagesToCheck = pagesToCheck.replaceAll("\\s+", ""); List<Integer> pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
String[] splitPoints = pagesToCheck.split(","); for (int pageNumber : pageList) {
for (String splitPoint : splitPoints) { PDPage page = document.getPage(pageNumber);
if (splitPoint.contains("-")) { if (hasImagesOnPage(page)) {
// Handle page ranges return true;
String[] range = splitPoint.split("-");
int startPage = Integer.parseInt(range[0]);
int endPage = Integer.parseInt(range[1]);
for (int i = startPage; i <= endPage; i++) {
textStripper.setStartPage(i);
textStripper.setEndPage(i);
pdfText += textStripper.getText(pdfDocument);
}
} else {
// Handle individual page
int page = Integer.parseInt(splitPoint);
textStripper.setStartPage(page);
textStripper.setEndPage(page);
pdfText += textStripper.getText(pdfDocument);
}
} }
} }
pdfDocument.close(); return false;
return pdfText.contains(text);
} }
public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) throws IOException {
String[] pageOrderArr = pageNumbersToCheck.split(",");
List<Integer> pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
for (int pageNumber : pageList) {
PDPage page = document.getPage(pageNumber);
if (hasTextOnPage(page, phrase)) {
return true;
}
}
return false;
}
public static boolean hasImagesOnPage(PDPage page) throws IOException { public static boolean hasImagesOnPage(PDPage page) throws IOException {
ImageFinder imageFinder = new ImageFinder(page); ImageFinder imageFinder = new ImageFinder(page);
@@ -113,12 +107,17 @@ public class PdfUtils {
} }
public static boolean hasText(PDDocument document, String phrase) throws IOException {
PDFTextStripper pdfStripper = new PDFTextStripper();
String text = pdfStripper.getText(document);
return text.contains(phrase);
}
public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
PDFTextStripper textStripper = new PDFTextStripper();
PDDocument tempDoc = new PDDocument();
tempDoc.addPage(page);
String pageText = textStripper.getText(tempDoc);
tempDoc.close();
return pageText.contains(phrase);
}
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException { public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException {
PDFTextStripper textStripper = new PDFTextStripper(); PDFTextStripper textStripper = new PDFTextStripper();

View File

@@ -1,6 +1,7 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -13,7 +14,7 @@ import java.util.concurrent.Semaphore;
public class ProcessExecutor { public class ProcessExecutor {
public enum Processes { public enum Processes {
LIBRE_OFFICE, OCR_MY_PDF, PYTHON_OPENCV, GHOSTSCRIPT LIBRE_OFFICE, OCR_MY_PDF, PYTHON_OPENCV, GHOSTSCRIPT, WEASYPRINT
} }
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>(); private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
@@ -25,6 +26,7 @@ public class ProcessExecutor {
case OCR_MY_PDF -> 2; case OCR_MY_PDF -> 2;
case PYTHON_OPENCV -> 8; case PYTHON_OPENCV -> 8;
case GHOSTSCRIPT -> 16; case GHOSTSCRIPT -> 16;
case WEASYPRINT -> 16;
}; };
return new ProcessExecutor(semaphoreLimit); return new ProcessExecutor(semaphoreLimit);
}); });
@@ -35,14 +37,21 @@ public class ProcessExecutor {
private ProcessExecutor(int semaphoreLimit) { private ProcessExecutor(int semaphoreLimit) {
this.semaphore = new Semaphore(semaphoreLimit); this.semaphore = new Semaphore(semaphoreLimit);
} }
public int runCommandWithOutputHandling(List<String> command) throws IOException, InterruptedException { public int runCommandWithOutputHandling(List<String> command) throws IOException, InterruptedException {
return runCommandWithOutputHandling(command, null);
}
public int runCommandWithOutputHandling(List<String> command, File workingDirectory) throws IOException, InterruptedException {
int exitCode = 1; int exitCode = 1;
semaphore.acquire(); semaphore.acquire();
try { try {
System.out.print("Running command: " + String.join(" ", command)); System.out.print("Running command: " + String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command); ProcessBuilder processBuilder = new ProcessBuilder(command);
// Use the working directory if it's set
if (workingDirectory != null) {
processBuilder.directory(workingDirectory);
}
Process process = processBuilder.start(); Process process = processBuilder.start();
// Read the error stream and standard output stream concurrently // Read the error stream and standard output stream concurrently

View File

@@ -1,50 +1,61 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
public class WebResponseUtils {
public class WebResponseUtils {
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException {
} return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
}
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
} return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
}
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException {
// Return the PDF as a response public static ResponseEntity<byte[]> multiPartFileToWebResponse(MultipartFile file) throws IOException {
HttpHeaders headers = new HttpHeaders(); String fileName = file.getOriginalFilename();
headers.setContentType(mediaType); MediaType mediaType = MediaType.parseMediaType(file.getContentType());
headers.setContentLength(bytes.length);
String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"); byte[] bytes = file.getBytes();
headers.setContentDispositionFormData("attachment", encodedDocName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK); return bytesToWebResponse(bytes, fileName, mediaType);
} }
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName) throws IOException { public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException {
return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF);
} // Return the PDF as a response
HttpHeaders headers = new HttpHeaders();
public static ResponseEntity<byte[]> pdfDocToWebResponse(PDDocument document, String docName) throws IOException { headers.setContentType(mediaType);
headers.setContentLength(bytes.length);
// Open Byte Array and save document to it String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
ByteArrayOutputStream baos = new ByteArrayOutputStream(); headers.setContentDispositionFormData("attachment", encodedDocName);
document.save(baos); return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
// Close the document }
document.close();
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName) throws IOException {
return boasToWebResponse(baos, docName); return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF);
} }
} public static ResponseEntity<byte[]> pdfDocToWebResponse(PDDocument document, String docName) throws IOException {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
// Close the document
document.close();
return boasToWebResponse(baos, docName);
}
}

View File

@@ -15,7 +15,7 @@ server.error.whitelabel.enabled=false
server.error.include-stacktrace=always server.error.include-stacktrace=always
server.error.include-exception=true server.error.include-exception=true
server.error.include-message=always server.error.include-message=always
\
server.servlet.session.tracking-modes=cookie server.servlet.session.tracking-modes=cookie
server.servlet.context-path=${APP_ROOT_PATH:/} server.servlet.context-path=${APP_ROOT_PATH:/}
@@ -26,3 +26,7 @@ spring.thymeleaf.encoding=UTF-8
server.connection-timeout=${CONNECTION_TIMEOUT:5m} server.connection-timeout=${CONNECTION_TIMEOUT:5m}
spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000} spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000}
spring.resources.static-locations=file:customFiles/static/
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
#spring.thymeleaf.cache=false

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,141 +21,320 @@ filesSelected=files selected
noFavourites=No favourites added noFavourites=No favourites added
bored=Bored Waiting? bored=Bored Waiting?
alphabet=Alphabet alphabet=Alphabet
downloadPdf=Download PDF
text=Text
font=Font
selectFillter=-- Select --
pageNum=Page Number
sizes.small=Small
sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
#############
# NAVBAR #
#############
navbar.convert=Convert
navbar.security=Security
navbar.other=Other
navbar.darkmode=Dark Mode
navbar.pageOps=Page Operations
navbar.settings=Settings
#############
# SETTINGS #
#############
settings.title=Settings
settings.update=Update available
settings.appVersion=App Version:
settings.downloadOption.title=Choose download option (For single file non zip downloads):
settings.downloadOption.1=Open in same window
settings.downloadOption.2=Open in new window
settings.downloadOption.3=Download file
settings.zipThreshold=Zip files when the number of downloaded files exceeds
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############
home.desc=Your locally hosted one-stop-shop for all your PDF needs. home.desc=Your locally hosted one-stop-shop for all your PDF needs.
navbar.convert=Convert
navbar.security=Security
navbar.other=Other
navbar.darkmode=Dark Mode
navbar.pageOps=Page Operations
home.multiTool.title=PDF Multi Tool home.multiTool.title=PDF Multi Tool
home.multiTool.desc=Merge, Rotate, Rearrange, and Remove pages home.multiTool.desc=Merge, Rotate, Rearrange, and Remove pages
multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side,interactive,intractable,move
home.merge.title=Merge home.merge.title=Merge
home.merge.desc=Easily merge multiple PDFs into one. home.merge.desc=Easily merge multiple PDFs into one.
merge.tags=merge,Page operations,Back end,server side
home.split.title=Split home.split.title=Split
home.split.desc=Split PDFs into multiple documents home.split.desc=Split PDFs into multiple documents
split.tags=Page operations,divide,Multi Page,cut,server side
home.rotate.title=Rotate home.rotate.title=Rotate
home.rotate.desc=Easily rotate your PDFs. home.rotate.desc=Easily rotate your PDFs.
rotate.tags=server side
home.imageToPdf.title=Image to PDF home.imageToPdf.title=Image to PDF
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF. home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDF to Image home.pdfToImage.title=PDF to Image
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF) home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=Organise home.pdfOrganiser.title=Organise
home.pdfOrganiser.desc=Remove/Rearrange pages in any order home.pdfOrganiser.desc=Remove/Rearrange pages in any order
pdfOrganiser.tags=duplex,even,odd,sort,move
home.addImage.title=Add image home.addImage.title=Add image
home.addImage.desc=Adds a image onto a set location on the PDF home.addImage.desc=Adds a image onto a set location on the PDF
addImage.tags=img,jpg,picture,photo
home.watermark.title=Add Watermark home.watermark.title=Add Watermark
home.watermark.desc=Add a custom watermark to your PDF document. home.watermark.desc=Add a custom watermark to your PDF document.
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
home.permissions.title=Change Permissions home.permissions.title=Change Permissions
home.permissions.desc=Change the permissions of your PDF document home.permissions.desc=Change the permissions of your PDF document
permissions.tags=read,write,edit,print
home.removePages.title=Remove home.removePages.title=Remove
home.removePages.desc=Delete unwanted pages from your PDF document. home.removePages.desc=Delete unwanted pages from your PDF document.
removePages.tags=Remove pages,delete pages
home.addPassword.title=Add Password home.addPassword.title=Add Password
home.addPassword.desc=Encrypt your PDF document with a password. home.addPassword.desc=Encrypt your PDF document with a password.
addPassword.tags=secure,security
home.removePassword.title=Remove Password home.removePassword.title=Remove Password
home.removePassword.desc=Remove password protection from your PDF document. home.removePassword.desc=Remove password protection from your PDF document.
removePassword.tags=secure,Decrypt,security,unpassword,delete password
home.compressPdfs.title=Compress home.compressPdfs.title=Compress
home.compressPdfs.desc=Compress PDFs to reduce their file size. home.compressPdfs.desc=Compress PDFs to reduce their file size.
compressPdfs.tags=squish,small,tiny
home.changeMetadata.title=Change Metadata home.changeMetadata.title=Change Metadata
home.changeMetadata.desc=Change/Remove/Add metadata from a PDF document home.changeMetadata.desc=Change/Remove/Add metadata from a PDF document
changeMetadata.tags==Title,author,date,creation,time,publisher,producer,stats
home.fileToPDF.title=Convert file to PDF home.fileToPDF.title=Convert file to PDF
home.fileToPDF.desc=Convert nearly any file to PDF (DOCX, PNG, XLS, PPT, TXT and more) home.fileToPDF.desc=Convert nearly any file to PDF (DOCX, PNG, XLS, PPT, TXT and more)
fileToPDF.tags=transformation,format,document,picture,slide,text,conversion,office,docs,word,excel,powerpoint
home.ocr.title=OCR / Cleanup scans home.ocr.title=OCR / Cleanup scans
home.ocr.desc=Cleanup scans and detects text from images within a PDF and re-adds it as text. home.ocr.desc=Cleanup scans and detects text from images within a PDF and re-adds it as text.
ocr.tags=recognition,text,image,scan,read,identify,detection,editable
home.extractImages.title=Extract Images home.extractImages.title=Extract Images
home.extractImages.desc=Extracts all images from a PDF and saves them to zip home.extractImages.desc=Extracts all images from a PDF and saves them to zip
extractImages.tags=picture,photo,save,archive,zip,capture,grab
home.pdfToPDFA.title=PDF to PDF/A home.pdfToPDFA.title=PDF to PDF/A
home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation
home.PDFToWord.title=PDF to Word home.PDFToWord.title=PDF to Word
home.PDFToWord.desc=Convert PDF to Word formats (DOC, DOCX and ODT) home.PDFToWord.desc=Convert PDF to Word formats (DOC, DOCX and ODT)
PDFToWord.tags=doc,docx,odt,word,transformation,format,conversion,office,microsoft,docfile
home.PDFToPresentation.title=PDF to Presentation home.PDFToPresentation.title=PDF to Presentation
home.PDFToPresentation.desc=Convert PDF to Presentation formats (PPT, PPTX and ODP) home.PDFToPresentation.desc=Convert PDF to Presentation formats (PPT, PPTX and ODP)
PDFToPresentation.tags=slides,show,office,microsoft
home.PDFToText.title=PDF to Text/RTF home.PDFToText.title=PDF to RTF (Text)
home.PDFToText.desc=Convert PDF to Text or RTF format home.PDFToText.desc=Convert PDF to Text or RTF format
PDFToText.tags=richformat,richtextformat,rich text format
home.PDFToHTML.title=PDF to HTML home.PDFToHTML.title=PDF to HTML
home.PDFToHTML.desc=Convert PDF to HTML format home.PDFToHTML.desc=Convert PDF to HTML format
PDFToHTML.tags=web content,browser friendly
home.PDFToXML.title=PDF to XML home.PDFToXML.title=PDF to XML
home.PDFToXML.desc=Convert PDF to XML format home.PDFToXML.desc=Convert PDF to XML format
PDFToXML.tags=data-extraction,structured-content,interop,transformation,convert
home.ScannerImageSplit.title=Detect/Split Scanned photos home.ScannerImageSplit.title=Detect/Split Scanned photos
home.ScannerImageSplit.desc=Splits multiple photos from within a photo/PDF home.ScannerImageSplit.desc=Splits multiple photos from within a photo/PDF
ScannerImageSplit.tags=separate,auto-detect,scans,multi-photo,organize
home.sign.title=Sign home.sign.title=Sign
home.sign.desc=Adds signature to PDF by drawing, text or image home.sign.desc=Adds signature to PDF by drawing, text or image
sign.tags=authorize,initials,drawn-signature,text-sign,image-signature
home.flatten.title=Flatten home.flatten.title=Flatten
home.flatten.desc=Remove all interactive elements and forms from a PDF home.flatten.desc=Remove all interactive elements and forms from a PDF
flatten.tags=static,deactivate,non-interactive,streamline
home.repair.title=Repair home.repair.title=Repair
home.repair.desc=Tries to repair a corrupt/broken PDF home.repair.desc=Tries to repair a corrupt/broken PDF
repair.tags=fix,restore,correction,recover
home.removeBlanks.title=Remove Blank pages home.removeBlanks.title=Remove Blank pages
home.removeBlanks.desc=Detects and removes blank pages from a document home.removeBlanks.desc=Detects and removes blank pages from a document
removeBlanks.tags=cleanup,streamline,non-content,organize
home.compare.title=Compare home.compare.title=Compare
home.compare.desc=Compares and shows the differences between 2 PDF Documents home.compare.desc=Compares and shows the differences between 2 PDF Documents
compare.tags=differentiate,contrast,changes,analysis
home.certSign.title=Sign with Certificate home.certSign.title=Sign with Certificate
home.certSign.desc=Signs a PDF with a Certificate/Key (PEM/P12) home.certSign.desc=Signs a PDF with a Certificate/Key (PEM/P12)
certSign.tags=authenticate,PEM,P12,official,encrypt
home.pageLayout.title=Multi-Page Layout home.pageLayout.title=Multi-Page Layout
home.pageLayout.desc=Merge multiple pages of a PDF document into a single page home.pageLayout.desc=Merge multiple pages of a PDF document into a single page
pageLayout.tags=merge,composite,single-view,organize
home.scalePages.title=Adjust page size/scale home.scalePages.title=Adjust page size/scale
home.scalePages.desc=Change the size/scale of a page and/or its contents. home.scalePages.desc=Change the size/scale of a page and/or its contents.
scalePages.tags=resize,modify,dimension,adapt
home.pipeline.title=Pipeline home.pipeline.title=Pipeline (Advanced)
home.pipeline.desc=Pipeline desc. home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
pipeline.tags=automate,sequence,scripted,batch-process
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect home.add-page-numbers.title=Add Page Numbers
home.add-page-numbers.desc=Add Page numbers throughout a document in a set location
add-page-numbers.tags=paginate,label,organize,index
downloadPdf=Download PDF home.auto-rename.title=Auto Rename PDF File
text=Text home.auto-rename.desc=Auto renames a PDF file based on its detected header
font=Font auto-rename.tags=auto-detect,header-based,organize,relabel
selectFillter=-- Select --
pageNum=Page Number
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
adjust-contrast.tags=color-correction,tune,modify,enhance
home.crop.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
crop.tags=trim,shrink,edit,shape
home.autoSplitPDF.title=Auto Split Pages
home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code
autoSplitPDF.tags=QR-based,separate,scan-segment,organize
home.sanitizePdf.title=Sanitize
home.sanitizePdf.desc=Remove scripts and other elements from PDF files
sanitizePdf.tags=clean,secure,safe,remove-threats
home.URLToPDF.title=URL/Website To PDF
home.URLToPDF.desc=Converts any http(s)URL to PDF
URLToPDF.tags=web-capture,save-page,web-to-doc,archive
home.HTMLToPDF.title=HTML to PDF
home.HTMLToPDF.desc=Converts any HTML file or zip to PDF
HTMLToPDF.tags=markup,web-content,transformation,convert
###########################
# #
# WEB PAGES #
# #
###########################
#url-to-pdf
URLToPDF.title=URL To PDF
URLToPDF.header=URL To PDF
URLToPDF.submit=Convert
URLToPDF.credit=Uses WeasyPrint
#html-to-pdf
HTMLToPDF.title=HTML To PDF
HTMLToPDF.header=HTML To PDF
HTMLToPDF.help=Accepts HTML files and ZIPs containing html/css/images etc required
HTMLToPDF.submit=Convert
HTMLToPDF.credit=Uses WeasyPrint
#sanitizePDF
sanitizePDF.title=Sanitize PDF
sanitizePDF.header=Sanitize a PDF file
sanitizePDF.selectText.1=Remove JavaScript actions
sanitizePDF.selectText.2=Remove embedded files
sanitizePDF.selectText.3=Remove metadata
sanitizePDF.selectText.4=Remove links
sanitizePDF.selectText.5=Remove fonts
sanitizePDF.submit=Sanitize PDF
#addPageNumbers
addPageNumbers.title=Add Page Numbers
addPageNumbers.header=Add Page Numbers
addPageNumbers.selectText.1=Select PDF file:
addPageNumbers.selectText.2=Margin Size
addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text
addPageNumbers.submit=Add Page Numbers
#auto-rename
auto-rename.title=Auto Rename
auto-rename.header=Auto Rename PDF
auto-rename.submit=Auto Rename
#adjustContrast
adjustContrast.title=Adjust Contrast
adjustContrast.header=Adjust Contrast
adjustContrast.contrast=Contrast:
adjustContrast.brightness=Brightness:
adjustContrast.saturation=Saturation:
adjustContrast.download=Download
#crop
crop.title=Crop
crop.header=Crop Image
crop.submit=Submit
#autoSplitPDF
autoSplitPDF.title=Auto Split PDF
autoSplitPDF.header=Auto Split PDF
autoSplitPDF.description=Print, Insert, Scan, upload, and let us auto-separate your documents. No manual work sorting needed.
autoSplitPDF.selectText.1=Print out some divider sheets from below (Black and white is fine).
autoSplitPDF.selectText.2=Scan all your documents at once by inserting the divider sheet between them.
autoSplitPDF.selectText.3=Upload the single large scanned PDF file and let Stirling PDF handle the rest.
autoSplitPDF.selectText.4=Divider pages are automatically detected and removed, guaranteeing a neat final document.
autoSplitPDF.formPrompt=Submit PDF containing Stirling-PDF Page dividers:
autoSplitPDF.duplexMode=Duplex Mode (Front and back scanning)
autoSplitPDF.dividerDownload1=Download 'Auto Splitter Divider (minimal).pdf'
autoSplitPDF.dividerDownload2=Download 'Auto Splitter Divider (with instructions).pdf'
autoSplitPDF.submit=Submit
#pipeline
pipeline.title=Pipeline pipeline.title=Pipeline
#pageLayout
pageLayout.title=Multi Page Layout pageLayout.title=Multi Page Layout
pageLayout.header=Multi Page Layout pageLayout.header=Multi Page Layout
pageLayout.pagesPerSheet=Pages per sheet: pageLayout.pagesPerSheet=Pages per sheet:
pageLayout.submit=Submit pageLayout.submit=Submit
#scalePages
scalePages.title=Adjust page-scale scalePages.title=Adjust page-scale
scalePages.header=Adjust page-scale scalePages.header=Adjust page-scale
scalePages.pageSize=Size of a page of the document. scalePages.pageSize=Size of a page of the document.
scalePages.scaleFactor=Zoom level (crop) of a page. scalePages.scaleFactor=Zoom level (crop) of a page.
scalePages.submit=Submit scalePages.submit=Submit
#certSign
certSign.title=Certificate Signing certSign.title=Certificate Signing
certSign.header=Sign a PDF with your certificate (Work in progress) certSign.header=Sign a PDF with your certificate (Work in progress)
certSign.selectPDF=Select a PDF File for Signing: certSign.selectPDF=Select a PDF File for Signing:
@@ -167,12 +346,11 @@ certSign.password=Enter Your Keystore or Private Key Password (If Any):
certSign.showSig=Show Signature certSign.showSig=Show Signature
certSign.reason=Reason certSign.reason=Reason
certSign.location=Location certSign.location=Location
certSign.name=Name certSign.name=Name
certSign.submit=Sign PDF certSign.submit=Sign PDF
#removeBlanks
removeBlanks.title=Remove Blanks removeBlanks.title=Remove Blanks
removeBlanks.header=Remove Blank Pages removeBlanks.header=Remove Blank Pages
removeBlanks.threshold=Threshold: removeBlanks.threshold=Threshold:
@@ -181,12 +359,16 @@ removeBlanks.whitePercent=White Percent (%):
removeBlanks.whitePercentDesc=Percent of page that must be white to be removed removeBlanks.whitePercentDesc=Percent of page that must be white to be removed
removeBlanks.submit=Remove Blanks removeBlanks.submit=Remove Blanks
#compare
compare.title=Compare compare.title=Compare
compare.header=Compare PDFs compare.header=Compare PDFs
compare.document.1=Document 1 compare.document.1=Document 1
compare.document.2=Document 2 compare.document.2=Document 2
compare.submit=Compare compare.submit=Compare
#sign
sign.title=Sign sign.title=Sign
sign.header=Sign PDFs sign.header=Sign PDFs
sign.upload=Upload Image sign.upload=Upload Image
@@ -195,14 +377,20 @@ sign.text=Text Input
sign.clear=Clear sign.clear=Clear
sign.add=Add sign.add=Add
#repair
repair.title=Repair repair.title=Repair
repair.header=Repair PDFs repair.header=Repair PDFs
repair.submit=Repair repair.submit=Repair
#flatten
flatten.title=Flatten flatten.title=Flatten
flatten.header=Flatten PDFs flatten.header=Flatten PDFs
flatten.submit=Flatten flatten.submit=Flatten
#ScannerImageSplit
ScannerImageSplit.selectText.1=Angle Threshold: ScannerImageSplit.selectText.1=Angle Threshold:
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10). ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
ScannerImageSplit.selectText.3=Tolerance: ScannerImageSplit.selectText.3=Tolerance:
@@ -214,19 +402,6 @@ ScannerImageSplit.selectText.8=Sets the minimum contour area threshold for a pho
ScannerImageSplit.selectText.9=Border Size: ScannerImageSplit.selectText.9=Border Size:
ScannerImageSplit.selectText.10=Sets the size of the border added and removed to prevent white borders in the output (default: 1). ScannerImageSplit.selectText.10=Sets the size of the border added and removed to prevent white borders in the output (default: 1).
navbar.settings=Settings
settings.title=Settings
settings.update=Update available
settings.appVersion=App Version:
settings.downloadOption.title=Choose download option (For single file non zip downloads):
settings.downloadOption.1=Open in same window
settings.downloadOption.2=Open in new window
settings.downloadOption.3=Download file
settings.zipThreshold=Zip files when the number of downloaded files exceeds
#OCR #OCR
ocr.title=OCR / Scan Cleanup ocr.title=OCR / Scan Cleanup
@@ -248,7 +423,7 @@ ocr.credit=This service uses OCRmyPDF and Tesseract for OCR.
ocr.submit=Process PDF with OCR ocr.submit=Process PDF with OCR
#extractImages
extractImages.title=Extract Images extractImages.title=Extract Images
extractImages.header=Extract Images extractImages.header=Extract Images
extractImages.selectText=Select image format to convert extracted images to extractImages.selectText=Select image format to convert extracted images to
@@ -288,11 +463,13 @@ merge.title=Merge
merge.header=Merge multiple PDFs (2+) merge.header=Merge multiple PDFs (2+)
merge.submit=Merge merge.submit=Merge
#pdfOrganiser #pdfOrganiser
pdfOrganiser.title=Page Organiser pdfOrganiser.title=Page Organiser
pdfOrganiser.header=PDF Page Organiser pdfOrganiser.header=PDF Page Organiser
pdfOrganiser.submit=Rearrange Pages pdfOrganiser.submit=Rearrange Pages
#multiTool #multiTool
multiTool.title=PDF Multi Tool multiTool.title=PDF Multi Tool
multiTool.header=PDF Multi Tool multiTool.header=PDF Multi Tool
@@ -304,6 +481,7 @@ pageRemover.header=PDF Page remover
pageRemover.pagesToDelete=Pages to delete (Enter a comma-separated list of page numbers) : pageRemover.pagesToDelete=Pages to delete (Enter a comma-separated list of page numbers) :
pageRemover.submit=Delete Pages pageRemover.submit=Delete Pages
#rotate #rotate
rotate.title=Rotate PDF rotate.title=Rotate PDF
rotate.header=Rotate PDF rotate.header=Rotate PDF
@@ -311,8 +489,6 @@ rotate.selectAngle=Select rotation angle (in multiples of 90 degrees):
rotate.submit=Rotate rotate.submit=Rotate
#merge #merge
split.title=Split PDF split.title=Split PDF
split.header=Split PDF split.header=Split PDF
@@ -337,6 +513,7 @@ imageToPDF.selectText.2=Auto rotate PDF
imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple images) imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple images)
imageToPDF.selectText.4=Merge into single PDF imageToPDF.selectText.4=Merge into single PDF
imageToPDF.selectText.5=Convert to separate PDFs imageToPDF.selectText.5=Convert to separate PDFs
#pdfToImage #pdfToImage
pdfToImage.title=PDF to Image pdfToImage.title=PDF to Image
@@ -351,6 +528,7 @@ pdfToImage.grey=Greyscale
pdfToImage.blackwhite=Black and White (May lose data!) pdfToImage.blackwhite=Black and White (May lose data!)
pdfToImage.submit=Convert pdfToImage.submit=Convert
#addPassword #addPassword
addPassword.title=Add Password addPassword.title=Add Password
addPassword.header=Add password (Encrypt) addPassword.header=Add password (Encrypt)
@@ -372,6 +550,7 @@ addPassword.selectText.15=Restricts what can be done with the document once it i
addPassword.selectText.16=Restricts the opening of the document itself addPassword.selectText.16=Restricts the opening of the document itself
addPassword.submit=Encrypt addPassword.submit=Encrypt
#watermark #watermark
watermark.title=Add Watermark watermark.title=Add Watermark
watermark.header=Add Watermark watermark.header=Add Watermark
@@ -384,6 +563,7 @@ watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.submit=Add Watermark watermark.submit=Add Watermark
#remove-watermark #remove-watermark
remove-watermark.title=Remove Watermark remove-watermark.title=Remove Watermark
remove-watermark.header=Remove Watermark remove-watermark.header=Remove Watermark
@@ -391,6 +571,7 @@ remove-watermark.selectText.1=Select PDF to remove watermark from:
remove-watermark.selectText.2=Watermark Text: remove-watermark.selectText.2=Watermark Text:
remove-watermark.submit=Remove Watermark remove-watermark.submit=Remove Watermark
#Change permissions #Change permissions
permissions.title=Change Permissions permissions.title=Change Permissions
permissions.header=Change Permissions permissions.header=Change Permissions
@@ -407,6 +588,7 @@ permissions.selectText.9=Prevent printing
permissions.selectText.10=Prevent printing different formats permissions.selectText.10=Prevent printing different formats
permissions.submit=Change permissions.submit=Change
#remove password #remove password
removePassword.title=Remove password removePassword.title=Remove password
removePassword.header=Remove password (Decrypt) removePassword.header=Remove password (Decrypt)
@@ -414,6 +596,8 @@ removePassword.selectText.1=Select PDF to Decrypt
removePassword.selectText.2=Password removePassword.selectText.2=Password
removePassword.submit=Remove removePassword.submit=Remove
#changeMetadata
changeMetadata.title=Change Metadata changeMetadata.title=Change Metadata
changeMetadata.header=Change Metadata changeMetadata.header=Change Metadata
changeMetadata.selectText.1=Please edit the variables you wish to change changeMetadata.selectText.1=Please edit the variables you wish to change
@@ -432,27 +616,30 @@ changeMetadata.selectText.4=Other Metadata:
changeMetadata.selectText.5=Add Custom Metadata Entry changeMetadata.selectText.5=Add Custom Metadata Entry
changeMetadata.submit=Change changeMetadata.submit=Change
#xlsToPdf
xlsToPdf.title=Excel to PDF xlsToPdf.title=Excel to PDF
xlsToPdf.header=Excel to PDF xlsToPdf.header=Excel to PDF
xlsToPdf.selectText.1=Select XLS or XLSX Excel sheet to convert xlsToPdf.selectText.1=Select XLS or XLSX Excel sheet to convert
xlsToPdf.convert=convert xlsToPdf.convert=convert
#pdfToPDFA
pdfToPDFA.title=PDF To PDF/A pdfToPDFA.title=PDF To PDF/A
pdfToPDFA.header=PDF To PDF/A pdfToPDFA.header=PDF To PDF/A
pdfToPDFA.credit=This service uses OCRmyPDF for PDF/A conversion pdfToPDFA.credit=This service uses OCRmyPDF for PDF/A conversion
pdfToPDFA.submit=Convert pdfToPDFA.submit=Convert
#PDFToWord
PDFToWord.title=PDF to Word PDFToWord.title=PDF to Word
PDFToWord.header=PDF to Word PDFToWord.header=PDF to Word
PDFToWord.selectText.1=Output file format PDFToWord.selectText.1=Output file format
PDFToWord.credit=This service uses LibreOffice for file conversion. PDFToWord.credit=This service uses LibreOffice for file conversion.
PDFToWord.submit=Convert PDFToWord.submit=Convert
#PDFToPresentation
PDFToPresentation.title=PDF to Presentation PDFToPresentation.title=PDF to Presentation
PDFToPresentation.header=PDF to Presentation PDFToPresentation.header=PDF to Presentation
PDFToPresentation.selectText.1=Output file format PDFToPresentation.selectText.1=Output file format
@@ -460,31 +647,23 @@ PDFToPresentation.credit=This service uses LibreOffice for file conversion.
PDFToPresentation.submit=Convert PDFToPresentation.submit=Convert
PDFToText.title=PDF to Text/RTF #PDFToText
PDFToText.header=PDF to Text/RTF PDFToText.title=PDF to RTF (Text)
PDFToText.header=PDF to RTF (Text)
PDFToText.selectText.1=Output file format PDFToText.selectText.1=Output file format
PDFToText.credit=This service uses LibreOffice for file conversion. PDFToText.credit=This service uses LibreOffice for file conversion.
PDFToText.submit=Convert PDFToText.submit=Convert
#PDFToHTML
PDFToHTML.title=PDF to HTML PDFToHTML.title=PDF to HTML
PDFToHTML.header=PDF to HTML PDFToHTML.header=PDF to HTML
PDFToHTML.credit=This service uses LibreOffice for file conversion. PDFToHTML.credit=This service uses LibreOffice for file conversion.
PDFToHTML.submit=Convert PDFToHTML.submit=Convert
#PDFToXML
PDFToXML.title=PDF to XML PDFToXML.title=PDF to XML
PDFToXML.header=PDF to XML PDFToXML.header=PDF to XML
PDFToXML.credit=This service uses LibreOffice for file conversion. PDFToXML.credit=This service uses LibreOffice for file conversion.
PDFToXML.submit=Convert PDFToXML.submit=Convert

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
########### ###########
# Generic # # Generic #
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr=left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
pdfPrompt=Hautatu PDFa(k) pdfPrompt=Hautatu PDFa(k)
@@ -21,138 +21,440 @@ filesSelected=Hautatutako fitxategiak
noFavourites=Ez dira gogokoak gehitu noFavourites=Ez dira gogokoak gehitu
bored=Itxaroten aspertuta? bored=Itxaroten aspertuta?
alphabet=Alfabetoa alphabet=Alfabetoa
############# downloadPdf=PDFa deskargatu
# HOME-PAGE # text=Testua
############# font=Letra-tipoa
home.desc=Zure leihatila bakarra autoostatatua zure PDF behar guztietarako selectFillter=-- Select --
pageNum=Orrialde-zenbakia
sizes.small=Small
sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=PDF dokumentua pasahitzarekin babestuta dago eta pasahitza ez da sartu edo akastuna da
#############
# NAVBAR #
#############
navbar.convert=Bihurtu navbar.convert=Bihurtu
navbar.security=Segurtasuna navbar.security=Segurtasuna
navbar.other=Beste bat navbar.other=Beste bat
navbar.darkmode=Modu iluna navbar.darkmode=Modu iluna
navbar.pageOps=Orrialde-eragiketak navbar.pageOps=Orrialde-eragiketak
navbar.settings=Ezarpenak
#############
# SETTINGS #
#############
settings.title=Ezarpenak
settings.update=Eguneratze eskuragarria
settings.appVersion=Aplikazioaren bertsioa:
settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe):
settings.downloadOption.1=Ireki leiho berean
settings.downloadOption.2=Ireki leiho berrian
settings.downloadOption.3=Deskargatu fitxategia
settings.zipThreshold=ZIP fitxategiak deskargatutako fitxategi kopurua gainditzen denean
#############
# HOME-PAGE #
#############
home.desc=Zure leihatila bakarra autoostatatua zure PDF behar guztietarako
home.multiTool.title=Erabilera anitzeko tresna PDF home.multiTool.title=Erabilera anitzeko tresna PDF
home.multiTool.desc= Orriak konbinatu, biratu, berrantolatu eta ezabatu home.multiTool.desc=Orriak konbinatu, biratu, berrantolatu eta ezabatu
multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side
home.merge.title=Elkartu home.merge.title=Elkartu
home.merge.desc=Elkartu zenbait PDF dokumentu bakar batean modu errazean home.merge.desc=Elkartu zenbait PDF dokumentu bakar batean modu errazean
merge.tags=merge,Page operations,Back end,server side
home.split.title=Zatitu home.split.title=Zatitu
home.split.desc=Zatitu PDFak zenbait dokumentutan home.split.desc=Zatitu PDFak zenbait dokumentutan
##########################
### TODO: Translate ###
##########################
split.tags=Page operations,divide,Multi Page,cut,server side
home.rotate.title=Biratu home.rotate.title=Biratu
home.rotate.desc=Biratu PDFak modu errazean home.rotate.desc=Biratu PDFak modu errazean
##########################
### TODO: Translate ###
##########################
rotate.tags=server side
home.imageToPdf.title=Irudia PDF bihurtu home.imageToPdf.title=Irudia PDF bihurtu
home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu
##########################
### TODO: Translate ###
##########################
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDFa irudi bihurtu home.pdfToImage.title=PDFa irudi bihurtu
home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu
##########################
### TODO: Translate ###
##########################
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=Antolatzailea home.pdfOrganiser.title=Antolatzailea
home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan
##########################
### TODO: Translate ###
##########################
pdfOrganiser.tags=duplex,even,odd,sort,move
home.addImage.title=Gehitu irudia PDFari home.addImage.title=Gehitu irudia PDFari
home.addImage.desc=Gehitu irudi bat PDFan ezarritako kokaleku batean (lanean) home.addImage.desc=Gehitu irudi bat PDFan ezarritako kokaleku batean (lanean)
##########################
### TODO: Translate ###
##########################
addImage.tags=img,jpg,picture,photo
home.watermark.title=Gehitu ur-marka home.watermark.title=Gehitu ur-marka
home.watermark.desc=Gehitu aurrez zehaztutako ur-marka bat PFD dokumentuari home.watermark.desc=Gehitu aurrez zehaztutako ur-marka bat PFD dokumentuari
##########################
home.remove-watermark.title= Ezabatu ur-marka ### TODO: Translate ###
home.remove-watermark.desc= Ezabatu ur-marka PDF dokumentutik ##########################
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
home.permissions.title=Aldatu baimenak home.permissions.title=Aldatu baimenak
home.permissions.desc=Aldatu PDF dokumentuaren baimenak home.permissions.desc=Aldatu PDF dokumentuaren baimenak
##########################
### TODO: Translate ###
##########################
permissions.tags=read,write,edit,print
home.removePages.title=Ezabatu home.removePages.title=Ezabatu
home.removePages.desc=Ezabatu nahi ez dituzun orrialdeak PDF dokumentutik home.removePages.desc=Ezabatu nahi ez dituzun orrialdeak PDF dokumentutik
##########################
### TODO: Translate ###
##########################
removePages.tags=Remove pages,delete pages
home.addPassword.title=Gehitu pasahitza home.addPassword.title=Gehitu pasahitza
home.addPassword.desc=Enkriptatu PDF dokumentua pasahitz batekin home.addPassword.desc=Enkriptatu PDF dokumentua pasahitz batekin
##########################
### TODO: Translate ###
##########################
addPassword.tags=secure,security
home.removePassword.title=Ezabatu pasahitza home.removePassword.title=Ezabatu pasahitza
home.removePassword.desc=Ezabatu pasahitza PDF dokumentutik home.removePassword.desc=Ezabatu pasahitza PDF dokumentutik
##########################
### TODO: Translate ###
##########################
removePassword.tags=secure,Decrypt,security,unpassword,delete password
home.compressPdfs.title=Konprimatu home.compressPdfs.title=Konprimatu
home.compressPdfs.desc=Konprimatu PDFak fitxategiaren tamaina murrizteko home.compressPdfs.desc=Konprimatu PDFak fitxategiaren tamaina murrizteko
##########################
### TODO: Translate ###
##########################
compressPdfs.tags=squish,small,tiny
home.changeMetadata.title=Aldatu metadatuak home.changeMetadata.title=Aldatu metadatuak
home.changeMetadata.desc=Aldatu/Ezabatu/Gehitu metadatuak PDF dokumentuari home.changeMetadata.desc=Aldatu/Ezabatu/Gehitu metadatuak PDF dokumentuari
##########################
### TODO: Translate ###
##########################
changeMetadata.tags==Title,author,date,creation,time,publisher,producer,stats
home.fileToPDF.title=Fitxategia PDF bihurtu home.fileToPDF.title=Fitxategia PDF bihurtu
home.fileToPDF.desc=PDF bihurtu ia edozein fitxategi (DOCX, PNG, XLS, PPT, TXT eta gehiago) home.fileToPDF.desc=PDF bihurtu ia edozein fitxategi (DOCX, PNG, XLS, PPT, TXT eta gehiago)
##########################
### TODO: Translate ###
##########################
fileToPDF.tags=transformation,format,document,picture,slide,text,conversion,office,docs,word,excel,powerpoint
home.ocr.title=OCR exekutatu PDFan eta/edo garbiketa-eskaneatzeak home.ocr.title=OCR exekutatu PDFan eta/edo garbiketa-eskaneatzeak
home.ocr.desc=Garbiketa-eskaneatzeak eta irudi-testuak detektatu PDF baten barruan eta berriz ere gehitu testu gisa home.ocr.desc=Garbiketa-eskaneatzeak eta irudi-testuak detektatu PDF baten barruan eta berriz ere gehitu testu gisa
##########################
### TODO: Translate ###
##########################
ocr.tags=recognition,text,image,scan,read,identify,detection,editable
home.extractImages.title=Atera irudiak home.extractImages.title=Atera irudiak
home.extractImages.desc=Atera irudi guztiak PDF batetik eta ZIPen gorde home.extractImages.desc=Atera irudi guztiak PDF batetik eta ZIPen gorde
##########################
### TODO: Translate ###
##########################
extractImages.tags=picture,photo,save,archive,zip,capture,grab
home.pdfToPDFA.title=PDFa PDF/A bihurtu home.pdfToPDFA.title=PDFa PDF/A bihurtu
home.pdfToPDFA.desc=PDFa PDF/A bihurtu luzaro biltegiratzeko home.pdfToPDFA.desc=PDFa PDF/A bihurtu luzaro biltegiratzeko
##########################
### TODO: Translate ###
##########################
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation
home.PDFToWord.title=PDFa Word Bihurtu home.PDFToWord.title=PDFa Word Bihurtu
home.PDFToWord.desc=PDF formatuak Word bihurtu (DOC, DOCX y ODT) home.PDFToWord.desc=PDF formatuak Word bihurtu (DOC, DOCX y ODT)
##########################
### TODO: Translate ###
##########################
PDFToWord.tags=doc,docx,odt,word,transformation,format,conversion,office,microsoft,docfile
home.PDFToPresentation.title=PDFa aurkezpen bihurtu home.PDFToPresentation.title=PDFa aurkezpen bihurtu
home.PDFToPresentation.desc=PDFa aurkezpen formatu bihurtu (PPT, PPTX y ODP) home.PDFToPresentation.desc=PDFa aurkezpen formatu bihurtu (PPT, PPTX y ODP)
##########################
### TODO: Translate ###
##########################
PDFToPresentation.tags=slides,show,office,microsoft
home.PDFToText.title=PDFa TXT edo RTF bihurtu home.PDFToText.title=PDFa TXT edo RTF bihurtu
home.PDFToText.desc=PDFa TXT edo RTF formatu bihurtu home.PDFToText.desc=PDFa TXT edo RTF formatu bihurtu
##########################
### TODO: Translate ###
##########################
PDFToText.tags=richformat,richtextformat,rich text format
home.PDFToHTML.title=PDFa HTML bihurtu home.PDFToHTML.title=PDFa HTML bihurtu
home.PDFToHTML.desc=PDFa HTML formatu bihurtu home.PDFToHTML.desc=PDFa HTML formatu bihurtu
##########################
### TODO: Translate ###
##########################
PDFToHTML.tags=web content,browser friendly
home.PDFToXML.title=PDFa XML bihurtu home.PDFToXML.title=PDFa XML bihurtu
home.PDFToXML.desc=PDFa XML formatu bihurtu home.PDFToXML.desc=PDFa XML formatu bihurtu
##########################
### TODO: Translate ###
##########################
PDFToXML.tags=data-extraction,structured-content,interop,transformation,convert
home.ScannerImageSplit.title=Detektatu/Zatitu argazki eskaneatuak home.ScannerImageSplit.title=Detektatu/Zatitu argazki eskaneatuak
home.ScannerImageSplit.desc=Hainbat argazki zatitu argazki/PDF baten barruan home.ScannerImageSplit.desc=Hainbat argazki zatitu argazki/PDF baten barruan
##########################
### TODO: Translate ###
##########################
ScannerImageSplit.tags=separate,auto-detect,scans,multi-photo,organize
home.sign.title=Sinatu home.sign.title=Sinatu
home.sign.desc=Gehitu sinadura PDFari marrazki, testu edo irudi bidez home.sign.desc=Gehitu sinadura PDFari marrazki, testu edo irudi bidez
##########################
### TODO: Translate ###
##########################
sign.tags=authorize,initials,drawn-signature,text-sign,image-signature
home.flatten.title=Lautu home.flatten.title=Lautu
home.flatten.desc=PDF batetik elementu eta inprimaki interaktibo guztiak ezabatu home.flatten.desc=PDF batetik elementu eta inprimaki interaktibo guztiak ezabatu
##########################
### TODO: Translate ###
##########################
flatten.tags=static,deactivate,non-interactive,streamline
home.repair.title=Konpondu home.repair.title=Konpondu
home.repair.desc=Saiatu PDF hondatu/kaltetu bat konpontzen home.repair.desc=Saiatu PDF hondatu/kaltetu bat konpontzen
##########################
### TODO: Translate ###
##########################
repair.tags=fix,restore,correction,recover
home.removeBlanks.title=Ezabatu orrialde zuriak home.removeBlanks.title=Ezabatu orrialde zuriak
home.removeBlanks.desc=Detektatu orrialde zuriak eta dokumentutik ezabatu home.removeBlanks.desc=Detektatu orrialde zuriak eta dokumentutik ezabatu
##########################
home.certSign.title=Sinatu ziurtagiriarekin ### TODO: Translate ###
home.certSign.desc=Sinatu PDF bat Ziurtagiri/Gako batekin (PEM/P12) ##########################
removeBlanks.tags=cleanup,streamline,non-content,organize
home.compare.title=Konparatu home.compare.title=Konparatu
home.compare.desc=Konparatu eta erakutsi 2 PDF dokumenturen aldeak home.compare.desc=Konparatu eta erakutsi 2 PDF dokumenturen aldeak
##########################
### TODO: Translate ###
##########################
compare.tags=differentiate,contrast,changes,analysis
home.certSign.title=Sinatu ziurtagiriarekin
home.certSign.desc=Sinatu PDF bat Ziurtagiri/Gako batekin (PEM/P12)
##########################
### TODO: Translate ###
##########################
certSign.tags=authenticate,PEM,P12,official,encrypt
home.pageLayout.title=Zenbait orrialderen diseinua home.pageLayout.title=Zenbait orrialderen diseinua
home.pageLayout.desc=Elkartu orri bakar batean PDF dokumentu baten zenbait orrialde home.pageLayout.desc=Elkartu orri bakar batean PDF dokumentu baten zenbait orrialde
##########################
### TODO: Translate ###
##########################
pageLayout.tags=merge,composite,single-view,organize
home.scalePages.title=Eskalatu/Doitu orrialdearen tamaina home.scalePages.title=Eskalatu/Doitu orrialdearen tamaina
home.scalePages.desc=Eskalatu/Aldatu orrialde baten tamaina eta/edo edukia home.scalePages.desc=Eskalatu/Aldatu orrialde baten tamaina eta/edo edukia
##########################
### TODO: Translate ###
##########################
scalePages.tags=resize,modify,dimension,adapt
error.pdfPassword=PDF dokumentua pasahitzarekin babestuta dago eta pasahitza ez da sartu edo akastuna da home.pipeline.title=Pipeline (Advanced)
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
##########################
### TODO: Translate ###
##########################
pipeline.tags=automate,sequence,scripted,batch-process
downloadPdf=PDFa deskargatu home.add-page-numbers.title=Add Page Numbers
text=Testua home.add-page-numbers.desc=Add Page numbers throughout a document in a set location
font=Letra-tipoa ##########################
selectFilter=-- Hautatu -- ### TODO: Translate ###
pageNum=Orrialde-zenbakia ##########################
add-page-numbers.tags=paginate,label,organize,index
home.auto-rename.title=Auto Rename PDF File
home.auto-rename.desc=Auto renames a PDF file based on its detected header
##########################
### TODO: Translate ###
##########################
auto-rename.tags=auto-detect,header-based,organize,relabel
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
##########################
### TODO: Translate ###
##########################
adjust-contrast.tags=color-correction,tune,modify,enhance
home.crop.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
##########################
### TODO: Translate ###
##########################
crop.tags=trim,shrink,edit,shape
home.autoSplitPDF.title=Auto Split Pages
home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code
##########################
### TODO: Translate ###
##########################
autoSplitPDF.tags=QR-based,separate,scan-segment,organize
home.sanitizePdf.title=Sanitize
home.sanitizePdf.desc=Remove scripts and other elements from PDF files
##########################
### TODO: Translate ###
##########################
sanitizePdf.tags=clean,secure,safe,remove-threats
##########################
### TODO: Translate ###
##########################
home.URLToPDF.title=URL/Website To PDF
home.URLToPDF.desc=Converts any http(s)URL to PDF
URLToPDF.tags=web-capture,save-page,web-to-doc,archive
##########################
### TODO: Translate ###
##########################
home.HTMLToPDF.title=HTML to PDF
home.HTMLToPDF.desc=Converts any HTML file or zip to PDF
HTMLToPDF.tags=markup,web-content,transformation,convert
###########################
# #
# WEB PAGES #
# #
###########################
#url-to-pdf
URLToPDF.title=URL To PDF
URLToPDF.header=URL To PDF
URLToPDF.submit=Convert
URLToPDF.credit=Uses WeasyPrint
#html-to-pdf
HTMLToPDF.title=HTML To PDF
HTMLToPDF.header=HTML To PDF
HTMLToPDF.help=Accepts HTML files and ZIPs containing html/css/images etc required
HTMLToPDF.submit=Convert
HTMLToPDF.credit=Uses WeasyPrint
#sanitizePDF
sanitizePDF.title=Sanitize PDF
sanitizePDF.header=Sanitize a PDF file
sanitizePDF.selectText.1=Remove JavaScript actions
sanitizePDF.selectText.2=Remove embedded files
sanitizePDF.selectText.3=Remove metadata
sanitizePDF.selectText.4=Remove links
sanitizePDF.selectText.5=Remove fonts
sanitizePDF.submit=Sanitize PDF
#addPageNumbers
addPageNumbers.title=Add Page Numbers
addPageNumbers.header=Add Page Numbers
addPageNumbers.selectText.1=Select PDF file:
addPageNumbers.selectText.2=Margin Size
addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text
addPageNumbers.submit=Add Page Numbers
#auto-rename
auto-rename.title=Auto Rename
auto-rename.header=Auto Rename PDF
auto-rename.submit=Auto Rename
#adjustContrast
adjustContrast.title=Adjust Contrast
adjustContrast.header=Adjust Contrast
adjustContrast.contrast=Contrast:
adjustContrast.brightness=Brightness:
adjustContrast.saturation=Saturation:
adjustContrast.download=Download
#crop
crop.title=Crop
crop.header=Crop Image
crop.submit=Submit
#autoSplitPDF
autoSplitPDF.title=Auto Split PDF
autoSplitPDF.header=Auto Split PDF
autoSplitPDF.description=Print, Insert, Scan, upload, and let us auto-separate your documents. No manual work sorting needed.
autoSplitPDF.selectText.1=Print out some divider sheets from below (Black and white is fine).
autoSplitPDF.selectText.2=Scan all your documents at once by inserting the divider sheet between them.
autoSplitPDF.selectText.3=Upload the single large scanned PDF file and let Stirling PDF handle the rest.
autoSplitPDF.selectText.4=Divider pages are automatically detected and removed, guaranteeing a neat final document.
autoSplitPDF.formPrompt=Submit PDF containing Stirling-PDF Page dividers:
autoSplitPDF.duplexMode=Duplex Mode (Front and back scanning)
autoSplitPDF.dividerDownload1=Download 'Auto Splitter Divider (minimal).pdf'
autoSplitPDF.dividerDownload2=Download 'Auto Splitter Divider (with instructions).pdf'
autoSplitPDF.submit=Submit
#pipeline
pipeline.title=Pipeline
#pageLayout
pageLayout.title=Hainbat orrialderen diseinua pageLayout.title=Hainbat orrialderen diseinua
pageLayout.header=Hainbat orrialderen diseinua pageLayout.header=Hainbat orrialderen diseinua
pageLayout.pagesPerSheet=Orrialdeak orriko: pageLayout.pagesPerSheet=Orrialdeak orriko:
pageLayout.submit=Entregatu pageLayout.submit=Entregatu
#scalePages
scalePages.title=Doitu orrialdearen eskala scalePages.title=Doitu orrialdearen eskala
scalePages.header=Doitu orrialdearen eskala scalePages.header=Doitu orrialdearen eskala
scalePages.pageSize=Dokumentuaren orrialdearen tamaina scalePages.pageSize=Dokumentuaren orrialdearen tamaina
scalePages.scaleFactor=Orriaren zoom maila (moztea) scalePages.scaleFactor=Orriaren zoom maila (moztea)
scalePages.submit=Entregatu scalePages.submit=Entregatu
#certSign
certSign.title=Ziurtagiriaren sinadura certSign.title=Ziurtagiriaren sinadura
certSign.header=Sinatu PDF bat haren ziurtagiriarekin (lanean) certSign.header=Sinatu PDF bat haren ziurtagiriarekin (lanean)
certSign.selectPDF=Hautatu PDF fitxategi bat sinatzeko: certSign.selectPDF=Hautatu PDF fitxategi bat sinatzeko:
@@ -167,6 +469,8 @@ certSign.location=Kokalekua
certSign.name=Izena certSign.name=Izena
certSign.submit=Sinatu PDFa certSign.submit=Sinatu PDFa
#removeBlanks
removeBlanks.title=Ezabatu zuriuneak removeBlanks.title=Ezabatu zuriuneak
removeBlanks.header=Ezabatu orrialde zuriak removeBlanks.header=Ezabatu orrialde zuriak
removeBlanks.threshold=Gutxieneko balioa: removeBlanks.threshold=Gutxieneko balioa:
@@ -175,28 +479,38 @@ removeBlanks.whitePercent=Zuriaren protzentajea (%):
removeBlanks.whitePercentDesc=Zuria izan behar den orriaren ehunekoa ezabatua izan dadin removeBlanks.whitePercentDesc=Zuria izan behar den orriaren ehunekoa ezabatua izan dadin
removeBlanks.submit=Ezabatu zuriuneak removeBlanks.submit=Ezabatu zuriuneak
#compare
compare.title=Konparatu compare.title=Konparatu
compare.header=Konparatu PDF fitxategiak compare.header=Konparatu PDF fitxategiak
compare.document.1=1. dokumentua compare.document.1=1. dokumentua
compare.document.2=2. dokumentua compare.document.2=2. dokumentua
compare.submit=Konparatu compare.submit=Konparatu
#sign
sign.title=Sinatu sign.title=Sinatu
sign.header=Sinatu PDF fitxategiak sign.header=Sinatu PDF fitxategiak
sign.upload=Igo irudia sign.upload=Igo irudia
sign.draw=Marraztu sinadura sign.draw=Marraztu sinadura
sign.text=Testua sartzea sign.text=Testua sartzea
sign.clear=Garbitu sign.clear=Garbitu
sign.add=Gehitu sign.add=Gehitu
#repair
repair.title=Konpondu repair.title=Konpondu
repair.header=Konpondu PDF fitxategiak repair.header=Konpondu PDF fitxategiak
repair.submit=Konpondu repair.submit=Konpondu
#flatten
flatten.title=Lautu flatten.title=Lautu
flatten.header=Akoplatu PDF fitxategiak flatten.header=Akoplatu PDF fitxategiak
flatten.submit=Lautu flatten.submit=Lautu
#ScannerImageSplit
ScannerImageSplit.selectText.1=Angeluaren gutxieneko balioa: ScannerImageSplit.selectText.1=Angeluaren gutxieneko balioa:
ScannerImageSplit.selectText.2=Ezarri eskatutako gutxieneko angelu absolutua irudia biratzeko (lehenetsia: 10). ScannerImageSplit.selectText.2=Ezarri eskatutako gutxieneko angelu absolutua irudia biratzeko (lehenetsia: 10).
ScannerImageSplit.selectText.3=Tolerantzia: ScannerImageSplit.selectText.3=Tolerantzia:
@@ -208,18 +522,6 @@ ScannerImageSplit.selectText.8=Ezarri inguruko arearen gutxieneko balioa argazki
ScannerImageSplit.selectText.9=Ertzaren tamaina: ScannerImageSplit.selectText.9=Ertzaren tamaina:
ScannerImageSplit.selectText.10=Ezarri gehitutako eta ezabatutako ertzaren tamaina irteeran ertz zuriak saihesteko (lehenetsia: 1). ScannerImageSplit.selectText.10=Ezarri gehitutako eta ezabatutako ertzaren tamaina irteeran ertz zuriak saihesteko (lehenetsia: 1).
navbar.settings=Ezarpenak
settings.title=Ezarpenak
settings.update=Eguneratze eskuragarria
settings.appVersion=Aplikazioaren bertsioa:
settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe):
settings.downloadOption.1=Ireki leiho berean
settings.downloadOption.2=Ireki leiho berrian
settings.downloadOption.3=Deskargatu fitxategia
settings.zipThreshold=ZIP fitxategiak deskargatutako fitxategi kopurua gainditzen denean
#OCR #OCR
ocr.title=OCR / Garbiketa-eskaneatzea ocr.title=OCR / Garbiketa-eskaneatzea
@@ -241,7 +543,7 @@ ocr.credit=Zerbitzu honek OCRmyPDF eta OCR-rako Tesseract erabiltzen ditu
ocr.submit=PDF prozesatu OCR-rekin ocr.submit=PDF prozesatu OCR-rekin
#extractImages
extractImages.title=Atera irudiak extractImages.title=Atera irudiak
extractImages.header=Atera irudiak extractImages.header=Atera irudiak
extractImages.selectText=Hautatu irudi-formatua ateratako irudiak bihurtzeko extractImages.selectText=Hautatu irudi-formatua ateratako irudiak bihurtzeko
@@ -269,8 +571,8 @@ compress.submit=Konprimatu
#Add image #Add image
addImage.title=Gehitu irudia addImage.title=Gehitu irudia
addImage.header=Gehitu PDF-irudia addImage.header=Gehitu PDF-irudia
addImage.everyPage=Orrialde guztiak? addImage.everyPage=Orrialde guztiak?
addImage.upload=Gehitu irudia addImage.upload=Gehitu irudia
addImage.submit=Gehitu irudia addImage.submit=Gehitu irudia
@@ -281,34 +583,36 @@ merge.title=Elkartu
merge.header=Elkartu zenbait PDF (2+) merge.header=Elkartu zenbait PDF (2+)
merge.submit=Elkartu merge.submit=Elkartu
#pdfOrganiser #pdfOrganiser
pdfOrganiser.title=Orrialdeen antolatzailea pdfOrganiser.title=Orrialdeen antolatzailea
pdfOrganiser.header=PDF orrialdeen antolatzailea pdfOrganiser.header=PDF orrialdeen antolatzailea
pdfOrganiser.submit=Antolatu orrialdeak pdfOrganiser.submit=Antolatu orrialdeak
#herramienta multiple
multiTool.title= PDF erabilera anitzeko tresna #multiTool
multiTool.title=PDF erabilera anitzeko tresna
multiTool.header=PDF erabilera anitzeko tresna multiTool.header=PDF erabilera anitzeko tresna
#pageRemover #pageRemover
pageRemover.title=Orrialdeen ezabatzailea pageRemover.title=Orrialdeen ezabatzailea
pageRemover.header=PDF orrialdeen ezabatzailea pageRemover.header=PDF orrialdeen ezabatzailea
pageRemover.pagesToDelete=Ezabatu beharreko orrialdeak (sartu komaz bereizitako orrialde-zenbakien zerrenda): pageRemover.pagesToDelete=Ezabatu beharreko orrialdeak (sartu komaz bereizitako orrialde-zenbakien zerrenda):
pageRemover.submit=Ezabatu orrialdeak pageRemover.submit=Ezabatu orrialdeak
#rotate #rotate
rotate.title=Biratu PDFa rotate.title=Biratu PDFa
rotate.header=Biratu PDFa rotate.header=Biratu PDFa
rotate.SeleccionaAngle=Hautatu errotazio-angelua (90 graduren multiploa): rotate.selectAngle=Select rotation angle (in multiples of 90 degrees):
rotate.submit=Biratu rotate.submit=Biratu
#merge #merge
split.title=Zatitu PDFa split.title=Zatitu PDFa
split.header=Zatitu PDFa split.header=Zatitu PDFa
split.desc.1=Hautatzen dituzun zenbakiak zatiketa egin nahi duzun orrialde-zenbakiak dira split.desc.1=Hautatzen dituzun zenbakiak zatiketa egin nahi duzun orrialde-zenbakiak dira
split.desc.2=Beraz, 1,3,7-8 hautatzean 10 orrialdeko dokumentua zatituko luke 6 PDF fitxategi bereizituetan split.desc.2=Beraz, 1,3,7-8 hautatzean 10 orrialdeko dokumentua zatituko luke 6 PDF fitxategi bereizituetan
split.desc.3=#1 Dokumentua: 1. orrialdea split.desc.3=#1 Dokumentua: 1. orrialdea
split.desc.4=#2 Dokumentua: 2. eta 3. orrialdeak split.desc.4=#2 Dokumentua: 2. eta 3. orrialdeak
@@ -329,6 +633,7 @@ imageToPDF.selectText.2=PDFaren errotazio automatikoa
imageToPDF.selectText.3=Fitxategi askoren logika (gaituta bakarrik zenbait irudirekin ari denean) imageToPDF.selectText.3=Fitxategi askoren logika (gaituta bakarrik zenbait irudirekin ari denean)
imageToPDF.selectText.4=Elkartu PDF bakar batean imageToPDF.selectText.4=Elkartu PDF bakar batean
imageToPDF.selectText.5=Bihurtu eta PDF bereizituak sortu imageToPDF.selectText.5=Bihurtu eta PDF bereizituak sortu
#pdfToImage #pdfToImage
pdfToImage.title=PDFa irudi bihurtu pdfToImage.title=PDFa irudi bihurtu
@@ -343,6 +648,7 @@ pdfToImage.grey=Gris-eskala
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!) pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
pdfToImage.submit=Bihurtu pdfToImage.submit=Bihurtu
#addPassword #addPassword
addPassword.title=Gehitu pasahitza addPassword.title=Gehitu pasahitza
addPassword.header=Gehitu pasahitza (enkriptatu) addPassword.header=Gehitu pasahitza (enkriptatu)
@@ -361,9 +667,10 @@ addPassword.selectText.12=Galarazi inprimatzea
addPassword.selectText.13=Galarazi zenbait formatu inprimatzea addPassword.selectText.13=Galarazi zenbait formatu inprimatzea
addPassword.selectText.14=Pasahitza addPassword.selectText.14=Pasahitza
addPassword.selectText.15=Mugatu zer egin daitekeen dokumentuarekin behin zabalduta (Irakurle guztiek onartu gabe) addPassword.selectText.15=Mugatu zer egin daitekeen dokumentuarekin behin zabalduta (Irakurle guztiek onartu gabe)
addPassword.selectText.16=Mugatu dokumentu bera zabaltzeko aukera addPassword.selectText.16=Mugatu dokumentu bera zabaltzeko aukera
addPassword.submit=Enkriptatu addPassword.submit=Enkriptatu
#watermark #watermark
watermark.title=Gehitu ur-marka watermark.title=Gehitu ur-marka
watermark.header=Gehitu ur-marka watermark.header=Gehitu ur-marka
@@ -376,6 +683,7 @@ watermark.selectText.6=Altuera (ur-marka bakoitzaren arteko espazioa bertikalean
watermark.selectText.7=Opakutasuna (0% - 100%): watermark.selectText.7=Opakutasuna (0% - 100%):
watermark.submit=Gehitu ur-marka watermark.submit=Gehitu ur-marka
#remove-watermark #remove-watermark
remove-watermark.title=Ezabatu ur-marka remove-watermark.title=Ezabatu ur-marka
remove-watermark.header=Ezabatu ur-marka remove-watermark.header=Ezabatu ur-marka
@@ -383,6 +691,7 @@ remove-watermark.selectText.1=Hautatu PDFa ur-marka ezabatzeko:
remove-watermark.selectText.2=Ur-markaren testua: remove-watermark.selectText.2=Ur-markaren testua:
remove-watermark.submit=Ezabatu ur-marka remove-watermark.submit=Ezabatu ur-marka
#Change permissions #Change permissions
permissions.title=Aldatu baimenak permissions.title=Aldatu baimenak
permissions.header=Aldatu baimenak permissions.header=Aldatu baimenak
@@ -399,6 +708,7 @@ permissions.selectText.9=Galarazi inprimatzea
permissions.selectText.10=Galarazi zenbait formatu inprimatzea permissions.selectText.10=Galarazi zenbait formatu inprimatzea
permissions.submit=Aldatu permissions.submit=Aldatu
#remove password #remove password
removePassword.title=Ezabatu pasahitza removePassword.title=Ezabatu pasahitza
removePassword.header=Ezabatu pasahitza (desenkriptatu) removePassword.header=Ezabatu pasahitza (desenkriptatu)
@@ -406,7 +716,9 @@ removePassword.selectText.1=Hautatu PDFa desenkriptatzeko
removePassword.selectText.2=Pasahitza removePassword.selectText.2=Pasahitza
removePassword.submit=Ezabatu removePassword.submit=Ezabatu
changeMetadata.title=Aldatu metadatuak
#changeMetadata
changeMetadata.title=Izenburua:
changeMetadata.header=Aldatu metadatuak changeMetadata.header=Aldatu metadatuak
changeMetadata.selectText.1=Editatu aldatu nahi dituzun aldagaiak changeMetadata.selectText.1=Editatu aldatu nahi dituzun aldagaiak
changeMetadata.selectText.2=Ezabatu metadatu guztiak changeMetadata.selectText.2=Ezabatu metadatu guztiak
@@ -424,27 +736,30 @@ changeMetadata.selectText.4=Beste metadatu batzuk:
changeMetadata.selectText.5=Gehitu metadatu pertsonalizatuen sarrera changeMetadata.selectText.5=Gehitu metadatu pertsonalizatuen sarrera
changeMetadata.submit=Aldatu changeMetadata.submit=Aldatu
#xlsToPdf
xlsToPdf.title=Excela PDF bihurtu xlsToPdf.title=Excela PDF bihurtu
xlsToPdf.header=Excela PDF bihurtu xlsToPdf.header=Excela PDF bihurtu
xlsToPdf.selectText.1=Hautatu Excel XLSren edo XLSXren kalkulu-orria bihurtzeko xlsToPdf.selectText.1=Hautatu Excel XLSren edo XLSXren kalkulu-orria bihurtzeko
xlsToPdf.convert=Bikurtu xlsToPdf.convert=Bikurtu
#pdfToPDFA
pdfToPDFA.title=PDFa PDF/A bihurtu pdfToPDFA.title=PDFa PDF/A bihurtu
pdfToPDFA.header=PDFa PDF/A bihurtu pdfToPDFA.header=PDFa PDF/A bihurtu
pdfToPDFA.credit=Zerbitzu honek OCRmyPDF erabiltzen du PDFak PDF/A bihurtzeko pdfToPDFA.credit=Zerbitzu honek OCRmyPDF erabiltzen du PDFak PDF/A bihurtzeko
pdfToPDFA.submit=Bihurtu pdfToPDFA.submit=Bihurtu
#PDFToWord
PDFToWord.title=PDFa Word bihurtu PDFToWord.title=PDFa Word bihurtu
PDFToWord.header=PDFa Word bihurtu PDFToWord.header=PDFa Word bihurtu
PDFToWord.selectText.1=Irteerako fitxategiaren formatua PDFToWord.selectText.1=Irteerako fitxategiaren formatua
PDFToWord.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko PDFToWord.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko
PDFToWord.submit=Bihurtu PDFToWord.submit=Bihurtu
#PDFToPresentation
PDFToPresentation.title=PDFa aurkezpen bihurtu PDFToPresentation.title=PDFa aurkezpen bihurtu
PDFToPresentation.header=PDFa aurkezpen bihurtu PDFToPresentation.header=PDFa aurkezpen bihurtu
PDFToPresentation.selectText.1=Irteerako fitxategiaren formatua PDFToPresentation.selectText.1=Irteerako fitxategiaren formatua
@@ -452,6 +767,7 @@ PDFToPresentation.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bi
PDFToPresentation.submit=Bihurtu PDFToPresentation.submit=Bihurtu
#PDFToText
PDFToText.title=PDFa TXT/RTF bihurtu PDFToText.title=PDFa TXT/RTF bihurtu
PDFToText.header=PDFa TXT/RTF bihurtu PDFToText.header=PDFa TXT/RTF bihurtu
PDFToText.selectText.1=Irteerako fitxategiaren formatua PDFToText.selectText.1=Irteerako fitxategiaren formatua
@@ -459,12 +775,15 @@ PDFToText.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko
PDFToText.submit=Bihurtu PDFToText.submit=Bihurtu
#PDFToHTML
PDFToHTML.title=PDFa HTML bihurtu PDFToHTML.title=PDFa HTML bihurtu
PDFToHTML.header=PDFa HTML bihurtu PDFToHTML.header=PDFa HTML bihurtu
PDFToHTML.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko PDFToHTML.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko
PDFToHTML.submit=Bihurtu PDFToHTML.submit=Bihurtu
#PDFToXML
PDFToXML.title=PDFa XML bihurtu PDFToXML.title=PDFa XML bihurtu
PDFToXML.header=PDFa XML bihurtu PDFToXML.header=PDFa XML bihurtu
PDFToXML.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko PDFToXML.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko
PDFToXML.submit=Bihurtu PDFToXML.submit=Bihurtu

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
########### ###########
# Generic # # Generic #
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr=left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
pdfPrompt=PDFを選択 pdfPrompt=PDFを選択
@@ -19,141 +19,478 @@ save=保存
close=閉じる close=閉じる
filesSelected=選択されたファイル filesSelected=選択されたファイル
noFavourites=お気に入りはありません noFavourites=お気に入りはありません
bored=待ち時間が退屈<EFBFBD><EFBFBD> bored=待ち時間が退屈
alphabet=\u30A2\u30EB\u30D5\u30A1\u30D9\u30C3\u30C8<EFBFBD> alphabet=\u30A2\u30EB\u30D5\u30A1\u30D9\u30C3\u30C8
downloadPdf=PDFをダウンロード
text=テキスト
font=フォント
selectFillter=-- 選択 --
pageNum=ページ番号
##########################
### TODO: Translate ###
##########################
sizes.small=Small
sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=PDFにパスワードが設定されてますが、パスワードが入力されてないか間違ってます。
#############
# NAVBAR #
#############
navbar.convert=変換
navbar.security=セキュリティ
navbar.other=その他
navbar.darkmode=ダークモード
navbar.pageOps=ページ操作
navbar.settings=設定
#############
# SETTINGS #
#############
settings.title=設定
settings.update=利用可能なアップデート
settings.appVersion=Appバージョン:
settings.downloadOption.title=ダウンロードオプション (zip以外の単一ファイル):
settings.downloadOption.1=同じウィンドウで開く
settings.downloadOption.2=新しいウィンドウで開く
settings.downloadOption.3=ファイルをダウンロード
settings.zipThreshold=このファイル数を超えたときにファイルを圧縮する
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############
home.desc=PDFのあらゆるニーズに対応するローカルホスティングされた総合窓口です。 home.desc=PDFのあらゆるニーズに対応するローカルホスティングされた総合窓口です。
navbar.convert=変換
navbar.security=セキュリティ
navbar.other=その他
navbar.darkmode=ダークモード
navbar.pageOps=ページ操作
home.multiTool.title=PDFマルチツール home.multiTool.title=PDFマルチツール
home.multiTool.desc=ページの結合、回転、並べ替え、削除します。 home.multiTool.desc=ページの結合、回転、並べ替え、削除します。
##########################
### TODO: Translate ###
##########################
multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side,interactive,intractable,move
home.merge.title=結合 home.merge.title=結合
home.merge.desc=複数のPDFを1つに結合します。 home.merge.desc=複数のPDFを1つに結合します。
##########################
### TODO: Translate ###
##########################
merge.tags=merge,Page operations,Back end,server side
home.split.title=分割 home.split.title=分割
home.split.desc=PDFを複数のドキュメントに分割します。 home.split.desc=PDFを複数のドキュメントに分割します。
##########################
### TODO: Translate ###
##########################
split.tags=Page operations,divide,Multi Page,cut,server side
home.rotate.title=回転 home.rotate.title=回転
home.rotate.desc=PDFを回転します。 home.rotate.desc=PDFを回転します。
##########################
### TODO: Translate ###
##########################
rotate.tags=server side
home.imageToPdf.title=画像をPDFに変換 home.imageToPdf.title=画像をPDFに変換
home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。 home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。
##########################
### TODO: Translate ###
##########################
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDFを画像に変換 home.pdfToImage.title=PDFを画像に変換
home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。 home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。
##########################
### TODO: Translate ###
##########################
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=整理 home.pdfOrganiser.title=整理
home.pdfOrganiser.desc=ページの削除/並べ替えします。 home.pdfOrganiser.desc=ページの削除/並べ替えします。
##########################
### TODO: Translate ###
##########################
pdfOrganiser.tags=duplex,even,odd,sort,move
home.addImage.title=画像の追加 home.addImage.title=画像の追加
home.addImage.desc=PDF上の任意の場所に画像を追加します。 home.addImage.desc=PDF上の任意の場所に画像を追加します。
##########################
### TODO: Translate ###
##########################
addImage.tags=img,jpg,picture,photo
home.watermark.title=透かしの追加 home.watermark.title=透かしの追加
home.watermark.desc=PDFに独自の透かしを追加します。 home.watermark.desc=PDFに独自の透かしを追加します。
##########################
home.remove-watermark.title=透かしの削除 ### TODO: Translate ###
home.remove-watermark.desc=PDFから透かしを削除します。 ##########################
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
home.permissions.title=権限の変更 home.permissions.title=権限の変更
home.permissions.desc=PDFの権限を変更します。 home.permissions.desc=PDFの権限を変更します。
##########################
### TODO: Translate ###
##########################
permissions.tags=read,write,edit,print
home.removePages.title=削除 home.removePages.title=削除
home.removePages.desc=PDFから不要なページを削除します。 home.removePages.desc=PDFから不要なページを削除します。
##########################
### TODO: Translate ###
##########################
removePages.tags=Remove pages,delete pages
home.addPassword.title=パスワードの追加 home.addPassword.title=パスワードの追加
home.addPassword.desc=PDFをパスワードで暗号化します。 home.addPassword.desc=PDFをパスワードで暗号化します。
##########################
### TODO: Translate ###
##########################
addPassword.tags=secure,security
home.removePassword.title=パスワードの削除 home.removePassword.title=パスワードの削除
home.removePassword.desc=PDFからパスワードの削除します。 home.removePassword.desc=PDFからパスワードの削除します。
##########################
### TODO: Translate ###
##########################
removePassword.tags=secure,Decrypt,security,unpassword,delete password
home.compressPdfs.title=圧縮 home.compressPdfs.title=圧縮
home.compressPdfs.desc=PDFを圧縮してファイルサイズを小さくします。 home.compressPdfs.desc=PDFを圧縮してファイルサイズを小さくします。
##########################
### TODO: Translate ###
##########################
compressPdfs.tags=squish,small,tiny
home.changeMetadata.title=メタデータの変更 home.changeMetadata.title=メタデータの変更
home.changeMetadata.desc=PDFのメタデータを変更/削除/追加します。 home.changeMetadata.desc=PDFのメタデータを変更/削除/追加します。
##########################
### TODO: Translate ###
##########################
changeMetadata.tags==Title,author,date,creation,time,publisher,producer,stats
home.fileToPDF.title=ファイルをPDFに変換 home.fileToPDF.title=ファイルをPDFに変換
home.fileToPDF.desc=ほぼすべてのファイルをPDFに変換します。 (DOCX, PNG, XLS, PPT, TXTなど) home.fileToPDF.desc=ほぼすべてのファイルをPDFに変換します。 (DOCX, PNG, XLS, PPT, TXTなど)
##########################
### TODO: Translate ###
##########################
fileToPDF.tags=transformation,format,document,picture,slide,text,conversion,office,docs,word,excel,powerpoint
home.ocr.title=OCR / クリーンアップ home.ocr.title=OCR / クリーンアップ
home.ocr.desc=クリーンアップはPDF内の画像からテキストを検出してテキストとして再追加します。 home.ocr.desc=クリーンアップはPDF内の画像からテキストを検出してテキストとして再追加します。
##########################
### TODO: Translate ###
##########################
ocr.tags=recognition,text,image,scan,read,identify,detection,editable
home.extractImages.title=画像の抽出 home.extractImages.title=画像の抽出
home.extractImages.desc=PDFからすべての画像を抽出してzipで保存します。 home.extractImages.desc=PDFからすべての画像を抽出してzipで保存します。
##########################
### TODO: Translate ###
##########################
extractImages.tags=picture,photo,save,archive,zip,capture,grab
home.pdfToPDFA.title=PDFをPDF/Aに変換 home.pdfToPDFA.title=PDFをPDF/Aに変換
home.pdfToPDFA.desc=長期保存のためにPDFをPDF/Aに変換。 home.pdfToPDFA.desc=長期保存のためにPDFをPDF/Aに変換。
##########################
### TODO: Translate ###
##########################
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation
home.PDFToWord.title=PDFをWordに変換 home.PDFToWord.title=PDFをWordに変換
home.PDFToWord.desc=PDFをWord形式に変換します。 (DOC, DOCX および ODT) home.PDFToWord.desc=PDFをWord形式に変換します。 (DOC, DOCX および ODT)
##########################
### TODO: Translate ###
##########################
PDFToWord.tags=doc,docx,odt,word,transformation,format,conversion,office,microsoft,docfile
home.PDFToPresentation.title=PDFをプレゼンテーションに変換 home.PDFToPresentation.title=PDFをプレゼンテーションに変換
home.PDFToPresentation.desc=PDFをプレゼンテーション形式に変換します。 (PPT, PPTX および ODP) home.PDFToPresentation.desc=PDFをプレゼンテーション形式に変換します。 (PPT, PPTX および ODP)
##########################
### TODO: Translate ###
##########################
PDFToPresentation.tags=slides,show,office,microsoft
home.PDFToText.title=PDFをText/RTFに変換 home.PDFToText.title=PDFをText/RTFに変換
home.PDFToText.desc=PDFをTextまたはRTF形式に変換します。 home.PDFToText.desc=PDFをTextまたはRTF形式に変換します。
##########################
### TODO: Translate ###
##########################
PDFToText.tags=richformat,richtextformat,rich text format
home.PDFToHTML.title=PDFをHTMLに変換 home.PDFToHTML.title=PDFをHTMLに変換
home.PDFToHTML.desc=PDFをHTML形式に変換します。 home.PDFToHTML.desc=PDFをHTML形式に変換します。
##########################
### TODO: Translate ###
##########################
PDFToHTML.tags=web content,browser friendly
home.PDFToXML.title=PDFをXMLに変換 home.PDFToXML.title=PDFをXMLに変換
home.PDFToXML.desc=PDFをXML形式に変換します。 home.PDFToXML.desc=PDFをXML形式に変換します。
##########################
### TODO: Translate ###
##########################
PDFToXML.tags=data-extraction,structured-content,interop,transformation,convert
home.ScannerImageSplit.title=スキャンされた画像の検出/分割 home.ScannerImageSplit.title=スキャンされた画像の検出/分割
home.ScannerImageSplit.desc=1枚の画像/PDFから複数の写真を分割します。 home.ScannerImageSplit.desc=1枚の画像/PDFから複数の写真を分割します。
##########################
### TODO: Translate ###
##########################
ScannerImageSplit.tags=separate,auto-detect,scans,multi-photo,organize
home.sign.title=署名 home.sign.title=署名
home.sign.desc=手書き、テキストまたは画像によってPDFに署名を追加します。 home.sign.desc=手書き、テキストまたは画像によってPDFに署名を追加します。
##########################
### TODO: Translate ###
##########################
sign.tags=authorize,initials,drawn-signature,text-sign,image-signature
home.flatten.title=平坦化 home.flatten.title=平坦化
home.flatten.desc=PDFからインタラクティブな要素とフォームをすべて削除します。 home.flatten.desc=PDFからインタラクティブな要素とフォームをすべて削除します。
##########################
### TODO: Translate ###
##########################
flatten.tags=static,deactivate,non-interactive,streamline
home.repair.title=修復 home.repair.title=修復
home.repair.desc=破損したPDFの修復を試みます。 home.repair.desc=破損したPDFの修復を試みます。
##########################
### TODO: Translate ###
##########################
repair.tags=fix,restore,correction,recover
home.removeBlanks.title=空白ページの削除 home.removeBlanks.title=空白ページの削除
home.removeBlanks.desc=ドキュメントから空白ページを検出して削除します。 home.removeBlanks.desc=ドキュメントから空白ページを検出して削除します。
##########################
### TODO: Translate ###
##########################
removeBlanks.tags=cleanup,streamline,non-content,organize
home.compare.title=比較 home.compare.title=比較
home.compare.desc=2つのPDFを比較して表示します。 home.compare.desc=2つのPDFを比較して表示します。
##########################
### TODO: Translate ###
##########################
compare.tags=differentiate,contrast,changes,analysis
home.certSign.title=証明書による署名 home.certSign.title=証明書による署名
home.certSign.desc=証明書/キーを使用してPDFに署名します。 (PEM/P12) home.certSign.desc=証明書/キーを使用してPDFに署名します。 (PEM/P12)
##########################
### TODO: Translate ###
##########################
certSign.tags=authenticate,PEM,P12,official,encrypt
home.pageLayout.title=マルチページレイアウト home.pageLayout.title=マルチページレイアウト
home.pageLayout.desc=PDFの複数のページを1ページに結合します。 home.pageLayout.desc=PDFの複数のページを1ページに結合します。
##########################
### TODO: Translate ###
##########################
pageLayout.tags=merge,composite,single-view,organize
home.scalePages.title=ページの縮尺の調整 home.scalePages.title=ページの縮尺の調整
home.scalePages.desc=ページやコンテンツの縮尺を変更します。 home.scalePages.desc=ページやコンテンツの縮尺を変更します。
##########################
### TODO: Translate ###
##########################
scalePages.tags=resize,modify,dimension,adapt
error.pdfPassword=PDFにパスワードが設定されてますが、パスワードが入力されてないか間違ってます。 ##########################
### TODO: Translate ###
##########################
home.pipeline.title=Pipeline (Advanced)
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
pipeline.tags=automate,sequence,scripted,batch-process
downloadPdf=PDFをダウンロード ##########################
text=テキスト ### TODO: Translate ###
font=フォント ##########################
selectFillter=-- 選択 -- home.add-page-numbers.title=Add Page Numbers
pageNum=ページ番号 home.add-page-numbers.desc=Add Page numbers throughout a document in a set location
add-page-numbers.tags=paginate,label,organize,index
##########################
### TODO: Translate ###
##########################
home.auto-rename.title=Auto Rename PDF File
home.auto-rename.desc=Auto renames a PDF file based on its detected header
auto-rename.tags=auto-detect,header-based,organize,relabel
##########################
### TODO: Translate ###
##########################
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
adjust-contrast.tags=color-correction,tune,modify,enhance
##########################
### TODO: Translate ###
##########################
home.crop.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
crop.tags=trim,shrink,edit,shape
##########################
### TODO: Translate ###
##########################
home.autoSplitPDF.title=Auto Split Pages
home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code
autoSplitPDF.tags=QR-based,separate,scan-segment,organize
##########################
### TODO: Translate ###
##########################
home.sanitizePdf.title=Sanitize
home.sanitizePdf.desc=Remove scripts and other elements from PDF files
sanitizePdf.tags=clean,secure,safe,remove-threats
##########################
### TODO: Translate ###
##########################
home.URLToPDF.title=URL/Website To PDF
home.URLToPDF.desc=Converts any http(s)URL to PDF
URLToPDF.tags=web-capture,save-page,web-to-doc,archive
##########################
### TODO: Translate ###
##########################
home.HTMLToPDF.title=HTML to PDF
home.HTMLToPDF.desc=Converts any HTML file or zip to PDF
HTMLToPDF.tags=markup,web-content,transformation,convert
###########################
# #
# WEB PAGES #
# #
###########################
#url-to-pdf
##########################
### TODO: Translate ###
##########################
URLToPDF.title=URL To PDF
URLToPDF.header=URL To PDF
URLToPDF.submit=Convert
URLToPDF.credit=Uses WeasyPrint
#html-to-pdf
##########################
### TODO: Translate ###
##########################
HTMLToPDF.title=HTML To PDF
HTMLToPDF.header=HTML To PDF
HTMLToPDF.help=Accepts HTML files and ZIPs containing html/css/images etc required
HTMLToPDF.submit=Convert
HTMLToPDF.credit=Uses WeasyPrint
#sanitizePDF
##########################
### TODO: Translate ###
##########################
sanitizePDF.title=Sanitize PDF
sanitizePDF.header=Sanitize a PDF file
sanitizePDF.selectText.1=Remove JavaScript actions
sanitizePDF.selectText.2=Remove embedded files
sanitizePDF.selectText.3=Remove metadata
sanitizePDF.selectText.4=Remove links
sanitizePDF.selectText.5=Remove fonts
sanitizePDF.submit=Sanitize PDF
#addPageNumbers
##########################
### TODO: Translate ###
##########################
addPageNumbers.title=Add Page Numbers
addPageNumbers.header=Add Page Numbers
addPageNumbers.selectText.1=Select PDF file:
addPageNumbers.selectText.2=Margin Size
addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text
addPageNumbers.submit=Add Page Numbers
#auto-rename
##########################
### TODO: Translate ###
##########################
auto-rename.title=Auto Rename
auto-rename.header=Auto Rename PDF
auto-rename.submit=Auto Rename
#adjustContrast
##########################
### TODO: Translate ###
##########################
adjustContrast.title=Adjust Contrast
adjustContrast.header=Adjust Contrast
adjustContrast.contrast=Contrast:
adjustContrast.brightness=Brightness:
adjustContrast.saturation=Saturation:
adjustContrast.download=Download
#crop
##########################
### TODO: Translate ###
##########################
crop.title=Crop
crop.header=Crop Image
crop.submit=Submit
#autoSplitPDF
##########################
### TODO: Translate ###
##########################
autoSplitPDF.title=Auto Split PDF
autoSplitPDF.header=Auto Split PDF
autoSplitPDF.description=Print, Insert, Scan, upload, and let us auto-separate your documents. No manual work sorting needed.
autoSplitPDF.selectText.1=Print out some divider sheets from below (Black and white is fine).
autoSplitPDF.selectText.2=Scan all your documents at once by inserting the divider sheet between them.
autoSplitPDF.selectText.3=Upload the single large scanned PDF file and let Stirling PDF handle the rest.
autoSplitPDF.selectText.4=Divider pages are automatically detected and removed, guaranteeing a neat final document.
autoSplitPDF.formPrompt=Submit PDF containing Stirling-PDF Page dividers:
autoSplitPDF.duplexMode=Duplex Mode (Front and back scanning)
autoSplitPDF.dividerDownload1=Download 'Auto Splitter Divider (minimal).pdf'
autoSplitPDF.dividerDownload2=Download 'Auto Splitter Divider (with instructions).pdf'
autoSplitPDF.submit=Submit
#pipeline
##########################
### TODO: Translate ###
##########################
pipeline.title=Pipeline
#pageLayout
pageLayout.title=マルチページレイアウト pageLayout.title=マルチページレイアウト
pageLayout.header=マルチページレイアウト pageLayout.header=マルチページレイアウト
pageLayout.pagesPerSheet=1枚あたりのページ数: pageLayout.pagesPerSheet=1枚あたりのページ数:
pageLayout.submit=送信 pageLayout.submit=送信
#scalePages
scalePages.title=ページの縮尺の調整 scalePages.title=ページの縮尺の調整
scalePages.header=ページの縮尺の調整 scalePages.header=ページの縮尺の調整
scalePages.pageSize=1ページのサイズ scalePages.pageSize=1ページのサイズ
scalePages.scaleFactor=1ページの拡大レベル (トリミング)。 scalePages.scaleFactor=1ページの拡大レベル (トリミング)。
scalePages.submit=送信 scalePages.submit=送信
#certSign
certSign.title=証明書による署名 certSign.title=証明書による署名
certSign.header=証明書を使用してPDFに署名します。 (進行中) certSign.header=証明書を使用してPDFに署名します。 (進行中)
certSign.selectPDF=署名するPDFファイルを選択: certSign.selectPDF=署名するPDFファイルを選択:
@@ -168,6 +505,8 @@ certSign.location=場所
certSign.name=名前 certSign.name=名前
certSign.submit=PDFに署名 certSign.submit=PDFに署名
#removeBlanks
removeBlanks.title=空白の削除 removeBlanks.title=空白の削除
removeBlanks.header=空白ページの削除 removeBlanks.header=空白ページの削除
removeBlanks.threshold=しきい値 : removeBlanks.threshold=しきい値 :
@@ -176,12 +515,16 @@ removeBlanks.whitePercent=白比率
removeBlanks.whitePercentDesc=削除するページの白の割合 removeBlanks.whitePercentDesc=削除するページの白の割合
removeBlanks.submit=空白ページの削除 removeBlanks.submit=空白ページの削除
#compare
compare.title=比較 compare.title=比較
compare.header=PDFの比較 compare.header=PDFの比較
compare.document.1=ドキュメント 1 compare.document.1=ドキュメント 1
compare.document.2=ドキュメント 2 compare.document.2=ドキュメント 2
compare.submit=比較 compare.submit=比較
#sign
sign.title=署名 sign.title=署名
sign.header=PDFに署名 sign.header=PDFに署名
sign.upload=画像をアップロード sign.upload=画像をアップロード
@@ -190,14 +533,20 @@ sign.text=テキスト入力
sign.clear=クリア sign.clear=クリア
sign.add=追加 sign.add=追加
#repair
repair.title=修復 repair.title=修復
repair.header=PDFを修復 repair.header=PDFを修復
repair.submit=修復 repair.submit=修復
#flatten
flatten.title=平坦化 flatten.title=平坦化
flatten.header=PDFを平坦化する flatten.header=PDFを平坦化する
flatten.submit=平坦化 flatten.submit=平坦化
#ScannerImageSplit
ScannerImageSplit.selectText.1=角度のしきい値: ScannerImageSplit.selectText.1=角度のしきい値:
ScannerImageSplit.selectText.2=画像を回転させるために必要な絶対角度の最小値を設定 (初期値:10)。 ScannerImageSplit.selectText.2=画像を回転させるために必要な絶対角度の最小値を設定 (初期値:10)。
ScannerImageSplit.selectText.3=許容範囲: ScannerImageSplit.selectText.3=許容範囲:
@@ -209,18 +558,6 @@ ScannerImageSplit.selectText.8=画像の最小の輪郭面積のしきい値を
ScannerImageSplit.selectText.9=境界線サイズ: ScannerImageSplit.selectText.9=境界線サイズ:
ScannerImageSplit.selectText.10=出力に白い縁取りが出ないように追加・削除される境界線の大きさを設定 (初期値:1)。 ScannerImageSplit.selectText.10=出力に白い縁取りが出ないように追加・削除される境界線の大きさを設定 (初期値:1)。
navbar.settings=設定
settings.title=設定
settings.update=利用可能なアップデート
settings.appVersion=Appバージョン:
settings.downloadOption.title=ダウンロードオプション (zip以外の単一ファイル):
settings.downloadOption.1=同じウィンドウで開く
settings.downloadOption.2=新しいウィンドウで開く
settings.downloadOption.3=ファイルをダウンロード
settings.zipThreshold=このファイル数を超えたときにファイルを圧縮する
#OCR #OCR
ocr.title=OCR / クリーンアップ ocr.title=OCR / クリーンアップ
@@ -242,7 +579,7 @@ ocr.credit=本サービスにはOCRにOCRmyPDFとTesseractを使用していま
ocr.submit=OCRでPDFを処理する ocr.submit=OCRでPDFを処理する
#extractImages
extractImages.title=画像の抽出 extractImages.title=画像の抽出
extractImages.header=画像の抽出 extractImages.header=画像の抽出
extractImages.selectText=抽出した画像のフォーマットを選択 extractImages.selectText=抽出した画像のフォーマットを選択
@@ -282,11 +619,13 @@ merge.title=結合
merge.header=複数のPDFを結合 (2ファイル以上) merge.header=複数のPDFを結合 (2ファイル以上)
merge.submit=結合 merge.submit=結合
#pdfOrganiser #pdfOrganiser
pdfOrganiser.title=整理 pdfOrganiser.title=整理
pdfOrganiser.header=PDFページの整理 pdfOrganiser.header=PDFページの整理
pdfOrganiser.submit=ページの整理 pdfOrganiser.submit=ページの整理
#multiTool #multiTool
multiTool.title=PDFマルチツール multiTool.title=PDFマルチツール
multiTool.header=PDFマルチツール multiTool.header=PDFマルチツール
@@ -298,6 +637,7 @@ pageRemover.header=PDFページ削除
pageRemover.pagesToDelete=削除するページ (ページ番号のカンマ区切りリストを入力してください): pageRemover.pagesToDelete=削除するページ (ページ番号のカンマ区切りリストを入力してください):
pageRemover.submit=ページ削除 pageRemover.submit=ページ削除
#rotate #rotate
rotate.title=PDFの回転 rotate.title=PDFの回転
rotate.header=PDFの回転 rotate.header=PDFの回転
@@ -305,8 +645,6 @@ rotate.selectAngle=回転角度を選択 (90度の倍数):
rotate.submit=回転 rotate.submit=回転
#merge #merge
split.title=PDFの分割 split.title=PDFの分割
split.header=PDFの分割 split.header=PDFの分割
@@ -331,6 +669,7 @@ imageToPDF.selectText.2=PDFの自動回転
imageToPDF.selectText.3=マルチファイルの処理 (複数の画像を操作する場合に有効になります) imageToPDF.selectText.3=マルチファイルの処理 (複数の画像を操作する場合に有効になります)
imageToPDF.selectText.4=1つのPDFに結合 imageToPDF.selectText.4=1つのPDFに結合
imageToPDF.selectText.5=個別のPDFに変換 imageToPDF.selectText.5=個別のPDFに変換
#pdfToImage #pdfToImage
pdfToImage.title=PDFを画像に変換 pdfToImage.title=PDFを画像に変換
@@ -345,6 +684,7 @@ pdfToImage.grey=グレースケール
pdfToImage.blackwhite=白黒 (データが失われる可能性があります!) pdfToImage.blackwhite=白黒 (データが失われる可能性があります!)
pdfToImage.submit=変換 pdfToImage.submit=変換
#addPassword #addPassword
addPassword.title=パスワードの追加 addPassword.title=パスワードの追加
addPassword.header=パスワードの追加 (暗号化) addPassword.header=パスワードの追加 (暗号化)
@@ -363,9 +703,10 @@ addPassword.selectText.12=印刷を禁止
addPassword.selectText.13=異なる形式の印刷を禁止 addPassword.selectText.13=異なる形式の印刷を禁止
addPassword.selectText.14=所有者パスワード addPassword.selectText.14=所有者パスワード
addPassword.selectText.15=ドキュメントを開いた後に実行できる操作を制限します (すべてのリーダーでサポートされているわけではありません) addPassword.selectText.15=ドキュメントを開いた後に実行できる操作を制限します (すべてのリーダーでサポートされているわけではありません)
addPassword.selectText.16=ドキュメントを開くことを制限します addPassword.selectText.16=ドキュメントを開くことを制限します
addPassword.submit=暗号化 addPassword.submit=暗号化
#watermark #watermark
watermark.title=透かしの追加 watermark.title=透かしの追加
watermark.header=透かしの追加 watermark.header=透かしの追加
@@ -378,6 +719,7 @@ watermark.selectText.6=高さスペース (各透かし間の垂直方向のス
watermark.selectText.7=不透明度 (0% - 100%): watermark.selectText.7=不透明度 (0% - 100%):
watermark.submit=透かしを追加 watermark.submit=透かしを追加
#remove-watermark #remove-watermark
remove-watermark.title=透かしの削除 remove-watermark.title=透かしの削除
remove-watermark.header=透かしの削除 remove-watermark.header=透かしの削除
@@ -385,6 +727,7 @@ remove-watermark.selectText.1=透かしを削除するPDFを選択:
remove-watermark.selectText.2=透かしのテキスト: remove-watermark.selectText.2=透かしのテキスト:
remove-watermark.submit=透かしを削除 remove-watermark.submit=透かしを削除
#Change permissions #Change permissions
permissions.title=権限の変更 permissions.title=権限の変更
permissions.header=権限の変更 permissions.header=権限の変更
@@ -401,6 +744,7 @@ permissions.selectText.9=印刷を禁止
permissions.selectText.10=異なる形式の印刷を禁止 permissions.selectText.10=異なる形式の印刷を禁止
permissions.submit=変更 permissions.submit=変更
#remove password #remove password
removePassword.title=パスワードの削除 removePassword.title=パスワードの削除
removePassword.header=パスワードの削除 (復号化) removePassword.header=パスワードの削除 (復号化)
@@ -408,7 +752,9 @@ removePassword.selectText.1=復号化するPDFを選択
removePassword.selectText.2=パスワード removePassword.selectText.2=パスワード
removePassword.submit=削除 removePassword.submit=削除
changeMetadata.title=メタデータの変更
#changeMetadata
changeMetadata.title=タイトル:
changeMetadata.header=メタデータの変更 changeMetadata.header=メタデータの変更
changeMetadata.selectText.1=変更したい変数を編集してください changeMetadata.selectText.1=変更したい変数を編集してください
changeMetadata.selectText.2=すべてのメタデータを削除 changeMetadata.selectText.2=すべてのメタデータを削除
@@ -426,27 +772,30 @@ changeMetadata.selectText.4=その他のメタデータ:
changeMetadata.selectText.5=カスタムメタデータの追加 changeMetadata.selectText.5=カスタムメタデータの追加
changeMetadata.submit=変更 changeMetadata.submit=変更
#xlsToPdf
xlsToPdf.title=ExcelをPDFに変換 xlsToPdf.title=ExcelをPDFに変換
xlsToPdf.header=ExcelをPDFに変換 xlsToPdf.header=ExcelをPDFに変換
xlsToPdf.selectText.1=変換するXLSまたはXLSX Execlシートを選択 xlsToPdf.selectText.1=変換するXLSまたはXLSX Execlシートを選択
xlsToPdf.convert=変換 xlsToPdf.convert=変換
#pdfToPDFA
pdfToPDFA.title=PDFをPDF/Aに変換 pdfToPDFA.title=PDFをPDF/Aに変換
pdfToPDFA.header=PDFをPDF/Aに変換 pdfToPDFA.header=PDFをPDF/Aに変換
pdfToPDFA.credit=本サービスはPDF/Aの変換にOCRmyPDFを使用しています。 pdfToPDFA.credit=本サービスはPDF/Aの変換にOCRmyPDFを使用しています。
pdfToPDFA.submit=変換 pdfToPDFA.submit=変換
#PDFToWord
PDFToWord.title=PDFをWordに変換 PDFToWord.title=PDFをWordに変換
PDFToWord.header=PDFをWordに変換 PDFToWord.header=PDFをWordに変換
PDFToWord.selectText.1=出力ファイル形式 PDFToWord.selectText.1=出力ファイル形式
PDFToWord.credit=本サービスはファイル変換にLibreOfficeを使用しています。 PDFToWord.credit=本サービスはファイル変換にLibreOfficeを使用しています。
PDFToWord.submit=変換 PDFToWord.submit=変換
#PDFToPresentation
PDFToPresentation.title=PDFをプレゼンテーションに変換 PDFToPresentation.title=PDFをプレゼンテーションに変換
PDFToPresentation.header=PDFをプレゼンテーションに変換 PDFToPresentation.header=PDFをプレゼンテーションに変換
PDFToPresentation.selectText.1=出力ファイル形式 PDFToPresentation.selectText.1=出力ファイル形式
@@ -454,6 +803,7 @@ PDFToPresentation.credit=本サービスはファイル変換にLibreOfficeを
PDFToPresentation.submit=変換 PDFToPresentation.submit=変換
#PDFToText
PDFToText.title=PDFをText/RTFに変換 PDFToText.title=PDFをText/RTFに変換
PDFToText.header=PDFをText/RTFに変換 PDFToText.header=PDFをText/RTFに変換
PDFToText.selectText.1=出力ファイル形式 PDFToText.selectText.1=出力ファイル形式
@@ -461,11 +811,14 @@ PDFToText.credit=本サービスはファイル変換にLibreOfficeを使用し
PDFToText.submit=変換 PDFToText.submit=変換
#PDFToHTML
PDFToHTML.title=PDFをHTMLに変換 PDFToHTML.title=PDFをHTMLに変換
PDFToHTML.header=PDFをHTMLに変換 PDFToHTML.header=PDFをHTMLに変換
PDFToHTML.credit=本サービスはファイル変換にLibreOfficeを使用しています。 PDFToHTML.credit=本サービスはファイル変換にLibreOfficeを使用しています。
PDFToHTML.submit=変換 PDFToHTML.submit=変換
#PDFToXML
PDFToXML.title=PDFをXMLに変換 PDFToXML.title=PDFをXMLに変換
PDFToXML.header=PDFをXMLに変換 PDFToXML.header=PDFをXMLに変換
PDFToXML.credit=本サービスはファイル変換にLibreOfficeを使用しています。 PDFToXML.credit=本サービスはファイル変換にLibreOfficeを使用しています。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,94 @@
#page-container { #page-container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#content-wrap { #content-wrap {
flex: 1; flex: 1;
} }
#footer { #footer {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
} }
.navbar {
html[lang-direction=ltr] * { height: auto; /* Adjusts height automatically based on content */
direction: ltr; white-space: nowrap; /* Prevents wrapping of navbar contents */
} }
html[lang-direction=rtl] * { /* TODO enable later
direction: rtl; .navbar .container {
text-align: right;
}
.ignore-rtl { max-width: 100%; //Allows the container to expand up to full width
direction: ltr !important; margin-left: auto;
text-align: left !important; margin-right: auto;
} }*/
.align-top { html[lang-direction=ltr] * {
position: absolute; direction: ltr;
top: 0; }
} html[lang-direction=rtl] * {
.align-center-right { direction: rtl;
position: absolute; text-align: right;
right: 0; }
top: 50%; .ignore-rtl {
} direction: ltr !important;
text-align: left !important;
.align-center-left { }
position: absolute;
left: 0; .align-top {
top: 50%; position: absolute;
} top: 0;
}
.align-bottom { .align-center-right {
position: absolute; position: absolute;
bottom: 0; right: 0;
} top: 50%;
}
.btn-group > label:first-of-type {
border-top-left-radius: 0.25rem !important; .align-center-left {
border-bottom-left-radius: 0.25rem !important; position: absolute;
} left: 0;
top: 50%;
html[lang-direction="rtl"] input.form-check-input { }
position: relative;
margin-left: 0px; .align-bottom {
} position: absolute;
html[lang-direction="rtl"] label.form-check-label { bottom: 0;
display: inline; }
}
.btn-group > label:first-of-type {
.margin-auto-parent { border-top-left-radius: 0.25rem !important;
width: 100%; border-bottom-left-radius: 0.25rem !important;
display: flex; }
}
.margin-center { html[lang-direction="rtl"] input.form-check-input {
margin: 0 auto; position: relative;
margin-left: 0px;
}
html[lang-direction="rtl"] label.form-check-label {
display: inline;
}
.margin-auto-parent {
width: 100%;
display: flex;
}
.margin-center {
margin: 0 auto;
}
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
}
.fixed-shadow-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
}
.shadow-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
}
.hidden {
display: none;
} }

View File

@@ -1,3 +1,17 @@
#searchBar {
background-image: url('/images/search.svg');
background-position: 16px 16px;
background-repeat: no-repeat;
width: 100%;
font-size: 16px;
margin-bottom: 12px;
padding: 12px 20px 12px 40px;
border: 1px solid #ddd;
}
.features-container { .features-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr)); grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr));

View File

@@ -1,25 +1,29 @@
.list-group-item { .list-group-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.filename { .filename {
flex-grow: 1; flex-grow: 1;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-right: 10px; margin-right: 10px;
} }
.arrows { .arrows {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.arrows .btn {
.move-up span, margin: 0 3px;
.move-down span { }
font-weight: bold;
font-size: 1.2em; .move-up span,
} .move-down span {
font-weight: bold;
font-size: 1.2em;
}

View File

@@ -1,3 +1,41 @@
#navbarSearch {
top: 100%;
right: 0;
}
#searchForm {
width: 200px; /* Adjust this value as needed */
}
/* Style the search results to match the navbar */
#searchResults {
max-height: 200px; /* Adjust this value as needed */
overflow-y: auto;
width: 100%;
}
#searchResults .dropdown-item {
display: flex;
align-items: center;
white-space: nowrap;
height: 50px; /* Fixed height */
overflow: hidden; /* Hide overflow */
}
#searchResults .icon {
margin-right: 10px;
}
#searchResults .icon-text {
display: inline;
overflow: hidden; /* Hide overflow */
text-overflow: ellipsis; /* Add ellipsis for long text */
}
.main-icon { .main-icon {
width: 36px; width: 36px;
height: 36px; height: 36px;

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 870 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-palette" viewBox="0 0 16 16">
<path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/>
<path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8zm-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7z"/>
</svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-crop" viewBox="0 0 16 16">
<path d="M3.5.5A.5.5 0 0 1 4 1v13h13a.5.5 0 0 1 0 1h-2v2a.5.5 0 0 1-1 0v-2H3.5a.5.5 0 0 1-.5-.5V4H1a.5.5 0 0 1 0-1h2V1a.5.5 0 0 1 .5-.5zm2.5 3a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4H6.5a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fonts" viewBox="0 0 16 16">
<path d="M12.258 3h-8.51l-.083 2.46h.479c.26-1.544.758-1.783 2.693-1.845l.424-.013v7.827c0 .663-.144.82-1.3.923v.52h4.082v-.52c-1.162-.103-1.306-.26-1.306-.923V3.602l.431.013c1.934.062 2.434.301 2.693 1.846h.479L12.258 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-filetype-html" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V11h-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5Zm-9.736 7.35v3.999h-.791v-1.714H1.79v1.714H1V11.85h.791v1.626h1.682V11.85h.79Zm2.251.662v3.337h-.794v-3.337H4.588v-.662h3.064v.662H6.515Zm2.176 3.337v-2.66h.038l.952 2.159h.516l.946-2.16h.038v2.661h.715V11.85h-.8l-1.14 2.596H9.93L8.79 11.85h-.805v3.999h.706Zm4.71-.674h1.696v.674H12.61V11.85h.79v3.325Z"/>
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@@ -0,0 +1 @@
<svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h48v48h-48z" fill="none"/><path d="m8.091 21c0 1.089-.576 1.695-1.304 2.464-.755.797-1.696 1.788-1.696 3.394v11.142c0 2.757 2.243 5 5 5h12c2.757 0 5-2.243 5-5v-11.143c0-2.316-2.045-3.302-4.022-4.254-2.447-1.179-4.978-2.397-4.978-6.104v-.215l-.088-.195c-.081-.179-.287-.608-.6-1.09h1.969l2.032-1.242 5.949 5.949 1.414-1.414-5.608-5.608 1.841-1.123v-6.561h-14.186l-5.713 3.428-.12 6.572h3.46c-.219.456-.351.961-.351 1.5v4.5zm-1.01-11.428 4.287-2.572h11.632v3.439l-4.19 2.561h-4.219-3-4.572zm3.01 11.428v-4.5c0-.827.673-1.5 1.5-1.5h3c.341 0 1.054.832 1.502 1.731.108 4.784 3.569 6.451 6.107 7.674 1.846.89 2.89 1.441 2.89 2.452v11.143c0 1.654-1.346 3-3 3h-12c-1.654 0-3-1.346-3-3v-11.143c0-.771.415-1.244 1.147-2.017.826-.87 1.854-1.953 1.854-3.84z"/><path d="m15.091 38h2v-5h5v-2h-5v-5h-2v5h-5v2h5z"/><circle cx="30.091" cy="8" r="2"/><circle cx="36.091" cy="8" r="2"/><circle cx="42.091" cy="8" r="2"/><circle cx="33.091" cy="13" r="2"/><circle cx="37.091" cy="17" r="2"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link" viewBox="0 0 16 16">
<path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4H9c-.086 0-.17.01-.25.031A2 2 0 0 1 7 10.5H4a2 2 0 1 1 0-4h1.535c.218-.376.495-.714.82-1z"/>
<path d="M9 5.5a3 3 0 0 0-2.83 4h1.098A2 2 0 0 1 9 6.5h3a2 2 0 1 1 0 4h-1.535a4.02 4.02 0 0 1-.82 1H12a3 3 0 1 0 0-6H9z"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@@ -1,30 +1,84 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const fileInput = document.getElementById(elementID);
// Prevent default behavior for drag events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
fileInput.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) { let overlay;
e.preventDefault(); let dragCounter = 0;
e.stopPropagation();
}
// Add drop event listener const dragenterListener = function() {
fileInput.addEventListener('drop', handleDrop, false); dragCounter++;
if (!overlay) {
// Create and show the overlay
overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = 0;
overlay.style.left = 0;
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.background = 'rgba(0, 0, 0, 0.5)';
overlay.style.color = '#fff';
overlay.style.zIndex = '1000';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.pointerEvents = 'none';
overlay.innerHTML = '<p>Drop files anywhere to upload</p>';
document.getElementById('content-wrap').appendChild(overlay);
}
};
const dragleaveListener = function() {
dragCounter--;
if (dragCounter === 0) {
// Hide and remove the overlay
if (overlay) {
overlay.remove();
overlay = null;
}
}
};
const dropListener = function(e) {
const dt = e.dataTransfer;
const files = dt.files;
// Access the file input element and assign dropped files
const fileInput = document.getElementById(elementID);
fileInput.files = files;
// Hide and remove the overlay
if (overlay) {
overlay.remove();
overlay = null;
}
// Reset drag counter
dragCounter = 0;
//handleFileInputChange(fileInput);
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
};
// Prevent default behavior for drag events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
document.body.addEventListener('dragenter', dragenterListener);
document.body.addEventListener('dragleave', dragleaveListener);
// Add drop event listener
document.body.addEventListener('drop', dropListener);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
fileInput.files = files;
handleFileInputChange(fileInput)
}
}); });
$("#"+elementID).on("change", function() { $("#"+elementID).on("change", function() {
handleFileInputChange(this); handleFileInputChange(this);
}); });
function handleFileInputChange(inputElement) { function handleFileInputChange(inputElement) {
const files = $(inputElement).get(0).files; const files = $(inputElement).get(0).files;
const fileNames = Array.from(files).map(f => f.name); const fileNames = Array.from(files).map(f => f.name);

View File

@@ -1,3 +1,29 @@
function filterCards() {
var input = document.getElementById('searchBar');
var filter = input.value.toUpperCase();
var cards = document.querySelectorAll('.feature-card');
for (var i = 0; i < cards.length; i++) {
var card = cards[i];
var title = card.querySelector('h5.card-title').innerText;
var text = card.querySelector('p.card-text').innerText;
// Get the navbar tags associated with the card
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
var navbarTags = navbarItem ? navbarItem.getAttribute('data-tags') : '';
var content = title + ' ' + text + ' ' + navbarTags;
if (content.toUpperCase().indexOf(filter) > -1) {
card.style.display = "";
} else {
card.style.display = "none";
}
}
}
function toggleFavorite(element) { function toggleFavorite(element) {
var img = element.querySelector('img'); var img = element.querySelector('img');
var card = element.closest('.feature-card'); var card = element.closest('.feature-card');
@@ -13,6 +39,7 @@ function toggleFavorite(element) {
} }
reorderCards(); reorderCards();
updateFavoritesDropdown(); updateFavoritesDropdown();
filterCards();
} }
function reorderCards() { function reorderCards() {
@@ -45,5 +72,7 @@ function initializeCards() {
}); });
reorderCards(); reorderCards();
updateFavoritesDropdown(); updateFavoritesDropdown();
filterCards();
} }
window.onload = initializeCards; window.onload = initializeCards;

View File

@@ -1,206 +1,207 @@
class PdfContainer { class PdfContainer {
fileName; fileName;
pagesContainer; pagesContainer;
pagesContainerWrapper; pagesContainerWrapper;
pdfAdapters; pdfAdapters;
constructor(id, wrapperId, pdfAdapters) { constructor(id, wrapperId, pdfAdapters) {
this.fileName = null; this.fileName = null;
this.pagesContainer = document.getElementById(id) this.pagesContainer = document.getElementById(id)
this.pagesContainerWrapper = document.getElementById(wrapperId); this.pagesContainerWrapper = document.getElementById(wrapperId);
this.movePageTo = this.movePageTo.bind(this); this.movePageTo = this.movePageTo.bind(this);
this.addPdfs = this.addPdfs.bind(this); this.addPdfs = this.addPdfs.bind(this);
this.rotateElement = this.rotateElement.bind(this); this.rotateElement = this.rotateElement.bind(this);
this.rotateAll = this.rotateAll.bind(this); this.rotateAll = this.rotateAll.bind(this);
this.exportPdf = this.exportPdf.bind(this); this.exportPdf = this.exportPdf.bind(this);
this.pdfAdapters = pdfAdapters; this.pdfAdapters = pdfAdapters;
this.pdfAdapters.forEach(adapter => { this.pdfAdapters.forEach(adapter => {
adapter.setActions({ adapter.setActions({
movePageTo: this.movePageTo, movePageTo: this.movePageTo,
addPdfs: this.addPdfs, addPdfs: this.addPdfs,
rotateElement: this.rotateElement, rotateElement: this.rotateElement,
}) })
}) })
window.addPdfs = this.addPdfs; window.addPdfs = this.addPdfs;
window.exportPdf = this.exportPdf; window.exportPdf = this.exportPdf;
window.rotateAll = this.rotateAll; window.rotateAll = this.rotateAll;
} }
movePageTo(startElement, endElement, scrollTo = false) { movePageTo(startElement, endElement, scrollTo = false) {
const childArray = Array.from(this.pagesContainer.childNodes); const childArray = Array.from(this.pagesContainer.childNodes);
const startIndex = childArray.indexOf(startElement); const startIndex = childArray.indexOf(startElement);
const endIndex = childArray.indexOf(endElement); const endIndex = childArray.indexOf(endElement);
this.pagesContainer.removeChild(startElement); this.pagesContainer.removeChild(startElement);
if(!endElement) { if(!endElement) {
this.pagesContainer.append(startElement); this.pagesContainer.append(startElement);
} else { } else {
this.pagesContainer.insertBefore(startElement, endElement); this.pagesContainer.insertBefore(startElement, endElement);
} }
if(scrollTo) { if(scrollTo) {
const { width } = startElement.getBoundingClientRect(); const { width } = startElement.getBoundingClientRect();
const vector = (endIndex !== -1 && startIndex > endIndex) const vector = (endIndex !== -1 && startIndex > endIndex)
? 0-width ? 0-width
: width; : width;
this.pagesContainerWrapper.scroll({ this.pagesContainerWrapper.scroll({
left: this.pagesContainerWrapper.scrollLeft + vector, left: this.pagesContainerWrapper.scrollLeft + vector,
}) })
} }
} }
addPdfs(nextSiblingElement) { addPdfs(nextSiblingElement) {
var input = document.createElement('input'); var input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.multiple = true; input.multiple = true;
input.setAttribute("accept", "application/pdf"); input.setAttribute("accept", "application/pdf");
input.onchange = async(e) => { input.onchange = async(e) => {
const files = e.target.files; const files = e.target.files;
this.fileName = files[0].name; this.fileName = files[0].name;
for (var i=0; i < files.length; i++) { for (var i=0; i < files.length; i++) {
await this.addPdfFile(files[i], nextSiblingElement); await this.addPdfFile(files[i], nextSiblingElement);
} }
document.querySelectorAll(".enable-on-file").forEach(element => { document.querySelectorAll(".enable-on-file").forEach(element => {
element.disabled = false; element.disabled = false;
}); });
} }
input.click(); input.click();
} }
rotateElement(element, deg) { rotateElement(element, deg) {
var lastTransform = element.style.rotate; var lastTransform = element.style.rotate;
if (!lastTransform) { if (!lastTransform) {
lastTransform = "0"; lastTransform = "0";
} }
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, '')); const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ''));
const newAngle = lastAngle + deg; const newAngle = lastAngle + deg;
element.style.rotate = newAngle + "deg"; element.style.rotate = newAngle + "deg";
} }
async addPdfFile(file, nextSiblingElement) { async addPdfFile(file, nextSiblingElement) {
const { renderer, pdfDocument } = await this.loadFile(file); const { renderer, pdfDocument } = await this.loadFile(file);
for (var i=0; i < renderer.pageCount; i++) { for (var i=0; i < renderer.pageCount; i++) {
const div = document.createElement('div'); const div = document.createElement('div');
div.classList.add("page-container"); div.classList.add("page-container");
var img = document.createElement('img'); var img = document.createElement('img');
img.classList.add('page-image') img.classList.add('page-image')
const imageSrc = await renderer.renderPage(i) const imageSrc = await renderer.renderPage(i)
img.src = imageSrc; img.src = imageSrc;
img.pageIdx = i; img.pageIdx = i;
img.rend = renderer; img.rend = renderer;
img.doc = pdfDocument; img.doc = pdfDocument;
div.appendChild(img); div.appendChild(img);
this.pdfAdapters.forEach((adapter) => { this.pdfAdapters.forEach((adapter) => {
adapter.adapt?.(div) adapter.adapt?.(div)
}) })
if (nextSiblingElement) { if (nextSiblingElement) {
this.pagesContainer.insertBefore(div, nextSiblingElement); this.pagesContainer.insertBefore(div, nextSiblingElement);
} else { } else {
this.pagesContainer.appendChild(div); this.pagesContainer.appendChild(div);
} }
} }
} }
async loadFile(file) { async loadFile(file) {
var objectUrl = URL.createObjectURL(file); var objectUrl = URL.createObjectURL(file);
var pdfDocument = await this.toPdfLib(objectUrl); var pdfDocument = await this.toPdfLib(objectUrl);
var renderer = await this.toRenderer(objectUrl); var renderer = await this.toRenderer(objectUrl);
return { renderer, pdfDocument }; return { renderer, pdfDocument };
} }
async toRenderer(objectUrl) { async toRenderer(objectUrl) {
const pdf = await pdfjsLib.getDocument(objectUrl).promise; pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
return { const pdf = await pdfjsLib.getDocument(objectUrl).promise;
document: pdf, return {
pageCount: pdf.numPages, document: pdf,
renderPage: async function(pageIdx) { pageCount: pdf.numPages,
const page = await this.document.getPage(pageIdx+1); renderPage: async function(pageIdx) {
const page = await this.document.getPage(pageIdx+1);
const canvas = document.createElement("canvas");
const canvas = document.createElement("canvas");
// set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) { // set the canvas size to the size of the page
canvas.width = page.view[3]; if (page.rotate == 90 || page.rotate == 270) {
canvas.height = page.view[2]; canvas.width = page.view[3];
} else { canvas.height = page.view[2];
canvas.width = page.view[2]; } else {
canvas.height = page.view[3]; canvas.width = page.view[2];
} canvas.height = page.view[3];
}
// render the page onto the canvas
var renderContext = { // render the page onto the canvas
canvasContext: canvas.getContext("2d"), var renderContext = {
viewport: page.getViewport({ scale: 1 }) canvasContext: canvas.getContext("2d"),
}; viewport: page.getViewport({ scale: 1 })
};
await page.render(renderContext).promise;
return canvas.toDataURL(); await page.render(renderContext).promise;
} return canvas.toDataURL();
}; }
} };
}
async toPdfLib(objectUrl) {
const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer()); async toPdfLib(objectUrl) {
const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes, { ignoreEncryption: true }); const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer());
return pdfDoc; const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes, { ignoreEncryption: true });
} return pdfDoc;
}
rotateAll(deg) {
for (var i=0; i<this.pagesContainer.childNodes.length; i++) { rotateAll(deg) {
const img = this.pagesContainer.childNodes[i].querySelector("img"); for (var i=0; i<this.pagesContainer.childNodes.length; i++) {
if (!img) continue; const img = this.pagesContainer.childNodes[i].querySelector("img");
this.rotateElement(img, deg) if (!img) continue;
} this.rotateElement(img, deg)
} }
}
async exportPdf() {
const pdfDoc = await PDFLib.PDFDocument.create(); async exportPdf() {
for (var i=0; i<this.pagesContainer.childNodes.length; i++) { const pdfDoc = await PDFLib.PDFDocument.create();
const img = this.pagesContainer.childNodes[i].querySelector("img"); for (var i=0; i<this.pagesContainer.childNodes.length; i++) {
if (!img) continue; const img = this.pagesContainer.childNodes[i].querySelector("img");
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]) if (!img) continue;
const page = pages[0]; const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx])
const page = pages[0];
const rotation = img.style.rotate;
if (rotation) { const rotation = img.style.rotate;
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, '')); if (rotation) {
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle)) const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
} page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle))
}
pdfDoc.addPage(page);
} pdfDoc.addPage(page);
const pdfBytes = await pdfDoc.save(); }
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); const pdfBytes = await pdfDoc.save();
const url = URL.createObjectURL(pdfBlob); const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const downloadOption = localStorage.getItem('downloadOption'); const url = URL.createObjectURL(pdfBlob);
const downloadOption = localStorage.getItem('downloadOption');
if (downloadOption === 'sameWindow') {
// Open the file in the same window if (downloadOption === 'sameWindow') {
window.location.href = url; // Open the file in the same window
} else if (downloadOption === 'newWindow') { window.location.href = url;
// Open the file in a new window } else if (downloadOption === 'newWindow') {
window.open(url, '_blank'); // Open the file in a new window
} else { window.open(url, '_blank');
// Download the file } else {
const downloadLink = document.createElement('a'); // Download the file
downloadLink.href = url; const downloadLink = document.createElement('a');
downloadLink.download = this.fileName ? this.fileName : 'managed.pdf'; downloadLink.href = url;
downloadLink.click(); downloadLink.download = this.fileName ? this.fileName : 'managed.pdf';
} downloadLink.click();
} }
} }
}
export default PdfContainer;
export default PdfContainer;

View File

@@ -1,419 +1,494 @@
document.getElementById('validateButton').addEventListener('click', function(event) { document.getElementById('validateButton').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
validatePipeline(); validatePipeline();
}); });
function validatePipeline() { function validatePipeline() {
let pipelineListItems = document.getElementById('pipelineList').children; let pipelineListItems = document.getElementById('pipelineList').children;
let isValid = true; let isValid = true;
let containsAddPassword = false; let containsAddPassword = false;
for (let i = 0; i < pipelineListItems.length - 1; i++) { for (let i = 0; i < pipelineListItems.length - 1; i++) {
let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent; let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent;
let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent; let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent;
if (currentOperation === '/add-password') { if (currentOperation === '/add-password') {
containsAddPassword = true; containsAddPassword = true;
} }
console.log(currentOperation); console.log(currentOperation);
console.log(apiDocs[currentOperation]); console.log(apiDocs[currentOperation]);
let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; let currentOperationDescription = apiDocs[currentOperation]?.post?.description || "";
let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; let nextOperationDescription = apiDocs[nextOperation]?.post?.description || "";
console.log("currentOperationDescription", currentOperationDescription); console.log("currentOperationDescription", currentOperationDescription);
console.log("nextOperationDescription", nextOperationDescription); console.log("nextOperationDescription", nextOperationDescription);
let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || ""; let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || "";
let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || ""; let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || "";
console.log("Operation " + currentOperation + " Output: " + currentOperationOutput); console.log("Operation " + currentOperation + " Output: " + currentOperationOutput);
console.log("Operation " + nextOperation + " Input: " + nextOperationInput); console.log("Operation " + nextOperation + " Input: " + nextOperationInput);
// Splitting in case of multiple possible output/input // Splitting in case of multiple possible output/input
let currentOperationOutputArr = currentOperationOutput.split('/'); let currentOperationOutputArr = currentOperationOutput.split('/');
let nextOperationInputArr = nextOperationInput.split('/'); let nextOperationInputArr = nextOperationInput.split('/');
if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') { if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') {
let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value)); let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value));
console.log(`Intersection: ${intersection}`); console.log(`Intersection: ${intersection}`);
if (intersection.length === 0) { if (intersection.length === 0) {
isValid = false; isValid = false;
console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
break; break;
} }
} }
} }
if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') { if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') {
alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.');
return false; return false;
} }
if (isValid) { if (isValid) {
console.log('Pipeline is valid'); console.log('Pipeline is valid');
// Continue with the pipeline operation // Continue with the pipeline operation
} else { } else {
console.error('Pipeline is not valid'); console.error('Pipeline is not valid');
// Stop operation, maybe display an error to the user // Stop operation, maybe display an error to the user
} }
return isValid; return isValid;
} }
document.getElementById('submitConfigBtn').addEventListener('click', function() { document.getElementById('submitConfigBtn').addEventListener('click', function() {
if (validatePipeline() === false) { if (validatePipeline() === false) {
return; return;
} }
let selectedOperation = document.getElementById('operationsDropdown').value; let selectedOperation = document.getElementById('operationsDropdown').value;
let parameters = operationSettings[selectedOperation] || {}; let parameters = operationSettings[selectedOperation] || {};
let pipelineConfig = { let pipelineConfig = {
"name": "uniquePipelineName", "name": "uniquePipelineName",
"pipeline": [{ "pipeline": [{
"operation": selectedOperation, "operation": selectedOperation,
"parameters": parameters "parameters": parameters
}] }],
}; "_examples": {
"outputDir": "{outputFolder}/{folderName}",
let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); "outputFileName": "{filename}-{pipelineName}-{date}-{time}"
},
let formData = new FormData(); "outputDir": "httpWebRequest",
"outputFileName": "{filename}"
let fileInput = document.getElementById('fileInput'); };
let files = fileInput.files;
let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2);
for (let i = 0; i < files.length; i++) {
console.log("files[i]", files[i].name); let formData = new FormData();
formData.append('fileInput', files[i], files[i].name);
} let fileInput = document.getElementById('fileInput-input');
let files = fileInput.files;
console.log("pipelineConfigJson", pipelineConfigJson);
formData.append('json', pipelineConfigJson); for (let i = 0; i < files.length; i++) {
console.log("formData", formData); console.log("files[i]", files[i].name);
formData.append('fileInput', files[i], files[i].name);
fetch('/handleData', { }
method: 'POST',
body: formData console.log("pipelineConfigJson", pipelineConfigJson);
}) formData.append('json', pipelineConfigJson);
.then(response => response.blob()) console.log("formData", formData);
.then(blob => {
fetch('/handleData', {
let url = window.URL.createObjectURL(blob); method: 'POST',
let a = document.createElement('a'); body: formData
a.href = url; })
a.download = 'outputfile'; .then(response => response.blob())
document.body.appendChild(a); .then(blob => {
a.click();
a.remove(); let url = window.URL.createObjectURL(blob);
}) let a = document.createElement('a');
.catch((error) => { a.href = url;
console.error('Error:', error); a.download = 'outputfile';
}); document.body.appendChild(a);
}); a.click();
a.remove();
let apiDocs = {}; })
.catch((error) => {
let operationSettings = {}; console.error('Error:', error);
});
fetch('v3/api-docs') });
.then(response => response.json())
.then(data => { let apiDocs = {};
apiDocs = data.paths; let operationSettings = {};
let operationsDropdown = document.getElementById('operationsDropdown');
const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here fetch('v3/api-docs')
.then(response => response.json())
operationsDropdown.innerHTML = ''; .then(data => {
let operationsByTag = {}; apiDocs = data.paths;
let operationsDropdown = document.getElementById('operationsDropdown');
// Group operations by tags const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here
Object.keys(data.paths).forEach(operationPath => {
let operation = data.paths[operationPath].post; operationsDropdown.innerHTML = '';
if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) {
let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag let operationsByTag = {};
if (!operationsByTag[operationTag]) {
operationsByTag[operationTag] = []; // Group operations by tags
} Object.keys(data.paths).forEach(operationPath => {
operationsByTag[operationTag].push(operationPath); let operation = data.paths[operationPath].post;
} if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) {
}); let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag
if (!operationsByTag[operationTag]) {
// Specify the order of tags operationsByTag[operationTag] = [];
let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; }
operationsByTag[operationTag].push(operationPath);
// Create dropdown options }
tagOrder.forEach(tag => { });
if (operationsByTag[tag]) {
let group = document.createElement('optgroup'); // Specify the order of tags
group.label = tag; let tagOrder = ["General", "Security", "Convert", "Other", "Filter"];
operationsByTag[tag].forEach(operationPath => { // Create dropdown options
let option = document.createElement('option'); tagOrder.forEach(tag => {
let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes if (operationsByTag[tag]) {
option.textContent = operationWithoutSlash; let group = document.createElement('optgroup');
option.value = operationPath; // Keep the value with slashes for querying group.label = tag;
group.appendChild(option);
}); operationsByTag[tag].forEach(operationPath => {
let option = document.createElement('option');
operationsDropdown.appendChild(group); let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes
} option.textContent = operationWithoutSlash;
}); option.value = operationPath; // Keep the value with slashes for querying
}); group.appendChild(option);
});
document.getElementById('addOperationBtn').addEventListener('click', function() { operationsDropdown.appendChild(group);
let selectedOperation = document.getElementById('operationsDropdown').value; }
let pipelineList = document.getElementById('pipelineList'); });
});
let listItem = document.createElement('li');
listItem.className = "list-group-item";
let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && document.getElementById('addOperationBtn').addEventListener('click', function() {
apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0); let selectedOperation = document.getElementById('operationsDropdown').value;
let pipelineList = document.getElementById('pipelineList');
listItem.innerHTML = ` let listItem = document.createElement('li');
<div class="d-flex justify-content-between align-items-center w-100"> listItem.className = "list-group-item";
<div class="operationName">${selectedOperation}</div> let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post &&
<div class="arrows d-flex"> ((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) ||
<button class="btn btn-secondary move-up btn-margin"><span>&uarr;</span></button> (apiDocs[selectedOperation].post.requestBody &&
<button class="btn btn-secondary move-down btn-margin"><span>&darr;</span></button> apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties)));
<button class="btn btn-warning pipelineSettings btn-margin" ${hasSettings ? "" : "disabled"}><span style="color: ${hasSettings ? "black" : "grey"};">⚙️</span></button>
<button class="btn btn-danger remove"><span>X</span></button>
</div>
</div>
`; listItem.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100">
pipelineList.appendChild(listItem); <div class="operationName">${selectedOperation}</div>
<div class="arrows d-flex">
listItem.querySelector('.move-up').addEventListener('click', function(event) { <button class="btn btn-secondary move-up btn-margin"><span>&uarr;</span></button>
event.preventDefault(); <button class="btn btn-secondary move-down btn-margin"><span>&darr;</span></button>
if (listItem.previousElementSibling) { <button class="btn btn-warning pipelineSettings btn-margin" ${hasSettings ? "" : "disabled"}><span style="color: ${hasSettings ? "black" : "grey"};">⚙️</span></button>
pipelineList.insertBefore(listItem, listItem.previousElementSibling); <button class="btn btn-danger remove"><span>X</span></button>
} </div>
}); </div>
`;
listItem.querySelector('.move-down').addEventListener('click', function(event) {
event.preventDefault(); pipelineList.appendChild(listItem);
if (listItem.nextElementSibling) {
pipelineList.insertBefore(listItem.nextElementSibling, listItem); listItem.querySelector('.move-up').addEventListener('click', function(event) {
} event.preventDefault();
}); if (listItem.previousElementSibling) {
pipelineList.insertBefore(listItem, listItem.previousElementSibling);
listItem.querySelector('.remove').addEventListener('click', function(event) { }
event.preventDefault(); });
pipelineList.removeChild(listItem);
}); listItem.querySelector('.move-down').addEventListener('click', function(event) {
event.preventDefault();
listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { if (listItem.nextElementSibling) {
event.preventDefault(); pipelineList.insertBefore(listItem.nextElementSibling, listItem);
showpipelineSettingsModal(selectedOperation); }
}); });
function showpipelineSettingsModal(operation) { listItem.querySelector('.remove').addEventListener('click', function(event) {
let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); event.preventDefault();
let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); pipelineList.removeChild(listItem);
let operationData = apiDocs[operation].post.parameters || []; });
pipelineSettingsContent.innerHTML = ''; listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) {
event.preventDefault();
operationData.forEach(parameter => { showpipelineSettingsModal(selectedOperation);
let parameterDiv = document.createElement('div'); });
parameterDiv.className = "form-group";
function showpipelineSettingsModal(operation) {
let parameterLabel = document.createElement('label'); let pipelineSettingsModal = document.getElementById('pipelineSettingsModal');
parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; let pipelineSettingsContent = document.getElementById('pipelineSettingsContent');
parameterLabel.title = parameter.description; let operationData = apiDocs[operation].post.parameters || [];
parameterDiv.appendChild(parameterLabel); let requestBodyData = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema.properties || {};
let parameterInput; // Combine operationData and requestBodyData into a single array
switch (parameter.schema.type) { operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({
case 'string': name: key,
case 'number': schema: requestBodyData[key]
case 'integer': })));
parameterInput = document.createElement('input');
parameterInput.type = parameter.schema.type === 'string' ? 'text' : 'number'; pipelineSettingsContent.innerHTML = '';
parameterInput.className = "form-control";
break; operationData.forEach(parameter => {
case 'boolean': // If the parameter name is 'fileInput', return early to skip the rest of this iteration
parameterInput = document.createElement('input'); if (parameter.name === 'fileInput') return;
parameterInput.type = 'checkbox';
break; let parameterDiv = document.createElement('div');
case 'array': parameterDiv.className = "form-group";
case 'object':
parameterInput = document.createElement('textarea'); let parameterLabel = document.createElement('label');
parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`; parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `;
parameterInput.className = "form-control"; parameterLabel.title = parameter.description;
break; parameterDiv.appendChild(parameterLabel);
case 'enum':
parameterInput = document.createElement('select'); let parameterInput;
parameterInput.className = "form-control";
parameter.schema.enum.forEach(option => { // check if enum exists in schema
let optionElement = document.createElement('option'); if (parameter.schema.enum) {
optionElement.value = option; // if enum exists, create a select element
optionElement.text = option; parameterInput = document.createElement('select');
parameterInput.appendChild(optionElement); parameterInput.className = "form-control";
});
break; // iterate over each enum value and create an option for it
default: parameter.schema.enum.forEach(value => {
parameterInput = document.createElement('input'); let option = document.createElement('option');
parameterInput.type = 'text'; option.value = value;
parameterInput.className = "form-control"; option.text = value;
} parameterInput.appendChild(option);
parameterInput.id = parameter.name; });
} else {
if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { // switch-case statement for handling non-enum types
let savedValue = operationSettings[operation][parameter.name]; switch (parameter.schema.type) {
case 'string':
switch (parameter.schema.type) { if (parameter.schema.format === 'binary') {
case 'number': // This is a file input
case 'integer':
parameterInput.value = savedValue.toString(); //parameterInput = document.createElement('input');
break; //parameterInput.type = 'file';
case 'boolean': //parameterInput.className = "form-control";
parameterInput.checked = savedValue;
break; parameterInput = document.createElement('input');
case 'array': parameterInput.type = 'text';
case 'object': parameterInput.className = "form-control";
parameterInput.value = JSON.stringify(savedValue); parameterInput.value = "automatedFileInput";
break; } else {
default: parameterInput = document.createElement('input');
parameterInput.value = savedValue; parameterInput.type = 'text';
} parameterInput.className = "form-control";
} }
break;
parameterDiv.appendChild(parameterInput); case 'number':
case 'integer':
pipelineSettingsContent.appendChild(parameterDiv); parameterInput = document.createElement('input');
}); parameterInput.type = 'number';
parameterInput.className = "form-control";
let saveButton = document.createElement('button'); break;
saveButton.textContent = "Save Settings"; case 'boolean':
saveButton.className = "btn btn-primary"; parameterInput = document.createElement('input');
saveButton.addEventListener('click', function(event) { parameterInput.type = 'checkbox';
event.preventDefault(); break;
let settings = {}; case 'array':
operationData.forEach(parameter => { case 'object':
let value = document.getElementById(parameter.name).value; parameterInput = document.createElement('textarea');
switch (parameter.schema.type) { parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`;
case 'number': parameterInput.className = "form-control";
case 'integer': break;
settings[parameter.name] = Number(value); default:
break; parameterInput = document.createElement('input');
case 'boolean': parameterInput.type = 'text';
settings[parameter.name] = document.getElementById(parameter.name).checked; parameterInput.className = "form-control";
break; }
case 'array': }
case 'object': parameterInput.id = parameter.name;
try {
settings[parameter.name] = JSON.parse(value); if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) {
} catch (err) { let savedValue = operationSettings[operation][parameter.name];
console.error(`Invalid JSON format for ${parameter.name}`);
} switch (parameter.schema.type) {
break; case 'number':
default: case 'integer':
settings[parameter.name] = value; parameterInput.value = savedValue.toString();
} break;
}); case 'boolean':
operationSettings[operation] = settings; parameterInput.checked = savedValue;
console.log(settings); break;
pipelineSettingsModal.style.display = "none"; case 'array':
}); case 'object':
pipelineSettingsContent.appendChild(saveButton); parameterInput.value = JSON.stringify(savedValue);
break;
pipelineSettingsModal.style.display = "block"; default:
parameterInput.value = savedValue;
pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { }
pipelineSettingsModal.style.display = "none"; }
}
parameterDiv.appendChild(parameterInput);
window.onclick = function(event) {
if (event.target == pipelineSettingsModal) { pipelineSettingsContent.appendChild(parameterDiv);
pipelineSettingsModal.style.display = "none"; });
}
} let saveButton = document.createElement('button');
} saveButton.textContent = "Save Settings";
saveButton.className = "btn btn-primary";
document.getElementById('savePipelineBtn').addEventListener('click', function() { saveButton.addEventListener('click', function(event) {
if (validatePipeline() === false) { event.preventDefault();
return; let settings = {};
} operationData.forEach(parameter => {
let pipelineList = document.getElementById('pipelineList').children; let value = document.getElementById(parameter.name).value;
let pipelineConfig = { switch (parameter.schema.type) {
"name": "uniquePipelineName", case 'number':
"pipeline": [] case 'integer':
}; settings[parameter.name] = Number(value);
break;
for (let i = 0; i < pipelineList.length; i++) { case 'boolean':
let operationName = pipelineList[i].querySelector('.operationName').textContent; settings[parameter.name] = document.getElementById(parameter.name).checked;
let parameters = operationSettings[operationName] || {}; break;
case 'array':
pipelineConfig.pipeline.push({ case 'object':
"operation": operationName, try {
"parameters": parameters settings[parameter.name] = JSON.parse(value);
}); } catch (err) {
} console.error(`Invalid JSON format for ${parameter.name}`);
}
let a = document.createElement('a'); break;
a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], { default:
type: 'application/json' settings[parameter.name] = value;
})); }
a.download = 'pipelineConfig.json'; });
a.style.display = 'none'; operationSettings[operation] = settings;
console.log(settings);
document.body.appendChild(a); pipelineSettingsModal.style.display = "none";
a.click(); });
document.body.removeChild(a); pipelineSettingsContent.appendChild(saveButton);
});
pipelineSettingsModal.style.display = "block";
document.getElementById('uploadPipelineBtn').addEventListener('click', function() {
document.getElementById('uploadPipelineInput').click(); pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() {
}); pipelineSettingsModal.style.display = "none";
}
document.getElementById('uploadPipelineInput').addEventListener('change', function(e) {
let reader = new FileReader(); window.onclick = function(event) {
reader.onload = function(event) { if (event.target == pipelineSettingsModal) {
let pipelineConfig = JSON.parse(event.target.result); pipelineSettingsModal.style.display = "none";
let pipelineList = document.getElementById('pipelineList'); }
}
while (pipelineList.firstChild) { }
pipelineList.removeChild(pipelineList.firstChild);
} document.getElementById('savePipelineBtn').addEventListener('click', function() {
if (validatePipeline() === false) {
pipelineConfig.pipeline.forEach(operationConfig => { return;
let operationsDropdown = document.getElementById('operationsDropdown'); }
operationsDropdown.value = operationConfig.operation; var pipelineName = document.getElementById('pipelineName').value;
operationSettings[operationConfig.operation] = operationConfig.parameters; let pipelineList = document.getElementById('pipelineList').children;
document.getElementById('addOperationBtn').click(); let pipelineConfig = {
"name": pipelineName,
let lastOperation = pipelineList.lastChild; "pipeline": [],
"_examples": {
lastOperation.querySelector('.pipelineSettings').click(); "outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
Object.keys(operationConfig.parameters).forEach(parameterName => { },
let input = document.getElementById(parameterName); "outputDir": "httpWebRequest",
if (input) { "outputFileName": "{filename}"
switch (input.type) { };
case 'checkbox':
input.checked = operationConfig.parameters[parameterName]; for (let i = 0; i < pipelineList.length; i++) {
break; let operationName = pipelineList[i].querySelector('.operationName').textContent;
case 'number': let parameters = operationSettings[operationName] || {};
input.value = operationConfig.parameters[parameterName].toString();
break; pipelineConfig.pipeline.push({
case 'text': "operation": operationName,
case 'textarea': "parameters": parameters
default: });
input.value = JSON.stringify(operationConfig.parameters[parameterName]); }
}
} let a = document.createElement('a');
}); a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], {
type: 'application/json'
document.querySelector('#pipelineSettingsModal .btn-primary').click(); }));
}); a.download = 'pipelineConfig.json';
}; a.style.display = 'none';
reader.readAsText(e.target.files[0]);
}); document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
async function processPipelineConfig(configString) {
let pipelineConfig = JSON.parse(configString);
let pipelineList = document.getElementById('pipelineList');
while (pipelineList.firstChild) {
pipelineList.removeChild(pipelineList.firstChild);
}
document.getElementById('pipelineName').value = pipelineConfig.name
for (const operationConfig of pipelineConfig.pipeline) {
let operationsDropdown = document.getElementById('operationsDropdown');
operationsDropdown.value = operationConfig.operation;
operationSettings[operationConfig.operation] = operationConfig.parameters;
// assuming addOperation is async
await new Promise((resolve) => {
document.getElementById('addOperationBtn').addEventListener('click', resolve, { once: true });
document.getElementById('addOperationBtn').click();
});
let lastOperation = pipelineList.lastChild;
Object.keys(operationConfig.parameters).forEach(parameterName => {
let input = document.getElementById(parameterName);
if (input) {
switch (input.type) {
case 'checkbox':
input.checked = operationConfig.parameters[parameterName];
break;
case 'number':
input.value = operationConfig.parameters[parameterName].toString();
break;
case 'file':
if (parameterName !== 'fileInput') {
// Create a new file input element
let newInput = document.createElement('input');
newInput.type = 'file';
newInput.id = parameterName;
// Add the new file input to the main page (change the selector according to your needs)
document.querySelector('#main').appendChild(newInput);
}
break;
case 'text':
case 'textarea':
default:
input.value = JSON.stringify(operationConfig.parameters[parameterName]);
}
}
});
}
}
document.getElementById('uploadPipelineBtn').addEventListener('click', function() {
document.getElementById('uploadPipelineInput').click();
});
document.getElementById('uploadPipelineInput').addEventListener('change', function(e) {
let reader = new FileReader();
reader.onload = function(event) {
processPipelineConfig(event.target.result);
};
reader.readAsText(e.target.files[0]);
});
document.getElementById('pipelineSelect').addEventListener('change', function(e) {
let selectedPipelineJson = e.target.value; // assuming the selected value is the JSON string of the pipeline config
processPipelineConfig(selectedPipelineJson);
});
}); });

View File

@@ -0,0 +1,75 @@
// Toggle search bar when the search icon is clicked
document.querySelector('#search-icon').addEventListener('click', function(e) {
e.preventDefault();
var searchBar = document.querySelector('#navbarSearch');
searchBar.classList.toggle('show');
});
window.onload = function() {
var items = document.querySelectorAll('.dropdown-item, .nav-link');
var dummyContainer = document.createElement('div');
dummyContainer.style.position = 'absolute';
dummyContainer.style.visibility = 'hidden';
dummyContainer.style.whiteSpace = 'nowrap'; // Ensure we measure full width
document.body.appendChild(dummyContainer);
var maxWidth = 0;
items.forEach(function(item) {
var clone = item.cloneNode(true);
dummyContainer.appendChild(clone);
var width = clone.offsetWidth;
if (width > maxWidth) {
maxWidth = width;
}
dummyContainer.removeChild(clone);
});
document.body.removeChild(dummyContainer);
// Store max width for later use
window.navItemMaxWidth = maxWidth;
};
// Show search results as user types in search box
document.querySelector('#navbarSearchInput').addEventListener('input', function(e) {
var searchText = e.target.value.toLowerCase();
var items = document.querySelectorAll('.dropdown-item, .nav-link');
var resultsBox = document.querySelector('#searchResults');
// Clear any previous results
resultsBox.innerHTML = '';
items.forEach(function(item) {
var titleElement = item.querySelector('.icon-text');
var iconElement = item.querySelector('.icon');
var itemHref = item.getAttribute('href');
var tags = item.getAttribute('data-tags') || ""; // If no tags, default to empty string
if (titleElement && iconElement && itemHref !== '#') {
var title = titleElement.innerText;
if ((title.toLowerCase().indexOf(searchText) !== -1 || tags.toLowerCase().indexOf(searchText) !== -1) && !resultsBox.querySelector(`a[href="${item.getAttribute('href')}"]`)) {
var result = document.createElement('a');
result.href = itemHref;
result.classList.add('dropdown-item');
var resultIcon = document.createElement('img');
resultIcon.src = iconElement.src;
resultIcon.alt = 'icon';
resultIcon.classList.add('icon');
result.appendChild(resultIcon);
var resultText = document.createElement('span');
resultText.textContent = title;
resultText.classList.add('icon-text');
result.appendChild(resultText);
resultsBox.appendChild(result);
}
}
});
// Set the width of the search results box to the maximum width
resultsBox.style.width = window.navItemMaxWidth + 'px';
});

View File

@@ -0,0 +1,44 @@
<!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=#{autoSplitPDF.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{autoSplitPDF.header}"></h2>
<!-- Added a brief description -->
<p th:text="#{autoSplitPDF.description}"></p>
<ul>
<li th:text="#{autoSplitPDF.selectText.1}"></li>
<li th:text="#{autoSplitPDF.selectText.2}"></li>
<li th:text="#{autoSplitPDF.selectText.3}"></li>
<li th:text="#{autoSplitPDF.selectText.4}"></li>
</ul>
<form method="post" enctype="multipart/form-data">
<p th:text="#{autoSplitPDF.formPrompt}"></p>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="duplexMode" id="duplexMode">
<label class="ml-3" for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label>
</div>
<p><a th:href="@{files/Auto Splitter Divider (minimal).pdf}" download th:text="#{autoSplitPDF.dividerDownload1}"></a></p>
<p><a th:href="@{files/Auto Splitter Divider (with instructions).pdf}" download th:text="#{autoSplitPDF.dividerDownload2}"></a></p>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoSplitPDF.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!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=#{HTMLToPDF.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{HTMLToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{html-to-pdf}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{HTMLToPDF.help}"></p>
<p class="mt-3" th:text="#{HTMLToPDF.credit}"></p>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -1,29 +1,29 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title})}"></th:block>
<body> <body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToHTML.header}"></h2> <h2 th:text="#{PDFToHTML.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{pdf-to-html}"> <form method="post" enctype="multipart/form-data" th:action="@{pdf-to-html}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToHTML.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToHTML.submit}"></button>
</form> </form>
<p class="mt-3" th:text="#{PDFToHTML.credit}"></p> <p class="mt-3" th:text="#{PDFToHTML.credit}"></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,35 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title})}"></th:block>
<body> <body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToText.header}"></h2> <h2 th:text="#{PDFToText.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{pdf-to-text}"> <form method="post" enctype="multipart/form-data" th:action="@{pdf-to-text}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="form-group">
<label th:text="#{PDFToText.selectText.1}"></label> <label th:text="#{PDFToText.selectText.1}"></label>
<select class="form-control" name="outputFormat"> <select class="form-control" name="outputFormat">
<option value="rtf">RTF</option> <option value="rtf">RTF</option>
<option value="txt:Text">TXT</option> </select>
</select> </div>
</div> <br>
<br> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
</form>
</form> <p class="mt-3" th:text="#{PDFToText.credit}"></p>
<p class="mt-3" th:text="#{PDFToText.credit}"></p> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div th:insert="~{fragments/footer.html :: footer}"></div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>

View File

@@ -0,0 +1,29 @@
<!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=#{URLToPDF.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{URLToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{url-to-pdf}">
<input type="text" class="form-control" id="urlInput" name="urlInput">
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{URLToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{URLToPDF.credit}"></p>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -0,0 +1,147 @@
<!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=#{crop.title})}"></th:block>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{crop.header}"></h2>
<form id="cropForm" action="/crop" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<input id="x" type="hidden" name="x">
<input id="y" type="hidden" name="y">
<input id="width" type="hidden" name="width">
<input id="height" type="hidden" name="height">
<button type="submit" class="btn btn-primary" th:text="#{crop.submit}"></button>
</form>
<div style="position: relative; display: inline-block;">
<canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div>
<script>
let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let isDrawing = false; // New flag to check if drawing is ongoing
let cropForm = document.getElementById('cropForm');
let fileInput = document.getElementById('fileInput-input');
let xInput = document.getElementById('x');
let yInput = document.getElementById('y');
let widthInput = document.getElementById('width');
let heightInput = document.getElementById('height');
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
fileInput.addEventListener('change', function(e) {
let file = e.target.files[0];
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
});
overlayCanvas.addEventListener('mousedown', function(e) {
// Clear previously drawn rectangle on the main canvas
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
renderPage(currentPage); // Re-render the PDF
// Clear the overlay canvas to ensure old drawings are removed
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = e.offsetX;
startY = e.offsetY;
isDrawing = true;
});
overlayCanvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return;
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
overlayContext.strokeStyle = 'red';
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
});
overlayCanvas.addEventListener('mouseup', function(e) {
isDrawing = false;
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
let flippedY = pdfCanvas.height - e.offsetY;
xInput.value = startX;
yInput.value = flippedY;
widthInput.value = rectWidth;
heightInput.value = rectHeight;
// Draw the final rectangle on the main canvas
context.strokeStyle = 'red';
context.strokeRect(startX, startY, rectWidth, rectHeight);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let viewport = page.getViewport({ scale: 1.0 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
</script>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}"> <div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}" data-tags="${tags}">
<a th:href="${cardLink}"> <a th:href="${cardLink}">
<div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title --> <div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title -->
<img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30"> <img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30">

View File

@@ -13,7 +13,7 @@
<div class="container "> <div class="container ">
<a class="navbar-brand" href="#" th:href="@{/}" > <a class="navbar-brand" href="#" th:href="@{/}" >
<img th:if="${@navBarText} == 'Stirling PDF'" class="main-icon" src="favicon.svg" alt="icon"> <img class="main-icon" src="favicon.svg" alt="icon">
<span class="icon-text" th:text="${@navBarText}"></span> <span class="icon-text" th:text="${@navBarText}"></span>
</a> </a>
@@ -40,20 +40,24 @@
</li>--> </li>-->
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon"> <img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
<span class="icon-text" th:text="#{navbar.pageOps}"></span> <span class="icon-text" th:text="#{navbar.pageOps}"></span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<!-- Existing menu items --> <!-- Existing menu items -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc', 'merge.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc', 'split.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'auto-split-pdf', 'images/layout-split.svg', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc', 'crop.tags')}"></div>
</div> </div>
</li> </li>
@@ -65,16 +69,19 @@
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<!-- Existing menu items --> <!-- Existing menu items -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'images/image.svg', 'home.imageToPdf.title', 'home.imageToPdf.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'images/image.svg', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'images/file.svg', 'home.fileToPDF.title', 'home.fileToPDF.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'images/file.svg', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'images/html.svg', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'images/url.svg', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags')}"></div>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'images/file-earmark-ppt.svg', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'images/file-earmark-ppt.svg', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'images/filetype-txt.svg', 'home.PDFToText.title', 'home.PDFToText.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'images/filetype-txt.svg', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'images/filetype-html.svg', 'home.PDFToHTML.title', 'home.PDFToHTML.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'images/filetype-html.svg', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'images/filetype-xml.svg', 'home.PDFToXML.title', 'home.PDFToXML.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'images/filetype-xml.svg', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'images/file-earmark-pdf.svg', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'images/file-earmark-pdf.svg', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags')}"></div>
@@ -89,33 +96,37 @@
<img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span> <img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div>
</div> </div>
</li> </li>
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{navbar.other}"></span> <span class="icon-text" th:text="#{navbar.other}"></span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc')}"></div> <!--<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags')}"></div> -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc', 'ocr.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'images/images.svg', 'home.extractImages.title', 'home.extractImages.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPdfs.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-metadata', 'images/clipboard-data.svg', 'home.changeMetadata.title', 'home.changeMetadata.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'images/images.svg', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-image-scans', 'images/scanner.svg', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-metadata', 'images/clipboard-data.svg', 'home.changeMetadata.title', 'home.changeMetadata.desc', 'changeMetadata.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sign', 'images/sign.svg', 'home.sign.title', 'home.sign.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-image-scans', 'images/scanner.svg', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc', 'ScannerImageSplit.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'images/flatten.svg', 'home.flatten.title', 'home.flatten.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('sign', 'images/sign.svg', 'home.sign.title', 'home.sign.desc', 'sign.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'images/flatten.svg', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-blanks', 'images/blank-file.svg', 'home.removeBlanks.title', 'home.removeBlanks.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc', 'repair.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-blanks', 'images/blank-file.svg', 'home.removeBlanks.title', 'home.removeBlanks.desc', 'removeBlanks.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc', 'compare.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-page-numbers', 'images/add-page-numbers.svg', 'home.add-page-numbers.title', 'home.add-page-numbers.desc', 'add-page-numbers.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags')}"></div>
</div> </div>
</li> </li>
@@ -203,6 +214,70 @@
</a> </a>
</li> </li>
<!-- Search Button and Search Bar -->
<li class="nav-item position-relative">
<a href="#" class="nav-link" id="search-icon">
<img class="navbar-icon" src="images/search.svg" alt="icon" width="24" height="24">
</a>
<!-- Search Bar -->
<div class="collapse position-absolute" id="navbarSearch">
<form class="d-flex p-2 bg-white border search-form" id="searchForm">
<input class="form-control search-input" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput">
</form>
<!-- Search Results -->
<div id="searchResults" class="border p-2 bg-white search-results"></div>
</div>
</li>
<style>
#search-icon i {
font-size: 24px; /* Adjust this to your desired size */
transition: color 0.3s;
}
#search-icon:hover i {
color: #666; /* Adjust this to your hover color */
}
#navbarSearch {
transition: all 0.3s;
max-height: 0;
overflow: hidden;
}
#navbarSearch.show {
max-height: 300px; /* Adjust this to your desired max height */
}
.search-input {
transition: border 0.3s, box-shadow 0.3s;
}
.search-input:focus {
border-color: #666; /* Adjust this to your focus color */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adjust this to your desired shadow */
}
#searchResults {
max-width: 300px; /* Adjust to your preferred width */
transition: height 0.3s ease; /* Smooth height transition */
}
/* Set a fixed height and styling for each search result item */
.search-results a {
display: flex;
align-items: center;
gap: 10px; /* space between icon and text */
height: 40px; /* Adjust based on your design */
overflow: hidden; /* Prevent content from overflowing */
white-space: nowrap; /* Prevent text from wrapping to next line */
text-overflow: ellipsis; /* Truncate text if it's too long */
}
</style>
</ul> </ul>
@@ -210,6 +285,7 @@
</div> </div>
<script src="js/favourites.js"></script> <script src="js/favourites.js"></script>
<script src="js/search.js"></script>
</nav> </nav>
<div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div> <div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div>

View File

@@ -1,6 +1,6 @@
<div th:fragment="navbarEntry (endpoint, imgSrc, titleKey, descKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}"> <div th:fragment="navbarEntry (endpoint, imgSrc, titleKey, descKey, tagKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
<a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}"> <a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}" th:data-tags="#{${tagKey}}">
<img class="icon" th:src="@{${imgSrc}}" alt="icon"> <img class="icon" th:src="@{${imgSrc}}" alt="icon">
<span class="icon-text" th:text="#{${titleKey}}"></span> <span class="icon-text" th:text="#{${titleKey}}"></span>
</a> </a>
</div> </div>

View File

@@ -20,15 +20,24 @@
</div> </div>
<br class="d-md-none"> <br class="d-md-none">
<!-- Features --> <!-- Features -->
<div class="features-container container"> <script src="js/homecard.js"></script>
<!-- <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div>--> <div class=" container">
<input type="text" id="searchBar" onkeyup="filterCards()" placeholder="Search for features...">
<div class="features-container ">
<!-- <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div> -->
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div> <div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div> <div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg')}"></div> <div th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg')}"></div> <div th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg')}"></div>
@@ -67,12 +76,17 @@
<div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div> <div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', svgPath='images/scale-pages.svg')}"></div> <div th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', svgPath='images/scale-pages.svg')}"></div>
<script src="js/homecard.js"></script> <div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', svgPath='images/sanitize.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', svgPath='images/url.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', svgPath='images/html.svg')}"></div>
</div> </div>
</div> </div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>
</body> </body>

View File

@@ -1,140 +1,141 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head> <head>
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{addImage.title})}"></th:block>
<script src="js/thirdParty/interact.min.js"></script> <script src="js/thirdParty/interact.min.js"></script>
</head> </head>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{addImage.header}"></h2> <h2 th:text="#{addImage.header}"></h2>
<!-- pdf selector --> <!-- pdf selector -->
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
<script> <script>
let originalFileName = ''; let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => { document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, ""); originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer(); const pdfData = await file.arrayBuffer();
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
await DraggableUtils.renderPage(pdfDoc, 0); const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = ''; document.querySelectorAll(".show-on-file-selected").forEach(el => {
}) el.style.cssText = '';
} })
}); }
document.addEventListener("DOMContentLoaded", () => { });
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.addEventListener("DOMContentLoaded", () => {
el.style.cssText = "display:none !important"; document.querySelectorAll(".show-on-file-selected").forEach(el => {
}) el.style.cssText = "display:none !important";
}); })
</script> });
</script>
<div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{addImage.upload}"> <div class="tab-group show-on-file-selected">
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=true, accept='image/*', inputText=#{imgPrompt})}"></div> <div class="tab-container" th:title="#{addImage.upload}">
<script> <div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=true, accept='image/*', inputText=#{imgPrompt})}"></div>
const imageUpload = document.querySelector('input[name=image-upload]'); <script>
imageUpload.addEventListener('change', e => { const imageUpload = document.querySelector('input[name=image-upload]');
if(!e.target.files) { imageUpload.addEventListener('change', e => {
return; if(!e.target.files) {
} return;
for (const imageFile of e.target.files) { }
var reader = new FileReader(); for (const imageFile of e.target.files) {
reader.readAsDataURL(imageFile); var reader = new FileReader();
reader.onloadend = function (e) { reader.readAsDataURL(imageFile);
DraggableUtils.createDraggableCanvasFromUrl(e.target.result); reader.onloadend = function (e) {
}; DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
} };
}); }
</script> });
</div> </script>
</div> </div>
</div>
<!-- draggables box -->
<div id="box-drag-container" class="show-on-file-selected"> <!-- draggables box -->
<canvas id="pdf-canvas"></canvas> <div id="box-drag-container" class="show-on-file-selected">
<script src="js/draggable-utils.js"></script> <canvas id="pdf-canvas"></canvas>
<div class="draggable-buttons-box ignore-rtl"> <script src="js/draggable-utils.js"></script>
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"> <div class="draggable-buttons-box ignore-rtl">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> <button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
<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"/> <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="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"/> <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"/>
</svg> <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"/>
</button> </svg>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto"> </button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16"> <button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
</svg> <path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</button> </svg>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()"> </button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16"> <button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
</svg> <path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</button> </svg>
</div> </button>
<style> </div>
#box-drag-container { <style>
position: relative; #box-drag-container {
margin: 20px 0; position: relative;
} margin: 20px 0;
#pdf-canvas { }
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384); #pdf-canvas {
width: 100%; box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
} width: 100%;
.draggable-buttons-box { }
position: absolute; .draggable-buttons-box {
top: 0; position: absolute;
padding: 10px; top: 0;
width: 100%; padding: 10px;
display: flex; width: 100%;
gap: 5px; display: flex;
} gap: 5px;
.draggable-buttons-box > button { }
z-index: 10; .draggable-buttons-box > button {
background-color: rgba(13, 110, 253, 0.1); z-index: 10;
} background-color: rgba(13, 110, 253, 0.1);
.draggable-canvas { }
border: 1px solid red; .draggable-canvas {
position: absolute; border: 1px solid red;
touch-action: none; position: absolute;
user-select: none; touch-action: none;
top: 0px; user-select: none;
left: 0; top: 0px;
} left: 0;
</style> }
</div> </style>
</div>
<!-- download button -->
<div class="margin-auto-parent"> <!-- download button -->
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button> <div class="margin-auto-parent">
</div> <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
<script> </div>
document.getElementById("download-pdf").addEventListener('click', async() => { <script>
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument(); document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdfBytes = await modifiedPdf.save(); const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' }); const modifiedPdfBytes = await modifiedPdf.save();
const link = document.createElement('a'); const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
link.href = URL.createObjectURL(blob); const link = document.createElement('a');
link.download = originalFileName + '_addedImage.pdf'; link.href = URL.createObjectURL(blob);
link.click(); link.download = originalFileName + '_addedImage.pdf';
}); link.click();
</script> });
</div> </script>
</div> </div>
</div> </div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
</div> <div th:insert="~{fragments/footer.html :: footer}"></div>
</body> </div>
</body>
</html> </html>

View File

@@ -0,0 +1,154 @@
<!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=#{autoCrop.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{addPageNumbers.header}"></h2>
<form method="post" enctype="multipart/form-data"
th:action="@{add-page-numbers}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br>
<div class="form-group">
<label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label> <select
class="form-control" id="customMargin" name="customMargin"
required>
<option value="small" th:text="#{sizes.small}"></option>
<option value="medium" selected th:text="#{sizes.medium}"></option>
<option value="large" th:text="#{sizes.large}"></option>
<option value="x-large" th:text="#{sizes.x-large}"></option>
</select>
</div>
<style>
.a4container {
position: relative;
width: 50%;
aspect-ratio: 0.707;
border: 1px solid #ddd;
box-sizing: border-box;
background-color: white;
}
.pageNumber {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
font-size: 1em;
color: #333;
cursor: pointer;
background-color: #ccc;
width: 15%;
height: 15%;
transform: translate(-50%, -50%);
}
.pageNumber:hover {
background-color: #eee;
}
#myForm {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.selectedPosition {
background-color: #0a0;
}
.selectedPosition.selectedHovered {
background-color: #006600;
}
</style>
<div class="form-group">
<label for="position" th:text="#{addPageNumbers.selectText.3}"></label>
<div class="a4container">
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div>
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div>
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div>
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div>
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div>
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div>
<div class="pageNumber" id="8" style="top: 90%; left: 50%;">8</div>
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
</div>
</div>
<input type="hidden" id="numberInput" name="position" min="1"
max="9" required>
<div class="form-group">
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label> <input
type="number" class="form-control" id="startingNumber"
name="startingNumber" min="1" required value="1" />
</div>
<div class="form-group">
<label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label> <input
type="text" class="form-control" id="pagesToNumber"
name="pagesToNumber"
placeholder="Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc" />
</div>
<div class="form-group">
<label for="customText" th:text="#{addPageNumbers.selectText.6}"></label> <input type="text"
class="form-control" id="customText" name="customText"
placeholder="Default just number, also accepts 'Page {n} of {total}', 'Tag-{n}' etc" />
</div>
<button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{addPageNumbers.submit}"></button>
</form>
</div>
</div>
</div>
<script>
let cells = document.querySelectorAll('.pageNumber');
let inputField = document.getElementById('numberInput');
cells.forEach(cell => {
cell.addEventListener('click', function(e) {
cells.forEach(cell => {
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
});
let selectedLocation = e.target.id;
inputField.value = selectedLocation;
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
e.target.classList.add('selectedHovered'); // Add selectedHovered class
});
cell.addEventListener('mouseenter', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.add('selectedHovered');
}
});
cell.addEventListener('mouseleave', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.remove('selectedHovered');
}
});
});
</script>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

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

View File

@@ -0,0 +1,30 @@
<!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=#{auto-rename.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{auto-rename.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{auto-rename}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{auto-rename.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -1,263 +1,263 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{changeMetadata.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{changeMetadata.title})}"></th:block>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{changeMetadata.header}"></h2> <h2 th:text="#{changeMetadata.header}"></h2>
<form method="post" id="form1" enctype="multipart/form-data" th:action="@{/update-metadata}"> <form method="post" id="form1" enctype="multipart/form-data" th:action="@{/update-metadata}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p> <p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p>
<div class="form-group-inline form-check"> <div class="form-group-inline form-check">
<input type="checkbox" class="form-check-input" id="deleteAll" name="deleteAll"> <input type="checkbox" class="form-check-input" id="deleteAll" name="deleteAll">
<label class="ml-3" for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label> <label class="ml-3" for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
</div> </div>
<div class="form-group-inline form-check"> <div class="form-group-inline form-check">
<input type="checkbox" class="form-check-input" id="customModeCheckbox"> <input type="checkbox" class="form-check-input" id="customModeCheckbox">
<label class="ml-3" for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label> <label class="ml-3" for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label> <label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label>
<input type="text" class="form-control" id="author" name="author"> <input type="text" class="form-control" id="author" name="author">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label> <label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label>
<input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59"> <input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label> <label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label>
<input type="text" class="form-control" id="creator" name="creator"> <input type="text" class="form-control" id="creator" name="creator">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label> <label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label>
<input type="text" class="form-control" id="keywords" name="keywords"> <input type="text" class="form-control" id="keywords" name="keywords">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label> <label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label>
<input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59"> <input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label> <label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label>
<input type="text" class="form-control" id="producer" name="producer"> <input type="text" class="form-control" id="producer" name="producer">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label> <label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label>
<input type="text" class="form-control" id="subject" name="subject"> <input type="text" class="form-control" id="subject" name="subject">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label> <label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label>
<input type="text" class="form-control" id="title" name="title"> <input type="text" class="form-control" id="title" name="title">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label> <label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label>
<select class="form-control" id="trapped" name="trapped"> <select class="form-control" id="trapped" name="trapped">
<option value="True" th:text="#{true}"></option> <option value="True" th:text="#{true}"></option>
<option value="False" th:text="#{false}" selected></option> <option value="False" th:text="#{false}" selected></option>
<option value="Unknown" th:text="#{unknown}"></option> <option value="Unknown" th:text="#{unknown}"></option>
</select> </select>
</div> </div>
<div id="customMetadata" style="display: none;"> <div id="customMetadata" style="display: none;">
<h3 th:text="#{changeMetadata.selectText.4}"></h3> <h3 th:text="#{changeMetadata.selectText.4}"></h3>
<div class="form-group" id="otherMetadataEntries"></div> <div class="form-group" id="otherMetadataEntries"></div>
</div> </div>
<div id="customMetadataEntries"></div> <div id="customMetadataEntries"></div>
<button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button> <button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button>
<br> <br>
<br> <br>
<button class="btn btn-primary" type="submit" id="submitBtn" th:text="#{changeMetadata.submit}"></button> <button class="btn btn-primary" type="submit" id="submitBtn" th:text="#{changeMetadata.submit}"></button>
<script> <script>
const deleteAllCheckbox = document.querySelector("#deleteAll"); const deleteAllCheckbox = document.querySelector("#deleteAll");
const inputs = document.querySelectorAll(".form-control"); const inputs = document.querySelectorAll(".form-control");
const customMetadataDiv = document.getElementById('customMetadata'); const customMetadataDiv = document.getElementById('customMetadata');
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries'); const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
deleteAllCheckbox.addEventListener("change", function(event) { deleteAllCheckbox.addEventListener("change", function(event) {
if (event.target !== deleteAllCheckbox) { if (event.target !== deleteAllCheckbox) {
return; return;
} }
inputs.forEach(input => { inputs.forEach(input => {
if (input === deleteAllCheckbox) { if (input === deleteAllCheckbox) {
return; return;
} }
input.disabled = deleteAllCheckbox.checked; input.disabled = deleteAllCheckbox.checked;
}); });
}); });
const customModeCheckbox = document.getElementById('customModeCheckbox'); const customModeCheckbox = document.getElementById('customModeCheckbox');
const addMetadataBtn = document.getElementById("addMetadataBtn"); const addMetadataBtn = document.getElementById("addMetadataBtn");
const customMetadataFormContainer = document.getElementById("customMetadataEntries"); const customMetadataFormContainer = document.getElementById("customMetadataEntries");
var count = 1; var count = 1;
const fileInput = document.querySelector("#fileInput-input"); const fileInput = document.querySelector("#fileInput-input");
const authorInput = document.querySelector("#author"); const authorInput = document.querySelector("#author");
const creationDateInput = document.querySelector("#creationDate"); const creationDateInput = document.querySelector("#creationDate");
const creatorInput = document.querySelector("#creator"); const creatorInput = document.querySelector("#creator");
const keywordsInput = document.querySelector("#keywords"); const keywordsInput = document.querySelector("#keywords");
const modificationDateInput = document.querySelector("#modificationDate"); const modificationDateInput = document.querySelector("#modificationDate");
const producerInput = document.querySelector("#producer"); const producerInput = document.querySelector("#producer");
const subjectInput = document.querySelector("#subject"); const subjectInput = document.querySelector("#subject");
const titleInput = document.querySelector("#title"); const titleInput = document.querySelector("#title");
const trappedInput = document.querySelector("#trapped"); const trappedInput = document.querySelector("#trapped");
var lastPDFFileMeta = null; var lastPDFFileMeta = null;
fileInput.addEventListener("change", async function() { fileInput.addEventListener("change", async function() {
while (otherMetadataEntriesDiv.firstChild) { while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild); otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
} }
while (customMetadataFormContainer.firstChild) { while (customMetadataFormContainer.firstChild) {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild); customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
} }
const file = this.files[0]; const file = this.files[0];
var url = URL.createObjectURL(file) var url = URL.createObjectURL(file)
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const pdf = await pdfjsLib.getDocument(url).promise; const pdf = await pdfjsLib.getDocument(url).promise;
const pdfMetadata = await pdf.getMetadata(); const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info lastPDFFile = pdfMetadata?.info
console.log(pdfMetadata); console.log(pdfMetadata);
if(!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) { if(!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
customModeCheckbox.disabled = true; customModeCheckbox.disabled = true;
customModeCheckbox.checked = false; customModeCheckbox.checked = false;
} else { } else {
customModeCheckbox.disabled = false; customModeCheckbox.disabled = false;
} }
authorInput.value = pdfMetadata?.info?.Author; authorInput.value = pdfMetadata?.info?.Author;
creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate); creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
creatorInput.value = pdfMetadata?.info?.Creator; creatorInput.value = pdfMetadata?.info?.Creator;
keywordsInput.value = pdfMetadata?.info?.Keywords; keywordsInput.value = pdfMetadata?.info?.Keywords;
modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate); modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
producerInput.value = pdfMetadata?.info?.Producer; producerInput.value = pdfMetadata?.info?.Producer;
subjectInput.value = pdfMetadata?.info?.Subject; subjectInput.value = pdfMetadata?.info?.Subject;
titleInput.value = pdfMetadata?.info?.Title; titleInput.value = pdfMetadata?.info?.Title;
console.log(pdfMetadata?.info); console.log(pdfMetadata?.info);
const trappedValue = pdfMetadata?.info?.Trapped; const trappedValue = pdfMetadata?.info?.Trapped;
// Get all options in the select element // Get all options in the select element
const options = trappedInput.options; const options = trappedInput.options;
// Loop through all options to find the one with a matching value // Loop through all options to find the one with a matching value
for (let i = 0; i < options.length; i++) { for (let i = 0; i < options.length; i++) {
if (options[i].value === trappedValue) { if (options[i].value === trappedValue) {
options[i].selected = true; options[i].selected = true;
break; break;
} }
} }
addExtra(); addExtra();
}); });
addMetadataBtn.addEventListener("click", () => { addMetadataBtn.addEventListener("click", () => {
const keyInput = document.createElement("input"); const keyInput = document.createElement("input");
keyInput.type = "text"; keyInput.type = "text";
keyInput.placeholder = 'Key'; keyInput.placeholder = 'Key';
keyInput.className = "form-control"; keyInput.className = "form-control";
keyInput.name = "customKey" + count; keyInput.name = "customKey" + count;
const valueInput = document.createElement("input"); const valueInput = document.createElement("input");
valueInput.type = "text"; valueInput.type = "text";
valueInput.placeholder = 'Value'; valueInput.placeholder = 'Value';
valueInput.className = "form-control"; valueInput.className = "form-control";
valueInput.name = "customValue" + count; valueInput.name = "customValue" + count;
count = count + 1; count = count + 1;
const formGroup = document.createElement("div"); const formGroup = document.createElement("div");
formGroup.className = "form-group"; formGroup.className = "form-group";
formGroup.appendChild(keyInput); formGroup.appendChild(keyInput);
formGroup.appendChild(valueInput); formGroup.appendChild(valueInput);
customMetadataFormContainer.appendChild(formGroup); customMetadataFormContainer.appendChild(formGroup);
}); });
function convertDateFormat(dateTimeString) { function convertDateFormat(dateTimeString) {
if (!dateTimeString || dateTimeString.length < 17) { if (!dateTimeString || dateTimeString.length < 17) {
return dateTimeString; return dateTimeString;
} }
const year = dateTimeString.substring(2, 6); const year = dateTimeString.substring(2, 6);
const month = dateTimeString.substring(6, 8); const month = dateTimeString.substring(6, 8);
const day = dateTimeString.substring(8, 10); const day = dateTimeString.substring(8, 10);
const hour = dateTimeString.substring(10, 12); const hour = dateTimeString.substring(10, 12);
const minute = dateTimeString.substring(12, 14); const minute = dateTimeString.substring(12, 14);
const second = dateTimeString.substring(14, 16); const second = dateTimeString.substring(14, 16);
return year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second; return year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second;
} }
function addExtra() { function addExtra() {
const event = document.getElementById("customModeCheckbox"); const event = document.getElementById("customModeCheckbox");
if (event.checked && lastPDFFile.Custom != null) { if (event.checked && lastPDFFile.Custom != null) {
customMetadataDiv.style.display = 'block'; customMetadataDiv.style.display = 'block';
for (const [key, value] of Object.entries(lastPDFFile.Custom)) { for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
if (key === 'Author' || key === 'CreationDate' || key === 'Creator' || key === 'Keywords' || key === 'ModDate' || key === 'Producer' || key === 'Subject' || key === 'Title' || key === 'Trapped') { if (key === 'Author' || key === 'CreationDate' || key === 'Creator' || key === 'Keywords' || key === 'ModDate' || key === 'Producer' || key === 'Subject' || key === 'Title' || key === 'Trapped') {
continue; continue;
} }
const entryDiv = document.createElement('div'); const entryDiv = document.createElement('div');
entryDiv.className = 'form-group'; entryDiv.className = 'form-group';
entryDiv.innerHTML = `<div class="form-group"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`; entryDiv.innerHTML = `<div class="form-group"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
otherMetadataEntriesDiv.appendChild(entryDiv); otherMetadataEntriesDiv.appendChild(entryDiv);
} }
} else { } else {
customMetadataDiv.style.display = 'none'; customMetadataDiv.style.display = 'none';
while (otherMetadataEntriesDiv.firstChild) { while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild); otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
} }
} }
} }
customModeCheckbox.addEventListener('change', (event) => { customModeCheckbox.addEventListener('change', (event) => {
addExtra(); addExtra();
}); });
</script> </script>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,190 +1,190 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{compare.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{compare.title})}"></th:block>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-9"> <div class="col-md-9">
<h2 th:text="#{compare.header}"></h2> <h2 th:text="#{compare.header}"></h2>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput2', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput2', multiple=false, accept='application/pdf')}"></div>
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button> <button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h3 th:text="#{compare.document.1}"></h3> <h3 th:text="#{compare.document.1}"></h3>
<div id="result1" class="result-column"></div> <div id="result1" class="result-column"></div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h3 th:text="#{compare.document.2}"></h3> <h3 th:text="#{compare.document.2}"></h3>
<div id="result2" class="result-column"></div> <div id="result2" class="result-column"></div>
</div> </div>
</div> </div>
<style> <style>
.result-column { .result-column {
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 15px; padding: 15px;
overflow-y: auto; overflow-y: auto;
height: calc(100vh - 400px); height: calc(100vh - 400px);
white-space: pre-wrap; white-space: pre-wrap;
} }
</style> </style>
<script> <script>
// get the elements // get the elements
var result1 = document.getElementById('result1'); var result1 = document.getElementById('result1');
var result2 = document.getElementById('result2'); var result2 = document.getElementById('result2');
// add event listeners // add event listeners
result1.addEventListener('scroll', function() { result1.addEventListener('scroll', function() {
result2.scrollTop = result1.scrollTop; result2.scrollTop = result1.scrollTop;
}); });
result2.addEventListener('scroll', function() { result2.addEventListener('scroll', function() {
result1.scrollTop = result2.scrollTop; result1.scrollTop = result2.scrollTop;
}); });
async function comparePDFs() { async function comparePDFs() {
const file1 = document.getElementById("fileInput-input").files[0]; const file1 = document.getElementById("fileInput-input").files[0];
const file2 = document.getElementById("fileInput2-input").files[0]; const file2 = document.getElementById("fileInput2-input").files[0];
if (!file1 || !file2) { if (!file1 || !file2) {
console.error("Please select two PDF files to compare"); console.error("Please select two PDF files to compare");
return; return;
} }
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const [pdf1, pdf2] = await Promise.all([ const [pdf1, pdf2] = await Promise.all([
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise, pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
]); ]);
const extractText = async (pdf) => { const extractText = async (pdf) => {
const pages = []; const pages = [];
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i); const page = await pdf.getPage(i);
const content = await page.getTextContent(); const content = await page.getTextContent();
const strings = content.items.map(item => item.str); const strings = content.items.map(item => item.str);
pages.push(strings.join(" ")); pages.push(strings.join(" "));
} }
return pages.join(" "); return pages.join(" ");
}; };
const [text1, text2] = await Promise.all([ const [text1, text2] = await Promise.all([
extractText(pdf1), extractText(pdf1),
extractText(pdf2) extractText(pdf2)
]); ]);
if (text1.trim() === "" || text2.trim() === "") { if (text1.trim() === "" || text2.trim() === "") {
alert("One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison."); alert("One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.");
return; return;
} }
const diff = (text1, text2) => { const diff = (text1, text2) => {
const words1 = text1.split(' '); const words1 = text1.split(' ');
const words2 = text2.split(' '); const words2 = text2.split(' ');
// Create a 2D array to hold our "matrix" // Create a 2D array to hold our "matrix"
const matrix = Array(words1.length + 1).fill(null).map(() => Array(words2.length + 1).fill(0)); const matrix = Array(words1.length + 1).fill(null).map(() => Array(words2.length + 1).fill(0));
// Perform standard LCS algorithm // Perform standard LCS algorithm
for (let i = 1; i <= words1.length; i++) { for (let i = 1; i <= words1.length; i++) {
for (let j = 1; j <= words2.length; j++) { for (let j = 1; j <= words2.length; j++) {
if (words1[i - 1] === words2[j - 1]) { if (words1[i - 1] === words2[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1] + 1; matrix[i][j] = matrix[i - 1][j - 1] + 1;
} else { } else {
matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]); matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]);
} }
} }
} }
let i = words1.length; let i = words1.length;
let j = words2.length; let j = words2.length;
const differences = []; const differences = [];
// Backtrack through the matrix to create the diff // Backtrack through the matrix to create the diff
while (i > 0 || j > 0) { while (i > 0 || j > 0) {
if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) { if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) {
differences.unshift(['black', words1[i - 1]]); differences.unshift(['black', words1[i - 1]]);
i--; i--;
j--; j--;
} else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
differences.unshift(['green', words2[j - 1]]); differences.unshift(['green', words2[j - 1]]);
j--; j--;
} else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) { } else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
differences.unshift(['red', words1[i - 1]]); differences.unshift(['red', words1[i - 1]]);
i--; i--;
} }
} }
return differences; return differences;
}; };
const differences = diff(text1, text2); const differences = diff(text1, text2);
const displayDifferences = (differences) => { const displayDifferences = (differences) => {
const resultDiv1 = document.getElementById("result1"); const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2"); const resultDiv2 = document.getElementById("result2");
resultDiv1.innerHTML = ""; resultDiv1.innerHTML = "";
resultDiv2.innerHTML = ""; resultDiv2.innerHTML = "";
differences.forEach(([color, word]) => { differences.forEach(([color, word]) => {
const span1 = document.createElement("span"); const span1 = document.createElement("span");
const span2 = document.createElement("span"); const span2 = document.createElement("span");
// If it's an addition, show it in green in the second document and transparent in the first // If it's an addition, show it in green in the second document and transparent in the first
if (color === "green") { if (color === "green") {
span1.style.color = "transparent"; span1.style.color = "transparent";
span1.style.userSelect = "none"; span1.style.userSelect = "none";
span2.style.color = color; span2.style.color = color;
} }
// If it's a deletion, show it in red in the first document and transparent in the second // If it's a deletion, show it in red in the first document and transparent in the second
else if (color === "red") { else if (color === "red") {
span1.style.color = color; span1.style.color = color;
span2.style.color = "transparent"; span2.style.color = "transparent";
span2.style.userSelect = "none"; span2.style.userSelect = "none";
} }
// If it's unchanged, show it in black in both // If it's unchanged, show it in black in both
else { else {
span1.style.color = color; span1.style.color = color;
span2.style.color = color; span2.style.color = color;
} }
span1.textContent = word; span1.textContent = word;
span2.textContent = word; span2.textContent = word;
resultDiv1.appendChild(span1); resultDiv1.appendChild(span1);
resultDiv2.appendChild(span2); resultDiv2.appendChild(span2);
// Add space after each word, or a new line if the word ends with a full stop // Add space after each word, or a new line if the word ends with a full stop
const spaceOrNewline1 = document.createElement("span"); const spaceOrNewline1 = document.createElement("span");
const spaceOrNewline2 = document.createElement("span"); const spaceOrNewline2 = document.createElement("span");
if (word.endsWith(".")) { if (word.endsWith(".")) {
spaceOrNewline1.innerHTML = "<br>"; spaceOrNewline1.innerHTML = "<br>";
spaceOrNewline2.innerHTML = "<br>"; spaceOrNewline2.innerHTML = "<br>";
} else { } else {
spaceOrNewline1.textContent = " "; spaceOrNewline1.textContent = " ";
spaceOrNewline2.textContent = " "; spaceOrNewline2.textContent = " ";
} }
resultDiv1.appendChild(spaceOrNewline1); resultDiv1.appendChild(spaceOrNewline1);
resultDiv2.appendChild(spaceOrNewline2); resultDiv2.appendChild(spaceOrNewline2);
}); });
}; };
console.log('Differences:', differences); console.log('Differences:', differences);
displayDifferences(differences); displayDifferences(differences);
} }
</script> </script>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>

View File

@@ -1,111 +1,126 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" <html th:lang="${#locale.toString()}"
th:lang-direction="#{language.direction}" th:lang-direction="#{language.direction}"
xmlns:th="http://www.thymeleaf.org"> xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{pipeline.title})}"></th:block> <th:block
<body> th:insert="~{fragments/common :: head(title=#{pipeline.title})}"></th:block>
<div id="page-container"> <style>
<div id="content-wrap"> .btn-margin {
<div th:insert="~{fragments/navbar.html :: navbar}"></div> margin-right: 2px;
<br> <br> }
<div class="container" id="dropContainer">
<div class="row justify-content-center"> .bordered-box {
<div class="col-md-6"> border: 1px solid #ddd;
padding: 20px;
<div class="mb-3"> margin: 20px;
<button id="savePipelineBtn" class="btn btn-success">Download</button> width: 70%;
}
<button id="validateButton" class="btn btn-success">Validate</button>
<div class="btn-group"> .center-element {
<button id="uploadPipelineBtn" class="btn btn-primary">Upload</button> width: 80%;
<input type="file" id="uploadPipelineInput" accept=".json" text-align: center;
style="display: none;"> margin: auto;
</div> }
</div> .element-margin {
margin: 10px 0; /* Adjust this value to increase/decrease the margin as needed */
<div id="pipelineContainer" class="card"> }
</style>
<!-- Pipeline Configuration Card Header -->
<div class="card-header"> <body>
<h2 class="card-title">Pipeline Configuration</h2> <div id="page-container">
</div> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<!-- Pipeline Configuration Body -->
<div class="card-body"> <br> <br>
<div class="mb-3"> <div class="container">
<select id="operationsDropdown" class="form-select"> <div class="row justify-content-center">
<!-- Options will be dynamically populated here -->
</select> <div class="bordered-box">
</div> <div class="text-right text-top">
<div class="mb-3"> <button id="uploadPipelineBtn" class="btn btn-primary">Upload
<button id="addOperationBtn" class="btn btn-primary">Add operation</button> Custom</button>
</div> <button type="button" class="btn btn-primary" data-toggle="modal"
<h3>Pipeline:</h3> data-target="#pipelineSettingsModal">Configure</button>
<ol id="pipelineList" class="list-group"> </div>
<!-- Pipeline operations will be dynamically populated here -->
</ol> <div class="center-element">
</div> <div class="element-margin">
<select id="pipelineSelect" class="custom-select">
<input type="file" id="fileInput" multiple> <option value="">Select a pipeline</option>
<th:block th:each="config : ${pipelineConfigsWithNames}">
<button class="btn btn-primary" id="submitConfigBtn">Submit</button> <option th:value="${config.json}" th:text="${config.name}"></option>
</th:block>
</select>
</div> </div>
<div class="element-margin">
<!-- pipelineSettings modal --> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=true)}"></div>
<div id="pipelineSettingsModal" class="modal"> </div>
<div class="modal-content"> <div class="element-margin">
<div class="modal-body"> <button class="btn btn-primary" id="submitConfigBtn">Submit</button>
<span class="close">&times;</span> </div>
<h2>Operation Settings</h2> </div>
<div id="pipelineSettingsContent"> </div>
<!-- pipelineSettings will be dynamically populated here -->
</div>
</div> <!-- The Modal -->
</div> <div class="modal" id="pipelineSettingsModal">
<script src="js/pipeline.js"></script> <div class="modal-dialog">
</div> <div class="modal-content">
</div>
</div> <!-- Modal Header -->
</div> <div class="modal-header">
</div> <h2 class="modal-title">Pipeline Configuration</h2>
<style> <button type="button" class="close" data-dismiss="modal">&times;</button>
.modal { </div>
display: none; /* Hidden by default */
position: fixed; /* Stay in place */ <!-- Modal body -->
z-index: 1; /* Sit on top */ <div class="modal-body">
padding-top: 100px; /* Location of the box */ <div class="mb-3">
left: 0; <label for="pipelineName" class="form-label">Pipeline
top: 0; Name</label> <input type="text" id="pipelineName"
width: 100%; /* Full width */ class="form-control" placeholder="Enter pipeline name here">
height: 100%; /* Full height */ </div>
overflow: auto; /* Enable scroll if needed */ <div class="mb-3">
background-color: rgb(0, 0, 0); /* Fallback color */ <select id="operationsDropdown" class="form-select">
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ <!-- Options will be dynamically populated here -->
} </select>
</div>
/* Modal Content */ <div class="mb-3">
.modal-content { <button id="addOperationBtn" class="btn btn-primary">Add
background-color: #fefefe; operation</button>
margin: auto; </div>
padding: 20px; <h3>Pipeline:</h3>
border: 1px solid #888; <ol id="pipelineList" class="list-group">
width: 50%; <!-- Pipeline operations will be dynamically populated here -->
} </ol>
<div id="pipelineSettingsContent">
.btn-margin { <!-- pipelineSettings will be dynamically populated here -->
margin-right: 2px; </div>
} </div>
.modal-body { <!-- Modal footer -->
display: flex; <div class="modal-footer">
flex-direction: column; <button id="savePipelineBtn" class="btn btn-success">Download</button>
} <button id="validateButton" class="btn btn-success">Validate</button>
</style> <div class="btn-group">
<div th:insert="~{fragments/footer.html :: footer}"></div> <input type="file" id="uploadPipelineInput" accept=".json"
</div> style="display: none;">
</div>
</body> </div>
</div>
</div>
</div>
<script src="js/pipeline.js"></script>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html> </html>

View File

@@ -1,132 +1,132 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{rotate.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{rotate.title})}"></th:block>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{rotate.header}"></h2> <h2 th:text="#{rotate.header}"></h2>
<form action="#" th:action="@{rotate-pdf}" th:object="${rotateForm}" method="post" enctype="multipart/form-data"> <form action="#" th:action="@{rotate-pdf}" th:object="${rotateForm}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<input type="hidden" id="angleInput" name="angle" value="0"> <input type="hidden" id="angleInput" name="angle" value="0">
<div id="editSection" style="display: none"> <div id="editSection" style="display: none">
<div class="previewContainer"> <div class="previewContainer">
<img id="pdf-preview" /> <img id="pdf-preview" />
</div> </div>
<div class="buttonContainer"> <div class="buttonContainer">
<button type="button" class="btn btn-secondary" onclick="rotate(-90)"> <button type="button" class="btn btn-secondary" onclick="rotate(-90)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16"> <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 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" /> <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> </svg>
</button> </button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{rotate.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{rotate.submit}"></button>
<button type="button" class="btn btn-secondary" onclick="rotate(90)"> <button type="button" class="btn btn-secondary" onclick="rotate(90)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> <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 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" /> <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> </svg>
</button> </button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <div th:insert="~{fragments/footer.html :: footer}"></div>
</div> </div>
<script> <script>
const angleInput = document.getElementById("angleInput"); const angleInput = document.getElementById("angleInput");
const fileInput = document.getElementById("fileInput-input"); const fileInput = document.getElementById("fileInput-input");
const preview = document.getElementById("pdf-preview"); const preview = document.getElementById("pdf-preview");
fileInput.addEventListener("change", async function() { fileInput.addEventListener("change", async function() {
console.log("loading pdf"); console.log("loading pdf");
document.querySelector("#editSection").style.display = ""; document.querySelector("#editSection").style.display = "";
var url = URL.createObjectURL(fileInput.files[0]) var url = URL.createObjectURL(fileInput.files[0])
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const pdf = await pdfjsLib.getDocument(url).promise; const pdf = await pdfjsLib.getDocument(url).promise;
const page = await pdf.getPage(1); const page = await pdf.getPage(1);
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
// set the canvas size to the size of the page // set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) { if (page.rotate == 90 || page.rotate == 270) {
canvas.width = page.view[3]; canvas.width = page.view[3];
canvas.height = page.view[2]; canvas.height = page.view[2];
} else { } else {
canvas.width = page.view[2]; canvas.width = page.view[2];
canvas.height = page.view[3]; canvas.height = page.view[3];
} }
// render the page onto the canvas // render the page onto the canvas
var renderContext = { var renderContext = {
canvasContext: canvas.getContext("2d"), canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 }) viewport: page.getViewport({ scale: 1 })
}; };
await page.render(renderContext).promise; await page.render(renderContext).promise;
preview.src = canvas.toDataURL(); preview.src = canvas.toDataURL();
}); });
function rotate(deg) { function rotate(deg) {
var lastTransform = preview.style.rotate; var lastTransform = preview.style.rotate;
if (!lastTransform) { if (!lastTransform) {
lastTransform = "0"; lastTransform = "0";
} }
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, '')); const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ''));
const newAngle = lastAngle + deg; const newAngle = lastAngle + deg;
preview.style.rotate = newAngle + "deg"; preview.style.rotate = newAngle + "deg";
angleInput.value = newAngle; angleInput.value = newAngle;
} }
</script> </script>
<style> <style>
#pdf-preview { #pdf-preview {
margin: 0 auto; margin: 0 auto;
display: block; display: block;
max-width: calc(100% - 30px); max-width: calc(100% - 30px);
max-height: calc(100% - 30px); max-height: calc(100% - 30px);
box-shadow: 0 0 4px rgba(100, 100, 100, .25); box-shadow: 0 0 4px rgba(100, 100, 100, .25);
transition: rotate .3s; transition: rotate .3s;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
translate: -50% -50%; translate: -50% -50%;
} }
.previewContainer { .previewContainer {
aspect-ratio: 1; aspect-ratio: 1;
width: 100%; width: 100%;
border: 1px solid rgba(0, 0, 0, .125); border: 1px solid rgba(0, 0, 0, .125);
border-radius: 0.25rem; border-radius: 0.25rem;
margin: 1rem 0; margin: 1rem 0;
padding: 15px; padding: 15px;
display: block; display: block;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
.buttonContainer { .buttonContainer {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
} }
</style> </style>
</body> </body>
</html> </html>

View File

@@ -3,7 +3,7 @@
<th:block th:insert="~{fragments/common :: head(title=#{watermark.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{watermark.title})}"></th:block>
<body> <body onload="toggleFileOption()">
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
@@ -16,30 +16,36 @@
<form method="post" enctype="multipart/form-data" action="add-watermark"> <form method="post" enctype="multipart/form-data" action="add-watermark">
<div class="form-group"> <div class="form-group">
<label th:text="#{watermark.selectText.1}"></label> <label th:text="#{watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}">
<input type="file" id="fileInput" name="fileInput" class="form-control-file" accept="application/pdf" required />
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="fontSize" th:text="#{alphabet} + ':'"></label> <label th:text="#{watermark.selectText.8}"></label>
<select class="form-control" name="alphabet" id="alphabet-select"> <select class="form-control" id="watermarkType" name="watermarkType" onchange="toggleFileOption()">
<option value="romain">Roman</option> <option value="text">Text</option>
<option value="arabic">العربية</option> <option value="image">Image</option>
<option value="japanese">日本語</option> </select>
<option value="korean">한국어</option>
<option value="chinese">简体中文</option>
</select>
</div> </div>
<div class="form-group">
<div id="watermarkTextGroup" class="form-group">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label> <label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required /> <input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
</div> </div>
<div id="watermarkImageGroup" class="form-group" style="display: none;">
<label for="watermarkImage" th:text="#{watermark.selectText.9}"></label>
<input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*" />
</div>
<div class="form-group"> <div class="form-group">
<label for="fontSize" th:text="#{watermark.selectText.3}"></label> <label for="fontSize" th:text="#{watermark.selectText.3}"></label>
<input type="text" id="fontSize" name="fontSize" class="form-control" value="30" /> <input type="text" id="fontSize" name="fontSize" class="form-control" value="30" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="opacity" th:text="#{watermark.selectText.7}"></label> <label for="opacity" th:text="#{watermark.selectText.7}"></label>
<input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateopacityValue()" /> <input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()" />
<input type="hidden" id="opacityReal" name="opacity" value="0.5"> <input type="hidden" id="opacityReal" name="opacity" value="0.5">
</div> </div>
@@ -48,7 +54,7 @@
const opacityInput = document.getElementById('opacity'); const opacityInput = document.getElementById('opacity');
const opacityRealInput = document.getElementById('opacityReal'); const opacityRealInput = document.getElementById('opacityReal');
const updateopacityValue = () => { const updateOpacityValue = () => {
let percentageValue = parseFloat(opacityInput.value.replace('%', '')); let percentageValue = parseFloat(opacityInput.value.replace('%', ''));
if (isNaN(percentageValue)) { if (isNaN(percentageValue)) {
percentageValue = 0; percentageValue = 0;
@@ -68,14 +74,15 @@
opacityInput.value = opacityInput.value.replace('%', ''); opacityInput.value = opacityInput.value.replace('%', '');
}); });
opacityInput.addEventListener('blur', () => { opacityInput.addEventListener('blur', () => {
updateopacityValue(); updateOpacityValue();
appendPercentageSymbol(); appendPercentageSymbol();
}); });
// Set initial values // Set initial values
updateopacityValue(); updateOpacityValue();
appendPercentageSymbol(); appendPercentageSymbol();
</script> </script>
<div class="form-group"> <div class="form-group">
<label for="rotation" th:text="#{watermark.selectText.4}"></label> <label for="rotation" th:text="#{watermark.selectText.4}"></label>
<input type="text" id="rotation" name="rotation" class="form-control" value="45" /> <input type="text" id="rotation" name="rotation" class="form-control" value="45" />
@@ -92,6 +99,29 @@
<input type="submit" id="submitBtn" th:value="#{watermark.submit}" class="btn btn-primary" /> <input type="submit" id="submitBtn" th:value="#{watermark.submit}" class="btn btn-primary" />
</div> </div>
</form> </form>
<script>
function toggleFileOption() {
const watermarkType = document.getElementById('watermarkType').value;
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
const watermarkText = document.getElementById('watermarkText');
const watermarkImage = document.getElementById('watermarkImage');
if (watermarkType === 'text') {
watermarkTextGroup.style.display = 'block';
watermarkText.required = true;
watermarkImageGroup.style.display = 'none';
watermarkImage.required = false;
} else if (watermarkType === 'image') {
watermarkTextGroup.style.display = 'none';
watermarkText.required = false;
watermarkImageGroup.style.display = 'block';
watermarkImage.required = true;
}
}
</script>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,53 @@
<!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=#{sanitizePDF.title})}"></th:block>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{sanitizePDF.header}"></h2>
<form action="sanitize-pdf" method="post" enctype="multipart/form-data">
<div class="form-group">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="removeJavaScript" name="removeJavaScript" checked>
<label class="form-check-label" for="removeJavaScript" th:text="#{sanitizePDF.selectText.1}"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="removeEmbeddedFiles" name="removeEmbeddedFiles" checked>
<label class="form-check-label" for="removeEmbeddedFiles" th:text="#{sanitizePDF.selectText.2}"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="removeMetadata" name="removeMetadata" checked>
<label class="form-check-label" for="removeMetadata" th:text="#{sanitizePDF.selectText.3}"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="removeLinks" name="removeLinks" checked>
<label class="form-check-label" for="removeLinks" th:text="#{sanitizePDF.selectText.4}"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="removeFonts" name="removeFonts" checked>
<label class="form-check-label" for="removeFonts" th:text="#{sanitizePDF.selectText.5}"></label>
</div>
<br />
<div class="form-group text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{sanitizePDF.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@@ -7,16 +7,27 @@
<script src="js/thirdParty/interact.min.js"></script> <script src="js/thirdParty/interact.min.js"></script>
</head> </head>
<th:block th:each="font : ${fonts}">
<style th:inline="text">
@font-face {
font-family: "[[${font}]]";
src: url('fonts/[[${font}]].woff2') format('woff2');
}
#font-select option[value="[[${font}]]"] {
font-family: "[[${font}]]", cursive;
}
</style>
</th:block>
<style> <style>
@font-face { select#font-select, select#font-select option {
font-family: 'Estonia'; height: 60px; /* Adjust as needed */
src: url(fonts/Estonia.woff2) format('woff2'); font-size: 30px; /* Adjust as needed */
} }
@font-face {
font-family: 'Tangerine';
src: url(fonts/Tangerine.woff2) format('woff2');
}
</style> </style>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
@@ -36,6 +47,7 @@
if (file) { if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, ""); originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer(); const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0); await DraggableUtils.renderPage(pdfDoc, 0);
@@ -169,9 +181,9 @@
<input type="text" class="form-control" id="sigText" name="sigText"> <input type="text" class="form-control" id="sigText" name="sigText">
<label th:text="#{font}"></label> <label th:text="#{font}"></label>
<select class="form-control" name="font" id="font-select"> <select class="form-control" name="font" id="font-select">
<option value="Estonia" class="estonia-font">Estonia</option> <option th:each="font : ${fonts}" th:value="${font}" th:text="${font}" th:class="${font.toLowerCase()+'-font'}"></option>
<option value="Tangerine" class="tangerine-font">Tangerine</option>
</select> </select>
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button> <button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
</div> </div>
@@ -196,20 +208,37 @@
DraggableUtils.createDraggableCanvasFromUrl(dataURL); DraggableUtils.createDraggableCanvasFromUrl(dataURL);
} }
</script> </script>
<style> <script>
#font-select option { const sigTextInput = document.getElementById('sigText');
font-size: 30px; const fontSelect = document.getElementById('font-select');
}
#font-select option[value="Estonia"] { const updateOptionTexts = () => {
font-family: 'Estonia', sans-serif; Array.from(fontSelect.options).forEach(option => {
} const fontName = option.value.replace(/-regular$/i, '');
#font-select option[value="Tangerine"] { option.text = sigTextInput.value || fontName;
font-family: 'Tangerine', cursive; });
} }
#font-select option[value="Windsong"] {
font-family: 'Windsong', cursive; sigTextInput.addEventListener('input', updateOptionTexts);
}
</style> fontSelect.addEventListener('change', (e) => {
e.target.style.fontFamily = e.target.value;
updateOptionTexts();
});
// Manually trigger the change event
fontSelect.dispatchEvent(new Event('change'));
</script>
<th:block th:each="font : ${fonts}">
<style th:inline="text">
#font-select option[value="/*[[${font}]]*/"] {
font-family: '/*[[${font}]]*/', cursive;
}
</style>
</th:block>
</div> </div>
</div> </div>
@@ -240,10 +269,6 @@
position: relative; position: relative;
margin: 20px 0; margin: 20px 0;
} }
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
}
.draggable-buttons-box { .draggable-buttons-box {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -271,6 +296,7 @@
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button> <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
</div> </div>
<script> <script>
document.getElementById("download-pdf").addEventListener('click', async() => { document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument(); const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();