mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2024-11-05 23:40:11 +01:00
commit
fefa8347da
44
Dockerfile
44
Dockerfile
@ -1,31 +1,39 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# Use the base image
|
||||||
FROM frooodle/stirling-pdf-base:beta4
|
FROM frooodle/stirling-pdf-base:beta4
|
||||||
|
|
||||||
ARG VERSION_TAG
|
# Set Environment Variables
|
||||||
ENV VERSION_TAG=$VERSION_TAG
|
ENV PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
DOCKER_ENABLE_SECURITY=false \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG
|
||||||
|
|
||||||
ENV DOCKER_ENABLE_SECURITY=false
|
# Create user and group
|
||||||
|
RUN groupadd -g $PGID stirlingpdfgroup && \
|
||||||
|
useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
||||||
|
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
# Create scripts folder and copy local scripts
|
# Set up necessary directories and permissions
|
||||||
RUN mkdir /scripts
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
COPY ./scripts/* /scripts/
|
COPY ./scripts/* /scripts/
|
||||||
|
|
||||||
#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/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
RUN fc-cache -f -v
|
|
||||||
|
|
||||||
# Always copy the JAR
|
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Expose the application port
|
# Set font cache and permissions
|
||||||
|
RUN fc-cache -f -v && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
chmod +x /scripts/init.sh
|
||||||
|
|
||||||
|
# Expose necessary ports
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Set environment variables
|
# Set user and run command
|
||||||
ENV APP_HOME_NAME="Stirling PDF"
|
USER stirlingpdfuser
|
||||||
|
|
||||||
# Run the application
|
|
||||||
RUN chmod +x /scripts/init.sh
|
|
||||||
ENTRYPOINT ["/scripts/init.sh"]
|
ENTRYPOINT ["/scripts/init.sh"]
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-jar", "/app.jar"]
|
||||||
|
@ -10,17 +10,43 @@ RUN apt-get update && \
|
|||||||
unoconv && \
|
unoconv && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy the application JAR file
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
DOCKER_ENABLE_SECURITY=false \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG
|
||||||
|
|
||||||
|
# Create user and group
|
||||||
|
RUN groupadd -g $PGID stirlingpdfgroup && \
|
||||||
|
useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
||||||
|
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
|
# Set up necessary directories and permissions
|
||||||
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
|
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
# Set font cache and permissions
|
||||||
|
RUN fc-cache -f -v && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Expose the application port
|
# Expose the application port
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
|
||||||
ENV DOCKER_ENABLE_SECURITY=false
|
ENV DOCKER_ENABLE_SECURITY=false
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
|
USER stirlingpdfuser
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-jar", "/app.jar"]
|
||||||
|
@ -1,16 +1,33 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# Build jbig2enc in a separate stage
|
||||||
FROM bellsoft/liberica-openjdk-alpine:17
|
FROM bellsoft/liberica-openjdk-alpine:17
|
||||||
|
|
||||||
# Copy the application JAR file
|
# Set Environment Variables
|
||||||
|
ENV PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
DOCKER_ENABLE_SECURITY=false \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG
|
||||||
|
|
||||||
|
# Create user and group using Alpine's addgroup and adduser
|
||||||
|
RUN addgroup -g $PGID stirlingpdfgroup && \
|
||||||
|
adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
|
||||||
|
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
|
# Set up necessary directories and permissions
|
||||||
|
RUN mkdir -p /scripts /configs /customFiles && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles
|
||||||
|
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
# Set font cache and permissions
|
||||||
|
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
# Expose the application port
|
# Expose the application port
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
ENV DOCKER_ENABLE_SECURITY=false
|
ENV DOCKER_ENABLE_SECURITY=false
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
|
@ -66,7 +66,6 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
|
|||||||
## Technologies used
|
## Technologies used
|
||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- PDFBox
|
- PDFBox
|
||||||
- IText7
|
|
||||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||||
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
|
@ -86,9 +86,9 @@ dependencies {
|
|||||||
|
|
||||||
//general PDF
|
//general PDF
|
||||||
implementation 'org.apache.pdfbox:pdfbox:2.0.29'
|
implementation 'org.apache.pdfbox:pdfbox:2.0.29'
|
||||||
|
implementation 'org.apache.pdfbox:xmpbox:2.0.29'
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
||||||
implementation 'com.itextpdf:itext7-core:7.2.5'
|
|
||||||
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'
|
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
|
||||||
|
@ -9,7 +9,6 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import stirling.software.SPDF.config.ConfigInitializer;
|
import stirling.software.SPDF.config.ConfigInitializer;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
@ -66,7 +65,6 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
GeneralUtils.createDir("customFiles/static/");
|
GeneralUtils.createDir("customFiles/static/");
|
||||||
GeneralUtils.createDir("customFiles/templates/");
|
GeneralUtils.createDir("customFiles/templates/");
|
||||||
GeneralUtils.createDir("config");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
@Bean(name = "loginEnabled")
|
||||||
public boolean loginEnabled() {
|
public boolean loginEnabled() {
|
||||||
System.out.println(applicationProperties.toString());
|
|
||||||
return applicationProperties.getSecurity().getEnableLogin();
|
return applicationProperties.getSecurity().getEnableLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
|
|
||||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file");
|
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -32,7 +32,6 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
if (keyValue.length != 2) {
|
if (keyValue.length != 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
||||||
parameters.put(keyValue[0], keyValue[1]);
|
parameters.put(keyValue[0], keyValue[1]);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
@ -37,7 +45,57 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
|||||||
throw new FileNotFoundException("Resource file not found: settings.yml.template");
|
throw new FileNotFoundException("Resource file not found: settings.yml.template");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If user file exists, we need to merge it with the template from the classpath
|
||||||
|
List<String> templateLines;
|
||||||
|
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||||
|
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines().collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeYamlFiles(templateLines, destPath, destPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException {
|
||||||
|
List<String> userLines = Files.readAllLines(userFilePath);
|
||||||
|
|
||||||
|
List<String> mergedLines = new ArrayList<>();
|
||||||
|
boolean insideAutoGenerated = false;
|
||||||
|
|
||||||
|
for (String line : templateLines) {
|
||||||
|
// Check if we've entered or left the AutomaticallyGenerated section
|
||||||
|
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
|
||||||
|
insideAutoGenerated = true;
|
||||||
|
mergedLines.add(line);
|
||||||
|
continue;
|
||||||
|
} else if (insideAutoGenerated && line.trim().isEmpty()) {
|
||||||
|
// We have reached the end of the AutomaticallyGenerated section
|
||||||
|
insideAutoGenerated = false;
|
||||||
|
mergedLines.add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insideAutoGenerated) {
|
||||||
|
// Add lines from user's settings if we are inside AutomaticallyGenerated
|
||||||
|
Optional<String> userAutoGenValue = userLines.stream().filter(l -> l.trim().startsWith(line.split(":")[0].trim())).findFirst();
|
||||||
|
if (userAutoGenValue.isPresent()) {
|
||||||
|
mergedLines.add(userAutoGenValue.get());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Outside of AutomaticallyGenerated, continue as before
|
||||||
|
if (line.contains(": ")) {
|
||||||
|
String key = line.split(": ")[0].trim();
|
||||||
|
Optional<String> userValue = userLines.stream().filter(l -> l.trim().startsWith(key)).findFirst();
|
||||||
|
if (userValue.isPresent()) {
|
||||||
|
mergedLines.add(userValue.get());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mergedLines.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,9 +1,5 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@ -18,14 +14,9 @@ public class OpenApiConfig {
|
|||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
String version = getClass().getPackage().getImplementationVersion();
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
Properties props = new Properties();
|
|
||||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
|
|
||||||
props.load(input);
|
|
||||||
version = props.getProperty("version");
|
|
||||||
} catch (IOException ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
version = "1.0.0"; // default version if all else fails
|
version = "1.0.0"; // default version if all else fails
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OpenAPI().components(new Components()).info(
|
return new OpenAPI().components(new Components()).info(
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
String method = request.getMethod();
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
// Check if the request is for static resources
|
||||||
|
boolean isStaticResource = requestURI.startsWith("/css/")
|
||||||
|
|| requestURI.startsWith("/js/")
|
||||||
|
|| requestURI.startsWith("/images/")
|
||||||
|
|| requestURI.startsWith("/public/")
|
||||||
|
|| requestURI.endsWith(".svg");
|
||||||
|
|
||||||
|
// If it's a static resource, just continue the filter chain and skip the logic below
|
||||||
|
if (isStaticResource) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
Optional<User> user = userService.findByUsername(authentication.getName());
|
||||||
|
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) {
|
||||||
|
response.sendRedirect("/change-creds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
@ -11,27 +11,26 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class InitialSecuritySetup {
|
public class InitialSecuritySetup {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ApplicationProperties applicationProperties;
|
ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
if (!userService.hasUsers()) {
|
if (!userService.hasUsers()) {
|
||||||
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
|
String initialUsername = "admin";
|
||||||
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
|
String initialPassword = "stirling";
|
||||||
if (initialUsername != null && initialPassword != null) {
|
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
||||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
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.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@ -42,6 +41,9 @@ public class SecurityConfiguration {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserAuthenticationFilter userAuthenticationFilter;
|
private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FirstLoginFilter firstLoginFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
@ -49,6 +51,7 @@ public class SecurityConfiguration {
|
|||||||
if(loginEnabledValue) {
|
if(loginEnabledValue) {
|
||||||
|
|
||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
http
|
http
|
||||||
.formLogin(formLogin -> formLogin
|
.formLogin(formLogin -> formLogin
|
||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
|
@ -44,7 +44,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
// Check for API key in the request headers if no authentication exists
|
// Check for API key in the request headers if no authentication exists
|
||||||
@ -74,14 +74,15 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
// If we still don't have any authentication, deny the request
|
// If we still don't have any authentication, deny the request
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
if ("GET".equalsIgnoreCase(method)) {
|
if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) {
|
||||||
response.sendRedirect("/login"); // redirect to the login page
|
response.sendRedirect("/login"); // redirect to the login page
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
|
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
@ -113,12 +113,23 @@ public class UserService {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, String password, String role, boolean firstLogin) {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.addAuthority(new Authority(role, user));
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(firstLogin);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role) {
|
public void saveUser(String username, String password, String role) {
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.addAuthority(new Authority(role, user));
|
user.addAuthority(new Authority(role, user));
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(false);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +179,12 @@ public class UserService {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeFirstUse(User user, boolean firstUse) {
|
||||||
|
user.setFirstLogin(firstUse);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
return passwordEncoder.matches(currentPassword, user.getPassword());
|
return passwordEncoder.matches(currentPassword, user.getPassword());
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,12 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -12,14 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
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 com.itextpdf.kernel.geom.PageSize;
|
|
||||||
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.xobject.PdfFormXObject;
|
|
||||||
|
|
||||||
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.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@ -32,7 +30,6 @@ public class CropController {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||||
|
|
||||||
|
|
||||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
@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")
|
@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(
|
public ResponseEntity<byte[]> cropPdf(
|
||||||
@ -40,39 +37,55 @@ public class CropController {
|
|||||||
@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 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 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 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 {
|
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height)
|
||||||
byte[] bytes = file.getBytes();
|
throws IOException {
|
||||||
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
|
|
||||||
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
|
|
||||||
PdfDocument pdfDoc = new PdfDocument(reader);
|
|
||||||
|
|
||||||
|
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(file.getBytes()));
|
||||||
|
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
|
for (int i = 0; i < totalPages; i++) {
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
|
|
||||||
|
// Create a new page with the size of the source page
|
||||||
|
PDPage newPage = new PDPage(sourcePage.getMediaBox());
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
|
||||||
|
|
||||||
|
// Import the source page as a form XObject
|
||||||
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
|
||||||
|
// Define the crop area
|
||||||
|
contentStream.addRect(x, y, width, height);
|
||||||
|
contentStream.clip();
|
||||||
|
|
||||||
|
// Draw the entire formXObject
|
||||||
|
contentStream.drawForm(formXObject);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
|
||||||
|
contentStream.close();
|
||||||
|
|
||||||
|
// Now, set the new page's media box to the cropped size
|
||||||
|
newPage.setMediaBox(new PDRectangle(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
PdfWriter writer = new PdfWriter(baos);
|
newDocument.save(baos);
|
||||||
PdfDocument outputPdf = new PdfDocument(writer);
|
newDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
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();
|
byte[] pdfContent = baos.toByteArray();
|
||||||
pdfDoc.close();
|
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfContent,
|
|
||||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -12,15 +19,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
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 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.xobject.PdfFormXObject;
|
|
||||||
|
|
||||||
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.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@ -34,68 +32,73 @@ public class MultiPageLayoutController {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||||
@Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
|
@Operation(
|
||||||
|
summary = "Merge multiple pages of a PDF document into a single page",
|
||||||
|
description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO"
|
||||||
|
)
|
||||||
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
||||||
@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 number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = {
|
@Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = {
|
||||||
"2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet)
|
"2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
if (pagesPerSheet != 2 && pagesPerSheet != 3
|
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
||||||
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
|
||||||
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
|
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
|
||||||
}
|
}
|
||||||
|
|
||||||
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
|
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
|
||||||
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
||||||
|
|
||||||
byte[] bytes = file.getBytes();
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
|
PDDocument newDocument = new PDDocument();
|
||||||
PdfDocument pdfDoc = new PdfDocument(reader);
|
PDPage newPage = new PDPage(PDRectangle.A4);
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
PdfWriter writer = new PdfWriter(baos);
|
float cellWidth = newPage.getMediaBox().getWidth() / cols;
|
||||||
PdfDocument outputPdf = new PdfDocument(writer);
|
float cellHeight = newPage.getMediaBox().getHeight() / rows;
|
||||||
PageSize pageSize = new PageSize(PageSize.A4.rotate());
|
|
||||||
|
|
||||||
int totalPages = pdfDoc.getNumberOfPages();
|
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
float cellWidth = pageSize.getWidth() / cols;
|
|
||||||
float cellHeight = pageSize.getHeight() / rows;
|
|
||||||
|
|
||||||
for (int i = 1; i <= totalPages; i += pagesPerSheet) {
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
PdfPage page = outputPdf.addNewPage(pageSize);
|
|
||||||
PdfCanvas pdfCanvas = new PdfCanvas(page);
|
|
||||||
|
|
||||||
for (int row = 0; row < rows; row++) {
|
for (int i = 0; i < totalPages; i++) {
|
||||||
for (int col = 0; col < cols; col++) {
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
int index = i + row * cols + col;
|
System.out.println("Reading page " + (i+1));
|
||||||
if (index <= totalPages) {
|
PDRectangle rect = sourcePage.getMediaBox();
|
||||||
// Get the page and calculate scaling factors
|
|
||||||
Rectangle rect = pdfDoc.getPage(index).getPageSize();
|
|
||||||
float scaleWidth = cellWidth / rect.getWidth();
|
float scaleWidth = cellWidth / rect.getWidth();
|
||||||
float scaleHeight = cellHeight / rect.getHeight();
|
float scaleHeight = cellHeight / rect.getHeight();
|
||||||
float scale = Math.min(scaleWidth, scaleHeight);
|
float scale = Math.min(scaleWidth, scaleHeight);
|
||||||
|
System.out.println("Scale for page " + (i+1) + ": " + scale);
|
||||||
|
|
||||||
PdfFormXObject formXObject = pdfDoc.getPage(index).copyAsFormXObject(outputPdf);
|
|
||||||
float x = col * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
|
||||||
float y = (rows - 1 - row) * cellHeight + (cellHeight - rect.getHeight() * scale) / 2;
|
|
||||||
|
|
||||||
// Save the graphics state, apply the transformations, add the object, and then
|
int rowIndex = i / cols;
|
||||||
// restore the graphics state
|
int colIndex = i % cols;
|
||||||
pdfCanvas.saveState();
|
|
||||||
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
|
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
||||||
pdfCanvas.addXObject(formXObject, 0, 0);
|
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
|
||||||
pdfCanvas.restoreState();
|
|
||||||
}
|
contentStream.saveGraphicsState();
|
||||||
}
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
}
|
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||||
|
|
||||||
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
contentStream.drawForm(formXObject);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
outputPdf.close();
|
|
||||||
byte[] pdfContent = baos.toByteArray();
|
|
||||||
pdfDoc.close();
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
contentStream.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
|
||||||
|
byte[] result = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,30 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
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.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
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 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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
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;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class ScalePagesController {
|
public class ScalePagesController {
|
||||||
@ -55,189 +38,76 @@ public class ScalePagesController {
|
|||||||
@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 targetPDRectangle,
|
||||||
@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)
|
@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, PDRectangle> sizeMap = new HashMap<>();
|
||||||
// Add A0 - A10
|
// Add A0 - A10
|
||||||
sizeMap.put("A0", PageSize.A0);
|
sizeMap.put("A0", PDRectangle.A0);
|
||||||
sizeMap.put("A1", PageSize.A1);
|
sizeMap.put("A1", PDRectangle.A1);
|
||||||
sizeMap.put("A2", PageSize.A2);
|
sizeMap.put("A2", PDRectangle.A2);
|
||||||
sizeMap.put("A3", PageSize.A3);
|
sizeMap.put("A3", PDRectangle.A3);
|
||||||
sizeMap.put("A4", PageSize.A4);
|
sizeMap.put("A4", PDRectangle.A4);
|
||||||
sizeMap.put("A5", PageSize.A5);
|
sizeMap.put("A5", PDRectangle.A5);
|
||||||
sizeMap.put("A6", PageSize.A6);
|
sizeMap.put("A6", PDRectangle.A6);
|
||||||
sizeMap.put("A7", PageSize.A7);
|
|
||||||
sizeMap.put("A8", PageSize.A8);
|
|
||||||
sizeMap.put("A9", PageSize.A9);
|
|
||||||
sizeMap.put("A10", PageSize.A10);
|
|
||||||
// Add B0 - B9
|
|
||||||
sizeMap.put("B0", PageSize.B0);
|
|
||||||
sizeMap.put("B1", PageSize.B1);
|
|
||||||
sizeMap.put("B2", PageSize.B2);
|
|
||||||
sizeMap.put("B3", PageSize.B3);
|
|
||||||
sizeMap.put("B4", PageSize.B4);
|
|
||||||
sizeMap.put("B5", PageSize.B5);
|
|
||||||
sizeMap.put("B6", PageSize.B6);
|
|
||||||
sizeMap.put("B7", PageSize.B7);
|
|
||||||
sizeMap.put("B8", PageSize.B8);
|
|
||||||
sizeMap.put("B9", PageSize.B9);
|
|
||||||
// Add other sizes
|
|
||||||
sizeMap.put("LETTER", PageSize.LETTER);
|
|
||||||
sizeMap.put("TABLOID", PageSize.TABLOID);
|
|
||||||
sizeMap.put("LEDGER", PageSize.LEDGER);
|
|
||||||
sizeMap.put("LEGAL", PageSize.LEGAL);
|
|
||||||
sizeMap.put("EXECUTIVE", PageSize.EXECUTIVE);
|
|
||||||
|
|
||||||
if (!sizeMap.containsKey(targetPageSize)) {
|
// Add other sizes
|
||||||
|
sizeMap.put("LETTER", PDRectangle.LETTER);
|
||||||
|
sizeMap.put("LEGAL", PDRectangle.LEGAL);
|
||||||
|
|
||||||
|
if (!sizeMap.containsKey(targetPDRectangle)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Invalid pageSize. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10");
|
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10");
|
||||||
}
|
}
|
||||||
|
|
||||||
PageSize pageSize = sizeMap.get(targetPageSize);
|
PDRectangle targetSize = sizeMap.get(targetPDRectangle);
|
||||||
|
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getBytes());
|
||||||
|
PDDocument outputDocument = new PDDocument();
|
||||||
|
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
for (int i = 0; i < totalPages; i++) {
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
|
PDRectangle sourceSize = sourcePage.getMediaBox();
|
||||||
|
|
||||||
|
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
|
||||||
|
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
|
||||||
|
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
|
||||||
|
|
||||||
|
PDPage newPage = new PDPage(targetSize);
|
||||||
|
outputDocument.addPage(newPage);
|
||||||
|
|
||||||
|
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
|
||||||
|
|
||||||
|
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
|
||||||
|
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
|
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(outputDocument);
|
||||||
|
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
contentStream.drawForm(form);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
byte[] bytes = file.getBytes();
|
|
||||||
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
|
|
||||||
PdfDocument pdfDoc = new PdfDocument(reader);
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
PdfWriter writer = new PdfWriter(baos);
|
outputDocument.save(baos);
|
||||||
PdfDocument outputPdf = new PdfDocument(writer);
|
outputDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
int totalPages = pdfDoc.getNumberOfPages();
|
|
||||||
|
|
||||||
for (int i = 1; i <= totalPages; i++) {
|
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
|
||||||
PdfPage page = outputPdf.addNewPage(pageSize);
|
|
||||||
PdfCanvas pdfCanvas = new PdfCanvas(page);
|
|
||||||
|
|
||||||
// Get the page and calculate scaling factors
|
|
||||||
Rectangle rect = pdfDoc.getPage(i).getPageSize();
|
|
||||||
float scaleWidth = pageSize.getWidth() / rect.getWidth();
|
|
||||||
float scaleHeight = pageSize.getHeight() / rect.getHeight();
|
|
||||||
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
|
|
||||||
System.out.println("Scale: " + scale);
|
|
||||||
|
|
||||||
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
|
|
||||||
float x = (pageSize.getWidth() - rect.getWidth() * scale) / 2; // Center Page
|
|
||||||
float y = (pageSize.getHeight() - rect.getHeight() * scale) / 2;
|
|
||||||
|
|
||||||
// Save the graphics state, apply the transformations, add the object, and then
|
|
||||||
// restore the graphics state
|
|
||||||
pdfCanvas.saveState();
|
|
||||||
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
|
|
||||||
pdfCanvas.addXObject(formXObject, 0, 0);
|
|
||||||
pdfCanvas.restoreState();
|
|
||||||
}
|
|
||||||
|
|
||||||
outputPdf.close();
|
|
||||||
byte[] pdfContent = baos.toByteArray();
|
|
||||||
pdfDoc.close();
|
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfContent,
|
|
||||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
|
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO
|
|
||||||
@Hidden
|
|
||||||
@PostMapping(value = "/auto-crop", consumes = "multipart/form-data")
|
|
||||||
public ResponseEntity<byte[]> cropPdf(@RequestParam("fileInput") MultipartFile file) throws IOException {
|
|
||||||
byte[] bytes = file.getBytes();
|
|
||||||
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 = pdfDoc.getPage(i);
|
|
||||||
Rectangle originalMediaBox = page.getMediaBox();
|
|
||||||
|
|
||||||
Rectangle contentBox = determineContentBox(page);
|
|
||||||
|
|
||||||
// Make sure we don't go outside the original media box.
|
|
||||||
Rectangle intersection = originalMediaBox.getIntersection(contentBox);
|
|
||||||
page.setCropBox(intersection);
|
|
||||||
|
|
||||||
// Copy page to the new document
|
|
||||||
outputPdf.addPage(page.copyTo(outputPdf));
|
|
||||||
}
|
|
||||||
|
|
||||||
outputPdf.close();
|
|
||||||
byte[] pdfContent = baos.toByteArray();
|
|
||||||
pdfDoc.close();
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""
|
|
||||||
+ file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf\"")
|
|
||||||
.contentType(MediaType.APPLICATION_PDF).body(pdfContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Rectangle determineContentBox(PdfPage page) {
|
|
||||||
// Extract the text from the page and find the bounding box.
|
|
||||||
TextBoundingRectangleFinder finder = new TextBoundingRectangleFinder();
|
|
||||||
PdfCanvasProcessor processor = new PdfCanvasProcessor(finder);
|
|
||||||
processor.processPageContent(page);
|
|
||||||
return finder.getBoundingBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TextBoundingRectangleFinder implements IEventListener {
|
|
||||||
private List<Rectangle> allTextBoxes = new ArrayList<>();
|
|
||||||
|
|
||||||
public Rectangle getBoundingBox() {
|
|
||||||
// Sort the text boxes based on their vertical position
|
|
||||||
allTextBoxes.sort(Comparator.comparingDouble(Rectangle::getTop));
|
|
||||||
|
|
||||||
// Consider a box an outlier if its top is more than 1.5 times the IQR above the
|
|
||||||
// third quartile.
|
|
||||||
int q1Index = allTextBoxes.size() / 4;
|
|
||||||
int q3Index = 3 * allTextBoxes.size() / 4;
|
|
||||||
double iqr = allTextBoxes.get(q3Index).getTop() - allTextBoxes.get(q1Index).getTop();
|
|
||||||
double threshold = allTextBoxes.get(q3Index).getTop() + 1.5 * iqr;
|
|
||||||
|
|
||||||
// Initialize boundingBox to the first non-outlier box
|
|
||||||
int i = 0;
|
|
||||||
while (i < allTextBoxes.size() && allTextBoxes.get(i).getTop() > threshold) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (i == allTextBoxes.size()) {
|
|
||||||
// If all boxes are outliers, just return the first one
|
|
||||||
return allTextBoxes.get(0);
|
|
||||||
}
|
|
||||||
Rectangle boundingBox = allTextBoxes.get(i);
|
|
||||||
|
|
||||||
// Extend the bounding box to include all non-outlier boxes
|
|
||||||
for (; i < allTextBoxes.size(); i++) {
|
|
||||||
Rectangle textBoundingBox = allTextBoxes.get(i);
|
|
||||||
if (textBoundingBox.getTop() > threshold) {
|
|
||||||
// This box is an outlier, skip it
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
float left = Math.min(boundingBox.getLeft(), textBoundingBox.getLeft());
|
|
||||||
float bottom = Math.min(boundingBox.getBottom(), textBoundingBox.getBottom());
|
|
||||||
float right = Math.max(boundingBox.getRight(), textBoundingBox.getRight());
|
|
||||||
float top = Math.max(boundingBox.getTop(), textBoundingBox.getTop());
|
|
||||||
|
|
||||||
// Add a small padding around the bounding box
|
|
||||||
float padding = 10;
|
|
||||||
boundingBox = new Rectangle(left - padding, bottom - padding, right - left + 2 * padding,
|
|
||||||
top - bottom + 2 * padding);
|
|
||||||
}
|
|
||||||
return boundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void eventOccurred(IEventData data, EventType type) {
|
|
||||||
if (type == EventType.RENDER_TEXT) {
|
|
||||||
TextRenderInfo renderInfo = (TextRenderInfo) data;
|
|
||||||
allTextBoxes.add(renderInfo.getBaseline().getBoundingRectangle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<EventType> getSupportedEvents() {
|
|
||||||
return Collections.singleton(EventType.RENDER_TEXT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -11,15 +18,6 @@ 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 com.itextpdf.kernel.geom.PageSize;
|
|
||||||
import com.itextpdf.kernel.geom.Rectangle;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfReader;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfWriter;
|
|
||||||
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
|
|
||||||
import com.itextpdf.layout.Document;
|
|
||||||
import com.itextpdf.layout.element.Image;
|
|
||||||
|
|
||||||
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;
|
||||||
@ -41,40 +39,50 @@ public class ToSinglePageController {
|
|||||||
@Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true)
|
@Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true)
|
||||||
MultipartFile file) throws IOException {
|
MultipartFile file) throws IOException {
|
||||||
|
|
||||||
PdfReader reader = new PdfReader(file.getInputStream());
|
// Load the source document
|
||||||
PdfDocument sourceDocument = new PdfDocument(reader);
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
|
|
||||||
|
// Calculate total height and max width
|
||||||
float totalHeight = 0;
|
float totalHeight = 0;
|
||||||
float width = 0;
|
float maxWidth = 0;
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
|
PDRectangle pageSize = page.getMediaBox();
|
||||||
Rectangle pageSize = sourceDocument.getPage(i).getPageSize();
|
|
||||||
totalHeight += pageSize.getHeight();
|
totalHeight += pageSize.getHeight();
|
||||||
if(width < pageSize.getWidth())
|
maxWidth = Math.max(maxWidth, pageSize.getWidth());
|
||||||
width = pageSize.getWidth();
|
}
|
||||||
|
|
||||||
|
// Create new document and page with calculated dimensions
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
|
// Initialize the content stream of the new page
|
||||||
|
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
|
||||||
|
contentStream.close();
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
float yOffset = totalHeight;
|
||||||
|
|
||||||
|
// For each page, copy its content to the new page at the correct offset
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page));
|
||||||
|
AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight());
|
||||||
|
layerUtility.wrapInSaveRestore(newPage);
|
||||||
|
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
|
||||||
|
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
|
||||||
|
yOffset -= page.getMediaBox().getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
PdfWriter writer = new PdfWriter(baos);
|
newDocument.save(baos);
|
||||||
PdfDocument newDocument = new PdfDocument(writer);
|
newDocument.close();
|
||||||
PageSize newPageSize = new PageSize(width, totalHeight);
|
|
||||||
newDocument.addNewPage(newPageSize);
|
|
||||||
|
|
||||||
Document layoutDoc = new Document(newDocument);
|
|
||||||
float yOffset = totalHeight;
|
|
||||||
|
|
||||||
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
|
|
||||||
PdfFormXObject pageCopy = sourceDocument.getPage(i).copyAsFormXObject(newDocument);
|
|
||||||
Image copiedPage = new Image(pageCopy);
|
|
||||||
copiedPage.setFixedPosition(0, yOffset - sourceDocument.getPage(i).getPageSize().getHeight());
|
|
||||||
yOffset -= sourceDocument.getPage(i).getPageSize().getHeight();
|
|
||||||
layoutDoc.add(copiedPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutDoc.close();
|
|
||||||
sourceDocument.close();
|
sourceDocument.close();
|
||||||
|
|
||||||
byte[] result = baos.toByteArray();
|
byte[] result = baos.toByteArray();
|
||||||
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
|
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,10 +13,11 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
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.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@ -40,50 +41,107 @@ public class UserController {
|
|||||||
return "redirect:/login?registered=true";
|
return "redirect:/login?registered=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/change-username")
|
@PostMapping("/change-username-and-password")
|
||||||
public ResponseEntity<String> changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) {
|
public RedirectView changeUsernameAndPassword(Principal principal,
|
||||||
|
@RequestParam String currentPassword,
|
||||||
|
@RequestParam String newUsername,
|
||||||
|
@RequestParam String newPassword,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
|
return new RedirectView("/change-creds?messageType=userNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
|
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(userService.usernameExists(newUsername)) {
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists.");
|
return new RedirectView("/change-creds?messageType=usernameExists");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
userService.changePassword(user, newPassword);
|
||||||
|
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) {
|
||||||
userService.changeUsername(user, newUsername);
|
userService.changeUsername(user, newUsername);
|
||||||
|
}
|
||||||
|
userService.changeFirstUse(user, false);
|
||||||
|
|
||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
return ResponseEntity.ok("Username updated successfully.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/change-password")
|
|
||||||
public ResponseEntity<String> changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
|
@PostMapping("/change-username")
|
||||||
|
public RedirectView changeUsername(Principal principal,
|
||||||
|
@RequestParam String currentPassword,
|
||||||
|
@RequestParam String newUsername,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
|
return new RedirectView("/account?messageType=incorrectPassword");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
|
return new RedirectView("/account?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newUsername != null && newUsername.length() > 0) {
|
||||||
|
userService.changeUsername(user, newUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout using Spring's utility
|
||||||
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/change-password")
|
||||||
|
public RedirectView changePassword(Principal principal,
|
||||||
|
@RequestParam String currentPassword,
|
||||||
|
@RequestParam String newPassword,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
if (principal == null) {
|
||||||
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
|
return new RedirectView("/account?messageType=incorrectPassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
@ -91,9 +149,10 @@ public class UserController {
|
|||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return ResponseEntity.ok("Password updated successfully.");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/updateUserSettings")
|
@PostMapping("/updateUserSettings")
|
||||||
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
||||||
Map<String, String[]> paramMap = request.getParameterMap();
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
@ -115,9 +174,14 @@ public class UserController {
|
|||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public String saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role) {
|
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role,
|
||||||
userService.saveUser(username, password, role);
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) {
|
||||||
return "redirect:/addUsers"; // Redirect to account page after adding the user
|
|
||||||
|
if(userService.usernameExists(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
userService.saveUser(username, password, role, forceChange);
|
||||||
|
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ public class ConvertEpubToPdf {
|
|||||||
|
|
||||||
// Assuming a pseudo-code function that merges multiple PDFs into one.
|
// Assuming a pseudo-code function that merges multiple PDFs into one.
|
||||||
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
|
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
|
||||||
// You can use a library such as iText or PDFBox to perform the merging here.
|
// You can use a library such as PDFBox to perform the merging here.
|
||||||
// Return the byte[] of the merged PDF.
|
// Return the byte[] of the merged PDF.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import java.awt.image.BufferedImageOp;
|
|||||||
import java.awt.image.ConvolveOp;
|
import java.awt.image.ConvolveOp;
|
||||||
import java.awt.image.Kernel;
|
import java.awt.image.Kernel;
|
||||||
import java.awt.image.RescaleOp;
|
import java.awt.image.RescaleOp;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
//Required for file input/output
|
//Required for file input/output
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -34,8 +35,6 @@ 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 com.itextpdf.io.source.ByteArrayOutputStream;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
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;
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api.other;
|
package stirling.software.SPDF.controller.api.other;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -15,19 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
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 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.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.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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@ -51,11 +41,10 @@ public class PageNumbersController {
|
|||||||
@Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
|
@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)
|
@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 {
|
throws IOException {
|
||||||
|
|
||||||
byte[] fileBytes = file.getBytes();
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
|
||||||
|
|
||||||
int pageNumber = startingNumber;
|
int pageNumber = startingNumber;
|
||||||
|
byte[] fileBytes = file.getBytes();
|
||||||
|
PDDocument document = PDDocument.load(fileBytes);
|
||||||
|
|
||||||
float marginFactor;
|
float marginFactor;
|
||||||
switch (customMargin.toLowerCase()) {
|
switch (customMargin.toLowerCase()) {
|
||||||
case "small":
|
case "small":
|
||||||
@ -68,78 +57,76 @@ public class PageNumbersController {
|
|||||||
marginFactor = 0.05f;
|
marginFactor = 0.05f;
|
||||||
break;
|
break;
|
||||||
case "x-large":
|
case "x-large":
|
||||||
marginFactor = 0.1f;
|
marginFactor = 0.075f;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
marginFactor = 0.035f;
|
marginFactor = 0.035f;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
float fontSize = 12.0f;
|
float fontSize = 12.0f;
|
||||||
|
PDType1Font font = PDType1Font.HELVETICA;
|
||||||
PdfReader reader = new PdfReader(bais);
|
if(pagesToNumber == null || pagesToNumber.length() == 0) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
pagesToNumber = "all";
|
||||||
PdfWriter writer = new PdfWriter(baos);
|
}
|
||||||
|
if(customText == null || customText.length() == 0) {
|
||||||
PdfDocument pdfDoc = new PdfDocument(reader, writer);
|
customText = "{n}";
|
||||||
|
}
|
||||||
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
|
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
|
||||||
|
|
||||||
for (int i : pagesToNumberList) {
|
for (int i : pagesToNumberList) {
|
||||||
PdfPage page = pdfDoc.getPage(i+1);
|
PDPage page = document.getPage(i);
|
||||||
Rectangle pageSize = page.getPageSize();
|
PDRectangle pageSize = page.getMediaBox();
|
||||||
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())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
|
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : 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;
|
float x, y;
|
||||||
TextAlignment alignment;
|
|
||||||
|
|
||||||
int xGroup = (position - 1) % 3;
|
int xGroup = (position - 1) % 3;
|
||||||
int yGroup = 2 - (position - 1) / 3;
|
int yGroup = 2 - (position - 1) / 3;
|
||||||
|
|
||||||
switch (xGroup) {
|
switch (xGroup) {
|
||||||
case 0: // left
|
case 0: // left
|
||||||
x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
|
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
|
||||||
alignment = TextAlignment.LEFT;
|
|
||||||
break;
|
break;
|
||||||
case 1: // center
|
case 1: // center
|
||||||
x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
|
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
|
||||||
alignment = TextAlignment.CENTER;
|
|
||||||
break;
|
break;
|
||||||
default: // right
|
default: // right
|
||||||
x = pageSize.getRight() - marginFactor * pageSize.getWidth();
|
x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
|
||||||
alignment = TextAlignment.RIGHT;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (yGroup) {
|
switch (yGroup) {
|
||||||
case 0: // bottom
|
case 0: // bottom
|
||||||
y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
|
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
|
||||||
break;
|
break;
|
||||||
case 1: // middle
|
case 1: // middle
|
||||||
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
|
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
|
||||||
break;
|
break;
|
||||||
default: // top
|
default: // top
|
||||||
y = pageSize.getTop() - marginFactor * pageSize.getHeight();
|
y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
new Canvas(pdfCanvas, page.getPageSize())
|
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||||
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
|
contentStream.beginText();
|
||||||
|
contentStream.setFont(font, fontSize);
|
||||||
|
contentStream.newLineAtOffset(x, y);
|
||||||
|
contentStream.showText(text);
|
||||||
|
contentStream.endText();
|
||||||
|
contentStream.close();
|
||||||
|
|
||||||
pageNumber++;
|
pageNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
document.close();
|
||||||
|
|
||||||
pdfDoc.close();
|
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
|
||||||
byte[] resultBytes = baos.toByteArray();
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package stirling.software.SPDF.controller.api.other;
|
package stirling.software.SPDF.controller.api.other;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -10,16 +14,6 @@ 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 com.itextpdf.kernel.pdf.PdfArray;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfDictionary;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfName;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfObject;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfReader;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfStream;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
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;
|
||||||
@RestController
|
@RestController
|
||||||
@ -28,55 +22,35 @@ public class ShowJavascript {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
|
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||||
@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(
|
public ResponseEntity<byte[]> extractHeader(
|
||||||
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile)
|
@RequestPart(value = "fileInput") MultipartFile inputFile) throws Exception {
|
||||||
throws Exception {
|
|
||||||
|
|
||||||
try (
|
|
||||||
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
|
|
||||||
) {
|
|
||||||
|
|
||||||
String name = "";
|
|
||||||
String script = "";
|
String script = "";
|
||||||
String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: ";
|
|
||||||
//Javascript
|
|
||||||
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
|
|
||||||
if (namesDict != null) {
|
|
||||||
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
|
|
||||||
if (javascriptDict != null) {
|
|
||||||
|
|
||||||
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
|
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
|
||||||
for (int i = 0; i < namesArray.size(); i += 2) {
|
|
||||||
if(namesArray.getAsString(i) != null)
|
|
||||||
name = namesArray.getAsString(i).toString();
|
|
||||||
|
|
||||||
PdfObject jsCode = namesArray.get(i+1);
|
if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) {
|
||||||
if (jsCode instanceof PdfStream) {
|
PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript();
|
||||||
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
|
|
||||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
|
||||||
script = "//" + entryName + name + "\n" +jsCodeStr;
|
|
||||||
|
|
||||||
} else if (jsCode instanceof PdfDictionary) {
|
if (jsTree != null) {
|
||||||
// If the JS code is in a dictionary, you'll need to know the key to use.
|
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
|
||||||
// Assuming the key is PdfName.JS:
|
|
||||||
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
|
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
|
||||||
if (jsCodeStream != null) {
|
String name = entry.getKey();
|
||||||
byte[] jsCodeBytes = jsCodeStream.getBytes();
|
PDActionJavaScript jsAction = entry.getValue();
|
||||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
String jsCodeStr = jsAction.getAction();
|
||||||
script = "//" + entryName + name + "\n" +jsCodeStr;
|
|
||||||
|
script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if (script.isEmpty()) {
|
||||||
}
|
|
||||||
if(script.equals("")) {
|
|
||||||
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
|
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
|
||||||
}
|
}
|
||||||
return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,23 +3,43 @@ package stirling.software.SPDF.controller.api.security;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.Principal;
|
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Arrays;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
|
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||||
|
import org.bouncycastle.cms.CMSTypedData;
|
||||||
|
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
import org.bouncycastle.util.io.pem.PemReader;
|
import org.bouncycastle.util.io.pem.PemReader;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -30,29 +50,12 @@ 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 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.PdfDocument;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfPage;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfReader;
|
|
||||||
import com.itextpdf.kernel.pdf.StampingProperties;
|
|
||||||
import com.itextpdf.signatures.BouncyCastleDigest;
|
|
||||||
import com.itextpdf.signatures.DigestAlgorithms;
|
|
||||||
import com.itextpdf.signatures.IExternalDigest;
|
|
||||||
import com.itextpdf.signatures.IExternalSignature;
|
|
||||||
import com.itextpdf.signatures.PdfPKCS7;
|
|
||||||
import com.itextpdf.signatures.PdfSignatureAppearance;
|
|
||||||
import com.itextpdf.signatures.PdfSigner;
|
|
||||||
import com.itextpdf.signatures.PrivateKeySignature;
|
|
||||||
import com.itextpdf.signatures.SignatureUtil;
|
|
||||||
|
|
||||||
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.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
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;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
public class CertSignController {
|
public class CertSignController {
|
||||||
@ -64,66 +67,46 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
||||||
@Operation(summary = "Sign PDF with a Digital Certificate",
|
@Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
|
||||||
description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
|
public ResponseEntity<byte[]> signPDF2(
|
||||||
public ResponseEntity<byte[]> signPDF(
|
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be signed") MultipartFile pdf,
|
||||||
@RequestPart(required = true, value = "fileInput")
|
|
||||||
@Parameter(description = "The input PDF file to be signed")
|
|
||||||
MultipartFile pdf,
|
|
||||||
|
|
||||||
@RequestParam(value = "certType", required = false)
|
@RequestParam(value = "certType", required = false) @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {
|
||||||
@Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"}))
|
"PKCS12", "PEM" })) String certType,
|
||||||
String certType,
|
|
||||||
|
|
||||||
@RequestParam(value = "key", required = false)
|
@RequestParam(value = "key", required = false) @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") MultipartFile privateKeyFile,
|
||||||
@Parameter(description = "The private key for the digital certificate (required for PEM type certificates)")
|
|
||||||
MultipartFile privateKeyFile,
|
|
||||||
|
|
||||||
@RequestParam(value = "cert", required = false)
|
@RequestParam(value = "cert", required = false) @Parameter(description = "The digital certificate (required for PEM type certificates)") MultipartFile certFile,
|
||||||
@Parameter(description = "The digital certificate (required for PEM type certificates)")
|
|
||||||
MultipartFile certFile,
|
|
||||||
|
|
||||||
@RequestParam(value = "p12", required = false)
|
@RequestParam(value = "p12", required = false) @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") MultipartFile p12File,
|
||||||
@Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)")
|
|
||||||
MultipartFile p12File,
|
|
||||||
|
|
||||||
@RequestParam(value = "password", required = false)
|
@RequestParam(value = "password", required = false) @Parameter(description = "The password for the keystore or the private key") String password,
|
||||||
@Parameter(description = "The password for the keystore or the private key")
|
|
||||||
String password,
|
|
||||||
|
|
||||||
@RequestParam(value = "showSignature", required = false)
|
@RequestParam(value = "showSignature", required = false) @Parameter(description = "Whether to visually show the signature in the PDF file") Boolean showSignature,
|
||||||
@Parameter(description = "Whether to visually show the signature in the PDF file")
|
|
||||||
Boolean showSignature,
|
|
||||||
|
|
||||||
@RequestParam(value = "reason", required = false)
|
@RequestParam(value = "reason", required = false) @Parameter(description = "The reason for signing the PDF") String reason,
|
||||||
@Parameter(description = "The reason for signing the PDF")
|
|
||||||
String reason,
|
|
||||||
|
|
||||||
@RequestParam(value = "location", required = false)
|
@RequestParam(value = "location", required = false) @Parameter(description = "The location where the PDF is signed") String location,
|
||||||
@Parameter(description = "The location where the PDF is signed")
|
|
||||||
String location,
|
|
||||||
|
|
||||||
@RequestParam(value = "name", required = false)
|
@RequestParam(value = "name", required = false) @Parameter(description = "The name of the signer") String name,
|
||||||
@Parameter(description = "The name of the signer")
|
|
||||||
String name,
|
|
||||||
|
|
||||||
@RequestParam(value = "pageNumber", required = false)
|
@RequestParam(value = "pageNumber", required = false) @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") Integer pageNumber)
|
||||||
@Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true")
|
throws Exception {
|
||||||
Integer pageNumber) throws Exception {
|
|
||||||
|
|
||||||
BouncyCastleProvider provider = new BouncyCastleProvider();
|
|
||||||
Security.addProvider(provider);
|
|
||||||
|
|
||||||
PrivateKey privateKey = null;
|
PrivateKey privateKey = null;
|
||||||
X509Certificate cert = null;
|
X509Certificate cert = null;
|
||||||
|
|
||||||
if (certType != null) {
|
if (certType != null) {
|
||||||
|
logger.info("Cert type provided: {}", certType);
|
||||||
switch (certType) {
|
switch (certType) {
|
||||||
case "PKCS12":
|
case "PKCS12":
|
||||||
if (p12File != null) {
|
if (p12File != null) {
|
||||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||||
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
|
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
|
||||||
String alias = ks.aliases().nextElement();
|
String alias = ks.aliases().nextElement();
|
||||||
|
if (!ks.isKeyEntry(alias)) {
|
||||||
|
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key.");
|
||||||
|
}
|
||||||
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
|
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
|
||||||
cert = (X509Certificate) ks.getCertificate(alias);
|
cert = (X509Certificate) ks.getCertificate(alias);
|
||||||
}
|
}
|
||||||
@ -131,154 +114,147 @@ public class CertSignController {
|
|||||||
case "PEM":
|
case "PEM":
|
||||||
if (privateKeyFile != null && certFile != null) {
|
if (privateKeyFile != null && certFile != null) {
|
||||||
// Load private key
|
// Load private key
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider);
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
|
||||||
if (isPEM(privateKeyFile.getBytes())) {
|
if (isPEM(privateKeyFile.getBytes())) {
|
||||||
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
|
privateKey = keyFactory
|
||||||
|
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
|
||||||
} else {
|
} else {
|
||||||
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
|
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load certificate
|
// Load certificate
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider);
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
|
||||||
|
BouncyCastleProvider.PROVIDER_NAME);
|
||||||
if (isPEM(certFile.getBytes())) {
|
if (isPEM(certFile.getBytes())) {
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
|
cert = (X509Certificate) certFactory
|
||||||
|
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
|
||||||
} else {
|
} else {
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
|
cert = (X509Certificate) certFactory
|
||||||
|
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PDSignature signature = new PDSignature();
|
||||||
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
|
||||||
|
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
|
||||||
|
signature.setName(name);
|
||||||
|
signature.setLocation(location);
|
||||||
|
signature.setReason(reason);
|
||||||
|
|
||||||
Principal principal = cert.getSubjectDN();
|
// Load the PDF
|
||||||
String dn = principal.getName();
|
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
|
||||||
|
logger.info("Successfully loaded the provided PDF");
|
||||||
|
SignatureOptions signatureOptions = new SignatureOptions();
|
||||||
|
|
||||||
// Extract the "CN" (Common Name) field from the distinguished name (if it's present)
|
// If you want to show the signature
|
||||||
String cn = null;
|
|
||||||
for (String part : dn.split(",")) {
|
|
||||||
if (part.trim().startsWith("CN=")) {
|
|
||||||
cn = part.trim().substring("CN=".length());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the PDF reader and stamper
|
|
||||||
PdfReader reader = new PdfReader(new ByteArrayInputStream(pdf.getBytes()));
|
|
||||||
ByteArrayOutputStream signedPdf = new ByteArrayOutputStream();
|
|
||||||
PdfSigner signer = new PdfSigner(reader, signedPdf, new StampingProperties());
|
|
||||||
|
|
||||||
// Set up the signing appearance
|
|
||||||
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
|
|
||||||
.setReason("Test")
|
|
||||||
.setLocation("TestLocation");
|
|
||||||
|
|
||||||
|
// ATTEMPT 2
|
||||||
if (showSignature != null && showSignature) {
|
if (showSignature != null && showSignature) {
|
||||||
float fontSize = 4; // the font size of the signature
|
PDPage page = document.getPage(pageNumber - 1);
|
||||||
float marginRight = 36; // Margin from the right
|
|
||||||
float marginBottom = 36; // Margin from the bottom
|
|
||||||
String signingDate = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date());
|
|
||||||
|
|
||||||
// Prepare the text for the digital signature
|
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
||||||
StringBuilder layer2TextBuilder = new StringBuilder(String.format("Digitally signed by: %s\nDate: %s",
|
if (acroForm == null) {
|
||||||
name != null ? name : "Unknown", signingDate));
|
acroForm = new PDAcroForm(document);
|
||||||
|
document.getDocumentCatalog().setAcroForm(acroForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new signature field and widget
|
||||||
|
|
||||||
|
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
||||||
|
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
||||||
|
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
|
||||||
|
widget.setRectangle(rect);
|
||||||
|
page.getAnnotations().add(widget);
|
||||||
|
|
||||||
|
// Set the appearance for the signature field
|
||||||
|
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
|
||||||
|
PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
|
||||||
|
appearanceStream.setResources(new PDResources());
|
||||||
|
appearanceStream.setBBox(rect);
|
||||||
|
appearanceDict.setNormalAppearance(appearanceStream);
|
||||||
|
widget.setAppearance(appearanceDict);
|
||||||
|
|
||||||
|
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
|
||||||
|
contentStream.beginText();
|
||||||
|
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
|
||||||
|
contentStream.newLineAtOffset(110, 130);
|
||||||
|
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown"));
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
|
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date()));
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
if (reason != null && !reason.isEmpty()) {
|
if (reason != null && !reason.isEmpty()) {
|
||||||
layer2TextBuilder.append("\nReason: ").append(reason);
|
contentStream.showText("Reason: " + reason);
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location != null && !location.isEmpty()) {
|
if (location != null && !location.isEmpty()) {
|
||||||
layer2TextBuilder.append("\nLocation: ").append(location);
|
contentStream.showText("Location: " + location);
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
}
|
}
|
||||||
String layer2Text = layer2TextBuilder.toString();
|
contentStream.endText();
|
||||||
// Get the PDF font and measure the width and height of the text block
|
|
||||||
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
|
|
||||||
float textWidth = Arrays.stream(layer2Text.split("\n"))
|
|
||||||
.map(line -> font.getWidth(line, fontSize))
|
|
||||||
.max(Float::compare)
|
|
||||||
.orElse(0f);
|
|
||||||
int numLines = layer2Text.split("\n").length;
|
|
||||||
float textHeight = numLines * fontSize;
|
|
||||||
|
|
||||||
// Calculate the signature rectangle size
|
|
||||||
float sigWidth = textWidth + marginRight * 2;
|
|
||||||
float sigHeight = textHeight + marginBottom * 2;
|
|
||||||
|
|
||||||
// Get the page size
|
|
||||||
PdfPage page = signer.getDocument().getPage(1);
|
|
||||||
Rectangle pageSize = page.getPageSize();
|
|
||||||
|
|
||||||
// Define the position and dimension of the signature field
|
|
||||||
Rectangle rect = new Rectangle(
|
|
||||||
pageSize.getRight() - sigWidth - marginRight,
|
|
||||||
pageSize.getBottom() + marginBottom,
|
|
||||||
sigWidth,
|
|
||||||
sigHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
// Configure the appearance of the digital signature
|
|
||||||
appearance.setPageRect(rect)
|
|
||||||
.setContact(name != null ? name : "")
|
|
||||||
.setPageNumber(pageNumber)
|
|
||||||
.setReason(reason != null ? reason : "")
|
|
||||||
.setLocation(location != null ? location : "")
|
|
||||||
.setReuseAppearance(false)
|
|
||||||
.setLayer2Text(layer2Text.toString());
|
|
||||||
|
|
||||||
signer.setFieldName("sig");
|
|
||||||
} else {
|
|
||||||
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the signer
|
// Add the widget annotation to the page
|
||||||
PrivateKeySignature pks = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName());
|
page.getAnnotations().add(widget);
|
||||||
IExternalSignature pss = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName());
|
|
||||||
IExternalDigest digest = new BouncyCastleDigest();
|
|
||||||
|
|
||||||
// Call iTex7 to sign the PDF
|
// Add the signature field to the acroform
|
||||||
signer.signDetached(digest, pks, new Certificate[] {cert}, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
|
acroForm.getFields().add(signatureField);
|
||||||
|
|
||||||
|
// Handle multiple signatures by ensuring a unique field name
|
||||||
System.out.println("Signed PDF size: " + signedPdf.size());
|
String baseFieldName = "Signature";
|
||||||
|
String signatureFieldName = baseFieldName;
|
||||||
System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray()));
|
int suffix = 1;
|
||||||
return WebResponseUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf");
|
while (acroForm.getField(signatureFieldName) != null) {
|
||||||
|
suffix++;
|
||||||
|
signatureFieldName = baseFieldName + suffix;
|
||||||
|
}
|
||||||
|
signatureField.setPartialName(signatureFieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPdfSigned(byte[] pdfData) throws IOException {
|
document.addSignature(signature, signatureOptions);
|
||||||
InputStream pdfStream = new ByteArrayInputStream(pdfData);
|
logger.info("Signature added to the PDF document");
|
||||||
PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfStream));
|
// External signing
|
||||||
SignatureUtil signatureUtil = new SignatureUtil(pdfDoc);
|
ExternalSigningSupport externalSigning = document
|
||||||
List<String> names = signatureUtil.getSignatureNames();
|
.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
|
||||||
|
|
||||||
boolean isSigned = false;
|
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
|
||||||
|
|
||||||
for (String name : names) {
|
// Using BouncyCastle to sign
|
||||||
PdfPKCS7 pkcs7 = signatureUtil.readSignatureData(name);
|
CMSTypedData cmsData = new CMSProcessableByteArray(content);
|
||||||
if (pkcs7 != null) {
|
|
||||||
System.out.println("Signature found.");
|
|
||||||
|
|
||||||
// Log certificate details
|
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||||
Certificate[] signChain = pkcs7.getSignCertificateChain();
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
for (Certificate cert : signChain) {
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
|
||||||
if (cert instanceof X509Certificate) {
|
|
||||||
X509Certificate x509 = (X509Certificate) cert;
|
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
|
||||||
System.out.println("Certificate Details:");
|
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
|
||||||
System.out.println("Subject: " + x509.getSubjectDN());
|
.build(signer, cert));
|
||||||
System.out.println("Issuer: " + x509.getIssuerDN());
|
|
||||||
System.out.println("Serial: " + x509.getSerialNumber());
|
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
|
||||||
System.out.println("Not Before: " + x509.getNotBefore());
|
CMSSignedData signedData = gen.generate(cmsData, false);
|
||||||
System.out.println("Not After: " + x509.getNotAfter());
|
|
||||||
|
byte[] cmsSignature = signedData.getEncoded();
|
||||||
|
logger.info("About to sign content using BouncyCastle");
|
||||||
|
externalSigning.setSignature(cmsSignature);
|
||||||
|
logger.info("Signature set successfully");
|
||||||
|
|
||||||
|
// After setting the signature, return the resultant PDF
|
||||||
|
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
|
||||||
|
document.save(signedPdfOutput);
|
||||||
|
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
|
||||||
|
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
isSigned = true;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfDoc.close();
|
|
||||||
|
|
||||||
return isSigned;
|
|
||||||
}
|
|
||||||
private byte[] parsePEM(byte[] content) throws IOException {
|
private byte[] parsePEM(byte[] content) throws IOException {
|
||||||
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
||||||
return pemReader.readPemObject().getContent();
|
return pemReader.readPemObject().getContent();
|
||||||
@ -289,8 +265,4 @@ public boolean isPdfSigned(byte[] pdfData) throws IOException {
|
|||||||
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
|
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
@ -11,14 +11,53 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.cos.COSDocument;
|
||||||
|
import org.apache.pdfbox.cos.COSInputStream;
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
|
import org.apache.pdfbox.cos.COSObject;
|
||||||
|
import org.apache.pdfbox.cos.COSStream;
|
||||||
import org.apache.pdfbox.cos.COSString;
|
import org.apache.pdfbox.cos.COSString;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDJavascriptNameTreeNode;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||||
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement;
|
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement;
|
||||||
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode;
|
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode;
|
||||||
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
|
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
|
||||||
|
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
|
||||||
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
|
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
|
import org.apache.xmpbox.XMPMetadata;
|
||||||
|
import org.apache.xmpbox.xml.DomXmpParser;
|
||||||
|
import org.apache.xmpbox.xml.XmpParsingException;
|
||||||
|
import org.apache.xmpbox.xml.XmpSerializer;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -29,29 +68,6 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.itextpdf.forms.PdfAcroForm;
|
|
||||||
import com.itextpdf.forms.fields.PdfFormField;
|
|
||||||
import com.itextpdf.kernel.geom.Rectangle;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfArray;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfCatalog;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfDictionary;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfName;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfObject;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfOutline;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfReader;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfResources;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfStream;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfString;
|
|
||||||
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
|
|
||||||
import com.itextpdf.kernel.pdf.annot.PdfFileAttachmentAnnotation;
|
|
||||||
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
|
|
||||||
import com.itextpdf.kernel.pdf.layer.PdfLayer;
|
|
||||||
import com.itextpdf.kernel.pdf.layer.PdfOCProperties;
|
|
||||||
import com.itextpdf.kernel.xmp.XMPException;
|
|
||||||
import com.itextpdf.kernel.xmp.XMPMeta;
|
|
||||||
import com.itextpdf.kernel.xmp.XMPMetaFactory;
|
|
||||||
import com.itextpdf.kernel.xmp.options.SerializeOptions;
|
|
||||||
|
|
||||||
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;
|
||||||
@ -72,7 +88,6 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
try (
|
try (
|
||||||
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
|
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
|
||||||
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
|
|
||||||
) {
|
) {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
ObjectNode jsonOutput = objectMapper.createObjectNode();
|
ObjectNode jsonOutput = objectMapper.createObjectNode();
|
||||||
@ -120,20 +135,16 @@ public class GetInfoOnPDF {
|
|||||||
boolean hasCompression = false;
|
boolean hasCompression = false;
|
||||||
String compressionType = "None";
|
String compressionType = "None";
|
||||||
|
|
||||||
// Check for object streams
|
COSDocument cosDoc = pdfBoxDoc.getDocument();
|
||||||
for (int i = 1; i <= itextDoc.getNumberOfPdfObjects(); i++) {
|
for (COSObject cosObject : cosDoc.getObjects()) {
|
||||||
PdfObject obj = itextDoc.getPdfObject(i);
|
if (cosObject.getObject() instanceof COSStream) {
|
||||||
if (obj != null && obj.isStream() && ((PdfStream) obj).get(PdfName.Type) == PdfName.ObjStm) {
|
COSStream cosStream = (COSStream) cosObject.getObject();
|
||||||
|
if (COSName.OBJ_STM.equals(cosStream.getItem(COSName.TYPE))) {
|
||||||
hasCompression = true;
|
hasCompression = true;
|
||||||
compressionType = "Object Streams";
|
compressionType = "Object Streams";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not compressed using object streams, check for compressed Xref tables
|
|
||||||
if (!hasCompression && itextDoc.getReader().hasRebuiltXref()) {
|
|
||||||
hasCompression = true;
|
|
||||||
compressionType = "Compressed Xref or Rebuilt Xref";
|
|
||||||
}
|
}
|
||||||
basicInfo.put("Compression", hasCompression);
|
basicInfo.put("Compression", hasCompression);
|
||||||
if(hasCompression)
|
if(hasCompression)
|
||||||
@ -144,9 +155,8 @@ public class GetInfoOnPDF {
|
|||||||
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
|
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
|
||||||
|
|
||||||
|
|
||||||
// Page Mode using iText7
|
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
|
||||||
PdfCatalog catalog = itextDoc.getCatalog();
|
String pageMode = catalog.getPageMode().name();
|
||||||
PdfName pageMode = catalog.getPdfObject().getAsName(PdfName.PageMode);
|
|
||||||
|
|
||||||
// Document Information using PDFBox
|
// Document Information using PDFBox
|
||||||
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
|
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
|
||||||
@ -157,11 +167,12 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
PdfAcroForm acroForm = PdfAcroForm.getAcroForm(itextDoc, false);
|
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
|
||||||
|
|
||||||
ObjectNode formFieldsNode = objectMapper.createObjectNode();
|
ObjectNode formFieldsNode = objectMapper.createObjectNode();
|
||||||
if (acroForm != null) {
|
if (acroForm != null) {
|
||||||
for (Map.Entry<String, PdfFormField> entry : acroForm.getFormFields().entrySet()) {
|
for (PDField field : acroForm.getFieldTree()) {
|
||||||
formFieldsNode.put(entry.getKey(), entry.getValue().getValueAsString());
|
formFieldsNode.put(field.getFullyQualifiedName(), field.getValueAsString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsonOutput.set("FormFields", formFieldsNode);
|
jsonOutput.set("FormFields", formFieldsNode);
|
||||||
@ -170,36 +181,42 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
//embeed files TODO size
|
|
||||||
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
|
|
||||||
if(itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) != null)
|
|
||||||
{
|
|
||||||
PdfDictionary embeddedFiles = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names)
|
|
||||||
.getAsDictionary(PdfName.EmbeddedFiles);
|
|
||||||
if (embeddedFiles != null) {
|
|
||||||
|
|
||||||
PdfArray namesArray = embeddedFiles.getAsArray(PdfName.Names);
|
//embeed files TODO size
|
||||||
if(namesArray != null) {
|
if(catalog.getNames() != null) {
|
||||||
for (int i = 0; i < namesArray.size(); i += 2) {
|
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
|
||||||
|
|
||||||
|
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
|
||||||
|
if (efTree != null) {
|
||||||
|
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
|
||||||
|
if (efMap != null) {
|
||||||
|
for (Map.Entry<String, PDComplexFileSpecification> entry : efMap.entrySet()) {
|
||||||
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
|
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
|
||||||
embeddedFileNode.put("Name", namesArray.getAsString(i).toString());
|
embeddedFileNode.put("Name", entry.getKey());
|
||||||
// Add other details if required
|
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
|
||||||
|
if (embeddedFile != null) {
|
||||||
|
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes
|
||||||
|
}
|
||||||
embeddedFilesArray.add(embeddedFileNode);
|
embeddedFilesArray.add(embeddedFileNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
other.set("EmbeddedFiles", embeddedFilesArray);
|
other.set("EmbeddedFiles", embeddedFilesArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//attachments TODO size
|
//attachments TODO size
|
||||||
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
||||||
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) {
|
for (PDPage page : pdfBoxDoc.getPages()) {
|
||||||
for (PdfAnnotation annotation : itextDoc.getPage(pageNum).getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
if (annotation instanceof PdfFileAttachmentAnnotation) {
|
if (annotation instanceof PDAnnotationFileAttachment) {
|
||||||
|
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation;
|
||||||
|
|
||||||
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
||||||
attachmentNode.put("Name", ((PdfFileAttachmentAnnotation) annotation).getName().toString());
|
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
|
||||||
attachmentNode.put("Description", annotation.getContents().getValue());
|
attachmentNode.put("Description", fileAttachmentAnnotation.getContents());
|
||||||
|
|
||||||
attachmentsArray.add(attachmentNode);
|
attachmentsArray.add(attachmentNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,54 +224,49 @@ public class GetInfoOnPDF {
|
|||||||
other.set("Attachments", attachmentsArray);
|
other.set("Attachments", attachmentsArray);
|
||||||
|
|
||||||
//Javascript
|
//Javascript
|
||||||
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
|
PDDocumentNameDictionary namesDict = catalog.getNames();
|
||||||
ArrayNode javascriptArray = objectMapper.createArrayNode();
|
ArrayNode javascriptArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
if (namesDict != null) {
|
if (namesDict != null) {
|
||||||
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
|
PDJavascriptNameTreeNode javascriptDict = namesDict.getJavaScript();
|
||||||
if (javascriptDict != null) {
|
if (javascriptDict != null) {
|
||||||
|
try {
|
||||||
|
Map<String, PDActionJavaScript> jsEntries = javascriptDict.getNames();
|
||||||
|
|
||||||
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
|
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
|
||||||
for (int i = 0; i < namesArray.size(); i += 2) {
|
|
||||||
ObjectNode jsNode = objectMapper.createObjectNode();
|
ObjectNode jsNode = objectMapper.createObjectNode();
|
||||||
if(namesArray.getAsString(i) != null)
|
jsNode.put("JS Name", entry.getKey());
|
||||||
jsNode.put("JS Name", namesArray.getAsString(i).toString());
|
|
||||||
|
|
||||||
// Here we check for a PdfStream object and retrieve the JS code from it
|
PDActionJavaScript jsAction = entry.getValue();
|
||||||
PdfObject jsCode = namesArray.get(i+1);
|
if (jsAction != null) {
|
||||||
if (jsCode instanceof PdfStream) {
|
String jsCodeStr = jsAction.getAction();
|
||||||
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
|
if (jsCodeStr != null) {
|
||||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
|
||||||
jsNode.put("JS Script Length", jsCodeStr.length());
|
jsNode.put("JS Script Length", jsCodeStr.length());
|
||||||
} else if (jsCode instanceof PdfDictionary) {
|
|
||||||
// If the JS code is in a dictionary, you'll need to know the key to use.
|
|
||||||
// Assuming the key is PdfName.JS:
|
|
||||||
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
|
|
||||||
if (jsCodeStream != null) {
|
|
||||||
byte[] jsCodeBytes = jsCodeStream.getBytes();
|
|
||||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
|
||||||
jsNode.put("JS Script Character Length", jsCodeStr.length());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
javascriptArray.add(jsNode);
|
javascriptArray.add(jsNode);
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
other.set("JavaScript", javascriptArray);
|
other.set("JavaScript", javascriptArray);
|
||||||
|
|
||||||
//TODO size
|
|
||||||
PdfOCProperties ocProperties = itextDoc.getCatalog().getOCProperties(false);
|
|
||||||
ArrayNode layersArray = objectMapper.createArrayNode();
|
|
||||||
if (ocProperties != null) {
|
|
||||||
|
|
||||||
for (PdfLayer layer : ocProperties.getLayers()) {
|
//TODO size
|
||||||
|
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties();
|
||||||
|
ArrayNode layersArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
|
if (ocProperties != null) {
|
||||||
|
for (PDOptionalContentGroup ocg : ocProperties.getOptionalContentGroups()) {
|
||||||
ObjectNode layerNode = objectMapper.createObjectNode();
|
ObjectNode layerNode = objectMapper.createObjectNode();
|
||||||
layerNode.put("Name", layer.getPdfObject().getAsString(PdfName.Name).toString());
|
layerNode.put("Name", ocg.getName());
|
||||||
layersArray.add(layerNode);
|
layersArray.add(layerNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
other.set("Layers", layersArray);
|
other.set("Layers", layersArray);
|
||||||
|
|
||||||
//TODO Security
|
//TODO Security
|
||||||
@ -263,12 +275,6 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Digital Signatures using iText7 TODO
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
||||||
ArrayNode structureTreeArray;
|
ArrayNode structureTreeArray;
|
||||||
try {
|
try {
|
||||||
@ -282,13 +288,13 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
boolean isPdfACompliant = checkOutputIntent(itextDoc, "PDF/A");
|
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
||||||
boolean isPdfXCompliant = checkOutputIntent(itextDoc, "PDF/X");
|
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
|
||||||
boolean isPdfECompliant = checkForStandard(itextDoc, "PDF/E");
|
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
|
||||||
boolean isPdfVTCompliant = checkForStandard(itextDoc, "PDF/VT");
|
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
|
||||||
boolean isPdfUACompliant = checkForStandard(itextDoc, "PDF/UA");
|
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
|
||||||
boolean isPdfBCompliant = checkForStandard(itextDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard.
|
boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard.
|
||||||
boolean isPdfSECCompliant = checkForStandard(itextDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
|
boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
|
||||||
|
|
||||||
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
||||||
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
||||||
@ -302,25 +308,37 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
|
||||||
ArrayNode bookmarksArray = objectMapper.createArrayNode();
|
ArrayNode bookmarksArray = objectMapper.createArrayNode();
|
||||||
PdfOutline root = itextDoc.getOutlines(false);
|
|
||||||
if (root != null) {
|
if (root != null) {
|
||||||
for (PdfOutline child : root.getAllChildren()) {
|
for (PDOutlineItem child : root.children()) {
|
||||||
addOutlinesToArray(child, bookmarksArray);
|
addOutlinesToArray(child, bookmarksArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
other.set("Bookmarks/Outline/TOC", bookmarksArray);
|
other.set("Bookmarks/Outline/TOC", bookmarksArray);
|
||||||
|
|
||||||
byte[] xmpBytes = itextDoc.getXmpMetadata();
|
|
||||||
|
|
||||||
|
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
|
||||||
|
|
||||||
String xmpString = null;
|
String xmpString = null;
|
||||||
if (xmpBytes != null) {
|
|
||||||
|
if (pdMetadata != null) {
|
||||||
try {
|
try {
|
||||||
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes);
|
COSInputStream is = pdMetadata.createInputStream();
|
||||||
xmpString = new String(XMPMetaFactory.serializeToBuffer(xmpMeta, new SerializeOptions()));
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
} catch (XMPException e) {
|
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
||||||
|
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||||
|
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
} catch (XmpParsingException | IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
other.put("XMPMetadata", xmpString);
|
other.put("XMPMetadata", xmpString);
|
||||||
|
|
||||||
|
|
||||||
@ -356,39 +374,57 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||||
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) {
|
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
||||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||||
|
|
||||||
|
// Retrieve the page
|
||||||
|
PDPage page = pdfBoxDoc.getPage(pageNum);
|
||||||
|
|
||||||
// Page-level Information
|
// Page-level Information
|
||||||
Rectangle pageSize = itextDoc.getPage(pageNum).getPageSize();
|
PDRectangle mediaBox = page.getMediaBox();
|
||||||
pageInfo.put("Width", pageSize.getWidth());
|
|
||||||
pageInfo.put("Height", pageSize.getHeight());
|
float width = mediaBox.getWidth();
|
||||||
pageInfo.put("Rotation", itextDoc.getPage(pageNum).getRotation());
|
float height = mediaBox.getHeight();
|
||||||
pageInfo.put("Page Orientation", getPageOrientation(pageSize.getWidth(),pageSize.getHeight()));
|
|
||||||
pageInfo.put("Standard Size", getPageSize(pageSize.getWidth(),pageSize.getHeight()));
|
pageInfo.put("Width", width);
|
||||||
|
pageInfo.put("Height", height);
|
||||||
|
pageInfo.put("Rotation", page.getRotation());
|
||||||
|
|
||||||
|
pageInfo.put("Page Orientation", getPageOrientation(width, height));
|
||||||
|
pageInfo.put("Standard Size", getPageSize(width, height));
|
||||||
|
|
||||||
// Boxes
|
// Boxes
|
||||||
pageInfo.put("MediaBox", itextDoc.getPage(pageNum).getMediaBox().toString());
|
pageInfo.put("MediaBox", mediaBox.toString());
|
||||||
pageInfo.put("CropBox", itextDoc.getPage(pageNum).getCropBox().toString());
|
|
||||||
pageInfo.put("BleedBox", itextDoc.getPage(pageNum).getBleedBox().toString());
|
// Assuming the following boxes are defined for your document; if not, you may get null values.
|
||||||
pageInfo.put("TrimBox", itextDoc.getPage(pageNum).getTrimBox().toString());
|
PDRectangle cropBox = page.getCropBox();
|
||||||
pageInfo.put("ArtBox", itextDoc.getPage(pageNum).getArtBox().toString());
|
pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
|
||||||
|
|
||||||
|
PDRectangle bleedBox = page.getBleedBox();
|
||||||
|
pageInfo.put("BleedBox", bleedBox == null ? "Undefined" : bleedBox.toString());
|
||||||
|
|
||||||
|
PDRectangle trimBox = page.getTrimBox();
|
||||||
|
pageInfo.put("TrimBox", trimBox == null ? "Undefined" : trimBox.toString());
|
||||||
|
|
||||||
|
PDRectangle artBox = page.getArtBox();
|
||||||
|
pageInfo.put("ArtBox", artBox == null ? "Undefined" : artBox.toString());
|
||||||
|
|
||||||
// Content Extraction
|
// Content Extraction
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
textStripper.setStartPage(pageNum -1);
|
textStripper.setStartPage(pageNum + 1);
|
||||||
textStripper.setEndPage(pageNum - 1);
|
textStripper.setEndPage(pageNum +1);
|
||||||
String pageText = textStripper.getText(pdfBoxDoc);
|
String pageText = textStripper.getText(pdfBoxDoc);
|
||||||
|
|
||||||
pageInfo.put("Text Characters Count", pageText.length()); //
|
pageInfo.put("Text Characters Count", pageText.length()); //
|
||||||
|
|
||||||
// Annotations
|
// Annotations
|
||||||
List<PdfAnnotation> annotations = itextDoc.getPage(pageNum).getAnnotations();
|
|
||||||
|
List<PDAnnotation> annotations = page.getAnnotations();
|
||||||
|
|
||||||
int subtypeCount = 0;
|
int subtypeCount = 0;
|
||||||
int contentsCount = 0;
|
int contentsCount = 0;
|
||||||
|
|
||||||
for (PdfAnnotation annotation : annotations) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
if (annotation.getSubtype() != null) {
|
if (annotation.getSubtype() != null) {
|
||||||
subtypeCount++; // Increase subtype count
|
subtypeCount++; // Increase subtype count
|
||||||
}
|
}
|
||||||
@ -403,27 +439,33 @@ public class GetInfoOnPDF {
|
|||||||
annotationsObject.put("ContentsCount", contentsCount);
|
annotationsObject.put("ContentsCount", contentsCount);
|
||||||
pageInfo.set("Annotations", annotationsObject);
|
pageInfo.set("Annotations", annotationsObject);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Images (simplified)
|
// Images (simplified)
|
||||||
// This part is non-trivial as images can be embedded in multiple ways in a PDF.
|
// This part is non-trivial as images can be embedded in multiple ways in a PDF.
|
||||||
// Here is a basic structure to recognize image XObjects on a page.
|
// Here is a basic structure to recognize image XObjects on a page.
|
||||||
ArrayNode imagesArray = objectMapper.createArrayNode();
|
ArrayNode imagesArray = objectMapper.createArrayNode();
|
||||||
PdfResources resources = itextDoc.getPage(pageNum).getResources();
|
PDResources resources = page.getResources();
|
||||||
for (PdfName name : resources.getResourceNames()) {
|
|
||||||
PdfObject obj = resources.getResource(name);
|
|
||||||
if (obj instanceof PdfStream) {
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
PdfStream stream = (PdfStream) obj;
|
PDXObject xObject = resources.getXObject(name);
|
||||||
if (PdfName.Image.equals(stream.getAsName(PdfName.Subtype))) {
|
if (xObject instanceof PDImageXObject) {
|
||||||
|
PDImageXObject image = (PDImageXObject) xObject;
|
||||||
|
|
||||||
ObjectNode imageNode = objectMapper.createObjectNode();
|
ObjectNode imageNode = objectMapper.createObjectNode();
|
||||||
imageNode.put("Width", stream.getAsNumber(PdfName.Width).intValue());
|
imageNode.put("Width", image.getWidth());
|
||||||
imageNode.put("Height", stream.getAsNumber(PdfName.Height).intValue());
|
imageNode.put("Height", image.getHeight());
|
||||||
PdfObject colorSpace = stream.get(PdfName.ColorSpace);
|
if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) {
|
||||||
if (colorSpace != null) {
|
imageNode.put("Name", image.getMetadata().getFile().getFile());
|
||||||
imageNode.put("ColorSpace", colorSpace.toString());
|
|
||||||
}
|
}
|
||||||
|
if (image.getColorSpace() != null) {
|
||||||
|
imageNode.put("ColorSpace", image.getColorSpace().getName());
|
||||||
|
}
|
||||||
|
|
||||||
imagesArray.add(imageNode);
|
imagesArray.add(imageNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pageInfo.set("Images", imagesArray);
|
pageInfo.set("Images", imagesArray);
|
||||||
|
|
||||||
|
|
||||||
@ -431,11 +473,12 @@ public class GetInfoOnPDF {
|
|||||||
ArrayNode linksArray = objectMapper.createArrayNode();
|
ArrayNode linksArray = objectMapper.createArrayNode();
|
||||||
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
||||||
|
|
||||||
for (PdfAnnotation annotation : annotations) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
if (annotation instanceof PdfLinkAnnotation) {
|
if (annotation instanceof PDAnnotationLink) {
|
||||||
PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation;
|
PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation;
|
||||||
if(linkAnnotation != null && linkAnnotation.getAction() != null) {
|
if (linkAnnotation.getAction() instanceof PDActionURI) {
|
||||||
String uri = linkAnnotation.getAction().toString();
|
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
|
||||||
|
String uri = uriAction.getURI();
|
||||||
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -449,83 +492,41 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
pageInfo.set("Links", linksArray);
|
pageInfo.set("Links", linksArray);
|
||||||
|
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
ArrayNode fontsArray = objectMapper.createArrayNode();
|
ArrayNode fontsArray = objectMapper.createArrayNode();
|
||||||
PdfDictionary fontDicts = resources.getResource(PdfName.Font);
|
|
||||||
Set<String> uniqueSubtypes = new HashSet<>(); // To store unique subtypes
|
|
||||||
|
|
||||||
// Map to store unique fonts and their counts
|
|
||||||
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
|
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
|
||||||
|
|
||||||
if (fontDicts != null) {
|
for (COSName fontName : resources.getFontNames()) {
|
||||||
for (PdfName key : fontDicts.keySet()) {
|
PDFont font = resources.getFont(fontName);
|
||||||
ObjectNode fontNode = objectMapper.createObjectNode(); // Create a new font node for each font
|
ObjectNode fontNode = objectMapper.createObjectNode();
|
||||||
PdfDictionary font = fontDicts.getAsDictionary(key);
|
|
||||||
|
|
||||||
boolean isEmbedded = font.containsKey(PdfName.FontFile) ||
|
fontNode.put("IsEmbedded", font.isEmbedded());
|
||||||
font.containsKey(PdfName.FontFile2) ||
|
|
||||||
font.containsKey(PdfName.FontFile3);
|
|
||||||
fontNode.put("IsEmbedded", isEmbedded);
|
|
||||||
|
|
||||||
if (font.containsKey(PdfName.Encoding)) {
|
// PDFBox provides Font's BaseFont (i.e., the font name) directly
|
||||||
String encoding = font.getAsName(PdfName.Encoding).toString();
|
fontNode.put("Name", font.getName());
|
||||||
fontNode.put("Encoding", encoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (font.getAsString(PdfName.BaseFont) != null) {
|
fontNode.put("Subtype", font.getType());
|
||||||
fontNode.put("Name", font.getAsString(PdfName.BaseFont).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
String subtype = null;
|
PDFontDescriptor fontDescriptor = font.getFontDescriptor();
|
||||||
if (font.containsKey(PdfName.Subtype)) {
|
|
||||||
subtype = font.getAsName(PdfName.Subtype).toString();
|
|
||||||
uniqueSubtypes.add(subtype); // Add to set to ensure uniqueness
|
|
||||||
}
|
|
||||||
fontNode.put("Subtype", subtype);
|
|
||||||
|
|
||||||
PdfDictionary fontDescriptor = font.getAsDictionary(PdfName.FontDescriptor);
|
|
||||||
if (fontDescriptor != null) {
|
if (fontDescriptor != null) {
|
||||||
if (fontDescriptor.containsKey(PdfName.ItalicAngle)) {
|
fontNode.put("ItalicAngle", fontDescriptor.getItalicAngle());
|
||||||
fontNode.put("ItalicAngle", fontDescriptor.getAsNumber(PdfName.ItalicAngle).floatValue());
|
int flags = fontDescriptor.getFlags();
|
||||||
|
fontNode.put("IsItalic", (flags & 1) != 0);
|
||||||
|
fontNode.put("IsBold", (flags & 64) != 0);
|
||||||
|
fontNode.put("IsFixedPitch", (flags & 2) != 0);
|
||||||
|
fontNode.put("IsSerif", (flags & 4) != 0);
|
||||||
|
fontNode.put("IsSymbolic", (flags & 8) != 0);
|
||||||
|
fontNode.put("IsScript", (flags & 16) != 0);
|
||||||
|
fontNode.put("IsNonsymbolic", (flags & 32) != 0);
|
||||||
|
|
||||||
|
fontNode.put("FontFamily", fontDescriptor.getFontFamily());
|
||||||
|
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity
|
||||||
|
fontNode.put("FontWeight", fontDescriptor.getFontWeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fontDescriptor.containsKey(PdfName.Flags)) {
|
|
||||||
int flags = fontDescriptor.getAsNumber(PdfName.Flags).intValue();
|
|
||||||
fontNode.put("IsItalic", (flags & 64) != 0);
|
|
||||||
fontNode.put("IsBold", (flags & 1 << 16) != 0);
|
|
||||||
fontNode.put("IsFixedPitch", (flags & 1) != 0);
|
|
||||||
fontNode.put("IsSerif", (flags & 2) != 0);
|
|
||||||
fontNode.put("IsSymbolic", (flags & 4) != 0);
|
|
||||||
fontNode.put("IsScript", (flags & 8) != 0);
|
|
||||||
fontNode.put("IsNonsymbolic", (flags & 16) != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fontDescriptor.containsKey(PdfName.FontFamily)) {
|
|
||||||
String fontFamily = fontDescriptor.getAsString(PdfName.FontFamily).toString();
|
|
||||||
fontNode.put("FontFamily", fontFamily);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fontDescriptor.containsKey(PdfName.FontStretch)) {
|
|
||||||
String fontStretch = fontDescriptor.getAsName(PdfName.FontStretch).toString();
|
|
||||||
fontNode.put("FontStretch", fontStretch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fontDescriptor.containsKey(PdfName.FontBBox)) {
|
|
||||||
PdfArray bbox = fontDescriptor.getAsArray(PdfName.FontBBox);
|
|
||||||
fontNode.put("FontBoundingBox", bbox.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fontDescriptor.containsKey(PdfName.FontWeight)) {
|
|
||||||
float fontWeight = fontDescriptor.getAsNumber(PdfName.FontWeight).floatValue();
|
|
||||||
fontNode.put("FontWeight", fontWeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (font.containsKey(PdfName.ToUnicode)) {
|
|
||||||
fontNode.put("HasToUnicodeMap", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fontNode.size() > 0) {
|
|
||||||
// Create a unique key for this font node based on its attributes
|
// Create a unique key for this font node based on its attributes
|
||||||
String uniqueKey = fontNode.toString();
|
String uniqueKey = fontNode.toString();
|
||||||
|
|
||||||
@ -539,8 +540,6 @@ public class GetInfoOnPDF {
|
|||||||
uniqueFontsMap.put(uniqueKey, fontNode);
|
uniqueFontsMap.put(uniqueKey, fontNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add unique font entries to fontsArray
|
// Add unique font entries to fontsArray
|
||||||
for (ObjectNode uniqueFontNode : uniqueFontsMap.values()) {
|
for (ObjectNode uniqueFontNode : uniqueFontsMap.values()) {
|
||||||
@ -552,42 +551,50 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Access resources dictionary
|
|
||||||
PdfDictionary resourcesDict = itextDoc.getPage(pageNum).getResources().getPdfObject();
|
|
||||||
|
|
||||||
// Color Spaces & ICC Profiles
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Access resources dictionary
|
||||||
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
|
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
|
||||||
PdfDictionary colorSpaces = resourcesDict.getAsDictionary(PdfName.ColorSpace);
|
|
||||||
if (colorSpaces != null) {
|
Iterable<COSName> colorSpaceNames = resources.getColorSpaceNames();
|
||||||
for (PdfName name : colorSpaces.keySet()) {
|
for (COSName name : colorSpaceNames) {
|
||||||
PdfObject colorSpaceObject = colorSpaces.get(name);
|
PDColorSpace colorSpace = resources.getColorSpace(name);
|
||||||
if (colorSpaceObject instanceof PdfArray) {
|
if (colorSpace instanceof PDICCBased) {
|
||||||
PdfArray colorSpaceArray = (PdfArray) colorSpaceObject;
|
PDICCBased iccBased = (PDICCBased) colorSpace;
|
||||||
if (colorSpaceArray.size() > 1 && colorSpaceArray.get(0) instanceof PdfName && PdfName.ICCBased.equals(colorSpaceArray.get(0))) {
|
PDStream iccData = iccBased.getPDStream();
|
||||||
ObjectNode iccProfileNode = objectMapper.createObjectNode();
|
byte[] iccBytes = iccData.toByteArray();
|
||||||
PdfStream iccStream = (PdfStream) colorSpaceArray.get(1);
|
|
||||||
byte[] iccData = iccStream.getBytes();
|
|
||||||
// TODO: Further decode and analyze the ICC data if needed
|
// TODO: Further decode and analyze the ICC data if needed
|
||||||
iccProfileNode.put("ICC Profile Length", iccData.length);
|
ObjectNode iccProfileNode = objectMapper.createObjectNode();
|
||||||
|
iccProfileNode.put("ICC Profile Length", iccBytes.length);
|
||||||
colorSpacesArray.add(iccProfileNode);
|
colorSpacesArray.add(iccProfileNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
|
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
|
||||||
|
|
||||||
|
|
||||||
// Other XObjects
|
// Other XObjects
|
||||||
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
|
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
|
||||||
PdfDictionary xObjects = resourcesDict.getAsDictionary(PdfName.XObject);
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
if (xObjects != null) {
|
PDXObject xObject = resources.getXObject(name);
|
||||||
for (PdfName name : xObjects.keySet()) {
|
String xObjectType;
|
||||||
PdfStream xObjectStream = xObjects.getAsStream(name);
|
|
||||||
String xObjectType = xObjectStream.getAsName(PdfName.Subtype).toString();
|
if (xObject instanceof PDImageXObject) {
|
||||||
|
xObjectType = "Image";
|
||||||
|
} else if (xObject instanceof PDFormXObject) {
|
||||||
|
xObjectType = "Form";
|
||||||
|
} else {
|
||||||
|
xObjectType = "Other";
|
||||||
|
}
|
||||||
|
|
||||||
// Increment the count for this type in the map
|
// Increment the count for this type in the map
|
||||||
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add the count map to pageInfo (or wherever you want to store it)
|
// Add the count map to pageInfo (or wherever you want to store it)
|
||||||
ObjectNode xObjectCountNode = objectMapper.createObjectNode();
|
ObjectNode xObjectCountNode = objectMapper.createObjectNode();
|
||||||
@ -598,14 +605,17 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ArrayNode multimediaArray = objectMapper.createArrayNode();
|
ArrayNode multimediaArray = objectMapper.createArrayNode();
|
||||||
for (PdfAnnotation annotation : annotations) {
|
|
||||||
if (PdfName.RichMedia.equals(annotation.getSubtype())) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
|
if ("RichMedia".equals(annotation.getSubtype())) {
|
||||||
ObjectNode multimediaNode = objectMapper.createObjectNode();
|
ObjectNode multimediaNode = objectMapper.createObjectNode();
|
||||||
// Extract details from the dictionary as needed
|
// Extract details from the annotation as needed
|
||||||
multimediaArray.add(multimediaNode);
|
multimediaArray.add(multimediaNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageInfo.set("Multimedia", multimediaArray);
|
pageInfo.set("Multimedia", multimediaArray);
|
||||||
|
|
||||||
|
|
||||||
@ -636,17 +646,21 @@ public class GetInfoOnPDF {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOutlinesToArray(PdfOutline outline, ArrayNode arrayNode) {
|
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||||
if (outline == null) return;
|
if (outline == null) return;
|
||||||
|
|
||||||
ObjectNode outlineNode = objectMapper.createObjectNode();
|
ObjectNode outlineNode = objectMapper.createObjectNode();
|
||||||
outlineNode.put("Title", outline.getTitle());
|
outlineNode.put("Title", outline.getTitle());
|
||||||
// You can add other properties if needed
|
// You can add other properties if needed
|
||||||
arrayNode.add(outlineNode);
|
arrayNode.add(outlineNode);
|
||||||
|
|
||||||
for (PdfOutline child : outline.getAllChildren()) {
|
PDOutlineItem child = outline.getFirstChild();
|
||||||
|
while (child != null) {
|
||||||
addOutlinesToArray(child, arrayNode);
|
addOutlinesToArray(child, arrayNode);
|
||||||
|
child = child.getNextSibling();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPageOrientation(double width, double height) {
|
public String getPageOrientation(double width, double height) {
|
||||||
if (width > height) {
|
if (width > height) {
|
||||||
return "Landscape";
|
return "Landscape";
|
||||||
@ -678,22 +692,26 @@ public class GetInfoOnPDF {
|
|||||||
return Math.abs(pageAspectRatio - aspectRatio) <= 0.05;
|
return Math.abs(pageAspectRatio - aspectRatio) <= 0.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkForStandard(PdfDocument document, String standardKeyword) {
|
|
||||||
// Check Output Intents
|
|
||||||
boolean foundInOutputIntents = checkOutputIntent(document, standardKeyword);
|
|
||||||
if (foundInOutputIntents) return true;
|
|
||||||
|
|
||||||
// Check XMP Metadata (rudimentary)
|
|
||||||
|
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
||||||
|
// Check XMP Metadata
|
||||||
try {
|
try {
|
||||||
byte[] metadataBytes = document.getXmpMetadata();
|
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||||
if (metadataBytes != null) {
|
if (pdMetadata != null) {
|
||||||
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(metadataBytes);
|
COSInputStream metaStream = pdMetadata.createInputStream();
|
||||||
String xmpString = xmpMeta.dumpObject();
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
|
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
new XmpSerializer().serialize(xmpMeta, baos, true);
|
||||||
|
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (xmpString.contains(standardKeyword)) {
|
if (xmpString.contains(standardKeyword)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (XMPException e) {
|
} catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -701,22 +719,6 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean checkOutputIntent(PdfDocument document, String standard) {
|
|
||||||
PdfArray outputIntents = document.getCatalog().getPdfObject().getAsArray(PdfName.OutputIntents);
|
|
||||||
if (outputIntents != null && !outputIntents.isEmpty()) {
|
|
||||||
for (int i = 0; i < outputIntents.size(); i++) {
|
|
||||||
PdfDictionary outputIntentDict = outputIntents.getAsDictionary(i);
|
|
||||||
if (outputIntentDict != null) {
|
|
||||||
PdfString s = outputIntentDict.getAsString(PdfName.S);
|
|
||||||
if (s != null && s.toString().contains(standard)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
||||||
ArrayNode elementsArray = objectMapper.createArrayNode();
|
ArrayNode elementsArray = objectMapper.createArrayNode();
|
||||||
if (nodes != null) {
|
if (nodes != null) {
|
||||||
@ -771,7 +773,7 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPageModeDescription(PdfName pageMode) {
|
private String getPageModeDescription(String pageMode) {
|
||||||
return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown";
|
return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
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.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.core.io.ResourceLoader;
|
|
||||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -27,10 +13,8 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
@Controller
|
@Controller
|
||||||
@ -107,6 +91,7 @@ public class AccountWebController {
|
|||||||
model.addAttribute("username", username);
|
model.addAttribute("username", username);
|
||||||
model.addAttribute("role", user.get().getRolesAsString());
|
model.addAttribute("role", user.get().getRolesAsString());
|
||||||
model.addAttribute("settings", settingsJson);
|
model.addAttribute("settings", settingsJson);
|
||||||
|
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
@ -116,5 +101,35 @@ public class AccountWebController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/change-creds")
|
||||||
|
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
// Cast the principal object to UserDetails
|
||||||
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
|
||||||
|
// Retrieve username and other attributes
|
||||||
|
String username = userDetails.getUsername();
|
||||||
|
|
||||||
|
// Fetch user details from the database
|
||||||
|
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
|
||||||
|
if (!user.isPresent()) {
|
||||||
|
// Handle error appropriately
|
||||||
|
return "redirect:/error"; // Example redirection in case of error
|
||||||
|
}
|
||||||
|
// Add attributes to the model
|
||||||
|
model.addAttribute("username", username);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
return "change-creds";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import java.util.Arrays;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@ -140,27 +141,96 @@ public class GeneralWebController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ResourceLoader resourceLoader;
|
private ResourceLoader resourceLoader;
|
||||||
|
|
||||||
private List<String> getFontNames() {
|
private List<FontResource> getFontNames() {
|
||||||
|
List<FontResource> fontNames = new ArrayList<>();
|
||||||
|
|
||||||
|
// Extract font names from classpath
|
||||||
|
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||||
|
|
||||||
|
// Extract font names from external directory
|
||||||
|
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
|
||||||
|
|
||||||
|
return fontNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||||
try {
|
try {
|
||||||
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||||
.getResources("classpath:static/fonts/*.woff2");
|
.getResources(locationPattern);
|
||||||
|
|
||||||
return Arrays.stream(resources)
|
return Arrays.stream(resources)
|
||||||
.map(resource -> {
|
.map(resource -> {
|
||||||
try {
|
try {
|
||||||
String filename = resource.getFilename();
|
String filename = resource.getFilename();
|
||||||
return filename.substring(0, filename.length() - 6); // Remove .woff2 extension
|
if (filename != null) {
|
||||||
|
int lastDotIndex = filename.lastIndexOf('.');
|
||||||
|
if (lastDotIndex != -1) {
|
||||||
|
String name = filename.substring(0, lastDotIndex);
|
||||||
|
String extension = filename.substring(lastDotIndex + 1);
|
||||||
|
return new FontResource(name, extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error processing filename", e);
|
throw new RuntimeException("Error processing filename", e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to read font directory", e);
|
throw new RuntimeException("Failed to read font directory from " + locationPattern, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getFormatFromExtension(String extension) {
|
||||||
|
switch (extension) {
|
||||||
|
case "ttf": return "truetype";
|
||||||
|
case "woff": return "woff";
|
||||||
|
case "woff2": return "woff2";
|
||||||
|
case "eot": return "embedded-opentype";
|
||||||
|
case "svg": return "svg";
|
||||||
|
default: return ""; // or throw an exception if an unexpected extension is encountered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FontResource {
|
||||||
|
private String name;
|
||||||
|
private String extension;
|
||||||
|
private String type;
|
||||||
|
public FontResource(String name, String extension) {
|
||||||
|
this.name = name;
|
||||||
|
this.extension = extension;
|
||||||
|
this.type = getFormatFromExtension(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtension() {
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtension(String extension) {
|
||||||
|
this.extension = extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/crop")
|
@GetMapping("/crop")
|
||||||
@Hidden
|
@Hidden
|
||||||
|
@ -104,7 +104,6 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
public static class Security {
|
public static class Security {
|
||||||
private Boolean enableLogin;
|
private Boolean enableLogin;
|
||||||
private InitialLogin initialLogin;
|
|
||||||
private Boolean csrfDisabled;
|
private Boolean csrfDisabled;
|
||||||
|
|
||||||
public Boolean getEnableLogin() {
|
public Boolean getEnableLogin() {
|
||||||
@ -115,14 +114,6 @@ public class ApplicationProperties {
|
|||||||
this.enableLogin = enableLogin;
|
this.enableLogin = enableLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InitialLogin getInitialLogin() {
|
|
||||||
return initialLogin != null ? initialLogin : new InitialLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInitialLogin(InitialLogin initialLogin) {
|
|
||||||
this.initialLogin = initialLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getCsrfDisabled() {
|
public Boolean getCsrfDisabled() {
|
||||||
return csrfDisabled;
|
return csrfDisabled;
|
||||||
}
|
}
|
||||||
@ -134,40 +125,9 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled="
|
return "Security [enableLogin=" + enableLogin + ", csrfDisabled="
|
||||||
+ csrfDisabled + "]";
|
+ csrfDisabled + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class InitialLogin {
|
|
||||||
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUsername(String username) {
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPassword(String password) {
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class System {
|
public static class System {
|
||||||
|
@ -40,6 +40,9 @@ public class User {
|
|||||||
@Column(name = "enabled")
|
@Column(name = "enabled")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Column(name = "isFirstLogin")
|
||||||
|
private Boolean isFirstLogin = false;
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
|
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
|
||||||
private Set<Authority> authorities = new HashSet<>();
|
private Set<Authority> authorities = new HashSet<>();
|
||||||
|
|
||||||
@ -50,6 +53,13 @@ public class User {
|
|||||||
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
|
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isFirstLogin() {
|
||||||
|
return isFirstLogin != null && isFirstLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstLogin(boolean isFirstLogin) {
|
||||||
|
this.isFirstLogin = isFirstLogin;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
|
@ -65,7 +65,8 @@ public class GeneralUtils {
|
|||||||
} else if (sizeStr.endsWith("B")) {
|
} else if (sizeStr.endsWith("B")) {
|
||||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||||
} else {
|
} else {
|
||||||
// Input string does not have a valid format, handle this case
|
// Assume MB if no unit is specified
|
||||||
|
return (long) (Double.parseDouble(sizeStr) * 1024 * 1024);
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// The numeric part of the input string cannot be parsed, handle this case
|
// The numeric part of the input string cannot be parsed, handle this case
|
||||||
|
@ -12,8 +12,6 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
|
||||||
import com.itextpdf.kernel.pdf.PdfWriter;
|
|
||||||
|
|
||||||
public class WebResponseUtils {
|
public class WebResponseUtils {
|
||||||
|
|
||||||
@ -61,18 +59,6 @@ public class WebResponseUtils {
|
|||||||
return boasToWebResponse(baos, docName);
|
return boasToWebResponse(baos, docName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResponseEntity<byte[]> pdfDocToWebResponse(PdfDocument document, String docName) throws IOException {
|
|
||||||
|
|
||||||
// Open Byte Array and save document to it
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
PdfWriter writer = new PdfWriter(baos);
|
|
||||||
PdfDocument newDocument = new PdfDocument(writer);
|
|
||||||
|
|
||||||
document.copyPagesTo(1, document.getNumberOfPages(), newDocument);
|
|
||||||
newDocument.close();
|
|
||||||
|
|
||||||
return boasToWebResponse(baos, docName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,11 @@ green=Green
|
|||||||
blue=Blue
|
blue=Blue
|
||||||
custom=Custom...
|
custom=Custom...
|
||||||
|
|
||||||
|
changedCredsMessage=Credentials changed!
|
||||||
|
notAuthenticatedMessage=User not authenticated.
|
||||||
|
userNotFoundMessage=User not found.
|
||||||
|
incorrectPasswordMessage=Current password is incorrect.
|
||||||
|
usernameExistsMessage=New Username already exists.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -71,6 +75,19 @@ settings.zipThreshold=Zip files when the number of downloaded files exceeds
|
|||||||
settings.signOut=Sign Out
|
settings.signOut=Sign Out
|
||||||
settings.accountSettings=Account Settings
|
settings.accountSettings=Account Settings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
changeCreds.title=Change Credentials
|
||||||
|
changeCreds.header=Update Your Account Details
|
||||||
|
changeCreds.changeUserAndPassword=You are using default login credentials. Please enter a new password (and username if wanted)
|
||||||
|
changeCreds.newUsername=New Username
|
||||||
|
changeCreds.oldPassword=Current Password
|
||||||
|
changeCreds.newPassword=New Password
|
||||||
|
changeCreds.confirmNewPassword=Confirm New Password
|
||||||
|
changeCreds.submit=Submit Changes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
account.title=Account Settings
|
account.title=Account Settings
|
||||||
account.accountSettings=Account Settings
|
account.accountSettings=Account Settings
|
||||||
account.adminSettings=Admin Settings - View and Add Users
|
account.adminSettings=Admin Settings - View and Add Users
|
||||||
@ -102,6 +119,7 @@ adminUserSettings.role=Role
|
|||||||
adminUserSettings.actions=Actions
|
adminUserSettings.actions=Actions
|
||||||
adminUserSettings.apiUser=Limited API User
|
adminUserSettings.apiUser=Limited API User
|
||||||
adminUserSettings.webOnlyUser=Web Only User
|
adminUserSettings.webOnlyUser=Web Only User
|
||||||
|
adminUserSettings.forceChange = Force user to change username/password on login
|
||||||
adminUserSettings.submit=Save User
|
adminUserSettings.submit=Save User
|
||||||
|
|
||||||
#############
|
#############
|
||||||
@ -750,13 +768,6 @@ changeMetadata.selectText.5=Add Custom Metadata Entry
|
|||||||
changeMetadata.submit=Change
|
changeMetadata.submit=Change
|
||||||
|
|
||||||
|
|
||||||
#xlsToPdf
|
|
||||||
xlsToPdf.title=Excel to PDF
|
|
||||||
xlsToPdf.header=Excel to PDF
|
|
||||||
xlsToPdf.selectText.1=Select XLS or XLSX Excel sheet to convert
|
|
||||||
xlsToPdf.convert=convert
|
|
||||||
|
|
||||||
|
|
||||||
#pdfToPDFA
|
#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
|
||||||
|
@ -4,16 +4,11 @@
|
|||||||
|
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
initialLogin:
|
|
||||||
username: 'username' # Specify the initial username for first boot (e.g. 'admin')
|
|
||||||
password: 'password' # Specify the initial password for first boot (e.g. 'password123')
|
|
||||||
csrfDisabled: true
|
csrfDisabled: true
|
||||||
|
|
||||||
system:
|
system:
|
||||||
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||||
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
rootURIPath: / # Set the application's root URI (e.g. /pdf-app)
|
|
||||||
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
|
|
||||||
|
|
||||||
#ui:
|
#ui:
|
||||||
# appName: exampleAppName # Application's visible name
|
# appName: exampleAppName # Application's visible name
|
||||||
|
@ -16,11 +16,30 @@
|
|||||||
<!-- User Settings Title -->
|
<!-- User Settings Title -->
|
||||||
<h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2>
|
<h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{notAuthenticatedMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'userNotFound'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{userNotFoundMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{incorrectPasswordMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- At the top of the user settings -->
|
<!-- At the top of the user settings -->
|
||||||
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
|
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
|
||||||
|
|
||||||
|
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||||
|
<span th:text="${error}">Error Message</span>
|
||||||
|
</div>
|
||||||
<!-- Change Username Form -->
|
<!-- Change Username Form -->
|
||||||
<h4></h4>
|
<h4></h4>
|
||||||
<form action="/change-username" method="post">
|
<form action="/change-username" method="post">
|
||||||
|
@ -43,6 +43,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
|
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
<form action="/admin/saveUser" method="post">
|
<form action="/admin/saveUser" method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" th:text="#{username}">Username</label>
|
<label for="username" th:text="#{username}">Username</label>
|
||||||
@ -61,6 +64,10 @@
|
|||||||
<option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
|
<option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
|
||||||
|
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Add other fields as required -->
|
<!-- Add other fields as required -->
|
||||||
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
||||||
|
72
src/main/resources/templates/change-creds.html
Normal file
72
src/main/resources/templates/change-creds.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<!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=#{changeCreds.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-9">
|
||||||
|
|
||||||
|
<!-- User Settings Title -->
|
||||||
|
<h2 class="text-center" th:text="#{changeCreds.header}">User Settings</h2>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{notAuthenticatedMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'userNotFound'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{userNotFoundMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{incorrectPasswordMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
|
||||||
|
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- At the top of the user settings -->
|
||||||
|
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Change Username Form -->
|
||||||
|
<h4></h4>
|
||||||
|
<h4 th:text="#{changeCreds.changeUserAndPassword}">Change Username and password</h4>
|
||||||
|
<form action="/change-username-and-password" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newUsername" th:text="#{changeCreds.newUsername}">New Username</label>
|
||||||
|
<input type="text" class="form-control" name="newUsername" id="newUsername" th:placeholder="${username}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="currentPassword" th:text="#{changeCreds.oldPassword}">Old Password</label>
|
||||||
|
<input type="password" class="form-control" name="currentPassword" id="currentPasswordPassword" th:placeholder="#{changeCreds.oldPassword}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newPassword" th:text="#{changeCreds.newPassword}">New Password</label>
|
||||||
|
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{changeCreds.newPassword}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="submit" class="btn btn-primary" th:text="#{changeCreds.submit}">Change credentials!</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -179,15 +179,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const urlParams = currentURL.searchParams;
|
const urlParams = currentURL.searchParams;
|
||||||
const currentLangParam = urlParams.get('lang') || defaultLocale;
|
const currentLangParam = urlParams.get('lang') || defaultLocale;
|
||||||
|
|
||||||
console.log("defaultLocale", defaultLocale)
|
|
||||||
console.log("storedLocale", storedLocale)
|
|
||||||
console.log("currentLangParam", currentLangParam)
|
|
||||||
|
|
||||||
if (currentLangParam !== storedLocale) {
|
if (defaultLocale !== storedLocale && currentLangParam !== storedLocale) {
|
||||||
urlParams.set('lang', storedLocale);
|
urlParams.set('lang', storedLocale);
|
||||||
currentURL.search = urlParams.toString();
|
currentURL.search = urlParams.toString();
|
||||||
|
|
||||||
console.log("redirecting to", currentURL.toString());
|
|
||||||
window.location.href = currentURL.toString();
|
window.location.href = currentURL.toString();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -238,14 +233,17 @@ function handleDropdownItemClick(event) {
|
|||||||
|
|
||||||
if (languageCode) {
|
if (languageCode) {
|
||||||
localStorage.setItem('languageCode', languageCode);
|
localStorage.setItem('languageCode', languageCode);
|
||||||
|
const currentLang = document.documentElement.getAttribute('lang');
|
||||||
|
if (currentLang !== languageCode) {
|
||||||
|
console.log("currentLang", currentLang)
|
||||||
|
console.log("languageCode", languageCode)
|
||||||
const currentUrl = window.location.href;
|
const currentUrl = window.location.href;
|
||||||
if (currentUrl.indexOf('?lang=') === -1) {
|
if (currentUrl.indexOf('?lang=') === -1) {
|
||||||
window.location.href = currentUrl + '?lang=' + languageCode;
|
window.location.href = currentUrl + '?lang=' + languageCode;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
|
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content
|
dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content
|
||||||
} else {
|
} else {
|
||||||
console.error("Language code is not set for this item.");
|
console.error("Language code is not set for this item.");
|
||||||
@ -258,6 +256,9 @@ function handleDropdownItemClick(event) {
|
|||||||
<div th:if="${logoutMessage}" class="alert alert-success"
|
<div th:if="${logoutMessage}" class="alert alert-success"
|
||||||
th:text="${logoutMessage}"></div>
|
th:text="${logoutMessage}"></div>
|
||||||
|
|
||||||
|
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'credsUpdated'}" class="alert alert-success">
|
||||||
|
<span th:text="#{changedCredsMessage}">Default message if not found</span>
|
||||||
|
</div>
|
||||||
<form th:action="@{login}" method="post">
|
<form th:action="@{login}" method="post">
|
||||||
<img class="mb-4" src="favicon.svg" alt="" width="144" height="144">
|
<img class="mb-4" src="favicon.svg" alt="" width="144" height="144">
|
||||||
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
|
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pageSize" th:text="#{scalePages.pageSize}"></label>
|
<label for="pageSize" th:text="#{scalePages.pageSize}"></label>
|
||||||
<select id="pageSize" name="pageSize" required>
|
<select id="pageSize" name="pageSize" required>
|
||||||
|
|
||||||
<option value="A0">A0</option>
|
<option value="A0">A0</option>
|
||||||
<option value="A1">A1</option>
|
<option value="A1">A1</option>
|
||||||
<option value="A2">A2</option>
|
<option value="A2">A2</option>
|
||||||
@ -27,25 +26,8 @@
|
|||||||
<option value="A4" selected>A4</option>
|
<option value="A4" selected>A4</option>
|
||||||
<option value="A5">A5</option>
|
<option value="A5">A5</option>
|
||||||
<option value="A6">A6</option>
|
<option value="A6">A6</option>
|
||||||
<option value="A7">A7</option>
|
|
||||||
<option value="A8">A8</option>
|
|
||||||
<option value="A9">A9</option>
|
|
||||||
<option value="A10">A10</option>
|
|
||||||
<option value="B0">B0</option>
|
|
||||||
<option value="B1">B1</option>
|
|
||||||
<option value="B2">B2</option>
|
|
||||||
<option value="B3">B3</option>
|
|
||||||
<option value="B4">B4</option>
|
|
||||||
<option value="B5">B5</option>
|
|
||||||
<option value="B6">B6</option>
|
|
||||||
<option value="B7">B7</option>
|
|
||||||
<option value="B8">B8</option>
|
|
||||||
<option value="B9">B9</option>
|
|
||||||
<option value="LETTER">Letter</option>
|
<option value="LETTER">Letter</option>
|
||||||
<option value="LEGAL">Legal</option>
|
<option value="LEGAL">Legal</option>
|
||||||
<option value="EXECUTIVE">Executive</option>
|
|
||||||
<option value="TABLOID">Tabloid</option>
|
|
||||||
<option value="LEDGER">Ledger</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -28,7 +28,16 @@
|
|||||||
<option value="image">Image</option>
|
<option value="image">Image</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="alphabetGroup" class="mb-3">
|
||||||
|
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
|
||||||
|
<select class="form-control" name="alphabet" id="alphabet-select">
|
||||||
|
<option value="roman">Roman</option>
|
||||||
|
<option value="arabic">العربية</option>
|
||||||
|
<option value="japanese">日本語</option>
|
||||||
|
<option value="korean">한국어</option>
|
||||||
|
<option value="chinese">简体中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div id="watermarkTextGroup" class="mb-3">
|
<div id="watermarkTextGroup" class="mb-3">
|
||||||
<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 />
|
||||||
@ -105,6 +114,7 @@
|
|||||||
const watermarkType = document.getElementById('watermarkType').value;
|
const watermarkType = document.getElementById('watermarkType').value;
|
||||||
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
|
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
|
||||||
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
|
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
|
||||||
|
const alphabetGroup = document.getElementById('alphabetGroup'); // This is the new addition
|
||||||
const watermarkText = document.getElementById('watermarkText');
|
const watermarkText = document.getElementById('watermarkText');
|
||||||
const watermarkImage = document.getElementById('watermarkImage');
|
const watermarkImage = document.getElementById('watermarkImage');
|
||||||
|
|
||||||
@ -113,11 +123,13 @@
|
|||||||
watermarkText.required = true;
|
watermarkText.required = true;
|
||||||
watermarkImageGroup.style.display = 'none';
|
watermarkImageGroup.style.display = 'none';
|
||||||
watermarkImage.required = false;
|
watermarkImage.required = false;
|
||||||
|
alphabetGroup.style.display = 'block';
|
||||||
} else if (watermarkType === 'image') {
|
} else if (watermarkType === 'image') {
|
||||||
watermarkTextGroup.style.display = 'none';
|
watermarkTextGroup.style.display = 'none';
|
||||||
watermarkText.required = false;
|
watermarkText.required = false;
|
||||||
watermarkImageGroup.style.display = 'block';
|
watermarkImageGroup.style.display = 'block';
|
||||||
watermarkImage.required = true;
|
watermarkImage.required = true;
|
||||||
|
alphabetGroup.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,15 +10,16 @@
|
|||||||
<th:block th:each="font : ${fonts}">
|
<th:block th:each="font : ${fonts}">
|
||||||
<style th:inline="text">
|
<style th:inline="text">
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "[[${font}]]";
|
font-family: "[[${font.name}]]";
|
||||||
src: url('fonts/[[${font}]].woff2') format('woff2');
|
src: url('fonts/[[${font.name}]].[[${font.extension}]]') format('[[${font.type}]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
#font-select option[value="[[${font}]]"] {
|
#font-select option[value="[[${font.name}]]"] {
|
||||||
font-family: "[[${font}]]", cursive;
|
font-family: "[[${font.name}]]", cursive;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
select#font-select, select#font-select option {
|
select#font-select, select#font-select option {
|
||||||
height: 60px; /* Adjust as needed */
|
height: 60px; /* Adjust as needed */
|
||||||
@ -181,9 +182,13 @@ select#font-select, select#font-select option {
|
|||||||
<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 th:each="font : ${fonts}" th:value="${font}" th:text="${font}" th:class="${font.toLowerCase()+'-font'}"></option>
|
<option th:each="font : ${fonts}"
|
||||||
|
th:value="${font.name}"
|
||||||
|
th:text="${font.name}"
|
||||||
|
th:class="${font.name.toLowerCase()+'-font'}">
|
||||||
|
</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>
|
||||||
@ -233,12 +238,13 @@ select#font-select, select#font-select option {
|
|||||||
|
|
||||||
<th:block th:each="font : ${fonts}">
|
<th:block th:each="font : ${fonts}">
|
||||||
<style th:inline="text">
|
<style th:inline="text">
|
||||||
#font-select option[value="/*[[${font}]]*/"] {
|
#font-select option[value='/*[[${font.name}]]*/'] {
|
||||||
font-family: '/*[[${font}]]*/', cursive;
|
font-family: '/*[[${font.name}]]*/', cursive;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user