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

View File

@ -25,21 +25,16 @@ class WatermarkRandomizerTest {
WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer1 = new WatermarkRandomizer(TEST_SEED);
WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer2 = new WatermarkRandomizer(TEST_SEED);
float[] pos1 = float[] pos1 = randomizer1.generateRandomPosition(800f, 600f, 100f, 50f);
randomizer1.generateRandomPosition( float[] pos2 = randomizer2.generateRandomPosition(800f, 600f, 100f, 50f);
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"); assertArrayEquals(pos1, pos2, "Same seed should produce same position");
} }
@Test @Test
@DisplayName("Should respect margin constraints") @DisplayName("Should keep watermark within page bounds")
void testGenerateRandomPositionWithMargin() { void testGenerateRandomPositionWithinBounds() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float margin = 20f;
float pageWidth = 800f; float pageWidth = 800f;
float pageHeight = 600f; float pageHeight = 600f;
float watermarkWidth = 100f; float watermarkWidth = 100f;
@ -48,59 +43,41 @@ class WatermarkRandomizerTest {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
float[] pos = float[] pos =
randomizer.generateRandomPosition( randomizer.generateRandomPosition(
pageWidth, pageWidth, pageHeight, watermarkWidth, watermarkHeight);
pageHeight,
watermarkWidth,
watermarkHeight,
margin,
null,
null,
null,
null);
assertTrue(pos[0] >= margin, "X position should respect margin"); assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= margin, "Y position should respect margin"); assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue( assertTrue(
pos[0] <= pageWidth - margin - watermarkWidth, pos[0] <= pageWidth - watermarkWidth,
"X position should not exceed page width minus margin"); "X position should not exceed page width minus watermark width");
assertTrue( assertTrue(
pos[1] <= pageHeight - margin - watermarkHeight, pos[1] <= pageHeight - watermarkHeight,
"Y position should not exceed page height minus margin"); "Y position should not exceed page height minus watermark height");
} }
} }
@Test @Test
@DisplayName("Should respect custom bounds") @DisplayName("Should handle small watermarks on large pages")
void testGenerateRandomPositionWithBounds() { void testGenerateRandomPositionSmallWatermark() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float boundsX = 100f; float pageWidth = 800f;
float boundsY = 100f; float pageHeight = 600f;
float boundsWidth = 400f;
float boundsHeight = 300f;
float watermarkWidth = 50f; float watermarkWidth = 50f;
float watermarkHeight = 30f; float watermarkHeight = 30f;
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
float[] pos = float[] pos =
randomizer.generateRandomPosition( randomizer.generateRandomPosition(
800f, pageWidth, pageHeight, watermarkWidth, watermarkHeight);
600f,
watermarkWidth,
watermarkHeight,
10f,
boundsX,
boundsY,
boundsWidth,
boundsHeight);
assertTrue(pos[0] >= boundsX, "X position should be within bounds"); assertTrue(pos[0] >= 0, "X position should be non-negative");
assertTrue(pos[1] >= boundsY, "Y position should be within bounds"); assertTrue(pos[1] >= 0, "Y position should be non-negative");
assertTrue( assertTrue(
pos[0] <= boundsX + boundsWidth - watermarkWidth, pos[0] <= pageWidth - watermarkWidth,
"X position should not exceed bounds"); "X position should not exceed page width minus watermark width");
assertTrue( assertTrue(
pos[1] <= boundsY + boundsHeight - watermarkHeight, pos[1] <= pageHeight - watermarkHeight,
"Y position should not exceed bounds"); "Y position should not exceed page height minus watermark height");
} }
} }
@ -114,7 +91,7 @@ class WatermarkRandomizerTest {
float watermarkHeight = 100f; float watermarkHeight = 100f;
int widthSpacer = 50; int widthSpacer = 50;
int heightSpacer = 50; int heightSpacer = 50;
int count = 5; int count = 150;
List<float[]> positions = List<float[]> positions =
randomizer.generateGridPositions( randomizer.generateGridPositions(
@ -137,20 +114,203 @@ class WatermarkRandomizerTest {
} }
@Test @Test
@DisplayName("Should generate unlimited grid when count is 0") @DisplayName("Should keep watermarks within page boundaries for unlimited grid")
void testGenerateGridPositionsUnlimited() { void testGenerateGridPositionsUnlimitedWithinBounds() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
float pageWidth = 400f; float pageWidth = 800f;
float pageHeight = 300f; float pageHeight = 600f;
float watermarkWidth = 100f; float watermarkWidth = 100f;
float watermarkHeight = 100f; float watermarkHeight = 80f;
int widthSpacer = 50;
int heightSpacer = 40;
List<float[]> positions = List<float[]> positions =
randomizer.generateGridPositions( randomizer.generateGridPositions(
pageWidth, pageHeight, watermarkWidth, watermarkHeight, 50, 50, 0); pageWidth,
pageHeight,
watermarkWidth,
watermarkHeight,
widthSpacer,
heightSpacer,
0);
assertNotNull(positions, "Positions should not be null"); 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") @DisplayName("Should return default when font list is empty")
void testSelectRandomFontEmpty() { void testSelectRandomFontEmpty() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
List<String> fonts = Arrays.asList(); List<String> fonts = List.of();
String font = randomizer.selectRandomFont(fonts); String font = randomizer.selectRandomFont(fonts);
@ -549,7 +709,7 @@ class WatermarkRandomizerTest {
@DisplayName("Should return 'none' when shading list is empty") @DisplayName("Should return 'none' when shading list is empty")
void testSelectRandomShadingEmpty() { void testSelectRandomShadingEmpty() {
WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED); WatermarkRandomizer randomizer = new WatermarkRandomizer(TEST_SEED);
List<String> shadings = Arrays.asList(); List<String> shadings = List.of();
String shading = randomizer.selectRandomShading(shadings); String shading = randomizer.selectRandomShading(shadings);

View File

@ -335,7 +335,6 @@ public class WatermarkController {
(request.getFontSizeMin() != null) ? request.getFontSizeMin() : fontSize; (request.getFontSizeMin() != null) ? request.getFontSizeMin() : fontSize;
float fontSizeMax = float fontSizeMax =
(request.getFontSizeMax() != null) ? request.getFontSizeMax() : fontSize; (request.getFontSizeMax() != null) ? request.getFontSizeMax() : fontSize;
float margin = (request.getMargin() != null) ? request.getMargin() : 10f;
// Extract per-letter configuration with defaults // Extract per-letter configuration with defaults
int perLetterFontCount = int perLetterFontCount =
@ -355,23 +354,6 @@ public class WatermarkController {
? request.getPerLetterOrientationMax() ? request.getPerLetterOrientationMax()
: 360f; : 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 = String resourceDir =
switch (alphabet) { switch (alphabet) {
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf"; case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
@ -403,30 +385,31 @@ public class WatermarkController {
// Determine positions based on a randomPosition flag // Determine positions based on a randomPosition flag
java.util.List<float[]> positions; 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) { if (randomPosition) {
// Generate random positions // Generate random positions
positions = new java.util.ArrayList<>(); positions = new java.util.ArrayList<>();
for (int i = 0; i < count; i++) { 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 = float[] pos =
randomizer.generateRandomPosition( randomizer.generateRandomPosition(
pageWidth, pageWidth, pageHeight, watermarkWidth, watermarkHeight);
pageHeight,
approxWidth,
approxHeight,
margin,
boundsX,
boundsY,
boundsWidth,
boundsHeight);
positions.add(pos); positions.add(pos);
} }
} else { } else {
// Generate grid positions (backward compatible) // Generate grid positions (backward compatible)
float watermarkWidth = 100 * fontSize / 1000;
float watermarkHeight = fontSize * textLines.length;
positions = positions =
randomizer.generateGridPositions( randomizer.generateGridPositions(
pageWidth, pageWidth,
@ -648,23 +631,6 @@ public class WatermarkController {
(request.getRotationMin() != null) ? request.getRotationMin() : rotation; (request.getRotationMin() != null) ? request.getRotationMin() : rotation;
float rotationMax = float rotationMax =
(request.getRotationMax() != null) ? request.getRotationMax() : rotation; (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 // Load the watermark image
BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
@ -693,15 +659,7 @@ public class WatermarkController {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
float[] pos = float[] pos =
randomizer.generateRandomPosition( randomizer.generateRandomPosition(
pageWidth, pageWidth, pageHeight, desiredPhysicalWidth, desiredPhysicalHeight);
pageHeight,
desiredPhysicalWidth,
desiredPhysicalHeight,
margin,
boundsX,
boundsY,
boundsWidth,
boundsHeight);
positions.add(pos); positions.add(pos);
} }
} else { } else {
@ -710,8 +668,8 @@ public class WatermarkController {
randomizer.generateGridPositions( randomizer.generateGridPositions(
pageWidth, pageWidth,
pageHeight, pageHeight,
desiredPhysicalWidth + widthSpacer, desiredPhysicalWidth,
desiredPhysicalHeight + heightSpacer, desiredPhysicalHeight,
widthSpacer, widthSpacer,
heightSpacer, heightSpacer,
count); count);

View File

@ -8,7 +8,6 @@ import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -187,20 +186,6 @@ public class AddWatermarkRequest extends PDFFile {
@Schema(description = "Random seed for deterministic randomness (optional, for testing)") @Schema(description = "Random seed for deterministic randomness (optional, for testing)")
private Long seed; 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( @Schema(
description = "Scale factor for image watermarks (1.0 = original size)", description = "Scale factor for image watermarks (1.0 = original size)",
defaultValue = "1.0") 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.count.help=Number of watermark instances per page (1-1000)
watermark.advanced.position.label=Random Positioning watermark.advanced.position.label=Random Positioning
watermark.advanced.position.help=Enable random placement instead of grid layout 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.label=Rotation Mode
watermark.advanced.rotation.mode.fixed=Fixed Angle watermark.advanced.rotation.mode.fixed=Fixed Angle
watermark.advanced.rotation.mode.range=Random Range 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.count.help=Number of watermark instances per page (1-1000)
watermark.advanced.position.label=Random Positioning watermark.advanced.position.label=Random Positioning
watermark.advanced.position.help=Enable random placement instead of grid layout 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.label=Rotation Mode
watermark.advanced.rotation.mode.fixed=Fixed Angle watermark.advanced.rotation.mode.fixed=Fixed Angle
watermark.advanced.rotation.mode.range=Random Range 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> <small class="form-text text-muted" th:text="#{watermark.advanced.position.help}">Enable random placement instead of grid layout</small>
</div> </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 --> <!-- Rotation Controls -->
<div class="mb-3"> <div class="mb-3">
<label th:text="#{watermark.advanced.rotation.mode.label}">Rotation Mode</label> <label th:text="#{watermark.advanced.rotation.mode.label}">Rotation Mode</label>
@ -530,8 +518,8 @@
// Validate count // Validate count
const count = parseInt(document.getElementById('count').value); const count = parseInt(document.getElementById('count').value);
if (count < 1 || count > 1000) { if (count < 0 || count > 1000) {
errors.push('Count must be between 1 and 1000'); errors.push('Count must be between 0 and 1000');
} }
// Validate rotation range // 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 // Validate mirroring probability
if (document.getElementById('randomMirroring').checked) { if (document.getElementById('randomMirroring').checked) {
const prob = parseFloat(document.getElementById('mirroringProbability').value); const prob = parseFloat(document.getElementById('mirroringProbability').value);

View File

@ -182,7 +182,6 @@ class WatermarkControllerIntegrationTest {
request.setConvertPDFToImage(false); request.setConvertPDFToImage(false);
request.setRandomPosition(true); request.setRandomPosition(true);
request.setCount(5); request.setCount(5);
request.setMargin(10f);
request.setSeed(12345L); // Use seed for deterministic testing request.setSeed(12345L); // Use seed for deterministic testing
ResponseEntity<byte[]> response = watermarkController.addWatermark(request); ResponseEntity<byte[]> response = watermarkController.addWatermark(request);
@ -467,29 +466,6 @@ class WatermarkControllerIntegrationTest {
assertNotNull(response, "Response should not be null"); assertNotNull(response, "Response should not be null");
assertEquals(200, response.getStatusCode().value(), "Should return 200 OK"); 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 @Nested
@ -625,7 +601,6 @@ class WatermarkControllerIntegrationTest {
request.setOpacity(0.5f); request.setOpacity(0.5f);
request.setRandomPosition(true); request.setRandomPosition(true);
request.setCount(5); request.setCount(5);
request.setMargin(20f);
request.setSeed(45454L); request.setSeed(45454L);
request.setConvertPDFToImage(false); request.setConvertPDFToImage(false);

View File

@ -925,56 +925,6 @@ class WatermarkValidationTest {
"Should have no violations on 'perLetterColorCount' field"); "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 @Test
@DisplayName("Should reject imageScale below 0.1") @DisplayName("Should reject imageScale below 0.1")
void testImageScaleBelowBound() { void testImageScaleBelowBound() {
@ -1024,38 +974,5 @@ class WatermarkValidationTest {
.noneMatch(v -> v.getPropertyPath().toString().equals("imageScale")), .noneMatch(v -> v.getPropertyPath().toString().equals("imageScale")),
"Should have no violations on 'imageScale' field"); "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");
}
} }
} }