react-document-perspective-crop
v0.1.1
Published
A production-ready React component for document scanning, perspective correction, and image cropping with OpenCV integration.
Maintainers
Readme
React Document Perspective Crop
A robust, production-ready React library for document scanning, edge detection, and perspective cropping using OpenCV.js.
Designed to be as capable as react-easy-crop or react-image-crop, but purpose-built for document scanning workflows.
Features
- 📸 Perspective Correction — Advanced 4-point perspective cropping with sub-pixel precision.
- 🤖 Auto Edge Detection — Built-in OpenCV-powered smart document detection.
- 🎨 Image Adjustments — Brightness, contrast, and sharpness controls.
- 🔄 Transformations — 90° rotations and interactive wheel-based zooming.
- 📱 Multi-Image Support — Batch processing with drag-and-drop thumbnail reordering.
- ⌨️ Keyboard Shortcuts — Full keyboard navigation with configurable shortcuts.
- ↩️ Undo / Redo — Per-image history stack for all editing operations.
- 🎨 Headless by Default — Use the polished built-in UI or build your own with the
usePerspectiveCrophook. - 💅 Framework Agnostic — Zero UI dependencies. Styled purely with CSS Custom Properties.
- 🌍 Fully Translatable — Every UI string can be overridden via the
i18n/translationsprop.
Table of Contents
- Installation
- Quick Start
- Component API
- Headless Hooks API
- Types Reference
- Theming
- Internationalization (i18n)
- Keyboard Shortcuts
- Sub-Components
- Utility Exports
- Running the Example Locally
- License
Installation
npm install react-document-perspective-cropPeer dependencies: react >= 18.0.0 and react-dom >= 18.0.0.
Quick Start
import React, { useState } from 'react';
import {
DocumentPerspectiveCrop,
type ExportResult,
} from 'react-document-perspective-crop';
import 'react-document-perspective-crop/styles';
function App() {
const [images, setImages] = useState<File[]>([]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) setImages(Array.from(e.target.files));
};
const handleSave = (results: readonly ExportResult[]) => {
console.log('Processed images:', results);
// results[0].blob — the cropped/adjusted image as a Blob
// results[0].width / results[0].height — exported dimensions
// results[0].filename — original filename
};
return (
<div>
<input type="file" multiple accept="image/*" onChange={handleFileChange} />
{images.length > 0 && (
<div style={{ height: '80vh', width: '100%' }}>
<DocumentPerspectiveCrop
images={images}
onSave={handleSave}
onCancel={() => console.log('Cancelled')}
/>
</div>
)}
</div>
);
}Component API
<DocumentPerspectiveCrop /> Props
The main component accepts a single props object of type DocumentPerspectiveCropProps.
Required Props
| Prop | Type | Description |
|------|------|-------------|
| images | readonly File[] | Image files to edit. The component initializes its internal state from this array. |
Feature Flags
Control which editing features are available to the user. All default to true.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| autoDetect | boolean | true | Enable automatic document edge detection on image load. |
| multiImage | boolean | true | Enable multi-image editing mode with thumbnail strip. |
| allowBrightness | boolean | true | Show the brightness adjustment slider. |
| allowContrast | boolean | true | Show the contrast adjustment slider. |
| allowSharpness | boolean | true | Show the sharpness adjustment slider. |
| allowRotate | boolean | true | Allow 90° rotation controls. |
| allowZoom | boolean | true | Allow zoom in/out controls and wheel zoom. |
| allowAddImages | boolean | true | Allow adding more images after initial load. |
| allowDelete | boolean | true | Allow deleting individual images. |
| allowUndo | boolean | true | Enable undo/redo functionality. |
| enableKeyboardShortcuts | boolean | true | Enable keyboard shortcut bindings. |
Output Configuration
Control the format and quality of exported images.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| maxWidth | number | 4096 | Maximum output width in pixels. Images wider than this are downscaled. |
| maxHeight | number | 4096 | Maximum output height in pixels. Images taller than this are downscaled. |
| outputFormat | ExportFormat | 'image/jpeg' | Output image format. One of 'image/jpeg', 'image/png', or 'image/webp'. |
| outputQuality | number | 0.95 | Output JPEG/WebP quality. Range: 0 to 1. Ignored for PNG. |
Appearance
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| theme | ThemeConfig | 'light' | Theme preset ('light' or 'dark') or a ThemeOverrides object for full customization. |
| className | string | '' | Additional CSS class name appended to the root element. |
| style | React.CSSProperties | — | Additional inline styles for the root element. |
Customization
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| detectionParams | Partial<DetectionParams> | — | Override the auto-detection algorithm parameters (Canny thresholds, sensitivity, etc.). See DetectionParams. |
| opencvUrl | string | 'https://docs.opencv.org/4.10.0/opencv.js' | Custom OpenCV.js URL. Overrides the default CDN. |
| opencvInstance | OpenCVInstance | — | Pre-loaded OpenCV instance. When provided, the library skips loading entirely. |
| translations | Partial<Translations> | — | Custom translations for all user-facing text strings. |
| i18n | Partial<Translations> | — | Alias for translations. Override any UI string. |
| acceptedFileTypes | string | 'image/jpeg,image/png,image/webp' | MIME types accepted by the "Add Image" file input. |
Callbacks
| Prop | Type | Description |
|------|------|-------------|
| onSave | (results: readonly ExportResult[]) => void | Called when the user clicks Save. Receives an array of ExportResult objects — one per image. |
| onCancel | () => void | Called when the user clicks Cancel or presses Escape. |
| onError | (error: Error) => void | Called when an error occurs during export or processing. |
| onOpenCVReady | () => void | Called once OpenCV.js has finished loading and is ready to use. |
| onImageChange | (index: number) => void | Called when the user switches to a different image. Receives the new active index. |
| onCornersChange | (corners: Corners \| null) => void | Called when the crop corners are modified (drag, auto-detect, or reset). |
Render Props
Override any visual element with your own implementation. Each render prop receives a typed props object.
| Prop | Type | Props Received | Description |
|------|------|----------------|-------------|
| renderToolbar | (props: ToolbarRenderProps) => ReactNode | See below | Replace the entire toolbar. |
| renderSidebar | (props: SidebarRenderProps) => ReactNode | See below | Replace the sidebar panel. |
| renderThumbnail | (props: ThumbnailRenderProps) => ReactNode | See below | Replace individual thumbnail items. |
| renderCornerHandle | (props: CornerHandleRenderProps) => ReactNode | See below | Replace the corner drag handles. |
| renderCropOverlay | (props: CropOverlayRenderProps) => ReactNode | See below | Replace the crop overlay polygon. |
ToolbarRenderProps
interface ToolbarRenderProps {
onAutoDetect: () => void; // Trigger auto-detection
onRotateLeft: () => void; // Rotate 90° counter-clockwise
onRotateRight: () => void; // Rotate 90° clockwise
onZoomIn: () => void; // Zoom in by 1.2x
onZoomOut: () => void; // Zoom out by 1.2x
onReset: () => void; // Reset all edits on current image
onUndo: () => void; // Undo last action
onRedo: () => void; // Redo last undone action
canUndo: boolean; // Whether undo stack has entries
canRedo: boolean; // Whether redo stack has entries
isLoading: boolean; // Whether a background operation is running
isOpenCVReady: boolean; // Whether OpenCV has loaded
scale: number; // Current zoom scale (1 = 100%)
rotation: number; // Current rotation in degrees (0/90/180/270)
}SidebarRenderProps
interface SidebarRenderProps {
brightness: number; // Current brightness (0–200, default: 100)
contrast: number; // Current contrast (0–200, default: 100)
sharpness: number; // Current sharpness (0–100, default: 0)
onBrightnessChange: (value: number) => void;
onContrastChange: (value: number) => void;
onSharpnessChange: (value: number) => void;
isDisabled: boolean; // True when no image is loaded
}ThumbnailRenderProps
interface ThumbnailRenderProps {
index: number; // Position in the image list
previewUrl: string; // Object URL for the thumbnail preview
isActive: boolean; // Whether this image is currently being edited
isEdited: boolean; // Whether the image has been modified
onSelect: () => void; // Switch to this image
onDelete: () => void; // Remove this image from the list
}CornerHandleRenderProps
interface CornerHandleRenderProps {
cornerKey: keyof Corners; // 'topLeft' | 'topRight' | 'bottomRight' | 'bottomLeft'
x: number; // X position in viewport coordinates
y: number; // Y position in viewport coordinates
onPointerDown: (e: React.PointerEvent) => void; // Attach to start dragging
}CropOverlayRenderProps
interface CropOverlayRenderProps {
corners: Corners; // Current corner positions
isDragging: boolean; // Whether a corner is being dragged
activeCorner: keyof Corners | null; // Which corner is being dragged
createCornerHandler: (key: keyof Corners) => (e: React.PointerEvent) => void;
}Headless Hooks API
Build a completely custom UI by composing individual hooks. Every hook is exported from the main package.
import {
usePerspectiveCrop,
useOpenCV,
useDocumentDetection,
useImageAdjustments,
useImageTransform,
useCornerDrag,
useMultiImage,
useUndoHistory,
useExport,
useKeyboardShortcuts,
useWheelZoom,
} from 'react-document-perspective-crop';usePerspectiveCrop
The main orchestration hook that composes all sub-hooks into a single, batteries-included API. Use this to build custom UIs without the <DocumentPerspectiveCrop /> component.
Config
interface PerspectiveCropConfig {
images: readonly File[]; // Image files to edit (required)
autoDetect?: boolean; // Auto-detect edges on load (default: true)
opencvUrl?: string; // Custom OpenCV.js URL
opencvInstance?: OpenCVInstance; // Pre-loaded OpenCV instance
maxWidth?: number; // Max export width (default: 4096)
maxHeight?: number; // Max export height (default: 4096)
outputFormat?: ExportFormat; // Export format (default: 'image/jpeg')
outputQuality?: number; // Export quality (default: 0.95)
onCornersChange?: (corners: Corners | null) => void; // Corner change callback
}Return Value
interface UsePerspectiveCropReturn {
// ── Multi-Image ──
images: readonly ImageState[]; // All image states
activeIndex: number; // Index of the active image
activeImage: ImageState | null; // The active image state (null if empty)
setActiveIndex: (index: number) => void; // Switch to image at index
addImages: (files: readonly File[]) => Promise<void>; // Add new images
removeImage: (index: number) => void; // Remove image at index
reorderImages: (from: number, to: number) => void; // Drag-and-drop reorder
// ── Corners ──
corners: Corners | null; // Current crop corners (null = full image)
setCorners: (corners: Corners | null) => void; // Update corners (pushes undo state)
// ── Transform ──
scale: number; // Current zoom level (0.1–6.0)
rotation: number; // Current rotation (0, 90, 180, 270)
zoomIn: () => void; // Zoom in by 1.2x
zoomOut: () => void; // Zoom out by 1.2x
rotateLeft: () => void; // Rotate 90° counter-clockwise
rotateRight: () => void; // Rotate 90° clockwise
// ── Adjustments ──
adjustments: ImageAdjustments; // { brightness, contrast, sharpness }
setBrightness: (value: number) => void; // Set brightness (0–200)
setContrast: (value: number) => void; // Set contrast (0–200)
setSharpness: (value: number) => void; // Set sharpness (0–100)
// ── Detection ──
autoDetect: () => Promise<boolean>; // Run auto-detection. Returns true if edges found.
// ── Undo / Redo ──
undo: () => void; // Undo last action
redo: () => void; // Redo last undone action
canUndo: boolean; // Whether undo is available
canRedo: boolean; // Whether redo is available
// ── Export ──
exportAll: () => Promise<readonly ExportResult[]>; // Export all images
reset: () => void; // Reset current image to original state
// ── Status ──
isLoading: boolean; // True during OpenCV load, image processing, or detection
isOpenCVReady: boolean; // True once OpenCV is loaded and available
error: Error | null; // Last error (OpenCV load or detection)
}Example
function CustomEditor({ files }: { files: File[] }) {
const crop = usePerspectiveCrop({
images: files,
outputFormat: 'image/png',
outputQuality: 1.0,
});
return (
<div>
{crop.activeImage && (
<img
src={crop.activeImage.previewUrl}
alt="Preview"
style={{ transform: `scale(${crop.scale}) rotate(${crop.rotation}deg)` }}
/>
)}
<div>
<button onClick={crop.autoDetect} disabled={!crop.isOpenCVReady}>
Auto Detect
</button>
<button onClick={crop.rotateLeft}>↶ Rotate</button>
<button onClick={crop.rotateRight}>↷ Rotate</button>
<button onClick={crop.zoomIn}>+ Zoom</button>
<button onClick={crop.zoomOut}>− Zoom</button>
<button onClick={crop.undo} disabled={!crop.canUndo}>Undo</button>
<button onClick={crop.redo} disabled={!crop.canRedo}>Redo</button>
</div>
<div>
<label>
Brightness
<input
type="range" min={0} max={200}
value={crop.adjustments.brightness}
onChange={(e) => crop.setBrightness(Number(e.target.value))}
/>
</label>
</div>
<button onClick={async () => {
const results = await crop.exportAll();
console.log(results);
}}>
Save All
</button>
</div>
);
}useOpenCV
Lazily loads and manages the OpenCV.js lifecycle with caching.
function useOpenCV(url?: string, existingInstance?: OpenCVInstance): UseOpenCVReturn;| Parameter | Type | Description |
|-----------|------|-------------|
| url | string? | Custom OpenCV.js URL. Omit to use the default CDN (https://docs.opencv.org/4.10.0/opencv.js). |
| existingInstance | OpenCVInstance? | Pre-loaded OpenCV instance. When provided, loading is skipped entirely. |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| cv | OpenCVInstance \| null | The loaded OpenCV instance, or null if not yet loaded. |
| isReady | boolean | true once OpenCV is loaded and usable. |
| isLoading | boolean | true while the OpenCV script is downloading. |
| error | Error \| null | Error that occurred during loading, if any. |
const { cv, isReady, isLoading, error } = useOpenCV();
if (isLoading) return <p>Loading OpenCV…</p>;
if (error) return <p>Error: {error.message}</p>;
// cv is now availableuseDocumentDetection
Wraps the OpenCV edge detection algorithm with React state management.
function useDocumentDetection(
cv: OpenCVInstance | null,
params?: Partial<DetectionParams>,
): UseDocumentDetectionReturn;| Parameter | Type | Description |
|-----------|------|-------------|
| cv | OpenCVInstance \| null | OpenCV instance from useOpenCV. |
| params | Partial<DetectionParams>? | Override detection algorithm parameters. |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| detect | (imageSource: string \| File) => Promise<Corners \| null> | Run detection on an image URL or File. Returns detected Corners or null if no document found. |
| isDetecting | boolean | true while detection is running. |
| error | Error \| null | Last detection error (auto-clears after 3 seconds). |
useImageAdjustments
Manages brightness, contrast, and sharpness state with pre-computed CSS filters.
function useImageAdjustments(
initialValues?: Partial<ImageAdjustments>,
filterId?: string,
): UseImageAdjustmentsReturn;| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| initialValues | Partial<ImageAdjustments>? | { brightness: 100, contrast: 100, sharpness: 0 } | Initial adjustment values. |
| filterId | string | 'rdpc-sharpness' | Unique SVG filter ID for this instance. |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| adjustments | ImageAdjustments | Current values: { brightness, contrast, sharpness }. |
| setBrightness | (value: number) => void | Set brightness (0–200, clamped). |
| setContrast | (value: number) => void | Set contrast (0–200, clamped). |
| setSharpness | (value: number) => void | Set sharpness (0–100, clamped). |
| resetAdjustments | () => void | Reset all adjustments to defaults. |
| cssFilter | string | Pre-computed CSS filter string for live preview (e.g. brightness(1.2) contrast(0.8) url(#rdpc-sharpness)). |
| sharpnessFilterId | string | SVG filter ID, or '' if sharpness is 0. |
| sharpnessKernelMatrix | string | SVG feConvolveMatrix kernel string for sharpness. |
const { adjustments, setBrightness, cssFilter } = useImageAdjustments();
<img src={url} style={{ filter: cssFilter }} />;useImageTransform
Manages zoom scale and rotation state.
function useImageTransform(): UseImageTransformReturn;Returns:
| Property | Type | Description |
|----------|------|-------------|
| scale | number | Current zoom level. Range: 0.1 to 6.0. Default: 1. |
| rotation | number | Current rotation in degrees. Values: 0, 90, 180, 270. |
| zoomIn | () => void | Multiply scale by 1.2 (clamped). |
| zoomOut | () => void | Divide scale by 1.2 (clamped). |
| setScale | (scale: number) => void | Set zoom to an exact value (clamped to 0.1–6.0). |
| rotateLeft | () => void | Rotate counter-clockwise by 90°. |
| rotateRight | () => void | Rotate clockwise by 90°. |
| resetTransform | () => void | Reset scale to 1 and rotation to 0. |
| handleWheel | (e: WheelEvent) => void | Wheel event handler — scroll up zooms in, scroll down zooms out. |
useCornerDrag
Handles interactive dragging of corner handles on the crop overlay with pointer capture.
function useCornerDrag(config: CornerDragConfig): UseCornerDragReturn;Config:
| Property | Type | Description |
|----------|------|-------------|
| corners | Corners \| null | Current corners in image coordinates. |
| onCornersChange | (corners: Corners) => void | Callback to update corners. |
| getDisplayBounds | () => ImageDisplayBounds \| null | Function returning current display geometry. |
| scale | number | Current zoom scale. |
| rotation | number | Current rotation in degrees. |
| rootRef | React.RefObject<HTMLElement \| null> | Root element ref for coordinate calculations. |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| isDragging | boolean | Whether a corner is currently being dragged. |
| activeCorner | keyof Corners \| null | Which corner is being dragged ('topLeft', 'topRight', etc.), or null. |
| createCornerHandler | (cornerKey: keyof Corners) => (e: React.PointerEvent) => void | Factory function — call it with a corner key to get a pointerdown handler. |
useMultiImage
Manages a list of images with add, remove, reorder, and per-image state updates. Handles object URL lifecycle automatically.
function useMultiImage(
initialFiles: readonly File[],
autoDetectFn?: (previewUrl: string) => Promise<Corners | null>,
): UseMultiImageReturn;| Parameter | Type | Description |
|-----------|------|-------------|
| initialFiles | readonly File[] | Initial files to load. |
| autoDetectFn | (previewUrl: string) => Promise<Corners \| null> | Optional auto-detection function called for each new image. |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| images | readonly ImageState[] | All image states in order. |
| activeIndex | number | Index of the currently active image. |
| activeImage | ImageState \| null | The currently active image state, or null if empty. |
| setActiveIndex | (index: number) => void | Switch to a specific image by index. |
| addImages | (files: readonly File[]) => Promise<void> | Add new images to the end of the list. Auto-detection runs if configured. |
| removeImage | (index: number) => void | Remove an image by index. Cannot remove the last image. |
| reorderImages | (fromIndex: number, toIndex: number) => void | Move an image from one position to another (drag-and-drop). |
| updateImage | (index: number, updates: Partial<ImageState>) => void | Partially update a specific image's state. |
| isProcessing | boolean | true while images are being loaded or added. |
useUndoHistory
Generic undo/redo history stack. Works with any serializable state type.
function useUndoHistory<T>(maxHistory?: number): UseUndoHistoryReturn<T>;| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| maxHistory | number | 50 | Maximum number of undo states retained. Oldest states are discarded when exceeded. |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| push | (state: T) => void | Push a state snapshot. Clears the redo stack. |
| undo | () => T \| null | Pop and return the previous state, or null if nothing to undo. |
| redo | () => T \| null | Pop and return the next state, or null if nothing to redo. |
| canUndo | boolean | Whether the undo stack has entries. |
| canRedo | boolean | Whether the redo stack has entries. |
| clear | () => void | Clear all history (both undo and redo). |
const history = useUndoHistory<MyState>();
// Before making a change:
history.push(currentState);
// Undo:
const prev = history.undo();
if (prev) applyState(prev);useExport
Handles batch and individual image export with perspective correction and adjustments applied.
function useExport(config: ExportConfig): UseExportReturn;Config:
| Property | Type | Description |
|----------|------|-------------|
| images | readonly ImageState[] | All image states to export. |
| cv | OpenCVInstance \| null | OpenCV instance for perspective warp. |
| maxWidth | number | Maximum export width. |
| maxHeight | number | Maximum export height. |
| format | ExportFormat | Output format ('image/jpeg', 'image/png', 'image/webp'). |
| quality | number | Output quality (0–1). |
Returns:
| Property | Type | Description |
|----------|------|-------------|
| exportAll | () => Promise<readonly ExportResult[]> | Export all images. Returns an array of results. |
| exportOne | (index: number) => Promise<ExportResult \| null> | Export a single image by index. Returns null if index is invalid. |
| isExporting | boolean | true while export is in progress. |
| error | Error \| null | Last export error, if any. |
useKeyboardShortcuts
Registers document-level keyboard shortcuts. Automatically ignores key events when focus is on form elements.
function useKeyboardShortcuts(
handlers: KeyboardShortcutHandlers,
enabled?: boolean,
): void;| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| handlers | KeyboardShortcutHandlers | — | Map of action names to handler functions. |
| enabled | boolean | true | Whether shortcuts are active. |
Handler Properties:
| Handler | Type | Triggered By |
|---------|------|-------------|
| onUndo | () => void | Ctrl+Z |
| onRedo | () => void | Ctrl+Shift+Z or Ctrl+Y |
| onSave | () => void | Ctrl+S |
| onRotateLeft | () => void | Ctrl+← |
| onRotateRight | () => void | Ctrl+→ |
| onZoomIn | () => void | Ctrl+= or Ctrl++ |
| onZoomOut | () => void | Ctrl+- |
| onReset | () => void | Ctrl+R |
| onAutoDetect | () => void | Ctrl+D |
| onDelete | () => void | Delete or Backspace |
| onNextImage | () => void | → |
| onPreviousImage | () => void | ← |
| onEscape | () => void | Escape |
Note: On macOS,
Ctrlis automatically mapped toCmd (⌘).
useWheelZoom
Utility hook that attaches a non-passive wheel event listener to a DOM element for zoom control.
function useWheelZoom(
elementRef: React.RefObject<HTMLElement | null>,
handleWheel: (e: WheelEvent) => void,
): void;| Parameter | Type | Description |
|-----------|------|-------------|
| elementRef | React.RefObject<HTMLElement \| null> | Ref to the element that should respond to wheel events. |
| handleWheel | (e: WheelEvent) => void | Wheel handler (e.g. from useImageTransform). |
Types Reference
All types are exported from the main package and available for import:
import type {
// Geometry
Point,
Corners,
CornerKey,
Rect,
ImageDisplayBounds,
Size,
// Image
ExportFormat,
ExportOutputType,
ExportResult,
ImageAdjustments,
ImageState,
ImageStateSnapshot,
ImageInput,
ResolvedImageSource,
DetectionParams,
// Component
DocumentPerspectiveCropProps,
OpenCVInstance,
ToolbarRenderProps,
SidebarRenderProps,
ThumbnailRenderProps,
CornerHandleRenderProps,
CropOverlayRenderProps,
// Hooks
UseOpenCVReturn,
UseDocumentDetectionReturn,
UseImageAdjustmentsReturn,
UseCornerDragReturn,
UseImageTransformReturn,
UseMultiImageReturn,
UseUndoHistoryReturn,
UseExportReturn,
UsePerspectiveCropReturn,
// Theming
ThemeOverrides,
ThemePreset,
ThemeConfig,
// i18n
Translations,
} from 'react-document-perspective-crop';Key Types
Point
interface Point {
x: number; // X coordinate in pixels
y: number; // Y coordinate in pixels
}Corners
Defines the four corners of the crop quadrilateral in the image's natural pixel coordinate space.
interface Corners {
topLeft: Point;
topRight: Point;
bottomRight: Point;
bottomLeft: Point;
}ExportResult
Returned by onSave and export functions. Contains the processed image data.
interface ExportResult {
blob?: Blob; // Exported image as a Blob (when outputType is 'blob')
base64?: string; // Exported image as a base64 string
file?: File; // Exported image as a File object
filename: string; // Original filename
width: number; // Exported image width in pixels
height: number; // Exported image height in pixels
}ImageAdjustments
interface ImageAdjustments {
brightness: number; // 0–200, default 100 (100 = no change)
contrast: number; // 0–200, default 100 (100 = no change)
sharpness: number; // 0–100, default 0 (0 = no sharpening)
}ImageState
Complete state for a single image in the editor.
interface ImageState {
id: string; // Unique identifier
originalFile: File; // The original File (immutable reference)
currentFile: File; // Current working File (may differ after rotation)
previewUrl: string; // Object URL for preview rendering
cropCorners: Corners | null; // Crop region (null = full image, no perspective crop)
scale: number; // Zoom level (default: 1)
rotation: number; // Rotation degrees (0, 90, 180, 270)
adjustments: ImageAdjustments; // Brightness/contrast/sharpness
isEdited: boolean; // Whether modified from original
}DetectionParams
Parameters controlling the OpenCV auto-detection algorithm. Override via the detectionParams prop.
interface DetectionParams {
cannyLow: number; // Lower Canny edge threshold (default: 50)
cannyHigh: number; // Upper Canny edge threshold (default: 150)
morphKernelSize: number; // Morphological closing kernel size (default: 5)
minAreaRatio: number; // Min contour area as ratio of image area (default: 0.05)
maxAreaRatio: number; // Max contour area as ratio of image area (default: 0.98)
sensitivity: number; // Detection sensitivity 0–100 (default: 70)
minAspectRatio: number; // Min acceptable aspect ratio (default: 0.4)
maxAspectRatio: number; // Max acceptable aspect ratio (default: 2.5)
}Theming
The component is styled entirely with CSS Custom Properties, making it trivial to theme.
Using Presets
<DocumentPerspectiveCrop theme="dark" ... />
<DocumentPerspectiveCrop theme="light" ... />Importing Theme CSS Directly
import 'react-document-perspective-crop/themes/light';
import 'react-document-perspective-crop/themes/dark';Custom Theme Overrides
Pass a ThemeOverrides object to the theme prop for granular control:
<DocumentPerspectiveCrop
theme={{
colorPrimary: '#8b5cf6',
colorSurface: '#0f0f23',
colorText: '#e2e8f0',
handleColor: '#a78bfa',
borderRadius: '12px',
transitionDuration: '300ms',
}}
...
/>All CSS Custom Properties
| Property | CSS Variable | Default | Description |
|----------|-------------|---------|-------------|
| colorPrimary | --rdpc-color-primary | #2563eb | Primary accent color |
| colorPrimaryHover | --rdpc-color-primary-hover | #1d4ed8 | Primary hover state |
| colorDanger | --rdpc-color-danger | #dc2626 | Destructive actions |
| colorSuccess | --rdpc-color-success | #16a34a | Success indicators |
| colorSurface | --rdpc-color-surface | #ffffff | Surface background |
| colorSurfaceElevated | --rdpc-color-surface-elevated | #f8fafc | Elevated surface (cards, panels) |
| colorText | --rdpc-color-text | #0f172a | Primary text |
| colorTextMuted | --rdpc-color-text-muted | #64748b | Secondary/muted text |
| colorBorder | --rdpc-color-border | #e2e8f0 | Border color |
| colorOverlay | --rdpc-color-overlay | rgba(0,0,0,0.4) | Crop overlay dimming |
| handleColor | --rdpc-handle-color | Primary color | Corner handle fill |
| handleBorderColor | --rdpc-handle-border-color | #ffffff | Corner handle border |
| handleSize | --rdpc-handle-size | 20px | Corner handle size |
| handleBorderWidth | --rdpc-handle-border-width | 2px | Corner handle border width |
| cropStrokeColor | --rdpc-crop-stroke-color | Primary color | Crop polygon stroke |
| cropStrokeWidth | --rdpc-crop-stroke-width | 2px | Crop polygon stroke width |
| cropFillColor | --rdpc-crop-fill-color | rgba(37,99,235,0.08) | Crop polygon fill |
| fontFamily | --rdpc-font-family | System font stack | Font family |
| fontSize | --rdpc-font-size | 14px | Base font size |
| spacingUnit | --rdpc-spacing-unit | 4px | Base spacing unit |
| borderRadius | --rdpc-border-radius | 8px | Panel border radius |
| borderRadiusButton | --rdpc-border-radius-button | 6px | Button border radius |
| shadowElevated | --rdpc-shadow-elevated | — | Box shadow for elevated elements |
| shadowHandle | --rdpc-shadow-handle | — | Box shadow for corner handles |
| transitionDuration | --rdpc-transition-duration | 200ms | Transition duration |
| transitionEasing | --rdpc-transition-easing | ease-in-out | Transition timing |
You can also override variables directly in CSS:
:root {
--rdpc-color-primary: #8b5cf6;
--rdpc-color-surface: #0f0f23;
--rdpc-handle-size: 24px;
--rdpc-border-radius: 12px;
}Internationalization (i18n)
Every user-facing string can be overridden via the i18n or translations prop. Pass a Partial<Translations> — only the keys you provide will be overridden.
<DocumentPerspectiveCrop
i18n={{
save: 'Speichern',
cancel: 'Abbrechen',
autoDetect: 'Automatisch erkennen',
brightness: 'Helligkeit',
contrast: 'Kontrast',
sharpness: 'Schärfe',
rotateLeft: 'Links drehen',
rotateRight: 'Rechts drehen',
loadingOpenCV: 'OpenCV wird geladen…',
autoDetectFailed: 'Kein Dokument gefunden.',
}}
...
/>All Translation Keys
| Key | Category | Default (English) |
|-----|----------|-------------------|
| save | Action | "Save" |
| cancel | Action | "Cancel" |
| reset | Action | "Reset" |
| undo | Action | "Undo" |
| redo | Action | "Redo" |
| autoDetect | Action | "Auto Detect" |
| addImage | Action | "Add Image" |
| deleteImage | Action | "Delete Image" |
| title | Label | "Edit Document" |
| brightness | Label | "Brightness" |
| contrast | Label | "Contrast" |
| sharpness | Label | "Sharpness" |
| rotateLeft | Label | "Rotate Left" |
| rotateRight | Label | "Rotate Right" |
| zoomIn | Label | "Zoom In" |
| zoomOut | Label | "Zoom Out" |
| transformControls | Label | "Transform Controls" |
| adjustments | Label | "Adjustments" |
| perspective | Label | "Perspective" |
| transform | Label | "Transform" |
| history | Label | "History" |
| thumbnailAlt | Label | "Thumbnail Alt" |
| loadingOpenCV | Status | "Loading OpenCV…" |
| processing | Status | "Processing…" |
| autoDetectFailed | Status | "Could not find one clear document." |
| cannotDeleteLast | Status | "Cannot delete last image." |
| imageCounter | Status | "Image Counter" |
| cropRegion | A11y | "Crop Region" |
| cornerHandle | A11y | "Corner Handle" |
| dragToReorder | A11y | "Drag to Reorder" |
Keyboard Shortcuts
All shortcuts are enabled by default. On macOS, Ctrl is automatically mapped to Cmd (⌘).
| Action | Shortcut | Description |
|--------|----------|-------------|
| Undo | Ctrl+Z | Undo the last action |
| Redo | Ctrl+Shift+Z / Ctrl+Y | Redo the last undone action |
| Save | Ctrl+S | Trigger save/export |
| Rotate Left | Ctrl+← | Rotate 90° counter-clockwise |
| Rotate Right | Ctrl+→ | Rotate 90° clockwise |
| Zoom In | Ctrl+= / Ctrl++ | Zoom in |
| Zoom Out | Ctrl+- | Zoom out |
| Reset | Ctrl+R | Reset current image |
| Auto Detect | Ctrl+D | Run auto edge detection |
| Delete Image | Delete / Backspace | Delete the active image |
| Next Image | → | Switch to next image |
| Previous Image | ← | Switch to previous image |
| Cancel/Close | Escape | Cancel and close |
Sub-Components
The built-in UI is composed of modular sub-components, all exported for reuse:
import {
// Main component
DocumentPerspectiveCrop,
// Canvas
CropCanvas,
CropOverlay,
CornerHandle,
ImageRenderer,
// Controls
Toolbar, // Toolbar.Button, Toolbar.Slider
Sidebar,
SidebarSection,
// Thumbnails
ThumbnailStrip,
ThumbnailItem,
AddImageButton,
// Feedback
StatusBar,
// Icons
Icons, // Icons.SaveIcon, Icons.AutoDetectIcon, etc.
} from 'react-document-perspective-crop';Utility Exports
Low-level utility functions and OpenCV wrappers are available under namespaced exports:
import { utils, opencv } from 'react-document-perspective-crop';utils
| Module | Contains |
|--------|----------|
| utils.canvas | buildCSSFilterWithSharpness(), computeSharpnessKernel() |
| utils.geometry | viewportToImageCoords(), imageToViewportCoords(), clamp(), and more |
| utils.export | exportImage() — the core export pipeline |
| utils.image | createObjectURL(), revokeObjectURL(), generateImageId() |
| utils.perspective | Perspective transformation math |
| utils.sorting | reorderArray(), computeNewActiveIndex(), computeIndexAfterDelete() |
| utils.dom | DOM helper utilities |
opencv
| Module | Contains |
|--------|----------|
| opencv.loader | loadOpenCV(), getCachedOpenCV() — OpenCV.js script loading and caching |
| opencv.detect | detectDocumentEdges() — Core edge detection algorithm |
| opencv.transform | Perspective warp functions |
| opencv.adjustments | Image adjustment processing |
Constants
import {
DEFAULTS, // Default prop values
DEFAULT_ADJUSTMENTS, // { brightness: 100, contrast: 100, sharpness: 0 }
SCALE, // Zoom constraints { min: 0.1, max: 6, default: 1, ... }
HANDLE, // Corner handle constants
ADJUSTMENT_RANGES, // Slider min/max/step/default for each adjustment
MAX_UNDO_HISTORY, // 50
DEFAULT_OPENCV_CDN_URL, // 'https://docs.opencv.org/4.10.0/opencv.js'
CORNER_KEYS, // ['topLeft', 'topRight', 'bottomRight', 'bottomLeft']
} from 'react-document-perspective-crop';Running the Example Locally
An interactive playground is included in the example/ directory.
- Clone the repository.
- Install dependencies:
npm install cd example npm install - Start the dev server:
npm run dev
License
MIT
