diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 6a953e49..f4c8c7c5 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -15,7 +15,7 @@ import jakarta.annotation.PostConstruct; import stirling.software.SPDF.utils.GeneralUtils; @SpringBootApplication -//@EnableScheduling +@EnableScheduling public class SPdfApplication { @Autowired diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java new file mode 100644 index 00000000..e56c1b40 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -0,0 +1,132 @@ +package stirling.software.SPDF.controller.api; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.SPDF.utils.GeneralUtils; +import stirling.software.SPDF.utils.WebResponseUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.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 CropController { + + 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 cropPdf( + @Parameter(description = "The input PDF file", required = true) @RequestParam("file") 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); + + 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"); + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java index ab27d9a0..3a4b5b1c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java @@ -114,9 +114,9 @@ public class PageNumbersController { for (int i : pagesToNumberList) { PdfPage page = pdfDoc.getPage(i+1); Rectangle pageSize = page.getPageSize(); - PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdfDoc); + PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc); - String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{p}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber); + String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber); PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA); float textWidth = font.getWidth(text, fontSize); diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java index d0325450..308927e6 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java @@ -20,7 +20,10 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; - +import java.io.FileOutputStream; +import java.io.OutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -48,247 +51,328 @@ import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.utils.WebResponseUtils; - @RestController @Tag(name = "Pipeline", description = "Pipeline APIs") public class Controller { + private static final Logger logger = LoggerFactory.getLogger(Controller.class); @Autowired private ObjectMapper objectMapper; - - - final String jsonFileName = "pipelineCofig.json"; + + final String jsonFileName = "pipelineConfig.json"; final String watchedFoldersDir = "watchedFolders/"; - @Scheduled(fixedRate = 5000) + final String finishedFoldersDir = "finishedFolders/"; + + @Scheduled(fixedRate = 25000) public void scanFolders() { + logger.info("Scanning folders..."); Path watchedFolderPath = Paths.get(watchedFoldersDir); - if (!Files.exists(watchedFolderPath)) { - try { - Files.createDirectories(watchedFolderPath); - } catch (IOException e) { - e.printStackTrace(); - return; - } - } - + if (!Files.exists(watchedFolderPath)) { + try { + Files.createDirectories(watchedFolderPath); + logger.info("Created directory: {}", watchedFolderPath); + } catch (IOException e) { + logger.error("Error creating directory: {}", watchedFolderPath, e); + return; + } + } try (Stream paths = Files.walk(watchedFolderPath)) { - paths.filter(Files::isDirectory).forEach(t -> { + paths.filter(Files::isDirectory).forEach(t -> { try { if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { handleDirectory(t); } } catch (Exception e) { - e.printStackTrace(); + logger.error("Error handling directory: {}", t, e); } }); - } catch (Exception e) { - e.printStackTrace(); - } + } catch (Exception e) { + logger.error("Error walking through directory: {}", watchedFolderPath, e); + } } - + private void handleDirectory(Path dir) throws Exception { - Path jsonFile = dir.resolve(jsonFileName); - Path processingDir = dir.resolve("processing"); // Directory to move files during processing - if (!Files.exists(processingDir)) { - Files.createDirectory(processingDir); - } - - if (Files.exists(jsonFile)) { - // Read JSON file - String jsonString; - try { - jsonString = new String(Files.readAllBytes(jsonFile)); - } catch (IOException e) { - e.printStackTrace(); - return; - } - - // Decode JSON to PipelineConfig - PipelineConfig config; - try { - config = objectMapper.readValue(jsonString, PipelineConfig.class); - // Assuming your PipelineConfig class has getters for all necessary fields, you can perform checks here - if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) { - throw new IOException("Invalid JSON format"); - } - } catch (IOException e) { - e.printStackTrace(); - return; - } - - // For each operation in the pipeline - for (PipelineOperation operation : config.getOperations()) { - // Collect all files based on fileInput - File[] files; - String fileInput = (String) operation.getParameters().get("fileInput"); - if ("automated".equals(fileInput)) { - // If fileInput is "automated", process all files in the directory - try (Stream paths = Files.list(dir)) { - files = paths.filter(path -> !path.equals(jsonFile)) - .map(Path::toFile) - .toArray(File[]::new); - } catch (IOException e) { - e.printStackTrace(); - return; - } - } else { - // If fileInput contains a path, process only this file - files = new File[]{new File(fileInput)}; - } - - // Prepare the files for processing - File[] filesToProcess = files.clone(); - for (File file : filesToProcess) { - Files.move(file.toPath(), processingDir.resolve(file.getName())); - } - - // Process the files - try { - List resources = handleFiles(filesToProcess, jsonString); - - // Move resultant files and rename them as per config in JSON file - for (Resource resource : resources) { - String outputFileName = config.getOutputPattern().replace("{filename}", resource.getFile().getName()); - outputFileName = outputFileName.replace("{pipelineName}", config.getName()); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); - outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter)); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss"); - outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter)); - // {filename} {folder} {date} {tmime} {pipeline} - - Files.move(resource.getFile().toPath(), Paths.get(config.getOutputDir(), outputFileName)); - } - - // If successful, delete the original files - for (File file : filesToProcess) { - Files.deleteIfExists(processingDir.resolve(file.getName())); - } - } catch (Exception e) { - // If an error occurs, move the original files back - for (File file : filesToProcess) { - Files.move(processingDir.resolve(file.getName()), file.toPath()); - } - throw e; - } - } - } - } - - - - -List processFiles(List outputFiles, String jsonString) throws Exception{ - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonNode = mapper.readTree(jsonString); - - JsonNode pipelineNode = jsonNode.get("pipeline"); - ByteArrayOutputStream logStream = new ByteArrayOutputStream(); - PrintStream logPrintStream = new PrintStream(logStream); - - boolean hasErrors = false; - - for (JsonNode operationNode : pipelineNode) { - String operation = operationNode.get("operation").asText(); - JsonNode parametersNode = operationNode.get("parameters"); - String inputFileExtension = ""; - if(operationNode.has("inputFileType")) { - inputFileExtension = operationNode.get("inputFileType").asText(); - } else { - inputFileExtension=".pdf"; + logger.info("Handling directory: {}", dir); + Path jsonFile = dir.resolve(jsonFileName); + Path processingDir = dir.resolve("processing"); // Directory to move files during processing + if (!Files.exists(processingDir)) { + Files.createDirectory(processingDir); + logger.info("Created processing directory: {}", processingDir); } - List newOutputFiles = new ArrayList<>(); - boolean hasInputFileType = false; - - for (Resource file : outputFiles) { - if (file.getFilename().endsWith(inputFileExtension)) { - hasInputFileType = true; - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("fileInput", file); - - Iterator> parameters = parametersNode.fields(); - while (parameters.hasNext()) { - Map.Entry parameter = parameters.next(); - body.add(parameter.getKey(), parameter.getValue().asText()); - } - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - HttpEntity> entity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - String url = "http://localhost:8080/" + operation; - - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); - - if (!response.getStatusCode().equals(HttpStatus.OK)) { - logPrintStream.println("Error: " + response.getBody()); - hasErrors = true; - continue; - } - - // Check if the response body is a zip file - if (isZip(response.getBody())) { - // Unzip the file and add all the files to the new output files - newOutputFiles.addAll(unzip(response.getBody())); - } else { - Resource outputResource = new ByteArrayResource(response.getBody()) { - @Override - public String getFilename() { - return file.getFilename(); // Preserving original filename - } - }; - newOutputFiles.add(outputResource); - } + if (Files.exists(jsonFile)) { + // Read JSON file + String jsonString; + try { + jsonString = new String(Files.readAllBytes(jsonFile)); + logger.info("Read JSON file: {}", jsonFile); + } catch (IOException e) { + logger.error("Error reading JSON file: {}", jsonFile, e); + return; } - if (!hasInputFileType) { - logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation); - hasErrors = true; - } - - outputFiles = newOutputFiles; + // Decode JSON to PipelineConfig + PipelineConfig config; + try { + config = objectMapper.readValue(jsonString, PipelineConfig.class); + // Assuming your PipelineConfig class has getters for all necessary fields, you + // can perform checks here + if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) { + throw new IOException("Invalid JSON format"); + } + logger.info("Parsed PipelineConfig: {}", config); + } catch (IOException e) { + logger.error("Error parsing PipelineConfig: {}", jsonString, e); + return; + } + + // For each operation in the pipeline + for (PipelineOperation operation : config.getOperations()) { + logger.info("Processing operation: {}", operation.toString()); + // Collect all files based on fileInput + File[] files; + String fileInput = (String) operation.getParameters().get("fileInput"); + if ("automated".equals(fileInput)) { + // If fileInput is "automated", process all files in the directory + try (Stream paths = Files.list(dir)) { + files = paths + .filter(path -> !Files.isDirectory(path)) // exclude directories + .filter(path -> !path.equals(jsonFile)) // exclude jsonFile + .map(Path::toFile) + .toArray(File[]::new); + + } catch (IOException e) { + e.printStackTrace(); + return; + } + } else { + // If fileInput contains a path, process only this file + files = new File[] { new File(fileInput) }; + } + + // Prepare the files for processing + List filesToProcess = new ArrayList<>(); + for (File file : files) { + logger.info(file.getName()); + logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName())); + Files.move(file.toPath(), processingDir.resolve(file.getName())); + filesToProcess.add(processingDir.resolve(file.getName()).toFile()); + } + + // Process the files + try { + List resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString); + + if(resources == null) { + return; + } + // Move resultant files and rename them as per config in JSON file + for (Resource resource : resources) { + String resourceName = resource.getFilename(); + String baseName = resourceName.substring(0, resourceName.lastIndexOf(".")); + String extension = resourceName.substring(resourceName.lastIndexOf(".")+1); + + String outputFileName = config.getOutputPattern().replace("{filename}", baseName); + + outputFileName = outputFileName.replace("{pipelineName}", config.getName()); + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter)); + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss"); + outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter)); + + outputFileName += "." + extension; + // {filename} {folder} {date} {tmime} {pipeline} + String outputDir = config.getOutputDir(); + + // Check if the environment variable 'automatedOutputFolder' is set + String outputFolder = System.getenv("automatedOutputFolder"); + + if (outputFolder == null || outputFolder.isEmpty()) { + // If the environment variable is not set, use the default value + outputFolder = finishedFoldersDir; + } + + // Replace the placeholders in the outputDir string + outputDir = outputDir.replace("{outputFolder}", outputFolder); + outputDir = outputDir.replace("{folderName}", dir.toString()); + outputDir = outputDir.replace("\\watchedFolders", ""); + Path outputPath; + + if (Paths.get(outputDir).isAbsolute()) { + // If it's an absolute path, use it directly + outputPath = Paths.get(outputDir); + } else { + // If it's a relative path, make it relative to the current working directory + outputPath = Paths.get(".", outputDir); + } + + + if (!Files.exists(outputPath)) { + try { + Files.createDirectories(outputPath); + logger.info("Created directory: {}", outputPath); + } catch (IOException e) { + logger.error("Error creating directory: {}", outputPath, e); + return; + } + } + logger.info("outputPath {}", outputPath); + logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString()); + File newFile = new File(outputPath.resolve(outputFileName).toString()); + OutputStream os = new FileOutputStream(newFile); + os.write(((ByteArrayResource)resource).getByteArray()); + os.close(); + logger.info("made {}", outputPath.resolve(outputFileName)); + } + + // If successful, delete the original files + for (File file : filesToProcess) { + Files.deleteIfExists(processingDir.resolve(file.getName())); + } + } catch (Exception e) { + // If an error occurs, move the original files back + for (File file : filesToProcess) { + Files.move(processingDir.resolve(file.getName()), file.toPath()); + } + throw e; + } + } } - logPrintStream.close(); + } + + List processFiles(List outputFiles, String jsonString) throws Exception { - } - return outputFiles; -} - - -List handleFiles(File[] files, String jsonString) throws Exception{ - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonNode = mapper.readTree(jsonString); - - JsonNode pipelineNode = jsonNode.get("pipeline"); - ByteArrayOutputStream logStream = new ByteArrayOutputStream(); - PrintStream logPrintStream = new PrintStream(logStream); - - boolean hasErrors = false; - List outputFiles = new ArrayList<>(); - - for (File file : files) { - Path path = Paths.get(file.getAbsolutePath()); - Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { - @Override - public String getFilename() { - return file.getName(); - } - }; - outputFiles.add(fileResource); - } - return processFiles(outputFiles, jsonString); -} - - List handleFiles(MultipartFile[] files, String jsonString) throws Exception{ + logger.info("Processing files... " + outputFiles); ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(jsonString); JsonNode pipelineNode = jsonNode.get("pipeline"); ByteArrayOutputStream logStream = new ByteArrayOutputStream(); PrintStream logPrintStream = new PrintStream(logStream); + + boolean hasErrors = false; + + for (JsonNode operationNode : pipelineNode) { + String operation = operationNode.get("operation").asText(); + logger.info("Running operation: {}", operation); + JsonNode parametersNode = operationNode.get("parameters"); + String inputFileExtension = ""; + if (operationNode.has("inputFileType")) { + inputFileExtension = operationNode.get("inputFileType").asText(); + } else { + inputFileExtension = ".pdf"; + } + + List newOutputFiles = new ArrayList<>(); + boolean hasInputFileType = false; + + for (Resource file : outputFiles) { + if (file.getFilename().endsWith(inputFileExtension)) { + hasInputFileType = true; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", file); + + Iterator> parameters = parametersNode.fields(); + while (parameters.hasNext()) { + Map.Entry parameter = parameters.next(); + body.add(parameter.getKey(), parameter.getValue().asText()); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> entity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + String url = "http://localhost:8080/" + operation; + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); + + if (!response.getStatusCode().equals(HttpStatus.OK)) { + logPrintStream.println("Error: " + response.getBody()); + hasErrors = true; + continue; + } + + // Check if the response body is a zip file + if (isZip(response.getBody())) { + // Unzip the file and add all the files to the new output files + newOutputFiles.addAll(unzip(response.getBody())); + } else { + Resource outputResource = new ByteArrayResource(response.getBody()) { + @Override + public String getFilename() { + return file.getFilename(); // Preserving original filename + } + }; + newOutputFiles.add(outputResource); + } + } + + if (!hasInputFileType) { + logPrintStream.println( + "No files with extension " + inputFileExtension + " found for operation " + operation); + hasErrors = true; + } + + outputFiles = newOutputFiles; + } + logPrintStream.close(); + + } + if (hasErrors) { + logger.error("Errors occurred during processing. Log: {}", logStream.toString()); + } + return outputFiles; + } + + List handleFiles(File[] files, String jsonString) throws Exception { + if(files == null || files.length == 0) { + logger.info("No files"); + return null; + } + + logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(jsonString); + + JsonNode pipelineNode = jsonNode.get("pipeline"); + + boolean hasErrors = false; + List outputFiles = new ArrayList<>(); + + for (File file : files) { + Path path = Paths.get(file.getAbsolutePath()); + System.out.println("Reading file: " + path); // debug statement + + if (Files.exists(path)) { + Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { + @Override + public String getFilename() { + return file.getName(); + } + }; + outputFiles.add(fileResource); + } else { + System.out.println("File not found: " + path); // debug statement + } + } + logger.info("Files successfully loaded. Starting processing..."); + return processFiles(outputFiles, jsonString); + } + + List handleFiles(MultipartFile[] files, String jsonString) throws Exception { + if(files == null || files.length == 0) { + logger.info("No files"); + return null; + } + logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(jsonString); + + JsonNode pipelineNode = jsonNode.get("pipeline"); + boolean hasErrors = false; List outputFiles = new ArrayList<>(); @@ -301,53 +385,59 @@ List handleFiles(File[] files, String jsonString) throws Exception{ }; outputFiles.add(fileResource); } + logger.info("Files successfully loaded. Starting processing..."); return processFiles(outputFiles, jsonString); } - + @PostMapping("/handleData") public ResponseEntity handleData(@RequestPart("fileInput") MultipartFile[] files, @RequestParam("json") String jsonString) { + logger.info("Received POST request to /handleData with {} files", files.length); try { - - List outputFiles = handleFiles(files, jsonString); + List outputFiles = handleFiles(files, jsonString); - if (outputFiles.size() == 1) { - // If there is only one file, return it directly - Resource singleFile = outputFiles.get(0); - InputStream is = singleFile.getInputStream(); - byte[] bytes = new byte[(int)singleFile.contentLength()]; - is.read(bytes); - is.close(); - - return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM); - } + if (outputFiles != null && outputFiles.size() == 1) { + // If there is only one file, return it directly + Resource singleFile = outputFiles.get(0); + InputStream is = singleFile.getInputStream(); + byte[] bytes = new byte[(int) singleFile.contentLength()]; + is.read(bytes); + is.close(); + + logger.info("Returning single file response..."); + return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), + MediaType.APPLICATION_OCTET_STREAM); + } else if (outputFiles == null) { + return null; + } // Create a ByteArrayOutputStream to hold the zip - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ZipOutputStream zipOut = new ZipOutputStream(baos); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zipOut = new ZipOutputStream(baos); - // Loop through each file and add it to the zip - for (Resource file : outputFiles) { - ZipEntry zipEntry = new ZipEntry(file.getFilename()); - zipOut.putNextEntry(zipEntry); + // Loop through each file and add it to the zip + for (Resource file : outputFiles) { + ZipEntry zipEntry = new ZipEntry(file.getFilename()); + zipOut.putNextEntry(zipEntry); - // Read the file into a byte array - InputStream is = file.getInputStream(); - byte[] bytes = new byte[(int)file.contentLength()]; - is.read(bytes); + // Read the file into a byte array + InputStream is = file.getInputStream(); + byte[] bytes = new byte[(int) file.contentLength()]; + is.read(bytes); - // Write the bytes of the file to the zip - zipOut.write(bytes, 0, bytes.length); - zipOut.closeEntry(); + // Write the bytes of the file to the zip + zipOut.write(bytes, 0, bytes.length); + zipOut.closeEntry(); - is.close(); - } + is.close(); + } - zipOut.close(); - + zipOut.close(); + + logger.info("Returning zipped file response..."); return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); } catch (Exception e) { - e.printStackTrace(); + logger.error("Error handling data: ", e); return null; } } @@ -362,6 +452,7 @@ List handleFiles(File[] files, String jsonString) throws Exception{ } private List unzip(byte[] data) throws IOException { + logger.info("Unzipping data of length: {}", data.length); List unzippedFiles = new ArrayList<>(); try (ByteArrayInputStream bais = new ByteArrayInputStream(data); @@ -387,6 +478,7 @@ List handleFiles(File[] files, String jsonString) throws Exception{ // If the unzipped file is a zip file, unzip it if (isZip(baos.toByteArray())) { + logger.info("File {} is a zip file. Unzipping...", filename); unzippedFiles.addAll(unzip(baos.toByteArray())); } else { unzippedFiles.add(fileResource); @@ -394,6 +486,8 @@ List handleFiles(File[] files, String jsonString) throws Exception{ } } + logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); return unzippedFiles; } + } diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 11a7c9bf..f30c54e5 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -68,4 +68,10 @@ public class GeneralWebController { return "sign"; } + @GetMapping("/crop") + @Hidden + public String cropForm(Model model) { + model.addAttribute("currentPage", "crop"); + return "crop"; + } } diff --git a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java index 8b079ba1..10c27bfc 100644 --- a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java +++ b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java @@ -22,4 +22,11 @@ public class PipelineOperation { public void setParameters(Map parameters) { this.parameters = parameters; } + + @Override + public String toString() { + return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]"; + } + + } \ No newline at end of file diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index ce7ee576..2a6941f6 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -146,6 +146,9 @@ home.auto-rename.desc=Auto renames a PDF file based on its detected header home.adjust-contrast.title=Adjust Colors/Contrast home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF +home.crop.title=Crop PDF +home.crop.desc=Crop a PDF to reduce its size (maintains text!) + error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect downloadPdf=Download PDF diff --git a/src/main/resources/static/images/crop.svg b/src/main/resources/static/images/crop.svg new file mode 100644 index 00000000..b7e17490 --- /dev/null +++ b/src/main/resources/static/images/crop.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/templates/crop.html b/src/main/resources/templates/crop.html new file mode 100644 index 00000000..b3abeba3 --- /dev/null +++ b/src/main/resources/templates/crop.html @@ -0,0 +1,105 @@ + + + + + + + +
+
+
+

+
+
+
+

+
+ + + + + + +
+ + + +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 226141f6..7b3902cc 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -40,7 +40,7 @@ --> - diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 145b65d1..012070c6 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -76,7 +76,7 @@
- +
diff --git a/src/main/resources/templates/other/add-page-numbers.html b/src/main/resources/templates/other/add-page-numbers.html index 59cd0403..2ea91a3e 100644 --- a/src/main/resources/templates/other/add-page-numbers.html +++ b/src/main/resources/templates/other/add-page-numbers.html @@ -15,30 +15,104 @@

-
-
- - -
- - -
- - -
- - -
- - -
- -
+
+
+
+ + +
+ + + +
+ +
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
+ +
+
+ +
+ + + +
+ + +
+
+ + +
+
+ + +
+ +
diff --git a/src/main/resources/templates/security/add-watermark.html b/src/main/resources/templates/security/add-watermark.html index 30091ea8..530388c9 100644 --- a/src/main/resources/templates/security/add-watermark.html +++ b/src/main/resources/templates/security/add-watermark.html @@ -22,7 +22,7 @@