1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-09-28 15:50:08 +02:00

formatting

This commit is contained in:
Anthony Stirling 2023-12-30 19:11:27 +00:00
parent 7b43fca6fc
commit 5f771b7851
155 changed files with 5539 additions and 4767 deletions

View File

@ -22,14 +22,14 @@ public class LibreOfficeListener {
private Process process; private Process process;
private LibreOfficeListener() { private LibreOfficeListener() {}
}
private boolean isListenerRunning() { private boolean isListenerRunning() {
try { try {
System.out.println("waiting for listener to start"); System.out.println("waiting for listener to start");
Socket socket = new Socket(); Socket socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second socket.connect(
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
socket.close(); socket.close();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
@ -49,21 +49,22 @@ public class LibreOfficeListener {
// Start a background thread to monitor the activity timeout // Start a background thread to monitor the activity timeout
executorService = Executors.newSingleThreadExecutor(); executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> { executorService.submit(
while (true) { () -> {
long idleTime = System.currentTimeMillis() - lastActivityTime; while (true) {
if (idleTime >= ACTIVITY_TIMEOUT) { long idleTime = System.currentTimeMillis() - lastActivityTime;
// If there has been no activity for too long, tear down the listener if (idleTime >= ACTIVITY_TIMEOUT) {
process.destroy(); // If there has been no activity for too long, tear down the listener
break; process.destroy();
} break;
try { }
Thread.sleep(5000); // Check for inactivity every 5 seconds try {
} catch (InterruptedException e) { Thread.sleep(5000); // Check for inactivity every 5 seconds
break; } catch (InterruptedException e) {
} break;
} }
}); }
});
// Wait for the listener to start up // Wait for the listener to start up
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@ -92,5 +93,4 @@ public class LibreOfficeListener {
process.destroy(); process.destroy();
} }
} }
} }

View File

@ -13,13 +13,12 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
@SpringBootApplication
@EnableScheduling @EnableScheduling
public class SPdfApplication { public class SPdfApplication {
@Autowired @Autowired private Environment env;
private Environment env;
@PostConstruct @PostConstruct
public void init() { public void init() {
@ -44,21 +43,24 @@ public class SPdfApplication {
} }
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
if (Files.exists(Paths.get("configs/settings.yml"))) { if (Files.exists(Paths.get("configs/settings.yml"))) {
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml")); app.setDefaultProperties(
Collections.singletonMap(
"spring.config.additional-location", "file:configs/settings.yml"));
} else { } else {
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); System.out.println(
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
} }
app.run(args); app.run(args);
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
} }
GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/static/");
GeneralUtils.createDir("customFiles/templates/"); GeneralUtils.createDir("customFiles/templates/");

View File

@ -5,13 +5,12 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class AppConfig { public class AppConfig {
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
@Bean(name = "loginEnabled") @Bean(name = "loginEnabled")
public boolean loginEnabled() { public boolean loginEnabled() {
return applicationProperties.getSecurity().getEnableLogin(); return applicationProperties.getSecurity().getEnableLogin();
@ -19,7 +18,7 @@ public class AppConfig {
@Bean(name = "appName") @Bean(name = "appName")
public String appName() { public String appName() {
String homeTitle = applicationProperties.getUi().getAppName(); String homeTitle = applicationProperties.getUi().getAppName();
return (homeTitle != null) ? homeTitle : "Stirling PDF"; return (homeTitle != null) ? homeTitle : "Stirling PDF";
} }
@ -31,28 +30,31 @@ public class AppConfig {
@Bean(name = "homeText") @Bean(name = "homeText")
public String homeText() { public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null"; return (applicationProperties.getUi().getHomeDescription() != null)
? applicationProperties.getUi().getHomeDescription()
: "null";
} }
@Bean(name = "navBarText") @Bean(name = "navBarText")
public String navBarText() { public String navBarText() {
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName(); String defaultNavBar =
applicationProperties.getUi().getAppNameNavbar() != null
? applicationProperties.getUi().getAppNameNavbar()
: applicationProperties.getUi().getAppName();
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
} }
@Bean(name = "enableAlphaFunctionality") @Bean(name = "enableAlphaFunctionality")
public boolean enableAlphaFunctionality() { public boolean enableAlphaFunctionality() {
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null ? applicationProperties.getSystem().getEnableAlphaFunctionality() : false; return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
? applicationProperties.getSystem().getEnableAlphaFunctionality()
: false;
} }
@Bean(name = "rateLimit") @Bean(name = "rateLimit")
public boolean rateLimit() { public boolean rateLimit() {
String appName = System.getProperty("rateLimit"); String appName = System.getProperty("rateLimit");
if (appName == null) if (appName == null) appName = System.getenv("rateLimit");
appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false; return (appName != null) ? Boolean.valueOf(appName) : false;
} }
}
}

View File

@ -15,10 +15,9 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(localeChangeInterceptor());
@ -35,25 +34,26 @@ public class Beans implements WebMvcConfigurer {
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set Locale defaultLocale =
Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
String tempLanguageTag = tempLocale.toLanguageTag(); String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); System.err.println(
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
} }
} }
} }
@ -61,5 +61,4 @@ public class Beans implements WebMvcConfigurer {
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }
} }

View File

@ -13,56 +13,62 @@ import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); private static final List<String> ALLOWED_PARAMS =
Arrays.asList(
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
@Override
@Override public boolean preHandle(
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) { if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>(); Map<String, String> parameters = new HashMap<>();
// Keep only the allowed parameters // Keep only the allowed parameters
String[] queryParameters = queryString.split("&"); String[] queryParameters = queryString.split("&");
for (String param : queryParameters) { for (String param : queryParameters) {
String[] keyValue = param.split("="); String[] keyValue = param.split("=");
if (keyValue.length != 2) { if (keyValue.length != 2) {
continue; continue;
} }
if (ALLOWED_PARAMS.contains(keyValue[0])) { if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]); parameters.put(keyValue[0], keyValue[1]);
} }
} }
// If there are any parameters that are not allowed // If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) { if (parameters.size() != queryParameters.length) {
// Construct new query string // Construct new query string
StringBuilder newQueryString = new StringBuilder(); StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) { for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (newQueryString.length() > 0) { if (newQueryString.length() > 0) {
newQueryString.append("&"); newQueryString.append("&");
} }
newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
} }
// Redirect to the URL with only allowed query parameters // Redirect to the URL with only allowed query parameters
String redirectUrl = requestURI + "?" + newQueryString; String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl); response.sendRedirect(redirectUrl);
return false; return false;
} }
} }
return true; return true;
} }
@Override @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, public void postHandle(
ModelAndView modelAndView) { HttpServletRequest request,
} HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {}
@Override @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, public void afterCompletion(
Exception ex) { HttpServletRequest request,
} HttpServletResponse response,
Object handler,
Exception ex) {}
} }

View File

@ -19,111 +19,125 @@ import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override @Override
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
try { try {
ensureConfigExists(); ensureConfigExists();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to initialize application configuration", e); throw new RuntimeException("Failed to initialize application configuration", e);
} }
} }
public void ensureConfigExists() throws IOException { public void ensureConfigExists() throws IOException {
// Define the path to the external config directory // Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml"); Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists // Check if the file already exists
if (Files.notExists(destPath)) { if (Files.notExists(destPath)) {
// Ensure the destination directory exists // Ensure the destination directory exists
Files.createDirectories(destPath.getParent()); Files.createDirectories(destPath.getParent());
// Copy the resource from classpath to the external directory // Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { try (InputStream in =
if (in != null) { getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
Files.copy(in, destPath); if (in != null) {
} else { Files.copy(in, destPath);
throw new FileNotFoundException("Resource file not found: settings.yml.template"); } 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; } else {
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { // If user file exists, we need to merge it with the template from the classpath
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines() List<String> templateLines;
.collect(Collectors.toList()); 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); mergeYamlFiles(templateLines, destPath, destPath);
} }
} }
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException { public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
List<String> userLines = Files.readAllLines(userFilePath); throws IOException {
List<String> mergedLines = new ArrayList<>(); List<String> userLines = Files.readAllLines(userFilePath);
boolean insideAutoGenerated = false; List<String> mergedLines = new ArrayList<>();
boolean beforeFirstKey = true; boolean insideAutoGenerated = false;
boolean beforeFirstKey = true;
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#"); Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
Function<String, String> extractKey = line -> { Function<String, String> extractKey =
String[] parts = line.split(":"); line -> {
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; String[] parts = line.split(":");
}; return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
};
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet()); Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
for (String line : templateLines) { for (String line : templateLines) {
String key = extractKey.apply(line); String key = extractKey.apply(line);
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) { if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
insideAutoGenerated = true; insideAutoGenerated = true;
mergedLines.add(line); mergedLines.add(line);
continue; continue;
} else if (insideAutoGenerated && line.trim().isEmpty()) { } else if (insideAutoGenerated && line.trim().isEmpty()) {
insideAutoGenerated = false; insideAutoGenerated = false;
mergedLines.add(line); mergedLines.add(line);
continue; continue;
} }
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) { if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
// Handle top comments and empty lines before the first key. // Handle top comments and empty lines before the first key.
mergedLines.add(line); mergedLines.add(line);
continue; continue;
} }
if (!key.isEmpty()) if (!key.isEmpty()) beforeFirstKey = false;
beforeFirstKey = false;
if (userKeys.contains(key)) { if (userKeys.contains(key)) {
// If user has any version (commented or uncommented) of this key, skip the // If user has any version (commented or uncommented) of this key, skip the
// template line // template line
Optional<String> userValue = userLines.stream() Optional<String> userValue =
.filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst(); userLines.stream()
if (userValue.isPresent()) .filter(
mergedLines.add(userValue.get()); l ->
continue; extractKey.apply(l).equalsIgnoreCase(key)
} && !isCommented.apply(l))
.findFirst();
if (userValue.isPresent()) mergedLines.add(userValue.get());
continue;
}
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the mergedLines.add(
// template line line); // If line is commented, empty or key not present in user's file,
continue; // retain the
} // template line
} continue;
}
}
// Add any additional uncommented user lines that are not present in the // Add any additional uncommented user lines that are not present in the
// template // template
for (String userLine : userLines) { for (String userLine : userLines) {
String userKey = extractKey.apply(userLine); String userKey = extractKey.apply(userLine);
boolean isPresentInTemplate = templateLines.stream().map(extractKey) boolean isPresentInTemplate =
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); templateLines.stream()
if (!isPresentInTemplate && !isCommented.apply(userLine)) { .map(extractKey)
mergedLines.add(userLine); .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
} if (!isPresentInTemplate && !isCommented.apply(userLine)) {
} mergedLines.add(userLine);
}
}
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
} }
}
}

View File

@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
@ -26,16 +27,16 @@ public class EndpointConfiguration {
init(); init();
processEnvironmentConfigs(); processEnvironmentConfigs();
} }
public void enableEndpoint(String endpoint) { public void enableEndpoint(String endpoint) {
endpointStatuses.put(endpoint, true); endpointStatuses.put(endpoint, true);
} }
public void disableEndpoint(String endpoint) { public void disableEndpoint(String endpoint) {
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
logger.info("Disabling {}", endpoint); logger.info("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false); endpointStatuses.put(endpoint, false);
} }
} }
public boolean isEndpointEnabled(String endpoint) { public boolean isEndpointEnabled(String endpoint) {
@ -66,7 +67,7 @@ public class EndpointConfiguration {
} }
} }
} }
public void init() { public void init() {
// Adding endpoints to "PageOps" group // Adding endpoints to "PageOps" group
addEndpointToGroup("PageOps", "remove-pages"); addEndpointToGroup("PageOps", "remove-pages");
@ -84,8 +85,7 @@ public class EndpointConfiguration {
addEndpointToGroup("PageOps", "split-by-size-or-count"); addEndpointToGroup("PageOps", "split-by-size-or-count");
addEndpointToGroup("PageOps", "overlay-pdf"); addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections"); addEndpointToGroup("PageOps", "split-pdf-by-sections");
// Adding endpoints to "Convert" group // Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("Convert", "img-to-pdf"); addEndpointToGroup("Convert", "img-to-pdf");
@ -101,8 +101,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "url-to-pdf"); addEndpointToGroup("Convert", "url-to-pdf");
addEndpointToGroup("Convert", "markdown-to-pdf"); addEndpointToGroup("Convert", "markdown-to-pdf");
addEndpointToGroup("Convert", "pdf-to-csv"); addEndpointToGroup("Convert", "pdf-to-csv");
// Adding endpoints to "Security" group // Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password"); addEndpointToGroup("Security", "add-password");
addEndpointToGroup("Security", "remove-password"); addEndpointToGroup("Security", "remove-password");
@ -111,8 +110,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "cert-sign"); addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact"); addEndpointToGroup("Security", "auto-redact");
// Adding endpoints to "Other" group // Adding endpoints to "Other" group
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "add-image"); addEndpointToGroup("Other", "add-image");
@ -130,10 +128,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("Other", "auto-rename");
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("Other", "show-javascript");
// CLI
//CLI
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("CLI", "extract-image-scans"); addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("CLI", "remove-blanks"); addEndpointToGroup("CLI", "remove-blanks");
@ -149,19 +145,18 @@ public class EndpointConfiguration {
addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf"); addEndpointToGroup("CLI", "url-to-pdf");
// python
//python
addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "remove-blanks"); addEndpointToGroup("Python", "remove-blanks");
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("Python", "url-to-pdf"); addEndpointToGroup("Python", "url-to-pdf");
//openCV // openCV
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("OpenCV", "remove-blanks"); addEndpointToGroup("OpenCV", "remove-blanks");
//LibreOffice // LibreOffice
addEndpointToGroup("LibreOffice", "repair"); addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
@ -170,14 +165,13 @@ public class EndpointConfiguration {
addEndpointToGroup("LibreOffice", "pdf-to-text"); addEndpointToGroup("LibreOffice", "pdf-to-text");
addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml"); addEndpointToGroup("LibreOffice", "pdf-to-xml");
// OCRmyPDF
//OCRmyPDF
addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
addEndpointToGroup("OCRmyPDF", "ocr-pdf"); addEndpointToGroup("OCRmyPDF", "ocr-pdf");
//Java // Java
addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-pages"); addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "split-pdfs"); addEndpointToGroup("Java", "split-pdfs");
@ -210,16 +204,14 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "split-by-size-or-count"); addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections"); addEndpointToGroup("Java", "split-pdf-by-sections");
//Javascript // Javascript
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Javascript", "sign");
addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast"); addEndpointToGroup("Javascript", "adjust-contrast");
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
@ -236,6 +228,4 @@ public class EndpointConfiguration {
} }
} }
} }
} }

View File

@ -10,11 +10,11 @@ import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
@Autowired @Autowired private EndpointConfiguration endpointConfiguration;
private EndpointConfiguration endpointConfiguration;
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) { if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
@ -23,4 +23,4 @@ public class EndpointInterceptor implements HandlerInterceptor {
} }
return true; return true;
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -21,4 +22,4 @@ public class MetricsConfig {
} }
}; };
} }
} }

View File

@ -8,6 +8,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -16,35 +17,48 @@ import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class MetricsFilter extends OncePerRequestFilter { public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
@Autowired @Autowired
public MetricsFilter(MeterRegistry meterRegistry) { public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
} }
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(
throws ServletException, IOException { HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
String uri = request.getRequestURI(); throws ServletException, IOException {
String uri = request.getRequestURI();
// System.out.println("uri="+uri + ", method=" + request.getMethod() ); // System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources // Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/v1/api-docs") || uri.endsWith("robots.txt") if (!(uri.startsWith("/js")
|| uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map") || uri.startsWith("/v1/api-docs")
|| uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger") || uri.endsWith("robots.txt")
|| uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) { || uri.startsWith("/images")
|| uri.startsWith("/images")
|| uri.endsWith(".png")
|| uri.endsWith(".ico")
Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod()) || uri.endsWith(".css")
.register(meterRegistry); || uri.endsWith(".map")
|| uri.endsWith(".svg")
|| uri.endsWith(".js")
|| uri.contains("swagger")
|| uri.startsWith("/api/v1/info")
|| uri.startsWith("/site.webmanifest")
|| uri.startsWith("/fonts")
|| uri.startsWith("/pdfjs"))) {
counter.increment(); Counter counter =
// System.out.println("Counted"); Counter.builder("http.requests")
} .tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
filterChain.doFilter(request, response); counter.increment();
} // System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
} }

View File

@ -9,34 +9,45 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
version = "1.0.0"; // default version if all else fails version = "1.0.0"; // default version if all else fails
} }
SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER) SecurityScheme apiKeyScheme =
.name("X-API-KEY"); new SecurityScheme()
if (!applicationProperties.getSecurity().getEnableLogin()) { .type(SecurityScheme.Type.APIKEY)
return new OpenAPI().components(new Components()) .in(SecurityScheme.In.HEADER)
.info(new Info().title("Stirling PDF API").version(version).description( .name("X-API-KEY");
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); if (!applicationProperties.getSecurity().getEnableLogin()) {
} else { return new OpenAPI()
return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) .components(new Components())
.info(new Info().title("Stirling PDF API").version(version).description( .info(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) new Info()
.addSecurityItem(new SecurityRequirement().addList("apiKey")); .title("Stirling PDF API")
} .version(version)
.description(
} "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} else {
} return new OpenAPI()
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
.info(
new Info()
.title("Stirling PDF API")
.version(version)
.description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
}
}
}

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
@ -17,4 +16,3 @@ public class StartupApplicationListener implements ApplicationListener<ContextRe
startTime = LocalDateTime.now(); startTime = LocalDateTime.now();
} }
} }

View File

@ -9,19 +9,18 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired @Autowired private EndpointInterceptor endpointInterceptor;
private EndpointInterceptor endpointInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor); registry.addInterceptor(endpointInterceptor);
} }
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources // Handler for external static resources
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/"); .addResourceLocations("file:customFiles/static/", "classpath:/static/");
//.setCachePeriod(0); // Optional: disable caching // .setCachePeriod(0); // Optional: disable caching
} }
} }

View File

@ -8,16 +8,18 @@ import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.core.io.support.PropertySourceFactory;
public class YamlPropertySourceFactory implements PropertySourceFactory { public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override @Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
throws IOException { throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource()); factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject(); Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties); return new PropertiesPropertySource(
encodedResource.getResource().getFilename(), properties);
} }
} }

View File

@ -12,36 +12,38 @@ import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired @Autowired private final LoginAttemptService loginAttemptService;
private final LoginAttemptService loginAttemptService;
@Autowired @Autowired
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
} }
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) public void onAuthenticationFailure(
throws IOException, ServletException { HttpServletRequest request,
String ip = request.getRemoteAddr(); HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: " + ip); logger.error("Failed login attempt from IP: " + ip);
String username = request.getParameter("username"); String username = request.getParameter("username");
if(loginAttemptService.loginAttemptCheck(username)) { if (loginAttemptService.loginAttemptCheck(username)) {
setDefaultFailureUrl("/login?error=locked"); setDefaultFailureUrl("/login?error=locked");
} else { } else {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials"); setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) { } else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked"); setDefaultFailureUrl("/login?error=locked");
} }
} }
super.onAuthenticationFailure(request, response, exception); super.onAuthenticationFailure(request, response, exception);
} }
} }

View File

@ -15,30 +15,33 @@ import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired @Autowired private LoginAttemptService loginAttemptService;
private LoginAttemptService loginAttemptService;
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { public void onAuthenticationSuccess(
String username = request.getParameter("username"); HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
String username = request.getParameter("username");
loginAttemptService.loginSucceeded(username); loginAttemptService.loginSucceeded(username);
// Get the saved request // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null; SavedRequest savedRequest =
if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) { session != null
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination // Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
// Redirect to the root URL (considering context path) // Redirect to the root URL (considering context path)
getRedirectStrategy().sendRedirect(request, response, "/"); getRedirectStrategy().sendRedirect(request, response, "/");
} }
//super.onAuthenticationSuccess(request, response, authentication);
}
// super.onAuthenticationSuccess(request, response, authentication);
}
} }

View File

@ -20,33 +20,38 @@ import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
@Autowired @Autowired private UserRepository userRepository;
private UserRepository userRepository;
@Autowired private LoginAttemptService loginAttemptService;
@Autowired
private LoginAttemptService loginAttemptService;
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username) User user =
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); userRepository
.findByUsername(username)
.orElseThrow(
() ->
new UsernameNotFoundException(
"No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
throw new LockedException("Your account has been locked due to too many failed login attempts."); throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
} }
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), user.getPassword(),
user.isEnabled(), user.isEnabled(),
true, true, true, true,
getAuthorities(user.getAuthorities()) true,
); true,
getAuthorities(user.getAuthorities()));
} }
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) { private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream() return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }

View File

@ -19,16 +19,16 @@ import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class FirstLoginFilter extends OncePerRequestFilter { public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired @Autowired @Lazy private UserService userService;
@Lazy
private UserService userService;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(
String method = request.getMethod(); HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
String requestURI = request.getRequestURI(); throws ServletException, IOException {
// Check if the request is for static resources String method = request.getMethod();
String requestURI = request.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
@ -36,11 +36,14 @@ public class FirstLoginFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsername(authentication.getName()); Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { if ("GET".equalsIgnoreCase(method)
&& user.isPresent()
&& user.get().isFirstLogin()
&& !"/change-creds".equals(requestURI)) {
response.sendRedirect("/change-creds"); response.sendRedirect("/change-creds");
return; return;
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -13,51 +14,53 @@ import stirling.software.SPDF.utils.RequestUriUtils;
public class IPRateLimitingFilter implements Filter { public class IPRateLimitingFilter implements Filter {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> requestCounts =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
private final int maxRequests; private final int maxRequests;
private final int maxGetRequests; private final int maxGetRequests;
public IPRateLimitingFilter(int maxRequests, int maxGetRequests) { public IPRateLimitingFilter(int maxRequests, int maxGetRequests) {
this.maxRequests = maxRequests; this.maxRequests = maxRequests;
this.maxGetRequests = maxGetRequests; this.maxGetRequests = maxGetRequests;
} }
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
if (request instanceof HttpServletRequest) { throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request; if (request instanceof HttpServletRequest) {
String method = httpRequest.getMethod(); HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI(); String method = httpRequest.getMethod();
// Check if the request is for static resources String requestURI = httpRequest.getRequestURI();
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); // Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) { if (isStaticResource) {
chain.doFilter(request, response); chain.doFilter(request, response);
return; return;
} }
String clientIp = request.getRemoteAddr(); String clientIp = request.getRemoteAddr();
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
if (!"GET".equalsIgnoreCase(method)) { if (!"GET".equalsIgnoreCase(method)) {
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) { if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
// Handle limit exceeded (e.g., send error response) // Handle limit exceeded (e.g., send error response)
response.getWriter().write("Rate limit exceeded"); response.getWriter().write("Rate limit exceeded");
return; return;
} }
} else { } else {
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) { if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
// Handle limit exceeded (e.g., send error response) // Handle limit exceeded (e.g., send error response)
response.getWriter().write("GET Rate limit exceeded"); response.getWriter().write("GET Rate limit exceeded");
return; return;
} }
} }
} }
chain.doFilter(request, response); chain.doFilter(request, response);
} }
public void resetRequestCounts() { public void resetRequestCounts() {
requestCounts.clear(); requestCounts.clear();
getCounts.clear(); getCounts.clear();

View File

@ -13,75 +13,76 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Component @Component
public class InitialSecuritySetup { public class InitialSecuritySetup {
@Autowired @Autowired private UserService userService;
private UserService userService;
@Autowired ApplicationProperties applicationProperties;
@Autowired @PostConstruct
ApplicationProperties applicationProperties; public void init() {
if (!userService.hasUsers()) {
@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());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword =
applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
@PostConstruct private void saveKeyToConfig(String key) throws IOException {
public void initSecretKey() throws IOException { Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); List<String> lines = Files.readAllLines(path);
if (secretKey == null || secretKey.isEmpty()) { boolean keyFound = false;
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void saveKeyToConfig(String key) throws IOException { // Search for the existing key to replace it or place to add it
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml for (int i = 0; i < lines.size(); i++) {
List<String> lines = Files.readAllLines(path); if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
boolean keyFound = false; keyFound = true;
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// Search for the existing key to replace it or place to add it // If the section doesn't exist, append it
for (int i = 0; i < lines.size(); i++) { if (!keyFound) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) { lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
keyFound = true; lines.add("AutomaticallyGenerated:");
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { lines.add(" key: " + key);
lines.set(i + 1, " key: " + key); }
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// If the section doesn't exist, append it // Write back to the file
if (!keyFound) { Files.write(path, lines);
lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); }
lines.add("AutomaticallyGenerated:"); }
lines.add(" key: " + key);
}
// Write back to the file
Files.write(path, lines);
}
}

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -12,39 +13,41 @@ import stirling.software.SPDF.model.AttemptCounter;
@Service @Service
public class LoginAttemptService { public class LoginAttemptService {
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
private int MAX_ATTEMPTS; private int MAX_ATTEMPTS;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
@PostConstruct @PostConstruct
public void init() { public void init() {
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount(); MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes()); ATTEMPT_INCREMENT_TIME =
TimeUnit.MINUTES.toMillis(
applicationProperties.getSecurity().getLoginResetTimeMinutes());
} }
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
new ConcurrentHashMap<>();
public void loginSucceeded(String key) { public void loginSucceeded(String key) {
attemptsCache.remove(key); attemptsCache.remove(key);
} }
public boolean loginAttemptCheck(String key) { public boolean loginAttemptCheck(String key) {
attemptsCache.compute(key, (k, attemptCounter) -> { attemptsCache.compute(
if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { key,
return new AttemptCounter(); (k, attemptCounter) -> {
} else { if (attemptCounter == null
attemptCounter.increment(); || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
return attemptCounter; return new AttemptCounter();
} } else {
}); attemptCounter.increment();
return attemptCounter;
}
});
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS; return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
} }
public boolean isBlocked(String key) { public boolean isBlocked(String key) {
AttemptCounter attemptCounter = attemptsCache.get(key); AttemptCounter attemptCounter = attemptsCache.get(key);
if (attemptCounter != null) { if (attemptCounter != null) {
@ -52,5 +55,4 @@ public class LoginAttemptService {
} }
return false; return false;
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -11,7 +12,7 @@ public class RateLimitResetScheduler {
this.rateLimitingFilter = rateLimitingFilter; this.rateLimitingFilter = rateLimitingFilter;
} }
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable @Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
public void resetRateLimit() { public void resetRateLimit() {
rateLimitingFilter.resetRequestCounts(); rateLimitingFilter.resetRequestCounts();
} }

View File

@ -19,104 +19,108 @@ import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration @Configuration
@EnableWebSecurity() @EnableWebSecurity()
@EnableMethodSecurity @EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired @Autowired private UserDetailsService userDetailsService;
private UserDetailsService userDetailsService;
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
private UserAuthenticationFilter userAuthenticationFilter;
@Autowired @Lazy private UserService userService;
@Autowired @Autowired
private LoginAttemptService loginAttemptService; @Qualifier("loginEnabled") public boolean loginEnabledValue;
@Autowired @Autowired private UserAuthenticationFilter userAuthenticationFilter;
private FirstLoginFilter firstLoginFilter;
@Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if(loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http
.formLogin(formLogin -> formLogin
.loginPage("/login")
.successHandler(new CustomAuthenticationSuccessHandler())
.defaultSuccessUrl("/")
.failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService))
.permitAll()
).requestCache(requestCache -> requestCache
.requestCache(new NullRequestCache())
)
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me")
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(authz -> authz
.requestMatchers(req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI if (loginEnabledValue) {
String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri;
return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") || http.csrf(csrf -> csrf.disable());
trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") || http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") || http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/"); http.formLogin(
} formLogin ->
).permitAll() formLogin
.anyRequest().authenticated() .loginPage("/login")
) .successHandler(
.userDetailsService(userDetailsService) new CustomAuthenticationSuccessHandler())
.authenticationProvider(authenticationProvider()); .defaultSuccessUrl("/")
} else { .failureHandler(
http.csrf(csrf -> csrf.disable()) new CustomAuthenticationFailureHandler(
.authorizeHttpRequests(authz -> authz loginAttemptService))
.anyRequest().permitAll() .permitAll())
); .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
} .logout(
logout ->
logout.logoutRequestMatcher(
new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me"))
.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(
authz ->
authz.requestMatchers(
req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI
String trimmedUri =
uri.startsWith(contextPath)
? uri.substring(
contextPath
.length())
: uri;
return trimmedUri.startsWith("/login")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith(
"/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/js/");
})
.permitAll()
.anyRequest()
.authenticated())
.userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider());
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return http.build(); return http.build();
} }
@Bean @Bean
public IPRateLimitingFilter rateLimitingFilter() { public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
} }
@Bean @Bean
public DaoAuthenticationProvider authenticationProvider() { public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
@ -124,13 +128,9 @@ public class SecurityConfiguration {
authProvider.setPasswordEncoder(passwordEncoder()); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; return authProvider;
} }
@Bean @Bean
public PersistentTokenRepository persistentTokenRepository() { public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(); return new JPATokenRepositoryImpl();
} }
} }

View File

@ -19,32 +19,28 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired @Autowired private UserDetailsService userDetailsService;
private UserDetailsService userDetailsService;
@Autowired @Lazy private UserService userService;
@Autowired @Autowired
@Lazy @Qualifier("loginEnabled") public boolean loginEnabledValue;
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(
HttpServletResponse response, HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
FilterChain filterChain) throws ServletException, IOException { throws ServletException, IOException {
if (!loginEnabledValue) { if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication // If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists // Check for API key in the request headers if no authentication exists
@ -52,15 +48,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
try { try {
// Use API key to authenticate. This requires you to have an authentication provider for API keys. // Use API key to authenticate. This requires you to have an authentication
UserDetails userDetails = userService.loadUserByApiKey(apiKey); // provider for API keys.
if(userDetails == null) UserDetails userDetails = userService.loadUserByApiKey(apiKey);
{ if (userDetails == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;
} }
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities()); authentication =
new ApiKeyAuthenticationToken(
userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
// If API key authentication fails, deny the request // If API key authentication fails, deny the request
@ -73,36 +71,38 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// If we still don't have any authentication, deny the request // If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod(); String method = request.getMethod();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) { if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page response.sendRedirect(contextPath + "/login"); // redirect to the login page
return; return;
} else { } else {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); response.getWriter()
return; .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); filterChain.doFilter(request, response);
} }
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
String[] permitAllPatterns = { String[] permitAllPatterns = {
contextPath + "/login", contextPath + "/login",
contextPath + "/register", contextPath + "/register",
contextPath + "/error", contextPath + "/error",
contextPath + "/images/", contextPath + "/images/",
contextPath + "/public/", contextPath + "/public/",
contextPath + "/css/", contextPath + "/css/",
contextPath + "/js/", contextPath + "/js/",
contextPath + "/pdfjs/", contextPath + "/pdfjs/",
contextPath + "/site.webmanifest" contextPath + "/site.webmanifest"
}; };
for (String pattern : permitAllPatterns) { for (String pattern : permitAllPatterns) {
@ -113,5 +113,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
return false; return false;
} }
} }

View File

@ -20,28 +20,28 @@ import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill; import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Component @Component
public class UserBasedRateLimitingFilter extends OncePerRequestFilter { public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired @Autowired private UserDetailsService userDetailsService;
private UserDetailsService userDetailsService;
@Autowired @Autowired
@Qualifier("rateLimit") @Qualifier("rateLimit") public boolean rateLimit;
public boolean rateLimit;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(
HttpServletResponse response, HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
FilterChain filterChain) throws ServletException, IOException { throws ServletException, IOException {
if (!rateLimit) { if (!rateLimit) {
// If rateLimit is not enabled, just pass all requests without rate limiting // If rateLimit is not enabled, just pass all requests without rate limiting
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
@ -60,7 +60,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
// Check for API key in the request headers // Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames identifier =
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else { } else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
@ -74,14 +75,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
identifier = request.getRemoteAddr(); identifier = request.getRemoteAddr();
} }
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) { if (request.getHeader("X-API-Key") != null) {
// It's an API call // It's an API call
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain); processRequest(
userRole.getApiCallsPerDay(),
identifier,
apiBuckets,
request,
response,
filterChain);
} else { } else {
// It's a Web UI call // It's a Web UI call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); processRequest(
userRole.getWebCallsPerDay(),
identifier,
webBuckets,
request,
response,
filterChain);
} }
} }
@ -98,8 +112,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
throw new IllegalStateException("User does not have a valid role."); throw new IllegalStateException("User does not have a valid role.");
} }
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets, private void processRequest(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) int limitPerDay,
String identifier,
Map<String, Bucket> buckets,
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException { throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
@ -116,10 +135,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
} }
private Bucket createUserBucket(int limitPerDay) { private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); Bandwidth limit =
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build(); return Bucket.builder().addLimit(limit).build();
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -21,38 +22,35 @@ import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Service
public class UserService implements UserServiceInterface{
@Autowired
private UserRepository userRepository;
@Autowired @Service
private PasswordEncoder passwordEncoder; public class UserService implements UserServiceInterface {
@Autowired private UserRepository userRepository;
@Autowired private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) { public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey); User user = getUserByApiKey(apiKey);
if (user == null) { if (user == null) {
throw new UsernameNotFoundException("API key is not valid"); throw new UsernameNotFoundException("API key is not valid");
} }
// Convert the user into an Authentication object // Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken( return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user) user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here) null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions) getAuthorities(user) // user's authorities (roles/permissions)
); );
} }
private Collection<? extends GrantedAuthority> getAuthorities(User user) { private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object. // Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream() return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private String generateApiKey() { private String generateApiKey() {
String apiKey; String apiKey;
do { do {
@ -62,9 +60,11 @@ public class UserService implements UserServiceInterface{
} }
public User addApiKeyToUser(String username) { public User addApiKeyToUser(String username) {
User user = userRepository.findByUsername(username) User user =
.orElseThrow(() -> new UsernameNotFoundException("User not found")); userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey()); user.setApiKey(generateApiKey());
return userRepository.save(user); return userRepository.save(user);
} }
@ -74,8 +74,10 @@ public class UserService implements UserServiceInterface{
} }
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username) User user =
.orElseThrow(() -> new UsernameNotFoundException("User not found")); userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey(); return user.getApiKey();
} }
@ -86,27 +88,25 @@ public class UserService implements UserServiceInterface{
public User getUserByApiKey(String apiKey) { public User getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey); return userRepository.findByApiKey(apiKey);
} }
public UserDetails loadUserByApiKey(String apiKey) { public UserDetails loadUserByApiKey(String apiKey) {
User userOptional = userRepository.findByApiKey(apiKey); User userOptional = userRepository.findByApiKey(apiKey);
if (userOptional != null) { if (userOptional != null) {
User user = userOptional; User user = userOptional;
// Convert your User entity to a UserDetails object with authorities // Convert your User entity to a UserDetails object with authorities
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), // you might not need this for API key auth user.getPassword(), // you might not need this for API key auth
getAuthorities(user) getAuthorities(user));
);
} }
return null; // or throw an exception return null; // or throw an exception
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
} }
public void saveUser(String username, String password) { public void saveUser(String username, String password) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
@ -124,7 +124,7 @@ public class UserService implements UserServiceInterface{
user.setFirstLogin(firstLogin); user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
} }
public void saveUser(String username, String password, String role) { public void saveUser(String username, String password, String role) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
@ -134,42 +134,42 @@ public class UserService implements UserServiceInterface{
user.setFirstLogin(false); user.setFirstLogin(false);
userRepository.save(user); userRepository.save(user);
} }
public void deleteUser(String username) { public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) { for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return; return;
} }
} }
userRepository.delete(userOpt.get()); userRepository.delete(userOpt.get());
} }
} }
public boolean usernameExists(String username) { public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent(); return userRepository.findByUsername(username).isPresent();
} }
public boolean hasUsers() { public boolean hasUsers() {
return userRepository.count() > 0; return userRepository.count() > 0;
} }
public void updateUserSettings(String username, Map<String, String> updates) { public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) { if (settingsMap == null) {
settingsMap = new HashMap<String,String>(); settingsMap = new HashMap<String, String>();
} }
settingsMap.clear(); settingsMap.clear();
settingsMap.putAll(updates); settingsMap.putAll(updates);
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
} }
} }
public Optional<User> findByUsername(String username) { public Optional<User> findByUsername(String username) {
@ -185,13 +185,12 @@ public class UserService implements UserServiceInterface{
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user); userRepository.save(user);
} }
public void changeFirstUse(User user, boolean firstUse) { public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse); user.setFirstLogin(firstUse);
userRepository.save(user); userRepository.save(user);
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword()); return passwordEncoder.matches(currentPassword, user.getPassword());
} }

View File

@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -28,59 +29,62 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class CropController { public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data") @PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) summary = "Crops a PDF document",
throws IOException { 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(@ModelAttribute CropPdfForm form) throws IOException {
PDDocument sourceDocument =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
PDDocument newDocument = new PDDocument();
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); int totalPages = sourceDocument.getNumberOfPages();
PDDocument newDocument = new PDDocument(); LayerUtility layerUtility = new LayerUtility(newDocument);
int totalPages = sourceDocument.getNumberOfPages(); for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
LayerUtility layerUtility = new LayerUtility(newDocument); // 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);
for (int i = 0; i < totalPages; i++) { // Import the source page as a form XObject
PDPage sourcePage = sourceDocument.getPage(i); PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, 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 contentStream.saveGraphicsState();
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.saveGraphicsState(); // Define the crop area
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
// Define the crop area contentStream.clip();
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
contentStream.clip();
// Draw the entire formXObject // Draw the entire formXObject
contentStream.drawForm(formXObject); contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState(); contentStream.restoreGraphicsState();
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
}
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(
new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_cropped.pdf");
}
} }

View File

@ -1,7 +1,15 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import io.swagger.v3.oas.annotations.Operation; import java.io.ByteArrayInputStream;
import io.swagger.v3.oas.annotations.tags.Tag; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@ -14,19 +22,13 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@ -34,7 +36,6 @@ public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class); private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException { private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = new PDDocument(); PDDocument mergedDoc = new PDDocument();
for (PDDocument doc : documents) { for (PDDocument doc : documents) {
@ -52,27 +53,39 @@ public class MergeController {
case "byDateModified": case "byDateModified":
return (file1, file2) -> { return (file1, file2) -> {
try { try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); BasicFileAttributes attr1 =
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); Files.readAttributes(
Paths.get(file1.getOriginalFilename()),
BasicFileAttributes.class);
BasicFileAttributes attr2 =
Files.readAttributes(
Paths.get(file2.getOriginalFilename()),
BasicFileAttributes.class);
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
} catch (IOException e) { } catch (IOException e) {
return 0; // If there's an error, treat them as equal return 0; // If there's an error, treat them as equal
} }
}; };
case "byDateCreated": case "byDateCreated":
return (file1, file2) -> { return (file1, file2) -> {
try { try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); BasicFileAttributes attr1 =
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); Files.readAttributes(
Paths.get(file1.getOriginalFilename()),
BasicFileAttributes.class);
BasicFileAttributes attr2 =
Files.readAttributes(
Paths.get(file2.getOriginalFilename()),
BasicFileAttributes.class);
return attr1.creationTime().compareTo(attr2.creationTime()); return attr1.creationTime().compareTo(attr2.creationTime());
} catch (IOException e) { } catch (IOException e) {
return 0; // If there's an error, treat them as equal return 0; // If there's an error, treat them as equal
} }
}; };
case "byPDFTitle": case "byPDFTitle":
return (file1, file2) -> { return (file1, file2) -> {
try (PDDocument doc1 = PDDocument.load(file1.getInputStream()); try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
PDDocument doc2 = PDDocument.load(file2.getInputStream())) { PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
String title1 = doc1.getDocumentInformation().getTitle(); String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2); return title1.compareTo(title2);
@ -82,14 +95,17 @@ public class MergeController {
}; };
case "orderProvided": case "orderProvided":
default: default:
return (file1, file2) -> 0; // Default is the order provided return (file1, file2) -> 0; // Default is the order provided
} }
} }
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(summary = "Merge multiple PDF files into one", @Operation(
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") summary = "Merge multiple PDF files into one",
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form) throws IOException { description =
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException {
try { try {
MultipartFile[] files = form.getFileInput(); MultipartFile[] files = form.getFileInput();
Arrays.sort(files, getSortComparator(form.getSortType())); Arrays.sort(files, getSortComparator(form.getSortType()));
@ -101,14 +117,16 @@ public class MergeController {
mergedDoc.addSource(new ByteArrayInputStream(file.getBytes())); mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
} }
mergedDoc.setDestinationFileName(files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); mergedDoc.setDestinationFileName(
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
mergedDoc.setDestinationStream(docOutputstream); mergedDoc.setDestinationStream(docOutputstream);
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
return WebResponseUtils.bytesToWebResponse(docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); return WebResponseUtils.bytesToWebResponse(
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
} catch (Exception ex) { } catch (Exception ex) {
logger.error("Error in merge pdf process", ex); logger.error("Error in merge pdf process", ex);
throw ex; throw ex;
} }
} }
} }

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.awt.Color; import java.awt.Color;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -23,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -31,94 +31,110 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController { public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Merge multiple pages of a PDF document into a single page", 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" 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(@ModelAttribute MergeMultiplePagesRequest request) public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
throws IOException { @ModelAttribute MergeMultiplePagesRequest request) throws IOException {
int pagesPerSheet = request.getPagesPerSheet(); int pagesPerSheet = request.getPagesPerSheet();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean addBorder = request.isAddBorder(); boolean addBorder = request.isAddBorder();
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); if (pagesPerSheet != 2
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); && pagesPerSheet != 3
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
PDDocument sourceDocument = PDDocument.load(file.getInputStream()); int cols =
PDDocument newDocument = new PDDocument(); pagesPerSheet == 2 || pagesPerSheet == 3
PDPage newPage = new PDPage(PDRectangle.A4); ? pagesPerSheet
newDocument.addPage(newPage); : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
int totalPages = sourceDocument.getNumberOfPages(); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
float cellWidth = newPage.getMediaBox().getWidth() / cols; PDDocument newDocument = new PDDocument();
float cellHeight = newPage.getMediaBox().getHeight() / rows; PDPage newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); int totalPages = sourceDocument.getNumberOfPages();
LayerUtility layerUtility = new LayerUtility(newDocument); float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows;
float borderThickness = 1.5f; // Specify border thickness as required PDPageContentStream contentStream =
contentStream.setLineWidth(borderThickness); new PDPageContentStream(
contentStream.setStrokingColor(Color.BLACK); newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 0; i < totalPages; i++) {
if (i != 0 && i % pagesPerSheet == 0) {
// Close the current content stream and create a new page and content stream
contentStream.close();
newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
}
PDPage sourcePage = sourceDocument.getPage(i); float borderThickness = 1.5f; // Specify border thickness as required
PDRectangle rect = sourcePage.getMediaBox(); contentStream.setLineWidth(borderThickness);
float scaleWidth = cellWidth / rect.getWidth(); contentStream.setStrokingColor(Color.BLACK);
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page for (int i = 0; i < totalPages; i++) {
int rowIndex = adjustedPageIndex / cols; if (i != 0 && i % pagesPerSheet == 0) {
int colIndex = adjustedPageIndex % cols; // Close the current content stream and create a new page and content stream
contentStream.close();
newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
contentStream =
new PDPageContentStream(
newDocument,
newPage,
PDPageContentStream.AppendMode.APPEND,
true,
true);
}
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; PDPage sourcePage = sourceDocument.getPage(i);
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); PDRectangle rect = sourcePage.getMediaBox();
float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
contentStream.saveGraphicsState(); int adjustedPageIndex =
contentStream.transform(Matrix.getTranslateInstance(x, y)); i % pagesPerSheet; // This will reset the index for every new page
contentStream.transform(Matrix.getScaleInstance(scale, scale)); int rowIndex = adjustedPageIndex / cols;
int colIndex = adjustedPageIndex % cols;
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
contentStream.drawForm(formXObject); float y =
newPage.getMediaBox().getHeight()
- ((rowIndex + 1) * cellHeight
- (cellHeight - rect.getHeight() * scale) / 2);
contentStream.restoreGraphicsState(); contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
if(addBorder) { contentStream.transform(Matrix.getScaleInstance(scale, scale));
// Draw border around each page
float borderX = colIndex * cellWidth;
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
contentStream.stroke();
}
}
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(formXObject);
contentStream.close(); // Close the final content stream contentStream.restoreGraphicsState();
sourceDocument.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (addBorder) {
newDocument.save(baos); // Draw border around each page
newDocument.close(); float borderX = colIndex * cellWidth;
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
contentStream.stroke();
}
}
byte[] result = baos.toByteArray(); contentStream.close(); // Close the final content stream
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); sourceDocument.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
}
} }

View File

@ -1,11 +1,13 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ArrayList;
import org.apache.pdfbox.multipdf.Overlay; import org.apache.pdfbox.multipdf.Overlay;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -18,36 +20,49 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class PdfOverlayController { public class PdfOverlayController {
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
@Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") @Operation(
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { summary = "Overlay PDF files in various modes",
description =
"Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
throws IOException {
MultipartFile baseFile = request.getFileInput(); MultipartFile baseFile = request.getFileInput();
int overlayPos = request.getOverlayPosition(); int overlayPos = request.getOverlayPosition();
MultipartFile[] overlayFiles = request.getOverlayFiles(); MultipartFile[] overlayFiles = request.getOverlayFiles();
File[] overlayPdfFiles = new File[overlayFiles.length]; File[] overlayPdfFiles = new File[overlayFiles.length];
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
try { try {
for (int i = 0; i < overlayFiles.length; i++) { for (int i = 0; i < overlayFiles.length; i++) {
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
} }
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay" String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay",
// "FixedRepeatOverlay"
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
Overlay overlay = new Overlay()) { Overlay overlay = new Overlay()) {
Map<Integer, String> overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles); Map<Integer, String> overlayGuide =
prepareOverlayGuide(
basePdf.getNumberOfPages(),
overlayPdfFiles,
mode,
counts,
tempFiles);
overlay.setInputPDF(basePdf); overlay.setInputPDF(basePdf);
if (overlayPos == 0) { if (overlayPos == 0) {
overlay.setOverlayPosition(Overlay.Position.FOREGROUND); overlay.setOverlayPosition(Overlay.Position.FOREGROUND);
@ -58,10 +73,13 @@ public class PdfOverlayController {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
overlay.overlay(overlayGuide).save(outputStream); overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray(); byte[] data = outputStream.toByteArray();
String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf String outputFilename =
baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); + "_overlayed.pdf"; // Remove file extension and append .pdf
}
return WebResponseUtils.bytesToWebResponse(
data, outputFilename, MediaType.APPLICATION_PDF);
}
} finally { } finally {
for (File overlayPdfFile : overlayPdfFiles) { for (File overlayPdfFile : overlayPdfFiles) {
if (overlayPdfFile != null) { if (overlayPdfFile != null) {
@ -76,7 +94,9 @@ public class PdfOverlayController {
} }
} }
private Map<Integer, String> prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles) throws IOException { private Map<Integer, String> prepareOverlayGuide(
int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles)
throws IOException {
Map<Integer, String> overlayGuide = new HashMap<>(); Map<Integer, String> overlayGuide = new HashMap<>();
switch (mode) { switch (mode) {
case "SequentialOverlay": case "SequentialOverlay":
@ -94,12 +114,19 @@ public class PdfOverlayController {
return overlayGuide; return overlayGuide;
} }
private void sequentialOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount, List<File> tempFiles) throws IOException { private void sequentialOverlay(
Map<Integer, String> overlayGuide,
File[] overlayFiles,
int basePageCount,
List<File> tempFiles)
throws IOException {
int overlayFileIndex = 0; int overlayFileIndex = 0;
int pageCountInCurrentOverlay = 0; int pageCountInCurrentOverlay = 0;
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) { if (pageCountInCurrentOverlay == 0
|| pageCountInCurrentOverlay
>= getNumberOfPages(overlayFiles[overlayFileIndex])) {
pageCountInCurrentOverlay = 0; pageCountInCurrentOverlay = 0;
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length; overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
} }
@ -125,13 +152,9 @@ public class PdfOverlayController {
} }
} }
private void interleavedOverlay(
Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount)
throws IOException {
private void interleavedOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException {
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length]; File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
@ -145,10 +168,12 @@ public class PdfOverlayController {
} }
} }
private void fixedRepeatOverlay(
private void fixedRepeatOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException { Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount)
throws IOException {
if (overlayFiles.length != counts.length) { if (overlayFiles.length != counts.length) {
throw new IllegalArgumentException("Counts array length must match the number of overlay files"); throw new IllegalArgumentException(
"Counts array length must match the number of overlay files");
} }
int currentPage = 1; int currentPage = 1;
for (int i = 0; i < overlayFiles.length; i++) { for (int i = 0; i < overlayFiles.length; i++) {
@ -167,7 +192,7 @@ public class PdfOverlayController {
} }
} }
} }
} }
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere. // Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined
// elsewhere.

View File

@ -17,200 +17,204 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages") @PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
@Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request ) summary = "Remove pages from a PDF file",
throws IOException { description =
"This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
String pagesToDelete = request.getPageNumbers(); String pagesToDelete = request.getPageNumbers();
PDDocument document = PDDocument.load(pdfFile.getBytes());
// Split the page order string into an array of page numbers or range of numbers PDDocument document = PDDocument.load(pdfFile.getBytes());
String[] pageOrderArr = pagesToDelete.split(",");
List<Integer> pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); // Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pagesToDelete.split(",");
for (int i = pagesToRemove.size() - 1; i >= 0; i--) { List<Integer> pagesToRemove =
int pageIndex = pagesToRemove.get(i); GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
document.removePage(pageIndex);
}
return WebResponseUtils.pdfDocToWebResponse(document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
} for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
int pageIndex = pagesToRemove.get(i);
document.removePage(pageIndex);
}
private List<Integer> removeFirst(int totalPages) { return WebResponseUtils.pdfDocToWebResponse(
if (totalPages <= 1) document,
return new ArrayList<>(); pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 2; i <= totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> removeLast(int totalPages) {
if (totalPages <= 1)
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i < totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> removeFirstAndLast(int totalPages) {
if (totalPages <= 2)
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 2; i < totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> reverseOrder(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = totalPages; i >= 1; i--) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> duplexSort(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
for (int i = 1; i <= half; i++) {
newPageOrder.add(i - 1);
if (i <= totalPages - half) { // Avoid going out of bounds
newPageOrder.add(totalPages - i);
}
}
return newPageOrder;
}
private List<Integer> bookletSort(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 0; i < totalPages / 2; i++) {
newPageOrder.add(i);
newPageOrder.add(totalPages - i - 1);
}
return newPageOrder;
}
private List<Integer> sideStitchBooklet(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 0; i < (totalPages + 3) / 4; i++) {
int begin = i * 4;
newPageOrder.add(Math.min(begin + 3, totalPages - 1));
newPageOrder.add(Math.min(begin, totalPages - 1));
newPageOrder.add(Math.min(begin + 1, totalPages - 1));
newPageOrder.add(Math.min(begin + 2, totalPages - 1));
}
return newPageOrder;
} }
private List<Integer> oddEvenSplit(int totalPages) { private List<Integer> removeFirst(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>(); if (totalPages <= 1) return new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) { List<Integer> newPageOrder = new ArrayList<>();
newPageOrder.add(i - 1); for (int i = 2; i <= totalPages; i++) {
} newPageOrder.add(i - 1);
for (int i = 2; i <= totalPages; i += 2) { }
newPageOrder.add(i - 1); return newPageOrder;
} }
return newPageOrder;
}
private List<Integer> processSortTypes(String sortTypes, int totalPages) { private List<Integer> removeLast(int totalPages) {
try { if (totalPages <= 1) return new ArrayList<>();
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); List<Integer> newPageOrder = new ArrayList<>();
switch (mode) { for (int i = 1; i < totalPages; i++) {
case REVERSE_ORDER: newPageOrder.add(i - 1);
return reverseOrder(totalPages); }
case DUPLEX_SORT: return newPageOrder;
return duplexSort(totalPages); }
case BOOKLET_SORT:
return bookletSort(totalPages);
case SIDE_STITCH_BOOKLET_SORT:
return sideStitchBooklet(totalPages);
case ODD_EVEN_SPLIT:
return oddEvenSplit(totalPages);
case REMOVE_FIRST:
return removeFirst(totalPages);
case REMOVE_LAST:
return removeLast(totalPages);
case REMOVE_FIRST_AND_LAST:
return removeFirstAndLast(totalPages);
default:
throw new IllegalArgumentException("Unsupported custom mode");
}
} catch (IllegalArgumentException e) {
logger.error("Unsupported custom mode", e);
return null;
}
}
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") private List<Integer> removeFirstAndLast(int totalPages) {
@Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF") if (totalPages <= 2) return new ArrayList<>();
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException { List<Integer> newPageOrder = new ArrayList<>();
MultipartFile pdfFile = request.getFileInput(); for (int i = 2; i < totalPages; i++) {
String pageOrder = request.getPageNumbers(); newPageOrder.add(i - 1);
String sortType = request.getCustomMode(); }
try { return newPageOrder;
// Load the input PDF }
PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Split the page order string into an array of page numbers or range of numbers private List<Integer> reverseOrder(int totalPages) {
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; List<Integer> newPageOrder = new ArrayList<>();
int totalPages = document.getNumberOfPages(); for (int i = totalPages; i >= 1; i--) {
List<Integer> newPageOrder; newPageOrder.add(i - 1);
if (sortType != null && sortType.length() > 0) { }
newPageOrder = processSortTypes(sortType, totalPages); return newPageOrder;
} else { }
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
}
logger.info("newPageOrder = " +newPageOrder);
logger.info("totalPages = " +totalPages);
// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {
newPages.add(document.getPage(newPageOrder.get(i)));
}
// Remove all the pages from the original document private List<Integer> duplexSort(int totalPages) {
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { List<Integer> newPageOrder = new ArrayList<>();
document.removePage(i); int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
} for (int i = 1; i <= half; i++) {
newPageOrder.add(i - 1);
if (i <= totalPages - half) { // Avoid going out of bounds
newPageOrder.add(totalPages - i);
}
}
return newPageOrder;
}
// Add the pages in the new order private List<Integer> bookletSort(int totalPages) {
for (PDPage page : newPages) { List<Integer> newPageOrder = new ArrayList<>();
document.addPage(page); for (int i = 0; i < totalPages / 2; i++) {
} newPageOrder.add(i);
newPageOrder.add(totalPages - i - 1);
}
return newPageOrder;
}
return WebResponseUtils.pdfDocToWebResponse(document, private List<Integer> sideStitchBooklet(int totalPages) {
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); List<Integer> newPageOrder = new ArrayList<>();
} catch (IOException e) { for (int i = 0; i < (totalPages + 3) / 4; i++) {
logger.error("Failed rearranging documents", e); int begin = i * 4;
return null; newPageOrder.add(Math.min(begin + 3, totalPages - 1));
} newPageOrder.add(Math.min(begin, totalPages - 1));
} newPageOrder.add(Math.min(begin + 1, totalPages - 1));
newPageOrder.add(Math.min(begin + 2, totalPages - 1));
}
return newPageOrder;
}
private List<Integer> oddEvenSplit(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) {
newPageOrder.add(i - 1);
}
for (int i = 2; i <= totalPages; i += 2) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
try {
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
switch (mode) {
case REVERSE_ORDER:
return reverseOrder(totalPages);
case DUPLEX_SORT:
return duplexSort(totalPages);
case BOOKLET_SORT:
return bookletSort(totalPages);
case SIDE_STITCH_BOOKLET_SORT:
return sideStitchBooklet(totalPages);
case ODD_EVEN_SPLIT:
return oddEvenSplit(totalPages);
case REMOVE_FIRST:
return removeFirst(totalPages);
case REMOVE_LAST:
return removeLast(totalPages);
case REMOVE_FIRST_AND_LAST:
return removeFirstAndLast(totalPages);
default:
throw new IllegalArgumentException("Unsupported custom mode");
}
} catch (IllegalArgumentException e) {
logger.error("Unsupported custom mode", e);
return null;
}
}
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
@Operation(
summary = "Rearrange pages in a PDF file",
description =
"This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF")
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
throws IOException {
MultipartFile pdfFile = request.getFileInput();
String pageOrder = request.getPageNumbers();
String sortType = request.getCustomMode();
try {
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
int totalPages = document.getNumberOfPages();
List<Integer> newPageOrder;
if (sortType != null && sortType.length() > 0) {
newPageOrder = processSortTypes(sortType, totalPages);
} else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
}
logger.info("newPageOrder = " + newPageOrder);
logger.info("totalPages = " + totalPages);
// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {
newPages.add(document.getPage(newPageOrder.get(i)));
}
// Remove all the pages from the original document
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
document.removePage(i);
}
// Add the pages in the new order
for (PDPage page : newPages) {
document.addPage(page);
}
return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_rearranged.pdf");
} catch (IOException e) {
logger.error("Failed rearranging documents", e);
return null;
}
}
} }

View File

@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -28,11 +29,11 @@ public class RotationController {
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
@Operation( @Operation(
summary = "Rotate a PDF file", summary = "Rotate a PDF file",
description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> rotatePDF( public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request)
@ModelAttribute RotatePDFRequest request) throws IOException { throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle(); Integer angle = request.getAngle();
// Load the PDF document // Load the PDF document
@ -45,8 +46,8 @@ public class RotationController {
page.setRotation(page.getRotation() + angle); page.setRotation(page.getRotation() + angle);
} }
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
} }
} }

View File

@ -23,88 +23,90 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class ScalePagesController { public class ScalePagesController {
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data") @PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request) throws IOException { summary = "Change the size of a PDF page/document",
MultipartFile file = request.getFileInput(); description =
String targetPDRectangle = request.getPageSize(); "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
float scaleFactor = request.getScaleFactor(); public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request)
throws IOException {
MultipartFile file = request.getFileInput();
String targetPDRectangle = request.getPageSize();
float scaleFactor = request.getScaleFactor();
Map<String, PDRectangle> sizeMap = new HashMap<>(); Map<String, PDRectangle> sizeMap = new HashMap<>();
// Add A0 - A10 // Add A0 - A10
sizeMap.put("A0", PDRectangle.A0); sizeMap.put("A0", PDRectangle.A0);
sizeMap.put("A1", PDRectangle.A1); sizeMap.put("A1", PDRectangle.A1);
sizeMap.put("A2", PDRectangle.A2); sizeMap.put("A2", PDRectangle.A2);
sizeMap.put("A3", PDRectangle.A3); sizeMap.put("A3", PDRectangle.A3);
sizeMap.put("A4", PDRectangle.A4); sizeMap.put("A4", PDRectangle.A4);
sizeMap.put("A5", PDRectangle.A5); sizeMap.put("A5", PDRectangle.A5);
sizeMap.put("A6", PDRectangle.A6); sizeMap.put("A6", PDRectangle.A6);
// Add other sizes // Add other sizes
sizeMap.put("LETTER", PDRectangle.LETTER); sizeMap.put("LETTER", PDRectangle.LETTER);
sizeMap.put("LEGAL", PDRectangle.LEGAL); sizeMap.put("LEGAL", PDRectangle.LEGAL);
if (!sizeMap.containsKey(targetPDRectangle)) { if (!sizeMap.containsKey(targetPDRectangle)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid PDRectangle. 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");
} }
PDRectangle targetSize = sizeMap.get(targetPDRectangle); PDRectangle targetSize = sizeMap.get(targetPDRectangle);
PDDocument sourceDocument = PDDocument.load(file.getBytes()); PDDocument sourceDocument = PDDocument.load(file.getBytes());
PDDocument outputDocument = new PDDocument(); PDDocument outputDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages(); int totalPages = sourceDocument.getNumberOfPages();
for (int i = 0; i < totalPages; i++) { for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i); PDPage sourcePage = sourceDocument.getPage(i);
PDRectangle sourceSize = sourcePage.getMediaBox(); PDRectangle sourceSize = sourcePage.getMediaBox();
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
PDPage newPage = new PDPage(targetSize);
outputDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
LayerUtility layerUtility = new LayerUtility(outputDocument);
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(form);
contentStream.restoreGraphicsState(); float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
contentStream.close(); 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));
ByteArrayOutputStream baos = new ByteArrayOutputStream(); LayerUtility layerUtility = new LayerUtility(outputDocument);
outputDocument.save(baos); PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
outputDocument.close(); contentStream.drawForm(form);
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
}
contentStream.restoreGraphicsState();
contentStream.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outputDocument.save(baos);
outputDocument.close();
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
}
} }

View File

@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -36,19 +37,24 @@ public class SplitPDFController {
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/split-pages") @PostMapping(consumes = "multipart/form-data", value = "/split-pages")
@Operation(summary = "Split a PDF file into separate documents", @Operation(
description = "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") summary = "Split a PDF file into separate documents",
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException { description =
MultipartFile file = request.getFileInput(); "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException {
MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers(); String pages = request.getPageNumbers();
// open the pdf document // open the pdf document
InputStream inputStream = file.getInputStream(); InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream); PDDocument document = PDDocument.load(inputStream);
List<Integer> pageNumbers = request.getPageNumbersList(document); List<Integer> pageNumbers = request.getPageNumbersList(document);
if(!pageNumbers.contains(document.getNumberOfPages() - 1)) if (!pageNumbers.contains(document.getNumberOfPages() - 1))
pageNumbers.add(document.getNumberOfPages()- 1); pageNumbers.add(document.getNumberOfPages() - 1);
logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); logger.info(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document // split the document
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
@ -72,7 +78,6 @@ public class SplitPDFController {
} }
} }
// closing the original document // closing the original document
document.close(); document.close();
@ -104,8 +109,7 @@ public class SplitPDFController {
Files.delete(zipFile); Files.delete(zipFile);
// return the Resource in the response // return the Resource in the response
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
}
}

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -25,17 +26,22 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") @Operation(
@Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO") summary = "Split PDF pages into smaller sections",
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { description =
"Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
@ -59,8 +65,6 @@ public class SplitPdfBySectionsController {
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data; byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
int pageNum = 1; int pageNum = 1;
for (int i = 0; i < splitDocumentsBoas.size(); i++) { for (int i = 0; i < splitDocumentsBoas.size(); i++) {
@ -82,10 +86,13 @@ public class SplitPdfBySectionsController {
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
public List<PDDocument> splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException { public List<PDDocument> splitPdfPages(
PDDocument document, int horizontalDivisions, int verticalDivisions)
throws IOException {
List<PDDocument> splitDocuments = new ArrayList<>(); List<PDDocument> splitDocuments = new ArrayList<>();
for (PDPage originalPage : document.getPages()) { for (PDPage originalPage : document.getPages()) {
@ -103,9 +110,12 @@ public class SplitPdfBySectionsController {
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight)); PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
subDoc.addPage(subPage); subDoc.addPage(subPage);
PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage)); PDFormXObject form =
layerUtility.importPageAsForm(
document, document.getPages().indexOf(originalPage));
try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) { try (PDPageContentStream contentStream =
new PDPageContentStream(subDoc, subPage)) {
// Set clipping area and position // Set clipping area and position
float translateX = -subPageWidth * i; float translateX = -subPageWidth * i;
float translateY = height - subPageHeight * (verticalDivisions - j); float translateY = height - subPageHeight * (verticalDivisions - j);
@ -127,9 +137,4 @@ public class SplitPdfBySectionsController {
return splitDocuments; return splitDocuments;
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -20,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -29,22 +31,23 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") @PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" @Operation(
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO") summary = "Auto split PDF pages into separate documents based on size or count",
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { description =
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>(); "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
PDDocument sourceDocument = PDDocument.load(file.getInputStream()); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
//0 = size, 1 = page count, 2 = doc count // 0 = size, 1 = page count, 2 = doc count
int type = request.getSplitType(); int type = request.getSplitType();
String value = request.getSplitValue(); String value = request.getSplitValue();
if (type == 0) { // Split by size if (type == 0) { // Split by size
long maxBytes = GeneralUtils.convertSizeToBytes(value); long maxBytes = GeneralUtils.convertSizeToBytes(value);
long currentSize = 0; long currentSize = 0;
@ -93,7 +96,7 @@ public class SplitPdfBySizeController {
splitDocumentsBoas.add(currentDocToByteArray(currentDoc)); splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
} }
} else if (type == 2) { // Split by doc count } else if (type == 2) { // Split by doc count
int documentCount = Integer.parseInt(value); int documentCount = Integer.parseInt(value);
int totalPageCount = sourceDocument.getNumberOfPages(); int totalPageCount = sourceDocument.getNumberOfPages();
int pagesPerDocument = totalPageCount / documentCount; int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount; int extraPages = totalPageCount % documentCount;
@ -114,9 +117,7 @@ public class SplitPdfBySizeController {
} }
sourceDocument.close(); sourceDocument.close();
Path zipFile = Files.createTempFile("split_documents", ".zip"); Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data; byte[] data;
@ -135,19 +136,18 @@ public class SplitPdfBySizeController {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
data = Files.readAllBytes(zipFile); data = Files.readAllBytes(zipFile);
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException { private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos); document.save(baos);
document.close(); document.close();
return baos; return baos;
} }
} }

View File

@ -20,8 +20,10 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@ -29,58 +31,61 @@ public class ToSinglePageController {
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class); private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
@Operation( @Operation(
summary = "Convert a multi-page PDF into a single long page PDF", summary = "Convert a multi-page PDF into a single long page PDF",
description = "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
throws IOException {
// Load the source document // Load the source document
PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream()); PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream());
// Calculate total height and max width // Calculate total height and max width
float totalHeight = 0; float totalHeight = 0;
float maxWidth = 0; float maxWidth = 0;
for (PDPage page : sourceDocument.getPages()) { for (PDPage page : sourceDocument.getPages()) {
PDRectangle pageSize = page.getMediaBox(); PDRectangle pageSize = page.getMediaBox();
totalHeight += pageSize.getHeight(); totalHeight += pageSize.getHeight();
maxWidth = Math.max(maxWidth, pageSize.getWidth()); maxWidth = Math.max(maxWidth, pageSize.getWidth());
} }
// Create new document and page with calculated dimensions // Create new document and page with calculated dimensions
PDDocument newDocument = new PDDocument(); PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
newDocument.addPage(newPage); newDocument.addPage(newPage);
// Initialize the content stream of the new page // Initialize the content stream of the new page
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
contentStream.close(); contentStream.close();
LayerUtility layerUtility = new LayerUtility(newDocument);
float yOffset = totalHeight;
// For each page, copy its content to the new page at the correct offset LayerUtility layerUtility = new LayerUtility(newDocument);
for (PDPage page : sourceDocument.getPages()) { float yOffset = totalHeight;
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page));
AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight());
layerUtility.wrapInSaveRestore(newPage);
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
yOffset -= page.getMediaBox().getHeight();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // For each page, copy its content to the new page at the correct offset
newDocument.save(baos); for (PDPage page : sourceDocument.getPages()) {
newDocument.close(); PDFormXObject form =
sourceDocument.close(); 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();
}
byte[] result = baos.toByteArray(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_singlePage.pdf");
} }
} }

View File

@ -29,14 +29,14 @@ import stirling.software.SPDF.model.User;
@Controller @Controller
@RequestMapping("/api/v1/user") @RequestMapping("/api/v1/user")
public class UserController { public class UserController {
@Autowired @Autowired private UserService userService;
private UserService userService;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) { public String register(
if(userService.usernameExists(username)) { @RequestParam String username, @RequestParam String password, Model model) {
if (userService.usernameExists(username)) {
model.addAttribute("error", "Username already exists"); model.addAttribute("error", "Username already exists");
return "register"; return "register";
} }
@ -44,39 +44,41 @@ public class UserController {
userService.saveUser(username, password); userService.saveUser(username, password);
return "redirect:/login?registered=true"; return "redirect:/login?registered=true";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username-and-password") @PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal, public RedirectView changeUsernameAndPassword(
@RequestParam String currentPassword, Principal principal,
@RequestParam String newUsername, @RequestParam String currentPassword,
@RequestParam String newPassword, @RequestParam String newUsername,
HttpServletRequest request, @RequestParam String newPassword,
HttpServletResponse response, HttpServletRequest request,
RedirectAttributes redirectAttributes) { HttpServletResponse response,
if (principal == null) { RedirectAttributes redirectAttributes) {
return new RedirectView("/change-creds?messageType=notAuthenticated"); if (principal == null) {
} return new RedirectView("/change-creds?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound"); return new RedirectView("/change-creds?messageType=userNotFound");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword"); return new RedirectView("/change-creds?messageType=incorrectPassword");
} }
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/change-creds?messageType=usernameExists");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/change-creds?messageType=usernameExists");
}
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) { if (newUsername != null
&& newUsername.length() > 0
&& !user.getUsername().equals(newUsername)) {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} }
userService.changeFirstUse(user, false); userService.changeFirstUse(user, false);
@ -87,36 +89,36 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username") @PostMapping("/change-username")
public RedirectView changeUsername(Principal principal, public RedirectView changeUsername(
@RequestParam String currentPassword, Principal principal,
@RequestParam String newUsername, @RequestParam String currentPassword,
HttpServletRequest request, @RequestParam String newUsername,
HttpServletResponse response, HttpServletRequest request,
RedirectAttributes redirectAttributes) { HttpServletResponse response,
if (principal == null) { RedirectAttributes redirectAttributes) {
return new RedirectView("/account?messageType=notAuthenticated"); if (principal == null) {
} return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound"); return new RedirectView("/account?messageType=userNotFound");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword"); return new RedirectView("/account?messageType=incorrectPassword");
} }
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/account?messageType=usernameExists"); return new RedirectView("/account?messageType=usernameExists");
} }
if(newUsername != null && newUsername.length() > 0) { if (newUsername != null && newUsername.length() > 0) {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} }
@ -125,30 +127,31 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-password") @PostMapping("/change-password")
public RedirectView changePassword(Principal principal, public RedirectView changePassword(
@RequestParam String currentPassword, Principal principal,
@RequestParam String newPassword, @RequestParam String currentPassword,
HttpServletRequest request, @RequestParam String newPassword,
HttpServletResponse response, HttpServletRequest request,
RedirectAttributes redirectAttributes) { HttpServletResponse response,
if (principal == null) { RedirectAttributes redirectAttributes) {
return new RedirectView("/account?messageType=notAuthenticated"); if (principal == null) {
} return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound"); return new RedirectView("/account?messageType=userNotFound");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword"); return new RedirectView("/account?messageType=incorrectPassword");
} }
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
@ -160,33 +163,37 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings") @PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) { public String updateUserSettings(HttpServletRequest request, Principal principal) {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap); System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]); updates.put(entry.getKey(), entry.getValue()[0]);
} }
System.out.println("Processed updates: " + updates); System.out.println("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user // Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates); userService.updateUserSettings(principal.getName(), updates);
return "redirect:/account"; // Redirect to a page of your choice after updating return "redirect:/account"; // Redirect to a page of your choice after updating
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser") @PostMapping("/admin/saveUser")
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role, public RedirectView saveUser(
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) { @RequestParam String username,
@RequestParam String password,
if(userService.usernameExists(username)) { @RequestParam String role,
return new RedirectView("/addUsers?messageType=usernameExists"); @RequestParam(name = "forceChange", required = false, defaultValue = "false")
} boolean forceChange) {
try {
if (userService.usernameExists(username)) {
return new RedirectView("/addUsers?messageType=usernameExists");
}
try {
// Validate the role // Validate the role
Role roleEnum = Role.fromString(role); Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) { if (roleEnum == Role.INTERNAL_API_USER) {
@ -197,28 +204,27 @@ public class UserController {
// If the role ID is not valid, redirect with an error message // If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole"); return new RedirectView("/addUsers?messageType=invalidRole");
} }
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, role, forceChange);
return new RedirectView("/addUsers"); // Redirect to account page after adding the user return new RedirectView("/addUsers"); // Redirect to account page after adding the user
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}") @PostMapping("/admin/deleteUser/{username}")
public String deleteUser(@PathVariable String username, Authentication authentication) { public String deleteUser(@PathVariable String username, Authentication authentication) {
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equals(username)) { if (currentUsername.equals(username)) {
throw new IllegalArgumentException("Cannot delete currently logined in user."); throw new IllegalArgumentException("Cannot delete currently logined in user.");
} }
userService.deleteUser(username); userService.deleteUser(username);
return "redirect:/addUsers"; return "redirect:/addUsers";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key") @PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) { public ResponseEntity<String> getApiKey(Principal principal) {
@ -247,6 +253,4 @@ public class UserController {
} }
return ResponseEntity.ok(apiKey); return ResponseEntity.ok(apiKey);
} }
} }

View File

@ -9,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -18,35 +19,30 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF { public class ConvertHtmlToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description =
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
MultipartFile fileInput = request.getFileInput();
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf") if (fileInput == null) {
@Operation( throw new IllegalArgumentException(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", "Please provide an HTML or ZIP file for conversion.");
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." }
)
public ResponseEntity<byte[]> HtmlToPdf(
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) { String originalFilename = fileInput.getOriginalFilename();
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion."); if (originalFilename == null
} || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
throw new IllegalArgumentException("File must be either .html or .zip format.");
}
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename);
String originalFilename = fileInput.getOriginalFilename(); String outputFilename =
if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { originalFilename.replaceFirst("[.][^.]+$", "")
throw new IllegalArgumentException("File must be either .html or .zip format."); + ".pdf"; // Remove file extension and append .pdf
}byte[] pdfBytes = FileToPdf.convertHtmlToPdf( fileInput.getBytes(), originalFilename);
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
} }

View File

@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@ -33,15 +34,18 @@ public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img") @PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
@Operation(summary = "Convert PDF to image(s)", @Operation(
description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional") summary = "Convert PDF to image(s)",
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException { description =
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String imageFormat = request.getImageFormat(); String imageFormat = request.getImageFormat();
String singleOrMultiple = request.getSingleOrMultiple(); String singleOrMultiple = request.getSingleOrMultiple();
String colorType = request.getColorType(); String colorType = request.getColorType();
String dpi = request.getDpi(); String dpi = request.getDpi();
byte[] pdfBytes = file.getBytes(); byte[] pdfBytes = file.getBytes();
ImageType colorTypeResult = ImageType.RGB; ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) { if ("greyscale".equals(colorType)) {
@ -54,7 +58,14 @@ public class ConvertImgPDFController {
byte[] result = null; byte[] result = null;
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
try { try {
result = PdfUtils.convertFromPdf(pdfBytes, imageFormat.toUpperCase(), colorTypeResult, singleImage, Integer.valueOf(dpi), filename); result =
PdfUtils.convertFromPdf(
pdfBytes,
imageFormat.toUpperCase(),
colorTypeResult,
singleImage,
Integer.valueOf(dpi),
filename);
} catch (IOException e) { } catch (IOException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
@ -65,29 +76,39 @@ public class ConvertImgPDFController {
if (singleImage) { if (singleImage) {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat))); headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat)));
ResponseEntity<Resource> response = new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK); ResponseEntity<Resource> response =
new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
return response; return response;
} else { } else {
ByteArrayResource resource = new ByteArrayResource(result); ByteArrayResource resource = new ByteArrayResource(result);
// return the Resource in the response // return the Resource in the response
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + "_convertedToImages.zip") .header(
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource); HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + filename + "_convertedToImages.zip")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(resource.contentLength())
.body(resource);
} }
} }
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf") @PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
@Operation(summary = "Convert images to a PDF file", @Operation(
description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?") summary = "Convert images to a PDF file",
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException { description =
"This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?")
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
throws IOException {
MultipartFile[] file = request.getFileInput(); MultipartFile[] file = request.getFileInput();
String fitOption = request.getFitOption(); String fitOption = request.getFitOption();
String colorType = request.getColorType(); String colorType = request.getColorType();
boolean autoRotate = request.isAutoRotate(); boolean autoRotate = request.isAutoRotate();
// Convert the file to PDF and get the resulting bytes // Convert the file to PDF and get the resulting bytes
byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType); byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType);
return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); return WebResponseUtils.bytesToWebResponse(
bytes,
file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
} }
private String getMediaType(String imageFormat) { private String getMediaType(String imageFormat) {

View File

@ -12,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -20,17 +21,16 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf { public class ConvertMarkdownToPdf {
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation( @Operation(
summary = "Convert a Markdown file to PDF", summary = "Convert a Markdown file to PDF",
description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format." description =
) "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.")
public ResponseEntity<byte[]> markdownToPdf( public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
@ModelAttribute GeneralFile request) throws Exception {
throws Exception { MultipartFile fileInput = request.getFileInput();
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) { if (fileInput == null) {
throw new IllegalArgumentException("Please provide a Markdown file for conversion."); throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
} }
@ -45,10 +45,12 @@ public class ConvertMarkdownToPdf {
Node document = parser.parse(new String(fileInput.getBytes())); Node document = parser.parse(new String(fileInput.getBytes()));
HtmlRenderer renderer = HtmlRenderer.builder().build(); HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(document); String htmlContent = renderer.render(document);
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -31,20 +32,33 @@ public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
// Check for valid file extension // Check for valid file extension
String originalFilename = inputFile.getOriginalFilename(); String originalFilename = inputFile.getOriginalFilename();
if (originalFilename == null || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { if (originalFilename == null
|| !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
throw new IllegalArgumentException("Invalid file extension"); throw new IllegalArgumentException("Invalid file extension");
} }
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); Path tempInputFile =
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
// Prepare the output file path // Prepare the output file path
Path tempOutputFile = Files.createTempFile("output_", ".pdf"); Path tempOutputFile = Files.createTempFile("output_", ".pdf");
// Run the LibreOffice command // Run the LibreOffice command
List<String> command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString())); List<String> command =
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command); new ArrayList<>(
Arrays.asList(
"unoconv",
"-vvv",
"-f",
"pdf",
"-o",
tempOutputFile.toString(),
tempInputFile.toString()));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
// Read the converted PDF file // Read the converted PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
@ -55,6 +69,7 @@ public class ConvertOfficeController {
return pdfBytes; return pdfBytes;
} }
private boolean isValidFileExtension(String fileExtension) { private boolean isValidFileExtension(String fileExtension) {
String extensionPattern = "^(?i)[a-z0-9]{2,4}$"; String extensionPattern = "^(?i)[a-z0-9]{2,4}$";
return fileExtension.matches(extensionPattern); return fileExtension.matches(extensionPattern);
@ -62,17 +77,19 @@ public class ConvertOfficeController {
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf") @PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
@Operation( @Operation(
summary = "Convert a file to a PDF using LibreOffice", summary = "Convert a file to a PDF using LibreOffice",
description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO" description =
) "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO")
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request) public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
throws Exception { throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
// unused but can start server instance if startup time is to long // unused but can start server instance if startup time is to long
// LibreOfficeListener.getInstance().start(); // LibreOfficeListener.getInstance().start();
byte[] pdfByteArray = convertToPdf(inputFile); byte[] pdfByteArray = convertToPdf(inputFile);
return WebResponseUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf"); return WebResponseUtils.bytesToWebResponse(
pdfByteArray,
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_convertedToPDF.pdf");
} }
} }

View File

@ -11,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
@ -22,51 +23,70 @@ import stirling.software.SPDF.utils.PDFToFile;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice { public class ConvertPDFToOffice {
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html") @PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request) summary = "Convert PDF to HTML",
throws Exception { description =
MultipartFile inputFile = request.getFileInput(); "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
PDFToFile pdfToFile = new PDFToFile(); public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); throws Exception {
} MultipartFile inputFile = request.getFileInput();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") @PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToPresentation(@ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { summary = "Convert PDF to Presentation format",
MultipartFile inputFile = request.getFileInput(); description =
String outputFormat = request.getOutputFormat(); "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
PDFToFile pdfToFile = new PDFToFile(); public ResponseEntity<byte[]> processPdfToPresentation(
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); @ModelAttribute PdfToPresentationRequest request)
} throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text") @PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { summary = "Convert PDF to Text or RTF format",
MultipartFile inputFile = request.getFileInput(); description =
String outputFormat = request.getOutputFormat(); "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
public ResponseEntity<byte[]> processPdfToRTForTXT(
@ModelAttribute PdfToTextOrRTFRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile(); PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word") @PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { summary = "Convert PDF to Word document",
MultipartFile inputFile = request.getFileInput(); description =
String outputFormat = request.getOutputFormat(); "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
PDFToFile pdfToFile = new PDFToFile(); public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request)
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); throws IOException, InterruptedException {
} MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml") @PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request) summary = "Convert PDF to XML",
throws Exception { description =
MultipartFile inputFile = request.getFileInput(); "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request)
PDFToFile pdfToFile = new PDFToFile(); throws Exception {
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); MultipartFile inputFile = request.getFileInput();
}
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");
}
} }

View File

@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -24,14 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA { public class ConvertPDFToPDFA {
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
@Operation( @Operation(
summary = "Convert a PDF to a PDF/A", summary = "Convert a PDF to a PDF/A",
description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request) public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request) throws Exception {
throws Exception { MultipartFile inputFile = request.getFileInput();
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
@ -50,7 +50,9 @@ public class ConvertPDFToPDFA {
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
command.add(tempOutputFile.toString()); command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
.runCommandWithOutputHandling(command);
// Read the optimized PDF file // Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
@ -60,8 +62,8 @@ public class ConvertPDFToPDFA {
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@ -25,52 +26,52 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF { public class ConvertWebsiteToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf") @PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation( @Operation(
summary = "Convert a URL to a PDF", summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO" description =
) "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
String URL = request.getUrlInput(); throws IOException, InterruptedException {
String URL = request.getUrlInput();
// Validate the URL format // Validate the URL format
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided."); throw new IllegalArgumentException("Invalid URL format provided.");
} }
Path tempOutputFile = null; Path tempOutputFile = null;
byte[] pdfBytes; byte[] pdfBytes;
try { try {
// Prepare the output file path // Prepare the output file path
tempOutputFile = Files.createTempFile("output_", ".pdf"); tempOutputFile = Files.createTempFile("output_", ".pdf");
// Prepare the OCRmyPDF command
List<String> command = new ArrayList<>();
command.add("weasyprint");
command.add(URL);
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command);
// Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile);
}
finally {
// Clean up the temporary files
Files.delete(tempOutputFile);
}
// Convert URL to a safe filename
String outputFilename = convertURLToFileName(URL);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
private String convertURLToFileName(String url) { // Prepare the OCRmyPDF command
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); List<String> command = new ArrayList<>();
if(safeName.length() > 50) { command.add("weasyprint");
safeName = safeName.substring(0, 50); // restrict to 50 characters command.add(URL);
} command.add(tempOutputFile.toString());
return safeName + ".pdf";
}
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command);
// Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile);
} finally {
// Clean up the temporary files
Files.delete(tempOutputFile);
}
// Convert URL to a safe filename
String outputFilename = convertURLToFileName(URL);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
private String convertURLToFileName(String url) {
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
if (safeName.length() > 50) {
safeName = safeName.substring(0, 50); // restrict to 50 characters
}
return safeName + ".pdf";
}
} }

View File

@ -22,6 +22,7 @@ import com.opencsv.CSVWriter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.CropController; import stirling.software.SPDF.controller.api.CropController;
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper; import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
import stirling.software.SPDF.model.api.extract.PDFFilePage; import stirling.software.SPDF.model.api.extract.PDFFilePage;
@ -34,21 +35,24 @@ public class ExtractController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data") @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") @Operation(
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) summary = "Extracts a PDF document to csv",
throws Exception { description =
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
ArrayList<String> tableData = new ArrayList<>(); ArrayList<String> tableData = new ArrayList<>();
int columnsCount = 0; int columnsCount = 0;
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { try (PDDocument document =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
final double res = 72; // PDF units are at 72 DPI final double res = 72; // PDF units are at 72 DPI
PDFTableStripper stripper = new PDFTableStripper(); PDFTableStripper stripper = new PDFTableStripper();
PDPage pdPage = document.getPage(form.getPageId() - 1); PDPage pdPage = document.getPage(form.getPageId() - 1);
stripper.extractTable(pdPage); stripper.extractTable(pdPage);
columnsCount = stripper.getColumns(); columnsCount = stripper.getColumns();
for (int c = 0; c < columnsCount; ++c) { for (int c = 0; c < columnsCount; ++c) {
for(int r=0; r<stripper.getRows(); ++r) { for (int r = 0; r < stripper.getRows(); ++r) {
tableData.add(stripper.getText(r, c)); tableData.add(stripper.getText(r, c));
} }
} }
@ -56,26 +60,33 @@ public class ExtractController {
ArrayList<String> notEmptyColumns = new ArrayList<>(); ArrayList<String> notEmptyColumns = new ArrayList<>();
for (String item: tableData) { for (String item : tableData) {
if(!item.trim().isEmpty()){ if (!item.trim().isEmpty()) {
notEmptyColumns.add(item); notEmptyColumns.add(item);
}else{ } else {
columnsCount--; columnsCount--;
} }
} }
List<String> fullTable = notEmptyColumns.stream().map((entity)-> List<String> fullTable =
entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList(); notEmptyColumns.stream()
.map(
(entity) ->
entity.replace('\n', ' ')
.replace('\r', ' ')
.trim()
.replaceAll("\\s{2,}", "|"))
.toList();
int rowsCount = fullTable.get(0).split("\\|").length; int rowsCount = fullTable.get(0).split("\\|").length;
ArrayList<String> headersList = getTableHeaders(columnsCount,fullTable); ArrayList<String> headersList = getTableHeaders(columnsCount, fullTable);
ArrayList<String> recordList = getRecordsList(rowsCount,fullTable); ArrayList<String> recordList = getRecordsList(rowsCount, fullTable);
if(headersList.size() == 0 && recordList.size() == 0) { if (headersList.size() == 0 && recordList.size() == 0) {
throw new Exception("No table detected, no headers or records found"); throw new Exception("No table detected, no headers or records found");
} }
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
try (CSVWriter csvWriter = new CSVWriter(writer)) { try (CSVWriter csvWriter = new CSVWriter(writer)) {
csvWriter.writeNext(headersList.toArray(new String[0])); csvWriter.writeNext(headersList.toArray(new String[0]));
@ -85,35 +96,41 @@ public class ExtractController {
} }
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build()); headers.setContentDisposition(
ContentDisposition.builder("attachment")
.filename(
form.getFileInput()
.getOriginalFilename()
.replaceFirst("[.][^.]+$", "")
+ "_extracted.csv")
.build());
headers.setContentType(MediaType.parseMediaType("text/csv")); headers.setContentType(MediaType.parseMediaType("text/csv"));
return ResponseEntity.ok() return ResponseEntity.ok().headers(headers).body(writer.toString());
.headers(headers)
.body(writer.toString());
} }
private ArrayList<String> getRecordsList( int rowsCounts ,List<String> items){ private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
ArrayList<String> recordsList = new ArrayList<>(); ArrayList<String> recordsList = new ArrayList<>();
for (int b=1; b<rowsCounts;b++) { for (int b = 1; b < rowsCounts; b++) {
StringBuilder strbldr = new StringBuilder(); StringBuilder strbldr = new StringBuilder();
for (int i=0;i<items.size();i++){ for (int i = 0; i < items.size(); i++) {
String[] parts = items.get(i).split("\\|"); String[] parts = items.get(i).split("\\|");
strbldr.append(parts[b]); strbldr.append(parts[b]);
if (i!= items.size()-1){ if (i != items.size() - 1) {
strbldr.append("|"); strbldr.append("|");
}
} }
recordsList.add(strbldr.toString());
} }
recordsList.add(strbldr.toString());
}
return recordsList; return recordsList;
} }
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items){
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
ArrayList<String> resultList = new ArrayList<>(); ArrayList<String> resultList = new ArrayList<>();
for (int i=0;i<columnsCount;i++){ for (int i = 0; i < columnsCount; i++) {
String[] parts = items.get(i).split("\\|"); String[] parts = items.get(i).split("\\|");
resultList.add(parts[0]); resultList.add(parts[0]);
} }

View File

@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFComparisonAndCount; import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.filter.ContainsTextRequest; import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
@ -28,169 +29,182 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Filter", description = "Filter APIs") @Tag(name = "Filter", description = "Filter APIs")
public class FilterController { public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
@Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") @Operation(
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { summary = "Checks if a PDF contains set text, returns true if does",
MultipartFile inputFile = request.getFileInput(); description = "Input:PDF Output:Boolean Type:SISO")
String text = request.getText(); public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
String pageNumber = request.getPageNumbers(); throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); String text = request.getText();
if (PdfUtils.hasText(pdfDocument, pageNumber, text)) String pageNumber = request.getPageNumbers();
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
return null;
}
// TODO PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") if (PdfUtils.hasText(pdfDocument, pageNumber, text))
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO") return WebResponseUtils.pdfDocToWebResponse(
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request) pdfDocument, inputFile.getOriginalFilename());
throws IOException, InterruptedException { return null;
MultipartFile inputFile = request.getFileInput(); }
String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasImages(pdfDocument, pageNumber))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
return null;
}
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") // TODO
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException { @Operation(
MultipartFile inputFile = request.getFileInput(); summary = "Checks if a PDF contains an image",
String pageCount = request.getPageCount(); description = "Input:PDF Output:Boolean Type:SISO")
String comparator = request.getComparator(); public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
// Load the PDF throws IOException, InterruptedException {
PDDocument document = PDDocument.load(inputFile.getInputStream()); MultipartFile inputFile = request.getFileInput();
int actualPageCount = document.getNumberOfPages(); String pageNumber = request.getPageNumbers();
boolean valid = false; PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
// Perform the comparison if (PdfUtils.hasImages(pdfDocument, pageNumber))
switch (comparator) { return WebResponseUtils.pdfDocToWebResponse(
case "Greater": pdfDocument, inputFile.getOriginalFilename());
valid = actualPageCount > Integer.parseInt(pageCount); return null;
break; }
case "Equal":
valid = actualPageCount == Integer.parseInt(pageCount);
break;
case "Less":
valid = actualPageCount < Integer.parseInt(pageCount);
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid) @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
return WebResponseUtils.multiPartFileToWebResponse(inputFile); @Operation(
return null; summary = "Checks if a PDF is greater, less or equal to a setPageCount",
} description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String pageCount = request.getPageCount();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());
int actualPageCount = document.getNumberOfPages();
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") boolean valid = false;
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") // Perform the comparison
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException { switch (comparator) {
MultipartFile inputFile = request.getFileInput(); case "Greater":
String standardPageSize = request.getStandardPageSize(); valid = actualPageCount > Integer.parseInt(pageCount);
String comparator = request.getComparator(); break;
case "Equal":
valid = actualPageCount == Integer.parseInt(pageCount);
break;
case "Less":
valid = actualPageCount < Integer.parseInt(pageCount);
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
// Load the PDF if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
PDDocument document = PDDocument.load(inputFile.getInputStream()); return null;
}
PDPage firstPage = document.getPage(0); @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
PDRectangle actualPageSize = firstPage.getMediaBox(); @Operation(
summary = "Checks if a PDF is of a certain size",
description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String standardPageSize = request.getStandardPageSize();
String comparator = request.getComparator();
// Calculate the area of the actual page size // Load the PDF
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); PDDocument document = PDDocument.load(inputFile.getInputStream());
// Get the standard size and calculate its area PDPage firstPage = document.getPage(0);
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); PDRectangle actualPageSize = firstPage.getMediaBox();
float standardArea = standardSize.getWidth() * standardSize.getHeight();
boolean valid = false; // Calculate the area of the actual page size
// Perform the comparison float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
switch (comparator) {
case "Greater":
valid = actualArea > standardArea;
break;
case "Equal":
valid = actualArea == standardArea;
break;
case "Less":
valid = actualArea < standardArea;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid) // Get the standard size and calculate its area
return WebResponseUtils.multiPartFileToWebResponse(inputFile); PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
return null; float standardArea = standardSize.getWidth() * standardSize.getHeight();
}
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") boolean valid = false;
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO") // Perform the comparison
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException { switch (comparator) {
MultipartFile inputFile = request.getFileInput(); case "Greater":
String fileSize = request.getFileSize(); valid = actualArea > standardArea;
String comparator = request.getComparator(); break;
case "Equal":
valid = actualArea == standardArea;
break;
case "Less":
valid = actualArea < standardArea;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
// Get the file size if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
long actualFileSize = inputFile.getSize(); return null;
}
boolean valid = false; @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
// Perform the comparison @Operation(
switch (comparator) { summary = "Checks if a PDF is a set file size",
case "Greater": description = "Input:PDF Output:Boolean Type:SISO")
valid = actualFileSize > Long.parseLong(fileSize); public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
break; throws IOException, InterruptedException {
case "Equal": MultipartFile inputFile = request.getFileInput();
valid = actualFileSize == Long.parseLong(fileSize); String fileSize = request.getFileSize();
break; String comparator = request.getComparator();
case "Less":
valid = actualFileSize < Long.parseLong(fileSize);
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid) // Get the file size
return WebResponseUtils.multiPartFileToWebResponse(inputFile); long actualFileSize = inputFile.getSize();
return null;
}
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") boolean valid = false;
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") // Perform the comparison
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException { switch (comparator) {
MultipartFile inputFile = request.getFileInput(); case "Greater":
int rotation = request.getRotation(); valid = actualFileSize > Long.parseLong(fileSize);
String comparator = request.getComparator(); break;
case "Equal":
valid = actualFileSize == Long.parseLong(fileSize);
break;
case "Less":
valid = actualFileSize < Long.parseLong(fileSize);
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
// Load the PDF if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
PDDocument document = PDDocument.load(inputFile.getInputStream()); return null;
}
// Get the rotation of the first page @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
PDPage firstPage = document.getPage(0); @Operation(
int actualRotation = firstPage.getRotation(); summary = "Checks if a PDF is of a certain rotation",
boolean valid = false; description = "Input:PDF Output:Boolean Type:SISO")
// Perform the comparison public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
switch (comparator) { throws IOException, InterruptedException {
case "Greater": MultipartFile inputFile = request.getFileInput();
valid = actualRotation > rotation; int rotation = request.getRotation();
break; String comparator = request.getComparator();
case "Equal":
valid = actualRotation == rotation;
break;
case "Less":
valid = actualRotation < rotation;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid) // Load the PDF
return WebResponseUtils.multiPartFileToWebResponse(inputFile); PDDocument document = PDDocument.load(inputFile.getInputStream());
return null;
} // Get the rotation of the first page
PDPage firstPage = document.getPage(0);
int actualRotation = firstPage.getRotation();
boolean valid = false;
// Perform the comparison
switch (comparator) {
case "Greater":
valid = actualRotation > rotation;
break;
case "Equal":
valid = actualRotation == rotation;
break;
case "Less":
valid = actualRotation < rotation;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
}
} }

View File

@ -19,8 +19,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@ -32,97 +34,105 @@ public class AutoRenameController {
private static final int LINE_LIMIT = 11; private static final int LINE_LIMIT = 11;
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename") @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@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") @Operation(
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception { 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(@ModelAttribute ExtractHeaderRequest request)
throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback(); Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = PDDocument.load(file.getInputStream()); PDDocument document = PDDocument.load(file.getInputStream());
PDFTextStripper reader = new PDFTextStripper() { PDFTextStripper reader =
class LineInfo { new PDFTextStripper() {
String text; class LineInfo {
float fontSize; String text;
float fontSize;
LineInfo(String text, float fontSize) { LineInfo(String text, float fontSize) {
this.text = text; this.text = text;
this.fontSize = fontSize; this.fontSize = fontSize;
} }
} }
List<LineInfo> lineInfos = new ArrayList<>(); List<LineInfo> lineInfos = new ArrayList<>();
StringBuilder lineBuilder = new StringBuilder(); StringBuilder lineBuilder = new StringBuilder();
float lastY = -1; float lastY = -1;
float maxFontSizeInLine = 0.0f; float maxFontSizeInLine = 0.0f;
int lineCount = 0; int lineCount = 0;
@Override @Override
protected void processTextPosition(TextPosition text) { protected void processTextPosition(TextPosition text) {
if (lastY != text.getY() && lineCount < LINE_LIMIT) { if (lastY != text.getY() && lineCount < LINE_LIMIT) {
processLine(); processLine();
lineBuilder = new StringBuilder(text.getUnicode()); lineBuilder = new StringBuilder(text.getUnicode());
maxFontSizeInLine = text.getFontSizeInPt(); maxFontSizeInLine = text.getFontSizeInPt();
lastY = text.getY(); lastY = text.getY();
lineCount++; lineCount++;
} else if (lineCount < LINE_LIMIT) { } else if (lineCount < LINE_LIMIT) {
lineBuilder.append(text.getUnicode()); lineBuilder.append(text.getUnicode());
if (text.getFontSizeInPt() > maxFontSizeInLine) { if (text.getFontSizeInPt() > maxFontSizeInLine) {
maxFontSizeInLine = text.getFontSizeInPt(); maxFontSizeInLine = text.getFontSizeInPt();
} }
} }
} }
private void processLine() { private void processLine() {
if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) { if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine)); lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
} }
} }
@Override @Override
public String getText(PDDocument doc) throws IOException { public String getText(PDDocument doc) throws IOException {
this.lineInfos.clear(); this.lineInfos.clear();
this.lineBuilder = new StringBuilder(); this.lineBuilder = new StringBuilder();
this.lastY = -1; this.lastY = -1;
this.maxFontSizeInLine = 0.0f; this.maxFontSizeInLine = 0.0f;
this.lineCount = 0; this.lineCount = 0;
super.getText(doc); super.getText(doc);
processLine(); // Process the last line processLine(); // Process the last line
// Merge lines with same font size // Merge lines with same font size
List<LineInfo> mergedLineInfos = new ArrayList<>(); List<LineInfo> mergedLineInfos = new ArrayList<>();
for (int i = 0; i < lineInfos.size(); i++) { for (int i = 0; i < lineInfos.size(); i++) {
String mergedText = lineInfos.get(i).text; String mergedText = lineInfos.get(i).text;
float fontSize = lineInfos.get(i).fontSize; float fontSize = lineInfos.get(i).fontSize;
while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) { while (i + 1 < lineInfos.size()
mergedText += " " + lineInfos.get(i + 1).text; && lineInfos.get(i + 1).fontSize == fontSize) {
i++; mergedText += " " + lineInfos.get(i + 1).text;
} i++;
mergedLineInfos.add(new LineInfo(mergedText, fontSize)); }
} mergedLineInfos.add(new LineInfo(mergedText, fontSize));
}
// Sort lines by font size in descending order and get the first one // Sort lines by font size in descending order and get the first one
mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); mergedLineInfos.sort(
String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
String title =
mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null); return title != null
} ? title
: (useFirstTextAsFallback
? (mergedLineInfos.isEmpty()
? null
: mergedLineInfos.get(mergedLineInfos.size() - 1)
.text)
: null);
}
};
}; String header = reader.getText(document);
String header = reader.getText(document);
// Sanitize the header string by removing characters not allowed in a filename. // Sanitize the header string by removing characters not allowed in a filename.
if (header != null && header.length() < 255) { if (header != null && header.length() < 255) {
header = header.replaceAll("[/\\\\?%*:|\"<>]", ""); header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
} else { } else {
logger.info("File has no good title to be found"); logger.info("File has no good title to be found");
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename()); return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt; import java.awt.image.DataBufferInt;
@ -32,6 +33,7 @@ import com.google.zxing.common.HybridBinarizer;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -43,8 +45,12 @@ public class AutoSplitPdfController {
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { summary = "Auto split PDF pages into separate documents",
description =
"This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean duplexMode = request.isDuplexMode(); boolean duplexMode = request.isDuplexMode();
@ -107,29 +113,48 @@ public class AutoSplitPdfController {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
data = Files.readAllBytes(zipFile); data = Files.readAllBytes(zipFile);
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private static String decodeQRCode(BufferedImage bufferedImage) { private static String decodeQRCode(BufferedImage bufferedImage) {
LuminanceSource source; LuminanceSource source;
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); source =
new PlanarYUVLuminanceSource(
pixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
byte[] newPixels = new byte[pixels.length]; byte[] newPixels = new byte[pixels.length];
for (int i = 0; i < pixels.length; i++) { for (int i = 0; i < pixels.length; i++) {
newPixels[i] = (byte) (pixels[i] & 0xff); newPixels[i] = (byte) (pixels[i] & 0xff);
} }
source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); source =
new PlanarYUVLuminanceSource(
newPixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else { } else {
throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); throw new IllegalArgumentException(
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
} }
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

View File

@ -28,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@ -39,17 +40,18 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController { public class BlankPageController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@Operation( @Operation(
summary = "Remove blank pages from a PDF file", summary = "Remove blank pages from a PDF file",
description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request)
MultipartFile inputFile = request.getFileInput(); throws IOException, InterruptedException {
int threshold = request.getThreshold(); MultipartFile inputFile = request.getFileInput();
float whitePercent = request.getWhitePercent(); int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent();
PDDocument document = null;
PDDocument document = null;
try { try {
document = PDDocument.load(inputFile.getInputStream()); document = PDDocument.load(inputFile.getInputStream());
PDPageTree pages = document.getDocumentCatalog().getPages(); PDPageTree pages = document.getDocumentCatalog().getPages();
@ -72,21 +74,34 @@ public class BlankPageController {
boolean hasImages = PdfUtils.hasImagesOnPage(page); boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) { if (hasImages) {
System.out.println("page " + pageIndex + " has image"); System.out.println("page " + pageIndex + " has image");
Path tempFile = Files.createTempFile("image_", ".png"); Path tempFile = Files.createTempFile("image_", ".png");
// Render image and save as temp file // Render image and save as temp file
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300); BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300);
ImageIO.write(image, "png", tempFile.toFile()); ImageIO.write(image, "png", tempFile.toFile());
List<String> command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "/scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent))); List<String> command =
new ArrayList<>(
Arrays.asList(
"python3",
System.getProperty("user.dir")
+ "/scripts/detect-blank-pages.py",
tempFile.toString(),
"--threshold",
String.valueOf(threshold),
"--white_percent",
String.valueOf(whitePercent)));
// Run CLI command // Run CLI command
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// does contain data // does contain data
if (returnCode.getRc() == 0) { if (returnCode.getRc() == 0) {
System.out.println("page " + pageIndex + " has image which is not blank"); System.out.println(
"page " + pageIndex + " has image which is not blank");
pagesToKeepIndex.add(pageIndex); pagesToKeepIndex.add(pageIndex);
} else { } else {
System.out.println("Skipping, Image was blank for page #" + pageIndex); System.out.println("Skipping, Image was blank for page #" + pageIndex);
@ -94,12 +109,12 @@ public class BlankPageController {
} }
} }
pageIndex++; pageIndex++;
} }
System.out.print("pagesToKeep=" + pagesToKeepIndex.size()); System.out.print("pagesToKeep=" + pagesToKeepIndex.size());
// Remove pages not present in pagesToKeepIndex // Remove pages not present in pagesToKeepIndex
List<Integer> pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); List<Integer> pageIndices =
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
for (Integer i : pageIndices) { for (Integer i : pageIndices) {
if (!pagesToKeepIndex.contains(i)) { if (!pagesToKeepIndex.contains(i)) {
@ -107,16 +122,15 @@ public class BlankPageController {
} }
} }
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_blanksRemoved.pdf");
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} finally { } finally {
if (document != null) if (document != null) document.close();
document.close();
} }
} }
} }

View File

@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@ -44,20 +45,23 @@ public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class); private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception { summary = "Optimize PDF file",
description =
"This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
Integer optimizeLevel = request.getOptimizeLevel(); Integer optimizeLevel = request.getOptimizeLevel();
String expectedOutputSizeString = request.getExpectedOutputSize(); String expectedOutputSizeString = request.getExpectedOutputSize();
if (expectedOutputSizeString == null && optimizeLevel == null) {
if(expectedOutputSizeString == null && optimizeLevel == null) {
throw new Exception("Both expected output size and optimize level are not specified"); throw new Exception("Both expected output size and optimize level are not specified");
} }
Long expectedOutputSize = 0L; Long expectedOutputSize = 0L;
boolean autoMode = false; boolean autoMode = false;
if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) { if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1) {
expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString); expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString);
autoMode = true; autoMode = true;
} }
@ -71,8 +75,9 @@ public class CompressController {
// Prepare the output file path // Prepare the output file path
Path tempOutputFile = Files.createTempFile("output_", ".pdf"); Path tempOutputFile = Files.createTempFile("output_", ".pdf");
// Determine initial optimization level based on expected size reduction, only if in autoMode // Determine initial optimization level based on expected size reduction, only if in
if(autoMode) { // autoMode
if (autoMode) {
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
if (sizeReductionRatio > 0.7) { if (sizeReductionRatio > 0.7) {
optimizeLevel = 1; optimizeLevel = 1;
@ -94,20 +99,20 @@ public class CompressController {
command.add("-dCompatibilityLevel=1.4"); command.add("-dCompatibilityLevel=1.4");
switch (optimizeLevel) { switch (optimizeLevel) {
case 1: case 1:
command.add("-dPDFSETTINGS=/prepress"); command.add("-dPDFSETTINGS=/prepress");
break; break;
case 2: case 2:
command.add("-dPDFSETTINGS=/printer"); command.add("-dPDFSETTINGS=/printer");
break; break;
case 3: case 3:
command.add("-dPDFSETTINGS=/ebook"); command.add("-dPDFSETTINGS=/ebook");
break; break;
case 4: case 4:
command.add("-dPDFSETTINGS=/screen"); command.add("-dPDFSETTINGS=/screen");
break; break;
default: default:
command.add("-dPDFSETTINGS=/default"); command.add("-dPDFSETTINGS=/default");
} }
command.add("-dNOPAUSE"); command.add("-dNOPAUSE");
@ -116,7 +121,9 @@ public class CompressController {
command.add("-sOutputFile=" + tempOutputFile.toString()); command.add("-sOutputFile=" + tempOutputFile.toString());
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
// Check if file size is within expected size or not auto mode so instantly finish // Check if file size is within expected size or not auto mode so instantly finish
long outputFileSize = Files.size(tempOutputFile); long outputFileSize = Files.size(tempOutputFile);
@ -125,19 +132,18 @@ public class CompressController {
} else { } else {
// Increase optimization level for next iteration // Increase optimization level for next iteration
optimizeLevel++; optimizeLevel++;
if(autoMode && optimizeLevel > 3) { if (autoMode && optimizeLevel > 3) {
System.out.println("Skipping level 4 due to bad results in auto mode"); System.out.println("Skipping level 4 due to bad results in auto mode");
sizeMet = true; sizeMet = true;
} else if(optimizeLevel == 5) { } else if (optimizeLevel == 5) {
} else { } else {
System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel); System.out.println(
"Increasing ghostscript optimisation level to " + optimizeLevel);
} }
} }
} }
if (expectedOutputSize != null && autoMode) { if (expectedOutputSize != null && autoMode) {
long outputFileSize = Files.size(tempOutputFile); long outputFileSize = Files.size(tempOutputFile);
if (outputFileSize > expectedOutputSize) { if (outputFileSize > expectedOutputSize) {
@ -157,8 +163,8 @@ public class CompressController {
BufferedImage bufferedImage = image.getImage(); BufferedImage bufferedImage = image.getImage();
// Calculate the new dimensions // Calculate the new dimensions
int newWidth = (int)(bufferedImage.getWidth() * scaleFactor); int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
int newHeight = (int)(bufferedImage.getHeight() * scaleFactor); int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
// If the new dimensions are zero, skip this iteration // If the new dimensions are zero, skip this iteration
if (newWidth == 0 || newHeight == 0) { if (newWidth == 0 || newHeight == 0) {
@ -166,23 +172,39 @@ public class CompressController {
} }
// Otherwise, proceed with the scaling // Otherwise, proceed with the scaling
Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); Image scaledImage =
bufferedImage.getScaledInstance(
newWidth, newHeight, Image.SCALE_SMOOTH);
// Convert the scaled image back to a BufferedImage // Convert the scaled image back to a BufferedImage
BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); BufferedImage scaledBufferedImage =
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null); new BufferedImage(
newWidth,
newHeight,
BufferedImage.TYPE_INT_RGB);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
// Compress the scaled image // Compress the scaled image
ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); ByteArrayOutputStream compressedImageStream =
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream); new ByteArrayOutputStream();
ImageIO.write(
scaledBufferedImage, "jpeg", compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray(); byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close(); compressedImageStream.close();
// Convert compressed image back to PDImageXObject // Convert compressed image back to PDImageXObject
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes); ByteArrayInputStream bais =
PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString()); new ByteArrayInputStream(imageBytes);
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc,
imageBytes,
image.getCOSObject().toString());
// Replace the image in the resources with the compressed version // Replace the image in the resources with the compressed
// version
res.put(name, compressedImage); res.put(name, compressedImage);
} }
} }
@ -194,16 +216,23 @@ public class CompressController {
long currentSize = Files.size(tempOutputFile); long currentSize = Files.size(tempOutputFile);
// Check if the overall PDF size is still larger than expectedOutputSize // Check if the overall PDF size is still larger than expectedOutputSize
if (currentSize > expectedOutputSize) { if (currentSize > expectedOutputSize) {
// Log the current file size and scaleFactor // Log the current file size and scaleFactor
System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize)); System.out.println(
"Current file size: "
+ FileUtils.byteCountToDisplaySize(currentSize));
System.out.println("Current scale factor: " + scaleFactor); System.out.println("Current scale factor: " + scaleFactor);
// The file is still too large, reduce scaleFactor and try again // The file is still too large, reduce scaleFactor and try again
scaleFactor *= 0.9; // reduce scaleFactor by 10% scaleFactor *= 0.9; // reduce scaleFactor by 10%
// Avoid scaleFactor being too small, causing the image to shrink to 0 // Avoid scaleFactor being too small, causing the image to shrink to 0
if(scaleFactor < 0.2 || previousFileSize == currentSize){ if (scaleFactor < 0.2 || previousFileSize == currentSize) {
throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes"); throw new RuntimeException(
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
+ FileUtils.byteCountToDisplaySize(currentSize)
+ ", "
+ currentSize
+ " bytes");
} }
previousFileSize = currentSize; previousFileSize = currentSize;
} else { } else {
@ -211,10 +240,7 @@ public class CompressController {
break; break;
} }
} }
} }
} }
} }
@ -222,9 +248,10 @@ public class CompressController {
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
// Check if optimized file is larger than the original // Check if optimized file is larger than the original
if(pdfBytes.length > inputFileSize) { if (pdfBytes.length > inputFileSize) {
// Log the occurrence // Log the occurrence
logger.warn("Optimized file is larger than the original. Returning the original file instead."); logger.warn(
"Optimized file is larger than the original. Returning the original file instead.");
// Read the original file again // Read the original file again
pdfBytes = Files.readAllBytes(tempInputFile); pdfBytes = Files.readAllBytes(tempInputFile);
@ -235,8 +262,8 @@ public class CompressController {
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@ -32,10 +32,12 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@ -44,18 +46,28 @@ public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation(summary = "Extract image scans from an input file", @Operation(
description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO") summary = "Extract image scans from an input file",
description =
"This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImageScans( public ResponseEntity<byte[]> extractImageScans(
@RequestBody( @RequestBody(
description = "Form data containing file and extraction parameters", description = "Form data containing file and extraction parameters",
required = true, required = true,
content = @Content( content =
mediaType = "multipart/form-data", @Content(
schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure mediaType = "multipart/form-data",
) schema =
) @Schema(
ExtractImageScansRequest form) throws IOException, InterruptedException { implementation =
ExtractImageScansRequest
.class) // This should
// represent
// your form's
// structure
))
ExtractImageScansRequest form)
throws IOException, InterruptedException {
String fileName = form.getFileInput().getOriginalFilename(); String fileName = form.getFileInput().getOriginalFilename();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1); String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
@ -64,7 +76,8 @@ public class ExtractImageScansController {
// Check if input file is a PDF // Check if input file is a PDF
if (extension.equalsIgnoreCase("pdf")) { if (extension.equalsIgnoreCase("pdf")) {
// Load PDF document // Load PDF document
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { try (PDDocument document =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages(); int pageCount = document.getNumberOfPages();
images = new ArrayList<>(); images = new ArrayList<>();
@ -84,7 +97,10 @@ public class ExtractImageScansController {
} }
} else { } else {
Path tempInputFile = Files.createTempFile("input_", "." + extension); Path tempInputFile = Files.createTempFile("input_", "." + extension);
Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(
form.getFileInput().getInputStream(),
tempInputFile,
StandardCopyOption.REPLACE_EXISTING);
// Add input file path to images list // Add input file path to images list
images.add(tempInputFile.toString()); images.add(tempInputFile.toString());
} }
@ -95,21 +111,28 @@ public class ExtractImageScansController {
for (int i = 0; i < images.size(); i++) { for (int i = 0; i < images.size(); i++) {
Path tempDir = Files.createTempDirectory("openCV_output"); Path tempDir = Files.createTempDirectory("openCV_output");
List<String> command = new ArrayList<>(Arrays.asList( List<String> command =
"python3", new ArrayList<>(
"./scripts/split_photos.py", Arrays.asList(
images.get(i), "python3",
tempDir.toString(), "./scripts/split_photos.py",
"--angle_threshold", String.valueOf(form.getAngleThreshold()), images.get(i),
"--tolerance", String.valueOf(form.getTolerance()), tempDir.toString(),
"--min_area", String.valueOf(form.getMinArea()), "--angle_threshold",
"--min_contour_area", String.valueOf(form.getMinContourArea()), String.valueOf(form.getAngleThreshold()),
"--border_size", String.valueOf(form.getBorderSize()) "--tolerance",
)); String.valueOf(form.getTolerance()),
"--min_area",
String.valueOf(form.getMinArea()),
"--min_contour_area",
String.valueOf(form.getMinContourArea()),
"--border_size",
String.valueOf(form.getBorderSize())));
// Run CLI command // Run CLI command
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// Read the output photos in temp directory // Read the output photos in temp directory
List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList()); List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList());
@ -126,10 +149,16 @@ public class ExtractImageScansController {
String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip"; String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip"); Path tempZipFile = Files.createTempFile("output_", ".zip");
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add processed images to the zip // Add processed images to the zip
for (int i = 0; i < processedImageBytes.size(); i++) { for (int i = 0; i < processedImageBytes.size(); i++) {
ZipEntry entry = new ZipEntry(fileName.replaceFirst("[.][^.]+$", "") + "_" + (i + 1) + ".png"); ZipEntry entry =
new ZipEntry(
fileName.replaceFirst("[.][^.]+$", "")
+ "_"
+ (i + 1)
+ ".png");
zipOut.putNextEntry(entry); zipOut.putNextEntry(entry);
zipOut.write(processedImageBytes.get(i)); zipOut.write(processedImageBytes.get(i));
zipOut.closeEntry(); zipOut.closeEntry();
@ -141,13 +170,15 @@ public class ExtractImageScansController {
// Clean up the temporary zip file // Clean up the temporary zip file
Files.delete(tempZipFile); Files.delete(tempZipFile);
return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else { } else {
// Return the processed image as a response // Return the processed image as a response
byte[] imageBytes = processedImageBytes.get(0); byte[] imageBytes = processedImageBytes.get(0);
return WebResponseUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG); return WebResponseUtils.bytesToWebResponse(
imageBytes,
fileName.replaceFirst("[.][^.]+$", "") + ".png",
MediaType.IMAGE_PNG);
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Image; import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -29,8 +30,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest; import stirling.software.SPDF.model.api.PDFWithImageFormatRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@ -39,13 +42,17 @@ public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-images") @PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation(summary = "Extract images from a PDF file", @Operation(
description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") summary = "Extract images from a PDF file",
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException { description =
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String format = request.getFormat(); String format = request.getFormat();
System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); System.out.println(
System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
PDDocument document = PDDocument.load(file.getBytes()); PDDocument document = PDDocument.load(file.getBytes());
// Create ByteArrayOutputStream to write zip file to byte array // Create ByteArrayOutputStream to write zip file to byte array
@ -69,24 +76,37 @@ public class ExtractImagesController {
if (page.getResources().isImageXObject(name)) { if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name); PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
int imageHash = image.hashCode(); int imageHash = image.hashCode();
if(processedImages.contains(imageHash)) { if (processedImages.contains(imageHash)) {
continue; // Skip already processed images continue; // Skip already processed images
} }
processedImages.add(imageHash); processedImages.add(imageHash);
// Convert image to desired format // Convert image to desired format
RenderedImage renderedImage = image.getImage(); RenderedImage renderedImage = image.getImage();
BufferedImage bufferedImage = null; BufferedImage bufferedImage = null;
if (format.equalsIgnoreCase("png")) { if (format.equalsIgnoreCase("png")) {
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_ARGB); bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_ARGB);
} else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) { } else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) {
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_RGB); bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_RGB);
} else if (format.equalsIgnoreCase("gif")) { } else if (format.equalsIgnoreCase("gif")) {
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_BYTE_INDEXED); bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED);
} }
// Write image to zip file // Write image to zip file
String imageName = filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; String imageName =
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
ZipEntry zipEntry = new ZipEntry(imageName); ZipEntry zipEntry = new ZipEntry(imageName);
zos.putNextEntry(zipEntry); zos.putNextEntry(zipEntry);
@ -111,7 +131,7 @@ public class ExtractImagesController {
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
return WebResponseUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.boasToWebResponse(
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View File

@ -3,21 +3,17 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.Color; import java.awt.Color;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp; import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp; import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.awt.image.RescaleOp; import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Random; import java.util.Random;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@ -40,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -50,102 +47,101 @@ public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class); private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
//TODO // TODO
@Hidden @Hidden
@PostMapping(consumes = "multipart/form-data", value = "/fakeScan") @PostMapping(consumes = "multipart/form-data", value = "/fakeScan")
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response." description =
) "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException { public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
PDDocument document = PDDocument.load(inputFile.getBytes()); PDDocument document = PDDocument.load(inputFile.getBytes());
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); ++page) for (int page = 0; page < document.getNumberOfPages(); ++page) {
{ BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); ImageIO.write(image, "png", new File("scanned-" + (page + 1) + ".png"));
ImageIO.write(image, "png", new File("scanned-" + (page+1) + ".png")); }
} document.close();
document.close();
// Constants // Constants
int scannedness = 90; // Value between 0 and 100 int scannedness = 90; // Value between 0 and 100
int dirtiness = 0; // Value between 0 and 100 int dirtiness = 0; // Value between 0 and 100
// Load the source image // Load the source image
BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png"));
// Create the destination image // Create the destination image
BufferedImage destinationImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); BufferedImage destinationImage =
new BufferedImage(
sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType());
// Apply a brightness and contrast effect based on the "scanned-ness" // Apply a brightness and contrast effect based on the "scanned-ness"
float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5
float offset = scannedness * 1.5f; // Between 0 and 150 float offset = scannedness * 1.5f; // Between 0 and 150
BufferedImageOp op = new RescaleOp(scaleFactor, offset, null); BufferedImageOp op = new RescaleOp(scaleFactor, offset, null);
op.filter(sourceImage, destinationImage); op.filter(sourceImage, destinationImage);
// Apply a rotation effect // Apply a rotation effect
double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees double rotationRequired =
double locationX = destinationImage.getWidth() / 2; Math.toRadians(
double locationY = destinationImage.getHeight() / 2; (new SecureRandom().nextInt(3 - 1)
AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY); + 1)); // Random angle between 1 and 3 degrees
AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); double locationX = destinationImage.getWidth() / 2;
destinationImage = rotateOp.filter(destinationImage, null); double locationY = destinationImage.getHeight() / 2;
AffineTransform tx =
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
destinationImage = rotateOp.filter(destinationImage, null);
// Apply a blur effect based on the "scanned-ness" // Apply a blur effect based on the "scanned-ness"
float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2 float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2
float[] matrix = { float[] matrix = {
blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity,
blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity,
blurIntensity, blurIntensity, blurIntensity blurIntensity, blurIntensity, blurIntensity
}; };
BufferedImageOp blurOp = new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); BufferedImageOp blurOp =
destinationImage = blurOp.filter(destinationImage, null); new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null);
destinationImage = blurOp.filter(destinationImage, null);
// Add noise to the image based on the "dirtiness" // Add noise to the image based on the "dirtiness"
Random random = new SecureRandom(); Random random = new SecureRandom();
for (int y = 0; y < destinationImage.getHeight(); y++) { for (int y = 0; y < destinationImage.getHeight(); y++) {
for (int x = 0; x < destinationImage.getWidth(); x++) { for (int x = 0; x < destinationImage.getWidth(); x++) {
if (random.nextInt(100) < dirtiness) { if (random.nextInt(100) < dirtiness) {
// Change the pixel color to black randomly based on the "dirtiness" // Change the pixel color to black randomly based on the "dirtiness"
destinationImage.setRGB(x, y, Color.BLACK.getRGB()); destinationImage.setRGB(x, y, Color.BLACK.getRGB());
} }
} }
} }
// Save the image // Save the image
ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); ImageIO.write(destinationImage, "PNG", new File("scanned-1.png"));
PDDocument documentOut = new PDDocument();
for (int page = 1; page <= document.getNumberOfPages(); ++page) {
BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png"));
// Adjust the dimensions of the page
PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1));
documentOut.addPage(pdPage);
PDDocument documentOut = new PDDocument(); PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim);
for (int page = 1; page <= document.getNumberOfPages(); ++page) PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage);
{
BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png")); // Draw the image with a slight offset and enlarged dimensions
contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2);
// Adjust the dimensions of the page contentStream.close();
PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1)); }
documentOut.addPage(pdPage); ByteArrayOutputStream baos = new ByteArrayOutputStream();
documentOut.save(baos);
PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim); documentOut.close();
PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage);
// Draw the image with a slight offset and enlarged dimensions
contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2);
contentStream.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
documentOut.save(baos);
documentOut.close();
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf";
return WebResponseUtils.boasToWebResponse(baos, outputFilename); return WebResponseUtils.boasToWebResponse(baos, outputFilename);
} }
} }

View File

@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -27,7 +28,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class MetadataController { public class MetadataController {
private String checkUndefined(String entry) { private String checkUndefined(String entry) {
// Check if the string is "undefined" // Check if the string is "undefined"
if ("undefined".equals(entry)) { if ("undefined".equals(entry)) {
@ -36,14 +36,16 @@ public class MetadataController {
} }
// Return the original string if it's not "undefined" // Return the original string if it's not "undefined"
return entry; return entry;
} }
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata") @PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(summary = "Update metadata of a PDF file", @Operation(
description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO") summary = "Update metadata of a PDF file",
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request) throws IOException { description =
"This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request)
throws IOException {
// Extract PDF file from the request object // Extract PDF file from the request object
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
@ -61,8 +63,8 @@ public class MetadataController {
// Extract additional custom parameters // Extract additional custom parameters
Map<String, String> allRequestParams = request.getAllRequestParams(); Map<String, String> allRequestParams = request.getAllRequestParams();
if(allRequestParams == null) { if (allRequestParams == null) {
allRequestParams = new java.util.HashMap<String, String>(); allRequestParams = new java.util.HashMap<String, String>();
} }
// Load the PDF file into a PDDocument // Load the PDF file into a PDDocument
PDDocument document = PDDocument.load(pdfFile.getBytes()); PDDocument document = PDDocument.load(pdfFile.getBytes());
@ -89,7 +91,9 @@ public class MetadataController {
} }
// Remove metadata from the PDF history // Remove metadata from the PDF history
document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata")); document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata"));
document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("PieceInfo")); document.getDocumentCatalog()
.getCOSObject()
.removeItem(COSName.getPDFName("PieceInfo"));
author = null; author = null;
creationDate = null; creationDate = null;
creator = null; creator = null;
@ -104,9 +108,17 @@ public class MetadataController {
for (Entry<String, String> entry : allRequestParams.entrySet()) { for (Entry<String, String> entry : allRequestParams.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
// Check if the key is a standard metadata key // Check if the key is a standard metadata key
if (!key.equalsIgnoreCase("Author") && !key.equalsIgnoreCase("CreationDate") && !key.equalsIgnoreCase("Creator") && !key.equalsIgnoreCase("Keywords") if (!key.equalsIgnoreCase("Author")
&& !key.equalsIgnoreCase("modificationDate") && !key.equalsIgnoreCase("Producer") && !key.equalsIgnoreCase("Subject") && !key.equalsIgnoreCase("Title") && !key.equalsIgnoreCase("CreationDate")
&& !key.equalsIgnoreCase("Trapped") && !key.contains("customKey") && !key.contains("customValue")) { && !key.equalsIgnoreCase("Creator")
&& !key.equalsIgnoreCase("Keywords")
&& !key.equalsIgnoreCase("modificationDate")
&& !key.equalsIgnoreCase("Producer")
&& !key.equalsIgnoreCase("Subject")
&& !key.equalsIgnoreCase("Title")
&& !key.equalsIgnoreCase("Trapped")
&& !key.contains("customKey")
&& !key.contains("customValue")) {
info.setCustomMetadataValue(key, entry.getValue()); info.setCustomMetadataValue(key, entry.getValue());
} else if (key.contains("customKey")) { } else if (key.contains("customKey")) {
int number = Integer.parseInt(key.replaceAll("\\D", "")); int number = Integer.parseInt(key.replaceAll("\\D", ""));
@ -119,7 +131,8 @@ public class MetadataController {
if (creationDate != null && creationDate.length() > 0) { if (creationDate != null && creationDate.length() > 0) {
Calendar creationDateCal = Calendar.getInstance(); Calendar creationDateCal = Calendar.getInstance();
try { try {
creationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); creationDateCal.setTime(
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
} catch (ParseException e) { } catch (ParseException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -130,7 +143,8 @@ public class MetadataController {
if (modificationDate != null && modificationDate.length() > 0) { if (modificationDate != null && modificationDate.length() > 0) {
Calendar modificationDateCal = Calendar.getInstance(); Calendar modificationDateCal = Calendar.getInstance();
try { try {
modificationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); modificationDateCal.setTime(
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
} catch (ParseException e) { } catch (ParseException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -147,7 +161,8 @@ public class MetadataController {
info.setTrapped(trapped); info.setTrapped(trapped);
document.setDocumentInformation(info); document.setDocumentInformation(info);
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf");
} }
} }

View File

@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -44,14 +45,21 @@ public class OCRController {
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) return Arrays.stream(files)
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); .filter(file -> file.getName().endsWith(".traineddata"))
.map(file -> file.getName().replace(".traineddata", ""))
.filter(lang -> !lang.equalsIgnoreCase("osd"))
.collect(Collectors.toList());
} }
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(summary = "Process a PDF file with OCR", @Operation(
description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional") summary = "Process a PDF file with OCR",
public ResponseEntity<byte[]> processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { description =
"This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR(
@ModelAttribute ProcessPdfWithOcrRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
List<String> selectedLanguages = request.getLanguages(); List<String> selectedLanguages = request.getLanguages();
Boolean sidecar = request.isSidecar(); Boolean sidecar = request.isSidecar();
@ -65,16 +73,17 @@ public class OCRController {
if (selectedLanguages == null || selectedLanguages.isEmpty()) { if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language."); throw new IOException("Please select at least one language.");
} }
if(!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) { if (!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) {
throw new IOException("ocrRenderType wrong"); throw new IOException("ocrRenderType wrong");
} }
// Get available Tesseract languages // Get available Tesseract languages
List<String> availableLanguages = getAvailableTesseractLanguages(); List<String> availableLanguages = getAvailableTesseractLanguages();
// Validate selected languages // Validate selected languages
selectedLanguages = selectedLanguages.stream().filter(availableLanguages::contains).toList(); selectedLanguages =
selectedLanguages.stream().filter(availableLanguages::contains).toList();
if (selectedLanguages.isEmpty()) { if (selectedLanguages.isEmpty()) {
throw new IOException("None of the selected languages are valid."); throw new IOException("None of the selected languages are valid.");
@ -92,8 +101,16 @@ public class OCRController {
// Run OCR Command // Run OCR Command
String languageOption = String.join("+", selectedLanguages); String languageOption = String.join("+", selectedLanguages);
List<String> command =
List<String> command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf", "--pdf-renderer" , ocrRenderType)); new ArrayList<>(
Arrays.asList(
"ocrmypdf",
"--verbose",
"2",
"--output-type",
"pdf",
"--pdf-renderer",
ocrRenderType));
if (sidecar != null && sidecar) { if (sidecar != null && sidecar) {
sidecarTextPath = Files.createTempFile("sidecar", ".txt"); sidecarTextPath = Files.createTempFile("sidecar", ".txt");
@ -120,42 +137,61 @@ public class OCRController {
} }
} }
command.addAll(Arrays.asList("--language", languageOption, tempInputFile.toString(), tempOutputFile.toString())); command.addAll(
Arrays.asList(
"--language",
languageOption,
tempInputFile.toString(),
tempOutputFile.toString()));
// Run CLI command // Run CLI command
ProcessExecutorResult result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); ProcessExecutorResult result =
if(result.getRc() != 0 && result.getMessages().contains("multiprocessing/synchronize.py") && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) { ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
command.add("--jobs"); .runCommandWithOutputHandling(command);
command.add("1"); if (result.getRc() != 0
result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); && result.getMessages().contains("multiprocessing/synchronize.py")
&& result.getMessages().contains("OSError: [Errno 38] Function not implemented")) {
command.add("--jobs");
command.add("1");
result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
.runCommandWithOutputHandling(command);
} }
// Remove images from the OCR processed PDF if the flag is set to true // Remove images from the OCR processed PDF if the flag is set to true
if (removeImagesAfter != null && removeImagesAfter) { if (removeImagesAfter != null && removeImagesAfter) {
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf"); Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
List<String> gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString()); List<String> gsCommand =
Arrays.asList(
"gs",
"-sDEVICE=pdfwrite",
"-dFILTERIMAGE",
"-o",
tempPdfWithoutImages.toString(),
tempOutputFile.toString());
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand); ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(gsCommand);
tempOutputFile = tempPdfWithoutImages; tempOutputFile = tempPdfWithoutImages;
} }
// Read the OCR processed PDF file // Read the OCR processed PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
// Clean up the temporary files // Clean up the temporary files
Files.delete(tempInputFile); Files.delete(tempInputFile);
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf";
if (sidecar != null && sidecar) { if (sidecar != null && sidecar) {
// Create a zip file containing both the PDF and the text file // Create a zip file containing both the PDF and the text file
String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; String outputZipFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip"); Path tempZipFile = Files.createTempFile("output_", ".zip");
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add PDF file to the zip // Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(outputFilename); ZipEntry pdfEntry = new ZipEntry(outputFilename);
zipOut.putNextEntry(pdfEntry); zipOut.putNextEntry(pdfEntry);
@ -177,13 +213,12 @@ public class OCRController {
Files.delete(sidecarTextPath); Files.delete(sidecarTextPath);
// Return the zip file containing both the PDF and the text file // Return the zip file containing both the PDF and the text file
return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else { } else {
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }
} }

View File

@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -27,9 +28,9 @@ public class OverlayImageController {
@PostMapping(consumes = "multipart/form-data", value = "/add-image") @PostMapping(consumes = "multipart/form-data", value = "/add-image")
@Operation( @Operation(
summary = "Overlay image onto a PDF file", summary = "Overlay image onto a PDF file",
description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO" description =
) "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO")
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) { public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile(); MultipartFile imageFile = request.getImageFile();
@ -41,7 +42,9 @@ public class OverlayImageController {
byte[] imageBytes = imageFile.getBytes(); byte[] imageBytes = imageFile.getBytes();
byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage); byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage);
return WebResponseUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); return WebResponseUtils.bytesToWebResponse(
result,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to add image to PDF", e); logger.error("Failed to add image to PDF", e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

View File

@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -33,16 +34,20 @@ public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class); private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException { summary = "Add page numbers to a PDF document",
description =
"This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String customMargin = request.getCustomMargin(); String customMargin = request.getCustomMargin();
int position = request.getPosition(); int position = request.getPosition();
int startingNumber = request.getStartingNumber(); int startingNumber = request.getStartingNumber();
String pagesToNumber = request.getPagesToNumber(); String pagesToNumber = request.getPagesToNumber();
String customText = request.getCustomText(); String customText = request.getCustomText();
int pageNumber = startingNumber; int pageNumber = startingNumber;
byte[] fileBytes = file.getBytes(); byte[] fileBytes = file.getBytes();
PDDocument document = PDDocument.load(fileBytes); PDDocument document = PDDocument.load(fileBytes);
float marginFactor; float marginFactor;
@ -58,9 +63,8 @@ public class PageNumbersController {
break; break;
case "x-large": case "x-large":
marginFactor = 0.075f; marginFactor = 0.075f;
break; break;
default: default:
marginFactor = 0.035f; marginFactor = 0.035f;
break; break;
@ -68,19 +72,29 @@ public class PageNumbersController {
float fontSize = 12.0f; float fontSize = 12.0f;
PDType1Font font = PDType1Font.HELVETICA; PDType1Font font = PDType1Font.HELVETICA;
if(pagesToNumber == null || pagesToNumber.length() == 0) { if (pagesToNumber == null || pagesToNumber.length() == 0) {
pagesToNumber = "all"; pagesToNumber = "all";
} }
if(customText == null || customText.length() == 0) { if (customText == null || customText.length() == 0) {
customText = "{n}"; customText = "{n}";
} }
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); List<Integer> pagesToNumberList =
GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
for (int i : pagesToNumberList) { for (int i : pagesToNumberList) {
PDPage page = document.getPage(i); PDPage page = document.getPage(i);
PDRectangle pageSize = page.getMediaBox(); PDRectangle pageSize = page.getMediaBox();
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); String text =
customText != null
? customText
.replace("{n}", String.valueOf(pageNumber))
.replace("{total}", String.valueOf(document.getNumberOfPages()))
.replace(
"{filename}",
file.getOriginalFilename()
.replaceFirst("[.][^.]+$", ""))
: String.valueOf(pageNumber);
float x, y; float x, y;
@ -88,10 +102,10 @@ public class PageNumbersController {
int yGroup = 2 - (position - 1) / 3; int yGroup = 2 - (position - 1) / 3;
switch (xGroup) { switch (xGroup) {
case 0: // left case 0: // left
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
break; break;
case 1: // center case 1: // center
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
break; break;
default: // right default: // right
@ -100,10 +114,10 @@ public class PageNumbersController {
} }
switch (yGroup) { switch (yGroup) {
case 0: // bottom case 0: // bottom
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
break; break;
case 1: // middle case 1: // middle
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
break; break;
default: // top default: // top
@ -111,7 +125,9 @@ public class PageNumbersController {
break; break;
} }
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true);
contentStream.beginText(); contentStream.beginText();
contentStream.setFont(font, fontSize); contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y); contentStream.newLineAtOffset(x, y);
@ -126,10 +142,9 @@ public class PageNumbersController {
document.save(baos); document.save(baos);
document.close(); document.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF); return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf",
MediaType.APPLICATION_PDF);
} }
} }

View File

@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -31,11 +32,12 @@ public class RepairController {
@PostMapping(consumes = "multipart/form-data", value = "/repair") @PostMapping(consumes = "multipart/form-data", value = "/repair")
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException, InterruptedException { public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
MultipartFile inputFile = request.getFileInput(); throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
@ -50,8 +52,9 @@ public class RepairController {
command.add("-sDEVICE=pdfwrite"); command.add("-sDEVICE=pdfwrite");
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
// Read the optimized PDF file // Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
@ -61,8 +64,8 @@ public class RepairController {
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@ -17,47 +17,60 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ShowJavascript { public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript") @PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") @Operation(
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
description = "desc. Input:PDF Output:JS Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception { public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String script = ""; String script = "";
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { 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";
}
}
}
if (script.isEmpty()) { if (document.getDocumentCatalog() != null
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; && 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";
}
}
} }
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js"); if (script.isEmpty()) {
script =
"PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
}
return WebResponseUtils.bytesToWebResponse(
script.getBytes(StandardCharsets.UTF_8),
inputFile.getOriginalFilename() + ".js");
} }
} }
} }

View File

@ -1,9 +1,11 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@ -17,44 +19,39 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import org.slf4j.Logger;
@Service @Service
public class ApiDocService { public class ApiDocService {
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>(); private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class); private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
@Autowired @Autowired private ServletContext servletContext;
private ServletContext servletContext;
private String getApiDocsUrl() { private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getPort(); String port = SPdfApplication.getPort();
return "http://localhost:"+ port + contextPath + "/v1/api-docs"; return "http://localhost:" + port + contextPath + "/v1/api-docs";
} }
@Autowired(required=false) @Autowired(required = false)
private UserServiceInterface userService; private UserServiceInterface userService;
private String getApiKeyForUser() { private String getApiKeyForUser() {
if(userService == null) if (userService == null) return "";
return ""; return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); }
}
JsonNode apiDocsJsonRootNode;
JsonNode apiDocsJsonRootNode;
// @EventListener(ApplicationReadyEvent.class)
private synchronized void loadApiDocumentation() {
//@EventListener(ApplicationReadyEvent.class) String apiDocsJson = "";
private synchronized void loadApiDocumentation() {
String apiDocsJson = "";
try { try {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
@ -64,49 +61,52 @@ public class ApiDocService {
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); ResponseEntity<String> response =
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
apiDocsJson = response.getBody(); apiDocsJson = response.getBody();
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
apiDocsJsonRootNode = mapper.readTree(apiDocsJson); apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
JsonNode paths = apiDocsJsonRootNode.path("paths"); JsonNode paths = apiDocsJsonRootNode.path("paths");
paths.fields().forEachRemaining(entry -> { paths.fields()
String path = entry.getKey(); .forEachRemaining(
JsonNode pathNode = entry.getValue(); entry -> {
if (pathNode.has("post")) { String path = entry.getKey();
JsonNode postNode = pathNode.get("post"); JsonNode pathNode = entry.getValue();
ApiEndpoint endpoint = new ApiEndpoint(path, postNode); if (pathNode.has("post")) {
apiDocumentation.put(path, endpoint); JsonNode postNode = pathNode.get("post");
} ApiEndpoint endpoint = new ApiEndpoint(path, postNode);
}); apiDocumentation.put(path, endpoint);
}
});
} catch (Exception e) { } catch (Exception e) {
// Handle exceptions // Handle exceptions
logger.error("Error grabbing swagger doc, body result {}", apiDocsJson); logger.error("Error grabbing swagger doc, body result {}", apiDocsJson);
} }
} }
public boolean isValidOperation(String operationName, Map<String, Object> parameters) { public boolean isValidOperation(String operationName, Map<String, Object> parameters) {
if(apiDocumentation.size() == 0) { if (apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
return false; return false;
} }
ApiEndpoint endpoint = apiDocumentation.get(operationName); ApiEndpoint endpoint = apiDocumentation.get(operationName);
return endpoint.areParametersValid(parameters); return endpoint.areParametersValid(parameters);
} }
public boolean isMultiInput(String operationName) { public boolean isMultiInput(String operationName) {
if(apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
return false; return false;
} }
ApiEndpoint endpoint = apiDocumentation.get(operationName); ApiEndpoint endpoint = apiDocumentation.get(operationName);
String description = endpoint.getDescription(); String description = endpoint.getDescription();
Pattern pattern = Pattern.compile("Type:(\\w+)"); Pattern pattern = Pattern.compile("Type:(\\w+)");
Matcher matcher = pattern.matcher(description); Matcher matcher = pattern.matcher(description);
@ -115,9 +115,8 @@ public class ApiDocService {
return type.startsWith("MI"); return type.startsWith("MI");
} }
return false; return false;
} }
} }
// Model class for API Endpoint // Model class for API Endpoint

View File

@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.model.api.HandleDataRequest;
@ -34,84 +35,80 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Pipeline", description = "Pipeline APIs") @Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController { public class PipelineController {
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired @Autowired PipelineProcessor processor;
PipelineProcessor processor;
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@Autowired
private ObjectMapper objectMapper;
@PostMapping("/handleData") @Autowired private ObjectMapper objectMapper;
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) throws JsonMappingException, JsonProcessingException {
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
MultipartFile[] files = request.getFileInput(); @PostMapping("/handleData")
String jsonString = request.getJson(); public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
if (files == null) { throws JsonMappingException, JsonProcessingException {
return null; if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
} return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); }
logger.info("Received POST request to /handleData with {} files", files.length);
try { MultipartFile[] files = request.getFileInput();
List<Resource> inputFiles = processor.generateInputFiles(files); String jsonString = request.getJson();
if(inputFiles == null || inputFiles.size() == 0) { if (files == null) {
return null; return null;
}
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class);
logger.info("Received POST request to /handleData with {} files", files.length);
try {
List<Resource> inputFiles = processor.generateInputFiles(files);
if (inputFiles == null || inputFiles.size() == 0) {
return null;
} }
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
if (outputFiles != null && outputFiles.size() == 1) { if (outputFiles != null && outputFiles.size() == 1) {
// If there is only one file, return it directly // If there is only one file, return it directly
Resource singleFile = outputFiles.get(0); Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream(); InputStream is = singleFile.getInputStream();
byte[] bytes = new byte[(int) singleFile.contentLength()]; byte[] bytes = new byte[(int) singleFile.contentLength()];
is.read(bytes); is.read(bytes);
is.close(); is.close();
logger.info("Returning single file response..."); logger.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), return WebResponseUtils.bytesToWebResponse(
MediaType.APPLICATION_OCTET_STREAM); bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
} else if (outputFiles == null) { } else if (outputFiles == null) {
return null; return null;
} }
// Create a ByteArrayOutputStream to hold the zip // Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos); ZipOutputStream zipOut = new ZipOutputStream(baos);
// Loop through each file and add it to the zip // Loop through each file and add it to the zip
for (Resource file : outputFiles) { for (Resource file : outputFiles) {
ZipEntry zipEntry = new ZipEntry(file.getFilename()); ZipEntry zipEntry = new ZipEntry(file.getFilename());
zipOut.putNextEntry(zipEntry); zipOut.putNextEntry(zipEntry);
// Read the file into a byte array // Read the file into a byte array
InputStream is = file.getInputStream(); InputStream is = file.getInputStream();
byte[] bytes = new byte[(int) file.contentLength()]; byte[] bytes = new byte[(int) file.contentLength()];
is.read(bytes); is.read(bytes);
// Write the bytes of the file to the zip // Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length); zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry(); zipOut.closeEntry();
is.close(); is.close();
} }
zipOut.close(); zipOut.close();
logger.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
logger.error("Error handling data: ", e);
return null;
}
}
logger.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse(
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
logger.error("Error handling data: ", e);
return null;
}
}
} }

View File

@ -33,50 +33,48 @@ import stirling.software.SPDF.model.PipelineOperation;
@Service @Service
public class PipelineDirectoryProcessor { public class PipelineDirectoryProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class); private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
@Autowired @Autowired private ObjectMapper objectMapper;
private ObjectMapper objectMapper; @Autowired private ApiDocService apiDocService;
@Autowired @Autowired private ApplicationProperties applicationProperties;
private ApiDocService apiDocService;
@Autowired
private ApplicationProperties applicationProperties;
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired @Autowired PipelineProcessor processor;
PipelineProcessor processor;
@Scheduled(fixedRate = 60000) @Scheduled(fixedRate = 60000)
public void scanFolders() { public void scanFolders() {
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
return; return;
} }
Path watchedFolderPath = Paths.get(watchedFoldersDir); Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) { if (!Files.exists(watchedFolderPath)) {
try { try {
Files.createDirectories(watchedFolderPath); Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath); logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e); logger.error("Error creating directory: {}", watchedFolderPath, e);
return; return;
} }
} }
try (Stream<Path> paths = Files.walk(watchedFolderPath)) { try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> { paths.filter(Files::isDirectory)
try { .forEach(
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { t -> {
handleDirectory(t); try {
} if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
} catch (Exception e) { handleDirectory(t);
logger.error("Error handling directory: {}", t, e); }
} } catch (Exception e) {
}); logger.error("Error handling directory: {}", t, e);
} catch (Exception e) { }
logger.error("Error walking through directory: {}", watchedFolderPath, e); });
} } catch (Exception e) {
} logger.error("Error walking through directory: {}", watchedFolderPath, e);
}
}
public void handleDirectory(Path dir) throws IOException { public void handleDirectory(Path dir) throws IOException {
logger.info("Handling directory: {}", dir); logger.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir); Path processingDir = createProcessingDirectory(dir);
@ -113,13 +111,14 @@ public class PipelineDirectoryProcessor {
return objectMapper.readValue(jsonString, PipelineConfig.class); return objectMapper.readValue(jsonString, PipelineConfig.class);
} }
private void processPipelineOperations(Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException { private void processPipelineOperations(
Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException {
for (PipelineOperation operation : config.getOperations()) { for (PipelineOperation operation : config.getOperations()) {
validateOperation(operation); validateOperation(operation);
File[] files = collectFilesForProcessing(dir, jsonFile, operation); File[] files = collectFilesForProcessing(dir, jsonFile, operation);
if(files == null || files.length == 0) { if (files == null || files.length == 0) {
logger.debug("No files detected for {} ", dir); logger.debug("No files detected for {} ", dir);
return; return;
} }
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir); List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir); runPipelineAgainstFiles(filesToProcess, config, dir, processingDir);
@ -132,20 +131,22 @@ public class PipelineDirectoryProcessor {
} }
} }
private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) throws IOException { private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation)
throws IOException {
try (Stream<Path> paths = Files.list(dir)) { try (Stream<Path> paths = Files.list(dir)) {
if ("automated".equals(operation.getParameters().get("fileInput"))) { if ("automated".equals(operation.getParameters().get("fileInput"))) {
return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile)) return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile))
.map(Path::toFile) .map(Path::toFile)
.toArray(File[]::new); .toArray(File[]::new);
} else { } else {
String fileInput = (String) operation.getParameters().get("fileInput"); String fileInput = (String) operation.getParameters().get("fileInput");
return new File[]{new File(fileInput)}; return new File[] {new File(fileInput)};
} }
} }
} }
private List<File> prepareFilesForProcessing(File[] files, Path processingDir) throws IOException { private List<File> prepareFilesForProcessing(File[] files, Path processingDir)
throws IOException {
List<File> filesToProcess = new ArrayList<>(); List<File> filesToProcess = new ArrayList<>();
for (File file : files) { for (File file : files) {
Path targetPath = resolveUniqueFilePath(processingDir, file.getName()); Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
@ -173,27 +174,33 @@ public class PipelineDirectoryProcessor {
if (dotIndex == -1) { if (dotIndex == -1) {
return originalFileName + suffix; return originalFileName + suffix;
} else { } else {
return originalFileName.substring(0, dotIndex) + suffix + originalFileName.substring(dotIndex); return originalFileName.substring(0, dotIndex)
+ suffix
+ originalFileName.substring(dotIndex);
} }
} }
private void runPipelineAgainstFiles(List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir) throws IOException { private void runPipelineAgainstFiles(
List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir)
throws IOException {
try { try {
List<Resource> inputFiles = processor.generateInputFiles(filesToProcess.toArray(new File[0])); List<Resource> inputFiles =
if(inputFiles == null || inputFiles.size() == 0) { processor.generateInputFiles(filesToProcess.toArray(new File[0]));
return; if (inputFiles == null || inputFiles.size() == 0) {
return;
} }
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
if (outputFiles == null) return; if (outputFiles == null) return;
moveAndRenameFiles(outputFiles, config, dir); moveAndRenameFiles(outputFiles, config, dir);
deleteOriginalFiles(filesToProcess, processingDir); deleteOriginalFiles(filesToProcess, processingDir);
} catch (Exception e) { } catch (Exception e) {
logger.error("error during processing", e); logger.error("error during processing", e);
moveFilesBack(filesToProcess, processingDir); moveFilesBack(filesToProcess, processingDir);
} }
} }
private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir) throws IOException { private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir)
throws IOException {
for (Resource resource : resources) { for (Resource resource : resources) {
String outputFileName = createOutputFileName(resource, config); String outputFileName = createOutputFileName(resource, config);
Path outputPath = determineOutputPath(config, dir); Path outputPath = determineOutputPath(config, dir);
@ -217,26 +224,36 @@ public class PipelineDirectoryProcessor {
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.')); String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1); String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
String outputFileName = config.getOutputPattern() String outputFileName =
.replace("{filename}", baseName) config.getOutputPattern()
.replace("{pipelineName}", config.getName()) .replace("{filename}", baseName)
.replace("{date}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))) .replace("{pipelineName}", config.getName())
.replace("{time}", LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss"))) .replace(
+ "." + extension; "{date}",
LocalDate.now()
.format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.replace(
"{time}",
LocalTime.now()
.format(DateTimeFormatter.ofPattern("HHmmss")))
+ "."
+ extension;
return outputFileName; return outputFileName;
} }
private Path determineOutputPath(PipelineConfig config, Path dir) { private Path determineOutputPath(PipelineConfig config, Path dir) {
String outputDir = config.getOutputDir() String outputDir =
.replace("{outputFolder}", finishedFoldersDir) config.getOutputDir()
.replace("{folderName}", dir.toString()) .replace("{outputFolder}", finishedFoldersDir)
.replaceAll("\\\\?watchedFolders", ""); .replace("{folderName}", dir.toString())
.replaceAll("\\\\?watchedFolders", "");
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
} }
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir) throws IOException { private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
throws IOException {
for (File file : filesToProcess) { for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName())); Files.deleteIfExists(processingDir.resolve(file.getName()));
logger.info("Deleted original file: {}", file.getName()); logger.info("Deleted original file: {}", file.getName());
@ -247,12 +264,13 @@ public class PipelineDirectoryProcessor {
for (File file : filesToProcess) { for (File file : filesToProcess) {
try { try {
Files.move(processingDir.resolve(file.getName()), file.toPath()); Files.move(processingDir.resolve(file.getName()), file.toPath());
logger.info("Moved file back to original location: {} , {}",file.toPath(), file.getName()); logger.info(
"Moved file back to original location: {} , {}",
file.toPath(),
file.getName());
} catch (IOException e) { } catch (IOException e) {
logger.error("Error moving file back to original location: {}", file.getName(), e); logger.error("Error moving file back to original location: {}", file.getName(), e);
} }
} }
} }
} }

View File

@ -34,7 +34,6 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
@ -43,152 +42,160 @@ import stirling.software.SPDF.model.Role;
@Service @Service
public class PipelineProcessor { public class PipelineProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class); private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
@Autowired private ApiDocService apiDocService;
@Autowired @Autowired(required = false)
private ApiDocService apiDocService;
@Autowired(required=false)
private UserServiceInterface userService; private UserServiceInterface userService;
@Autowired
private ServletContext servletContext;
@Autowired private ServletContext servletContext;
private String getApiKeyForUser() {
if (userService == null) return "";
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
private String getApiKeyForUser() { private String getBaseUrl() {
if (userService == null) String contextPath = servletContext.getContextPath();
return ""; String port = SPdfApplication.getPort();
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
return "http://localhost:" + port + contextPath + "/";
}
private String getBaseUrl() { List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
String contextPath = servletContext.getContextPath(); throws Exception {
String port = SPdfApplication.getPort();
return "http://localhost:" + port + contextPath + "/"; ByteArrayOutputStream logStream = new ByteArrayOutputStream();
} PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config) throws Exception {
ByteArrayOutputStream logStream = new ByteArrayOutputStream(); for (PipelineOperation pipelineOperation : config.getOperations()) {
PrintStream logPrintStream = new PrintStream(logStream); String operation = pipelineOperation.getOperation();
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
boolean hasErrors = false; logger.info(
"Running operation: {} isMultiInputOperation {}",
operation,
isMultiInputOperation);
Map<String, Object> parameters = pipelineOperation.getParameters();
String inputFileExtension = "";
for (PipelineOperation pipelineOperation : config.getOperations()) { // TODO
String operation = pipelineOperation.getOperation(); // if (operationNode.has("inputFileType")) {
boolean isMultiInputOperation = apiDocService.isMultiInput(operation); // inputFileExtension = operationNode.get("inputFileType").asText();
// } else {
inputFileExtension = ".pdf";
// }
final String finalInputFileExtension = inputFileExtension;
logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation); String url = getBaseUrl() + operation;
Map<String, Object> parameters = pipelineOperation.getParameters();
String inputFileExtension = "";
//TODO
//if (operationNode.has("inputFileType")) {
// inputFileExtension = operationNode.get("inputFileType").asText();
//} else {
inputFileExtension = ".pdf";
//}
final String finalInputFileExtension = inputFileExtension;
String url = getBaseUrl() + operation;
List<Resource> newOutputFiles = new ArrayList<>();
if (!isMultiInputOperation) {
for (Resource file : outputFiles) {
boolean hasInputFileType = false;
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
List<Resource> newOutputFiles = new ArrayList<>();
for(Entry<String, Object> entry : parameters.entrySet()) { if (!isMultiInputOperation) {
body.add(entry.getKey(), entry.getValue()); for (Resource file : outputFiles) {
} boolean hasInputFileType = false;
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
ResponseEntity<byte[]> response = sendWebRequest(url, body); for (Entry<String, Object> entry : parameters.entrySet()) {
body.add(entry.getKey(), entry.getValue());
}
// If the operation is filter and the response body is null or empty, skip this ResponseEntity<byte[]> response = sendWebRequest(url, body);
// file
if (operation.startsWith("filter-")
&& (response.getBody() == null || response.getBody().length == 0)) {
logger.info("Skipping file due to failing {}", operation);
continue;
}
if (!response.getStatusCode().equals(HttpStatus.OK)) { // If the operation is filter and the response body is null or empty, skip
logPrintStream.println("Error: " + response.getBody()); // this
hasErrors = true; // file
continue; if (operation.startsWith("filter-")
} && (response.getBody() == null || response.getBody().length == 0)) {
processOutputFiles(operation, file.getFilename(), response, newOutputFiles); logger.info("Skipping file due to failing {}", operation);
continue;
} }
if (!hasInputFileType) { if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println( logPrintStream.println("Error: " + response.getBody());
"No files with extension " + inputFileExtension + " found for operation " + operation); hasErrors = true;
hasErrors = true; continue;
} }
processOutputFiles(operation, file.getFilename(), response, newOutputFiles);
}
outputFiles = newOutputFiles; if (!hasInputFileType) {
} logPrintStream.println(
"No files with extension "
+ inputFileExtension
+ " found for operation "
+ operation);
hasErrors = true;
}
} else { outputFiles = newOutputFiles;
// Filter and collect all files that match the inputFileExtension }
List<Resource> matchingFiles = outputFiles.stream()
.filter(file -> file.getFilename().endsWith(finalInputFileExtension))
.collect(Collectors.toList());
// Check if there are matching files } else {
if (!matchingFiles.isEmpty()) { // Filter and collect all files that match the inputFileExtension
// Create a new MultiValueMap for the request body List<Resource> matchingFiles =
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); outputFiles.stream()
.filter(
file ->
file.getFilename()
.endsWith(finalInputFileExtension))
.collect(Collectors.toList());
// Add all matching files to the body // Check if there are matching files
for (Resource file : matchingFiles) { if (!matchingFiles.isEmpty()) {
body.add("fileInput", file); // Create a new MultiValueMap for the request body
} MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
for(Entry<String, Object> entry : parameters.entrySet()) { // Add all matching files to the body
body.add(entry.getKey(), entry.getValue()); for (Resource file : matchingFiles) {
} body.add("fileInput", file);
}
ResponseEntity<byte[]> response = sendWebRequest(url, body);
// Handle the response for (Entry<String, Object> entry : parameters.entrySet()) {
if (response.getStatusCode().equals(HttpStatus.OK)) { body.add(entry.getKey(), entry.getValue());
processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles); }
} else {
// Log error if the response status is not OK
logPrintStream.println("Error in multi-input operation: " + response.getBody());
hasErrors = true;
}
} else {
logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation);
hasErrors = true;
}
}
logPrintStream.close();
} ResponseEntity<byte[]> response = sendWebRequest(url, body);
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
return outputFiles;
}
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body ){ // Handle the response
RestTemplate restTemplate = new RestTemplate(); if (response.getStatusCode().equals(HttpStatus.OK)) {
processOutputFiles(
// Set up headers, including API key operation,
matchingFiles.get(0).getFilename(),
response,
newOutputFiles);
} else {
// Log error if the response status is not OK
logPrintStream.println(
"Error in multi-input operation: " + response.getBody());
hasErrors = true;
}
} else {
logPrintStream.println(
"No files with extension "
+ inputFileExtension
+ " found for multi-input operation "
+ operation);
hasErrors = true;
}
}
logPrintStream.close();
}
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
return outputFiles;
}
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
headers.add("X-API-Key", apiKey); headers.add("X-API-Key", apiKey);
@ -199,134 +206,141 @@ public class PipelineProcessor {
// Make the request to the REST endpoint // Make the request to the REST endpoint
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
} }
private List<Resource> processOutputFiles(String operation, String fileName, ResponseEntity<byte[]> response, List<Resource> newOutputFiles) throws IOException{
// Define filename
String newFilename;
if ("auto-rename".equals(operation)) {
// If the operation is "auto-rename", generate a new filename.
// This is a simple example of generating a filename using current timestamp.
// Modify as per your needs.
newFilename = "file_" + System.currentTimeMillis();
} else {
// Otherwise, keep the original filename.
newFilename = fileName;
}
// Check if the response body is a zip file private List<Resource> processOutputFiles(
if (isZip(response.getBody())) { String operation,
// Unzip the file and add all the files to the new output files String fileName,
newOutputFiles.addAll(unzip(response.getBody())); ResponseEntity<byte[]> response,
} else { List<Resource> newOutputFiles)
Resource outputResource = new ByteArrayResource(response.getBody()) { throws IOException {
@Override // Define filename
public String getFilename() { String newFilename;
return newFilename; if ("auto-rename".equals(operation)) {
} // If the operation is "auto-rename", generate a new filename.
}; // This is a simple example of generating a filename using current timestamp.
newOutputFiles.add(outputResource); // Modify as per your needs.
} newFilename = "file_" + System.currentTimeMillis();
} else {
return newOutputFiles; // Otherwise, keep the original filename.
newFilename = fileName;
} }
List<Resource> generateInputFiles(File[] files) throws Exception {
if (files == null || files.length == 0) {
logger.info("No files");
return null;
}
// Check if the response body is a zip file
List<Resource> outputFiles = new ArrayList<>(); if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource =
new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return newFilename;
}
};
newOutputFiles.add(outputResource);
}
for (File file : files) { return newOutputFiles;
Path path = Paths.get(file.getAbsolutePath()); }
logger.info("Reading file: " + path); // debug statement
if (Files.exists(path)) { List<Resource> generateInputFiles(File[] files) throws Exception {
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { if (files == null || files.length == 0) {
@Override logger.info("No files");
public String getFilename() { return null;
return file.getName(); }
}
};
outputFiles.add(fileResource);
} else {
logger.info("File not found: " + path);
}
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
List<Resource> generateInputFiles(MultipartFile[] files) throws Exception { List<Resource> outputFiles = new ArrayList<>();
if (files == null || files.length == 0) {
logger.info("No files");
return null;
}
List<Resource> outputFiles = new ArrayList<>(); for (File file : files) {
Path path = Paths.get(file.getAbsolutePath());
logger.info("Reading file: " + path); // debug statement
for (MultipartFile file : files) { if (Files.exists(path)) {
Resource fileResource = new ByteArrayResource(file.getBytes()) { Resource fileResource =
@Override new ByteArrayResource(Files.readAllBytes(path)) {
public String getFilename() { @Override
return file.getOriginalFilename(); public String getFilename() {
} return file.getName();
}; }
outputFiles.add(fileResource); };
} outputFiles.add(fileResource);
logger.info("Files successfully loaded. Starting processing..."); } else {
return outputFiles; logger.info("File not found: " + path);
} }
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
private boolean isZip(byte[] data) { List<Resource> generateInputFiles(MultipartFile[] files) throws Exception {
if (data == null || data.length < 4) { if (files == null || files.length == 0) {
return false; logger.info("No files");
} return null;
}
// Check the first four bytes of the data against the standard zip magic number List<Resource> outputFiles = new ArrayList<>();
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException { for (MultipartFile file : files) {
logger.info("Unzipping data of length: {}", data.length); Resource fileResource =
List<Resource> unzippedFiles = new ArrayList<>(); new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(data); private boolean isZip(byte[] data) {
ZipInputStream zis = new ZipInputStream(bais)) { if (data == null || data.length < 4) {
return false;
}
ZipEntry entry; // Check the first four bytes of the data against the standard zip magic number
while ((entry = zis.getNextEntry()) != null) { return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
ByteArrayOutputStream baos = new ByteArrayOutputStream(); }
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) { private List<Resource> unzip(byte[] data) throws IOException {
baos.write(buffer, 0, count); logger.info("Unzipping data of length: {}", data.length);
} List<Resource> unzippedFiles = new ArrayList<>();
final String filename = entry.getName(); try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
Resource fileResource = new ByteArrayResource(baos.toByteArray()) { ZipInputStream zis = new ZipInputStream(bais)) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it ZipEntry entry;
if (isZip(baos.toByteArray())) { while ((entry = zis.getNextEntry()) != null) {
logger.info("File {} is a zip file. Unzipping...", filename); ByteArrayOutputStream baos = new ByteArrayOutputStream();
unzippedFiles.addAll(unzip(baos.toByteArray())); byte[] buffer = new byte[1024];
} else { int count;
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); while ((count = zis.read(buffer)) != -1) {
return unzippedFiles; baos.write(buffer, 0, count);
} }
final String filename = entry.getName();
Resource fileResource =
new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
public interface UserServiceInterface { public interface UserServiceInterface {
String getApiKeyForUser(String username); String getApiKeyForUser(String username);
} }

View File

@ -53,6 +53,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -61,198 +62,228 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class CertSignController { public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static { static {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @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") @Operation(
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { summary = "Sign PDF with a Digital Certificate",
MultipartFile pdf = request.getFileInput(); description =
String certType = request.getCertType(); "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")
MultipartFile privateKeyFile = request.getPrivateKeyFile(); public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
MultipartFile certFile = request.getCertFile(); throws Exception {
MultipartFile p12File = request.getP12File(); MultipartFile pdf = request.getFileInput();
String password = request.getPassword(); String certType = request.getCertType();
Boolean showSignature = request.isShowSignature(); MultipartFile privateKeyFile = request.getPrivateKeyFile();
String reason = request.getReason(); MultipartFile certFile = request.getCertFile();
String location = request.getLocation(); MultipartFile p12File = request.getP12File();
String name = request.getName(); String password = request.getPassword();
Integer pageNumber = request.getPageNumber(); Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber();
PrivateKey privateKey = null; PrivateKey privateKey = null;
X509Certificate cert = null; X509Certificate cert = null;
if (certType != null) { if (certType != null) {
logger.info("Cert type provided: {}", certType); logger.info("Cert type provided: {}", certType);
switch (certType) { switch (certType) {
case "PKCS12": case "PKCS12":
if (p12File != null) { if (p12File != null) {
KeyStore ks = KeyStore.getInstance("PKCS12"); KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); ks.load(
String alias = ks.aliases().nextElement(); new ByteArrayInputStream(p12File.getBytes()),
if (!ks.isKeyEntry(alias)) { password.toCharArray());
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key."); String alias = ks.aliases().nextElement();
} if (!ks.isKeyEntry(alias)) {
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); throw new IllegalArgumentException(
cert = (X509Certificate) ks.getCertificate(alias); "The provided PKCS12 file does not contain a private key.");
} }
break; privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
case "PEM": cert = (X509Certificate) ks.getCertificate(alias);
if (privateKeyFile != null && certFile != null) { }
// Load private key break;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); case "PEM":
if (isPEM(privateKeyFile.getBytes())) { if (privateKeyFile != null && certFile != null) {
privateKey = keyFactory // Load private key
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); KeyFactory keyFactory =
} else { KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); if (isPEM(privateKeyFile.getBytes())) {
} privateKey =
keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(
parsePEM(privateKeyFile.getBytes())));
} else {
privateKey =
keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
}
// Load certificate // Load certificate
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", CertificateFactory certFactory =
BouncyCastleProvider.PROVIDER_NAME); CertificateFactory.getInstance(
if (isPEM(certFile.getBytes())) { "X.509", BouncyCastleProvider.PROVIDER_NAME);
cert = (X509Certificate) certFactory if (isPEM(certFile.getBytes())) {
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); cert =
} else { (X509Certificate)
cert = (X509Certificate) certFactory certFactory.generateCertificate(
.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); new ByteArrayInputStream(
} parsePEM(certFile.getBytes())));
} } else {
break; cert =
} (X509Certificate)
} certFactory.generateCertificate(
PDSignature signature = new PDSignature(); new ByteArrayInputStream(certFile.getBytes()));
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter }
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); }
signature.setName(name); break;
signature.setLocation(location); }
signature.setReason(reason); }
signature.setSignDate(Calendar.getInstance()); PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
// Load the PDF signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
try (PDDocument document = PDDocument.load(pdf.getBytes())) { signature.setName(name);
logger.info("Successfully loaded the provided PDF"); signature.setLocation(location);
SignatureOptions signatureOptions = new SignatureOptions(); signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
// If you want to show the signature // Load the PDF
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
logger.info("Successfully loaded the provided PDF");
SignatureOptions signatureOptions = new SignatureOptions();
// ATTEMPT 2 // If you want to show the signature
if (showSignature != null && showSignature) {
PDPage page = document.getPage(pageNumber - 1);
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); // ATTEMPT 2
if (acroForm == null) { if (showSignature != null && showSignature) {
acroForm = new PDAcroForm(document); PDPage page = document.getPage(pageNumber - 1);
document.getDocumentCatalog().setAcroForm(acroForm);
}
// Create a new signature field and widget PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm == null) {
acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
PDSignatureField signatureField = new PDSignatureField(acroForm); // Create a new signature field and widget
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
widget.setRectangle(rect);
page.getAnnotations().add(widget);
// Set the appearance for the signature field PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDAppearanceStream appearanceStream = new PDAppearanceStream(document); PDRectangle rect =
appearanceStream.setResources(new PDResources()); new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
appearanceStream.setBBox(rect); widget.setRectangle(rect);
appearanceDict.setNormalAppearance(appearanceStream); page.getAnnotations().add(widget);
widget.setAppearance(appearanceDict);
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) { // Set the appearance for the signature field
contentStream.beginText(); PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
contentStream.newLineAtOffset(110, 130); appearanceStream.setResources(new PDResources());
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown")); appearanceStream.setBBox(rect);
contentStream.newLineAtOffset(0, -15); appearanceDict.setNormalAppearance(appearanceStream);
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date())); widget.setAppearance(appearanceDict);
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();
}
// Add the widget annotation to the page try (PDPageContentStream contentStream =
page.getAnnotations().add(widget); 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();
}
// Add the signature field to the acroform // Add the widget annotation to the page
acroForm.getFields().add(signatureField); page.getAnnotations().add(widget);
// Handle multiple signatures by ensuring a unique field name // Add the signature field to the acroform
String baseFieldName = "Signature"; acroForm.getFields().add(signatureField);
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());
byte[] content = IOUtils.toByteArray(externalSigning.getContent()); // 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);
}
// Using BouncyCastle to sign document.addSignature(signature, signatureOptions);
CMSTypedData cmsData = new CMSProcessableByteArray(content); logger.info("Signature added to the PDF document");
// External signing
ExternalSigningSupport externalSigning =
document.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); byte[] content = IOUtils.toByteArray(externalSigning.getContent());
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( // Using BouncyCastle to sign
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()) CMSTypedData cmsData = new CMSProcessableByteArray(content);
.build(signer, cert));
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
CMSSignedData signedData = gen.generate(cmsData, false); ContentSigner signer =
new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(privateKey);
byte[] cmsSignature = signedData.getEncoded(); gen.addSignerInfoGenerator(
logger.info("About to sign content using BouncyCastle"); new JcaSignerInfoGeneratorBuilder(
externalSigning.setSignature(cmsSignature); new JcaDigestCalculatorProviderBuilder()
logger.info("Signature set successfully"); .setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build())
.build(signer, cert));
// After setting the signature, return the resultant PDF gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { CMSSignedData signedData = gen.generate(cmsData, false);
document.save(signedPdfOutput);
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
} catch (Exception e) { byte[] cmsSignature = signedData.getEncoded();
e.printStackTrace(); logger.info("About to sign content using BouncyCastle");
} externalSigning.setSignature(cmsSignature);
} catch (Exception e) { logger.info("Signature set successfully");
e.printStackTrace();
}
return null; // 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");
private byte[] parsePEM(byte[] content) throws IOException { } catch (Exception e) {
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); e.printStackTrace();
return pemReader.readPemObject().getContent(); }
} } catch (Exception e) {
e.printStackTrace();
}
private boolean isPEM(byte[] content) { return null;
String contentStr = new String(content); }
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
private byte[] parsePEM(byte[] content) throws IOException {
PemReader pemReader =
new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
return pemReader.readPemObject().getContent();
}
private boolean isPEM(byte[] content) {
String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
} }

View File

@ -72,23 +72,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class GetInfoOnPDF { public class GetInfoOnPDF {
static ObjectMapper objectMapper = new ObjectMapper();
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") static ObjectMapper objectMapper = new ObjectMapper();
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
throws IOException { MultipartFile inputFile = request.getFileInput();
MultipartFile inputFile = request.getFileInput(); try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) {
try (
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
) {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
ObjectNode jsonOutput = objectMapper.createObjectNode(); ObjectNode jsonOutput = objectMapper.createObjectNode();
@ -100,8 +99,7 @@ public class GetInfoOnPDF {
ObjectNode compliancy = objectMapper.createObjectNode(); ObjectNode compliancy = objectMapper.createObjectNode();
ObjectNode encryption = objectMapper.createObjectNode(); ObjectNode encryption = objectMapper.createObjectNode();
ObjectNode other = objectMapper.createObjectNode(); ObjectNode other = objectMapper.createObjectNode();
metadata.put("Title", info.getTitle()); metadata.put("Title", info.getTitle());
metadata.put("Author", info.getAuthor()); metadata.put("Author", info.getAuthor());
metadata.put("Subject", info.getSubject()); metadata.put("Subject", info.getSubject());
@ -111,14 +109,11 @@ public class GetInfoOnPDF {
metadata.put("CreationDate", formatDate(info.getCreationDate())); metadata.put("CreationDate", formatDate(info.getCreationDate()));
metadata.put("ModificationDate", formatDate(info.getModificationDate())); metadata.put("ModificationDate", formatDate(info.getModificationDate()));
jsonOutput.set("Metadata", metadata); jsonOutput.set("Metadata", metadata);
// Total file size of the PDF // Total file size of the PDF
long fileSizeInBytes = inputFile.getSize(); long fileSizeInBytes = inputFile.getSize();
basicInfo.put("FileSizeInBytes", fileSizeInBytes); basicInfo.put("FileSizeInBytes", fileSizeInBytes);
// Number of words, paragraphs, and images in the entire document // Number of words, paragraphs, and images in the entire document
String fullText = new PDFTextStripper().getText(pdfBoxDoc); String fullText = new PDFTextStripper().getText(pdfBoxDoc);
String[] words = fullText.split("\\s+"); String[] words = fullText.split("\\s+");
@ -129,8 +124,7 @@ public class GetInfoOnPDF {
// Number of characters in the entire document (including spaces and special characters) // Number of characters in the entire document (including spaces and special characters)
int charCount = fullText.length(); int charCount = fullText.length();
basicInfo.put("CharacterCount", charCount); basicInfo.put("CharacterCount", charCount);
// Initialize the flags and types // Initialize the flags and types
boolean hasCompression = false; boolean hasCompression = false;
String compressionType = "None"; String compressionType = "None";
@ -147,26 +141,21 @@ public class GetInfoOnPDF {
} }
} }
basicInfo.put("Compression", hasCompression); basicInfo.put("Compression", hasCompression);
if(hasCompression) if (hasCompression) basicInfo.put("CompressionType", compressionType);
basicInfo.put("CompressionType", compressionType);
String language = pdfBoxDoc.getDocumentCatalog().getLanguage(); String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
basicInfo.put("Language", language); basicInfo.put("Language", language);
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog(); PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
String pageMode = catalog.getPageMode().name(); String pageMode = catalog.getPageMode().name();
// Document Information using PDFBox // Document Information using PDFBox
docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
docInfoNode.put("Trapped", info.getTrapped()); docInfoNode.put("Trapped", info.getTrapped());
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));; docInfoNode.put("Page Mode", getPageModeDescription(pageMode));
;
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
ObjectNode formFieldsNode = objectMapper.createObjectNode(); ObjectNode formFieldsNode = objectMapper.createObjectNode();
@ -177,41 +166,37 @@ public class GetInfoOnPDF {
} }
jsonOutput.set("FormFields", formFieldsNode); jsonOutput.set("FormFields", formFieldsNode);
// embeed files TODO size
if (catalog.getNames() != null) {
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
//embeed files TODO size if (efTree != null) {
if(catalog.getNames() != null) { Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); if (efMap != null) {
for (Map.Entry<String, PDComplexFileSpecification> entry :
ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); efMap.entrySet()) {
if (efTree != null) { ObjectNode embeddedFileNode = objectMapper.createObjectNode();
Map<String, PDComplexFileSpecification> efMap = efTree.getNames(); embeddedFileNode.put("Name", entry.getKey());
if (efMap != null) { PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
for (Map.Entry<String, PDComplexFileSpecification> entry : efMap.entrySet()) { if (embeddedFile != null) {
ObjectNode embeddedFileNode = objectMapper.createObjectNode(); embeddedFileNode.put(
embeddedFileNode.put("Name", entry.getKey()); "FileSize", embeddedFile.getLength()); // size in bytes
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); }
if (embeddedFile != null) { embeddedFilesArray.add(embeddedFileNode);
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes }
} }
embeddedFilesArray.add(embeddedFileNode); }
} other.set("EmbeddedFiles", embeddedFilesArray);
}
}
other.set("EmbeddedFiles", embeddedFilesArray);
} }
// attachments TODO size
//attachments TODO size
ArrayNode attachmentsArray = objectMapper.createArrayNode(); ArrayNode attachmentsArray = objectMapper.createArrayNode();
for (PDPage page : pdfBoxDoc.getPages()) { for (PDPage page : pdfBoxDoc.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) { for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationFileAttachment) { if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation; PDAnnotationFileAttachment fileAttachmentAnnotation =
(PDAnnotationFileAttachment) annotation;
ObjectNode attachmentNode = objectMapper.createObjectNode(); ObjectNode attachmentNode = objectMapper.createObjectNode();
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
@ -223,7 +208,7 @@ public class GetInfoOnPDF {
} }
other.set("Attachments", attachmentsArray); other.set("Attachments", attachmentsArray);
//Javascript // Javascript
PDDocumentNameDictionary namesDict = catalog.getNames(); PDDocumentNameDictionary namesDict = catalog.getNames();
ArrayNode javascriptArray = objectMapper.createArrayNode(); ArrayNode javascriptArray = objectMapper.createArrayNode();
@ -254,9 +239,9 @@ public class GetInfoOnPDF {
} }
other.set("JavaScript", javascriptArray); other.set("JavaScript", javascriptArray);
// TODO size
//TODO size PDOptionalContentProperties ocProperties =
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties(); pdfBoxDoc.getDocumentCatalog().getOCProperties();
ArrayNode layersArray = objectMapper.createArrayNode(); ArrayNode layersArray = objectMapper.createArrayNode();
if (ocProperties != null) { if (ocProperties != null) {
@ -268,34 +253,38 @@ public class GetInfoOnPDF {
} }
other.set("Layers", layersArray); other.set("Layers", layersArray);
//TODO Security
// TODO Security
PDStructureTreeRoot structureTreeRoot =
pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
ArrayNode structureTreeArray; ArrayNode structureTreeArray;
try { try {
if(structureTreeRoot != null) { if (structureTreeRoot != null) {
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids()); structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
other.set("StructureTree", structureTreeArray); other.set("StructureTree", structureTreeArray);
} }
} catch (Exception e) { } catch (Exception e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
} }
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA"); 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 isPdfBCompliant =
boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. 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/ACompliant", isPdfACompliant);
compliancy.put("IsPDF/XCompliant", isPdfXCompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
compliancy.put("IsPDF/ECompliant", isPdfECompliant); compliancy.put("IsPDF/ECompliant", isPdfECompliant);
@ -304,10 +293,6 @@ public class GetInfoOnPDF {
compliancy.put("IsPDF/BCompliant", isPdfBCompliant); compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant); compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline(); PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
ArrayNode bookmarksArray = objectMapper.createArrayNode(); ArrayNode bookmarksArray = objectMapper.createArrayNode();
@ -318,33 +303,29 @@ public class GetInfoOnPDF {
} }
other.set("Bookmarks/Outline/TOC", bookmarksArray); other.set("Bookmarks/Outline/TOC", bookmarksArray);
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
String xmpString = null; String xmpString = null;
if (pdMetadata != null) { if (pdMetadata != null) {
try { try {
COSInputStream is = pdMetadata.createInputStream(); COSInputStream is = pdMetadata.createInputStream();
DomXmpParser domXmpParser = new DomXmpParser(); DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(is); XMPMetadata xmpMeta = domXmpParser.parse(is);
ByteArrayOutputStream os = new ByteArrayOutputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, os, true); new XmpSerializer().serialize(xmpMeta, os, true);
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
} catch (XmpParsingException | IOException e) { } catch (XmpParsingException | IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
other.put("XMPMetadata", xmpString); other.put("XMPMetadata", xmpString);
if (pdfBoxDoc.isEncrypted()) { if (pdfBoxDoc.isEncrypted()) {
encryption.put("IsEncrypted", true); encryption.put("IsEncrypted", true);
// Retrieve encryption details using getEncryption() // Retrieve encryption details using getEncryption()
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption(); PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
@ -353,31 +334,30 @@ public class GetInfoOnPDF {
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission(); AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
if (ap != null) { if (ap != null) {
ObjectNode permissionsNode = objectMapper.createObjectNode(); ObjectNode permissionsNode = objectMapper.createObjectNode();
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument()); permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
permissionsNode.put("CanExtractContent", ap.canExtractContent()); permissionsNode.put("CanExtractContent", ap.canExtractContent());
permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility()); permissionsNode.put(
"CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put("CanFillInForm", ap.canFillInForm()); permissionsNode.put("CanFillInForm", ap.canFillInForm());
permissionsNode.put("CanModify", ap.canModify()); permissionsNode.put("CanModify", ap.canModify());
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations()); permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
permissionsNode.put("CanPrint", ap.canPrint()); permissionsNode.put("CanPrint", ap.canPrint());
permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded()); permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded());
encryption.set("Permissions", permissionsNode); // set the node under "Permissions" encryption.set(
} "Permissions", permissionsNode); // set the node under "Permissions"
}
// Add other encryption-related properties as needed // Add other encryption-related properties as needed
} else { } else {
encryption.put("IsEncrypted", false); encryption.put("IsEncrypted", false);
} }
ObjectNode pageInfoParent = objectMapper.createObjectNode(); ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode(); ObjectNode pageInfo = objectMapper.createObjectNode();
// Retrieve the page // Retrieve the page
PDPage page = pdfBoxDoc.getPage(pageNum); PDPage page = pdfBoxDoc.getPage(pageNum);
// Page-level Information // Page-level Information
@ -387,20 +367,20 @@ public class GetInfoOnPDF {
float height = mediaBox.getHeight(); float height = mediaBox.getHeight();
ObjectNode sizeInfo = objectMapper.createObjectNode(); ObjectNode sizeInfo = objectMapper.createObjectNode();
getDimensionInfo(sizeInfo, width, height); getDimensionInfo(sizeInfo, width, height);
sizeInfo.put("Standard Page", getPageSize(width, height)); sizeInfo.put("Standard Page", getPageSize(width, height));
pageInfo.set("Size", sizeInfo); pageInfo.set("Size", sizeInfo);
pageInfo.put("Rotation", page.getRotation()); pageInfo.put("Rotation", page.getRotation());
pageInfo.put("Page Orientation", getPageOrientation(width, height)); pageInfo.put("Page Orientation", getPageOrientation(width, height));
// Boxes // Boxes
pageInfo.put("MediaBox", mediaBox.toString()); pageInfo.put("MediaBox", mediaBox.toString());
// Assuming the following boxes are defined for your document; if not, you may get null values. // Assuming the following boxes are defined for your document; if not, you may get
// null values.
PDRectangle cropBox = page.getCropBox(); PDRectangle cropBox = page.getCropBox();
pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString()); pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
@ -416,13 +396,13 @@ public class GetInfoOnPDF {
// Content Extraction // Content Extraction
PDFTextStripper textStripper = new PDFTextStripper(); PDFTextStripper textStripper = new PDFTextStripper();
textStripper.setStartPage(pageNum + 1); textStripper.setStartPage(pageNum + 1);
textStripper.setEndPage(pageNum +1); textStripper.setEndPage(pageNum + 1);
String pageText = textStripper.getText(pdfBoxDoc); String pageText = textStripper.getText(pdfBoxDoc);
pageInfo.put("Text Characters Count", pageText.length()); // pageInfo.put("Text Characters Count", pageText.length()); //
// Annotations // Annotations
List<PDAnnotation> annotations = page.getAnnotations(); List<PDAnnotation> annotations = page.getAnnotations();
int subtypeCount = 0; int subtypeCount = 0;
@ -430,10 +410,10 @@ public class GetInfoOnPDF {
for (PDAnnotation annotation : annotations) { for (PDAnnotation annotation : annotations) {
if (annotation.getSubtype() != null) { if (annotation.getSubtype() != null) {
subtypeCount++; // Increase subtype count subtypeCount++; // Increase subtype count
} }
if (annotation.getContents() != null) { if (annotation.getContents() != null) {
contentsCount++; // Increase contents count contentsCount++; // Increase contents count
} }
} }
@ -442,26 +422,25 @@ public class GetInfoOnPDF {
annotationsObject.put("SubtypeCount", subtypeCount); annotationsObject.put("SubtypeCount", subtypeCount);
annotationsObject.put("ContentsCount", contentsCount); annotationsObject.put("ContentsCount", contentsCount);
pageInfo.set("Annotations", annotationsObject); pageInfo.set("Annotations", annotationsObject);
// Images (simplified) // Images (simplified)
// This part is non-trivial as images can be embedded in multiple ways in a PDF. // This part is non-trivial as images can be embedded in multiple ways in a PDF.
// Here is a basic structure to recognize image XObjects on a page. // Here is a basic structure to recognize image XObjects on a page.
ArrayNode imagesArray = objectMapper.createArrayNode(); ArrayNode imagesArray = objectMapper.createArrayNode();
PDResources resources = page.getResources(); PDResources resources = page.getResources();
for (COSName name : resources.getXObjectNames()) { for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name); PDXObject xObject = resources.getXObject(name);
if (xObject instanceof PDImageXObject) { if (xObject instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xObject; PDImageXObject image = (PDImageXObject) xObject;
ObjectNode imageNode = objectMapper.createObjectNode(); ObjectNode imageNode = objectMapper.createObjectNode();
imageNode.put("Width", image.getWidth()); imageNode.put("Width", image.getWidth());
imageNode.put("Height", image.getHeight()); imageNode.put("Height", image.getHeight());
if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) { if (image.getMetadata() != null
imageNode.put("Name", image.getMetadata().getFile().getFile()); && image.getMetadata().getFile() != null
&& image.getMetadata().getFile().getFile() != null) {
imageNode.put("Name", image.getMetadata().getFile().getFile());
} }
if (image.getColorSpace() != null) { if (image.getColorSpace() != null) {
imageNode.put("ColorSpace", image.getColorSpace().getName()); imageNode.put("ColorSpace", image.getColorSpace().getName());
@ -472,10 +451,9 @@ public class GetInfoOnPDF {
} }
pageInfo.set("Images", imagesArray); pageInfo.set("Images", imagesArray);
// Links // Links
ArrayNode linksArray = objectMapper.createArrayNode(); ArrayNode linksArray = objectMapper.createArrayNode();
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
for (PDAnnotation annotation : annotations) { for (PDAnnotation annotation : annotations) {
if (annotation instanceof PDAnnotationLink) { if (annotation instanceof PDAnnotationLink) {
@ -483,7 +461,7 @@ public class GetInfoOnPDF {
if (linkAnnotation.getAction() instanceof PDActionURI) { if (linkAnnotation.getAction() instanceof PDActionURI) {
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction(); PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
String uri = uriAction.getURI(); String uri = uriAction.getURI();
uniqueURIs.add(uri); // Add to set to ensure uniqueness uniqueURIs.add(uri); // Add to set to ensure uniqueness
} }
} }
} }
@ -495,8 +473,7 @@ public class GetInfoOnPDF {
linksArray.add(linkNode); linksArray.add(linkNode);
} }
pageInfo.set("Links", linksArray); pageInfo.set("Links", linksArray);
// Fonts // Fonts
ArrayNode fontsArray = objectMapper.createArrayNode(); ArrayNode fontsArray = objectMapper.createArrayNode();
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>(); Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
@ -526,13 +503,13 @@ public class GetInfoOnPDF {
fontNode.put("IsNonsymbolic", (flags & 32) != 0); fontNode.put("IsNonsymbolic", (flags & 32) != 0);
fontNode.put("FontFamily", fontDescriptor.getFontFamily()); fontNode.put("FontFamily", fontDescriptor.getFontFamily());
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity // Font stretch and BBox are not directly available in PDFBox's API, so
// these are omitted for simplicity
fontNode.put("FontWeight", fontDescriptor.getFontWeight()); fontNode.put("FontWeight", fontDescriptor.getFontWeight());
} }
// Create a unique key for this font node based on its attributes // Create a unique key for this font node based on its attributes
String uniqueKey = fontNode.toString(); String uniqueKey = fontNode.toString();
// Increment count if this font exists, or initialize it if new // Increment count if this font exists, or initialize it if new
if (uniqueFontsMap.containsKey(uniqueKey)) { if (uniqueFontsMap.containsKey(uniqueKey)) {
@ -551,17 +528,7 @@ public class GetInfoOnPDF {
} }
pageInfo.set("Fonts", fontsArray); pageInfo.set("Fonts", fontsArray);
// Access resources dictionary // Access resources dictionary
ArrayNode colorSpacesArray = objectMapper.createArrayNode(); ArrayNode colorSpacesArray = objectMapper.createArrayNode();
@ -572,7 +539,7 @@ public class GetInfoOnPDF {
PDICCBased iccBased = (PDICCBased) colorSpace; PDICCBased iccBased = (PDICCBased) colorSpace;
PDStream iccData = iccBased.getPDStream(); PDStream iccData = iccBased.getPDStream();
byte[] iccBytes = iccData.toByteArray(); byte[] iccBytes = iccData.toByteArray();
// TODO: Further decode and analyze the ICC data if needed // TODO: Further decode and analyze the ICC data if needed
ObjectNode iccProfileNode = objectMapper.createObjectNode(); ObjectNode iccProfileNode = objectMapper.createObjectNode();
iccProfileNode.put("ICC Profile Length", iccBytes.length); iccProfileNode.put("ICC Profile Length", iccBytes.length);
@ -580,14 +547,14 @@ public class GetInfoOnPDF {
} }
} }
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
// Other XObjects // Other XObjects
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type Map<String, Integer> xObjectCountMap =
new HashMap<>(); // To store the count for each type
for (COSName name : resources.getXObjectNames()) { for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name); PDXObject xObject = resources.getXObject(name);
String xObjectType; String xObjectType;
if (xObject instanceof PDImageXObject) { if (xObject instanceof PDImageXObject) {
xObjectType = "Image"; xObjectType = "Image";
} else if (xObject instanceof PDFormXObject) { } else if (xObject instanceof PDFormXObject) {
@ -597,7 +564,8 @@ public class GetInfoOnPDF {
} }
// Increment the count for this type in the map // Increment the count for this type in the map
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); xObjectCountMap.put(
xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
} }
// Add the count map to pageInfo (or wherever you want to store it) // Add the count map to pageInfo (or wherever you want to store it)
@ -606,14 +574,11 @@ public class GetInfoOnPDF {
xObjectCountNode.put(entry.getKey(), entry.getValue()); xObjectCountNode.put(entry.getKey(), entry.getValue());
} }
pageInfo.set("XObjectCounts", xObjectCountNode); pageInfo.set("XObjectCounts", xObjectCountNode);
ArrayNode multimediaArray = objectMapper.createArrayNode(); ArrayNode multimediaArray = objectMapper.createArrayNode();
for (PDAnnotation annotation : annotations) { for (PDAnnotation annotation : annotations) {
if ("RichMedia".equals(annotation.getSubtype())) { if ("RichMedia".equals(annotation.getSubtype())) {
ObjectNode multimediaNode = objectMapper.createObjectNode(); ObjectNode multimediaNode = objectMapper.createObjectNode();
// Extract details from the annotation as needed // Extract details from the annotation as needed
multimediaArray.add(multimediaNode); multimediaArray.add(multimediaNode);
@ -622,32 +587,29 @@ public class GetInfoOnPDF {
pageInfo.set("Multimedia", multimediaArray); pageInfo.set("Multimedia", multimediaArray);
pageInfoParent.set("Page " + (pageNum + 1), pageInfo);
pageInfoParent.set("Page " + (pageNum+1), pageInfo);
} }
jsonOutput.set("BasicInfo", basicInfo); jsonOutput.set("BasicInfo", basicInfo);
jsonOutput.set("DocumentInfo", docInfoNode); jsonOutput.set("DocumentInfo", docInfoNode);
jsonOutput.set("Compliancy", compliancy); jsonOutput.set("Compliancy", compliancy);
jsonOutput.set("Encryption", encryption); jsonOutput.set("Encryption", encryption);
jsonOutput.set("Other", other); jsonOutput.set("Other", other);
jsonOutput.set("PerPageInfo", pageInfoParent); jsonOutput.set("PerPageInfo", pageInfoParent);
// Save JSON to file // Save JSON to file
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); String jsonString =
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
return WebResponseUtils.bytesToWebResponse(
return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON); jsonString.getBytes(StandardCharsets.UTF_8),
"response.json",
MediaType.APPLICATION_JSON);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
return null; return null;
} }
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
@ -665,7 +627,7 @@ public class GetInfoOnPDF {
} }
} }
public String getPageOrientation(double width, double height) { public String getPageOrientation(double width, double height) {
if (width > height) { if (width > height) {
return "Landscape"; return "Landscape";
} else if (height > width) { } else if (height > width) {
@ -674,6 +636,7 @@ public class GetInfoOnPDF {
return "Square"; return "Square";
} }
} }
public String getPageSize(float width, float height) { public String getPageSize(float width, float height) {
// Define standard page sizes // Define standard page sizes
Map<String, PDRectangle> standardSizes = new HashMap<>(); Map<String, PDRectangle> standardSizes = new HashMap<>();
@ -696,21 +659,22 @@ public class GetInfoOnPDF {
return "Custom"; return "Custom";
} }
private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) { private boolean isCloseToSize(
float width, float height, float standardWidth, float standardHeight) {
float tolerance = 1.0f; // You can adjust the tolerance as needed float tolerance = 1.0f; // You can adjust the tolerance as needed
return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance; return Math.abs(width - standardWidth) <= tolerance
&& Math.abs(height - standardHeight) <= tolerance;
} }
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
float ppi = 72; // Points Per Inch float ppi = 72; // Points Per Inch
float widthInInches = width / ppi; float widthInInches = width / ppi;
float heightInInches = height / ppi; float heightInInches = height / ppi;
float widthInCm = widthInInches * 2.54f; float widthInCm = widthInInches * 2.54f;
float heightInCm = heightInInches * 2.54f; float heightInCm = heightInInches * 2.54f;
dimensionInfo.put("Width (px)", String.format("%.2f", width)); dimensionInfo.put("Width (px)", String.format("%.2f", width));
dimensionInfo.put("Height (px)", String.format("%.2f", height)); dimensionInfo.put("Height (px)", String.format("%.2f", height));
dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches)); dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches));
@ -720,33 +684,33 @@ public class GetInfoOnPDF {
return dimensionInfo; return dimensionInfo;
} }
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);
public static boolean checkForStandard(PDDocument document, String standardKeyword) { if (xmpString.contains(standardKeyword)) {
// Check XMP Metadata return true;
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;
} }
} catch (
Exception
e) { // Catching general exception for brevity, ideally you'd catch specific
// exceptions.
e.printStackTrace();
} }
} catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
e.printStackTrace();
}
return false;
}
return false;
}
public ArrayNode exploreStructureTree(List<Object> nodes) { public ArrayNode exploreStructureTree(List<Object> nodes) {
ArrayNode elementsArray = objectMapper.createArrayNode(); ArrayNode elementsArray = objectMapper.createArrayNode();
if (nodes != null) { if (nodes != null) {
@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return elementsArray; return elementsArray;
} }
public String getContent(PDStructureElement structureElement) { public String getContent(PDStructureElement structureElement) {
StringBuilder contentBuilder = new StringBuilder(); StringBuilder contentBuilder = new StringBuilder();
@ -790,8 +753,7 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return contentBuilder.toString(); return contentBuilder.toString();
} }
private String formatDate(Calendar calendar) { private String formatDate(Calendar calendar) {
if (calendar != null) { if (calendar != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

View File

@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.AddPasswordRequest;
import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
@ -26,29 +28,31 @@ public class PasswordController {
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class); private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-password") @PostMapping(consumes = "multipart/form-data", value = "/remove-password")
@Operation( @Operation(
summary = "Remove password from a PDF file", summary = "Remove password from a PDF file",
description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException { public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
String password = request.getPassword(); String password = request.getPassword();
PDDocument document = PDDocument.load(fileInput.getBytes(), password); PDDocument document = PDDocument.load(fileInput.getBytes(), password);
document.setAllSecurityToBeRemoved(true); document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf");
} }
@PostMapping(consumes = "multipart/form-data", value = "/add-password") @PostMapping(consumes = "multipart/form-data", value = "/add-password")
@Operation( @Operation(
summary = "Add password to a PDF file", summary = "Add password to a PDF file",
description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF" description =
) "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) throws IOException { public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
String ownerPassword = request.getOwnerPassword(); String ownerPassword = request.getOwnerPassword();
String password = request.getPassword(); String password = request.getPassword();
@ -74,16 +78,19 @@ public class PasswordController {
ap.setCanPrintFaithful(!canPrintFaithful); ap.setCanPrintFaithful(!canPrintFaithful);
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
if(!"".equals(ownerPassword) || !"".equals(password)) { if (!"".equals(ownerPassword) || !"".equals(password)) {
spp.setEncryptionKeyLength(keyLength); spp.setEncryptionKeyLength(keyLength);
} }
spp.setPermissions(ap); spp.setPermissions(ap);
document.protect(spp); document.protect(spp);
if("".equals(ownerPassword) && "".equals(password)) if ("".equals(ownerPassword) && "".equals(password))
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf"); return WebResponseUtils.pdfDocToWebResponse(
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
} }
} }

View File

@ -26,10 +26,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
@ -37,11 +39,13 @@ public class RedactController {
private static final Logger logger = LoggerFactory.getLogger(RedactController.class); private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data") @PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
@Operation(summary = "Redacts listOfText in a PDF document", @Operation(
description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") summary = "Redacts listOfText in a PDF document",
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request) throws Exception { description =
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request)
throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String listOfTextString = request.getListOfText(); String listOfTextString = request.getListOfText();
boolean useRegex = request.isUseRegex(); boolean useRegex = request.isUseRegex();
@ -49,15 +53,15 @@ public class RedactController {
String colorString = request.getRedactColor(); String colorString = request.getRedactColor();
float customPadding = request.getCustomPadding(); float customPadding = request.getCustomPadding();
boolean convertPDFToImage = request.isConvertPDFToImage(); boolean convertPDFToImage = request.isConvertPDFToImage();
System.out.println(listOfTextString); System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n"); String[] listOfText = listOfTextString.split("\n");
byte[] bytes = file.getBytes(); byte[] bytes = file.getBytes();
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes)); PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
Color redactColor; Color redactColor;
try { try {
if (!colorString.startsWith("#")) { if (!colorString.startsWith("#")) {
colorString = "#" + colorString; colorString = "#" + colorString;
} }
redactColor = Color.decode(colorString); redactColor = Color.decode(colorString);
@ -66,18 +70,14 @@ public class RedactController {
redactColor = Color.BLACK; redactColor = Color.BLACK;
} }
for (String text : listOfText) { for (String text : listOfText) {
text = text.trim(); text = text.trim();
System.out.println(text); System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document); List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding,redactColor); redactFoundText(document, foundTexts, customPadding, redactColor);
} }
if (convertPDFToImage) { if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument(); PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
@ -97,27 +97,33 @@ public class RedactController {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos); document.save(baos);
document.close(); document.close();
byte[] pdfContent = baos.toByteArray(); byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, return WebResponseUtils.bytesToWebResponse(
pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf"); file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
} }
private void redactFoundText(
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor) throws IOException { PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor)
throws IOException {
var allPages = document.getDocumentCatalog().getPages(); var allPages = document.getDocumentCatalog().getPages();
for (PDFText block : blocks) { for (PDFText block : blocks) {
var page = allPages.get(block.getPageIndex()); var page = allPages.get(block.getPageIndex());
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true); PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setNonStrokingColor(redactColor); contentStream.setNonStrokingColor(redactColor);
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding; float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
PDRectangle pageBox = page.getBBox(); PDRectangle pageBox = page.getBBox();
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding); contentStream.addRect(
block.getX1(),
pageBox.getHeight() - block.getY1() - padding,
block.getX2() - block.getX1(),
block.getY2() - block.getY1() + 2 * padding);
contentStream.fill(); contentStream.fill();
contentStream.close(); contentStream.close();
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSDictionary;
@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -36,59 +38,68 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class SanitizeController { public class SanitizeController {
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
@Operation(summary = "Sanitize a PDF file", @Operation(
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") summary = "Sanitize a PDF file",
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException { description =
MultipartFile inputFile = request.getFileInput(); "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
boolean removeJavaScript = request.isRemoveJavaScript(); public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request)
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); throws IOException {
boolean removeMetadata = request.isRemoveMetadata(); MultipartFile inputFile = request.getFileInput();
boolean removeLinks = request.isRemoveLinks(); boolean removeJavaScript = request.isRemoveJavaScript();
boolean removeFonts = request.isRemoveFonts(); boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
boolean removeMetadata = request.isRemoveMetadata();
boolean removeLinks = request.isRemoveLinks();
boolean removeFonts = request.isRemoveFonts();
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if (removeJavaScript) { if (removeJavaScript) {
sanitizeJavaScript(document); sanitizeJavaScript(document);
} }
if (removeEmbeddedFiles) { if (removeEmbeddedFiles) {
sanitizeEmbeddedFiles(document); sanitizeEmbeddedFiles(document);
} }
if (removeMetadata) { if (removeMetadata) {
sanitizeMetadata(document); sanitizeMetadata(document);
} }
if (removeLinks) { if (removeLinks) {
sanitizeLinks(document); sanitizeLinks(document);
} }
if (removeFonts) { if (removeFonts) {
sanitizeFonts(document); sanitizeFonts(document);
} }
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf"); return WebResponseUtils.pdfDocToWebResponse(
} document,
} inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
private void sanitizeJavaScript(PDDocument document) throws IOException { + "_sanitized.pdf");
// Get the root dictionary (catalog) of the PDF }
PDDocumentCatalog catalog = document.getDocumentCatalog(); }
// Get the Names dictionary private void sanitizeJavaScript(PDDocument document) throws IOException {
COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); // Get the root dictionary (catalog) of the PDF
PDDocumentCatalog catalog = document.getDocumentCatalog();
if (namesDict != null) { // Get the Names dictionary
// Get the JavaScript dictionary COSDictionary namesDict =
COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
if (javaScriptDict != null) { if (namesDict != null) {
// Remove the JavaScript dictionary // Get the JavaScript dictionary
namesDict.removeItem(COSName.getPDFName("JavaScript")); COSDictionary javaScriptDict =
} (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
}
if (javaScriptDict != null) {
for (PDPage page : document.getPages()) { // Remove the JavaScript dictionary
namesDict.removeItem(COSName.getPDFName("JavaScript"));
}
}
for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) { for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationWidget) { if (annotation instanceof PDAnnotationWidget) {
PDAnnotationWidget widget = (PDAnnotationWidget) annotation; PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
@ -96,33 +107,30 @@ public class SanitizeController {
if (action instanceof PDActionJavaScript) { if (action instanceof PDActionJavaScript) {
widget.setAction(null); widget.setAction(null);
} }
} }
} }
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm != null) { if (acroForm != null) {
for (PDField field : acroForm.getFields()) { for (PDField field : acroForm.getFields()) {
PDFormFieldAdditionalActions actions = field.getActions(); PDFormFieldAdditionalActions actions = field.getActions();
if(actions != null) { if (actions != null) {
if (actions.getC() instanceof PDActionJavaScript) { if (actions.getC() instanceof PDActionJavaScript) {
actions.setC(null); actions.setC(null);
} }
if (actions.getF() instanceof PDActionJavaScript) { if (actions.getF() instanceof PDActionJavaScript) {
actions.setF(null); actions.setF(null);
} }
if (actions.getK() instanceof PDActionJavaScript) { if (actions.getK() instanceof PDActionJavaScript) {
actions.setK(null); actions.setK(null);
} }
if (actions.getV() instanceof PDActionJavaScript) { if (actions.getV() instanceof PDActionJavaScript) {
actions.setV(null); actions.setV(null);
} }
} }
} }
} }
} }
} }
private void sanitizeEmbeddedFiles(PDDocument document) { private void sanitizeEmbeddedFiles(PDDocument document) {
PDPageTree allPages = document.getPages(); PDPageTree allPages = document.getPages();
@ -134,7 +142,6 @@ public class SanitizeController {
res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
} }
} }
private void sanitizeMetadata(PDDocument document) { private void sanitizeMetadata(PDDocument document) {
PDMetadata metadata = document.getDocumentCatalog().getMetadata(); PDMetadata metadata = document.getDocumentCatalog().getMetadata();
@ -143,8 +150,6 @@ public class SanitizeController {
} }
} }
private void sanitizeLinks(PDDocument document) throws IOException { private void sanitizeLinks(PDDocument document) throws IOException {
for (PDPage page : document.getPages()) { for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) { for (PDAnnotation annotation : page.getAnnotations()) {
@ -163,5 +168,4 @@ public class SanitizeController {
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
} }
} }
} }

View File

@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -38,154 +39,198 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class WatermarkController { public class WatermarkController {
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark") @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception { summary = "Add watermark to a PDF file",
MultipartFile pdfFile = request.getFileInput(); description =
String watermarkType = request.getWatermarkType(); "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
String watermarkText = request.getWatermarkText(); public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request)
MultipartFile watermarkImage = request.getWatermarkImage(); throws IOException, Exception {
String alphabet = request.getAlphabet(); MultipartFile pdfFile = request.getFileInput();
float fontSize = request.getFontSize(); String watermarkType = request.getWatermarkType();
float rotation = request.getRotation(); String watermarkText = request.getWatermarkText();
float opacity = request.getOpacity(); MultipartFile watermarkImage = request.getWatermarkImage();
int widthSpacer = request.getWidthSpacer(); String alphabet = request.getAlphabet();
int heightSpacer = request.getHeightSpacer(); float fontSize = request.getFontSize();
float rotation = request.getRotation();
float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer();
// Load the input PDF // Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream()); PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Create a page in the document // Create a page in the document
for (PDPage page : document.getPages()) { for (PDPage page : document.getPages()) {
// Get the page's content stream // Get the page's content stream
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream contentStream =
PDPageContentStream.AppendMode.APPEND, true); new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true);
// Set transparency // Set transparency
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
graphicsState.setNonStrokingAlphaConstant(opacity); graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream.setGraphicsStateParameters(graphicsState); contentStream.setGraphicsStateParameters(graphicsState);
if (watermarkType.equalsIgnoreCase("text")) { if (watermarkType.equalsIgnoreCase("text")) {
addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer, addTextWatermark(
fontSize, alphabet); contentStream,
} else if (watermarkType.equalsIgnoreCase("image")) { watermarkText,
addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer, document,
fontSize); page,
} rotation,
widthSpacer,
heightSpacer,
fontSize,
alphabet);
} else if (watermarkType.equalsIgnoreCase("image")) {
addImageWatermark(
contentStream,
watermarkImage,
document,
page,
rotation,
widthSpacer,
heightSpacer,
fontSize);
}
// Close the content stream // Close the content stream
contentStream.close(); contentStream.close();
} }
return WebResponseUtils.pdfDocToWebResponse(document, return WebResponseUtils.pdfDocToWebResponse(
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); document,
} pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
}
private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document, private void addTextWatermark(
PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException { PDPageContentStream contentStream,
String resourceDir = ""; String watermarkText,
PDFont font = PDType1Font.HELVETICA_BOLD; PDDocument document,
switch (alphabet) { PDPage page,
case "arabic": float rotation,
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; int widthSpacer,
break; int heightSpacer,
case "japanese": float fontSize,
resourceDir = "static/fonts/Meiryo.ttf"; String alphabet)
break; throws IOException {
case "korean": String resourceDir = "";
resourceDir = "static/fonts/malgun.ttf"; PDFont font = PDType1Font.HELVETICA_BOLD;
break; switch (alphabet) {
case "chinese": case "arabic":
resourceDir = "static/fonts/SimSun.ttf"; resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
break; break;
case "roman": case "japanese":
default: resourceDir = "static/fonts/Meiryo.ttf";
resourceDir = "static/fonts/NotoSans-Regular.ttf"; break;
break; case "korean":
} resourceDir = "static/fonts/malgun.ttf";
break;
case "chinese":
resourceDir = "static/fonts/SimSun.ttf";
break;
case "roman":
default:
resourceDir = "static/fonts/NotoSans-Regular.ttf";
break;
}
if (!resourceDir.equals("")) {
if(!resourceDir.equals("")) {
ClassPathResource classPathResource = new ClassPathResource(resourceDir); ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = File.createTempFile("NotoSansFont", fileExtension); File tempFile = File.createTempFile("NotoSansFont", fileExtension);
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os); IOUtils.copy(is, os);
} }
font = PDType0Font.load(document, tempFile); font = PDType0Font.load(document, tempFile);
tempFile.deleteOnExit(); tempFile.deleteOnExit();
} }
contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
// Set size and location of text watermark contentStream.setFont(font, fontSize);
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
float watermarkHeight = heightSpacer + fontSize;
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
// Add the text watermark // Set size and location of text watermark
for (int i = 0; i < watermarkRows; i++) { float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
for (int j = 0; j < watermarkCols; j++) { float watermarkHeight = heightSpacer + fontSize;
contentStream.beginText(); float pageWidth = page.getMediaBox().getWidth();
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), float pageHeight = page.getMediaBox().getHeight();
j * watermarkWidth, i * watermarkHeight)); int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
contentStream.showText(watermarkText); int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
contentStream.endText();
}
}
}
private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation, // Add the text watermark
int widthSpacer, int heightSpacer, float fontSize) throws IOException { for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
contentStream.beginText();
contentStream.setTextMatrix(
Matrix.getRotateInstance(
(float) Math.toRadians(rotation),
j * watermarkWidth,
i * watermarkHeight));
contentStream.showText(watermarkText);
contentStream.endText();
}
}
}
// Load the watermark image private void addImageWatermark(
BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); PDPageContentStream contentStream,
MultipartFile watermarkImage,
PDDocument document,
PDPage page,
float rotation,
int widthSpacer,
int heightSpacer,
float fontSize)
throws IOException {
// Compute width based on original aspect ratio // Load the watermark image
float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
// Desired physical height (in PDF points) // Compute width based on original aspect ratio
float desiredPhysicalHeight = fontSize ; float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
// Desired physical width based on the aspect ratio // Desired physical height (in PDF points)
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; float desiredPhysicalHeight = fontSize;
// Convert the BufferedImage to PDImageXObject // Desired physical width based on the aspect ratio
PDImageXObject xobject = LosslessFactory.createFromImage(document, image); float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
// Calculate the number of rows and columns for watermarks // Convert the BufferedImage to PDImageXObject
float pageWidth = page.getMediaBox().getWidth(); PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
for (int i = 0; i < watermarkRows; i++) { // Calculate the number of rows and columns for watermarks
for (int j = 0; j < watermarkCols; j++) { float pageWidth = page.getMediaBox().getWidth();
float x = j * (desiredPhysicalWidth + widthSpacer); float pageHeight = page.getMediaBox().getHeight();
float y = i * (desiredPhysicalHeight + heightSpacer); int watermarkRows =
(int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
int watermarkCols =
(int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
// Save the graphics state for (int i = 0; i < watermarkRows; i++) {
contentStream.saveGraphicsState(); for (int j = 0; j < watermarkCols; j++) {
float x = j * (desiredPhysicalWidth + widthSpacer);
float y = i * (desiredPhysicalHeight + heightSpacer);
// Create rotation matrix and rotate // Save the graphics state
contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2)); contentStream.saveGraphicsState();
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
// Draw the image and restore the graphics state
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
contentStream.restoreGraphicsState();
}
}
}
// Create rotation matrix and rotate
contentStream.transform(
Matrix.getTranslateInstance(
x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
contentStream.transform(
Matrix.getTranslateInstance(
-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
// Draw the image and restore the graphics state
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
contentStream.restoreGraphicsState();
}
}
}
} }

View File

@ -24,91 +24,79 @@ import org.apache.pdfbox.text.PDFTextStripperByArea;
import org.apache.pdfbox.text.TextPosition; import org.apache.pdfbox.text.TextPosition;
/** /**
* Class to extract tabular data from a PDF. Works by making a first pass of the page to group all
* nearby text items together, and then inferring a 2D grid from these regions. Each table cell is
* then extracted using a PDFTextStripperByArea object.
* *
* Class to extract tabular data from a PDF. * <p>Works best when headers are included in the detected region, to ensure representative text in
* Works by making a first pass of the page to group all nearby text items * every column.
* together, and then inferring a 2D grid from these regions. Each table cell
* is then extracted using a PDFTextStripperByArea object.
* *
* Works best when * <p>Based upon DrawPrintTextLocations PDFBox example
* headers are included in the detected region, to ensure representative text
* in every column.
*
* Based upon DrawPrintTextLocations PDFBox example
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java) * (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
* *
* @author Beldaz * @author Beldaz
*/ */
public class PDFTableStripper extends PDFTextStripper public class PDFTableStripper extends PDFTextStripper {
{
/** /**
* This will print the documents data, for each table cell. * This will print the documents data, for each table cell.
* *
* @param args The command line arguments. * @param args The command line arguments.
*
* @throws IOException If there is an error parsing the document. * @throws IOException If there is an error parsing the document.
*/ */
/* /*
* Used in methods derived from DrawPrintTextLocations * Used in methods derived from DrawPrintTextLocations
*/ */
private AffineTransform flipAT; private AffineTransform flipAT;
private AffineTransform rotateAT; private AffineTransform rotateAT;
/** /** Regions updated by calls to writeString */
* Regions updated by calls to writeString
*/
private Set<Rectangle2D> boxes; private Set<Rectangle2D> boxes;
// Border to allow when finding intersections // Border to allow when finding intersections
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter) private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
private double dy = 0.000; // Rows of text tend to overlap, so need to extend private double dy = 0.000; // Rows of text tend to overlap, so need to extend
/** /** Region in which to find table (otherwise whole page) */
* Region in which to find table (otherwise whole page)
*/
private Rectangle2D regionArea; private Rectangle2D regionArea;
/** /** Number of rows in inferred table */
* Number of rows in inferred table private int nRows = 0;
*/
private int nRows=0;
/** /** Number of columns in inferred table */
* Number of columns in inferred table private int nCols = 0;
*/
private int nCols=0;
/** /** This is the object that does the text extraction */
* This is the object that does the text extraction
*/
private PDFTextStripperByArea regionStripper; private PDFTextStripperByArea regionStripper;
/** /**
* 1D intervals - used for calculateTableRegions() * 1D intervals - used for calculateTableRegions()
* @author Beldaz
* *
* @author Beldaz
*/ */
public static class Interval { public static class Interval {
double start; double start;
double end; double end;
public Interval(double start, double end) { public Interval(double start, double end) {
this.start=start; this.end = end; this.start = start;
this.end = end;
} }
public void add(Interval col) { public void add(Interval col) {
if(col.start<start) if (col.start < start) start = col.start;
start = col.start; if (col.end > end) end = col.end;
if(col.end>end)
end = col.end;
} }
public static void addTo(Interval x, LinkedList<Interval> columns) { public static void addTo(Interval x, LinkedList<Interval> columns) {
int p = 0; int p = 0;
Iterator<Interval> it = columns.iterator(); Iterator<Interval> it = columns.iterator();
// Find where x should go // Find where x should go
while(it.hasNext()) { while (it.hasNext()) {
Interval col = it.next(); Interval col = it.next();
if(x.end>=col.start) { if (x.end >= col.start) {
if(x.start<=col.end) { // overlaps if (x.start <= col.end) { // overlaps
x.add(col); x.add(col);
it.remove(); it.remove();
} }
@ -116,30 +104,26 @@ public class PDFTableStripper extends PDFTextStripper
} }
++p; ++p;
} }
while(it.hasNext()) { while (it.hasNext()) {
Interval col = it.next(); Interval col = it.next();
if(x.start>col.end) if (x.start > col.end) break;
break;
x.add(col); x.add(col);
it.remove(); it.remove();
} }
columns.add(p, x); columns.add(p, x);
} }
} }
/** /**
* Instantiate a new PDFTableStripper object. * Instantiate a new PDFTableStripper object.
* *
* @param document * @param document
* @throws IOException If there is an error loading the properties. * @throws IOException If there is an error loading the properties.
*/ */
public PDFTableStripper() throws IOException public PDFTableStripper() throws IOException {
{
super.setShouldSeparateByBeads(false); super.setShouldSeparateByBeads(false);
regionStripper = new PDFTextStripperByArea(); regionStripper = new PDFTextStripperByArea();
regionStripper.setSortByPosition( true ); regionStripper.setSortByPosition(true);
} }
/** /**
@ -147,18 +131,15 @@ public class PDFTableStripper extends PDFTextStripper
* *
* @param rect The rectangle area to retrieve the text from. * @param rect The rectangle area to retrieve the text from.
*/ */
public void setRegion(Rectangle2D rect ) public void setRegion(Rectangle2D rect) {
{
regionArea = rect; regionArea = rect;
} }
public int getRows() public int getRows() {
{
return nRows; return nRows;
} }
public int getColumns() public int getColumns() {
{
return nCols; return nCols;
} }
@ -167,13 +148,11 @@ public class PDFTableStripper extends PDFTextStripper
* *
* @return The text that was identified in that region. * @return The text that was identified in that region.
*/ */
public String getText(int row, int col) public String getText(int row, int col) {
{ return regionStripper.getTextForRegion("el" + col + "x" + row);
return regionStripper.getTextForRegion("el"+col+"x"+row);
} }
public void extractTable(PDPage pdPage) throws IOException public void extractTable(PDPage pdPage) throws IOException {
{
setStartPage(getCurrentPageNo()); setStartPage(getCurrentPageNo());
setEndPage(getCurrentPageNo()); setEndPage(getCurrentPageNo());
@ -186,11 +165,9 @@ public class PDFTableStripper extends PDFTextStripper
// page may be rotated // page may be rotated
rotateAT = new AffineTransform(); rotateAT = new AffineTransform();
int rotation = pdPage.getRotation(); int rotation = pdPage.getRotation();
if (rotation != 0) if (rotation != 0) {
{
PDRectangle mediaBox = pdPage.getMediaBox(); PDRectangle mediaBox = pdPage.getMediaBox();
switch (rotation) switch (rotation) {
{
case 90: case 90:
rotateAT.translate(mediaBox.getHeight(), 0); rotateAT.translate(mediaBox.getHeight(), 0);
break; break;
@ -213,11 +190,12 @@ public class PDFTableStripper extends PDFTextStripper
Rectangle2D[][] regions = calculateTableRegions(); Rectangle2D[][] regions = calculateTableRegions();
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions"); // System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
for(int i=0; i<nCols; ++i) { // regions");
for(int j=0; j<nRows; ++j) { for (int i = 0; i < nCols; ++i) {
for (int j = 0; j < nRows; ++j) {
final Rectangle2D region = regions[i][j]; final Rectangle2D region = regions[i][j];
regionStripper.addRegion("el"+i+"x"+j, region); regionStripper.addRegion("el" + i + "x" + j, region);
} }
} }
@ -227,8 +205,8 @@ public class PDFTableStripper extends PDFTextStripper
/** /**
* Infer a rectangular grid of regions from the boxes field. * Infer a rectangular grid of regions from the boxes field.
* *
* @return 2D array of table regions (as Rectangle2D objects). Note that * @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
* some of these regions may have no content. * may have no content.
*/ */
private Rectangle2D[][] calculateTableRegions() { private Rectangle2D[][] calculateTableRegions() {
@ -238,7 +216,7 @@ public class PDFTableStripper extends PDFTextStripper
LinkedList<Interval> columns = new LinkedList<Interval>(); LinkedList<Interval> columns = new LinkedList<Interval>();
LinkedList<Interval> rows = new LinkedList<Interval>(); LinkedList<Interval> rows = new LinkedList<Interval>();
for(Rectangle2D box: boxes) { for (Rectangle2D box : boxes) {
Interval x = new Interval(box.getMinX(), box.getMaxX()); Interval x = new Interval(box.getMinX(), box.getMaxX());
Interval y = new Interval(box.getMinY(), box.getMaxY()); Interval y = new Interval(box.getMinY(), box.getMaxY());
@ -249,12 +227,17 @@ public class PDFTableStripper extends PDFTextStripper
nRows = rows.size(); nRows = rows.size();
nCols = columns.size(); nCols = columns.size();
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows]; Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
int i=0; int i = 0;
// Label regions from top left, rather than the transformed orientation // Label regions from top left, rather than the transformed orientation
for(Interval column: columns) { for (Interval column : columns) {
int j=0; int j = 0;
for(Interval row: rows) { for (Interval row : rows) {
regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start); regions[nCols - i - 1][nRows - j - 1] =
new Rectangle2D.Double(
column.start,
row.start,
column.end - column.start,
row.end - row.start);
++j; ++j;
} }
++i; ++i;
@ -264,18 +247,15 @@ public class PDFTableStripper extends PDFTextStripper
} }
/** /**
* Register each character's bounding box, updating boxes field to maintain * Register each character's bounding box, updating boxes field to maintain a list of all
* a list of all distinct groups of characters. * distinct groups of characters.
* *
* Overrides the default functionality of PDFTextStripper. * <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
* Most of this is taken from DrawPrintTextLocations.java, with extra steps * DrawPrintTextLocations.java, with extra steps at end of main loop
* at end of main loop
*/ */
@Override @Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
{ for (TextPosition text : textPositions) {
for (TextPosition text : textPositions)
{
// glyph space -> user space // glyph space -> user space
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix // note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
AffineTransform at = text.getTextMatrix().createAffineTransform(); AffineTransform at = text.getTextMatrix().createAffineTransform();
@ -283,37 +263,35 @@ public class PDFTableStripper extends PDFTextStripper
BoundingBox bbox = font.getBoundingBox(); BoundingBox bbox = font.getBoundingBox();
// advance width, bbox height (glyph space) // advance width, bbox height (glyph space)
float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars float xadvance =
Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
Rectangle2D.Float rect =
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
if (font instanceof PDType3Font) if (font instanceof PDType3Font) {
{
// bbox and font matrix are unscaled // bbox and font matrix are unscaled
at.concatenate(font.getFontMatrix().createAffineTransform()); at.concatenate(font.getFontMatrix().createAffineTransform());
} } else {
else
{
// bbox and font matrix are already scaled to 1000 // bbox and font matrix are already scaled to 1000
at.scale(1/1000f, 1/1000f); at.scale(1 / 1000f, 1 / 1000f);
} }
Shape s = at.createTransformedShape(rect); Shape s = at.createTransformedShape(rect);
s = flipAT.createTransformedShape(s); s = flipAT.createTransformedShape(s);
s = rotateAT.createTransformedShape(s); s = rotateAT.createTransformedShape(s);
// //
// Merge character's bounding box with boxes field // Merge character's bounding box with boxes field
// //
Rectangle2D bounds = s.getBounds2D(); Rectangle2D bounds = s.getBounds2D();
// Pad sides to detect almost touching boxes // Pad sides to detect almost touching boxes
Rectangle2D hitbox = bounds.getBounds2D(); Rectangle2D hitbox = bounds.getBounds2D();
hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy); hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy);
hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy); hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy);
// Find all overlapping boxes // Find all overlapping boxes
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>(); List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
for(Rectangle2D box: boxes) { for (Rectangle2D box : boxes) {
if(box.intersects(hitbox)) { if (box.intersects(hitbox)) {
intersectList.add(box); intersectList.add(box);
} }
} }
@ -321,38 +299,30 @@ public class PDFTableStripper extends PDFTextStripper
// Combine all touching boxes and update // Combine all touching boxes and update
// (NOTE: Potentially this could leave some overlapping boxes un-merged, // (NOTE: Potentially this could leave some overlapping boxes un-merged,
// but it's sufficient for now and get's fixed up in calculateTableRegions) // but it's sufficient for now and get's fixed up in calculateTableRegions)
for(Rectangle2D box: intersectList) { for (Rectangle2D box : intersectList) {
bounds.add(box); bounds.add(box);
boxes.remove(box); boxes.remove(box);
} }
boxes.add(bounds); boxes.add(bounds);
} }
} }
/** /**
* This method does nothing in this derived class, because beads and regions are incompatible. Beads are * This method does nothing in this derived class, because beads and regions are incompatible.
* ignored when stripping by area. * Beads are ignored when stripping by area.
* *
* @param aShouldSeparateByBeads The new grouping of beads. * @param aShouldSeparateByBeads The new grouping of beads.
*/ */
@Override @Override
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
{
}
/** /** Adapted from PDFTextStripperByArea {@inheritDoc} */
* Adapted from PDFTextStripperByArea
* {@inheritDoc}
*/
@Override @Override
protected void processTextPosition( TextPosition text ) protected void processTextPosition(TextPosition text) {
{ if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
if(regionArea!=null && !regionArea.contains( text.getX(), text.getY() ) ) {
// skip character // skip character
} else { } else {
super.processTextPosition( text ); super.processTextPosition(text);
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -15,138 +16,140 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Controller @Controller
@Tag(name = "Account Security", description = "Account Security APIs") @Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController { public class AccountWebController {
@GetMapping("/login") @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
if (request.getParameter("error") != null) {
model.addAttribute("error", request.getParameter("error")); if (request.getParameter("error") != null) {
}
if (request.getParameter("logout") != null) {
model.addAttribute("logoutMessage", "You have been logged out."); model.addAttribute("error", request.getParameter("error"));
} }
if (request.getParameter("logout") != null) {
return "login";
}
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
model.addAttribute("logoutMessage", "You have been logged out.");
}
@PreAuthorize("hasRole('ROLE_ADMIN')") return "login";
@GetMapping("/addUsers") }
public String showAddUserForm(Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll();
Iterator<User> iterator = allUsers.iterator();
while(iterator.hasNext()) { @Autowired
User user = iterator.next(); private UserRepository userRepository; // Assuming you have a repository for user operations
if(user != null) {
for (Authority authority : user.getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove();
break; // Break out of the inner loop once the user is removed
}
}
}
}
model.addAttribute("users", allUsers); @PreAuthorize("hasRole('ROLE_ADMIN')")
model.addAttribute("currentUsername", authentication.getName()); @GetMapping("/addUsers")
return "addUsers"; public String showAddUserForm(Model model, Authentication authentication) {
} List<User> allUsers = userRepository.findAll();
Iterator<User> iterator = allUsers.iterator();
while (iterator.hasNext()) {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") User user = iterator.next();
@GetMapping("/account") if (user != null) {
public String account(HttpServletRequest request, Model model, Authentication authentication) { for (Authority authority : user.getAuthorities()) {
if (authentication == null || !authentication.isAuthenticated()) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove();
break; // Break out of the inner loop once the user is removed
}
}
}
}
model.addAttribute("users", allUsers);
model.addAttribute("currentUsername", authentication.getName());
return "addUsers";
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@GetMapping("/account")
public String account(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails // Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal; UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes // Retrieve username and other attributes
String username = userDetails.getUsername(); String username = userDetails.getUsername();
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists Optional<User> user =
if (!user.isPresent()) { userRepository.findByUsername(
// Handle error appropriately username); // Assuming findByUsername method exists
return "redirect:/error"; // Example redirection in case of error if (!user.isPresent()) {
} // Handle error appropriately
return "redirect:/error"; // Example redirection in case of error
}
// Convert settings map to JSON string // Convert settings map to JSON string
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
String settingsJson; String settingsJson;
try { try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
// Handle JSON conversion error // Handle JSON conversion error
e.printStackTrace(); e.printStackTrace();
return "redirect:/error"; // Example redirection in case of error return "redirect:/error"; // Example redirection in case of error
} }
// Add attributes to the model // Add attributes to the model
model.addAttribute("username", username); model.addAttribute("username", username);
model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson); model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
} }
} else { } else {
return "redirect:/";
}
return "account";
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@GetMapping("/change-creds")
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
if (authentication != null && authentication.isAuthenticated()) { return "account";
Object principal = authentication.getPrincipal(); }
if (principal instanceof UserDetails) { @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
// Cast the principal object to UserDetails @GetMapping("/change-creds")
UserDetails userDetails = (UserDetails) principal; 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();
// Retrieve username and other attributes if (principal instanceof UserDetails) {
String username = userDetails.getUsername(); // Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Fetch user details from the database // Retrieve username and other attributes
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists String username = userDetails.getUsername();
if (!user.isPresent()) {
// Handle error appropriately // Fetch user details from the database
return "redirect:/error"; // Example redirection in case of error Optional<User> user =
} userRepository.findByUsername(
// Add attributes to the model username); // Assuming findByUsername method exists
model.addAttribute("username", username); if (!user.isPresent()) {
} // Handle error appropriately
} else { return "redirect:/error"; // Example redirection in case of error
return "redirect:/"; }
} // Add attributes to the model
return "change-creds"; model.addAttribute("username", username);
} }
} else {
return "redirect:/";
}
return "change-creds";
}
} }

View File

@ -25,14 +25,14 @@ public class ConverterWebController {
model.addAttribute("currentPage", "html-to-pdf"); model.addAttribute("currentPage", "html-to-pdf");
return "convert/html-to-pdf"; return "convert/html-to-pdf";
} }
@GetMapping("/markdown-to-pdf") @GetMapping("/markdown-to-pdf")
@Hidden @Hidden
public String convertMarkdownToPdfForm(Model model) { public String convertMarkdownToPdfForm(Model model) {
model.addAttribute("currentPage", "markdown-to-pdf"); model.addAttribute("currentPage", "markdown-to-pdf");
return "convert/markdown-to-pdf"; return "convert/markdown-to-pdf";
} }
@GetMapping("/url-to-pdf") @GetMapping("/url-to-pdf")
@Hidden @Hidden
public String convertURLToPdfForm(Model model) { public String convertURLToPdfForm(Model model) {
@ -40,25 +40,22 @@ public class ConverterWebController {
return "convert/url-to-pdf"; return "convert/url-to-pdf";
} }
@GetMapping("/pdf-to-img") @GetMapping("/pdf-to-img")
@Hidden @Hidden
public String pdfToimgForm(Model model) { public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img"); model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img"; return "convert/pdf-to-img";
} }
@GetMapping("/file-to-pdf") @GetMapping("/file-to-pdf")
@Hidden @Hidden
public String convertToPdfForm(Model model) { public String convertToPdfForm(Model model) {
model.addAttribute("currentPage", "file-to-pdf"); model.addAttribute("currentPage", "file-to-pdf");
return "convert/file-to-pdf"; return "convert/file-to-pdf";
} }
// PDF TO......
//PDF TO......
@GetMapping("/pdf-to-html") @GetMapping("/pdf-to-html")
@Hidden @Hidden
public ModelAndView pdfToHTML() { public ModelAndView pdfToHTML() {
@ -107,7 +104,6 @@ public class ConverterWebController {
return modelAndView; return modelAndView;
} }
@GetMapping("/pdf-to-pdfa") @GetMapping("/pdf-to-pdfa")
@Hidden @Hidden
public String pdfToPdfAForm(Model model) { public String pdfToPdfAForm(Model model) {

View File

@ -32,70 +32,63 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class GeneralWebController { public class GeneralWebController {
@GetMapping("/pipeline")
@Hidden
@GetMapping("/pipeline") public String pipelineForm(Model model) {
@Hidden model.addAttribute("currentPage", "pipeline");
public String pipelineForm(Model model) {
model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>(); List<String> pipelineConfigs = new ArrayList<>();
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>(); List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
if(new File("./pipeline/defaultWebUIConfigs/").exists()) {
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
List<Path> jsonFiles = paths
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.collect(Collectors.toList());
for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content);
}
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, new TypeReference<Map<String, Object>>(){});
String name = (String) jsonContent.get("name");
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", config);
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
} catch (IOException e) {
e.printStackTrace();
}
}
if(pipelineConfigsWithNames.size() == 0) {
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", "");
configWithName.put("name", "No preloaded configs found");
pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
model.addAttribute("pipelineConfigs", pipelineConfigs); if (new File("./pipeline/defaultWebUIConfigs/").exists()) {
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
List<Path> jsonFiles =
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.collect(Collectors.toList());
return "pipeline"; for (Path jsonFile : jsonFiles) {
} String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content);
}
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent =
new ObjectMapper()
.readValue(config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name");
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", config);
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (pipelineConfigsWithNames.size() == 0) {
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", "");
configWithName.put("name", "No preloaded configs found");
pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
model.addAttribute("pipelineConfigs", pipelineConfigs);
return "pipeline";
}
@GetMapping("/merge-pdfs") @GetMapping("/merge-pdfs")
@Hidden @Hidden
public String mergePdfForm(Model model) { public String mergePdfForm(Model model) {
model.addAttribute("currentPage", "merge-pdfs"); model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs"; return "merge-pdfs";
} }
@GetMapping("/split-pdf-by-sections") @GetMapping("/split-pdf-by-sections")
@Hidden @Hidden
public String splitPdfBySections(Model model) { public String splitPdfBySections(Model model) {
@ -109,57 +102,56 @@ public class GeneralWebController {
model.addAttribute("currentPage", "view-pdf"); model.addAttribute("currentPage", "view-pdf");
return "view-pdf"; return "view-pdf";
} }
@GetMapping("/multi-tool") @GetMapping("/multi-tool")
@Hidden @Hidden
public String multiToolForm(Model model) { public String multiToolForm(Model model) {
model.addAttribute("currentPage", "multi-tool"); model.addAttribute("currentPage", "multi-tool");
return "multi-tool"; return "multi-tool";
} }
@GetMapping("/remove-pages") @GetMapping("/remove-pages")
@Hidden @Hidden
public String pageDeleter(Model model) { public String pageDeleter(Model model) {
model.addAttribute("currentPage", "remove-pages"); model.addAttribute("currentPage", "remove-pages");
return "remove-pages"; return "remove-pages";
} }
@GetMapping("/pdf-organizer") @GetMapping("/pdf-organizer")
@Hidden @Hidden
public String pageOrganizer(Model model) { public String pageOrganizer(Model model) {
model.addAttribute("currentPage", "pdf-organizer"); model.addAttribute("currentPage", "pdf-organizer");
return "pdf-organizer"; return "pdf-organizer";
} }
@GetMapping("/extract-page") @GetMapping("/extract-page")
@Hidden @Hidden
public String extractPages(Model model) { public String extractPages(Model model) {
model.addAttribute("currentPage", "extract-page"); model.addAttribute("currentPage", "extract-page");
return "extract-page"; return "extract-page";
} }
@GetMapping("/pdf-to-single-page") @GetMapping("/pdf-to-single-page")
@Hidden @Hidden
public String pdfToSinglePage(Model model) { public String pdfToSinglePage(Model model) {
model.addAttribute("currentPage", "pdf-to-single-page"); model.addAttribute("currentPage", "pdf-to-single-page");
return "pdf-to-single-page"; return "pdf-to-single-page";
} }
@GetMapping("/rotate-pdf") @GetMapping("/rotate-pdf")
@Hidden @Hidden
public String rotatePdfForm(Model model) { public String rotatePdfForm(Model model) {
model.addAttribute("currentPage", "rotate-pdf"); model.addAttribute("currentPage", "rotate-pdf");
return "rotate-pdf"; return "rotate-pdf";
} }
@GetMapping("/split-pdfs") @GetMapping("/split-pdfs")
@Hidden @Hidden
public String splitPdfForm(Model model) { public String splitPdfForm(Model model) {
model.addAttribute("currentPage", "split-pdfs"); model.addAttribute("currentPage", "split-pdfs");
return "split-pdfs"; return "split-pdfs";
} }
@GetMapping("/sign") @GetMapping("/sign")
@Hidden @Hidden
public String signForm(Model model) { public String signForm(Model model) {
@ -167,22 +159,20 @@ public class GeneralWebController {
model.addAttribute("fonts", getFontNames()); model.addAttribute("fonts", getFontNames());
return "sign"; return "sign";
} }
@GetMapping("/multi-page-layout") @GetMapping("/multi-page-layout")
@Hidden @Hidden
public String multiPageLayoutForm(Model model) { public String multiPageLayoutForm(Model model) {
model.addAttribute("currentPage", "multi-page-layout"); model.addAttribute("currentPage", "multi-page-layout");
return "multi-page-layout"; return "multi-page-layout";
} }
@GetMapping("/scale-pages") @GetMapping("/scale-pages")
@Hidden @Hidden
public String scalePagesFrom(Model model) { public String scalePagesFrom(Model model) {
model.addAttribute("currentPage", "scale-pages"); model.addAttribute("currentPage", "scale-pages");
return "scale-pages"; return "scale-pages";
} }
@GetMapping("/split-by-size-or-count") @GetMapping("/split-by-size-or-count")
@Hidden @Hidden
@ -190,18 +180,16 @@ public class GeneralWebController {
model.addAttribute("currentPage", "split-by-size-or-count"); model.addAttribute("currentPage", "split-by-size-or-count");
return "split-by-size-or-count"; return "split-by-size-or-count";
} }
@GetMapping("/overlay-pdf") @GetMapping("/overlay-pdf")
@Hidden @Hidden
public String overlayPdf(Model model) { public String overlayPdf(Model model) {
model.addAttribute("currentPage", "overlay-pdf"); model.addAttribute("currentPage", "overlay-pdf");
return "overlay-pdf"; return "overlay-pdf";
} }
@Autowired private ResourceLoader resourceLoader;
@Autowired
private ResourceLoader resourceLoader;
private List<FontResource> getFontNames() { private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>(); List<FontResource> fontNames = new ArrayList<>();
@ -216,25 +204,27 @@ public class GeneralWebController {
private List<FontResource> getFontNamesFromLocation(String locationPattern) { private List<FontResource> getFontNamesFromLocation(String locationPattern) {
try { try {
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader) Resource[] resources =
.getResources(locationPattern); ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
.getResources(locationPattern);
return Arrays.stream(resources) return Arrays.stream(resources)
.map(resource -> { .map(
try { resource -> {
String filename = resource.getFilename(); try {
if (filename != null) { String filename = resource.getFilename();
int lastDotIndex = filename.lastIndexOf('.'); if (filename != null) {
if (lastDotIndex != -1) { int lastDotIndex = filename.lastIndexOf('.');
String name = filename.substring(0, lastDotIndex); if (lastDotIndex != -1) {
String extension = filename.substring(lastDotIndex + 1); String name = filename.substring(0, lastDotIndex);
return new FontResource(name, extension); String extension = filename.substring(lastDotIndex + 1);
return new FontResource(name, extension);
}
}
return null;
} catch (Exception e) {
throw new RuntimeException("Error processing filename", e);
} }
} })
return null;
} catch (Exception e) {
throw new RuntimeException("Error processing filename", e);
}
})
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());
} catch (Exception e) { } catch (Exception e) {
@ -242,64 +232,65 @@ public class GeneralWebController {
} }
} }
public String getFormatFromExtension(String extension) { public String getFormatFromExtension(String extension) {
switch (extension) { switch (extension) {
case "ttf": return "truetype"; case "ttf":
case "woff": return "woff"; return "truetype";
case "woff2": return "woff2"; case "woff":
case "eot": return "embedded-opentype"; return "woff";
case "svg": return "svg"; case "woff2":
default: return ""; // or throw an exception if an unexpected extension is encountered 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 { public class FontResource {
private String name; private String name;
private String extension; private String extension;
private String type; private String type;
public FontResource(String name, String extension) { public FontResource(String name, String extension) {
this.name = name; this.name = name;
this.extension = extension; this.extension = extension;
this.type = getFormatFromExtension(extension); this.type = getFormatFromExtension(extension);
} }
public String getName() { public String getName() {
return name; return name;
} }
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public String getExtension() { public String getExtension() {
return extension; return extension;
} }
public void setExtension(String extension) { public void setExtension(String extension) {
this.extension = extension; this.extension = extension;
} }
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
} }
@GetMapping("/crop") @GetMapping("/crop")
@Hidden @Hidden
public String cropForm(Model model) { public String cropForm(Model model) {
model.addAttribute("currentPage", "crop"); model.addAttribute("currentPage", "crop");
return "crop"; return "crop";
} }
@GetMapping("/auto-split-pdf") @GetMapping("/auto-split-pdf")
@Hidden @Hidden

View File

@ -8,20 +8,19 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Controller @Controller
public class HomeWebController { public class HomeWebController {
@GetMapping("/about") @GetMapping("/about")
@Hidden @Hidden
public String gameForm(Model model) { public String gameForm(Model model) {
model.addAttribute("currentPage", "about"); model.addAttribute("currentPage", "about");
return "about"; return "about";
} }
@GetMapping("/") @GetMapping("/")
public String home(Model model) { public String home(Model model) {
model.addAttribute("currentPage", "home"); model.addAttribute("currentPage", "home");
@ -32,21 +31,18 @@ public class HomeWebController {
public String root(Model model) { public String root(Model model) {
return "redirect:/"; return "redirect:/";
} }
@Autowired
ApplicationProperties applicationProperties;
@Autowired ApplicationProperties applicationProperties;
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody @ResponseBody
@Hidden @Hidden
public String getRobotsTxt() { public String getRobotsTxt() {
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility(); Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
if(Boolean.TRUE.equals(allowGoogle)) { if (Boolean.TRUE.equals(allowGoogle)) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /"; return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else { } else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /"; return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Comparator; import java.util.Comparator;
@ -22,6 +23,7 @@ import io.micrometer.core.instrument.MeterRegistry;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -31,30 +33,28 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Tag(name = "Info", description = "Info APIs") @Tag(name = "Info", description = "Info APIs")
public class MetricsController { public class MetricsController {
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
private boolean metricsEnabled; private boolean metricsEnabled;
@PostConstruct @PostConstruct
public void init() { public void init() {
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
if(metricsEnabled == null) if (metricsEnabled == null) metricsEnabled = true;
metricsEnabled = true;
this.metricsEnabled = metricsEnabled; this.metricsEnabled = metricsEnabled;
} }
public MetricsController(MeterRegistry meterRegistry) { public MetricsController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
} }
@GetMapping("/status") @GetMapping("/status")
@Operation(summary = "Application status and version", @Operation(
description = "This endpoint returns the status of the application and its version number.") summary = "Application status and version",
description =
"This endpoint returns the status of the application and its version number.")
public ResponseEntity<?> getStatus() { public ResponseEntity<?> getStatus() {
if (!metricsEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
@ -65,38 +65,46 @@ public class MetricsController {
status.put("version", getClass().getPackage().getImplementationVersion()); status.put("version", getClass().getPackage().getImplementationVersion());
return ResponseEntity.ok(status); return ResponseEntity.ok(status);
} }
@GetMapping("/loads") @GetMapping("/loads")
@Operation(summary = "GET request count", @Operation(
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") summary = "GET request count",
public ResponseEntity<?> getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) { description =
if (!metricsEnabled) { "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
public ResponseEntity<?> getPageLoads(
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
Optional<String> endpoint) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
double count = 0.0; double count = 0.0;
for (Meter meter : meterRegistry.getMeters()) { for (Meter meter : meterRegistry.getMeters()) {
if (meter.getId().getName().equals("http.requests")) { if (meter.getId().getName().equals("http.requests")) {
String method = meter.getId().getTag("method"); String method = meter.getId().getTag("method");
if (method != null && method.equals("GET")) { if (method != null && method.equals("GET")) {
if (endpoint.isPresent() && !endpoint.get().isBlank()) { if (endpoint.isPresent() && !endpoint.get().isBlank()) {
if(!endpoint.get().startsWith("/")) { if (!endpoint.get().startsWith("/")) {
endpoint = Optional.of("/" + endpoint.get()); endpoint = Optional.of("/" + endpoint.get());
} }
System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri")); System.out.println(
if(endpoint.get().equals(meter.getId().getTag("uri"))){ "loads "
if (meter instanceof Counter) { + endpoint.get()
count += ((Counter) meter).count(); + " vs "
} + meter.getId().getTag("uri"));
} if (endpoint.get().equals(meter.getId().getTag("uri"))) {
} else { if (meter instanceof Counter) {
if (meter instanceof Counter) { count += ((Counter) meter).count();
count += ((Counter) meter).count(); }
} }
} } else {
if (meter instanceof Counter) {
count += ((Counter) meter).count();
}
}
} }
} }
} }
@ -108,10 +116,11 @@ public class MetricsController {
} }
@GetMapping("/loads/all") @GetMapping("/loads/all")
@Operation(summary = "GET requests count for all endpoints", @Operation(
summary = "GET requests count for all endpoints",
description = "This endpoint returns the count of GET requests for each endpoint.") description = "This endpoint returns the count of GET requests for each endpoint.")
public ResponseEntity<?> getAllEndpointLoads() { public ResponseEntity<?> getAllEndpointLoads() {
if (!metricsEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
@ -133,10 +142,11 @@ public class MetricsController {
} }
} }
List<EndpointCount> results = counts.entrySet().stream() List<EndpointCount> results =
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) counts.entrySet().stream()
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.collect(Collectors.toList()); .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
return ResponseEntity.ok(results); return ResponseEntity.ok(results);
} catch (Exception e) { } catch (Exception e) {
@ -147,35 +157,41 @@ public class MetricsController {
public class EndpointCount { public class EndpointCount {
private String endpoint; private String endpoint;
private double count; private double count;
public EndpointCount(String endpoint, double count) {
this.endpoint = endpoint;
this.count = count;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public double getCount() {
return count;
}
public void setCount(double count) {
this.count = count;
}
public EndpointCount(String endpoint, double count) {
this.endpoint = endpoint;
this.count = count;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public double getCount() {
return count;
}
public void setCount(double count) {
this.count = count;
}
} }
@GetMapping("/requests") @GetMapping("/requests")
@Operation(summary = "POST request count", @Operation(
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") summary = "POST request count",
public ResponseEntity<?> getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) { description =
if (!metricsEnabled) { "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
public ResponseEntity<?> getTotalRequests(
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
Optional<String> endpoint) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
double count = 0.0; double count = 0.0;
for (Meter meter : meterRegistry.getMeters()) { for (Meter meter : meterRegistry.getMeters()) {
@ -199,18 +215,18 @@ public class MetricsController {
} }
} }
} }
return ResponseEntity.ok(count); return ResponseEntity.ok(count);
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.ok(-1); return ResponseEntity.ok(-1);
} }
} }
@GetMapping("/requests/all") @GetMapping("/requests/all")
@Operation(summary = "POST requests count for all endpoints", @Operation(
summary = "POST requests count for all endpoints",
description = "This endpoint returns the count of POST requests for each endpoint.") description = "This endpoint returns the count of POST requests for each endpoint.")
public ResponseEntity<?> getAllPostRequests() { public ResponseEntity<?> getAllPostRequests() {
if (!metricsEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
@ -232,10 +248,11 @@ public class MetricsController {
} }
} }
List<EndpointCount> results = counts.entrySet().stream() List<EndpointCount> results =
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) counts.entrySet().stream()
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.collect(Collectors.toList()); .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
return ResponseEntity.ok(results); return ResponseEntity.ok(results);
} catch (Exception e) { } catch (Exception e) {
@ -243,7 +260,6 @@ public class MetricsController {
} }
} }
@GetMapping("/uptime") @GetMapping("/uptime")
public ResponseEntity<?> getUptime() { public ResponseEntity<?> getUptime() {
if (!metricsEnabled) { if (!metricsEnabled) {

View File

@ -23,7 +23,7 @@ public class OtherWebController {
model.addAttribute("currentPage", "compress-pdf"); model.addAttribute("currentPage", "compress-pdf");
return "misc/compress-pdf"; return "misc/compress-pdf";
} }
@GetMapping("/extract-image-scans") @GetMapping("/extract-image-scans")
@Hidden @Hidden
public ModelAndView extractImageScansForm() { public ModelAndView extractImageScansForm() {
@ -31,37 +31,34 @@ public class OtherWebController {
modelAndView.addObject("currentPage", "extract-image-scans"); modelAndView.addObject("currentPage", "extract-image-scans");
return modelAndView; return modelAndView;
} }
@GetMapping("/show-javascript") @GetMapping("/show-javascript")
@Hidden @Hidden
public String extractJavascriptForm(Model model) { public String extractJavascriptForm(Model model) {
model.addAttribute("currentPage", "show-javascript"); model.addAttribute("currentPage", "show-javascript");
return "misc/show-javascript"; return "misc/show-javascript";
} }
@GetMapping("/add-page-numbers") @GetMapping("/add-page-numbers")
@Hidden @Hidden
public String addPageNumbersForm(Model model) { public String addPageNumbersForm(Model model) {
model.addAttribute("currentPage", "add-page-numbers"); model.addAttribute("currentPage", "add-page-numbers");
return "misc/add-page-numbers"; return "misc/add-page-numbers";
} }
@GetMapping("/extract-images") @GetMapping("/extract-images")
@Hidden @Hidden
public String extractImagesForm(Model model) { public String extractImagesForm(Model model) {
model.addAttribute("currentPage", "extract-images"); model.addAttribute("currentPage", "extract-images");
return "misc/extract-images"; return "misc/extract-images";
} }
@GetMapping("/flatten") @GetMapping("/flatten")
@Hidden @Hidden
public String flattenForm(Model model) { public String flattenForm(Model model) {
model.addAttribute("currentPage", "flatten"); model.addAttribute("currentPage", "flatten");
return "misc/flatten"; return "misc/flatten";
} }
@GetMapping("/change-metadata") @GetMapping("/change-metadata")
@Hidden @Hidden
@ -69,22 +66,25 @@ public class OtherWebController {
model.addAttribute("currentPage", "change-metadata"); model.addAttribute("currentPage", "change-metadata");
return "misc/change-metadata"; return "misc/change-metadata";
} }
@GetMapping("/compare") @GetMapping("/compare")
@Hidden @Hidden
public String compareForm(Model model) { public String compareForm(Model model) {
model.addAttribute("currentPage", "compare"); model.addAttribute("currentPage", "compare");
return "misc/compare"; return "misc/compare";
} }
public List<String> getAvailableTesseractLanguages() { public List<String> getAvailableTesseractLanguages() {
String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata"; String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata";
File[] files = new File(tessdataDir).listFiles(); File[] files = new File(tessdataDir).listFiles();
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) return Arrays.stream(files)
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); .filter(file -> file.getName().endsWith(".traineddata"))
.map(file -> file.getName().replace(".traineddata", ""))
.filter(lang -> !lang.equalsIgnoreCase("osd"))
.collect(Collectors.toList());
} }
@GetMapping("/ocr-pdf") @GetMapping("/ocr-pdf")
@ -97,29 +97,28 @@ public class OtherWebController {
modelAndView.addObject("currentPage", "ocr-pdf"); modelAndView.addObject("currentPage", "ocr-pdf");
return modelAndView; return modelAndView;
} }
@GetMapping("/add-image") @GetMapping("/add-image")
@Hidden @Hidden
public String overlayImage(Model model) { public String overlayImage(Model model) {
model.addAttribute("currentPage", "add-image"); model.addAttribute("currentPage", "add-image");
return "misc/add-image"; return "misc/add-image";
} }
@GetMapping("/adjust-contrast") @GetMapping("/adjust-contrast")
@Hidden @Hidden
public String contrast(Model model) { public String contrast(Model model) {
model.addAttribute("currentPage", "adjust-contrast"); model.addAttribute("currentPage", "adjust-contrast");
return "misc/adjust-contrast"; return "misc/adjust-contrast";
} }
@GetMapping("/repair") @GetMapping("/repair")
@Hidden @Hidden
public String repairForm(Model model) { public String repairForm(Model model) {
model.addAttribute("currentPage", "repair"); model.addAttribute("currentPage", "repair");
return "misc/repair"; return "misc/repair";
} }
@GetMapping("/remove-blanks") @GetMapping("/remove-blanks")
@Hidden @Hidden
public String removeBlanksForm(Model model) { public String removeBlanksForm(Model model) {
@ -140,14 +139,11 @@ public class OtherWebController {
model.addAttribute("currentPage", "auto-crop"); model.addAttribute("currentPage", "auto-crop");
return "misc/auto-crop"; return "misc/auto-crop";
} }
@GetMapping("/auto-rename") @GetMapping("/auto-rename")
@Hidden @Hidden
public String autoRenameForm(Model model) { public String autoRenameForm(Model model) {
model.addAttribute("currentPage", "auto-rename"); model.addAttribute("currentPage", "auto-rename");
return "misc/auto-rename"; return "misc/auto-rename";
} }
} }

View File

@ -10,20 +10,21 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class SecurityWebController { public class SecurityWebController {
@GetMapping("/auto-redact") @GetMapping("/auto-redact")
@Hidden @Hidden
public String autoRedactForm(Model model) { public String autoRedactForm(Model model) {
model.addAttribute("currentPage", "auto-redact"); model.addAttribute("currentPage", "auto-redact");
return "security/auto-redact"; return "security/auto-redact";
} }
@GetMapping("/add-password") @GetMapping("/add-password")
@Hidden @Hidden
public String addPasswordForm(Model model) { public String addPasswordForm(Model model) {
model.addAttribute("currentPage", "add-password"); model.addAttribute("currentPage", "add-password");
return "security/add-password"; return "security/add-password";
} }
@GetMapping("/change-permissions") @GetMapping("/change-permissions")
@Hidden @Hidden
public String permissionsForm(Model model) { public String permissionsForm(Model model) {
@ -44,21 +45,21 @@ public class SecurityWebController {
model.addAttribute("currentPage", "add-watermark"); model.addAttribute("currentPage", "add-watermark");
return "security/add-watermark"; return "security/add-watermark";
} }
@GetMapping("/cert-sign") @GetMapping("/cert-sign")
@Hidden @Hidden
public String certSignForm(Model model) { public String certSignForm(Model model) {
model.addAttribute("currentPage", "cert-sign"); model.addAttribute("currentPage", "cert-sign");
return "security/cert-sign"; return "security/cert-sign";
} }
@GetMapping("/sanitize-pdf") @GetMapping("/sanitize-pdf")
@Hidden @Hidden
public String sanitizeForm(Model model) { public String sanitizeForm(Model model) {
model.addAttribute("currentPage", "sanitize-pdf"); model.addAttribute("currentPage", "sanitize-pdf");
return "security/sanitize-pdf"; return "security/sanitize-pdf";
} }
@GetMapping("/get-info-on-pdf") @GetMapping("/get-info-on-pdf")
@Hidden @Hidden
public String getInfo(Model model) { public String getInfo(Model model) {

View File

@ -9,14 +9,16 @@ public class ApiEndpoint {
private String name; private String name;
private Map<String, JsonNode> parameters; private Map<String, JsonNode> parameters;
private String description; private String description;
public ApiEndpoint(String name, JsonNode postNode) { public ApiEndpoint(String name, JsonNode postNode) {
this.name = name; this.name = name;
this.parameters = new HashMap<>(); this.parameters = new HashMap<>();
postNode.path("parameters").forEach(paramNode -> { postNode.path("parameters")
String paramName = paramNode.path("name").asText(); .forEach(
parameters.put(paramName, paramNode); paramNode -> {
}); String paramName = paramNode.path("name").asText();
parameters.put(paramName, paramNode);
});
this.description = postNode.path("description").asText(); this.description = postNode.path("description").asText();
} }
@ -32,11 +34,9 @@ public class ApiEndpoint {
public String getDescription() { public String getDescription() {
return description; return description;
} }
@Override @Override
public String toString() { public String toString() {
return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]"; return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]";
} }
}
}

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.util.Collection; import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
@ -16,9 +17,10 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
setAuthenticated(false); setAuthenticated(false);
} }
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) { public ApiKeyAuthenticationToken(
Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
this.principal = principal; // principal can be a UserDetails object this.principal = principal; // principal can be a UserDetails object
this.credentials = apiKey; this.credentials = apiKey;
super.setAuthenticated(true); // this authentication is trusted super.setAuthenticated(true); // this authentication is trusted
} }
@ -36,7 +38,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) { if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); throw new IllegalArgumentException(
"Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead.");
} }
super.setAuthenticated(false); super.setAuthenticated(false);
} }

View File

@ -12,357 +12,376 @@ import stirling.software.SPDF.config.YamlPropertySourceFactory;
@ConfigurationProperties(prefix = "") @ConfigurationProperties(prefix = "")
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class) @PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
public class ApplicationProperties { public class ApplicationProperties {
private Security security; private Security security;
private System system; private System system;
private Ui ui; private Ui ui;
private Endpoints endpoints; private Endpoints endpoints;
private Metrics metrics; private Metrics metrics;
private AutomaticallyGenerated automaticallyGenerated; private AutomaticallyGenerated automaticallyGenerated;
private AutoPipeline autoPipeline; private AutoPipeline autoPipeline;
public AutoPipeline getAutoPipeline() { public AutoPipeline getAutoPipeline() {
return autoPipeline != null ? autoPipeline : new AutoPipeline(); return autoPipeline != null ? autoPipeline : new AutoPipeline();
} }
public void setAutoPipeline(AutoPipeline autoPipeline) { public void setAutoPipeline(AutoPipeline autoPipeline) {
this.autoPipeline = autoPipeline; this.autoPipeline = autoPipeline;
} }
public Security getSecurity() { public Security getSecurity() {
return security != null ? security : new Security(); return security != null ? security : new Security();
} }
public void setSecurity(Security security) { public void setSecurity(Security security) {
this.security = security; this.security = security;
} }
public System getSystem() { public System getSystem() {
return system != null ? system : new System(); return system != null ? system : new System();
} }
public void setSystem(System system) { public void setSystem(System system) {
this.system = system; this.system = system;
} }
public Ui getUi() { public Ui getUi() {
return ui != null ? ui : new Ui(); return ui != null ? ui : new Ui();
} }
public void setUi(Ui ui) { public void setUi(Ui ui) {
this.ui = ui; this.ui = ui;
} }
public Endpoints getEndpoints() { public Endpoints getEndpoints() {
return endpoints != null ? endpoints : new Endpoints(); return endpoints != null ? endpoints : new Endpoints();
} }
public void setEndpoints(Endpoints endpoints) { public void setEndpoints(Endpoints endpoints) {
this.endpoints = endpoints; this.endpoints = endpoints;
} }
public Metrics getMetrics() { public Metrics getMetrics() {
return metrics != null ? metrics : new Metrics(); return metrics != null ? metrics : new Metrics();
} }
public void setMetrics(Metrics metrics) { public void setMetrics(Metrics metrics) {
this.metrics = metrics; this.metrics = metrics;
} }
public AutomaticallyGenerated getAutomaticallyGenerated() { public AutomaticallyGenerated getAutomaticallyGenerated() {
return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated(); return automaticallyGenerated != null
} ? automaticallyGenerated
: new AutomaticallyGenerated();
public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) { }
this.automaticallyGenerated = automaticallyGenerated;
} public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) {
this.automaticallyGenerated = automaticallyGenerated;
@Override }
public String toString() {
return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints=" @Override
+ endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated public String toString() {
+ ", autoPipeline=" + autoPipeline + "]"; return "ApplicationProperties [security="
} + security
+ ", system="
public static class AutoPipeline { + system
private String outputFolder; + ", ui="
+ ui
public String getOutputFolder() { + ", endpoints="
return outputFolder; + endpoints
} + ", metrics="
+ metrics
public void setOutputFolder(String outputFolder) { + ", automaticallyGenerated="
this.outputFolder = outputFolder; + automaticallyGenerated
} + ", autoPipeline="
+ autoPipeline
@Override + "]";
public String toString() { }
return "AutoPipeline [outputFolder=" + outputFolder + "]";
} public static class AutoPipeline {
private String outputFolder;
public String getOutputFolder() {
} return outputFolder;
public static class Security { }
private Boolean enableLogin;
private Boolean csrfDisabled; public void setOutputFolder(String outputFolder) {
private InitialLogin initialLogin; this.outputFolder = outputFolder;
private int loginAttemptCount; }
private long loginResetTimeMinutes;
@Override
public String toString() {
public int getLoginAttemptCount() { return "AutoPipeline [outputFolder=" + outputFolder + "]";
return loginAttemptCount; }
} }
public void setLoginAttemptCount(int loginAttemptCount) { public static class Security {
this.loginAttemptCount = loginAttemptCount; private Boolean enableLogin;
} private Boolean csrfDisabled;
private InitialLogin initialLogin;
public long getLoginResetTimeMinutes() { private int loginAttemptCount;
return loginResetTimeMinutes; private long loginResetTimeMinutes;
}
public int getLoginAttemptCount() {
public void setLoginResetTimeMinutes(long loginResetTimeMinutes) { return loginAttemptCount;
this.loginResetTimeMinutes = loginResetTimeMinutes; }
}
public void setLoginAttemptCount(int loginAttemptCount) {
public InitialLogin getInitialLogin() { this.loginAttemptCount = loginAttemptCount;
return initialLogin != null ? initialLogin : new InitialLogin(); }
}
public long getLoginResetTimeMinutes() {
public void setInitialLogin(InitialLogin initialLogin) { return loginResetTimeMinutes;
this.initialLogin = initialLogin; }
}
public void setLoginResetTimeMinutes(long loginResetTimeMinutes) {
public Boolean getEnableLogin() { this.loginResetTimeMinutes = loginResetTimeMinutes;
return enableLogin; }
}
public InitialLogin getInitialLogin() {
public void setEnableLogin(Boolean enableLogin) { return initialLogin != null ? initialLogin : new InitialLogin();
this.enableLogin = enableLogin; }
}
public void setInitialLogin(InitialLogin initialLogin) {
public Boolean getCsrfDisabled() { this.initialLogin = initialLogin;
return csrfDisabled; }
}
public Boolean getEnableLogin() {
public void setCsrfDisabled(Boolean csrfDisabled) { return enableLogin;
this.csrfDisabled = csrfDisabled; }
}
public void setEnableLogin(Boolean enableLogin) {
this.enableLogin = enableLogin;
@Override }
public String toString() {
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" public Boolean getCsrfDisabled() {
+ csrfDisabled + "]"; return csrfDisabled;
} }
public static class InitialLogin { public void setCsrfDisabled(Boolean csrfDisabled) {
this.csrfDisabled = csrfDisabled;
private String username; }
private String password;
@Override
public String getUsername() { public String toString() {
return username; return "Security [enableLogin="
} + enableLogin
+ ", initialLogin="
public void setUsername(String username) { + initialLogin
this.username = username; + ", csrfDisabled="
} + csrfDisabled
+ "]";
public String getPassword() { }
return password;
} public static class InitialLogin {
public void setPassword(String password) { private String username;
this.password = password; private String password;
}
public String getUsername() {
@Override return username;
public String toString() { }
return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]";
} public void setUsername(String username) {
this.username = username;
}
} public String getPassword() {
} return password;
}
public static class System {
private String defaultLocale; public void setPassword(String password) {
private Boolean googlevisibility; this.password = password;
private String rootURIPath; }
private String customStaticFilePath;
private Integer maxFileSize; @Override
public String toString() {
private Boolean enableAlphaFunctionality; return "InitialLogin [username="
+ username
+ ", password="
+ (password != null && !password.isEmpty() ? "MASKED" : "NULL")
+ "]";
public Boolean getEnableAlphaFunctionality() { }
return enableAlphaFunctionality; }
} }
public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) { public static class System {
this.enableAlphaFunctionality = enableAlphaFunctionality; private String defaultLocale;
} private Boolean googlevisibility;
private String rootURIPath;
public String getDefaultLocale() { private String customStaticFilePath;
return defaultLocale; private Integer maxFileSize;
}
private Boolean enableAlphaFunctionality;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale; public Boolean getEnableAlphaFunctionality() {
} return enableAlphaFunctionality;
}
public Boolean getGooglevisibility() {
return googlevisibility; public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) {
} this.enableAlphaFunctionality = enableAlphaFunctionality;
}
public void setGooglevisibility(Boolean googlevisibility) {
this.googlevisibility = googlevisibility; public String getDefaultLocale() {
} return defaultLocale;
}
public String getRootURIPath() {
return rootURIPath; public void setDefaultLocale(String defaultLocale) {
} this.defaultLocale = defaultLocale;
}
public void setRootURIPath(String rootURIPath) {
this.rootURIPath = rootURIPath; public Boolean getGooglevisibility() {
} return googlevisibility;
}
public String getCustomStaticFilePath() {
return customStaticFilePath; public void setGooglevisibility(Boolean googlevisibility) {
} this.googlevisibility = googlevisibility;
}
public void setCustomStaticFilePath(String customStaticFilePath) {
this.customStaticFilePath = customStaticFilePath; public String getRootURIPath() {
} return rootURIPath;
}
public Integer getMaxFileSize() {
return maxFileSize; public void setRootURIPath(String rootURIPath) {
} this.rootURIPath = rootURIPath;
}
public void setMaxFileSize(Integer maxFileSize) {
this.maxFileSize = maxFileSize; public String getCustomStaticFilePath() {
} return customStaticFilePath;
}
@Override
public String toString() { public void setCustomStaticFilePath(String customStaticFilePath) {
return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility this.customStaticFilePath = customStaticFilePath;
+ ", rootURIPath=" + rootURIPath + ", customStaticFilePath=" + customStaticFilePath }
+ ", maxFileSize=" + maxFileSize + ", enableAlphaFunctionality=" + enableAlphaFunctionality + "]";
} public Integer getMaxFileSize() {
return maxFileSize;
}
} public void setMaxFileSize(Integer maxFileSize) {
this.maxFileSize = maxFileSize;
public static class Ui { }
private String appName;
private String homeDescription; @Override
private String appNameNavbar; public String toString() {
return "System [defaultLocale="
public String getAppName() { + defaultLocale
if(appName != null && appName.trim().length() == 0) + ", googlevisibility="
return null; + googlevisibility
return appName; + ", rootURIPath="
} + rootURIPath
+ ", customStaticFilePath="
public void setAppName(String appName) { + customStaticFilePath
this.appName = appName; + ", maxFileSize="
} + maxFileSize
+ ", enableAlphaFunctionality="
public String getHomeDescription() { + enableAlphaFunctionality
if(homeDescription != null && homeDescription.trim().length() == 0) + "]";
return null; }
return homeDescription; }
}
public static class Ui {
public void setHomeDescription(String homeDescription) { private String appName;
this.homeDescription = homeDescription; private String homeDescription;
} private String appNameNavbar;
public String getAppNameNavbar() { public String getAppName() {
if(appNameNavbar != null && appNameNavbar.trim().length() == 0) if (appName != null && appName.trim().length() == 0) return null;
return null; return appName;
return appNameNavbar; }
}
public void setAppName(String appName) {
public void setAppNameNavbar(String appNameNavbar) { this.appName = appName;
this.appNameNavbar = appNameNavbar; }
}
public String getHomeDescription() {
@Override if (homeDescription != null && homeDescription.trim().length() == 0) return null;
public String toString() { return homeDescription;
return "UserInterface [appName=" + appName + ", homeDescription=" + homeDescription + ", appNameNavbar=" + appNameNavbar + "]"; }
}
} public void setHomeDescription(String homeDescription) {
this.homeDescription = homeDescription;
}
public static class Endpoints {
private List<String> toRemove; public String getAppNameNavbar() {
private List<String> groupsToRemove; if (appNameNavbar != null && appNameNavbar.trim().length() == 0) return null;
return appNameNavbar;
public List<String> getToRemove() { }
return toRemove;
} public void setAppNameNavbar(String appNameNavbar) {
this.appNameNavbar = appNameNavbar;
public void setToRemove(List<String> toRemove) { }
this.toRemove = toRemove;
} @Override
public String toString() {
public List<String> getGroupsToRemove() { return "UserInterface [appName="
return groupsToRemove; + appName
} + ", homeDescription="
+ homeDescription
public void setGroupsToRemove(List<String> groupsToRemove) { + ", appNameNavbar="
this.groupsToRemove = groupsToRemove; + appNameNavbar
} + "]";
}
@Override }
public String toString() {
return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]"; public static class Endpoints {
} private List<String> toRemove;
private List<String> groupsToRemove;
} public List<String> getToRemove() {
return toRemove;
public static class Metrics { }
private Boolean enabled;
public void setToRemove(List<String> toRemove) {
public Boolean getEnabled() { this.toRemove = toRemove;
return enabled; }
}
public List<String> getGroupsToRemove() {
public void setEnabled(Boolean enabled) { return groupsToRemove;
this.enabled = enabled; }
}
public void setGroupsToRemove(List<String> groupsToRemove) {
@Override this.groupsToRemove = groupsToRemove;
public String toString() { }
return "Metrics [enabled=" + enabled + "]";
} @Override
public String toString() {
return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]";
} }
}
public static class AutomaticallyGenerated {
private String key; public static class Metrics {
private Boolean enabled;
public String getKey() {
return key; public Boolean getEnabled() {
} return enabled;
}
public void setKey(String key) {
this.key = key; public void setEnabled(Boolean enabled) {
} this.enabled = enabled;
}
@Override
public String toString() { @Override
return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]"; public String toString() {
} return "Metrics [enabled=" + enabled + "]";
}
} }
public static class AutomaticallyGenerated {
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
@Override
public String toString() {
return "AutomaticallyGenerated [key="
+ (key != null && !key.isEmpty() ? "MASKED" : "NULL")
+ "]";
}
}
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public class AttemptCounter { public class AttemptCounter {
private int attemptCount; private int attemptCount;
private long lastAttemptTime; private long lastAttemptTime;

View File

@ -13,19 +13,15 @@ import jakarta.persistence.Table;
@Table(name = "authorities") @Table(name = "authorities")
public class Authority { public class Authority {
public Authority() { public Authority() {}
} public Authority(String authority, User user) {
this.authority = authority;
this.user = user;
public Authority(String authority, User user) { user.getAuthorities().add(this);
this.authority = authority; }
this.user = user;
user.getAuthorities().add(this);
}
@Id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ -36,29 +32,27 @@ public class Authority {
@JoinColumn(name = "user_id") @JoinColumn(name = "user_id")
private User user; private User user;
public Long getId() { public Long getId() {
return id; return id;
} }
public void setId(Long id) { public void setId(Long id) {
this.id = id; this.id = id;
} }
public String getAuthority() { public String getAuthority() {
return authority; return authority;
} }
public void setAuthority(String authority) { public void setAuthority(String authority) {
this.authority = authority; this.authority = authority;
} }
public User getUser() { public User getUser() {
return user; return user;
} }
public void setUser(User user) { public void setUser(User user) {
this.user = user; this.user = user;
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public class PDFText { public class PDFText {
private final int pageIndex; private final int pageIndex;
private final float x1; private final float x1;
@ -39,4 +40,4 @@ public class PDFText {
public String getText() { public String getText() {
return text; return text;
} }
} }

View File

@ -24,38 +24,37 @@ public class PersistentLogin {
@Column(name = "last_used", nullable = false) @Column(name = "last_used", nullable = false)
private Date lastUsed; private Date lastUsed;
public String getSeries() { public String getSeries() {
return series; return series;
} }
public void setSeries(String series) { public void setSeries(String series) {
this.series = series; this.series = series;
} }
public String getUsername() { public String getUsername() {
return username; return username;
} }
public void setUsername(String username) { public void setUsername(String username) {
this.username = username; this.username = username;
} }
public String getToken() { public String getToken() {
return token; return token;
} }
public void setToken(String token) { public void setToken(String token) {
this.token = token; this.token = token;
} }
public Date getLastUsed() { public Date getLastUsed() {
return lastUsed; return lastUsed;
} }
public void setLastUsed(Date lastUsed) { public void setLastUsed(Date lastUsed) {
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
} }
// Getters, setters, etc. // Getters, setters, etc.
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
@ -14,7 +15,6 @@ public class PipelineConfig {
@JsonProperty("outputFileName") @JsonProperty("outputFileName")
private String outputPattern; private String outputPattern;
public String getName() { public String getName() {
return name; return name;
} }
@ -46,6 +46,4 @@ public class PipelineConfig {
public void setOutputPattern(String outputPattern) { public void setOutputPattern(String outputPattern) {
this.outputPattern = outputPattern; this.outputPattern = outputPattern;
} }
} }

View File

@ -3,30 +3,27 @@ package stirling.software.SPDF.model;
import java.util.Map; import java.util.Map;
public class PipelineOperation { public class PipelineOperation {
private String operation; private String operation;
private Map<String, Object> parameters; private Map<String, Object> parameters;
public String getOperation() {
return operation;
}
public String getOperation() { public void setOperation(String operation) {
return operation; this.operation = operation;
} }
public void setOperation(String operation) { public Map<String, Object> getParameters() {
this.operation = operation; return parameters;
} }
public Map<String, Object> getParameters() { public void setParameters(Map<String, Object> parameters) {
return parameters; this.parameters = parameters;
} }
public void setParameters(Map<String, Object> parameters) { @Override
this.parameters = parameters; public String toString() {
} return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
}
@Override }
public String toString() {
return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
}
}

View File

@ -1,7 +1,8 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public enum Role { public enum Role {
// Unlimited access // Unlimited access
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE), ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE),
// Unlimited access // Unlimited access
@ -15,12 +16,11 @@ public enum Role {
// 0 API calls per day and 20 web calls // 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20), WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20),
INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
DEMO_USER("ROLE_DEMO_USER", 100, 100); INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
DEMO_USER("ROLE_DEMO_USER", 100, 100);
private final String roleId; private final String roleId;
private final int apiCallsPerDay; private final int apiCallsPerDay;
private final int webCallsPerDay; private final int webCallsPerDay;
@ -42,7 +42,7 @@ public enum Role {
public int getWebCallsPerDay() { public int getWebCallsPerDay() {
return webCallsPerDay; return webCallsPerDay;
} }
public static Role fromString(String roleId) { public static Role fromString(String roleId) {
for (Role role : Role.values()) { for (Role role : Role.values()) {
if (role.getRoleId().equalsIgnoreCase(roleId)) { if (role.getRoleId().equalsIgnoreCase(roleId)) {
@ -51,5 +51,4 @@ public enum Role {
} }
throw new IllegalArgumentException("No Role defined for id: " + roleId); throw new IllegalArgumentException("No Role defined for id: " + roleId);
} }
} }

View File

@ -1,4 +1,12 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public enum SortTypes { public enum SortTypes {
REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, SIDE_STITCH_BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST, REVERSE_ORDER,
} DUPLEX_SORT,
BOOKLET_SORT,
SIDE_STITCH_BOOKLET_SORT,
ODD_EVEN_SPLIT,
REMOVE_FIRST,
REMOVE_LAST,
REMOVE_FIRST_AND_LAST,
}

View File

@ -19,15 +19,16 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapKeyColumn; import jakarta.persistence.MapKeyColumn;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
public class User { public class User {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id") @Column(name = "user_id")
private Long id; private Long id;
@Column(name = "username", unique = true) @Column(name = "username", unique = true)
private String username; private String username;
@ -36,13 +37,13 @@ public class User {
@Column(name = "apiKey") @Column(name = "apiKey")
private String apiKey; private String apiKey;
@Column(name = "enabled") @Column(name = "enabled")
private boolean enabled; private boolean enabled;
@Column(name = "isFirstLogin") @Column(name = "isFirstLogin")
private Boolean isFirstLogin = false; private Boolean isFirstLogin = false;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>(); private Set<Authority> authorities = new HashSet<>();
@ -50,85 +51,83 @@ public class User {
@MapKeyColumn(name = "setting_key") @MapKeyColumn(name = "setting_key")
@Column(name = "setting_value") @Column(name = "setting_value")
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings. private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
public boolean isFirstLogin() {
public boolean isFirstLogin() { return isFirstLogin != null && isFirstLogin;
return isFirstLogin != null && isFirstLogin; }
}
public void setFirstLogin(boolean isFirstLogin) { public void setFirstLogin(boolean isFirstLogin) {
this.isFirstLogin = isFirstLogin; this.isFirstLogin = isFirstLogin;
} }
public Long getId() { public Long getId() {
return id; return id;
} }
public void setId(Long id) { public void setId(Long id) {
this.id = id; this.id = id;
} }
public String getApiKey() { public String getApiKey() {
return apiKey; return apiKey;
} }
public void setApiKey(String apiKey) { public void setApiKey(String apiKey) {
this.apiKey = apiKey; this.apiKey = apiKey;
} }
public Map<String, String> getSettings() { public Map<String, String> getSettings() {
return settings; return settings;
} }
public void setSettings(Map<String, String> settings) { public void setSettings(Map<String, String> settings) {
this.settings = settings; this.settings = settings;
} }
public String getUsername() { public String getUsername() {
return username; return username;
} }
public void setUsername(String username) { public void setUsername(String username) {
this.username = username; this.username = username;
} }
public String getPassword() { public String getPassword() {
return password; return password;
} }
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; this.password = password;
} }
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
public Set<Authority> getAuthorities() { public Set<Authority> getAuthorities() {
return authorities; return authorities;
} }
public void setAuthorities(Set<Authority> authorities) { public void setAuthorities(Set<Authority> authorities) {
this.authorities = authorities; this.authorities = authorities;
} }
public void addAuthorities(Set<Authority> authorities) {
this.authorities.addAll(authorities);
}
public void addAuthority(Authority authorities) {
this.authorities.add(authorities);
}
public String getRolesAsString() {
return this.authorities.stream()
.map(Authority::getAuthority)
.collect(Collectors.joining(", "));
}
public void addAuthorities(Set<Authority> authorities) {
this.authorities.addAll(authorities);
}
public void addAuthority(Authority authorities) {
this.authorities.add(authorities);
}
public String getRolesAsString() {
return this.authorities.stream()
.map(Authority::getAuthority)
.collect(Collectors.joining(", "));
}
} }

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -12,6 +13,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class GeneralFile { public class GeneralFile {
@Schema(description = "The input file") @Schema(description = "The input file")
private MultipartFile fileInput; private MultipartFile fileInput;
} }

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -11,6 +12,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode @EqualsAndHashCode
public class ImageFile { public class ImageFile {
@Schema(description = "The input image file") @Schema(description = "The input image file")
private MultipartFile fileInput; private MultipartFile fileInput;
} }

View File

@ -3,13 +3,15 @@ package stirling.software.SPDF.model.api;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode @EqualsAndHashCode
public class MultiplePDFFiles { public class MultiplePDFFiles {
@Schema(description = "The input PDF files", type = "array", format = "binary") @Schema(description = "The input PDF files", type = "array", format = "binary")
private MultipartFile[] fileInput; private MultipartFile[] fileInput;
} }

View File

@ -1,16 +1,18 @@
package stirling.software.SPDF.model.api; package stirling.software.SPDF.model.api;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper=true) @EqualsAndHashCode(callSuper = true)
public class PDFComparison extends PDFFile { public class PDFComparison extends PDFFile {
@Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { @Schema(
"Greater", "Equal", "Less" }) description = "The comparison type, accepts Greater, Equal, Less than",
allowableValues = {"Greater", "Equal", "Less"})
private String comparator; private String comparator;
} }

View File

@ -1,15 +1,15 @@
package stirling.software.SPDF.model.api; package stirling.software.SPDF.model.api;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper=true) @EqualsAndHashCode(callSuper = true)
public class PDFComparisonAndCount extends PDFComparison { public class PDFComparisonAndCount extends PDFComparison {
@Schema(description = "Count") @Schema(description = "Count")
private String pageCount; private String pageCount;
} }

View File

@ -3,11 +3,13 @@ package stirling.software.SPDF.model.api;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode @EqualsAndHashCode
public class PDFFile { public class PDFFile {
@Schema(description = "The input PDF file") @Schema(description = "The input PDF file")
private MultipartFile fileInput; private MultipartFile fileInput;
} }

View File

@ -1,14 +1,16 @@
package stirling.software.SPDF.model.api; package stirling.software.SPDF.model.api;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode(callSuper=true) @EqualsAndHashCode(callSuper = true)
public class PDFWithImageFormatRequest extends PDFFile { public class PDFWithImageFormatRequest extends PDFFile {
@Schema(description = "The output image format e.g., 'png', 'jpeg', or 'gif'", @Schema(
allowableValues = { "png", "jpeg", "gif" }) description = "The output image format e.g., 'png', 'jpeg', or 'gif'",
allowableValues = {"png", "jpeg", "gif"})
private String format; private String format;
} }

View File

@ -7,36 +7,38 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper=true) @EqualsAndHashCode(callSuper = true)
public class PDFWithPageNums extends PDFFile { public class PDFWithPageNums extends PDFFile {
@Schema(description = "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"") @Schema(
description =
"The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"")
private String pageNumbers; private String pageNumbers;
@Hidden @Hidden
public List<Integer> getPageNumbersList(){ public List<Integer> getPageNumbersList() {
int pageCount = 0; int pageCount = 0;
try { try {
pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages(); pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages();
} catch (IOException e) { } catch (IOException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
} }
return GeneralUtils.parsePageString(pageNumbers, pageCount); return GeneralUtils.parsePageString(pageNumbers, pageCount);
}
}
@Hidden
@Hidden public List<Integer> getPageNumbersList(PDDocument doc) {
public List<Integer> getPageNumbersList(PDDocument doc){ int pageCount = 0;
int pageCount = 0; pageCount = doc.getNumberOfPages();
pageCount = doc.getNumberOfPages(); return GeneralUtils.parsePageString(pageNumbers, pageCount);
return GeneralUtils.parsePageString(pageNumbers, pageCount); }
}
} }

Some files were not shown because too many files have changed in this diff Show More