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>