import { RpEditorConfig, RpEditorResult, RpEditorEvents, EditorMode, ImageFilterPreset, ImageAdjustments } from './types/index.js'; import { EventEmitter } from './utils/event-emitter.js'; export declare class RpImageEditor extends EventEmitter { private static readonly MIN_ZOOM; private static readonly MAX_ZOOM; private config; private localePack; private container; private wrapperEl; private canvasEl; private fabricCanvas; private baseImage; private originalImageBlob; private imageInfo; private cropModule; private drawModule; private textModule; private eraserModule; private calloutModule; private shapeModule; private historyModule; private toolbar; private currentMode; private zoomLevel; private isPanning; private lastPanX; private lastPanY; private isDestroyed; private isErasing; private eraserDidRemove; private eraserCursorEl; private eraserBrushHandlers; private lastPinchDistance; private cumulativeRotation; private rotationImageBaseline; private processedSourceImage; private loaderEl; private brushOpacity; private currentBrushColor; private keydownHandler; private textInputHostEl; private ghostImageEl; private activeFilterPreset; private adjustments; private adjustDebounce; /** Axis-aligned bounds of the visible base image in canvas coordinates. */ private getImageAnnotationBounds; /** Build an absolute clip-rect matching the base image bounds. */ private buildImageClipRect; /** Rebuild clip-paths for all annotations so they behave like image content. */ private refreshAnnotationClipPaths; /** Return true when object bbox intersects the base image bounds. */ private intersectsImageBounds; /** Keep an annotation's bbox inside the image by translating it. */ private constrainAnnotationToImageBounds; /** Shift a custom arrow object while preserving its endpoint geometry. */ private translateArrowAnnotation; /** Shift a custom polyline while preserving every vertex position. */ private translatePolylineAnnotation; /** * While an annotation is being scaled via its corner/side handles, cap the * scale so its bounding box can never grow past the base-image footprint. * The handle opposite the one being dragged stays fixed (Fabric anchors the * transform origin there), and we shrink scaleX/scaleY just enough to keep * the moving edges on the image border. This prevents shapes from spilling * outside the image regardless of the current zoom level. */ private constrainScalingToImageBounds; constructor(container: HTMLElement, config?: Partial); /** * Back-fill the merged config with strings from the resolved locale * pack. Any per-key value the consumer set explicitly wins \u2014 we * only assign when the current value is `undefined`. Called once * from the constructor after `mergeConfig`. */ private applyLocalePack; /** * Load an image into the editor */ loadImage(source: File | Blob | string): Promise; /** * Set the editor mode */ setMode(mode: EditorMode): void; /** * Zoom in */ zoomIn(factor?: number): void; /** * Zoom out */ zoomOut(factor?: number): void; /** * Set zoom level */ setZoom(level: number): void; /** * Rotate left (−90°) */ rotateLeft(): Promise; /** * Rotate right (+90°) */ rotateRight(): Promise; /** * Undo last action */ undo(): Promise; /** * Redo last undone action */ redo(): Promise; /** * Toggle browser fullscreen on the editor container. Silently * no-ops if the Fullscreen API is unavailable or the request is * denied by the browser (e.g. not user-initiated). */ toggleFullscreen(): void; /** * Reset to original image */ reset(): Promise; /** * Delete the currently selected annotation (callout or other). * Returns true if something was deleted. */ deleteSelectedAnnotation(): boolean; /** * Set brush/text color */ setColor(color: string): void; /** * Return #000000 or #ffffff — whichever contrasts better with `bg`. * Accepts hex (#rgb / #rrggbb) and rgb()/rgba() strings. */ private contrastTextFor; private parseColorToRgb; /** * Walk the canvas and re-fill any callout-label belonging to a * currently-selected callout with a color that contrasts with `bg`. * Labels the user has explicitly recolored are marked * `_rpUserSetTextColor` and left alone. */ private recolorSelectedCalloutLabels; /** * Set brush width */ setBrushWidth(width: number): void; /** * Adjust the freehand brush opacity (0..1). Applied by re-emitting * the current brush color as rgba() to the DrawModule — module * internals are not modified. */ setBrushOpacity(opacity: number): void; /** * Mirror the base image horizontally. Preserves rotation baseline * tracking by resetting cumulativeRotation only when needed. */ flipHorizontal(): void; /** * Mirror the base image vertically. */ flipVertical(): void; /** * Apply a one-click color filter preset. Passing `'none'` clears the * preset while leaving Adjust sliders intact. */ applyFilterPreset(preset: ImageFilterPreset): void; /** * Update a single Adjust knob. Values are clamped to sane ranges: * brightness/contrast/saturation ∈ [-1, 1], blur ∈ [0, 1]. * Rebuilds the filter stack with a small debounce so dragging is * cheap on large photos. */ setAdjustment(key: K, value: ImageAdjustments[K]): void; /** Snapshot of the current filter + adjust state. */ getImageEffects(): { preset: ImageFilterPreset; adjustments: ImageAdjustments; }; /** Clear filter + all adjust knobs. */ resetImageEffects(save?: boolean): void; /** * Rebuild `baseImage.filters` from `activeFilterPreset` + `adjustments` * and re-apply. Ghost preview is refreshed so the letterboxed edges * stay in sync with the composited pixels. */ private rebuildImageFilters; /** * Ensure the current filter backend can render `img` at full size. * The WebGL backend caps at `fabric.textureSize`; anything larger * comes back cropped to the top-left. We first bump textureSize to * what the GPU actually supports, then fall back to the 2D backend * if the image is still bigger than that ceiling. */ private ensureFilterBackendCanHandle; /** * Get the edited result */ getResult(): Promise; /** * Destroy the editor and clean up resources */ destroy(): void; /** * Get current mode */ getMode(): EditorMode; /** * Get current zoom level */ getZoomLevel(): number; private initializeCanvas; private loadImageOntoCanvas; /** * Fast path used by `rotate()`: skip the PNG `toDataURL` + re-decode * roundtrip by handing an already-rendered HTML element (image or * canvas) straight to Fabric. On 10–15 MB photos this cuts ~hundreds * of ms per rotation vs. going through a data URL. */ private loadImageElementOntoCanvas; /** * Shared mount logic: size the canvas to the wrapper, place the image * at the origin with an explicit uniform scale, and replace any prior * base image while leaving annotations intact. */ private installBaseImage; private initializeModules; private renderToolbar; private installKeyboardShortcuts; private deactivateCurrentMode; private activateMoveMode; /** * Freeze every annotation object on the canvas: not selectable, * not evented, no hover cursor override. The base image is * skipped (it's already locked at install time via * installBaseImage()). Called by activateMoveMode() — tools that * need to interact with annotations (e.g. eraser) explicitly * re-enable them in their own activate() hook. */ private lockAnnotations; /** * Keep freehand paths fixed like baked pixels: visible/exported but * never selectable or draggable. */ private lockDrawPath; private activateCropMode; private applyCrop; /** * Translate + rescale a single annotation from old canvas coordinates * (pre-crop) into new canvas coordinates (post-crop / re-fit). * * new_pos = newImgOrigin + (old_pos - crop_origin) * factor * new_scale = old_scale * factor * * `newImgLeft`/`newImgTop` are the top-left of the new base image * inside the new canvas (it's centered, so they're non-zero when the * cropped image doesn't fill the wrapper in both dimensions). * * Arrow objects store their endpoints in canvas coordinates so they * need a dedicated path that updates x1/y1/x2/y2 and rebuilds the bbox. */ private transformAnnotationForCrop; private rotate; private rotateInternal; /** * Visual center + uniform display scale of the current base image * in canvas-pixel space. */ private computeImageGeometry; /** * Build a baseline snapshot describing the annotation as it would * appear at cumulativeRotation === 0. If `prevCum` is non-zero the * snapshot is recovered by inverse-rotating the object's current * state around the current image center. * * Stored fields depend on the object type: * - rpArrow: endpoints in baseline canvas coords + base stroke/head sizes * - everything else: visual center + base scale + base angle */ private computeRotationBaseline; /** * Position one annotation by rotating its baseline state by the FULL * cumulative angle around the baseline image center, then mapping * into the new image center + scale. * * Callout pieces (box/border/label/anchor) rotate WITH the image just * like every other annotation so the callout stays visually locked to * the photo content. The callout-tail bitmap is regenerated by * `refreshAllTails` (which is now rotation-aware), so we leave it alone * here. */ private applyRotationFromBaseline; /** * Clamp the viewport translation so the base image can never be panned * fully outside the canvas. Mirrors Pintura's behaviour: the image is * always kept edge-to-edge with the canvas (when zoomed in the image * covers the canvas; when zoomed out / at fit-to-canvas it stays fully * within the canvas). Prevents Apply from producing a 0-byte file when * the user panned the image completely out of view. */ private clampViewportPan; private setupGestureHandlers; private setupTouchGestures; private getTouchDistance; private setupResizeObserver; /** * Re-fit the base image to the current wrapper size and re-anchor all * annotations proportionally. Used both by the ResizeObserver (e.g. * entering/exiting fullscreen) and after undo/redo — where the state * restored from JSON carries the base image's geometry from when the * snapshot was taken (possibly a different canvas size), which would * otherwise leave the image shrunk in the top-left corner. */ private fitBaseImageToWrapper; /** * Remap the absolute-coordinate geometry of an rpArrow / rpPolyline when * the base image is re-fitted (e.g. entering/exiting fullscreen). * * These shapes keep their vertices in canvas-pixel coordinates * (x1/y1/x2/y2 for arrows, points[] for polylines) and expect * left/top = 0 and scale = 1. Their draggable vertex controls read those * raw coordinates directly, so scaling them via object-level scaleX/left/top * (like ordinary annotations) moves the rendered line but leaves the handle * dots behind. Instead we transform every vertex by the same * origin-relative scale used for the base image and rebuild the bounding * box, keeping the body and its handles perfectly aligned. * * @returns true if the object was a geometry-based shape and was handled. */ private remapGeometryShape; private refreshBaseImageRef; private loadHtmlImage; /** * Convert a hex or rgb color into an rgba() string with the given * alpha in [0..1]. Falls back to the raw color when parsing fails * so DrawModule still receives a valid CSS color. */ private colorWithAlpha; private base64ToBlob; /** * Show a translucent overlay with a spinner on top of the canvas * wrapper. Used while a slow op (e.g. rotating a very large photo) * is in flight so the UI feels responsive instead of frozen. */ private showLoader; private hideLoader; /** * Swap the Fabric canvas cursor for a tool-specific icon so drawing * feels like a pencil / erasing feels like an eraser instead of the * generic `crosshair` (+). We override both `defaultCursor` (idle) * and `freeDrawingCursor` (which Fabric flips to `crosshair` when * `isDrawingMode` toggles on) with inline SVG data URIs — no extra * assets to bundle. Hotspot is the pencil tip / eraser tip. */ private applyToolCursor; private buildPencilCursor; private buildEraserCursor; /** * Layer drag-through erasing on top of the eraser module's * click-to-delete behaviour. Down/move fires a hit test around the * pointer (radius = eraser width) and removes every annotation whose * bounds intersect the eraser circle — so users can sweep a stroke * across drawings to wipe them out, matching the drawing feel. * A small circle follows the cursor so the user sees the affected * area. */ private enableBrushEraser; private disableBrushEraser; /** * Mount a translucent copy of the current base image in the wrapper, * positioned to align with the on-canvas image. Because the Fabric * canvas is sized exactly to the visible image, this "ghost" is what * makes the parts of the image panned outside the canvas still * visible (faded) in move mode — giving the user a preview of what * will be cropped away on Apply, similar to the crop tool's dimmed * backdrop. */ private showGhostImage; private hideGhostImage; /** * Rebuild the ghost image (source + position) — used when the base * image changes (undo/redo, resize) while move mode is active. */ private refreshGhostImage; /** * Keep the ghost image aligned with the on-canvas image after any * pan, zoom, or wrapper resize. */ private updateGhostImagePosition; /** * Resolve after the next paint so an overlay added immediately * before this call is actually visible before subsequent heavy * synchronous work blocks the main thread. */ private nextPaint; }