Merge pull request #342 from Frooodle/itextRemoval

Itext removal
This commit is contained in:
Anthony Stirling 2023-09-06 22:23:29 +01:00 committed by GitHub
commit fefa8347da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1462 additions and 1255 deletions

View file

@ -1,31 +1,39 @@
# Build jbig2enc in a separate stage
# Use the base image
FROM frooodle/stirling-pdf-base:beta4
ARG VERSION_TAG
ENV VERSION_TAG=$VERSION_TAG
# Set Environment Variables
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
RUN mkdir /scripts
# Set up necessary directories and permissions
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/
#Install fonts
RUN mkdir /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
RUN fc-cache -f -v
# Always copy the 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
# Set environment variables
ENV APP_HOME_NAME="Stirling PDF"
# Run the application
RUN chmod +x /scripts/init.sh
# Set user and run command
USER stirlingpdfuser
ENTRYPOINT ["/scripts/init.sh"]
CMD ["java", "-jar", "/app.jar"]

View file

@ -10,17 +10,43 @@ RUN apt-get update && \
unoconv && \
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
# Set font cache and permissions
RUN fc-cache -f -v && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Expose the application port
EXPOSE 8080
# Set environment variables
ENV GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
ENV DOCKER_ENABLE_SECURITY=false
# Run the application
USER stirlingpdfuser
CMD ["java", "-jar", "/app.jar"]

View file

@ -1,16 +1,33 @@
# Build jbig2enc in a separate stage
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
# Set font cache and permissions
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Expose the application port
EXPOSE 8080
# Set environment variables
ENV GROUPS_TO_REMOVE=CLI
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
ENV DOCKER_ENABLE_SECURITY=false
# Run the application

View file

@ -66,7 +66,6 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
## Technologies used
- Spring Boot + Thymeleaf
- PDFBox
- IText7
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript

View file

@ -86,9 +86,9 @@ dependencies {
//general PDF
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:bcpkix-jdk15on:1.70'
implementation 'com.itextpdf:itext7-core:7.2.5'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'

View file

@ -9,7 +9,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils;
@ -66,7 +65,6 @@ public class SPdfApplication {
GeneralUtils.createDir("customFiles/static/");
GeneralUtils.createDir("customFiles/templates/");
GeneralUtils.createDir("config");

View file

@ -13,7 +13,6 @@ public class AppConfig {
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
System.out.println(applicationProperties.toString());
return applicationProperties.getSecurity().getEnableLogin();
}

View file

@ -13,7 +13,7 @@ import jakarta.servlet.http.HttpServletResponse;
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
@ -32,7 +32,6 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
if (keyValue.length != 2) {
continue;
}
if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]);
}

View file

@ -1,10 +1,18 @@
package stirling.software.SPDF.config;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
@ -20,24 +28,74 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
}
}
public void ensureConfigExists() throws IOException {
// Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml");
public void ensureConfigExists() throws IOException {
// Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists
if (Files.notExists(destPath)) {
// Ensure the destination directory exists
Files.createDirectories(destPath.getParent());
// Check if the file already exists
if (Files.notExists(destPath)) {
// Ensure the destination directory exists
Files.createDirectories(destPath.getParent());
// Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
if (in != null) {
Files.copy(in, destPath);
} else {
throw new FileNotFoundException("Resource file not found: settings.yml.template");
}
}
}
}
// Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
if (in != null) {
Files.copy(in, destPath);
} else {
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);
}
}

View file

@ -1,9 +1,5 @@
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.Configuration;
@ -18,14 +14,9 @@ public class OpenApiConfig {
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
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
}
}
return new OpenAPI().components(new Components()).info(

View file

@ -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);
}
}

View file

@ -11,28 +11,27 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
@Component
public class InitialSecuritySetup {
@Autowired
private UserService userService;
@Autowired
ApplicationProperties applicationProperties;
@PostConstruct
public void init() {
if (!userService.hasUsers()) {
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
}
String initialUsername = "admin";
String initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}

View file

@ -1,15 +1,14 @@
package stirling.software.SPDF.config.security;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
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.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -42,6 +41,9 @@ public class SecurityConfiguration {
@Autowired
private UserAuthenticationFilter userAuthenticationFilter;
@Autowired
private FirstLoginFilter firstLoginFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@ -49,6 +51,7 @@ public class SecurityConfiguration {
if(loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http
.formLogin(formLogin -> formLogin
.loginPage("/login")

View file

@ -44,7 +44,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
return;
}
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists
@ -74,13 +74,14 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod();
if ("GET".equalsIgnoreCase(method)) {
if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) {
response.sendRedirect("/login"); // redirect to the login page
return;
} else {
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");
return;
}
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");
return;
}
filterChain.doFilter(request, response);

View file

@ -113,12 +113,23 @@ public class UserService {
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) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
user.setFirstLogin(false);
userRepository.save(user);
}
@ -168,6 +179,12 @@ public class UserService {
userRepository.save(user);
}
public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse);
userRepository.save(user);
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}

View file

@ -4,6 +4,12 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.LoggerFactory;
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.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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
@ -30,49 +28,64 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
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")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
byte[] bytes = file.getBytes();
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
PdfCanvas pdfCanvas = new PdfCanvas(page);
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height)
throws IOException {
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.rectangle(x, y, width, height);
pdfCanvas.clip();
pdfCanvas.addXObject(formXObject, -x, -y);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
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();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
}
}

View file

@ -1,9 +1,16 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.LoggerFactory;
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.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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
@ -34,68 +32,73 @@ public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@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(
@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 = {
"2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet)
throws IOException {
@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 = {
"2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet)
throws IOException {
if (pagesPerSheet != 2 && pagesPerSheet != 3
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (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);
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
PageSize pageSize = new PageSize(PageSize.A4.rotate());
int totalPages = sourceDocument.getNumberOfPages();
float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows;
int totalPages = pdfDoc.getNumberOfPages();
float cellWidth = pageSize.getWidth() / cols;
float cellHeight = pageSize.getHeight() / rows;
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
for (int i = 1; i <= totalPages; i += pagesPerSheet) {
PdfPage page = outputPdf.addNewPage(pageSize);
PdfCanvas pdfCanvas = new PdfCanvas(page);
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int index = i + row * cols + col;
if (index <= totalPages) {
// Get the page and calculate scaling factors
Rectangle rect = pdfDoc.getPage(index).getPageSize();
float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
System.out.println("Reading page " + (i+1));
PDRectangle rect = sourcePage.getMediaBox();
float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight();
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
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
pdfCanvas.addXObject(formXObject, 0, 0);
pdfCanvas.restoreState();
}
}
}
}
int rowIndex = i / cols;
int colIndex = i % cols;
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
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();
}
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");
}
}

View file

@ -1,47 +1,30 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.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.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "General", description = "General APIs")
public class 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 = {
"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",
"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)
throws IOException {
Map<String, PageSize> sizeMap = new HashMap<>();
Map<String, PDRectangle> sizeMap = new HashMap<>();
// Add A0 - A10
sizeMap.put("A0", PageSize.A0);
sizeMap.put("A1", PageSize.A1);
sizeMap.put("A2", PageSize.A2);
sizeMap.put("A3", PageSize.A3);
sizeMap.put("A4", PageSize.A4);
sizeMap.put("A5", PageSize.A5);
sizeMap.put("A6", PageSize.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);
sizeMap.put("A0", PDRectangle.A0);
sizeMap.put("A1", PDRectangle.A1);
sizeMap.put("A2", PDRectangle.A2);
sizeMap.put("A3", PDRectangle.A3);
sizeMap.put("A4", PDRectangle.A4);
sizeMap.put("A5", PDRectangle.A5);
sizeMap.put("A6", PDRectangle.A6);
// 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);
sizeMap.put("LETTER", PDRectangle.LETTER);
sizeMap.put("LEGAL", PDRectangle.LEGAL);
if (!sizeMap.containsKey(targetPageSize)) {
if (!sizeMap.containsKey(targetPDRectangle)) {
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);
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
PDDocument sourceDocument = PDDocument.load(file.getBytes());
PDDocument outputDocument = new PDDocument();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
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);
int totalPages = pdfDoc.getNumberOfPages();
contentStream.restoreGraphicsState();
contentStream.close();
}
for (int i = 1; i <= totalPages; i++) {
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,
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outputDocument.save(baos);
outputDocument.close();
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
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);
}
}
}

View file

@ -1,8 +1,15 @@
package stirling.software.SPDF.controller.api;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream;
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.LoggerFactory;
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.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.Parameter;
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)
MultipartFile file) throws IOException {
PdfReader reader = new PdfReader(file.getInputStream());
PdfDocument sourceDocument = new PdfDocument(reader);
float totalHeight = 0;
float width = 0;
// Load the source document
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
Rectangle pageSize = sourceDocument.getPage(i).getPageSize();
totalHeight += pageSize.getHeight();
if(width < pageSize.getWidth())
width = pageSize.getWidth();
}
// Calculate total height and max width
float totalHeight = 0;
float maxWidth = 0;
for (PDPage page : sourceDocument.getPages()) {
PDRectangle pageSize = page.getMediaBox();
totalHeight += pageSize.getHeight();
maxWidth = Math.max(maxWidth, pageSize.getWidth());
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument newDocument = new PdfDocument(writer);
PageSize newPageSize = new PageSize(width, totalHeight);
newDocument.addNewPage(newPageSize);
// Create new document and page with calculated dimensions
PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
newDocument.addPage(newPage);
Document layoutDoc = new Document(newDocument);
float yOffset = totalHeight;
// 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 (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);
}
// 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();
}
layoutDoc.close();
sourceDocument.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
}
}

View file

@ -13,10 +13,11 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
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.PostMapping;
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.HttpServletResponse;
@ -40,59 +41,117 @@ public class UserController {
return "redirect:/login?registered=true";
}
@PostMapping("/change-username")
public ResponseEntity<String> changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if(userOpt == null || userOpt.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
}
User user = userOpt.get();
if(!userService.isPasswordCorrect(user, currentPassword)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
}
if(userService.usernameExists(newUsername)) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists.");
}
@PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newUsername,
@RequestParam String newPassword,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated");
}
userService.changeUsername(user, newUsername);
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
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.changeFirstUse(user, false);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return ResponseEntity.ok("Username updated successfully.");
return new RedirectView("/login?messageType=credsUpdated");
}
@PostMapping("/change-username")
public RedirectView changeUsername(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newUsername,
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");
}
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 ResponseEntity<String> changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
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 ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
}
User user = userOpt.get();
if(!userService.isPasswordCorrect(user, currentPassword)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
}
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);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return ResponseEntity.ok("Password updated successfully.");
return new RedirectView("/login?messageType=credsUpdated");
}
@PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) {
@ -115,9 +174,14 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser")
public String saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role) {
userService.saveUser(username, password, role);
return "redirect:/addUsers"; // Redirect to account page after adding the user
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) {
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
}

View file

@ -71,7 +71,7 @@ public class ConvertEpubToPdf {
// Assuming a pseudo-code function that merges multiple PDFs into one.
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 null;
}

View file

@ -9,6 +9,7 @@ import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream;
//Required for file input/output
import java.io.File;
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.multipart.MultipartFile;
import com.itextpdf.io.source.ByteArrayOutputStream;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;

View file

@ -1,11 +1,14 @@
package stirling.software.SPDF.controller.api.other;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
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.LoggerFactory;
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.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.Parameter;
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 = "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 {
int pageNumber = startingNumber;
byte[] fileBytes = file.getBytes();
PDDocument document = PDDocument.load(fileBytes);
byte[] fileBytes = file.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
int pageNumber = startingNumber;
float marginFactor;
switch (customMargin.toLowerCase()) {
case "small":
@ -68,78 +57,76 @@ public class PageNumbersController {
marginFactor = 0.05f;
break;
case "x-large":
marginFactor = 0.1f;
break;
marginFactor = 0.075f;
break;
default:
marginFactor = 0.035f;
break;
}
float fontSize = 12.0f;
PdfReader reader = new PdfReader(bais);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(reader, writer);
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
PDType1Font font = PDType1Font.HELVETICA;
if(pagesToNumber == null || pagesToNumber.length() == 0) {
pagesToNumber = "all";
}
if(customText == null || customText.length() == 0) {
customText = "{n}";
}
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
for (int i : pagesToNumberList) {
PdfPage page = pdfDoc.getPage(i+1);
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
PDPage page = document.getPage(i);
PDRectangle pageSize = page.getMediaBox();
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.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);
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
float x, y;
TextAlignment alignment;
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3;
switch (xGroup) {
case 0: // left
x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
alignment = TextAlignment.LEFT;
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
break;
case 1: // center
x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
alignment = TextAlignment.CENTER;
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
break;
default: // right
x = pageSize.getRight() - marginFactor * pageSize.getWidth();
alignment = TextAlignment.RIGHT;
x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
break;
}
switch (yGroup) {
case 0: // bottom
y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
break;
default: // top
y = pageSize.getTop() - marginFactor * pageSize.getHeight();
break;
}
case 0: // bottom
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
break;
default: // top
y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
break;
}
new Canvas(pdfCanvas, page.getPageSize())
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();
contentStream.close();
pageNumber++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
pdfDoc.close();
byte[] resultBytes = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
}

View file

@ -1,7 +1,11 @@
package stirling.software.SPDF.controller.api.other;
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.LoggerFactory;
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.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 stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -28,55 +22,35 @@ public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@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(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile)
throws Exception {
@RequestPart(value = "fileInput") MultipartFile inputFile) throws Exception {
String script = "";
try (
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
) {
String name = "";
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) {
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) {
PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript();
if (jsTree != null) {
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
String name = entry.getKey();
PDActionJavaScript jsAction = entry.getValue();
String jsCodeStr = jsAction.getAction();
script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n";
}
}
}
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
for (int i = 0; i < namesArray.size(); i += 2) {
if(namesArray.getAsString(i) != null)
name = namesArray.getAsString(i).toString();
if (script.isEmpty()) {
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
}
PdfObject jsCode = namesArray.get(i+1);
if (jsCode instanceof PdfStream) {
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
script = "//" + entryName + name + "\n" +jsCodeStr;
} 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);
script = "//" + entryName + name + "\n" +jsCodeStr;
}
}
}
}
}
if(script.equals("")) {
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");
}
}

View file

@ -3,23 +3,43 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
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.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.io.pem.PemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,267 +50,219 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Security", description = "Security APIs")
public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static {
Security.addProvider(new BouncyCastleProvider());
}
static {
Security.addProvider(new BouncyCastleProvider());
}
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@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")
public ResponseEntity<byte[]> signPDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be signed")
MultipartFile pdf,
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@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")
public ResponseEntity<byte[]> signPDF2(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be signed") MultipartFile pdf,
@RequestParam(value = "certType", required = false)
@Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"}))
String certType,
@RequestParam(value = "certType", required = false) @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {
"PKCS12", "PEM" })) String certType,
@RequestParam(value = "key", required = false)
@Parameter(description = "The private key for the digital certificate (required for PEM type certificates)")
MultipartFile privateKeyFile,
@RequestParam(value = "key", required = false) @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") MultipartFile privateKeyFile,
@RequestParam(value = "cert", required = false)
@Parameter(description = "The digital certificate (required for PEM type certificates)")
MultipartFile certFile,
@RequestParam(value = "cert", required = false) @Parameter(description = "The digital certificate (required for PEM type certificates)") MultipartFile certFile,
@RequestParam(value = "p12", required = false)
@Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)")
MultipartFile p12File,
@RequestParam(value = "p12", required = false) @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") MultipartFile p12File,
@RequestParam(value = "password", required = false)
@Parameter(description = "The password for the keystore or the private key")
String password,
@RequestParam(value = "password", required = false) @Parameter(description = "The password for the keystore or the private key") String password,
@RequestParam(value = "showSignature", required = false)
@Parameter(description = "Whether to visually show the signature in the PDF file")
Boolean showSignature,
@RequestParam(value = "showSignature", required = false) @Parameter(description = "Whether to visually show the signature in the PDF file") Boolean showSignature,
@RequestParam(value = "reason", required = false)
@Parameter(description = "The reason for signing the PDF")
String reason,
@RequestParam(value = "reason", required = false) @Parameter(description = "The reason for signing the PDF") String reason,
@RequestParam(value = "location", required = false)
@Parameter(description = "The location where the PDF is signed")
String location,
@RequestParam(value = "location", required = false) @Parameter(description = "The location where the PDF is signed") String location,
@RequestParam(value = "name", required = false)
@Parameter(description = "The name of the signer")
String name,
@RequestParam(value = "name", required = false) @Parameter(description = "The name of the signer") String name,
@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) throws Exception {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
@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)
throws Exception {
PrivateKey privateKey = null;
X509Certificate cert = null;
if (certType != null) {
switch (certType) {
case "PKCS12":
if (p12File != null) {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
String alias = ks.aliases().nextElement();
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
cert = (X509Certificate) ks.getCertificate(alias);
}
break;
case "PEM":
if (privateKeyFile != null && certFile != null) {
// Load private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider);
if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
} else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
}
PrivateKey privateKey = null;
X509Certificate cert = null;
// Load certificate
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider);
if (isPEM(certFile.getBytes())) {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
} else {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
}
}
break;
}
}
if (certType != null) {
logger.info("Cert type provided: {}", certType);
switch (certType) {
case "PKCS12":
if (p12File != null) {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
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());
cert = (X509Certificate) ks.getCertificate(alias);
}
break;
case "PEM":
if (privateKeyFile != null && certFile != null) {
// Load private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
} else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
}
Principal principal = cert.getSubjectDN();
String dn = principal.getName();
// Load certificate
CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(certFile.getBytes())) {
cert = (X509Certificate) certFactory
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
} else {
cert = (X509Certificate) certFactory
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
}
}
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);
// Extract the "CN" (Common Name) field from the distinguished name (if it's present)
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());
// Load the PDF
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
logger.info("Successfully loaded the provided PDF");
SignatureOptions signatureOptions = new SignatureOptions();
// Set up the signing appearance
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
.setReason("Test")
.setLocation("TestLocation");
// If you want to show the signature
if (showSignature != null && showSignature) {
float fontSize = 4; // the font size of the signature
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());
// ATTEMPT 2
if (showSignature != null && showSignature) {
PDPage page = document.getPage(pageNumber - 1);
// Prepare the text for the digital signature
StringBuilder layer2TextBuilder = new StringBuilder(String.format("Digitally signed by: %s\nDate: %s",
name != null ? name : "Unknown", signingDate));
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm == null) {
acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
if (reason != null && !reason.isEmpty()) {
layer2TextBuilder.append("\nReason: ").append(reason);
}
// Create a new signature field and widget
if (location != null && !location.isEmpty()) {
layer2TextBuilder.append("\nLocation: ").append(location);
}
String layer2Text = layer2TextBuilder.toString();
// 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;
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);
// Calculate the signature rectangle size
float sigWidth = textWidth + marginRight * 2;
float sigHeight = textHeight + marginBottom * 2;
// 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);
// Get the page size
PdfPage page = signer.getDocument().getPage(1);
Rectangle pageSize = page.getPageSize();
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()) {
contentStream.showText("Reason: " + reason);
contentStream.newLineAtOffset(0, -15);
}
if (location != null && !location.isEmpty()) {
contentStream.showText("Location: " + location);
contentStream.newLineAtOffset(0, -15);
}
contentStream.endText();
}
// Define the position and dimension of the signature field
Rectangle rect = new Rectangle(
pageSize.getRight() - sigWidth - marginRight,
pageSize.getBottom() + marginBottom,
sigWidth,
sigHeight
);
// Add the widget annotation to the page
page.getAnnotations().add(widget);
// 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());
// Add the signature field to the acroform
acroForm.getFields().add(signatureField);
signer.setFieldName("sig");
} else {
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
}
// Set up the signer
PrivateKeySignature pks = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName());
IExternalSignature pss = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName());
IExternalDigest digest = new BouncyCastleDigest();
// Handle multiple signatures by ensuring a unique field name
String baseFieldName = "Signature";
String signatureFieldName = baseFieldName;
int suffix = 1;
while (acroForm.getField(signatureFieldName) != null) {
suffix++;
signatureFieldName = baseFieldName + suffix;
}
signatureField.setPartialName(signatureFieldName);
}
document.addSignature(signature, signatureOptions);
logger.info("Signature added to the PDF document");
// External signing
ExternalSigningSupport externalSigning = document
.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
// Call iTex7 to sign the PDF
signer.signDetached(digest, pks, new Certificate[] {cert}, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
System.out.println("Signed PDF size: " + signedPdf.size());
// Using BouncyCastle to sign
CMSTypedData cmsData = new CMSProcessableByteArray(content);
System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray()));
return WebResponseUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf");
}
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
public boolean isPdfSigned(byte[] pdfData) throws IOException {
InputStream pdfStream = new ByteArrayInputStream(pdfData);
PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfStream));
SignatureUtil signatureUtil = new SignatureUtil(pdfDoc);
List<String> names = signatureUtil.getSignatureNames();
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
.build(signer, cert));
boolean isSigned = false;
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
CMSSignedData signedData = gen.generate(cmsData, false);
for (String name : names) {
PdfPKCS7 pkcs7 = signatureUtil.readSignatureData(name);
if (pkcs7 != null) {
System.out.println("Signature found.");
byte[] cmsSignature = signedData.getEncoded();
logger.info("About to sign content using BouncyCastle");
externalSigning.setSignature(cmsSignature);
logger.info("Signature set successfully");
// Log certificate details
Certificate[] signChain = pkcs7.getSignCertificateChain();
for (Certificate cert : signChain) {
if (cert instanceof X509Certificate) {
X509Certificate x509 = (X509Certificate) cert;
System.out.println("Certificate Details:");
System.out.println("Subject: " + x509.getSubjectDN());
System.out.println("Issuer: " + x509.getIssuerDN());
System.out.println("Serial: " + x509.getSerialNumber());
System.out.println("Not Before: " + x509.getNotBefore());
System.out.println("Not After: " + x509.getNotAfter());
}
}
// 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");
isSigned = true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
pdfDoc.close();
return null;
}
return isSigned;
}
private byte[] parsePEM(byte[] content) throws IOException {
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
return pemReader.readPemObject().getContent();
}
private boolean isPEM(byte[] content) {
String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
private byte[] parsePEM(byte[] content) throws IOException {
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
return pemReader.readPemObject().getContent();
}
private boolean isPEM(byte[] content) {
String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
}

View file

@ -1,7 +1,7 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@ -11,14 +11,53 @@ import java.util.List;
import java.util.Map;
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.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
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.PDStructureNode;
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.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.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.ResponseEntity;
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.node.ArrayNode;
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.Parameter;
@ -72,7 +88,6 @@ public class GetInfoOnPDF {
try (
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
) {
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode jsonOutput = objectMapper.createObjectNode();
@ -120,21 +135,17 @@ public class GetInfoOnPDF {
boolean hasCompression = false;
String compressionType = "None";
// Check for object streams
for (int i = 1; i <= itextDoc.getNumberOfPdfObjects(); i++) {
PdfObject obj = itextDoc.getPdfObject(i);
if (obj != null && obj.isStream() && ((PdfStream) obj).get(PdfName.Type) == PdfName.ObjStm) {
hasCompression = true;
compressionType = "Object Streams";
break;
COSDocument cosDoc = pdfBoxDoc.getDocument();
for (COSObject cosObject : cosDoc.getObjects()) {
if (cosObject.getObject() instanceof COSStream) {
COSStream cosStream = (COSStream) cosObject.getObject();
if (COSName.OBJ_STM.equals(cosStream.getItem(COSName.TYPE))) {
hasCompression = true;
compressionType = "Object Streams";
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);
if(hasCompression)
basicInfo.put("CompressionType", compressionType);
@ -144,9 +155,8 @@ public class GetInfoOnPDF {
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
// Page Mode using iText7
PdfCatalog catalog = itextDoc.getCatalog();
PdfName pageMode = catalog.getPdfObject().getAsName(PdfName.PageMode);
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
String pageMode = catalog.getPageMode().name();
// Document Information using PDFBox
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
@ -157,49 +167,56 @@ public class GetInfoOnPDF {
PdfAcroForm acroForm = PdfAcroForm.getAcroForm(itextDoc, false);
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
ObjectNode formFieldsNode = objectMapper.createObjectNode();
if (acroForm != null) {
for (Map.Entry<String, PdfFormField> entry : acroForm.getFormFields().entrySet()) {
formFieldsNode.put(entry.getKey(), entry.getValue().getValueAsString());
for (PDField field : acroForm.getFieldTree()) {
formFieldsNode.put(field.getFullyQualifiedName(), field.getValueAsString());
}
}
jsonOutput.set("FormFields", formFieldsNode);
//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);
if(namesArray != null) {
for (int i = 0; i < namesArray.size(); i += 2) {
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
embeddedFileNode.put("Name", namesArray.getAsString(i).toString());
// Add other details if required
embeddedFilesArray.add(embeddedFileNode);
if(catalog.getNames() != null) {
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();
embeddedFileNode.put("Name", entry.getKey());
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
if (embeddedFile != null) {
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes
}
embeddedFilesArray.add(embeddedFileNode);
}
}
}
}
other.set("EmbeddedFiles", embeddedFilesArray);
}
}
other.set("EmbeddedFiles", embeddedFilesArray);
//attachments TODO size
ArrayNode attachmentsArray = objectMapper.createArrayNode();
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) {
for (PdfAnnotation annotation : itextDoc.getPage(pageNum).getAnnotations()) {
if (annotation instanceof PdfFileAttachmentAnnotation) {
for (PDPage page : pdfBoxDoc.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation;
ObjectNode attachmentNode = objectMapper.createObjectNode();
attachmentNode.put("Name", ((PdfFileAttachmentAnnotation) annotation).getName().toString());
attachmentNode.put("Description", annotation.getContents().getValue());
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
attachmentNode.put("Description", fileAttachmentAnnotation.getContents());
attachmentsArray.add(attachmentNode);
}
}
@ -207,65 +224,54 @@ public class GetInfoOnPDF {
other.set("Attachments", attachmentsArray);
//Javascript
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
PDDocumentNameDictionary namesDict = catalog.getNames();
ArrayNode javascriptArray = objectMapper.createArrayNode();
if (namesDict != null) {
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
PDJavascriptNameTreeNode javascriptDict = namesDict.getJavaScript();
if (javascriptDict != null) {
try {
Map<String, PDActionJavaScript> jsEntries = javascriptDict.getNames();
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
for (int i = 0; i < namesArray.size(); i += 2) {
ObjectNode jsNode = objectMapper.createObjectNode();
if(namesArray.getAsString(i) != null)
jsNode.put("JS Name", namesArray.getAsString(i).toString());
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
ObjectNode jsNode = objectMapper.createObjectNode();
jsNode.put("JS Name", entry.getKey());
// Here we check for a PdfStream object and retrieve the JS code from it
PdfObject jsCode = namesArray.get(i+1);
if (jsCode instanceof PdfStream) {
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
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());
PDActionJavaScript jsAction = entry.getValue();
if (jsAction != null) {
String jsCodeStr = jsAction.getAction();
if (jsCodeStr != null) {
jsNode.put("JS Script Length", jsCodeStr.length());
}
}
javascriptArray.add(jsNode);
}
javascriptArray.add(jsNode);
} catch (IOException e) {
e.printStackTrace();
}
}
}
other.set("JavaScript", javascriptArray);
//TODO size
PdfOCProperties ocProperties = itextDoc.getCatalog().getOCProperties(false);
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties();
ArrayNode layersArray = objectMapper.createArrayNode();
if (ocProperties != null) {
for (PdfLayer layer : ocProperties.getLayers()) {
for (PDOptionalContentGroup ocg : ocProperties.getOptionalContentGroups()) {
ObjectNode layerNode = objectMapper.createObjectNode();
layerNode.put("Name", layer.getPdfObject().getAsString(PdfName.Name).toString());
layerNode.put("Name", ocg.getName());
layersArray.add(layerNode);
}
}
other.set("Layers", layersArray);
//TODO Security
// Digital Signatures using iText7 TODO
@ -282,13 +288,13 @@ public class GetInfoOnPDF {
}
boolean isPdfACompliant = checkOutputIntent(itextDoc, "PDF/A");
boolean isPdfXCompliant = checkOutputIntent(itextDoc, "PDF/X");
boolean isPdfECompliant = checkForStandard(itextDoc, "PDF/E");
boolean isPdfVTCompliant = checkForStandard(itextDoc, "PDF/VT");
boolean isPdfUACompliant = checkForStandard(itextDoc, "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 isPdfSECCompliant = checkForStandard(itextDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
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(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
@ -302,27 +308,39 @@ public class GetInfoOnPDF {
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
ArrayNode bookmarksArray = objectMapper.createArrayNode();
PdfOutline root = itextDoc.getOutlines(false);
if (root != null) {
for (PdfOutline child : root.getAllChildren()) {
for (PDOutlineItem child : root.children()) {
addOutlinesToArray(child, bookmarksArray);
}
}
other.set("Bookmarks/Outline/TOC", bookmarksArray);
byte[] xmpBytes = itextDoc.getXmpMetadata();
String xmpString = null;
if (xmpBytes != null) {
try {
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes);
xmpString = new String(XMPMetaFactory.serializeToBuffer(xmpMeta, new SerializeOptions()));
} catch (XMPException e) {
e.printStackTrace();
}
}
other.put("XMPMetadata", xmpString);
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
String xmpString = null;
if (pdMetadata != null) {
try {
COSInputStream is = pdMetadata.createInputStream();
DomXmpParser domXmpParser = new DomXmpParser();
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();
}
}
other.put("XMPMetadata", xmpString);
if (pdfBoxDoc.isEncrypted()) {
@ -356,43 +374,61 @@ public class GetInfoOnPDF {
ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) {
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode();
// Retrieve the page
PDPage page = pdfBoxDoc.getPage(pageNum);
// Page-level Information
Rectangle pageSize = itextDoc.getPage(pageNum).getPageSize();
pageInfo.put("Width", pageSize.getWidth());
pageInfo.put("Height", pageSize.getHeight());
pageInfo.put("Rotation", itextDoc.getPage(pageNum).getRotation());
pageInfo.put("Page Orientation", getPageOrientation(pageSize.getWidth(),pageSize.getHeight()));
pageInfo.put("Standard Size", getPageSize(pageSize.getWidth(),pageSize.getHeight()));
PDRectangle mediaBox = page.getMediaBox();
float width = mediaBox.getWidth();
float height = mediaBox.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
pageInfo.put("MediaBox", itextDoc.getPage(pageNum).getMediaBox().toString());
pageInfo.put("CropBox", itextDoc.getPage(pageNum).getCropBox().toString());
pageInfo.put("BleedBox", itextDoc.getPage(pageNum).getBleedBox().toString());
pageInfo.put("TrimBox", itextDoc.getPage(pageNum).getTrimBox().toString());
pageInfo.put("ArtBox", itextDoc.getPage(pageNum).getArtBox().toString());
pageInfo.put("MediaBox", mediaBox.toString());
// Assuming the following boxes are defined for your document; if not, you may get null values.
PDRectangle cropBox = page.getCropBox();
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
PDFTextStripper textStripper = new PDFTextStripper();
textStripper.setStartPage(pageNum -1);
textStripper.setEndPage(pageNum - 1);
textStripper.setStartPage(pageNum + 1);
textStripper.setEndPage(pageNum +1);
String pageText = textStripper.getText(pdfBoxDoc);
pageInfo.put("Text Characters Count", pageText.length()); //
// Annotations
List<PdfAnnotation> annotations = itextDoc.getPage(pageNum).getAnnotations();
// Annotations
List<PDAnnotation> annotations = page.getAnnotations();
int subtypeCount = 0;
int contentsCount = 0;
for (PdfAnnotation annotation : annotations) {
if(annotation.getSubtype() != null) {
for (PDAnnotation annotation : annotations) {
if (annotation.getSubtype() != null) {
subtypeCount++; // Increase subtype count
}
if(annotation.getContents() != null) {
if (annotation.getContents() != null) {
contentsCount++; // Increase contents count
}
}
@ -403,25 +439,31 @@ public class GetInfoOnPDF {
annotationsObject.put("ContentsCount", contentsCount);
pageInfo.set("Annotations", annotationsObject);
// Images (simplified)
// 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.
ArrayNode imagesArray = objectMapper.createArrayNode();
PdfResources resources = itextDoc.getPage(pageNum).getResources();
for (PdfName name : resources.getResourceNames()) {
PdfObject obj = resources.getResource(name);
if (obj instanceof PdfStream) {
PdfStream stream = (PdfStream) obj;
if (PdfName.Image.equals(stream.getAsName(PdfName.Subtype))) {
ObjectNode imageNode = objectMapper.createObjectNode();
imageNode.put("Width", stream.getAsNumber(PdfName.Width).intValue());
imageNode.put("Height", stream.getAsNumber(PdfName.Height).intValue());
PdfObject colorSpace = stream.get(PdfName.ColorSpace);
if (colorSpace != null) {
imageNode.put("ColorSpace", colorSpace.toString());
}
imagesArray.add(imageNode);
PDResources resources = page.getResources();
for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name);
if (xObject instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xObject;
ObjectNode imageNode = objectMapper.createObjectNode();
imageNode.put("Width", image.getWidth());
imageNode.put("Height", image.getHeight());
if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) {
imageNode.put("Name", image.getMetadata().getFile().getFile());
}
if (image.getColorSpace() != null) {
imageNode.put("ColorSpace", image.getColorSpace().getName());
}
imagesArray.add(imageNode);
}
}
pageInfo.set("Images", imagesArray);
@ -431,12 +473,13 @@ public class GetInfoOnPDF {
ArrayNode linksArray = objectMapper.createArrayNode();
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
for (PdfAnnotation annotation : annotations) {
if (annotation instanceof PdfLinkAnnotation) {
PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation;
if(linkAnnotation != null && linkAnnotation.getAction() != null) {
String uri = linkAnnotation.getAction().toString();
uniqueURIs.add(uri); // Add to set to ensure uniqueness
for (PDAnnotation annotation : annotations) {
if (annotation instanceof PDAnnotationLink) {
PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation;
if (linkAnnotation.getAction() instanceof PDActionURI) {
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
String uri = uriAction.getURI();
uniqueURIs.add(uri); // Add to set to ensure uniqueness
}
}
}
@ -449,96 +492,52 @@ public class GetInfoOnPDF {
}
pageInfo.set("Links", linksArray);
// Fonts
// Fonts
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<>();
if (fontDicts != null) {
for (PdfName key : fontDicts.keySet()) {
ObjectNode fontNode = objectMapper.createObjectNode(); // Create a new font node for each font
PdfDictionary font = fontDicts.getAsDictionary(key);
for (COSName fontName : resources.getFontNames()) {
PDFont font = resources.getFont(fontName);
ObjectNode fontNode = objectMapper.createObjectNode();
boolean isEmbedded = font.containsKey(PdfName.FontFile) ||
font.containsKey(PdfName.FontFile2) ||
font.containsKey(PdfName.FontFile3);
fontNode.put("IsEmbedded", isEmbedded);
fontNode.put("IsEmbedded", font.isEmbedded());
if (font.containsKey(PdfName.Encoding)) {
String encoding = font.getAsName(PdfName.Encoding).toString();
fontNode.put("Encoding", encoding);
}
// PDFBox provides Font's BaseFont (i.e., the font name) directly
fontNode.put("Name", font.getName());
if (font.getAsString(PdfName.BaseFont) != null) {
fontNode.put("Name", font.getAsString(PdfName.BaseFont).toString());
}
fontNode.put("Subtype", font.getType());
String subtype = null;
if (font.containsKey(PdfName.Subtype)) {
subtype = font.getAsName(PdfName.Subtype).toString();
uniqueSubtypes.add(subtype); // Add to set to ensure uniqueness
}
fontNode.put("Subtype", subtype);
PDFontDescriptor fontDescriptor = font.getFontDescriptor();
PdfDictionary fontDescriptor = font.getAsDictionary(PdfName.FontDescriptor);
if (fontDescriptor != null) {
if (fontDescriptor.containsKey(PdfName.ItalicAngle)) {
fontNode.put("ItalicAngle", fontDescriptor.getAsNumber(PdfName.ItalicAngle).floatValue());
}
if (fontDescriptor != null) {
fontNode.put("ItalicAngle", fontDescriptor.getItalicAngle());
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);
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);
}
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.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);
}
// Create a unique key for this font node based on its attributes
String uniqueKey = fontNode.toString();
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
String uniqueKey = fontNode.toString();
// Increment count if this font exists, or initialize it if new
if (uniqueFontsMap.containsKey(uniqueKey)) {
ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey);
int count = existingFontNode.get("Count").asInt() + 1;
existingFontNode.put("Count", count);
} else {
fontNode.put("Count", 1);
uniqueFontsMap.put(uniqueKey, fontNode);
}
}
// Increment count if this font exists, or initialize it if new
if (uniqueFontsMap.containsKey(uniqueKey)) {
ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey);
int count = existingFontNode.get("Count").asInt() + 1;
existingFontNode.put("Count", count);
} else {
fontNode.put("Count", 1);
uniqueFontsMap.put(uniqueKey, fontNode);
}
}
@ -552,41 +551,49 @@ public class GetInfoOnPDF {
// Access resources dictionary
PdfDictionary resourcesDict = itextDoc.getPage(pageNum).getResources().getPdfObject();
// Color Spaces & ICC Profiles
// Access resources dictionary
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
PdfDictionary colorSpaces = resourcesDict.getAsDictionary(PdfName.ColorSpace);
if (colorSpaces != null) {
for (PdfName name : colorSpaces.keySet()) {
PdfObject colorSpaceObject = colorSpaces.get(name);
if (colorSpaceObject instanceof PdfArray) {
PdfArray colorSpaceArray = (PdfArray) colorSpaceObject;
if (colorSpaceArray.size() > 1 && colorSpaceArray.get(0) instanceof PdfName && PdfName.ICCBased.equals(colorSpaceArray.get(0))) {
ObjectNode iccProfileNode = objectMapper.createObjectNode();
PdfStream iccStream = (PdfStream) colorSpaceArray.get(1);
byte[] iccData = iccStream.getBytes();
// TODO: Further decode and analyze the ICC data if needed
iccProfileNode.put("ICC Profile Length", iccData.length);
colorSpacesArray.add(iccProfileNode);
}
}
Iterable<COSName> colorSpaceNames = resources.getColorSpaceNames();
for (COSName name : colorSpaceNames) {
PDColorSpace colorSpace = resources.getColorSpace(name);
if (colorSpace instanceof PDICCBased) {
PDICCBased iccBased = (PDICCBased) colorSpace;
PDStream iccData = iccBased.getPDStream();
byte[] iccBytes = iccData.toByteArray();
// TODO: Further decode and analyze the ICC data if needed
ObjectNode iccProfileNode = objectMapper.createObjectNode();
iccProfileNode.put("ICC Profile Length", iccBytes.length);
colorSpacesArray.add(iccProfileNode);
}
}
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
// Other XObjects
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
PdfDictionary xObjects = resourcesDict.getAsDictionary(PdfName.XObject);
if (xObjects != null) {
for (PdfName name : xObjects.keySet()) {
PdfStream xObjectStream = xObjects.getAsStream(name);
String xObjectType = xObjectStream.getAsName(PdfName.Subtype).toString();
// Increment the count for this type in the map
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name);
String xObjectType;
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
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
}
// Add the count map to pageInfo (or wherever you want to store it)
@ -598,14 +605,17 @@ public class GetInfoOnPDF {
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();
// Extract details from the dictionary as needed
// Extract details from the annotation as needed
multimediaArray.add(multimediaNode);
}
}
pageInfo.set("Multimedia", multimediaArray);
@ -636,17 +646,21 @@ public class GetInfoOnPDF {
return null;
}
private static void addOutlinesToArray(PdfOutline outline, ArrayNode arrayNode) {
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return;
ObjectNode outlineNode = objectMapper.createObjectNode();
outlineNode.put("Title", outline.getTitle());
// You can add other properties if needed
arrayNode.add(outlineNode);
for (PdfOutline child : outline.getAllChildren()) {
PDOutlineItem child = outline.getFirstChild();
while (child != null) {
addOutlinesToArray(child, arrayNode);
child = child.getNextSibling();
}
}
public String getPageOrientation(double width, double height) {
if (width > height) {
return "Landscape";
@ -678,45 +692,33 @@ public class GetInfoOnPDF {
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)
try {
byte[] metadataBytes = document.getXmpMetadata();
if (metadataBytes != null) {
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(metadataBytes);
String xmpString = xmpMeta.dumpObject();
if (xmpString.contains(standardKeyword)) {
return true;
}
}
} catch (XMPException e) {
e.printStackTrace();
}
return false;
}
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;
}
}
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
// Check XMP Metadata
try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (pdMetadata != null) {
COSInputStream metaStream = pdMetadata.createInputStream();
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)) {
return true;
}
}
return false;
} catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
e.printStackTrace();
}
return false;
}
public ArrayNode exploreStructureTree(List<Object> nodes) {
ArrayNode elementsArray = objectMapper.createArrayNode();
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";
}
}

View file

@ -1,22 +1,8 @@
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.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.core.Authentication;
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.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Controller
@ -107,6 +91,7 @@ public class AccountWebController {
model.addAttribute("username", username);
model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
}
} else {
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";
}
}

View file

@ -9,6 +9,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -140,27 +141,96 @@ public class GeneralWebController {
@Autowired
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 {
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
.getResources("classpath:static/fonts/*.woff2");
.getResources(locationPattern);
return Arrays.stream(resources)
.map(resource -> {
try {
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) {
throw new RuntimeException("Error processing filename", e);
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
} 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")
@Hidden

View file

@ -104,7 +104,6 @@ public class ApplicationProperties {
}
public static class Security {
private Boolean enableLogin;
private InitialLogin initialLogin;
private Boolean csrfDisabled;
public Boolean getEnableLogin() {
@ -115,14 +114,6 @@ public class ApplicationProperties {
this.enableLogin = enableLogin;
}
public InitialLogin getInitialLogin() {
return initialLogin != null ? initialLogin : new InitialLogin();
}
public void setInitialLogin(InitialLogin initialLogin) {
this.initialLogin = initialLogin;
}
public Boolean getCsrfDisabled() {
return csrfDisabled;
}
@ -134,40 +125,9 @@ public class ApplicationProperties {
@Override
public String toString() {
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled="
return "Security [enableLogin=" + enableLogin + ", 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 {

View file

@ -40,6 +40,9 @@ public class User {
@Column(name = "enabled")
private boolean enabled;
@Column(name = "isFirstLogin")
private Boolean isFirstLogin = false;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>();
@ -50,7 +53,14 @@ public class User {
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() {
return id;
}

View file

@ -65,7 +65,8 @@ public class GeneralUtils {
} else if (sizeStr.endsWith("B")) {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} 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) {
// The numeric part of the input string cannot be parsed, handle this case

View file

@ -12,8 +12,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
public class WebResponseUtils {
@ -61,18 +59,6 @@ public class WebResponseUtils {
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);
}
}

View file

@ -43,7 +43,11 @@ green=Green
blue=Blue
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.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.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
@ -102,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.forceChange = Force user to change username/password on login
adminUserSettings.submit=Save User
#############
@ -750,13 +768,6 @@ changeMetadata.selectText.5=Add Custom Metadata Entry
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.title=PDF To PDF/A
pdfToPDFA.header=PDF To PDF/A

View file

@ -4,16 +4,11 @@
security:
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
system:
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
rootURIPath: / # Set the application's root URI (e.g. /pdf-app)
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
#ui:
# appName: exampleAppName # Application's visible name

View file

@ -16,11 +16,30 @@
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{account.accountSettings}">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>
<div th:if="${error}" class="alert alert-danger" role="alert">
<span th:text="${error}">Error Message</span>
</div>
<!-- Change Username Form -->
<h4></h4>
<form action="/change-username" method="post">

View file

@ -12,7 +12,7 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{adminUserSettings.header}">Admin User Control Settings</h2>
@ -43,6 +43,9 @@
<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">
<div class="mb-3">
<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>
</select>
</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 -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>

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

View file

@ -179,15 +179,10 @@ document.addEventListener('DOMContentLoaded', function() {
const urlParams = currentURL.searchParams;
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);
currentURL.search = urlParams.toString();
console.log("redirecting to", currentURL.toString());
window.location.href = currentURL.toString();
return;
}
@ -235,17 +230,20 @@ function handleDropdownItemClick(event) {
event.preventDefault();
const languageCode = event.currentTarget.dataset.bsLanguageCode;
const dropdown = document.getElementById('languageDropdown');
if (languageCode) {
localStorage.setItem('languageCode', languageCode);
const currentUrl = window.location.href;
if (currentUrl.indexOf('?lang=') === -1) {
window.location.href = currentUrl + '?lang=' + languageCode;
} else {
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + 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;
if (currentUrl.indexOf('?lang=') === -1) {
window.location.href = currentUrl + '?lang=' + languageCode;
} else {
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
}
}
dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content
} else {
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"
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">
<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>

View file

@ -19,33 +19,15 @@
<div class="mb-3">
<label for="pageSize" th:text="#{scalePages.pageSize}"></label>
<select id="pageSize" name="pageSize" required>
<option value="A0">A0</option>
<option value="A1">A1</option>
<option value="A2">A2</option>
<option value="A3">A3</option>
<option value="A4" selected>A4</option>
<option value="A5">A5</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="A6">A6</option>
<option value="LETTER">Letter</option>
<option value="LEGAL">Legal</option>
<option value="EXECUTIVE">Executive</option>
<option value="TABLOID">Tabloid</option>
<option value="LEDGER">Ledger</option>
</select>
</div>
<div class="mb-3">

View file

@ -28,7 +28,16 @@
<option value="image">Image</option>
</select>
</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">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
@ -101,25 +110,28 @@
</form>
<script>
function toggleFileOption() {
const watermarkType = document.getElementById('watermarkType').value;
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
const watermarkText = document.getElementById('watermarkText');
const watermarkImage = document.getElementById('watermarkImage');
if (watermarkType === 'text') {
watermarkTextGroup.style.display = 'block';
watermarkText.required = true;
watermarkImageGroup.style.display = 'none';
watermarkImage.required = false;
} else if (watermarkType === 'image') {
watermarkTextGroup.style.display = 'none';
watermarkText.required = false;
watermarkImageGroup.style.display = 'block';
watermarkImage.required = true;
}
}
function toggleFileOption() {
const watermarkType = document.getElementById('watermarkType').value;
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
const alphabetGroup = document.getElementById('alphabetGroup'); // This is the new addition
const watermarkText = document.getElementById('watermarkText');
const watermarkImage = document.getElementById('watermarkImage');
if (watermarkType === 'text') {
watermarkTextGroup.style.display = 'block';
watermarkText.required = true;
watermarkImageGroup.style.display = 'none';
watermarkImage.required = false;
alphabetGroup.style.display = 'block';
} else if (watermarkType === 'image') {
watermarkTextGroup.style.display = 'none';
watermarkText.required = false;
watermarkImageGroup.style.display = 'block';
watermarkImage.required = true;
alphabetGroup.style.display = 'none';
}
}
</script>
</div>

View file

@ -10,15 +10,16 @@
<th:block th:each="font : ${fonts}">
<style th:inline="text">
@font-face {
font-family: "[[${font}]]";
src: url('fonts/[[${font}]].woff2') format('woff2');
font-family: "[[${font.name}]]";
src: url('fonts/[[${font.name}]].[[${font.extension}]]') format('[[${font.type}]]');
}
#font-select option[value="[[${font}]]"] {
font-family: "[[${font}]]", cursive;
#font-select option[value="[[${font.name}]]"] {
font-family: "[[${font.name}]]", cursive;
}
</style>
</th:block>
<style>
select#font-select, select#font-select option {
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">
<label th:text="#{font}"></label>
<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>
<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>
</div>
@ -231,14 +236,15 @@ select#font-select, select#font-select option {
</script>
<th:block th:each="font : ${fonts}">
<th:block th:each="font : ${fonts}">
<style th:inline="text">
#font-select option[value="/*[[${font}]]*/"] {
font-family: '/*[[${font}]]*/', cursive;
#font-select option[value='/*[[${font.name}]]*/'] {
font-family: '/*[[${font.name}]]*/', cursive;
}
</style>
</th:block>
</div>
</div>