Anatomy of a JPEG File: Markers, Segments and Hidden Data

A JPEG is a stream of segments, each announced by a two-byte marker. Once you know the marker system you can read the structure of any JPEG in a hex editor, find exactly where its metadata lives, and understand why certain edits leave traces behind. This page covers the whole layout.

The structure

Every marker is FF followed by a code byte. The file starts with FF D8 (Start of Image) and ends with FF D9 (End of Image). Between them, most markers are followed by a two-byte length and their data. The compressed pixels come last, after FF DA.

The marker system

// Most segments FF xx LL LL [data] β”‚ β”‚ β””β”€β”¬β”€β”˜ β”‚ β”‚ └── length, big-endian, INCLUDES these two bytes β”‚ └─────── marker code └────────── always FF // So the data is (length - 2) bytes. // Standalone markers β€” SOI, EOI, RSTn β€” have no length at all.
MarkerNameContains
FF D8SOIStart of Image β€” no length
FF E0APP0JFIF header, density, optional thumbnail
FF E1APP1EXIF or XMP metadata
FF E2APP2ICC colour profile
FF EDAPP13Photoshop IRB, IPTC data
FF DBDQTQuantisation table
FF C0SOF0Baseline frame header
FF C2SOF2Progressive frame header
FF C4DHTHuffman table
FF DDDRIRestart interval
FF DASOSStart of Scan β€” compressed data follows
FF D0–FF D7RST0–7Restart markers inside the data
FF FECOMFree-text comment
FF D9EOIEnd of Image β€” no length

A typical camera JPEG looks like this in order:

FF D8 SOI FF E1 [Exif…] APP1 β€” EXIF, camera settings, GPS, thumbnail FF E2 [ICC…] APP2 β€” colour profile FF DB [64 bytes] DQT β€” luminance quantisation table FF DB [64 bytes] DQT β€” chrominance quantisation table FF C0 [17 bytes] SOF0 β€” dimensions, components, subsampling FF C4 [...] DHT β€” Huffman tables (usually four) FF DA [...] SOS β€” scan header [...] compressed data β€” the bulk of the file FF D9 EOI

Where the metadata lives

APP1 is the important segment for anyone thinking about privacy or provenance.

FF E1 LL LL 45 78 69 66 00 00 [TIFF structure] β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ "Exif\0\0" // Inside, a complete TIFF header: 49 49 β†’ "II" little-endian, or 4D 4D "MM" big-endian 2A 00 β†’ 42, the TIFF magic number 08 00 00 00 β†’ offset to the first image file directory

The EXIF block is a TIFF file embedded inside a JPEG, with its own byte-order marker and its own directory structure. This is a historical artefact β€” EXIF was defined as an extension of TIFF β€” and it explains why EXIF parsers look like TIFF parsers and why the byte order inside the EXIF block can differ from anything else in the file.

EXIF directoryTypically contains
IFD0Camera make and model, orientation, resolution, software, timestamps
Exif IFDExposure, aperture, ISO, focal length, flash, lens
GPS IFDLatitude, longitude, altitude, direction
IFD1The embedded thumbnail
Interop IFDCompatibility identifiers
Maker notesVendor-specific, often including a serial number

🚨 The thumbnail that outlives the edit

IFD1 holds a small JPEG preview β€” typically 160 Γ— 120 β€” so that image browsers can show a grid quickly without decoding full-size files.

The problem is that some editors write a modified main image and leave the thumbnail untouched. A photo that was cropped to remove someone, or had a region painted over to redact it, can still carry a thumbnail showing the original, unedited scene. This has produced real disclosures β€” the visible image is clean and the metadata is not.

Always strip metadata rather than trusting an editor to update it:

# See whether a thumbnail is present exiftool -ThumbnailImage -b photo.jpg > thumb.jpg # Remove everything exiftool -all= photo.jpg # Keep orientation, drop the rest (see the note below) exiftool -all= -tagsfromfile @ -Orientation photo.jpg

One caution that catches people: stripping all metadata also removes the Orientation tag. If the pixels were stored sideways with a tag telling viewers to rotate them, removing the tag leaves the photo permanently sideways. Bake the rotation into the pixels first with magick -auto-orient, then strip.

DQT β€” the quality setting, made concrete

The quantisation table is 64 values, one per DCT coefficient. Each stored coefficient is divided by its table entry and rounded, which is the step where information is discarded.

// A typical luminance table at high quality 3 2 2 3 5 8 10 12 2 2 3 4 5 12 12 11 3 3 3 5 8 11 14 11 3 3 4 6 10 17 16 12 4 4 7 11 14 22 21 15 5 7 11 13 16 21 23 18 10 13 16 17 21 24 24 20 14 18 19 20 22 20 21 20 // Small values top-left β†’ low frequencies preserved // Large values bottom-right β†’ fine detail rounded away

There is no "quality: 85" field anywhere in a JPEG. The quality setting exists only in the encoder β€” it scales a reference table, and what is stored is the result. Tools that report a quality figure are inferring it by comparing the embedded table against known references.

This is also how JPEG forensics works. Different cameras and software use characteristic tables, so the DQT can indicate which program produced a file, and the presence of tables inconsistent with the claimed source suggests re-encoding.

SOF β€” dimensions and subsampling

FF C0 00 11 08 04 38 06 A8 03 01 22 00 02 11 01 03 11 01 β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── component 3 (Cr) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └───────────── component 2 (Cb) β”‚ β”‚ β”‚ β”‚ β”‚ └──────────────────────── component 1 (Y) β”‚ β”‚ β”‚ β”‚ └── number of components (3 = colour) β”‚ β”‚ β”‚ └──────── width = 0x06A8 = 1704 β”‚ β”‚ └────────────── height = 0x0438 = 1080 β”‚ └───────────────── sample precision, 8 bits └─────────────────────── segment length

Each component is three bytes: an identifier, sampling factors packed into one byte, and which quantisation table it uses. The sampling factors encode the chroma subsampling:

Y factor byteSubsamplingColour resolution
0x114:4:4Full
0x214:2:2Half horizontally
0x224:2:0Half in both directions

That single byte determines whether red text in the image will look sharp or fringed. 0x22 β€” the default for photographs β€” discards three quarters of the colour information, which is invisible on a photograph and very visible on saturated text.

Byte stuffing

Compressed data is a bitstream, and it can naturally produce a byte with the value FF. Since FF introduces every marker, a decoder would misread it as the start of a segment.

The solution is byte stuffing: the encoder writes FF 00 whenever the data produces a genuine FF, and the decoder discards the 00. So FF 00 in the compressed region is data, and FF followed by anything else is a real marker.

// Finding the true end of image data FF 00 β†’ stuffed data byte, keep reading FF D0–FF D7 β†’ restart marker, keep reading FF D9 β†’ End of Image, stop

⚠️ Data hides after EOI

Most decoders stop at FF D9 and ignore everything after it. That makes the tail of a JPEG a convenient hiding place β€” a common technique appends a complete ZIP archive after EOI, producing a file that is simultaneously a valid image and a valid archive.

If you accept image uploads, truncate at FF D9, or better, re-encode every upload. Re-encoding produces a file containing only what your own encoder wrote, which removes appended data, unexpected segments and malformed structures in one step.

Baseline and progressive

Baseline (SOF0)Progressive (SOF2)
StructureOne scan, top to bottomSeveral scans of increasing detail
Partial loadTop portion, sharpWhole image, blurry, then sharpens
File sizeBaseline2–10% smaller
Decode costLowerHigher β€” multiple passes
MemoryRow by rowWhole image buffered

Progressive is usually the better choice on the web: smaller files, and a full-frame preview appears sooner, which measures better on perceived-performance metrics. The exception is very large images on memory-constrained devices, where buffering the whole frame is a real cost.

# Which is this file? identify -verbose photo.jpg | grep Interlace # "None" = baseline, "JPEG" = progressive # Convert losslessly β€” no re-encoding of coefficients jpegtran -progressive -copy all -outfile out.jpg in.jpg

Operations that do not re-encode

Because the compressed data is organised in 8Γ—8 blocks, some transformations can be performed by rearranging blocks without decoding them. No quality is lost at all.

# All lossless β€” the coefficients are never touched jpegtran -rotate 90 -copy all -outfile out.jpg in.jpg jpegtran -flip horizontal -copy all -outfile out.jpg in.jpg jpegtran -progressive -copy all -outfile out.jpg in.jpg jpegtran -optimize -copy all -outfile out.jpg in.jpg # Cropping is lossless only on 8-pixel boundaries jpegtran -crop 640x480+16+8 -outfile out.jpg in.jpg

-optimize is worth knowing: it recomputes the Huffman tables for the actual data rather than using generic ones, typically saving 3–8% with no quality change whatsoever. Many encoders skip it because it requires a second pass.

Rotation is only fully lossless when the dimensions are multiples of 8 (or 16 with subsampling). Otherwise the edge blocks cannot be rearranged cleanly and the tool either trims a few pixels or falls back to re-encoding.

Inspecting a file

# List every marker and segment exiftool -v3 photo.jpg | head -40 # Structure summary identify -verbose photo.jpg # Read the markers yourself xxd photo.jpg | head -5 # Find every marker position in the file xxd -p photo.jpg | tr -d '\n' | grep -ob 'ffd8\|ffe1\|ffdb\|ffc0\|ffc2\|ffda\|ffd9'

Strip metadata before you share

Remove GPS coordinates, camera details and embedded thumbnails in your browser β€” the photo is never uploaded anywhere.

Open the EXIF Remover β†’

Summary

  • Every marker is FF plus a code. Files run from FF D8 to FF D9.
  • EXIF lives in APP1 and is a complete TIFF structure embedded inside the JPEG.
  • The embedded thumbnail can survive an edit and show the original scene.
  • There is no quality field β€” only the quantisation table the setting produced.
  • One byte in SOF sets chroma subsampling, which decides whether text looks sharp.
  • FF 00 is stuffed data; any other FF pair is a real marker.
  • Data after EOI is ignored by decoders. Re-encode uploads to remove it.
  • Rotation, progressive conversion and Huffman optimisation are lossless via jpegtran.

Frequently Asked Questions

What are the first bytes of a JPEG file?

FF D8, the Start of Image marker, usually followed immediately by FF E0 or FF E1 for the first application segment. Every JPEG begins FF D8 and ends FF D9, and every marker in between begins with an FF byte followed by a code identifying what it is.

Where is EXIF data stored in a JPEG?

In the APP1 segment, marker FF E1, which begins with the string Exif followed by two null bytes. The EXIF block itself is a TIFF structure β€” complete with its own byte order marker β€” embedded inside the JPEG, which is why EXIF parsing code so often looks like TIFF parsing code.

Why does a JPEG contain a thumbnail and why does that matter?

Cameras embed a small preview in the EXIF block so image browsers can display it quickly. The privacy issue is that some editors update the main image without regenerating the thumbnail, so a photo that was cropped or redacted can still contain a thumbnail showing the original.

What is byte stuffing in JPEG?

Compressed image data can contain a legitimate FF byte, which would otherwise look like the start of a marker. The encoder inserts a 00 after every such FF, and the decoder removes it. That is why you see FF 00 sequences throughout the compressed data of every JPEG.

What is the difference between baseline and progressive JPEG?

Baseline stores the image in one top-to-bottom pass, marked by SOF0. Progressive stores it in several passes of increasing detail, marked by SOF2, so a partial download shows the whole image blurry rather than the top part sharp. Progressive files are usually 2 to 10% smaller as well.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.