@gmitch215/tinyimg 1.1.1 → 1.2.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 CHANGED
@@ -172,9 +172,15 @@ half the time. Across twelve JPEGs, milliseconds per megapixel spread 7.1x while
172
172
  megabyte spread 1.95x at this scale. The practical rule for a 200 px thumbnail is roughly **480 KB of
173
173
  compressed source**, whatever its dimensions.
174
174
 
175
- The estimate `decide()` returns still prices against source samples and is wrong by up to 12.8x for
176
- that reason, so treat `budgetMs` as a coarse guard rather than a promise. Past the ceiling the
177
- artifact has to be computed once and cached; see [Caching](#caching).
175
+ `decide().estimateMs` covers decode and resample and leaves the encoder out. On 878 held-out cells
176
+ timed against the v1.1.1 model (one machine, sources with alpha excluded) it ran a median 2.0x under
177
+ the clock, 38.6% of cells landed within 0.6x to 2.0x, and the worst was 10.97x under. Adding the
178
+ encoder, which is what `budgetMs` compares against, moved the median to 1.3x under and 59.8% in
179
+ band, with the worst at 9.04x under (a JPEG source) and 7.44x over (a PNG). AVIF sources were the
180
+ worst group, 3.7% in band. The model in this release prices the compressed bytes as well as the
181
+ samples and carries a calibrated upper bound, described under [Admission](#admission). Treat
182
+ `budgetMs` as a coarse guard with no guarantee behind it. Past the ceiling the artifact has to be computed
183
+ once and cached; see [Caching](#caching).
178
184
 
179
185
  A progressive JPEG is the exception worth knowing about. At an eighth scale the transform reads the
180
186
  DC term alone, so the scans that carry only high-frequency detail are stepped over rather than
@@ -190,13 +196,16 @@ the per-stage profile and the comparison against `@jsquash`.
190
196
 
191
197
  ### Gaps
192
198
 
193
- - **AVIF encode has no partition search.** Decode is complete and matches `dav1d` exactly. The
199
+ - **AVIF encode has no partition search.** Decode reads AV1 intra. The
194
200
  encoder uses fixed 8x8 blocks and picks the luma mode from four candidates by absolute
195
201
  difference, so its files run 1.1x to 1.3x libaom's size at matched quality. A partition search is
196
202
  where most of AV1's compression lives.
197
203
  - **No 10 or 12 bit AVIF.** The reconstruction is 8 bits a sample end to end, so an HDR AVIF is
198
204
  refused rather than truncated. 4:2:2 is refused too, for a reason in the format: the
199
205
  specification's own subsampled-size table has no answer for a tall block on such a plane.
206
+ - **Some AVIF files are refused rather than decoded wrong.** Film grain, superres, `grid` and
207
+ `iovl` images, a cropping `clap` aperture, PQ and HLG transfer, and an alpha plane the decoder
208
+ cannot read each return an unsupported-variant error.
200
209
  - **No animation.** Both decode a first frame; neither re-times or composes one.
201
210
  - **You own the caching.** Images caches derivatives at the edge as a product feature. tinyimg gives
202
211
  you the key and leaves the cache to you; see [Caching](#caching).
@@ -304,11 +313,11 @@ The options apply in a fixed order, exported as `TRANSFORM_ORDER`: crop, resize
304
313
  the color adjustments, blur, sharpen, trim. An option object has no order of its own, so two callers
305
314
  writing the same keys get the same image.
306
315
 
307
- A JPEG with an EXIF orientation is turned upright before anything else runs, so `width`, `height`,
316
+ A JPEG with an EXIF orientation, or an AVIF with `irot` and `imir` properties, is turned upright before anything else runs, so `width`, `height`,
308
317
  `crop` and `fit` all read the image as displayed. A phone photo stored landscape and tagged 90
309
318
  degrees comes out portrait, and a request for 400 wide is a portrait 400 wide. `probe` reports the
310
319
  tag as `orientation`, and the chainable API applies it with `Image.autoOrient()`, which has to be
311
- the first operation. Only JPEG reports it; every other format reports 1.
320
+ the first operation. Only JPEG and AVIF report it; every other format reports 1.
312
321
 
313
322
  ### Fit Modes
314
323
 
@@ -357,6 +366,11 @@ it, call `dispose()` in a `finally`. Encoding does not consume the plan.
357
366
 
358
367
  `pixels()` hands back the raw samples for a caller who wants them rather than a container.
359
368
 
369
+ `edit(fn)` runs after the plan, like `trim` and `rotateFree`, and hands `fn` the materialized image as
370
+ RGBA to change in place. It is for per-pixel arithmetic the library does not carry, such as another
371
+ library's exact blend or a fixed convolution kernel. The extent and channel count cannot change, and
372
+ the view is valid only inside the call.
373
+
360
374
  ## The Planner
361
375
 
362
376
  Nothing runs until the output is asked for. Operations append to a fixed-capacity list, and the plan
@@ -396,13 +410,19 @@ On that chain the planner is worth 3.63x against running the same operations one
396
410
  | ------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
397
411
  | PNG | 8 and 16 bit, gray/palette/truecolor with and without alpha, Adam7 | hash-chain LZ77, static and dynamic Huffman, the five filters |
398
412
  | JPEG | baseline and progressive, 4:4:4 / 4:2:2 / 4:2:0, CMYK and YCCK, restart markers | quality-scaled tables, optimized Huffman, progressive scan script |
399
- | WebP | VP8 lossy and VP8L lossless | both modes |
413
+ | WebP | VP8 lossy and VP8L lossless | both modes, alpha kept through an `ALPH` chunk when lossy |
400
414
  | GIF | LZW, local and global palettes, interlace, transparency | median-cut palette with Floyd-Steinberg dithering |
401
415
  | TIFF | strips, uncompressed / PackBits / LZW / Deflate, both byte orders | uncompressed and PackBits |
402
416
  | BMP | uncompressed and RLE8 | uncompressed |
403
417
  | AVIF | AV1 intra: every transform, all 13 intra modes, deblock and CDEF | fixed 8x8 partitions, no rate-distortion search |
404
418
  | HEIF | container only; `probe` answers and decode reports a specific error | no |
405
419
 
420
+ A lossy WebP with transparency is written as a `VP8X` header, an `ALPH` chunk holding the alpha plane
421
+ as a lossless stream, and the lossy frame, so alpha is exact at any `quality`; an image whose alpha
422
+ is all 255 gets the plain frame. The JPEG and AVIF encoders write no alpha, and GIF keeps one bit of
423
+ it. When a `transform` output has transparency its format cannot carry, `degraded` contains
424
+ `'alpha'`.
425
+
406
426
  JPEG carries true DCT-domain scaled decode at 1/2, 1/4 and 1/8, and region decode that Huffman-scans
407
427
  past blocks outside the box without transforming them. PNG, GIF, TIFF and BMP row-skip and
408
428
  column-skip.
@@ -732,9 +752,9 @@ const result = await transform(tinyimg, source, {
732
752
  result.format; // 'webp' if it fits inside 7 ms, otherwise 'jpeg'
733
753
  ```
734
754
 
735
- A format you name is never substituted, however far over budget it is. The estimate is accurate to
736
- about 20% on the machine its rates were measured on, so give a budget below the limit rather than at
737
- it, and use [`measure`](#work-counters) when you need what a request cost.
755
+ A format you name is never substituted, however far over budget it is. On the machine its rates were
756
+ measured on, the estimate's median decode error on images held out of the fit is within 10% for every
757
+ format except TIFF (54% under), the encoder is priced separately, and single requests spread wider, so give a budget below the limit rather than at it, and use [`measure`](#work-counters) when you need what a request cost.
738
758
 
739
759
  **A budget the container change cannot meet degrades the plan, and the result says what it gave up.**
740
760
  Container first, because a cheaper encoder costs bytes and nothing else; then the planner's own
@@ -767,6 +787,108 @@ The output extent never changes. A budget decides how the pixels are made, not h
767
787
  is degraded when the estimate already fits. `degraded` is empty on a pass-through, which needs no
768
788
  lever at all.
769
789
 
790
+ ### Admission
791
+
792
+ Naming `admission` beside `budgetMs` turns the budget into a decision on the calibrated upper bound
793
+ instead of the point estimate. `'strict'` throws `TinyImgBudgetError`, carrying the numbers, when the
794
+ bound plus the encoder is over budget, and gives nothing up. `'degrade'` takes the ladder above,
795
+ judged on the bound, and runs the cheapest request that results. `'passthrough'` degrades the same
796
+ way and returns the source bytes unchanged, with `'passthrough'` in `degraded`, when no rung fits;
797
+ that needs the output format to be the source's own, or none asked for.
798
+
799
+ ```ts
800
+ const result = await transform(tinyimg, source, {
801
+ width: 200,
802
+ budgetMs: 10,
803
+ admission: 'passthrough',
804
+ platformScale: 1.9
805
+ });
806
+ ```
807
+
808
+ An admitted request also stops itself. The module sums the work its decoders and encoders have
809
+ counted into a work clock, a function of the counters and not of a timer, because a Worker's timers
810
+ do not advance during CPU work. When the clock passes the budget, the operation returns
811
+ `TINYIMG_ERR_BUDGET`, which `transform` throws as `TinyImgBudgetError`, or answers with the source
812
+ under `'passthrough'`. `Image.admit(budgetMs, options)` returns the decision without running
813
+ anything.
814
+
815
+ The bound is a calibrated prediction under exchangeability, not a guarantee. It is relative to the
816
+ machine the cost model was timed on, so `platformScale` carries your own factor; a Worker ran the same
817
+ module 1.9x slower at the median and 2.4x to 4.7x slower on cheap requests, and the library applies
818
+ none. Leaving `admission` unset keeps `budgetMs` exactly as described above.
819
+
820
+ How it held on 423 requests the model never saw, at five budgets from 5 to 100 ms (2,115 request and
821
+ budget pairs, timed on the Apple M2 Pro the model was calibrated on): the bound admitted 739 pairs
822
+ and 67 of them, 9.07%, ran over budget. The v1.1.1 estimate admitted 1,174 and 451 of them, 38.42%,
823
+ ran over, so the bound admits with 6.73x fewer overruns. It misses the 5% overrun rate set for it
824
+ before the run, so use it as a filter and not as a limit. AVIF sources are the weak spot: 26 of the
825
+ 32 pairs admitted for them ran over. The other formats together ran over on about 6%. The bound also
826
+ refuses requests that fit, 7.31% of the pairs that did (WebP sources 33.9%). The test left out
827
+ sources with alpha, and lossy WebP with alpha and TIFF encode are priced less tightly (held-out
828
+ coverage 0.82 and 0.89). The tables are in `bench/baselines/v1.2/admission-summary.md`.
829
+
830
+ ### Selection
831
+
832
+ `Image.select(options)` searches the decode levers (scale, filter, effort), the output format and its
833
+ quality together, drops each combination that breaks a limit, and returns the best of the rest. It
834
+ reads no pixels and no clock. `Image.bytesFor(selection)` runs the choice and returns the encoded bytes.
835
+
836
+ ```ts
837
+ using image = await Image.open(tinyimg, source);
838
+ image.resize(400, 300);
839
+
840
+ const selection = image.select({ budgetMs: 20, platformScale: 1.9, minDb: 35 });
841
+
842
+ if (selection.feasible) {
843
+ const bytes = await image.bytesFor(selection);
844
+ // serve bytes as selection.choice.format
845
+ } else {
846
+ // selection.reason names the check the nearest miss failed
847
+ }
848
+ ```
849
+
850
+ | Option | Default | Meaning |
851
+ | ----------------- | ------------------------------- | ----------------------------------------------------------------- |
852
+ | `budgetMs` | no limit | CPU the request may spend, judged on the calibrated upper bound |
853
+ | `platformScale` | `1` | How much slower your machine is than the calibrated one |
854
+ | `peakBytes` | the module's heap ceiling | Largest heap bound a candidate may have |
855
+ | `minDb` | no floor | Predicted PSNR against the undegraded output, in dB |
856
+ | `formats` | the source's own, JPEG and WebP | Output containers to consider |
857
+ | `objective` | `'bytes'` | `'bytes'`, `'time'` or `'quality'`, among the candidates that fit |
858
+ | `uncharacterized` | `'refuse'` | `'allow'` keeps a candidate whose quality is unknown |
859
+ | `assumeOpaque` | `false` | Treat an RGBA source as opaque, so JPEG, AVIF and GIF qualify |
860
+
861
+ A `Selection` carries `feasible`, `reason`, `choice` (format, quality, whether it is lossless, the
862
+ lattice position `bytesFor` runs, and the predicted upper-bound cost, peak heap, dB and bytes), the
863
+ output `width` and `height`, whether the source counted as having `alpha`, the measured `coverage` of
864
+ each table, and every candidate in `considered` with the check that removed it. An infeasible
865
+ selection is a normal answer.
866
+
867
+ On the held-out test images at 10, 20 and 50 ms, with and without a 35 dB floor, the joint search
868
+ served 37.8% of 1,137 requests, the planner's own ladder 17.2% and format-and-quality chosen
869
+ independently 33.8%. Against independent choice the gain is 4 points, interval 3.0 to 5.1, and all of
870
+ it comes from requests without a quality floor. The tables are in
871
+ `bench/baselines/v1.2/selector-summary.md`.
872
+
873
+ The predictions come from tables measured on other images, so each one has a stated reach:
874
+
875
+ - **`db` is a table percentile.** It composes the fifth percentile of the decode lever with the fifth
876
+ percentile of the encoder. On images held out of the fit the lever table's percentile held for
877
+ 89.3% of cells (WebP sources 64.5%) and the encode table's for 94.8%, against a 95% target. Read it
878
+ as a likely floor, not a guarantee; `coverage` reports the figure for your source and format.
879
+ - **`bytes` is a median over 99 opaque images.** On the images the table was fitted on, it ordered
880
+ two formats at the same quality tier correctly in 93.5% of pairs. It cannot say how large this
881
+ image will be: measured bytes over predicted ran from 0.32 to 0.58 at the fifth percentile and
882
+ 1.27 to 1.84 at the 95th, by format, at quality 75.
883
+ - **Alpha is priced pessimistically.** The tables come from opaque images, so `db` is low and `bytes`
884
+ leaves out the alpha plane. An RGBA source cannot go to JPEG, AVIF or GIF unless `assumeOpaque` is
885
+ set, and the planner cannot see whether the pixels are opaque.
886
+ - **Lossless TIFF, BMP and WebP outputs have no quality class.** Their error is unmeasured, so
887
+ `'refuse'` drops them, and asking for `formats: ['tiff']` or `['bmp']` returns an infeasible
888
+ selection with reason `'uncharacterized'`. A TIFF or BMP source can still go to JPEG or WebP.
889
+ `uncharacterized: 'allow'` keeps the unclassified candidates with an unknown `db`, which cannot
890
+ satisfy a `minDb`.
891
+
770
892
  ## Caching
771
893
 
772
894
  A cache hit costs no CPU at all, which is how a request too expensive to serve synchronously still
@@ -799,6 +921,9 @@ const { work } = await tinyimg.measure(() => transform(tinyimg, source, { width:
799
921
  work.samplesPerTransform; // 1 at an eighth scale, 4 at a quarter, 64 at full
800
922
  work.decodedSamples / work.sourceSamples; // what the reduction was worth
801
923
  work.filtered; // macroblocks a region did not skip
924
+ work.entropyBytes; // compressed bytes the decoder read, which a region or progressive 1/8 lowers
925
+ work.encodeProbes; // match candidates a PNG encode compared, the cost `encoded` cannot see
926
+ work.alphaDropped; // samples whose alpha a JPEG, AVIF or GIF encode wrote without it
802
927
  ```
803
928
 
804
929
  `samplesPerTransform` is the one to read for a scaled decode: a decode transforming whole blocks and
package/dist/image.d.ts CHANGED
@@ -1,5 +1,44 @@
1
- import { type Color, type Fit, type FlipAxis, type Gravity, type ImageFormat, type PlanDecision, type RawImage, type ResampleFilter, type Source } from './types.js';
1
+ import { type Admission, type Color, type Fit, type FlipAxis, type Gravity, type ImageFormat, type PlanDecision, type RawImage, type ResampleFilter, type Selection, type Source } from './types.js';
2
2
  import { TinyImgModule } from './wasm.js';
3
+ /** What {@link Image.admit} takes beyond the budget. */
4
+ export interface AdmitOptions {
5
+ /**
6
+ * `'degrade'` walks the budget ladder on the upper bound before it rejects; `'strict'` rejects
7
+ * a request that is over as it stands. Defaults to `'degrade'`.
8
+ */
9
+ mode?: 'strict' | 'degrade';
10
+ /** The container the result is written as, which prices the encoder. Defaults to the source's. */
11
+ format?: ImageFormat;
12
+ /** How much slower this machine is than the calibrated one. Defaults to 1. */
13
+ platformScale?: number;
14
+ /** The encoder's quality, which moves its price for JPEG, WebP and AVIF. */
15
+ quality?: number;
16
+ /** Whether WebP is written lossless, which is dearer to encode. */
17
+ lossless?: boolean;
18
+ /** The encoder's compression level for PNG and TIFF. Defaults to `'auto'`. */
19
+ compression?: EncodeOptions['compression'];
20
+ /** The encoder's effort. Defaults to `'fancy'`. */
21
+ effort?: 'fancy' | 'fast';
22
+ }
23
+ /** What {@link Image.select} takes. */
24
+ export interface SelectOptions {
25
+ /** CPU the request may spend, in milliseconds on the machine it runs on. Omitted means no limit. */
26
+ budgetMs?: number;
27
+ /** How much slower this machine is than the calibrated one. Defaults to 1. */
28
+ platformScale?: number;
29
+ /** Largest heap bound a candidate may have, in bytes. Defaults to the module's heap ceiling. */
30
+ peakBytes?: number;
31
+ /** Quality floor as predicted PSNR in dB against the undegraded output. Omitted means no floor. */
32
+ minDb?: number;
33
+ /** Output containers to consider. Defaults to the source's own, JPEG and WebP. */
34
+ formats?: ImageFormat[];
35
+ /** What to minimize among the candidates that fit. Defaults to `'bytes'`. */
36
+ objective?: 'bytes' | 'time' | 'quality';
37
+ /** What to do with a candidate whose quality is unknown. Defaults to `'refuse'`. */
38
+ uncharacterized?: 'refuse' | 'allow';
39
+ /** Treat an RGBA source as opaque, so the search may pick JPEG, AVIF or GIF. */
40
+ assumeOpaque?: boolean;
41
+ }
3
42
  /** What {@link Image.bytes} and its siblings take. */
4
43
  export interface EncodeOptions {
5
44
  /** Quality for a lossy format, 1 through 100. */
@@ -138,12 +177,13 @@ export declare class Image {
138
177
  /** Mirrors along one axis or both. */
139
178
  flip(axis?: FlipAxis): this;
140
179
  /**
141
- * Turns the source upright by the orientation its EXIF names.
180
+ * Turns the source upright by the orientation its EXIF (JPEG) or its `irot` and `imir`
181
+ * properties (AVIF) name.
142
182
  *
143
183
  * The decoder returns pixels as stored and a plan's output carries no EXIF block, so an image
144
184
  * tagged as rotated would otherwise come out sideways. This appends the flip and turn that
145
185
  * orientation stands for, which fold into the output addressing like any other, so it costs no
146
- * pass. A source with no tag, and every format but JPEG, appends nothing.
186
+ * pass. A source with no tag, and every format but JPEG and AVIF, appends nothing.
147
187
  *
148
188
  * Call it before any other operation: a crop or resize after it works in display coordinates,
149
189
  * and one before it would not.
@@ -170,6 +210,18 @@ export declare class Image {
170
210
  * @param tolerance How far a pixel may differ from the border color and still be trimmed.
171
211
  */
172
212
  trim(tolerance?: number): this;
213
+ /**
214
+ * Changes the materialized pixels in place, with a function of your own.
215
+ *
216
+ * **Runs after the plan rather than inside it.** The escape hatch for per-pixel arithmetic the
217
+ * library does not carry, such as another library's exact integer blend or a fixed convolution
218
+ * kernel. The image is widened to RGBA first, so `fn` always sees four channels, and it gets a
219
+ * live view into the module's memory: write to `pixels` and the encoder reads what you wrote.
220
+ * The extent and the channel count cannot change, and the view is only valid inside the call.
221
+ *
222
+ * @param fn Receives the extent and `width * height * 4` bytes, rows tightly packed.
223
+ */
224
+ edit(fn: (image: RawImage) => void): this;
173
225
  /** Scales every channel. 1 is unchanged. */
174
226
  brightness(factor: number): this;
175
227
  /** Scales every channel about mid gray. 1 is unchanged. */
@@ -266,8 +318,43 @@ export declare class Image {
266
318
  * {@link transform}, which does that for you.
267
319
  *
268
320
  * @param milliseconds The budget, or 0 to remove it.
321
+ * @param basis Which cost the ladder compares to the budget. `'point'`, the default, is the
322
+ * estimate; `'upper'` is the calibrated upper bound, so a rung that fits is one whose bound
323
+ * fits.
324
+ */
325
+ budget(milliseconds: number, basis?: 'point' | 'upper'): this;
326
+ /**
327
+ * Decides whether this request fits a CPU budget, on the calibrated upper bound.
328
+ *
329
+ * Prices the plan undegraded, adds the encoder for the format being written, and multiplies by
330
+ * `platformScale`. The request is admitted when that bound is within `budgetMs`. In the default
331
+ * `'degrade'` mode an over-budget request walks the planner's ladder on the bound and is
332
+ * `'degraded'` when a rung fits and `'reject'` when none does; `'strict'` rejects without
333
+ * trying. The plan's own {@link budget} is not read and not changed.
334
+ *
335
+ * The bound is a prediction under exchangeability, not a guarantee, and it is relative to the
336
+ * machine the cost model was calibrated on.
337
+ *
338
+ * @param budgetMs CPU the request may spend, in milliseconds on the machine it runs on.
339
+ * @param options The mode, the output format, the machine factor and the encoder's settings.
340
+ * @return The decision and the numbers behind it.
269
341
  */
270
- budget(milliseconds: number): this;
342
+ admit(budgetMs: number, options?: AdmitOptions): Admission;
343
+ /**
344
+ * Searches the ways to run this request and picks the best one that fits the limits.
345
+ *
346
+ * Crosses the planner's decode levers (scale, filter, effort) with the output format and its
347
+ * quality, drops each combination that breaks a limit, and takes the best by `objective`. It
348
+ * reads no pixels and no clock, so it costs a few table lookups per candidate. Run the choice
349
+ * with {@link bytesFor}.
350
+ *
351
+ * The limits are the options', not the plan's own {@link budget}. A selection with `feasible`
352
+ * false is a normal answer: `reason` names the check the nearest miss failed.
353
+ *
354
+ * @param options The limits and the objective.
355
+ * @return The choice, the predicted figures behind it and every candidate looked at.
356
+ */
357
+ select(options?: SelectOptions): Selection;
271
358
  /**
272
359
  * Runs the plan and encodes the result.
273
360
  *
@@ -279,6 +366,19 @@ export declare class Image {
279
366
  * @return The encoded bytes.
280
367
  */
281
368
  bytes(format?: ImageFormat, options?: EncodeOptions): Promise<Uint8Array<ArrayBuffer>>;
369
+ /**
370
+ * Runs the plan the way a {@link select} chose and encodes it as that choice says.
371
+ *
372
+ * Decodes at the lattice point the selection names (its scale, filter and effort) and writes
373
+ * the format, quality and coding it names. `options` supplies the settings the selection does
374
+ * not carry: `progressive`, `stripMetadata`, `effort` and `compression`. The selection has to
375
+ * come from this image, with no operation added since.
376
+ *
377
+ * @param selection A feasible selection from {@link select}.
378
+ * @param options Encoder settings other than quality and lossless.
379
+ * @return The encoded bytes.
380
+ */
381
+ bytesFor(selection: Selection, options?: Omit<EncodeOptions, 'quality' | 'lossless'>): Promise<Uint8Array<ArrayBuffer>>;
282
382
  /**
283
383
  * Runs the plan and hands back the raw samples.
284
384
  *
@@ -1 +1 @@
1
- {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/ts/image.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/ts/image.ts"],"names":[],"mappings":"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"}
package/dist/image.js CHANGED
@@ -72,13 +72,69 @@ const Field = {
72
72
  passes: 14,
73
73
  kernels: 15,
74
74
  cost: 16,
75
- degraded: 17
75
+ degraded: 17,
76
+ costUpper: 18
76
77
  };
77
78
  const DEGRADES = [
78
79
  [1 << 0, 'effort'],
79
80
  [1 << 1, 'filter'],
80
81
  [1 << 2, 'scale']
81
82
  ];
83
+ const DECISIONS = ['admit', 'degraded', 'reject'];
84
+ const AdmissionField = {
85
+ decision: 0,
86
+ degraded: 1,
87
+ cost: 2,
88
+ costUpper: 3,
89
+ encode: 4,
90
+ encodeUpper: 5,
91
+ margin: 6
92
+ };
93
+ const SELECT_REASONS = [
94
+ 'none',
95
+ 'format',
96
+ 'alpha',
97
+ 'uncharacterized',
98
+ 'peak',
99
+ 'quality',
100
+ 'budget'
101
+ ];
102
+ const OBJECTIVES = ['bytes', 'time', 'quality'];
103
+ const SelectionField = {
104
+ feasible: 0,
105
+ reason: 1,
106
+ candidate: 2,
107
+ format: 3,
108
+ quality: 4,
109
+ lossless: 5,
110
+ costUpper: 6,
111
+ peak: 7,
112
+ dbMilli: 8,
113
+ dbMedianMilli: 9,
114
+ dbKnown: 10,
115
+ bytesPerPixelMicro: 12,
116
+ bytesKnown: 13,
117
+ predictedBytes: 14,
118
+ considered: 15,
119
+ width: 17,
120
+ height: 18,
121
+ alpha: 20,
122
+ leverCoverage: 21,
123
+ encodeCoverage: 23,
124
+ chosen: 24
125
+ };
126
+ const ConsideredField = {
127
+ format: 1,
128
+ quality: 2,
129
+ lossless: 3,
130
+ reason: 4,
131
+ dbMilli: 6,
132
+ dbKnown: 7,
133
+ bytesPerPixelMicro: 8,
134
+ bytesKnown: 9,
135
+ costUpper: 10,
136
+ peak: 11
137
+ };
82
138
  export const COMPRESSION = ['auto', 'none', 'fast', 'default', 'best'];
83
139
  export class Image {
84
140
  #module;
@@ -186,6 +242,25 @@ export class Image {
186
242
  });
187
243
  return this;
188
244
  }
245
+ edit(fn) {
246
+ this.#alive();
247
+ this.#after.push((image) => {
248
+ const module = this.#module;
249
+ module.check(module.exports.tiny_image_convert_channels(image, 4), 'widen to RGBA');
250
+ const width = module.exports.tiny_image_getwidth(image);
251
+ const height = module.exports.tiny_image_getheight(image);
252
+ const data = module.exports.tiny_image_getdata(image);
253
+ fn({
254
+ width,
255
+ height,
256
+ channels: 4,
257
+ pixels: module
258
+ .bytes()
259
+ .subarray(data, data + width * height * 4)
260
+ });
261
+ });
262
+ return this;
263
+ }
189
264
  brightness(factor) {
190
265
  return this.#append(this.#module.exports.tiny_plan_brightness(this.#plan, factor), 'brightness');
191
266
  }
@@ -272,6 +347,7 @@ export class Image {
272
347
  passes: field('passes'),
273
348
  kernels: KERNELS.filter(([bit]) => (bits & bit) !== 0).map(([, name]) => name),
274
349
  estimateMs: field('cost') / 1000,
350
+ costUpperMs: field('costUpper') / 1000,
275
351
  degraded: DEGRADES.filter(([bit]) => (field('degraded') & bit) !== 0).map(([, name]) => name)
276
352
  };
277
353
  }
@@ -279,12 +355,126 @@ export class Image {
279
355
  module.free(resolution);
280
356
  }
281
357
  }
282
- budget(milliseconds) {
358
+ budget(milliseconds, basis = 'point') {
283
359
  this.#alive();
284
360
  this.#module.check(this.#module.exports.tiny_plan_set_budget(this.#plan, Math.max(0, Math.round(milliseconds * 1000))), 'budget setting');
361
+ this.#module.check(this.#module.exports.tiny_plan_set_budget_basis(this.#plan, basis === 'upper' ? 1 : 0), 'budget basis setting');
285
362
  return this;
286
363
  }
364
+ admit(budgetMs, options = {}) {
365
+ this.#alive();
366
+ const module = this.#module;
367
+ const format = options.format ?? this.#format;
368
+ const opts = module.alloc(module.exports.tiny_admit_opts_sizeof());
369
+ const out = module.alloc(module.exports.tiny_admission_sizeof());
370
+ try {
371
+ const view = module.view();
372
+ const values = [
373
+ Math.min(0xffffffff, Math.max(0, Math.round(budgetMs * 1000))),
374
+ Math.min(0xffffffff, Math.max(1, Math.round((options.platformScale ?? 1) * 1000))),
375
+ options.mode === 'strict' ? 0 : 1,
376
+ format === 'unknown' ? 0 : module.formatId(format),
377
+ COMPRESSION.indexOf(options.compression ?? 'auto'),
378
+ options.quality ?? 0,
379
+ options.lossless ? 1 : 0,
380
+ options.effort === 'fast' ? 1 : 0
381
+ ];
382
+ values.forEach((value, index) => view.setUint32(opts + index * 4, value, true));
383
+ module.check(module.exports.tiny_plan_admit(this.#plan, opts, out), 'admit the plan');
384
+ const field = (name) => module.exports.tiny_admission_field(out, AdmissionField[name]);
385
+ return {
386
+ decision: DECISIONS[field('decision')] ?? 'reject',
387
+ costMs: field('cost') / 1000,
388
+ costUpperMs: field('costUpper') / 1000,
389
+ encodeMs: field('encode') / 1000,
390
+ encodeUpperMs: field('encodeUpper') / 1000,
391
+ marginMs: (field('margin') | 0) / 1000,
392
+ degraded: DEGRADES.filter(([bit]) => (field('degraded') & bit) !== 0).map(([, name]) => name)
393
+ };
394
+ }
395
+ finally {
396
+ module.free(out);
397
+ module.free(opts);
398
+ }
399
+ }
400
+ select(options = {}) {
401
+ this.#alive();
402
+ const module = this.#module;
403
+ const opts = module.alloc(module.exports.tiny_select_opts_sizeof());
404
+ const out = module.alloc(module.exports.tiny_selection_sizeof());
405
+ try {
406
+ const view = module.view();
407
+ const mask = (options.formats ?? []).reduce((bits, format) => bits | (1 << module.formatId(format)), 0);
408
+ const u32 = (value) => Math.min(0xffffffff, Math.max(0, Math.round(value)));
409
+ const objective = OBJECTIVES.indexOf(options.objective ?? 'bytes');
410
+ if (objective < 0)
411
+ throw new TypeError(`'${options.objective}' is not an objective`);
412
+ view.setUint32(opts, u32((options.budgetMs ?? 0) * 1000), true);
413
+ view.setUint32(opts + 4, u32(Math.max(1, (options.platformScale ?? 1) * 1000)), true);
414
+ view.setUint32(opts + 8, u32(options.peakBytes ?? 0), true);
415
+ view.setFloat32(opts + 12, options.minDb ?? 0, true);
416
+ view.setUint32(opts + 16, mask >>> 0, true);
417
+ view.setUint32(opts + 20, objective, true);
418
+ view.setUint32(opts + 24, options.uncharacterized === 'allow' ? 1 : 0, true);
419
+ view.setUint32(opts + 28, options.assumeOpaque ? 1 : 0, true);
420
+ module.check(module.exports.tiny_plan_select(this.#plan, opts, out), 'select');
421
+ const field = (name) => module.exports.tiny_selection_field(out, SelectionField[name]);
422
+ const width = field('width');
423
+ const height = field('height');
424
+ const read = (index) => {
425
+ const part = (name) => module.exports.tiny_selection_considered_field(out, index, ConsideredField[name]);
426
+ return {
427
+ format: module.formatName(part('format')),
428
+ quality: part('quality'),
429
+ lossless: part('lossless') !== 0,
430
+ reason: SELECT_REASONS[part('reason')] ?? 'none',
431
+ costUpperMs: part('costUpper') / 1000,
432
+ peakBytes: part('peak'),
433
+ db: part('dbKnown') ? part('dbMilli') / 1000 : undefined,
434
+ bytes: part('bytesKnown')
435
+ ? Math.round((part('bytesPerPixelMicro') / 1e6) * width * height)
436
+ : undefined
437
+ };
438
+ };
439
+ const considered = Array.from({ length: field('considered') }, (_, index) => read(index));
440
+ const feasible = field('feasible') !== 0;
441
+ return {
442
+ feasible,
443
+ reason: SELECT_REASONS[field('reason')] ?? 'none',
444
+ choice: feasible
445
+ ? {
446
+ ...considered[field('chosen')],
447
+ candidate: field('candidate'),
448
+ dbMedian: field('dbKnown') ? field('dbMedianMilli') / 1000 : undefined,
449
+ ...(field('bytesKnown') ? { bytes: field('predictedBytes') } : {})
450
+ }
451
+ : undefined,
452
+ width,
453
+ height,
454
+ alpha: field('alpha') !== 0,
455
+ coverage: {
456
+ lever: field('leverCoverage') / 1000,
457
+ encode: field('encodeCoverage') / 1000
458
+ },
459
+ considered
460
+ };
461
+ }
462
+ finally {
463
+ module.free(out);
464
+ module.free(opts);
465
+ }
466
+ }
287
467
  async bytes(format, options = {}) {
468
+ return this.#encode(format, options);
469
+ }
470
+ async bytesFor(selection, options = {}) {
471
+ const choice = selection.choice;
472
+ if (!selection.feasible || !choice) {
473
+ throw new TypeError(`the selection is not feasible (${selection.reason}), so there is nothing to run`);
474
+ }
475
+ return this.#encode(choice.format, { ...options, quality: choice.quality, lossless: choice.lossless }, choice.candidate);
476
+ }
477
+ #encode(format, options, lattice) {
288
478
  this.#alive();
289
479
  const target = format ?? this.#format;
290
480
  if (target === 'unknown') {
@@ -303,11 +493,11 @@ export class Image {
303
493
  try {
304
494
  module.check(module.exports.tiny_writer_init(writer, 0), 'prepare the encoder');
305
495
  try {
306
- if (this.#after.length === 0) {
496
+ if (this.#after.length === 0 && lattice === undefined) {
307
497
  module.check(module.exports.tiny_plan_encode(this.#plan, module.formatId(target), opts, writer), 'run and encode');
308
498
  }
309
499
  else {
310
- this.#runWithAfter(target, opts, writer);
500
+ this.#runWithAfter(target, opts, writer, lattice);
311
501
  }
312
502
  return module.copyOut(module.exports.tiny_writer_data(writer), module.exports.tiny_writer_size(writer));
313
503
  }
@@ -384,11 +574,13 @@ export class Image {
384
574
  this.#module.check(result, `append a ${what}`);
385
575
  return this;
386
576
  }
387
- #runWithAfter(target, opts, writer) {
577
+ #runWithAfter(target, opts, writer, lattice) {
388
578
  const module = this.#module;
389
579
  const image = module.alloc(module.exports.tiny_image_sizeof());
390
580
  try {
391
- module.check(module.exports.tiny_plan_run(this.#plan, image), 'run the plan');
581
+ module.check(lattice === undefined
582
+ ? module.exports.tiny_plan_run(this.#plan, image)
583
+ : module.exports.tiny_plan_run_lattice(this.#plan, lattice, image), 'run the plan');
392
584
  try {
393
585
  for (const step of this.#after)
394
586
  step(image);