parent
d70aec5daa
commit
b078905889
32 changed files with 1575 additions and 0 deletions
@ -0,0 +1,5 @@ |
||||
FROM openjdk:17-jdk-slim |
||||
COPY build/libs/*.jar app.jar |
||||
EXPOSE 8080 |
||||
ENV LOG_LEVEL=INFO |
||||
ENTRYPOINT ["java","-jar","/app.jar","-Dlogging.level=${LOG_LEVEL}"] |
@ -0,0 +1,33 @@ |
||||
pipeline { |
||||
agent any |
||||
stages { |
||||
stage('Build') { |
||||
steps { |
||||
sh 'chmod 755 gradlew' |
||||
sh './gradlew build' |
||||
} |
||||
} |
||||
stage('Docker Build') { |
||||
steps { |
||||
script { |
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim() |
||||
def image = "frooodle/s-pdf:$appVersion" |
||||
sh "docker build -t $image ." |
||||
} |
||||
} |
||||
} |
||||
stage('Docker Push') { |
||||
steps { |
||||
script { |
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim() |
||||
def image = "frooodle/s-pdf:$appVersion" |
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) { |
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN" |
||||
sh "docker push $image" |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
# Stirling-PDF |
||||
|
||||
This is a locally hosted web application that allows you to perform various operations on PDF files, such as splitting and adding images. |
||||
|
||||
## Features |
||||
|
||||
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files. |
||||
- Merge multiple PDFs together into a single resultant file |
||||
- Convert PDFs to and from images |
||||
- Reorganize PDF pages into different orders. |
||||
- Add images to PDFs at specified locations. |
||||
- Dark mode support. |
||||
|
||||
## Technologies used |
||||
- Spring Boot + Thymeleaf |
||||
- PDFBox |
||||
- HTML, CSS, JavaScript |
||||
- Docker |
||||
|
||||
## How to use |
||||
|
||||
### Locally |
||||
|
||||
Prerequisites |
||||
- Java 17 or later |
||||
- Gradle 6.0 or later |
||||
|
||||
1. Clone or download the repository. |
||||
2. Build the project using Gradle by running `./gradlew build` |
||||
3. Start the application by running `./gradlew bootRun` |
||||
|
||||
|
||||
### Docker |
||||
docker pull frooodle/s-pdf |
||||
|
||||
docker run -p 8080:8080 frooodle/s-pdf |
||||
|
||||
|
||||
## How to View |
||||
1. Open a web browser and navigate to `http://localhost:8080/` |
||||
2. Use the application by following the instructions on the website. |
||||
|
||||
## Note |
||||
The application is currently not thread-safe |
@ -0,0 +1,29 @@ |
||||
plugins { |
||||
id 'java' |
||||
id 'org.springframework.boot' version '3.0.2' |
||||
id 'io.spring.dependency-management' version '1.1.0' |
||||
} |
||||
|
||||
group = 'stirling.software' |
||||
version = '0.0.1-SNAPSHOT' |
||||
sourceCompatibility = '17' |
||||
|
||||
repositories { |
||||
mavenCentral() |
||||
} |
||||
|
||||
dependencies { |
||||
implementation 'org.springframework.boot:spring-boot-starter-web' |
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' |
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test' |
||||
implementation 'org.apache.pdfbox:pdfbox:2.0.27' |
||||
implementation 'log4j:log4j' |
||||
} |
||||
|
||||
tasks.named('test') { |
||||
useJUnitPlatform() |
||||
} |
||||
task printVersion { |
||||
println project.version |
||||
} |
||||
|
Binary file not shown.
@ -0,0 +1,5 @@ |
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
@ -0,0 +1,240 @@ |
||||
#!/bin/sh |
||||
|
||||
# |
||||
# Copyright © 2015-2021 the original authors. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# https://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
# |
||||
|
||||
############################################################################## |
||||
# |
||||
# Gradle start up script for POSIX generated by Gradle. |
||||
# |
||||
# Important for running: |
||||
# |
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is |
||||
# noncompliant, but you have some other compliant shell such as ksh or |
||||
# bash, then to run this script, type that shell name before the whole |
||||
# command line, like: |
||||
# |
||||
# ksh Gradle |
||||
# |
||||
# Busybox and similar reduced shells will NOT work, because this script |
||||
# requires all of these POSIX shell features: |
||||
# * functions; |
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», |
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»; |
||||
# * compound commands having a testable exit status, especially «case»; |
||||
# * various built-in commands including «command», «set», and «ulimit». |
||||
# |
||||
# Important for patching: |
||||
# |
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided |
||||
# by Bash, Ksh, etc; in particular arrays are avoided. |
||||
# |
||||
# The "traditional" practice of packing multiple parameters into a |
||||
# space-separated string is a well documented source of bugs and security |
||||
# problems, so this is (mostly) avoided, by progressively accumulating |
||||
# options in "$@", and eventually passing that to Java. |
||||
# |
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, |
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; |
||||
# see the in-line comments for details. |
||||
# |
||||
# There are tweaks for specific operating systems such as AIX, CygWin, |
||||
# Darwin, MinGW, and NonStop. |
||||
# |
||||
# (3) This script is generated from the Groovy template |
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt |
||||
# within the Gradle project. |
||||
# |
||||
# You can find Gradle at https://github.com/gradle/gradle/. |
||||
# |
||||
############################################################################## |
||||
|
||||
# Attempt to set APP_HOME |
||||
|
||||
# Resolve links: $0 may be a link |
||||
app_path=$0 |
||||
|
||||
# Need this for daisy-chained symlinks. |
||||
while |
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path |
||||
[ -h "$app_path" ] |
||||
do |
||||
ls=$( ls -ld "$app_path" ) |
||||
link=${ls#*' -> '} |
||||
case $link in #( |
||||
/*) app_path=$link ;; #( |
||||
*) app_path=$APP_HOME$link ;; |
||||
esac |
||||
done |
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit |
||||
|
||||
APP_NAME="Gradle" |
||||
APP_BASE_NAME=${0##*/} |
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' |
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value. |
||||
MAX_FD=maximum |
||||
|
||||
warn () { |
||||
echo "$*" |
||||
} >&2 |
||||
|
||||
die () { |
||||
echo |
||||
echo "$*" |
||||
echo |
||||
exit 1 |
||||
} >&2 |
||||
|
||||
# OS specific support (must be 'true' or 'false'). |
||||
cygwin=false |
||||
msys=false |
||||
darwin=false |
||||
nonstop=false |
||||
case "$( uname )" in #( |
||||
CYGWIN* ) cygwin=true ;; #( |
||||
Darwin* ) darwin=true ;; #( |
||||
MSYS* | MINGW* ) msys=true ;; #( |
||||
NONSTOP* ) nonstop=true ;; |
||||
esac |
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM. |
||||
if [ -n "$JAVA_HOME" ] ; then |
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |
||||
# IBM's JDK on AIX uses strange locations for the executables |
||||
JAVACMD=$JAVA_HOME/jre/sh/java |
||||
else |
||||
JAVACMD=$JAVA_HOME/bin/java |
||||
fi |
||||
if [ ! -x "$JAVACMD" ] ; then |
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the |
||||
location of your Java installation." |
||||
fi |
||||
else |
||||
JAVACMD=java |
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the |
||||
location of your Java installation." |
||||
fi |
||||
|
||||
# Increase the maximum file descriptors if we can. |
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then |
||||
case $MAX_FD in #( |
||||
max*) |
||||
MAX_FD=$( ulimit -H -n ) || |
||||
warn "Could not query maximum file descriptor limit" |
||||
esac |
||||
case $MAX_FD in #( |
||||
'' | soft) :;; #( |
||||
*) |
||||
ulimit -n "$MAX_FD" || |
||||
warn "Could not set maximum file descriptor limit to $MAX_FD" |
||||
esac |
||||
fi |
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order: |
||||
# * args from the command line |
||||
# * the main class name |
||||
# * -classpath |
||||
# * -D...appname settings |
||||
# * --module-path (only if needed) |
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. |
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java |
||||
if "$cygwin" || "$msys" ; then |
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) |
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) |
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" ) |
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh |
||||
for arg do |
||||
if |
||||
case $arg in #( |
||||
-*) false ;; # don't mess with options #( |
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath |
||||
[ -e "$t" ] ;; #( |
||||
*) false ;; |
||||
esac |
||||
then |
||||
arg=$( cygpath --path --ignore --mixed "$arg" ) |
||||
fi |
||||
# Roll the args list around exactly as many times as the number of |
||||
# args, so each arg winds up back in the position where it started, but |
||||
# possibly modified. |
||||
# |
||||
# NB: a `for` loop captures its iteration list before it begins, so |
||||
# changing the positional parameters here affects neither the number of |
||||
# iterations, nor the values presented in `arg`. |
||||
shift # remove old arg |
||||
set -- "$@" "$arg" # push replacement arg |
||||
done |
||||
fi |
||||
|
||||
# Collect all arguments for the java command; |
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of |
||||
# shell script including quotes and variable substitutions, so put them in |
||||
# double quotes to make sure that they get re-expanded; and |
||||
# * put everything else in single quotes, so that it's not re-expanded. |
||||
|
||||
set -- \ |
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \ |
||||
-classpath "$CLASSPATH" \ |
||||
org.gradle.wrapper.GradleWrapperMain \ |
||||
"$@" |
||||
|
||||
# Stop when "xargs" is not available. |
||||
if ! command -v xargs >/dev/null 2>&1 |
||||
then |
||||
die "xargs is not available" |
||||
fi |
||||
|
||||
# Use "xargs" to parse quoted args. |
||||
# |
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. |
||||
# |
||||
# In Bash we could simply go: |
||||
# |
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) && |
||||
# set -- "${ARGS[@]}" "$@" |
||||
# |
||||
# but POSIX shell has neither arrays nor command substitution, so instead we |
||||
# post-process each arg (as a line of input to sed) to backslash-escape any |
||||
# character that might be a shell metacharacter, then use eval to reverse |
||||
# that process (while maintaining the separation between arguments), and wrap |
||||
# the whole thing up as a single "set" statement. |
||||
# |
||||
# This will of course break if any of these variables contains a newline or |
||||
# an unmatched quote. |
||||
# |
||||
|
||||
eval "set -- $( |
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | |
||||
xargs -n1 | |
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | |
||||
tr '\n' ' ' |
||||
)" '"$@"' |
||||
|
||||
exec "$JAVACMD" "$@" |
@ -0,0 +1,91 @@ |
||||
@rem |
||||
@rem Copyright 2015 the original author or authors. |
||||
@rem |
||||
@rem Licensed under the Apache License, Version 2.0 (the "License"); |
||||
@rem you may not use this file except in compliance with the License. |
||||
@rem You may obtain a copy of the License at |
||||
@rem |
||||
@rem https://www.apache.org/licenses/LICENSE-2.0 |
||||
@rem |
||||
@rem Unless required by applicable law or agreed to in writing, software |
||||
@rem distributed under the License is distributed on an "AS IS" BASIS, |
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
@rem See the License for the specific language governing permissions and |
||||
@rem limitations under the License. |
||||
@rem |
||||
|
||||
@if "%DEBUG%"=="" @echo off |
||||
@rem ########################################################################## |
||||
@rem |
||||
@rem Gradle startup script for Windows |
||||
@rem |
||||
@rem ########################################################################## |
||||
|
||||
@rem Set local scope for the variables with windows NT shell |
||||
if "%OS%"=="Windows_NT" setlocal |
||||
|
||||
set DIRNAME=%~dp0 |
||||
if "%DIRNAME%"=="" set DIRNAME=. |
||||
set APP_BASE_NAME=%~n0 |
||||
set APP_HOME=%DIRNAME% |
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter. |
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi |
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" |
||||
|
||||
@rem Find java.exe |
||||
if defined JAVA_HOME goto findJavaFromJavaHome |
||||
|
||||
set JAVA_EXE=java.exe |
||||
%JAVA_EXE% -version >NUL 2>&1 |
||||
if %ERRORLEVEL% equ 0 goto execute |
||||
|
||||
echo. |
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||
echo. |
||||
echo Please set the JAVA_HOME variable in your environment to match the |
||||
echo location of your Java installation. |
||||
|
||||
goto fail |
||||
|
||||
:findJavaFromJavaHome |
||||
set JAVA_HOME=%JAVA_HOME:"=% |
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe |
||||
|
||||
if exist "%JAVA_EXE%" goto execute |
||||
|
||||
echo. |
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% |
||||
echo. |
||||
echo Please set the JAVA_HOME variable in your environment to match the |
||||
echo location of your Java installation. |
||||
|
||||
goto fail |
||||
|
||||
:execute |
||||
@rem Setup the command line |
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |
||||
|
||||
|
||||
@rem Execute Gradle |
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* |
||||
|
||||
:end |
||||
@rem End local scope for the variables with windows NT shell |
||||
if %ERRORLEVEL% equ 0 goto mainEnd |
||||
|
||||
:fail |
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of |
||||
rem the _cmd.exe /c_ return code! |
||||
set EXIT_CODE=%ERRORLEVEL% |
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1 |
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% |
||||
exit /b %EXIT_CODE% |
||||
|
||||
:mainEnd |
||||
if "%OS%"=="Windows_NT" endlocal |
||||
|
||||
:omega |
@ -0,0 +1 @@ |
||||
rootProject.name = 'S-PDF' |
@ -0,0 +1,13 @@ |
||||
package stirling.software.SPDF; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
|
||||
@SpringBootApplication |
||||
public class SPdfApplication { |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(SPdfApplication.class, args); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,32 @@ |
||||
package stirling.software.SPDF.controller; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
import stirling.software.SPDF.utils.PdfUtils; |
||||
|
||||
@Controller |
||||
public class FromPDFController { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FromPDFController.class); |
||||
|
||||
@GetMapping("/convert-from-pdf") |
||||
public String convertFromPdfForm() { |
||||
return "convert-from-pdf"; |
||||
} |
||||
|
||||
@PostMapping("/convert-from-pdf") |
||||
public byte[] convertToImage(@RequestParam("fileInput") MultipartFile file, |
||||
@RequestParam("imageFormat") String imageFormat) throws IOException { |
||||
byte[] pdfBytes = file.getBytes(); |
||||
return PdfUtils.convertFromPdf(pdfBytes, imageFormat); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,47 @@ |
||||
package stirling.software.SPDF.controller; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
import stirling.software.SPDF.utils.PdfUtils; |
||||
|
||||
@Controller |
||||
public class OverlayImageController { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class); |
||||
|
||||
@GetMapping("/add-image") |
||||
public String overlayImage() { |
||||
return "add-image"; |
||||
} |
||||
|
||||
@PostMapping("/add-image") |
||||
public ResponseEntity<byte[]> overlayImage(@RequestParam("fileInput") MultipartFile pdfFile, |
||||
@RequestParam("fileInput2") MultipartFile imageFile, @RequestParam("x") float x, |
||||
@RequestParam("y") float y) { |
||||
try { |
||||
byte[] pdfBytes = pdfFile.getBytes(); |
||||
byte[] imageBytes = imageFile.getBytes(); |
||||
byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y); |
||||
HttpHeaders headers = new HttpHeaders(); |
||||
headers.setContentType(MediaType.APPLICATION_PDF); |
||||
headers.setContentDispositionFormData("attachment", "overlayed.pdf"); |
||||
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0"); |
||||
return new ResponseEntity<>(result, headers, HttpStatus.OK); |
||||
} catch (IOException e) { |
||||
logger.error("Failed to add image to PDF", e); |
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,88 @@ |
||||
package stirling.software.SPDF.controller; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument; |
||||
import org.apache.pdfbox.pdmodel.PDPage; |
||||
import org.apache.pdfbox.pdmodel.PDPageTree; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.core.io.InputStreamResource; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.ui.Model; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
@Controller |
||||
public class PdfController { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PdfController.class); |
||||
|
||||
@GetMapping("/") |
||||
public String root(Model model) { |
||||
return "redirect:/home"; |
||||
} |
||||
|
||||
@GetMapping("/merge-pdfs") |
||||
public String hello(Model model) { |
||||
model.addAttribute("message", "Hello, World!"); |
||||
return "merge-pdfs"; |
||||
} |
||||
|
||||
@GetMapping("/home") |
||||
public String home(Model model) { |
||||
model.addAttribute("message", "Hello, World!"); |
||||
return "home"; |
||||
} |
||||
|
||||
@PostMapping("/merge-pdfs") |
||||
public ResponseEntity<InputStreamResource> mergePdfs(@RequestParam("fileInput") MultipartFile[] files) |
||||
throws IOException { |
||||
// Read the input PDF files into PDDocument objects
|
||||
List<PDDocument> documents = new ArrayList<>(); |
||||
|
||||
// Loop through the files array and read each file into a PDDocument
|
||||
for (MultipartFile file : files) { |
||||
documents.add(PDDocument.load(file.getInputStream())); |
||||
} |
||||
|
||||
PDDocument mergedDoc = mergeDocuments(documents); |
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
||||
mergedDoc.save(byteArrayOutputStream); |
||||
mergedDoc.close(); |
||||
|
||||
// Create an InputStreamResource from the merged PDF
|
||||
InputStreamResource resource = new InputStreamResource( |
||||
new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); |
||||
|
||||
// Return the merged PDF as a response
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body(resource); |
||||
} |
||||
|
||||
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException { |
||||
// Create a new empty document
|
||||
PDDocument mergedDoc = new PDDocument(); |
||||
|
||||
// Iterate over the list of documents and add their pages to the merged document
|
||||
for (PDDocument doc : documents) { |
||||
// Get all pages from the current document
|
||||
PDPageTree pages = doc.getPages(); |
||||
// Iterate over the pages and add them to the merged document
|
||||
for (PDPage page : pages) { |
||||
mergedDoc.addPage(page); |
||||
} |
||||
} |
||||
|
||||
// Return the merged document
|
||||
return mergedDoc; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,107 @@ |
||||
package stirling.software.SPDF.controller; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument; |
||||
import org.apache.pdfbox.pdmodel.PDPage; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.ui.Model; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
@Controller |
||||
public class RearrangePagesPDFController { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); |
||||
|
||||
@GetMapping("/pdf-organizer") |
||||
public String pageOrganizer(Model model) { |
||||
return "pdf-organizer"; |
||||
} |
||||
|
||||
@PostMapping("/rearrange-pages") |
||||
public ResponseEntity<byte[]> rearrangePages(@RequestParam("fileInput") MultipartFile pdfFile, |
||||
@RequestParam("pageOrder") String pageOrder) { |
||||
try { |
||||
// Load the input PDF
|
||||
PDDocument document = PDDocument.load(pdfFile.getInputStream()); |
||||
|
||||
// Split the page order string into an array of page numbers or range of numbers
|
||||
String[] pageOrderArr = pageOrder.split(","); |
||||
List<Integer> newPageOrder = new ArrayList<Integer>(); |
||||
//int[] newPageOrder = new int[pageOrderArr.length];
|
||||
int totalPages = document.getNumberOfPages(); |
||||
|
||||
// loop through the page order array
|
||||
for (String element : pageOrderArr) { |
||||
// check if the element contains a range of pages
|
||||
if (element.contains("-")) { |
||||
// split the range into start and end page
|
||||
String[] range = element.split("-"); |
||||
int start = Integer.parseInt(range[0]); |
||||
int end = Integer.parseInt(range[1]); |
||||
// check if the end page is greater than total pages
|
||||
if (end > totalPages) { |
||||
end = totalPages; |
||||
} |
||||
// loop through the range of pages
|
||||
for (int j = start; j <= end; j++) { |
||||
// print the current index
|
||||
newPageOrder.add( j - 1); |
||||
} |
||||
} else { |
||||
// if the element is a single page
|
||||
newPageOrder.add( Integer.parseInt(element) - 1); |
||||
} |
||||
} |
||||
|
||||
// Create a new list to hold the pages in the new order
|
||||
List<PDPage> newPages = new ArrayList<>(); |
||||
for (int i = 0; i < newPageOrder.size(); i++) { |
||||
newPages.add(document.getPage(newPageOrder.get(i))); |
||||
} |
||||
|
||||
// Remove all the pages from the original document
|
||||
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { |
||||
document.removePage(i); |
||||
} |
||||
|
||||
// Add the pages in the new order
|
||||
for (PDPage page : newPages) { |
||||
document.addPage(page); |
||||
} |
||||
|
||||
// Save the rearranged PDF to a ByteArrayOutputStream
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
||||
document.save(outputStream); |
||||
|
||||
// Close the original document
|
||||
document.close(); |
||||
|
||||
// Prepare the response headers
|
||||
HttpHeaders headers = new HttpHeaders(); |
||||
headers.setContentType(MediaType.APPLICATION_PDF); |
||||
headers.setContentDispositionFormData("attachment", "rearranged.pdf"); |
||||
headers.setContentLength(outputStream.size()); |
||||
|
||||
// Return the response with the PDF data and headers
|
||||
return new ResponseEntity<>(outputStream.toByteArray(), headers, HttpStatus.OK); |
||||
} catch (IOException e) { |
||||
|
||||
logger.error("Failed rearranging documents", e); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,140 @@ |
||||
package stirling.software.SPDF.controller; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStream; |
||||
import java.net.URI; |
||||
import java.nio.file.FileSystem; |
||||
import java.nio.file.FileSystems; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.Paths; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument; |
||||
import org.apache.pdfbox.pdmodel.PDPage; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.core.io.ByteArrayResource; |
||||
import org.springframework.core.io.Resource; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
@Controller |
||||
public class SplitPDFController { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); |
||||
|
||||
@GetMapping("/split-pdfs") |
||||
public String splitPdfForm() { |
||||
return "split-pdfs"; |
||||
} |
||||
|
||||
@PostMapping("/split-pages") |
||||
public ResponseEntity<Resource> splitPdf(@RequestParam("fileInput") MultipartFile file, |
||||
@RequestParam("pages") String pages) throws IOException { |
||||
// parse user input
|
||||
|
||||
// open the pdf document
|
||||
InputStream inputStream = file.getInputStream(); |
||||
PDDocument document = PDDocument.load(inputStream); |
||||
|
||||
List<Integer> pageNumbers = new ArrayList<>(); |
||||
pages = pages.replaceAll("\\s+", ""); // remove whitespaces
|
||||
if (pages.toLowerCase().equals("all")) { |
||||
for (int i = 0; i < document.getNumberOfPages(); i++) { |
||||
pageNumbers.add(i); |
||||
} |
||||
} else { |
||||
List<String> pageNumbersStr = new ArrayList<>(Arrays.asList(pages.split(","))); |
||||
if (!pageNumbersStr.contains(String.valueOf(document.getNumberOfPages()))) { |
||||
String lastpage = String.valueOf(document.getNumberOfPages()); |
||||
pageNumbersStr.add(lastpage); |
||||
} |
||||
for (String page : pageNumbersStr) { |
||||
if (page.contains("-")) { |
||||
String[] range = page.split("-"); |
||||
int start = Integer.parseInt(range[0]); |
||||
int end = Integer.parseInt(range[1]); |
||||
for (int i = start; i <= end; i++) { |
||||
pageNumbers.add(i); |
||||
} |
||||
} else { |
||||
pageNumbers.add(Integer.parseInt(page)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
logger.info("Splitting PDF into pages: {}", |
||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); |
||||
|
||||
// split the document
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); |
||||
int currentPage = 0; |
||||
for (int pageNumber : pageNumbers) { |
||||
try (PDDocument splitDocument = new PDDocument()) { |
||||
for (int i = currentPage; i < pageNumber; i++) { |
||||
PDPage page = document.getPage(i); |
||||
splitDocument.addPage(page); |
||||
logger.debug("Adding page {} to split document", i); |
||||
} |
||||
currentPage = pageNumber; |
||||
logger.debug("Setting current page to {}", currentPage); |
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
splitDocument.save(baos); |
||||
|
||||
splitDocumentsBoas.add(baos); |
||||
} catch (Exception e) { |
||||
logger.error("Failed splitting documents and saving them", e); |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
// closing the original document
|
||||
document.close(); |
||||
|
||||
// create the zip file
|
||||
Path zipFile = Paths.get("split_documents.zip"); |
||||
URI uri = URI.create("jar:file:" + zipFile.toUri().getPath()); |
||||
Map<String, String> env = new HashMap<>(); |
||||
env.put("create", "true"); |
||||
FileSystem zipfs = FileSystems.newFileSystem(uri, env); |
||||
|
||||
// loop through the split documents and write them to the zip file
|
||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) { |
||||
String fileName = "split_document_" + (i + 1) + ".pdf"; |
||||
ByteArrayOutputStream baos = splitDocumentsBoas.get(i); |
||||
byte[] pdf = baos.toByteArray(); |
||||
Path pathInZipfile = zipfs.getPath(fileName); |
||||
try (OutputStream os = Files.newOutputStream(pathInZipfile)) { |
||||
os.write(pdf); |
||||
logger.info("Wrote split document {} to zip file", fileName); |
||||
} catch (Exception e) { |
||||
logger.error("Failed writing to zip", e); |
||||
throw e; |
||||
} |
||||
} |
||||
zipfs.close(); |
||||
logger.info("Successfully created zip file with split documents: {}", zipFile.toString()); |
||||
byte[] data = Files.readAllBytes(zipFile); |
||||
ByteArrayResource resource = new ByteArrayResource(data); |
||||
new File("split_documents.zip").delete(); |
||||
// return the Resource in the response
|
||||
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=split_documents.zip") |
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource); |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
package stirling.software.SPDF.controller; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.multipart.MultipartFile; |
||||
|
||||
import stirling.software.SPDF.utils.PdfUtils; |
||||
|
||||
@Controller |
||||
public class ToPDFController { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToPDFController.class); |
||||
|
||||
@GetMapping("/convert-to-pdf") |
||||
public String convertToPdfForm() { |
||||
return "convert-to-pdf"; |
||||
} |
||||
|
||||
@PostMapping("/convert-to-pdf") |
||||
public ResponseEntity<byte[]> convertToPdf(@RequestParam("fileInput") MultipartFile file) throws IOException { |
||||
// Convert the file to PDF and get the resulting bytes
|
||||
byte[] bytes = PdfUtils.convertToPdf(file.getInputStream()); |
||||
logger.info("File {} successfully converted to pdf", file.getOriginalFilename()); |
||||
|
||||
HttpHeaders headers = new HttpHeaders(); |
||||
headers.setContentType(MediaType.APPLICATION_PDF); |
||||
String filename = "converted.pdf"; |
||||
headers.setContentDispositionFormData(filename, filename); |
||||
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0"); |
||||
ResponseEntity<byte[]> response = new ResponseEntity<>(bytes, headers, HttpStatus.OK); |
||||
return response; |
||||
} |
||||
} |
@ -0,0 +1,124 @@ |
||||
package stirling.software.SPDF.utils; |
||||
|
||||
import java.awt.image.BufferedImage; |
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.File; |
||||
import java.io.FileOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.util.Iterator; |
||||
|
||||
import javax.imageio.IIOImage; |
||||
import javax.imageio.ImageIO; |
||||
import javax.imageio.ImageWriter; |
||||
import javax.imageio.stream.ImageOutputStream; |
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument; |
||||
import org.apache.pdfbox.pdmodel.PDPage; |
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream; |
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; |
||||
import org.apache.pdfbox.rendering.ImageType; |
||||
import org.apache.pdfbox.rendering.PDFRenderer; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
public class PdfUtils { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); |
||||
|
||||
public static byte[] convertToPdf(InputStream imageStream) throws IOException { |
||||
|
||||
// Create a File object for the image
|
||||
File imageFile = new File("image.jpg"); |
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(imageFile); InputStream input = imageStream) { |
||||
byte[] buffer = new byte[1024]; |
||||
int len; |
||||
// Read from the input stream and write to the file
|
||||
while ((len = input.read(buffer)) != -1) { |
||||
fos.write(buffer, 0, len); |
||||
} |
||||
logger.info("Image successfully written to file: {}", imageFile.getAbsolutePath()); |
||||
} catch (IOException e) { |
||||
logger.error("Error writing image to file: {}", imageFile.getAbsolutePath(), e); |
||||
throw e; |
||||
} |
||||
|
||||
try (PDDocument doc = new PDDocument()) { |
||||
// Create a new PDF page
|
||||
PDPage page = new PDPage(); |
||||
doc.addPage(page); |
||||
|
||||
// Create an image object from the image file
|
||||
PDImageXObject image = PDImageXObject.createFromFileByContent(imageFile, doc); |
||||
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { |
||||
// Draw the image onto the page
|
||||
contentStream.drawImage(image, 0, 0); |
||||
logger.info("Image successfully added to PDF"); |
||||
} catch (IOException e) { |
||||
logger.error("Error adding image to PDF", e); |
||||
throw e; |
||||
} |
||||
|
||||
// Create a ByteArrayOutputStream to save the PDF to
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
||||
doc.save(byteArrayOutputStream); |
||||
logger.info("PDF successfully saved to byte array"); |
||||
return byteArrayOutputStream.toByteArray(); |
||||
} |
||||
} |
||||
|
||||
public static byte[] convertFromPdf(byte[] inputStream, String imageType) throws IOException { |
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { |
||||
// Create a PDFRenderer to convert the PDF to an image
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document); |
||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(0, 300, ImageType.RGB); |
||||
|
||||
// Get an ImageWriter for the specified image type
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(imageType); |
||||
ImageWriter writer = writers.next(); |
||||
|
||||
// Create a ByteArrayOutputStream to save the image to
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) { |
||||
writer.setOutput(ios); |
||||
// Write the image to the output stream
|
||||
writer.write(new IIOImage(bim, null, null)); |
||||
// Log that the image was successfully written to the byte array
|
||||
logger.info("Image successfully written to byte array"); |
||||
} |
||||
return baos.toByteArray(); |
||||
} catch (IOException e) { |
||||
// Log an error message if there is an issue converting the PDF to an image
|
||||
logger.error("Error converting PDF to image", e); |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
public static byte[] overlayImage(byte[] pdfBytes, byte[] imageBytes, float x, float y) throws IOException { |
||||
|
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) { |
||||
// Get the first page of the PDF
|
||||
PDPage page = document.getPage(0); |
||||
try (PDPageContentStream contentStream = new PDPageContentStream(document, page, |
||||
PDPageContentStream.AppendMode.APPEND, true)) { |
||||
// Create an image object from the image bytes
|
||||
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, ""); |
||||
// Draw the image onto the page at the specified x and y coordinates
|
||||
contentStream.drawImage(image, x, y); |
||||
logger.info("Image successfully overlayed onto PDF"); |
||||
} |
||||
// Create a ByteArrayOutputStream to save the PDF to
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
document.save(baos); |
||||
logger.info("PDF successfully saved to byte array"); |
||||
return baos.toByteArray(); |
||||
} catch (IOException e) { |
||||
// Log an error message if there is an issue overlaying the image onto the PDF
|
||||
logger.error("Error overlaying image onto PDF", e); |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
spring.http.multipart.max-file-size=1GB |
||||
spring.http.multipart.max-request-size=1GB |
||||
|
||||
multipart.enabled=true |
||||
multipart.max-file-size=1000MB |
||||
multipart.max-request-size=1000MB |
||||
|
||||
spring.servlet.multipart.max-file-size=1000MB |
||||
spring.servlet.multipart.max-request-size=1000MB |
@ -0,0 +1,8 @@ |
||||
log4j.rootLogger=ERROR,stdout |
||||
log4j.logger.com.endeca=INFO |
||||
# Logger for crawl metrics |
||||
log4j.logger.com.endeca.itl.web.metrics=INFO |
||||
|
||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender |
||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout |
||||
log4j.appender.stdout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t[%t]\t%m%n |
@ -0,0 +1,14 @@ |
||||
/* Dark Mode Styles */ |
||||
body { |
||||
background-color: #333; |
||||
color: #fff; |
||||
} |
||||
|
||||
.dark-card { |
||||
background-color: #333 !important; |
||||
color: white; |
||||
} |
||||
.jumbotron { |
||||
background-color: #222; /* or any other dark color */ |
||||
color: #fff; /* or any other light color */ |
||||
} |
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,5 @@ |
||||
#footer { |
||||
position: absolute; |
||||
bottom: 0; |
||||
width: 100%; |
||||
} |
@ -0,0 +1,51 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
|
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF Add-Image</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<br> |
||||
<br> |
||||
<div class="container"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-md-6"> |
||||
<h2>Add image to PDF</h2> |
||||
|
||||
|
||||
|
||||
<form method="post" th:action="@{/add-image}" |
||||
enctype="multipart/form-data"> |
||||
|
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput" |
||||
name="fileInput" required> <label |
||||
class="custom-file-label" for="fileInput">Choose PDF</label> |
||||
</div> |
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput2" |
||||
name="fileInput2" required> <label |
||||
class="custom-file-label" for="fileInput2">Choose Image</label> |
||||
</div> |
||||
<div class="form-group"> |
||||
<label for="x">X</label> <input type="number" class="form-control" |
||||
id="x" name="x" step="0.01" required> |
||||
</div> |
||||
<div class="form-group"> |
||||
<label for="y">Y</label> <input type="number" class="form-control" |
||||
id="y" name="y" step="0.01" required> |
||||
</div> |
||||
<button type="submit" class="btn btn-primary">Submit</button> |
||||
</form> |
||||
<th:block th:insert="~{common :: filelist}"></th:block> |
||||
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
|
||||
</html> |
@ -0,0 +1,77 @@ |
||||
<head th:fragment="head"> |
||||
<link rel="shortcut icon" href="favicon.svg"> |
||||
<script |
||||
src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script> |
||||
<link rel="stylesheet" |
||||
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> |
||||
<script |
||||
src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> |
||||
<meta charset="UTF-8"> |
||||
|
||||
<link rel="stylesheet" th:href="@{dark-mode.css}" id="dark-mode-styles"> |
||||
<script> |
||||
function toggleDarkMode() { |
||||
var checkbox = document.getElementById("toggle-dark-mode"); |
||||
var darkModeStyles = document.getElementById("dark-mode-styles"); |
||||
if (checkbox.checked) { |
||||
localStorage.setItem("dark-mode", "on"); |
||||
darkModeStyles.disabled = false; |
||||
} else { |
||||
localStorage.setItem("dark-mode", "off"); |
||||
darkModeStyles.disabled = true; |
||||
} |
||||
} |
||||
$(document).ready(function () { |
||||
var darkModeStyles = document.getElementById("dark-mode-styles"); |
||||
var checkbox = document.getElementById("toggle-dark-mode"); |
||||
if(localStorage.getItem("dark-mode") == "on"){ |
||||
darkModeStyles.disabled = false; |
||||
checkbox.checked = true; |
||||
} |
||||
if(localStorage.getItem("dark-mode") == "off"){ |
||||
darkModeStyles.disabled = true; |
||||
checkbox.checked = false; |
||||
} |
||||
|
||||
}); |
||||
</script> |
||||
<link rel="stylesheet" href="general.css"> |
||||
</head> |
||||
|
||||
<th:block th:fragment="filelist"> |
||||
<div id="fileList"></div> |
||||
<div id="fileList2"></div> |
||||
<script> |
||||
var input = document.getElementById("fileInput"); |
||||
var output = document.getElementById("fileList"); |
||||
|
||||
input.addEventListener("change", function() { |
||||
var files = input.files; |
||||
var fileNames = ""; |
||||
|
||||
for (var i = 0; i < files.length; i++) { |
||||
fileNames += (i + 1) + ". " + files[i].name + "<br>"; |
||||
} |
||||
|
||||
output.innerHTML = fileNames; |
||||
}); |
||||
|
||||
</script> |
||||
<script> |
||||
var input2 = document.getElementById("fileInput2"); |
||||
var output2 = document.getElementById("fileList2"); |
||||
if(input2 != null && !input2) { |
||||
input2.addEventListener("change", function() { |
||||
var files = input2.files; |
||||
var fileNames = ""; |
||||
|
||||
for (var i = 0; i < files.length; i++) { |
||||
fileNames += (i + 1) + ". " + files[i].name + "<br>"; |
||||
} |
||||
|
||||
output2.innerHTML = fileNames; |
||||
}); |
||||
} |
||||
</script> |
||||
</th:block> |
||||
|
@ -0,0 +1,41 @@ |
||||
|
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF ConvertFromPDF</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<br> |
||||
<br> |
||||
<div class="container"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-md-6"> |
||||
<h2>PDF to img</h2> |
||||
<form method="post" enctype="multipart/form-data" |
||||
th:action="@{/convert-to-image}"> |
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput" |
||||
name="fileInput" required> <label |
||||
class="custom-file-label" for="fileInput">Choose PDF</label> |
||||
</div> |
||||
<div class="form-group"> |
||||
<label>Image Format</label> <select class="form-control" |
||||
name="imageFormat"> |
||||
<option value="jpg">JPEG</option> |
||||
<option value="png">PNG</option> |
||||
<option value="gif">GIF</option> |
||||
</select> |
||||
</div> |
||||
<button type="submit" class="btn btn-primary">Convert</button> |
||||
</form> |
||||
<th:block th:insert="~{common :: filelist}"></th:block> |
||||
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
</html> |
@ -0,0 +1,33 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF ConvertToPDF</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<br> |
||||
<br> |
||||
<div class="container"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-md-6"> |
||||
<h2>Image to PDF</h2> |
||||
|
||||
<form method="post" enctype="multipart/form-data" |
||||
th:action="@{/convert-to-pdf}"> |
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput" |
||||
name="fileInput" required> <label |
||||
class="custom-file-label" for="fileInput">Choose Image</label> |
||||
</div> |
||||
<button type="submit" class="btn btn-primary">Convert</button> |
||||
|
||||
</form> |
||||
<th:block th:insert="~{common :: filelist}"></th:block> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
</html> |
@ -0,0 +1,11 @@ |
||||
<div th:fragment="footer"> |
||||
<link rel="stylesheet" |
||||
href="https://use.fontawesome.com/releases/v5.6.1/css/all.css"> |
||||
<footer id="footer" class="text-center py-3"> |
||||
<a href="https://github.com/Frooodle" target="_blank" class="mx-1"> |
||||
<i class="fab fa-github fa-2x"></i> |
||||
</a> <a href="https://hub.docker.com/u/frooodle" target="_blank" |
||||
class="mx-1"> <i class="fab fa-docker fa-2x"></i> |
||||
</a> |
||||
</footer> |
||||
</div> |
@ -0,0 +1,84 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<!-- Jumbotron --> |
||||
<div class="jumbotron jumbotron-fluid" id="jumbotron"> |
||||
<div class="container"> |
||||
<h1 class="display-4">Stirling PDF</h1> |
||||
<p class="lead">Your locally hosted one-stop-shop for all your |
||||
PDF needs. (Made 100% in ChatGPT in 1 day as a experiment)</p> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Features --> |
||||
<div class="container"> |
||||
<div class="row h-100"> |
||||
<div class="col-4 h-100"> |
||||
<div class="dark-card card"> |
||||
<div class="card-body"> |
||||
<h5 class="card-title">Merge PDFs</h5> |
||||
<p class="card-text">Easily merge multiple PDFs into one.</p> |
||||
<a href="#" class="btn btn-primary" th:href="@{/merge-pdfs}">Go</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col-4 h-100"> |
||||
<div class="dark-card card"> |
||||
<div class="card-body"> |
||||
<h5 class="card-title">Split PDFs</h5> |
||||
<p class="card-text">Split your PDFs into multiple single-page |
||||
documents or at specific page numbers.</p> |
||||
<a href="#" class="btn btn-primary" th:href="@{/split-pdfs}">Go</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col-4 h-100"> |
||||
<div class="dark-card card"> |
||||
<div class="card-body"> |
||||
<h5 class="card-title">Convert to PDF</h5> |
||||
<p class="card-text">Convert images to PDF.</p> |
||||
<a href="#" class="btn btn-primary" th:href="@{/convert-to-pdf}">Go</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="row h-100"> |
||||
<div class="col-4 h-100"> |
||||
<div class="dark-card card"> |
||||
<div class="card-body dark-card"> |
||||
<h5 class="card-title">Convert from PDF</h5> |
||||
<p class="card-text">Convert PDF to Image.</p> |
||||
<a href="#" class="btn btn-primary" th:href="@{/convert-from-pdf}">Go</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col-4 h-100"> |
||||
<div class="dark-card card"> |
||||
<div class="card-body"> |
||||
<h5 class="card-title">Add image to PDF</h5> |
||||
<p class="card-text">Adds image/watermark to a PDF</p> |
||||
<a href="#" class="btn btn-primary" th:href="@{/add-image}">Go</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col-4 h-100"> |
||||
<div class="dark-card card"> |
||||
<div class="card-body"> |
||||
<h5 class="card-title">PDF Organizer</h5> |
||||
<p class="card-text">Rearrange PDF pages into any order (or |
||||
remove)</p> |
||||
<a href="#" class="btn btn-primary" th:href="@{/pdf-organizer}">Go</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
</html> |
@ -0,0 +1,61 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF MergePDFs</title> |
||||
|
||||
<script> |
||||
dropContainer.ondragover = dropContainer.ondragenter = function(evt) { |
||||
evt.preventDefault(); |
||||
}; |
||||
|
||||
dropContainer.ondrop = function(evt) { |
||||
// pretty simple -- but not for IE :( |
||||
fileInput.files = evt.dataTransfer.files; |
||||
|
||||
// If you want to use some of the dropped files |
||||
const dT = new DataTransfer(); |
||||
dT.items.add(evt.dataTransfer.files[0]); |
||||
dT.items.add(evt.dataTransfer.files[3]); |
||||
fileInput.files = dT.files; |
||||
|
||||
evt.preventDefault(); |
||||
}; |
||||
</script> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<br> |
||||
<br> |
||||
<div class="container"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-md-6" id="dropContainer"> |
||||
<h2>Merge multiple PDFs (2+)</h2> |
||||
<form action="/merge-pdfs" method="post" |
||||
enctype="multipart/form-data"> |
||||
<div class="form-group"> |
||||
<label>Select (or drag & drop) all PDFs to merge</label> |
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput" |
||||
name="fileInput" multiple> <label |
||||
class="custom-file-label">Choose PDFs</label> |
||||
</div> |
||||
</div> |
||||
<div class="form-group text-center"> |
||||
<button type="submit" class="btn btn-primary">Merge</button> |
||||
</div> |
||||
</form> |
||||
<th:block th:insert="~{common :: filelist}"></th:block> |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
</html> |
@ -0,0 +1,47 @@ |
||||
<div th:fragment="navbar"> |
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light"> |
||||
<div class="container"> |
||||
<a class="navbar-brand" href="#" th:href="@{/home}">Stirling PDF</a> |
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" |
||||
data-target="#navbarNav" aria-controls="navbarNav" |
||||
aria-expanded="false" aria-label="Toggle navigation"> |
||||
<span class="navbar-toggler-icon"></span> |
||||
</button> |
||||
<div class="collapse navbar-collapse" id="navbarNav"> |
||||
<ul class="navbar-nav"> |
||||
|
||||
<li class="nav-item"><a class="nav-link" href="#" |
||||
th:href="@{/merge-pdfs}" |
||||
th:classappend="${currentPage}=='/merge-pdfs' ? 'active' : ''">Merge |
||||
PDFs</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="#" |
||||
th:href="@{/split-pdfs}" |
||||
th:classappend="${currentPage}=='/split-pdfs' ? 'active' : ''">Split |
||||
PDFs</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="#" |
||||
th:href="@{/convert-to-pdf}" |
||||
th:classappend="${currentPage}=='/convert-to-pdf' ? 'active' : ''">Convert |
||||
to PDF</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="#" |
||||
th:href="@{/convert-from-pdf}" |
||||
th:classappend="${currentPage}=='/convert-from-pdf' ? 'active' : ''">Convert |
||||
from PDF</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="#" |
||||
th:href="@{/add-image}" |
||||
th:classappend="${currentPage}=='/add-image' ? 'active' : ''">Add |
||||
image to PDF</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="#" |
||||
th:href="@{/pdf-organizer}" |
||||
th:classappend="${currentPage}=='/pdf-organizer' ? 'active' : ''">PDF |
||||
Organizer</a></li> |
||||
|
||||
<input type="checkbox" id="toggle-dark-mode" |
||||
th:onclick="javascript:toggleDarkMode()"> |
||||
<a class="nav-link" href="#" for="toggle-dark-mode">Dark Mode</a> |
||||
|
||||
</ul> |
||||
|
||||
</div> |
||||
</div> |
||||
</nav> |
||||
</div> |
@ -0,0 +1,42 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF Organizer</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<br> |
||||
<br> |
||||
<div class="container"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-md-6"> |
||||
<h2>PDF Page Organizer</h2> |
||||
|
||||
|
||||
|
||||
<form th:action="@{/rearrange-pages}" method="post" |
||||
enctype="multipart/form-data"> |
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput" |
||||
name="fileInput" required> <label |
||||
class="custom-file-label" for="fileInput">Choose PDF</label> |
||||
</div> |
||||
<div class="form-group"> |
||||
<label for="pageOrder">Page Order (Enter a comma-separated |
||||
list of page numbers) :</label> <input type="text" class="form-control" |
||||
id="fileInput" name="pageOrder" |
||||
placeholder="(e.g. 1,3,2 or 4-8,2,10-12)" required> |
||||
</div> |
||||
<button type="submit" class="btn btn-primary">Rearrange |
||||
Pages</button> |
||||
</form> |
||||
<th:block th:insert="~{common :: filelist}"></th:block> |
||||
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
</html> |
@ -0,0 +1,49 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org"> |
||||
<head> |
||||
<th:block th:insert="~{common :: head}"></th:block> |
||||
<title>S-PDF Split PDFs</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div th:insert="~{navbar.html :: navbar}"></div> |
||||
<br> |
||||
<br> |
||||
<div class="container"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-md-6"> |
||||
<h1>Split PDF</h1> |
||||
<p>The numbers you select are the page number you wish to do a |
||||
split on</p> |
||||
<p>As such selecting 1,3,7-8 would split a 12 page document into |
||||
6 separate PDFS with:</p> |
||||
<p>Document #1: Page 1</p> |
||||
<p>Document #2: Page 2 and 3</p> |
||||
<p>Document #3: Page 4, 5 and 6</p> |
||||
<p>Document #4: Page 7</p> |
||||
<p>Document #5: Page 8</p> |
||||
<p>Document #6: Page 9 and 10</p> |
||||
|
||||
<form th:action="@{/split-pages}" method="post" |
||||
enctype="multipart/form-data"> |
||||
<div class="custom-file"> |
||||
<input type="file" class="custom-file-input" id="fileInput" |
||||
name="fileInput" required> <label |
||||
class="custom-file-label" for="fileInput">Choose PDF</label> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label for="pages">Enter pages to split on:</label> <input |
||||
type="text" class="form-control" id="pages" name="pages" |
||||
placeholder="1,3,5-10"> |
||||
</div> |
||||
<br> |
||||
<button type="submit" class="btn btn-primary">Submit</button> |
||||
</form> |
||||
<th:block th:insert="~{common :: filelist}"></th:block> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div th:insert="~{footer.html :: footer}"></div> |
||||
</body> |
||||
</html> |
Loading…
Reference in new issue