@gmitch215/tinyimg 1.0.0 → 1.1.1
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/LICENSES/DejaVu.txt +187 -0
- package/LICENSES/OpenCV-LBP-Cascades.txt +77 -0
- package/README.md +395 -129
- package/dist/image.d.ts +67 -1
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js +42 -5
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/text.d.ts +174 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +307 -0
- package/dist/text.js.map +1 -0
- package/dist/tinyimg.wasm +0 -0
- package/dist/transform.d.ts +16 -0
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +33 -8
- package/dist/transform.js.map +1 -1
- package/dist/types.d.ts +29 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm.d.ts +84 -8
- package/dist/wasm.d.ts.map +1 -1
- package/dist/wasm.js +59 -5
- package/dist/wasm.js.map +1 -1
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🏞️ tinyimg
|
|
2
2
|
|
|
3
|
-
> Lightweight & fast image processing library for
|
|
3
|
+
> Lightweight & fast image processing library for WebAssembly
|
|
4
4
|
|
|
5
5
|
tinyimg decodes, transforms, draws on and re-encodes images inside a Worker, with no binding, no
|
|
6
6
|
subrequest and no per-transformation charge. It is freestanding C compiled to one wasm32 module,
|
|
@@ -23,6 +23,10 @@ own CPU budget.
|
|
|
23
23
|
- [Drawing](#drawing)
|
|
24
24
|
- [Effects](#effects)
|
|
25
25
|
- [Text](#text)
|
|
26
|
+
- [Setting a Run](#setting-a-run)
|
|
27
|
+
- [Mixed Styles](#mixed-styles)
|
|
28
|
+
- [What the Face Says](#what-the-face-says)
|
|
29
|
+
- [Open Graph Cards](#open-graph-cards)
|
|
26
30
|
- [Face Detection](#face-detection)
|
|
27
31
|
- [Color Management](#color-management)
|
|
28
32
|
- [Reading the dB Figures](#reading-the-db-figures)
|
|
@@ -48,8 +52,9 @@ own CPU budget.
|
|
|
48
52
|
what to decode before decoding it; see [The Planner](#the-planner).
|
|
49
53
|
- **A drawing surface.** Shapes, polygons, gradients, compositing with blend modes, and a display
|
|
50
54
|
list with a transform stack, which is the Canvas-shaped API the runtime does not have.
|
|
51
|
-
- **
|
|
52
|
-
|
|
55
|
+
- **Works off a bare instantiate.** A Latin face, four ICC profiles and two face cascades are
|
|
56
|
+
compiled in, so text, color conversion and face detection need no blob and no subrequest. A wider
|
|
57
|
+
glyph set or a cascade of your own loads at runtime.
|
|
53
58
|
- **TypeScript-first.** The types are the contract and the documentation.
|
|
54
59
|
|
|
55
60
|
## Compared With Cloudflare Images
|
|
@@ -58,54 +63,81 @@ Both do the same job in different places. Images Transformations runs on Cloudfl
|
|
|
58
63
|
bills per transformation. tinyimg runs in your Worker's isolate and bills as CPU time, which on the
|
|
59
64
|
Workers Paid plan is a resource the $5 subscription already includes 30 million milliseconds of.
|
|
60
65
|
|
|
61
|
-
| | Cloudflare Images | tinyimg
|
|
62
|
-
| --------------------- | --------------------------------------- |
|
|
63
|
-
| Charge per transform | $0.50 / 1,000 unique, 5,000 free / mo | none
|
|
64
|
-
| What you pay instead | nothing else | Worker CPU time
|
|
65
|
-
| Billing granularity | unique source and flags, once per month | every invocation that is not cached
|
|
66
|
-
| Subrequest or binding | required | none
|
|
67
|
-
| AVIF output | yes |
|
|
68
|
-
| Runs on Workers Free | 5,000 transforms / mo |
|
|
69
|
-
| Raw pixel access | no | yes
|
|
70
|
-
| Shapes and gradients | no | yes
|
|
71
|
-
| Text
|
|
66
|
+
| | Cloudflare Images | tinyimg |
|
|
67
|
+
| --------------------- | --------------------------------------- | ----------------------------------- |
|
|
68
|
+
| Charge per transform | $0.50 / 1,000 unique, 5,000 free / mo | none |
|
|
69
|
+
| What you pay instead | nothing else | Worker CPU time |
|
|
70
|
+
| Billing granularity | unique source and flags, once per month | every invocation that is not cached |
|
|
71
|
+
| Subrequest or binding | required | none |
|
|
72
|
+
| AVIF output | yes | yes |
|
|
73
|
+
| Runs on Workers Free | 5,000 transforms / mo | see [Speed](#speed) |
|
|
74
|
+
| Raw pixel access | no | yes |
|
|
75
|
+
| Shapes and gradients | no | yes |
|
|
76
|
+
| Text measurement | no | yes |
|
|
77
|
+
| Text wrapping | no | yes |
|
|
78
|
+
| Text stroke or shadow | no | yes |
|
|
79
|
+
| Mixed styles in a run | no | yes |
|
|
80
|
+
| Ligatures and GPOS | no | yes |
|
|
81
|
+
| Variable font axes | no | yes |
|
|
82
|
+
|
|
83
|
+
Images renders text as of 2026: `.text(content, options)` takes a font by URL (TrueType, OpenType
|
|
84
|
+
or WOFF, up to 20 MB), a colour and a size, up to 1,000 characters and 4096 x 4096 pixels, positioned
|
|
85
|
+
through the `draw` array with Porter-Duff compositing. The row that used to read "one string, one
|
|
86
|
+
font, one size" understated it.
|
|
87
|
+
|
|
88
|
+
What it does not do is give the caller any measurements back, so nothing can be positioned relative
|
|
89
|
+
to the text. That is the difference the text rows above describe: wrapping a caption to a box,
|
|
90
|
+
centring a line inside a plate, or fitting a title to a width all need the metrics first, and
|
|
91
|
+
`measureText` is the entry point Images has no equivalent of. The rest follows from having a layout
|
|
92
|
+
rather than a string: an outline and a shadow so a headline survives being drawn over a photograph,
|
|
93
|
+
several styles on one baseline, and one variable file serving every weight.
|
|
72
94
|
|
|
73
95
|
### Cost
|
|
74
96
|
|
|
75
97
|
Past the included CPU, Workers bills $0.02 per million CPU milliseconds, so a transformation costs
|
|
76
|
-
its own compute multiplied by that.
|
|
98
|
+
its own compute multiplied by that. Four routes to the same 800 px page:
|
|
77
99
|
|
|
78
100
|
| Route | `effort` | Compute | Per million | Within the included CPU |
|
|
79
101
|
| ----------- | -------- | ------: | ----------: | ----------------------: |
|
|
80
|
-
| 800 px
|
|
81
|
-
| 800 px WebP | fast |
|
|
82
|
-
| 800 px
|
|
102
|
+
| 800 px JPEG | either | 16.7 ms | $0.33 | 1,796,000 |
|
|
103
|
+
| 800 px WebP | fast | 22.4 ms | $0.45 | 1,339,000 |
|
|
104
|
+
| 800 px WebP | fancy | 25.5 ms | $0.51 | 1,176,000 |
|
|
105
|
+
| 800 px AVIF | either | 45 ms | $0.90 | 666,000 |
|
|
83
106
|
|
|
84
107
|
Against **$500 per million** for Images at $0.50 per 1,000. The absolute figures are small whichever
|
|
85
108
|
route you take. What the spread buys is CPU headroom, and headroom is what the Free plan rations.
|
|
109
|
+
AVIF costs the most compute and produces the smallest file: 17,860 bytes against WebP's 20,572 and
|
|
110
|
+
JPEG's 21,561 on the same 800 px request.
|
|
86
111
|
|
|
87
112
|
Images bills a unique source and flag combination **once per calendar month** however often it is
|
|
88
|
-
served, so a small set of derivatives under heavy traffic is already cheap there.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
served, so a small set of derivatives under heavy traffic is already cheap there. Since July 2026
|
|
114
|
+
the Images binding bills the same way rather than per call, and `.info()` is not billed at all.
|
|
115
|
+
tinyimg charges for every invocation that runs, so put
|
|
116
|
+
[Workers Cache](https://developers.cloudflare.com/workers/cache/) in front of it and a hit costs no
|
|
117
|
+
CPU at all.
|
|
118
|
+
|
|
119
|
+
The split follows from that. Images is cheaper for a fixed set of derivatives you serve repeatedly.
|
|
120
|
+
tinyimg is cheaper when the derivatives are unbounded, and the clearest case is text that varies per
|
|
121
|
+
request: a social card carrying a page title is a unique transformation per page, so it bills every
|
|
122
|
+
page and never reuses one.
|
|
92
123
|
|
|
93
124
|
### Speed
|
|
94
125
|
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
Fastest of 15 runs in bun on darwin-arm64 over `sf-24.jpg` at 1835x1032. The minimum rather than the
|
|
127
|
+
median, because a busy machine can only move a timing upward. This is not Worker CPU time on
|
|
128
|
+
Cloudflare's hardware, so profile your own images.
|
|
97
129
|
|
|
98
130
|
| Request | `effort: 'fancy'` | `effort: 'fast'` |
|
|
99
131
|
| ---------------------- | ----------------: | ---------------: |
|
|
100
|
-
| 200 px cover, JPEG q80 |
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
| 800 px cover,
|
|
105
|
-
| 800 px cover, WebP q80 | 41.6 ms | 33.4 ms |
|
|
132
|
+
| 200 px cover, JPEG q80 | 5.1 ms | 5.0 ms |
|
|
133
|
+
| 400 px cover, JPEG q80 | 9.1 ms | 9.2 ms |
|
|
134
|
+
| 800 px cover, JPEG q80 | 16.7 ms | 16.8 ms |
|
|
135
|
+
| 800 px cover, WebP q80 | 25.5 ms | 22.4 ms |
|
|
136
|
+
| 800 px cover, AVIF q80 | 45 ms | 45 ms |
|
|
106
137
|
|
|
107
|
-
`effort` moves WebP
|
|
108
|
-
prediction search to bound and the
|
|
138
|
+
`effort` moves WebP and leaves JPEG and AVIF output flat, because the WebP encoder has a 4x4
|
|
139
|
+
prediction search to bound and neither of the others has an equivalent. A PNG output is the other
|
|
140
|
+
one it moves; see [Budget and Effort](#budget-and-effort).
|
|
109
141
|
|
|
110
142
|
JPEG's own lever is on the decode side, and a thumbnail cannot show it. Replicating chroma instead
|
|
111
143
|
of filtering it is worth 1.11x to 1.25x on a full decode, but the saving scales with output samples
|
|
@@ -118,32 +150,53 @@ under 0.1 ms; neither depends on effort.
|
|
|
118
150
|
|
|
119
151
|
The Workers Free plan allows 10 ms of CPU per request and does not let a caller pay for more, so a
|
|
120
152
|
request either fits or fails. Workers Paid defaults to 30 seconds, raised to 5 minutes with
|
|
121
|
-
`limits.cpu_ms`, and nothing above comes close to it.
|
|
153
|
+
`limits.cpu_ms`, and nothing above comes close to it. Every millisecond below is measured on
|
|
154
|
+
darwin-arm64; Workers is 1.9x slower at the median and up to 4.7x on the cheapest requests, which are
|
|
155
|
+
the ones nearest the limit, so scale before comparing.
|
|
122
156
|
|
|
123
157
|
**What fits Free is decided by the source, not by the size you ask for.** Every row below produces
|
|
124
|
-
the same 200 px thumbnail from the same eighth-scale decode
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| Source | Megapixels | 200 px wide, JPEG |
|
|
128
|
-
| --------- | ---------: | ----------------: |
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
the same 200 px thumbnail from the same eighth-scale decode. The spread is the entropy decode, which
|
|
159
|
+
no output size can reduce.
|
|
160
|
+
|
|
161
|
+
| Source | Megapixels | Compressed | 200 px wide, JPEG |
|
|
162
|
+
| --------- | ---------: | ---------: | ----------------: |
|
|
163
|
+
| 250x187 | 0.05 | 23 KB | 2.2 ms |
|
|
164
|
+
| 2272x3000 | 6.82 | 215 KB | 8.0 ms |
|
|
165
|
+
| 2308x3000 | 6.92 | 435 KB | 12.1 ms |
|
|
166
|
+
| 1920x1280 | 2.46 | 828 KB | 15.4 ms |
|
|
167
|
+
| 3600x2700 | 9.72 | 2721 KB | 53 ms |
|
|
168
|
+
|
|
169
|
+
**Compressed bytes predict it and megapixels do not.** The second and fourth rows make the point on
|
|
170
|
+
their own: 2272x3000 carries 2.8x the pixels of 1920x1280, a quarter of the bytes, and finishes in
|
|
171
|
+
half the time. Across twelve JPEGs, milliseconds per megapixel spread 7.1x while milliseconds per
|
|
172
|
+
megabyte spread 1.95x at this scale. The practical rule for a 200 px thumbnail is roughly **480 KB of
|
|
173
|
+
compressed source**, whatever its dimensions.
|
|
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).
|
|
178
|
+
|
|
179
|
+
A progressive JPEG is the exception worth knowing about. At an eighth scale the transform reads the
|
|
180
|
+
DC term alone, so the scans that carry only high-frequency detail are stepped over rather than
|
|
181
|
+
decoded: `road.jpg` at 160 px goes from 28.5 ms to **6.5 ms**, byte-identical, which moves that
|
|
182
|
+
request from outside the Free budget to inside it.
|
|
183
|
+
|
|
184
|
+
JPEG and PNG decode are slightly ahead of libjpeg and libpng, at 1.06x-1.09x and 1.03x-1.08x; WebP
|
|
185
|
+
decode is 0.68x-0.72x behind libwebp. JPEG and WebP encode are ahead at 3.8x-4.0x and 1.31x-1.38x.
|
|
186
|
+
PNG encode is 12x behind, and that row is an operating-point difference as much as a speed one, since
|
|
187
|
+
it produces a file 47% smaller; `compression: 'fast'` closes most of it at a stated size cost.
|
|
139
188
|
[`TECHNICAL_REPORT.md`](https://github.com/gmitch215/tinyimg/blob/master/TECHNICAL_REPORT.md) has
|
|
140
189
|
the per-stage profile and the comparison against `@jsquash`.
|
|
141
190
|
|
|
142
191
|
### Gaps
|
|
143
192
|
|
|
144
|
-
- **
|
|
145
|
-
|
|
146
|
-
|
|
193
|
+
- **AVIF encode has no partition search.** Decode is complete and matches `dav1d` exactly. The
|
|
194
|
+
encoder uses fixed 8x8 blocks and picks the luma mode from four candidates by absolute
|
|
195
|
+
difference, so its files run 1.1x to 1.3x libaom's size at matched quality. A partition search is
|
|
196
|
+
where most of AV1's compression lives.
|
|
197
|
+
- **No 10 or 12 bit AVIF.** The reconstruction is 8 bits a sample end to end, so an HDR AVIF is
|
|
198
|
+
refused rather than truncated. 4:2:2 is refused too, for a reason in the format: the
|
|
199
|
+
specification's own subsampled-size table has no answer for a tall block on such a plane.
|
|
147
200
|
- **No animation.** Both decode a first frame; neither re-times or composes one.
|
|
148
201
|
- **You own the caching.** Images caches derivatives at the edge as a product feature. tinyimg gives
|
|
149
202
|
you the key and leaves the cache to you; see [Caching](#caching).
|
|
@@ -155,8 +208,10 @@ compositing, and a rasterized text string. Past that:
|
|
|
155
208
|
|
|
156
209
|
- **A drawing surface.** Lines, rectangles, rounded rectangles, circles, ellipses, polygons with both
|
|
157
210
|
fill rules, linear and radial gradients, and a display list with a transform matrix stack.
|
|
158
|
-
- **Text as layout.** Wrapping, alignment,
|
|
159
|
-
|
|
211
|
+
- **Text as layout.** Wrapping, alignment, justification, vertical alignment, ellipsis, per-line
|
|
212
|
+
metrics, outlines, shadows, mixed styles on one baseline, fallback chains, ligatures, GPOS kerning
|
|
213
|
+
and variable font axes, against Images' single styled string. `og` renders a whole Open Graph card
|
|
214
|
+
in one call.
|
|
160
215
|
- **Raw pixels.** `decode` and `Image.pixels` hand back samples, so anything not in the operation set
|
|
161
216
|
is still reachable.
|
|
162
217
|
- **Effects and warps.** Sobel, emboss, morphology, median, dither, halftone, duotone, split-tone,
|
|
@@ -243,12 +298,18 @@ carry `.bytes()`, `.blob()`, `.response()` and `.dataUrl()`.
|
|
|
243
298
|
| `format` | `ImageFormat` | Container to encode as. Defaults to the source's own. |
|
|
244
299
|
| `quality` | `number` | 1 through 100, for a lossy format. |
|
|
245
300
|
| `lossless`, `progressive` | `boolean` | Where the format has the mode. |
|
|
246
|
-
| `metadata` | `'keep' \| 'none'` |
|
|
301
|
+
| `metadata` | `'keep' \| 'none'` | Accepted, but a transformation writes no EXIF either way. |
|
|
247
302
|
|
|
248
303
|
The options apply in a fixed order, exported as `TRANSFORM_ORDER`: crop, resize or fit, rotate, flip,
|
|
249
304
|
the color adjustments, blur, sharpen, trim. An option object has no order of its own, so two callers
|
|
250
305
|
writing the same keys get the same image.
|
|
251
306
|
|
|
307
|
+
A JPEG with an EXIF orientation is turned upright before anything else runs, so `width`, `height`,
|
|
308
|
+
`crop` and `fit` all read the image as displayed. A phone photo stored landscape and tagged 90
|
|
309
|
+
degrees comes out portrait, and a request for 400 wide is a portrait 400 wide. `probe` reports the
|
|
310
|
+
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.
|
|
312
|
+
|
|
252
313
|
### Fit Modes
|
|
253
314
|
|
|
254
315
|
| Mode | Absorbs the mismatch by | Scale may |
|
|
@@ -331,20 +392,34 @@ On that chain the planner is worth 3.63x against running the same operations one
|
|
|
331
392
|
|
|
332
393
|
## Codec Support
|
|
333
394
|
|
|
334
|
-
| Format
|
|
335
|
-
|
|
|
336
|
-
| PNG
|
|
337
|
-
| JPEG
|
|
338
|
-
| WebP
|
|
339
|
-
| GIF
|
|
340
|
-
| TIFF
|
|
341
|
-
| BMP
|
|
342
|
-
| AVIF,
|
|
395
|
+
| Format | Decode | Encode |
|
|
396
|
+
| ------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
397
|
+
| PNG | 8 and 16 bit, gray/palette/truecolor with and without alpha, Adam7 | hash-chain LZ77, static and dynamic Huffman, the five filters |
|
|
398
|
+
| 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 |
|
|
400
|
+
| GIF | LZW, local and global palettes, interlace, transparency | median-cut palette with Floyd-Steinberg dithering |
|
|
401
|
+
| TIFF | strips, uncompressed / PackBits / LZW / Deflate, both byte orders | uncompressed and PackBits |
|
|
402
|
+
| BMP | uncompressed and RLE8 | uncompressed |
|
|
403
|
+
| AVIF | AV1 intra: every transform, all 13 intra modes, deblock and CDEF | fixed 8x8 partitions, no rate-distortion search |
|
|
404
|
+
| HEIF | container only; `probe` answers and decode reports a specific error | no |
|
|
343
405
|
|
|
344
406
|
JPEG carries true DCT-domain scaled decode at 1/2, 1/4 and 1/8, and region decode that Huffman-scans
|
|
345
407
|
past blocks outside the box without transforming them. PNG, GIF, TIFF and BMP row-skip and
|
|
346
408
|
column-skip.
|
|
347
409
|
|
|
410
|
+
PNG and deflate-compressed TIFF take a `compression` option, which is a third axis beside quality and
|
|
411
|
+
effort: for a lossless format the whole output is the compressor's, so quality says nothing about it.
|
|
412
|
+
|
|
413
|
+
| `compression` | What it does |
|
|
414
|
+
| ------------- | ------------------------------------------------------------------------------------------- |
|
|
415
|
+
| `auto` | Reads quality, which is what every caller got before the option existed |
|
|
416
|
+
| `none` | Entropy codes without searching for matches, which beats `fast` on filtered photograph rows |
|
|
417
|
+
| `fast` | One hash chain probe per position |
|
|
418
|
+
| `default` | A bounded chain walk with lazy matching |
|
|
419
|
+
| `best` | A long chain walk |
|
|
420
|
+
|
|
421
|
+
On a 96 px logo `best` is 664 bytes against 835 at `default`, for 4.3x the CPU.
|
|
422
|
+
|
|
348
423
|
`probe` reads headers only, for every format including the ones the library cannot decode:
|
|
349
424
|
|
|
350
425
|
```ts
|
|
@@ -390,14 +465,132 @@ TrueType and OpenType with `glyf` outlines, plus PSF and BDF bitmap faces, dispa
|
|
|
390
465
|
bytes. Quadratic flattening and a scanline fill with four vertical subsamples and exact horizontal
|
|
391
466
|
coverage.
|
|
392
467
|
|
|
468
|
+
A Latin face is compiled into the module, so drawing text needs no setup:
|
|
469
|
+
|
|
393
470
|
```ts
|
|
394
|
-
|
|
395
|
-
if (font) await tinyimg.loadBlob('font', 'sans', font.body);
|
|
471
|
+
using font = Font.load(tinyimg);
|
|
396
472
|
```
|
|
397
473
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
474
|
+
It covers ASCII and eight accented letters in 21 kB. A wider glyph set or a different face is a blob
|
|
475
|
+
under a name of your own, and a fallback chain puts one behind the other rather than replacing it:
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const noto = await fetch('https://example.com/NotoSansSC-Regular.ttf');
|
|
479
|
+
await tinyimg.loadBlob('font', 'cjk', await noto.arrayBuffer());
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`tiny_image_draw_text`, `tiny_image_draw_text_in` and `tiny_text_measure` share one layout walk, so
|
|
483
|
+
measuring and then drawing at the measured position lands where the measurement said.
|
|
484
|
+
|
|
485
|
+
### Setting a Run
|
|
486
|
+
|
|
487
|
+
`TinyTextStyle` carries the size, tracking, line height, kerning, an outline and a shadow.
|
|
488
|
+
`TinyTextBox` carries the rectangle: a wrap width, a height, horizontal alignment including justify,
|
|
489
|
+
vertical alignment, and whether text the box cannot hold is clipped or ends in an ellipsis.
|
|
490
|
+
|
|
491
|
+
A run with an outline or a shadow is drawn in three passes, every shadow then every outline then
|
|
492
|
+
every fill, so a letter's outline never prints over its neighbor's fill. The outline is a dilation of
|
|
493
|
+
the glyph's coverage rather than an offset of its curve, which rounds a sharp interior corner that a
|
|
494
|
+
true offset would extend to a point; at the one to four pixels a card uses the difference is not
|
|
495
|
+
visible.
|
|
496
|
+
|
|
497
|
+
`tiny_text_lines` reports the layout one line at a time: the byte range, the width, where the line
|
|
498
|
+
sits once aligned, its baseline, and whether the box clipped it or an ellipsis ended it. It is what
|
|
499
|
+
tells a caller a headline was truncated before the card is served.
|
|
500
|
+
|
|
501
|
+
### Mixed Styles
|
|
502
|
+
|
|
503
|
+
`tiny_image_draw_text_runs` takes up to sixteen spans, each with its own face, size, color, outline
|
|
504
|
+
and shadow, and flows them as one run: a wrap can land in the middle of a span, and a line that mixes
|
|
505
|
+
sizes sits on one baseline, which is the largest ascent on that line. Kerning does not cross a span
|
|
506
|
+
boundary, because a kern pair belongs to one face and the glyphs either side may not.
|
|
507
|
+
|
|
508
|
+
### What the Face Says
|
|
509
|
+
|
|
510
|
+
| Table | Read for |
|
|
511
|
+
| ------ | ----------------------------------------------------------------------- |
|
|
512
|
+
| `kern` | Legacy pair kerning |
|
|
513
|
+
| `GPOS` | Pair kerning under the `kern` feature, formats 1 and 2 |
|
|
514
|
+
| `GSUB` | Ligatures under the `liga` feature, longest match first |
|
|
515
|
+
| `fvar` | Variation axes, their ranges and their defaults |
|
|
516
|
+
| `avar` | Axis remapping, so the midpoint of an axis is where the designer put it |
|
|
517
|
+
| `gvar` | Outline and advance deltas, with regions and inferred points |
|
|
518
|
+
|
|
519
|
+
Modern faces ship `GPOS` and often no `kern` at all, so a reader that only knew the legacy table
|
|
520
|
+
would read most fonts as unkerned. `GPOS` is consulted first and the legacy table is the fallback.
|
|
521
|
+
|
|
522
|
+
One variable file serves every weight:
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
tiny_font_set_axis(font, TINYIMG_AXIS_WEIGHT, 700);
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
The outlines are interpolated from the face's own deltas, and so are the advances, which come from
|
|
529
|
+
the same deltas' phantom points. Points no region names are inferred from their neighbors along the
|
|
530
|
+
contour, which is what keeps an outline whole rather than torn.
|
|
531
|
+
|
|
532
|
+
### Open Graph Cards
|
|
533
|
+
|
|
534
|
+
`og` renders one:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
const card = await og(tinyimg, {
|
|
538
|
+
title: 'How a planner made the decode cheap',
|
|
539
|
+
subtitle: 'Deciding what to decode before decoding it',
|
|
540
|
+
footer: 'tinyimg.gmitch215.dev',
|
|
541
|
+
background: '#0b1020'
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
return card.response({ 'cache-control': 'public, max-age=86400' });
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
1200x630 by default, with the headline wrapped, truncated with an ellipsis when it does not fit, and
|
|
548
|
+
a shadow under the text. `image` covers the card with a photograph, fitted by the planner, so a
|
|
549
|
+
4000 px source is never decoded at 4000 px to end up 1200 wide. The result reports `titleTruncated`
|
|
550
|
+
and `missingGlyphs`, which are the two ways a card goes out wrong.
|
|
551
|
+
|
|
552
|
+
A Worker that serves cards for its own pages, cached so each one is rendered once:
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
import wasm from '@gmitch215/tinyimg/tinyimg.wasm';
|
|
556
|
+
import { og, TinyImgModule } from '@gmitch215/tinyimg';
|
|
557
|
+
|
|
558
|
+
const tinyimg = TinyImgModule.load(wasm);
|
|
559
|
+
|
|
560
|
+
export default {
|
|
561
|
+
async fetch(request: Request, env: unknown, ctx: ExecutionContext): Promise<Response> {
|
|
562
|
+
const url = new URL(request.url);
|
|
563
|
+
const cache = caches.default;
|
|
564
|
+
|
|
565
|
+
const hit = await cache.match(request);
|
|
566
|
+
if (hit) return hit;
|
|
567
|
+
|
|
568
|
+
const card = await og(tinyimg, {
|
|
569
|
+
title: url.searchParams.get('title') ?? 'Untitled',
|
|
570
|
+
subtitle: url.searchParams.get('subtitle') ?? undefined,
|
|
571
|
+
footer: url.hostname,
|
|
572
|
+
background: '#0b1020',
|
|
573
|
+
format: 'png'
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
if (card.missingGlyphs > 0) {
|
|
577
|
+
// the compiled-in face is a Latin subset; load a wider one for anything else
|
|
578
|
+
console.warn(`${card.missingGlyphs} codepoints had no glyph`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const response = card.response({
|
|
582
|
+
'cache-control': 'public, max-age=604800',
|
|
583
|
+
'x-title-truncated': String(card.titleTruncated)
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
ctx.waitUntil(cache.put(request, response.clone()));
|
|
587
|
+
|
|
588
|
+
return response;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Nothing is loaded and no subrequest is made, so the whole card is CPU and one cache write.
|
|
401
594
|
|
|
402
595
|
100 characters at 16 px cost 0.156 ms.
|
|
403
596
|
|
|
@@ -406,28 +599,23 @@ measurement said. Kern pairs are applied when the face carries a `kern` table.
|
|
|
406
599
|
An LBP cascade over an integral image, multi-scale, with union-find grouping. The cascades are
|
|
407
600
|
OpenCV's `lbpcascade_frontalface_improved.xml` and `lbpcascade_profileface.xml`.
|
|
408
601
|
|
|
409
|
-
|
|
410
|
-
that `scripts/fixtures.ts` builds from the OpenCV source: 7.4 kB for the frontal cascade against 54 kB
|
|
411
|
-
of XML. `loadBlob` rejects one still in XML. Both packed cascades are published, and
|
|
412
|
-
`bun run blobs` rebuilds them if you would rather host your own:
|
|
602
|
+
Both cascades are compiled into the module, so detection needs no setup:
|
|
413
603
|
|
|
414
604
|
```ts
|
|
415
|
-
const frontal = await fetch('https://cdn.gmitch215.dev/tinyimg/cascades/lbp-frontalface.bin');
|
|
416
|
-
const profile = await fetch('https://cdn.gmitch215.dev/tinyimg/cascades/lbp-profileface.bin');
|
|
417
|
-
|
|
418
|
-
await tinyimg.loadBlob('cascade', 'frontal', await frontal.arrayBuffer());
|
|
419
|
-
await tinyimg.loadBlob('cascade', 'profile', await profile.arrayBuffer());
|
|
420
|
-
|
|
421
605
|
const faces = await tinyimg.detectFaces(source);
|
|
422
606
|
// [{ x, y, width, height, neighbors }]
|
|
423
607
|
```
|
|
424
608
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
609
|
+
The module carries no XML parser, so a cascade is a packed form that `scripts/fixtures.ts` builds
|
|
610
|
+
from the OpenCV source: 7.4 kB for the frontal cascade against 54 kB of XML. `loadBlob` rejects one
|
|
611
|
+
still in XML, and `bun run blobs` rebuilds the packed pair if you would rather train your own.
|
|
428
612
|
|
|
429
|
-
|
|
430
|
-
|
|
613
|
+
Loading a cascade of your own replaces both builtins rather than joining them, which is what a host
|
|
614
|
+
substituting a default expects. With none loaded, the two that ship both run and their results are
|
|
615
|
+
grouped, so a frontal and a profile cascade find both kinds of face and a face that fires both is one
|
|
616
|
+
box. Detections match OpenCV's own detector within a pixel on identical pixels.
|
|
617
|
+
|
|
618
|
+
Feeds `gravity: 'face'`, which therefore works without loading anything.
|
|
431
619
|
|
|
432
620
|
## Color Management
|
|
433
621
|
|
|
@@ -474,35 +662,56 @@ against the source's header first, and hands the original bytes back when there
|
|
|
474
662
|
look like; effort says how hard to work to get there. It applies to the decode, the resize and the
|
|
475
663
|
encode, and each part gives up something measured:
|
|
476
664
|
|
|
477
|
-
| Stage | What `fast` does
|
|
478
|
-
| ------ |
|
|
479
|
-
| Decode | WebP skips deblocking
|
|
480
|
-
| Decode |
|
|
481
|
-
|
|
|
482
|
-
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
665
|
+
| Stage | What `fast` does | Speed | Quality |
|
|
666
|
+
| ------ | -------------------------------------------- | -------------- | --------------- |
|
|
667
|
+
| Decode | WebP skips deblocking | 1.53x | 46.8 dB |
|
|
668
|
+
| Decode | WebP reduces in the plane domain | 2.05x to 3.35x | 38.9 dB |
|
|
669
|
+
| Decode | JPEG replicates chroma instead of filtering | 1.11x to 1.25x | 43.6 to 59.5 dB |
|
|
670
|
+
| Resize | Bilinear instead of Catmull-Rom, enlarging | 3.2x | 45.7 dB |
|
|
671
|
+
| Encode | WebP bounds the 4x4 prediction search | 1.18x to 1.45x | within 0.05 dB |
|
|
672
|
+
| Encode | PNG compresses one candidate stream, not two | 1.59x to 2.08x | see below |
|
|
673
|
+
|
|
674
|
+
The plane-domain reduction reaches a scaled WebP request only, and its speed figure includes the
|
|
675
|
+
deblocking skip on the same request. It averages the luma and chroma planes over each output pixel
|
|
676
|
+
and converts once, where the default converts the frame and averages the result, so it also never
|
|
677
|
+
allocates the full frame of RGBA.
|
|
678
|
+
|
|
679
|
+
The PNG row is the one whose cost depends on what you are encoding. The encoder compresses an
|
|
680
|
+
adaptively filtered stream and an unfiltered one and keeps the smaller; `fast` compresses only the
|
|
681
|
+
adaptive one. On a photograph the adaptive stream wins anyway, so the output is byte-identical and
|
|
682
|
+
the speed is free: `sf-24.jpg` at 800 px measured 142 ms against 68. On flat artwork the unfiltered
|
|
683
|
+
stream is the one that would have won, so `fast` pays 7.0% more bytes on `base-mono.gif`, 17.2% on
|
|
684
|
+
`webassembly.png` and 63.1% on a 96x96 logo. Resizing a photograph gets the speed for nothing;
|
|
685
|
+
encoding an icon should stay at `fancy`.
|
|
686
|
+
|
|
687
|
+
**A lossy decoder has more to trade than a lossless one.** A lossless bitstream defines its pixels
|
|
688
|
+
exactly, so every step is needed to produce them and PNG, GIF, TIFF and lossless WebP decode
|
|
689
|
+
identically at either effort. So does a 4:4:4 JPEG, which has no chroma to upsample. A reduction
|
|
690
|
+
keeps its filter too, because an area average is already both the cheapest and the right answer;
|
|
691
|
+
only enlarging has a filter to step down. What the lossless formats do have is an encoder with a
|
|
692
|
+
search to bound, which is the PNG row above.
|
|
489
693
|
|
|
490
694
|
A filter you name is never substituted, and neither is a format. Effort decides what the library
|
|
491
695
|
left open, not what you asked for.
|
|
492
696
|
|
|
493
|
-
End to end, on requests through the whole pipeline
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
|
497
|
-
|
|
|
498
|
-
|
|
|
499
|
-
| 800 px, JPEG source,
|
|
500
|
-
| 800 px,
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
697
|
+
End to end, on requests through the whole pipeline. Fastest of 25 interleaved runs, so contention
|
|
698
|
+
moves a row one way only:
|
|
699
|
+
|
|
700
|
+
| Request | `fancy` | `fast` | |
|
|
701
|
+
| ----------------------------- | ------: | -----: | --------- |
|
|
702
|
+
| 200 px, WebP source, WebP out | 15 ms | 7.6 ms | 1.9x |
|
|
703
|
+
| 800 px, JPEG source, PNG out | 103 ms | 57 ms | 1.8x |
|
|
704
|
+
| 800 px, WebP source, WebP out | 44 ms | 36 ms | 1.3x |
|
|
705
|
+
| 800 px, JPEG source, WebP out | 28 ms | 22 ms | 1.1x-1.4x |
|
|
706
|
+
| 800 px, JPEG source, JPEG out | 17 ms | 17 ms | no lever |
|
|
707
|
+
| 800 px, JPEG source, AVIF out | 53 ms | 53 ms | no lever |
|
|
708
|
+
|
|
709
|
+
A WebP source gains most at 200 px, where both the decode and the encode have something to give and
|
|
710
|
+
the decode additionally reduces in the plane domain. The two bottom rows produce byte-identical
|
|
711
|
+
output at either effort, which is how you can tell there is nothing to spend rather than something
|
|
712
|
+
being spent badly: a 4:4:4 JPEG re-encoded as JPEG has no deblocking pass, no chroma to replicate
|
|
713
|
+
and no prediction search, and the AVIF encoder does not read the effort field at all. Timings on
|
|
714
|
+
those rows differ between runs by a few percent in either direction, which is the machine.
|
|
506
715
|
|
|
507
716
|
Encoded size can go either way under `fast`, because the per-subblock choice minimizes prediction
|
|
508
717
|
error rather than rate: a diagonal mode can win on error and then cost more bits to code. Flat-color
|
|
@@ -527,6 +736,37 @@ A format you name is never substituted, however far over budget it is. The estim
|
|
|
527
736
|
about 20% on the machine its rates were measured on, so give a budget below the limit rather than at
|
|
528
737
|
it, and use [`measure`](#work-counters) when you need what a request cost.
|
|
529
738
|
|
|
739
|
+
**A budget the container change cannot meet degrades the plan, and the result says what it gave up.**
|
|
740
|
+
Container first, because a cheaper encoder costs bytes and nothing else; then the planner's own
|
|
741
|
+
levers, in the order of what they cost the picture:
|
|
742
|
+
|
|
743
|
+
| Reported as | What it gives up |
|
|
744
|
+
| ------------- | -------------------------------------------------------------------------------------- |
|
|
745
|
+
| `compression` | A lossless output's compressor is turned down, which costs bytes and not fidelity |
|
|
746
|
+
| `effort` | The decode runs fast, and an enlargement left open steps down a filter |
|
|
747
|
+
| `filter` | A filter left open goes to nearest |
|
|
748
|
+
| `scale` | The decoder is asked for a coarser grid than the output needs, so the result is softer |
|
|
749
|
+
|
|
750
|
+
`compression` comes first because on a PNG or TIFF output the encoder costs more than everything the
|
|
751
|
+
planner controls, so softening the picture to pay for the compressor would be the wrong trade. It
|
|
752
|
+
measured 2.8x to 3.7x on a budgeted PNG request. Naming `compression` pins the level and the planner
|
|
753
|
+
degrades instead.
|
|
754
|
+
|
|
755
|
+
```ts
|
|
756
|
+
const result = await transform(tinyimg, source, { width: 600, budgetMs: 4 });
|
|
757
|
+
|
|
758
|
+
result.degraded; // ['effort'] when the effort tier was enough to fit
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
The order is not the intuitive one, and a measurement decided it: reducing the decode before the
|
|
762
|
+
filter turns a cheap box reduction into an interpolating enlargement, which on one 600 px request
|
|
763
|
+
measured 22,780 microseconds against 21,801 for no degradation at all. A rung that is not cheaper is
|
|
764
|
+
skipped, so what comes back is never softer for more CPU than the undegraded plan.
|
|
765
|
+
|
|
766
|
+
The output extent never changes. A budget decides how the pixels are made, not how many, and nothing
|
|
767
|
+
is degraded when the estimate already fits. `degraded` is empty on a pass-through, which needs no
|
|
768
|
+
lever at all.
|
|
769
|
+
|
|
530
770
|
## Caching
|
|
531
771
|
|
|
532
772
|
A cache hit costs no CPU at all, which is how a request too expensive to serve synchronously still
|
|
@@ -566,9 +806,17 @@ averaging them away reads 64 whatever its name is.
|
|
|
566
806
|
|
|
567
807
|
## Blobs
|
|
568
808
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
809
|
+
A default of each kind is compiled into the module: a Latin face, the four ICC profiles and both LBP
|
|
810
|
+
cascades, about 45 kB. Text, profile conversion and face detection therefore work off a bare
|
|
811
|
+
instantiate, and `builtinBlobs` lists what is there.
|
|
812
|
+
|
|
813
|
+
A blob is how you replace one or add to it: a wider glyph set, another cascade, a profile of your
|
|
814
|
+
own. `loadBlob` takes bytes from anywhere, so there are three ways to deliver them and no code change
|
|
815
|
+
between them.
|
|
816
|
+
|
|
817
|
+
A name resolves against the loaded blobs first and then the builtins, so `sans` always answers. An
|
|
818
|
+
enumeration is different: one loaded blob of a kind hides every builtin of that kind, which is what
|
|
819
|
+
stops a host that installed its own cascade from also running the two that ship.
|
|
572
820
|
|
|
573
821
|
The prebuilt set is published at `https://cdn.gmitch215.dev/tinyimg/`, with
|
|
574
822
|
`https://cdn.gmitch215.xyz/tinyimg/` and `https://cdn.gmitch215.blog/tinyimg/` as mirrors.
|
|
@@ -601,8 +849,11 @@ import cascade from './lbp-frontalface.bin';
|
|
|
601
849
|
await tinyimg.loadBlob('cascade', 'frontal', cascade);
|
|
602
850
|
```
|
|
603
851
|
|
|
604
|
-
Eight blobs can be resident at once. `freeBlob` releases one and `freeBlobs` releases all
|
|
605
|
-
|
|
852
|
+
Eight blobs can be resident at once. `freeBlob` releases one and `freeBlobs` releases all; neither
|
|
853
|
+
touches a builtin, which lives in the module's data section. A cascade is parsed at load, so a bad
|
|
854
|
+
one fails at startup instead of becoming a search that finds nothing.
|
|
855
|
+
|
|
856
|
+
The face and the cascades are third-party data. Their notices are in `LICENSES/`.
|
|
606
857
|
|
|
607
858
|
## Error Handling
|
|
608
859
|
|
|
@@ -635,21 +886,24 @@ try {
|
|
|
635
886
|
|
|
636
887
|
## Size
|
|
637
888
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
breakdown.
|
|
889
|
+
The wasm module is 778 kB, of which 45 kB is the data it carries: a Latin face, four ICC profiles and
|
|
890
|
+
two detection cascades. The AV1 codec behind AVIF is 283 kB of it, about half of that in generated
|
|
891
|
+
tables. The wrapper is 52 kB of JavaScript. `bun run size` prints the breakdown per translation unit.
|
|
641
892
|
|
|
642
|
-
|
|
643
|
-
|
|
893
|
+
Speed comes first, then size, then memory. Size still gets measured, because a bigger module costs
|
|
894
|
+
startup time and `tiny` is in the name, but it no longer wins an argument against speed.
|
|
895
|
+
|
|
896
|
+
Every codec can be compiled out, so a caller who only handles PNG can link a module with nothing else
|
|
897
|
+
in it. `--gc-sections` then drops the implementation along with its registry entry.
|
|
644
898
|
|
|
645
899
|
```sh
|
|
646
900
|
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=cmake/wasm32.cmake \
|
|
647
|
-
-DCMAKE_C_FLAGS="-DTINYIMG_NO_WEBP -DTINYIMG_NO_TIFF -DTINYIMG_NO_GIF"
|
|
901
|
+
-DCMAKE_C_FLAGS="-DTINYIMG_NO_WEBP -DTINYIMG_NO_TIFF -DTINYIMG_NO_GIF -DTINYIMG_NO_AVIF"
|
|
648
902
|
cmake --build build
|
|
649
903
|
```
|
|
650
904
|
|
|
651
|
-
That build is
|
|
652
|
-
|
|
905
|
+
That build is 366 kB, and `tinyimg.features` reports the four as absent, so a caller can check
|
|
906
|
+
before offering a format rather than calling and handling a failure.
|
|
653
907
|
|
|
654
908
|
Build it into its own directory and `bin/tinyimg.wasm` is still where it lands, because that path is
|
|
655
909
|
absolute in `CMakeLists.txt`. Run `bun run build:wasm` afterwards to put the full module back.
|
|
@@ -661,7 +915,11 @@ absolute in `CMakeLists.txt`. Run `bun run build:wasm` afterwards to put the ful
|
|
|
661
915
|
browser and throws on Workers.
|
|
662
916
|
- **The Workers Free plan allows 10 ms of CPU per request**, which is not configurable and is below
|
|
663
917
|
the cost of most transformations; see [Speed](#speed). Workers Paid defaults to 30 seconds and goes
|
|
664
|
-
to 5 minutes with `limits.cpu_ms`. A Worker over the limit returns error 1102.
|
|
918
|
+
to 5 minutes with `limits.cpu_ms`. A Worker over the limit returns error 1102. Enforcement is
|
|
919
|
+
elastic, so an isolated request over the limit may still complete; sustained ones do not, and a
|
|
920
|
+
single test request that passed is not evidence that a request fits.
|
|
921
|
+
- **Response body bytes are not billed as CPU.** A 2.88 MB body measured 99 ms of wall clock and zero
|
|
922
|
+
CPU, so the limit applies to the transformation and not to serving it.
|
|
665
923
|
- **32 MiB per decoded image**, inside 64 MiB of linear memory, with a 16 megapixel ceiling on top
|
|
666
924
|
that only grayscale reaches. The byte cap is the one that binds, because an allocation is bytes: it
|
|
667
925
|
allows 11.2 megapixels of RGB and 8.4 of RGBA. Past either, the library reports
|
|
@@ -684,15 +942,17 @@ absolute in `CMakeLists.txt`. Run `bun run build:wasm` afterwards to put the ful
|
|
|
684
942
|
found at a moderate reduction and lost at full resolution, and a side-facing one can be the
|
|
685
943
|
reverse. `detectFaces` picks a default that suits frontal faces; the C entry point
|
|
686
944
|
`tiny_image_detect_faces_ex` searches exactly what it is told.
|
|
687
|
-
-
|
|
688
|
-
|
|
945
|
+
- **No hand-written SIMD kernels.** The flag is on and the autovectorizer runs, which is worth
|
|
946
|
+
1.04x to 1.15x now that the build is `-O2` rather than `-Oz`. Nothing uses intrinsics directly, so
|
|
947
|
+
the widest stages are still scalar.
|
|
689
948
|
|
|
690
949
|
## Out of Scope
|
|
691
950
|
|
|
692
|
-
- **
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
951
|
+
- **HEIF decode and encode.** HEIF shares AVIF's container and carries HEVC rather than AV1. HEVC
|
|
952
|
+
decoders are covered by active patent pools, which is a licensing question rather than a technical
|
|
953
|
+
one and the reason this is declined rather than queued. The container parse ships so `probe`
|
|
954
|
+
answers, and Safari is the only browser that displays HEIF, so it matters as a camera source
|
|
955
|
+
format rather than an output one.
|
|
696
956
|
- **JPEG XL.** No browser ships it by default.
|
|
697
957
|
- **Animation editing.** Composing or re-timing frames is a different product.
|
|
698
958
|
- **LUT-based ICC profiles.** Rejected with a specific error rather than approximated.
|
|
@@ -717,4 +977,10 @@ bun run test
|
|
|
717
977
|
|
|
718
978
|
## License
|
|
719
979
|
|
|
720
|
-
MIT
|
|
980
|
+
MIT.
|
|
981
|
+
|
|
982
|
+
The module compiles in two third-party works, and their notices ship with the package in
|
|
983
|
+
`LICENSES/`. The Latin face is a subset of DejaVu Sans, under the Bitstream Vera Fonts license
|
|
984
|
+
(`LICENSES/DejaVu.txt`). The two face cascades are packed from OpenCV's LBP data files, under the
|
|
985
|
+
3-clause BSD license (`LICENSES/OpenCV-LBP-Cascades.txt`). The four ICC profiles are generated from
|
|
986
|
+
published primaries and transfer curves rather than copied from anyone's files.
|