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