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