@gmitch215/tinyimg 1.0.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.
@@ -0,0 +1,263 @@
1
+ import { type Color, type Fit, type FlipAxis, type Gravity, type ImageFormat, type PlanDecision, type RawImage, type ResampleFilter, type Source } from './types.js';
2
+ import { TinyImgModule } from './wasm.js';
3
+ /** What {@link Image.bytes} and its siblings take. */
4
+ export interface EncodeOptions {
5
+ /** Quality for a lossy format, 1 through 100. */
6
+ quality?: number;
7
+ /** Use the format's lossless mode where it has one. */
8
+ lossless?: boolean;
9
+ /** Write a progressive or interlaced stream. */
10
+ progressive?: boolean;
11
+ /** Drop EXIF and other metadata from the output. */
12
+ stripMetadata?: boolean;
13
+ /**
14
+ * How much computation the encoder may spend. `'fancy'` is the default.
15
+ *
16
+ * A separate axis from {@link EncodeOptions.quality}. Quality says what the output should look
17
+ * like; this says how hard to work to get there. `'fast'` bounds the WebP encoder's 4x4
18
+ * prediction search to its four whole-block modes, which over six fixtures at 800 wide measured
19
+ * 1.18x to 1.45x faster for between 8.8% smaller and 17.5% larger, never more than 0.05 dB
20
+ * apart. Flat-color illustrations pay the most, because hard diagonal edges are what the
21
+ * omitted modes are for.
22
+ *
23
+ * Formats with nothing to trade ignore it and do the exact thing.
24
+ */
25
+ effort?: 'fancy' | 'fast';
26
+ }
27
+ /** What {@link Image.fit} takes beyond the target extent. */
28
+ export interface FitOptions {
29
+ /** Which part a crop keeps, or where a pad puts the image. */
30
+ gravity?: Gravity;
31
+ /** Fills whatever a pad leaves empty. */
32
+ background?: Color;
33
+ /** Weights the fit's scale samples through. `auto` leaves the choice to the planner. */
34
+ filter?: ResampleFilter;
35
+ }
36
+ /**
37
+ * A chainable transformation over one source.
38
+ *
39
+ * **Nothing runs until you ask for the output.** Every method here appends to a plan, and the plan
40
+ * runs once, when {@link bytes} or one of its siblings is called. That is not an implementation
41
+ * detail: it is what lets a 100x100 thumbnail of a 16 megapixel photograph decode a 500x500 region
42
+ * at a quarter scale instead of 16 megapixels, and the decision cannot be made after the first
43
+ * operation has already run.
44
+ *
45
+ * The handle lives in the module's memory, so an image has to be released. Prefer `using`:
46
+ *
47
+ * ```ts
48
+ * using image = await tinyimg.open(request.body);
49
+ * const webp = await image.fit(800, 600, { gravity: 'face' }).sharpen(1).bytes('webp', { quality: 80 });
50
+ * ```
51
+ *
52
+ * Without `using`, call {@link dispose} in a `finally`. {@link bytes} does not dispose, because a
53
+ * caller may want two encodings of one plan.
54
+ */
55
+ export declare class Image {
56
+ #private;
57
+ private constructor();
58
+ /**
59
+ * Opens a source without decoding it.
60
+ *
61
+ * The header is read so the planner knows the extent it is working from; no pixel is touched
62
+ * until the plan runs.
63
+ *
64
+ * @param module The loaded module.
65
+ * @param source The encoded image, in any shape {@link Source} accepts.
66
+ * @return A transformation with no operations in it yet.
67
+ */
68
+ static open(module: TinyImgModule, source: Source): Promise<Image>;
69
+ /** The container the source arrived in, and the default {@link bytes} encodes back to. */
70
+ get sourceFormat(): ImageFormat | 'unknown';
71
+ /**
72
+ * Frames the source holds, which is 1 for a still image.
73
+ *
74
+ * Only the first is decoded, so a value above 1 means an encode of this plan is a still taken
75
+ * from an animation. The count comes from the header read {@link open} already performed.
76
+ */
77
+ get sourceFrames(): number;
78
+ /**
79
+ * Operations appended so far.
80
+ *
81
+ * The count before any rewrite, so it says what was asked for rather than what will run; see
82
+ * {@link decide} for the latter.
83
+ */
84
+ get operations(): number;
85
+ /**
86
+ * Resamples to an extent.
87
+ *
88
+ * @param width Target width. Zero, or omitted with a height given, keeps the aspect ratio.
89
+ * @param height Target height. Same rule.
90
+ * @param filter Weights to sample through. `auto` is an area average down and a cubic up,
91
+ * which is the right answer for almost every request.
92
+ */
93
+ resize(width: number, height?: number, filter?: ResampleFilter): this;
94
+ /** Takes a rectangle, in the coordinates the previous operation produced. */
95
+ crop(x: number, y: number, width: number, height: number): this;
96
+ /**
97
+ * Resamples and then crops or pads to an exact extent.
98
+ *
99
+ * @param width Target width.
100
+ * @param height Target height.
101
+ * @param options `fit` defaults to `cover`; `gravity` to `center`; `filter` to `auto`.
102
+ */
103
+ fit(width: number, height: number, options?: FitOptions & {
104
+ fit?: Fit;
105
+ }): this;
106
+ /**
107
+ * Turns by a multiple of 90 degrees clockwise.
108
+ *
109
+ * Folded into the output addressing rather than run as a pass, so any number of turns and flips
110
+ * cost one pass between them. {@link rotateFree} is the arbitrary-angle form.
111
+ */
112
+ rotate(degrees: 0 | 90 | 180 | 270): this;
113
+ /** Mirrors along one axis or both. */
114
+ flip(axis?: FlipAxis): this;
115
+ /**
116
+ * Turns by an arbitrary angle, filling the corners with the background.
117
+ *
118
+ * **Runs after the plan rather than inside it.** An arbitrary rotation changes the extent by a
119
+ * non-integer factor, so it cannot fold into the sample map the way a quarter turn does; it is
120
+ * applied to the materialized image. A quarter turn passed here is still handed to
121
+ * {@link rotate}, which is free.
122
+ */
123
+ rotateFree(degrees: number): this;
124
+ /**
125
+ * Trims a uniform border.
126
+ *
127
+ * **Runs after the plan rather than inside it.** How much to trim is a function of the pixels,
128
+ * and the planner decides the decode region and scale before a pixel is read, so a trim cannot
129
+ * be a plan operation. Chain it before a resize and the resize still happens first.
130
+ *
131
+ * @param tolerance How far a pixel may differ from the border color and still be trimmed.
132
+ */
133
+ trim(tolerance?: number): this;
134
+ /** Scales every channel. 1 is unchanged. */
135
+ brightness(factor: number): this;
136
+ /** Scales every channel about mid gray. 1 is unchanged. */
137
+ contrast(factor: number): this;
138
+ /** Moves every channel toward or away from its luminance. 1 is unchanged. */
139
+ saturation(factor: number): this;
140
+ /** Rotates the hue, in degrees. */
141
+ hue(degrees: number): this;
142
+ /** Raises every channel to a power. 1 is unchanged. */
143
+ gamma(value: number): this;
144
+ /** Replaces every channel with the pixel's luminance, and drops the color channels. */
145
+ grayscale(): this;
146
+ /** Subtracts every channel from full scale, leaving alpha alone. */
147
+ invert(): this;
148
+ /**
149
+ * Blurs.
150
+ *
151
+ * A true gaussian, as three box passes. The planner moves a blur that feeds a reduction of two
152
+ * or more to after the reduction, at a scaled sigma, which is exact for a gaussian.
153
+ *
154
+ * @param sigma Radius in pixels. Zero is a no-op and is eliminated.
155
+ */
156
+ blur(sigma: number): this;
157
+ /**
158
+ * Sharpens with an unsharp mask.
159
+ *
160
+ * @param amount How much of the high-frequency difference to add back. 1 is a typical value.
161
+ * @param sigma Radius of the blur it is measured against.
162
+ */
163
+ sharpen(amount: number, sigma?: number): this;
164
+ /**
165
+ * Sets what a pad or an arbitrary rotation fills with.
166
+ *
167
+ * Not an operation: it is a property of the plan, so calling it twice replaces rather than
168
+ * stacking, and calling it after {@link fit} still applies to that fit.
169
+ */
170
+ background(color: Color): this;
171
+ /**
172
+ * Turns operation fusion off, so every operation runs as its own pass.
173
+ *
174
+ * **Leave this on.** It exists to measure what the planner is worth: with it off the same chain
175
+ * costs one traversal and one buffer per operation, which is what the library would be without
176
+ * the planner. `bench/` is the only caller that should ever pass `false`.
177
+ *
178
+ * @param enabled False to run one operation per pass.
179
+ */
180
+ fusion(enabled: boolean): this;
181
+ /**
182
+ * Chooses how much work the decode may spend.
183
+ *
184
+ * `fancy`, the default, decodes to the bitstream's definition. `fast` lets a lossy decoder drop
185
+ * its smoothing pass, which is 1.53x on a lossy WebP for 46.8 dB and 1.11x to 1.25x on a
186
+ * subsampled JPEG for 43.6 to 59.5 dB.
187
+ *
188
+ * A lossless format has nothing to drop, because it defines its pixels exactly, so PNG, GIF,
189
+ * TIFF and lossless WebP decode identically either way. So does a 4:4:4 JPEG, which has no
190
+ * chroma to upsample.
191
+ *
192
+ * This is the decode side only. The encoder's effort is an argument to {@link bytes}.
193
+ *
194
+ * @param effort How hard to work.
195
+ */
196
+ effort(effort: 'fancy' | 'fast'): this;
197
+ /**
198
+ * Reports what the planner decided, without producing the image.
199
+ *
200
+ * The saving is visible here: `region` and `scale` are what the decoder will be asked for, and
201
+ * `eliminated` and `collapsed` are what the rewrites removed. Useful in a log line, and it is
202
+ * how the library's own tests assert a plan rather than inferring it from the pixels.
203
+ */
204
+ decide(): PlanDecision;
205
+ /**
206
+ * Runs the plan and encodes the result.
207
+ *
208
+ * The plan is not consumed, so this can be called twice for two formats of one transformation
209
+ * and the planner's work is done once.
210
+ *
211
+ * @param format Container to encode as. Defaults to the source's own.
212
+ * @param options Encoder settings.
213
+ * @return The encoded bytes.
214
+ */
215
+ bytes(format?: ImageFormat, options?: EncodeOptions): Promise<Uint8Array<ArrayBuffer>>;
216
+ /**
217
+ * Runs the plan and hands back the raw samples.
218
+ *
219
+ * Below the encoders, for a caller who wants the pixels themselves: a histogram, a hand-written
220
+ * kernel, or a measurement that should not include an encoder's time. Rows are tightly packed
221
+ * and there is no stride separate from `width * channels`.
222
+ *
223
+ * @return The extent, the channel count and `width * height * channels` bytes.
224
+ */
225
+ pixels(): Promise<RawImage>;
226
+ /**
227
+ * The result as a `Blob`, ready to hand to a `FormData` or a `Response`.
228
+ *
229
+ * @param format Container to encode as.
230
+ * @param options Encoder settings.
231
+ */
232
+ blob(format?: ImageFormat, options?: EncodeOptions): Promise<Blob>;
233
+ /**
234
+ * The result as a `Response`, which is what a Worker returns.
235
+ *
236
+ * @param format Container to encode as.
237
+ * @param options Encoder settings, plus `headers` merged into the response's own.
238
+ */
239
+ response(format?: ImageFormat, options?: EncodeOptions & {
240
+ headers?: HeadersInit;
241
+ }): Promise<Response>;
242
+ /**
243
+ * The result as a `data:` URL.
244
+ *
245
+ * For an inline `<img src>` or a placeholder. Base64 costs a third more bytes than the image,
246
+ * so this is for small results.
247
+ *
248
+ * @param format Container to encode as.
249
+ * @param options Encoder settings.
250
+ */
251
+ dataUrl(format?: ImageFormat, options?: EncodeOptions): Promise<string>;
252
+ /**
253
+ * Releases the plan and the source bytes.
254
+ *
255
+ * Safe to call twice. Every later call on the image throws instead of reading freed memory.
256
+ */
257
+ dispose(): void;
258
+ /** Lets `using` release the handle; see {@link dispose}. */
259
+ [Symbol.dispose](): void;
260
+ }
261
+ /** The content type a container is served as. */
262
+ export declare function mimeFor(format: ImageFormat | 'unknown'): string;
263
+ //# sourceMappingURL=image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/ts/image.ts"],"names":[],"mappings":"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"}
package/dist/image.js ADDED
@@ -0,0 +1,391 @@
1
+ import { readColor, readSource } from './types.js';
2
+ import { TinyImgModule } from './wasm.js';
3
+ const FITS = {
4
+ 'scale-down': 0,
5
+ contain: 1,
6
+ cover: 2,
7
+ crop: 3,
8
+ 'aspect-crop': 4,
9
+ 'aspect-contain': 5,
10
+ 'aspect-cover': 6,
11
+ pad: 7,
12
+ stretch: 8,
13
+ fill: 9,
14
+ 'scale-up': 10
15
+ };
16
+ const GRAVITIES = {
17
+ center: 0,
18
+ north: 1,
19
+ south: 2,
20
+ west: 3,
21
+ east: 4,
22
+ 'north-west': 5,
23
+ 'north-east': 6,
24
+ 'south-west': 7,
25
+ 'south-east': 8,
26
+ auto: 9,
27
+ face: 10
28
+ };
29
+ const FILTERS = {
30
+ auto: 0,
31
+ nearest: 1,
32
+ bilinear: 2,
33
+ box: 3,
34
+ 'catmull-rom': 4
35
+ };
36
+ const KERNELS = [
37
+ [1 << 0, 'region'],
38
+ [1 << 1, 'scaled'],
39
+ [1 << 2, 'copy'],
40
+ [1 << 3, 'resample'],
41
+ [1 << 4, 'orient'],
42
+ [1 << 5, 'color'],
43
+ [1 << 6, 'pad'],
44
+ [1 << 7, 'gray-decode'],
45
+ [1 << 8, 'neighborhood']
46
+ ];
47
+ const FX_UNSHARP = 0;
48
+ const Field = {
49
+ regionX: 0,
50
+ regionY: 1,
51
+ regionWidth: 2,
52
+ regionHeight: 3,
53
+ scale: 4,
54
+ decodeWidth: 5,
55
+ decodeHeight: 6,
56
+ width: 7,
57
+ height: 8,
58
+ channels: 9,
59
+ ops: 10,
60
+ eliminated: 11,
61
+ collapsed: 12,
62
+ colorStages: 13,
63
+ passes: 14,
64
+ kernels: 15
65
+ };
66
+ export class Image {
67
+ #module;
68
+ #plan;
69
+ #buffer;
70
+ #size;
71
+ #format;
72
+ #frames;
73
+ #after = [];
74
+ #background;
75
+ #disposed = false;
76
+ constructor(module, plan, buffer, size, format, frames) {
77
+ this.#module = module;
78
+ this.#plan = plan;
79
+ this.#buffer = buffer;
80
+ this.#size = size;
81
+ this.#format = format;
82
+ this.#frames = frames;
83
+ }
84
+ static async open(module, source) {
85
+ const bytes = await readSource(source);
86
+ const info = await module.probe(bytes);
87
+ const buffer = module.copyIn(bytes);
88
+ const plan = module.alloc(module.exports.tiny_plan_sizeof());
89
+ try {
90
+ module.check(module.exports.tiny_plan_init(plan, buffer, bytes.byteLength), 'open the image');
91
+ }
92
+ catch (error) {
93
+ module.free(plan);
94
+ module.free(buffer);
95
+ throw error;
96
+ }
97
+ return new Image(module, plan, buffer, bytes.byteLength, info.format, info.frames);
98
+ }
99
+ get sourceFormat() {
100
+ return this.#format;
101
+ }
102
+ get sourceFrames() {
103
+ return this.#frames;
104
+ }
105
+ get operations() {
106
+ this.#alive();
107
+ return this.#module.exports.tiny_plan_count(this.#plan);
108
+ }
109
+ resize(width, height = 0, filter = 'auto') {
110
+ return this.#append(filter === 'auto'
111
+ ? this.#module.exports.tiny_plan_resize(this.#plan, width, height)
112
+ : this.#module.exports.tiny_plan_resize_with(this.#plan, width, height, FILTERS[filter]), 'resize');
113
+ }
114
+ crop(x, y, width, height) {
115
+ return this.#append(this.#module.exports.tiny_plan_crop(this.#plan, x, y, width, height), 'crop');
116
+ }
117
+ fit(width, height, options = {}) {
118
+ if (options.background !== undefined)
119
+ this.background(options.background);
120
+ return this.#append(this.#module.exports.tiny_plan_fit_with(this.#plan, width, height, FITS[options.fit ?? 'cover'], GRAVITIES[options.gravity ?? 'center'], FILTERS[options.filter ?? 'auto']), 'fit');
121
+ }
122
+ rotate(degrees) {
123
+ return this.#append(this.#module.exports.tiny_plan_rotate(this.#plan, degrees), 'rotate');
124
+ }
125
+ flip(axis = 'horizontal') {
126
+ if (axis === 'horizontal' || axis === 'both') {
127
+ this.#append(this.#module.exports.tiny_plan_flip_horizontal(this.#plan), 'flip');
128
+ }
129
+ if (axis === 'vertical' || axis === 'both') {
130
+ this.#append(this.#module.exports.tiny_plan_flip_vertical(this.#plan), 'flip');
131
+ }
132
+ return this;
133
+ }
134
+ rotateFree(degrees) {
135
+ this.#alive();
136
+ const quarter = ((degrees % 360) + 360) % 360;
137
+ if (quarter % 90 === 0)
138
+ return this.rotate(quarter);
139
+ const background = this.#background;
140
+ this.#after.push((image) => {
141
+ const color = background ? this.#module.copyIn(background) : 0;
142
+ try {
143
+ this.#module.check(this.#module.exports.tiny_image_rotate(image, degrees, color), 'rotate');
144
+ }
145
+ finally {
146
+ if (color !== 0)
147
+ this.#module.free(color);
148
+ }
149
+ });
150
+ return this;
151
+ }
152
+ trim(tolerance = 8) {
153
+ this.#alive();
154
+ this.#after.push((image) => {
155
+ this.#module.check(this.#module.exports.tiny_image_trim(image, tolerance), 'trim');
156
+ });
157
+ return this;
158
+ }
159
+ brightness(factor) {
160
+ return this.#append(this.#module.exports.tiny_plan_brightness(this.#plan, factor), 'brightness');
161
+ }
162
+ contrast(factor) {
163
+ return this.#append(this.#module.exports.tiny_plan_contrast(this.#plan, factor), 'contrast');
164
+ }
165
+ saturation(factor) {
166
+ return this.#append(this.#module.exports.tiny_plan_saturation(this.#plan, factor), 'saturation');
167
+ }
168
+ hue(degrees) {
169
+ return this.#append(this.#module.exports.tiny_plan_hue(this.#plan, degrees), 'hue');
170
+ }
171
+ gamma(value) {
172
+ return this.#append(this.#module.exports.tiny_plan_gamma(this.#plan, value), 'gamma');
173
+ }
174
+ grayscale() {
175
+ return this.#append(this.#module.exports.tiny_plan_grayscale(this.#plan), 'grayscale');
176
+ }
177
+ invert() {
178
+ return this.#append(this.#module.exports.tiny_plan_invert(this.#plan), 'invert');
179
+ }
180
+ blur(sigma) {
181
+ return this.#append(this.#module.exports.tiny_plan_gaussian_blur(this.#plan, sigma), 'blur');
182
+ }
183
+ sharpen(amount, sigma = 1) {
184
+ this.#alive();
185
+ const params = this.#module.alloc(16);
186
+ const view = this.#module.view();
187
+ view.setFloat32(params, sigma, true);
188
+ view.setFloat32(params + 4, amount, true);
189
+ view.setFloat32(params + 8, 0, true);
190
+ view.setFloat32(params + 12, 0, true);
191
+ try {
192
+ return this.#append(this.#module.exports.tiny_plan_effect(this.#plan, FX_UNSHARP, params), 'sharpen');
193
+ }
194
+ finally {
195
+ this.#module.free(params);
196
+ }
197
+ }
198
+ background(color) {
199
+ this.#alive();
200
+ this.#background = readColor(color, 4);
201
+ const pointer = this.#module.copyIn(this.#background);
202
+ try {
203
+ this.#module.check(this.#module.exports.tiny_plan_background(this.#plan, pointer), 'set the background');
204
+ }
205
+ finally {
206
+ this.#module.free(pointer);
207
+ }
208
+ return this;
209
+ }
210
+ fusion(enabled) {
211
+ return this.#append(this.#module.exports.tiny_plan_set_fusion(this.#plan, enabled ? 1 : 0), 'fusion setting');
212
+ }
213
+ effort(effort) {
214
+ return this.#append(this.#module.exports.tiny_plan_set_effort(this.#plan, effort === 'fast' ? 1 : 0), 'effort setting');
215
+ }
216
+ decide() {
217
+ this.#alive();
218
+ const module = this.#module;
219
+ const resolution = module.alloc(module.exports.tiny_plan_resolution_sizeof());
220
+ try {
221
+ module.check(module.exports.tiny_plan_resolve(this.#plan, resolution), 'resolve the plan');
222
+ const field = (name) => module.exports.tiny_plan_field(resolution, Field[name]);
223
+ const bits = field('kernels');
224
+ return {
225
+ region: {
226
+ x: field('regionX'),
227
+ y: field('regionY'),
228
+ width: field('regionWidth'),
229
+ height: field('regionHeight')
230
+ },
231
+ scale: field('scale'),
232
+ decoded: { width: field('decodeWidth'), height: field('decodeHeight') },
233
+ output: {
234
+ width: field('width'),
235
+ height: field('height'),
236
+ channels: field('channels')
237
+ },
238
+ operations: field('ops'),
239
+ eliminated: field('eliminated'),
240
+ collapsed: field('collapsed'),
241
+ colorStages: field('colorStages'),
242
+ passes: field('passes'),
243
+ kernels: KERNELS.filter(([bit]) => (bits & bit) !== 0).map(([, name]) => name),
244
+ estimateMs: module.exports.tiny_plan_cost(this.#plan) / 1000
245
+ };
246
+ }
247
+ finally {
248
+ module.free(resolution);
249
+ }
250
+ }
251
+ async bytes(format, options = {}) {
252
+ this.#alive();
253
+ const target = format ?? this.#format;
254
+ if (target === 'unknown') {
255
+ throw new TypeError('the source format is unknown, so a format has to be given');
256
+ }
257
+ const module = this.#module;
258
+ const writer = module.alloc(module.exports.tiny_writer_sizeof());
259
+ const opts = module.alloc(5);
260
+ const view = module.view();
261
+ view.setUint8(opts, options.quality ?? 0);
262
+ view.setUint8(opts + 1, options.lossless ? 1 : 0);
263
+ view.setUint8(opts + 2, options.progressive ? 1 : 0);
264
+ view.setUint8(opts + 3, options.stripMetadata ? 1 : 0);
265
+ view.setUint8(opts + 4, options.effort === 'fast' ? 1 : 0);
266
+ try {
267
+ module.check(module.exports.tiny_writer_init(writer, 0), 'prepare the encoder');
268
+ try {
269
+ if (this.#after.length === 0) {
270
+ module.check(module.exports.tiny_plan_encode(this.#plan, module.formatId(target), opts, writer), 'run and encode');
271
+ }
272
+ else {
273
+ this.#runWithAfter(target, opts, writer);
274
+ }
275
+ return module.copyOut(module.exports.tiny_writer_data(writer), module.exports.tiny_writer_size(writer));
276
+ }
277
+ finally {
278
+ module.exports.tiny_writer_free(writer);
279
+ }
280
+ }
281
+ finally {
282
+ module.free(opts);
283
+ module.free(writer);
284
+ }
285
+ }
286
+ async pixels() {
287
+ this.#alive();
288
+ const module = this.#module;
289
+ const image = module.alloc(module.exports.tiny_image_sizeof());
290
+ try {
291
+ module.check(module.exports.tiny_plan_run(this.#plan, image), 'run the plan');
292
+ try {
293
+ for (const step of this.#after)
294
+ step(image);
295
+ return {
296
+ width: module.exports.tiny_image_getwidth(image),
297
+ height: module.exports.tiny_image_getheight(image),
298
+ channels: module.exports.tiny_image_getchannels(image),
299
+ pixels: module.copyOut(module.exports.tiny_image_getdata(image), module.exports.tiny_image_getsize(image))
300
+ };
301
+ }
302
+ finally {
303
+ module.exports.tiny_image_destroy(image);
304
+ }
305
+ }
306
+ finally {
307
+ module.free(image);
308
+ }
309
+ }
310
+ async blob(format, options = {}) {
311
+ const target = format ?? this.#format;
312
+ const bytes = await this.bytes(format, options);
313
+ return new Blob([bytes], { type: mimeFor(target) });
314
+ }
315
+ async response(format, options = {}) {
316
+ const target = format ?? this.#format;
317
+ const bytes = await this.bytes(format, options);
318
+ const headers = new Headers(options.headers);
319
+ headers.set('content-type', mimeFor(target));
320
+ headers.set('content-length', String(bytes.byteLength));
321
+ return new Response(bytes, { headers });
322
+ }
323
+ async dataUrl(format, options = {}) {
324
+ const target = format ?? this.#format;
325
+ const bytes = await this.bytes(format, options);
326
+ let binary = '';
327
+ for (const byte of bytes)
328
+ binary += String.fromCharCode(byte);
329
+ return `data:${mimeFor(target)};base64,${btoa(binary)}`;
330
+ }
331
+ dispose() {
332
+ if (this.#disposed)
333
+ return;
334
+ this.#disposed = true;
335
+ this.#module.free(this.#plan);
336
+ this.#module.free(this.#buffer);
337
+ }
338
+ [Symbol.dispose]() {
339
+ this.dispose();
340
+ }
341
+ #alive() {
342
+ if (this.#disposed)
343
+ throw new TypeError('this image has been disposed');
344
+ }
345
+ #append(result, what) {
346
+ this.#alive();
347
+ this.#module.check(result, `append a ${what}`);
348
+ return this;
349
+ }
350
+ #runWithAfter(target, opts, writer) {
351
+ const module = this.#module;
352
+ const image = module.alloc(module.exports.tiny_image_sizeof());
353
+ try {
354
+ module.check(module.exports.tiny_plan_run(this.#plan, image), 'run the plan');
355
+ try {
356
+ for (const step of this.#after)
357
+ step(image);
358
+ module.check(module.exports.tiny_image_encode(image, module.formatId(target), opts, writer), 'encode');
359
+ }
360
+ finally {
361
+ module.exports.tiny_image_destroy(image);
362
+ }
363
+ }
364
+ finally {
365
+ module.free(image);
366
+ }
367
+ }
368
+ }
369
+ export function mimeFor(format) {
370
+ switch (format) {
371
+ case 'png':
372
+ return 'image/png';
373
+ case 'jpeg':
374
+ return 'image/jpeg';
375
+ case 'bmp':
376
+ return 'image/bmp';
377
+ case 'gif':
378
+ return 'image/gif';
379
+ case 'tiff':
380
+ return 'image/tiff';
381
+ case 'webp':
382
+ return 'image/webp';
383
+ case 'avif':
384
+ return 'image/avif';
385
+ case 'heif':
386
+ return 'image/heif';
387
+ default:
388
+ return 'application/octet-stream';
389
+ }
390
+ }
391
+ //# sourceMappingURL=image.js.map