Table of Contents

JPEG

JPEG files load like any other: baseline, extended and progressive, Huffman or arithmetic-coded, gray, YCbCr, RGB, CMYK and YCCK, any chroma subsampling. The decoded pixels match libjpeg-turbo's djpeg exactly.

Arithmetic, 12-bit and lossless files

Arithmetic-coded files load like Huffman-coded ones. 12-bit files load in their natural 16-bit types (Gray16, Rgb16, or Cmyk16 for four components), each sample scaled so 4095 becomes 65535; ask for an 8-bit type and they are rounded to it. Lossless JPEG, the kind inside DNG tiles and many DICOM files, decodes to its exact samples at 2 to 16 bits: up to 8 bits as Gray8 or Rgb8, deeper as Gray16 or Rgb16. Three-component lossless files are RGB unless a JFIF segment or Adobe transform says YCbCr. Lossless files have no DCT, so MinimumSize reduces them after a full decode. JpegDetails.Precision, Arithmetic and Lossless say what a file is; SampleImage.Load gives the stored samples at the file's own precision, without color conversion (see Stored samples). Hierarchical files are refused with UnsupportedFormatException.

Browsers show none of these modes. Egami writes each of them on request (below).

Threads

A large file decodes on up to MaxParallelism threads with the same pixels as one thread. A file with restart markers is split at the markers already in it; a progressive file's scans run as a pipeline, each refinement scan a few rows behind the scan before it, and the last pass is split by rows. A sequential file without markers overlaps its entropy decoding with the rest on a second thread. Damaged data falls back to one thread, so warnings and recovery are as before.

Scaled decode

// At least 100 × 75 from the stored JPEG, decoded at 1/2, 1/4 or 1/8 scale straight from the DCT.
using Raster preview = Raster.Load("duck.jpg", options: new ReaderOptions { MinimumSize = new Size(100, 75) });

ReaderOptions.MinimumSize picks the smallest of 1/1, 1/2, 1/4 and 1/8 scale that still covers the size you need, and decodes at that scale straight from the DCT. For a thumbnail from a 12-megapixel photo it is many times faster than decoding in full and resizing. Other formats ignore it.

Orientation

Cameras and phones store pixels as the sensor saw them and say in EXIF how to turn them. Raster.Probe reports the orientation, and AutoOrient applies it; see resizing and geometry. To turn the file itself upright without re-encoding it, use JpegTransform with AutoOrient (see lossless rotation).

Writing

using FileStream output = File.Create("duck-q85.jpg");
image.Save(output, JpegFormat.Info, new JpegWriterOptions
{
    Quality = 85,                                  // 75 by default, on libjpeg's scale
    Subsampling = ChromaSubsampling.Subsample444,  // 4:2:0 by default
    OptimizeHuffman = true,                        // a few percent smaller, a little slower
});
Option Default Does
Quality 75 1 to 100, the same scale as libjpeg's cjpeg
Subsampling Subsample420 Subsample422 or Subsample444 keep more color detail
OptimizeHuffman false Tables built for the image: a few percent smaller
ColorType Auto Gray writes one gray component whatever the pixel type; Cmyk and Ycck write four (Auto picks Cmyk for CMYK pixels)
Progressive false Scans that show a coarse image first and refine it, with tables optimized per scan
Arithmetic false Arithmetic coding instead of Huffman: smaller files, same pixels, no browser support
Lossless none A JpegLosslessOptions writes lossless JPEG: the exact samples, no DCT
Precision 8 12 keeps more of 16-bit and float pixels
RestartInterval 0 Restart markers every so many blocks, for damaged-stream recovery
Background none Composites alpha onto this color first; without it alpha is ignored

Progressive

// Shows coarse first and sharpens as it loads; usually a few percent smaller, and decodes to the same pixels.
image.Save("duck-progressive.jpg", new JpegWriterOptions { Progressive = true });

Progressive writes the scan script libjpeg's cjpeg -progressive writes: ten scans for color, six for gray, spectral selection and successive approximation, each scan with its own optimized Huffman tables. The file decodes to exactly the pixels the baseline file at the same quality gives, and is the same file cjpeg -progressive writes. The encoder holds every quantized coefficient until the last scan, about 2 bytes per sample (72 MB for a 24-megapixel 4:2:0 photo), counted against SafetyLimits.MaxTotalBytes; a streamed encode buffers them. Re-saving a progressive file writes baseline unless you set it.

CMYK and YCCK

// CMYK pixels save as CMYK, inverted behind an Adobe marker as Photoshop writes; YCCK is usually smaller.
using Raster<Cmyk8> inks = image.As<Cmyk8>();
inks.Save("duck-cmyk-out.jpg", JpegWriterOptions.Default);
inks.Save("duck-ycck.jpg", new JpegWriterOptions { ColorType = JpegColorType.Ycck });

Cmyk8 and Cmyk16 pixels save as four CMYK components with an Adobe marker, the values inverted as Photoshop writes them and every reader expects, and no JFIF segment (JFIF covers gray and YCbCr only). Ycck converts cyan, magenta and yellow to YCbCr as libjpeg does and subsamples their chroma by Subsampling, keeping black at full size; it is usually smaller. Other pixel types written as Cmyk or Ycck are converted with the plain formula. djpeg, ImageMagick and Pillow read the files to the same inks.

12-bit

// 16-bit and float pixels keep more than 8 bits in a 12-bit file; it loads back as Rgb16. No browser shows it.
using Raster<Rgb16> deep = image.As<Rgb16>();
deep.Save("duck-12bit.jpg", new JpegWriterOptions { Precision = 12, Quality = 95 });

Precision = 12 writes 12-bit samples (an extended, SOF1 file, or SOF2 when progressive) from any pixel type, narrowing 16-bit samples to 12 bits; it is worth it for 16-bit and float pixels. The Huffman tables are always optimized, as libjpeg-turbo does at 12 bits, and quantization tables pass 255 at low quality, in 16-bit entries. libjpeg-turbo's djpeg reads them; browsers, Pillow and many ImageMagick builds do not.

Arithmetic coding

// Arithmetic coding: the same pixels in a smaller file, for archives; browsers do not read it.
image.Save("duck-arithmetic.jpg", new JpegWriterOptions { Quality = 90, Arithmetic = true });

// An existing Huffman-coded file repacked with arithmetic coding, with no change to its pixels.
JpegTransform.Apply("duck-progressive.jpg", "duck-repacked.jpg", new JpegTransformOptions { Arithmetic = true });

Arithmetic codes the scans with T.81's adaptive arithmetic coder instead of Huffman codes: a sequential (SOF9) file, or a progressive (SOF10) one with Progressive, at 8 or 12 bits, in every color type, subsampling and restart interval. The quantized coefficients are the same, so the file decodes to exactly the pixels of the Huffman file with the same options, and is about 4 to 8 % smaller than with optimized Huffman tables; it is no larger than cjpeg -arithmetic's file at the same settings (libjpeg adds a 12-byte DAC segment Egami leaves out). A sequential file needs no tables, so it streams in one pass at either precision and ignores OptimizeHuffman. libjpeg-turbo, ImageMagick and Egami read these files; browsers do not, and Pillow fails on some. They suit archives, not the web. An Ultra HDR gain map is left out with a Save.GainMapDropped warning, and GainMap with Arithmetic throws NotSupportedException. Re-saving an arithmetic file writes Huffman codes unless you set it.

Lossless

// Lossless JPEG: the exact samples back, here 16-bit; the predictor is picked for the image unless you name one.
image.As<Rgb16>().Save("duck-lossless.jpg", new JpegWriterOptions { Lossless = new JpegLosslessOptions() });

// Predictor 1 (the sample to the left), arithmetic-coded (SOF11, which only Egami and Richter's libjpeg read).
image.Save("duck-lossless-arithmetic.jpg", new JpegWriterOptions
{
    Lossless = new JpegLosslessOptions { Predictor = 1 },
    Arithmetic = true,
});

Lossless writes lossless JPEG (T.81 annex H, the kind DNG raw tiles and DICOM use): no DCT and no quantization, each sample predicted from its neighbors and the difference coded, so the file loads back to exactly the samples written. The components are the pixels' own: gray, RGB behind an Adobe marker, or CMYK, never subsampled or converted to YCbCr. Quality, Subsampling, OptimizeHuffman and Precision do not apply; Progressive or ColorType = Ycck with it throws ArgumentException.

// Samples that are not pixels, such as a camera's 14-bit sensor data, as a lossless JPEG stream (a DNG or TIFF tile).
ushort[] samples = new ushort[64 * 32];
for (int i = 0; i < samples.Length; i++)
{
    samples[i] = (ushort)((i * 37) % 16384);
}

using (MemoryStream tile = new())
{
    JpegLosslessWriter.Write(tile, samples, components: 1, width: 64, precision: 14, predictor: null);

    // A colour filter array tile as DNG lays it out: each row's photosites coded in pairs, half as wide.
    using MemoryStream cfa = new();
    JpegLosslessWriter.WriteCfa(cfa, samples, width: 64, precision: 14);
    Assert.True(tile.Length > 0 && cfa.Length > 0);
}

JpegLosslessWriter codes samples that are not pixels, at 2 to 16 bits with 1 to 4 components, exactly as saving the same samples as pixels codes them; it is how TIFF's and DNG's lossless JPEG tiles are written.

JpegLosslessOptions Default Does
Predictor automatic 1 to 7, T.81's predictors (1 left, 2 above, 3 above left, 4 to 7 combinations); automatic tries each on rows spread through the image and keeps the smallest
PointTransform 0 Drops that many low bits from every sample first: smaller, no longer lossless (reported as Save.DepthReduced)
Precision the pixel type's 2 to 16 bits; 8 for 8-bit pixel types, 16 for 16-bit and float ones. Fewer bits round the samples (reported)

The Huffman tables are always made for the image, so every sample is held until the end, about 2 bytes each, counted against SafetyLimits.MaxTotalBytes. RestartInterval is rounded up to whole rows of pixels, as T.81 asks of lossless files. With Arithmetic the file is SOF11, up to a few percent smaller (6 % on an 8-bit photo, the same at 16 bits); only Egami and Thomas Richter's libjpeg read SOF11, while libjpeg-turbo, DNG and DICOM readers read the Huffman-coded SOF3. libjpeg-turbo's djpeg decodes Egami's SOF3 files to exactly the samples written, and at the same predictor they are within 0.2 % of cjpeg -lossless's size. No browser reads either. An Ultra HDR gain map is left out with a Save.GainMapDropped warning, and GainMap with Lossless throws NotSupportedException.

Lossless rotation, flips and crops

// Turns the photo upright by its EXIF orientation without re-encoding: no quality is lost, and the tag is reset.
JpegTransform.Apply("sideways.jpg", "upright.jpg", new JpegTransformOptions { Operation = JpegTransformOperation.AutoOrient });

// Rotates, crops and drops the color in one pass; a file can be transformed in place.
JpegTransformResult result = JpegTransform.Apply("upright.jpg", "upright.jpg", new JpegTransformOptions
{
    Operation = JpegTransformOperation.Rotate90,
    Crop = new Rect(0, 0, 200, 120),
    Grayscale = true,
});
Console.WriteLine($"{result.Width} × {result.Height}");

JpegTransform turns, mirrors, crops and drops the color of a JPEG without decoding it to pixels, as jpegtran does: the quantized DCT coefficients move and change sign block by block, and are coded again, so the result has exactly the quality the file had. Quantization tables and chroma subsampling stay as they were. It takes every mode the reader takes except lossless JPEG: baseline, progressive, arithmetic, 12-bit, gray, CMYK and YCCK; the result is Huffman-coded unless Arithmetic is set, so an existing file can also be repacked smaller with no change to its pixels (jpegtran -arithmetic). The result decodes to the same pixels as the file jpegtran makes with the same switches, on every subsampling and edge case.

Option Default Does
Operation None FlipHorizontal, FlipVertical, Rotate90, Rotate180, Rotate270, Transpose, Transverse, or AutoOrient: what the EXIF orientation needs, then the tag is reset to normal
Crop none The area to keep, after the operation; its top-left corner moves to the MCU grid (8 or 16 pixels), as jpegtran -crop does
Grayscale false Keeps only luma: a gray file with no loss. YCbCr files only
Edges Trim What happens at a mirrored edge that is not a whole number of MCUs (below)
Progressive false Writes progressive scans, as JpegWriterOptions.Progressive does
OptimizeHuffman true Tables built for the result, a few percent smaller than the standard ones
Arithmetic false Arithmetic coding, as JpegWriterOptions.Arithmetic writes: smaller, same pixels
CopyMetadata true Copies EXIF, ICC, XMP, IPTC, comments and other segments

A block of 8 × 8 pixels moves as a whole, so an edge that is not a whole number of MCUs (8 pixels, or 16 with subsampled chroma) cannot be mirrored to the other side intact. JpegTransformEdges.Trim drops that partial MCU from each mirrored edge, so the image loses up to 15 pixels there, as jpegtran -trim does; Strict throws instead, as -perfect does; Keep leaves it in place unmirrored, as jpegtran does by default. Edges that are not mirrored keep every pixel, and Transpose never loses any.

EXIF's pixel dimensions follow the result, and its thumbnail gets the same rotation, flip and gray conversion; after a crop the thumbnail is dropped, since it would still show the whole picture. An MPF index (the extra images some cameras append) is dropped too. Other segments are copied byte for byte. The result's size and any warnings about a damaged source are in the returned JpegTransformResult.

The whole image's coefficients are held while it runs, about 2 bytes per sample, counted against SafetyLimits.MaxTotalBytes. A 12-megapixel photo turns in 170 ms with optimized tables (jpegtran -optimize: 305 ms), 130 ms with the standard ones (jpegtran: 246 ms) and 260 ms written progressive (jpegtran: 458 ms).

What the file says

using Raster image = Raster.Load("duck.jpg");
if (image.Metadata.FormatDetails is JpegDetails jpeg)
{
    Console.WriteLine($"Progressive: {jpeg.Progressive}, saved at about quality {jpeg.EstimatedQuality}");
}

EXIF, XMP (including extended XMP) and ICC profiles are read and written back. A JPEG saved again is re-encoded: pixels lose a little each time, so keep a lossless master if you edit repeatedly, or use JpegTransform for rotations, flips and crops.

Details

The components, color space and estimated quality are in JpegDetails. JpegReaderOptions.FastUpsampling trades a little chroma sharpness for speed; JpegColorType.Gray writes one channel.

CMYK and YCCK files load as RGB by the plain formula, R = (1 − C)(1 − K). Raster.Load<Cmyk8> returns the file's ink amounts instead, without the RGB step; print-accurate color needs the embedded ICC profile, which Egami keeps but does not apply yet.

using Raster photo = Raster.Load("duck.jpg", options: new JpegReaderOptions { FastUpsampling = true });
JpegDetails details = (JpegDetails)photo.Metadata.FormatDetails!;
Console.WriteLine($"{details.ColorSpace}, progressive: {details.Progressive}, quality about {details.EstimatedQuality}");
foreach (JpegComponent component in details.Components)
{
    Console.WriteLine($"Component {component.Id}: sampled {component.HorizontalSampling} × {component.VerticalSampling}");
}

// Gray output, and 4:4:4 (no chroma subsampling) for sharp colored edges.
photo.Save("gray.jpg", new JpegWriterOptions { ColorType = JpegColorType.Gray });
photo.Save("sharp.jpg", new JpegWriterOptions { Quality = 90, Subsampling = ChromaSubsampling.Subsample444 });

Multi-Picture JPEG files

Phones store more than one image in a JPEG: a gain map for HDR, large previews, the second view of a stereo pair. The CIPA Multi-Picture Format (MPF) lists them in an APP2 segment. JpegDetails.MultiPicture gives that index as MpfData: an MpfImage for each image with its MpfImageType, flags, length, and offset from the start of the file (the same numbers ExifTool prints). Raster.Probe counts them in RasterInfo.ImageCount, and Raster.LoadAll loads each JPEG one, the primary first. A damaged index never fails a load: the primary loads with a Jpeg.MpfDamaged warning.

// A JPEG's Multi-Picture index lists the images stored after the primary: previews, stereo views, a gain map.
using Raster photo = Raster.Load("ultrahdr.jpg");
MpfData? index = ((JpegDetails)photo.Metadata.FormatDetails!).MultiPicture;
foreach (MpfImage image in index?.Images ?? [])
{
    Console.WriteLine($"{image.Type}: {image.Length:N0} bytes at {image.Offset:N0}");
}

// LoadAll gives every JPEG image the index lists, the primary first; a plain JPEG gives one.
using RasterList all = Raster.LoadAll("ultrahdr.jpg");

The gain maps of Ultra HDR photos are in the HDR guide.

Recipes

Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.

Read a CMYK JPEG's inks

// A CMYK JPEG loads as RGB by the plain formula, as before; ask for Cmyk8 to get the file's ink amounts instead.
using Raster<Cmyk8> inks = Raster.Load<Cmyk8>("duck-cmyk.jpg");
Cmyk8 corner = inks.Row(0)[0];
Console.WriteLine($"Cyan {corner.C}, magenta {corner.M}, yellow {corner.Y}, black {corner.K} (0 = no ink)");

// Resizing filters the four inks, so black stays in the black channel; color adjustments go through RGB instead.
inks.Apply(ops => ops.Fit(160, 160));

Run it

samples/Jpeg is a console app that does what this guide shows on the bundled images: dotnet run --project samples/Jpeg writes its results to ./output.