Remove margin and bounding box constraints from watermark configuration

- Simplified random position generation by removing `margin` and `bounds` parameters.
- Updated `WatermarkRandomizer` to avoid use of obsolete constraints.
- Refactored tests to validate new approach while ensuring watermarks stay within page boundaries.
- Adjusted UI and localization files to remove unused margin and bounds options.
This commit is contained in:
antonarhipov 2025-10-31 19:46:19 +02:00
parent 055b2ce2fc
commit 22df750009
9 changed files with 274 additions and 321 deletions

View File

@ -23,45 +23,24 @@ public class WatermarkRandomizer {
}
/**
* Generates a random position within the given bounds and margins.
* Generates a random position within the page.
*
* @param pageWidth Width of the page
* @param pageHeight Height of the page
* @param watermarkWidth Width of the watermark
* @param watermarkHeight Height of the watermark
* @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);
float pageWidth, float pageHeight, float watermarkWidth, float watermarkHeight) {
// 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;
float maxX = Math.max(0, pageWidth - watermarkWidth);
float maxY = Math.max(0, pageHeight - watermarkHeight);
// 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);
// Generate random position within page
float x = random.nextFloat() * maxX;
float y = random.nextFloat() * maxY;
return new float[] {x, y};
}
@ -89,35 +68,49 @@ public class WatermarkRandomizer {
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);
// Calculate automatic margins to keep watermarks within page boundaries
float maxX = Math.max(0, pageWidth - watermarkWidth);
float maxY = Math.max(0, pageHeight - watermarkHeight);
// Calculate how many rows and columns can fit within the page
// Note: watermarkWidth/Height are the actual watermark dimensions (not including spacers)
// We need to account for the spacing between watermarks when calculating grid capacity
int maxRows = (int) Math.floor(maxY / (watermarkHeight + heightSpacer));
int maxCols = (int) Math.floor(maxX / (watermarkWidth + widthSpacer));
if (count == 0) {
// Unlimited grid: fill entire page using spacer-based grid
for (int i = 0; i <= maxRows; i++) {
for (int j = 0; j <= maxCols; j++) {
// Unlimited grid: fill page using spacer-based grid
// Ensure watermarks stay within visible area
for (int i = 0; i < maxRows; i++) {
for (int j = 0; j < maxCols; j++) {
float x = j * (watermarkWidth + widthSpacer);
float y = i * (watermarkHeight + heightSpacer);
// Clamp to ensure within bounds
x = Math.min(x, maxX);
y = Math.min(y, maxY);
positions.add(new float[] {x, y});
}
}
} else {
// Limited count: distribute evenly across 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);
// Limited count: distribute evenly across page
// Calculate optimal distribution based on page aspect ratio
// Don't use spacer-based limits; instead ensure positions fit within maxX/maxY
int cols = (int) Math.ceil(Math.sqrt(count * pageWidth / pageHeight));
int rows = (int) Math.ceil((double) count / cols);
// Calculate step sizes to distribute watermarks evenly across available grid
int colStep = Math.max(1, maxCols / cols);
int rowStep = Math.max(1, maxRows / rows);
// Calculate spacing to distribute watermarks evenly within the visible area
// Account for watermark dimensions to prevent overflow at edges
float xSpacing = (cols > 1) ? maxX / (cols - 1) : 0;
float ySpacing = (rows > 1) ? maxY / (rows - 1) : 0;
int generated = 0;
for (int i = 0; i < 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);
for (int i = 0; i < rows && generated < count; i++) {
for (int j = 0; j < cols && generated < count; j++) {
float x = j * xSpacing;
float y = i * ySpacing;
// Clamp to ensure within bounds
x = Math.min(x, maxX);
y = Math.min(y, maxY);
positions.add(new float[] {x, y});
generated++;
}

View File

@ -25,21 +25,16 @@ class WatermarkRandomizerTest {
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);
float[] pos1 = randomizer1.generateRandomPosition(800f, 600f, 100f, 50f);
float[] pos2 = randomizer2.generateRandomPosition(800f, 600f, 100f, 50f);
assertArrayEquals(pos1, pos2, "Same seed should produce same position");
}
@Test
@DisplayName("Should respect margin constraints")
void testGenerateRandomPositionWithMargin() {
@DisplayName("Should keep watermark within page bounds")
void testGenerateRandomPositionWithinBounds() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float margin = 20f;
float pageWidth = 800f;
float pageHeight = 600f;
float watermarkWidth = 100f;
@ -48,59 +43,41 @@ class WatermarkRandomizerTest {
for (int i = 0; i < 10; i++) {
float[] pos =
randomizer.generateRandomPosition(
pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
margin,
null,
null,
null,
null);
pageWidth, pageHeight, watermarkWidth, watermarkHeight);
assertTrue(pos[0] >= margin, "X position should respect margin");
assertTrue(pos[1] >= margin, "Y position should respect margin");
assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue(
pos[0] <= pageWidth - margin - watermarkWidth,
"X position should not exceed page width minus margin");
pos[0] <= pageWidth - watermarkWidth,
"X position should not exceed page width minus watermark width");
assertTrue(
pos[1] <= pageHeight - margin - watermarkHeight,
"Y position should not exceed page height minus margin");
pos[1] <= pageHeight - watermarkHeight,
"Y position should not exceed page height minus watermark height");
}
}
@Test
@DisplayName("Should respect custom bounds")
void testGenerateRandomPositionWithBounds() {
@DisplayName("Should handle small watermarks on large pages")
void testGenerateRandomPositionSmallWatermark() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float boundsX = 100f;
float boundsY = 100f;
float boundsWidth = 400f;
float boundsHeight = 300f;
float pageWidth = 800f;
float pageHeight = 600f;
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);
pageWidth, pageHeight, watermarkWidth, watermarkHeight);
assertTrue(pos[0] >= boundsX, "X position should be within bounds");
assertTrue(pos[1] >= boundsY, "Y position should be within bounds");
assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue(
pos[0] <= boundsX + boundsWidth - watermarkWidth,
"X position should not exceed bounds");
pos[0] <= pageWidth - watermarkWidth,
"X position should not exceed page width minus watermark width");
assertTrue(
pos[1] <= boundsY + boundsHeight - watermarkHeight,
"Y position should not exceed bounds");
pos[1] <= pageHeight - watermarkHeight,
"Y position should not exceed page height minus watermark height");
}
}
@ -114,7 +91,7 @@ class WatermarkRandomizerTest {
float watermarkHeight = 100f;
int widthSpacer = 50;
int heightSpacer = 50;
int count = 5;
int count = 150;
List<float[]> positions =
randomizer.generateGridPositions(
@ -137,20 +114,203 @@ class WatermarkRandomizerTest {
}
@Test
@DisplayName("Should generate unlimited grid when count is 0")
void testGenerateGridPositionsUnlimited() {
@DisplayName("Should keep watermarks within page boundaries for unlimited grid")
void testGenerateGridPositionsUnlimitedWithinBounds() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float pageWidth = 400f;
float pageHeight = 300f;
float pageWidth = 800f;
float pageHeight = 600f;
float watermarkWidth = 100f;
float watermarkHeight = 100f;
float watermarkHeight = 80f;
int widthSpacer = 50;
int heightSpacer = 40;
List<float[]> positions =
randomizer.generateGridPositions(
pageWidth, pageHeight, watermarkWidth, watermarkHeight, 50, 50, 0);
pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
widthSpacer,
heightSpacer,
0);
assertNotNull(positions, "Positions should not be null");
assertTrue(positions.size() > 0, "Should generate at least one position");
assertFalse(positions.isEmpty(), "Should generate at least one position");
// Verify all watermarks fit within page boundaries
for (float[] pos : positions) {
assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue(
pos[0] + watermarkWidth <= pageWidth,
String.format(
"Watermark right edge (%.2f) should not exceed page width (%.2f)",
pos[0] + watermarkWidth, pageWidth));
assertTrue(
pos[1] + watermarkHeight <= pageHeight,
String.format(
"Watermark top edge (%.2f) should not exceed page height (%.2f)",
pos[1] + watermarkHeight, pageHeight));
}
}
@Test
@DisplayName("Should handle large watermarks on small pages")
void testGenerateGridPositionsLargeWatermark() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float pageWidth = 300f;
float pageHeight = 200f;
float watermarkWidth = 150f;
float watermarkHeight = 100f;
int widthSpacer = 20;
int heightSpacer = 15;
int count = 4;
List<float[]> positions =
randomizer.generateGridPositions(
pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
widthSpacer,
heightSpacer,
count);
assertNotNull(positions, "Positions should not be null");
// Verify all watermarks fit within page boundaries
for (float[] pos : positions) {
assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue(
pos[0] + watermarkWidth <= pageWidth,
String.format(
"Watermark right edge (%.2f) should not exceed page width (%.2f)",
pos[0] + watermarkWidth, pageWidth));
assertTrue(
pos[1] + watermarkHeight <= pageHeight,
String.format(
"Watermark top edge (%.2f) should not exceed page height (%.2f)",
pos[1] + watermarkHeight, pageHeight));
}
}
@Test
@DisplayName("Should handle edge case with zero spacers")
void testGenerateGridPositionsZeroSpacers() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float pageWidth = 600f;
float pageHeight = 400f;
float watermarkWidth = 100f;
float watermarkHeight = 80f;
int widthSpacer = 0;
int heightSpacer = 0;
int count = 6;
List<float[]> positions =
randomizer.generateGridPositions(
pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
widthSpacer,
heightSpacer,
count);
assertNotNull(positions, "Positions should not be null");
// Verify all watermarks fit within page boundaries
for (float[] pos : positions) {
assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue(
pos[0] + watermarkWidth <= pageWidth,
String.format(
"Watermark right edge (%.2f) should not exceed page width (%.2f)",
pos[0] + watermarkWidth, pageWidth));
assertTrue(
pos[1] + watermarkHeight <= pageHeight,
String.format(
"Watermark top edge (%.2f) should not exceed page height (%.2f)",
pos[1] + watermarkHeight, pageHeight));
}
}
@Test
@DisplayName("Should handle edge case with large spacers")
void testGenerateGridPositionsLargeSpacers() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float pageWidth = 1000f;
float pageHeight = 800f;
float watermarkWidth = 80f;
float watermarkHeight = 60f;
int widthSpacer = 200;
int heightSpacer = 150;
int count = 0; // Unlimited
List<float[]> positions =
randomizer.generateGridPositions(
pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
widthSpacer,
heightSpacer,
count);
assertNotNull(positions, "Positions should not be null");
// Verify all watermarks fit within page boundaries
for (float[] pos : positions) {
assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue(
pos[0] + watermarkWidth <= pageWidth,
String.format(
"Watermark right edge (%.2f) should not exceed page width (%.2f)",
pos[0] + watermarkWidth, pageWidth));
assertTrue(
pos[1] + watermarkHeight <= pageHeight,
String.format(
"Watermark top edge (%.2f) should not exceed page height (%.2f)",
pos[1] + watermarkHeight, pageHeight));
}
}
@Test
@DisplayName("Should handle watermark exactly fitting page dimensions")
void testGenerateGridPositionsExactFit() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float pageWidth = 400f;
float pageHeight = 300f;
float watermarkWidth = 400f;
float watermarkHeight = 300f;
int widthSpacer = 0;
int heightSpacer = 0;
int count = 1;
List<float[]> positions =
randomizer.generateGridPositions(
pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
widthSpacer,
heightSpacer,
count);
assertNotNull(positions, "Positions should not be null");
assertEquals(1, positions.size(), "Should generate exactly one position");
float[] pos = positions.get(0);
assertEquals(0f, pos[0], 0.01f, "X position should be 0");
assertEquals(0f, pos[1], 0.01f, "Y position should be 0");
assertTrue(
pos[0] + watermarkWidth <= pageWidth,
"Watermark right edge should not exceed page width");
assertTrue(
pos[1] + watermarkHeight <= pageHeight,
"Watermark top edge should not exceed page height");
}
}
@ -332,7 +492,7 @@ class WatermarkRandomizerTest {
@DisplayName("Should return default when font list is empty")
void testSelectRandomFontEmpty() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
List<String> fonts = Arrays.asList();
List<String> fonts = List.of();
String font = randomizer.selectRandomFont(fonts);
@ -549,7 +709,7 @@ class WatermarkRandomizerTest {
@DisplayName("Should return 'none' when shading list is empty")
void testSelectRandomShadingEmpty() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
List<String> shadings = Arrays.asList();
List<String> shadings = List.of();
String shading = randomizer.selectRandomShading(shadings);

View File

@ -335,7 +335,6 @@ public class WatermarkController {
(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 =
@ -355,23 +354,6 @@ public class WatermarkController {
? 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) {
try {
boundsX = Float.parseFloat(boundsParts[0].trim());
boundsY = Float.parseFloat(boundsParts[1].trim());
boundsWidth = Float.parseFloat(boundsParts[2].trim());
boundsHeight = Float.parseFloat(boundsParts[3].trim());
} catch (NumberFormatException e) {
log.error("Invalid bounds format: {}", request.getBounds(), e);
boundsX = boundsY = boundsWidth = boundsHeight = null;
}
}
}
String resourceDir =
switch (alphabet) {
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
@ -403,30 +385,31 @@ public class WatermarkController {
// Determine positions based on a randomPosition flag
java.util.List<float[]> positions;
// Calculate approximate watermark dimensions for positioning
// Estimate width based on average character width (more accurate than fixed 100)
float avgCharWidth = fontSize * 0.6f; // Approximate average character width
float maxLineWidth = 0;
for (String line : textLines) {
float lineWidth = line.length() * avgCharWidth;
if (lineWidth > maxLineWidth) {
maxLineWidth = lineWidth;
}
}
float watermarkWidth = maxLineWidth;
float watermarkHeight = fontSize * textLines.length;
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);
pageWidth, pageHeight, watermarkWidth, watermarkHeight);
positions.add(pos);
}
} else {
// Generate grid positions (backward compatible)
float watermarkWidth = 100 * fontSize / 1000;
float watermarkHeight = fontSize * textLines.length;
positions =
randomizer.generateGridPositions(
pageWidth,
@ -648,23 +631,6 @@ public class WatermarkController {
(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) {
try {
boundsX = Float.parseFloat(boundsParts[0].trim());
boundsY = Float.parseFloat(boundsParts[1].trim());
boundsWidth = Float.parseFloat(boundsParts[2].trim());
boundsHeight = Float.parseFloat(boundsParts[3].trim());
} catch (NumberFormatException e) {
log.warn("Invalid bounds format: {}", request.getBounds(), e);
}
}
}
// Load the watermark image
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
@ -693,15 +659,7 @@ public class WatermarkController {
for (int i = 0; i < count; i++) {
float[] pos =
randomizer.generateRandomPosition(
pageWidth,
pageHeight,
desiredPhysicalWidth,
desiredPhysicalHeight,
margin,
boundsX,
boundsY,
boundsWidth,
boundsHeight);
pageWidth, pageHeight, desiredPhysicalWidth, desiredPhysicalHeight);
positions.add(pos);
}
} else {
@ -710,8 +668,8 @@ public class WatermarkController {
randomizer.generateGridPositions(
pageWidth,
pageHeight,
desiredPhysicalWidth + widthSpacer,
desiredPhysicalHeight + heightSpacer,
desiredPhysicalWidth,
desiredPhysicalHeight,
widthSpacer,
heightSpacer,
count);

View File

@ -8,7 +8,6 @@ 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;
@ -187,20 +186,6 @@ public class AddWatermarkRequest extends PDFFile {
@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")

View File

@ -1554,10 +1554,6 @@ watermark.advanced.count.label=Number of Watermarks
watermark.advanced.count.help=Number of watermark instances per page (1-1000)
watermark.advanced.position.label=Random Positioning
watermark.advanced.position.help=Enable random placement instead of grid layout
watermark.advanced.margin.label=Margin from Edges (points)
watermark.advanced.margin.help=Minimum distance from page edges (0-500)
watermark.advanced.bounds.label=Bounding Box (optional)
watermark.advanced.bounds.help=Constrain placement to specific area (format: x,y,width,height)
watermark.advanced.rotation.mode.label=Rotation Mode
watermark.advanced.rotation.mode.fixed=Fixed Angle
watermark.advanced.rotation.mode.range=Random Range

View File

@ -1550,10 +1550,6 @@ watermark.advanced.count.label=Number of Watermarks
watermark.advanced.count.help=Number of watermark instances per page (1-1000)
watermark.advanced.position.label=Random Positioning
watermark.advanced.position.help=Enable random placement instead of grid layout
watermark.advanced.margin.label=Margin from Edges (points)
watermark.advanced.margin.help=Minimum distance from page edges (0-500)
watermark.advanced.bounds.label=Bounding Box (optional)
watermark.advanced.bounds.help=Constrain placement to specific area (format: x,y,width,height)
watermark.advanced.rotation.mode.label=Rotation Mode
watermark.advanced.rotation.mode.fixed=Fixed Angle
watermark.advanced.rotation.mode.range=Random Range

View File

@ -150,18 +150,6 @@
<small class="form-text text-muted" th:text="#{watermark.advanced.position.help}">Enable random placement instead of grid layout</small>
</div>
<div id="marginGroup" class="mb-3">
<label for="margin" th:text="#{watermark.advanced.margin.label}">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" th:text="#{watermark.advanced.margin.help}">Minimum distance from page edges (0-500)</small>
</div>
<div id="boundsGroup" class="mb-3">
<label for="bounds" th:text="#{watermark.advanced.bounds.label}">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" th:text="#{watermark.advanced.bounds.help}">Constrain placement to specific area (format: x,y,width,height)</small>
</div>
<!-- Rotation Controls -->
<div class="mb-3">
<label th:text="#{watermark.advanced.rotation.mode.label}">Rotation Mode</label>
@ -530,8 +518,8 @@
// Validate count
const count = parseInt(document.getElementById('count').value);
if (count < 1 || count > 1000) {
errors.push('Count must be between 1 and 1000');
if (count < 0 || count > 1000) {
errors.push('Count must be between 0 and 1000');
}
// Validate rotation range
@ -557,21 +545,6 @@
}
}
// 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);

View File

@ -182,7 +182,6 @@ class WatermarkControllerIntegrationTest {
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);
@ -467,29 +466,6 @@ class WatermarkControllerIntegrationTest {
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
@ -625,7 +601,6 @@ class WatermarkControllerIntegrationTest {
request.setOpacity(0.5f);
request.setRandomPosition(true);
request.setCount(5);
request.setMargin(20f);
request.setSeed(45454L);
request.setConvertPDFToImage(false);

View File

@ -925,56 +925,6 @@ class WatermarkValidationTest {
"Should have no violations on 'perLetterColorCount' field");
}
@Test
@DisplayName("Should reject margin below 0.0")
void testMarginBelowBound() {
// Arrange
request.setMargin(-1f);
// Act
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
// Assert
assertFalse(violations.isEmpty(), "Should have validation errors");
assertTrue(
violations.stream()
.anyMatch(v -> v.getPropertyPath().toString().equals("margin")),
"Should have violation on 'margin' field");
}
@Test
@DisplayName("Should reject margin above 500.0")
void testMarginAboveBound() {
// Arrange
request.setMargin(501f);
// Act
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
// Assert
assertFalse(violations.isEmpty(), "Should have validation errors");
assertTrue(
violations.stream()
.anyMatch(v -> v.getPropertyPath().toString().equals("margin")),
"Should have violation on 'margin' field");
}
@Test
@DisplayName("Should accept valid margin value")
void testMarginValid() {
// Arrange
request.setMargin(10f);
// Act
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
// Assert
assertTrue(
violations.stream()
.noneMatch(v -> v.getPropertyPath().toString().equals("margin")),
"Should have no violations on 'margin' field");
}
@Test
@DisplayName("Should reject imageScale below 0.1")
void testImageScaleBelowBound() {
@ -1024,38 +974,5 @@ class WatermarkValidationTest {
.noneMatch(v -> v.getPropertyPath().toString().equals("imageScale")),
"Should have no violations on 'imageScale' field");
}
@Test
@DisplayName("Should reject invalid bounds format")
void testBoundsInvalidFormat() {
// Arrange
request.setBounds("invalid");
// Act
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
// Assert
assertFalse(violations.isEmpty(), "Should have validation errors");
assertTrue(
violations.stream()
.anyMatch(v -> v.getPropertyPath().toString().equals("bounds")),
"Should have violation on 'bounds' field");
}
@Test
@DisplayName("Should accept valid bounds format")
void testBoundsValidFormat() {
// Arrange
request.setBounds("100,100,200,200");
// Act
Set<ConstraintViolation<AddWatermarkRequest>> violations = validator.validate(request);
// Assert
assertTrue(
violations.stream()
.noneMatch(v -> v.getPropertyPath().toString().equals("bounds")),
"Should have no violations on 'bounds' field");
}
}
}