wtfgif 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/BENCHMARKS.md +185 -0
- package/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/compiled.d.ts +77 -0
- package/dist/constants/gif.d.ts +17 -0
- package/dist/decoder/pool.d.ts +2 -0
- package/dist/decoder/reader.d.ts +80 -0
- package/dist/encoder/writer.d.ts +59 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/native/oneOff.d.ts +1 -0
- package/dist/native/runtime.d.ts +27 -0
- package/dist/types.d.ts +171 -0
- package/dist/utils/netscape.d.ts +16 -0
- package/dist/utils/palette.d.ts +4 -0
- package/dist/utils/subblocks.d.ts +4 -0
- package/dist/wasm/coreBackend.d.ts +2 -0
- package/dist/wasm/runtime.d.ts +24 -0
- package/dist/wasm-core/package.json +3 -0
- package/dist/wasm-core/wtfgif_core.d.ts +81 -0
- package/dist/wasm-core/wtfgif_core.js +1326 -0
- package/dist/wasm-core/wtfgif_core_bg.wasm +0 -0
- package/dist/wasm-core/wtfgif_core_bg.wasm.d.ts +51 -0
- package/dist/wasm-web/package.json +3 -0
- package/dist/wasm-web/wtfgif_core.d.ts +157 -0
- package/dist/wasm-web/wtfgif_core.js +1392 -0
- package/dist/wasm-web/wtfgif_core_bg.wasm +0 -0
- package/dist/wasm-web/wtfgif_core_bg.wasm.d.ts +51 -0
- package/package.json +125 -0
package/BENCHMARKS.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
Performance claims are only meaningful when the backend, cache state, fixtures,
|
|
4
|
+
and correctness checks are explicit.
|
|
5
|
+
|
|
6
|
+
## MakeEmoji pixel-perfect timing export
|
|
7
|
+
|
|
8
|
+
This reproduces MakeEmoji's current export after its preview frames have been
|
|
9
|
+
decoded: each RGBA frame is quantized with MakeEmoji's `image-q` settings and
|
|
10
|
+
written with `omggif`. It compares that path with a one-shot wtfgif timing edit
|
|
11
|
+
and with a GIF compiled while the upload loads.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
BENCH_ITERATIONS=5 BENCH_WARMUP_ITERATIONS=2 \
|
|
15
|
+
npm run bench:makeemoji-retime
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Five measured samples per implementation and fixture, with two warmups:
|
|
19
|
+
|
|
20
|
+
| Fixture | One-shot parse + retime | Page-load compiled retime |
|
|
21
|
+
| --- | ---: | ---: |
|
|
22
|
+
| 18d | 1,110.2x | 4,225.9x |
|
|
23
|
+
| Clap | 321.6x | 951.5x |
|
|
24
|
+
| Homer | 9,418.9x | 29,954.7x |
|
|
25
|
+
| Chipmunk | 6,706.7x | 27,473.8x |
|
|
26
|
+
| GIGACHAD | 5,386.7x | 12,998.1x |
|
|
27
|
+
| NODDERS | 1,414.5x | 3,158.2x |
|
|
28
|
+
| Proud | 5,005.4x | 32,190.9x |
|
|
29
|
+
| catJAM | 3,953.6x | 6,151.6x |
|
|
30
|
+
| excuseme | 4,922.4x | 10,105.7x |
|
|
31
|
+
| party_blob | 7,268.6x | 16,010.2x |
|
|
32
|
+
| partyparrot | 600.4x | 1,643.9x |
|
|
33
|
+
| tenor | 26,241.3x | 72,672.1x |
|
|
34
|
+
| **Geometric mean** | **3,338.5x** | **9,469.6x** |
|
|
35
|
+
|
|
36
|
+
Every wtfgif output had the requested delays and exact composited RGBA pixels.
|
|
37
|
+
The unit suite separately verifies that original compressed frame data is
|
|
38
|
+
retained. MakeEmoji's current quantize-and-reencode output was pixel-perfect on
|
|
39
|
+
only 2 of 12 fixtures.
|
|
40
|
+
|
|
41
|
+
This result comes from doing less work, not from a faster quantizer: timing
|
|
42
|
+
changes do not require decoding or recompressing the image stream.
|
|
43
|
+
|
|
44
|
+
The page-load compiled column is the intended web path and the >100x contract.
|
|
45
|
+
Cold one-shot results on tiny GIFs are more sensitive to JavaScript startup,
|
|
46
|
+
JIT, and microsecond timer noise; a separate three-sample filtered run observed
|
|
47
|
+
a 59.3x one-shot outlier while the compiled edit remained 782.5x.
|
|
48
|
+
|
|
49
|
+
The same benchmark exercises strict reverse and MakeEmoji-style boomerang on
|
|
50
|
+
fixtures whose full-canvas/disposal structure proves that frame order is
|
|
51
|
+
independent:
|
|
52
|
+
|
|
53
|
+
| Operation | Slowest accepted fixture | Geometric mean |
|
|
54
|
+
| --- | ---: | ---: |
|
|
55
|
+
| Reverse | 5,329.5x | 18,054.3x |
|
|
56
|
+
| Boomerang | 6,866.7x | 21,720.1x |
|
|
57
|
+
|
|
58
|
+
The structural path compares each output frame with its mapped source frame,
|
|
59
|
+
including exact composited RGBA and delay values. Fixtures that cannot be
|
|
60
|
+
proven independent are rejected and excluded, rather than silently rewritten.
|
|
61
|
+
|
|
62
|
+
## Experimental cold pixel-perfect reencoding
|
|
63
|
+
|
|
64
|
+
The current research checkpoint starts a fresh process for each sample and
|
|
65
|
+
reencodes every GIF image descriptor with the experimental native addon. The
|
|
66
|
+
timer excludes the decoded-pixel verification that follows each sample. Every
|
|
67
|
+
output and source GIF is then decoded and compared; any pixel mismatch aborts
|
|
68
|
+
the run.
|
|
69
|
+
|
|
70
|
+
Command:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
BENCH_ITERATIONS=31 BENCH_REENCODE_ONLY=1 npm run bench
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
31 fresh-process samples per implementation and fixture:
|
|
77
|
+
|
|
78
|
+
| Fixture | wtfgif faster than omggif |
|
|
79
|
+
| --- | ---: |
|
|
80
|
+
| 18d | 142.65x |
|
|
81
|
+
| Clap | 100.97x |
|
|
82
|
+
| Homer | 191.85x |
|
|
83
|
+
| Chip | 200.35x |
|
|
84
|
+
| GIG | 180.09x |
|
|
85
|
+
| NOD | 111.37x |
|
|
86
|
+
| Proud | 229.79x |
|
|
87
|
+
| cat | 180.53x |
|
|
88
|
+
| excuse | 117.74x |
|
|
89
|
+
| blob | 126.32x |
|
|
90
|
+
| parrot | 149.29x |
|
|
91
|
+
| tenor | 214.14x |
|
|
92
|
+
| Geometric mean | 156.69x |
|
|
93
|
+
|
|
94
|
+
These are experimental-addon results on the development machine, not a promise
|
|
95
|
+
for the portable npm package or other hardware. The 1.0.0 npm artifact ships
|
|
96
|
+
JavaScript and WebAssembly; it does not ship a prebuilt native addon.
|
|
97
|
+
|
|
98
|
+
## Page-load-prepared portable pixel-perfect remux
|
|
99
|
+
|
|
100
|
+
This is the web product benchmark. Wasm initialization and generic hot-path
|
|
101
|
+
preparation happen before the clock, as they would during page load. Each timed
|
|
102
|
+
sample is still the first real GIF processed in a fresh process. Preparation
|
|
103
|
+
uses unrelated generated GIFs and cannot cache a fixture's input or output.
|
|
104
|
+
|
|
105
|
+
wtfgif validates and rewrites the GIF container while preserving original LZW
|
|
106
|
+
frame payloads. omggif decodes and freshly recompresses the frames. The
|
|
107
|
+
externally tested contract is the same—an owned GIF with identical decoded
|
|
108
|
+
pixels—but the algorithms intentionally differ.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
BENCH_ITERATIONS=31 BENCH_REENCODE_ONLY=1 \
|
|
112
|
+
WTFGIF_BENCH_BACKEND=wasm \
|
|
113
|
+
WTFGIF_PREPARE_WASM_AT_PAGE_LOAD=1 \
|
|
114
|
+
WTFGIF_REENCODE_MODE=remux npm run bench
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Fixture | wtfgif faster than omggif |
|
|
118
|
+
| --- | ---: |
|
|
119
|
+
| 18d | 738.64x |
|
|
120
|
+
| Clap | 309.84x |
|
|
121
|
+
| Homer | 983.53x |
|
|
122
|
+
| Chip | 768.77x |
|
|
123
|
+
| GIG | 607.23x |
|
|
124
|
+
| NOD | 512.23x |
|
|
125
|
+
| Proud | 681.13x |
|
|
126
|
+
| cat | 519.37x |
|
|
127
|
+
| excuse | 480.06x |
|
|
128
|
+
| blob | 955.88x |
|
|
129
|
+
| parrot | 244.09x |
|
|
130
|
+
| tenor | 1,238.09x |
|
|
131
|
+
| Geometric mean | 609.73x |
|
|
132
|
+
|
|
133
|
+
Every fixture clears 100x. The slowest is partyparrot at 244.09x.
|
|
134
|
+
|
|
135
|
+
The excluded initialization plus generic preparation cost was measured
|
|
136
|
+
separately across 31 fresh processes on the same machine: 8.64 ms median,
|
|
137
|
+
9.86 ms p95, and 9.91 ms maximum.
|
|
138
|
+
|
|
139
|
+
## Cold portable Rust/WebAssembly reencoding
|
|
140
|
+
|
|
141
|
+
The same pixel-perfect Rust reencoder now runs in browsers, Cloudflare Workers,
|
|
142
|
+
Vercel Edge, and Node through WebAssembly.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
BENCH_ITERATIONS=31 BENCH_REENCODE_ONLY=1 \
|
|
146
|
+
WTFGIF_BENCH_BACKEND=wasm npm run bench
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Fixture | wtfgif faster than omggif |
|
|
150
|
+
| --- | ---: |
|
|
151
|
+
| 18d | 8.19x |
|
|
152
|
+
| Clap | 1.23x |
|
|
153
|
+
| Homer | 14.68x |
|
|
154
|
+
| Chip | 17.70x |
|
|
155
|
+
| GIG | 15.44x |
|
|
156
|
+
| NOD | 8.60x |
|
|
157
|
+
| Proud | 20.00x |
|
|
158
|
+
| cat | 14.42x |
|
|
159
|
+
| excuse | 9.60x |
|
|
160
|
+
| blob | 9.74x |
|
|
161
|
+
| parrot | 3.38x |
|
|
162
|
+
| tenor | 16.00x |
|
|
163
|
+
| Geometric mean | 9.45x |
|
|
164
|
+
|
|
165
|
+
These are first-call numbers in 31 fresh Node processes per implementation and
|
|
166
|
+
fixture. They include Wasm's first-call cost and use no warmup. Browser and edge
|
|
167
|
+
support is separately exercised by real-browser, Cloudflare workerd, and Vercel
|
|
168
|
+
Edge VM smoke tests.
|
|
169
|
+
|
|
170
|
+
Page-load preparation improves fresh LZW recompression, but it does not remove
|
|
171
|
+
the actual LZW decode and encode work. Profiling found decoding responsible for
|
|
172
|
+
roughly 57–85% of its hot runtime and literal reencoding for roughly 15–43%.
|
|
173
|
+
The remux API avoids both stages by preserving the original compressed payload.
|
|
174
|
+
|
|
175
|
+
## Correctness gates
|
|
176
|
+
|
|
177
|
+
- Source and reencoded decoded pixels must match exactly for every timed sample.
|
|
178
|
+
- Source and remuxed decoded pixels must match exactly for every timed sample.
|
|
179
|
+
- A randomized parity stress run covered 200 generated GIFs across 4, 16, 64,
|
|
180
|
+
128, and 256 colors, global palettes, and identical local palettes.
|
|
181
|
+
- Unit tests compare reader and writer behavior with omggif across the fixture
|
|
182
|
+
corpus.
|
|
183
|
+
|
|
184
|
+
Record new results here only with the exact command, sample count, environment,
|
|
185
|
+
backend, and correctness gate.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to wtfgif are documented here.
|
|
4
|
+
|
|
5
|
+
## 1.2.0 - 2026-07-19
|
|
6
|
+
|
|
7
|
+
- Add `compileGif()`, an immutable compiled-GIF representation that retains
|
|
8
|
+
original palettes, frame rectangles, extensions, and compressed LZW data.
|
|
9
|
+
- Add `CompiledGif.withDelays()` and `retimeGifPixelPerfect()` for timing edits
|
|
10
|
+
that patch metadata without decoding, quantizing, or recompressing pixels.
|
|
11
|
+
- Add `CompiledGif.withLoop()` with Netscape and ANIMEXTS loop support.
|
|
12
|
+
- Add strict `reverseFrames()` and MakeEmoji-compatible `boomerangFrames()`
|
|
13
|
+
linker paths for structurally independent full-canvas animations.
|
|
14
|
+
- Preserve exact pixels and compressed frame payloads across the real fixture
|
|
15
|
+
corpus, including local palettes, interlacing, transparency, disposal,
|
|
16
|
+
comments, and application extensions.
|
|
17
|
+
- Add a MakeEmoji-shaped timing export benchmark. The page-load-compiled path
|
|
18
|
+
measured at least 951x faster on every fixture.
|
|
19
|
+
- Benchmark proven-safe reverse at least 5,329x faster and boomerang at least
|
|
20
|
+
6,866x faster while checking exact mapped pixels and delays.
|
|
21
|
+
|
|
22
|
+
## 1.1.0 - 2026-07-19
|
|
23
|
+
|
|
24
|
+
- Run the pixel-perfect one-shot Rust reencoder and all-frame RGBA decoder
|
|
25
|
+
through WebAssembly in browsers and edge runtimes.
|
|
26
|
+
- Add static Wasm initialization for Cloudflare Workers and Vercel Edge.
|
|
27
|
+
- Add `remuxGifPixelPerfect()`, a portable lossless Rust/Wasm path that
|
|
28
|
+
preserves original compressed frame payloads and exceeds 100x on every
|
|
29
|
+
page-load-prepared benchmark fixture.
|
|
30
|
+
- Prepare generic Wasm and public API hot paths during explicit page-load
|
|
31
|
+
initialization.
|
|
32
|
+
- Add explicit `rust` and `wasm` encoder backend choices plus fast-backend
|
|
33
|
+
status reporting.
|
|
34
|
+
- Validate real reencoding and decoded-pixel parity in the packaged browser
|
|
35
|
+
build, Cloudflare workerd, Vercel Edge VM, and the full GIF corpus.
|
|
36
|
+
- Add an interactive GitHub Pages race across all 12 benchmark fixtures with
|
|
37
|
+
live browser timing and post-timer full RGBA parity verification.
|
|
38
|
+
|
|
39
|
+
## 1.0.0 - 2026-07-19
|
|
40
|
+
|
|
41
|
+
First public release.
|
|
42
|
+
|
|
43
|
+
### Highlights
|
|
44
|
+
|
|
45
|
+
- Drop-in `GifReader` and `GifWriter` APIs for core omggif usage, including
|
|
46
|
+
plain arrays, typed arrays, Node buffers, CommonJS, ESM, and TypeScript.
|
|
47
|
+
- Portable JavaScript fallback plus packaged WebAssembly builds for Node and
|
|
48
|
+
browsers.
|
|
49
|
+
- Higher-level helpers for indexed and RGBA frame encoding, composited frame
|
|
50
|
+
preparation, delta frames, and configurable compression.
|
|
51
|
+
- Pixel-parity tests against omggif and installed-tarball validation across
|
|
52
|
+
CommonJS, ESM, browser exports, and WebAssembly loading.
|
|
53
|
+
- Experimental injectable Node addon for one-off decode and pixel-perfect
|
|
54
|
+
reencoding research. Prebuilt native binaries are not part of 1.0.0.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Bug reports and focused pull requests are welcome.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
wtfgif requires Node 20.16 or newer and the stable Rust toolchain. Install
|
|
8
|
+
`wasm-pack`, then install dependencies:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cargo install wasm-pack --locked
|
|
12
|
+
npm ci
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Before opening a pull request
|
|
16
|
+
|
|
17
|
+
Run the complete release gate:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run check
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Changes to decoding or encoding must include a regression test. Compatibility
|
|
24
|
+
changes should compare behavior with omggif when applicable. Performance
|
|
25
|
+
changes should preserve decoded pixels and report the command, fixture set, and
|
|
26
|
+
whether the measurement used a warm process, cold process, WebAssembly, or the
|
|
27
|
+
experimental native addon.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Matt Popovich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# wtfgif
|
|
2
|
+
|
|
3
|
+
An `omggif`-compatible GIF reader and writer, plus pixel-perfect fast paths
|
|
4
|
+
that avoid rebuilding pixels when the requested edit does not require it.
|
|
5
|
+
|
|
6
|
+
**[Run the browser benchmark →](https://mpopv.github.io/wtfgif/)**
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install wtfgif
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Replace omggif
|
|
13
|
+
|
|
14
|
+
For the core `GifReader` and `GifWriter` API, change the import:
|
|
15
|
+
|
|
16
|
+
```diff
|
|
17
|
+
- import { GifReader, GifWriter } from "omggif";
|
|
18
|
+
+ import { GifReader, GifWriter } from "wtfgif";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
CommonJS and TypeScript are supported:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const { GifReader, GifWriter } = require("wtfgif");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This is the compatibility path. It accepts the same indexed pixels, palettes,
|
|
28
|
+
frame rectangles, delays, transparency, and disposal options used by omggif.
|
|
29
|
+
It does not promise a 100x speedup for arbitrary new pixels.
|
|
30
|
+
|
|
31
|
+
## The 100x fast paths
|
|
32
|
+
|
|
33
|
+
wtfgif is fastest when pixels are already valid GIF data and the edit can be
|
|
34
|
+
expressed as a container change:
|
|
35
|
+
|
|
36
|
+
| Operation | Fixtures | Slowest speedup | Correctness |
|
|
37
|
+
| --- | ---: | ---: | --- |
|
|
38
|
+
| Compiled timing edit | 12/12 | **951.5x** | Exact pixels, delays, and original LZW payloads |
|
|
39
|
+
| Prepared Wasm remux | 12/12 | **244.09x** | Exact decoded animation |
|
|
40
|
+
| Strict frame reverse | 2/12 eligible | **5,329.5x** | Exact mapped pixels and delays |
|
|
41
|
+
| Strict boomerang | 2/12 eligible | **6,866.7x** | Exact mapped pixels and delays |
|
|
42
|
+
|
|
43
|
+
These are not claims that every wtfgif call is 100x faster. They are results
|
|
44
|
+
for the named operations across the accepted benchmark fixtures. Timing,
|
|
45
|
+
reverse, and boomerang are compared with MakeEmoji's `image-q` + `omggif`
|
|
46
|
+
export; remux is compared with `omggif` decode + fresh encode. See
|
|
47
|
+
[BENCHMARKS.md](BENCHMARKS.md) for commands, fixtures, sample counts, cold
|
|
48
|
+
results, and correctness gates.
|
|
49
|
+
|
|
50
|
+
## Edit timing without touching pixels
|
|
51
|
+
|
|
52
|
+
Compile once when a GIF is loaded:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { compileGif } from "wtfgif";
|
|
56
|
+
|
|
57
|
+
const gif = compileGif(inputGif);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Export new frame delays later. GIF delays are measured in centiseconds:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const outputGif = gif
|
|
64
|
+
.withDelays(Uint16Array.of(4, 8, 12, 8))
|
|
65
|
+
.toUint8Array();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`compileGif()` retains the original GIF structure and compressed image data.
|
|
69
|
+
`withDelays()` changes only timing metadata, inserting a graphics control
|
|
70
|
+
extension when one is missing. It never decodes, quantizes, or recompresses a
|
|
71
|
+
frame.
|
|
72
|
+
|
|
73
|
+
For a one-shot timing edit:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { retimeGifPixelPerfect } from "wtfgif";
|
|
77
|
+
|
|
78
|
+
const outputGif = retimeGifPixelPerfect(inputGif, delaysInCentiseconds);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
These timing APIs are synchronous JavaScript. They need no Wasm initialization
|
|
82
|
+
and run in Node, browsers, Web Workers, Cloudflare Workers, and Vercel Edge.
|
|
83
|
+
|
|
84
|
+
## Reverse or boomerang proven-independent frames
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const gif = compileGif(inputGif);
|
|
88
|
+
|
|
89
|
+
if (gif.canReorderFrames) {
|
|
90
|
+
const reversed = gif.reverseFrames();
|
|
91
|
+
const boomerang = gif.boomerangFrames();
|
|
92
|
+
// A, B, C becomes A, B, C, C, B, A
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This path reorders complete compressed frame records. It deliberately rejects
|
|
97
|
+
GIFs whose appearance can depend on the preceding frame, including unsafe
|
|
98
|
+
partial-frame composition or metadata anchored between frames. A rejection is
|
|
99
|
+
a request to use a real decode-and-render pipeline, not a malformed output.
|
|
100
|
+
|
|
101
|
+
## Remux with Wasm
|
|
102
|
+
|
|
103
|
+
Initialize Wasm once per JavaScript realm during page or worker startup:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import {
|
|
107
|
+
initializeWasmGlobally,
|
|
108
|
+
remuxGifPixelPerfect,
|
|
109
|
+
} from "wtfgif";
|
|
110
|
+
|
|
111
|
+
await initializeWasmGlobally();
|
|
112
|
+
|
|
113
|
+
const outputGif = remuxGifPixelPerfect(inputGif);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`remuxGifPixelPerfect()` validates and rewrites the GIF container while
|
|
117
|
+
preserving palettes and compressed frame payloads. It guarantees the displayed
|
|
118
|
+
animation: dimensions, frames, timing, loop behavior, disposal, transparency,
|
|
119
|
+
and decoded pixels. Non-rendering extension metadata such as comments is not
|
|
120
|
+
part of its contract.
|
|
121
|
+
|
|
122
|
+
Cloudflare Workers and Vercel Edge require static Wasm imports:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import {
|
|
126
|
+
initializeWasmModule,
|
|
127
|
+
remuxGifPixelPerfect,
|
|
128
|
+
} from "wtfgif";
|
|
129
|
+
import init, * as rust from "wtfgif/wasm-web";
|
|
130
|
+
import wasm from "wtfgif/wasm-web/wasm";
|
|
131
|
+
|
|
132
|
+
await initializeWasmModule({ ...rust, default: init }, wasm);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Initialization is not global across a website: the window and each Worker,
|
|
136
|
+
Node process, or edge isolate are separate realms and must initialize their
|
|
137
|
+
own module.
|
|
138
|
+
|
|
139
|
+
## What is not 100x
|
|
140
|
+
|
|
141
|
+
- `GifReader` and `GifWriter` are drop-in compatibility APIs, not universal
|
|
142
|
+
100x APIs.
|
|
143
|
+
- Encoding arbitrary new pixels still requires palette selection and LZW
|
|
144
|
+
compression. Wasm initialization cannot make that work disappear.
|
|
145
|
+
- `reencodeGifPixelPerfect()` performs fresh LZW encoding and is substantially
|
|
146
|
+
slower than remuxing.
|
|
147
|
+
- Cold startup is excluded from the prepared-Wasm and compiled-edit results.
|
|
148
|
+
The measured Wasm initialization and preparation cost was 8.64 ms median;
|
|
149
|
+
one tiny-GIF timing run also produced a 59.3x cold one-shot outlier.
|
|
150
|
+
- Reverse and boomerang are fast only when wtfgif can prove that raw frame
|
|
151
|
+
reordering preserves the rendered animation.
|
|
152
|
+
|
|
153
|
+
The speedup comes from recognizing when an edit does not need a pixel pipeline.
|
|
154
|
+
Instead of decode → quantize → recompress, wtfgif validates the structure and
|
|
155
|
+
copies already-correct compressed bytes.
|
|
156
|
+
|
|
157
|
+
## Verify
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm run check
|
|
161
|
+
|
|
162
|
+
BENCH_ITERATIONS=5 BENCH_WARMUP_ITERATIONS=2 \
|
|
163
|
+
npm run bench:makeemoji-retime
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { GifBinary } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* A delay in GIF centiseconds, or one delay for every image frame.
|
|
4
|
+
*/
|
|
5
|
+
export type GifFrameDelays = number | ArrayLike<number>;
|
|
6
|
+
/**
|
|
7
|
+
* A parsed GIF whose compressed image data and unrelated metadata can be
|
|
8
|
+
* copied without decoding or re-encoding pixels.
|
|
9
|
+
*
|
|
10
|
+
* Instances are immutable. Transformations share the parsed source layout;
|
|
11
|
+
* bytes are copied only when `toUint8Array()` is called.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CompiledGif {
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
readonly frameCount: number;
|
|
17
|
+
readonly delays: readonly number[];
|
|
18
|
+
readonly loopCount: number | null;
|
|
19
|
+
readonly canReorderFrames: boolean;
|
|
20
|
+
private readonly layout;
|
|
21
|
+
private readonly requestedDelays;
|
|
22
|
+
private readonly requestedLoopCount;
|
|
23
|
+
private readonly hasLoopOverride;
|
|
24
|
+
private constructor();
|
|
25
|
+
/** Parse a GIF into an immutable compiled representation. */
|
|
26
|
+
static from(input: GifBinary): CompiledGif;
|
|
27
|
+
/**
|
|
28
|
+
* Return a compiled view with new per-frame delays.
|
|
29
|
+
*
|
|
30
|
+
* A scalar applies to every frame. An array-like value must have exactly
|
|
31
|
+
* one entry per image frame. Values are integer centiseconds in
|
|
32
|
+
* the GIF range 0...65535.
|
|
33
|
+
*/
|
|
34
|
+
withDelays(delays: GifFrameDelays): CompiledGif;
|
|
35
|
+
/**
|
|
36
|
+
* Return a compiled view with a Netscape loop count.
|
|
37
|
+
*
|
|
38
|
+
* `0` means loop forever. If the source has no Netscape extension, a
|
|
39
|
+
* canonical extension is inserted without touching image data.
|
|
40
|
+
*/
|
|
41
|
+
withLoop(loopCount: number): CompiledGif;
|
|
42
|
+
/**
|
|
43
|
+
* Reorder or duplicate structurally independent frames without decoding.
|
|
44
|
+
*
|
|
45
|
+
* This strict fast path accepts full-canvas frames that are either all
|
|
46
|
+
* opaque or all restore a canonical base with disposal 2/3. It throws for
|
|
47
|
+
* GIFs whose frame composition could depend on the original order.
|
|
48
|
+
*/
|
|
49
|
+
reorderFrames(order: ArrayLike<number>): Uint8Array;
|
|
50
|
+
/** Reverse all frames through the proven-safe structural fast path. */
|
|
51
|
+
reverseFrames(): Uint8Array;
|
|
52
|
+
/**
|
|
53
|
+
* Append the complete reverse sequence, matching MakeEmoji's boomerang
|
|
54
|
+
* semantics: `[A, B, C, C, B, A]`.
|
|
55
|
+
*/
|
|
56
|
+
boomerangFrames(): Uint8Array;
|
|
57
|
+
/**
|
|
58
|
+
* Materialize the transformed GIF.
|
|
59
|
+
*
|
|
60
|
+
* Existing delays and loop counts are patched in place. A frame without a
|
|
61
|
+
* graphic-control extension receives the minimal eight-byte extension.
|
|
62
|
+
*/
|
|
63
|
+
toUint8Array(): Uint8Array;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse a GIF once so metadata-only transformations can reuse its original
|
|
67
|
+
* palettes, image descriptors, extensions, and compressed LZW sub-blocks.
|
|
68
|
+
*/
|
|
69
|
+
export declare function compileGif(input: GifBinary): CompiledGif;
|
|
70
|
+
/**
|
|
71
|
+
* Rewrite frame delays without decoding or recompressing a single pixel.
|
|
72
|
+
*/
|
|
73
|
+
export declare function retimeGifPixelPerfect(input: GifBinary, delays: GifFrameDelays): Uint8Array;
|
|
74
|
+
/** Reverse structurally independent GIF frames without touching pixel data. */
|
|
75
|
+
export declare function reverseGifPixelPerfect(input: GifBinary): Uint8Array;
|
|
76
|
+
/** Create a MakeEmoji-style forward-then-reverse GIF without pixel work. */
|
|
77
|
+
export declare function boomerangGifPixelPerfect(input: GifBinary): Uint8Array;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const enum GIF {
|
|
2
|
+
G = 71,
|
|
3
|
+
I = 73,
|
|
4
|
+
F = 70,
|
|
5
|
+
_8 = 56,
|
|
6
|
+
_9 = 57,
|
|
7
|
+
A = 97,
|
|
8
|
+
EXT = 33,
|
|
9
|
+
IMG = 44,
|
|
10
|
+
TRAILER = 59,
|
|
11
|
+
GCE = 249,
|
|
12
|
+
APPLICATION = 255,
|
|
13
|
+
PLAINTEXT = 1,
|
|
14
|
+
COMMENT = 254,
|
|
15
|
+
NETSCAPE_LEN = 11,
|
|
16
|
+
MAX_CODE = 4096
|
|
17
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { FrameInfo, GifBinary, GifDecodeBackend, GifDecodeBackendStatus, GifPixelBuffer, PreparedGifFrames, PrepareFramesOptions } from "../types";
|
|
2
|
+
export declare class GifReader {
|
|
3
|
+
private buf;
|
|
4
|
+
private width_;
|
|
5
|
+
private height_;
|
|
6
|
+
private globalPaletteOffset;
|
|
7
|
+
private globalPaletteSize;
|
|
8
|
+
private frames;
|
|
9
|
+
private loop_count;
|
|
10
|
+
private pooledTables;
|
|
11
|
+
private decTable;
|
|
12
|
+
private stack;
|
|
13
|
+
private firstByte;
|
|
14
|
+
private activePreparedFrames;
|
|
15
|
+
private static decodeBackend;
|
|
16
|
+
static createPooled(buf: GifBinary): GifReader;
|
|
17
|
+
static createUnpooled(buf: GifBinary): GifReader;
|
|
18
|
+
static setDecodeBackend(backend: GifDecodeBackend | null): void;
|
|
19
|
+
static getDecodeBackendStatus(): GifDecodeBackendStatus;
|
|
20
|
+
constructor(buf: GifBinary, _usePooling?: boolean);
|
|
21
|
+
get width(): number;
|
|
22
|
+
get height(): number;
|
|
23
|
+
/**
|
|
24
|
+
* Simplified LZW decoder that outputs palette indices instead of RGBA
|
|
25
|
+
*/
|
|
26
|
+
private lzwDecodeToIndices;
|
|
27
|
+
numFrames(): number;
|
|
28
|
+
loopCount(): number | null;
|
|
29
|
+
frameInfo(i: number): FrameInfo;
|
|
30
|
+
private ensureDecoderTables;
|
|
31
|
+
private getFrameCodes;
|
|
32
|
+
private getFramePalette;
|
|
33
|
+
private getFrameIndices;
|
|
34
|
+
decodeAndBlitFrameBGRA(frameNum: number, pixels: GifPixelBuffer): void;
|
|
35
|
+
decodeAndBlitFrameRGBA(frameNum: number, pixels: GifPixelBuffer): void;
|
|
36
|
+
decodeFrameToTransferableRGBA(frameNum: number): ArrayBuffer;
|
|
37
|
+
decodeFrameToTransferableBGRA(frameNum: number): ArrayBuffer;
|
|
38
|
+
decodeFrameIntoBuffer(frameNum: number, buffer: ArrayBuffer, format?: "rgba" | "bgra"): void;
|
|
39
|
+
preparePlayback(options?: Omit<PrepareFramesOptions, "composited">): PreparedGifFrames;
|
|
40
|
+
prepareFrames(options?: PrepareFramesOptions): PreparedGifFrames;
|
|
41
|
+
prepareFramesAsync(options?: PrepareFramesOptions): Promise<PreparedGifFrames>;
|
|
42
|
+
decodeAndBlitCompositedFrameRGBA(frameNum: number, pixels: Uint8Array): void;
|
|
43
|
+
decodeAndBlitCompositedFrameBGRA(frameNum: number, pixels: Uint8Array): void;
|
|
44
|
+
private normalizePrepareFramesOptions;
|
|
45
|
+
private normalizeFrameIndices;
|
|
46
|
+
private prepareFramesWithBackend;
|
|
47
|
+
private createBackendPreparedFramesResult;
|
|
48
|
+
private prepareCompositedFrames;
|
|
49
|
+
private prepareUncompositedFrames;
|
|
50
|
+
private createPreparedFramesResult;
|
|
51
|
+
private createPreparedPlayer;
|
|
52
|
+
private copyPreparedFrame;
|
|
53
|
+
private copyPreparedFrame32;
|
|
54
|
+
private blitPreparedColors;
|
|
55
|
+
private blitPreparedIndices;
|
|
56
|
+
private getPreparedTarget32;
|
|
57
|
+
private applyFrameDisposal;
|
|
58
|
+
private clearFrameRect;
|
|
59
|
+
private static findChangedRect;
|
|
60
|
+
private static copyRectPixels;
|
|
61
|
+
private static blitRectPixels;
|
|
62
|
+
private enforcePreparedByteBudget;
|
|
63
|
+
private static hashPixels;
|
|
64
|
+
private static findMatchingPixels;
|
|
65
|
+
private static pixelsEqual;
|
|
66
|
+
dispose(): void;
|
|
67
|
+
private clearFrameCaches;
|
|
68
|
+
returnToPool(): void;
|
|
69
|
+
static getPoolStats(): {
|
|
70
|
+
available: number;
|
|
71
|
+
totalCreated: number;
|
|
72
|
+
hits: number;
|
|
73
|
+
misses: number;
|
|
74
|
+
};
|
|
75
|
+
private decodeAndBlitFrame32;
|
|
76
|
+
private decodeAndBlitCompatibleFrame;
|
|
77
|
+
private getFrameColors;
|
|
78
|
+
private blitFrameIndicesToCanvas;
|
|
79
|
+
private lzwDecodeToPixels;
|
|
80
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FrameOptions, GifBinary, GifOptions, PaletteRGB } from "../types";
|
|
2
|
+
export type IndexedGifFrame = Uint8Array | number[];
|
|
3
|
+
export type IndexedGifFrames = Uint8Array | IndexedGifFrame[];
|
|
4
|
+
export type EncodeIndexedGifFramesBackend = "auto" | "rust" | "wasm" | "native" | "javascript";
|
|
5
|
+
export type RgbaGifFrame = Uint8Array | Uint8ClampedArray;
|
|
6
|
+
export type RgbaGifFrames = Uint8Array | Uint8ClampedArray | RgbaGifFrame[];
|
|
7
|
+
export type GifFrameDelay = number | readonly number[] | Uint16Array;
|
|
8
|
+
export type GifCompressionMode = "balanced" | "fast";
|
|
9
|
+
export interface EncodeIndexedGifFramesOptions {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
frames: IndexedGifFrames;
|
|
13
|
+
frameCount?: number;
|
|
14
|
+
palette: PaletteRGB;
|
|
15
|
+
delay?: GifFrameDelay;
|
|
16
|
+
loop?: number | null;
|
|
17
|
+
backend?: EncodeIndexedGifFramesBackend;
|
|
18
|
+
delta?: boolean;
|
|
19
|
+
compression?: GifCompressionMode;
|
|
20
|
+
}
|
|
21
|
+
export interface EncodeRgbaGifFramesOptions {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
frames: RgbaGifFrames;
|
|
25
|
+
frameCount?: number;
|
|
26
|
+
palette?: PaletteRGB;
|
|
27
|
+
delay?: GifFrameDelay;
|
|
28
|
+
loop?: number | null;
|
|
29
|
+
backend?: EncodeIndexedGifFramesBackend;
|
|
30
|
+
delta?: boolean;
|
|
31
|
+
alphaThreshold?: number;
|
|
32
|
+
compression?: GifCompressionMode;
|
|
33
|
+
}
|
|
34
|
+
type IndexedPixels = IndexedGifFrame;
|
|
35
|
+
export declare function encodeIndexedGifFrames(options: EncodeIndexedGifFramesOptions): Uint8Array;
|
|
36
|
+
export declare function encodeRgbaGifFrames(options: EncodeRgbaGifFramesOptions): Uint8Array;
|
|
37
|
+
export declare class GifWriter {
|
|
38
|
+
private buf;
|
|
39
|
+
readonly width: number;
|
|
40
|
+
readonly height: number;
|
|
41
|
+
private p;
|
|
42
|
+
private ended;
|
|
43
|
+
private loopCount;
|
|
44
|
+
private globalPalette;
|
|
45
|
+
private background;
|
|
46
|
+
private globalColorCount;
|
|
47
|
+
private previousIndexedFrame;
|
|
48
|
+
constructor(buf: GifBinary, width: number, height: number, gopts?: GifOptions);
|
|
49
|
+
addFrame(x: number, y: number, w: number, h: number, indexedPixels: IndexedPixels, opts?: FrameOptions): number;
|
|
50
|
+
addFrameDelta(indexedPixels: IndexedPixels, opts?: FrameOptions): number;
|
|
51
|
+
private writeFrame;
|
|
52
|
+
end(): number;
|
|
53
|
+
getOutputBuffer(): GifBinary;
|
|
54
|
+
setOutputBuffer(v: GifBinary): void;
|
|
55
|
+
getOutputBufferPosition(): number;
|
|
56
|
+
setOutputBufferPosition(v: number): void;
|
|
57
|
+
private updatePreviousIndexedFrame;
|
|
58
|
+
}
|
|
59
|
+
export {};
|