mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-24 04:26:14 +00:00
Compare commits
No commits in common. "6cdbf55264078cab014a986a2409b5649a20c41c" and "2950bb3a2f64b2a419cc5ffbf2dc90da635c0950" have entirely different histories.
6cdbf55264
...
2950bb3a2f
@ -239,54 +239,34 @@ public class StampController {
|
|||||||
|
|
||||||
PDRectangle pageSize = page.getMediaBox();
|
PDRectangle pageSize = page.getMediaBox();
|
||||||
float x, y;
|
float x, y;
|
||||||
// Split the stampText into multiple lines
|
|
||||||
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) {
|
if (overrideX >= 0 && overrideY >= 0) {
|
||||||
// Use override values if provided
|
// Use override values if provided
|
||||||
x = overrideX;
|
x = overrideX;
|
||||||
y = overrideY;
|
y = overrideY;
|
||||||
} else {
|
} else {
|
||||||
// Base positioning on the true multi-line block size
|
x = calculatePositionX(pageSize, position, fontSize, font, fontSize, stampText, margin);
|
||||||
x = calculatePositionX(pageSize, position, maxWidth, null, 0, null, margin);
|
y =
|
||||||
y = calculatePositionY(pageSize, position, blockHeight, margin);
|
calculatePositionY(
|
||||||
|
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
|
||||||
}
|
}
|
||||||
|
// Split the stampText into multiple lines
|
||||||
|
String[] lines = stampText.split("\\\\n");
|
||||||
|
|
||||||
// After anchoring the block, draw from the top line downward
|
// Calculate dynamic line height based on font ascent and descent
|
||||||
float adjustedX = x;
|
float ascent = font.getFontDescriptor().getAscent();
|
||||||
float adjustedY = y;
|
float descent = font.getFontDescriptor().getDescent();
|
||||||
float pivotX = adjustedX + maxWidth / 2f;
|
float lineHeight = ((ascent - descent) / 1000) * fontSize;
|
||||||
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();
|
contentStream.beginText();
|
||||||
for (int i = 0; i < lines.length; i++) {
|
for (int i = 0; i < lines.length; i++) {
|
||||||
String line = lines[i];
|
String line = lines[i];
|
||||||
// Start from top line: yTop = adjustedY + blockHeight - capHeight
|
// Set the text matrix for each line with rotation
|
||||||
float yLine = adjustedY + blockHeight - capHeight - (i * lineHeight);
|
contentStream.setTextMatrix(
|
||||||
contentStream.setTextMatrix(Matrix.getTranslateInstance(adjustedX, yLine));
|
Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight)));
|
||||||
contentStream.showText(line);
|
contentStream.showText(line);
|
||||||
}
|
}
|
||||||
contentStream.endText();
|
contentStream.endText();
|
||||||
contentStream.restoreGraphicsState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addImageStamp(
|
private void addImageStamp(
|
||||||
@ -330,17 +310,9 @@ public class StampController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentStream.saveGraphicsState();
|
contentStream.saveGraphicsState();
|
||||||
// Rotate and scale about the center of the image
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
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.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
|
||||||
contentStream.drawImage(
|
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
|
||||||
xobject,
|
|
||||||
-desiredPhysicalWidth / 2f,
|
|
||||||
-desiredPhysicalHeight / 2f,
|
|
||||||
desiredPhysicalWidth,
|
|
||||||
desiredPhysicalHeight);
|
|
||||||
contentStream.restoreGraphicsState();
|
contentStream.restoreGraphicsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Button, Group, Stack, Text } from "@mantine/core";
|
import { Button, Group, Stack, Text } from "@mantine/core";
|
||||||
import FitText from "./FitText";
|
|
||||||
|
|
||||||
export interface ButtonOption<T> {
|
export interface ButtonOption<T> {
|
||||||
value: T;
|
value: T;
|
||||||
@ -14,8 +13,6 @@ interface ButtonSelectorProps<T> {
|
|||||||
label?: string;
|
label?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
buttonClassName?: string;
|
|
||||||
textClassName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonSelector = <T extends string>({
|
const ButtonSelector = <T extends string>({
|
||||||
@ -25,8 +22,6 @@ const ButtonSelector = <T extends string>({
|
|||||||
label = undefined,
|
label = undefined,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
fullWidth = true,
|
fullWidth = true,
|
||||||
buttonClassName,
|
|
||||||
textClassName,
|
|
||||||
}: ButtonSelectorProps<T>) => {
|
}: ButtonSelectorProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<Stack gap='var(--mantine-spacing-sm)'>
|
<Stack gap='var(--mantine-spacing-sm)'>
|
||||||
@ -46,7 +41,6 @@ const ButtonSelector = <T extends string>({
|
|||||||
color={value === option.value ? 'var(--color-primary-500)' : 'var(--text-muted)'}
|
color={value === option.value ? 'var(--color-primary-500)' : 'var(--text-muted)'}
|
||||||
onClick={() => onChange(option.value)}
|
onClick={() => onChange(option.value)}
|
||||||
disabled={disabled || option.disabled}
|
disabled={disabled || option.disabled}
|
||||||
className={buttonClassName}
|
|
||||||
style={{
|
style={{
|
||||||
flex: fullWidth ? 1 : undefined,
|
flex: fullWidth ? 1 : undefined,
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@ -54,13 +48,7 @@ const ButtonSelector = <T extends string>({
|
|||||||
fontSize: 'var(--mantine-font-size-sm)'
|
fontSize: 'var(--mantine-font-size-sm)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FitText
|
{option.label}
|
||||||
text={option.label}
|
|
||||||
lines={1}
|
|
||||||
minimumFontScale={0.5}
|
|
||||||
fontSize={10}
|
|
||||||
className={textClassName}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -127,9 +127,7 @@
|
|||||||
/* Information text container */
|
/* Information text container */
|
||||||
.informationContainer {
|
.informationContainer {
|
||||||
background-color: var(--information-text-bg);
|
background-color: var(--information-text-bg);
|
||||||
padding: 2px;
|
padding: 8px;
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@ -140,8 +138,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.informationText {
|
.informationText {
|
||||||
font-size: 0.75rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
color: var(--information-text-color);
|
color: var(--information-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,48 +110,6 @@ export default function StampPreview({ parameters, onParameterChange, file, show
|
|||||||
)
|
)
|
||||||
), [containerSize, parameters, imageMeta, pageSize, showQuickGrid, hoverTile, pageThumbnail]);
|
), [containerSize, parameters, imageMeta, pageSize, showQuickGrid, hoverTile, pageThumbnail]);
|
||||||
|
|
||||||
// Keep center fixed when scaling via slider (or any fontSize changes)
|
|
||||||
const prevDimsRef = useRef<{ fontSize: number; widthPx: number; heightPx: number; leftPx: number; bottomPx: number } | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const itemStyle = style.item as any;
|
|
||||||
if (!itemStyle || containerSize.width <= 0 || containerSize.height <= 0) return;
|
|
||||||
|
|
||||||
const parse = (v: any) => parseFloat(String(v).replace('px', '')) || 0;
|
|
||||||
const leftPx = parse(itemStyle.left);
|
|
||||||
const bottomPx = parse(itemStyle.bottom);
|
|
||||||
const widthPx = parse(itemStyle.width);
|
|
||||||
const heightPx = parse(itemStyle.height);
|
|
||||||
|
|
||||||
const prev = prevDimsRef.current;
|
|
||||||
const hasOverrides = parameters.overrideX >= 0 && parameters.overrideY >= 0;
|
|
||||||
const canAdjust = hasOverrides && !showQuickGrid;
|
|
||||||
if (
|
|
||||||
prev &&
|
|
||||||
canAdjust &&
|
|
||||||
parameters.fontSize !== prev.fontSize &&
|
|
||||||
prev.widthPx > 0 &&
|
|
||||||
prev.heightPx > 0 &&
|
|
||||||
widthPx > 0 &&
|
|
||||||
heightPx > 0
|
|
||||||
) {
|
|
||||||
const centerX = prev.leftPx + prev.widthPx / 2;
|
|
||||||
const centerY = prev.bottomPx + prev.heightPx / 2;
|
|
||||||
const newLeftPx = centerX - widthPx / 2;
|
|
||||||
const newBottomPx = centerY - heightPx / 2;
|
|
||||||
|
|
||||||
const widthPts = pageSize?.widthPts ?? 595.28;
|
|
||||||
const heightPts = pageSize?.heightPts ?? 841.89;
|
|
||||||
const scaleX = containerSize.width / widthPts;
|
|
||||||
const scaleY = containerSize.height / heightPts;
|
|
||||||
const newLeftPts = Math.max(0, Math.min(containerSize.width, newLeftPx)) / scaleX;
|
|
||||||
const newBottomPts = Math.max(0, Math.min(containerSize.height, newBottomPx)) / scaleY;
|
|
||||||
onParameterChange('overrideX', newLeftPts as any);
|
|
||||||
onParameterChange('overrideY', newBottomPts as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
prevDimsRef.current = { fontSize: parameters.fontSize, widthPx, heightPx, leftPx, bottomPx };
|
|
||||||
}, [parameters.fontSize, style.item, containerSize, pageSize, showQuickGrid, parameters.overrideX, parameters.overrideY, onParameterChange]);
|
|
||||||
|
|
||||||
// Drag/resize/rotate interactions
|
// Drag/resize/rotate interactions
|
||||||
const draggingRef = useRef<{ type: 'move' | 'resize' | 'rotate'; startX: number; startY: number; initLeft: number; initBottom: number; initHeight: number; centerX: number; centerY: number } | null>(null);
|
const draggingRef = useRef<{ type: 'move' | 'resize' | 'rotate'; startX: number; startY: number; initLeft: number; initBottom: number; initHeight: number; centerX: number; centerY: number } | null>(null);
|
||||||
|
|
||||||
|
@ -192,7 +192,7 @@ export function computeStampPreviewStyle(
|
|||||||
height: `${heightPx}px`,
|
height: `${heightPx}px`,
|
||||||
opacity: displayOpacity,
|
opacity: displayOpacity,
|
||||||
transform: `rotate(${-parameters.rotation}deg)`,
|
transform: `rotate(${-parameters.rotation}deg)`,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'left bottom',
|
||||||
color: parameters.customColor,
|
color: parameters.customColor,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
@ -9,7 +9,9 @@ export const buildAddStampFormData = (parameters: AddStampParameters, file: File
|
|||||||
formData.append('pageNumbers', parameters.pageNumbers);
|
formData.append('pageNumbers', parameters.pageNumbers);
|
||||||
formData.append('customMargin', 'medium');
|
formData.append('customMargin', 'medium');
|
||||||
formData.append('position', String(parameters.position));
|
formData.append('position', String(parameters.position));
|
||||||
const effectiveFontSize = parameters.fontSize;
|
const effectiveFontSize = parameters.stampType === 'text'
|
||||||
|
? parameters.fontSize * 2.2 // upscale to match the preview size
|
||||||
|
: parameters.fontSize;
|
||||||
formData.append('fontSize', String(effectiveFontSize));
|
formData.append('fontSize', String(effectiveFontSize));
|
||||||
formData.append('rotation', String(parameters.rotation));
|
formData.append('rotation', String(parameters.rotation));
|
||||||
formData.append('opacity', String(parameters.opacity / 100));
|
formData.append('opacity', String(parameters.opacity / 100));
|
||||||
|
@ -11,7 +11,7 @@ import StampPreview from "../components/tools/addStamp/StampPreview";
|
|||||||
import LocalIcon from "../components/shared/LocalIcon";
|
import LocalIcon from "../components/shared/LocalIcon";
|
||||||
import styles from "../components/tools/addStamp/StampPreview.module.css";
|
import styles from "../components/tools/addStamp/StampPreview.module.css";
|
||||||
import { Tooltip } from "../components/shared/Tooltip";
|
import { Tooltip } from "../components/shared/Tooltip";
|
||||||
import ButtonSelector from "../components/shared/ButtonSelector";
|
import FitText from "../components/shared/FitText";
|
||||||
|
|
||||||
const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -99,17 +99,24 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
<Stack gap="md" justify="space-between" flex={1}>
|
<Stack gap="md" justify="space-between" flex={1}>
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm" fw={500} mb="xs">{t('AddStampRequest.stampType', 'Stamp Type')}</Text>
|
<Text size="sm" fw={500} mb="xs">{t('AddStampRequest.stampType', 'Stamp Type')}</Text>
|
||||||
<ButtonSelector
|
<Group className={styles.modeToggleGroup} grow>
|
||||||
value={params.parameters.stampType}
|
<Button
|
||||||
onChange={(v: 'text' | 'image') => params.updateParameter('stampType', v)}
|
variant={params.parameters.stampType === 'text' ? 'filled' : 'outline'}
|
||||||
options={[
|
className={styles.modeToggleButton}
|
||||||
{ value: 'text', label: t('watermark.type.1', 'Text') },
|
onClick={() => params.updateParameter('stampType', 'text')}
|
||||||
{ value: 'image', label: t('watermark.type.2', 'Image') },
|
disabled={endpointLoading}
|
||||||
]}
|
>
|
||||||
disabled={endpointLoading}
|
{t('watermark.type.1', 'Text')}
|
||||||
buttonClassName={styles.modeToggleButton}
|
</Button>
|
||||||
textClassName={styles.modeToggleButtonText}
|
<Button
|
||||||
/>
|
variant={params.parameters.stampType === 'image' ? 'filled' : 'outline'}
|
||||||
|
className={styles.modeToggleButton}
|
||||||
|
onClick={() => params.updateParameter('stampType', 'image')}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
>
|
||||||
|
{t('watermark.type.2', 'Image')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{params.parameters.stampType === 'text' && (
|
{params.parameters.stampType === 'text' && (
|
||||||
@ -201,31 +208,45 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
<Stack gap="md" justify="space-between">
|
<Stack gap="md" justify="space-between">
|
||||||
{/* Mode toggle: Quick grid vs Custom drag - only show for image stamps */}
|
{/* Mode toggle: Quick grid vs Custom drag - only show for image stamps */}
|
||||||
{params.parameters.stampType === 'image' && (
|
{params.parameters.stampType === 'image' && (
|
||||||
<ButtonSelector
|
<Group className={styles.modeToggleGroup} grow>
|
||||||
value={quickPositionModeSelected ? 'quick' : 'custom'}
|
<Button
|
||||||
onChange={(v: 'quick' | 'custom') => {
|
variant={quickPositionModeSelected ? 'filled' : 'outline'}
|
||||||
const isQuick = v === 'quick';
|
className={styles.modeToggleButton}
|
||||||
setQuickPositionModeSelected(isQuick);
|
onClick={() => {
|
||||||
setCustomPositionModeSelected(!isQuick);
|
setQuickPositionModeSelected(true);
|
||||||
}}
|
setCustomPositionModeSelected(false);
|
||||||
options={[
|
}}
|
||||||
{ value: 'quick', label: t('quickPosition', 'Quick Position') },
|
>
|
||||||
{ value: 'custom', label: t('customPosition', 'Custom Position') },
|
<FitText
|
||||||
]}
|
text={t('quickPosition', 'Quick Position')}
|
||||||
disabled={endpointLoading}
|
lines={1}
|
||||||
buttonClassName={styles.modeToggleButton}
|
minimumFontScale={0.5}
|
||||||
textClassName={styles.modeToggleButtonText}
|
fontSize={10}
|
||||||
/>
|
className={styles.modeToggleButtonText}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={customPositionModeSelected ? 'filled' : 'outline'}
|
||||||
|
className={styles.modeToggleButton}
|
||||||
|
onClick={() => {
|
||||||
|
setQuickPositionModeSelected(false);
|
||||||
|
setCustomPositionModeSelected(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FitText
|
||||||
|
text={t('customPosition', 'Custom Position')}
|
||||||
|
lines={1}
|
||||||
|
minimumFontScale={0.5}
|
||||||
|
fontSize={10}
|
||||||
|
className={styles.modeToggleButtonText}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{params.parameters.stampType === 'image' && customPositionModeSelected && (
|
{params.parameters.stampType === 'image' && customPositionModeSelected && (
|
||||||
<div className={styles.informationContainer}>
|
<div className={styles.informationContainer}>
|
||||||
<Text className={styles.informationText}>{t('AddStampRequest.customPosition', 'Drag the stamp to the desired location in the preview window.')}</Text>
|
<Text className={styles.informationText}>{t('customPosition', 'Drag the stamp to the desired location in the preview window.')}</Text>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{params.parameters.stampType === 'image' && !customPositionModeSelected && (
|
|
||||||
<div className={styles.informationContainer}>
|
|
||||||
<Text className={styles.informationText}>{t('AddStampRequest.quickPosition', 'Select a position on the page to place the stamp.')}</Text>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user