Skip to content

std/jpeg ​

Decode and encode JPEG (ITU T.81, JFIF/Exif) in pure Milo. The decoder reads baseline and progressive Huffman-coded files: grayscale and YCbCr with any integral chroma subsampling (4:4:4, 4:2:2, 4:4:0, 4:2:0, 4:1:1), Adobe RGB, CMYK and YCCK, and restart markers. The encoder writes baseline JFIF at quality 1-100 with 4:2:0 or 4:4:4 chroma.

Both sides reproduce libjpeg-turbo's default integer arithmetic, so a decoded image is byte-for-byte what djpeg produces and an encoded file is byte-for-byte what cjpeg -baseline writes for the same pixels and quality. scripts/jpeg-grade.ts checks both against the installed libjpeg-turbo, and the fixtures pin frozen outputs of it.

Arithmetic-coded, lossless, hierarchical and 12-bit files are rejected with an error naming the feature. Decoded pixels are std/image's Image: 1 channel for grayscale files, 3 (RGB) for everything else.

milo
from "std/jpeg" import { Jpeg, JpegSubsampling }

Example ​

milo
from "std/fs" import { readFile, writeFile }
from "std/image" import { Image, ResizeFilter }

let src = readFile("photo.jpg")!
// Cameras store the sensor's orientation and tag the file instead of rotating pixels.
let img = Jpeg.decode(src)!.oriented(Jpeg.orientation(src))
let thumb = img.resize(320, 320 * img.height / img.width, ResizeFilter.Area)!
writeFile("thumb.jpg", Jpeg.encode(thumb, 85, JpegSubsampling.Chroma420)!)!

Limits ​

A frame over JPEG_MAX_PIXELS (2^27, about 134 megapixels) is refused before anything is allocated, as is a header that promises more blocks than the file has bits to code, so a hostile header cannot ask for gigabytes. A file with more than 1000 scans is refused too: each scan walks every block it covers. Truncated entropy data is an error rather than a gray-filled partial image; damaged data that runs into a marker decodes on zeros, as it does in libjpeg. scripts/fuzz-jpeg.ts holds the decoder to "Ok or Err, never a trap" over mutated and truncated files under a memory cap.

API reference ​

Jpeg ​

milo
pub struct Jpeg

Jpeg namespace: Jpeg.decode(src) reads a JPEG, Jpeg.encode(img, ...) writes one, Jpeg.orientation(src) reads the EXIF orientation tag.

Jpeg.decode ​

milo
fn Jpeg.decode(src: &string): Result<Image, string>

Decode a baseline or progressive JPEG to an Image: 1 channel for a grayscale file, 3 (RGB) for everything else (YCbCr, RGB, CMYK/YCCK flattened through K). Pixels are as stored: EXIF orientation is NOT applied (see Jpeg.orientation and Image.oriented). Err on a malformed or truncated file, on arithmetic-coded, lossless, hierarchical or 12-bit JPEG, and on a frame over JPEG_MAX_PIXELS.

Jpeg.encode ​

milo
fn Jpeg.encode(img: &Image, quality: i64, subsampling: JpegSubsampling): Result<string, string>

Encode img as a baseline JFIF JPEG. quality is 1..100 on cjpeg's scale (the Annex K tables at 50, scaled; 75-90 is the usual range). A 1-channel image is written as grayscale and subsampling is ignored; a 4-channel image loses its alpha. Err on a channel count other than 1/3/4, a side outside 1..65535, a pixel buffer of the wrong length, or a quality outside 1..100.

Jpeg.orientation ​

milo
fn Jpeg.orientation(src: &string): i64

The EXIF orientation tag (1-8; 1 = upright) from the file's APP1 segment, or 1 when there is none. Only the headers before the first scan are read.

JpegSubsampling ​

milo
pub enum JpegSubsampling

How much chroma resolution the encoder keeps. Chroma420 averages each 2x2 block of Cb/Cr (cjpeg's default, half the data); Chroma444 keeps every sample.

Variants:

  • Chroma420
  • Chroma444