optiq-image 0.0.0-stage → 0.2.0-beta.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/CHANGELOG.md +35 -0
- package/LICENSE +201 -0
- package/NOTICE +30 -0
- package/README.md +259 -2
- package/dist/batch-worker.d.ts +6 -0
- package/dist/batch-worker.js +17 -0
- package/dist/batch.d.ts +16 -0
- package/dist/batch.js +51 -0
- package/dist/classify.d.ts +31 -0
- package/dist/classify.js +86 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +176 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.js +195 -0
- package/dist/codecs/canvas.d.ts +9 -0
- package/dist/codecs/canvas.js +64 -0
- package/dist/codecs/fallback.d.ts +11 -0
- package/dist/codecs/fallback.js +59 -0
- package/dist/codecs/types.d.ts +15 -0
- package/dist/codecs/types.js +1 -0
- package/dist/codecs/wasm.d.ts +6 -0
- package/dist/codecs/wasm.js +125 -0
- package/dist/container/bytes.d.ts +17 -0
- package/dist/container/bytes.js +87 -0
- package/dist/container/exif.d.ts +6 -0
- package/dist/container/exif.js +73 -0
- package/dist/container/gif.d.ts +14 -0
- package/dist/container/gif.js +199 -0
- package/dist/container/icc.d.ts +8 -0
- package/dist/container/icc.js +35 -0
- package/dist/container/isobmff.d.ts +28 -0
- package/dist/container/isobmff.js +149 -0
- package/dist/container/jpeg.d.ts +46 -0
- package/dist/container/jpeg.js +171 -0
- package/dist/container/png.d.ts +43 -0
- package/dist/container/png.js +139 -0
- package/dist/container/webp.d.ts +39 -0
- package/dist/container/webp.js +149 -0
- package/dist/engine.d.ts +24 -0
- package/dist/engine.js +588 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +31 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +9 -0
- package/dist/node.d.ts +10 -0
- package/dist/node.js +68 -0
- package/dist/options.d.ts +30 -0
- package/dist/options.js +71 -0
- package/dist/pixels.d.ts +25 -0
- package/dist/pixels.js +124 -0
- package/dist/plan.d.ts +35 -0
- package/dist/plan.js +77 -0
- package/dist/probe.d.ts +29 -0
- package/dist/probe.js +118 -0
- package/dist/protocol.d.ts +26 -0
- package/dist/protocol.js +1 -0
- package/dist/quality.d.ts +18 -0
- package/dist/quality.js +176 -0
- package/dist/react.d.ts +26 -0
- package/dist/react.js +35 -0
- package/dist/run.d.ts +5 -0
- package/dist/run.js +11 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +1 -0
- package/dist/upload.d.ts +87 -0
- package/dist/upload.js +95 -0
- package/dist/vite.d.ts +17 -0
- package/dist/vite.js +69 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +24 -0
- package/package.json +136 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0-beta.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- `mode: "fast"`: SSIM ≥ 0.975 like `balanced`, no AVIF, no oxipng, ≤ 5 encodes. Benchmark: −54.9% total
|
|
6
|
+
(balanced −61.2%), median 405 ms in Chromium (balanced 1.3 s), median peak RSS 184 MB (balanced 304 MB).
|
|
7
|
+
- `optiq-image` CLI (`npx optiq-image <files|folders>`): dry run by default, `--out`, `--in-place`, parallel workers.
|
|
8
|
+
- `optiq-image/vite`: Vite plugin that optimises build output in place (same names and formats).
|
|
9
|
+
- `optiq-image/react`: `useOptiq(file, options)` hook.
|
|
10
|
+
- `optimizeFiles()` batch API for Node (worker_threads pool).
|
|
11
|
+
|
|
12
|
+
## 0.1.0-beta.0 (unreleased)
|
|
13
|
+
|
|
14
|
+
Audit fixes (see docs/audit.md):
|
|
15
|
+
|
|
16
|
+
- A perceptual mode could return the original although `lossless` mode found a smaller pixel-exact file (P3 photo
|
|
17
|
+
PNG: 794 KB returned, 472 KB available). Quality ranges now reach JPEG 98 / WebP 99 / AVIF 95, and lossless
|
|
18
|
+
candidates are added when no re-encode meets the threshold for a losslessly stored input.
|
|
19
|
+
- SSIM is now bounded by the alpha plane's own SSIM for images with transparency.
|
|
20
|
+
- Warning when the output is larger than the input because the original was not an allowed result.
|
|
21
|
+
- SSIM 20–54% faster with numerically identical results.
|
|
22
|
+
- Verified with Vite 7 and Vite 8.
|
|
23
|
+
|
|
24
|
+
First public beta.
|
|
25
|
+
|
|
26
|
+
- `compressImage` / `createCompressor`: Web Worker pool, `AbortSignal` cancellation (terminates the worker),
|
|
27
|
+
progress events, typed `OptiqError` codes, canvas fallback when WASM cannot load.
|
|
28
|
+
- Modes: `lossless` (pixel-exact, verified), `visually-lossless` (SSIM ≥ 0.99), `balanced` (SSIM ≥ 0.975).
|
|
29
|
+
- Codecs (via jSquash): MozJPEG, libwebp, libavif, oxipng, Rust `png` and `resize`; own static-GIF decoder.
|
|
30
|
+
- Format sniffing from content; EXIF orientation, ICC, EXIF/XMP preservation; animation never silently flattened.
|
|
31
|
+
- Tile-mosaic quality search for images above 4 MP.
|
|
32
|
+
- `optiq-image/upload`: presigned PUT/POST upload with progress and original-file fallback.
|
|
33
|
+
- `optiq-image/node`: Node.js codec initialisation for tests, benchmarks and server-side use.
|
|
34
|
+
|
|
35
|
+
Tested in Chromium, Firefox and WebKit. Known limitations: no AVIF metadata writer; HEIC unsupported.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Optiq contributors
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Optiq
|
|
2
|
+
Copyright 2026 Optiq contributors
|
|
3
|
+
|
|
4
|
+
This product is licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
5
|
+
|
|
6
|
+
Optiq does not bundle codec binaries itself. It depends on the following packages, which an application
|
|
7
|
+
bundle that uses Optiq will include. Their licences permit commercial redistribution provided the notices
|
|
8
|
+
are reproduced; the full texts ship inside each package (LICENSE and codec/LICENSE.codec.md).
|
|
9
|
+
|
|
10
|
+
@jsquash/jpeg, @jsquash/webp, @jsquash/avif, @jsquash/png, @jsquash/oxipng, @jsquash/resize
|
|
11
|
+
Apache-2.0, Copyright 2023 jamsinclair; derived from Squoosh, Copyright 2020 Google Inc.
|
|
12
|
+
|
|
13
|
+
MozJPEG / libjpeg-turbo (in @jsquash/jpeg)
|
|
14
|
+
IJG License, Modified (3-clause) BSD License, zlib License
|
|
15
|
+
"This software is based in part on the work of the Independent JPEG Group."
|
|
16
|
+
|
|
17
|
+
libwebp (in @jsquash/webp)
|
|
18
|
+
BSD-3-Clause, Copyright (c) 2010, Google Inc.
|
|
19
|
+
|
|
20
|
+
libavif and libaom (in @jsquash/avif)
|
|
21
|
+
libavif: BSD-2-Clause, Copyright 2019 Joe Drago and contributors
|
|
22
|
+
libaom: BSD-2-Clause, Copyright (c) 2016, Alliance for Open Media, plus the AOMedia Patent License 1.0
|
|
23
|
+
(@jsquash/avif does not ship these texts; see https://github.com/AOMediaCodec/libavif and
|
|
24
|
+
https://aomedia.googlesource.com/aom/+/refs/heads/main/LICENSE and PATENTS)
|
|
25
|
+
|
|
26
|
+
png crate (in @jsquash/png) MIT OR Apache-2.0, The image-rs Developers
|
|
27
|
+
oxipng (in @jsquash/oxipng) MIT, Copyright (c) 2016 Joshua Holmer
|
|
28
|
+
resize crate (in @jsquash/resize) MIT, Copyright (c) 2015 PistonDevelopers
|
|
29
|
+
hqx (in @jsquash/resize) Apache-2.0
|
|
30
|
+
magic-kernel (in @jsquash/resize) MIT, Copyright (c) 2024 Serhii Tatarintsev
|
package/README.md
CHANGED
|
@@ -1,3 +1,260 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Baukaalm/optiq/main/brand/logo-white.svg" />
|
|
4
|
+
<img src="https://raw.githubusercontent.com/Baukaalm/optiq/main/brand/logo-black.svg" width="96" height="96" alt="Optiq" />
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<h1 align="center">Optiq</h1>
|
|
9
|
+
|
|
10
|
+
**[Try the demo](https://baukaalm.github.io/optiq/try)** · **[Documentation](https://baukaalm.github.io/optiq/)** · [Benchmarks](https://baukaalm.github.io/optiq/benchmarks)
|
|
11
|
+
|
|
12
|
+
Browser-side image optimisation for uploads. For every image Optiq encodes a bounded set of candidates
|
|
13
|
+
(MozJPEG, libwebp, libavif, oxipng — compiled to WebAssembly), **measures** each one, and returns the smallest
|
|
14
|
+
output that satisfies your constraint:
|
|
15
|
+
|
|
16
|
+
- `lossless` — decoded pixels (including alpha) are verified identical to the input;
|
|
17
|
+
- `visually-lossless` — pixels may differ; SSIM ≥ 0.99 against the input (configurable);
|
|
18
|
+
- `balanced` (default) — SSIM ≥ 0.975, smaller search budget, faster AVIF preset.
|
|
19
|
+
|
|
20
|
+
Work runs in a Web Worker; the main thread only posts a `Blob` and receives an `ArrayBuffer`. Results carry the
|
|
21
|
+
chosen codec/settings, every candidate tried, the measured quality, warnings, fallbacks and unmet constraints.
|
|
22
|
+
|
|
23
|
+
> Benchmarks: see [Benchmark results](#benchmark-results) and `bench/results/RESULTS.md`. Optiq is not
|
|
24
|
+
> uniformly better than the alternatives; the trade-offs (size vs. time vs. memory) are listed there per category.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pnpm add optiq-image # or npm / yarn
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requirements: an ES2022 browser with module Workers and WebAssembly (Chrome/Edge 80+, Firefox 114+, Safari 15+).
|
|
33
|
+
AVIF *output* is only useful where your audience can decode AVIF (Chrome 85+, Firefox 93+, Safari 16.4+); remove it
|
|
34
|
+
with `autoFormats: ["jpeg", "png", "webp"]` otherwise.
|
|
35
|
+
|
|
36
|
+
### Bundler setup (Vite 7 and 8)
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// vite.config.ts
|
|
40
|
+
export default defineConfig({
|
|
41
|
+
worker: { format: "es" }, // codecs are loaded with dynamic import() inside the worker
|
|
42
|
+
optimizeDeps: { // dev only: pre-bundling breaks `new URL("x.wasm", import.meta.url)`
|
|
43
|
+
exclude: ["optiq-image", "@jsquash/jpeg", "@jsquash/png", "@jsquash/webp", "@jsquash/avif", "@jsquash/oxipng", "@jsquash/resize"],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
webpack 5 and Parcel 2 understand `new Worker(new URL("./worker.js", import.meta.url), { type: "module" })` and
|
|
49
|
+
`new URL("*.wasm", import.meta.url)` natively. For anything else pass `workerFactory` to `createCompressor`.
|
|
50
|
+
Each codec is a separate chunk, fetched on first use (AVIF ≈ 3.5 MB, others 35–350 KB).
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { compressImage, isOptiqError } from "optiq-image";
|
|
56
|
+
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
try {
|
|
59
|
+
const result = await compressImage(file, {
|
|
60
|
+
mode: "balanced",
|
|
61
|
+
maxWidthOrHeight: 2560,
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
onProgress: (e) => console.log(e.stage, Math.round(e.progress * 100), e.bestBytes),
|
|
64
|
+
});
|
|
65
|
+
result.blob; // Blob with the right MIME type (image/avif, image/webp, ...)
|
|
66
|
+
result.fileName; // "IMG_0042.webp" — extension matches the encoded bytes
|
|
67
|
+
result.sizeReductionRatio, result.qualityMetric, result.codec, result.warnings;
|
|
68
|
+
if (!result.constraintsMet) console.warn(result.unmetConstraints);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (isOptiqError(e)) console.warn(e.code, e.message); // e.g. UNSUPPORTED_FORMAT, ABORTED
|
|
71
|
+
// the original `file` is untouched — upload it instead
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
React:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
function useCompressed(file?: File) {
|
|
79
|
+
const [state, set] = useState<{ result?: CompressionResult; error?: unknown; progress: number }>({ progress: 0 });
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!file) return;
|
|
82
|
+
const ac = new AbortController();
|
|
83
|
+
compressImage(file, { signal: ac.signal, onProgress: (e) => set((s) => ({ ...s, progress: e.progress })) })
|
|
84
|
+
.then((result) => set({ result, progress: 1 }), (error) => !ac.signal.aborted && set({ error, progress: 0 }));
|
|
85
|
+
return () => ac.abort(); // terminates the worker job
|
|
86
|
+
}, [file]);
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For explicit control over workers use `createCompressor({ maxWorkers, workerFactory, codecFallback, allowMainThread, recycleAfterPixels })`.
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
| Option | Default | Meaning |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `mode` | `"balanced"` | `lossless`, `visually-lossless`, `balanced`, `fast` (no AVIF, ≤ 5 encodes; ~3× faster) |
|
|
98
|
+
| `minQuality` | 0.99 / 0.975 | SSIM threshold (ignored in lossless). Gradients use a stricter value half-way to 1 |
|
|
99
|
+
| `maxOutputBytes` | – | Upper bound. If impossible without violating quality, the result lists it in `unmetConstraints` |
|
|
100
|
+
| `maxWidthOrHeight` | – | Downscale longest side. In `lossless` mode only with `allowLossyResize: true` |
|
|
101
|
+
| `targetFormat` | `"auto"` | `auto`, `original`, `jpeg`, `png`, `webp`, `avif` |
|
|
102
|
+
| `autoFormats` | all four | Formats `auto` may choose |
|
|
103
|
+
| `allowFormatConversion` | `true` | `false` keeps the input format |
|
|
104
|
+
| `maxCandidates` | 10 / 16 / 4 | Encode budget (balanced / visually-lossless / lossless) |
|
|
105
|
+
| `searchStrategy` | `"binary"` | `fixed` = one encode per format at a preset quality |
|
|
106
|
+
| `preserveMetadata` | `true` | Keep EXIF/XMP/ICC. `false` strips EXIF (incl. GPS) and XMP; non-sRGB ICC is kept anyway |
|
|
107
|
+
| `preserveAnimation` | `true` | Animated inputs are only optimised at container level. `false` permits flattening to frame 1 |
|
|
108
|
+
| `minSavingsRatio` | `0.02` | Keep the original unless re-encoding saves at least this |
|
|
109
|
+
| `maxInputBytes` / `maxInputPixels` | 100 MiB / 50 MP | Checked before reading / before decoding |
|
|
110
|
+
| `onUnmetConstraints` | `"best-effort"` | `throw` → `CONSTRAINTS_UNSATISFIABLE` |
|
|
111
|
+
|
|
112
|
+
Progress stages: `reading → probing → decoding → (resizing) → analyzing → encoding … → finalizing → done`.
|
|
113
|
+
|
|
114
|
+
Error codes: `INVALID_INPUT`, `UNSUPPORTED_FORMAT`, `MALFORMED_INPUT`, `INPUT_TOO_LARGE`, `INVALID_OPTIONS`,
|
|
115
|
+
`INCOMPATIBLE_OPTIONS` (e.g. JPEG requested for a transparent image), `CONSTRAINTS_UNSATISFIABLE`,
|
|
116
|
+
`CODEC_UNAVAILABLE`, `UNSUPPORTED_ENVIRONMENT`, `WORKER_ERROR`, `ABORTED` (`name === "AbortError"`).
|
|
117
|
+
|
|
118
|
+
## React, CLI, Vite plugin
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { useOptiq } from "optiq-image/react";
|
|
122
|
+
const { status, result, progress, cancel } = useOptiq(file, { mode: "fast" });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
[View example](https://github.com/Baukaalm/optiq/blob/main/docs/examples/PhotoUploadForm.tsx) (upload form with progress, cancel and S3) · [React hook docs](https://baukaalm.github.io/optiq/guide/react)
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
npx optiq-image ./photos --out ./photos-optimized # static images, in parallel worker threads
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { optiq } from "optiq-image/vite"; // optimises build output in place
|
|
133
|
+
export default defineConfig({ plugins: [optiq()] });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Uploading to S3
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { compressAndUpload } from "optiq-image/upload";
|
|
140
|
+
const out = await compressAndUpload(file, { requestPresignedUpload: (req) => fetch("/api/presign", …).then((r) => r.json()) });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Compression is optional and never blocks the upload: if it fails or does not help, the original is uploaded.
|
|
144
|
+
Presigned URLs must come from your backend — see [docs/guide/s3.md](https://github.com/Baukaalm/optiq/blob/main/docs/guide/s3.md) for the contract and a backend sketch.
|
|
145
|
+
|
|
146
|
+
## Supported formats
|
|
147
|
+
|
|
148
|
+
| | Input | Output |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| JPEG | 8-bit RGB/gray (CMYK, 12-bit: kept as is) | MozJPEG, progressive |
|
|
151
|
+
| PNG | all incl. 16-bit, APNG (container-level only) | oxipng (lossless) |
|
|
152
|
+
| WebP | lossy, lossless, animated (container-level only) | lossy / lossless |
|
|
153
|
+
| AVIF | still (animated / rotated: kept as is) | libavif |
|
|
154
|
+
| GIF | static (converted), animated (kept as is) | – |
|
|
155
|
+
| HEIC/HEIF | detected → `UNSUPPORTED_FORMAT` | – |
|
|
156
|
+
| SVG | detected → returned unchanged, never rasterised | – |
|
|
157
|
+
|
|
158
|
+
Formats are detected from file content. Extensions and MIME types are ignored (a `.jpg` that is a PNG is a PNG).
|
|
159
|
+
|
|
160
|
+
## Guarantees and limitations
|
|
161
|
+
|
|
162
|
+
- **Lossless** means decoded RGBA equality, verified per candidate, for the decoder Optiq uses. JPEG
|
|
163
|
+
decoders are not bit-exact across implementations, so “lossless” for a JPEG input is only ever a byte-level
|
|
164
|
+
container rewrite (metadata removal), never a re-encode.
|
|
165
|
+
- **Transparency** is never dropped: JPEG is excluded when any pixel has alpha < 255.
|
|
166
|
+
- **Orientation**: EXIF orientation is applied to the pixels of re-encoded outputs (tag reset to 1) and preserved in
|
|
167
|
+
container-level outputs.
|
|
168
|
+
- **Colour**: ICC profiles are carried into JPEG/PNG/WebP outputs. AVIF output is tagged sRGB and is not used for
|
|
169
|
+
images with a non-sRGB profile. sRGB profiles are dropped only with `preserveMetadata: false` (or for AVIF output,
|
|
170
|
+
where the `nclx` sRGB tag is equivalent). No colour conversion is performed.
|
|
171
|
+
- **Metadata**: kept by default. AVIF output is skipped when EXIF/XMP must be preserved (no AVIF metadata writer).
|
|
172
|
+
With `preserveMetadata: false`, EXIF (including GPS), XMP, comments and trailing data are removed.
|
|
173
|
+
- **Animation** is never silently flattened.
|
|
174
|
+
- **SSIM is a proxy for perception**, not perception: it under-weights banding in smooth gradients and chroma
|
|
175
|
+
bleeding. Validate thresholds on your own content.
|
|
176
|
+
- **Memory**: large images need several hundred MB inside the worker (see benchmarks). WASM memory is returned by
|
|
177
|
+
terminating the worker, which happens automatically after jobs above 8 MP.
|
|
178
|
+
- **Speed**: searching costs multiple encodes. Use `balanced`, lower `maxCandidates`, `searchStrategy: "fixed"`, or
|
|
179
|
+
drop AVIF from `autoFormats` when latency matters more than bytes.
|
|
180
|
+
|
|
181
|
+
## Licensing
|
|
182
|
+
|
|
183
|
+
Optiq: Apache-2.0. Bundled codecs (via jSquash, Apache-2.0 wrappers): MozJPEG (IJG + BSD-3 + zlib),
|
|
184
|
+
libwebp (BSD-3), libavif (BSD-2) with libaom (BSD-2 + AOMedia patent licence), Rust `png`, `oxipng`, `resize`
|
|
185
|
+
(MIT/Apache-2.0). All permit commercial redistribution with notices. No GPL code is included
|
|
186
|
+
(libimagequant was rejected for that reason).
|
|
187
|
+
|
|
188
|
+
## Benchmark results
|
|
189
|
+
|
|
190
|
+
24 images (`bench/corpus/manifest.json`): 10 Kodak photos (8 JPEG q92, 2 PNG), 3 phone-style 12 MP JPEGs,
|
|
191
|
+
2 screenshots, 2 text graphics, 4 transparent illustrations, 3 gradients. Apple M4 Pro (14 cores, 24 GB), macOS arm64, Node 24.15 and
|
|
192
|
+
headless Chromium 156. Every output is decoded with the same decoders and scored with the same SSIM against the
|
|
193
|
+
input. Full tables, per-image rows and the optimisation log: [`bench/results/RESULTS.md`](https://github.com/Baukaalm/optiq/blob/main/bench/results/RESULTS.md).
|
|
194
|
+
|
|
195
|
+
**Perceptual, all 24 images (Node; same encoders as the browser)**
|
|
196
|
+
|
|
197
|
+
| Tool | Total output | Reduction | Meets SSIM ≥ 0.975 | Median time | Median peak RSS |
|
|
198
|
+
|---|---|---|---|---|---|
|
|
199
|
+
| Optiq `balanced` | 13.25 → 5.15 MB | 61.1% | 24/24 | 1.8 s | 301 MB |
|
|
200
|
+
| Optiq `fast` (no AVIF, ≤ 5 encodes) | 13.25 → 5.97 MB | 54.9% | 24/24 | 521 ms | 184 MB |
|
|
201
|
+
| Optiq `balanced`, `preserveMetadata: false` | 13.25 → 4.73 MB | 64.3% | 24/24 | 1.5 s | 297 MB |
|
|
202
|
+
| Optiq `visually-lossless` | 13.25 → 6.38 MB | 51.8% | 24/24 (≥ 0.99: 24/24) | 2.3 s | 325 MB |
|
|
203
|
+
| libvips WebP q80 | 13.25 → 6.50 MB | 51.0% | 17/24 | 59 ms | 105 MB |
|
|
204
|
+
| libvips MozJPEG q80 (opaque only) | 12.82 → 6.87 MB | 46.4% | 11/20 | 54 ms | 106 MB |
|
|
205
|
+
| libvips AVIF q50 | 13.25 → 3.41 MB | 74.3% | 11/24 (photos: 0/10) | 590 ms | 151 MB |
|
|
206
|
+
|
|
207
|
+
**Browser (Chromium), against browser-image-compression 2.0.2 and canvas**
|
|
208
|
+
|
|
209
|
+
| Tool | Total output | Reduction | Meets SSIM ≥ 0.975 | Median time |
|
|
210
|
+
|---|---|---|---|---|
|
|
211
|
+
| Optiq `balanced` | 13.25 → 5.15 MB | 61.1% | 24/24 | 1.4 s |
|
|
212
|
+
| Optiq `fast` | 13.25 → 5.97 MB | 54.9% | 24/24 | 405 ms |
|
|
213
|
+
| browser-image-compression `initialQuality: 0.8` | 13.25 → 10.21 MB | 22.9% | 19/24 | 78 ms |
|
|
214
|
+
| browser-image-compression `fileType: webp, 0.8` | 13.25 → 6.52 MB | 50.8% | 18/24 | 37 ms |
|
|
215
|
+
| browser-image-compression `maxSizeMB: 1` | 13.25 → 7.14 MB | 46.1% | 21/24 (3 large photos were downscaled, not scored) | 128 ms |
|
|
216
|
+
| canvas WebP 0.8 | 13.25 → 6.52 MB | 50.8% | 18/24 | 36 ms |
|
|
217
|
+
|
|
218
|
+
What the numbers say:
|
|
219
|
+
|
|
220
|
+
- **Per image, no baseline dominated Optiq `balanced`** (smaller *and* higher SSIM) on any image.
|
|
221
|
+
`balanced` dominated MozJPEG q80 on 11/20, WebP q80 on 10/24, browser-image-compression WebP 0.8 on 10/24 and
|
|
222
|
+
AVIF q50 on 2/24; everything else is a trade-off.
|
|
223
|
+
- **AVIF q50 is much smaller on photos, but at SSIM 0.93–0.97**, below the 0.975 constraint on all 10 Kodak photos.
|
|
224
|
+
Optiq spends bytes to stay above the threshold; whether that threshold is right for you is a product decision.
|
|
225
|
+
- **Category matters.** Screenshots/text: −90.6% / −91.8% vs. −62.8% / −46.6% for WebP q80 (lossless WebP and
|
|
226
|
+
screen-content AVIF win there). Large 12 MP phone photos: −41.5% with metadata kept (AVIF excluded because EXIF
|
|
227
|
+
must be preserved), −47.7% with `preserveMetadata: false`, vs. −30.7% for WebP q80.
|
|
228
|
+
- **Optiq is slow and memory-hungry by comparison.** It runs several encodes plus decodes and SSIM per
|
|
229
|
+
image: 1–4 s typical, 3–4 s for 12 MP photos (8–12 s when AVIF is allowed), 16.6 s for a 5 MP screenshot in
|
|
230
|
+
`balanced` and 38 s in `visually-lossless`; median peak RSS ≈ 300 MB, up to 1.1 GB. Single canvas/libvips encodes
|
|
231
|
+
take 5–800 ms. Work runs in a worker, so the UI stays responsive (max main-thread gap 11 ms during a 12 MP job).
|
|
232
|
+
|
|
233
|
+
**Lossless** (`sc-lossless`, Node): pixel-exact on 24/24 and never larger than the input (total −20.4%).
|
|
234
|
+
PNG photos −27% (oxipng), screenshots −76% and text −91% (about the same as libvips lossless WebP effort 6),
|
|
235
|
+
transparent illustrations −42% and gradients −66%, where libvips PNG max compression reached −60% / −76% but its
|
|
236
|
+
outputs were not pixel-exact against the reference. JPEG inputs: only metadata can be removed losslessly (0% with
|
|
237
|
+
the default `preserveMetadata: true`; no jpegtran in this build).
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
Toolchain: Node ≥ 20 (tested with 24.15), pnpm 11, Playwright Chromium (`pnpm exec playwright install chromium chromium-headless-shell`).
|
|
242
|
+
No Rust toolchain is required: all WASM comes prebuilt from pinned jSquash packages.
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
pnpm install
|
|
246
|
+
pnpm typecheck
|
|
247
|
+
pnpm test # unit + integration tests in Node (real WASM codecs)
|
|
248
|
+
pnpm test:browser # builds, bundles a consumer page with Vite, runs it in Chromium (incl. CSP/no-WASM fallback)
|
|
249
|
+
pnpm test:demo # demo end-to-end (light/dark/mobile): compress → presign → PUT to mock S3
|
|
250
|
+
pnpm pack:check # pack the tarball, install it in a fresh Vite app, typecheck, build, run (does not publish)
|
|
251
|
+
pnpm demo # React + HeroUI demo with a mock presigned-S3 backend
|
|
252
|
+
pnpm docs:dev # VitePress documentation site
|
|
253
|
+
pnpm bench:corpus # download/render the benchmark corpus (24 images)
|
|
254
|
+
pnpm bench:node # Optiq vs libvips (sharp), with peak RSS per run
|
|
255
|
+
pnpm bench:browser # Optiq vs browser-image-compression and canvas, in Chromium
|
|
256
|
+
pnpm bench:report # writes bench/results/RESULTS.md
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Documentation site (guides, API reference generated from the TSDoc comments, benchmarks, architecture):
|
|
260
|
+
`pnpm docs:dev`, or read the Markdown in [`docs/`](https://github.com/Baukaalm/optiq/blob/main/docs/). Design notes: [docs/architecture.md](https://github.com/Baukaalm/optiq/blob/main/docs/architecture.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// worker_threads entry for batch optimisation in Node (CLI, Vite plugin). One image at a time per thread:
|
|
2
|
+
// the WASM codecs are single-threaded, so parallelism comes from running several of these.
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { parentPort } from "node:worker_threads";
|
|
5
|
+
import { serializeError } from "./errors.js";
|
|
6
|
+
import { optimizeInNode } from "./node.js";
|
|
7
|
+
parentPort.on("message", async (req) => {
|
|
8
|
+
try {
|
|
9
|
+
const input = new Uint8Array(await readFile(req.path));
|
|
10
|
+
const out = await optimizeInNode(input, req.options);
|
|
11
|
+
const bytes = out.bytes ? out.bytes.slice().buffer : undefined;
|
|
12
|
+
parentPort.postMessage({ id: req.id, ok: true, bytes, report: out.report }, bytes ? [bytes] : []);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
parentPort.postMessage({ id: req.id, ok: false, error: serializeError(e) });
|
|
16
|
+
}
|
|
17
|
+
});
|
package/dist/batch.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EngineReport } from "./engine.js";
|
|
2
|
+
import { type OptiqError } from "./errors.js";
|
|
3
|
+
import type { OptiqOptions } from "./types.js";
|
|
4
|
+
export interface BatchItemResult {
|
|
5
|
+
path: string;
|
|
6
|
+
/** Output bytes; undefined when the input was already optimal (report.usedOriginal). */
|
|
7
|
+
bytes?: Uint8Array;
|
|
8
|
+
report?: EngineReport;
|
|
9
|
+
error?: OptiqError;
|
|
10
|
+
}
|
|
11
|
+
export interface BatchOptions {
|
|
12
|
+
/** Worker threads. Default: available cores − 1 (at least 1). Each holds a few copies of one decoded image. */
|
|
13
|
+
concurrency?: number;
|
|
14
|
+
onResult?: (r: BatchItemResult, done: number, total: number) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function optimizeFiles(paths: string[], options: OptiqOptions, batch?: BatchOptions): Promise<BatchItemResult[]>;
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch optimisation of files in Node using a pool of worker threads (used by the CLI and the Vite plugin).
|
|
3
|
+
*/
|
|
4
|
+
import { availableParallelism } from "node:os";
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
6
|
+
import { deserializeError } from "./errors.js";
|
|
7
|
+
export async function optimizeFiles(paths, options, batch = {}) {
|
|
8
|
+
const n = Math.max(1, Math.min(paths.length, batch.concurrency ?? Math.max(1, availableParallelism() - 1)));
|
|
9
|
+
const results = new Array(paths.length);
|
|
10
|
+
if (!paths.length)
|
|
11
|
+
return results;
|
|
12
|
+
let next = 0;
|
|
13
|
+
let done = 0;
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
const workers = [];
|
|
16
|
+
const finish = () => {
|
|
17
|
+
for (const w of workers)
|
|
18
|
+
void w.terminate();
|
|
19
|
+
resolve();
|
|
20
|
+
};
|
|
21
|
+
const feed = (w) => {
|
|
22
|
+
if (next >= paths.length)
|
|
23
|
+
return;
|
|
24
|
+
const id = next++;
|
|
25
|
+
w.postMessage({ id, path: paths[id], options });
|
|
26
|
+
};
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
const w = new Worker(new URL("./batch-worker.js", import.meta.url));
|
|
29
|
+
workers.push(w);
|
|
30
|
+
w.on("message", (m) => {
|
|
31
|
+
const r = m.ok
|
|
32
|
+
? { path: paths[m.id], bytes: m.bytes ? new Uint8Array(m.bytes) : undefined, report: m.report }
|
|
33
|
+
: { path: paths[m.id], error: deserializeError(m.error) };
|
|
34
|
+
results[m.id] = r;
|
|
35
|
+
done++;
|
|
36
|
+
batch.onResult?.(r, done, paths.length);
|
|
37
|
+
if (done === paths.length)
|
|
38
|
+
finish();
|
|
39
|
+
else
|
|
40
|
+
feed(w);
|
|
41
|
+
});
|
|
42
|
+
w.on("error", (e) => {
|
|
43
|
+
for (const x of workers)
|
|
44
|
+
void x.terminate();
|
|
45
|
+
reject(e);
|
|
46
|
+
});
|
|
47
|
+
feed(w);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RawImage } from "./pixels.js";
|
|
2
|
+
import type { ImageCategory } from "./types.js";
|
|
3
|
+
export interface ImageFeatures {
|
|
4
|
+
sampledPixels: number;
|
|
5
|
+
/** Distinct RGBA values among sampled pixels (capped at 4096). */
|
|
6
|
+
uniqueColors: number;
|
|
7
|
+
/** Share of sampled pixels having the single most common colour (background). */
|
|
8
|
+
dominantColorShare: number;
|
|
9
|
+
/** Horizontally/vertically adjacent pairs with identical RGBA. */
|
|
10
|
+
flatShare: number;
|
|
11
|
+
/** Adjacent pairs with luma step > 64 (hard edges: text, UI lines). */
|
|
12
|
+
hardEdgeShare: number;
|
|
13
|
+
/** Adjacent non-identical pairs with luma step <= 3 (smooth ramps). */
|
|
14
|
+
smoothStepShare: number;
|
|
15
|
+
/** Pixels with alpha < 255. */
|
|
16
|
+
transparentShare: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic feature extraction on a grid of up to 24x24 patches of 16x16 contiguous pixels.
|
|
20
|
+
* Adjacency is only evaluated inside patches, so it reflects the full-resolution content without scanning every pixel.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractFeatures(img: RawImage): ImageFeatures;
|
|
23
|
+
/**
|
|
24
|
+
* Heuristic rules with hand-set thresholds, checked against the test fixtures and the benchmark corpus (see docs/architecture.md);
|
|
25
|
+
* the category only steers which codecs/settings are tried first, never the quality decision.
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyFeatures(f: ImageFeatures): ImageCategory;
|
|
28
|
+
export declare function classify(img: RawImage): {
|
|
29
|
+
category: ImageCategory;
|
|
30
|
+
features: ImageFeatures;
|
|
31
|
+
};
|