@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 +135 -10
- package/dist/image.d.ts +104 -4
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js +198 -6
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tinyimg.wasm +0 -0
- package/dist/transform.d.ts +7 -0
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +97 -11
- package/dist/transform.js.map +1 -1
- package/dist/types.d.ts +160 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm.d.ts +57 -0
- package/dist/wasm.d.ts.map +1 -1
- package/dist/wasm.js +17 -3
- package/dist/wasm.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
|
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.
|
|
736
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*
|
package/dist/image.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/ts/image.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,KAAK,EACV,KAAK,GAAG,EACR,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/ts/image.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,GAAG,EACR,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,cAAc,EAEnB,KAAK,SAAS,EACd,KAAK,MAAM,EACX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAkH1C,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC5B,kGAAkG;IAClG,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC3C,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B;AAuDD,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC7B,oGAAoG;IACpG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,oFAAoF;IACpF,eAAe,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IACrC,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC7B,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;CAC5D;AAED,sEAAsE;AACtE,eAAO,MAAM,WAAW,sDAAuD,CAAC;AAEhF,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IAC1B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,wFAAwF;IACxF,MAAM,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,KAAK;;IAajB,OAAO;IAkBP;;;;;;;;;OASG;WACU,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IA6BxE,0FAA0F;IAC1F,IAAI,YAAY,IAAI,WAAW,GAAG,SAAS,CAE1C;IAED;;;;;OAKG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;;;OAKG;IACH,IAAI,UAAU,IAAI,MAAM,CAGvB;IAID;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,MAAM,GAAE,cAAuB,GAAG,IAAI;IAcxE,6EAA6E;IAC7E,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAO/D;;;;;;OAMG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,UAAU,GAAG;QAAE,GAAG,CAAC,EAAE,GAAG,CAAA;KAAO,GAAG,IAAI;IAgBlF;;;;;OAKG;IACH,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI;IAIzC,sCAAsC;IACtC,IAAI,CAAC,IAAI,GAAE,QAAuB,GAAG,IAAI;IAWzC;;;;;;;;;;;;;OAaG;IACH,UAAU,IAAI,IAAI;IAalB;;;;;;;OAOG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAwBjC;;;;;;;;OAQG;IACH,IAAI,CAAC,SAAS,SAAI,GAAG,IAAI;IAUzB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,IAAI;IA6BzC,4CAA4C;IAC5C,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAOhC,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO9B,6EAA6E;IAC7E,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAOhC,mCAAmC;IACnC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI1B,uDAAuD;IACvD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B,uFAAuF;IACvF,SAAS,IAAI,IAAI;IAIjB,oEAAoE;IACpE,MAAM,IAAI,IAAI;IAId;;;;;;;OAOG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAOzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,IAAI;IAqBxC;;;;;OAKG;IACH,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAoB9B;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAO9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI;IAWtC;;;;;;OAMG;IACH,MAAM,IAAI,YAAY;IAmDtB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,OAAO,GAAG,OAAiB,GAAG,IAAI;IAiBtE;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,SAAS;IA8C9D;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,SAAS;IAuF9C;;;;;;;;;OASG;IACG,KAAK,CACV,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,aAAkB,GACzB,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAInC;;;;;;;;;;;OAWG;IACG,QAAQ,CACb,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,UAAU,CAAM,GACvD,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAuEnC;;;;;;;;OAQG;IACG,MAAM,IAAI,OAAO,CAAC,QAAQ,CAAC;IA6BjC;;;;;OAKG;IACG,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5E;;;;;OAKG;IACG,QAAQ,CACb,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,WAAW,CAAA;KAAO,GACrD,OAAO,CAAC,QAAQ,CAAC;IAWpB;;;;;;;;OAQG;IACG,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAcjF;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAQf,4DAA4D;IAC5D,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAgDxB;AAED,iDAAiD;AACjD,wBAAgB,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAqB/D"}
|
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(
|
|
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);
|