mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-10-25 11:17:28 +02:00 
			
		
		
		
	watermark features
This commit is contained in:
		
							parent
							
								
									5d926b022b
								
							
						
					
					
						commit
						cdbf1fa73a
					
				| @ -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(); | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user