mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
Add Crop to V2 (#4471)
# Description of Changes Add Crop to V2 --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
This commit is contained in:
39
frontend/src/hooks/tools/crop/useCropOperation.ts
Normal file
39
frontend/src/hooks/tools/crop/useCropOperation.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { CropParameters, defaultParameters } from './useCropParameters';
|
||||
|
||||
// Static configuration that can be used by both the hook and automation executor
|
||||
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
const cropArea = parameters.cropArea;
|
||||
|
||||
// Backend expects precise float values for PDF coordinates
|
||||
formData.append("x", cropArea.x.toString());
|
||||
formData.append("y", cropArea.y.toString());
|
||||
formData.append("width", cropArea.width.toString());
|
||||
formData.append("height", cropArea.height.toString());
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const cropOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildCropFormData,
|
||||
operationType: 'crop',
|
||||
endpoint: '/api/v1/general/crop',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useCropOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<CropParameters>({
|
||||
...cropOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(
|
||||
t('crop.error.failed', 'An error occurred while cropping the PDF.')
|
||||
)
|
||||
});
|
||||
};
|
||||
141
frontend/src/hooks/tools/crop/useCropParameters.ts
Normal file
141
frontend/src/hooks/tools/crop/useCropParameters.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
import { useCallback } from 'react';
|
||||
import { Rectangle, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea, isRectangle } from '../../../utils/cropCoordinates';
|
||||
import { DEFAULT_CROP_AREA } from '../../../constants/cropConstants';
|
||||
|
||||
export interface CropParameters extends BaseParameters {
|
||||
cropArea: Rectangle;
|
||||
}
|
||||
|
||||
export const defaultParameters: CropParameters = {
|
||||
cropArea: DEFAULT_CROP_AREA,
|
||||
};
|
||||
|
||||
export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
||||
/** Set crop area with PDF bounds validation */
|
||||
setCropArea: (cropArea: Rectangle, pdfBounds?: PDFBounds) => void;
|
||||
/** Get current crop area as CropArea object */
|
||||
getCropArea: () => Rectangle;
|
||||
/** Reset to full PDF dimensions */
|
||||
resetToFullPDF: (pdfBounds: PDFBounds) => void;
|
||||
/** Check if current crop area is valid for the PDF */
|
||||
isCropAreaValid: (pdfBounds?: PDFBounds) => boolean;
|
||||
/** Check if crop area covers the entire PDF */
|
||||
isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean;
|
||||
/** Update crop area with constraints applied */
|
||||
updateCropAreaConstrained: (cropArea: Partial<Rectangle>, pdfBounds?: PDFBounds) => void;
|
||||
};
|
||||
|
||||
export const useCropParameters = (): CropParametersHook => {
|
||||
const baseHook = useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'crop',
|
||||
validateFn: (params) => {
|
||||
const rect = params.cropArea;
|
||||
// Basic validation - coordinates and dimensions must be positive
|
||||
return rect.x >= 0 &&
|
||||
rect.y >= 0 &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0;
|
||||
},
|
||||
});
|
||||
|
||||
// Get current crop area as CropArea object
|
||||
const getCropArea = useCallback((): Rectangle => {
|
||||
return baseHook.parameters.cropArea;
|
||||
}, [baseHook.parameters]);
|
||||
|
||||
// Set crop area with optional PDF bounds validation
|
||||
const setCropArea = useCallback((cropArea: Rectangle, pdfBounds?: PDFBounds) => {
|
||||
let finalCropArea = roundCropArea(cropArea);
|
||||
|
||||
// Apply PDF bounds constraints if provided
|
||||
if (pdfBounds) {
|
||||
finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds);
|
||||
}
|
||||
baseHook.updateParameter('cropArea', finalCropArea);
|
||||
}, [baseHook]);
|
||||
|
||||
// Reset to cover entire PDF
|
||||
const resetToFullPDF = useCallback((pdfBounds: PDFBounds) => {
|
||||
const fullCropArea = createFullPDFCropArea(pdfBounds);
|
||||
setCropArea(fullCropArea);
|
||||
}, [setCropArea]);
|
||||
|
||||
// Check if current crop area is valid for the given PDF bounds
|
||||
const isCropAreaValid = useCallback((pdfBounds?: PDFBounds): boolean => {
|
||||
const cropArea = getCropArea();
|
||||
|
||||
// Basic validation
|
||||
if (cropArea.x < 0 || cropArea.y < 0 || cropArea.width <= 0 || cropArea.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// PDF bounds validation if provided
|
||||
if (pdfBounds) {
|
||||
const tolerance = 0.01; // Small tolerance for floating point precision
|
||||
return cropArea.x + cropArea.width <= pdfBounds.actualWidth + tolerance &&
|
||||
cropArea.y + cropArea.height <= pdfBounds.actualHeight + tolerance;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [getCropArea]);
|
||||
|
||||
// Check if crop area covers the entire PDF
|
||||
const isFullPDFCrop = useCallback((pdfBounds?: PDFBounds): boolean => {
|
||||
if (!pdfBounds) return false;
|
||||
|
||||
const cropArea = getCropArea();
|
||||
const tolerance = 0.5; // Allow 0.5 point tolerance for floating point precision
|
||||
|
||||
return Math.abs(cropArea.x) < tolerance &&
|
||||
Math.abs(cropArea.y) < tolerance &&
|
||||
Math.abs(cropArea.width - pdfBounds.actualWidth) < tolerance &&
|
||||
Math.abs(cropArea.height - pdfBounds.actualHeight) < tolerance;
|
||||
}, [getCropArea]);
|
||||
|
||||
// Update crop area with constraints applied
|
||||
const updateCropAreaConstrained = useCallback((
|
||||
partialCropArea: Partial<Rectangle>,
|
||||
pdfBounds?: PDFBounds
|
||||
) => {
|
||||
const currentCropArea = getCropArea();
|
||||
const newCropArea = { ...currentCropArea, ...partialCropArea };
|
||||
setCropArea(newCropArea, pdfBounds);
|
||||
}, [getCropArea, setCropArea]);
|
||||
|
||||
// Enhanced validation that considers PDF bounds
|
||||
const validateParameters = useCallback((pdfBounds?: PDFBounds): boolean => {
|
||||
return baseHook.validateParameters() && isCropAreaValid(pdfBounds);
|
||||
}, [baseHook, isCropAreaValid]);
|
||||
|
||||
// Override updateParameter to ensure positive values
|
||||
const updateParameter = useCallback(<K extends keyof CropParameters>(
|
||||
parameter: K,
|
||||
value: CropParameters[K]
|
||||
) => {
|
||||
|
||||
if(isRectangle(value)) {
|
||||
value.x = Math.max(0.1, value.x); // Minimum 0.1 point
|
||||
value.x = Math.max(0.1, value.y); // Minimum 0.1 point
|
||||
value.width = Math.max(0, value.width); // Minimum 0 point
|
||||
value.height = Math.max(0, value.height); // Minimum 0 point
|
||||
}
|
||||
|
||||
baseHook.updateParameter(parameter, value);
|
||||
}, [baseHook]);
|
||||
|
||||
|
||||
return {
|
||||
...baseHook,
|
||||
updateParameter,
|
||||
validateParameters: () => validateParameters(),
|
||||
setCropArea,
|
||||
getCropArea,
|
||||
resetToFullPDF,
|
||||
isCropAreaValid,
|
||||
isFullPDFCrop,
|
||||
updateCropAreaConstrained,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user