mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Watermark functionality enhancements for #3421
- Grid layout and random positioning of watermarks per page - Random orientation - Per watermark font randomization of color and size - Text watermark per-letter randomization of font, color, size, and orientation - Sharing - Random mirroring of image watermarks - Add `WatermarkRandomizer` utility and enhance watermark request handling with new validations and randomization features
This commit is contained in:
parent
db1d0138dd
commit
db142af139
@ -0,0 +1,315 @@
|
||||
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 {
|
||||
|
||||
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 given bounds and margins.
|
||||
*
|
||||
* @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 margin Minimum margin from page edges
|
||||
* @param boundsX Optional bounding box X coordinate (null for full page)
|
||||
* @param boundsY Optional bounding box Y coordinate (null for full page)
|
||||
* @param boundsWidth Optional bounding box width (null for full page)
|
||||
* @param boundsHeight Optional bounding box height (null for full page)
|
||||
* @return Array with [x, y] coordinates
|
||||
*/
|
||||
public float[] generateRandomPosition(
|
||||
float pageWidth,
|
||||
float pageHeight,
|
||||
float watermarkWidth,
|
||||
float watermarkHeight,
|
||||
float margin,
|
||||
Float boundsX,
|
||||
Float boundsY,
|
||||
Float boundsWidth,
|
||||
Float boundsHeight) {
|
||||
|
||||
// Determine effective bounds
|
||||
float effectiveX = (boundsX != null) ? boundsX : margin;
|
||||
float effectiveY = (boundsY != null) ? boundsY : margin;
|
||||
float effectiveWidth = (boundsWidth != null) ? boundsWidth : (pageWidth - 2 * margin);
|
||||
float effectiveHeight = (boundsHeight != null) ? boundsHeight : (pageHeight - 2 * margin);
|
||||
|
||||
// Calculate available space
|
||||
float maxX = Math.max(effectiveX, effectiveX + effectiveWidth - watermarkWidth);
|
||||
float maxY = Math.max(effectiveY, effectiveY + effectiveHeight - watermarkHeight);
|
||||
float minX = effectiveX;
|
||||
float minY = effectiveY;
|
||||
|
||||
// Generate random position within bounds
|
||||
float x = minX + random.nextFloat() * Math.max(0, maxX - minX);
|
||||
float y = minY + random.nextFloat() * Math.max(0, maxY - minY);
|
||||
|
||||
return new float[] {x, y};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 how many rows and columns can fit on the page based on spacers
|
||||
int maxRows = (int) (pageHeight / (watermarkHeight + heightSpacer) + 1);
|
||||
int maxCols = (int) (pageWidth / (watermarkWidth + widthSpacer) + 1);
|
||||
|
||||
if (count == 0) {
|
||||
// Unlimited grid: fill entire page using spacer-based grid
|
||||
for (int i = 0; i <= maxRows; i++) {
|
||||
for (int j = 0; j <= maxCols; j++) {
|
||||
float x = j * (watermarkWidth + widthSpacer);
|
||||
float y = i * (watermarkHeight + heightSpacer);
|
||||
positions.add(new float[] {x, y});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Limited count: distribute evenly across the page using spacer-based grid
|
||||
// Calculate optimal distribution
|
||||
int cols =
|
||||
Math.min((int) Math.ceil(Math.sqrt(count * pageWidth / pageHeight)), maxCols);
|
||||
int rows = Math.min((int) Math.ceil((double) count / cols), maxRows);
|
||||
|
||||
// Calculate step sizes to distribute watermarks evenly across available grid
|
||||
int colStep = Math.max(1, maxCols / cols);
|
||||
int rowStep = Math.max(1, maxRows / rows);
|
||||
|
||||
int generated = 0;
|
||||
for (int i = 0; i < maxRows && generated < count; i += rowStep) {
|
||||
for (int j = 0; j < maxCols && generated < count; j += colStep) {
|
||||
float x = j * (watermarkWidth + widthSpacer);
|
||||
float y = i * (watermarkHeight + heightSpacer);
|
||||
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) {
|
||||
// Predefined palette of common colors
|
||||
Color[] palette = {
|
||||
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
|
||||
};
|
||||
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 rotation for per-letter orientation within a safe range.
|
||||
*
|
||||
* @param maxRotation Maximum rotation angle in degrees (applied as +/- range)
|
||||
* @return Random rotation angle in degrees
|
||||
*/
|
||||
public float generatePerLetterRotation(float maxRotation) {
|
||||
return -maxRotation + random.nextFloat() * (2 * maxRotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Predefined palette of common colors
|
||||
Color[] fullPalette = {
|
||||
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
|
||||
};
|
||||
|
||||
// Limit to requested count
|
||||
int actualCount = Math.min(colorCount, fullPalette.length);
|
||||
actualCount = Math.max(1, actualCount); // At least 1
|
||||
|
||||
return fullPalette[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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,603 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@DisplayName("WatermarkRandomizer Unit Tests")
|
||||
class WatermarkRandomizerTest {
|
||||
|
||||
private static final long TEST_SEED = 12345L;
|
||||
|
||||
@Nested
|
||||
@DisplayName("Position Randomization Tests")
|
||||
class PositionRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate deterministic random position with seed")
|
||||
void testGenerateRandomPositionWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
float[] pos1 =
|
||||
randomizer1.generateRandomPosition(
|
||||
800f, 600f, 100f, 50f, 10f, null, null, null, null);
|
||||
float[] pos2 =
|
||||
randomizer2.generateRandomPosition(
|
||||
800f, 600f, 100f, 50f, 10f, null, null, null, null);
|
||||
|
||||
assertArrayEquals(pos1, pos2, "Same seed should produce same position");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should respect margin constraints")
|
||||
void testGenerateRandomPositionWithMargin() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float margin = 20f;
|
||||
float pageWidth = 800f;
|
||||
float pageHeight = 600f;
|
||||
float watermarkWidth = 100f;
|
||||
float watermarkHeight = 50f;
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
float[] pos =
|
||||
randomizer.generateRandomPosition(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
watermarkWidth,
|
||||
watermarkHeight,
|
||||
margin,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
assertTrue(pos[0] >= margin, "X position should respect margin");
|
||||
assertTrue(pos[1] >= margin, "Y position should respect margin");
|
||||
assertTrue(
|
||||
pos[0] <= pageWidth - margin - watermarkWidth,
|
||||
"X position should not exceed page width minus margin");
|
||||
assertTrue(
|
||||
pos[1] <= pageHeight - margin - watermarkHeight,
|
||||
"Y position should not exceed page height minus margin");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should respect custom bounds")
|
||||
void testGenerateRandomPositionWithBounds() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float boundsX = 100f;
|
||||
float boundsY = 100f;
|
||||
float boundsWidth = 400f;
|
||||
float boundsHeight = 300f;
|
||||
float watermarkWidth = 50f;
|
||||
float watermarkHeight = 30f;
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
float[] pos =
|
||||
randomizer.generateRandomPosition(
|
||||
800f,
|
||||
600f,
|
||||
watermarkWidth,
|
||||
watermarkHeight,
|
||||
10f,
|
||||
boundsX,
|
||||
boundsY,
|
||||
boundsWidth,
|
||||
boundsHeight);
|
||||
|
||||
assertTrue(pos[0] >= boundsX, "X position should be within bounds");
|
||||
assertTrue(pos[1] >= boundsY, "Y position should be within bounds");
|
||||
assertTrue(
|
||||
pos[0] <= boundsX + boundsWidth - watermarkWidth,
|
||||
"X position should not exceed bounds");
|
||||
assertTrue(
|
||||
pos[1] <= boundsY + boundsHeight - watermarkHeight,
|
||||
"Y position should not exceed bounds");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate grid positions correctly")
|
||||
void testGenerateGridPositions() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float pageWidth = 800f;
|
||||
float pageHeight = 600f;
|
||||
float watermarkWidth = 100f;
|
||||
float watermarkHeight = 100f;
|
||||
int widthSpacer = 50;
|
||||
int heightSpacer = 50;
|
||||
int count = 5;
|
||||
|
||||
List<float[]> positions =
|
||||
randomizer.generateGridPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
watermarkWidth,
|
||||
watermarkHeight,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
count);
|
||||
|
||||
assertNotNull(positions, "Positions should not be null");
|
||||
assertEquals(count, positions.size(), "Should generate requested count of positions");
|
||||
|
||||
// Verify positions are within page bounds
|
||||
for (float[] pos : positions) {
|
||||
assertTrue(pos[0] >= 0 && pos[0] <= pageWidth, "X position within page width");
|
||||
assertTrue(pos[1] >= 0 && pos[1] <= pageHeight, "Y position within page height");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate unlimited grid when count is 0")
|
||||
void testGenerateGridPositionsUnlimited() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float pageWidth = 400f;
|
||||
float pageHeight = 300f;
|
||||
float watermarkWidth = 100f;
|
||||
float watermarkHeight = 100f;
|
||||
|
||||
List<float[]> positions =
|
||||
randomizer.generateGridPositions(
|
||||
pageWidth, pageHeight, watermarkWidth, watermarkHeight, 50, 50, 0);
|
||||
|
||||
assertNotNull(positions, "Positions should not be null");
|
||||
assertTrue(positions.size() > 0, "Should generate at least one position");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Rotation Randomization Tests")
|
||||
class RotationRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate deterministic rotation with seed")
|
||||
void testGenerateRandomRotationWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
float rotation1 = randomizer1.generateRandomRotation(0f, 360f);
|
||||
float rotation2 = randomizer2.generateRandomRotation(0f, 360f);
|
||||
|
||||
assertEquals(rotation1, rotation2, "Same seed should produce same rotation");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate rotation within range")
|
||||
void testGenerateRandomRotationInRange() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float minRotation = -45f;
|
||||
float maxRotation = 45f;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
float rotation = randomizer.generateRandomRotation(minRotation, maxRotation);
|
||||
assertTrue(
|
||||
rotation >= minRotation && rotation <= maxRotation,
|
||||
"Rotation should be within specified range");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return fixed rotation when min equals max")
|
||||
void testGenerateRandomRotationFixed() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float fixedRotation = 30f;
|
||||
|
||||
float rotation = randomizer.generateRandomRotation(fixedRotation, fixedRotation);
|
||||
|
||||
assertEquals(
|
||||
fixedRotation, rotation, "Should return fixed rotation when min equals max");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate per-letter rotation within symmetric range")
|
||||
void testGeneratePerLetterRotation() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float maxRotation = 30f;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
float rotation = randomizer.generatePerLetterRotation(maxRotation);
|
||||
assertTrue(
|
||||
rotation >= -maxRotation && rotation <= maxRotation,
|
||||
"Per-letter rotation should be within +/- maxRotation");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate per-letter rotation in specified range")
|
||||
void testGeneratePerLetterRotationInRange() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float minRotation = -15f;
|
||||
float maxRotation = 45f;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
float rotation =
|
||||
randomizer.generatePerLetterRotationInRange(minRotation, maxRotation);
|
||||
assertTrue(
|
||||
rotation >= minRotation && rotation <= maxRotation,
|
||||
"Per-letter rotation should be within specified range");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return fixed per-letter rotation when min equals max")
|
||||
void testGeneratePerLetterRotationInRangeFixed() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float fixedRotation = 20f;
|
||||
|
||||
float rotation =
|
||||
randomizer.generatePerLetterRotationInRange(fixedRotation, fixedRotation);
|
||||
|
||||
assertEquals(
|
||||
fixedRotation, rotation, "Should return fixed rotation when min equals max");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Mirroring Randomization Tests")
|
||||
class MirroringRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate deterministic mirroring decision with seed")
|
||||
void testShouldMirrorWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
boolean mirror1 = randomizer1.shouldMirror(0.5f);
|
||||
boolean mirror2 = randomizer2.shouldMirror(0.5f);
|
||||
|
||||
assertEquals(mirror1, mirror2, "Same seed should produce same mirroring decision");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should always mirror with probability 1.0")
|
||||
void testShouldMirrorAlways() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertTrue(
|
||||
randomizer.shouldMirror(1.0f), "Should always mirror with probability 1.0");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should never mirror with probability 0.0")
|
||||
void testShouldMirrorNever() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertFalse(
|
||||
randomizer.shouldMirror(0.0f), "Should never mirror with probability 0.0");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should mirror approximately 50% of the time with probability 0.5")
|
||||
void testShouldMirrorProbability() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
int mirrorCount = 0;
|
||||
int iterations = 100;
|
||||
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
if (randomizer.shouldMirror(0.5f)) {
|
||||
mirrorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some variance (30-70%)
|
||||
assertTrue(
|
||||
mirrorCount >= 30 && mirrorCount <= 70,
|
||||
"Should mirror approximately 50% of the time");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Font Randomization Tests")
|
||||
class FontRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should select deterministic font with seed")
|
||||
void testSelectRandomFontWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
List<String> fonts = Arrays.asList("Arial", "Times", "Courier");
|
||||
|
||||
String font1 = randomizer1.selectRandomFont(fonts);
|
||||
String font2 = randomizer2.selectRandomFont(fonts);
|
||||
|
||||
assertEquals(font1, font2, "Same seed should produce same font selection");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should select font from provided list")
|
||||
void testSelectRandomFontFromList() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
List<String> fonts = Arrays.asList("Arial", "Times", "Courier");
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String font = randomizer.selectRandomFont(fonts);
|
||||
assertTrue(fonts.contains(font), "Selected font should be from the provided list");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return default when font list is empty")
|
||||
void testSelectRandomFontEmpty() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
List<String> fonts = Arrays.asList();
|
||||
|
||||
String font = randomizer.selectRandomFont(fonts);
|
||||
|
||||
assertEquals("default", font, "Should return 'default' when list is empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return default when font list is null")
|
||||
void testSelectRandomFontNull() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
String font = randomizer.selectRandomFont(null);
|
||||
|
||||
assertEquals("default", font, "Should return 'default' when list is null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should select font from count")
|
||||
void testSelectRandomFontFromCount() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
int fontCount = 5;
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String font = randomizer.selectRandomFontFromCount(fontCount);
|
||||
assertNotNull(font, "Selected font should not be null");
|
||||
assertFalse(font.isEmpty(), "Selected font should not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle font count exceeding available fonts")
|
||||
void testSelectRandomFontFromCountExceedsAvailable() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
int fontCount = 100; // More than available
|
||||
|
||||
String font = randomizer.selectRandomFontFromCount(fontCount);
|
||||
|
||||
assertNotNull(font, "Should still return a valid font");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Font Size Randomization Tests")
|
||||
class FontSizeRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate deterministic font size with seed")
|
||||
void testGenerateRandomFontSizeWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
float size1 = randomizer1.generateRandomFontSize(10f, 50f);
|
||||
float size2 = randomizer2.generateRandomFontSize(10f, 50f);
|
||||
|
||||
assertEquals(size1, size2, "Same seed should produce same font size");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate font size within range")
|
||||
void testGenerateRandomFontSizeInRange() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float minSize = 10f;
|
||||
float maxSize = 50f;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
float size = randomizer.generateRandomFontSize(minSize, maxSize);
|
||||
assertTrue(
|
||||
size >= minSize && size <= maxSize,
|
||||
"Font size should be within specified range");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return fixed font size when min equals max")
|
||||
void testGenerateRandomFontSizeFixed() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
float fixedSize = 30f;
|
||||
|
||||
float size = randomizer.generateRandomFontSize(fixedSize, fixedSize);
|
||||
|
||||
assertEquals(fixedSize, size, "Should return fixed size when min equals max");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Color Randomization Tests")
|
||||
class ColorRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate deterministic color with seed")
|
||||
void testGenerateRandomColorWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
Color color1 = randomizer1.generateRandomColor(false);
|
||||
Color color2 = randomizer2.generateRandomColor(false);
|
||||
|
||||
assertEquals(color1, color2, "Same seed should produce same color");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate color from palette")
|
||||
void testGenerateRandomColorFromPalette() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
Color[] expectedPalette = {
|
||||
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
|
||||
};
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
Color color = randomizer.generateRandomColor(true);
|
||||
boolean inPalette = false;
|
||||
for (Color paletteColor : expectedPalette) {
|
||||
if (color.equals(paletteColor)) {
|
||||
inPalette = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(inPalette, "Color should be from predefined palette");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate RGB color when not using palette")
|
||||
void testGenerateRandomColorRGB() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Color color = randomizer.generateRandomColor(false);
|
||||
assertNotNull(color, "Color should not be null");
|
||||
assertTrue(color.getRed() >= 0 && color.getRed() <= 255, "Red component valid");
|
||||
assertTrue(
|
||||
color.getGreen() >= 0 && color.getGreen() <= 255, "Green component valid");
|
||||
assertTrue(color.getBlue() >= 0 && color.getBlue() <= 255, "Blue component valid");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate color from limited palette")
|
||||
void testGenerateRandomColorFromPaletteWithCount() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
int colorCount = 4;
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Color color = randomizer.generateRandomColorFromPalette(colorCount);
|
||||
assertNotNull(color, "Color should not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle color count exceeding palette size")
|
||||
void testGenerateRandomColorFromPaletteExceedsSize() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
int colorCount = 100; // More than palette size
|
||||
|
||||
Color color = randomizer.generateRandomColorFromPalette(colorCount);
|
||||
|
||||
assertNotNull(color, "Should still return a valid color");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle color count of 1")
|
||||
void testGenerateRandomColorFromPaletteCountOne() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
Color color = randomizer.generateRandomColorFromPalette(1);
|
||||
|
||||
assertEquals(Color.BLACK, color, "Should return first color in palette");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Shading Randomization Tests")
|
||||
class ShadingRandomizationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should select deterministic shading with seed")
|
||||
void testSelectRandomShadingWithSeed() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
List<String> shadings = Arrays.asList("none", "light", "dark");
|
||||
|
||||
String shading1 = randomizer1.selectRandomShading(shadings);
|
||||
String shading2 = randomizer2.selectRandomShading(shadings);
|
||||
|
||||
assertEquals(shading1, shading2, "Same seed should produce same shading selection");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should select shading from provided list")
|
||||
void testSelectRandomShadingFromList() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
List<String> shadings = Arrays.asList("none", "light", "dark");
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String shading = randomizer.selectRandomShading(shadings);
|
||||
assertTrue(
|
||||
shadings.contains(shading),
|
||||
"Selected shading should be from the provided list");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return 'none' when shading list is empty")
|
||||
void testSelectRandomShadingEmpty() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
List<String> shadings = Arrays.asList();
|
||||
|
||||
String shading = randomizer.selectRandomShading(shadings);
|
||||
|
||||
assertEquals("none", shading, "Should return 'none' when list is empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return 'none' when shading list is null")
|
||||
void testSelectRandomShadingNull() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
String shading = randomizer.selectRandomShading(null);
|
||||
|
||||
assertEquals("none", shading, "Should return 'none' when list is null");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Random Instance Tests")
|
||||
class RandomInstanceTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return non-null Random instance")
|
||||
void testGetRandom() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
assertNotNull(randomizer.getRandom(), "Random instance should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should use seeded Random for deterministic behavior")
|
||||
void testSeededRandomBehavior() {
|
||||
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
|
||||
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
|
||||
|
||||
int value1 = randomizer1.getRandom().nextInt(100);
|
||||
int value2 = randomizer2.getRandom().nextInt(100);
|
||||
|
||||
assertEquals(value1, value2, "Seeded Random should produce same values");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should use non-deterministic Random when seed is null")
|
||||
void testNonSeededRandomBehavior() {
|
||||
WatermarkRandomizer randomizer = new WatermarkRandomizer(null);
|
||||
|
||||
assertNotNull(
|
||||
randomizer.getRandom(), "Random instance should not be null even without seed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,14 +38,17 @@ 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.WatermarkRandomizer;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@ -66,6 +69,149 @@ 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) {
|
||||
String errorMsg =
|
||||
String.format("Opacity must be between 0.0 and 1.0, but got: %.2f", opacity);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Validate rotation range: rotationMin <= rotationMax
|
||||
Float rotationMin = request.getRotationMin();
|
||||
Float rotationMax = request.getRotationMax();
|
||||
if (rotationMin != null && rotationMax != null && rotationMin > rotationMax) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Rotation minimum (%.2f) must be less than or equal to rotation maximum (%.2f)",
|
||||
rotationMin, rotationMax);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Validate font size range: fontSizeMin <= fontSizeMax
|
||||
Float fontSizeMin = request.getFontSizeMin();
|
||||
Float fontSizeMax = request.getFontSizeMax();
|
||||
if (fontSizeMin != null && fontSizeMax != null && fontSizeMin > fontSizeMax) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Font size minimum (%.2f) must be less than or equal to font size maximum (%.2f)",
|
||||
fontSizeMin, fontSizeMax);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Validate a 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 (!customColor.matches("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$")) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Invalid color format: '%s'. Expected hex format like #RRGGBB or #RRGGBBAA",
|
||||
customColor);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate mirroring probability bounds (0.0 - 1.0)
|
||||
Float mirroringProbability = request.getMirroringProbability();
|
||||
if (mirroringProbability != null
|
||||
&& (mirroringProbability < 0.0f || mirroringProbability > 1.0f)) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Mirroring probability must be between 0.0 and 1.0, but got: %.2f",
|
||||
mirroringProbability);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Validate watermark type
|
||||
String watermarkType = request.getWatermarkType();
|
||||
if (watermarkType == null
|
||||
|| (!watermarkType.equalsIgnoreCase("text")
|
||||
&& !watermarkType.equalsIgnoreCase("image"))) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Watermark type must be 'text' or 'image', but got: '%s'",
|
||||
watermarkType);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Validate text watermark has text
|
||||
if ("text".equalsIgnoreCase(watermarkType)) {
|
||||
String watermarkText = request.getWatermarkText();
|
||||
if (watermarkText == null || watermarkText.trim().isEmpty()) {
|
||||
String errorMsg = "Watermark text is required when watermark type is 'text'";
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate image watermark has image
|
||||
if ("image".equalsIgnoreCase(watermarkType)) {
|
||||
MultipartFile watermarkImage = request.getWatermarkImage();
|
||||
if (watermarkImage == null || watermarkImage.isEmpty()) {
|
||||
String errorMsg = "Watermark image is required when watermark type is 'image'";
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Validate image type - only allow common image formats
|
||||
String contentType = watermarkImage.getContentType();
|
||||
String originalFilename = watermarkImage.getOriginalFilename();
|
||||
if (contentType != null && !isSupportedImageType(contentType)) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Unsupported image type: '%s'. Supported types: PNG, JPG, JPEG, GIF, BMP",
|
||||
contentType);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// Additional check based on file extension
|
||||
if (originalFilename != null && !hasSupportedImageExtension(originalFilename)) {
|
||||
String errorMsg =
|
||||
String.format(
|
||||
"Unsupported image file extension in: '%s'. Supported extensions: .png, .jpg, .jpeg, .gif, .bmp",
|
||||
originalFilename);
|
||||
log.warn("Validation failed: {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
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,32 +220,37 @@ 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
|
||||
Integer count = (request.getCount() != null) ? request.getCount() : 1;
|
||||
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);
|
||||
|
||||
@ -113,31 +264,13 @@ public class WatermarkController {
|
||||
|
||||
// Set transparency
|
||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||
graphicsState.setNonStrokingAlphaConstant(opacity);
|
||||
graphicsState.setNonStrokingAlphaConstant(request.getOpacity());
|
||||
contentStream.setGraphicsStateParameters(graphicsState);
|
||||
|
||||
if ("text".equalsIgnoreCase(watermarkType)) {
|
||||
addTextWatermark(
|
||||
contentStream,
|
||||
watermarkText,
|
||||
document,
|
||||
page,
|
||||
rotation,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize,
|
||||
alphabet,
|
||||
customColor);
|
||||
addTextWatermark(contentStream, document, page, request, randomizer);
|
||||
} else if ("image".equalsIgnoreCase(watermarkType)) {
|
||||
addImageWatermark(
|
||||
contentStream,
|
||||
watermarkImage,
|
||||
document,
|
||||
page,
|
||||
rotation,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize);
|
||||
addImageWatermark(contentStream, document, page, request, randomizer);
|
||||
}
|
||||
|
||||
// Close the content stream
|
||||
@ -158,19 +291,73 @@ 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;
|
||||
float margin = (request.getMargin() != null) ? request.getMargin() : 10f;
|
||||
|
||||
// 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;
|
||||
|
||||
// Parse bounds if provided
|
||||
Float boundsX = null, boundsY = null, boundsWidth = null, boundsHeight = null;
|
||||
if (request.getBounds() != null && !request.getBounds().isEmpty()) {
|
||||
String[] boundsParts = request.getBounds().split(",");
|
||||
if (boundsParts.length == 4) {
|
||||
boundsX = Float.parseFloat(boundsParts[0].trim());
|
||||
boundsY = Float.parseFloat(boundsParts[1].trim());
|
||||
boundsWidth = Float.parseFloat(boundsParts[2].trim());
|
||||
boundsHeight = Float.parseFloat(boundsParts[3].trim());
|
||||
}
|
||||
}
|
||||
|
||||
String resourceDir =
|
||||
switch (alphabet) {
|
||||
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
||||
case "japanese" -> "static/fonts/Meiryo.ttf";
|
||||
@ -183,6 +370,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 +380,144 @@ 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;
|
||||
if (randomPosition) {
|
||||
// Generate random positions
|
||||
positions = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
// Use approximate watermark dimensions for positioning
|
||||
float approxWidth = widthSpacer + 100; // Approximate
|
||||
float approxHeight = heightSpacer + fontSize * textLines.length;
|
||||
float[] pos =
|
||||
randomizer.generateRandomPosition(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
approxWidth,
|
||||
approxHeight,
|
||||
margin,
|
||||
boundsX,
|
||||
boundsY,
|
||||
boundsWidth,
|
||||
boundsHeight);
|
||||
positions.add(pos);
|
||||
}
|
||||
} else {
|
||||
// Generate grid positions (backward compatible)
|
||||
float watermarkWidth = 100 * fontSize / 1000;
|
||||
float watermarkHeight = fontSize * textLines.length;
|
||||
positions =
|
||||
randomizer.generateGridPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
watermarkWidth,
|
||||
watermarkHeight,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
count);
|
||||
}
|
||||
|
||||
// Calculating the number of rows and columns.
|
||||
// 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");
|
||||
|
||||
int watermarkRows = (int) (pageHeight / newWatermarkHeight + 1);
|
||||
int watermarkCols = (int) (pageWidth / newWatermarkWidth + 1);
|
||||
// Render each watermark instance
|
||||
for (float[] pos : positions) {
|
||||
float x = pos[0];
|
||||
float y = pos[1];
|
||||
|
||||
// Add the text watermark
|
||||
for (int i = 0; i <= watermarkRows; i++) {
|
||||
for (int j = 0; j <= watermarkCols; j++) {
|
||||
// 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,
|
||||
document,
|
||||
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);
|
||||
contentStream.beginText();
|
||||
contentStream.setTextMatrix(
|
||||
Matrix.getRotateInstance(
|
||||
(float) Math.toRadians(rotation),
|
||||
j * newWatermarkWidth,
|
||||
i * newWatermarkHeight));
|
||||
Matrix.getRotateInstance((float) Math.toRadians(wmRotation), x, y));
|
||||
|
||||
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 +525,138 @@ public class WatermarkController {
|
||||
}
|
||||
}
|
||||
|
||||
private void renderTextWithPerLetterVariations(
|
||||
PDPageContentStream contentStream,
|
||||
PDDocument document,
|
||||
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 {
|
||||
|
||||
float currentX = startX;
|
||||
float currentY = startY;
|
||||
|
||||
for (String line : textLines) {
|
||||
currentX = startX;
|
||||
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;
|
||||
|
||||
float letterRotation =
|
||||
perLetterOrientation
|
||||
? randomizer.generatePerLetterRotationInRange(
|
||||
perLetterOrientationMin, perLetterOrientationMax)
|
||||
: baseRotation;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Set font and color
|
||||
contentStream.setFont(letterFont, letterSize);
|
||||
contentStream.setNonStrokingColor(letterColor);
|
||||
|
||||
// Render the character
|
||||
contentStream.beginText();
|
||||
contentStream.setTextMatrix(
|
||||
Matrix.getRotateInstance(
|
||||
(float) Math.toRadians(letterRotation), currentX, currentY));
|
||||
contentStream.showText(charStr);
|
||||
contentStream.endText();
|
||||
|
||||
// Advance position
|
||||
float charWidth = letterFont.getStringWidth(charStr) * letterSize / 1000;
|
||||
currentX += charWidth;
|
||||
}
|
||||
currentY -= 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;
|
||||
float margin = (request.getMargin() != null) ? request.getMargin() : 10f;
|
||||
|
||||
// Parse bounds if provided
|
||||
Float boundsX = null, boundsY = null, boundsWidth = null, boundsHeight = null;
|
||||
if (request.getBounds() != null && !request.getBounds().isEmpty()) {
|
||||
String[] boundsParts = request.getBounds().split(",");
|
||||
if (boundsParts.length == 4) {
|
||||
boundsX = Float.parseFloat(boundsParts[0].trim());
|
||||
boundsY = Float.parseFloat(boundsParts[1].trim());
|
||||
boundsWidth = Float.parseFloat(boundsParts[2].trim());
|
||||
boundsHeight = Float.parseFloat(boundsParts[3].trim());
|
||||
}
|
||||
}
|
||||
|
||||
// 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,35 +664,108 @@ 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);
|
||||
|
||||
// Save the graphics state
|
||||
contentStream.saveGraphicsState();
|
||||
|
||||
// Create rotation matrix and rotate
|
||||
contentStream.transform(
|
||||
Matrix.getTranslateInstance(
|
||||
x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
|
||||
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
|
||||
contentStream.transform(
|
||||
Matrix.getTranslateInstance(
|
||||
-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
|
||||
|
||||
// Draw the image and restore the graphics state
|
||||
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
|
||||
contentStream.restoreGraphicsState();
|
||||
// Determine positions based on a randomPosition flag
|
||||
java.util.List<float[]> positions;
|
||||
if (randomPosition) {
|
||||
// Generate random positions
|
||||
positions = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
float[] pos =
|
||||
randomizer.generateRandomPosition(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
desiredPhysicalWidth,
|
||||
desiredPhysicalHeight,
|
||||
margin,
|
||||
boundsX,
|
||||
boundsY,
|
||||
boundsWidth,
|
||||
boundsHeight);
|
||||
positions.add(pos);
|
||||
}
|
||||
} else {
|
||||
// Generate grid positions (backward compatible)
|
||||
positions =
|
||||
randomizer.generateGridPositions(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
desiredPhysicalWidth + widthSpacer,
|
||||
desiredPhysicalHeight + heightSpacer,
|
||||
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();
|
||||
|
||||
// Translate to center of image position
|
||||
contentStream.transform(
|
||||
Matrix.getTranslateInstance(
|
||||
x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
|
||||
|
||||
// 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));
|
||||
|
||||
// Draw the image and restore the graphics state
|
||||
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
|
||||
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,12 @@ 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 jakarta.validation.constraints.Pattern;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@ -54,4 +60,151 @@ 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 = "Minimum margin from page edges in points", defaultValue = "10")
|
||||
@DecimalMin(value = "0.0", message = "Margin must be >= 0.0")
|
||||
@DecimalMax(value = "500.0", message = "Margin must be <= 500.0")
|
||||
private Float margin;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Bounding box constraint for watermark placement (format: x,y,width,height)")
|
||||
@Pattern(
|
||||
regexp =
|
||||
"^(\\d+(\\.\\d+)?),\\s*(\\d+(\\.\\d+)?),\\s*(\\d+(\\.\\d+)?),\\s*(\\d+(\\.\\d+)?)$",
|
||||
message = "Bounds must be in format: x,y,width,height")
|
||||
private String bounds;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@ -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,38 +95,242 @@
|
||||
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>
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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 class="form-control form-control-color" style="background-color: #d3d3d3;">
|
||||
<input type="color" id="customColor" name="customColor" value="#d3d3d3">
|
||||
</div>
|
||||
<script>
|
||||
let colorInput = document.getElementById("customColor");
|
||||
if (colorInput) {
|
||||
let colorInputContainer = colorInput.parentElement;
|
||||
if (colorInputContainer) {
|
||||
colorInput.onchange = function() {
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
}
|
||||
<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>
|
||||
|
||||
</script>
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="heightSpacer" th:text="#{watermark.selectText.6}"></label>
|
||||
<input type="text" id="heightSpacer" name="heightSpacer" class="form-control" value="50">
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<script>
|
||||
let colorInput = document.getElementById("customColor");
|
||||
if (colorInput) {
|
||||
let colorInputContainer = colorInput.parentElement;
|
||||
if (colorInputContainer) {
|
||||
colorInput.onchange = function() {
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
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">
|
||||
Advanced Options
|
||||
</button>
|
||||
<div id="advancedOptions" class="collapse">
|
||||
<!-- Watermark Count -->
|
||||
<div class="mb-3">
|
||||
<label for="count">Number of Watermarks</label>
|
||||
<input type="number" id="count" name="count" class="form-control" value="1" min="1" max="1000">
|
||||
<small class="form-text text-muted">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" class="form-check-input">
|
||||
<label for="randomPosition" class="form-check-label">Random Positioning</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Enable random placement instead of grid layout</small>
|
||||
</div>
|
||||
|
||||
<div id="marginGroup" class="mb-3">
|
||||
<label for="margin">Margin from Edges (points)</label>
|
||||
<input type="number" id="margin" name="margin" class="form-control" value="10" min="0" max="500" step="0.1">
|
||||
<small class="form-text text-muted">Minimum distance from page edges (0-500)</small>
|
||||
</div>
|
||||
|
||||
<div id="boundsGroup" class="mb-3">
|
||||
<label for="bounds">Bounding Box (optional)</label>
|
||||
<input type="text" id="bounds" name="bounds" class="form-control" placeholder="x,y,width,height">
|
||||
<small class="form-text text-muted">Constrain placement to specific area (format: x,y,width,height)</small>
|
||||
</div>
|
||||
|
||||
<!-- Rotation Controls -->
|
||||
<div class="mb-3">
|
||||
<label>Rotation Mode</label>
|
||||
<select class="form-control" id="rotationMode" onchange="toggleRotationMode()">
|
||||
<option value="fixed">Fixed Angle</option>
|
||||
<option value="range">Random Range</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="rotationRangeGroup" class="mb-3" style="display: none;">
|
||||
<label for="rotationMin">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">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">Random angle between min and max</small>
|
||||
<div id="rotationRangeError" class="invalid-feedback" style="display: none;">
|
||||
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" class="form-check-input" onchange="toggleMirroringProbability()">
|
||||
<label for="randomMirroring" class="form-check-label">Random Mirroring</label>
|
||||
<small class="form-text text-muted">For images only</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mirroringProbabilityGroup" class="mb-3" style="display: none;">
|
||||
<label for="mirroringProbability">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">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" class="form-check-input" onchange="toggleFontMode()">
|
||||
<label for="randomFont" class="form-check-label">Random Font</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Enable random font selection for text watermarks</small>
|
||||
</div>
|
||||
|
||||
<div id="fontNameGroup" class="mb-3">
|
||||
<label for="fontName">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">Leave empty for default font</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Font Size Mode</label>
|
||||
<select class="form-control" id="fontSizeMode" onchange="toggleFontSizeMode()">
|
||||
<option value="fixed">Fixed Size</option>
|
||||
<option value="range">Random Range</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="fontSizeRangeGroup" class="mb-3" style="display: none;">
|
||||
<label for="fontSizeMin">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">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">Random size between min and max</small>
|
||||
<div id="fontSizeRangeError" class="invalid-feedback" style="display: none;">
|
||||
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" class="form-check-input" onchange="toggleColorMode()">
|
||||
<label for="randomColor" class="form-check-label">Random Color</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-Letter Variation Controls (for text watermarks) -->
|
||||
<div id="perLetterGroup" class="mb-3">
|
||||
<label>Per-Letter Variations</label>
|
||||
|
||||
<!-- Per-Letter Font -->
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="perLetterFont" name="perLetterFont" class="form-check-input" onchange="togglePerLetterFontConfig()">
|
||||
<label for="perLetterFont" class="form-check-label">Vary Font per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterFontConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterFontCount">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">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" class="form-check-input" onchange="togglePerLetterColorConfig()">
|
||||
<label for="perLetterColor" class="form-check-label">Vary Color per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterColorConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterColorCount">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">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" class="form-check-input" onchange="togglePerLetterSizeConfig()">
|
||||
<label for="perLetterSize" class="form-check-label">Vary Size per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterSizeConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterSizeMin">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">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">Font size range (1-500, default: 10-100)</small>
|
||||
<div id="perLetterSizeError" class="invalid-feedback" style="display: none;">
|
||||
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" class="form-check-input" onchange="togglePerLetterOrientationConfig()">
|
||||
<label for="perLetterOrientation" class="form-check-label">Vary Orientation per Letter</label>
|
||||
</div>
|
||||
<div id="perLetterOrientationConfig" class="mb-2 ms-4" style="display: none;">
|
||||
<label for="perLetterOrientationMin">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">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">Rotation angle range (-360 to 360, default: 0-360)</small>
|
||||
<div id="perLetterOrientationError" class="invalid-feedback" style="display: none;">
|
||||
Minimum must be less than or equal to maximum
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="form-text text-muted">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 type="checkbox" id="shadingRandom" name="shadingRandom" class="form-check-input" onchange="toggleShadingMode()">
|
||||
<label for="shadingRandom" class="form-check-label">Random Shading</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shadingFixedGroup" class="mb-3">
|
||||
<label for="shading">Shading Style</label>
|
||||
<select class="form-control" id="shading" name="shading">
|
||||
<option value="none">None</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Image Scale Control (for image watermarks) -->
|
||||
<div id="imageScaleGroup" class="mb-3" style="display: none;">
|
||||
<label for="imageScale">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">Scale factor (1.0 = original size, 0.1-10.0)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="seed">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">For deterministic randomness (testing)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-check mb-3">
|
||||
@ -141,7 +347,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 +360,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 +380,266 @@
|
||||
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 < 1 || count > 1000) {
|
||||
errors.push('Count must be between 1 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 margin
|
||||
const margin = parseFloat(document.getElementById('margin').value);
|
||||
if (margin < 0 || margin > 500) {
|
||||
errors.push('Margin must be between 0 and 500');
|
||||
}
|
||||
|
||||
// Validate bounds format (if provided)
|
||||
const bounds = document.getElementById('bounds').value.trim();
|
||||
if (bounds) {
|
||||
const boundsPattern = /^(\d+(\.\d+)?),\s*(\d+(\.\d+)?),\s*(\d+(\.\d+)?),\s*(\d+(\.\d+)?)$/;
|
||||
if (!boundsPattern.test(bounds)) {
|
||||
errors.push('Bounds must be in format: x,y,width,height');
|
||||
}
|
||||
}
|
||||
|
||||
// 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,730 @@
|
||||
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.setMargin(10f);
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should apply text watermark with custom bounds")
|
||||
void testTextWatermarkWithBounds() throws Exception {
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setAlphabet("roman");
|
||||
request.setFileInput(testPdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("Bounded");
|
||||
request.setOpacity(0.5f);
|
||||
request.setFontSize(25f);
|
||||
request.setCustomColor("#00FF00");
|
||||
request.setConvertPDFToImage(false);
|
||||
request.setRandomPosition(true);
|
||||
request.setCount(3);
|
||||
request.setBounds("100,100,300,200");
|
||||
request.setSeed(99000L);
|
||||
|
||||
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.setMargin(20f);
|
||||
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,711 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
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 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;
|
||||
private MockMultipartFile mockPdfFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
request = new AddWatermarkRequest();
|
||||
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 {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce count minimum of 1")
|
||||
void testCountMinimum() {
|
||||
// Note: This tests the @Min annotation on count field
|
||||
// The actual validation happens at the framework level
|
||||
request.setCount(0);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce count maximum of 1000")
|
||||
void testCountMaximum() {
|
||||
// Note: This tests the @Max annotation on count field
|
||||
request.setCount(1001);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce rotation min/max bounds of -360 to 360")
|
||||
void testRotationBounds() {
|
||||
// Note: This tests the @DecimalMin/@DecimalMax annotations
|
||||
request.setRotationMin(-361f);
|
||||
request.setRotationMax(361f);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce font size bounds of 1.0 to 500.0")
|
||||
void testFontSizeBounds() {
|
||||
// Note: This tests the @DecimalMin/@DecimalMax annotations
|
||||
request.setFontSizeMin(0.5f);
|
||||
request.setFontSizeMax(501f);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce per-letter font count bounds of 1 to 20")
|
||||
void testPerLetterFontCountBounds() {
|
||||
// Note: This tests the @Min/@Max annotations
|
||||
request.setPerLetterFontCount(0);
|
||||
request.setPerLetterFontCount(21);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce per-letter color count bounds of 1 to 20")
|
||||
void testPerLetterColorCountBounds() {
|
||||
// Note: This tests the @Min/@Max annotations
|
||||
request.setPerLetterColorCount(0);
|
||||
request.setPerLetterColorCount(21);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce margin bounds of 0.0 to 500.0")
|
||||
void testMarginBounds() {
|
||||
// Note: This tests the @DecimalMin/@DecimalMax annotations
|
||||
request.setMargin(-1f);
|
||||
request.setMargin(501f);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should enforce image scale bounds of 0.1 to 10.0")
|
||||
void testImageScaleBounds() {
|
||||
// Note: This tests the @DecimalMin/@DecimalMax annotations
|
||||
request.setImageScale(0.05f);
|
||||
request.setImageScale(11f);
|
||||
// Framework validation would reject this before reaching controller
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate bounds format pattern")
|
||||
void testBoundsFormatPattern() {
|
||||
// Note: This tests the @Pattern annotation on bounds field
|
||||
request.setBounds("invalid");
|
||||
// Framework validation would reject this before reaching controller
|
||||
|
||||
request.setBounds("100,100,200,200"); // Valid format
|
||||
// Framework validation would accept this
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user