[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 <[email protected]>
This commit is contained in:
Balázs Szücs
2025-12-25 14:22:46 +00:00
committed by GitHub
parent d97820c232
commit 43a5f72b01
4 changed files with 86 additions and 67 deletions
@@ -2968,6 +2968,7 @@ header = "Crop PDF"
submit = "Apply Crop" submit = "Apply Crop"
noFileSelected = "Select a PDF file to begin cropping" noFileSelected = "Select a PDF file to begin cropping"
reset = "Reset to full PDF" reset = "Reset to full PDF"
autoCrop = "Auto-crop whitespace"
[crop.preview] [crop.preview]
title = "Crop Area Selection" title = "Crop Area Selection"
@@ -1,5 +1,5 @@
import { useMemo, useState, useEffect } from "react"; 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 { useTranslation } from "react-i18next";
import RestartAltIcon from "@mui/icons-material/RestartAlt"; import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { CropParametersHook } from "@app/hooks/tools/crop/useCropParameters"; import { CropParametersHook } from "@app/hooks/tools/crop/useCropParameters";
@@ -151,7 +151,16 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
return ( return (
<Stack gap="md" data-tour="crop-settings"> <Stack gap="md" data-tour="crop-settings">
{/* PDF Preview with Crop Selector */} {/* 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}
/>
{/* PDF Preview with Crop Selector - Only show when autoCrop is false */}
{!parameters.parameters.autoCrop && (
<Stack gap="xs"> <Stack gap="xs">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
@@ -202,8 +211,10 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
</Center> </Center>
</Stack> </Stack>
)}
{/* Manual Coordinate Input */} {/* Manual Coordinate Input - Only show when autoCrop is false */}
{!parameters.parameters.autoCrop && (
<CropCoordinateInputs <CropCoordinateInputs
cropArea={cropArea} cropArea={cropArea}
onCoordinateChange={handleCoordinateChange} onCoordinateChange={handleCoordinateChange}
@@ -211,9 +222,10 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
pdfBounds={pdfBounds} pdfBounds={pdfBounds}
showAutomationInfo={false} showAutomationInfo={false}
/> />
)}
{/* Validation Alert */} {/* Validation Alert - Only show when autoCrop is false */}
{!isCropValid && ( {!parameters.parameters.autoCrop && !isCropValid && (
<Alert color="red" variant="light"> <Alert color="red" variant="light">
<Text size="xs"> <Text size="xs">
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")} {t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
@@ -7,13 +7,17 @@ import { CropParameters, defaultParameters } from '@app/hooks/tools/crop/useCrop
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => { export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
const formData = new FormData(); const formData = new FormData();
formData.append("fileInput", file); formData.append("fileInput", file);
if (!parameters.autoCrop) {
const cropArea = parameters.cropArea; const cropArea = parameters.cropArea;
// Backend expects precise float values for PDF coordinates
formData.append("x", cropArea.x.toString()); formData.append("x", cropArea.x.toString());
formData.append("y", cropArea.y.toString()); formData.append("y", cropArea.y.toString());
formData.append("width", cropArea.width.toString()); formData.append("width", cropArea.width.toString());
formData.append("height", cropArea.height.toString()); formData.append("height", cropArea.height.toString());
}
formData.append("autoCrop", parameters.autoCrop.toString());
return formData; return formData;
}; };
@@ -6,10 +6,12 @@ import { DEFAULT_CROP_AREA } from '@app/constants/cropConstants';
export interface CropParameters extends BaseParameters { export interface CropParameters extends BaseParameters {
cropArea: Rectangle; cropArea: Rectangle;
autoCrop: boolean;
} }
export const defaultParameters: CropParameters = { export const defaultParameters: CropParameters = {
cropArea: DEFAULT_CROP_AREA, cropArea: DEFAULT_CROP_AREA,
autoCrop: false,
}; };
export type CropParametersHook = BaseParametersHook<CropParameters> & { export type CropParametersHook = BaseParametersHook<CropParameters> & {