npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-document-perspective-crop

v0.1.1

Published

A production-ready React component for document scanning, perspective correction, and image cropping with OpenCV integration.

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.

npm version License: MIT TypeScript


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 usePerspectiveCrop hook.
  • 💅 Framework Agnostic — Zero UI dependencies. Styled purely with CSS Custom Properties.
  • 🌍 Fully Translatable — Every UI string can be overridden via the i18n / translations prop.

Table of Contents


Installation

npm install react-document-perspective-crop

Peer 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 available

useDocumentDetection

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 (0200, clamped). | | setContrast | (value: number) => void | Set contrast (0200, clamped). | | setSharpness | (value: number) => void | Set sharpness (0100, 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.16.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 (01). |

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, Ctrl is automatically mapped to Cmd (⌘).


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.

  1. Clone the repository.
  2. Install dependencies:
    npm install
    cd example
    npm install
  3. Start the dev server:
    npm run dev

License

MIT