1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-11-05 23:40:11 +01:00

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 FROM frooodle/stirling-pdf-base:beta4
ARG VERSION_TAG # Set Environment Variables
ENV VERSION_TAG=$VERSION_TAG ENV PUID=1000 \
PGID=1000 \
UMASK=022 \
DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG
ENV DOCKER_ENABLE_SECURITY=false # Create user and group
RUN groupadd -g $PGID stirlingpdfgroup && \
useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Create scripts folder and copy local scripts # Set up necessary directories and permissions
RUN mkdir /scripts RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
# Copy necessary files
COPY ./scripts/* /scripts/ COPY ./scripts/* /scripts/
#Install fonts
RUN mkdir /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
RUN fc-cache -f -v
# Always copy the JAR
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
# Expose the application port # Set font cache and permissions
RUN fc-cache -f -v && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
chmod +x /scripts/init.sh
# Expose necessary ports
EXPOSE 8080 EXPOSE 8080
# Set environment variables # Set user and run command
ENV APP_HOME_NAME="Stirling PDF" USER stirlingpdfuser
# Run the application
RUN chmod +x /scripts/init.sh
ENTRYPOINT ["/scripts/init.sh"] ENTRYPOINT ["/scripts/init.sh"]
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-jar", "/app.jar"]

View File

@ -10,17 +10,43 @@ RUN apt-get update && \
unoconv && \ unoconv && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Copy the application JAR file
# Set Environment Variables
ENV PUID=1000 \
PGID=1000 \
UMASK=022 \
DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG
# Create user and group
RUN groupadd -g $PGID stirlingpdfgroup && \
useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles && \
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
# Copy necessary files
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
# Set font cache and permissions
RUN fc-cache -f -v && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Expose the application port # Expose the application port
EXPOSE 8080 EXPOSE 8080
# Set environment variables # Set environment variables
ENV GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
ENV DOCKER_ENABLE_SECURITY=false ENV DOCKER_ENABLE_SECURITY=false
# Run the application # Run the application
USER stirlingpdfuser
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-jar", "/app.jar"]

View File

@ -1,16 +1,33 @@
# Build jbig2enc in a separate stage # Build jbig2enc in a separate stage
FROM bellsoft/liberica-openjdk-alpine:17 FROM bellsoft/liberica-openjdk-alpine:17
# Copy the application JAR file # Set Environment Variables
ENV PUID=1000 \
PGID=1000 \
UMASK=022 \
DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG
# Create user and group using Alpine's addgroup and adduser
RUN addgroup -g $PGID stirlingpdfgroup && \
adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions
RUN mkdir -p /scripts /configs /customFiles && \
chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
# Set font cache and permissions
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Expose the application port # Expose the application port
EXPOSE 8080 EXPOSE 8080
# Set environment variables # Set environment variables
ENV GROUPS_TO_REMOVE=CLI ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
ENV DOCKER_ENABLE_SECURITY=false ENV DOCKER_ENABLE_SECURITY=false
# Run the application # Run the application

View File

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

View File

@ -86,9 +86,9 @@ dependencies {
//general PDF //general PDF
implementation 'org.apache.pdfbox:pdfbox:2.0.29' implementation 'org.apache.pdfbox:pdfbox:2.0.29'
implementation 'org.apache.pdfbox:xmpbox:2.0.29'
implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
implementation 'com.itextpdf:itext7-core:7.2.5'
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core' implementation 'io.micrometer:micrometer-core'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1' implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file"); private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
@Override @Override
@ -32,7 +32,6 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
if (keyValue.length != 2) { if (keyValue.length != 2) {
continue; continue;
} }
if (ALLOWED_PARAMS.contains(keyValue[0])) { if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]); parameters.put(keyValue[0], keyValue[1]);
} }

View File

@ -1,10 +1,18 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.BufferedReader;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
@ -37,7 +45,57 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
throw new FileNotFoundException("Resource file not found: settings.yml.template"); throw new FileNotFoundException("Resource file not found: settings.yml.template");
} }
} }
} else {
// If user file exists, we need to merge it with the template from the classpath
List<String> templateLines;
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines().collect(Collectors.toList());
}
mergeYamlFiles(templateLines, destPath, destPath);
} }
} }
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException {
List<String> userLines = Files.readAllLines(userFilePath);
List<String> mergedLines = new ArrayList<>();
boolean insideAutoGenerated = false;
for (String line : templateLines) {
// Check if we've entered or left the AutomaticallyGenerated section
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
insideAutoGenerated = true;
mergedLines.add(line);
continue;
} else if (insideAutoGenerated && line.trim().isEmpty()) {
// We have reached the end of the AutomaticallyGenerated section
insideAutoGenerated = false;
mergedLines.add(line);
continue;
}
if (insideAutoGenerated) {
// Add lines from user's settings if we are inside AutomaticallyGenerated
Optional<String> userAutoGenValue = userLines.stream().filter(l -> l.trim().startsWith(line.split(":")[0].trim())).findFirst();
if (userAutoGenValue.isPresent()) {
mergedLines.add(userAutoGenValue.get());
continue;
}
} else {
// Outside of AutomaticallyGenerated, continue as before
if (line.contains(": ")) {
String key = line.split(": ")[0].trim();
Optional<String> userValue = userLines.stream().filter(l -> l.trim().startsWith(key)).findFirst();
if (userValue.isPresent()) {
mergedLines.add(userValue.get());
continue;
}
}
mergedLines.add(line);
}
}
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
}
} }

View File

@ -1,9 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -18,14 +14,9 @@ public class OpenApiConfig {
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
Properties props = new Properties();
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
props.load(input);
version = props.getProperty("version");
} catch (IOException ex) {
ex.printStackTrace();
version = "1.0.0"; // default version if all else fails version = "1.0.0"; // default version if all else fails
}
} }
return new OpenAPI().components(new Components()).info( return new OpenAPI().components(new Components()).info(

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

View File

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

View File

@ -44,7 +44,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists // Check for API key in the request headers if no authentication exists
@ -74,14 +74,15 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// If we still don't have any authentication, deny the request // If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod(); String method = request.getMethod();
if ("GET".equalsIgnoreCase(method)) { if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) {
response.sendRedirect("/login"); // redirect to the login page response.sendRedirect("/login"); // redirect to the login page
return; return;
} } else {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
return; return;
} }
}
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }

View File

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

View File

@ -4,6 +4,12 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -12,14 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -32,7 +30,6 @@ public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data") @PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") @Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf( public ResponseEntity<byte[]> cropPdf(
@ -40,39 +37,55 @@ public class CropController {
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x, @Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y, @Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width, @Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException { @Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height)
byte[] bytes = file.getBytes(); throws IOException {
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(file.getBytes()));
PDDocument newDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages();
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
// Create a new page with the size of the source page
PDPage newPage = new PDPage(sourcePage.getMediaBox());
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
// Import the source page as a form XObject
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.saveGraphicsState();
// Define the crop area
contentStream.addRect(x, y, width, height);
contentStream.clip();
// Draw the entire formXObject
contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState();
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(new PDRectangle(x, y, width, height));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos); newDocument.save(baos);
PdfDocument outputPdf = new PdfDocument(writer); newDocument.close();
sourceDocument.close();
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
PdfCanvas pdfCanvas = new PdfCanvas(page);
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.rectangle(x, y, width, height);
pdfCanvas.clip();
pdfCanvas.addXObject(formXObject, -x, -y);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray(); byte[] pdfContent = baos.toByteArray();
pdfDoc.close(); return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
} }
} }

View File

@ -1,9 +1,16 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -12,15 +19,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -34,68 +32,73 @@ public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO") @Operation(
summary = "Merge multiple pages of a PDF document into a single page",
description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne( public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = { @Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = {
"2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet) "2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet)
throws IOException { throws IOException {
if (pagesPerSheet != 2 && pagesPerSheet != 3 if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
} }
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
byte[] bytes = file.getBytes(); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); PDDocument newDocument = new PDDocument();
PdfDocument pdfDoc = new PdfDocument(reader); PDPage newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); int totalPages = sourceDocument.getNumberOfPages();
PdfWriter writer = new PdfWriter(baos); float cellWidth = newPage.getMediaBox().getWidth() / cols;
PdfDocument outputPdf = new PdfDocument(writer); float cellHeight = newPage.getMediaBox().getHeight() / rows;
PageSize pageSize = new PageSize(PageSize.A4.rotate());
int totalPages = pdfDoc.getNumberOfPages(); PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
float cellWidth = pageSize.getWidth() / cols;
float cellHeight = pageSize.getHeight() / rows;
for (int i = 1; i <= totalPages; i += pagesPerSheet) { LayerUtility layerUtility = new LayerUtility(newDocument);
PdfPage page = outputPdf.addNewPage(pageSize);
PdfCanvas pdfCanvas = new PdfCanvas(page);
for (int row = 0; row < rows; row++) { for (int i = 0; i < totalPages; i++) {
for (int col = 0; col < cols; col++) { PDPage sourcePage = sourceDocument.getPage(i);
int index = i + row * cols + col; System.out.println("Reading page " + (i+1));
if (index <= totalPages) { PDRectangle rect = sourcePage.getMediaBox();
// Get the page and calculate scaling factors
Rectangle rect = pdfDoc.getPage(index).getPageSize();
float scaleWidth = cellWidth / rect.getWidth(); float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight(); float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight); float scale = Math.min(scaleWidth, scaleHeight);
System.out.println("Scale for page " + (i+1) + ": " + scale);
PdfFormXObject formXObject = pdfDoc.getPage(index).copyAsFormXObject(outputPdf);
float x = col * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
float y = (rows - 1 - row) * cellHeight + (cellHeight - rect.getHeight() * scale) / 2;
// Save the graphics state, apply the transformations, add the object, and then int rowIndex = i / cols;
// restore the graphics state int colIndex = i % cols;
pdfCanvas.saveState();
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y); float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
pdfCanvas.addXObject(formXObject, 0, 0); float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
pdfCanvas.restoreState();
} contentStream.saveGraphicsState();
} contentStream.transform(Matrix.getTranslateInstance(x, y));
} contentStream.transform(Matrix.getScaleInstance(scale, scale));
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState();
} }
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); contentStream.close();
sourceDocument.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
} }
} }

View File

@ -1,47 +1,30 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class ScalePagesController { public class ScalePagesController {
@ -55,189 +38,76 @@ public class ScalePagesController {
@Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = { @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = {
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4",
"B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL", "B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL",
"EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize, "EXECUTIVE" })) @RequestParam("pageSize") String targetPDRectangle,
@Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor) @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor)
throws IOException { throws IOException {
Map<String, PageSize> sizeMap = new HashMap<>(); Map<String, PDRectangle> sizeMap = new HashMap<>();
// Add A0 - A10 // Add A0 - A10
sizeMap.put("A0", PageSize.A0); sizeMap.put("A0", PDRectangle.A0);
sizeMap.put("A1", PageSize.A1); sizeMap.put("A1", PDRectangle.A1);
sizeMap.put("A2", PageSize.A2); sizeMap.put("A2", PDRectangle.A2);
sizeMap.put("A3", PageSize.A3); sizeMap.put("A3", PDRectangle.A3);
sizeMap.put("A4", PageSize.A4); sizeMap.put("A4", PDRectangle.A4);
sizeMap.put("A5", PageSize.A5); sizeMap.put("A5", PDRectangle.A5);
sizeMap.put("A6", PageSize.A6); sizeMap.put("A6", PDRectangle.A6);
sizeMap.put("A7", PageSize.A7);
sizeMap.put("A8", PageSize.A8);
sizeMap.put("A9", PageSize.A9);
sizeMap.put("A10", PageSize.A10);
// Add B0 - B9
sizeMap.put("B0", PageSize.B0);
sizeMap.put("B1", PageSize.B1);
sizeMap.put("B2", PageSize.B2);
sizeMap.put("B3", PageSize.B3);
sizeMap.put("B4", PageSize.B4);
sizeMap.put("B5", PageSize.B5);
sizeMap.put("B6", PageSize.B6);
sizeMap.put("B7", PageSize.B7);
sizeMap.put("B8", PageSize.B8);
sizeMap.put("B9", PageSize.B9);
// Add other sizes
sizeMap.put("LETTER", PageSize.LETTER);
sizeMap.put("TABLOID", PageSize.TABLOID);
sizeMap.put("LEDGER", PageSize.LEDGER);
sizeMap.put("LEGAL", PageSize.LEGAL);
sizeMap.put("EXECUTIVE", PageSize.EXECUTIVE);
if (!sizeMap.containsKey(targetPageSize)) { // Add other sizes
sizeMap.put("LETTER", PDRectangle.LETTER);
sizeMap.put("LEGAL", PDRectangle.LEGAL);
if (!sizeMap.containsKey(targetPDRectangle)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid pageSize. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10");
} }
PageSize pageSize = sizeMap.get(targetPageSize); PDRectangle targetSize = sizeMap.get(targetPDRectangle);
PDDocument sourceDocument = PDDocument.load(file.getBytes());
PDDocument outputDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages();
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
PDRectangle sourceSize = sourcePage.getMediaBox();
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
PDPage newPage = new PDPage(targetSize);
outputDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
LayerUtility layerUtility = new LayerUtility(outputDocument);
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
contentStream.close();
}
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos); outputDocument.save(baos);
PdfDocument outputPdf = new PdfDocument(writer); outputDocument.close();
sourceDocument.close();
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) { return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
PdfPage page = outputPdf.addNewPage(pageSize);
PdfCanvas pdfCanvas = new PdfCanvas(page);
// Get the page and calculate scaling factors
Rectangle rect = pdfDoc.getPage(i).getPageSize();
float scaleWidth = pageSize.getWidth() / rect.getWidth();
float scaleHeight = pageSize.getHeight() / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
System.out.println("Scale: " + scale);
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
float x = (pageSize.getWidth() - rect.getWidth() * scale) / 2; // Center Page
float y = (pageSize.getHeight() - rect.getHeight() * scale) / 2;
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
pdfCanvas.addXObject(formXObject, 0, 0);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
} }
//TODO
@Hidden
@PostMapping(value = "/auto-crop", consumes = "multipart/form-data")
public ResponseEntity<byte[]> cropPdf(@RequestParam("fileInput") MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = pdfDoc.getPage(i);
Rectangle originalMediaBox = page.getMediaBox();
Rectangle contentBox = determineContentBox(page);
// Make sure we don't go outside the original media box.
Rectangle intersection = originalMediaBox.getIntersection(contentBox);
page.setCropBox(intersection);
// Copy page to the new document
outputPdf.addPage(page.copyTo(outputPdf));
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""
+ file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf\"")
.contentType(MediaType.APPLICATION_PDF).body(pdfContent);
}
private Rectangle determineContentBox(PdfPage page) {
// Extract the text from the page and find the bounding box.
TextBoundingRectangleFinder finder = new TextBoundingRectangleFinder();
PdfCanvasProcessor processor = new PdfCanvasProcessor(finder);
processor.processPageContent(page);
return finder.getBoundingBox();
}
private static class TextBoundingRectangleFinder implements IEventListener {
private List<Rectangle> allTextBoxes = new ArrayList<>();
public Rectangle getBoundingBox() {
// Sort the text boxes based on their vertical position
allTextBoxes.sort(Comparator.comparingDouble(Rectangle::getTop));
// Consider a box an outlier if its top is more than 1.5 times the IQR above the
// third quartile.
int q1Index = allTextBoxes.size() / 4;
int q3Index = 3 * allTextBoxes.size() / 4;
double iqr = allTextBoxes.get(q3Index).getTop() - allTextBoxes.get(q1Index).getTop();
double threshold = allTextBoxes.get(q3Index).getTop() + 1.5 * iqr;
// Initialize boundingBox to the first non-outlier box
int i = 0;
while (i < allTextBoxes.size() && allTextBoxes.get(i).getTop() > threshold) {
i++;
}
if (i == allTextBoxes.size()) {
// If all boxes are outliers, just return the first one
return allTextBoxes.get(0);
}
Rectangle boundingBox = allTextBoxes.get(i);
// Extend the bounding box to include all non-outlier boxes
for (; i < allTextBoxes.size(); i++) {
Rectangle textBoundingBox = allTextBoxes.get(i);
if (textBoundingBox.getTop() > threshold) {
// This box is an outlier, skip it
continue;
}
float left = Math.min(boundingBox.getLeft(), textBoundingBox.getLeft());
float bottom = Math.min(boundingBox.getBottom(), textBoundingBox.getBottom());
float right = Math.max(boundingBox.getRight(), textBoundingBox.getRight());
float top = Math.max(boundingBox.getTop(), textBoundingBox.getTop());
// Add a small padding around the bounding box
float padding = 10;
boundingBox = new Rectangle(left - padding, bottom - padding, right - left + 2 * padding,
top - bottom + 2 * padding);
}
return boundingBox;
}
@Override
public void eventOccurred(IEventData data, EventType type) {
if (type == EventType.RENDER_TEXT) {
TextRenderInfo renderInfo = (TextRenderInfo) data;
allTextBoxes.add(renderInfo.getBaseline().getBoundingRectangle());
}
}
@Override
public Set<EventType> getSupportedEvents() {
return Collections.singleton(EventType.RENDER_TEXT);
}
}
} }

View File

@ -1,8 +1,15 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -11,15 +18,6 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -41,40 +39,50 @@ public class ToSinglePageController {
@Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true) @Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true)
MultipartFile file) throws IOException { MultipartFile file) throws IOException {
PdfReader reader = new PdfReader(file.getInputStream()); // Load the source document
PdfDocument sourceDocument = new PdfDocument(reader); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
// Calculate total height and max width
float totalHeight = 0; float totalHeight = 0;
float width = 0; float maxWidth = 0;
for (PDPage page : sourceDocument.getPages()) {
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) { PDRectangle pageSize = page.getMediaBox();
Rectangle pageSize = sourceDocument.getPage(i).getPageSize();
totalHeight += pageSize.getHeight(); totalHeight += pageSize.getHeight();
if(width < pageSize.getWidth()) maxWidth = Math.max(maxWidth, pageSize.getWidth());
width = pageSize.getWidth(); }
// Create new document and page with calculated dimensions
PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
newDocument.addPage(newPage);
// Initialize the content stream of the new page
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
contentStream.close();
LayerUtility layerUtility = new LayerUtility(newDocument);
float yOffset = totalHeight;
// For each page, copy its content to the new page at the correct offset
for (PDPage page : sourceDocument.getPages()) {
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page));
AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight());
layerUtility.wrapInSaveRestore(newPage);
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
yOffset -= page.getMediaBox().getHeight();
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos); newDocument.save(baos);
PdfDocument newDocument = new PdfDocument(writer); newDocument.close();
PageSize newPageSize = new PageSize(width, totalHeight);
newDocument.addNewPage(newPageSize);
Document layoutDoc = new Document(newDocument);
float yOffset = totalHeight;
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
PdfFormXObject pageCopy = sourceDocument.getPage(i).copyAsFormXObject(newDocument);
Image copiedPage = new Image(pageCopy);
copiedPage.setFixedPosition(0, yOffset - sourceDocument.getPage(i).getPageSize().getHeight());
yOffset -= sourceDocument.getPage(i).getPageSize().getHeight();
layoutDoc.add(copiedPage);
}
layoutDoc.close();
sourceDocument.close(); sourceDocument.close();
byte[] result = baos.toByteArray(); byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
} }
} }

View File

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

View File

@ -71,7 +71,7 @@ public class ConvertEpubToPdf {
// Assuming a pseudo-code function that merges multiple PDFs into one. // Assuming a pseudo-code function that merges multiple PDFs into one.
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) { private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
// You can use a library such as iText or PDFBox to perform the merging here. // You can use a library such as PDFBox to perform the merging here.
// Return the byte[] of the merged PDF. // Return the byte[] of the merged PDF.
return null; return null;
} }

View File

@ -9,6 +9,7 @@ import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.awt.image.RescaleOp; import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream;
//Required for file input/output //Required for file input/output
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -34,8 +35,6 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.source.ByteArrayOutputStream;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;

View File

@ -1,11 +1,14 @@
package stirling.software.SPDF.controller.api.other; package stirling.software.SPDF.controller.api.other;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -15,19 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -51,11 +41,10 @@ public class PageNumbersController {
@Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber, @Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
@Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText) @Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
throws IOException { throws IOException {
byte[] fileBytes = file.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
int pageNumber = startingNumber; int pageNumber = startingNumber;
byte[] fileBytes = file.getBytes();
PDDocument document = PDDocument.load(fileBytes);
float marginFactor; float marginFactor;
switch (customMargin.toLowerCase()) { switch (customMargin.toLowerCase()) {
case "small": case "small":
@ -68,78 +57,76 @@ public class PageNumbersController {
marginFactor = 0.05f; marginFactor = 0.05f;
break; break;
case "x-large": case "x-large":
marginFactor = 0.1f; marginFactor = 0.075f;
break; break;
default: default:
marginFactor = 0.035f; marginFactor = 0.035f;
break; break;
} }
float fontSize = 12.0f; float fontSize = 12.0f;
PDType1Font font = PDType1Font.HELVETICA;
PdfReader reader = new PdfReader(bais); if(pagesToNumber == null || pagesToNumber.length() == 0) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); pagesToNumber = "all";
PdfWriter writer = new PdfWriter(baos); }
if(customText == null || customText.length() == 0) {
PdfDocument pdfDoc = new PdfDocument(reader, writer); customText = "{n}";
}
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages()); List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
for (int i : pagesToNumberList) { for (int i : pagesToNumberList) {
PdfPage page = pdfDoc.getPage(i+1); PDPage page = document.getPage(i);
Rectangle pageSize = page.getPageSize(); PDRectangle pageSize = page.getMediaBox();
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
float textWidth = font.getWidth(text, fontSize);
float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
float x, y; float x, y;
TextAlignment alignment;
int xGroup = (position - 1) % 3; int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3; int yGroup = 2 - (position - 1) / 3;
switch (xGroup) { switch (xGroup) {
case 0: // left case 0: // left
x = pageSize.getLeft() + marginFactor * pageSize.getWidth(); x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
alignment = TextAlignment.LEFT;
break; break;
case 1: // center case 1: // center
x = pageSize.getLeft() + (pageSize.getWidth()) / 2; x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
alignment = TextAlignment.CENTER;
break; break;
default: // right default: // right
x = pageSize.getRight() - marginFactor * pageSize.getWidth(); x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
alignment = TextAlignment.RIGHT;
break; break;
} }
switch (yGroup) { switch (yGroup) {
case 0: // bottom case 0: // bottom
y = pageSize.getBottom() + marginFactor * pageSize.getHeight(); y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
break; break;
case 1: // middle case 1: // middle
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2; y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
break; break;
default: // top default: // top
y = pageSize.getTop() - marginFactor * pageSize.getHeight(); y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
break; break;
} }
new Canvas(pdfCanvas, page.getPageSize()) PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment); contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();
contentStream.close();
pageNumber++; pageNumber++;
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
pdfDoc.close(); return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
byte[] resultBytes = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
} }

View File

@ -1,7 +1,11 @@
package stirling.software.SPDF.controller.api.other; package stirling.software.SPDF.controller.api.other;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -10,16 +14,6 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfStream;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -28,55 +22,35 @@ public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript") @PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader( public ResponseEntity<byte[]> extractHeader(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile) @RequestPart(value = "fileInput") MultipartFile inputFile) throws Exception {
throws Exception {
try (
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
) {
String name = "";
String script = ""; String script = "";
String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: ";
//Javascript
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
if (namesDict != null) {
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
if (javascriptDict != null) {
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names); try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
for (int i = 0; i < namesArray.size(); i += 2) {
if(namesArray.getAsString(i) != null)
name = namesArray.getAsString(i).toString();
PdfObject jsCode = namesArray.get(i+1); if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) {
if (jsCode instanceof PdfStream) { PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript();
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
script = "//" + entryName + name + "\n" +jsCodeStr;
} else if (jsCode instanceof PdfDictionary) { if (jsTree != null) {
// If the JS code is in a dictionary, you'll need to know the key to use. Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
// Assuming the key is PdfName.JS:
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS); for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
if (jsCodeStream != null) { String name = entry.getKey();
byte[] jsCodeBytes = jsCodeStream.getBytes(); PDActionJavaScript jsAction = entry.getValue();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); String jsCodeStr = jsAction.getAction();
script = "//" + entryName + name + "\n" +jsCodeStr;
script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n";
} }
} }
} }
} if (script.isEmpty()) {
}
if(script.equals("")) {
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
} }
return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js");
}
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js");
}
} }

View File

@ -3,23 +3,43 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.Security; import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Arrays; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -30,29 +50,12 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.signatures.BouncyCastleDigest;
import com.itextpdf.signatures.DigestAlgorithms;
import com.itextpdf.signatures.IExternalDigest;
import com.itextpdf.signatures.IExternalSignature;
import com.itextpdf.signatures.PdfPKCS7;
import com.itextpdf.signatures.PdfSignatureAppearance;
import com.itextpdf.signatures.PdfSigner;
import com.itextpdf.signatures.PrivateKeySignature;
import com.itextpdf.signatures.SignatureUtil;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class CertSignController { public class CertSignController {
@ -64,66 +67,46 @@ public class CertSignController {
} }
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(summary = "Sign PDF with a Digital Certificate", @Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") public ResponseEntity<byte[]> signPDF2(
public ResponseEntity<byte[]> signPDF( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be signed") MultipartFile pdf,
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be signed")
MultipartFile pdf,
@RequestParam(value = "certType", required = false) @RequestParam(value = "certType", required = false) @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {
@Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"})) "PKCS12", "PEM" })) String certType,
String certType,
@RequestParam(value = "key", required = false) @RequestParam(value = "key", required = false) @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") MultipartFile privateKeyFile,
@Parameter(description = "The private key for the digital certificate (required for PEM type certificates)")
MultipartFile privateKeyFile,
@RequestParam(value = "cert", required = false) @RequestParam(value = "cert", required = false) @Parameter(description = "The digital certificate (required for PEM type certificates)") MultipartFile certFile,
@Parameter(description = "The digital certificate (required for PEM type certificates)")
MultipartFile certFile,
@RequestParam(value = "p12", required = false) @RequestParam(value = "p12", required = false) @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") MultipartFile p12File,
@Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)")
MultipartFile p12File,
@RequestParam(value = "password", required = false) @RequestParam(value = "password", required = false) @Parameter(description = "The password for the keystore or the private key") String password,
@Parameter(description = "The password for the keystore or the private key")
String password,
@RequestParam(value = "showSignature", required = false) @RequestParam(value = "showSignature", required = false) @Parameter(description = "Whether to visually show the signature in the PDF file") Boolean showSignature,
@Parameter(description = "Whether to visually show the signature in the PDF file")
Boolean showSignature,
@RequestParam(value = "reason", required = false) @RequestParam(value = "reason", required = false) @Parameter(description = "The reason for signing the PDF") String reason,
@Parameter(description = "The reason for signing the PDF")
String reason,
@RequestParam(value = "location", required = false) @RequestParam(value = "location", required = false) @Parameter(description = "The location where the PDF is signed") String location,
@Parameter(description = "The location where the PDF is signed")
String location,
@RequestParam(value = "name", required = false) @RequestParam(value = "name", required = false) @Parameter(description = "The name of the signer") String name,
@Parameter(description = "The name of the signer")
String name,
@RequestParam(value = "pageNumber", required = false) @RequestParam(value = "pageNumber", required = false) @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") Integer pageNumber)
@Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") throws Exception {
Integer pageNumber) throws Exception {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
PrivateKey privateKey = null; PrivateKey privateKey = null;
X509Certificate cert = null; X509Certificate cert = null;
if (certType != null) { if (certType != null) {
logger.info("Cert type provided: {}", certType);
switch (certType) { switch (certType) {
case "PKCS12": case "PKCS12":
if (p12File != null) { if (p12File != null) {
KeyStore ks = KeyStore.getInstance("PKCS12"); KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
String alias = ks.aliases().nextElement(); String alias = ks.aliases().nextElement();
if (!ks.isKeyEntry(alias)) {
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key.");
}
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
cert = (X509Certificate) ks.getCertificate(alias); cert = (X509Certificate) ks.getCertificate(alias);
} }
@ -131,154 +114,147 @@ public class CertSignController {
case "PEM": case "PEM":
if (privateKeyFile != null && certFile != null) { if (privateKeyFile != null && certFile != null) {
// Load private key // Load private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider); KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(privateKeyFile.getBytes())) { if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); privateKey = keyFactory
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
} else { } else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
} }
// Load certificate // Load certificate
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider); CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(certFile.getBytes())) { if (isPEM(certFile.getBytes())) {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); cert = (X509Certificate) certFactory
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
} else { } else {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); cert = (X509Certificate) certFactory
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
} }
} }
break; break;
} }
} }
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
Principal principal = cert.getSubjectDN(); // Load the PDF
String dn = principal.getName(); try (PDDocument document = PDDocument.load(pdf.getBytes())) {
logger.info("Successfully loaded the provided PDF");
SignatureOptions signatureOptions = new SignatureOptions();
// Extract the "CN" (Common Name) field from the distinguished name (if it's present) // If you want to show the signature
String cn = null;
for (String part : dn.split(",")) {
if (part.trim().startsWith("CN=")) {
cn = part.trim().substring("CN=".length());
break;
}
}
// Set up the PDF reader and stamper
PdfReader reader = new PdfReader(new ByteArrayInputStream(pdf.getBytes()));
ByteArrayOutputStream signedPdf = new ByteArrayOutputStream();
PdfSigner signer = new PdfSigner(reader, signedPdf, new StampingProperties());
// Set up the signing appearance
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
.setReason("Test")
.setLocation("TestLocation");
// ATTEMPT 2
if (showSignature != null && showSignature) { if (showSignature != null && showSignature) {
float fontSize = 4; // the font size of the signature PDPage page = document.getPage(pageNumber - 1);
float marginRight = 36; // Margin from the right
float marginBottom = 36; // Margin from the bottom
String signingDate = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date());
// Prepare the text for the digital signature PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
StringBuilder layer2TextBuilder = new StringBuilder(String.format("Digitally signed by: %s\nDate: %s", if (acroForm == null) {
name != null ? name : "Unknown", signingDate)); acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
// Create a new signature field and widget
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
widget.setRectangle(rect);
page.getAnnotations().add(widget);
// Set the appearance for the signature field
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
appearanceStream.setResources(new PDResources());
appearanceStream.setBBox(rect);
appearanceDict.setNormalAppearance(appearanceStream);
widget.setAppearance(appearanceDict);
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.newLineAtOffset(110, 130);
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown"));
contentStream.newLineAtOffset(0, -15);
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date()));
contentStream.newLineAtOffset(0, -15);
if (reason != null && !reason.isEmpty()) { if (reason != null && !reason.isEmpty()) {
layer2TextBuilder.append("\nReason: ").append(reason); contentStream.showText("Reason: " + reason);
contentStream.newLineAtOffset(0, -15);
} }
if (location != null && !location.isEmpty()) { if (location != null && !location.isEmpty()) {
layer2TextBuilder.append("\nLocation: ").append(location); contentStream.showText("Location: " + location);
contentStream.newLineAtOffset(0, -15);
} }
String layer2Text = layer2TextBuilder.toString(); contentStream.endText();
// Get the PDF font and measure the width and height of the text block
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
float textWidth = Arrays.stream(layer2Text.split("\n"))
.map(line -> font.getWidth(line, fontSize))
.max(Float::compare)
.orElse(0f);
int numLines = layer2Text.split("\n").length;
float textHeight = numLines * fontSize;
// Calculate the signature rectangle size
float sigWidth = textWidth + marginRight * 2;
float sigHeight = textHeight + marginBottom * 2;
// Get the page size
PdfPage page = signer.getDocument().getPage(1);
Rectangle pageSize = page.getPageSize();
// Define the position and dimension of the signature field
Rectangle rect = new Rectangle(
pageSize.getRight() - sigWidth - marginRight,
pageSize.getBottom() + marginBottom,
sigWidth,
sigHeight
);
// Configure the appearance of the digital signature
appearance.setPageRect(rect)
.setContact(name != null ? name : "")
.setPageNumber(pageNumber)
.setReason(reason != null ? reason : "")
.setLocation(location != null ? location : "")
.setReuseAppearance(false)
.setLayer2Text(layer2Text.toString());
signer.setFieldName("sig");
} else {
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
} }
// Set up the signer // Add the widget annotation to the page
PrivateKeySignature pks = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName()); page.getAnnotations().add(widget);
IExternalSignature pss = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName());
IExternalDigest digest = new BouncyCastleDigest();
// Call iTex7 to sign the PDF // Add the signature field to the acroform
signer.signDetached(digest, pks, new Certificate[] {cert}, null, null, null, 0, PdfSigner.CryptoStandard.CMS); acroForm.getFields().add(signatureField);
// Handle multiple signatures by ensuring a unique field name
System.out.println("Signed PDF size: " + signedPdf.size()); String baseFieldName = "Signature";
String signatureFieldName = baseFieldName;
System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray())); int suffix = 1;
return WebResponseUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf"); while (acroForm.getField(signatureFieldName) != null) {
suffix++;
signatureFieldName = baseFieldName + suffix;
}
signatureField.setPartialName(signatureFieldName);
} }
public boolean isPdfSigned(byte[] pdfData) throws IOException { document.addSignature(signature, signatureOptions);
InputStream pdfStream = new ByteArrayInputStream(pdfData); logger.info("Signature added to the PDF document");
PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfStream)); // External signing
SignatureUtil signatureUtil = new SignatureUtil(pdfDoc); ExternalSigningSupport externalSigning = document
List<String> names = signatureUtil.getSignatureNames(); .saveIncrementalForExternalSigning(new ByteArrayOutputStream());
boolean isSigned = false; byte[] content = IOUtils.toByteArray(externalSigning.getContent());
for (String name : names) { // Using BouncyCastle to sign
PdfPKCS7 pkcs7 = signatureUtil.readSignatureData(name); CMSTypedData cmsData = new CMSProcessableByteArray(content);
if (pkcs7 != null) {
System.out.println("Signature found.");
// Log certificate details CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Certificate[] signChain = pkcs7.getSignCertificateChain(); ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
for (Certificate cert : signChain) { .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
if (cert instanceof X509Certificate) {
X509Certificate x509 = (X509Certificate) cert; gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
System.out.println("Certificate Details:"); new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
System.out.println("Subject: " + x509.getSubjectDN()); .build(signer, cert));
System.out.println("Issuer: " + x509.getIssuerDN());
System.out.println("Serial: " + x509.getSerialNumber()); gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
System.out.println("Not Before: " + x509.getNotBefore()); CMSSignedData signedData = gen.generate(cmsData, false);
System.out.println("Not After: " + x509.getNotAfter());
byte[] cmsSignature = signedData.getEncoded();
logger.info("About to sign content using BouncyCastle");
externalSigning.setSignature(cmsSignature);
logger.info("Signature set successfully");
// After setting the signature, return the resultant PDF
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
document.save(signedPdfOutput);
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
} catch (Exception e) {
e.printStackTrace();
} }
} catch (Exception e) {
e.printStackTrace();
} }
isSigned = true; return null;
}
} }
pdfDoc.close();
return isSigned;
}
private byte[] parsePEM(byte[] content) throws IOException { private byte[] parsePEM(byte[] content) throws IOException {
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
return pemReader.readPemObject().getContent(); return pemReader.readPemObject().getContent();
@ -289,8 +265,4 @@ public boolean isPdfSigned(byte[] pdfData) throws IOException {
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
} }
} }

View File

@ -1,7 +1,7 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
@ -11,14 +11,53 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.cos.COSInputStream;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.PDJavascriptNameTreeNode;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.PDEncryption; import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.xml.DomXmpParser;
import org.apache.xmpbox.xml.XmpParsingException;
import org.apache.xmpbox.xml.XmpSerializer;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -29,29 +68,6 @@ import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfCatalog;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfOutline;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfResources;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfFileAttachmentAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.layer.PdfLayer;
import com.itextpdf.kernel.pdf.layer.PdfOCProperties;
import com.itextpdf.kernel.xmp.XMPException;
import com.itextpdf.kernel.xmp.XMPMeta;
import com.itextpdf.kernel.xmp.XMPMetaFactory;
import com.itextpdf.kernel.xmp.options.SerializeOptions;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -72,7 +88,6 @@ public class GetInfoOnPDF {
try ( try (
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
) { ) {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
ObjectNode jsonOutput = objectMapper.createObjectNode(); ObjectNode jsonOutput = objectMapper.createObjectNode();
@ -120,20 +135,16 @@ public class GetInfoOnPDF {
boolean hasCompression = false; boolean hasCompression = false;
String compressionType = "None"; String compressionType = "None";
// Check for object streams COSDocument cosDoc = pdfBoxDoc.getDocument();
for (int i = 1; i <= itextDoc.getNumberOfPdfObjects(); i++) { for (COSObject cosObject : cosDoc.getObjects()) {
PdfObject obj = itextDoc.getPdfObject(i); if (cosObject.getObject() instanceof COSStream) {
if (obj != null && obj.isStream() && ((PdfStream) obj).get(PdfName.Type) == PdfName.ObjStm) { COSStream cosStream = (COSStream) cosObject.getObject();
if (COSName.OBJ_STM.equals(cosStream.getItem(COSName.TYPE))) {
hasCompression = true; hasCompression = true;
compressionType = "Object Streams"; compressionType = "Object Streams";
break; break;
} }
} }
// If not compressed using object streams, check for compressed Xref tables
if (!hasCompression && itextDoc.getReader().hasRebuiltXref()) {
hasCompression = true;
compressionType = "Compressed Xref or Rebuilt Xref";
} }
basicInfo.put("Compression", hasCompression); basicInfo.put("Compression", hasCompression);
if(hasCompression) if(hasCompression)
@ -144,9 +155,8 @@ public class GetInfoOnPDF {
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
// Page Mode using iText7 PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
PdfCatalog catalog = itextDoc.getCatalog(); String pageMode = catalog.getPageMode().name();
PdfName pageMode = catalog.getPdfObject().getAsName(PdfName.PageMode);
// Document Information using PDFBox // Document Information using PDFBox
docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
@ -157,11 +167,12 @@ public class GetInfoOnPDF {
PdfAcroForm acroForm = PdfAcroForm.getAcroForm(itextDoc, false); PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
ObjectNode formFieldsNode = objectMapper.createObjectNode(); ObjectNode formFieldsNode = objectMapper.createObjectNode();
if (acroForm != null) { if (acroForm != null) {
for (Map.Entry<String, PdfFormField> entry : acroForm.getFormFields().entrySet()) { for (PDField field : acroForm.getFieldTree()) {
formFieldsNode.put(entry.getKey(), entry.getValue().getValueAsString()); formFieldsNode.put(field.getFullyQualifiedName(), field.getValueAsString());
} }
} }
jsonOutput.set("FormFields", formFieldsNode); jsonOutput.set("FormFields", formFieldsNode);
@ -170,36 +181,42 @@ public class GetInfoOnPDF {
//embeed files TODO size
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
if(itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) != null)
{
PdfDictionary embeddedFiles = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names)
.getAsDictionary(PdfName.EmbeddedFiles);
if (embeddedFiles != null) {
PdfArray namesArray = embeddedFiles.getAsArray(PdfName.Names); //embeed files TODO size
if(namesArray != null) { if(catalog.getNames() != null) {
for (int i = 0; i < namesArray.size(); i += 2) { PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
if (efTree != null) {
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
if (efMap != null) {
for (Map.Entry<String, PDComplexFileSpecification> entry : efMap.entrySet()) {
ObjectNode embeddedFileNode = objectMapper.createObjectNode(); ObjectNode embeddedFileNode = objectMapper.createObjectNode();
embeddedFileNode.put("Name", namesArray.getAsString(i).toString()); embeddedFileNode.put("Name", entry.getKey());
// Add other details if required PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
if (embeddedFile != null) {
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes
}
embeddedFilesArray.add(embeddedFileNode); embeddedFilesArray.add(embeddedFileNode);
} }
} }
}
} }
other.set("EmbeddedFiles", embeddedFilesArray); other.set("EmbeddedFiles", embeddedFilesArray);
}
//attachments TODO size //attachments TODO size
ArrayNode attachmentsArray = objectMapper.createArrayNode(); ArrayNode attachmentsArray = objectMapper.createArrayNode();
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) { for (PDPage page : pdfBoxDoc.getPages()) {
for (PdfAnnotation annotation : itextDoc.getPage(pageNum).getAnnotations()) { for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PdfFileAttachmentAnnotation) { if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation;
ObjectNode attachmentNode = objectMapper.createObjectNode(); ObjectNode attachmentNode = objectMapper.createObjectNode();
attachmentNode.put("Name", ((PdfFileAttachmentAnnotation) annotation).getName().toString()); attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
attachmentNode.put("Description", annotation.getContents().getValue()); attachmentNode.put("Description", fileAttachmentAnnotation.getContents());
attachmentsArray.add(attachmentNode); attachmentsArray.add(attachmentNode);
} }
} }
@ -207,54 +224,49 @@ public class GetInfoOnPDF {
other.set("Attachments", attachmentsArray); other.set("Attachments", attachmentsArray);
//Javascript //Javascript
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names); PDDocumentNameDictionary namesDict = catalog.getNames();
ArrayNode javascriptArray = objectMapper.createArrayNode(); ArrayNode javascriptArray = objectMapper.createArrayNode();
if (namesDict != null) { if (namesDict != null) {
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript); PDJavascriptNameTreeNode javascriptDict = namesDict.getJavaScript();
if (javascriptDict != null) { if (javascriptDict != null) {
try {
Map<String, PDActionJavaScript> jsEntries = javascriptDict.getNames();
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names); for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
for (int i = 0; i < namesArray.size(); i += 2) {
ObjectNode jsNode = objectMapper.createObjectNode(); ObjectNode jsNode = objectMapper.createObjectNode();
if(namesArray.getAsString(i) != null) jsNode.put("JS Name", entry.getKey());
jsNode.put("JS Name", namesArray.getAsString(i).toString());
// Here we check for a PdfStream object and retrieve the JS code from it PDActionJavaScript jsAction = entry.getValue();
PdfObject jsCode = namesArray.get(i+1); if (jsAction != null) {
if (jsCode instanceof PdfStream) { String jsCodeStr = jsAction.getAction();
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes(); if (jsCodeStr != null) {
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
jsNode.put("JS Script Length", jsCodeStr.length()); jsNode.put("JS Script Length", jsCodeStr.length());
} else if (jsCode instanceof PdfDictionary) {
// If the JS code is in a dictionary, you'll need to know the key to use.
// Assuming the key is PdfName.JS:
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
if (jsCodeStream != null) {
byte[] jsCodeBytes = jsCodeStream.getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
jsNode.put("JS Script Character Length", jsCodeStr.length());
} }
} }
javascriptArray.add(jsNode); javascriptArray.add(jsNode);
} }
} catch (IOException e) {
e.printStackTrace();
}
} }
} }
other.set("JavaScript", javascriptArray); other.set("JavaScript", javascriptArray);
//TODO size
PdfOCProperties ocProperties = itextDoc.getCatalog().getOCProperties(false);
ArrayNode layersArray = objectMapper.createArrayNode();
if (ocProperties != null) {
for (PdfLayer layer : ocProperties.getLayers()) { //TODO size
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties();
ArrayNode layersArray = objectMapper.createArrayNode();
if (ocProperties != null) {
for (PDOptionalContentGroup ocg : ocProperties.getOptionalContentGroups()) {
ObjectNode layerNode = objectMapper.createObjectNode(); ObjectNode layerNode = objectMapper.createObjectNode();
layerNode.put("Name", layer.getPdfObject().getAsString(PdfName.Name).toString()); layerNode.put("Name", ocg.getName());
layersArray.add(layerNode); layersArray.add(layerNode);
} }
} }
other.set("Layers", layersArray); other.set("Layers", layersArray);
//TODO Security //TODO Security
@ -263,12 +275,6 @@ public class GetInfoOnPDF {
// Digital Signatures using iText7 TODO
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot(); PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
ArrayNode structureTreeArray; ArrayNode structureTreeArray;
try { try {
@ -282,13 +288,13 @@ public class GetInfoOnPDF {
} }
boolean isPdfACompliant = checkOutputIntent(itextDoc, "PDF/A"); boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
boolean isPdfXCompliant = checkOutputIntent(itextDoc, "PDF/X"); boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
boolean isPdfECompliant = checkForStandard(itextDoc, "PDF/E"); boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
boolean isPdfVTCompliant = checkForStandard(itextDoc, "PDF/VT"); boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
boolean isPdfUACompliant = checkForStandard(itextDoc, "PDF/UA"); boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
boolean isPdfBCompliant = checkForStandard(itextDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard.
boolean isPdfSECCompliant = checkForStandard(itextDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
compliancy.put("IsPDF/ACompliant", isPdfACompliant); compliancy.put("IsPDF/ACompliant", isPdfACompliant);
compliancy.put("IsPDF/XCompliant", isPdfXCompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
@ -302,25 +308,37 @@ public class GetInfoOnPDF {
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
ArrayNode bookmarksArray = objectMapper.createArrayNode(); ArrayNode bookmarksArray = objectMapper.createArrayNode();
PdfOutline root = itextDoc.getOutlines(false);
if (root != null) { if (root != null) {
for (PdfOutline child : root.getAllChildren()) { for (PDOutlineItem child : root.children()) {
addOutlinesToArray(child, bookmarksArray); addOutlinesToArray(child, bookmarksArray);
} }
} }
other.set("Bookmarks/Outline/TOC", bookmarksArray); other.set("Bookmarks/Outline/TOC", bookmarksArray);
byte[] xmpBytes = itextDoc.getXmpMetadata();
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
String xmpString = null; String xmpString = null;
if (xmpBytes != null) {
if (pdMetadata != null) {
try { try {
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes); COSInputStream is = pdMetadata.createInputStream();
xmpString = new String(XMPMetaFactory.serializeToBuffer(xmpMeta, new SerializeOptions())); DomXmpParser domXmpParser = new DomXmpParser();
} catch (XMPException e) { XMPMetadata xmpMeta = domXmpParser.parse(is);
ByteArrayOutputStream os = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, os, true);
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
} catch (XmpParsingException | IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
other.put("XMPMetadata", xmpString); other.put("XMPMetadata", xmpString);
@ -356,39 +374,57 @@ public class GetInfoOnPDF {
ObjectNode pageInfoParent = objectMapper.createObjectNode(); ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) { for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode(); ObjectNode pageInfo = objectMapper.createObjectNode();
// Retrieve the page
PDPage page = pdfBoxDoc.getPage(pageNum);
// Page-level Information // Page-level Information
Rectangle pageSize = itextDoc.getPage(pageNum).getPageSize(); PDRectangle mediaBox = page.getMediaBox();
pageInfo.put("Width", pageSize.getWidth());
pageInfo.put("Height", pageSize.getHeight()); float width = mediaBox.getWidth();
pageInfo.put("Rotation", itextDoc.getPage(pageNum).getRotation()); float height = mediaBox.getHeight();
pageInfo.put("Page Orientation", getPageOrientation(pageSize.getWidth(),pageSize.getHeight()));
pageInfo.put("Standard Size", getPageSize(pageSize.getWidth(),pageSize.getHeight())); pageInfo.put("Width", width);
pageInfo.put("Height", height);
pageInfo.put("Rotation", page.getRotation());
pageInfo.put("Page Orientation", getPageOrientation(width, height));
pageInfo.put("Standard Size", getPageSize(width, height));
// Boxes // Boxes
pageInfo.put("MediaBox", itextDoc.getPage(pageNum).getMediaBox().toString()); pageInfo.put("MediaBox", mediaBox.toString());
pageInfo.put("CropBox", itextDoc.getPage(pageNum).getCropBox().toString());
pageInfo.put("BleedBox", itextDoc.getPage(pageNum).getBleedBox().toString()); // Assuming the following boxes are defined for your document; if not, you may get null values.
pageInfo.put("TrimBox", itextDoc.getPage(pageNum).getTrimBox().toString()); PDRectangle cropBox = page.getCropBox();
pageInfo.put("ArtBox", itextDoc.getPage(pageNum).getArtBox().toString()); pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
PDRectangle bleedBox = page.getBleedBox();
pageInfo.put("BleedBox", bleedBox == null ? "Undefined" : bleedBox.toString());
PDRectangle trimBox = page.getTrimBox();
pageInfo.put("TrimBox", trimBox == null ? "Undefined" : trimBox.toString());
PDRectangle artBox = page.getArtBox();
pageInfo.put("ArtBox", artBox == null ? "Undefined" : artBox.toString());
// Content Extraction // Content Extraction
PDFTextStripper textStripper = new PDFTextStripper(); PDFTextStripper textStripper = new PDFTextStripper();
textStripper.setStartPage(pageNum -1); textStripper.setStartPage(pageNum + 1);
textStripper.setEndPage(pageNum - 1); textStripper.setEndPage(pageNum +1);
String pageText = textStripper.getText(pdfBoxDoc); String pageText = textStripper.getText(pdfBoxDoc);
pageInfo.put("Text Characters Count", pageText.length()); // pageInfo.put("Text Characters Count", pageText.length()); //
// Annotations // Annotations
List<PdfAnnotation> annotations = itextDoc.getPage(pageNum).getAnnotations();
List<PDAnnotation> annotations = page.getAnnotations();
int subtypeCount = 0; int subtypeCount = 0;
int contentsCount = 0; int contentsCount = 0;
for (PdfAnnotation annotation : annotations) { for (PDAnnotation annotation : annotations) {
if (annotation.getSubtype() != null) { if (annotation.getSubtype() != null) {
subtypeCount++; // Increase subtype count subtypeCount++; // Increase subtype count
} }
@ -403,27 +439,33 @@ public class GetInfoOnPDF {
annotationsObject.put("ContentsCount", contentsCount); annotationsObject.put("ContentsCount", contentsCount);
pageInfo.set("Annotations", annotationsObject); pageInfo.set("Annotations", annotationsObject);
// Images (simplified) // Images (simplified)
// This part is non-trivial as images can be embedded in multiple ways in a PDF. // This part is non-trivial as images can be embedded in multiple ways in a PDF.
// Here is a basic structure to recognize image XObjects on a page. // Here is a basic structure to recognize image XObjects on a page.
ArrayNode imagesArray = objectMapper.createArrayNode(); ArrayNode imagesArray = objectMapper.createArrayNode();
PdfResources resources = itextDoc.getPage(pageNum).getResources(); PDResources resources = page.getResources();
for (PdfName name : resources.getResourceNames()) {
PdfObject obj = resources.getResource(name);
if (obj instanceof PdfStream) { for (COSName name : resources.getXObjectNames()) {
PdfStream stream = (PdfStream) obj; PDXObject xObject = resources.getXObject(name);
if (PdfName.Image.equals(stream.getAsName(PdfName.Subtype))) { if (xObject instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xObject;
ObjectNode imageNode = objectMapper.createObjectNode(); ObjectNode imageNode = objectMapper.createObjectNode();
imageNode.put("Width", stream.getAsNumber(PdfName.Width).intValue()); imageNode.put("Width", image.getWidth());
imageNode.put("Height", stream.getAsNumber(PdfName.Height).intValue()); imageNode.put("Height", image.getHeight());
PdfObject colorSpace = stream.get(PdfName.ColorSpace); if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) {
if (colorSpace != null) { imageNode.put("Name", image.getMetadata().getFile().getFile());
imageNode.put("ColorSpace", colorSpace.toString());
} }
if (image.getColorSpace() != null) {
imageNode.put("ColorSpace", image.getColorSpace().getName());
}
imagesArray.add(imageNode); imagesArray.add(imageNode);
} }
} }
}
pageInfo.set("Images", imagesArray); pageInfo.set("Images", imagesArray);
@ -431,11 +473,12 @@ public class GetInfoOnPDF {
ArrayNode linksArray = objectMapper.createArrayNode(); ArrayNode linksArray = objectMapper.createArrayNode();
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
for (PdfAnnotation annotation : annotations) { for (PDAnnotation annotation : annotations) {
if (annotation instanceof PdfLinkAnnotation) { if (annotation instanceof PDAnnotationLink) {
PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation; PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation;
if(linkAnnotation != null && linkAnnotation.getAction() != null) { if (linkAnnotation.getAction() instanceof PDActionURI) {
String uri = linkAnnotation.getAction().toString(); PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
String uri = uriAction.getURI();
uniqueURIs.add(uri); // Add to set to ensure uniqueness uniqueURIs.add(uri); // Add to set to ensure uniqueness
} }
} }
@ -449,83 +492,41 @@ public class GetInfoOnPDF {
} }
pageInfo.set("Links", linksArray); pageInfo.set("Links", linksArray);
// Fonts // Fonts
ArrayNode fontsArray = objectMapper.createArrayNode(); ArrayNode fontsArray = objectMapper.createArrayNode();
PdfDictionary fontDicts = resources.getResource(PdfName.Font);
Set<String> uniqueSubtypes = new HashSet<>(); // To store unique subtypes
// Map to store unique fonts and their counts
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>(); Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
if (fontDicts != null) { for (COSName fontName : resources.getFontNames()) {
for (PdfName key : fontDicts.keySet()) { PDFont font = resources.getFont(fontName);
ObjectNode fontNode = objectMapper.createObjectNode(); // Create a new font node for each font ObjectNode fontNode = objectMapper.createObjectNode();
PdfDictionary font = fontDicts.getAsDictionary(key);
boolean isEmbedded = font.containsKey(PdfName.FontFile) || fontNode.put("IsEmbedded", font.isEmbedded());
font.containsKey(PdfName.FontFile2) ||
font.containsKey(PdfName.FontFile3);
fontNode.put("IsEmbedded", isEmbedded);
if (font.containsKey(PdfName.Encoding)) { // PDFBox provides Font's BaseFont (i.e., the font name) directly
String encoding = font.getAsName(PdfName.Encoding).toString(); fontNode.put("Name", font.getName());
fontNode.put("Encoding", encoding);
}
if (font.getAsString(PdfName.BaseFont) != null) { fontNode.put("Subtype", font.getType());
fontNode.put("Name", font.getAsString(PdfName.BaseFont).toString());
}
String subtype = null; PDFontDescriptor fontDescriptor = font.getFontDescriptor();
if (font.containsKey(PdfName.Subtype)) {
subtype = font.getAsName(PdfName.Subtype).toString();
uniqueSubtypes.add(subtype); // Add to set to ensure uniqueness
}
fontNode.put("Subtype", subtype);
PdfDictionary fontDescriptor = font.getAsDictionary(PdfName.FontDescriptor);
if (fontDescriptor != null) { if (fontDescriptor != null) {
if (fontDescriptor.containsKey(PdfName.ItalicAngle)) { fontNode.put("ItalicAngle", fontDescriptor.getItalicAngle());
fontNode.put("ItalicAngle", fontDescriptor.getAsNumber(PdfName.ItalicAngle).floatValue()); int flags = fontDescriptor.getFlags();
fontNode.put("IsItalic", (flags & 1) != 0);
fontNode.put("IsBold", (flags & 64) != 0);
fontNode.put("IsFixedPitch", (flags & 2) != 0);
fontNode.put("IsSerif", (flags & 4) != 0);
fontNode.put("IsSymbolic", (flags & 8) != 0);
fontNode.put("IsScript", (flags & 16) != 0);
fontNode.put("IsNonsymbolic", (flags & 32) != 0);
fontNode.put("FontFamily", fontDescriptor.getFontFamily());
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity
fontNode.put("FontWeight", fontDescriptor.getFontWeight());
} }
if (fontDescriptor.containsKey(PdfName.Flags)) {
int flags = fontDescriptor.getAsNumber(PdfName.Flags).intValue();
fontNode.put("IsItalic", (flags & 64) != 0);
fontNode.put("IsBold", (flags & 1 << 16) != 0);
fontNode.put("IsFixedPitch", (flags & 1) != 0);
fontNode.put("IsSerif", (flags & 2) != 0);
fontNode.put("IsSymbolic", (flags & 4) != 0);
fontNode.put("IsScript", (flags & 8) != 0);
fontNode.put("IsNonsymbolic", (flags & 16) != 0);
}
if (fontDescriptor.containsKey(PdfName.FontFamily)) {
String fontFamily = fontDescriptor.getAsString(PdfName.FontFamily).toString();
fontNode.put("FontFamily", fontFamily);
}
if (fontDescriptor.containsKey(PdfName.FontStretch)) {
String fontStretch = fontDescriptor.getAsName(PdfName.FontStretch).toString();
fontNode.put("FontStretch", fontStretch);
}
if (fontDescriptor.containsKey(PdfName.FontBBox)) {
PdfArray bbox = fontDescriptor.getAsArray(PdfName.FontBBox);
fontNode.put("FontBoundingBox", bbox.toString());
}
if (fontDescriptor.containsKey(PdfName.FontWeight)) {
float fontWeight = fontDescriptor.getAsNumber(PdfName.FontWeight).floatValue();
fontNode.put("FontWeight", fontWeight);
}
}
if (font.containsKey(PdfName.ToUnicode)) {
fontNode.put("HasToUnicodeMap", true);
}
if (fontNode.size() > 0) {
// Create a unique key for this font node based on its attributes // Create a unique key for this font node based on its attributes
String uniqueKey = fontNode.toString(); String uniqueKey = fontNode.toString();
@ -539,8 +540,6 @@ public class GetInfoOnPDF {
uniqueFontsMap.put(uniqueKey, fontNode); uniqueFontsMap.put(uniqueKey, fontNode);
} }
} }
}
}
// Add unique font entries to fontsArray // Add unique font entries to fontsArray
for (ObjectNode uniqueFontNode : uniqueFontsMap.values()) { for (ObjectNode uniqueFontNode : uniqueFontsMap.values()) {
@ -552,42 +551,50 @@ public class GetInfoOnPDF {
// Access resources dictionary
PdfDictionary resourcesDict = itextDoc.getPage(pageNum).getResources().getPdfObject();
// Color Spaces & ICC Profiles
// Access resources dictionary
ArrayNode colorSpacesArray = objectMapper.createArrayNode(); ArrayNode colorSpacesArray = objectMapper.createArrayNode();
PdfDictionary colorSpaces = resourcesDict.getAsDictionary(PdfName.ColorSpace);
if (colorSpaces != null) { Iterable<COSName> colorSpaceNames = resources.getColorSpaceNames();
for (PdfName name : colorSpaces.keySet()) { for (COSName name : colorSpaceNames) {
PdfObject colorSpaceObject = colorSpaces.get(name); PDColorSpace colorSpace = resources.getColorSpace(name);
if (colorSpaceObject instanceof PdfArray) { if (colorSpace instanceof PDICCBased) {
PdfArray colorSpaceArray = (PdfArray) colorSpaceObject; PDICCBased iccBased = (PDICCBased) colorSpace;
if (colorSpaceArray.size() > 1 && colorSpaceArray.get(0) instanceof PdfName && PdfName.ICCBased.equals(colorSpaceArray.get(0))) { PDStream iccData = iccBased.getPDStream();
ObjectNode iccProfileNode = objectMapper.createObjectNode(); byte[] iccBytes = iccData.toByteArray();
PdfStream iccStream = (PdfStream) colorSpaceArray.get(1);
byte[] iccData = iccStream.getBytes();
// TODO: Further decode and analyze the ICC data if needed // TODO: Further decode and analyze the ICC data if needed
iccProfileNode.put("ICC Profile Length", iccData.length); ObjectNode iccProfileNode = objectMapper.createObjectNode();
iccProfileNode.put("ICC Profile Length", iccBytes.length);
colorSpacesArray.add(iccProfileNode); colorSpacesArray.add(iccProfileNode);
} }
} }
}
}
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
// Other XObjects // Other XObjects
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
PdfDictionary xObjects = resourcesDict.getAsDictionary(PdfName.XObject); for (COSName name : resources.getXObjectNames()) {
if (xObjects != null) { PDXObject xObject = resources.getXObject(name);
for (PdfName name : xObjects.keySet()) { String xObjectType;
PdfStream xObjectStream = xObjects.getAsStream(name);
String xObjectType = xObjectStream.getAsName(PdfName.Subtype).toString(); if (xObject instanceof PDImageXObject) {
xObjectType = "Image";
} else if (xObject instanceof PDFormXObject) {
xObjectType = "Form";
} else {
xObjectType = "Other";
}
// Increment the count for this type in the map // Increment the count for this type in the map
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
} }
}
// Add the count map to pageInfo (or wherever you want to store it) // Add the count map to pageInfo (or wherever you want to store it)
ObjectNode xObjectCountNode = objectMapper.createObjectNode(); ObjectNode xObjectCountNode = objectMapper.createObjectNode();
@ -598,14 +605,17 @@ public class GetInfoOnPDF {
ArrayNode multimediaArray = objectMapper.createArrayNode(); ArrayNode multimediaArray = objectMapper.createArrayNode();
for (PdfAnnotation annotation : annotations) {
if (PdfName.RichMedia.equals(annotation.getSubtype())) { for (PDAnnotation annotation : annotations) {
if ("RichMedia".equals(annotation.getSubtype())) {
ObjectNode multimediaNode = objectMapper.createObjectNode(); ObjectNode multimediaNode = objectMapper.createObjectNode();
// Extract details from the dictionary as needed // Extract details from the annotation as needed
multimediaArray.add(multimediaNode); multimediaArray.add(multimediaNode);
} }
} }
pageInfo.set("Multimedia", multimediaArray); pageInfo.set("Multimedia", multimediaArray);
@ -636,17 +646,21 @@ public class GetInfoOnPDF {
return null; return null;
} }
private static void addOutlinesToArray(PdfOutline outline, ArrayNode arrayNode) { private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return; if (outline == null) return;
ObjectNode outlineNode = objectMapper.createObjectNode(); ObjectNode outlineNode = objectMapper.createObjectNode();
outlineNode.put("Title", outline.getTitle()); outlineNode.put("Title", outline.getTitle());
// You can add other properties if needed // You can add other properties if needed
arrayNode.add(outlineNode); arrayNode.add(outlineNode);
for (PdfOutline child : outline.getAllChildren()) { PDOutlineItem child = outline.getFirstChild();
while (child != null) {
addOutlinesToArray(child, arrayNode); addOutlinesToArray(child, arrayNode);
child = child.getNextSibling();
} }
} }
public String getPageOrientation(double width, double height) { public String getPageOrientation(double width, double height) {
if (width > height) { if (width > height) {
return "Landscape"; return "Landscape";
@ -678,22 +692,26 @@ public class GetInfoOnPDF {
return Math.abs(pageAspectRatio - aspectRatio) <= 0.05; return Math.abs(pageAspectRatio - aspectRatio) <= 0.05;
} }
public boolean checkForStandard(PdfDocument document, String standardKeyword) {
// Check Output Intents
boolean foundInOutputIntents = checkOutputIntent(document, standardKeyword);
if (foundInOutputIntents) return true;
// Check XMP Metadata (rudimentary)
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
// Check XMP Metadata
try { try {
byte[] metadataBytes = document.getXmpMetadata(); PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (metadataBytes != null) { if (pdMetadata != null) {
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(metadataBytes); COSInputStream metaStream = pdMetadata.createInputStream();
String xmpString = xmpMeta.dumpObject(); DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, baos, true);
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
if (xmpString.contains(standardKeyword)) { if (xmpString.contains(standardKeyword)) {
return true; return true;
} }
} }
} catch (XMPException e) { } catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
e.printStackTrace(); e.printStackTrace();
} }
@ -701,22 +719,6 @@ public class GetInfoOnPDF {
} }
public boolean checkOutputIntent(PdfDocument document, String standard) {
PdfArray outputIntents = document.getCatalog().getPdfObject().getAsArray(PdfName.OutputIntents);
if (outputIntents != null && !outputIntents.isEmpty()) {
for (int i = 0; i < outputIntents.size(); i++) {
PdfDictionary outputIntentDict = outputIntents.getAsDictionary(i);
if (outputIntentDict != null) {
PdfString s = outputIntentDict.getAsString(PdfName.S);
if (s != null && s.toString().contains(standard)) {
return true;
}
}
}
}
return false;
}
public ArrayNode exploreStructureTree(List<Object> nodes) { public ArrayNode exploreStructureTree(List<Object> nodes) {
ArrayNode elementsArray = objectMapper.createArrayNode(); ArrayNode elementsArray = objectMapper.createArrayNode();
if (nodes != null) { if (nodes != null) {
@ -771,7 +773,7 @@ public class GetInfoOnPDF {
} }
} }
private String getPageModeDescription(PdfName pageMode) { private String getPageModeDescription(String pageMode) {
return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown"; return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown";
} }
} }

View File

@ -1,22 +1,8 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@ -27,10 +13,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Controller @Controller
@ -107,6 +91,7 @@ public class AccountWebController {
model.addAttribute("username", username); model.addAttribute("username", username);
model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson); model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
} }
} else { } else {
return "redirect:/"; return "redirect:/";
@ -116,5 +101,35 @@ public class AccountWebController {
@GetMapping("/change-creds")
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/";
}
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
String username = userDetails.getUsername();
// Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
if (!user.isPresent()) {
// Handle error appropriately
return "redirect:/error"; // Example redirection in case of error
}
// Add attributes to the model
model.addAttribute("username", username);
}
} else {
return "redirect:/";
}
return "change-creds";
}
} }

View File

@ -9,6 +9,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -140,27 +141,96 @@ public class GeneralWebController {
@Autowired @Autowired
private ResourceLoader resourceLoader; private ResourceLoader resourceLoader;
private List<String> getFontNames() { private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>();
// Extract font names from classpath
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
// Extract font names from external directory
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
return fontNames;
}
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
try { try {
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader) Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
.getResources("classpath:static/fonts/*.woff2"); .getResources(locationPattern);
return Arrays.stream(resources) return Arrays.stream(resources)
.map(resource -> { .map(resource -> {
try { try {
String filename = resource.getFilename(); String filename = resource.getFilename();
return filename.substring(0, filename.length() - 6); // Remove .woff2 extension if (filename != null) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex != -1) {
String name = filename.substring(0, lastDotIndex);
String extension = filename.substring(lastDotIndex + 1);
return new FontResource(name, extension);
}
}
return null;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Error processing filename", e); throw new RuntimeException("Error processing filename", e);
} }
}) })
.filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to read font directory", e); throw new RuntimeException("Failed to read font directory from " + locationPattern, e);
} }
} }
public String getFormatFromExtension(String extension) {
switch (extension) {
case "ttf": return "truetype";
case "woff": return "woff";
case "woff2": return "woff2";
case "eot": return "embedded-opentype";
case "svg": return "svg";
default: return ""; // or throw an exception if an unexpected extension is encountered
}
}
public class FontResource {
private String name;
private String extension;
private String type;
public FontResource(String name, String extension) {
this.name = name;
this.extension = extension;
this.type = getFormatFromExtension(extension);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExtension() {
return extension;
}
public void setExtension(String extension) {
this.extension = extension;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
@GetMapping("/crop") @GetMapping("/crop")
@Hidden @Hidden

View File

@ -104,7 +104,6 @@ public class ApplicationProperties {
} }
public static class Security { public static class Security {
private Boolean enableLogin; private Boolean enableLogin;
private InitialLogin initialLogin;
private Boolean csrfDisabled; private Boolean csrfDisabled;
public Boolean getEnableLogin() { public Boolean getEnableLogin() {
@ -115,14 +114,6 @@ public class ApplicationProperties {
this.enableLogin = enableLogin; this.enableLogin = enableLogin;
} }
public InitialLogin getInitialLogin() {
return initialLogin != null ? initialLogin : new InitialLogin();
}
public void setInitialLogin(InitialLogin initialLogin) {
this.initialLogin = initialLogin;
}
public Boolean getCsrfDisabled() { public Boolean getCsrfDisabled() {
return csrfDisabled; return csrfDisabled;
} }
@ -134,40 +125,9 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" return "Security [enableLogin=" + enableLogin + ", csrfDisabled="
+ csrfDisabled + "]"; + csrfDisabled + "]";
} }
public static class InitialLogin {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]";
}
}
} }
public static class System { public static class System {

View File

@ -40,6 +40,9 @@ public class User {
@Column(name = "enabled") @Column(name = "enabled")
private boolean enabled; private boolean enabled;
@Column(name = "isFirstLogin")
private Boolean isFirstLogin = false;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>(); private Set<Authority> authorities = new HashSet<>();
@ -50,6 +53,13 @@ public class User {
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings. private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
public boolean isFirstLogin() {
return isFirstLogin != null && isFirstLogin;
}
public void setFirstLogin(boolean isFirstLogin) {
this.isFirstLogin = isFirstLogin;
}
public Long getId() { public Long getId() {
return id; return id;

View File

@ -65,7 +65,8 @@ public class GeneralUtils {
} else if (sizeStr.endsWith("B")) { } else if (sizeStr.endsWith("B")) {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} else { } else {
// Input string does not have a valid format, handle this case // Assume MB if no unit is specified
return (long) (Double.parseDouble(sizeStr) * 1024 * 1024);
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// The numeric part of the input string cannot be parsed, handle this case // The numeric part of the input string cannot be parsed, handle this case

View File

@ -12,8 +12,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
public class WebResponseUtils { public class WebResponseUtils {
@ -61,18 +59,6 @@ public class WebResponseUtils {
return boasToWebResponse(baos, docName); return boasToWebResponse(baos, docName);
} }
public static ResponseEntity<byte[]> pdfDocToWebResponse(PdfDocument document, String docName) throws IOException {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument newDocument = new PdfDocument(writer);
document.copyPagesTo(1, document.getNumberOfPages(), newDocument);
newDocument.close();
return boasToWebResponse(baos, docName);
}
} }

View File

@ -43,7 +43,11 @@ green=Green
blue=Blue blue=Blue
custom=Custom... custom=Custom...
changedCredsMessage=Credentials changed!
notAuthenticatedMessage=User not authenticated.
userNotFoundMessage=User not found.
incorrectPasswordMessage=Current password is incorrect.
usernameExistsMessage=New Username already exists.
@ -71,6 +75,19 @@ settings.zipThreshold=Zip files when the number of downloaded files exceeds
settings.signOut=Sign Out settings.signOut=Sign Out
settings.accountSettings=Account Settings settings.accountSettings=Account Settings
changeCreds.title=Change Credentials
changeCreds.header=Update Your Account Details
changeCreds.changeUserAndPassword=You are using default login credentials. Please enter a new password (and username if wanted)
changeCreds.newUsername=New Username
changeCreds.oldPassword=Current Password
changeCreds.newPassword=New Password
changeCreds.confirmNewPassword=Confirm New Password
changeCreds.submit=Submit Changes
account.title=Account Settings account.title=Account Settings
account.accountSettings=Account Settings account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users account.adminSettings=Admin Settings - View and Add Users
@ -102,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.forceChange = Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
############# #############
@ -750,13 +768,6 @@ changeMetadata.selectText.5=Add Custom Metadata Entry
changeMetadata.submit=Change changeMetadata.submit=Change
#xlsToPdf
xlsToPdf.title=Excel to PDF
xlsToPdf.header=Excel to PDF
xlsToPdf.selectText.1=Select XLS or XLSX Excel sheet to convert
xlsToPdf.convert=convert
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF To PDF/A pdfToPDFA.title=PDF To PDF/A
pdfToPDFA.header=PDF To PDF/A pdfToPDFA.header=PDF To PDF/A

View File

@ -4,16 +4,11 @@
security: security:
enableLogin: false # set to 'true' to enable login enableLogin: false # set to 'true' to enable login
initialLogin:
username: 'username' # Specify the initial username for first boot (e.g. 'admin')
password: 'password' # Specify the initial password for first boot (e.g. 'password123')
csrfDisabled: true csrfDisabled: true
system: system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
rootURIPath: / # Set the application's root URI (e.g. /pdf-app)
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
#ui: #ui:
# appName: exampleAppName # Application's visible name # appName: exampleAppName # Application's visible name

View File

@ -16,11 +16,30 @@
<!-- User Settings Title --> <!-- User Settings Title -->
<h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2> <h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2>
<hr> <hr>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger">
<span th:text="#{notAuthenticatedMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'userNotFound'}" class="alert alert-danger">
<span th:text="#{userNotFoundMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger">
<span th:text="#{incorrectPasswordMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
</div>
<!-- At the top of the user settings --> <!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3> <h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<div th:if="${error}" class="alert alert-danger" role="alert">
<span th:text="${error}">Error Message</span>
</div>
<!-- Change Username Form --> <!-- Change Username Form -->
<h4></h4> <h4></h4>
<form action="/change-username" method="post"> <form action="/change-username" method="post">

View File

@ -43,6 +43,9 @@
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2> <h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
</div>
<form action="/admin/saveUser" method="post"> <form action="/admin/saveUser" method="post">
<div class="mb-3"> <div class="mb-3">
<label for="username" th:text="#{username}">Username</label> <label for="username" th:text="#{username}">Username</label>
@ -61,6 +64,10 @@
<option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option> <option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
</select> </select>
</div> </div>
<div class="mb-3">
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
</div>
<!-- Add other fields as required --> <!-- Add other fields as required -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button> <button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>

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 urlParams = currentURL.searchParams;
const currentLangParam = urlParams.get('lang') || defaultLocale; const currentLangParam = urlParams.get('lang') || defaultLocale;
console.log("defaultLocale", defaultLocale)
console.log("storedLocale", storedLocale)
console.log("currentLangParam", currentLangParam)
if (currentLangParam !== storedLocale) { if (defaultLocale !== storedLocale && currentLangParam !== storedLocale) {
urlParams.set('lang', storedLocale); urlParams.set('lang', storedLocale);
currentURL.search = urlParams.toString(); currentURL.search = urlParams.toString();
console.log("redirecting to", currentURL.toString());
window.location.href = currentURL.toString(); window.location.href = currentURL.toString();
return; return;
} }
@ -238,14 +233,17 @@ function handleDropdownItemClick(event) {
if (languageCode) { if (languageCode) {
localStorage.setItem('languageCode', languageCode); localStorage.setItem('languageCode', languageCode);
const currentLang = document.documentElement.getAttribute('lang');
if (currentLang !== languageCode) {
console.log("currentLang", currentLang)
console.log("languageCode", languageCode)
const currentUrl = window.location.href; const currentUrl = window.location.href;
if (currentUrl.indexOf('?lang=') === -1) { if (currentUrl.indexOf('?lang=') === -1) {
window.location.href = currentUrl + '?lang=' + languageCode; window.location.href = currentUrl + '?lang=' + languageCode;
} else { } else {
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode); window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
} }
}
dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content
} else { } else {
console.error("Language code is not set for this item."); console.error("Language code is not set for this item.");
@ -258,6 +256,9 @@ function handleDropdownItemClick(event) {
<div th:if="${logoutMessage}" class="alert alert-success" <div th:if="${logoutMessage}" class="alert alert-success"
th:text="${logoutMessage}"></div> th:text="${logoutMessage}"></div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'credsUpdated'}" class="alert alert-success">
<span th:text="#{changedCredsMessage}">Default message if not found</span>
</div>
<form th:action="@{login}" method="post"> <form th:action="@{login}" method="post">
<img class="mb-4" src="favicon.svg" alt="" width="144" height="144"> <img class="mb-4" src="favicon.svg" alt="" width="144" height="144">
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1> <h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>

View File

@ -19,7 +19,6 @@
<div class="mb-3"> <div class="mb-3">
<label for="pageSize" th:text="#{scalePages.pageSize}"></label> <label for="pageSize" th:text="#{scalePages.pageSize}"></label>
<select id="pageSize" name="pageSize" required> <select id="pageSize" name="pageSize" required>
<option value="A0">A0</option> <option value="A0">A0</option>
<option value="A1">A1</option> <option value="A1">A1</option>
<option value="A2">A2</option> <option value="A2">A2</option>
@ -27,25 +26,8 @@
<option value="A4" selected>A4</option> <option value="A4" selected>A4</option>
<option value="A5">A5</option> <option value="A5">A5</option>
<option value="A6">A6</option> <option value="A6">A6</option>
<option value="A7">A7</option>
<option value="A8">A8</option>
<option value="A9">A9</option>
<option value="A10">A10</option>
<option value="B0">B0</option>
<option value="B1">B1</option>
<option value="B2">B2</option>
<option value="B3">B3</option>
<option value="B4">B4</option>
<option value="B5">B5</option>
<option value="B6">B6</option>
<option value="B7">B7</option>
<option value="B8">B8</option>
<option value="B9">B9</option>
<option value="LETTER">Letter</option> <option value="LETTER">Letter</option>
<option value="LEGAL">Legal</option> <option value="LEGAL">Legal</option>
<option value="EXECUTIVE">Executive</option>
<option value="TABLOID">Tabloid</option>
<option value="LEDGER">Ledger</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@ -28,7 +28,16 @@
<option value="image">Image</option> <option value="image">Image</option>
</select> </select>
</div> </div>
<div id="alphabetGroup" class="mb-3">
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
<select class="form-control" name="alphabet" id="alphabet-select">
<option value="roman">Roman</option>
<option value="arabic">العربية</option>
<option value="japanese">日本語</option>
<option value="korean">한국어</option>
<option value="chinese">简体中文</option>
</select>
</div>
<div id="watermarkTextGroup" class="mb-3"> <div id="watermarkTextGroup" class="mb-3">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label> <label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required /> <input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
@ -105,6 +114,7 @@
const watermarkType = document.getElementById('watermarkType').value; const watermarkType = document.getElementById('watermarkType').value;
const watermarkTextGroup = document.getElementById('watermarkTextGroup'); const watermarkTextGroup = document.getElementById('watermarkTextGroup');
const watermarkImageGroup = document.getElementById('watermarkImageGroup'); const watermarkImageGroup = document.getElementById('watermarkImageGroup');
const alphabetGroup = document.getElementById('alphabetGroup'); // This is the new addition
const watermarkText = document.getElementById('watermarkText'); const watermarkText = document.getElementById('watermarkText');
const watermarkImage = document.getElementById('watermarkImage'); const watermarkImage = document.getElementById('watermarkImage');
@ -113,11 +123,13 @@
watermarkText.required = true; watermarkText.required = true;
watermarkImageGroup.style.display = 'none'; watermarkImageGroup.style.display = 'none';
watermarkImage.required = false; watermarkImage.required = false;
alphabetGroup.style.display = 'block';
} else if (watermarkType === 'image') { } else if (watermarkType === 'image') {
watermarkTextGroup.style.display = 'none'; watermarkTextGroup.style.display = 'none';
watermarkText.required = false; watermarkText.required = false;
watermarkImageGroup.style.display = 'block'; watermarkImageGroup.style.display = 'block';
watermarkImage.required = true; watermarkImage.required = true;
alphabetGroup.style.display = 'none';
} }
} }
</script> </script>

View File

@ -10,15 +10,16 @@
<th:block th:each="font : ${fonts}"> <th:block th:each="font : ${fonts}">
<style th:inline="text"> <style th:inline="text">
@font-face { @font-face {
font-family: "[[${font}]]"; font-family: "[[${font.name}]]";
src: url('fonts/[[${font}]].woff2') format('woff2'); src: url('fonts/[[${font.name}]].[[${font.extension}]]') format('[[${font.type}]]');
} }
#font-select option[value="[[${font}]]"] { #font-select option[value="[[${font.name}]]"] {
font-family: "[[${font}]]", cursive; font-family: "[[${font.name}]]", cursive;
} }
</style> </style>
</th:block> </th:block>
<style> <style>
select#font-select, select#font-select option { select#font-select, select#font-select option {
height: 60px; /* Adjust as needed */ height: 60px; /* Adjust as needed */
@ -181,9 +182,13 @@ select#font-select, select#font-select option {
<input type="text" class="form-control" id="sigText" name="sigText"> <input type="text" class="form-control" id="sigText" name="sigText">
<label th:text="#{font}"></label> <label th:text="#{font}"></label>
<select class="form-control" name="font" id="font-select"> <select class="form-control" name="font" id="font-select">
<option th:each="font : ${fonts}" th:value="${font}" th:text="${font}" th:class="${font.toLowerCase()+'-font'}"></option> <option th:each="font : ${fonts}"
th:value="${font.name}"
th:text="${font.name}"
th:class="${font.name.toLowerCase()+'-font'}">
</option>
</select> </select>
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button> <button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
</div> </div>
@ -233,12 +238,13 @@ select#font-select, select#font-select option {
<th:block th:each="font : ${fonts}"> <th:block th:each="font : ${fonts}">
<style th:inline="text"> <style th:inline="text">
#font-select option[value="/*[[${font}]]*/"] { #font-select option[value='/*[[${font.name}]]*/'] {
font-family: '/*[[${font}]]*/', cursive; font-family: '/*[[${font.name}]]*/', cursive;
} }
</style> </style>
</th:block> </th:block>
</div> </div>
</div> </div>