From cdbf1fa73a8fe73730d535197bfa7c2fdda16090 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 12 Jul 2023 23:27:36 +0100 Subject: [PATCH] watermark features --- .../api/security/WatermarkController.java | 224 +++++++++--------- .../templates/security/add-watermark.html | 62 +++-- 2 files changed, 162 insertions(+), 124 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index 4ef8604b..962f578e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -1,12 +1,15 @@ package stirling.software.SPDF.controller.api.security; import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import java.util.List; + +import javax.imageio.ImageIO; import org.apache.commons.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; @@ -15,6 +18,8 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.util.Matrix; import org.springframework.core.io.ClassPathResource; @@ -30,124 +35,127 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; import io.swagger.v3.oas.annotations.media.Schema; + @RestController @Tag(name = "Security", description = "Security APIs") public class WatermarkController { - @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") - @Operation(summary = "Add watermark to a PDF file", - description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark text, font size, rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") - public ResponseEntity<byte[]> addWatermark( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF file to add a watermark") - MultipartFile pdfFile, - @RequestParam(defaultValue = "roman", name = "alphabet") - @Parameter(description = "The selected alphabet", - schema = @Schema(type = "string", - allowableValues = {"roman","arabic","japanese","korean","chinese"}, - defaultValue = "roman")) - String alphabet, - @RequestParam("watermarkText") - @Parameter(description = "The watermark text to add to the PDF file") - String watermarkText, - @RequestParam(defaultValue = "30", name = "fontSize") - @Parameter(description = "The font size of the watermark text", example = "30") - float fontSize, - @RequestParam(defaultValue = "0", name = "rotation") - @Parameter(description = "The rotation of the watermark text in degrees", example = "0") - float rotation, - @RequestParam(defaultValue = "0.5", name = "opacity") - @Parameter(description = "The opacity of the watermark text (0.0 - 1.0)", example = "0.5") - float opacity, - @RequestParam(defaultValue = "50", name = "widthSpacer") - @Parameter(description = "The width spacer between watermark texts", example = "50") - int widthSpacer, - @RequestParam(defaultValue = "50", name = "heightSpacer") - @Parameter(description = "The height spacer between watermark texts", example = "50") - int heightSpacer) throws IOException, Exception { + @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") + @Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") + public ResponseEntity<byte[]> addWatermark( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to add a watermark") MultipartFile pdfFile, + @RequestPart(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType, + @RequestPart(required = false) @Parameter(description = "The watermark text") String watermarkText, + @RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage, + @RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize, + @RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation, + @RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity, + @RequestParam(defaultValue = "50", name = "widthSpacer") @Parameter(description = "The width spacer between watermark elements", example = "50") int widthSpacer, + @RequestParam(defaultValue = "50", name = "heightSpacer") @Parameter(description = "The height spacer between watermark elements", example = "50") int heightSpacer) + throws IOException, Exception { - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); - String producer = document.getDocumentInformation().getProducer(); - // Create a page in the document - for (PDPage page : document.getPages()) { + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); - // Get the page's content stream - PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); + // Create a page in the document + for (PDPage page : document.getPages()) { - // Set transparency - PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); - graphicsState.setNonStrokingAlphaConstant(opacity); - contentStream.setGraphicsStateParameters(graphicsState); + // Get the page's content stream + PDPageContentStream contentStream = new PDPageContentStream(document, page, + PDPageContentStream.AppendMode.APPEND, true); + // Set transparency + PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); + graphicsState.setNonStrokingAlphaConstant(opacity); + contentStream.setGraphicsStateParameters(graphicsState); - String resourceDir = ""; - PDFont font = PDType1Font.HELVETICA_BOLD; - switch (alphabet) { - case "arabic": - resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; - break; - case "japanese": - resourceDir = "static/fonts/Meiryo.ttf"; - break; - case "korean": - resourceDir = "static/fonts/malgun.ttf"; - break; - case "chinese": - resourceDir = "static/fonts/SimSun.ttf"; - break; - case "roman": - default: - resourceDir = "static/fonts/NotoSans-Regular.ttf"; - break; - } + if (watermarkType.equalsIgnoreCase("text")) { + addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer, + fontSize); + } else if (watermarkType.equalsIgnoreCase("image")) { + addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer, + fontSize); + } - - if(!resourceDir.equals("")) { - ClassPathResource classPathResource = new ClassPathResource(resourceDir); - String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); - File tempFile = File.createTempFile("NotoSansFont", fileExtension); - try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { - IOUtils.copy(is, os); - } - - font = PDType0Font.load(document, tempFile); - tempFile.deleteOnExit(); - } - contentStream.beginText(); - contentStream.setFont(font, fontSize); - contentStream.setNonStrokingColor(Color.LIGHT_GRAY); + // Close the content stream + contentStream.close(); + } - // Set size and location of watermark - float pageWidth = page.getMediaBox().getWidth(); - float pageHeight = page.getMediaBox().getHeight(); - float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; - float watermarkHeight = heightSpacer + fontSize; - int watermarkRows = (int) (pageHeight / watermarkHeight + 1); - int watermarkCols = (int) (pageWidth / watermarkWidth + 1); + return WebResponseUtils.pdfDocToWebResponse(document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); + } - // Add the watermark text - for (int i = 0; i < watermarkRows; i++) { - for (int j = 0; j < watermarkCols; j++) { - - if(producer.contains("Google Docs")) { - //This fixes weird unknown google docs y axis rotation/flip issue - //TODO: Long term fix one day - //contentStream.setTextMatrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight); - Matrix matrix = new Matrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight); - contentStream.setTextMatrix(matrix); - } else { - contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight)); - } - contentStream.showTextWithPositioning(new Object[] { watermarkText }); - } - } - contentStream.endText(); + private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document, + PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize) throws IOException { + // Set font and other properties for text watermark + PDFont font = PDType1Font.HELVETICA_BOLD; + contentStream.setFont(font, fontSize); + contentStream.setNonStrokingColor(Color.LIGHT_GRAY); - // Close the content stream - contentStream.close(); - } - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); - } + // Set size and location of text watermark + float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; + float watermarkHeight = heightSpacer + fontSize; + float pageWidth = page.getMediaBox().getWidth(); + float pageHeight = page.getMediaBox().getHeight(); + int watermarkRows = (int) (pageHeight / watermarkHeight + 1); + int watermarkCols = (int) (pageWidth / watermarkWidth + 1); + + // Add the text watermark + for (int i = 0; i < watermarkRows; i++) { + for (int j = 0; j < watermarkCols; j++) { + contentStream.beginText(); + contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), + j * watermarkWidth, i * watermarkHeight)); + contentStream.showText(watermarkText); + contentStream.endText(); + } + } + } + + private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation, + int widthSpacer, int heightSpacer, float fontSize) throws IOException { + +// Load the watermark image +BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); + +// Compute width based on original aspect ratio +float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); + +// Desired physical height (in PDF points) +float desiredPhysicalHeight = fontSize ; + +// Desired physical width based on the aspect ratio +float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; + +// Convert the BufferedImage to PDImageXObject +PDImageXObject xobject = LosslessFactory.createFromImage(document, image); + +// Calculate the number of rows and columns for watermarks +float pageWidth = page.getMediaBox().getWidth(); +float pageHeight = page.getMediaBox().getHeight(); +int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer)); +int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer)); + +for (int i = 0; i < watermarkRows; i++) { +for (int j = 0; j < watermarkCols; j++) { +float x = j * (desiredPhysicalWidth + widthSpacer); +float y = i * (desiredPhysicalHeight + heightSpacer); + +// Save the graphics state +contentStream.saveGraphicsState(); + +// Create rotation matrix and rotate +contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2)); +contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); +contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2)); + +// Draw the image and restore the graphics state +contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight); +contentStream.restoreGraphicsState(); +} + +} + + } } diff --git a/src/main/resources/templates/security/add-watermark.html b/src/main/resources/templates/security/add-watermark.html index 530388c9..060805fa 100644 --- a/src/main/resources/templates/security/add-watermark.html +++ b/src/main/resources/templates/security/add-watermark.html @@ -3,7 +3,7 @@ <th:block th:insert="~{fragments/common :: head(title=#{watermark.title})}"></th:block> -<body> +<body onload="toggleFileOption()"> <div id="page-container"> <div id="content-wrap"> <div th:insert="~{fragments/navbar.html :: navbar}"></div> @@ -16,30 +16,36 @@ <form method="post" enctype="multipart/form-data" action="add-watermark"> <div class="form-group"> <label th:text="#{watermark.selectText.1}"></label> - <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')}"> + <input type="file" id="fileInput" name="fileInput" class="form-control-file" accept="application/pdf" required /> + </div> </div> - + <div class="form-group"> - <label for="fontSize" th:text="#{alphabet} + ':'"></label> - <select class="form-control" name="alphabet" id="alphabet-select"> - <option value="roman">Roman</option> - <option value="arabic">العربية</option> - <option value="japanese">日本語</option> - <option value="korean">한국어</option> - <option value="chinese">简体中文</option> - </select> + <label th:text="#{watermark.selectText.8}"></label> + <select class="form-control" id="watermarkType" name="watermarkType" onchange="toggleFileOption()"> + <option value="text">Text</option> + <option value="image">Image</option> + </select> </div> - <div class="form-group"> + + <div id="watermarkTextGroup" class="form-group"> <label for="watermarkText" th:text="#{watermark.selectText.2}"></label> <input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required /> </div> + + <div id="watermarkImageGroup" class="form-group" style="display: none;"> + <label for="watermarkImage" th:text="#{watermark.selectText.9}"></label> + <input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*" /> + </div> + <div class="form-group"> <label for="fontSize" th:text="#{watermark.selectText.3}"></label> <input type="text" id="fontSize" name="fontSize" class="form-control" value="30" /> </div> <div class="form-group"> <label for="opacity" th:text="#{watermark.selectText.7}"></label> - <input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateopacityValue()" /> + <input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()" /> <input type="hidden" id="opacityReal" name="opacity" value="0.5"> </div> @@ -48,7 +54,7 @@ const opacityInput = document.getElementById('opacity'); const opacityRealInput = document.getElementById('opacityReal'); - const updateopacityValue = () => { + const updateOpacityValue = () => { let percentageValue = parseFloat(opacityInput.value.replace('%', '')); if (isNaN(percentageValue)) { percentageValue = 0; @@ -68,14 +74,15 @@ opacityInput.value = opacityInput.value.replace('%', ''); }); opacityInput.addEventListener('blur', () => { - updateopacityValue(); + updateOpacityValue(); appendPercentageSymbol(); }); // Set initial values - updateopacityValue(); + updateOpacityValue(); appendPercentageSymbol(); </script> + <div class="form-group"> <label for="rotation" th:text="#{watermark.selectText.4}"></label> <input type="text" id="rotation" name="rotation" class="form-control" value="45" /> @@ -92,6 +99,29 @@ <input type="submit" id="submitBtn" th:value="#{watermark.submit}" class="btn btn-primary" /> </div> </form> + + <script> + function toggleFileOption() { + const watermarkType = document.getElementById('watermarkType').value; + const watermarkTextGroup = document.getElementById('watermarkTextGroup'); + const watermarkImageGroup = document.getElementById('watermarkImageGroup'); + const watermarkText = document.getElementById('watermarkText'); + const watermarkImage = document.getElementById('watermarkImage'); + + if (watermarkType === 'text') { + watermarkTextGroup.style.display = 'block'; + watermarkText.required = true; + watermarkImageGroup.style.display = 'none'; + watermarkImage.required = false; + } else if (watermarkType === 'image') { + watermarkTextGroup.style.display = 'none'; + watermarkText.required = false; + watermarkImageGroup.style.display = 'block'; + watermarkImage.required = true; + } + } + </script> + </div> </div> </div>