Merge branch 'main' into ru_translation_update

This commit is contained in:
Anthony Stirling
2023-10-08 00:08:27 +01:00
committed by GitHub
255 changed files with 13645 additions and 4670 deletions

View File

@@ -1,20 +1,19 @@
package stirling.software.SPDF;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
//@EnableScheduling
public class SPdfApplication {
@@ -48,7 +47,15 @@ public class SPdfApplication {
}
public static void main(String[] args) {
SpringApplication.run(SPdfApplication.class, args);
SpringApplication app = new SpringApplication(SPdfApplication.class);
app.addInitializers(new ConfigInitializer());
if (Files.exists(Paths.get("configs/settings.yml"))) {
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml"));
} else {
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
}
app.run(args);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
@@ -58,7 +65,6 @@ public class SPdfApplication {
GeneralUtils.createDir("customFiles/static/");
GeneralUtils.createDir("customFiles/templates/");
GeneralUtils.createDir("config");

View File

@@ -1,42 +1,55 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(name = "appName")
public String appName() {
String appName = System.getProperty("APP_HOME_NAME");
if (appName == null)
appName = System.getenv("APP_HOME_NAME");
return (appName != null) ? appName : "Stirling PDF";
}
@Bean(name = "appVersion")
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
}
@Bean(name = "homeText")
public String homeText() {
String homeText = System.getProperty("APP_HOME_DESCRIPTION");
if (homeText == null)
homeText = System.getenv("APP_HOME_DESCRIPTION");
return (homeText != null) ? homeText : "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String navBarText = System.getProperty("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getProperty("APP_HOME_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_HOME_NAME");
return (navBarText != null) ? navBarText : "Stirling PDF";
}
package stirling.software.SPDF.config;
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class AppConfig {
@Autowired
ApplicationProperties applicationProperties;
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
return applicationProperties.getSecurity().getEnableLogin();
}
@Bean(name = "appName")
public String appName() {
String homeTitle = applicationProperties.getUi().getAppName();
return (homeTitle != null) ? homeTitle : "Stirling PDF";
}
@Bean(name = "appVersion")
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
}
@Bean(name = "homeText")
public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
}
@Bean(name = "rateLimit")
public boolean rateLimit() {
String appName = System.getProperty("rateLimit");
if (appName == null)
appName = System.getenv("rateLimit");
System.out.println("rateLimit=" + appName);
return (appName != null) ? Boolean.valueOf(appName) : false;
}
}

View File

@@ -2,6 +2,7 @@ package stirling.software.SPDF.config;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
@@ -10,9 +11,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class Beans implements WebMvcConfigurer {
@Autowired
ApplicationProperties applicationProperties;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
@@ -29,10 +35,9 @@ public class Beans implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = System.getProperty("APP_LOCALE");
if (appLocaleEnv == null)
appLocaleEnv = System.getenv("APP_LOCALE");
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {

View File

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

View File

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

View File

@@ -1,20 +1,28 @@
package stirling.software.SPDF.config;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
public EndpointConfiguration() {
private final ApplicationProperties applicationProperties;
@Autowired
public EndpointConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
init();
processEnvironmentConfigs();
}
@@ -198,21 +206,19 @@ public class EndpointConfiguration {
}
private void processEnvironmentConfigs() {
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (endpointsToRemove != null) {
String[] endpoints = endpointsToRemove.split(",");
for (String endpoint : endpoints) {
for (String endpoint : endpointsToRemove) {
disableEndpoint(endpoint.trim());
}
}
if (groupsToRemove != null) {
String[] groups = groupsToRemove.split(",");
for (String group : groups) {
for (String group : groupsToRemove) {
disableGroup(group.trim());
}
}

View File

@@ -1,26 +1,26 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EndpointInterceptor implements HandlerInterceptor {
@Autowired
private EndpointConfiguration endpointConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}
return true;
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EndpointInterceptor implements HandlerInterceptor {
@Autowired
private EndpointConfiguration endpointConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}
return true;
}
}

View File

@@ -1,24 +1,24 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL;
}
return MeterFilterReply.DENY;
}
};
}
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL;
}
return MeterFilterReply.DENY;
}
};
}
}

View File

@@ -1,48 +1,48 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
//System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
//System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,36 +1,27 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
Properties props = new Properties();
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
props.load(input);
version = props.getProperty("version");
} catch (IOException ex) {
ex.printStackTrace();
version = "1.0.0"; // default version if all else fails
}
}
return new OpenAPI().components(new Components()).info(
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."));
}
}
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
version = "1.0.0"; // default version if all else fails
}
return new OpenAPI().components(new Components()).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."));
}
}

View File

@@ -1,20 +1,20 @@
package stirling.software.SPDF.config;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
startTime = LocalDateTime.now();
}
}
package stirling.software.SPDF.config;
import java.time.LocalDateTime;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
startTime = LocalDateTime.now();
}
}

View File

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

View File

@@ -0,0 +1,23 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.Properties;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}

View File

@@ -0,0 +1,26 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked");
}
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -0,0 +1,45 @@
package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getAuthorities())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,53 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.User;
@Component
public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired
@Lazy
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String method = request.getMethod();
String requestURI = request.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = requestURI.startsWith("/css/")
|| requestURI.startsWith("/js/")
|| requestURI.startsWith("/images/")
|| requestURI.startsWith("/public/")
|| requestURI.endsWith(".svg");
// If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) {
filterChain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) {
response.sendRedirect("/change-creds");
return;
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,85 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
@Component
public class InitialSecuritySetup {
@Autowired
private UserService userService;
@Autowired
ApplicationProperties applicationProperties;
@PostConstruct
public void init() {
if (!userService.hasUsers()) {
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
}
@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);
}
}
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
List<String> lines = Files.readAllLines(path);
boolean keyFound = false;
// Search for the existing key to replace it or place to add it
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
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;
}
}
}
// If the section doesn't exist, append it
if (!keyFound) {
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

@@ -0,0 +1,106 @@
package stirling.software.SPDF.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration
@EnableWebSecurity()
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
private UserAuthenticationFilter userAuthenticationFilter;
@Autowired
private FirstLoginFilter firstLoginFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if(loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/")
.failureHandler(new CustomAuthenticationFailureHandler())
.permitAll()
)
.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 -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/"))
.permitAll()
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider());
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
);
}
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl();
}
}

View File

@@ -0,0 +1,113 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response);
return;
}
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
try {
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if(userDetails == null)
{
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
// If API key authentication fails, deny the request
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
}
}
// If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod();
if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) {
response.sendRedirect("/login"); // redirect to the login page
return;
} else {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
return;
}
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI();
String[] permitAllPatterns = {
"/login",
"/register",
"/error",
"/images/",
"/public/",
"/css/",
"/js/"
};
for (String pattern : permitAllPatterns) {
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,125 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.Role;
@Component
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("rateLimit")
public boolean rateLimit;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!rateLimit) {
// If rateLimit is not enabled, just pass all requests without rate limiting
filterChain.doFilter(request, response);
return;
}
String method = request.getMethod();
if (!"POST".equalsIgnoreCase(method)) {
// If the request is not a POST, just pass it through without rate limiting
filterChain.doFilter(request, response);
return;
}
String identifier = null;
// Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
identifier = userDetails.getUsername();
}
}
// If neither API key nor an authenticated user is present, use IP address
if (identifier == null) {
identifier = request.getRemoteAddr();
}
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) {
// It's an API call
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
} else {
// It's a Web UI call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
}
}
private Role getRoleFromAuthentication(Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
try {
return Role.fromString(authority.getAuthority());
} catch (IllegalArgumentException ex) {
// Ignore and continue to next authority.
}
}
}
throw new IllegalStateException("User does not have a valid role.");
}
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
filterChain.doFilter(request, response);
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
response.getWriter().write("Rate limit exceeded for POST requests.");
}
}
private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build();
}
}

View File

@@ -0,0 +1,191 @@
package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey);
if (user == null) {
throw new UsernameNotFoundException("API key is not valid");
}
// Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
private String generateApiKey() {
String apiKey;
do {
apiKey = UUID.randomUUID().toString();
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
return apiKey;
}
public User addApiKeyToUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey());
return userRepository.save(user);
}
public User refreshApiKeyForUser(String username) {
return addApiKeyToUser(username); // reuse the add API key method for refreshing
}
public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey();
}
public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null;
}
public User getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey);
}
public UserDetails loadUserByApiKey(String apiKey) {
User userOptional = userRepository.findByApiKey(apiKey);
if (userOptional != null) {
User user = userOptional;
// Convert your User entity to a UserDetails object with authorities
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user)
);
}
return null; // or throw an exception
}
public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
}
public void saveUser(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
userRepository.save(user);
}
public void saveUser(String username, String password, String role, boolean firstLogin) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
user.setFirstLogin(firstLogin);
userRepository.save(user);
}
public void saveUser(String username, String password, String role) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
user.setFirstLogin(false);
userRepository.save(user);
}
public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
userRepository.delete(userOpt.get());
}
}
public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent();
}
public boolean hasUsers() {
return userRepository.count() > 0;
}
public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) {
settingsMap = new HashMap<String,String>();
}
settingsMap.clear();
settingsMap.putAll(updates);
user.setSettings(settingsMap);
userRepository.save(user);
}
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public void changeUsername(User user, String newUsername) {
user.setUsername(newUsername);
userRepository.save(user);
}
public void changePassword(User user, String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse);
userRepository.save(user);
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}
}

View File

@@ -1,132 +1,86 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
byte[] bytes = file.getBytes();
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
PdfCanvas pdfCanvas = new PdfCanvas(page);
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form)
throws IOException {
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.rectangle(x, y, width, height);
pdfCanvas.clip();
pdfCanvas.addXObject(formXObject, -x, -y);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
PDDocument newDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages();
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
// Create a new page with the size of the source page
PDPage newPage = new PDPage(sourcePage.getMediaBox());
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
// Import the source page as a form XObject
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.saveGraphicsState();
// Define the crop area
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
contentStream.clip();
// Draw the entire formXObject
contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState();
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(new PDRectangle(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,80 +1,115 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
// Create a new empty document
PDDocument mergedDoc = new PDDocument();
// Iterate over the list of documents and add their pages to the merged document
for (PDDocument doc : documents) {
// Get all pages from the current document
PDPageTree pages = doc.getPages();
// Iterate over the pages and add them to the merged document
for (PDPage page : pages) {
mergedDoc.addPage(page);
}
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = new PDDocument();
for (PDDocument doc : documents) {
for (PDPage page : doc.getPages()) {
mergedDoc.addPage(page);
}
}
return mergedDoc;
}
// Return the merged document
return mergedDoc;
private Comparator<MultipartFile> getSortComparator(String sortType) {
switch (sortType) {
case "byFileName":
return Comparator.comparing(MultipartFile::getOriginalFilename);
case "byDateModified":
return (file1, file2) -> {
try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
} catch (IOException e) {
return 0; // If there's an error, treat them as equal
}
};
case "byDateCreated":
return (file1, file2) -> {
try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
return attr1.creationTime().compareTo(attr2.creationTime());
} catch (IOException e) {
return 0; // If there's an error, treat them as equal
}
};
case "byPDFTitle":
return (file1, file2) -> {
try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2);
} catch (IOException e) {
return 0;
}
};
case "orderProvided":
default:
return (file1, file2) -> 0; // Default is the order provided
}
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(summary = "Merge multiple PDF files into one",
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 {
MultipartFile[] files = form.getFileInput();
Arrays.sort(files, getSortComparator(form.getSortType()));
List<PDDocument> documents = new ArrayList<>();
for (MultipartFile file : files) {
try (InputStream is = file.getInputStream()) {
documents.add(PDDocument.load(is));
}
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(
summary = "Merge multiple PDF files into one",
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF files to be merged into a single file", required = true)
MultipartFile[] files) throws IOException {
// Read the input PDF files into PDDocument objects
List<PDDocument> documents = new ArrayList<>();
// Loop through the files array and read each file into a PDDocument
for (MultipartFile file : files) {
documents.add(PDDocument.load(file.getInputStream()));
}
PDDocument mergedDoc = mergeDocuments(documents);
// Return the merged PDF as a response
try (PDDocument mergedDoc = mergeDocuments(documents)) {
ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
for (PDDocument doc : documents) {
// Close the document after processing
doc.close();
}
return response;
} finally {
for (PDDocument doc : documents) {
if (doc != null) {
doc.close();
}
}
}
}
}

View File

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

@@ -9,7 +9,9 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
@@ -17,12 +19,14 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController {
@@ -30,11 +34,12 @@ public class RearrangePagesPDFController {
@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")
public ResponseEntity<byte[]> deletePages(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile,
@RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete)
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request )
throws IOException {
MultipartFile pdfFile = request.getFileInput();
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
@@ -51,9 +56,7 @@ public class RearrangePagesPDFController {
}
private enum CustomMode {
REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST,
}
private List<Integer> removeFirst(int totalPages) {
if (totalPages <= 1)
@@ -114,6 +117,18 @@ public class RearrangePagesPDFController {
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) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) {
@@ -125,9 +140,9 @@ public class RearrangePagesPDFController {
return newPageOrder;
}
private List<Integer> processCustomMode(String customMode, int totalPages) {
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
try {
CustomMode mode = CustomMode.valueOf(customMode.toUpperCase());
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
switch (mode) {
case REVERSE_ORDER:
return reverseOrder(totalPages);
@@ -135,6 +150,8 @@ public class RearrangePagesPDFController {
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:
@@ -154,16 +171,10 @@ public class RearrangePagesPDFController {
@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(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to rearrange pages") MultipartFile pdfFile,
@RequestParam(required = false, value = "pageOrder") @Parameter(description = "The new page order as a comma-separated list of page numbers, page ranges (e.g., '1,3,5-7'), 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')") String pageOrder,
@RequestParam(required = false, value = "customMode") @Parameter(schema = @Schema(implementation = CustomMode.class, description = "The custom mode for page rearrangement. "
+ "Valid values are:\n" + "REVERSE_ORDER: Reverses the order of all pages.\n"
+ "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). "
+ "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n"
+ "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n"
+ "REMOVE_FIRST: Removes the first page.\n" + "REMOVE_LAST: Removes the last page.\n"
+ "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")) String customMode) {
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());
@@ -171,15 +182,14 @@ public class RearrangePagesPDFController {
// 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();
System.out.println("pageOrder=" + pageOrder);
System.out.println("customMode length =" + customMode.length());
List<Integer> newPageOrder;
if (customMode != null && customMode.length() > 0) {
newPageOrder = processCustomMode(customMode, totalPages);
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++) {

View File

@@ -8,18 +8,19 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class RotationController {
@@ -31,13 +32,9 @@ public class RotationController {
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The PDF file to be rotated", required = true)
MultipartFile pdfFile,
@RequestParam("angle")
@Parameter(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90", required = true)
Integer angle) throws IOException {
@ModelAttribute RotatePDFRequest request) throws IOException {
MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle();
// Load the PDF document
PDDocument document = PDDocument.load(pdfFile.getBytes());

View File

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

View File

@@ -17,19 +17,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.multipdf.Splitter;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class SplitPDFController {
@@ -38,57 +38,36 @@ public class SplitPDFController {
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
@Operation(summary = "Split a PDF file into separate documents",
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")
public ResponseEntity<byte[]> splitPdf(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be split")
MultipartFile file,
@RequestParam("pages")
@Parameter(description = "The pages to be included in separate documents. Specify individual page numbers (e.g., '1,3,5'), ranges (e.g., '1-3,5-7'), or 'all' for every page.")
String pages) throws IOException {
// parse user input
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException {
MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers();
// open the pdf document
InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream);
List<Integer> pageNumbers = new ArrayList<>();
pages = pages.replaceAll("\\s+", ""); // remove whitespaces
if (pages.toLowerCase().equals("all")) {
for (int i = 0; i < document.getNumberOfPages(); i++) {
pageNumbers.add(i);
}
} else {
String[] splitPoints = pages.split(",");
for (String splitPoint : splitPoints) {
List<Integer> orderedPages = GeneralUtils.parsePageList(new String[] {splitPoint}, document.getNumberOfPages());
pageNumbers.addAll(orderedPages);
}
// Add the last page as a split point
pageNumbers.add(document.getNumberOfPages() - 1);
}
List<Integer> pageNumbers = request.getPageNumbersList(document);
if(!pageNumbers.contains(document.getNumberOfPages() - 1))
pageNumbers.add(document.getNumberOfPages()- 1);
logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document
Splitter splitter = new Splitter();
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
int previousPageNumber = 0;
int previousPageNumber = 1; // PDFBox uses 1-based indexing for pages.
for (int splitPoint : pageNumbers) {
try (PDDocument splitDocument = new PDDocument()) {
for (int i = previousPageNumber; i <= splitPoint; i++) {
PDPage page = document.getPage(i);
splitDocument.addPage(page);
logger.debug("Adding page {} to split document", i);
}
previousPageNumber = splitPoint + 1;
splitPoint = splitPoint + 1;
splitter.setStartPage(previousPageNumber);
splitter.setEndPage(splitPoint);
List<PDDocument> splitDocuments = splitter.split(document);
for (PDDocument splitDoc : splitDocuments) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
splitDocument.save(baos);
splitDoc.save(baos);
splitDocumentsBoas.add(baos);
} catch (Exception e) {
logger.error("Failed splitting documents and saving them", e);
throw e;
splitDoc.close();
}
previousPageNumber = splitPoint + 1;
}

View File

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

View File

@@ -0,0 +1,234 @@
package stirling.software.SPDF.controller.api;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
@Controller
@RequestMapping("/api/v1/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) {
if(userService.usernameExists(username)) {
model.addAttribute("error", "Username already exists");
return "register";
}
userService.saveUser(username, password);
return "redirect:/login?registered=true";
}
@PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newUsername,
@RequestParam String newPassword,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/change-creds?messageType=usernameExists");
}
userService.changePassword(user, newPassword);
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) {
userService.changeUsername(user, newUsername);
}
userService.changeFirstUse(user, false);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView("/login?messageType=credsUpdated");
}
@PostMapping("/change-username")
public RedirectView changeUsername(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newUsername,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/account?messageType=usernameExists");
}
if(newUsername != null && newUsername.length() > 0) {
userService.changeUsername(user, newUsername);
}
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView("/login?messageType=credsUpdated");
}
@PostMapping("/change-password")
public RedirectView changePassword(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newPassword,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword");
}
userService.changePassword(user, newPassword);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView("/login?messageType=credsUpdated");
}
@PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) {
Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]);
}
System.out.println("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates);
return "redirect:/account"; // Redirect to a page of your choice after updating
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser")
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) {
if(userService.usernameExists(username)) {
return new RedirectView("/addUsers?messageType=usernameExists");
}
userService.saveUser(username, password, role, forceChange);
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}")
public String deleteUser(@PathVariable String username, Authentication authentication) {
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equals(username)) {
throw new IllegalArgumentException("Cannot delete currently logined in user.");
}
userService.deleteUser(username);
return "redirect:/addUsers";
}
@PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
String username = principal.getName();
String apiKey = userService.getApiKeyForUser(username);
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
}
return ResponseEntity.ok(apiKey);
}
@PostMapping("/update-api-key")
public ResponseEntity<String> updateApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
String username = principal.getName();
User user = userService.refreshApiKeyForUser(username);
String apiKey = user.getApiKey();
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
}
return ResponseEntity.ok(apiKey);
}
}

View File

@@ -1,36 +1,5 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.ResponseEntity;
import io.swagger.v3.oas.annotations.Operation;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
@@ -41,7 +10,29 @@ import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertEpubToPdf {
//TODO
@@ -52,9 +43,9 @@ public class ConvertEpubToPdf {
description = "This endpoint takes an EPUB file input and converts it to a single PDF."
)
public ResponseEntity<byte[]> epubToSinglePdf(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput)
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide an EPUB file for conversion.");
}
@@ -83,7 +74,7 @@ public class ConvertEpubToPdf {
// Assuming a pseudo-code function that merges multiple PDFs into one.
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
// You can use a library such as iText or PDFBox to perform the merging here.
// You can use a library such as PDFBox to perform the merging here.
// Return the byte[] of the merged PDF.
return null;
}

View File

@@ -1,42 +1,33 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/html-to-pdf")
@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(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput) throws IOException, InterruptedException {
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion.");

View File

@@ -11,44 +11,35 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
@Operation(summary = "Convert PDF to image(s)",
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be converted")
MultipartFile file,
@RequestParam("imageFormat")
@Parameter(description = "The output image format", schema = @Schema(allowableValues = {"png", "jpeg", "jpg", "gif"}))
String imageFormat,
@RequestParam("singleOrMultiple")
@Parameter(description = "Choose between a single image containing all pages or separate images for each page", schema = @Schema(allowableValues = {"single", "multiple"}))
String singleOrMultiple,
@RequestParam("colorType")
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"color", "greyscale", "blackwhite"}))
String colorType,
@RequestParam("dpi")
@Parameter(description = "The DPI (dots per inch) for the output image(s)")
String dpi) throws IOException {
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String imageFormat = request.getImageFormat();
String singleOrMultiple = request.getSingleOrMultiple();
String colorType = request.getColorType();
String dpi = request.getDpi();
byte[] pdfBytes = file.getBytes();
ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) {
@@ -83,24 +74,17 @@ public class ConvertImgPDFController {
}
}
@PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
@Operation(summary = "Convert images to a PDF file",
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input images to be converted to a PDF file")
MultipartFile[] file,
@RequestParam(defaultValue = "false", name = "stretchToFit")
@Parameter(description = "Whether to stretch the images to fit the PDF page or maintain the aspect ratio", example = "false")
boolean stretchToFit,
@RequestParam("colorType")
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"color", "greyscale", "blackwhite"}))
String colorType,
@RequestParam(defaultValue = "false", name = "autoRotate")
@Parameter(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true")
boolean autoRotate) throws IOException {
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException {
MultipartFile[] file = request.getFileInput();
String fitOption = request.getFitOption();
String colorType = request.getColorType();
boolean autoRotate = request.isAutoRotate();
// Convert the file to PDF and get the resulting bytes
byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate, colorType);
byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType);
return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
}

View File

@@ -1,33 +1,35 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf {
@PostMapping(consumes = "multipart/form-data", value = "/markdown-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation(
summary = "Convert a Markdown file to PDF",
description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format."
)
public ResponseEntity<byte[]> markdownToPdf(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput)
throws IOException, InterruptedException {
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");

View File

@@ -10,20 +10,22 @@ import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
@@ -58,19 +60,14 @@ public class ConvertOfficeController {
return fileExtension.matches(extensionPattern);
}
@PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
@Operation(
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"
)
public ResponseEntity<byte[]> processFileToPDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(
description = "The input file to be converted to a PDF file using LibreOffice",
required = true
)
MultipartFile inputFile
) throws IOException, InterruptedException {
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
// unused but can start server instance if startup time is to long
// LibreOfficeListener.getInstance().start();

View File

@@ -3,69 +3,67 @@ package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
import stirling.software.SPDF.utils.PDFToFile;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-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")
public ResponseEntity<byte[]> processPdfToHTML(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
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-to-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")
public ResponseEntity<byte[]> processPdfToPresentation(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = {
"ppt", "pptx", "odp" })) String outputFormat)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToPresentation(@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-to-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")
public ResponseEntity<byte[]> processPdfToRTForTXT(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = {
"rtf", "txt:Text" })) String outputFormat)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) 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-to-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")
public ResponseEntity<byte[]> processPdfToWord(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = {
"doc", "docx", "odt" })) String outputFormat)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request) 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-to-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")
public ResponseEntity<byte[]> processPdfToXML(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");

View File

@@ -1,37 +1,37 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
@Operation(
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"
)
public ResponseEntity<byte[]> pdfToPdfA(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true)
MultipartFile inputFile) throws IOException, InterruptedException {
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");

View File

@@ -7,15 +7,14 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -23,17 +22,16 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/url-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation(
summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format."
)
public ResponseEntity<byte[]> urlToPdf(
@RequestParam(required = true, value = "urlInput")
@Parameter(description = "The input URL to be converted to a PDF file", required = true)
String URL) throws IOException, InterruptedException {
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException {
String URL = request.getUrlInput();
// Validate the URL format
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {

View File

@@ -1,39 +1,40 @@
package stirling.software.SPDF.controller.api.filters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController
@RequestMapping("/api/v1/filter")
@Tag(name = "Filter", description = "Filter APIs")
public class FilterController {
@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")
public ResponseEntity<byte[]> containsText(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
@Parameter(description = "The text to check for", required = true) String text,
@Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String text = request.getText();
String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
@@ -43,10 +44,11 @@ public class FilterController {
// TODO
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> containsImage(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
@Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
throws IOException, InterruptedException {
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());
@@ -55,12 +57,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageCount(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Page Count", required = true) String pageCount,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
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();
@@ -88,12 +88,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageSize(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Standard Page Size", required = true) String standardPageSize,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String standardPageSize = request.getStandardPageSize();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());
@@ -131,12 +129,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> fileSize(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "File Size", required = true) String fileSize,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String fileSize = request.getFileSize();
String comparator = request.getComparator();
// Get the file size
long actualFileSize = inputFile.getSize();
@@ -164,12 +160,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageRotation(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Rotation in degrees", required = true) int rotation,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
int rotation = request.getRotation();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());

View File

@@ -1,77 +1,29 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.text.TextPosition;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.util.ArrayList;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.text.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.ResponseEntity;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoRenameController {
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
@@ -81,10 +33,9 @@ public class AutoRenameController {
@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")
public ResponseEntity<byte[]> extractHeader(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the header is to be extracted.", required = true) MultipartFile file,
@RequestParam(required = false, defaultValue = "false") @Parameter(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false) Boolean useFirstTextAsFallback)
throws Exception {
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception {
MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = PDDocument.load(file.getInputStream());
PDFTextStripper reader = new PDFTextStripper() {

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
@@ -16,8 +16,9 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@@ -29,21 +30,23 @@ import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoSplitPdfController {
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
@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 Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(
@RequestParam("fileInput") @Parameter(description = "The input PDF file which needs to be split into separate documents based on QR code boundaries.", required = true) MultipartFile file,
@RequestParam(value ="duplexMode",defaultValue = "false") @Parameter(description = "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", required = false) boolean duplexMode)
throws IOException {
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException {
MultipartFile file = request.getFileInput();
boolean duplexMode = request.isDuplexMode();
InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream);

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.IOException;
@@ -20,22 +20,23 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@@ -43,16 +44,10 @@ public class BlankPageController {
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"
)
public ResponseEntity<byte[]> removeBlankPages(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file from which blank pages will be removed", required = true)
MultipartFile inputFile,
@RequestParam(defaultValue = "10", name = "threshold")
@Parameter(description = "The threshold value to determine blank pages", example = "10")
int threshold,
@RequestParam(defaultValue = "99.9", name = "whitePercent")
@Parameter(description = "The percentage of white color on a page to consider it as blank", example = "99.9")
float whitePercent) throws IOException, InterruptedException {
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent();
PDDocument document = null;
try {

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.Image;
import java.awt.image.BufferedImage;
@@ -22,35 +22,34 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@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")
public ResponseEntity<byte[]> optimizePdf(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
@RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = {
"1", "2", "3", "4", "5" })) Integer optimizeLevel,
@RequestParam(value = "expectedOutputSize", required = false) @Parameter(description = "The expected output size, e.g. '100MB', '25KB', etc.", required = false) String expectedOutputSizeString)
throws Exception {
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception {
MultipartFile inputFile = request.getFileInput();
Integer optimizeLevel = request.getOptimizeLevel();
String expectedOutputSizeString = request.getExpectedOutputSize();
if(expectedOutputSizeString == null && optimizeLevel == null) {
throw new Exception("Both expected output size and optimize level are not specified");

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@@ -24,20 +24,21 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@@ -46,26 +47,16 @@ public class ExtractImageScansController {
@Operation(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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input file containing image scans")
MultipartFile inputFile,
@RequestParam(name = "angle_threshold", defaultValue = "5")
@Parameter(description = "The angle threshold for the image scan extraction", example = "5")
int angleThreshold,
@RequestParam(name = "tolerance", defaultValue = "20")
@Parameter(description = "The tolerance for the image scan extraction", example = "20")
int tolerance,
@RequestParam(name = "min_area", defaultValue = "8000")
@Parameter(description = "The minimum area for the image scan extraction", example = "8000")
int minArea,
@RequestParam(name = "min_contour_area", defaultValue = "500")
@Parameter(description = "The minimum contour area for the image scan extraction", example = "500")
int minContourArea,
@RequestParam(name = "border_size", defaultValue = "1")
@Parameter(description = "The border size for the image scan extraction", example = "1")
int borderSize) throws IOException, InterruptedException {
String fileName = inputFile.getOriginalFilename();
@RequestBody(
description = "Form data containing file and extraction parameters",
required = true,
content = @Content(
mediaType = "multipart/form-data",
schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure
)
)
ExtractImageScansRequest form) throws IOException, InterruptedException {
String fileName = form.getFileInput().getOriginalFilename();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
List<String> images = new ArrayList<>();
@@ -73,7 +64,7 @@ public class ExtractImageScansController {
// Check if input file is a PDF
if (extension.equalsIgnoreCase("pdf")) {
// Load PDF document
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputFile.getBytes()))) {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages();
images = new ArrayList<>();
@@ -93,7 +84,7 @@ public class ExtractImageScansController {
}
} else {
Path tempInputFile = Files.createTempFile("input_", "." + extension);
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
// Add input file path to images list
images.add(tempInputFile.toString());
}
@@ -109,11 +100,11 @@ public class ExtractImageScansController {
"./scripts/split_photos.py",
images.get(i),
tempDir.toString(),
"--angle_threshold", String.valueOf(angleThreshold),
"--tolerance", String.valueOf(tolerance),
"--min_area", String.valueOf(minArea),
"--min_contour_area", String.valueOf(minContourArea),
"--border_size", String.valueOf(borderSize)
"--angle_threshold", String.valueOf(form.getAngleThreshold()),
"--tolerance", String.valueOf(form.getTolerance()),
"--min_area", String.valueOf(form.getMinArea()),
"--min_contour_area", String.valueOf(form.getMinContourArea()),
"--border_size", String.valueOf(form.getBorderSize())
));

View File

@@ -1,5 +1,8 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
@@ -20,19 +23,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@@ -40,13 +43,9 @@ public class ExtractImagesController {
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation(summary = "Extract images from a PDF file",
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file containing images")
MultipartFile file,
@RequestParam("format")
@Parameter(description = "The output image format e.g., 'png', 'jpeg', or 'gif'", schema = @Schema(allowableValues = {"png", "jpeg", "gif"}))
String format) throws IOException {
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String format = request.getFormat();
System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
PDDocument document = PDDocument.load(file.getBytes());
@@ -62,7 +61,8 @@ public class ExtractImagesController {
int imageIndex = 1;
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
int pageNum = 1;
int pageNum = 0;
Set<Integer> processedImages = new HashSet<>();
// Iterate over each page
for (PDPage page : document.getPages()) {
++pageNum;
@@ -70,7 +70,12 @@ public class ExtractImagesController {
for (COSName name : page.getResources().getXObjectNames()) {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
int imageHash = image.hashCode();
if(processedImages.contains(imageHash)) {
continue; // Skip already processed images
}
processedImages.add(imageHash);
// Convert image to desired format
RenderedImage renderedImage = image.getImage();
BufferedImage bufferedImage = null;

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.Color;
import java.awt.geom.AffineTransform;
@@ -9,6 +9,7 @@ import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream;
//Required for file input/output
import java.io.File;
import java.io.IOException;
@@ -29,21 +30,21 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.source.ByteArrayOutputStream;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
@@ -55,10 +56,8 @@ public class FakeScanControllerWIP {
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."
)
public ResponseEntity<byte[]> repairPdf(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be repaired", required = true)
MultipartFile inputFile) throws IOException, InterruptedException {
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput();
PDDocument document = PDDocument.load(inputFile.getBytes());
PDFRenderer pdfRenderer = new PDFRenderer(document);

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.text.ParseException;
@@ -11,19 +11,20 @@ import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class MetadataController {
@@ -41,44 +42,28 @@ public class MetadataController {
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(summary = "Update metadata of a PDF file",
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to update metadata")
MultipartFile pdfFile,
@RequestParam(value = "deleteAll", required = false, defaultValue = "false")
@Parameter(description = "Delete all metadata if set to true")
Boolean deleteAll,
@RequestParam(value = "author", required = false)
@Parameter(description = "The author of the document")
String author,
@RequestParam(value = "creationDate", required = false)
@Parameter(description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)")
String creationDate,
@RequestParam(value = "creator", required = false)
@Parameter(description = "The creator of the document")
String creator,
@RequestParam(value = "keywords", required = false)
@Parameter(description = "The keywords for the document")
String keywords,
@RequestParam(value = "modificationDate", required = false)
@Parameter(description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)")
String modificationDate,
@RequestParam(value = "producer", required = false)
@Parameter(description = "The producer of the document")
String producer,
@RequestParam(value = "subject", required = false)
@Parameter(description = "The subject of the document")
String subject,
@RequestParam(value = "title", required = false)
@Parameter(description = "The title of the document")
String title,
@RequestParam(value = "trapped", required = false)
@Parameter(description = "The trapped status of the document")
String trapped,
@Parameter(description = "Map list of key and value of custom parameters, note these must start with customKey and customValue if they are non standard")
@RequestParam Map<String, String> allRequestParams)
throws IOException {
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request) throws IOException {
// Extract PDF file from the request object
MultipartFile pdfFile = request.getFileInput();
// Extract metadata information
Boolean deleteAll = request.isDeleteAll();
String author = request.getAuthor();
String creationDate = request.getCreationDate();
String creator = request.getCreator();
String keywords = request.getKeywords();
String modificationDate = request.getModificationDate();
String producer = request.getProducer();
String subject = request.getSubject();
String title = request.getTitle();
String trapped = request.getTrapped();
// Extract additional custom parameters
Map<String, String> allRequestParams = request.getAllRequestParams();
if(allRequestParams == null) {
allRequestParams = new java.util.HashMap<String, String>();
}
// Load the PDF file into a PDDocument
PDDocument document = PDDocument.load(pdfFile.getBytes());

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.File;
import java.io.FileOutputStream;
@@ -18,22 +18,22 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class OCRController {
private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
@@ -51,35 +51,16 @@ public class OCRController {
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(summary = "Process a PDF file with OCR",
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be processed with OCR")
MultipartFile inputFile,
@RequestParam("languages")
@Parameter(description = "List of languages to use in OCR processing")
List<String> selectedLanguages,
@RequestParam(name = "sidecar", required = false)
@Parameter(description = "Include OCR text in a sidecar text file if set to true")
Boolean sidecar,
@RequestParam(name = "deskew", required = false)
@Parameter(description = "Deskew the input file if set to true")
Boolean deskew,
@RequestParam(name = "clean", required = false)
@Parameter(description = "Clean the input file if set to true")
Boolean clean,
@RequestParam(name = "clean-final", required = false)
@Parameter(description = "Clean the final output if set to true")
Boolean cleanFinal,
@RequestParam(name = "ocrType", required = false)
@Parameter(description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", schema = @Schema(allowableValues = {"skip-text", "force-ocr", "Normal"}))
String ocrType,
@RequestParam(name = "ocrRenderType", required = false, defaultValue = "hocr")
@Parameter(description = "Specify the OCR render type, either 'hocr' or 'sandwich'", schema = @Schema(allowableValues = {"hocr", "sandwich"}))
String ocrRenderType,
@RequestParam(name = "removeImagesAfter", required = false)
@Parameter(description = "Remove images from the output PDF if set to true")
Boolean removeImagesAfter) throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
List<String> selectedLanguages = request.getLanguages();
Boolean sidecar = request.isSidecar();
Boolean deskew = request.isDeskew();
Boolean clean = request.isClean();
Boolean cleanFinal = request.isCleanFinal();
String ocrType = request.getOcrType();
String ocrRenderType = request.getOcrRenderType();
Boolean removeImagesAfter = request.isRemoveImagesAfter();
// --output-type pdfa
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language.");

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
@@ -6,20 +6,21 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class OverlayImageController {
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
@@ -29,22 +30,12 @@ public class OverlayImageController {
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"
)
public ResponseEntity<byte[]> overlayImage(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to overlay the image onto.", required = true)
MultipartFile pdfFile,
@RequestParam("fileInput2")
@Parameter(description = "The image file to be overlaid onto the PDF.", required = true)
MultipartFile imageFile,
@RequestParam("x")
@Parameter(description = "The x-coordinate at which to place the top-left corner of the image.", example = "0")
float x,
@RequestParam("y")
@Parameter(description = "The y-coordinate at which to place the top-left corner of the image.", example = "0")
float y,
@RequestParam("everyPage")
@Parameter(description = "Whether to overlay the image onto every page of the PDF.", example = "false")
boolean everyPage) {
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile();
float x = request.getX();
float y = request.getY();
boolean everyPage = request.isEveryPage();
try {
byte[] pdfBytes = pdfFile.getBytes();
byte[] imageBytes = imageFile.getBytes();

View File

@@ -0,0 +1,135 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.misc.AddPageNumbersRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@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")
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String customMargin = request.getCustomMargin();
int position = request.getPosition();
int startingNumber = request.getStartingNumber();
String pagesToNumber = request.getPagesToNumber();
String customText = request.getCustomText();
int pageNumber = startingNumber;
byte[] fileBytes = file.getBytes();
PDDocument document = PDDocument.load(fileBytes);
float marginFactor;
switch (customMargin.toLowerCase()) {
case "small":
marginFactor = 0.02f;
break;
case "medium":
marginFactor = 0.035f;
break;
case "large":
marginFactor = 0.05f;
break;
case "x-large":
marginFactor = 0.075f;
break;
default:
marginFactor = 0.035f;
break;
}
float fontSize = 12.0f;
PDType1Font font = PDType1Font.HELVETICA;
if(pagesToNumber == null || pagesToNumber.length() == 0) {
pagesToNumber = "all";
}
if(customText == null || customText.length() == 0) {
customText = "{n}";
}
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
for (int i : pagesToNumberList) {
PDPage page = document.getPage(i);
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);
float x, y;
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3;
switch (xGroup) {
case 0: // left
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
break;
case 1: // center
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
break;
default: // right
x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
break;
}
switch (yGroup) {
case 0: // bottom
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
break;
default: // top
y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
break;
}
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();
contentStream.close();
pageNumber++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
}
}

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.nio.file.Files;
@@ -9,20 +9,22 @@ import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class RepairController {
private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
@@ -32,11 +34,8 @@ public class RepairController {
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"
)
public ResponseEntity<byte[]> repairPdf(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be repaired", required = true)
MultipartFile inputFile) throws IOException, InterruptedException {
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile());

View File

@@ -0,0 +1,61 @@
package stirling.software.SPDF.controller.api.misc;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
MultipartFile inputFile = request.getFileInput();
String script = "";
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()) {
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
}
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js");
}
}
}

View File

@@ -1,174 +0,0 @@
package stirling.software.SPDF.controller.api.other;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@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")
public ResponseEntity<byte[]> addPageNumbers(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin,
@Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position,
@Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber,
@Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
@Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
throws IOException {
byte[] fileBytes = file.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
int pageNumber = startingNumber;
float marginFactor;
switch (customMargin.toLowerCase()) {
case "small":
marginFactor = 0.02f;
break;
case "medium":
marginFactor = 0.035f;
break;
case "large":
marginFactor = 0.05f;
break;
case "x-large":
marginFactor = 0.1f;
break;
default:
marginFactor = 0.035f;
break;
}
float fontSize = 12.0f;
PdfReader reader = new PdfReader(bais);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(reader, writer);
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
for (int i : pagesToNumberList) {
PdfPage page = pdfDoc.getPage(i+1);
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
float textWidth = font.getWidth(text, fontSize);
float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
float x, y;
TextAlignment alignment;
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3;
switch (xGroup) {
case 0: // left
x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
alignment = TextAlignment.LEFT;
break;
case 1: // center
x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
alignment = TextAlignment.CENTER;
break;
default: // right
x = pageSize.getRight() - marginFactor * pageSize.getWidth();
alignment = TextAlignment.RIGHT;
break;
}
switch (yGroup) {
case 0: // bottom
y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
break;
default: // top
y = pageSize.getTop() - marginFactor * pageSize.getHeight();
break;
}
new Canvas(pdfCanvas, page.getPageSize())
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
pageNumber++;
}
pdfDoc.close();
byte[] resultBytes = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
}
}

View File

@@ -1,141 +0,0 @@
package stirling.software.SPDF.controller.api.other;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.text.TextPosition;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ArrayList;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.text.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.ResponseEntity;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile)
throws Exception {
try (
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
) {
String name = "";
String script = "";
String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: ";
//Javascript
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
if (namesDict != null) {
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
if (javascriptDict != null) {
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
for (int i = 0; i < namesArray.size(); i += 2) {
if(namesArray.getAsString(i) != null)
name = namesArray.getAsString(i).toString();
PdfObject jsCode = namesArray.get(i+1);
if (jsCode instanceof PdfStream) {
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
script = "//" + entryName + name + "\n" +jsCodeStr;
} else if (jsCode instanceof PdfDictionary) {
// If the JS code is in a dictionary, you'll need to know the key to use.
// Assuming the key is PdfName.JS:
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
if (jsCodeStream != null) {
byte[] jsCodeBytes = jsCodeStream.getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
script = "//" + entryName + name + "\n" +jsCodeStr;
}
}
}
}
}
if(script.equals("")) {
script = "PDF '" +inputFile.getOriginalFilename() + "' does not contain Javascript";
}
return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js");
}
}
}

View File

@@ -3,8 +3,10 @@ package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -20,8 +22,7 @@ import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,9 +37,9 @@ import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
@@ -47,11 +48,14 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.api.HandleDataRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/pipeline")
@Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController {
@@ -91,6 +95,10 @@ public class PipelineController {
}
}
@Autowired
ApplicationProperties applicationProperties;
private void handleDirectory(Path dir) throws Exception {
logger.info("Handling directory: {}", dir);
Path jsonFile = dir.resolve(jsonFileName);
@@ -182,8 +190,7 @@ public class PipelineController {
// {filename} {folder} {date} {tmime} {pipeline}
String outputDir = config.getOutputDir();
// Check if the environment variable 'automatedOutputFolder' is set
String outputFolder = System.getenv("automatedOutputFolder");
String outputFolder = applicationProperties.getAutoPipeline().getOutputFolder();
if (outputFolder == null || outputFolder.isEmpty()) {
// If the environment variable is not set, use the default value
@@ -413,8 +420,9 @@ public class PipelineController {
}
@PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) {
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) {
MultipartFile[] files = request.getFileInputs();
String jsonString = request.getJsonString();
logger.info("Received POST request to /handleData with {} files", files.length);
try {
List<Resource> outputFiles = handleFiles(files, jsonString);

View File

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

View File

@@ -8,18 +8,19 @@ import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class PasswordController {
@@ -31,13 +32,12 @@ public class PasswordController {
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"
)
public ResponseEntity<byte[]> removePassword(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file from which the password should be removed", required = true)
MultipartFile fileInput,
@RequestParam(name = "password")
@Parameter(description = "The password of the PDF file", required = true)
String password) throws IOException {
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException {
MultipartFile fileInput = request.getFileInput();
String password = request.getPassword();
PDDocument document = PDDocument.load(fileInput.getBytes(), password);
document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf");
@@ -48,44 +48,19 @@ public class PasswordController {
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"
)
public ResponseEntity<byte[]> addPassword(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to which the password should be added", required = true)
MultipartFile fileInput,
@RequestParam(value = "", name = "ownerPassword")
@Parameter(description = "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)")
String ownerPassword,
@RequestParam( name = "password", required = false)
@Parameter(description = "The password to be added to the PDF file (Restricts the opening of the document itself.)")
String password,
@RequestParam( name = "keyLength", required = false)
@Parameter(description = "The length of the encryption key", schema = @Schema(allowableValues = {"40", "128", "256"}))
int keyLength,
@RequestParam( name = "canAssembleDocument", required = false)
@Parameter(description = "Whether the document assembly is allowed", example = "false")
boolean canAssembleDocument,
@RequestParam( name = "canExtractContent", required = false)
@Parameter(description = "Whether content extraction for accessibility is allowed", example = "false")
boolean canExtractContent,
@RequestParam( name = "canExtractForAccessibility", required = false)
@Parameter(description = "Whether content extraction for accessibility is allowed", example = "false")
boolean canExtractForAccessibility,
@RequestParam( name = "canFillInForm", required = false)
@Parameter(description = "Whether form filling is allowed", example = "false")
boolean canFillInForm,
@RequestParam( name = "canModify", required = false)
@Parameter(description = "Whether the document modification is allowed", example = "false")
boolean canModify,
@RequestParam( name = "canModifyAnnotations", required = false)
@Parameter(description = "Whether modification of annotations is allowed", example = "false")
boolean canModifyAnnotations,
@RequestParam(name = "canPrint", required = false)
@Parameter(description = "Whether printing of the document is allowed", example = "false")
boolean canPrint,
@RequestParam( name = "canPrintFaithful", required = false)
@Parameter(description = "Whether faithful printing is allowed", example = "false")
boolean canPrintFaithful
) throws IOException {
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) throws IOException {
MultipartFile fileInput = request.getFileInput();
String ownerPassword = request.getOwnerPassword();
String password = request.getPassword();
int keyLength = request.getKeyLength();
boolean canAssembleDocument = request.isCanAssembleDocument();
boolean canExtractContent = request.isCanExtractContent();
boolean canExtractForAccessibility = request.isCanExtractForAccessibility();
boolean canFillInForm = request.isCanFillInForm();
boolean canModify = request.isCanModify();
boolean canModifyAnnotations = request.isCanModifyAnnotations();
boolean canPrint = request.isCanPrint();
boolean canPrintFaithful = request.isCanPrintFaithful();
PDDocument document = PDDocument.load(fileInput.getBytes());
AccessPermission ap = new AccessPermission();
@@ -98,15 +73,15 @@ public class PasswordController {
ap.setCanPrint(!canPrint);
ap.setCanPrintFaithful(!canPrintFaithful);
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
spp.setEncryptionKeyLength(keyLength);
if(!"".equals(ownerPassword) || !"".equals(password)) {
spp.setEncryptionKeyLength(keyLength);
}
spp.setPermissions(ap);
document.protect(spp);
if("".equals(ownerPassword) && "".equals(password))
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
}

View File

@@ -0,0 +1,123 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.PDFText;
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class RedactController {
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
@Operation(summary = "Redacts listOfText in a PDF document",
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();
String listOfTextString = request.getListOfText();
boolean useRegex = request.isUseRegex();
boolean wholeWordSearchBool = request.isWholeWordSearch();
String colorString = request.getRedactColor();
float customPadding = request.getCustomPadding();
boolean convertPDFToImage = request.isConvertPDFToImage();
System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n");
byte[] bytes = file.getBytes();
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
Color redactColor;
try {
if (!colorString.startsWith("#")) {
colorString = "#" + colorString;
}
redactColor = Color.decode(colorString);
} catch (NumberFormatException e) {
logger.warn("Invalid color string provided. Using default color BLACK for redaction.");
redactColor = Color.BLACK;
}
for (String text : listOfText) {
text = text.trim();
System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding,redactColor);
}
if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream = new PDPageContentStream(imageDocument, newPage);
contentStream.drawImage(pdImage, 0, 0);
contentStream.close();
}
document.close();
document = imageDocument;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
}
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor) throws IOException {
var allPages = document.getDocumentCatalog().getPages();
for (PDFText block : blocks) {
var page = allPages.get(block.getPageIndex());
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setNonStrokingColor(redactColor);
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
PDRectangle pageBox = page.getBBox();
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding);
contentStream.fill();
contentStream.close();
}
}
}

View File

@@ -1,58 +1,51 @@
package stirling.software.SPDF.controller.api.security;
import java.io.IOException;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionLaunch;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import java.io.IOException;
import java.io.InputStream;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class SanitizeController {
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
@Operation(summary = "Sanitize a PDF file",
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> sanitizePDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be sanitized")
MultipartFile inputFile,
@RequestParam(name = "removeJavaScript", required = false, defaultValue = "true")
@Parameter(description = "Remove JavaScript actions from the PDF if set to true")
Boolean removeJavaScript,
@RequestParam(name = "removeEmbeddedFiles", required = false, defaultValue = "true")
@Parameter(description = "Remove embedded files from the PDF if set to true")
Boolean removeEmbeddedFiles,
@RequestParam(name = "removeMetadata", required = false, defaultValue = "true")
@Parameter(description = "Remove metadata from the PDF if set to true")
Boolean removeMetadata,
@RequestParam(name = "removeLinks", required = false, defaultValue = "true")
@Parameter(description = "Remove links from the PDF if set to true")
Boolean removeLinks,
@RequestParam(name = "removeFonts", required = false, defaultValue = "true")
@Parameter(description = "Remove fonts from the PDF if set to true")
Boolean removeFonts) throws IOException {
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException {
MultipartFile inputFile = request.getFileInput();
boolean removeJavaScript = request.isRemoveJavaScript();
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
boolean removeMetadata = request.isRemoveMetadata();
boolean removeLinks = request.isRemoveLinks();
boolean removeFonts = request.isRemoveFonts();
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if (removeJavaScript) {

View File

@@ -1,8 +1,6 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
@@ -24,40 +22,35 @@ import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class WatermarkController {
@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")
public ResponseEntity<byte[]> addWatermark(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to add a watermark") MultipartFile pdfFile,
@RequestParam(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType,
@RequestParam(required = false) @Parameter(description = "The watermark text") String watermarkText,
@RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage,
@RequestParam(defaultValue = "roman", name = "alphabet") @Parameter(description = "The selected alphabet",
schema = @Schema(type = "string",
allowableValues = {"roman","arabic","japanese","korean","chinese"},
defaultValue = "roman")) String alphabet,
@RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize,
@RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation,
@RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity,
@RequestParam(defaultValue = "50", name = "widthSpacer") @Parameter(description = "The width spacer between watermark elements", example = "50") int widthSpacer,
@RequestParam(defaultValue = "50", name = "heightSpacer") @Parameter(description = "The height spacer between watermark elements", example = "50") int heightSpacer)
throws IOException, Exception {
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
String watermarkType = request.getWatermarkType();
String watermarkText = request.getWatermarkText();
MultipartFile watermarkImage = request.getWatermarkImage();
String alphabet = request.getAlphabet();
float fontSize = request.getFontSize();
float rotation = request.getRotation();
float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer();
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());

View File

@@ -0,0 +1,135 @@
package stirling.software.SPDF.controller.web;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Controller
@Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController {
@GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/";
}
if (request.getParameter("error") != null) {
model.addAttribute("error", request.getParameter("error"));
}
if (request.getParameter("logout") != null) {
model.addAttribute("logoutMessage", "You have been logged out.");
}
return "login";
}
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers")
public String showAddUserForm(Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll();
model.addAttribute("users", allUsers);
model.addAttribute("currentUsername", authentication.getName());
return "addUsers";
}
@GetMapping("/account")
public String account(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/";
}
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
String username = userDetails.getUsername();
// Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
if (!user.isPresent()) {
// Handle error appropriately
return "redirect:/error"; // Example redirection in case of error
}
// Convert settings map to JSON string
ObjectMapper objectMapper = new ObjectMapper();
String settingsJson;
try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) {
// Handle JSON conversion error
e.printStackTrace();
return "redirect:/error"; // Example redirection in case of error
}
// Add attributes to the model
model.addAttribute("username", username);
model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
}
} else {
return "redirect:/";
}
return "account";
}
@GetMapping("/change-creds")
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/";
}
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
String username = userDetails.getUsername();
// Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
if (!user.isPresent()) {
// Handle error appropriately
return "redirect:/error"; // Example redirection in case of error
}
// Add attributes to the model
model.addAttribute("username", username);
}
} else {
return "redirect:/";
}
return "change-creds";
}
}

View File

@@ -9,6 +9,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -20,14 +21,19 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController {
@GetMapping("/pipeline")
@Hidden
public String pipelineForm(Model model) {
@@ -46,7 +52,8 @@ public class GeneralWebController {
}
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, Map.class);
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);
@@ -133,30 +140,116 @@ public class GeneralWebController {
return "sign";
}
@GetMapping("/multi-page-layout")
@Hidden
public String multiPageLayoutForm(Model model) {
model.addAttribute("currentPage", "multi-page-layout");
return "multi-page-layout";
}
@GetMapping("/scale-pages")
@Hidden
public String scalePagesFrom(Model model) {
model.addAttribute("currentPage", "scale-pages");
return "scale-pages";
}
@Autowired
private ResourceLoader resourceLoader;
private List<String> getFontNames() {
private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>();
// Extract font names from classpath
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
// Extract font names from external directory
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
return fontNames;
}
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
try {
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
.getResources("classpath:static/fonts/*.woff2");
.getResources(locationPattern);
return Arrays.stream(resources)
.map(resource -> {
try {
String filename = resource.getFilename();
return filename.substring(0, filename.length() - 6); // Remove .woff2 extension
if (filename != null) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex != -1) {
String name = filename.substring(0, lastDotIndex);
String extension = filename.substring(lastDotIndex + 1);
return new FontResource(name, extension);
}
}
return null;
} catch (Exception e) {
throw new RuntimeException("Error processing filename", e);
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("Failed to read font directory", e);
throw new RuntimeException("Failed to read font directory from " + locationPattern, e);
}
}
public String getFormatFromExtension(String extension) {
switch (extension) {
case "ttf": return "truetype";
case "woff": return "woff";
case "woff2": return "woff2";
case "eot": return "embedded-opentype";
case "svg": return "svg";
default: return ""; // or throw an exception if an unexpected extension is encountered
}
}
public class FontResource {
private String name;
private String extension;
private String type;
public FontResource(String name, String extension) {
this.name = name;
this.extension = extension;
this.type = getFormatFromExtension(extension);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExtension() {
return extension;
}
public void setExtension(String extension) {
this.extension = extension;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
@GetMapping("/crop")
@Hidden

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@@ -7,6 +8,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.oas.annotations.Hidden;
import stirling.software.SPDF.model.ApplicationProperties;
@Controller
public class HomeWebController {
@@ -31,18 +33,16 @@ public class HomeWebController {
return "redirect:/";
}
@Autowired
ApplicationProperties applicationProperties;
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@Hidden
public String getRobotsTxt() {
String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = "false";
if (Boolean.parseBoolean(allowGoogleVisibility)) {
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
if(Boolean.TRUE.equals(allowGoogle)) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";

View File

@@ -8,7 +8,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -24,26 +24,28 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties;
@RestController
@RequestMapping("/api/v1")
@Tag(name = "API", description = "Info APIs")
public class MetricsController {
@Autowired
ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry;
private boolean isEndpointEnabled;
private boolean metricsEnabled;
@PostConstruct
public void init() {
String isEndpointEnabled = System.getProperty("ENABLE_API_METRICS");
if (isEndpointEnabled == null) {
isEndpointEnabled = System.getenv("ENABLE_API_METRICS");
if (isEndpointEnabled == null) {
isEndpointEnabled = "true";
}
}
this.isEndpointEnabled = "true".equalsIgnoreCase(isEndpointEnabled);
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
if(metricsEnabled == null)
metricsEnabled = true;
this.metricsEnabled = metricsEnabled;
}
public MetricsController(MeterRegistry meterRegistry) {
@@ -54,7 +56,7 @@ public class MetricsController {
@Operation(summary = "Application status and version",
description = "This endpoint returns the status of the application and its version number.")
public ResponseEntity<?> getStatus() {
if (!isEndpointEnabled) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
@@ -68,7 +70,7 @@ public class MetricsController {
@Operation(summary = "GET request count",
description = "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 (!isEndpointEnabled) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
try {
@@ -109,7 +111,7 @@ public class MetricsController {
@Operation(summary = "GET requests count for all endpoints",
description = "This endpoint returns the count of GET requests for each endpoint.")
public ResponseEntity<?> getAllEndpointLoads() {
if (!isEndpointEnabled) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
try {
@@ -170,7 +172,7 @@ public class MetricsController {
@Operation(summary = "POST request count",
description = "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 (!isEndpointEnabled) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
try {
@@ -208,7 +210,7 @@ public class MetricsController {
@Operation(summary = "POST requests count for all endpoints",
description = "This endpoint returns the count of POST requests for each endpoint.")
public ResponseEntity<?> getAllPostRequests() {
if (!isEndpointEnabled) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
try {
@@ -244,7 +246,7 @@ public class MetricsController {
@GetMapping("/uptime")
public ResponseEntity<?> getUptime() {
if (!isEndpointEnabled) {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}

View File

@@ -15,19 +15,19 @@ import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller
@Tag(name = "Other", description = "Other APIs")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class OtherWebController {
@GetMapping("/compress-pdf")
@Hidden
public String compressPdfForm(Model model) {
model.addAttribute("currentPage", "compress-pdf");
return "other/compress-pdf";
return "misc/compress-pdf";
}
@GetMapping("/extract-image-scans")
@Hidden
public ModelAndView extractImageScansForm() {
ModelAndView modelAndView = new ModelAndView("other/extract-image-scans");
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
modelAndView.addObject("currentPage", "extract-image-scans");
return modelAndView;
}
@@ -36,7 +36,7 @@ public class OtherWebController {
@Hidden
public String extractJavascriptForm(Model model) {
model.addAttribute("currentPage", "show-javascript");
return "other/show-javascript";
return "misc/show-javascript";
}
@@ -44,21 +44,21 @@ public class OtherWebController {
@Hidden
public String addPageNumbersForm(Model model) {
model.addAttribute("currentPage", "add-page-numbers");
return "other/add-page-numbers";
return "misc/add-page-numbers";
}
@GetMapping("/extract-images")
@Hidden
public String extractImagesForm(Model model) {
model.addAttribute("currentPage", "extract-images");
return "other/extract-images";
return "misc/extract-images";
}
@GetMapping("/flatten")
@Hidden
public String flattenForm(Model model) {
model.addAttribute("currentPage", "flatten");
return "other/flatten";
return "misc/flatten";
}
@@ -67,14 +67,14 @@ public class OtherWebController {
@Hidden
public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "change-metadata");
return "other/change-metadata";
return "misc/change-metadata";
}
@GetMapping("/compare")
@Hidden
public String compareForm(Model model) {
model.addAttribute("currentPage", "compare");
return "other/compare";
return "misc/compare";
}
public List<String> getAvailableTesseractLanguages() {
@@ -90,7 +90,7 @@ public class OtherWebController {
@GetMapping("/ocr-pdf")
@Hidden
public ModelAndView ocrPdfPage() {
ModelAndView modelAndView = new ModelAndView("other/ocr-pdf");
ModelAndView modelAndView = new ModelAndView("misc/ocr-pdf");
List<String> languages = getAvailableTesseractLanguages();
Collections.sort(languages);
modelAndView.addObject("languages", languages);
@@ -103,56 +103,43 @@ public class OtherWebController {
@Hidden
public String overlayImage(Model model) {
model.addAttribute("currentPage", "add-image");
return "other/add-image";
return "misc/add-image";
}
@GetMapping("/adjust-contrast")
@Hidden
public String contrast(Model model) {
model.addAttribute("currentPage", "adjust-contrast");
return "other/adjust-contrast";
return "misc/adjust-contrast";
}
@GetMapping("/repair")
@Hidden
public String repairForm(Model model) {
model.addAttribute("currentPage", "repair");
return "other/repair";
return "misc/repair";
}
@GetMapping("/remove-blanks")
@Hidden
public String removeBlanksForm(Model model) {
model.addAttribute("currentPage", "remove-blanks");
return "other/remove-blanks";
}
@GetMapping("/multi-page-layout")
@Hidden
public String multiPageLayoutForm(Model model) {
model.addAttribute("currentPage", "multi-page-layout");
return "other/multi-page-layout";
}
@GetMapping("/scale-pages")
@Hidden
public String scalePagesFrom(Model model) {
model.addAttribute("currentPage", "scale-pages");
return "other/scale-pages";
return "misc/remove-blanks";
}
@GetMapping("/auto-crop")
@Hidden
public String autoCropForm(Model model) {
model.addAttribute("currentPage", "auto-crop");
return "other/auto-crop";
return "misc/auto-crop";
}
@GetMapping("/auto-rename")
@Hidden
public String autoRenameForm(Model model) {
model.addAttribute("currentPage", "auto-rename");
return "other/auto-rename";
return "misc/auto-rename";
}

View File

@@ -11,6 +11,12 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Security", description = "Security APIs")
public class SecurityWebController {
@GetMapping("/auto-redact")
@Hidden
public String autoRedactForm(Model model) {
model.addAttribute("currentPage", "auto-redact");
return "security/auto-redact";
}
@GetMapping("/add-password")
@Hidden

View File

@@ -0,0 +1,49 @@
package stirling.software.SPDF.model;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public ApiKeyAuthenticationToken(String apiKey) {
super(null);
this.principal = null;
this.credentials = apiKey;
setAuthenticated(false);
}
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal; // principal can be a UserDetails object
this.credentials = apiKey;
super.setAuthenticated(true); // this authentication is trusted
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead.");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}

View File

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

@@ -0,0 +1,64 @@
package stirling.software.SPDF.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "authorities")
public class Authority {
public Authority() {
}
public Authority(String authority, User user) {
this.authority = authority;
this.user = user;
user.getAuthorities().add(this);
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "authority")
private String authority;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAuthority() {
return authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}

View File

@@ -0,0 +1,42 @@
package stirling.software.SPDF.model;
public class PDFText {
private final int pageIndex;
private final float x1;
private final float y1;
private final float x2;
private final float y2;
private final String text;
public PDFText(int pageIndex, float x1, float y1, float x2, float y2, String text) {
this.pageIndex = pageIndex;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.text = text;
}
public int getPageIndex() {
return pageIndex;
}
public float getX1() {
return x1;
}
public float getY1() {
return y1;
}
public float getX2() {
return x2;
}
public float getY2() {
return y2;
}
public String getText() {
return text;
}
}

View File

@@ -0,0 +1,61 @@
package stirling.software.SPDF.model;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "persistent_logins")
public class PersistentLogin {
@Id
@Column(name = "series")
private String series;
@Column(name = "username", length = 64, nullable = false)
private String username;
@Column(name = "token", length = 64, nullable = false)
private String token;
@Column(name = "last_used", nullable = false)
private Date lastUsed;
public String getSeries() {
return series;
}
public void setSeries(String series) {
this.series = series;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Date getLastUsed() {
return lastUsed;
}
public void setLastUsed(Date lastUsed) {
this.lastUsed = lastUsed;
}
// Getters, setters, etc.
}

View File

@@ -0,0 +1,50 @@
package stirling.software.SPDF.model;
public enum Role {
// Unlimited access
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE),
// Unlimited access
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
// 40 API calls Per Day, 40 web calls
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40),
// 20 API calls Per Day, 20 web calls
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20),
// 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20);
private final String roleId;
private final int apiCallsPerDay;
private final int webCallsPerDay;
Role(String roleId, int apiCallsPerDay, int webCallsPerDay) {
this.roleId = roleId;
this.apiCallsPerDay = apiCallsPerDay;
this.webCallsPerDay = webCallsPerDay;
}
public String getRoleId() {
return roleId;
}
public int getApiCallsPerDay() {
return apiCallsPerDay;
}
public int getWebCallsPerDay() {
return webCallsPerDay;
}
public static Role fromString(String roleId) {
for (Role role : Role.values()) {
if (role.getRoleId().equalsIgnoreCase(roleId)) {
return role;
}
}
throw new IllegalArgumentException("No Role defined for id: " + roleId);
}
}

View File

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

View File

@@ -0,0 +1,134 @@
package stirling.software.SPDF.model;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.persistence.CascadeType;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapKeyColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "username", unique = true)
private String username;
@Column(name = "password")
private String password;
@Column(name = "apiKey")
private String apiKey;
@Column(name = "enabled")
private boolean enabled;
@Column(name = "isFirstLogin")
private Boolean isFirstLogin = false;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>();
@ElementCollection
@MapKeyColumn(name = "setting_key")
@Column(name = "setting_value")
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
public boolean isFirstLogin() {
return isFirstLogin != null && isFirstLogin;
}
public void setFirstLogin(boolean isFirstLogin) {
this.isFirstLogin = isFirstLogin;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public Map<String, String> getSettings() {
return settings;
}
public void setSettings(Map<String, String> settings) {
this.settings = settings;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Set<Authority> getAuthorities() {
return authorities;
}
public void setAuthorities(Set<Authority> 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(", "));
}
}

View File

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

View File

@@ -0,0 +1,20 @@
package stirling.software.SPDF.model.api;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode
public class HandleDataRequest {
@Schema(description = "The input files")
private MultipartFile[] fileInputs;
@Schema(description = "JSON String")
private String jsonString;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
package stirling.software.SPDF.model.api;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.utils.GeneralUtils;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper=true)
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')\"")
private String pageNumbers;
public List<Integer> getPageNumbersList(){
int pageCount = 0;
try {
pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return GeneralUtils.parsePageString(pageNumbers, pageCount);
}
public List<Integer> getPageNumbersList(PDDocument doc){
int pageCount = 0;
pageCount = doc.getNumberOfPages();
return GeneralUtils.parsePageString(pageNumbers, pageCount);
}
}

View File

@@ -0,0 +1,16 @@
package stirling.software.SPDF.model.api;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper=true)
public class PDFWithPageSize extends PDFFile {
@Schema(description = "The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL.",
allowableValues = {
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"
})
private String pageSize;
}

View File

@@ -0,0 +1,23 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class ConvertToImageRequest extends PDFFile {
@Schema(description = "The output image format", allowableValues = {"png", "jpeg", "jpg", "gif"})
private String imageFormat;
@Schema(description = "Choose between a single image containing all pages or separate images for each page", allowableValues = {"single", "multiple"})
private String singleOrMultiple;
@Schema(description = "The color type of the output image(s)", allowableValues = {"color", "greyscale", "blackwhite"})
private String colorType;
@Schema(description = "The DPI (dots per inch) for the output image(s)")
private String dpi;
}

View File

@@ -0,0 +1,27 @@
package stirling.software.SPDF.model.api.converters;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode
public class ConvertToPdfRequest {
@Schema(description = "The input images to be converted to a PDF file")
private MultipartFile[] fileInput;
@Schema(description = "Option to determine how the image will fit onto the page",
allowableValues = { "fillPage", "fitDocumentToImage", "maintainAspectRatio" })
private String fitOption;
@Schema(description = "The color type of the output image(s)", allowableValues = {"color", "greyscale", "blackwhite"})
private String colorType;
@Schema(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true")
private boolean autoRotate;
}

View File

@@ -0,0 +1,14 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class PdfToPresentationRequest extends PDFFile {
@Schema(description = "The output Presentation format", allowableValues = {"ppt", "pptx", "odp"})
private String outputFormat;
}

View File

@@ -0,0 +1,14 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class PdfToTextOrRTFRequest extends PDFFile {
@Schema(description = "The output Text or RTF format", allowableValues = {"rtf", "txt:Text"})
private String outputFormat;
}

View File

@@ -0,0 +1,14 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class PdfToWordRequest extends PDFFile {
@Schema(description = "The output Word document format", allowableValues = {"doc", "docx", "odt"})
private String outputFormat;
}

View File

@@ -0,0 +1,13 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode
public class UrlToPdfRequest {
@Schema(description = "The input URL to be converted to a PDF file", required = true)
private String urlInput;
}

View File

@@ -0,0 +1,14 @@
package stirling.software.SPDF.model.api.filter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFWithPageNums;
@Data
@EqualsAndHashCode(callSuper=true)
public class ContainsTextRequest extends PDFWithPageNums {
@Schema(description = "The text to check for", required = true)
private String text;
}

View File

@@ -0,0 +1,16 @@
package stirling.software.SPDF.model.api.filter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFComparison;
@Data
@EqualsAndHashCode(callSuper=true)
public class FileSizeRequest extends PDFComparison {
@Schema(description = "File Size", required = true)
private String fileSize;
}

View File

@@ -0,0 +1,15 @@
package stirling.software.SPDF.model.api.filter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFComparison;
@Data
@EqualsAndHashCode(callSuper=true)
public class PageRotationRequest extends PDFComparison {
@Schema(description = "Rotation in degrees", required = true)
private int rotation;
}

View File

@@ -0,0 +1,16 @@
package stirling.software.SPDF.model.api.filter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFComparison;
@Data
@EqualsAndHashCode(callSuper=true)
public class PageSizeRequest extends PDFComparison {
@Schema(description = "Standard Page Size", required = true)
private String standardPageSize;
}

View File

@@ -0,0 +1,24 @@
package stirling.software.SPDF.model.api.general;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class CropPdfForm extends PDFFile {
@Schema(description = "The x-coordinate of the top-left corner of the crop area", type = "number")
private float x;
@Schema(description = "The y-coordinate of the top-left corner of the crop area", type = "number")
private float y;
@Schema(description = "The width of the crop area", type = "number")
private float width;
@Schema(description = "The height of the crop area", type = "number")
private float height;
}

View File

@@ -0,0 +1,18 @@
package stirling.software.SPDF.model.api.general;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class MergeMultiplePagesRequest extends PDFFile {
@Schema(description = "The number of pages to fit onto a single sheet in the output PDF.",
type = "integer", allowableValues = {"2", "3", "4", "9", "16"})
private int pagesPerSheet;
@Schema(description = "Boolean for if you wish to add border around the pages")
private boolean addBorder;
}

View File

@@ -0,0 +1,22 @@
package stirling.software.SPDF.model.api.general;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.MultiplePDFFiles;
@Data
@EqualsAndHashCode(callSuper=true)
public class MergePdfsRequest extends MultiplePDFFiles {
@Schema(description = "The type of sorting to be applied on the input files before merging.",
allowableValues = {
"orderProvided",
"byFileName",
"byDateModified",
"byDateCreated",
"byPDFTitle"
},
defaultValue = "orderProvided")
private String sortType = "orderProvided";
}

View File

@@ -0,0 +1,23 @@
package stirling.software.SPDF.model.api.general;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums;
@Data
@EqualsAndHashCode(callSuper=true)
public class RearrangePagesRequest extends PDFWithPageNums {
@Schema(implementation = SortTypes.class, description = "The custom mode for page rearrangement. Valid values are:\n"
+ "REVERSE_ORDER: Reverses the order of all pages.\n"
+ "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). "
+ "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n"
+ "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n"
+ "REMOVE_FIRST: Removes the first page.\n" + "REMOVE_LAST: Removes the last page.\n"
+ "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")
private String customMode;
}

View File

@@ -0,0 +1,14 @@
package stirling.software.SPDF.model.api.general;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper=true)
public class RotatePDFRequest extends PDFFile {
@Schema(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90")
private Integer angle;
}

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