Merge branch 'main' into patch-1

This commit is contained in:
Anthony Stirling 2023-07-19 23:33:46 +01:00 committed by GitHub
commit 1883b477a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 12846 additions and 8464 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

@ -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

@ -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

@ -86,6 +86,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 +106,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
@ -161,10 +164,11 @@ Using the same method you can also change
- 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 - 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.0'
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")
} }

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,208 @@
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("Security", "change-permissions"); // Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-watermark"); addEndpointToGroup("Security", "add-password");
addEndpointToGroup("Security", "cert-sign"); addEndpointToGroup("Security", "remove-password");
addEndpointToGroup("Security", "change-permissions");
addEndpointToGroup("Security", "add-watermark");
addEndpointToGroup("Security", "cert-sign");
// Adding endpoints to "Other" group addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "compress-pdf"); // Adding endpoints to "Other" group
addEndpointToGroup("Other", "extract-images"); addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "change-metadata"); addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "extract-image-scans"); addEndpointToGroup("Other", "compress-pdf");
addEndpointToGroup("Other", "sign"); addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Other", "flatten"); addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Other", "repair"); addEndpointToGroup("Other", "extract-image-scans");
addEndpointToGroup("Other", "remove-blanks"); addEndpointToGroup("Other", "sign");
addEndpointToGroup("Other", "compare"); 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"); //CLI
addEndpointToGroup("CLI", "remove-blanks"); addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("CLI", "repair"); addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("CLI", "pdf-to-pdfa"); addEndpointToGroup("CLI", "remove-blanks");
addEndpointToGroup("CLI", "file-to-pdf"); addEndpointToGroup("CLI", "repair");
addEndpointToGroup("CLI", "xlsx-to-pdf"); addEndpointToGroup("CLI", "pdf-to-pdfa");
addEndpointToGroup("CLI", "pdf-to-word"); addEndpointToGroup("CLI", "file-to-pdf");
addEndpointToGroup("CLI", "pdf-to-presentation"); addEndpointToGroup("CLI", "xlsx-to-pdf");
addEndpointToGroup("CLI", "pdf-to-text"); addEndpointToGroup("CLI", "pdf-to-word");
addEndpointToGroup("CLI", "pdf-to-html"); addEndpointToGroup("CLI", "pdf-to-presentation");
addEndpointToGroup("CLI", "pdf-to-xml"); addEndpointToGroup("CLI", "pdf-to-text");
addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "pdf-to-html");
addEndpointToGroup("CLI", "pdf-to-xml");
//python addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "remove-blanks"); //python
addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "remove-blanks");
//openCV
addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("OpenCV", "remove-blanks"); //openCV
addEndpointToGroup("OpenCV", "extract-image-scans");
//LibreOffice addEndpointToGroup("OpenCV", "remove-blanks");
addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf"); //LibreOffice
addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "pdf-to-word"); addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-presentation"); addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-text"); addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("LibreOffice", "pdf-to-xml"); addEndpointToGroup("LibreOffice", "pdf-to-text");
addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml");
//OCRmyPDF
addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); //OCRmyPDF
addEndpointToGroup("OCRmyPDF", "ocr-pdf"); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
//Java addEndpointToGroup("OCRmyPDF", "ocr-pdf");
addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-pages"); //Java
addEndpointToGroup("Java", "split-pdfs"); addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "pdf-organizer"); addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "rotate-pdf"); addEndpointToGroup("Java", "split-pdfs");
addEndpointToGroup("Java", "pdf-to-img"); addEndpointToGroup("Java", "pdf-organizer");
addEndpointToGroup("Java", "img-to-pdf"); addEndpointToGroup("Java", "rotate-pdf");
addEndpointToGroup("Java", "add-password"); addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "remove-password"); addEndpointToGroup("Java", "img-to-pdf");
addEndpointToGroup("Java", "change-permissions"); addEndpointToGroup("Java", "add-password");
addEndpointToGroup("Java", "add-watermark"); addEndpointToGroup("Java", "remove-password");
addEndpointToGroup("Java", "add-image"); addEndpointToGroup("Java", "change-permissions");
addEndpointToGroup("Java", "extract-images"); addEndpointToGroup("Java", "add-watermark");
addEndpointToGroup("Java", "change-metadata"); addEndpointToGroup("Java", "add-image");
addEndpointToGroup("Java", "cert-sign"); addEndpointToGroup("Java", "extract-images");
addEndpointToGroup("Java", "multi-page-layout"); addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Java", "scale-pages"); addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "multi-page-layout");
addEndpointToGroup("Java", "scale-pages");
//Javascript addEndpointToGroup("Java", "add-page-numbers");
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Java", "auto-rename");
addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Java", "auto-split-pdf");
addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "crop");
}
//Javascript
private void processEnvironmentConfigs() { addEndpointToGroup("Javascript", "pdf-organizer");
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE"); addEndpointToGroup("Javascript", "sign");
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE"); addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast");
if (endpointsToRemove != null) {
String[] endpoints = endpointsToRemove.split(",");
for (String endpoint : endpoints) { }
disableEndpoint(endpoint.trim());
} private void processEnvironmentConfigs() {
} String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
if (groupsToRemove != null) {
String[] groups = groupsToRemove.split(","); if (endpointsToRemove != null) {
for (String group : groups) { String[] endpoints = endpointsToRemove.split(",");
disableGroup(group.trim()); 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("file") 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

@ -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,137 @@
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)
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<>(); // create this list to store ByteArrayOutputStreams for zipping
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);
}
}
// After all pages are added to splitDocuments, convert each to ByteArrayOutputStream and add to splitDocumentsBoas
for (PDDocument splitDocument : splitDocuments) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
splitDocument.save(baos);
splitDocumentsBoas.add(baos);
splitDocument.close();
}
document.close();
// After this line, you can find your zip logic integrated
Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
// loop through the split documents and write them to the zip file
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf"; // You should replace "originalFileName" with the real file name
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
// Add PDF file to the zip
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 the Resource in the response
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,176 @@
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 ResponseEntity.ok()
.header("Content-Type", "application/pdf; charset=UTF-8")
.header("Content-Disposition", "inline; filename=" + URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"))
.body(resultBytes);
}
}

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,21 +1,76 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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 +120,35 @@ 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() {
try {
return Files.list(Paths.get("src/main/resources/static/fonts"))
.map(Path::getFileName)
.map(Path::toString)
.filter(name -> name.endsWith(".woff2"))
.map(name -> name.substring(0, name.length() - 6)) // Remove .woff2 extension
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException("Failed to read font directory", e);
}
}
@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,113 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.util.ArrayList; import java.io.IOException;
import java.util.List; import java.nio.file.Files;
import java.nio.file.Path;
public class GeneralUtils { import java.nio.file.Paths;
import java.util.ArrayList;
public static Long convertSizeToBytes(String sizeStr) { import java.util.List;
if (sizeStr == null) {
return null; public class GeneralUtils {
}
public static Long convertSizeToBytes(String sizeStr) {
sizeStr = sizeStr.trim().toUpperCase(); if (sizeStr == null) {
try { return null;
if (sizeStr.endsWith("KB")) { }
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
} else if (sizeStr.endsWith("MB")) { sizeStr = sizeStr.trim().toUpperCase();
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024); try {
} else if (sizeStr.endsWith("GB")) { if (sizeStr.endsWith("KB")) {
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024); return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
} else if (sizeStr.endsWith("B")) { } else if (sizeStr.endsWith("MB")) {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024);
} else { } else if (sizeStr.endsWith("GB")) {
// Input string does not have a valid format, handle this case return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024);
} } else if (sizeStr.endsWith("B")) {
} catch (NumberFormatException e) { return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
// The numeric part of the input string cannot be parsed, handle this case } else {
} // Input string does not have a valid format, handle this case
}
return null; } catch (NumberFormatException e) {
} // The numeric part of the input string cannot be parsed, handle this case
}
public static List<Integer> parsePageList(String[] pageOrderArr, int totalPages) {
List<Integer> newPageOrder = new ArrayList<>(); return null;
}
// loop through the page order array
for (String element : pageOrderArr) { public static List<Integer> parsePageList(String[] pageOrderArr, int totalPages) {
// check if the element contains a range of pages List<Integer> newPageOrder = new ArrayList<>();
if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) {
// Handle page order as a function // loop through the page order array
int coefficient = 0; for (String element : pageOrderArr) {
int constant = 0; if (element.equalsIgnoreCase("all")) {
boolean coefficientExists = false; for (int i = 0; i < totalPages; i++) {
boolean constantExists = false; newPageOrder.add(i);
}
if (element.contains("n")) { // As all pages are already added, no need to check further
String[] parts = element.split("n"); break;
if (!parts[0].equals("") && parts[0] != null) { }
coefficient = Integer.parseInt(parts[0]); else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) {
coefficientExists = true; // Handle page order as a function
} int coefficient = 0;
if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { int constant = 0;
constant = Integer.parseInt(parts[1]); boolean coefficientExists = false;
constantExists = true; boolean constantExists = false;
}
} else if (element.contains("+")) { if (element.contains("n")) {
constant = Integer.parseInt(element.replace("+", "")); String[] parts = element.split("n");
constantExists = true; if (!parts[0].equals("") && parts[0] != null) {
} coefficient = Integer.parseInt(parts[0]);
coefficientExists = true;
for (int i = 1; i <= totalPages; i++) { }
int pageNum = coefficientExists ? coefficient * i : i; if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) {
pageNum += constantExists ? constant : 0; constant = Integer.parseInt(parts[1]);
constantExists = true;
if (pageNum <= totalPages && pageNum > 0) { }
newPageOrder.add(pageNum - 1); } else if (element.contains("+")) {
} constant = Integer.parseInt(element.replace("+", ""));
} constantExists = true;
} else if (element.contains("-")) { }
// split the range into start and end page
String[] range = element.split("-"); for (int i = 1; i <= totalPages; i++) {
int start = Integer.parseInt(range[0]); int pageNum = coefficientExists ? coefficient * i : i;
int end = Integer.parseInt(range[1]); pageNum += constantExists ? constant : 0;
// check if the end page is greater than total pages
if (end > totalPages) { if (pageNum <= totalPages && pageNum > 0) {
end = totalPages; newPageOrder.add(pageNum - 1);
} }
// loop through the range of pages }
for (int j = start; j <= end; j++) { } else if (element.contains("-")) {
// print the current index // split the range into start and end page
newPageOrder.add(j - 1); String[] range = element.split("-");
} int start = Integer.parseInt(range[0]);
} else { int end = Integer.parseInt(range[1]);
// if the element is a single page // check if the end page is greater than total pages
newPageOrder.add(Integer.parseInt(element) - 1); if (end > totalPages) {
} end = totalPages;
} }
// loop through the range of pages
return newPageOrder; 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,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,23 +21,53 @@ 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
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
@ -132,30 +162,113 @@ home.pageLayout.desc=Merge multiple pages of a PDF document into a single page
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.
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
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
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
selectFillter=-- Select --
pageNum=Page Number
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
home.crop.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
home.autoSplitPDF.title=Auto Split Pages
home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code
home.sanitizePdf.title=Sanitize
home.sanitizePdf.desc=Remove scripts and other elements from PDF files
###########################
# #
# WEB PAGES #
# #
###########################
#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.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 +280,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 +293,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 +311,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 +336,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 +357,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 +397,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 +415,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 +423,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 +447,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 +462,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 +484,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 +497,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 +505,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 +522,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 +530,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 +550,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,6 +581,7 @@ PDFToPresentation.credit=This service uses LibreOffice for file conversion.
PDFToPresentation.submit=Convert PDFToPresentation.submit=Convert
#PDFToText
PDFToText.title=PDF to Text/RTF PDFToText.title=PDF to Text/RTF
PDFToText.header=PDF to Text/RTF PDFToText.header=PDF to Text/RTF
PDFToText.selectText.1=Output file format PDFToText.selectText.1=Output file format
@ -467,24 +589,15 @@ 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

@ -21,22 +21,65 @@ 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 ##########################
### TODO: Translate ###
##########################
selectFillter=-- Select --
pageNum=Orrialde-zenbakia
##########################
### TODO: Translate ###
##########################
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
##########################
### TODO: Translate ###
##########################
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
##########################
### TODO: Translate ###
##########################
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
@ -59,16 +102,13 @@ home.addImage.desc=Gehitu irudi bat PDFan ezarritako kokaleku batean (lanean)
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
home.remove-watermark.desc= Ezabatu ur-marka PDF dokumentutik
home.permissions.title=Aldatu baimenak home.permissions.title=Aldatu baimenak
home.permissions.desc=Aldatu PDF dokumentuaren baimenak home.permissions.desc=Aldatu PDF dokumentuaren baimenak
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
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
home.removePassword.title=Ezabatu pasahitza home.removePassword.title=Ezabatu pasahitza
@ -78,7 +118,7 @@ home.compressPdfs.title=Konprimatu
home.compressPdfs.desc=Konprimatu PDFak fitxategiaren tamaina murrizteko home.compressPdfs.desc=Konprimatu PDFak fitxategiaren tamaina murrizteko
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
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)
@ -122,37 +162,128 @@ home.repair.desc=Saiatu PDF hondatu/kaltetu bat konpontzen
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
home.certSign.desc=Sinatu PDF bat Ziurtagiri/Gako batekin (PEM/P12)
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
home.certSign.title=Sinatu ziurtagiriarekin
home.certSign.desc=Sinatu PDF bat Ziurtagiri/Gako batekin (PEM/P12)
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
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
error.pdfPassword=PDF dokumentua pasahitzarekin babestuta dago eta pasahitza ez da sartu edo akastuna da ##########################
### TODO: Translate ###
##########################
home.pipeline.title=Pipeline (Advanced)
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
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 --
pageNum=Orrialde-zenbakia
home.auto-rename.title=Auto Rename PDF File
home.auto-rename.desc=Auto renames a PDF file based on its detected header
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
home.crop.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
home.autoSplitPDF.title=Auto Split Pages
home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code
home.sanitizePdf.title=Sanitize
home.sanitizePdf.desc=Remove scripts and other elements from PDF files
###########################
# #
# WEB PAGES #
# #
###########################
#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.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 +298,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 +308,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:
@ -207,19 +350,7 @@ ScannerImageSplit.selectText.7=Inguruko area gutxienekoa:
ScannerImageSplit.selectText.8=Ezarri inguruko arearen gutxieneko balioa argazki batentzat ScannerImageSplit.selectText.8=Ezarri inguruko arearen gutxieneko balioa argazki batentzat
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 +372,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 +400,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 +412,39 @@ 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): ##########################
### TODO: Translate ###
##########################
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 +465,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 +480,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 +499,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 +515,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 +523,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 +540,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 +548,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 +568,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 +599,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 +607,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

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,72 @@
#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%;
} }
html[lang-direction=ltr] * { html[lang-direction=ltr] * {
direction: ltr; direction: ltr;
} }
html[lang-direction=rtl] * { html[lang-direction=rtl] * {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
.ignore-rtl { .ignore-rtl {
direction: ltr !important; direction: ltr !important;
text-align: left !important; text-align: left !important;
} }
.align-top { .align-top {
position: absolute; position: absolute;
top: 0; top: 0;
} }
.align-center-right { .align-center-right {
position: absolute; position: absolute;
right: 0; right: 0;
top: 50%; top: 50%;
} }
.align-center-left { .align-center-left {
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;
} }
.align-bottom { .align-bottom {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
} }
.btn-group > label:first-of-type { .btn-group > label:first-of-type {
border-top-left-radius: 0.25rem !important; border-top-left-radius: 0.25rem !important;
border-bottom-left-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important;
} }
html[lang-direction="rtl"] input.form-check-input { html[lang-direction="rtl"] input.form-check-input {
position: relative; position: relative;
margin-left: 0px; margin-left: 0px;
} }
html[lang-direction="rtl"] label.form-check-label { html[lang-direction="rtl"] label.form-check-label {
display: inline; display: inline;
} }
.margin-auto-parent { .margin-auto-parent {
width: 100%; width: 100%;
display: flex; display: flex;
} }
.margin-center { .margin-center {
margin: 0 auto; margin: 0 auto;
}
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
} }

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 @@
<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 KiB

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,24 @@
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;
var tags = card.getAttribute('data-tags');
var content = title + ' ' + text + ' ' + tags;
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 +34,7 @@ function toggleFavorite(element) {
} }
reorderCards(); reorderCards();
updateFavoritesDropdown(); updateFavoritesDropdown();
filterCards();
} }
function reorderCards() { function reorderCards() {
@ -45,5 +67,7 @@ function initializeCards() {
}); });
reorderCards(); reorderCards();
updateFavoritesDropdown(); updateFavoritesDropdown();
filterCards();
} }
window.onload = initializeCards; window.onload = initializeCards;

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,72 @@
// 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');
if (titleElement && iconElement && itemHref !== '#') {
var title = titleElement.innerText;
if (title.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,40 @@
<!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>
<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,145 @@
<!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="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('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.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);
});
}
</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,7 +40,7 @@
</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>
@ -54,6 +54,10 @@
<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')}"></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')}"></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')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'auto-split-pdf', 'images/layout-split.svg', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc')}"></div>
</div> </div>
</li> </li>
@ -94,17 +98,19 @@
<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')}"></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')}"></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')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc')}"></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 ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc')}"></div> -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc')}"></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 ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc')}"></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 ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc')}"></div>
@ -116,6 +122,8 @@
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc')}"></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 ('remove-blanks', 'images/blank-file.svg', 'home.removeBlanks.title', 'home.removeBlanks.desc')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc')}"></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')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc')}"></div>
</div> </div>
</li> </li>
@ -203,6 +211,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 +282,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

@ -20,9 +20,14 @@
</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>
@ -67,12 +72,16 @@
<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>
<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>
<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='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='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.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> </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

@ -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,293 @@
<!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-6">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <h2 th:text="#{adjustContrast.header}"></h2>
<div class="form-group"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
<label for="contrastRange">Contrast</label> <h4>
<input name="contrastRange" type="range" class="form-control-range" id="contrastRange" min="-100" max="100" value="0" step="1"> <span th:text="#{adjustContrast.contrast}"></span> <span id="contrast-val">100</span>%
</div> </h4>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button> <input type="range" min="0" max="200" value="100"
</form> 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"
</div> id="brightness-slider" />
</body>
</html> <h4>
<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" />
</br>
<canvas id="pdf-canvas"></canvas>
<button id="download-button" class="btn btn-primary" th:text="#{adjustContrast.download}"></button>
<script src="pdfjs/pdf.js"></script>
<script>
var canvas = document.getElementById('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);
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);
};
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();
});
});
}
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,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

@ -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">
@ -169,9 +180,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 +207,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 +268,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 +295,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();