Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
147 commits
Select commit Hold shift + click to select a range
1814f1d
build: remove libjpeg-turbo
claude Oct 27, 2026
fba4e49
jpeg: add a decoder in safe Rust (bun_jpeg)
claude Oct 27, 2026
b3d495a
jpeg: add the encoder
claude Oct 27, 2026
870a61b
image: decode and encode JPEG with bun_jpeg
claude Oct 27, 2026
4701d02
tmp: the harness that compares bun_jpeg with libjpeg-turbo
claude Oct 27, 2026
d6b9aa7
jpeg: keep clear of the byte-search lint
claude Oct 27, 2026
930a741
jpeg: include libjpeg-turbo's licenses and copyright notices
claude Oct 27, 2026
bf12679
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 27, 2026
918d55a
jpeg: compute the encoder's divisors once
claude Oct 27, 2026
1c9eafb
Keep prettier off libjpeg-turbo's license
claude Oct 27, 2026
fa813f1
test: JPEG files made with libjpeg-turbo's cjpeg and jpegtran
claude Oct 27, 2026
1313a1a
wip: golden tests
claude Oct 27, 2026
5799077
wip: golden tests, recorded
claude Oct 27, 2026
9dc27e4
test: compare Bun.Image's JPEG results with those of the last build o…
claude Oct 27, 2026
c157571
jpeg: put the ICC profile together only when asked
claude Oct 27, 2026
3308d2c
jpeg: convert YCbCr to RGB in 16 bits
claude Oct 27, 2026
e42cde5
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Oct 27, 2026
60472ed
jpeg: share constants and helpers between the modules
claude Oct 27, 2026
0b4c282
jpeg: write the DCTs so that the compiler vectorizes them
claude Oct 27, 2026
d932889
jpeg: encode only the coefficients that are not zero
claude Oct 27, 2026
468aab9
jpeg: do not zero the pixels before writing them, upsample in 16 bits
claude Oct 27, 2026
8eb8f13
jpeg: one load for a Huffman code and its size
claude Oct 27, 2026
f97d523
jpeg: vectorize the inverse DCTs of the other sizes too
claude Oct 27, 2026
378144f
jpeg: one body for the two ways to decode an MCU, codes ready to be o…
claude Oct 27, 2026
6a7986c
jpeg: golden tests for CMYK and YCCK files, drop a field that was onl…
claude Oct 27, 2026
f1657b0
tmp: jpegdiff bench can profile the reference too
claude Oct 27, 2026
1a63634
jpeg: a struct for what fill_bytewise returns
claude Oct 27, 2026
535c0a9
jpeg: drop the record of warnings, which nothing in Bun reads
claude Oct 27, 2026
16b9d18
jpeg: remove stores and branches that have no effect
claude Oct 27, 2026
8ee24ed
jpeg: remove a field that is never read
claude Oct 27, 2026
22f3a8b
jpeg: the workspace's allocation error, decode() takes the decoder, s…
claude Oct 27, 2026
7bd46ad
jpeg: one helper for the low frequencies and for zigzag order, broken…
claude Oct 27, 2026
bf23ffc
jpeg: spell out names, comments in the present tense, golden tests re…
claude Oct 27, 2026
b175a40
jpeg: the encoder names its components and tables instead of numberin…
claude Oct 27, 2026
2f7caba
jpeg: no made-up values for broken invariants in the decoder either
claude Oct 27, 2026
53cf45f
jpeg: one description of an MCU's blocks, one way to derive a Huffman…
claude Oct 27, 2026
f91366f
jpeg: names in the arithmetic decoder and bits.rs, an internal error …
claude Oct 27, 2026
706576a
jpeg: the length of a long Huffman code by comparisons instead of a loop
claude Oct 27, 2026
f02396b
jpeg: YCbCr to RGB in 16 bits and 16 pixels at a time, the progressiv…
claude Oct 27, 2026
75c4701
jpeg: a lookup for whole short AC coefficients, a single stuffed FF w…
claude Oct 27, 2026
b960067
jpeg: no room for the whole image before the first scan has turned ou…
claude Oct 27, 2026
b0b3c5a
jpeg: the AC refinement pass goes through a band by bits instead of c…
claude Oct 27, 2026
0f176ca
jpegdiff: a tool that times both decoders on every file of a directory
claude Oct 27, 2026
b2e0ffa
jpeg: running out of memory for the list of APP2 segments is an error
claude Oct 27, 2026
ae96de1
Bun.Image: refuse a JPEG with more than 500 scans
claude Oct 27, 2026
825a666
jpeg: golden tests for 259 files that stand for bugs fixed in libjpeg…
claude Oct 27, 2026
b012cb3
jpeg: the whole-image array of a component is zeroed at its first sca…
claude Oct 27, 2026
5093d08
jpeg: every allocation of the decoder can fail
claude Oct 27, 2026
63485ca
jpeg: Highway kernels for color conversion, downsampling and upsampling
claude Oct 28, 2026
a1a8d9b
jpegdiff: build the kernels, choose their target, check them alone
claude Oct 28, 2026
93f1ab6
Merge branch 'main' into claude/libjpeg-turbo-rust-17fiip
claude Oct 28, 2026
8a69b45
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 28, 2026
fd0528b
test: the trimmed runtime has no texts of libjpeg-turbo to look for
claude Oct 28, 2026
392c15d
Merge branch 'main' into claude/libjpeg-turbo-rust-17fiip
claude Oct 29, 2026
d6c7a5f
jpeg: stop encoding once the output cannot grow, refuse a symbol with…
claude Oct 29, 2026
ab05c6f
jpeg: say what scaled_size takes, update a comment on the codecs, a n…
claude Oct 29, 2026
6f0f9b2
image: leave the encoded JPEG's buffer as it is, look for the JPEG co…
claude Oct 29, 2026
56be61e
jpegdiff: fail on no input, on a failed job and on a failed variant, …
claude Oct 29, 2026
0f31846
aot tests: do not pin what depends on the numbers of built-ins in one…
claude Oct 29, 2026
492ecf9
jpeg: quote the whole attribution of every header in NOTICE.md, and t…
claude Oct 29, 2026
c41020a
jpeg: list the headers whose definitions src/jpeg carries in NOTICE.md
claude Oct 29, 2026
e253a9e
license: state that Bun is based in part on the IJG's work, link the …
claude Oct 29, 2026
77ddcb1
Cargo.toml: give the real reason why bun_jpeg is optimized in debug b…
claude Oct 29, 2026
7f44580
image: document ERR_OUT_OF_MEMORY, and the limit of 500 scans in the …
claude Oct 29, 2026
b06b9bb
ci: the Rust lint jobs no longer install nasm
claude Oct 29, 2026
d785df3
ci: look for nasm only in the build step for Windows x64
claude Oct 29, 2026
e2fbe68
source lints: a new Highway kernel may have a file of its own
claude Oct 29, 2026
b33f3cf
image: point from the README to src/jpeg/NOTICE.md
claude Oct 29, 2026
c69886f
jpeg: golden tests look at the ICC profile, 135 more files, a picture…
claude Oct 29, 2026
9241ab8
image tests: the limits of the JPEG codec, profiles at the size of a …
claude Oct 29, 2026
0dc1745
image tests: give the JPEG row kernels rows that AddressSanitizer wat…
claude Oct 29, 2026
02b499f
simd-baseline: pin what the JPEG codec makes of the gradient, decode …
claude Oct 29, 2026
6f2a879
image: tests and documentation for the refusal of redundant JPEG scans
claude Oct 29, 2026
d35d9f6
jpeg: remove checks that cannot fail, state that can be computed and …
claude Oct 29, 2026
dbd2eda
jpeg: find each run's table and row once per row of MCUs in lossless …
claude Oct 29, 2026
0cfa507
jpeg: fail if the decoder did not produce every row
claude Oct 29, 2026
bc36ce3
jpeg kernels: refuse to compile without optimization
claude Oct 29, 2026
6e92e98
jpeg: refuse a scan of what an earlier scan has coded
claude Oct 29, 2026
e6ed930
jpeg: notice before the transform that the headers did not fit in memory
claude Oct 29, 2026
b40f034
Merge branch 'main' into claude/libjpeg-turbo-rust-17fiip
claude Oct 29, 2026
fe6e98d
jpegdiff: quote two paths, run exactly --iters iterations, seeds with…
claude Oct 29, 2026
c0c8191
jpeg: the bytes that fill_bytewise reads may not be the bytes its cal…
claude Oct 29, 2026
8f2951d
jpeg: golden tests expect a stream with a redundant scan to be refused
claude Oct 29, 2026
7575750
image: search for the next marker in exif.rs without asserting that t…
claude Oct 29, 2026
9055cc1
image tests: 17 more golden files for the Huffman decoders, DC predic…
claude Oct 29, 2026
03deace
jpeg: no AVX-512 variant of the row kernels
claude Oct 29, 2026
9f3a89a
jpegdiff: a redundant scan only excuses a stream that the reference d…
claude Oct 29, 2026
3ed3bb5
jpeg: the fallible allocation helpers get a module of their own
claude Oct 29, 2026
5e785f2
jpeg: the zigzag order and what goes by it get a module of their own
claude Oct 29, 2026
8cce61e
jpeg: Output moves from the Huffman coder to the file with most of it…
claude Oct 29, 2026
9dec26f
jpeg: #[rustfmt::skip] keeps the 8 by 8 tables in shape
claude Oct 29, 2026
868f299
jpeg: one countdown to the restart marker for the four entropy decoders
claude Oct 29, 2026
648084b
jpeg: both progressive decoders start a scan with the same function
claude Oct 29, 2026
6f751ec
jpeg: one formula for fixed-point constants, and every constant writt…
claude Oct 29, 2026
86becda
jpeg: names for a component's size in whole MCUs and for the side of …
claude Oct 29, 2026
61dc148
jpeg: the trait for a source of bits moves next to the two sources
claude Oct 29, 2026
1776642
jpeg: only the AC tables of sequential scans have the table of short …
claude Oct 29, 2026
ff8b314
jpeg: a Huffman table that several components share is derived once p…
claude Oct 29, 2026
4a21b5d
jpeg: no room for correction bits in the scans that have none
claude Oct 29, 2026
544413f
jpeg: the arithmetic decoder resets the statistics of a component once
claude Oct 29, 2026
fe7f1bd
jpeg: of the APP2 segments only the pieces of an ICC profile are kept
claude Oct 29, 2026
9f1d268
jpeg: libjpeg-turbo's texts for three errors
claude Oct 29, 2026
c1233a5
jpeg: the multipliers of the inverse DCT belong to the coefficient co…
claude Oct 29, 2026
a645e59
jpeg: no bare booleans where rows are emitted and upsampled
claude Oct 29, 2026
e183a21
jpeg: the initial state of a bit reader and of the arithmetic decoder…
claude Oct 29, 2026
9507d7a
jpeg: names for the numbers in the decoder
claude Oct 29, 2026
3ebff55
jpeg: the encoder takes sizes from the component's sampling factor in…
claude Oct 29, 2026
62d465e
jpeg: names for the numbers in the encoder
claude Oct 29, 2026
c1a0092
jpeg: the limit on Al is the standard's
claude Oct 29, 2026
e119174
jpeg: no bare booleans for the kind of frame and for the pass of the …
claude Oct 29, 2026
7157302
jpeg: bit_width from the standard library, and no name for two things…
claude Oct 29, 2026
3ea5cbb
jpeg: the output pass looks up a component's coefficients and multipl…
claude Oct 29, 2026
5b9fc97
jpeg: idct2x2 writes its shifts as idct4x4 does, and idct1x1 stays in…
claude Oct 29, 2026
31d7234
jpeg: one type for the decoder's rows of samples, differences and blocks
claude Oct 29, 2026
5d56595
jpeg: the same name in Rust and in C++ for what makes a row twice as …
claude Oct 29, 2026
b8ffb41
highway: the JPEG wrappers leave short rows to the caller without a call
claude Oct 29, 2026
de56d5b
jpeg: the testing hook for the kernels has no bare boolean for the on…
claude Oct 29, 2026
a1e7209
jpeg: one line for each comment of the kernels, their wrappers and th…
claude Oct 29, 2026
3660961
jpeg: one line for each comment of the decoder
claude Oct 29, 2026
3f3f802
jpeg: one line for each comment of the encoder and of the shared modules
claude Oct 29, 2026
3482297
image: one line for each comment that the change of the JPEG codec to…
claude Oct 29, 2026
6f0962d
jpeg: a loop for each lossless predictor, and a run of samples withou…
claude Oct 29, 2026
fff61aa
jpeg: zero_if_empty says what it does, and a component of full size t…
claude Oct 29, 2026
b01c1cf
jpeg: comments and a message that said too little or the wrong thing
claude Oct 29, 2026
4d06b29
jpeg: 14 more golden files, for changes to the decoder that the tests…
claude Oct 29, 2026
2ac3067
jpeg: the comment on the kernels' test hook says that downsampling re…
claude Oct 29, 2026
6c3fa24
jpeg: a test for a height that changes while the file is decoded
claude Oct 29, 2026
c1af7be
jpeg: 18 more golden files and 102 more pictures, for changes that th…
claude Oct 29, 2026
e80eec4
image tests: a race test that hangs leaves no child behind
claude Oct 29, 2026
2b80e8b
jpeg: 2 more golden files, for two more changes that the tests let th…
claude Oct 29, 2026
eb50974
image tests: the wrappers of the JPEG row kernels, with rows of diffe…
claude Oct 30, 2026
2ddcdc5
image tests: a type that `bun test --debug-aot` refuses
claude Oct 30, 2026
3a47a5b
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Oct 30, 2026
350d410
test: send a request again when the server never saw it, in three AOT…
claude Oct 30, 2026
8224fed
test: the same for the two other AOT node:http programs with that loop
claude Oct 30, 2026
1cb18cc
test: one shape of build-scaling grows 2.58 times in memory
claude Oct 30, 2026
788120f
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Oct 31, 2026
6f83dfd
image tests: the rule on redundant scans, where a wrong version of it…
claude Oct 31, 2026
26b67b1
image tests: eleven more golden files, for changes to the decoder tha…
claude Oct 31, 2026
f68fbeb
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Oct 31, 2026
0e9fda7
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Nov 1, 2026
07da79c
image tests: two more golden files, for two marker codes that could b…
claude Nov 1, 2026
e4011bf
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Nov 5, 2026
43b9eae
image tests: two more golden files, for a refinement scan that takes …
claude Nov 5, 2026
0d2debc
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Nov 7, 2026
7aab6fb
image tests: maxPixels in front of a JPEG decode, the order of the tw…
claude Nov 7, 2026
1e3c2a3
Merge remote-tracking branch 'origin/main' into claude/libjpeg-turbo-…
claude Nov 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .buildkite/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,12 @@ function getBuildCommand(target: Target, options: PipelineOptions): string {
* deps + C++ + Rust + link on one agent.
*/
function getBuildBunStep(platform: Platform, options: PipelineOptions): CommandStep {
const { arch } = platform;
// Best-effort nasm for x64 (BoringSSL win-x64, libjpeg-turbo SIMD); images bake it, and the build's own error is clearer.
const { os, arch } = platform;
// Best-effort nasm for BoringSSL's win-x64 assembly; images bake it, and the build's own error is clearer.
const nasmSetup =
arch === "x64"
os === "windows" && arch === "x64"
? [
"which nasm || (apt-get update -qq && apt-get install -y -qq nasm) || dnf install -y -q nasm || yum install -y -q nasm || brew install nasm || true",
"which nasm || (apt-get update -qq && apt-get install -y -qq nasm) || dnf install -y -q nasm || yum install -y -q nasm || true",
]
: [];
return {
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/rust-lint-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ runs:
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-${LLVM_VERSION_MAJOR} main" | sudo tee /etc/apt/sources.list.d/llvm.list > /dev/null
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends cmake ninja-build nasm clang-${LLVM_VERSION_MAJOR} lld-${LLVM_VERSION_MAJOR} llvm-${LLVM_VERSION_MAJOR}
sudo apt-get install -y -qq --no-install-recommends cmake ninja-build clang-${LLVM_VERSION_MAJOR} lld-${LLVM_VERSION_MAJOR} llvm-${LLVM_VERSION_MAJOR}

- name: Setup Rust
if: inputs.toolchain != ''
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ test/expected-durations.json
# vendored, generated bundle of github.com/jarred-sumner/xmac (see its header)
scripts/build/xmac.mjs

# libjpeg-turbo's, which has to stay as it is
src/jpeg/LICENSE.md

# the output codeblocks need to stay minified
docs/bundler/minifier.mdx
# Prettier would reformat the code examples, and test/cli/lint/rule-docs.test.ts runs them exactly as written
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"src/bundler",
"src/collections",
"src/css",
"src/errno",

Check warning on line 11 in Cargo.toml

View workflow job for this annotation

GitHub Actions / lol-html cargo test

explicit `package.readme` can be inferred
"src/glob",
"src/http",
"src/install",
Expand Down Expand Up @@ -69,6 +69,7 @@
"src/trim/removable_features",
"src/highway",
"src/histogram",
"src/jpeg",
"src/http_jsc",
"src/http_types",
"src/ini",
Expand Down Expand Up @@ -449,6 +450,7 @@
bun_removable_features = { path = "src/trim/removable_features" }
bun_highway = { path = "src/highway" }
bun_histogram = { path = "src/histogram" }
bun_jpeg = { path = "src/jpeg" }
bun_http_jsc = { path = "src/http_jsc" }
bun_http_types = { path = "src/http_types" }
bun_ini = { path = "src/ini" }
Expand Down Expand Up @@ -514,6 +516,11 @@
bun_which = { path = "src/which" }
bun_transpiler = { path = "src/transpiler" }

# The codec's loops run for every sample of an image, and at opt-level 0 they take 20 to 60 times as long.
# The overflow checks and debug assertions stay.
[profile.dev.package.bun_jpeg]
opt-level = 3

# Loops over every byte of an image: unoptimized, a debug build takes seconds per megapixel. Assertions and overflow
# checks stay on. scripts/build/flags.ts does the same for highway_png.cpp.
[profile.dev.package.bun_png]
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Bun statically links these libraries:
| [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) | BSD 2-Clause |
| [`libuv`](https://github.com/libuv/libuv) (on Windows) | MIT |
| [`libdeflate`](https://github.com/ebiggers/libdeflate) | MIT |
| [`libjpeg-turbo`](https://github.com/libjpeg-turbo/libjpeg-turbo) | [BSD 3-Clause / IJG / zlib](https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/LICENSE.md) |
| [`libjpeg-turbo`](https://github.com/libjpeg-turbo/libjpeg-turbo) (not linked: `src/jpeg` is derived from parts of it). This software is based in part on the work of the Independent JPEG Group | [BSD 3-Clause / IJG / zlib](src/jpeg/LICENSE.md) |
| [`libspng`](https://github.com/randy408/libspng) (not linked: `src/png` is derived from parts of it) | BSD 2-Clause |
| [`stb_image`](https://github.com/nothings/stb) (not linked: the PNG codec finds the Paeth predictor its way) | MIT or public domain |
| [`libwebp`](https://github.com/webmproject/libwebp) | BSD 3-Clause |
Expand Down
4 changes: 3 additions & 1 deletion docs/project/license.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Bun statically links these libraries:
| [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) | BSD 2-Clause |
| [`libuv`](https://github.com/libuv/libuv) (on Windows) | MIT |
| [`libdeflate`](https://github.com/ebiggers/libdeflate) | MIT |
| [`libjpeg-turbo`](https://github.com/libjpeg-turbo/libjpeg-turbo) | BSD 3-Clause / IJG / zlib |
| [`libjpeg-turbo`](https://github.com/libjpeg-turbo/libjpeg-turbo) (not linked: `src/jpeg` is derived from parts of it) | [BSD 3-Clause / IJG / zlib](https://github.com/oven-sh/bun/blob/main/src/jpeg/LICENSE.md) |
| [`libspng`](https://github.com/randy408/libspng) (not linked: `src/png` is derived from parts of it) | BSD 2-Clause (portions derived from libpng, PNG Reference Library License v2) |
| [`stb_image`](https://github.com/nothings/stb) (not linked: the PNG codec finds the Paeth predictor its way) | MIT or public domain |
| [`libwebp`](https://github.com/webmproject/libwebp) | BSD 3-Clause |
Expand All @@ -54,6 +54,8 @@ Bun statically links these libraries:
| A fork of [`uWebsockets`](https://github.com/jarred-sumner/uwebsockets) | Apache 2.0 licensed |
| Parts of [Tigerbeetle's IO code](https://github.com/tigerbeetle/tigerbeetle/blob/532c8b70b9142c17e07737ab6d3da68d7500cbca/src/io/windows.zig#L1) | Apache 2.0 licensed |

The IJG license requires this statement: this software is based in part on the work of the Independent JPEG Group.

## Polyfills

For compatibility, Bun embeds the following packages into its binary and injects them if imported.
Expand Down
22 changes: 12 additions & 10 deletions docs/runtime/image.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Image
description: Decode, transform, and encode images with a fast native pipeline
---

`Bun.Image` is a chainable image pipeline for decoding, resizing, rotating, and re-encoding JPEG, PNG, WebP, HEIC, and AVIF. It is built on libjpeg-turbo, libwebp, a built-in PNG codec, and SIMD geometry kernels, with zero npm dependencies and no native addon build step.
`Bun.Image` is a chainable image pipeline for decoding, resizing, rotating, and re-encoding JPEG, PNG, WebP, HEIC, and AVIF. It is built on built-in JPEG and PNG codecs, libwebp, and SIMD geometry kernels, with zero npm dependencies and no native addon build step.

```ts
await Bun.file("photo.jpg").image().resize(400, 400, { fit: "inside" }).webp({ quality: 80 }).write("thumb.webp");
Expand All @@ -29,7 +29,7 @@ Bun sniffs the format from the bytes and ignores extensions and `Content-Type`.

When passing a `TypedArray`/`ArrayBuffer`, don't mutate it while a terminal is pending — decode runs off-thread and borrows the bytes. Bun refuses `SharedArrayBuffer` and resizable buffers; use `buf.slice()` to pass a fixed view.

Bun decodes a damaged JPEG when libjpeg-turbo can finish the decode with only a warning. Examples are stray bytes between segments, a missing end-of-image marker, and truncated scan data. libjpeg-turbo fills the blocks that have no data with flat grey. A progressive file that is cut after its first scan has data for every block, so Bun returns the whole picture with less detail. When libjpeg-turbo cannot finish the decode, the terminal rejects with `ERR_IMAGE_DECODE_FAILED`. `metadata()` reads only the header, so it can resolve for a file that a terminal rejects.
Bun decodes a damaged JPEG in the cases where libjpeg-turbo finishes the decode with only a warning. Examples are stray bytes between segments, a missing end-of-image marker, and truncated scan data. The blocks that have no data come back flat grey. A progressive file that is cut after its first scan has data for every block, so Bun returns the whole picture with less detail. When the decode cannot finish, the terminal rejects with `ERR_IMAGE_DECODE_FAILED`. The terminal also rejects with `ERR_IMAGE_DECODE_FAILED` for a JPEG with more than 500 scans, and for one with a scan of what an earlier scan has already coded. Encoders write about 10 scans and none of them twice. Each scan costs a pass over the whole image, however few bytes it has. `metadata()` reads only the header, so it can resolve for a file that a terminal rejects.

Bun does not verify the checksums in a PNG. It skips the CRC of each chunk and the Adler-32 of the pixel data, so a file with damaged bytes can decode to different pixels. Bun reads `IDAT` chunks until one of them completes the last row, and ignores everything after the data of that chunk. When the file ends before the data of that chunk does, the terminal rejects with `ERR_IMAGE_DECODE_FAILED`. The terminal also rejects when the pixel data ends before the last row.

Expand All @@ -47,6 +47,8 @@ new Bun.Image(input, {

A decoded image takes 4 bytes per pixel, so the default `maxPixels` allows 1 GiB for one image. A PNG of 32 KiB can decode to that size. The decode needs more memory while it runs: up to 20 bytes per pixel for a PNG that is one very wide row, which is 5 GiB at the default. For untrusted input, set `maxPixels` to the largest image your application accepts.

A terminal rejects with `ERR_OUT_OF_MEMORY` when a codec reports a failed allocation. The JPEG codec reports all of its own. Elsewhere in the pipeline a failed allocation can end the process. Keep `maxPixels` within what your memory allows: Bun checks it before it allocates the pixel buffer.

## Metadata

Read `width`, `height`, and `format` without decoding pixel data:
Expand Down Expand Up @@ -186,14 +188,14 @@ For a passive "image in clipboard, press ⌘V" hint, poll `clipboardChangeCount(

## Platform backends

| | Linux | macOS | Windows |
| ---------------------- | ---------------------------------- | ------------------- | ------------ |
| JPEG / PNG / WebP | libjpeg-turbo · built-in · libwebp | same | same |
| BMP / GIF (decode) | built-in | ImageIO | WIC |
| TIFF (decode) | ❌ | ImageIO | WIC |
| Resize / rotate / flip | Highway SIMD | Accelerate vImage ³ | Highway SIMD |
| HEIC / AVIF | ❌ `ERR_IMAGE_FORMAT_UNSUPPORTED` | ImageIO ² | WIC ¹ |
| Clipboard | ❌ returns `null` | NSPasteboard | Win32 |
| | Linux | macOS | Windows |
| ---------------------- | --------------------------------- | ------------------- | ------------ |
| JPEG / PNG / WebP | built-in · built-in · libwebp | same | same |
| BMP / GIF (decode) | built-in | ImageIO | WIC |
| TIFF (decode) | ❌ | ImageIO | WIC |
| Resize / rotate / flip | Highway SIMD | Accelerate vImage ³ | Highway SIMD |
| HEIC / AVIF | ❌ `ERR_IMAGE_FORMAT_UNSUPPORTED` | ImageIO ² | WIC ¹ |
| Clipboard | ❌ returns `null` | NSPasteboard | Win32 |

¹ Windows requires the **HEIF Image Extensions** / **AV1 Video Extension** from the Microsoft Store.
² AVIF _encode_ needs an OS AV1 encoder — Apple Silicon M3+ only. Intel Mac and M1/M2 reject with `ERR_IMAGE_FORMAT_UNSUPPORTED`; AVIF _decode_ works everywhere ImageIO does (macOS 13+).
Expand Down
4 changes: 4 additions & 0 deletions misctools/jpegdiff/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target*/
Cargo.lock
kernels
kernels-arm64
19 changes: 19 additions & 0 deletions misctools/jpegdiff/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "jpegdiff"
version = "0.0.0"
edition = "2024"

[workspace]

[dependencies]
bun_jpeg = { path = "../../src/jpeg" }

[profile.release]
debug = "line-tables-only"
overflow-checks = true
debug-assertions = true

[profile.fast]
inherits = "release"
overflow-checks = false
debug-assertions = false
36 changes: 36 additions & 0 deletions misctools/jpegdiff/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# jpegdiff

Compares `src/jpeg` (`bun_jpeg`) with libjpeg-turbo 3.2.0 as Bun linked it. Temporary: it is here while the replacement
is under review, so that whoever works on it has the same tools, and goes away before the merge.

Set-up, on x86-64 Linux with clang, nasm and llvm-ar:

1. `git clone https://github.com/libjpeg-turbo/libjpeg-turbo /root/ref/libjpeg-turbo` and check out
`c85e6b905bf237038faa936dab160ebfc5da0344`.
2. Copy it to `/root/ref/libjpeg-turbo-patched` and apply what Bun applied, from the commit before the removal:
`git show 92e819d8:patches/libjpeg-turbo/<file>` for each file of `git ls-tree 92e819d8 patches/libjpeg-turbo/`
(`scripts/build/deps/libjpeg-turbo.ts` at that commit says how: two patches and the `jbun_stubs.c` overlay).
3. `mkdir -p /root/ref/build && cp -r include /root/ref/build/` (the headers that the build generated), then
`./build-ref.sh`, which writes `/root/ref/build/libtjref.a`.
4. `vendor/highway` has to be there, which it is after any build of Bun (`scripts/build/deps/highway.ts` has the commit and
two patches): `build.rs` compiles `src/jsc/bindings/highway_jpeg.cpp`, the kernels that `bun_jpeg` calls, with `clang++-23`.
5. `cargo build --release` here. That profile keeps overflow checks and debug assertions; `--profile fast` is for timing.

Programs:

- `jpegdiff <file or dir>...`: decodes at every scale from 8/8 to 1/8 with both and compares pixels, size, ICC profile,
whether it fails, and at which stage. A stream that `bun_jpeg` refuses for a redundant scan, which libjpeg-turbo
decodes, is counted as `redundant-scan` and not compared.
- `mutate <dir>... --iters N --seed S --threads T`: the same for random mutations of the files. Failures are saved to
`/root/ref/corpus/fail`.
- `encfuzz --iters N --seed S --threads T --max-dim D`: encodes generated images with both and compares the bytes.
- `encspecial`: encoder cases that random images do not reach.
- `bench [file.jpg]...`: times both.
- `tools/kernels.sh`: checks the kernels alone, for every target of this machine and under qemu for those of arm64.

`HWY_TARGET=SSSE3` (or `AVX2`) in the environment makes any of the programs use the kernels for that target and not
for the best one that the CPU has.

`src/reference.rs` replays what `src/runtime/image/codec_jpeg.rs` did at 92e819d8. `tools/gen_corpus.sh` makes the seed
corpus with libjpeg-turbo's `cjpeg` and `jpegtran`. `regress/` holds inputs that once differed. The reference takes
`JSIMD_FORCENONE=1` and `JSIMD_FORCESSE2=1` in the environment.
24 changes: 24 additions & 0 deletions misctools/jpegdiff/build-ref.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
# Builds /root/ref/build/libtjref.a from the patched libjpeg-turbo, the way scripts/build/deps/libjpeg-turbo.ts did (x64, release flags).
set -euo pipefail
SRC=/root/ref/libjpeg-turbo-patched
OUT=/root/ref/build
CC=${CC:-clang-23}
EXTRA=${EXTRA:-}
OBJ=$OUT/obj${SUFFIX:-}
rm -rf "$OBJ"
mkdir -p $OBJ
JPEG8="jcapimin jcapistd jccoefct jccolor jcdctmgr jcdiffct jchuff jcicc jcinit jclhuff jclossls jcmainct jcmarker jcmaster jcomapi jcparam jcphuff jcprepct jcsample jctrans jdapimin jdapistd jdatadst jdatasrc jdcoefct jdcolor jddctmgr jddiffct jdhuff jdicc jdinput jdlhuff jdlossls jdmainct jdmarker jdmaster jdmerge jdphuff jdpostct jdsample jdtrans jfdctflt jfdctfst jfdctint jidctflt jidctfst jidctint jidctred jaricom jcarith jdarith jerror jmemmgr jmemnobs jquant1 jquant2 jutils jpeg_nbits"
TJ="turbojpeg transupp jdatadst-tj jdatasrc-tj"
SIMD="jsimdcpu jfdctflt-sse jccolor-sse2 jcgray-sse2 jchuff-sse2 jcphuff-sse2 jcsample-sse2 jdcolor-sse2 jdmerge-sse2 jdsample-sse2 jfdctfst-sse2 jfdctint-sse2 jidctflt-sse2 jidctfst-sse2 jidctint-sse2 jidctred-sse2 jquantf-sse2 jquanti-sse2 jccolor-avx2 jcgray-avx2 jcsample-avx2 jdcolor-avx2 jdmerge-avx2 jdsample-avx2 jfdctint-avx2 jidctint-avx2 jquanti-avx2"
CFLAGS="-march=nehalem -O2 -g -fPIC -fno-omit-frame-pointer -I$SRC/src -I$OUT/include -DBUN_8BIT_ONLY -DUSE_CLZ_INTRINSIC $EXTRA"
for f in $JPEG8 $TJ; do $CC $CFLAGS -c $SRC/src/$f.c -o $OBJ/$f.o & done
$CC $CFLAGS -c $SRC/jbun_stubs.c -o $OBJ/jbun_stubs.o &
$CC $CFLAGS -c $SRC/simd/jsimd.c -o $OBJ/jsimd.o &
for f in $SIMD; do nasm -felf64 -DELF -D__x86_64__ -DPIC -I$SRC/simd/nasm/ -I$SRC/simd/x86_64/ -o $OBJ/$f.o $SRC/simd/x86_64/$f.asm & done
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
failed=0
for job in $(jobs -p); do wait "$job" || failed=1; done
[ $failed = 0 ]
rm -f $OUT/libtjref${SUFFIX:-}.a
llvm-ar-23 rcs $OUT/libtjref${SUFFIX:-}.a $OBJ/*.o
ls -la $OUT/libtjref${SUFFIX:-}.a
Loading
Loading