watermark features
This commit is contained in:
parent
5d926b022b
commit
cdbf1fa73a
2 changed files with 162 additions and 124 deletions
|
@ -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 a new issue