mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Merge b2d7f4e827 into be824b126f
This commit is contained in:
commit
481e7b414d
@ -14,6 +14,8 @@ public final class RegexPatternUtils {
|
||||
|
||||
private static final String WHITESPACE_REGEX = "\\s++";
|
||||
private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$";
|
||||
private static final Pattern COLOR_PATTERN =
|
||||
Pattern.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$");
|
||||
|
||||
private RegexPatternUtils() {
|
||||
super();
|
||||
@ -310,6 +312,10 @@ public final class RegexPatternUtils {
|
||||
return EXTENSION_REGEX;
|
||||
}
|
||||
|
||||
public static Pattern getColorPattern() {
|
||||
return COLOR_PATTERN;
|
||||
}
|
||||
|
||||
/** Pattern for extracting non-numeric characters */
|
||||
public Pattern getNumericExtractionPattern() {
|
||||
return getPattern("\\D");
|
||||
|
||||
@ -0,0 +1,359 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Utility class for generating randomized watermark attributes with deterministic (seedable)
|
||||
* randomness. Supports position, rotation, mirroring, font selection, size, color, and shading.
|
||||
*/
|
||||
public class WatermarkRandomizer {
|
||||
|
||||
public static final Color[] PALETTE =
|
||||
new Color[] {
|
||||
Color.BLACK,
|
||||
Color.DARK_GRAY,
|
||||
Color.GRAY,
|
||||
Color.LIGHT_GRAY,
|
||||
Color.RED,
|
||||
Color.BLUE,
|
||||
Color.GREEN,
|
||||
Color.ORANGE,
|
||||
Color.MAGENTA,
|
||||
Color.CYAN,
|
||||
Color.PINK,
|
||||
Color.YELLOW
|
||||
};
|
||||
|
||||
private final Random random;
|
||||
|
||||
/**
|
||||
* Creates a WatermarkRandomizer with an optional seed for deterministic randomness.
|
||||
*
|
||||
* @param seed Optional seed value; if null, uses non-deterministic randomness
|
||||
*/
|
||||
public WatermarkRandomizer(Long seed) {
|
||||
this.random = (seed != null) ? new Random(seed) : new Random();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random position within the page.
|
||||
*
|
||||
* @param pageWidth Width of the page
|
||||
* @param pageHeight Height of the page
|
||||
* @param watermarkWidth Width of the watermark
|
||||
* @param watermarkHeight Height of the watermark
|
||||
* @return Array with [x, y] coordinates
|
||||
*/
|
||||
public float[] generateRandomPosition(
|
||||
float pageWidth, float pageHeight, float watermarkWidth, float watermarkHeight) {
|
||||
|
||||
// Calculate available space
|
||||
float maxX = Math.max(0, pageWidth - watermarkWidth);
|
||||
float maxY = Math.max(0, pageHeight - watermarkHeight);
|
||||
|
||||
// Generate random position within page
|
||||
float x = random.nextFloat() * maxX;
|
||||
float y = random.nextFloat() * maxY;
|
||||
|
||||
return new float[] {x, y};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates multiple random positions with collision detection to ensure minimum spacing.
|
||||
*
|
||||
* <p>This method uses collision detection to ensure that each watermark maintains minimum
|
||||
* separation from all previously placed watermarks. If a valid position cannot be found after
|
||||
* multiple attempts, the method will still return the requested count but some positions may
|
||||
* not satisfy spacing constraints.
|
||||
*
|
||||
* @param pageWidth Width of the page
|
||||
* @param pageHeight Height of the page
|
||||
* @param watermarkWidth Width of the watermark
|
||||
* @param watermarkHeight Height of the watermark
|
||||
* @param widthSpacer Horizontal spacing between watermarks (minimum separation)
|
||||
* @param heightSpacer Vertical spacing between watermarks (minimum separation)
|
||||
* @param count Number of positions to generate
|
||||
* @return List of [x, y] coordinate arrays
|
||||
*/
|
||||
public List<float[]> generateRandomPositions(
|
||||
float pageWidth,
|
||||
float pageHeight,
|
||||
float watermarkWidth,
|
||||
float watermarkHeight,
|
||||
int widthSpacer,
|
||||
int heightSpacer,
|
||||
int count) {
|
||||
|
||||
List<float[]> positions = new ArrayList<>();
|
||||
float maxX = Math.max(0, pageWidth - watermarkWidth);
|
||||
float maxY = Math.max(0, pageHeight - watermarkHeight);
|
||||
|
||||
// Prevent infinite loops with a maximum attempts limit
|
||||
int maxAttempts = count * 10;
|
||||
int attempts = 0;
|
||||
|
||||
while (positions.size() < count && attempts < maxAttempts) {
|
||||
// Generate a random position
|
||||
float x = random.nextFloat() * maxX;
|
||||
float y = random.nextFloat() * maxY;
|
||||
|
||||
// Check if position maintains minimum spacing from existing positions
|
||||
boolean validPosition = true;
|
||||
|
||||
if (widthSpacer > 0 || heightSpacer > 0) {
|
||||
for (float[] existing : positions) {
|
||||
float dx = Math.abs(x - existing[0]);
|
||||
float dy = Math.abs(y - existing[1]);
|
||||
|
||||
// Check if the new position overlaps or violates spacing constraints
|
||||
// Two watermarks violate spacing if their bounding boxes (including spacers)
|
||||
// overlap
|
||||
if (dx < (watermarkWidth + widthSpacer)
|
||||
&& dy < (watermarkHeight + heightSpacer)) {
|
||||
validPosition = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validPosition) {
|
||||
positions.add(new float[] {x, y});
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
// If we couldn't generate enough positions with spacing constraints,
|
||||
// fill remaining positions without spacing constraints to meet the requested count
|
||||
while (positions.size() < count) {
|
||||
float x = random.nextFloat() * maxX;
|
||||
float y = random.nextFloat() * maxY;
|
||||
positions.add(new float[] {x, y});
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of fixed grid positions based on spacers.
|
||||
*
|
||||
* @param pageWidth Width of the page
|
||||
* @param pageHeight Height of the page
|
||||
* @param watermarkWidth Width of the watermark (includes spacing)
|
||||
* @param watermarkHeight Height of the watermark (includes spacing)
|
||||
* @param widthSpacer Horizontal spacing between watermarks
|
||||
* @param heightSpacer Vertical spacing between watermarks
|
||||
* @param count Maximum number of watermarks (0 for unlimited grid)
|
||||
* @return List of [x, y] coordinate arrays
|
||||
*/
|
||||
public List<float[]> generateGridPositions(
|
||||
float pageWidth,
|
||||
float pageHeight,
|
||||
float watermarkWidth,
|
||||
float watermarkHeight,
|
||||
int widthSpacer,
|
||||
int heightSpacer,
|
||||
int count) {
|
||||
|
||||
List<float[]> positions = new ArrayList<>();
|
||||
|
||||
// Calculate automatic margins to keep watermarks within page boundaries
|
||||
float maxX = Math.max(0, pageWidth - watermarkWidth);
|
||||
float maxY = Math.max(0, pageHeight - watermarkHeight);
|
||||
|
||||
// Calculate how many rows and columns can fit within the page
|
||||
// Note: watermarkWidth/Height are the actual watermark dimensions (not including spacers)
|
||||
// We need to account for the spacing between watermarks when calculating grid capacity
|
||||
int maxRows = (int) Math.floor(maxY / (watermarkHeight + heightSpacer));
|
||||
int maxCols = (int) Math.floor(maxX / (watermarkWidth + widthSpacer));
|
||||
|
||||
if (count == 0) {
|
||||
// Unlimited grid: fill page using spacer-based grid
|
||||
// Ensure watermarks stay within visible area
|
||||
for (int i = 0; i < maxRows; i++) {
|
||||
for (int j = 0; j < maxCols; j++) {
|
||||
float x = j * (watermarkWidth + widthSpacer);
|
||||
float y = i * (watermarkHeight + heightSpacer);
|
||||
// Clamp to ensure within bounds
|
||||
x = Math.min(x, maxX);
|
||||
y = Math.min(y, maxY);
|
||||
positions.add(new float[] {x, y});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Limited count: distribute evenly across page
|
||||
// Calculate optimal distribution based on page aspect ratio
|
||||
// Don't use spacer-based limits; instead ensure positions fit within maxX/maxY
|
||||
int cols = (int) Math.ceil(Math.sqrt(count * pageWidth / pageHeight));
|
||||
int rows = (int) Math.ceil((double) count / cols);
|
||||
|
||||
// Calculate spacing to distribute watermarks evenly within the visible area
|
||||
// Account for watermark dimensions to prevent overflow at edges
|
||||
float xSpacing = (cols > 1) ? maxX / (cols - 1) : 0;
|
||||
float ySpacing = (rows > 1) ? maxY / (rows - 1) : 0;
|
||||
|
||||
int generated = 0;
|
||||
for (int i = 0; i < rows && generated < count; i++) {
|
||||
for (int j = 0; j < cols && generated < count; j++) {
|
||||
float x = j * xSpacing;
|
||||
float y = i * ySpacing;
|
||||
// Clamp to ensure within bounds
|
||||
x = Math.min(x, maxX);
|
||||
y = Math.min(y, maxY);
|
||||
positions.add(new float[] {x, y});
|
||||
generated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random rotation angle within the specified range.
|
||||
*
|
||||
* @param rotationMin Minimum rotation angle in degrees
|
||||
* @param rotationMax Maximum rotation angle in degrees
|
||||
* @return Random rotation angle in degrees
|
||||
*/
|
||||
public float generateRandomRotation(float rotationMin, float rotationMax) {
|
||||
if (rotationMin == rotationMax) {
|
||||
return rotationMin;
|
||||
}
|
||||
return rotationMin + random.nextFloat() * (rotationMax - rotationMin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to mirror based on probability.
|
||||
*
|
||||
* @param probability Probability of mirroring (0.0 to 1.0)
|
||||
* @return true if should mirror, false otherwise
|
||||
*/
|
||||
public boolean shouldMirror(float probability) {
|
||||
return random.nextFloat() < probability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a random font from the available font list.
|
||||
*
|
||||
* @param availableFonts List of available font names
|
||||
* @return Random font name from the list
|
||||
*/
|
||||
public String selectRandomFont(List<String> availableFonts) {
|
||||
if (availableFonts == null || availableFonts.isEmpty()) {
|
||||
return "default";
|
||||
}
|
||||
return availableFonts.get(random.nextInt(availableFonts.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random font size within the specified range.
|
||||
*
|
||||
* @param fontSizeMin Minimum font size
|
||||
* @param fontSizeMax Maximum font size
|
||||
* @return Random font size
|
||||
*/
|
||||
public float generateRandomFontSize(float fontSizeMin, float fontSizeMax) {
|
||||
if (fontSizeMin == fontSizeMax) {
|
||||
return fontSizeMin;
|
||||
}
|
||||
return fontSizeMin + random.nextFloat() * (fontSizeMax - fontSizeMin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random color from a predefined palette or full spectrum.
|
||||
*
|
||||
* @param usePalette If true, uses a predefined palette; otherwise generates random RGB
|
||||
* @return Random Color object
|
||||
*/
|
||||
public Color generateRandomColor(boolean usePalette) {
|
||||
if (usePalette) {
|
||||
return PALETTE[random.nextInt(PALETTE.length)];
|
||||
} else {
|
||||
// Generate random RGB color
|
||||
return new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a random shading style from available options.
|
||||
*
|
||||
* @param availableShadings List of available shading styles
|
||||
* @return Random shading style
|
||||
*/
|
||||
public String selectRandomShading(List<String> availableShadings) {
|
||||
if (availableShadings == null || availableShadings.isEmpty()) {
|
||||
return "none";
|
||||
}
|
||||
return availableShadings.get(random.nextInt(availableShadings.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random color from a limited palette.
|
||||
*
|
||||
* @param colorCount Number of colors to select from (1-12 from predefined palette)
|
||||
* @return Random Color object from the limited palette
|
||||
*/
|
||||
public Color generateRandomColorFromPalette(int colorCount) {
|
||||
// Limit to requested count
|
||||
int actualCount = Math.min(colorCount, PALETTE.length);
|
||||
actualCount = Math.max(1, actualCount); // At least 1
|
||||
|
||||
return PALETTE[random.nextInt(actualCount)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a random font from a limited list of available fonts.
|
||||
*
|
||||
* @param fontCount Number of fonts to select from
|
||||
* @return Random font name
|
||||
*/
|
||||
public String selectRandomFontFromCount(int fontCount) {
|
||||
// Predefined list of common PDF fonts
|
||||
String[] availableFonts = {
|
||||
"Helvetica",
|
||||
"Times-Roman",
|
||||
"Courier",
|
||||
"Helvetica-Bold",
|
||||
"Times-Bold",
|
||||
"Courier-Bold",
|
||||
"Helvetica-Oblique",
|
||||
"Times-Italic",
|
||||
"Courier-Oblique",
|
||||
"Symbol",
|
||||
"ZapfDingbats"
|
||||
};
|
||||
|
||||
// Limit to requested count
|
||||
int actualCount = Math.min(fontCount, availableFonts.length);
|
||||
actualCount = Math.max(1, actualCount); // At least 1
|
||||
|
||||
return availableFonts[random.nextInt(actualCount)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random rotation for per-letter orientation within specified range.
|
||||
*
|
||||
* @param minRotation Minimum rotation angle in degrees
|
||||
* @param maxRotation Maximum rotation angle in degrees
|
||||
* @return Random rotation angle in degrees
|
||||
*/
|
||||
public float generatePerLetterRotationInRange(float minRotation, float maxRotation) {
|
||||
if (minRotation == maxRotation) {
|
||||
return minRotation;
|
||||
}
|
||||
return minRotation + random.nextFloat() * (maxRotation - minRotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying Random instance for advanced use cases.
|
||||
*
|
||||
* @return The Random instance
|
||||
*/
|
||||
public Random getRandom() {
|
||||
return random;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import static stirling.software.common.util.RegexPatternUtils.getColorPattern;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.beans.PropertyEditorSupport;
|
||||
@ -15,13 +17,18 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
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.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
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.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -38,14 +45,13 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.common.util.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@ -66,6 +72,162 @@ public class WatermarkController {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates watermark request parameters and enforces safety caps. Throws
|
||||
* IllegalArgumentException with descriptive messages for validation failures.
|
||||
*/
|
||||
private void validateWatermarkRequest(AddWatermarkRequest request) {
|
||||
// Validate opacity bounds (0.0 - 1.0)
|
||||
float opacity = request.getOpacity();
|
||||
if (opacity < 0.0f || opacity > 1.0f) {
|
||||
log.error("Opacity must be between 0.0 and 1.0, but got: {}", opacity);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.opacityOutOfRange", // TODO
|
||||
"Opacity must be between 0.0 and 1.0, but got: {0}",
|
||||
opacity);
|
||||
}
|
||||
|
||||
// Validate rotation range: rotationMin <= rotationMax
|
||||
Float rotationMin = request.getRotationMin();
|
||||
Float rotationMax = request.getRotationMax();
|
||||
if (rotationMin != null && rotationMax != null && rotationMin > rotationMax) {
|
||||
log.error(
|
||||
"Rotation minimum ({}) must be less than or equal to rotation maximum ({})",
|
||||
rotationMin,
|
||||
rotationMax);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.rotationRangeInvalid", // TODO
|
||||
"Rotation minimum ({0}) must be less than or equal to rotation maximum ({1})",
|
||||
rotationMin,
|
||||
rotationMax);
|
||||
}
|
||||
|
||||
// Validate font size range: fontSizeMin <= fontSizeMax
|
||||
Float fontSizeMin = request.getFontSizeMin();
|
||||
Float fontSizeMax = request.getFontSizeMax();
|
||||
if (fontSizeMin != null && fontSizeMax != null && fontSizeMin > fontSizeMax) {
|
||||
log.error(
|
||||
"Font size minimum ({}) must be less than or equal to font size maximum ({})",
|
||||
fontSizeMin,
|
||||
fontSizeMax);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.fontSizeRangeInvalid", // TODO
|
||||
"Font size minimum ({0}) must be less than or equal to font size maximum ({1})",
|
||||
fontSizeMin,
|
||||
fontSizeMax);
|
||||
}
|
||||
|
||||
// Validate color format when not using random color
|
||||
String customColor = request.getCustomColor();
|
||||
Boolean randomColor = request.getRandomColor();
|
||||
if (customColor != null && !Boolean.TRUE.equals(randomColor)) {
|
||||
// Check if color is valid hex format (#RRGGBB or #RRGGBBAA)
|
||||
if (!getColorPattern().matcher(customColor).matches()) {
|
||||
log.error(
|
||||
"Invalid color format: {}. Expected hex format like #RRGGBB or #RRGGBBAA",
|
||||
customColor);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidColorFormat", // TODO
|
||||
"Invalid color format: {0}. Expected hex format like #RRGGBB or #RRGGBBAA",
|
||||
customColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate mirroring probability bounds (0.0 - 1.0)
|
||||
Float mirroringProbability = request.getMirroringProbability();
|
||||
if (mirroringProbability != null
|
||||
&& (mirroringProbability < 0.0f || mirroringProbability > 1.0f)) {
|
||||
log.error(
|
||||
"Mirroring probability must be between 0.0 and 1.0, but got: {}",
|
||||
mirroringProbability);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.mirroringProbabilityOutOfRange", // TODO
|
||||
"Mirroring probability must be between 0.0 and 1.0, but got: {0}",
|
||||
mirroringProbability);
|
||||
}
|
||||
|
||||
// Validate watermark type
|
||||
String watermarkType = request.getWatermarkType();
|
||||
if (watermarkType == null
|
||||
|| (!watermarkType.equalsIgnoreCase("text")
|
||||
&& !watermarkType.equalsIgnoreCase("image"))) {
|
||||
log.error("Watermark type must be 'text' or 'image', but got: {}", watermarkType);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.unsupportedWatermarkType", // TODO
|
||||
"Watermark type must be ''text'' or ''image'', but got: {0}", // single quotes
|
||||
// must be escaped
|
||||
watermarkType);
|
||||
}
|
||||
|
||||
// Validate text watermark has text
|
||||
if ("text".equalsIgnoreCase(watermarkType)) {
|
||||
String watermarkText = request.getWatermarkText();
|
||||
if (watermarkText == null || watermarkText.trim().isEmpty()) {
|
||||
log.error("Watermark text is required when watermark type is 'text'");
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.watermarkTextRequired", // TODO
|
||||
"Watermark text is required when watermark type is 'text'");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate image watermark has image
|
||||
if ("image".equalsIgnoreCase(watermarkType)) {
|
||||
MultipartFile watermarkImage = request.getWatermarkImage();
|
||||
if (watermarkImage == null || watermarkImage.isEmpty()) {
|
||||
log.error("Watermark image is required when watermark type is 'image'");
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.watermarkImageRequired", // TODO
|
||||
"Watermark image is required when watermark type is 'image'");
|
||||
}
|
||||
|
||||
// Validate image type - only allow common image formats
|
||||
String contentType = watermarkImage.getContentType();
|
||||
String originalFilename = watermarkImage.getOriginalFilename();
|
||||
if (contentType != null && !isSupportedImageType(contentType)) {
|
||||
log.error(
|
||||
"Unsupported image type: {}. Supported types: PNG, JPG, JPEG, GIF, BMP",
|
||||
contentType);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.unsupportedContentType", // TODO
|
||||
"Unsupported image type: {0}. Supported types: PNG, JPG, JPEG, GIF, BMP",
|
||||
contentType);
|
||||
}
|
||||
|
||||
// Additional check based on file extension
|
||||
if (originalFilename != null && !hasSupportedImageExtension(originalFilename)) {
|
||||
log.error(
|
||||
"Unsupported image file extension in: {}. Supported extensions: .png, .jpg, .jpeg, .gif, .bmp",
|
||||
originalFilename);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.unsupportedImageFileType", // TODO
|
||||
"Unsupported image file extension in: {0}. Supported extensions: .png, .jpg, .jpeg, .gif, .bmp",
|
||||
originalFilename);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Watermark request validation passed");
|
||||
}
|
||||
|
||||
/** Checks if the content type is a supported image format. */
|
||||
private boolean isSupportedImageType(String contentType) {
|
||||
return contentType.equals("image/png")
|
||||
|| contentType.equals("image/jpeg")
|
||||
|| contentType.equals("image/jpg")
|
||||
|| contentType.equals("image/gif")
|
||||
|| contentType.equals("image/bmp")
|
||||
|| contentType.equals("image/x-ms-bmp");
|
||||
}
|
||||
|
||||
/** Checks if the filename has a supported image extension. */
|
||||
private boolean hasSupportedImageExtension(String filename) {
|
||||
String lowerFilename = filename.toLowerCase();
|
||||
return lowerFilename.endsWith(".png")
|
||||
|| lowerFilename.endsWith(".jpg")
|
||||
|| lowerFilename.endsWith(".jpeg")
|
||||
|| lowerFilename.endsWith(".gif")
|
||||
|| lowerFilename.endsWith(".bmp");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-watermark")
|
||||
@Operation(
|
||||
summary = "Add watermark to a PDF file",
|
||||
@ -74,73 +236,85 @@ public class WatermarkController {
|
||||
+ " watermark type (text or image), rotation, opacity, width spacer, and"
|
||||
+ " height spacer. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request)
|
||||
throws IOException, Exception {
|
||||
throws IOException {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
String pdfFileName = pdfFile.getOriginalFilename();
|
||||
if (pdfFileName != null && (pdfFileName.contains("..") || pdfFileName.startsWith("/"))) {
|
||||
log.error("Security violation: Invalid file path in pdfFile: {}", pdfFileName);
|
||||
throw new SecurityException("Invalid file path in pdfFile");
|
||||
}
|
||||
String watermarkType = request.getWatermarkType();
|
||||
String watermarkText = request.getWatermarkText();
|
||||
MultipartFile watermarkImage = request.getWatermarkImage();
|
||||
if (watermarkImage != null) {
|
||||
String watermarkImageFileName = watermarkImage.getOriginalFilename();
|
||||
if (watermarkImageFileName != null
|
||||
&& (watermarkImageFileName.contains("..")
|
||||
|| watermarkImageFileName.startsWith("/"))) {
|
||||
log.error(
|
||||
"Security violation: Invalid file path in watermarkImage: {}",
|
||||
watermarkImageFileName);
|
||||
throw new SecurityException("Invalid file path in watermarkImage");
|
||||
}
|
||||
}
|
||||
String alphabet = request.getAlphabet();
|
||||
float fontSize = request.getFontSize();
|
||||
float rotation = request.getRotation();
|
||||
float opacity = request.getOpacity();
|
||||
int widthSpacer = request.getWidthSpacer();
|
||||
int heightSpacer = request.getHeightSpacer();
|
||||
String customColor = request.getCustomColor();
|
||||
|
||||
// Validate request parameters and enforce safety caps
|
||||
validateWatermarkRequest(request);
|
||||
|
||||
// Extract new fields with defaults for backward compatibility
|
||||
boolean convertPdfToImage = Boolean.TRUE.equals(request.getConvertPDFToImage());
|
||||
|
||||
// Create a randomizer with optional seed for deterministic behavior
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(request.getSeed());
|
||||
|
||||
// Load the input PDF
|
||||
PDDocument document = pdfDocumentFactory.load(pdfFile);
|
||||
|
||||
// Create a page in the document
|
||||
// Create a PDFormXObject to cache watermarks (Solution 5: PDFormXObject caching)
|
||||
PDFormXObject watermarkForm = null;
|
||||
|
||||
// Iterate through pages
|
||||
for (PDPage page : document.getPages()) {
|
||||
|
||||
// Get the page's content stream
|
||||
// Create the watermark form on the first page
|
||||
if (watermarkForm == null) {
|
||||
// Create form XObject with the same dimensions as the page
|
||||
// Following the pattern from CertSignController
|
||||
PDRectangle pageBox = page.getMediaBox();
|
||||
PDStream stream = new PDStream(document);
|
||||
watermarkForm = new PDFormXObject(stream);
|
||||
watermarkForm.setResources(new PDResources());
|
||||
watermarkForm.setFormType(1);
|
||||
watermarkForm.setBBox(new PDRectangle(pageBox.getWidth(), pageBox.getHeight()));
|
||||
|
||||
// Create appearance stream for the form
|
||||
PDAppearanceStream appearanceStream =
|
||||
new PDAppearanceStream(watermarkForm.getCOSObject());
|
||||
|
||||
// Create a content stream for the appearance
|
||||
PDPageContentStream formStream =
|
||||
new PDPageContentStream(document, appearanceStream);
|
||||
|
||||
// Set transparency in the form
|
||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||
graphicsState.setNonStrokingAlphaConstant(request.getOpacity());
|
||||
formStream.setGraphicsStateParameters(graphicsState);
|
||||
|
||||
// Render watermarks into the form
|
||||
if ("text".equalsIgnoreCase(watermarkType)) {
|
||||
addTextWatermark(formStream, document, page, request, randomizer);
|
||||
} else if ("image".equalsIgnoreCase(watermarkType)) {
|
||||
addImageWatermark(formStream, document, page, request, randomizer);
|
||||
}
|
||||
|
||||
// Close the form stream
|
||||
formStream.close();
|
||||
}
|
||||
|
||||
// Draw the cached form on the current page
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
|
||||
// Set transparency
|
||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||
graphicsState.setNonStrokingAlphaConstant(opacity);
|
||||
contentStream.setGraphicsStateParameters(graphicsState);
|
||||
|
||||
if ("text".equalsIgnoreCase(watermarkType)) {
|
||||
addTextWatermark(
|
||||
contentStream,
|
||||
watermarkText,
|
||||
document,
|
||||
page,
|
||||
rotation,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize,
|
||||
alphabet,
|
||||
customColor);
|
||||
} else if ("image".equalsIgnoreCase(watermarkType)) {
|
||||
addImageWatermark(
|
||||
contentStream,
|
||||
watermarkImage,
|
||||
document,
|
||||
page,
|
||||
rotation,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize);
|
||||
}
|
||||
|
||||
// Close the content stream
|
||||
contentStream.drawForm(watermarkForm);
|
||||
contentStream.close();
|
||||
}
|
||||
|
||||
@ -158,19 +332,60 @@ public class WatermarkController {
|
||||
|
||||
private void addTextWatermark(
|
||||
PDPageContentStream contentStream,
|
||||
String watermarkText,
|
||||
PDDocument document,
|
||||
PDPage page,
|
||||
float rotation,
|
||||
int widthSpacer,
|
||||
int heightSpacer,
|
||||
float fontSize,
|
||||
String alphabet,
|
||||
String colorString)
|
||||
AddWatermarkRequest request,
|
||||
WatermarkRandomizer randomizer)
|
||||
throws IOException {
|
||||
String resourceDir = "";
|
||||
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
resourceDir =
|
||||
|
||||
String watermarkText = request.getWatermarkText();
|
||||
String alphabet = request.getAlphabet();
|
||||
String colorString = request.getCustomColor();
|
||||
float rotation = request.getRotation();
|
||||
int widthSpacer = request.getWidthSpacer();
|
||||
int heightSpacer = request.getHeightSpacer();
|
||||
float fontSize = request.getFontSize();
|
||||
|
||||
// Extract new fields with defaults
|
||||
int count = (request.getCount() != null) ? request.getCount() : 1;
|
||||
boolean randomPosition = Boolean.TRUE.equals(request.getRandomPosition());
|
||||
boolean randomFont = Boolean.TRUE.equals(request.getRandomFont());
|
||||
boolean randomColor = Boolean.TRUE.equals(request.getRandomColor());
|
||||
boolean perLetterFont = Boolean.TRUE.equals(request.getPerLetterFont());
|
||||
boolean perLetterColor = Boolean.TRUE.equals(request.getPerLetterColor());
|
||||
boolean perLetterSize = Boolean.TRUE.equals(request.getPerLetterSize());
|
||||
boolean perLetterOrientation = Boolean.TRUE.equals(request.getPerLetterOrientation());
|
||||
boolean shadingRandom = Boolean.TRUE.equals(request.getShadingRandom());
|
||||
String shading = request.getShading();
|
||||
|
||||
float rotationMin =
|
||||
(request.getRotationMin() != null) ? request.getRotationMin() : rotation;
|
||||
float rotationMax =
|
||||
(request.getRotationMax() != null) ? request.getRotationMax() : rotation;
|
||||
float fontSizeMin =
|
||||
(request.getFontSizeMin() != null) ? request.getFontSizeMin() : fontSize;
|
||||
float fontSizeMax =
|
||||
(request.getFontSizeMax() != null) ? request.getFontSizeMax() : fontSize;
|
||||
|
||||
// Extract per-letter configuration with defaults
|
||||
int perLetterFontCount =
|
||||
(request.getPerLetterFontCount() != null) ? request.getPerLetterFontCount() : 2;
|
||||
int perLetterColorCount =
|
||||
(request.getPerLetterColorCount() != null) ? request.getPerLetterColorCount() : 4;
|
||||
float perLetterSizeMin =
|
||||
(request.getPerLetterSizeMin() != null) ? request.getPerLetterSizeMin() : 10f;
|
||||
float perLetterSizeMax =
|
||||
(request.getPerLetterSizeMax() != null) ? request.getPerLetterSizeMax() : 100f;
|
||||
float perLetterOrientationMin =
|
||||
(request.getPerLetterOrientationMin() != null)
|
||||
? request.getPerLetterOrientationMin()
|
||||
: 0f;
|
||||
float perLetterOrientationMax =
|
||||
(request.getPerLetterOrientationMax() != null)
|
||||
? request.getPerLetterOrientationMax()
|
||||
: 360f;
|
||||
|
||||
String resourceDir =
|
||||
switch (alphabet) {
|
||||
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
||||
case "japanese" -> "static/fonts/Meiryo.ttf";
|
||||
@ -183,6 +398,8 @@ public class WatermarkController {
|
||||
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
|
||||
|
||||
PDFont font;
|
||||
try (InputStream is = classPathResource.getInputStream();
|
||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(is, os);
|
||||
@ -191,63 +408,184 @@ public class WatermarkController {
|
||||
Files.deleteIfExists(tempFile.toPath());
|
||||
}
|
||||
|
||||
contentStream.setFont(font, fontSize);
|
||||
|
||||
Color redactColor;
|
||||
try {
|
||||
if (!colorString.startsWith("#")) {
|
||||
colorString = "#" + colorString;
|
||||
}
|
||||
redactColor = Color.decode(colorString);
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
redactColor = Color.LIGHT_GRAY;
|
||||
}
|
||||
contentStream.setNonStrokingColor(redactColor);
|
||||
|
||||
String[] textLines =
|
||||
RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(watermarkText);
|
||||
float maxLineWidth = 0;
|
||||
|
||||
for (int i = 0; i < textLines.length; ++i) {
|
||||
maxLineWidth = Math.max(maxLineWidth, font.getStringWidth(textLines[i]));
|
||||
}
|
||||
|
||||
// Set size and location of text watermark
|
||||
float watermarkWidth = widthSpacer + maxLineWidth * fontSize / 1000;
|
||||
float watermarkHeight = heightSpacer + fontSize * textLines.length;
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
float pageHeight = page.getMediaBox().getHeight();
|
||||
|
||||
// Calculating the new width and height depending on the angle.
|
||||
float radians = (float) Math.toRadians(rotation);
|
||||
float newWatermarkWidth =
|
||||
(float)
|
||||
(Math.abs(watermarkWidth * Math.cos(radians))
|
||||
+ Math.abs(watermarkHeight * Math.sin(radians)));
|
||||
float newWatermarkHeight =
|
||||
(float)
|
||||
(Math.abs(watermarkWidth * Math.sin(radians))
|
||||
+ Math.abs(watermarkHeight * Math.cos(radians)));
|
||||
// Determine positions based on a randomPosition flag
|
||||
java.util.List<float[]> positions;
|
||||
|
||||
// Calculating the number of rows and columns.
|
||||
// Calculate approximate watermark dimensions for positioning
|
||||
// Estimate width based on average character width (more accurate than fixed 100)
|
||||
float avgCharWidth = fontSize * 0.6f; // Approximate average character width
|
||||
float maxLineWidth = 0;
|
||||
for (String line : textLines) {
|
||||
float lineWidth = line.length() * avgCharWidth;
|
||||
if (lineWidth > maxLineWidth) {
|
||||
maxLineWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
float watermarkWidth = maxLineWidth;
|
||||
float watermarkHeight = fontSize * textLines.length;
|
||||
|
||||
int watermarkRows = (int) (pageHeight / newWatermarkHeight + 1);
|
||||
int watermarkCols = (int) (pageWidth / newWatermarkWidth + 1);
|
||||
if (randomPosition) {
|
||||
// Generate random positions with collision detection
|
||||
positions =
|
||||
randomizer.generateRandomPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
watermarkWidth,
|
||||
watermarkHeight,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
count);
|
||||
} else {
|
||||
// Generate grid positions (backward compatible)
|
||||
positions =
|
||||
randomizer.generateGridPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
watermarkWidth,
|
||||
watermarkHeight,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
count);
|
||||
}
|
||||
|
||||
// Define available fonts for random selection
|
||||
java.util.List<String> availableFonts =
|
||||
java.util.Arrays.asList(
|
||||
"Helvetica",
|
||||
"Times-Roman",
|
||||
"Courier",
|
||||
"Helvetica-Bold",
|
||||
"Times-Bold",
|
||||
"Courier-Bold");
|
||||
|
||||
// Render each watermark instance
|
||||
for (float[] pos : positions) {
|
||||
float x = pos[0];
|
||||
float y = pos[1];
|
||||
|
||||
// Determine the font for this watermark instance
|
||||
PDFont wmFont;
|
||||
if (randomFont) {
|
||||
try {
|
||||
String selectedFontName = randomizer.selectRandomFont(availableFonts);
|
||||
wmFont = new PDType1Font(Standard14Fonts.getMappedFontName(selectedFontName));
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to load random font, using base font instead", e);
|
||||
wmFont = font; // Fall back to the base font loaded earlier
|
||||
}
|
||||
} else {
|
||||
wmFont = font; // Use the base font loaded from alphabet selection
|
||||
}
|
||||
|
||||
// Determine rotation for this watermark
|
||||
float wmRotation = randomizer.generateRandomRotation(rotationMin, rotationMax);
|
||||
|
||||
// Determine font size for this watermark
|
||||
float wmFontSize = randomizer.generateRandomFontSize(fontSizeMin, fontSizeMax);
|
||||
|
||||
// Determine color for this watermark
|
||||
Color wmColor;
|
||||
if (randomColor) {
|
||||
wmColor = randomizer.generateRandomColor(true);
|
||||
} else {
|
||||
try {
|
||||
String colorStr = colorString;
|
||||
if (!colorStr.startsWith("#")) {
|
||||
colorStr = "#" + colorStr;
|
||||
}
|
||||
wmColor = Color.decode(colorStr);
|
||||
} catch (Exception e) {
|
||||
wmColor = Color.LIGHT_GRAY;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine and apply shading style
|
||||
String wmShading =
|
||||
shadingRandom
|
||||
? randomizer.selectRandomShading(
|
||||
java.util.Arrays.asList("none", "light", "dark"))
|
||||
: (shading != null ? shading : "none");
|
||||
|
||||
// Apply shading by adjusting color intensity
|
||||
wmColor = applyShadingToColor(wmColor, wmShading);
|
||||
|
||||
// Render text with per-letter variations if enabled
|
||||
if (perLetterFont || perLetterColor || perLetterSize || perLetterOrientation) {
|
||||
renderTextWithPerLetterVariations(
|
||||
contentStream,
|
||||
textLines,
|
||||
wmFont,
|
||||
wmFontSize,
|
||||
wmColor,
|
||||
wmRotation,
|
||||
x,
|
||||
y,
|
||||
perLetterFont,
|
||||
perLetterColor,
|
||||
perLetterSize,
|
||||
perLetterOrientation,
|
||||
perLetterSizeMin,
|
||||
perLetterSizeMax,
|
||||
perLetterFontCount,
|
||||
perLetterColorCount,
|
||||
perLetterOrientationMin,
|
||||
perLetterOrientationMax,
|
||||
randomizer);
|
||||
} else {
|
||||
// Standard rendering without per-letter variations
|
||||
contentStream.setFont(wmFont, wmFontSize);
|
||||
contentStream.setNonStrokingColor(wmColor);
|
||||
|
||||
// Calculate actual watermark dimensions for center-point rotation
|
||||
float actualMaxLineWidth = 0;
|
||||
for (String textLine : textLines) {
|
||||
float lineWidth = wmFont.getStringWidth(textLine) * wmFontSize / 1000;
|
||||
if (lineWidth > actualMaxLineWidth) {
|
||||
actualMaxLineWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
float actualWatermarkWidth = actualMaxLineWidth;
|
||||
float actualWatermarkHeight = wmFontSize * textLines.length;
|
||||
|
||||
// Calculate center point of the watermark
|
||||
float centerX = x + actualWatermarkWidth / 2;
|
||||
float centerY = y + actualWatermarkHeight / 2;
|
||||
|
||||
// Apply rotation around center point
|
||||
// To rotate around center, we need to:
|
||||
// 1. Calculate offset from center to bottom-left corner
|
||||
// 2. Rotate that offset
|
||||
// 3. Add rotated offset to center to get final position
|
||||
double rotationRad = Math.toRadians(wmRotation);
|
||||
double cos = Math.cos(rotationRad);
|
||||
double sin = Math.sin(rotationRad);
|
||||
|
||||
// Offset from center to bottom-left corner (where text starts)
|
||||
float offsetX = -actualWatermarkWidth / 2;
|
||||
float offsetY = -actualWatermarkHeight / 2;
|
||||
|
||||
// Apply rotation to the offset
|
||||
float rotatedOffsetX = (float) (offsetX * cos - offsetY * sin);
|
||||
float rotatedOffsetY = (float) (offsetX * sin + offsetY * cos);
|
||||
|
||||
// Final position where text should start
|
||||
float finalX = centerX + rotatedOffsetX;
|
||||
float finalY = centerY + rotatedOffsetY;
|
||||
|
||||
// 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 * newWatermarkWidth,
|
||||
i * newWatermarkHeight));
|
||||
(float) Math.toRadians(wmRotation), finalX, finalY));
|
||||
|
||||
for (int k = 0; k < textLines.length; ++k) {
|
||||
contentStream.showText(textLines[k]);
|
||||
contentStream.newLineAtOffset(0, -fontSize);
|
||||
for (String textLine : textLines) {
|
||||
contentStream.showText(textLine);
|
||||
contentStream.newLineAtOffset(0, -wmFontSize);
|
||||
}
|
||||
|
||||
contentStream.endText();
|
||||
@ -255,25 +593,168 @@ public class WatermarkController {
|
||||
}
|
||||
}
|
||||
|
||||
private void renderTextWithPerLetterVariations(
|
||||
PDPageContentStream contentStream,
|
||||
String[] textLines,
|
||||
PDFont baseFont,
|
||||
float baseFontSize,
|
||||
Color baseColor,
|
||||
float baseRotation,
|
||||
float startX,
|
||||
float startY,
|
||||
boolean perLetterFont,
|
||||
boolean perLetterColor,
|
||||
boolean perLetterSize,
|
||||
boolean perLetterOrientation,
|
||||
float fontSizeMin,
|
||||
float fontSizeMax,
|
||||
int perLetterFontCount,
|
||||
int perLetterColorCount,
|
||||
float perLetterOrientationMin,
|
||||
float perLetterOrientationMax,
|
||||
WatermarkRandomizer randomizer)
|
||||
throws IOException {
|
||||
|
||||
// Convert base rotation to radians for trigonometric calculations
|
||||
double baseRotationRad = Math.toRadians(baseRotation);
|
||||
double cosBase = Math.cos(baseRotationRad);
|
||||
double sinBase = Math.sin(baseRotationRad);
|
||||
|
||||
float currentLineOffset = 0; // Vertical offset for each line
|
||||
|
||||
for (String line : textLines) {
|
||||
float currentCharOffset = 0; // Horizontal offset along the baseline for current line
|
||||
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
String charStr = String.valueOf(c);
|
||||
|
||||
// Determine per-letter attributes
|
||||
float letterSize =
|
||||
perLetterSize
|
||||
? randomizer.generateRandomFontSize(fontSizeMin, fontSizeMax)
|
||||
: baseFontSize;
|
||||
|
||||
Color letterColor =
|
||||
perLetterColor
|
||||
? randomizer.generateRandomColorFromPalette(perLetterColorCount)
|
||||
: baseColor;
|
||||
|
||||
// Calculate per-letter rotation: base rotation + additional per-letter variation
|
||||
float additionalRotation =
|
||||
perLetterOrientation
|
||||
? randomizer.generatePerLetterRotationInRange(
|
||||
perLetterOrientationMin, perLetterOrientationMax)
|
||||
: 0f;
|
||||
float letterRotation = baseRotation + additionalRotation;
|
||||
|
||||
// Determine per-letter font
|
||||
PDFont letterFont = baseFont;
|
||||
if (perLetterFont) {
|
||||
try {
|
||||
String randomFontName =
|
||||
randomizer.selectRandomFontFromCount(perLetterFontCount);
|
||||
letterFont =
|
||||
new PDType1Font(Standard14Fonts.getMappedFontName(randomFontName));
|
||||
} catch (Exception e) {
|
||||
// Fall back to base font if font loading fails
|
||||
log.warn("Failed to load random font, using base font instead", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate letter dimensions for center-point rotation
|
||||
float charWidth = letterFont.getStringWidth(charStr) * letterSize / 1000;
|
||||
float charHeight = letterSize; // Approximate height as font size
|
||||
|
||||
// Calculate the position of this letter along the rotated baseline
|
||||
// Transform from local coordinates (along baseline) to page coordinates
|
||||
float localX = currentCharOffset;
|
||||
float localY = -currentLineOffset; // Negative because text lines go downward
|
||||
|
||||
// Apply rotation transformation to position the letter on the rotated baseline
|
||||
float transformedX = (float) (startX + localX * cosBase - localY * sinBase);
|
||||
float transformedY = (float) (startY + localX * sinBase + localY * cosBase);
|
||||
|
||||
// Calculate letter's center point on the rotated baseline
|
||||
float letterCenterX = transformedX + charWidth / 2;
|
||||
float letterCenterY = transformedY + charHeight / 2;
|
||||
|
||||
// Apply rotation around letter center
|
||||
// Calculate offset from center to bottom-left corner
|
||||
double letterRotationRad = Math.toRadians(letterRotation);
|
||||
double cosLetter = Math.cos(letterRotationRad);
|
||||
double sinLetter = Math.sin(letterRotationRad);
|
||||
|
||||
float offsetX = -charWidth / 2;
|
||||
float offsetY = -charHeight / 2;
|
||||
|
||||
// Rotate the offset
|
||||
float rotatedOffsetX = (float) (offsetX * cosLetter - offsetY * sinLetter);
|
||||
float rotatedOffsetY = (float) (offsetX * sinLetter + offsetY * cosLetter);
|
||||
|
||||
// Final position where letter should be rendered
|
||||
float finalLetterX = letterCenterX + rotatedOffsetX;
|
||||
float finalLetterY = letterCenterY + rotatedOffsetY;
|
||||
|
||||
// Set font and color
|
||||
contentStream.setFont(letterFont, letterSize);
|
||||
contentStream.setNonStrokingColor(letterColor);
|
||||
|
||||
// Render the character at the transformed position with combined rotation
|
||||
contentStream.beginText();
|
||||
contentStream.setTextMatrix(
|
||||
Matrix.getRotateInstance(
|
||||
(float) Math.toRadians(letterRotation),
|
||||
finalLetterX,
|
||||
finalLetterY));
|
||||
contentStream.showText(charStr);
|
||||
contentStream.endText();
|
||||
|
||||
// Advance position along the baseline for the next character
|
||||
currentCharOffset += charWidth;
|
||||
}
|
||||
|
||||
// Move to next line (advance vertically in local coordinates)
|
||||
currentLineOffset += baseFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
private void addImageWatermark(
|
||||
PDPageContentStream contentStream,
|
||||
MultipartFile watermarkImage,
|
||||
PDDocument document,
|
||||
PDPage page,
|
||||
float rotation,
|
||||
int widthSpacer,
|
||||
int heightSpacer,
|
||||
float fontSize)
|
||||
AddWatermarkRequest request,
|
||||
WatermarkRandomizer randomizer)
|
||||
throws IOException {
|
||||
|
||||
MultipartFile watermarkImage = request.getWatermarkImage();
|
||||
float rotation = request.getRotation();
|
||||
int widthSpacer = request.getWidthSpacer();
|
||||
int heightSpacer = request.getHeightSpacer();
|
||||
float fontSize = request.getFontSize();
|
||||
|
||||
// Extract new fields with defaults
|
||||
int count = (request.getCount() != null) ? request.getCount() : 1;
|
||||
boolean randomPosition = Boolean.TRUE.equals(request.getRandomPosition());
|
||||
boolean randomMirroring = Boolean.TRUE.equals(request.getRandomMirroring());
|
||||
float mirroringProbability =
|
||||
(request.getMirroringProbability() != null)
|
||||
? request.getMirroringProbability()
|
||||
: 0.5f;
|
||||
float imageScale = (request.getImageScale() != null) ? request.getImageScale() : 1.0f;
|
||||
float rotationMin =
|
||||
(request.getRotationMin() != null) ? request.getRotationMin() : rotation;
|
||||
float rotationMax =
|
||||
(request.getRotationMax() != null) ? request.getRotationMax() : rotation;
|
||||
|
||||
// Load the watermark image
|
||||
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
|
||||
|
||||
// Compute width based on original aspect ratio
|
||||
// Compute width based on an original aspect ratio
|
||||
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
|
||||
|
||||
// Desired physical height (in PDF points)
|
||||
float desiredPhysicalHeight = fontSize;
|
||||
// Desired physical height (in PDF points) with scale applied
|
||||
float desiredPhysicalHeight = fontSize * imageScale;
|
||||
|
||||
// Desired physical width based on the aspect ratio
|
||||
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
|
||||
@ -281,27 +762,64 @@ public class WatermarkController {
|
||||
// Convert the BufferedImage to PDImageXObject
|
||||
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
|
||||
|
||||
// Calculate the number of rows and columns for watermarks
|
||||
// Get page dimensions
|
||||
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);
|
||||
// Determine positions based on a randomPosition flag
|
||||
java.util.List<float[]> positions;
|
||||
if (randomPosition) {
|
||||
// Generate random positions with collision detection
|
||||
positions =
|
||||
randomizer.generateRandomPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
desiredPhysicalWidth,
|
||||
desiredPhysicalHeight,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
count);
|
||||
} else {
|
||||
// Generate grid positions (backward compatible)
|
||||
positions =
|
||||
randomizer.generateGridPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
desiredPhysicalWidth,
|
||||
desiredPhysicalHeight,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
count);
|
||||
}
|
||||
|
||||
// Render each watermark instance
|
||||
for (float[] pos : positions) {
|
||||
float x = pos[0];
|
||||
float y = pos[1];
|
||||
|
||||
// Determine rotation for this watermark
|
||||
float wmRotation = randomizer.generateRandomRotation(rotationMin, rotationMax);
|
||||
|
||||
// Determine if this watermark should be mirrored
|
||||
boolean shouldMirror = randomMirroring && randomizer.shouldMirror(mirroringProbability);
|
||||
|
||||
// Save the graphics state
|
||||
contentStream.saveGraphicsState();
|
||||
|
||||
// Create rotation matrix and rotate
|
||||
// Translate to center of image position
|
||||
contentStream.transform(
|
||||
Matrix.getTranslateInstance(
|
||||
x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
|
||||
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
|
||||
|
||||
// Apply rotation
|
||||
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(wmRotation), 0, 0));
|
||||
|
||||
// Apply mirroring if needed (horizontal flip)
|
||||
if (shouldMirror) {
|
||||
contentStream.transform(Matrix.getScaleInstance(-1, 1));
|
||||
}
|
||||
|
||||
// Translate back to draw from corner
|
||||
contentStream.transform(
|
||||
Matrix.getTranslateInstance(
|
||||
-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
|
||||
@ -311,5 +829,35 @@ public class WatermarkController {
|
||||
contentStream.restoreGraphicsState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies shading to a color by adjusting its intensity.
|
||||
*
|
||||
* @param color Original color
|
||||
* @param shading Shading style: "none", "light", or "dark"
|
||||
* @return Color with shading applied
|
||||
*/
|
||||
private Color applyShadingToColor(Color color, String shading) {
|
||||
if (shading == null || "none".equalsIgnoreCase(shading)) {
|
||||
return color;
|
||||
}
|
||||
|
||||
int r = color.getRed();
|
||||
int g = color.getGreen();
|
||||
int b = color.getBlue();
|
||||
|
||||
if ("light".equalsIgnoreCase(shading)) {
|
||||
// Lighten the color by moving towards white
|
||||
r = r + (255 - r) / 2;
|
||||
g = g + (255 - g) / 2;
|
||||
b = b + (255 - b) / 2;
|
||||
} else if ("dark".equalsIgnoreCase(shading)) {
|
||||
// Darken the color by moving towards black
|
||||
r = r / 2;
|
||||
g = g / 2;
|
||||
b = b / 2;
|
||||
}
|
||||
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,11 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@ -54,4 +59,137 @@ public class AddWatermarkRequest extends PDFFile {
|
||||
defaultValue = "false",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean convertPDFToImage;
|
||||
|
||||
// New fields for enhanced watermarking (Phase 1)
|
||||
|
||||
@Schema(description = "Number of watermark instances per page or document", defaultValue = "1")
|
||||
@Min(value = 1, message = "Count must be at least 1")
|
||||
@Max(value = 1000, message = "Count must not exceed 1000")
|
||||
private Integer count;
|
||||
|
||||
@Schema(description = "Enable random positioning of watermarks", defaultValue = "false")
|
||||
private Boolean randomPosition;
|
||||
|
||||
@Schema(
|
||||
description = "Minimum rotation angle in degrees (used when rotation range is enabled)",
|
||||
defaultValue = "0")
|
||||
@DecimalMin(value = "-360.0", message = "Rotation minimum must be >= -360")
|
||||
@DecimalMax(value = "360.0", message = "Rotation minimum must be <= 360")
|
||||
private Float rotationMin;
|
||||
|
||||
@Schema(
|
||||
description = "Maximum rotation angle in degrees (used when rotation range is enabled)",
|
||||
defaultValue = "0")
|
||||
@DecimalMin(value = "-360.0", message = "Rotation maximum must be >= -360")
|
||||
@DecimalMax(value = "360.0", message = "Rotation maximum must be <= 360")
|
||||
private Float rotationMax;
|
||||
|
||||
@Schema(description = "Enable random mirroring of watermarks", defaultValue = "false")
|
||||
private Boolean randomMirroring;
|
||||
|
||||
@Schema(
|
||||
description = "Probability of mirroring when randomMirroring is enabled (0.0 - 1.0)",
|
||||
defaultValue = "0.5")
|
||||
@DecimalMin(value = "0.0", message = "Mirroring probability must be >= 0.0")
|
||||
@DecimalMax(value = "1.0", message = "Mirroring probability must be <= 1.0")
|
||||
private Float mirroringProbability;
|
||||
|
||||
@Schema(description = "Specific font name to use for text watermarks")
|
||||
private String fontName;
|
||||
|
||||
@Schema(
|
||||
description = "Enable random font selection for text watermarks",
|
||||
defaultValue = "false")
|
||||
private Boolean randomFont;
|
||||
|
||||
@Schema(
|
||||
description = "Minimum font size (used when font size range is enabled)",
|
||||
defaultValue = "10")
|
||||
@DecimalMin(value = "1.0", message = "Font size minimum must be >= 1.0")
|
||||
@DecimalMax(value = "500.0", message = "Font size minimum must be <= 500.0")
|
||||
private Float fontSizeMin;
|
||||
|
||||
@Schema(
|
||||
description = "Maximum font size (used when font size range is enabled)",
|
||||
defaultValue = "100")
|
||||
@DecimalMin(value = "1.0", message = "Font size maximum must be >= 1.0")
|
||||
@DecimalMax(value = "500.0", message = "Font size maximum must be <= 500.0")
|
||||
private Float fontSizeMax;
|
||||
|
||||
@Schema(description = "Enable random color selection for watermarks", defaultValue = "false")
|
||||
private Boolean randomColor;
|
||||
|
||||
@Schema(
|
||||
description = "Enable per-letter font variation in text watermarks",
|
||||
defaultValue = "false")
|
||||
private Boolean perLetterFont;
|
||||
|
||||
@Schema(
|
||||
description = "Enable per-letter color variation in text watermarks",
|
||||
defaultValue = "false")
|
||||
private Boolean perLetterColor;
|
||||
|
||||
@Schema(
|
||||
description = "Enable per-letter size variation in text watermarks",
|
||||
defaultValue = "false")
|
||||
private Boolean perLetterSize;
|
||||
|
||||
@Schema(
|
||||
description = "Enable per-letter orientation variation in text watermarks",
|
||||
defaultValue = "false")
|
||||
private Boolean perLetterOrientation;
|
||||
|
||||
@Schema(
|
||||
description = "Number of fonts to randomly select from for per-letter font variation",
|
||||
defaultValue = "2")
|
||||
@Min(value = 1, message = "Font count must be at least 1")
|
||||
@Max(value = 20, message = "Font count must not exceed 20")
|
||||
private Integer perLetterFontCount;
|
||||
|
||||
@Schema(description = "Minimum font size for per-letter size variation", defaultValue = "10")
|
||||
@DecimalMin(value = "1.0", message = "Per-letter size minimum must be >= 1.0")
|
||||
@DecimalMax(value = "500.0", message = "Per-letter size minimum must be <= 500.0")
|
||||
private Float perLetterSizeMin;
|
||||
|
||||
@Schema(description = "Maximum font size for per-letter size variation", defaultValue = "100")
|
||||
@DecimalMin(value = "1.0", message = "Per-letter size maximum must be >= 1.0")
|
||||
@DecimalMax(value = "500.0", message = "Per-letter size maximum must be <= 500.0")
|
||||
private Float perLetterSizeMax;
|
||||
|
||||
@Schema(
|
||||
description = "Number of colors to randomly select from for per-letter color variation",
|
||||
defaultValue = "4")
|
||||
@Min(value = 1, message = "Color count must be at least 1")
|
||||
@Max(value = 20, message = "Color count must not exceed 20")
|
||||
private Integer perLetterColorCount;
|
||||
|
||||
@Schema(
|
||||
description = "Minimum rotation angle in degrees for per-letter orientation variation",
|
||||
defaultValue = "0")
|
||||
@DecimalMin(value = "-360.0", message = "Per-letter orientation minimum must be >= -360")
|
||||
@DecimalMax(value = "360.0", message = "Per-letter orientation minimum must be <= 360")
|
||||
private Float perLetterOrientationMin;
|
||||
|
||||
@Schema(
|
||||
description = "Maximum rotation angle in degrees for per-letter orientation variation",
|
||||
defaultValue = "360")
|
||||
@DecimalMin(value = "-360.0", message = "Per-letter orientation maximum must be >= -360")
|
||||
@DecimalMax(value = "360.0", message = "Per-letter orientation maximum must be <= 360")
|
||||
private Float perLetterOrientationMax;
|
||||
|
||||
@Schema(description = "Shading style for text watermarks (e.g., 'none', 'light', 'dark')")
|
||||
private String shading;
|
||||
|
||||
@Schema(description = "Enable random shading selection for watermarks", defaultValue = "false")
|
||||
private Boolean shadingRandom;
|
||||
|
||||
@Schema(description = "Random seed for deterministic randomness (optional, for testing)")
|
||||
private Long seed;
|
||||
|
||||
@Schema(
|
||||
description = "Scale factor for image watermarks (1.0 = original size)",
|
||||
defaultValue = "1.0")
|
||||
@DecimalMin(value = "0.1", message = "Image scale must be >= 0.1")
|
||||
@DecimalMax(value = "10.0", message = "Image scale must be <= 10.0")
|
||||
private Float imageScale;
|
||||
}
|
||||
|
||||
@ -1583,6 +1583,61 @@ watermark.selectText.10=Convert PDF to PDF-Image
|
||||
watermark.submit=Add Watermark
|
||||
watermark.type.1=Text
|
||||
watermark.type.2=Image
|
||||
watermark.advanced.title=Advanced Options
|
||||
watermark.advanced.count.label=Number of Watermarks
|
||||
watermark.advanced.count.help=Number of watermark instances per page (1-1000)
|
||||
watermark.advanced.position.label=Random Positioning
|
||||
watermark.advanced.position.help=Enable random placement instead of grid layout
|
||||
watermark.advanced.rotation.mode.label=Rotation Mode
|
||||
watermark.advanced.rotation.mode.fixed=Fixed Angle
|
||||
watermark.advanced.rotation.mode.range=Random Range
|
||||
watermark.advanced.rotation.min.label=Rotation Min (degrees)
|
||||
watermark.advanced.rotation.max.label=Rotation Max (degrees)
|
||||
watermark.advanced.rotation.range.help=Random angle between min and max
|
||||
watermark.advanced.rotation.range.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.mirroring.label=Random Mirroring
|
||||
watermark.advanced.mirroring.help=For images only
|
||||
watermark.advanced.mirroring.probability.label=Mirroring Probability
|
||||
watermark.advanced.mirroring.probability.help=Probability of mirroring (0.0-1.0)
|
||||
watermark.advanced.font.random.label=Random Font
|
||||
watermark.advanced.font.random.help=Enable random font selection for text watermarks
|
||||
watermark.advanced.font.name.label=Font Name
|
||||
watermark.advanced.font.name.help=Leave empty for default font
|
||||
watermark.advanced.font.size.mode.label=Font Size Mode
|
||||
watermark.advanced.font.size.mode.fixed=Fixed Size
|
||||
watermark.advanced.font.size.mode.range=Random Range
|
||||
watermark.advanced.font.size.min.label=Font Size Min
|
||||
watermark.advanced.font.size.max.label=Font Size Max
|
||||
watermark.advanced.font.size.range.help=Random size between min and max
|
||||
watermark.advanced.font.size.range.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.color.random.label=Random Color
|
||||
watermark.advanced.perLetter.title=Per-Letter Variations
|
||||
watermark.advanced.perLetter.font.label=Vary Font per Letter
|
||||
watermark.advanced.perLetter.font.count.label=Number of Fonts
|
||||
watermark.advanced.perLetter.font.count.help=Number of fonts to randomly select from (1-20, default: 2)
|
||||
watermark.advanced.perLetter.color.label=Vary Color per Letter
|
||||
watermark.advanced.perLetter.color.count.label=Number of Colors
|
||||
watermark.advanced.perLetter.color.count.help=Number of colors to randomly select from (1-12, default: 4)
|
||||
watermark.advanced.perLetter.size.label=Vary Size per Letter
|
||||
watermark.advanced.perLetter.size.min.label=Size Min
|
||||
watermark.advanced.perLetter.size.max.label=Size Max
|
||||
watermark.advanced.perLetter.size.help=Font size range (1-500, default: 10-100)
|
||||
watermark.advanced.perLetter.size.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.perLetter.orientation.label=Vary Orientation per Letter
|
||||
watermark.advanced.perLetter.orientation.min.label=Angle Min (degrees)
|
||||
watermark.advanced.perLetter.orientation.max.label=Angle Max (degrees)
|
||||
watermark.advanced.perLetter.orientation.help=Rotation angle range (-360 to 360, default: 0-360)
|
||||
watermark.advanced.perLetter.orientation.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.perLetter.help=Apply variations at the letter level for dynamic appearance
|
||||
watermark.advanced.shading.random.label=Random Shading
|
||||
watermark.advanced.shading.label=Shading Style
|
||||
watermark.advanced.shading.none=None
|
||||
watermark.advanced.shading.light=Light
|
||||
watermark.advanced.shading.dark=Dark
|
||||
watermark.advanced.image.scale.label=Image Scale Factor
|
||||
watermark.advanced.image.scale.help=Scale factor (1.0 = original size, 0.1-10.0)
|
||||
watermark.advanced.seed.label=Random Seed (optional)
|
||||
watermark.advanced.seed.help=For deterministic randomness (testing)
|
||||
|
||||
|
||||
#Change permissions
|
||||
|
||||
@ -1267,6 +1267,8 @@ flatten.flattenOnlyForms=Flatten only forms
|
||||
flatten.renderDpi=Rendering DPI (optional, recommended 150 DPI):
|
||||
flatten.renderDpi.help=Leave blank to use the system default. Higher DPI sharpens output but increases processing time and file size.
|
||||
flatten.submit=Flatten
|
||||
flatten.renderDpi=Rendering DPI (optional, recommended 150 DPI):
|
||||
flatten.renderDpi.help=Leave blank to use the system default. Higher DPI sharpens output but increases processing time and file size.
|
||||
|
||||
|
||||
#ScannerImageSplit
|
||||
@ -1568,6 +1570,61 @@ watermark.selectText.10=Convert PDF to PDF-Image
|
||||
watermark.submit=Add Watermark
|
||||
watermark.type.1=Text
|
||||
watermark.type.2=Image
|
||||
watermark.advanced.title=Advanced Options
|
||||
watermark.advanced.count.label=Number of Watermarks
|
||||
watermark.advanced.count.help=Number of watermark instances per page (1-1000)
|
||||
watermark.advanced.position.label=Random Positioning
|
||||
watermark.advanced.position.help=Enable random placement instead of grid layout
|
||||
watermark.advanced.rotation.mode.label=Rotation Mode
|
||||
watermark.advanced.rotation.mode.fixed=Fixed Angle
|
||||
watermark.advanced.rotation.mode.range=Random Range
|
||||
watermark.advanced.rotation.min.label=Rotation Min (degrees)
|
||||
watermark.advanced.rotation.max.label=Rotation Max (degrees)
|
||||
watermark.advanced.rotation.range.help=Random angle between min and max
|
||||
watermark.advanced.rotation.range.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.mirroring.label=Random Mirroring
|
||||
watermark.advanced.mirroring.help=For images only
|
||||
watermark.advanced.mirroring.probability.label=Mirroring Probability
|
||||
watermark.advanced.mirroring.probability.help=Probability of mirroring (0.0-1.0)
|
||||
watermark.advanced.font.random.label=Random Font
|
||||
watermark.advanced.font.random.help=Enable random font selection for text watermarks
|
||||
watermark.advanced.font.name.label=Font Name
|
||||
watermark.advanced.font.name.help=Leave empty for default font
|
||||
watermark.advanced.font.size.mode.label=Font Size Mode
|
||||
watermark.advanced.font.size.mode.fixed=Fixed Size
|
||||
watermark.advanced.font.size.mode.range=Random Range
|
||||
watermark.advanced.font.size.min.label=Font Size Min
|
||||
watermark.advanced.font.size.max.label=Font Size Max
|
||||
watermark.advanced.font.size.range.help=Random size between min and max
|
||||
watermark.advanced.font.size.range.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.color.random.label=Random Color
|
||||
watermark.advanced.perLetter.title=Per-Letter Variations
|
||||
watermark.advanced.perLetter.font.label=Vary Font per Letter
|
||||
watermark.advanced.perLetter.font.count.label=Number of Fonts
|
||||
watermark.advanced.perLetter.font.count.help=Number of fonts to randomly select from (1-20, default: 2)
|
||||
watermark.advanced.perLetter.color.label=Vary Color per Letter
|
||||
watermark.advanced.perLetter.color.count.label=Number of Colors
|
||||
watermark.advanced.perLetter.color.count.help=Number of colors to randomly select from (1-12, default: 4)
|
||||
watermark.advanced.perLetter.size.label=Vary Size per Letter
|
||||
watermark.advanced.perLetter.size.min.label=Size Min
|
||||
watermark.advanced.perLetter.size.max.label=Size Max
|
||||
watermark.advanced.perLetter.size.help=Font size range (1-500, default: 10-100)
|
||||
watermark.advanced.perLetter.size.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.perLetter.orientation.label=Vary Orientation per Letter
|
||||
watermark.advanced.perLetter.orientation.min.label=Angle Min (degrees)
|
||||
watermark.advanced.perLetter.orientation.max.label=Angle Max (degrees)
|
||||
watermark.advanced.perLetter.orientation.help=Rotation angle range (-360 to 360, default: 0-360)
|
||||
watermark.advanced.perLetter.orientation.error=Minimum must be less than or equal to maximum
|
||||
watermark.advanced.perLetter.help=Apply variations at the letter level for dynamic appearance
|
||||
watermark.advanced.shading.random.label=Random Shading
|
||||
watermark.advanced.shading.label=Shading Style
|
||||
watermark.advanced.shading.none=None
|
||||
watermark.advanced.shading.light=Light
|
||||
watermark.advanced.shading.dark=Dark
|
||||
watermark.advanced.image.scale.label=Image Scale Factor
|
||||
watermark.advanced.image.scale.help=Scale factor (1.0 = original size, 0.1-10.0)
|
||||
watermark.advanced.seed.label=Random Seed (optional)
|
||||
watermark.advanced.seed.help=For deterministic randomness (testing)
|
||||
|
||||
|
||||
#Change permissions
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
<option value="image" th:text="#{watermark.type.2}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="alphabetGroup" class="mb-3">
|
||||
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
|
||||
<select class="form-control" name="alphabet" id="alphabet-select">
|
||||
@ -51,10 +52,11 @@
|
||||
<input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fontSize" th:text="#{watermark.selectText.3}"></label>
|
||||
<input type="text" id="fontSize" name="fontSize" class="form-control" value="30">
|
||||
<div id="fontSizeFixedGroup" class="mb-3">
|
||||
<label for="fontSize" th:text="#{watermark.selectText.3}">Font Size</label>
|
||||
<input type="number" id="fontSize" name="fontSize" class="form-control" value="30" min="1" max="500" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="opacity" th:text="#{watermark.selectText.7}"></label>
|
||||
<input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()">
|
||||
@ -93,10 +95,11 @@
|
||||
appendPercentageSymbol();
|
||||
</script>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="rotation" th:text="#{watermark.selectText.4}"></label>
|
||||
<input type="text" id="rotation" name="rotation" class="form-control" value="45">
|
||||
<div id="rotationFixedGroup" class="mb-3">
|
||||
<label for="rotation" th:text="#{watermark.selectText.4}">Rotation Angle (degrees)</label>
|
||||
<input type="number" id="rotation" name="rotation" class="form-control" value="45" min="-360" max="360" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="widthSpacer" th:text="#{watermark.selectText.5}"></label>
|
||||
<input type="text" id="widthSpacer" name="widthSpacer" class="form-control" value="50">
|
||||
@ -105,9 +108,9 @@
|
||||
<label for="heightSpacer" th:text="#{watermark.selectText.6}"></label>
|
||||
<input type="text" id="heightSpacer" name="heightSpacer" class="form-control" value="50">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="customColor" class="form-label" th:text="#{watermark.customColor}">Custom
|
||||
Color</label>
|
||||
|
||||
<div id="customColorGroup" class="mb-3">
|
||||
<label for="customColor" class="form-label" th:text="#{watermark.customColor}">Custom Color</label>
|
||||
<div class="form-control form-control-color" style="background-color: #d3d3d3;">
|
||||
<input type="color" id="customColor" name="customColor" value="#d3d3d3">
|
||||
</div>
|
||||
@ -122,10 +125,202 @@
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Options -->
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-link" data-bs-toggle="collapse" data-bs-target="#advancedOptions" th:text="#{watermark.advanced.title}">
|
||||
Advanced Options
|
||||
</button>
|
||||
<div id="advancedOptions" class="collapse">
|
||||
<!-- Watermark Count -->
|
||||
<div class="mb-3">
|
||||
<label for="count" th:text="#{watermark.advanced.count.label}">Number of Watermarks</label>
|
||||
<input type="number" id="count" name="count" class="form-control" value="30" min="1" max="1000">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.count.help}">Number of watermark instances per page (1-1000)</small>
|
||||
</div>
|
||||
|
||||
<!-- Positioning Mode -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="randomPosition" name="randomPosition" checked>
|
||||
<label for="randomPosition" th:text="#{watermark.advanced.position.label}">Random Positioning</label>
|
||||
</div>
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.position.help}">Enable random placement instead of grid layout</small>
|
||||
</div>
|
||||
|
||||
<!-- Rotation Controls -->
|
||||
<div class="mb-3">
|
||||
<label th:text="#{watermark.advanced.rotation.mode.label}">Rotation Mode</label>
|
||||
<select class="form-control" id="rotationMode" onchange="toggleRotationMode()">
|
||||
<option value="fixed" th:text="#{watermark.advanced.rotation.mode.fixed}">Fixed Angle</option>
|
||||
<option value="range" th:text="#{watermark.advanced.rotation.mode.range}">Random Range</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="rotationRangeGroup" class="mb-3" style="display: none;">
|
||||
<label for="rotationMin" th:text="#{watermark.advanced.rotation.min.label}">Rotation Min (degrees)</label>
|
||||
<input type="number" id="rotationMin" name="rotationMin" class="form-control" value="0" min="-360" max="360" step="0.1">
|
||||
<label for="rotationMax" th:text="#{watermark.advanced.rotation.max.label}">Rotation Max (degrees)</label>
|
||||
<input type="number" id="rotationMax" name="rotationMax" class="form-control" value="45" min="-360" max="360" step="0.1">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.rotation.range.help}">Random angle between min and max</small>
|
||||
<div id="rotationRangeError" class="invalid-feedback" style="display: none;" th:text="#{watermark.advanced.rotation.range.error}">
|
||||
Minimum must be less than or equal to maximum
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mirroring Controls -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="randomMirroring" name="randomMirroring" onchange="toggleMirroringProbability()">
|
||||
<label for="randomMirroring" th:text="#{watermark.advanced.mirroring.label}">Random Mirroring</label>
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.mirroring.help}">For images only</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mirroringProbabilityGroup" class="mb-3" style="display: none;">
|
||||
<label for="mirroringProbability" th:text="#{watermark.advanced.mirroring.probability.label}">Mirroring Probability</label>
|
||||
<input type="number" id="mirroringProbability" name="mirroringProbability" class="form-control" value="0.5" min="0" max="1" step="0.1">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.mirroring.probability.help}">Probability of mirroring (0.0-1.0)</small>
|
||||
</div>
|
||||
|
||||
<!-- Font Controls (for text watermarks) -->
|
||||
<div id="fontGroup" class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="randomFont" name="randomFont" onchange="toggleFontMode()">
|
||||
<label for="randomFont" th:text="#{watermark.advanced.font.random.label}">Random Font</label>
|
||||
</div>
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.font.random.help}">Enable random font selection for text watermarks</small>
|
||||
</div>
|
||||
|
||||
<div id="fontNameGroup" class="mb-3">
|
||||
<label for="fontName" th:text="#{watermark.advanced.font.name.label}">Font Name</label>
|
||||
<input type="text" id="fontName" name="fontName" class="form-control" placeholder="e.g., Helvetica, Times-Roman">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.font.name.help}">Leave empty for default font</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{watermark.advanced.font.size.mode.label}">Font Size Mode</label>
|
||||
<select class="form-control" id="fontSizeMode" onchange="toggleFontSizeMode()">
|
||||
<option value="fixed" th:text="#{watermark.advanced.font.size.mode.fixed}">Fixed Size</option>
|
||||
<option value="range" th:text="#{watermark.advanced.font.size.mode.range}">Random Range</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="fontSizeRangeGroup" class="mb-3" style="display: none;">
|
||||
<label for="fontSizeMin" th:text="#{watermark.advanced.font.size.min.label}">Font Size Min</label>
|
||||
<input type="number" id="fontSizeMin" name="fontSizeMin" class="form-control" value="10" min="1" max="500" step="0.1">
|
||||
<label for="fontSizeMax" th:text="#{watermark.advanced.font.size.max.label}">Font Size Max</label>
|
||||
<input type="number" id="fontSizeMax" name="fontSizeMax" class="form-control" value="100" min="1" max="500" step="0.1">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.font.size.range.help}">Random size between min and max</small>
|
||||
<div id="fontSizeRangeError" class="invalid-feedback" style="display: none;" th:text="#{watermark.advanced.font.size.range.error}">
|
||||
Minimum must be less than or equal to maximum
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color Controls -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="randomColor" name="randomColor" onchange="toggleColorMode()">
|
||||
<label for="randomColor" th:text="#{watermark.advanced.color.random.label}">Random Color</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-Letter Variation Controls (for text watermarks) -->
|
||||
<div id="perLetterGroup" class="mb-3">
|
||||
<label th:text="#{watermark.advanced.perLetter.title}">Per-Letter Variations</label>
|
||||
|
||||
<!-- Per-Letter Font -->
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="perLetterFont" name="perLetterFont" onchange="togglePerLetterFontConfig()">
|
||||
<label for="perLetterFont" th:text="#{watermark.advanced.perLetter.font.label}">Vary Font per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterFontConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterFontCount" th:text="#{watermark.advanced.perLetter.font.count.label}">Number of Fonts</label>
|
||||
<input type="number" id="perLetterFontCount" name="perLetterFontCount" class="form-control" value="2" min="1" max="20">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.perLetter.font.count.help}">Number of fonts to randomly select from (1-20, default: 2)</small>
|
||||
</div>
|
||||
|
||||
<!-- Per-Letter Color -->
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="perLetterColor" name="perLetterColor" onchange="togglePerLetterColorConfig()">
|
||||
<label for="perLetterColor" th:text="#{watermark.advanced.perLetter.color.label}">Vary Color per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterColorConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterColorCount" th:text="#{watermark.advanced.perLetter.color.count.label}">Number of Colors</label>
|
||||
<input type="number" id="perLetterColorCount" name="perLetterColorCount" class="form-control" value="4" min="1" max="12">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.perLetter.color.count.help}">Number of colors to randomly select from (1-12, default: 4)</small>
|
||||
</div>
|
||||
|
||||
<!-- Per-Letter Size -->
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="perLetterSize" name="perLetterSize" onchange="togglePerLetterSizeConfig()">
|
||||
<label for="perLetterSize" th:text="#{watermark.advanced.perLetter.size.label}">Vary Size per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterSizeConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterSizeMin" th:text="#{watermark.advanced.perLetter.size.min.label}">Size Min</label>
|
||||
<input type="number" id="perLetterSizeMin" name="perLetterSizeMin" class="form-control" value="10" min="1" max="500" step="0.1">
|
||||
<label for="perLetterSizeMax" th:text="#{watermark.advanced.perLetter.size.max.label}">Size Max</label>
|
||||
<input type="number" id="perLetterSizeMax" name="perLetterSizeMax" class="form-control" value="100" min="1" max="500" step="0.1">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.perLetter.size.help}">Font size range (1-500, default: 10-100)</small>
|
||||
<div id="perLetterSizeError" class="invalid-feedback" style="display: none;" th:text="#{watermark.advanced.perLetter.size.error}">
|
||||
Minimum must be less than or equal to maximum
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-Letter Orientation -->
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="perLetterOrientation" name="perLetterOrientation" onchange="togglePerLetterOrientationConfig()">
|
||||
<label for="perLetterOrientation" th:text="#{watermark.advanced.perLetter.orientation.label}">Vary Orientation per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterOrientationConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterOrientationMin" th:text="#{watermark.advanced.perLetter.orientation.min.label}">Angle Min (degrees)</label>
|
||||
<input type="number" id="perLetterOrientationMin" name="perLetterOrientationMin" class="form-control" value="0" min="-360" max="360" step="1">
|
||||
<label for="perLetterOrientationMax" th:text="#{watermark.advanced.perLetter.orientation.max.label}">Angle Max (degrees)</label>
|
||||
<input type="number" id="perLetterOrientationMax" name="perLetterOrientationMax" class="form-control" value="360" min="-360" max="360" step="1">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.perLetter.orientation.help}">Rotation angle range (-360 to 360, default: 0-360)</small>
|
||||
<div id="perLetterOrientationError" class="invalid-feedback" style="display: none;" th:text="#{watermark.advanced.perLetter.orientation.error}">
|
||||
Minimum must be less than or equal to maximum
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.perLetter.help}">Apply variations at the letter level for dynamic appearance</small>
|
||||
</div>
|
||||
|
||||
<!-- Shading Controls (for text watermarks) -->
|
||||
<div id="shadingGroup" class="mb-3">
|
||||
<div class="form-check">
|
||||
<input id="shadingRandom" name="shadingRandom" type="checkbox" onchange="toggleShadingMode()">
|
||||
<label for="shadingRandom" th:text="#{watermark.advanced.shading.random.label}">Random Shading</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="shadingFixedGroup" class="mb-3">
|
||||
<label for="shading" th:text="#{watermark.advanced.shading.label}">Shading Style</label>
|
||||
<select class="form-control" id="shading" name="shading">
|
||||
<option value="none" th:text="#{watermark.advanced.shading.none}">None</option>
|
||||
<option value="light" th:text="#{watermark.advanced.shading.light}">Light</option>
|
||||
<option value="dark" th:text="#{watermark.advanced.shading.dark}">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Image Scale Control (for image watermarks) -->
|
||||
<div id="imageScaleGroup" class="mb-3" style="display: none;">
|
||||
<label for="imageScale" th:text="#{watermark.advanced.image.scale.label}">Image Scale Factor</label>
|
||||
<input type="number" id="imageScale" name="imageScale" class="form-control" value="1.0" min="0.1" max="10" step="0.1">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.image.scale.help}">Scale factor (1.0 = original size, 0.1-10.0)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="seed" th:text="#{watermark.advanced.seed.label}">Random Seed (optional)</label>
|
||||
<input type="number" id="seed" name="seed" class="form-control" placeholder="Leave empty for random">
|
||||
<small class="form-text text-muted" th:text="#{watermark.advanced.seed.help}">For deterministic randomness (testing)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input id="convertPDFToImage" name="convertPDFToImage" type="checkbox">
|
||||
@ -141,7 +336,7 @@
|
||||
const watermarkType = document.getElementById('watermarkType').value;
|
||||
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
|
||||
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
|
||||
const alphabetGroup = document.getElementById('alphabetGroup'); // This is the new addition
|
||||
const alphabetGroup = document.getElementById('alphabetGroup');
|
||||
const watermarkText = document.getElementById('watermarkText');
|
||||
const watermarkImage = document.getElementById('watermarkImage');
|
||||
|
||||
@ -154,6 +349,17 @@
|
||||
watermarkImageGroup.style.display = 'none';
|
||||
watermarkImage.required = false;
|
||||
alphabetGroup.style.display = 'block';
|
||||
|
||||
// Show text-specific controls
|
||||
const textSpecific = ['fontGroup', 'fontNameGroup', 'perLetterGroup', 'shadingGroup', 'shadingFixedGroup'];
|
||||
textSpecific.forEach(id => {
|
||||
const elem = document.getElementById(id);
|
||||
if (elem) elem.style.display = 'block';
|
||||
});
|
||||
|
||||
// Hide image-specific controls
|
||||
const imageScaleGroup = document.getElementById('imageScaleGroup');
|
||||
if (imageScaleGroup) imageScaleGroup.style.display = 'none';
|
||||
} else if (watermarkType === 'image') {
|
||||
if (watermarkImage.hasAttribute('required')) {
|
||||
watermarkImage.removeAttribute('required');
|
||||
@ -163,8 +369,251 @@
|
||||
watermarkImageGroup.style.display = 'block';
|
||||
watermarkImage.required = true;
|
||||
alphabetGroup.style.display = 'none';
|
||||
|
||||
// Hide text-specific controls
|
||||
const textSpecific = ['fontGroup', 'fontNameGroup', 'perLetterGroup', 'shadingGroup', 'shadingFixedGroup'];
|
||||
textSpecific.forEach(id => {
|
||||
const elem = document.getElementById(id);
|
||||
if (elem) elem.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show image-specific controls
|
||||
const imageScaleGroup = document.getElementById('imageScaleGroup');
|
||||
if (imageScaleGroup) imageScaleGroup.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRotationMode() {
|
||||
const mode = document.getElementById('rotationMode').value;
|
||||
const rotationFixed = document.getElementById('rotation');
|
||||
const rotationMin = document.getElementById('rotationMin');
|
||||
const rotationMax = document.getElementById('rotationMax');
|
||||
|
||||
if (mode === 'fixed') {
|
||||
document.getElementById('rotationFixedGroup').style.display = 'block';
|
||||
document.getElementById('rotationRangeGroup').style.display = 'none';
|
||||
if (rotationFixed) rotationFixed.disabled = false;
|
||||
if (rotationMin) rotationMin.disabled = true;
|
||||
if (rotationMax) rotationMax.disabled = true;
|
||||
} else {
|
||||
document.getElementById('rotationFixedGroup').style.display = 'none';
|
||||
document.getElementById('rotationRangeGroup').style.display = 'block';
|
||||
if (rotationFixed) rotationFixed.disabled = true;
|
||||
if (rotationMin) rotationMin.disabled = false;
|
||||
if (rotationMax) rotationMax.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMirroringProbability() {
|
||||
const enabled = document.getElementById('randomMirroring').checked;
|
||||
document.getElementById('mirroringProbabilityGroup').style.display =
|
||||
enabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleFontMode() {
|
||||
const randomFont = document.getElementById('randomFont').checked;
|
||||
document.getElementById('fontNameGroup').style.display =
|
||||
randomFont ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function toggleFontSizeMode() {
|
||||
const mode = document.getElementById('fontSizeMode').value;
|
||||
const fontSizeFixed = document.getElementById('fontSize');
|
||||
const fontSizeMin = document.getElementById('fontSizeMin');
|
||||
const fontSizeMax = document.getElementById('fontSizeMax');
|
||||
|
||||
if (mode === 'fixed') {
|
||||
document.getElementById('fontSizeFixedGroup').style.display = 'block';
|
||||
document.getElementById('fontSizeRangeGroup').style.display = 'none';
|
||||
if (fontSizeFixed) fontSizeFixed.disabled = false;
|
||||
if (fontSizeMin) fontSizeMin.disabled = true;
|
||||
if (fontSizeMax) fontSizeMax.disabled = true;
|
||||
} else {
|
||||
document.getElementById('fontSizeFixedGroup').style.display = 'none';
|
||||
document.getElementById('fontSizeRangeGroup').style.display = 'block';
|
||||
if (fontSizeFixed) fontSizeFixed.disabled = true;
|
||||
if (fontSizeMin) fontSizeMin.disabled = false;
|
||||
if (fontSizeMax) fontSizeMax.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleColorMode() {
|
||||
const random = document.getElementById('randomColor').checked;
|
||||
document.getElementById('customColorGroup').style.display =
|
||||
random ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function toggleShadingMode() {
|
||||
const random = document.getElementById('shadingRandom').checked;
|
||||
document.getElementById('shadingFixedGroup').style.display =
|
||||
random ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function togglePerLetterFontConfig() {
|
||||
const checkbox = document.getElementById('perLetterFont');
|
||||
const config = document.getElementById('perLetterFontConfig');
|
||||
config.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function togglePerLetterColorConfig() {
|
||||
const checkbox = document.getElementById('perLetterColor');
|
||||
const config = document.getElementById('perLetterColorConfig');
|
||||
config.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function togglePerLetterSizeConfig() {
|
||||
const checkbox = document.getElementById('perLetterSize');
|
||||
const config = document.getElementById('perLetterSizeConfig');
|
||||
config.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function togglePerLetterOrientationConfig() {
|
||||
const checkbox = document.getElementById('perLetterOrientation');
|
||||
const config = document.getElementById('perLetterOrientationConfig');
|
||||
config.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Add range validation
|
||||
function addRangeValidation(minId, maxId, errorMsgId) {
|
||||
const minInput = document.getElementById(minId);
|
||||
const maxInput = document.getElementById(maxId);
|
||||
const errorMsg = document.getElementById(errorMsgId);
|
||||
|
||||
if (!minInput || !maxInput) return;
|
||||
|
||||
function validate() {
|
||||
const min = parseFloat(minInput.value);
|
||||
const max = parseFloat(maxInput.value);
|
||||
|
||||
if (min > max) {
|
||||
minInput.classList.add('is-invalid');
|
||||
maxInput.classList.add('is-invalid');
|
||||
if (errorMsg) errorMsg.style.display = 'block';
|
||||
} else {
|
||||
minInput.classList.remove('is-invalid');
|
||||
maxInput.classList.remove('is-invalid');
|
||||
if (errorMsg) errorMsg.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
minInput.addEventListener('input', validate);
|
||||
maxInput.addEventListener('input', validate);
|
||||
}
|
||||
|
||||
// Apply range validations on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
addRangeValidation('rotationMin', 'rotationMax', 'rotationRangeError');
|
||||
addRangeValidation('fontSizeMin', 'fontSizeMax', 'fontSizeRangeError');
|
||||
addRangeValidation('perLetterSizeMin', 'perLetterSizeMax', 'perLetterSizeError');
|
||||
addRangeValidation('perLetterOrientationMin', 'perLetterOrientationMax', 'perLetterOrientationError');
|
||||
|
||||
// Initialize toggle states to disable hidden inputs
|
||||
toggleRotationMode();
|
||||
toggleFontSizeMode();
|
||||
});
|
||||
|
||||
// Form submission validation
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
const errors = [];
|
||||
|
||||
// Validate count
|
||||
const count = parseInt(document.getElementById('count').value);
|
||||
if (count < 0 || count > 1000) {
|
||||
errors.push('Count must be between 0 and 1000');
|
||||
}
|
||||
|
||||
// Validate rotation range
|
||||
const rotationMode = document.getElementById('rotationMode').value;
|
||||
if (rotationMode === 'range') {
|
||||
const rotMin = parseFloat(document.getElementById('rotationMin').value);
|
||||
const rotMax = parseFloat(document.getElementById('rotationMax').value);
|
||||
if (rotMin > rotMax) {
|
||||
errors.push('Rotation minimum must be less than or equal to maximum');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate font size range
|
||||
const fontSizeMode = document.getElementById('fontSizeMode').value;
|
||||
if (fontSizeMode === 'range') {
|
||||
const sizeMin = parseFloat(document.getElementById('fontSizeMin').value);
|
||||
const sizeMax = parseFloat(document.getElementById('fontSizeMax').value);
|
||||
if (sizeMin > sizeMax) {
|
||||
errors.push('Font size minimum must be less than or equal to maximum');
|
||||
}
|
||||
if (sizeMin < 1 || sizeMax > 500) {
|
||||
errors.push('Font size must be between 1 and 500');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate mirroring probability
|
||||
if (document.getElementById('randomMirroring').checked) {
|
||||
const prob = parseFloat(document.getElementById('mirroringProbability').value);
|
||||
if (prob < 0 || prob > 1) {
|
||||
errors.push('Mirroring probability must be between 0.0 and 1.0');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate image scale
|
||||
const watermarkType = document.getElementById('watermarkType').value;
|
||||
if (watermarkType === 'image') {
|
||||
const scale = parseFloat(document.getElementById('imageScale').value);
|
||||
if (scale < 0.1 || scale > 10) {
|
||||
errors.push('Image scale must be between 0.1 and 10.0');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate opacity
|
||||
const opacity = parseFloat(document.getElementById('opacityReal').value);
|
||||
if (opacity < 0 || opacity > 1) {
|
||||
errors.push('Opacity must be between 0.0 and 1.0');
|
||||
}
|
||||
|
||||
// Validate per-letter configurations
|
||||
if (document.getElementById('perLetterFont').checked) {
|
||||
const fontCount = parseInt(document.getElementById('perLetterFontCount').value);
|
||||
if (fontCount < 1 || fontCount > 20) {
|
||||
errors.push('Per-letter font count must be between 1 and 20');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getElementById('perLetterColor').checked) {
|
||||
const colorCount = parseInt(document.getElementById('perLetterColorCount').value);
|
||||
if (colorCount < 1 || colorCount > 12) {
|
||||
errors.push('Per-letter color count must be between 1 and 12');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getElementById('perLetterSize').checked) {
|
||||
const sizeMin = parseFloat(document.getElementById('perLetterSizeMin').value);
|
||||
const sizeMax = parseFloat(document.getElementById('perLetterSizeMax').value);
|
||||
if (sizeMin > sizeMax) {
|
||||
errors.push('Per-letter size minimum must be less than or equal to maximum');
|
||||
}
|
||||
if (sizeMin < 1 || sizeMax > 500) {
|
||||
errors.push('Per-letter size must be between 1 and 500');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getElementById('perLetterOrientation').checked) {
|
||||
const orientMin = parseFloat(document.getElementById('perLetterOrientationMin').value);
|
||||
const orientMax = parseFloat(document.getElementById('perLetterOrientationMax').value);
|
||||
if (orientMin > orientMax) {
|
||||
errors.push('Per-letter orientation minimum must be less than or equal to maximum');
|
||||
}
|
||||
if (orientMin < -360 || orientMax > 360) {
|
||||
errors.push('Per-letter orientation must be between -360 and 360');
|
||||
}
|
||||
}
|
||||
|
||||
// Display errors
|
||||
if (errors.length > 0) {
|
||||
e.preventDefault();
|
||||
alert('Please fix the following errors:\n\n' + errors.join('\n'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,705 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("Watermark Controller Integration Tests")
|
||||
class WatermarkControllerIntegrationTest {
|
||||
|
||||
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@InjectMocks private WatermarkController watermarkController;
|
||||
|
||||
private MockMultipartFile testPdfFile;
|
||||
private MockMultipartFile testImageFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
// Create a simple test PDF
|
||||
PDDocument document = new PDDocument();
|
||||
document.addPage(new org.apache.pdfbox.pdmodel.PDPage());
|
||||
File tempPdf = File.createTempFile("test", ".pdf");
|
||||
document.save(tempPdf);
|
||||
document.close();
|
||||
|
||||
byte[] pdfBytes = Files.readAllBytes(tempPdf.toPath());
|
||||
testPdfFile = new MockMultipartFile("fileInput", "test.pdf", "application/pdf", pdfBytes);
|
||||
tempPdf.delete();
|
||||
|
||||
// Create a simple test image (1x1 pixel PNG)
|
||||
byte[] imageBytes =
|
||||
new byte[] {
|
||||
(byte) 0x89,
|
||||
0x50,
|
||||
0x4E,
|
||||
0x47,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x1A,
|
||||
0x0A,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0D,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x08,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1F,
|
||||
0x15,
|
||||
(byte) 0xC4,
|
||||
(byte) 0x89,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41,
|
||||
0x54,
|
||||
0x78,
|
||||
(byte) 0x9C,
|
||||
0x63,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x01,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x2D,
|
||||
(byte) 0xB4,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4E,
|
||||
0x44,
|
||||
(byte) 0xAE,
|
||||
0x42,
|
||||
0x60,
|
||||
(byte) 0x82
|
||||
};
|
||||
testImageFile =
|
||||
new MockMultipartFile("watermarkImage", "test.png", "image/png", imageBytes);
|
||||
|
||||
// Configure mock to return a real PDDocument when load is called
|
||||
when(pdfDocumentFactory.load(any(org.springframework.web.multipart.MultipartFile.class)))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
org.springframework.web.multipart.MultipartFile file =
|
||||
invocation.getArgument(0);
|
||||
return Loader.loadPDF(file.getBytes());
|
||||
});
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Text Watermark Integration Tests")
|
||||
class TextWatermarkIntegrationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with fixed positioning")
|
||||
void testTextWatermarkFixedPositioning() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Test Watermark");
|
||||
request.setOpacity(0.5f);
|
||||
request.setFontSize(30f);
|
||||
request.setRotation(45f);
|
||||
request.setWidthSpacer(100);
|
||||
request.setHeightSpacer(100);
|
||||
request.setCustomColor("#FF0000");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomPosition(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
assertNotNull(response.getBody(), "Response body should not be null");
|
||||
assertTrue(response.getBody().length > 0, "Response should contain PDF data");
|
||||
|
||||
// Verify the output is a valid PDF
|
||||
try (PDDocument resultDoc = Loader.loadPDF(response.getBody())) {
|
||||
assertEquals(1, resultDoc.getNumberOfPages(), "Should have 1 page");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with random positioning")
|
||||
void testTextWatermarkRandomPositioning() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Random");
|
||||
request.setOpacity(0.7f);
|
||||
request.setFontSize(20f);
|
||||
request.setCustomColor("#0000FF");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomPosition(true);
|
||||
request.setCount(5);
|
||||
request.setSeed(12345L); // Use seed for deterministic testing
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
assertTrue(response.getBody().length > 0, "Response should contain PDF data");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with rotation range")
|
||||
void testTextWatermarkWithRotationRange() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Rotated");
|
||||
request.setOpacity(0.5f);
|
||||
request.setFontSize(25f);
|
||||
request.setCustomColor("#00FF00");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomPosition(true);
|
||||
request.setCount(3);
|
||||
request.setRotationMin(-45f);
|
||||
request.setRotationMax(45f);
|
||||
request.setSeed(54321L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with per-letter font variation")
|
||||
void testTextWatermarkPerLetterFont() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Mixed");
|
||||
request.setOpacity(0.6f);
|
||||
request.setFontSize(30f);
|
||||
request.setCustomColor("#FF00FF");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setPerLetterFont(true);
|
||||
request.setPerLetterFontCount(3);
|
||||
request.setSeed(99999L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with per-letter color variation")
|
||||
void testTextWatermarkPerLetterColor() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Colors");
|
||||
request.setOpacity(0.8f);
|
||||
request.setFontSize(28f);
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setPerLetterColor(true);
|
||||
request.setPerLetterColorCount(4);
|
||||
request.setSeed(11111L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with per-letter size variation")
|
||||
void testTextWatermarkPerLetterSize() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Sizes");
|
||||
request.setOpacity(0.5f);
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setPerLetterSize(true);
|
||||
request.setPerLetterSizeMin(15f);
|
||||
request.setPerLetterSizeMax(35f);
|
||||
request.setSeed(22222L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with per-letter orientation variation")
|
||||
void testTextWatermarkPerLetterOrientation() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Tilted");
|
||||
request.setOpacity(0.6f);
|
||||
request.setFontSize(25f);
|
||||
request.setCustomColor("#FFAA00");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setPerLetterOrientation(true);
|
||||
request.setPerLetterOrientationMin(-30f);
|
||||
request.setPerLetterOrientationMax(30f);
|
||||
request.setSeed(33333L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with all per-letter variations enabled")
|
||||
void testTextWatermarkAllPerLetterVariations() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Chaos");
|
||||
request.setOpacity(0.7f);
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setPerLetterFont(true);
|
||||
request.setPerLetterFontCount(2);
|
||||
request.setPerLetterColor(true);
|
||||
request.setPerLetterColorCount(3);
|
||||
request.setPerLetterSize(true);
|
||||
request.setPerLetterSizeMin(20f);
|
||||
request.setPerLetterSizeMax(40f);
|
||||
request.setPerLetterOrientation(true);
|
||||
request.setPerLetterOrientationMin(-20f);
|
||||
request.setPerLetterOrientationMax(20f);
|
||||
request.setSeed(44444L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with random font")
|
||||
void testTextWatermarkRandomFont() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Random Font");
|
||||
request.setOpacity(0.5f);
|
||||
request.setFontSize(30f);
|
||||
request.setCustomColor("#AA00FF");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomFont(true);
|
||||
request.setSeed(55555L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with random color")
|
||||
void testTextWatermarkRandomColor() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Random Color");
|
||||
request.setOpacity(0.6f);
|
||||
request.setFontSize(28f);
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomColor(true);
|
||||
request.setSeed(66666L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with font size range")
|
||||
void testTextWatermarkFontSizeRange() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Size Range");
|
||||
request.setOpacity(0.5f);
|
||||
request.setCustomColor("#00AAFF");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomPosition(true);
|
||||
request.setCount(3);
|
||||
request.setFontSizeMin(20f);
|
||||
request.setFontSizeMax(40f);
|
||||
request.setSeed(77777L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with opacity variations")
|
||||
void testTextWatermarkOpacityVariations() throws Exception {
|
||||
// Test minimum opacity
|
||||
AddWatermarkRequest request1 = new AddWatermarkRequest();
|
||||
request1.setFileInput(testPdfFile);
|
||||
request1.setWatermarkType("text");
|
||||
request1.setWatermarkText("Min Opacity");
|
||||
request1.setOpacity(0.0f);
|
||||
request1.setFontSize(30f);
|
||||
request1.setCustomColor("#000000");
|
||||
request1.setConvertPDFToImage(false);
|
||||
request1.setAlphabet("roman");
|
||||
|
||||
ResponseEntity<byte[]> response1 = watermarkController.addWatermark(request1);
|
||||
assertEquals(200, response1.getStatusCode().value(), "Should handle min opacity");
|
||||
|
||||
// Test maximum opacity
|
||||
AddWatermarkRequest request2 = new AddWatermarkRequest();
|
||||
request2.setFileInput(testPdfFile);
|
||||
request2.setWatermarkType("text");
|
||||
request2.setWatermarkText("Max Opacity");
|
||||
request2.setOpacity(1.0f);
|
||||
request2.setFontSize(30f);
|
||||
request2.setCustomColor("#000000");
|
||||
request2.setConvertPDFToImage(false);
|
||||
request2.setAlphabet("roman");
|
||||
|
||||
ResponseEntity<byte[]> response2 = watermarkController.addWatermark(request2);
|
||||
assertEquals(200, response2.getStatusCode().value(), "Should handle max opacity");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with shading")
|
||||
void testTextWatermarkWithShading() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Shaded");
|
||||
request.setOpacity(0.6f);
|
||||
request.setFontSize(30f);
|
||||
request.setCustomColor("#FF0000");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setShading("light");
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with random shading")
|
||||
void testTextWatermarkRandomShading() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Random Shade");
|
||||
request.setOpacity(0.6f);
|
||||
request.setFontSize(30f);
|
||||
request.setCustomColor("#0000FF");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setShadingRandom(true);
|
||||
request.setSeed(88888L);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Image Watermark Integration Tests")
|
||||
class ImageWatermarkIntegrationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply image watermark with default settings")
|
||||
void testImageWatermarkDefault() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.5f);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
assertTrue(response.getBody().length > 0, "Response should contain PDF data");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply image watermark with scaling")
|
||||
void testImageWatermarkWithScaling() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.7f);
|
||||
request.setImageScale(2.0f);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply image watermark with rotation")
|
||||
void testImageWatermarkWithRotation() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.6f);
|
||||
request.setRotation(45f);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply image watermark with rotation range")
|
||||
void testImageWatermarkWithRotationRange() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.5f);
|
||||
request.setRandomPosition(true);
|
||||
request.setCount(3);
|
||||
request.setRotationMin(-30f);
|
||||
request.setRotationMax(30f);
|
||||
request.setSeed(12121L);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply image watermark with mirroring")
|
||||
void testImageWatermarkWithMirroring() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.6f);
|
||||
request.setRandomMirroring(true);
|
||||
request.setMirroringProbability(1.0f); // Always mirror for testing
|
||||
request.setSeed(23232L);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply image watermark with scaling, rotation, and mirroring")
|
||||
void testImageWatermarkCombined() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.7f);
|
||||
request.setImageScale(1.5f);
|
||||
request.setRotation(30f);
|
||||
request.setRandomMirroring(true);
|
||||
request.setMirroringProbability(0.5f);
|
||||
request.setSeed(34343L);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply multiple image watermarks with random positioning")
|
||||
void testMultipleImageWatermarksRandom() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.5f);
|
||||
request.setRandomPosition(true);
|
||||
request.setCount(5);
|
||||
request.setSeed(45454L);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Convert to Image Tests")
|
||||
class ConvertToImageTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should convert PDF to image after applying text watermark")
|
||||
void testConvertToImageWithTextWatermark() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Convert Test");
|
||||
request.setOpacity(0.5f);
|
||||
request.setFontSize(30f);
|
||||
request.setCustomColor("#FF0000");
|
||||
request.setConvertPDFToImage(true);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
assertTrue(response.getBody().length > 0, "Response should contain PDF data");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should convert PDF to image after applying image watermark")
|
||||
void testConvertToImageWithImageWatermark() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(testImageFile);
|
||||
request.setOpacity(0.6f);
|
||||
request.setConvertPDFToImage(true);
|
||||
|
||||
ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
|
||||
|
||||
assertNotNull(response, "Response should not be null");
|
||||
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Deterministic Randomness Tests")
|
||||
class DeterministicRandomnessTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should produce identical results with same seed")
|
||||
void testDeterministicWithSeed() throws Exception {
|
||||
AddWatermarkRequest request1 = new AddWatermarkRequest();
|
||||
request1.setFileInput(testPdfFile);
|
||||
request1.setWatermarkType("text");
|
||||
request1.setWatermarkText("Deterministic");
|
||||
request1.setOpacity(0.5f);
|
||||
request1.setFontSize(25f);
|
||||
request1.setCustomColor("#0000FF");
|
||||
request1.setRandomPosition(true);
|
||||
request1.setCount(5);
|
||||
request1.setSeed(99999L);
|
||||
request1.setConvertPDFToImage(false);
|
||||
request1.setAlphabet("roman");
|
||||
|
||||
ResponseEntity<byte[]> response1 = watermarkController.addWatermark(request1);
|
||||
|
||||
AddWatermarkRequest request2 = new AddWatermarkRequest();
|
||||
request2.setFileInput(testPdfFile);
|
||||
request2.setWatermarkType("text");
|
||||
request2.setWatermarkText("Deterministic");
|
||||
request2.setOpacity(0.5f);
|
||||
request2.setFontSize(25f);
|
||||
request2.setCustomColor("#0000FF");
|
||||
request2.setRandomPosition(true);
|
||||
request2.setCount(5);
|
||||
request2.setSeed(99999L);
|
||||
request2.setConvertPDFToImage(false);
|
||||
request2.setAlphabet("roman");
|
||||
|
||||
ResponseEntity<byte[]> response2 = watermarkController.addWatermark(request2);
|
||||
|
||||
assertNotNull(response1, "First response should not be null");
|
||||
assertNotNull(response2, "Second response should not be null");
|
||||
assertEquals(200, response1.getStatusCode().value(), "First request should succeed");
|
||||
assertEquals(200, response2.getStatusCode().value(), "Second request should succeed");
|
||||
|
||||
// Note: Exact byte comparison may fail due to PDF metadata (timestamps, etc.)
|
||||
// But both should produce valid PDFs of similar size
|
||||
assertTrue(
|
||||
Math.abs(response1.getBody().length - response2.getBody().length) < 1000,
|
||||
"PDFs should be similar in size");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,978 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@DisplayName("Watermark Validation Tests")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WatermarkValidationTest {
|
||||
|
||||
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@InjectMocks private WatermarkController watermarkController;
|
||||
|
||||
private AddWatermarkRequest request;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
request = new AddWatermarkRequest();
|
||||
MockMultipartFile mockPdfFile =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", "application/pdf", "test content".getBytes());
|
||||
request.setFileInput(mockPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Test Watermark");
|
||||
request.setOpacity(0.5f);
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
// Mock PDDocument with empty pages to avoid NullPointerException
|
||||
// Use lenient() because some tests don't reach the document loading code
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
lenient().when(pdfDocumentFactory.load(any(MultipartFile.class))).thenReturn(mockDocument);
|
||||
|
||||
// Mock getPages() to return an empty iterable
|
||||
org.apache.pdfbox.pdmodel.PDPageTree mockPageTree =
|
||||
mock(org.apache.pdfbox.pdmodel.PDPageTree.class);
|
||||
lenient().when(mockDocument.getPages()).thenReturn(mockPageTree);
|
||||
lenient().when(mockPageTree.iterator()).thenReturn(Collections.emptyIterator());
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Opacity Validation Tests")
|
||||
class OpacityValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject opacity below 0.0")
|
||||
void testOpacityBelowMinimum() {
|
||||
request.setOpacity(-0.1f);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Opacity must be between 0.0 and 1.0"),
|
||||
"Error message should mention opacity bounds");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject opacity above 1.0")
|
||||
void testOpacityAboveMaximum() {
|
||||
request.setOpacity(1.1f);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Opacity must be between 0.0 and 1.0"),
|
||||
"Error message should mention opacity bounds");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Rotation Range Validation Tests")
|
||||
class RotationRangeValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid rotation range")
|
||||
void testValidRotationRange() {
|
||||
request.setRotationMin(-45f);
|
||||
request.setRotationMax(45f);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept equal rotation min and max")
|
||||
void testEqualRotationMinMax() {
|
||||
request.setRotationMin(30f);
|
||||
request.setRotationMax(30f);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject rotation min greater than max")
|
||||
void testRotationMinGreaterThanMax() {
|
||||
request.setRotationMin(45f);
|
||||
request.setRotationMax(-45f);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Rotation minimum")
|
||||
&& exception.getMessage().contains("must be less than or equal to"),
|
||||
"Error message should mention rotation range constraint");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept null rotation values")
|
||||
void testNullRotationValues() {
|
||||
request.setRotationMin(null);
|
||||
request.setRotationMax(null);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept only rotation min set")
|
||||
void testOnlyRotationMinSet() {
|
||||
request.setRotationMin(-30f);
|
||||
request.setRotationMax(null);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept only rotation max set")
|
||||
void testOnlyRotationMaxSet() {
|
||||
request.setRotationMin(null);
|
||||
request.setRotationMax(30f);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Font Size Range Validation Tests")
|
||||
class FontSizeRangeValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid font size range")
|
||||
void testValidFontSizeRange() {
|
||||
request.setFontSizeMin(10f);
|
||||
request.setFontSizeMax(50f);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept equal font size min and max")
|
||||
void testEqualFontSizeMinMax() {
|
||||
request.setFontSizeMin(30f);
|
||||
request.setFontSizeMax(30f);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject font size min greater than max")
|
||||
void testFontSizeMinGreaterThanMax() {
|
||||
request.setFontSizeMin(50f);
|
||||
request.setFontSizeMax(10f);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Font size minimum")
|
||||
&& exception.getMessage().contains("must be less than or equal to"),
|
||||
"Error message should mention font size range constraint");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept null font size values")
|
||||
void testNullFontSizeValues() {
|
||||
request.setFontSizeMin(null);
|
||||
request.setFontSizeMax(null);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Color Format Validation Tests")
|
||||
class ColorFormatValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid 6-digit hex color")
|
||||
void testValidHexColor6Digits() {
|
||||
request.setCustomColor("#FF0000");
|
||||
request.setRandomColor(false);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid 8-digit hex color with alpha")
|
||||
void testValidHexColor8Digits() {
|
||||
request.setCustomColor("#FF0000AA");
|
||||
request.setRandomColor(false);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept lowercase hex color")
|
||||
void testValidHexColorLowercase() {
|
||||
request.setCustomColor("#ff0000");
|
||||
request.setRandomColor(false);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept mixed case hex color")
|
||||
void testValidHexColorMixedCase() {
|
||||
request.setCustomColor("#Ff00Aa");
|
||||
request.setRandomColor(false);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject hex color without hash")
|
||||
void testInvalidHexColorNoHash() {
|
||||
request.setCustomColor("FF0000");
|
||||
request.setRandomColor(false);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Invalid color format"),
|
||||
"Error message should mention invalid color format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject hex color with wrong length")
|
||||
void testInvalidHexColorWrongLength() {
|
||||
request.setCustomColor("#FFF");
|
||||
request.setRandomColor(false);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Invalid color format"),
|
||||
"Error message should mention invalid color format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject hex color with invalid characters")
|
||||
void testInvalidHexColorInvalidChars() {
|
||||
request.setCustomColor("#GGGGGG");
|
||||
request.setRandomColor(false);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Invalid color format"),
|
||||
"Error message should mention invalid color format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should skip color validation when using random color")
|
||||
void testSkipValidationWithRandomColor() {
|
||||
request.setCustomColor("invalid");
|
||||
request.setRandomColor(true);
|
||||
|
||||
// Should not throw exception because random color is enabled
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should skip color validation when custom color is null")
|
||||
void testSkipValidationWithNullColor() {
|
||||
request.setCustomColor(null);
|
||||
request.setRandomColor(false);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Mirroring Probability Validation Tests")
|
||||
class MirroringProbabilityValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid mirroring probability values")
|
||||
void testValidMirroringProbability() {
|
||||
request.setMirroringProbability(0.0f);
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
|
||||
request.setMirroringProbability(0.5f);
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
|
||||
request.setMirroringProbability(1.0f);
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject mirroring probability below 0.0")
|
||||
void testMirroringProbabilityBelowMinimum() {
|
||||
request.setMirroringProbability(-0.1f);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception
|
||||
.getMessage()
|
||||
.contains("Mirroring probability must be between 0.0 and 1.0"),
|
||||
"Error message should mention mirroring probability bounds");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject mirroring probability above 1.0")
|
||||
void testMirroringProbabilityAboveMaximum() {
|
||||
request.setMirroringProbability(1.5f);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception
|
||||
.getMessage()
|
||||
.contains("Mirroring probability must be between 0.0 and 1.0"),
|
||||
"Error message should mention mirroring probability bounds");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept null mirroring probability")
|
||||
void testNullMirroringProbability() {
|
||||
request.setMirroringProbability(null);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Watermark Type Validation Tests")
|
||||
class WatermarkTypeValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept 'text' watermark type")
|
||||
void testTextWatermarkType() {
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Test");
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept 'image' watermark type")
|
||||
void testImageWatermarkType() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.png", "image/png", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept case-insensitive watermark type")
|
||||
void testCaseInsensitiveWatermarkType() {
|
||||
request.setWatermarkType("TEXT");
|
||||
request.setWatermarkText("Test");
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
|
||||
request.setWatermarkType("Image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.png", "image/png", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject invalid watermark type")
|
||||
void testInvalidWatermarkType() {
|
||||
request.setWatermarkType("invalid");
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Watermark type must be 'text' or 'image'"),
|
||||
"Error message should mention valid watermark types");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject null watermark type")
|
||||
void testNullWatermarkType() {
|
||||
request.setWatermarkType(null);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Watermark type must be 'text' or 'image'"),
|
||||
"Error message should mention valid watermark types");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject text watermark without text")
|
||||
void testTextWatermarkWithoutText() {
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText(null);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Watermark text is required"),
|
||||
"Error message should mention missing watermark text");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject text watermark with empty text")
|
||||
void testTextWatermarkWithEmptyText() {
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText(" ");
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Watermark text is required"),
|
||||
"Error message should mention missing watermark text");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject image watermark without image")
|
||||
void testImageWatermarkWithoutImage() {
|
||||
request.setWatermarkType("image");
|
||||
request.setWatermarkImage(null);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Watermark image is required"),
|
||||
"Error message should mention missing watermark image");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject image watermark with empty image")
|
||||
void testImageWatermarkWithEmptyImage() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile emptyImage =
|
||||
new MockMultipartFile("watermarkImage", "", "image/png", new byte[0]);
|
||||
request.setWatermarkImage(emptyImage);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Watermark image is required"),
|
||||
"Error message should mention missing watermark image");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Image Type Validation Tests")
|
||||
class ImageTypeValidationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept PNG image")
|
||||
void testAcceptPngImage() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.png", "image/png", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept JPG image")
|
||||
void testAcceptJpgImage() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.jpg", "image/jpeg", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept JPEG image")
|
||||
void testAcceptJpegImage() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage",
|
||||
"test.jpeg",
|
||||
"image/jpeg",
|
||||
"image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept GIF image")
|
||||
void testAcceptGifImage() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.gif", "image/gif", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept BMP image")
|
||||
void testAcceptBmpImage() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.bmp", "image/bmp", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
assertDoesNotThrow(() -> watermarkController.addWatermark(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject unsupported image content type")
|
||||
void testRejectUnsupportedImageContentType() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage",
|
||||
"test.svg",
|
||||
"image/svg+xml",
|
||||
"image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Unsupported image type"),
|
||||
"Error message should mention unsupported image type");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject unsupported image file extension")
|
||||
void testRejectUnsupportedImageExtension() {
|
||||
request.setWatermarkType("image");
|
||||
MockMultipartFile imageFile =
|
||||
new MockMultipartFile(
|
||||
"watermarkImage", "test.svg", "image/png", "image content".getBytes());
|
||||
request.setWatermarkImage(imageFile);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> watermarkController.addWatermark(request));
|
||||
|
||||
assertTrue(
|
||||
exception.getMessage().contains("Unsupported image file extension"),
|
||||
"Error message should mention unsupported file extension");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Annotation-based Validation Tests")
|
||||
class AnnotationBasedValidationTests {
|
||||
|
||||
private Validator validator;
|
||||
|
||||
@BeforeEach
|
||||
void setUpValidator() {
|
||||
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
|
||||
validator = factory.getValidator();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject count below minimum of 1")
|
||||
void testCountMinimum() {
|
||||
// Arrange
|
||||
request.setCount(0);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("count")),
|
||||
"Should have violation on 'count' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid count value")
|
||||
void testCountValid() {
|
||||
// Arrange
|
||||
request.setCount(5);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.noneMatch(v -> v.getPropertyPath().toString().equals("count")),
|
||||
"Should have no violations on 'count' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject count above maximum of 1000")
|
||||
void testCountMaximum() {
|
||||
// Arrange
|
||||
request.setCount(1001);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("count")),
|
||||
"Should have violation on 'count' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject rotationMin below -360")
|
||||
void testRotationMinBelowBound() {
|
||||
// Arrange
|
||||
request.setRotationMin(-361f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("rotationMin")),
|
||||
"Should have violation on 'rotationMin' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject rotationMax above 360")
|
||||
void testRotationMaxAboveBound() {
|
||||
// Arrange
|
||||
request.setRotationMax(361f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("rotationMax")),
|
||||
"Should have violation on 'rotationMax' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid rotation values")
|
||||
void testRotationValid() {
|
||||
// Arrange
|
||||
request.setRotationMin(-180f);
|
||||
request.setRotationMax(180f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.noneMatch(
|
||||
v ->
|
||||
v.getPropertyPath().toString().equals("rotationMin")
|
||||
|| v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("rotationMax")),
|
||||
"Should have no violations on rotation fields");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject fontSizeMin below 1.0")
|
||||
void testFontSizeMinBelowBound() {
|
||||
// Arrange
|
||||
request.setFontSizeMin(0.5f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("fontSizeMin")),
|
||||
"Should have violation on 'fontSizeMin' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject fontSizeMax above 500.0")
|
||||
void testFontSizeMaxAboveBound() {
|
||||
// Arrange
|
||||
request.setFontSizeMax(501f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("fontSizeMax")),
|
||||
"Should have violation on 'fontSizeMax' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid font size values")
|
||||
void testFontSizeValid() {
|
||||
// Arrange
|
||||
request.setFontSizeMin(10f);
|
||||
request.setFontSizeMax(100f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.noneMatch(
|
||||
v ->
|
||||
v.getPropertyPath().toString().equals("fontSizeMin")
|
||||
|| v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("fontSizeMax")),
|
||||
"Should have no violations on font size fields");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject perLetterFontCount below 1")
|
||||
void testPerLetterFontCountMinimum() {
|
||||
// Arrange
|
||||
request.setPerLetterFontCount(0);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(
|
||||
v ->
|
||||
v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("perLetterFontCount")),
|
||||
"Should have violation on 'perLetterFontCount' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject perLetterFontCount above 20")
|
||||
void testPerLetterFontCountMaximum() {
|
||||
// Arrange
|
||||
request.setPerLetterFontCount(21);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(
|
||||
v ->
|
||||
v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("perLetterFontCount")),
|
||||
"Should have violation on 'perLetterFontCount' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid perLetterFontCount value")
|
||||
void testPerLetterFontCountValid() {
|
||||
// Arrange
|
||||
request.setPerLetterFontCount(5);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.noneMatch(
|
||||
v ->
|
||||
v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("perLetterFontCount")),
|
||||
"Should have no violations on 'perLetterFontCount' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject perLetterColorCount below 1")
|
||||
void testPerLetterColorCountMinimum() {
|
||||
// Arrange
|
||||
request.setPerLetterColorCount(0);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(
|
||||
v ->
|
||||
v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("perLetterColorCount")),
|
||||
"Should have violation on 'perLetterColorCount' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject perLetterColorCount above 20")
|
||||
void testPerLetterColorCountMaximum() {
|
||||
// Arrange
|
||||
request.setPerLetterColorCount(21);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(
|
||||
v ->
|
||||
v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("perLetterColorCount")),
|
||||
"Should have violation on 'perLetterColorCount' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid perLetterColorCount value")
|
||||
void testPerLetterColorCountValid() {
|
||||
// Arrange
|
||||
request.setPerLetterColorCount(4);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.noneMatch(
|
||||
v ->
|
||||
v.getPropertyPath()
|
||||
.toString()
|
||||
.equals("perLetterColorCount")),
|
||||
"Should have no violations on 'perLetterColorCount' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject imageScale below 0.1")
|
||||
void testImageScaleBelowBound() {
|
||||
// Arrange
|
||||
request.setImageScale(0.05f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("imageScale")),
|
||||
"Should have violation on 'imageScale' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject imageScale above 10.0")
|
||||
void testImageScaleAboveBound() {
|
||||
// Arrange
|
||||
request.setImageScale(11f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertFalse(violations.isEmpty(), "Should have validation errors");
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.anyMatch(v -> v.getPropertyPath().toString().equals("imageScale")),
|
||||
"Should have violation on 'imageScale' field");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should accept valid imageScale value")
|
||||
void testImageScaleValid() {
|
||||
// Arrange
|
||||
request.setImageScale(1.5f);
|
||||
|
||||
// Act
|
||||
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
violations.stream()
|
||||
.noneMatch(v -> v.getPropertyPath().toString().equals("imageScale")),
|
||||
"Should have no violations on 'imageScale' field");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user