/** * Pick the smallest format this browser can actually produce. * * @example * ```ts * const type = await bestSupportedType(["image/avif", "image/webp", "image/jpeg"]); * ``` * * @param preferences Formats in preference order. * @returns The first supported one, falling back to `image/jpeg`, which * every canvas implementation encodes. */ export declare function bestSupportedType(preferences?: readonly ImageType[]): Promise; /** An encoded frame, and where in the video it came from. */ export declare interface CapturedFrame extends ProcessedImage { /** * The instant actually captured, in milliseconds. * * Not necessarily the `atMs` asked for: a seek lands on a frame boundary, * so a request for 12 500 ms in a 30 fps video captures 12 500 at best and * 12 466,67 in practice. Report this one, not the request. */ readonly atMs: number; /** * Whether a newly presented frame was observed before the pixels were read. * * `true` only when `requestVideoFrameCallback` reported a frame going to * the compositor. Measured in Chromium, 2026-09-04: that callback fires * while a video **plays** and does **not** fire for a seek on a paused * element — so capturing from a playing video (a screen-recording print) * can be confirmed, and **capturing at an `atMs` reports `false`**, having * settled on `seeked` plus two animation frames instead. * * So `false` is the normal result for a seek, not a warning. It says the * capture is best effort by the standard of what browsers expose, and * treating it as a failure would reject the majority of correct captures. */ readonly confirmed: boolean; } /** * Capture a frame from a video as encoded image bytes. * * Without `atMs` it reads the frame on screen, which is what a screen or camera * recording wants — a print of what is being recorded. With `atMs` it seeks, * waits for that instant's frame to be presented, captures, and puts the player * back. * * @example Print of a screen recording in progress * ```ts * const shot = await captureFrame(videoRef.current!, { * type: "image/webp", * quality: 0.9, * }); * await shareOrDownloadBlob(shot.blob, "print.webp"); * ``` * * @example A poster from ten seconds in, scaled down * ```ts * const poster = await captureFrame(video, { atMs: 10_000, width: 640 }); * setPosterUrl(URL.createObjectURL(poster.blob)); * console.log(`landed on ${poster.atMs}ms`); * ``` * * @param video The element to read. It needs data — the capture waits for * `loadeddata` when the element has none yet. * @param options Instant, output box, format, and how long to wait. * @returns The encoded frame, the instant it actually came from, and whether a * presented frame confirmed that instant. * @throws {@link FrameSeekError} when the video has no data or no timeline in * time, or the seek did not land. * @throws {@link ImageDecodeError} when the pixels cannot be read — a * cross-origin video without `crossOrigin` is the common one. * @throws {@link ImageEncodeError} when the canvas produces no bytes. */ export declare function captureFrame(video: HTMLVideoElement, options?: CaptureFrameOptions): Promise; /** Options for {@link captureFrame}. */ export declare interface CaptureFrameOptions extends ResizeOptions { /** * Instant to capture, in milliseconds. Left out: the frame on screen now. * * Clamped to the video's duration. Seeking snaps to a frame boundary, so * the frame you get is the one **containing** this instant — the result * reports where it actually landed in {@link CapturedFrame.atMs}. */ readonly atMs?: number; /** * Put `currentTime` and playback back where they were. Default `true`. * * Only relevant with `atMs`: capturing the current frame moves nothing. * Pass `false` when the capture is meant to leave the player parked on the * frame it took. */ readonly restore?: boolean; /** * How long to wait for the seek and for the frame after it. Default `3000`. * * Reached means {@link FrameSeekError}, never a frame from the wrong * instant: a picture of the wrong moment is worse than an error, because * nothing downstream can tell. */ readonly timeoutMs?: number; /** Abort the capture. Rejects with an `AbortError` `DOMException`. */ readonly signal?: AbortSignal; } /** What {@link compressToTarget} produced. */ export declare interface CompressedImage extends ProcessedImage { /** The quality it settled on. */ readonly quality: number; /** How many encodes it took. */ readonly attempts: number; /** * Whether the result actually fits the budget. * * `false` means the image could not reach it even at `minQuality` — * reported rather than thrown, because a 2.1 MB result against a 2 MB * budget is usually still worth uploading, and that call is the * caller's. */ readonly withinBudget: boolean; } /** Options for {@link compressToTarget}. */ export declare interface CompressOptions extends ResizeOptions { /** Byte budget the result must fit in. */ readonly maxBytes: number; /** Lowest quality worth producing. Defaults to `0.4`. */ readonly minQuality?: number; /** Highest quality to start from. Defaults to `0.92`. */ readonly maxQuality?: number; /** Search steps. Defaults to `6`, which resolves quality to ~1%. */ readonly steps?: number; } /** * Compress an image until it fits a byte budget. * * @example * ```ts * const upload = await compressToTarget(file, { * maxBytes: 2 * 1024 * 1024, * width: 2000, * type: "image/webp", * }); * * if (!upload.withinBudget) { * console.warn(`still ${upload.bytes} bytes at quality ${upload.quality}`); * } * ``` * * Resizing first is what usually does the work: halving the long edge * removes three quarters of the pixels, which no quality setting matches. * Pass `width`/`height` when the source is a full-resolution photo. * * @param source Anything decodable. * @param options Byte budget plus the usual resize and format options. * @returns The best result found, and whether it fits. * @throws {@link ImageDecodeError} when the source cannot be decoded. */ export declare function compressToTarget(source: ImageSource, options: CompressOptions): Promise; /** * Create a drawing surface, preferring `OffscreenCanvas`. * * `OffscreenCanvas` works inside a worker, which is where a PWA wants this * running: resizing a 12-megapixel photo on the main thread blocks the UI * for tens of milliseconds per image. * * @param width Surface width in pixels. * @param height Surface height in pixels. * @returns The surface. * @throws {@link ImagingUnavailableError} when neither kind exists — a * server render, or a test environment without a canvas. */ export declare function createSurface(width: number, height: number): Surface; /** * Produce several sizes from a single decode. * * @example * ```ts * const [thumb, card] = await createThumbnails(file, [ * { name: "thumb", size: 96 }, * { name: "card", size: 480 }, * ]); * ``` * * Sizes are the **longest edge**, and the aspect ratio is kept, so a * portrait and a landscape photo both fit the same grid cell without a * separate calculation per orientation. * * @param source Anything decodable. * @param specs The sizes to produce. * @param options Shared format and quality. * @returns One result per spec, in the order requested. * @throws {@link ImageDecodeError} when the source cannot be decoded. */ export declare function createThumbnails(source: ImageSource, specs: readonly ThumbnailSpec[], options?: EncodeOptions): Promise; /** * Crop a rectangle out of an image, in source pixels. * * The rectangle is clamped to the image, so a crop dragged past the edge * produces a smaller result instead of transparent padding. * * @example * ```ts * const badge = await cropImage(file, { x: 120, y: 80, width: 400, height: 400 }); * ``` * * @param source Anything decodable. * @param rect The region to keep. * @param options Format and quality. * @returns The encoded crop. * @throws {@link ImageDecodeError} when the source cannot be decoded. */ export declare function cropImage(source: ImageSource, rect: CropRect, options?: EncodeOptions): Promise; /** A rectangle in source pixels. */ export declare interface CropRect { readonly x: number; readonly y: number; readonly width: number; readonly height: number; } /** A decoded image, ready to draw. */ export declare interface DecodedImage { /** The pixels. */ readonly bitmap: ImageBitmap; /** Width in pixels, after orientation was applied. */ readonly width: number; /** Height in pixels, after orientation was applied. */ readonly height: number; } /** * Decode any supported source into a bitmap, oriented as the photographer * held the camera. * * @example * ```ts * const { bitmap, width, height } = await decodeImage(file); * ``` * * @param source A `Blob`/`File`, URL string, `ImageBitmap`, `ImageData`, * `HTMLImageElement`, or a canvas. * @returns The decoded pixels and their dimensions. * @throws {@link ImageDecodeError} when the bytes are not a decodable image, * or the URL cannot be fetched. */ export declare function decodeImage(source: ImageSource): Promise; /** Background used when a format cannot carry transparency. */ export declare const DEFAULT_BACKGROUND = "#ffffff"; /** Search steps when the caller does not choose. */ export declare const DEFAULT_COMPRESS_STEPS = 6; /** How long a seek and the frame after it may take, in milliseconds. */ export declare const DEFAULT_FRAME_TIMEOUT_MS = 3000; /** Highest quality to start from by default. */ export declare const DEFAULT_MAX_QUALITY = 0.92; /** Lowest quality worth producing by default. */ export declare const DEFAULT_MIN_QUALITY = 0.4; /** Quality used when the caller does not choose. */ export declare const DEFAULT_QUALITY = 0.85; /** Format used when the caller does not choose. */ export declare const DEFAULT_TYPE: ImageType; /** * Draw a bitmap into a surface with high-quality filtering. * * @param bitmap The source pixels. * @param target Destination surface. * @param box Where to draw inside the destination. * @param background Fill painted before drawing. * * @tempest-limits param-count — source, destination, destination geometry, and an * optional background: the same four things `CanvasRenderingContext2D.drawImage` * takes, in the same order. Public surface, and a wrapper over a browser primitive * reads best when it keeps that primitive's shape. */ export declare function drawScaled(bitmap: ImageBitmap, target: Surface, box: { x: number; y: number; width: number; height: number; }, background?: string): void; /** * Encode a surface into image bytes. * * @param surface The canvas to encode. * @param options Format and quality. * @returns The blob plus the dimensions and the type actually produced. * @throws {@link ImageEncodeError} when the canvas produces nothing. */ export declare function encodeImage(surface: Surface, options?: EncodeOptions): Promise; /** Options for {@link encodeImage}. */ export declare interface EncodeOptions { /** Output format. Defaults to `image/jpeg`. */ readonly type?: ImageType; /** Quality for lossy formats, `0`-`1`. Defaults to `0.85`. */ readonly quality?: number; } /** * Mirror an image horizontally, vertically, or both. * * @example * ```ts * const selfie = await flipImage(capture, { horizontal: true }); * ``` * * @param source Anything decodable. * @param axes Which axes to mirror. * @param options Format and quality. * @returns The flipped image. * @throws {@link ImageDecodeError} when the source cannot be decoded. */ export declare function flipImage(source: ImageSource, axes: { horizontal?: boolean; vertical?: boolean; }, options?: EncodeOptions): Promise; /** * The frame asked for never arrived. * * Its own class because the alternative is the failure this module refuses to * produce: a frame from the wrong instant. `seeked` firing does not mean the * frame for the new position is composited and readable, so a capture that * gave up waiting has to say so — a thumbnail of the neighbouring frame looks * exactly like a correct one, and nothing downstream can tell. */ export declare class FrameSeekError extends ImagingError { constructor(message: string, options?: ErrorOptions); } /** * Get a 2-D context configured for image work. * * `imageSmoothingQuality = "high"` is the setting that makes a steep * downscale average its source pixels instead of sampling them sparsely. * * @param surface The surface to draw on. * @param background Optional fill painted before anything else. * @returns The context. * @throws {@link ImagingUnavailableError} when the context cannot be created. */ export declare function getContext(surface: Surface, background?: string): SurfaceContext; /** The source could not be decoded into pixels. */ export declare class ImageDecodeError extends ImagingError { constructor(message: string, options?: ErrorOptions); } /** The canvas could not produce encoded bytes. */ export declare class ImageEncodeError extends ImagingError { constructor(message: string, options?: ErrorOptions); } /** What a file holds, without decoding all of it. */ export declare interface ImageInfo { readonly width: number; readonly height: number; /** MIME type as reported by the blob. */ readonly type: string; /** Size in bytes. */ readonly bytes: number; /** `width / height`. */ readonly aspectRatio: number; } /** Lifecycle of a processing call. */ export declare type ImageProcessingStatus = "idle" | "working" | "done" | "error"; /** * Types for browser-side image processing. */ /** * Anything the module can decode. * * `HTMLVideoElement` reads the frame the element is **currently showing** — * `createImageBitmap` accepts it as a `CanvasImageSource`, so nothing here * special-cases it. Reading a chosen instant instead of the current one needs * the seek to be confirmed first, which is what `captureFrame` is for. */ export declare type ImageSource = Blob | File | ImageBitmap | ImageData | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | string; /** Encodable image formats. */ export declare type ImageType = "image/jpeg" | "image/png" | "image/webp" | "image/avif"; /** * Errors thrown by the imaging module. * * `name` is a literal string on every subclass: minifiers rename classes, * and a derived `new.target.name` ships as `error.name === "t"`. */ /** Base class for every error this module throws. */ export declare class ImagingError extends Error { constructor(message: string, options?: ErrorOptions); } /** The environment has no canvas to draw on. */ export declare class ImagingUnavailableError extends ImagingError { constructor(message: string, options?: ErrorOptions); } /** The result of an operation that produced an encoded image. */ export declare interface ProcessedImage { /** The encoded bytes. */ readonly blob: Blob; /** Output width. */ readonly width: number; /** Output height. */ readonly height: number; /** Output format actually produced — not necessarily the one asked for. */ readonly type: string; /** Size in bytes. */ readonly bytes: number; } /** * Read an image's dimensions, type and size. * * Decodes to measure, which is the only reliable way in a browser — there * is no header parser here, and guessing dimensions from a MIME type is not * a thing. Cheap enough for a preview, not for a thousand files in a loop. * * @example * ```ts * const info = await readImageInfo(file); * if (info.bytes > 5_000_000) { * // ask before uploading * } * ``` * * @param blob The image file. * @returns Its dimensions, MIME type, byte size and aspect ratio. * @throws {@link ImageDecodeError} when the blob is not a decodable image. */ export declare function readImageInfo(blob: Blob): Promise; /** * How a resize fits the requested box. * * - `contain`: the whole image fits inside the box; the result may be * smaller than the box in one dimension. * - `cover`: the box is filled; the overflow is cropped, centred. * - `fill`: the image is stretched to the box, changing its aspect ratio. * - `pad`: like `contain`, but the result is exactly the box, with the * remainder painted in `background`. */ export declare type ResizeFit = "contain" | "cover" | "fill" | "pad"; /** * Resize an image, re-encoding it. * * @example * ```ts * const resized = await resizeImage(file, { width: 1600, type: "image/webp" }); * console.log(resized.width, resized.bytes, resized.type); * ``` * * @param source Anything decodable. * @param options Target box, fit, format and quality. * @returns The encoded result. * @throws {@link ImageDecodeError} when the source cannot be decoded. * @throws {@link ImageEncodeError} when the canvas produces no bytes. */ export declare function resizeImage(source: ImageSource, options?: ResizeOptions): Promise; /** Options for {@link resizeImage}. */ export declare interface ResizeOptions extends EncodeOptions { /** Target width in pixels. */ readonly width?: number; /** Target height in pixels. */ readonly height?: number; /** How the image fits the box. Defaults to `contain`. */ readonly fit?: ResizeFit; /** Fill colour for `pad`, and behind transparency when encoding JPEG. */ readonly background?: string; /** * Never scale an image up. * * On by default: enlarging a photo adds no detail and multiplies the * bytes, which is the opposite of what a resize is usually for. */ readonly withoutEnlargement?: boolean; } /** * Rotate an image by a multiple of 90 degrees. * * Restricted to right angles on purpose: an arbitrary angle needs a * decision about the corners (crop, pad, or grow the canvas) that belongs * to the caller's design, not to a utility default. * * @example * ```ts * const upright = await rotateImage(file, 90); * ``` * * @param source Anything decodable. * @param degrees `90`, `180`, `270` — or any multiple, normalised. * @param options Format and quality. * @returns The rotated image. * @throws {@link ImageDecodeError} when the source cannot be decoded. * @throws {@link RangeError} when the angle is not a multiple of 90. */ export declare function rotateImage(source: ImageSource, degrees: number, options?: EncodeOptions): Promise; /** * Whether this browser can actually encode a format. * * Asks for a 1x1 image in that type and checks what came back, because * that is the only answer that counts: a browser that "supports" WebP for * display may still not encode it. * * @example * ```ts * const type = (await supportsImageType("image/webp")) ? "image/webp" : "image/jpeg"; * const resized = await resizeImage(file, { width: 1200, type }); * ``` * * @param type The format to test. * @returns Whether encoding produces that type. Cached per type. */ export declare function supportsImageType(type: ImageType): Promise; /** * The drawing surface, and the one thing everyone gets wrong on it. * * **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the * transparent pixels black. Filling the surface first is the difference * between a photo on a white background and one with a black hole in it. * * What is *not* here is worth recording. The received wisdom for downscaling * on a canvas is to halve repeatedly, because a single `drawImage` into a * much smaller box was said to alias. That was implemented here, and then * measured: on a 512 px checkerboard reduced to 32 px, the stepwise result * and the single high-quality draw were **pixel-identical** (standard * deviation 0.0 on both) in Chromium and Firefox — while stepwise cost * **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and * allocated three intermediate canvases on a device that may not have the * memory. Modern engines honour `imageSmoothingQuality = "high"`, which is * what this module sets. The halving was deleted rather than kept "just in * case": unmeasurable benefit at 300x the cost is not insurance, it is * ballast. */ /** A canvas this module can draw on, on the main thread or in a worker. */ export declare type Surface = OffscreenCanvas | HTMLCanvasElement; /** A 2-D context from either surface kind. */ export declare type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D; /** A produced thumbnail. */ export declare interface Thumbnail extends ProcessedImage { readonly name: string; } /** One requested size. */ export declare interface ThumbnailSpec { /** Name to find it by in the result. */ readonly name: string; /** Longest edge in pixels. */ readonly size: number; /** Format and quality, overriding the shared options. */ readonly encode?: EncodeOptions; } /** * The browser cannot encode the requested format. * * Worth its own class because the failure is otherwise silent: asking a * canvas for an unsupported type does not throw — it hands back a PNG, * and an app that trusted the request ships 4 MB where it expected 300 KB. */ export declare class UnsupportedImageTypeError extends ImagingError { constructor(message: string, options?: ErrorOptions); } /** * Hold an object URL for a blob, and revoke it when it is replaced. * * @example * ```tsx * function Preview({ file }: { file: File | null }) { * const { url } = useImagePreview(file); * return url === null ? null : ; * } * ``` * * @param source The blob to preview, or `null`. * @returns The object URL, valid until the source changes or the component * unmounts. */ export declare function useImagePreview(source: Blob | null | undefined): UseImagePreviewResult; /** What {@link useImagePreview} returns. */ export declare interface UseImagePreviewResult { /** Object URL for the current source, or `null`. */ readonly url: string | null; } /** * Run image operations with status tracking, safe against unmount. * * @example * ```tsx * function Upload() { * const { compress, isWorking, result } = useImageProcessing(); * * async function onPick(file: File) { * const ready = await compress(file, { maxBytes: 1_000_000, width: 1600 }); * await fetch("/api/photos", { method: "POST", body: ready.blob }); * } * * return onPick(e.target.files![0]!)} />; * } * ``` * * The returned promises still reject on failure, so a caller can `try` * around them; `status` and `error` exist for rendering, not for swallowing * the failure. * * @returns The operations plus their state. */ export declare function useImageProcessing(): UseImageProcessingResult; /** What {@link useImageProcessing} returns. */ export declare interface UseImageProcessingResult { /** Resize (and re-encode) an image. */ readonly resize: (source: ImageSource, options?: ResizeOptions) => Promise; /** Compress an image into a byte budget. */ readonly compress: (source: ImageSource, options: CompressOptions) => Promise; /** The most recent result. */ readonly result: ProcessedImage | null; /** Where the last call is. */ readonly status: ImageProcessingStatus; /** Why the last call failed. */ readonly error: Error | null; /** Whether a call is in flight. */ readonly isWorking: boolean; } export { }