tempest-react-sdk 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -73
- package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js +6 -6
- package/dist/components/VideoPlayer/VideoPlayer.cjs +2 -0
- package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -0
- package/dist/components/VideoPlayer/VideoPlayer.js +229 -0
- package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -0
- package/dist/components/VideoPlayer/VideoPlayer.module.cjs +2 -0
- package/dist/components/VideoPlayer/VideoPlayer.module.cjs.map +1 -0
- package/dist/components/VideoPlayer/VideoPlayer.module.js +19 -0
- package/dist/components/VideoPlayer/VideoPlayer.module.js.map +1 -0
- package/dist/components/VideoPlayer/playback-rates.cjs +2 -0
- package/dist/components/VideoPlayer/playback-rates.cjs.map +1 -0
- package/dist/components/VideoPlayer/playback-rates.js +11 -0
- package/dist/components/VideoPlayer/playback-rates.js.map +1 -0
- package/dist/imaging/exceptions.cjs +1 -1
- package/dist/imaging/exceptions.cjs.map +1 -1
- package/dist/imaging/exceptions.js +5 -1
- package/dist/imaging/exceptions.js.map +1 -1
- package/dist/imaging/frame.cjs +2 -0
- package/dist/imaging/frame.cjs.map +1 -0
- package/dist/imaging/frame.js +82 -0
- package/dist/imaging/frame.js.map +1 -0
- package/dist/imaging.cjs +1 -1
- package/dist/imaging.d.ts +611 -494
- package/dist/imaging.js +10 -9
- package/dist/styles/VideoPlayer.css +23 -0
- package/dist/styles/media.css +22 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +142 -23
- package/dist/tempest-react-sdk.js +213 -211
- package/package.json +1 -1
package/dist/imaging.d.ts
CHANGED
|
@@ -12,528 +12,645 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function bestSupportedType(preferences?: readonly ImageType[]): Promise<ImageType>;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
export declare interface
|
|
17
|
-
/** The quality it settled on. */
|
|
18
|
-
readonly quality: number;
|
|
19
|
-
/** How many encodes it took. */
|
|
20
|
-
readonly attempts: number;
|
|
15
|
+
/** An encoded frame, and where in the video it came from. */
|
|
16
|
+
export declare interface CapturedFrame extends ProcessedImage {
|
|
21
17
|
/**
|
|
22
|
-
*
|
|
18
|
+
* The instant actually captured, in milliseconds.
|
|
23
19
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* caller's.
|
|
20
|
+
* Not necessarily the `atMs` asked for: a seek lands on a frame boundary,
|
|
21
|
+
* so a request for 12 500 ms in a 30 fps video captures 12 500 at best and
|
|
22
|
+
* 12 466,67 in practice. Report this one, not the request.
|
|
28
23
|
*/
|
|
29
|
-
readonly
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
readonly atMs: number;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a newly presented frame was observed before the pixels were read.
|
|
27
|
+
*
|
|
28
|
+
* `true` only when `requestVideoFrameCallback` reported a frame going to
|
|
29
|
+
* the compositor. Measured in Chromium, 2026-09-04: that callback fires
|
|
30
|
+
* while a video **plays** and does **not** fire for a seek on a paused
|
|
31
|
+
* element — so capturing from a playing video (a screen-recording print)
|
|
32
|
+
* can be confirmed, and **capturing at an `atMs` reports `false`**, having
|
|
33
|
+
* settled on `seeked` plus two animation frames instead.
|
|
34
|
+
*
|
|
35
|
+
* So `false` is the normal result for a seek, not a warning. It says the
|
|
36
|
+
* capture is best effort by the standard of what browsers expose, and
|
|
37
|
+
* treating it as a failure would reject the majority of correct captures.
|
|
38
|
+
*/
|
|
39
|
+
readonly confirmed: boolean;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
/**
|
|
45
|
-
*
|
|
43
|
+
* Capture a frame from a video as encoded image bytes.
|
|
46
44
|
*
|
|
47
|
-
*
|
|
45
|
+
* Without `atMs` it reads the frame on screen, which is what a screen or camera
|
|
46
|
+
* recording wants — a print of what is being recorded. With `atMs` it seeks,
|
|
47
|
+
* waits for that instant's frame to be presented, captures, and puts the player
|
|
48
|
+
* back.
|
|
49
|
+
*
|
|
50
|
+
* @example Print of a screen recording in progress
|
|
48
51
|
* ```ts
|
|
49
|
-
* const
|
|
50
|
-
* maxBytes: 2 * 1024 * 1024,
|
|
51
|
-
* width: 2000,
|
|
52
|
+
* const shot = await captureFrame(videoRef.current!, {
|
|
52
53
|
* type: "image/webp",
|
|
54
|
+
* quality: 0.9,
|
|
53
55
|
* });
|
|
54
|
-
*
|
|
55
|
-
* if (!upload.withinBudget) {
|
|
56
|
-
* console.warn(`still ${upload.bytes} bytes at quality ${upload.quality}`);
|
|
57
|
-
* }
|
|
56
|
+
* await shareOrDownloadBlob(shot.blob, "print.webp");
|
|
58
57
|
* ```
|
|
59
58
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* @example A poster from ten seconds in, scaled down
|
|
60
|
+
* ```ts
|
|
61
|
+
* const poster = await captureFrame(video, { atMs: 10_000, width: 640 });
|
|
62
|
+
* setPosterUrl(URL.createObjectURL(poster.blob));
|
|
63
|
+
* console.log(`landed on ${poster.atMs}ms`);
|
|
64
|
+
* ```
|
|
63
65
|
*
|
|
64
|
-
* @param
|
|
65
|
-
*
|
|
66
|
-
* @
|
|
67
|
-
* @
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* `OffscreenCanvas` works inside a worker, which is where a PWA wants this
|
|
75
|
-
* running: resizing a 12-megapixel photo on the main thread blocks the UI
|
|
76
|
-
* for tens of milliseconds per image.
|
|
77
|
-
*
|
|
78
|
-
* @param width Surface width in pixels.
|
|
79
|
-
* @param height Surface height in pixels.
|
|
80
|
-
* @returns The surface.
|
|
81
|
-
* @throws {@link ImagingUnavailableError} when neither kind exists — a
|
|
82
|
-
* server render, or a test environment without a canvas.
|
|
83
|
-
*/
|
|
84
|
-
export declare function createSurface(width: number, height: number): Surface;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Produce several sizes from a single decode.
|
|
88
|
-
*
|
|
89
|
-
* @example
|
|
90
|
-
* ```ts
|
|
91
|
-
* const [thumb, card] = await createThumbnails(file, [
|
|
92
|
-
* { name: "thumb", size: 96 },
|
|
93
|
-
* { name: "card", size: 480 },
|
|
94
|
-
* ]);
|
|
95
|
-
* ```
|
|
96
|
-
*
|
|
97
|
-
* Sizes are the **longest edge**, and the aspect ratio is kept, so a
|
|
98
|
-
* portrait and a landscape photo both fit the same grid cell without a
|
|
99
|
-
* separate calculation per orientation.
|
|
100
|
-
*
|
|
101
|
-
* @param source Anything decodable.
|
|
102
|
-
* @param specs The sizes to produce.
|
|
103
|
-
* @param options Shared format and quality.
|
|
104
|
-
* @returns One result per spec, in the order requested.
|
|
105
|
-
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
106
|
-
*/
|
|
107
|
-
export declare function createThumbnails(source: ImageSource, specs: readonly ThumbnailSpec[], options?: EncodeOptions): Promise<Thumbnail[]>;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Crop a rectangle out of an image, in source pixels.
|
|
111
|
-
*
|
|
112
|
-
* The rectangle is clamped to the image, so a crop dragged past the edge
|
|
113
|
-
* produces a smaller result instead of transparent padding.
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* ```ts
|
|
117
|
-
* const badge = await cropImage(file, { x: 120, y: 80, width: 400, height: 400 });
|
|
118
|
-
* ```
|
|
119
|
-
*
|
|
120
|
-
* @param source Anything decodable.
|
|
121
|
-
* @param rect The region to keep.
|
|
122
|
-
* @param options Format and quality.
|
|
123
|
-
* @returns The encoded crop.
|
|
124
|
-
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
125
|
-
*/
|
|
126
|
-
export declare function cropImage(source: ImageSource, rect: CropRect, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
127
|
-
|
|
128
|
-
/** A rectangle in source pixels. */
|
|
129
|
-
export declare interface CropRect {
|
|
130
|
-
readonly x: number;
|
|
131
|
-
readonly y: number;
|
|
132
|
-
readonly width: number;
|
|
133
|
-
readonly height: number;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** A decoded image, ready to draw. */
|
|
137
|
-
export declare interface DecodedImage {
|
|
138
|
-
/** The pixels. */
|
|
139
|
-
readonly bitmap: ImageBitmap;
|
|
140
|
-
/** Width in pixels, after orientation was applied. */
|
|
141
|
-
readonly width: number;
|
|
142
|
-
/** Height in pixels, after orientation was applied. */
|
|
143
|
-
readonly height: number;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Decode any supported source into a bitmap, oriented as the photographer
|
|
148
|
-
* held the camera.
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```ts
|
|
152
|
-
* const { bitmap, width, height } = await decodeImage(file);
|
|
153
|
-
* ```
|
|
154
|
-
*
|
|
155
|
-
* @param source A `Blob`/`File`, URL string, `ImageBitmap`, `ImageData`,
|
|
156
|
-
* `HTMLImageElement`, or a canvas.
|
|
157
|
-
* @returns The decoded pixels and their dimensions.
|
|
158
|
-
* @throws {@link ImageDecodeError} when the bytes are not a decodable image,
|
|
159
|
-
* or the URL cannot be fetched.
|
|
160
|
-
*/
|
|
161
|
-
export declare function decodeImage(source: ImageSource): Promise<DecodedImage>;
|
|
162
|
-
|
|
163
|
-
/** Background used when a format cannot carry transparency. */
|
|
164
|
-
export declare const DEFAULT_BACKGROUND = "#ffffff";
|
|
165
|
-
|
|
166
|
-
/** Search steps when the caller does not choose. */
|
|
167
|
-
export declare const DEFAULT_COMPRESS_STEPS = 6;
|
|
168
|
-
|
|
169
|
-
/** Highest quality to start from by default. */
|
|
170
|
-
export declare const DEFAULT_MAX_QUALITY = 0.92;
|
|
171
|
-
|
|
172
|
-
/** Lowest quality worth producing by default. */
|
|
173
|
-
export declare const DEFAULT_MIN_QUALITY = 0.4;
|
|
174
|
-
|
|
175
|
-
/** Quality used when the caller does not choose. */
|
|
176
|
-
export declare const DEFAULT_QUALITY = 0.85;
|
|
177
|
-
|
|
178
|
-
/** Format used when the caller does not choose. */
|
|
179
|
-
export declare const DEFAULT_TYPE: ImageType;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Draw a bitmap into a surface with high-quality filtering.
|
|
183
|
-
*
|
|
184
|
-
* @param bitmap The source pixels.
|
|
185
|
-
* @param target Destination surface.
|
|
186
|
-
* @param box Where to draw inside the destination.
|
|
187
|
-
* @param background Fill painted before drawing.
|
|
188
|
-
*
|
|
189
|
-
* @tempest-limits param-count — source, destination, destination geometry, and an
|
|
190
|
-
* optional background: the same four things `CanvasRenderingContext2D.drawImage`
|
|
191
|
-
* takes, in the same order. Public surface, and a wrapper over a browser primitive
|
|
192
|
-
* reads best when it keeps that primitive's shape.
|
|
193
|
-
*/
|
|
194
|
-
export declare function drawScaled(bitmap: ImageBitmap, target: Surface, box: {
|
|
195
|
-
x: number;
|
|
196
|
-
y: number;
|
|
197
|
-
width: number;
|
|
198
|
-
height: number;
|
|
199
|
-
}, background?: string): void;
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Encode a surface into image bytes.
|
|
203
|
-
*
|
|
204
|
-
* @param surface The canvas to encode.
|
|
205
|
-
* @param options Format and quality.
|
|
206
|
-
* @returns The blob plus the dimensions and the type actually produced.
|
|
207
|
-
* @throws {@link ImageEncodeError} when the canvas produces nothing.
|
|
208
|
-
*/
|
|
209
|
-
export declare function encodeImage(surface: Surface, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
210
|
-
|
|
211
|
-
/** Options for {@link encodeImage}. */
|
|
212
|
-
export declare interface EncodeOptions {
|
|
213
|
-
/** Output format. Defaults to `image/jpeg`. */
|
|
214
|
-
readonly type?: ImageType;
|
|
215
|
-
/** Quality for lossy formats, `0`-`1`. Defaults to `0.85`. */
|
|
216
|
-
readonly quality?: number;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Mirror an image horizontally, vertically, or both.
|
|
221
|
-
*
|
|
222
|
-
* @example
|
|
223
|
-
* ```ts
|
|
224
|
-
* const selfie = await flipImage(capture, { horizontal: true });
|
|
225
|
-
* ```
|
|
226
|
-
*
|
|
227
|
-
* @param source Anything decodable.
|
|
228
|
-
* @param axes Which axes to mirror.
|
|
229
|
-
* @param options Format and quality.
|
|
230
|
-
* @returns The flipped image.
|
|
231
|
-
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
232
|
-
*/
|
|
233
|
-
export declare function flipImage(source: ImageSource, axes: {
|
|
234
|
-
horizontal?: boolean;
|
|
235
|
-
vertical?: boolean;
|
|
236
|
-
}, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Get a 2-D context configured for image work.
|
|
240
|
-
*
|
|
241
|
-
* `imageSmoothingQuality = "high"` is the setting that makes a steep
|
|
242
|
-
* downscale average its source pixels instead of sampling them sparsely.
|
|
243
|
-
*
|
|
244
|
-
* @param surface The surface to draw on.
|
|
245
|
-
* @param background Optional fill painted before anything else.
|
|
246
|
-
* @returns The context.
|
|
247
|
-
* @throws {@link ImagingUnavailableError} when the context cannot be created.
|
|
248
|
-
*/
|
|
249
|
-
export declare function getContext(surface: Surface, background?: string): SurfaceContext;
|
|
250
|
-
|
|
251
|
-
/** The source could not be decoded into pixels. */
|
|
252
|
-
export declare class ImageDecodeError extends ImagingError {
|
|
253
|
-
constructor(message: string, options?: ErrorOptions);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** The canvas could not produce encoded bytes. */
|
|
257
|
-
export declare class ImageEncodeError extends ImagingError {
|
|
258
|
-
constructor(message: string, options?: ErrorOptions);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** What a file holds, without decoding all of it. */
|
|
262
|
-
export declare interface ImageInfo {
|
|
263
|
-
readonly width: number;
|
|
264
|
-
readonly height: number;
|
|
265
|
-
/** MIME type as reported by the blob. */
|
|
266
|
-
readonly type: string;
|
|
267
|
-
/** Size in bytes. */
|
|
268
|
-
readonly bytes: number;
|
|
269
|
-
/** `width / height`. */
|
|
270
|
-
readonly aspectRatio: number;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Lifecycle of a processing call. */
|
|
274
|
-
export declare type ImageProcessingStatus = "idle" | "working" | "done" | "error";
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Types for browser-side image processing.
|
|
278
|
-
*/
|
|
279
|
-
/** Anything the module can decode. */
|
|
280
|
-
export declare type ImageSource = Blob | File | ImageBitmap | ImageData | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | string;
|
|
281
|
-
|
|
282
|
-
/** Encodable image formats. */
|
|
283
|
-
export declare type ImageType = "image/jpeg" | "image/png" | "image/webp" | "image/avif";
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Errors thrown by the imaging module.
|
|
287
|
-
*
|
|
288
|
-
* `name` is a literal string on every subclass: minifiers rename classes,
|
|
289
|
-
* and a derived `new.target.name` ships as `error.name === "t"`.
|
|
290
|
-
*/
|
|
291
|
-
/** Base class for every error this module throws. */
|
|
292
|
-
export declare class ImagingError extends Error {
|
|
293
|
-
constructor(message: string, options?: ErrorOptions);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** The environment has no canvas to draw on. */
|
|
297
|
-
export declare class ImagingUnavailableError extends ImagingError {
|
|
298
|
-
constructor(message: string, options?: ErrorOptions);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** The result of an operation that produced an encoded image. */
|
|
302
|
-
export declare interface ProcessedImage {
|
|
303
|
-
/** The encoded bytes. */
|
|
304
|
-
readonly blob: Blob;
|
|
305
|
-
/** Output width. */
|
|
306
|
-
readonly width: number;
|
|
307
|
-
/** Output height. */
|
|
308
|
-
readonly height: number;
|
|
309
|
-
/** Output format actually produced — not necessarily the one asked for. */
|
|
310
|
-
readonly type: string;
|
|
311
|
-
/** Size in bytes. */
|
|
312
|
-
readonly bytes: number;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Read an image's dimensions, type and size.
|
|
317
|
-
*
|
|
318
|
-
* Decodes to measure, which is the only reliable way in a browser — there
|
|
319
|
-
* is no header parser here, and guessing dimensions from a MIME type is not
|
|
320
|
-
* a thing. Cheap enough for a preview, not for a thousand files in a loop.
|
|
321
|
-
*
|
|
322
|
-
* @example
|
|
323
|
-
* ```ts
|
|
324
|
-
* const info = await readImageInfo(file);
|
|
325
|
-
* if (info.bytes > 5_000_000) {
|
|
326
|
-
* // ask before uploading
|
|
327
|
-
* }
|
|
328
|
-
* ```
|
|
329
|
-
*
|
|
330
|
-
* @param blob The image file.
|
|
331
|
-
* @returns Its dimensions, MIME type, byte size and aspect ratio.
|
|
332
|
-
* @throws {@link ImageDecodeError} when the blob is not a decodable image.
|
|
66
|
+
* @param video The element to read. It needs data — the capture waits for
|
|
67
|
+
* `loadeddata` when the element has none yet.
|
|
68
|
+
* @param options Instant, output box, format, and how long to wait.
|
|
69
|
+
* @returns The encoded frame, the instant it actually came from, and whether a
|
|
70
|
+
* presented frame confirmed that instant.
|
|
71
|
+
* @throws {@link FrameSeekError} when the video has no data or no timeline in
|
|
72
|
+
* time, or the seek did not land.
|
|
73
|
+
* @throws {@link ImageDecodeError} when the pixels cannot be read — a
|
|
74
|
+
* cross-origin video without `crossOrigin` is the common one.
|
|
75
|
+
* @throws {@link ImageEncodeError} when the canvas produces no bytes.
|
|
333
76
|
*/
|
|
334
|
-
export declare function
|
|
77
|
+
export declare function captureFrame(video: HTMLVideoElement, options?: CaptureFrameOptions): Promise<CapturedFrame>;
|
|
78
|
+
|
|
79
|
+
/** Options for {@link captureFrame}. */
|
|
80
|
+
export declare interface CaptureFrameOptions extends ResizeOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Instant to capture, in milliseconds. Left out: the frame on screen now.
|
|
83
|
+
*
|
|
84
|
+
* Clamped to the video's duration. Seeking snaps to a frame boundary, so
|
|
85
|
+
* the frame you get is the one **containing** this instant — the result
|
|
86
|
+
* reports where it actually landed in {@link CapturedFrame.atMs}.
|
|
87
|
+
*/
|
|
88
|
+
readonly atMs?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Put `currentTime` and playback back where they were. Default `true`.
|
|
91
|
+
*
|
|
92
|
+
* Only relevant with `atMs`: capturing the current frame moves nothing.
|
|
93
|
+
* Pass `false` when the capture is meant to leave the player parked on the
|
|
94
|
+
* frame it took.
|
|
95
|
+
*/
|
|
96
|
+
readonly restore?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* How long to wait for the seek and for the frame after it. Default `3000`.
|
|
99
|
+
*
|
|
100
|
+
* Reached means {@link FrameSeekError}, never a frame from the wrong
|
|
101
|
+
* instant: a picture of the wrong moment is worse than an error, because
|
|
102
|
+
* nothing downstream can tell.
|
|
103
|
+
*/
|
|
104
|
+
readonly timeoutMs?: number;
|
|
105
|
+
/** Abort the capture. Rejects with an `AbortError` `DOMException`. */
|
|
106
|
+
readonly signal?: AbortSignal;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** What {@link compressToTarget} produced. */
|
|
110
|
+
export declare interface CompressedImage extends ProcessedImage {
|
|
111
|
+
/** The quality it settled on. */
|
|
112
|
+
readonly quality: number;
|
|
113
|
+
/** How many encodes it took. */
|
|
114
|
+
readonly attempts: number;
|
|
115
|
+
/**
|
|
116
|
+
* Whether the result actually fits the budget.
|
|
117
|
+
*
|
|
118
|
+
* `false` means the image could not reach it even at `minQuality` —
|
|
119
|
+
* reported rather than thrown, because a 2.1 MB result against a 2 MB
|
|
120
|
+
* budget is usually still worth uploading, and that call is the
|
|
121
|
+
* caller's.
|
|
122
|
+
*/
|
|
123
|
+
readonly withinBudget: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Options for {@link compressToTarget}. */
|
|
127
|
+
export declare interface CompressOptions extends ResizeOptions {
|
|
128
|
+
/** Byte budget the result must fit in. */
|
|
129
|
+
readonly maxBytes: number;
|
|
130
|
+
/** Lowest quality worth producing. Defaults to `0.4`. */
|
|
131
|
+
readonly minQuality?: number;
|
|
132
|
+
/** Highest quality to start from. Defaults to `0.92`. */
|
|
133
|
+
readonly maxQuality?: number;
|
|
134
|
+
/** Search steps. Defaults to `6`, which resolves quality to ~1%. */
|
|
135
|
+
readonly steps?: number;
|
|
136
|
+
}
|
|
335
137
|
|
|
336
138
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* - `contain`: the whole image fits inside the box; the result may be
|
|
340
|
-
* smaller than the box in one dimension.
|
|
341
|
-
* - `cover`: the box is filled; the overflow is cropped, centred.
|
|
342
|
-
* - `fill`: the image is stretched to the box, changing its aspect ratio.
|
|
343
|
-
* - `pad`: like `contain`, but the result is exactly the box, with the
|
|
344
|
-
* remainder painted in `background`.
|
|
345
|
-
*/
|
|
346
|
-
export declare type ResizeFit = "contain" | "cover" | "fill" | "pad";
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Resize an image, re-encoding it.
|
|
139
|
+
* Compress an image until it fits a byte budget.
|
|
350
140
|
*
|
|
351
141
|
* @example
|
|
352
142
|
* ```ts
|
|
353
|
-
* const
|
|
354
|
-
*
|
|
143
|
+
* const upload = await compressToTarget(file, {
|
|
144
|
+
* maxBytes: 2 * 1024 * 1024,
|
|
145
|
+
* width: 2000,
|
|
146
|
+
* type: "image/webp",
|
|
147
|
+
* });
|
|
148
|
+
*
|
|
149
|
+
* if (!upload.withinBudget) {
|
|
150
|
+
* console.warn(`still ${upload.bytes} bytes at quality ${upload.quality}`);
|
|
151
|
+
* }
|
|
355
152
|
* ```
|
|
356
153
|
*
|
|
154
|
+
* Resizing first is what usually does the work: halving the long edge
|
|
155
|
+
* removes three quarters of the pixels, which no quality setting matches.
|
|
156
|
+
* Pass `width`/`height` when the source is a full-resolution photo.
|
|
157
|
+
*
|
|
357
158
|
* @param source Anything decodable.
|
|
358
|
-
* @param options
|
|
359
|
-
* @returns The
|
|
159
|
+
* @param options Byte budget plus the usual resize and format options.
|
|
160
|
+
* @returns The best result found, and whether it fits.
|
|
360
161
|
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
361
|
-
|
|
162
|
+
*/
|
|
163
|
+
export declare function compressToTarget(source: ImageSource, options: CompressOptions): Promise<CompressedImage>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a drawing surface, preferring `OffscreenCanvas`.
|
|
167
|
+
*
|
|
168
|
+
* `OffscreenCanvas` works inside a worker, which is where a PWA wants this
|
|
169
|
+
* running: resizing a 12-megapixel photo on the main thread blocks the UI
|
|
170
|
+
* for tens of milliseconds per image.
|
|
171
|
+
*
|
|
172
|
+
* @param width Surface width in pixels.
|
|
173
|
+
* @param height Surface height in pixels.
|
|
174
|
+
* @returns The surface.
|
|
175
|
+
* @throws {@link ImagingUnavailableError} when neither kind exists — a
|
|
176
|
+
* server render, or a test environment without a canvas.
|
|
177
|
+
*/
|
|
178
|
+
export declare function createSurface(width: number, height: number): Surface;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Produce several sizes from a single decode.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* const [thumb, card] = await createThumbnails(file, [
|
|
186
|
+
* { name: "thumb", size: 96 },
|
|
187
|
+
* { name: "card", size: 480 },
|
|
188
|
+
* ]);
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* Sizes are the **longest edge**, and the aspect ratio is kept, so a
|
|
192
|
+
* portrait and a landscape photo both fit the same grid cell without a
|
|
193
|
+
* separate calculation per orientation.
|
|
194
|
+
*
|
|
195
|
+
* @param source Anything decodable.
|
|
196
|
+
* @param specs The sizes to produce.
|
|
197
|
+
* @param options Shared format and quality.
|
|
198
|
+
* @returns One result per spec, in the order requested.
|
|
199
|
+
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
200
|
+
*/
|
|
201
|
+
export declare function createThumbnails(source: ImageSource, specs: readonly ThumbnailSpec[], options?: EncodeOptions): Promise<Thumbnail[]>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Crop a rectangle out of an image, in source pixels.
|
|
205
|
+
*
|
|
206
|
+
* The rectangle is clamped to the image, so a crop dragged past the edge
|
|
207
|
+
* produces a smaller result instead of transparent padding.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const badge = await cropImage(file, { x: 120, y: 80, width: 400, height: 400 });
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @param source Anything decodable.
|
|
215
|
+
* @param rect The region to keep.
|
|
216
|
+
* @param options Format and quality.
|
|
217
|
+
* @returns The encoded crop.
|
|
218
|
+
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
219
|
+
*/
|
|
220
|
+
export declare function cropImage(source: ImageSource, rect: CropRect, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
221
|
+
|
|
222
|
+
/** A rectangle in source pixels. */
|
|
223
|
+
export declare interface CropRect {
|
|
224
|
+
readonly x: number;
|
|
225
|
+
readonly y: number;
|
|
226
|
+
readonly width: number;
|
|
227
|
+
readonly height: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A decoded image, ready to draw. */
|
|
231
|
+
export declare interface DecodedImage {
|
|
232
|
+
/** The pixels. */
|
|
233
|
+
readonly bitmap: ImageBitmap;
|
|
234
|
+
/** Width in pixels, after orientation was applied. */
|
|
235
|
+
readonly width: number;
|
|
236
|
+
/** Height in pixels, after orientation was applied. */
|
|
237
|
+
readonly height: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Decode any supported source into a bitmap, oriented as the photographer
|
|
242
|
+
* held the camera.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* const { bitmap, width, height } = await decodeImage(file);
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @param source A `Blob`/`File`, URL string, `ImageBitmap`, `ImageData`,
|
|
250
|
+
* `HTMLImageElement`, or a canvas.
|
|
251
|
+
* @returns The decoded pixels and their dimensions.
|
|
252
|
+
* @throws {@link ImageDecodeError} when the bytes are not a decodable image,
|
|
253
|
+
* or the URL cannot be fetched.
|
|
362
254
|
*/
|
|
363
|
-
export declare function
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
export declare
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
255
|
+
export declare function decodeImage(source: ImageSource): Promise<DecodedImage>;
|
|
256
|
+
|
|
257
|
+
/** Background used when a format cannot carry transparency. */
|
|
258
|
+
export declare const DEFAULT_BACKGROUND = "#ffffff";
|
|
259
|
+
|
|
260
|
+
/** Search steps when the caller does not choose. */
|
|
261
|
+
export declare const DEFAULT_COMPRESS_STEPS = 6;
|
|
262
|
+
|
|
263
|
+
/** How long a seek and the frame after it may take, in milliseconds. */
|
|
264
|
+
export declare const DEFAULT_FRAME_TIMEOUT_MS = 3000;
|
|
265
|
+
|
|
266
|
+
/** Highest quality to start from by default. */
|
|
267
|
+
export declare const DEFAULT_MAX_QUALITY = 0.92;
|
|
268
|
+
|
|
269
|
+
/** Lowest quality worth producing by default. */
|
|
270
|
+
export declare const DEFAULT_MIN_QUALITY = 0.4;
|
|
271
|
+
|
|
272
|
+
/** Quality used when the caller does not choose. */
|
|
273
|
+
export declare const DEFAULT_QUALITY = 0.85;
|
|
274
|
+
|
|
275
|
+
/** Format used when the caller does not choose. */
|
|
276
|
+
export declare const DEFAULT_TYPE: ImageType;
|
|
383
277
|
|
|
384
278
|
/**
|
|
385
|
-
*
|
|
279
|
+
* Draw a bitmap into a surface with high-quality filtering.
|
|
386
280
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
281
|
+
* @param bitmap The source pixels.
|
|
282
|
+
* @param target Destination surface.
|
|
283
|
+
* @param box Where to draw inside the destination.
|
|
284
|
+
* @param background Fill painted before drawing.
|
|
390
285
|
*
|
|
391
|
-
* @
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
286
|
+
* @tempest-limits param-count — source, destination, destination geometry, and an
|
|
287
|
+
* optional background: the same four things `CanvasRenderingContext2D.drawImage`
|
|
288
|
+
* takes, in the same order. Public surface, and a wrapper over a browser primitive
|
|
289
|
+
* reads best when it keeps that primitive's shape.
|
|
290
|
+
*/
|
|
291
|
+
export declare function drawScaled(bitmap: ImageBitmap, target: Surface, box: {
|
|
292
|
+
x: number;
|
|
293
|
+
y: number;
|
|
294
|
+
width: number;
|
|
295
|
+
height: number;
|
|
296
|
+
}, background?: string): void;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Encode a surface into image bytes.
|
|
395
300
|
*
|
|
396
|
-
* @param
|
|
397
|
-
* @param degrees `90`, `180`, `270` — or any multiple, normalised.
|
|
301
|
+
* @param surface The canvas to encode.
|
|
398
302
|
* @param options Format and quality.
|
|
399
|
-
* @returns The
|
|
400
|
-
* @throws {@link
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
*
|
|
426
|
-
* **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the
|
|
427
|
-
* transparent pixels black. Filling the surface first is the difference
|
|
428
|
-
* between a photo on a white background and one with a black hole in it.
|
|
429
|
-
*
|
|
430
|
-
* What is *not* here is worth recording. The received wisdom for downscaling
|
|
431
|
-
* on a canvas is to halve repeatedly, because a single `drawImage` into a
|
|
432
|
-
* much smaller box was said to alias. That was implemented here, and then
|
|
433
|
-
* measured: on a 512 px checkerboard reduced to 32 px, the stepwise result
|
|
434
|
-
* and the single high-quality draw were **pixel-identical** (standard
|
|
435
|
-
* deviation 0.0 on both) in Chromium and Firefox — while stepwise cost
|
|
436
|
-
* **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and
|
|
437
|
-
* allocated three intermediate canvases on a device that may not have the
|
|
438
|
-
* memory. Modern engines honour `imageSmoothingQuality = "high"`, which is
|
|
439
|
-
* what this module sets. The halving was deleted rather than kept "just in
|
|
440
|
-
* case": unmeasurable benefit at 300x the cost is not insurance, it is
|
|
441
|
-
* ballast.
|
|
442
|
-
*/
|
|
443
|
-
/** A canvas this module can draw on, on the main thread or in a worker. */
|
|
444
|
-
export declare type Surface = OffscreenCanvas | HTMLCanvasElement;
|
|
445
|
-
|
|
446
|
-
/** A 2-D context from either surface kind. */
|
|
447
|
-
export declare type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
|
|
448
|
-
|
|
449
|
-
/** A produced thumbnail. */
|
|
450
|
-
export declare interface Thumbnail extends ProcessedImage {
|
|
451
|
-
readonly name: string;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/** One requested size. */
|
|
455
|
-
export declare interface ThumbnailSpec {
|
|
456
|
-
/** Name to find it by in the result. */
|
|
457
|
-
readonly name: string;
|
|
458
|
-
/** Longest edge in pixels. */
|
|
459
|
-
readonly size: number;
|
|
460
|
-
/** Format and quality, overriding the shared options. */
|
|
461
|
-
readonly encode?: EncodeOptions;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* The browser cannot encode the requested format.
|
|
466
|
-
*
|
|
467
|
-
* Worth its own class because the failure is otherwise silent: asking a
|
|
468
|
-
* canvas for an unsupported type does not throw — it hands back a PNG,
|
|
469
|
-
* and an app that trusted the request ships 4 MB where it expected 300 KB.
|
|
470
|
-
*/
|
|
471
|
-
export declare class UnsupportedImageTypeError extends ImagingError {
|
|
472
|
-
constructor(message: string, options?: ErrorOptions);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Hold an object URL for a blob, and revoke it when it is replaced.
|
|
477
|
-
*
|
|
478
|
-
* @example
|
|
479
|
-
* ```tsx
|
|
480
|
-
* function Preview({ file }: { file: File | null }) {
|
|
481
|
-
* const { url } = useImagePreview(file);
|
|
482
|
-
* return url === null ? null : <img src={url} alt="" />;
|
|
483
|
-
* }
|
|
484
|
-
* ```
|
|
485
|
-
*
|
|
486
|
-
* @param source The blob to preview, or `null`.
|
|
487
|
-
* @returns The object URL, valid until the source changes or the component
|
|
488
|
-
* unmounts.
|
|
489
|
-
*/
|
|
490
|
-
export declare function useImagePreview(source: Blob | null | undefined): UseImagePreviewResult;
|
|
491
|
-
|
|
492
|
-
/** What {@link useImagePreview} returns. */
|
|
493
|
-
export declare interface UseImagePreviewResult {
|
|
494
|
-
/** Object URL for the current source, or `null`. */
|
|
495
|
-
readonly url: string | null;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Run image operations with status tracking, safe against unmount.
|
|
500
|
-
*
|
|
501
|
-
* @example
|
|
502
|
-
* ```tsx
|
|
503
|
-
* function Upload() {
|
|
504
|
-
* const { compress, isWorking, result } = useImageProcessing();
|
|
505
|
-
*
|
|
506
|
-
* async function onPick(file: File) {
|
|
507
|
-
* const ready = await compress(file, { maxBytes: 1_000_000, width: 1600 });
|
|
508
|
-
* await fetch("/api/photos", { method: "POST", body: ready.blob });
|
|
509
|
-
* }
|
|
510
|
-
*
|
|
511
|
-
* return <input type="file" disabled={isWorking} onChange={(e) => onPick(e.target.files![0]!)} />;
|
|
512
|
-
* }
|
|
513
|
-
* ```
|
|
514
|
-
*
|
|
515
|
-
* The returned promises still reject on failure, so a caller can `try`
|
|
516
|
-
* around them; `status` and `error` exist for rendering, not for swallowing
|
|
517
|
-
* the failure.
|
|
518
|
-
*
|
|
519
|
-
* @returns The operations plus their state.
|
|
303
|
+
* @returns The blob plus the dimensions and the type actually produced.
|
|
304
|
+
* @throws {@link ImageEncodeError} when the canvas produces nothing.
|
|
305
|
+
*/
|
|
306
|
+
export declare function encodeImage(surface: Surface, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
307
|
+
|
|
308
|
+
/** Options for {@link encodeImage}. */
|
|
309
|
+
export declare interface EncodeOptions {
|
|
310
|
+
/** Output format. Defaults to `image/jpeg`. */
|
|
311
|
+
readonly type?: ImageType;
|
|
312
|
+
/** Quality for lossy formats, `0`-`1`. Defaults to `0.85`. */
|
|
313
|
+
readonly quality?: number;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Mirror an image horizontally, vertically, or both.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* const selfie = await flipImage(capture, { horizontal: true });
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* @param source Anything decodable.
|
|
325
|
+
* @param axes Which axes to mirror.
|
|
326
|
+
* @param options Format and quality.
|
|
327
|
+
* @returns The flipped image.
|
|
328
|
+
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
520
329
|
*/
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
330
|
+
export declare function flipImage(source: ImageSource, axes: {
|
|
331
|
+
horizontal?: boolean;
|
|
332
|
+
vertical?: boolean;
|
|
333
|
+
}, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The frame asked for never arrived.
|
|
337
|
+
*
|
|
338
|
+
* Its own class because the alternative is the failure this module refuses to
|
|
339
|
+
* produce: a frame from the wrong instant. `seeked` firing does not mean the
|
|
340
|
+
* frame for the new position is composited and readable, so a capture that
|
|
341
|
+
* gave up waiting has to say so — a thumbnail of the neighbouring frame looks
|
|
342
|
+
* exactly like a correct one, and nothing downstream can tell.
|
|
343
|
+
*/
|
|
344
|
+
export declare class FrameSeekError extends ImagingError {
|
|
345
|
+
constructor(message: string, options?: ErrorOptions);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get a 2-D context configured for image work.
|
|
350
|
+
*
|
|
351
|
+
* `imageSmoothingQuality = "high"` is the setting that makes a steep
|
|
352
|
+
* downscale average its source pixels instead of sampling them sparsely.
|
|
353
|
+
*
|
|
354
|
+
* @param surface The surface to draw on.
|
|
355
|
+
* @param background Optional fill painted before anything else.
|
|
356
|
+
* @returns The context.
|
|
357
|
+
* @throws {@link ImagingUnavailableError} when the context cannot be created.
|
|
358
|
+
*/
|
|
359
|
+
export declare function getContext(surface: Surface, background?: string): SurfaceContext;
|
|
360
|
+
|
|
361
|
+
/** The source could not be decoded into pixels. */
|
|
362
|
+
export declare class ImageDecodeError extends ImagingError {
|
|
363
|
+
constructor(message: string, options?: ErrorOptions);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The canvas could not produce encoded bytes. */
|
|
367
|
+
export declare class ImageEncodeError extends ImagingError {
|
|
368
|
+
constructor(message: string, options?: ErrorOptions);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** What a file holds, without decoding all of it. */
|
|
372
|
+
export declare interface ImageInfo {
|
|
373
|
+
readonly width: number;
|
|
374
|
+
readonly height: number;
|
|
375
|
+
/** MIME type as reported by the blob. */
|
|
376
|
+
readonly type: string;
|
|
377
|
+
/** Size in bytes. */
|
|
378
|
+
readonly bytes: number;
|
|
379
|
+
/** `width / height`. */
|
|
380
|
+
readonly aspectRatio: number;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Lifecycle of a processing call. */
|
|
384
|
+
export declare type ImageProcessingStatus = "idle" | "working" | "done" | "error";
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Types for browser-side image processing.
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* Anything the module can decode.
|
|
391
|
+
*
|
|
392
|
+
* `HTMLVideoElement` reads the frame the element is **currently showing** —
|
|
393
|
+
* `createImageBitmap` accepts it as a `CanvasImageSource`, so nothing here
|
|
394
|
+
* special-cases it. Reading a chosen instant instead of the current one needs
|
|
395
|
+
* the seek to be confirmed first, which is what `captureFrame` is for.
|
|
396
|
+
*/
|
|
397
|
+
export declare type ImageSource = Blob | File | ImageBitmap | ImageData | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | string;
|
|
398
|
+
|
|
399
|
+
/** Encodable image formats. */
|
|
400
|
+
export declare type ImageType = "image/jpeg" | "image/png" | "image/webp" | "image/avif";
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Errors thrown by the imaging module.
|
|
404
|
+
*
|
|
405
|
+
* `name` is a literal string on every subclass: minifiers rename classes,
|
|
406
|
+
* and a derived `new.target.name` ships as `error.name === "t"`.
|
|
407
|
+
*/
|
|
408
|
+
/** Base class for every error this module throws. */
|
|
409
|
+
export declare class ImagingError extends Error {
|
|
410
|
+
constructor(message: string, options?: ErrorOptions);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The environment has no canvas to draw on. */
|
|
414
|
+
export declare class ImagingUnavailableError extends ImagingError {
|
|
415
|
+
constructor(message: string, options?: ErrorOptions);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The result of an operation that produced an encoded image. */
|
|
419
|
+
export declare interface ProcessedImage {
|
|
420
|
+
/** The encoded bytes. */
|
|
421
|
+
readonly blob: Blob;
|
|
422
|
+
/** Output width. */
|
|
423
|
+
readonly width: number;
|
|
424
|
+
/** Output height. */
|
|
425
|
+
readonly height: number;
|
|
426
|
+
/** Output format actually produced — not necessarily the one asked for. */
|
|
427
|
+
readonly type: string;
|
|
428
|
+
/** Size in bytes. */
|
|
429
|
+
readonly bytes: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Read an image's dimensions, type and size.
|
|
434
|
+
*
|
|
435
|
+
* Decodes to measure, which is the only reliable way in a browser — there
|
|
436
|
+
* is no header parser here, and guessing dimensions from a MIME type is not
|
|
437
|
+
* a thing. Cheap enough for a preview, not for a thousand files in a loop.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* const info = await readImageInfo(file);
|
|
442
|
+
* if (info.bytes > 5_000_000) {
|
|
443
|
+
* // ask before uploading
|
|
444
|
+
* }
|
|
445
|
+
* ```
|
|
446
|
+
*
|
|
447
|
+
* @param blob The image file.
|
|
448
|
+
* @returns Its dimensions, MIME type, byte size and aspect ratio.
|
|
449
|
+
* @throws {@link ImageDecodeError} when the blob is not a decodable image.
|
|
450
|
+
*/
|
|
451
|
+
export declare function readImageInfo(blob: Blob): Promise<ImageInfo>;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* How a resize fits the requested box.
|
|
455
|
+
*
|
|
456
|
+
* - `contain`: the whole image fits inside the box; the result may be
|
|
457
|
+
* smaller than the box in one dimension.
|
|
458
|
+
* - `cover`: the box is filled; the overflow is cropped, centred.
|
|
459
|
+
* - `fill`: the image is stretched to the box, changing its aspect ratio.
|
|
460
|
+
* - `pad`: like `contain`, but the result is exactly the box, with the
|
|
461
|
+
* remainder painted in `background`.
|
|
462
|
+
*/
|
|
463
|
+
export declare type ResizeFit = "contain" | "cover" | "fill" | "pad";
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Resize an image, re-encoding it.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```ts
|
|
470
|
+
* const resized = await resizeImage(file, { width: 1600, type: "image/webp" });
|
|
471
|
+
* console.log(resized.width, resized.bytes, resized.type);
|
|
472
|
+
* ```
|
|
473
|
+
*
|
|
474
|
+
* @param source Anything decodable.
|
|
475
|
+
* @param options Target box, fit, format and quality.
|
|
476
|
+
* @returns The encoded result.
|
|
477
|
+
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
478
|
+
* @throws {@link ImageEncodeError} when the canvas produces no bytes.
|
|
479
|
+
*/
|
|
480
|
+
export declare function resizeImage(source: ImageSource, options?: ResizeOptions): Promise<ProcessedImage>;
|
|
481
|
+
|
|
482
|
+
/** Options for {@link resizeImage}. */
|
|
483
|
+
export declare interface ResizeOptions extends EncodeOptions {
|
|
484
|
+
/** Target width in pixels. */
|
|
485
|
+
readonly width?: number;
|
|
486
|
+
/** Target height in pixels. */
|
|
487
|
+
readonly height?: number;
|
|
488
|
+
/** How the image fits the box. Defaults to `contain`. */
|
|
489
|
+
readonly fit?: ResizeFit;
|
|
490
|
+
/** Fill colour for `pad`, and behind transparency when encoding JPEG. */
|
|
491
|
+
readonly background?: string;
|
|
492
|
+
/**
|
|
493
|
+
* Never scale an image up.
|
|
494
|
+
*
|
|
495
|
+
* On by default: enlarging a photo adds no detail and multiplies the
|
|
496
|
+
* bytes, which is the opposite of what a resize is usually for.
|
|
497
|
+
*/
|
|
498
|
+
readonly withoutEnlargement?: boolean;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Rotate an image by a multiple of 90 degrees.
|
|
503
|
+
*
|
|
504
|
+
* Restricted to right angles on purpose: an arbitrary angle needs a
|
|
505
|
+
* decision about the corners (crop, pad, or grow the canvas) that belongs
|
|
506
|
+
* to the caller's design, not to a utility default.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* ```ts
|
|
510
|
+
* const upright = await rotateImage(file, 90);
|
|
511
|
+
* ```
|
|
512
|
+
*
|
|
513
|
+
* @param source Anything decodable.
|
|
514
|
+
* @param degrees `90`, `180`, `270` — or any multiple, normalised.
|
|
515
|
+
* @param options Format and quality.
|
|
516
|
+
* @returns The rotated image.
|
|
517
|
+
* @throws {@link ImageDecodeError} when the source cannot be decoded.
|
|
518
|
+
* @throws {@link RangeError} when the angle is not a multiple of 90.
|
|
519
|
+
*/
|
|
520
|
+
export declare function rotateImage(source: ImageSource, degrees: number, options?: EncodeOptions): Promise<ProcessedImage>;
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Whether this browser can actually encode a format.
|
|
524
|
+
*
|
|
525
|
+
* Asks for a 1x1 image in that type and checks what came back, because
|
|
526
|
+
* that is the only answer that counts: a browser that "supports" WebP for
|
|
527
|
+
* display may still not encode it.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* const type = (await supportsImageType("image/webp")) ? "image/webp" : "image/jpeg";
|
|
532
|
+
* const resized = await resizeImage(file, { width: 1200, type });
|
|
533
|
+
* ```
|
|
534
|
+
*
|
|
535
|
+
* @param type The format to test.
|
|
536
|
+
* @returns Whether encoding produces that type. Cached per type.
|
|
537
|
+
*/
|
|
538
|
+
export declare function supportsImageType(type: ImageType): Promise<boolean>;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The drawing surface, and the one thing everyone gets wrong on it.
|
|
542
|
+
*
|
|
543
|
+
* **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the
|
|
544
|
+
* transparent pixels black. Filling the surface first is the difference
|
|
545
|
+
* between a photo on a white background and one with a black hole in it.
|
|
546
|
+
*
|
|
547
|
+
* What is *not* here is worth recording. The received wisdom for downscaling
|
|
548
|
+
* on a canvas is to halve repeatedly, because a single `drawImage` into a
|
|
549
|
+
* much smaller box was said to alias. That was implemented here, and then
|
|
550
|
+
* measured: on a 512 px checkerboard reduced to 32 px, the stepwise result
|
|
551
|
+
* and the single high-quality draw were **pixel-identical** (standard
|
|
552
|
+
* deviation 0.0 on both) in Chromium and Firefox — while stepwise cost
|
|
553
|
+
* **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and
|
|
554
|
+
* allocated three intermediate canvases on a device that may not have the
|
|
555
|
+
* memory. Modern engines honour `imageSmoothingQuality = "high"`, which is
|
|
556
|
+
* what this module sets. The halving was deleted rather than kept "just in
|
|
557
|
+
* case": unmeasurable benefit at 300x the cost is not insurance, it is
|
|
558
|
+
* ballast.
|
|
559
|
+
*/
|
|
560
|
+
/** A canvas this module can draw on, on the main thread or in a worker. */
|
|
561
|
+
export declare type Surface = OffscreenCanvas | HTMLCanvasElement;
|
|
562
|
+
|
|
563
|
+
/** A 2-D context from either surface kind. */
|
|
564
|
+
export declare type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
|
|
565
|
+
|
|
566
|
+
/** A produced thumbnail. */
|
|
567
|
+
export declare interface Thumbnail extends ProcessedImage {
|
|
568
|
+
readonly name: string;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** One requested size. */
|
|
572
|
+
export declare interface ThumbnailSpec {
|
|
573
|
+
/** Name to find it by in the result. */
|
|
574
|
+
readonly name: string;
|
|
575
|
+
/** Longest edge in pixels. */
|
|
576
|
+
readonly size: number;
|
|
577
|
+
/** Format and quality, overriding the shared options. */
|
|
578
|
+
readonly encode?: EncodeOptions;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* The browser cannot encode the requested format.
|
|
583
|
+
*
|
|
584
|
+
* Worth its own class because the failure is otherwise silent: asking a
|
|
585
|
+
* canvas for an unsupported type does not throw — it hands back a PNG,
|
|
586
|
+
* and an app that trusted the request ships 4 MB where it expected 300 KB.
|
|
587
|
+
*/
|
|
588
|
+
export declare class UnsupportedImageTypeError extends ImagingError {
|
|
589
|
+
constructor(message: string, options?: ErrorOptions);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Hold an object URL for a blob, and revoke it when it is replaced.
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```tsx
|
|
597
|
+
* function Preview({ file }: { file: File | null }) {
|
|
598
|
+
* const { url } = useImagePreview(file);
|
|
599
|
+
* return url === null ? null : <img src={url} alt="" />;
|
|
600
|
+
* }
|
|
601
|
+
* ```
|
|
602
|
+
*
|
|
603
|
+
* @param source The blob to preview, or `null`.
|
|
604
|
+
* @returns The object URL, valid until the source changes or the component
|
|
605
|
+
* unmounts.
|
|
606
|
+
*/
|
|
607
|
+
export declare function useImagePreview(source: Blob | null | undefined): UseImagePreviewResult;
|
|
608
|
+
|
|
609
|
+
/** What {@link useImagePreview} returns. */
|
|
610
|
+
export declare interface UseImagePreviewResult {
|
|
611
|
+
/** Object URL for the current source, or `null`. */
|
|
612
|
+
readonly url: string | null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Run image operations with status tracking, safe against unmount.
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* ```tsx
|
|
620
|
+
* function Upload() {
|
|
621
|
+
* const { compress, isWorking, result } = useImageProcessing();
|
|
622
|
+
*
|
|
623
|
+
* async function onPick(file: File) {
|
|
624
|
+
* const ready = await compress(file, { maxBytes: 1_000_000, width: 1600 });
|
|
625
|
+
* await fetch("/api/photos", { method: "POST", body: ready.blob });
|
|
626
|
+
* }
|
|
627
|
+
*
|
|
628
|
+
* return <input type="file" disabled={isWorking} onChange={(e) => onPick(e.target.files![0]!)} />;
|
|
629
|
+
* }
|
|
630
|
+
* ```
|
|
631
|
+
*
|
|
632
|
+
* The returned promises still reject on failure, so a caller can `try`
|
|
633
|
+
* around them; `status` and `error` exist for rendering, not for swallowing
|
|
634
|
+
* the failure.
|
|
635
|
+
*
|
|
636
|
+
* @returns The operations plus their state.
|
|
637
|
+
*/
|
|
638
|
+
export declare function useImageProcessing(): UseImageProcessingResult;
|
|
639
|
+
|
|
640
|
+
/** What {@link useImageProcessing} returns. */
|
|
641
|
+
export declare interface UseImageProcessingResult {
|
|
642
|
+
/** Resize (and re-encode) an image. */
|
|
643
|
+
readonly resize: (source: ImageSource, options?: ResizeOptions) => Promise<ProcessedImage>;
|
|
644
|
+
/** Compress an image into a byte budget. */
|
|
645
|
+
readonly compress: (source: ImageSource, options: CompressOptions) => Promise<CompressedImage>;
|
|
646
|
+
/** The most recent result. */
|
|
647
|
+
readonly result: ProcessedImage | null;
|
|
648
|
+
/** Where the last call is. */
|
|
649
|
+
readonly status: ImageProcessingStatus;
|
|
650
|
+
/** Why the last call failed. */
|
|
651
|
+
readonly error: Error | null;
|
|
652
|
+
/** Whether a call is in flight. */
|
|
653
|
+
readonly isWorking: boolean;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export { }
|