diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 590ace5d4..85b1f5825 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -239,34 +239,54 @@ public class StampController { PDRectangle pageSize = page.getMediaBox(); float x, y; - - if (overrideX >= 0 && overrideY >= 0) { - // Use override values if provided - x = overrideX; - y = overrideY; - } else { - x = calculatePositionX(pageSize, position, fontSize, font, fontSize, stampText, margin); - y = - calculatePositionY( - pageSize, position, calculateTextCapHeight(font, fontSize), margin); - } // Split the stampText into multiple lines - String[] lines = stampText.split("\\\\n"); + String[] lines = stampText.split("\\r?\\n|\\\\n"); // Calculate dynamic line height based on font ascent and descent float ascent = font.getFontDescriptor().getAscent(); float descent = font.getFontDescriptor().getDescent(); float lineHeight = ((ascent - descent) / 1000) * fontSize; + // Compute a single pivot for the entire text block to avoid line-by-line wobble + float capHeight = calculateTextCapHeight(font, fontSize); + float blockHeight = Math.max(lineHeight, lineHeight * Math.max(1, lines.length)); + float maxWidth = 0f; + for (String ln : lines) { + maxWidth = Math.max(maxWidth, calculateTextWidth(ln, font, fontSize)); + } + + if (overrideX >= 0 && overrideY >= 0) { + // Use override values if provided + x = overrideX; + y = overrideY; + } else { + // Base positioning on the true multi-line block size + x = calculatePositionX(pageSize, position, maxWidth, null, 0, null, margin); + y = calculatePositionY(pageSize, position, blockHeight, margin); + } + + // After anchoring the block, draw from the top line downward + float adjustedX = x; + float adjustedY = y; + float pivotX = adjustedX + maxWidth / 2f; + float pivotY = adjustedY + blockHeight / 2f; + + // Apply rotation about the block center at the graphics state level + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(pivotX, pivotY)); + contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); + contentStream.transform(Matrix.getTranslateInstance(-pivotX, -pivotY)); + contentStream.beginText(); for (int i = 0; i < lines.length; i++) { String line = lines[i]; - // Set the text matrix for each line with rotation - contentStream.setTextMatrix( - Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight))); + // Start from top line: yTop = adjustedY + blockHeight - capHeight + float yLine = adjustedY + blockHeight - capHeight - (i * lineHeight); + contentStream.setTextMatrix(Matrix.getTranslateInstance(adjustedX, yLine)); contentStream.showText(line); } contentStream.endText(); + contentStream.restoreGraphicsState(); } private void addImageStamp( @@ -310,9 +330,17 @@ public class StampController { } contentStream.saveGraphicsState(); - contentStream.transform(Matrix.getTranslateInstance(x, y)); + // Rotate and scale about the center of the image + float centerX = x + (desiredPhysicalWidth / 2f); + float centerY = y + (desiredPhysicalHeight / 2f); + contentStream.transform(Matrix.getTranslateInstance(centerX, centerY)); contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); - contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight); + contentStream.drawImage( + xobject, + -desiredPhysicalWidth / 2f, + -desiredPhysicalHeight / 2f, + desiredPhysicalWidth, + desiredPhysicalHeight); contentStream.restoreGraphicsState(); } diff --git a/frontend/src/tools/AddStamp.tsx b/frontend/src/tools/AddStamp.tsx index 704b447cf..cf9f220e8 100644 --- a/frontend/src/tools/AddStamp.tsx +++ b/frontend/src/tools/AddStamp.tsx @@ -11,7 +11,6 @@ import StampPreview from "../components/tools/addStamp/StampPreview"; import LocalIcon from "../components/shared/LocalIcon"; import styles from "../components/tools/addStamp/StampPreview.module.css"; import { Tooltip } from "../components/shared/Tooltip"; -import FitText from "../components/shared/FitText"; import ButtonSelector from "../components/shared/ButtonSelector"; const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {