lots of stuff and garbage code for automate to cleanup lots
This commit is contained in:
parent
5877b40be5
commit
6e726ac2a6
13 changed files with 699 additions and 272 deletions
|
@ -15,7 +15,7 @@ import jakarta.annotation.PostConstruct;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
//@EnableScheduling
|
@EnableScheduling
|
||||||
public class SPdfApplication {
|
public class SPdfApplication {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|
|
@ -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<byte[]> 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -114,9 +114,9 @@ public class PageNumbersController {
|
||||||
for (int i : pagesToNumberList) {
|
for (int i : pagesToNumberList) {
|
||||||
PdfPage page = pdfDoc.getPage(i+1);
|
PdfPage page = pdfDoc.getPage(i+1);
|
||||||
Rectangle pageSize = page.getPageSize();
|
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);
|
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
|
||||||
float textWidth = font.getWidth(text, fontSize);
|
float textWidth = font.getWidth(text, fontSize);
|
||||||
|
|
|
@ -20,7 +20,10 @@ import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
import java.util.zip.ZipOutputStream;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
@ -48,29 +51,31 @@ import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.PipelineOperation;
|
import stirling.software.SPDF.model.PipelineOperation;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
||||||
public class Controller {
|
public class Controller {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Controller.class);
|
||||||
@Autowired
|
@Autowired
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
final String jsonFileName = "pipelineConfig.json";
|
||||||
final String jsonFileName = "pipelineCofig.json";
|
|
||||||
final String watchedFoldersDir = "watchedFolders/";
|
final String watchedFoldersDir = "watchedFolders/";
|
||||||
@Scheduled(fixedRate = 5000)
|
final String finishedFoldersDir = "finishedFolders/";
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 25000)
|
||||||
public void scanFolders() {
|
public void scanFolders() {
|
||||||
|
logger.info("Scanning folders...");
|
||||||
Path watchedFolderPath = Paths.get(watchedFoldersDir);
|
Path watchedFolderPath = Paths.get(watchedFoldersDir);
|
||||||
if (!Files.exists(watchedFolderPath)) {
|
if (!Files.exists(watchedFolderPath)) {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(watchedFolderPath);
|
Files.createDirectories(watchedFolderPath);
|
||||||
|
logger.info("Created directory: {}", watchedFolderPath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("Error creating directory: {}", watchedFolderPath, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
|
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
|
||||||
paths.filter(Files::isDirectory).forEach(t -> {
|
paths.filter(Files::isDirectory).forEach(t -> {
|
||||||
try {
|
try {
|
||||||
|
@ -78,19 +83,21 @@ public class Controller {
|
||||||
handleDirectory(t);
|
handleDirectory(t);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Error handling directory: {}", t, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Error walking through directory: {}", watchedFolderPath, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDirectory(Path dir) throws Exception {
|
private void handleDirectory(Path dir) throws Exception {
|
||||||
|
logger.info("Handling directory: {}", dir);
|
||||||
Path jsonFile = dir.resolve(jsonFileName);
|
Path jsonFile = dir.resolve(jsonFileName);
|
||||||
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
|
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
|
||||||
if (!Files.exists(processingDir)) {
|
if (!Files.exists(processingDir)) {
|
||||||
Files.createDirectory(processingDir);
|
Files.createDirectory(processingDir);
|
||||||
|
logger.info("Created processing directory: {}", processingDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.exists(jsonFile)) {
|
if (Files.exists(jsonFile)) {
|
||||||
|
@ -98,8 +105,9 @@ public class Controller {
|
||||||
String jsonString;
|
String jsonString;
|
||||||
try {
|
try {
|
||||||
jsonString = new String(Files.readAllBytes(jsonFile));
|
jsonString = new String(Files.readAllBytes(jsonFile));
|
||||||
|
logger.info("Read JSON file: {}", jsonFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("Error reading JSON file: {}", jsonFile, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,56 +115,114 @@ public class Controller {
|
||||||
PipelineConfig config;
|
PipelineConfig config;
|
||||||
try {
|
try {
|
||||||
config = objectMapper.readValue(jsonString, PipelineConfig.class);
|
config = objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||||
// Assuming your PipelineConfig class has getters for all necessary fields, you can perform checks here
|
// 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) {
|
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
|
||||||
throw new IOException("Invalid JSON format");
|
throw new IOException("Invalid JSON format");
|
||||||
}
|
}
|
||||||
|
logger.info("Parsed PipelineConfig: {}", config);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("Error parsing PipelineConfig: {}", jsonString, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each operation in the pipeline
|
// For each operation in the pipeline
|
||||||
for (PipelineOperation operation : config.getOperations()) {
|
for (PipelineOperation operation : config.getOperations()) {
|
||||||
|
logger.info("Processing operation: {}", operation.toString());
|
||||||
// Collect all files based on fileInput
|
// Collect all files based on fileInput
|
||||||
File[] files;
|
File[] files;
|
||||||
String fileInput = (String) operation.getParameters().get("fileInput");
|
String fileInput = (String) operation.getParameters().get("fileInput");
|
||||||
if ("automated".equals(fileInput)) {
|
if ("automated".equals(fileInput)) {
|
||||||
// If fileInput is "automated", process all files in the directory
|
// If fileInput is "automated", process all files in the directory
|
||||||
try (Stream<Path> paths = Files.list(dir)) {
|
try (Stream<Path> paths = Files.list(dir)) {
|
||||||
files = paths.filter(path -> !path.equals(jsonFile))
|
files = paths
|
||||||
|
.filter(path -> !Files.isDirectory(path)) // exclude directories
|
||||||
|
.filter(path -> !path.equals(jsonFile)) // exclude jsonFile
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.toArray(File[]::new);
|
.toArray(File[]::new);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If fileInput contains a path, process only this file
|
// If fileInput contains a path, process only this file
|
||||||
files = new File[]{new File(fileInput)};
|
files = new File[] { new File(fileInput) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the files for processing
|
// Prepare the files for processing
|
||||||
File[] filesToProcess = files.clone();
|
List<File> filesToProcess = new ArrayList<>();
|
||||||
for (File file : filesToProcess) {
|
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()));
|
Files.move(file.toPath(), processingDir.resolve(file.getName()));
|
||||||
|
filesToProcess.add(processingDir.resolve(file.getName()).toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the files
|
// Process the files
|
||||||
try {
|
try {
|
||||||
List<Resource> resources = handleFiles(filesToProcess, jsonString);
|
List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
|
||||||
|
|
||||||
|
if(resources == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Move resultant files and rename them as per config in JSON file
|
// Move resultant files and rename them as per config in JSON file
|
||||||
for (Resource resource : resources) {
|
for (Resource resource : resources) {
|
||||||
String outputFileName = config.getOutputPattern().replace("{filename}", resource.getFile().getName());
|
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());
|
outputFileName = outputFileName.replace("{pipelineName}", config.getName());
|
||||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
|
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||||
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
|
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
|
||||||
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
|
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
|
||||||
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
|
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
|
||||||
// {filename} {folder} {date} {tmime} {pipeline}
|
|
||||||
|
|
||||||
Files.move(resource.getFile().toPath(), Paths.get(config.getOutputDir(), outputFileName));
|
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
|
// If successful, delete the original files
|
||||||
|
@ -174,10 +240,9 @@ public class Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception {
|
||||||
|
|
||||||
|
logger.info("Processing files... " + outputFiles);
|
||||||
|
|
||||||
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception{
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
JsonNode jsonNode = mapper.readTree(jsonString);
|
JsonNode jsonNode = mapper.readTree(jsonString);
|
||||||
|
|
||||||
|
@ -189,12 +254,13 @@ List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throw
|
||||||
|
|
||||||
for (JsonNode operationNode : pipelineNode) {
|
for (JsonNode operationNode : pipelineNode) {
|
||||||
String operation = operationNode.get("operation").asText();
|
String operation = operationNode.get("operation").asText();
|
||||||
|
logger.info("Running operation: {}", operation);
|
||||||
JsonNode parametersNode = operationNode.get("parameters");
|
JsonNode parametersNode = operationNode.get("parameters");
|
||||||
String inputFileExtension = "";
|
String inputFileExtension = "";
|
||||||
if(operationNode.has("inputFileType")) {
|
if (operationNode.has("inputFileType")) {
|
||||||
inputFileExtension = operationNode.get("inputFileType").asText();
|
inputFileExtension = operationNode.get("inputFileType").asText();
|
||||||
} else {
|
} else {
|
||||||
inputFileExtension=".pdf";
|
inputFileExtension = ".pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Resource> newOutputFiles = new ArrayList<>();
|
List<Resource> newOutputFiles = new ArrayList<>();
|
||||||
|
@ -244,7 +310,8 @@ List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throw
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasInputFileType) {
|
if (!hasInputFileType) {
|
||||||
logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation);
|
logPrintStream.println(
|
||||||
|
"No files with extension " + inputFileExtension + " found for operation " + operation);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,23 +320,33 @@ List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throw
|
||||||
logPrintStream.close();
|
logPrintStream.close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (hasErrors) {
|
||||||
|
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
|
||||||
|
}
|
||||||
return outputFiles;
|
return outputFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Resource> 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());
|
||||||
|
|
||||||
List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
JsonNode jsonNode = mapper.readTree(jsonString);
|
JsonNode jsonNode = mapper.readTree(jsonString);
|
||||||
|
|
||||||
JsonNode pipelineNode = jsonNode.get("pipeline");
|
JsonNode pipelineNode = jsonNode.get("pipeline");
|
||||||
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
|
||||||
PrintStream logPrintStream = new PrintStream(logStream);
|
|
||||||
|
|
||||||
boolean hasErrors = false;
|
boolean hasErrors = false;
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
Path path = Paths.get(file.getAbsolutePath());
|
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)) {
|
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
|
||||||
@Override
|
@Override
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
|
@ -277,17 +354,24 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
outputFiles.add(fileResource);
|
outputFiles.add(fileResource);
|
||||||
|
} else {
|
||||||
|
System.out.println("File not found: " + path); // debug statement
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
logger.info("Files successfully loaded. Starting processing...");
|
||||||
return processFiles(outputFiles, jsonString);
|
return processFiles(outputFiles, jsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception{
|
List<Resource> 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();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
JsonNode jsonNode = mapper.readTree(jsonString);
|
JsonNode jsonNode = mapper.readTree(jsonString);
|
||||||
|
|
||||||
JsonNode pipelineNode = jsonNode.get("pipeline");
|
JsonNode pipelineNode = jsonNode.get("pipeline");
|
||||||
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
|
||||||
PrintStream logPrintStream = new PrintStream(logStream);
|
|
||||||
|
|
||||||
boolean hasErrors = false;
|
boolean hasErrors = false;
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
|
@ -301,25 +385,30 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
};
|
};
|
||||||
outputFiles.add(fileResource);
|
outputFiles.add(fileResource);
|
||||||
}
|
}
|
||||||
|
logger.info("Files successfully loaded. Starting processing...");
|
||||||
return processFiles(outputFiles, jsonString);
|
return processFiles(outputFiles, jsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/handleData")
|
@PostMapping("/handleData")
|
||||||
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
|
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
|
||||||
@RequestParam("json") String jsonString) {
|
@RequestParam("json") String jsonString) {
|
||||||
|
logger.info("Received POST request to /handleData with {} files", files.length);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
List<Resource> outputFiles = handleFiles(files, jsonString);
|
List<Resource> outputFiles = handleFiles(files, jsonString);
|
||||||
|
|
||||||
if (outputFiles.size() == 1) {
|
if (outputFiles != null && outputFiles.size() == 1) {
|
||||||
// If there is only one file, return it directly
|
// If there is only one file, return it directly
|
||||||
Resource singleFile = outputFiles.get(0);
|
Resource singleFile = outputFiles.get(0);
|
||||||
InputStream is = singleFile.getInputStream();
|
InputStream is = singleFile.getInputStream();
|
||||||
byte[] bytes = new byte[(int)singleFile.contentLength()];
|
byte[] bytes = new byte[(int) singleFile.contentLength()];
|
||||||
is.read(bytes);
|
is.read(bytes);
|
||||||
is.close();
|
is.close();
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
|
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
|
// Create a ByteArrayOutputStream to hold the zip
|
||||||
|
@ -333,7 +422,7 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
|
|
||||||
// Read the file into a byte array
|
// Read the file into a byte array
|
||||||
InputStream is = file.getInputStream();
|
InputStream is = file.getInputStream();
|
||||||
byte[] bytes = new byte[(int)file.contentLength()];
|
byte[] bytes = new byte[(int) file.contentLength()];
|
||||||
is.read(bytes);
|
is.read(bytes);
|
||||||
|
|
||||||
// Write the bytes of the file to the zip
|
// Write the bytes of the file to the zip
|
||||||
|
@ -345,9 +434,10 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
|
|
||||||
zipOut.close();
|
zipOut.close();
|
||||||
|
|
||||||
|
logger.info("Returning zipped file response...");
|
||||||
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Error handling data: ", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,6 +452,7 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Resource> unzip(byte[] data) throws IOException {
|
private List<Resource> unzip(byte[] data) throws IOException {
|
||||||
|
logger.info("Unzipping data of length: {}", data.length);
|
||||||
List<Resource> unzippedFiles = new ArrayList<>();
|
List<Resource> unzippedFiles = new ArrayList<>();
|
||||||
|
|
||||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||||
|
@ -387,6 +478,7 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
|
|
||||||
// If the unzipped file is a zip file, unzip it
|
// If the unzipped file is a zip file, unzip it
|
||||||
if (isZip(baos.toByteArray())) {
|
if (isZip(baos.toByteArray())) {
|
||||||
|
logger.info("File {} is a zip file. Unzipping...", filename);
|
||||||
unzippedFiles.addAll(unzip(baos.toByteArray()));
|
unzippedFiles.addAll(unzip(baos.toByteArray()));
|
||||||
} else {
|
} else {
|
||||||
unzippedFiles.add(fileResource);
|
unzippedFiles.add(fileResource);
|
||||||
|
@ -394,6 +486,8 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
||||||
return unzippedFiles;
|
return unzippedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,4 +68,10 @@ public class GeneralWebController {
|
||||||
return "sign";
|
return "sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/crop")
|
||||||
|
@Hidden
|
||||||
|
public String cropForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "crop");
|
||||||
|
return "crop";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,4 +22,11 @@ public class PipelineOperation {
|
||||||
public void setParameters(Map<String, Object> parameters) {
|
public void setParameters(Map<String, Object> parameters) {
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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.title=Adjust Colors/Contrast
|
||||||
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
|
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
|
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
|
||||||
|
|
||||||
downloadPdf=Download PDF
|
downloadPdf=Download PDF
|
||||||
|
|
3
src/main/resources/static/images/crop.svg
Normal file
3
src/main/resources/static/images/crop.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-crop" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5.5A.5.5 0 0 1 4 1v13h13a.5.5 0 0 1 0 1h-2v2a.5.5 0 0 1-1 0v-2H3.5a.5.5 0 0 1-.5-.5V4H1a.5.5 0 0 1 0-1h2V1a.5.5 0 0 1 .5-.5zm2.5 3a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4H6.5a.5.5 0 0 1-.5-.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 353 B |
105
src/main/resources/templates/crop.html
Normal file
105
src/main/resources/templates/crop.html
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<!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=#{crop.title})}"></th:block>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<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-6">
|
||||||
|
<h2 th:text="#{crop.header}"></h2>
|
||||||
|
<form id="cropForm" action="/crop" method="post" enctype="multipart/form-data">
|
||||||
|
<input id="fileInput" type="file" name="file" required>
|
||||||
|
<input id="x" type="hidden" name="x">
|
||||||
|
<input id="y" type="hidden" name="y">
|
||||||
|
<input id="width" type="hidden" name="width">
|
||||||
|
<input id="height" type="hidden" name="height">
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let canvas = document.getElementById('pdf-canvas');
|
||||||
|
let context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
let cropForm = document.getElementById('cropForm');
|
||||||
|
let fileInput = document.getElementById('fileInput');
|
||||||
|
let xInput = document.getElementById('x');
|
||||||
|
let yInput = document.getElementById('y');
|
||||||
|
let widthInput = document.getElementById('width');
|
||||||
|
let heightInput = document.getElementById('height');
|
||||||
|
|
||||||
|
let pdfDoc = null;
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 0;
|
||||||
|
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let rectWidth = 0;
|
||||||
|
let rectHeight = 0;
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', function(e) {
|
||||||
|
let file = e.target.files[0];
|
||||||
|
if (file.type === 'application/pdf') {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = function(ev) {
|
||||||
|
let typedArray = new Uint8Array(reader.result);
|
||||||
|
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
|
||||||
|
pdfDoc = pdf;
|
||||||
|
totalPages = pdf.numPages;
|
||||||
|
renderPage(currentPage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', function(e) {
|
||||||
|
startX = e.offsetX;
|
||||||
|
startY = e.offsetY;
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseup', function(e) {
|
||||||
|
rectWidth = e.offsetX - startX;
|
||||||
|
rectHeight = e.offsetY - startY;
|
||||||
|
|
||||||
|
// Flip the y-coordinate
|
||||||
|
let flippedY = canvas.height - e.offsetY;
|
||||||
|
|
||||||
|
xInput.value = startX;
|
||||||
|
yInput.value = flippedY;
|
||||||
|
widthInput.value = rectWidth;
|
||||||
|
heightInput.value = rectHeight;
|
||||||
|
|
||||||
|
context.strokeStyle = 'red';
|
||||||
|
context.strokeRect(startX, startY, rectWidth, rectHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function renderPage(pageNumber) {
|
||||||
|
pdfDoc.getPage(pageNumber).then(function(page) {
|
||||||
|
let viewport = page.getViewport({ scale: 1.0 });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
let renderContext = { canvasContext: context, viewport: viewport };
|
||||||
|
page.render(renderContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -40,7 +40,7 @@
|
||||||
</li>-->
|
</li>-->
|
||||||
|
|
||||||
<li class="nav-item nav-item-separator"></li>
|
<li class="nav-item nav-item-separator"></li>
|
||||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' ? 'active' : ''">
|
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' ? 'active' : ''">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
|
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
|
||||||
<span class="icon-text" th:text="#{navbar.pageOps}"></span>
|
<span class="icon-text" th:text="#{navbar.pageOps}"></span>
|
||||||
|
@ -98,13 +98,14 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item nav-item-separator"></li>
|
<li class="nav-item nav-item-separator"></li>
|
||||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' ? 'active' : ''">
|
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='crop' ? 'active' : ''">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
|
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
|
||||||
<span class="icon-text" th:text="#{navbar.other}"></span>
|
<span class="icon-text" th:text="#{navbar.other}"></span>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc')}"></div>
|
||||||
|
@ -118,6 +119,8 @@
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-page-numbers', 'images/add-page-numbers.svg', 'home.add-page-numbers.title', 'home.add-page-numbers.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-page-numbers', 'images/add-page-numbers.svg', 'home.add-page-numbers.title', 'home.add-page-numbers.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc')}"></div>
|
||||||
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc')}"></div>
|
||||||
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc')}"></div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
<div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg')}"></div>
|
<div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg')}"></div>
|
||||||
<div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg')}"></div>
|
<div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg')}"></div>
|
||||||
<div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg')}"></div>
|
<div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg')}"></div>
|
||||||
|
<div th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.svg')}"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,26 +17,100 @@
|
||||||
<form method="post" enctype="multipart/form-data" th:action="@{add-page-numbers}">
|
<form method="post" enctype="multipart/form-data" th:action="@{add-page-numbers}">
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||||
<br>
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
<label for="customMargin">Margin Size</label>
|
<label for="customMargin">Margin Size</label>
|
||||||
<select id="customMargin" name="customMargin" required>
|
<select class="form-control" id="customMargin" name="customMargin" required>
|
||||||
<option value="" disabled selected>Select a margin size</option>
|
<option value="" disabled selected>Select a margin size</option>
|
||||||
<option value="small">Small</option>
|
<option value="small">Small</option>
|
||||||
<option value="medium">Medium</option>
|
<option value="medium">Medium</option>
|
||||||
<option value="large">Large</option>
|
<option value="large">Large</option>
|
||||||
|
<option value="x-large">X-Large</option>
|
||||||
</select>
|
</select>
|
||||||
<br>
|
</div>
|
||||||
|
<style>
|
||||||
|
.a4container {
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 0; /* No gap between the cells */
|
||||||
|
width: 50%;
|
||||||
|
aspect-ratio: 0.707; /* this sets the width-height ratio approximately same as A4 paper */
|
||||||
|
justify-items: stretch; /* Stretch items to fill their cells */
|
||||||
|
align-items: stretch; /* Stretch items to fill their cells */
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #ccc;
|
||||||
|
border: 1px solid #fff; /* Add a border to each cell */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myForm {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
<label for="position">Position</label>
|
<label for="position">Position</label>
|
||||||
<input type="number" id="position" name="position" placeholder="Position: 1 of 9 positions" min="1" max="9" required/>
|
<div class="a4container">
|
||||||
<br>
|
<div id="1" class="cell">1</div>
|
||||||
|
<div id="2" class="cell">2</div>
|
||||||
|
<div id="3" class="cell">3</div>
|
||||||
|
<div id="4" class="cell">4</div>
|
||||||
|
<div id="5" class="cell">5</div>
|
||||||
|
<div id="6" class="cell">6</div>
|
||||||
|
<div id="7" class="cell">7</div>
|
||||||
|
<div id="8" class="cell">8</div>
|
||||||
|
<div id="9" class="cell">9</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<form id="myForm">
|
||||||
|
<input type="number" id="numberInput" name="number" min="1" max="9" required>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let cells = document.querySelectorAll('.cell');
|
||||||
|
let inputField = document.getElementById('numberInput');
|
||||||
|
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.addEventListener('click', function(e) {
|
||||||
|
let selectedLocation = e.target.id;
|
||||||
|
inputField.value = selectedLocation; // Set input field's value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
<label for="startingNumber">Starting Number</label>
|
<label for="startingNumber">Starting Number</label>
|
||||||
<input type="number" id="startingNumber" name="startingNumber" placeholder="Starting number" min="1" required/>
|
<input type="number" class="form-control" id="startingNumber" name="startingNumber" min="1" required value="1"/>
|
||||||
<br>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<label for="pagesToNumber">Pages to Number</label>
|
<label for="pagesToNumber">Pages to Number</label>
|
||||||
<input type="text" id="pagesToNumber" name="pagesToNumber" placeholder="Which pages to number, default all"/>
|
<input type="text" class="form-control" id="pagesToNumber" name="pagesToNumber" placeholder="Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc" />
|
||||||
<br>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<label for="customText">Custom Text</label>
|
<label for="customText">Custom Text</label>
|
||||||
<input type="text" id="customText" name="customText" placeholder="Custom text: defaults to just number but can have things like 'Page {n} of {p}'"/>
|
<input type="text" class="form-control" id="customText" name="customText" placeholder="Default just number, also accepts 'Page {n} of {total}', 'Tag-{n}' etc"/>
|
||||||
<br>
|
</div>
|
||||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPageNumbers.submit}"></button>
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPageNumbers.submit}"></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
|
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
|
||||||
<select class="form-control" name="alphabet" id="alphabet-select">
|
<select class="form-control" name="alphabet" id="alphabet-select">
|
||||||
<option value="romain">Roman</option>
|
<option value="roman">Roman</option>
|
||||||
<option value="arabic">العربية</option>
|
<option value="arabic">العربية</option>
|
||||||
<option value="japanese">日本語</option>
|
<option value="japanese">日本語</option>
|
||||||
<option value="korean">한국어</option>
|
<option value="korean">한국어</option>
|
||||||
|
|
Loading…
Reference in a new issue