Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

266 lines
8.3 KiB
TypeScript
Raw Normal View History

2025-09-19 15:44:41 +01:00
import { useMemo, useState, useEffect } from "react";
2025-09-19 14:28:12 +01:00
import { Stack, Text, Box, Group, NumberInput, ActionIcon, Center, Alert } from "@mantine/core";
import { useTranslation } from "react-i18next";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
import CropAreaSelector from "./CropAreaSelector";
import {
calculatePDFBounds,
PDFBounds,
2025-09-19 15:44:41 +01:00
CropArea
2025-09-19 14:28:12 +01:00
} from "../../../utils/cropCoordinates";
import { pdfWorkerManager } from "../../../services/pdfWorkerManager";
2025-09-19 15:44:41 +01:00
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
2025-09-19 14:28:12 +01:00
interface CropSettingsProps {
parameters: CropParametersHook;
disabled?: boolean;
}
2025-09-19 15:44:41 +01:00
const CONTAINER_SIZE = 250; // Fit within actual pane width
2025-09-19 14:28:12 +01:00
const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
const { t } = useTranslation();
const { selectedFiles, selectedFileStubs } = useSelectedFiles();
// Get the first selected file for preview
const selectedStub = useMemo(() => {
return selectedFileStubs.length > 0 ? selectedFileStubs[0] : null;
}, [selectedFileStubs]);
// Get the first selected file for PDF processing
const selectedFile = useMemo(() => {
return selectedFiles.length > 0 ? selectedFiles[0] : null;
}, [selectedFiles]);
// Get thumbnail for the selected file
const [thumbnail, setThumbnail] = useState<string | null>(null);
const [pdfBounds, setPdfBounds] = useState<PDFBounds | null>(null);
useEffect(() => {
const loadPDFDimensions = async () => {
if (!selectedStub || !selectedFile) {
setPdfBounds(null);
setThumbnail(null);
return;
}
setThumbnail(selectedStub.thumbnailUrl || null);
try {
// Get PDF dimensions from the actual file
const arrayBuffer = await selectedFile.arrayBuffer();
// Load PDF to get actual dimensions
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, {
disableAutoFetch: true,
disableStream: true,
stopAtErrors: false
});
const firstPage = await pdf.getPage(1);
const viewport = firstPage.getViewport({ scale: 1 });
const pdfWidth = viewport.width;
const pdfHeight = viewport.height;
const bounds = calculatePDFBounds(pdfWidth, pdfHeight, CONTAINER_SIZE, CONTAINER_SIZE);
setPdfBounds(bounds);
// Initialize crop area to full PDF if parameters are still default
const isDefault = parameters.parameters.width === 595 &&
parameters.parameters.height === 842 &&
parameters.parameters.x === 0 &&
parameters.parameters.y === 0;
if (isDefault) {
parameters.resetToFullPDF(bounds);
}
// Cleanup PDF
pdfWorkerManager.destroyDocument(pdf);
} catch (error) {
console.error('Failed to load PDF dimensions:', error);
// Fallback to A4 dimensions if PDF loading fails
const bounds = calculatePDFBounds(595, 842, CONTAINER_SIZE, CONTAINER_SIZE);
setPdfBounds(bounds);
if (parameters.parameters.width === 595 && parameters.parameters.height === 842) {
parameters.resetToFullPDF(bounds);
}
}
};
loadPDFDimensions();
}, [selectedStub, selectedFile, parameters]);
// Current crop area
const cropArea = parameters.getCropArea();
2025-09-19 15:44:41 +01:00
2025-09-19 14:28:12 +01:00
// Handle crop area changes from the selector
const handleCropAreaChange = (newCropArea: CropArea) => {
if (pdfBounds) {
parameters.setCropArea(newCropArea, pdfBounds);
}
};
// Handle manual coordinate input changes
const handleCoordinateChange = (field: keyof CropArea, value: number | string) => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
if (isNaN(numValue)) return;
const newCropArea = { ...cropArea, [field]: numValue };
if (pdfBounds) {
parameters.setCropArea(newCropArea, pdfBounds);
}
};
// Reset to full PDF
const handleReset = () => {
if (pdfBounds) {
parameters.resetToFullPDF(pdfBounds);
}
};
if (!selectedStub || !pdfBounds) {
return (
<Center style={{ height: '200px' }}>
<Text color="dimmed">
{t("crop.noFileSelected", "Select a PDF file to begin cropping")}
</Text>
</Center>
);
}
const isCropValid = parameters.isCropAreaValid(pdfBounds);
const isFullCrop = parameters.isFullPDFCrop(pdfBounds);
return (
<Stack gap="md">
{/* PDF Preview with Crop Selector */}
<Stack gap="xs">
<Group justify="space-between" align="center">
<Text size="sm" fw={500}>
{t("crop.preview.title", "Crop Area Selection")}
</Text>
<ActionIcon
variant="outline"
onClick={handleReset}
disabled={disabled || isFullCrop}
title={t("crop.reset", "Reset to full PDF")}
aria-label={t("crop.reset", "Reset to full PDF")}
>
<RestartAltIcon style={{ fontSize: '1rem' }} />
</ActionIcon>
</Group>
<Center>
<Box
style={{
width: CONTAINER_SIZE,
height: CONTAINER_SIZE,
border: '1px solid var(--mantine-color-gray-3)',
borderRadius: '8px',
backgroundColor: 'var(--mantine-color-gray-0)',
overflow: 'hidden',
position: 'relative'
}}
>
<CropAreaSelector
pdfBounds={pdfBounds}
cropArea={cropArea}
onCropAreaChange={handleCropAreaChange}
disabled={disabled}
>
<DocumentThumbnail
file={selectedStub}
thumbnail={thumbnail}
style={{
width: pdfBounds.thumbnailWidth,
height: pdfBounds.thumbnailHeight,
position: 'absolute',
left: pdfBounds.offsetX,
top: pdfBounds.offsetY
}}
/>
</CropAreaSelector>
</Box>
</Center>
</Stack>
{/* Manual Coordinate Input */}
<Stack gap="xs">
<Text size="sm" fw={500}>
{t("crop.coordinates.title", "Precise Coordinates (PDF Points)")}
</Text>
<Group grow>
<NumberInput
label={t("crop.coordinates.x", "X (Left)")}
value={Math.round(cropArea.x * 10) / 10}
onChange={(value) => handleCoordinateChange('x', value)}
disabled={disabled}
min={0}
max={pdfBounds.actualWidth}
step={0.1}
decimalScale={1}
size="xs"
/>
<NumberInput
label={t("crop.coordinates.y", "Y (Bottom)")}
value={Math.round(cropArea.y * 10) / 10}
onChange={(value) => handleCoordinateChange('y', value)}
disabled={disabled}
min={0}
max={pdfBounds.actualHeight}
step={0.1}
decimalScale={1}
size="xs"
/>
</Group>
<Group grow>
<NumberInput
label={t("crop.coordinates.width", "Width")}
value={Math.round(cropArea.width * 10) / 10}
onChange={(value) => handleCoordinateChange('width', value)}
disabled={disabled}
min={0.1}
max={pdfBounds.actualWidth}
step={0.1}
decimalScale={1}
size="xs"
/>
<NumberInput
label={t("crop.coordinates.height", "Height")}
value={Math.round(cropArea.height * 10) / 10}
onChange={(value) => handleCoordinateChange('height', value)}
disabled={disabled}
min={0.1}
max={pdfBounds.actualHeight}
step={0.1}
decimalScale={1}
size="xs"
/>
</Group>
{/* Validation Alert */}
{!isCropValid && (
<Alert color="red" variant="light">
<Text size="xs">
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
</Text>
</Alert>
)}
</Stack>
</Stack>
);
};
export default CropSettings;