[V2] feat(crop): add auto-crop whitespace option to crop tool UI (#5275)

# Description of Changes

This pull request adds an auto-crop feature to the PDF crop tool,
allowing users to automatically crop whitespace from PDFs. The UI now
includes an "Auto-crop whitespace" checkbox, and when enabled, manual
crop controls are hidden. The crop operation logic and form data
submission have been updated to support this new option.

**Auto-crop Feature Implementation**

* Added an `autoCrop` boolean parameter to the `CropParameters`
interface and set its default value to `false` in
`useCropParameters.ts`.
* Updated the crop operation logic in `useCropOperation.ts` to include
the `autoCrop` parameter in the form data and only send manual crop
coordinates if `autoCrop` is disabled.

**User Interface Updates**

* Added an "Auto-crop whitespace" checkbox to the crop settings UI in
`CropSettings.tsx`, which toggles the auto-crop feature
* Modified the crop settings UI to hide manual crop controls and
validation alerts when auto-crop is enabled

**Localization**

* Added a new translation string for "Auto-crop whitespace" in the
English locale file `translation.toml`.



<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/a92988b8-eea0-47e7-961f-b4a6e018ff2f"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-12-25 15:22:46 +01:00 committed by GitHub
parent d97820c232
commit 43a5f72b01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 86 additions and 67 deletions

View File

@ -2968,6 +2968,7 @@ header = "Crop PDF"
submit = "Apply Crop"
noFileSelected = "Select a PDF file to begin cropping"
reset = "Reset to full PDF"
autoCrop = "Auto-crop whitespace"
[crop.preview]
title = "Crop Area Selection"

View File

@ -1,5 +1,5 @@
import { useMemo, useState, useEffect } from "react";
import { Stack, Text, Box, Group, ActionIcon, Center, Alert } from "@mantine/core";
import { Stack, Text, Box, Group, ActionIcon, Center, Alert, Checkbox } from "@mantine/core";
import { useTranslation } from "react-i18next";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { CropParametersHook } from "@app/hooks/tools/crop/useCropParameters";
@ -151,69 +151,81 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
return (
<Stack gap="md" data-tour="crop-settings">
{/* 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 */}
<CropCoordinateInputs
cropArea={cropArea}
onCoordinateChange={handleCoordinateChange}
{/* Auto-Crop Checkbox */}
<Checkbox
label={t("crop.autoCrop", "Auto-crop whitespace")}
checked={parameters.parameters.autoCrop}
onChange={(e) => parameters.updateParameter('autoCrop', e.currentTarget.checked)}
disabled={disabled}
pdfBounds={pdfBounds}
showAutomationInfo={false}
/>
{/* Validation Alert */}
{!isCropValid && (
{/* PDF Preview with Crop Selector - Only show when autoCrop is false */}
{!parameters.parameters.autoCrop && (
<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 - Only show when autoCrop is false */}
{!parameters.parameters.autoCrop && (
<CropCoordinateInputs
cropArea={cropArea}
onCoordinateChange={handleCoordinateChange}
disabled={disabled}
pdfBounds={pdfBounds}
showAutomationInfo={false}
/>
)}
{/* Validation Alert - Only show when autoCrop is false */}
{!parameters.parameters.autoCrop && !isCropValid && (
<Alert color="red" variant="light">
<Text size="xs">
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}

View File

@ -7,13 +7,17 @@ import { CropParameters, defaultParameters } from '@app/hooks/tools/crop/useCrop
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());
if (!parameters.autoCrop) {
const cropArea = parameters.cropArea;
formData.append("x", cropArea.x.toString());
formData.append("y", cropArea.y.toString());
formData.append("width", cropArea.width.toString());
formData.append("height", cropArea.height.toString());
}
formData.append("autoCrop", parameters.autoCrop.toString());
return formData;
};

View File

@ -6,10 +6,12 @@ import { DEFAULT_CROP_AREA } from '@app/constants/cropConstants';
export interface CropParameters extends BaseParameters {
cropArea: Rectangle;
autoCrop: boolean;
}
export const defaultParameters: CropParameters = {
cropArea: DEFAULT_CROP_AREA,
autoCrop: false,
};
export type CropParametersHook = BaseParametersHook<CropParameters> & {