lots of stuff and garbage code for automate to cleanup lots

This commit is contained in:
Anthony Stirling 2023-07-09 00:05:33 +01:00
parent 5877b40be5
commit 6e726ac2a6
13 changed files with 699 additions and 272 deletions

View file

@ -15,7 +15,7 @@ import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
//@EnableScheduling
@EnableScheduling
public class SPdfApplication {
@Autowired

View file

@ -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");
}
}

View file

@ -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);

View file

@ -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,29 +51,31 @@ 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);
logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) {
e.printStackTrace();
logger.error("Error creating directory: {}", watchedFolderPath, e);
return;
}
}
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> {
try {
@ -78,19 +83,21 @@ public class Controller {
handleDirectory(t);
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Error handling directory: {}", t, e);
}
});
} catch (Exception e) {
e.printStackTrace();
logger.error("Error walking through directory: {}", watchedFolderPath, e);
}
}
private void handleDirectory(Path dir) throws Exception {
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);
}
if (Files.exists(jsonFile)) {
@ -98,8 +105,9 @@ public class Controller {
String jsonString;
try {
jsonString = new String(Files.readAllBytes(jsonFile));
logger.info("Read JSON file: {}", jsonFile);
} catch (IOException e) {
e.printStackTrace();
logger.error("Error reading JSON file: {}", jsonFile, e);
return;
}
@ -107,26 +115,32 @@ public class Controller {
PipelineConfig config;
try {
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) {
throw new IOException("Invalid JSON format");
}
logger.info("Parsed PipelineConfig: {}", config);
} catch (IOException e) {
e.printStackTrace();
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<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)
.toArray(File[]::new);
} catch (IOException e) {
e.printStackTrace();
return;
@ -137,26 +151,78 @@ public class Controller {
}
// Prepare the files for processing
File[] filesToProcess = files.clone();
for (File file : filesToProcess) {
List<File> 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<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
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());
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));
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
@ -174,10 +240,9 @@ public class Controller {
}
}
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception {
logger.info("Processing files... " + outputFiles);
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
@ -189,6 +254,7 @@ List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throw
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")) {
@ -244,7 +310,8 @@ List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throw
}
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;
}
@ -253,23 +320,33 @@ List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throw
logPrintStream.close();
}
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
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());
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<Resource> 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() {
@ -277,17 +354,24 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
}
};
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<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();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
@ -301,17 +385,18 @@ List<Resource> 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<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) {
logger.info("Received POST request to /handleData with {} files", files.length);
try {
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
Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream();
@ -319,7 +404,11 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
is.read(bytes);
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
@ -345,9 +434,10 @@ List<Resource> handleFiles(File[] files, String jsonString) throws Exception{
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<Resource> handleFiles(File[] files, String jsonString) throws Exception{
}
private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>();
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 (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<Resource> handleFiles(File[] files, String jsonString) throws Exception{
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
}

View file

@ -68,4 +68,10 @@ public class GeneralWebController {
return "sign";
}
@GetMapping("/crop")
@Hidden
public String cropForm(Model model) {
model.addAttribute("currentPage", "crop");
return "crop";
}
}

View file

@ -22,4 +22,11 @@ public class PipelineOperation {
public void setParameters(Map<String, Object> parameters) {
this.parameters = parameters;
}
@Override
public String toString() {
return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
}
}

View file

@ -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

View 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

View 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>

View file

@ -40,7 +40,7 @@
</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">
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
<span class="icon-text" th:text="#{navbar.pageOps}"></span>
@ -98,13 +98,14 @@
</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">
<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>
</a>
<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 ('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>
@ -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 ('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 ('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>
</li>

View file

@ -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='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='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.svg')}"></div>

View file

@ -17,26 +17,100 @@
<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>
<br>
<div class="form-group">
<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="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
<option value="x-large">X-Large</option>
</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>
<input type="number" id="position" name="position" placeholder="Position: 1 of 9 positions" min="1" max="9" required/>
<br>
<div class="a4container">
<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>
<input type="number" id="startingNumber" name="startingNumber" placeholder="Starting number" min="1" required/>
<br>
<input type="number" class="form-control" id="startingNumber" name="startingNumber" min="1" required value="1"/>
</div>
<div class="form-group">
<label for="pagesToNumber">Pages to Number</label>
<input type="text" id="pagesToNumber" name="pagesToNumber" placeholder="Which pages to number, default all"/>
<br>
<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" />
</div>
<div class="form-group">
<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}'"/>
<br>
<input type="text" class="form-control" id="customText" name="customText" placeholder="Default just number, also accepts 'Page {n} of {total}', 'Tag-{n}' etc"/>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPageNumbers.submit}"></button>
</form>

View file

@ -22,7 +22,7 @@
<div class="form-group">
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
<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="japanese">日本語</option>
<option value="korean">한국어</option>