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.