What Is a JPG File? The Compressed Picture That Also Stores Where and When You Took It
JPG files are compressed pictures that can also store places, times and extra bytes, and decoders accept edited ones with no warning. Jump to your question below, or work through it in order.
What Is a JPG File? A picture saved in marked segments, and the ending is only a label
Short answer
JPG files are pictures saved with lossy JPEG compression and stored as chains of marked segments. Besides the pixels, they can store the camera make, the time, GPS positions and even other files. The first three bytes, FF D8 FF, are the usual sign of one, and the ending .jpg is only a label.
We read 1,052 public JPG files, made 15 of our own and damaged one JPG file in 50 ways. Five decoders and our script handled the 50 copies in different ways, and only 2 copies gave the same answer in all six. Nine edits to the metadata passed all five decoders with no warning.
To see what a JPG file stores about you, read the privacy part. To check one, read the six steps. If your JPG file will not open, begin with the eight causes.
The ending .jpg is only a label. What makes a file a JPG file is its first bytes, FF D8 FF, and the chain of segments after them. Those segments can store more than the picture, so two JPG files can differ widely inside.
Picture a phone photo that arrives by email as IMG_2041.jpg. It opens in every app you own, so it feels like one flat picture. Inside, it is a list of segments. Some segments store pixels, and others store the camera, the day, the place and small previews.
Wikipedia on JPEG says the Joint Photographic Experts Group made the format, and that ISO/IEC 10918 and ITU-T T.81 define it. The first public release came on 18 September 1992. The same page lists .jpg, .jpeg, .jpe, .jif, .jfif and .jfi as endings. It names image/jpeg as the MIME type.
FileInfo says .jpeg and .jpg name the same JPG file format, and that .jpg is more common because early Windows allowed only three letters after the dot. It files the format under raster images and says it uses lossy compression to cut the size with little visible loss.
Two more names matter. JFIF is the wrapper that most JPG files use, and Exif is the camera metadata format. Wikipedia on JFIF says Eric Hamilton of C-Cube led JFIF. Wikipedia on Exif says Exif lives in the APP1 segment and can store location, time and device identifiers.
Where our JPG files come from
We worked with three sets of JPG files. We read 1,052 public JPG files from 13 sources, which are 12 code projects and the Go standard library. We made 15 JPG files ourselves, starting from one base file, base.jpg. We also damaged copies of base.jpg in 50 ways and read every copy with six readers.
The public JPG files are test and sample files. About 316 are camera samples from the metadata extractor project, and 98 more are test cases made to break readers. The rest come from image libraries, with 279 from the sharp tests alone. Odd files are common, so the counts show what projects collect and not what phones send.
The ending is no guide to what JPG files contain. Two JPG files can differ in compression, color layout, metadata and extra bytes at the end. Our tests below show what the segments say and what happens when they change.
JPG file facts at a glance
| Property | Value |
|---|---|
| Meaning | Joint Photographic Experts Group, the group that made the format. The files use lossy compression for continuous tone pictures. |
| Endings | .jpg and .jpeg for the same format. Wikipedia also lists .jpe, .jif, .jfif and .jfi. |
| Start |
FF D8, the start marker. IANA and Wikipedia give FF D8 FF as the magic number.
|
| MIME type |
image/, registered with IANA.
|
| Size limit | Wikipedia says 65,535 by 65,535 pixels for JPEG and JFIF. |
| Usual metadata | JFIF in 384 of our 956 parsed public JPG files and Exif in 547. Another 211 store neither. |
| Our base file | base.jpg has 15,745 bytes and 480 by 320 pixels, and about 90 percent of its bytes are picture data. |
| Opens with | Every browser, phone and photo app. Windows Photos, Apple Preview and Paint also open JPG files. |
| Our tests | 1,052 public JPG files, 15 made files, 50 damaged copies and six readers, on Linux. We opened no JPG file in a viewer. |
Wrong page? Look at the PNG file for lossless pictures, the GIF file for short animations and the BMP file for raw bitmaps.
What Is Inside a JPG File? Four bands, and 90 percent of the bytes are the picture
A JPG file is a chain of segments, and every segment starts with FF and a marker byte. In base.jpg, 940 bytes are metadata and 603 bytes are tables and headers. The other 14,198 bytes are picture data, and the JPG file has 15,745 bytes in all.
We made base.jpg with Pillow from a drawn picture of 480 by 320 pixels at quality 85. We added camera make, time, GPS position, color profile and comment data, so that every part has something to read. The figure shows its parts in file order.
Read the figure from the top. The file starts with FF D8 and ends with FF D9. The segments between them all begin with FF, a marker byte and a length. The Library of Congress record names the same markers, with FF C0 for a baseline frame and FF C2 for a progressive one.
The first bytes and the marker list
00000000: ffd8 ffe0 0010 4a46 4946 0001 0100 0001 ......JFIF......
00000010: 0001 0000 ffe1 011b 4578 6966 0000 4d4d ........Exif..MM
The first two bytes are the start marker. The next four are FF E0 00 10, a JFIF segment with a length of 16 bytes. Then comes the word JFIF and a null byte. The segment after it starts with FF E1, and its letters spell Exif.
byte marker length what
0 FF D8 - start of image
2 FF E0 16 JFIF segment
20 FF E1 283 Exif segment
305 FF E2 604 ICC profile
911 FF FE 29 comment
942 FF DB 67 quantization table
1011 FF DB 67 quantization table
1080 FF C0 17 baseline frame header
1099 FF C4 31 Huffman table
1132 FF C4 181 Huffman table
1315 FF C4 31 Huffman table
1348 FF C4 181 Huffman table
1531 FF DA 12 scan header
1545 - 14198 picture data
15743 FF D9 - end of image
A segment length counts its own two length bytes and not the marker. The reader adds 2 to the length and lands on the next marker. This is why a bad length value, like in our A5 and A6 copies, sends readers to the wrong place.
The metadata segments
| Segment | What the sources say | In base.jpg |
|---|---|---|
APP0 JFIF
|
The JFIF segment follows the start marker. Wikipedia lists the version, a density unit, two density values and an optional thumbnail. | Version 1.01, density 1 by 1 with no unit |
APP1 Exif
|
Exif lives in the APP1 segment as a TIFF structure. The segment is limited to 64 KB, per Wikipedia. | Make, orientation, date and GPS in 285 bytes |
APP2 ICC
|
A color profile can follow in the APP2 segment. It is our choice of segment here and not a rule from the sources above. | An sRGB profile of 606 bytes |
COM comment
|
A free text segment that readers ignore. | The words made by us for the JPG page |
JFIF and Exif both want to come first. Wikipedia says the two are mutually incompatible for that reason, yet many cameras and programs write both. Our public JPG files agree with it. Of 956 parsed files, 197 store both segments and 350 store Exif without JFIF.
The tables, the frame and the picture
Two quantization tables set how much detail every block keeps. The frame header gives the size, bit depth and sampling. Four Huffman tables decode the bits that follow. Then the scan header starts the picture data, which is 14,198 bytes in base.jpg.
In the picture data, every FF byte is followed by 00, so it cannot be mistaken for a marker. Restart markers, FF D0 to FF D7, can also appear. A reader walks these bytes until it meets the next marker. For base.jpg that is FF D9 at byte 15,743.
Most bytes are picture. In base.jpg, 90 percent of the bytes are scan data and 6 percent are metadata. That share changes from file to file. The largest tail in our public JPG files has 18,469,861 bytes, and no decoder reads it.
What Do JPG Files Store? Three in four Exif files name a camera, and 83 files have a tail
The ending .jpg says nothing about what JPG files contain. Of 956 public JPG files that parse, 872 are baseline and 77 are progressive. Exif is in 547 files, GPS positions in 66 and bytes after the end marker in 83.
Two JPG files can look alike in a file manager and differ inside. We counted the compression mode, the chroma sampling, the metadata segments and the extra bytes in the public JPG files. The figure shows the counts.
The compression modes
| Mode | What it does | Public files |
|---|---|---|
| Baseline |
The common mode. Wikipedia says coefficients are coded block by block. The frame marker is FF C0.
|
872 of 956 |
| Progressive |
Several scans of rising detail, so a slow page can show a rough picture first. The frame marker is FF C2.
|
77 of 956 |
| Extended |
The same family with more tables or 12 bit samples. The frame marker is FF C1.
|
5 of 956 |
| Arithmetic and lossless | Wikipedia calls both rare. We found one arithmetic file and one lossless file. | 2 of 956 |
Chroma sampling shows how much color detail a JPG file keeps. We saw 516 files with 4:2:0, 212 with 4:2:2 and 151 with 4:4:4. Another 49 files have one or four color channels, like grayscale and CMYK pictures. Our base.jpg uses 4:2:0.
What the metadata stores
Of 956 parsed JPG files, 547 store Exif, 384 store JFIF and 176 store an XMP packet. Another 107 store an Adobe segment, 105 store an ICC profile and 33 store a comment. A total of 211 store none of these. Many of those come from image library tests, which write bare files.
Among the 547 Exif files, 409 name a camera make and 404 store the time the picture was taken. Another 66 store GPS positions, which is 12 percent. A further 67 store an orientation other than upright, and 31 of those say rotated 90 right.
Bytes after the end marker
Of the 956 JPG files, 83 have bytes after FF D9. Of those 83, 28 start with a second JPEG file. Three are only zeros, and 7 start with text. The largest tail is 18,469,861 bytes. A reader that stops at the end marker never sees any of it.
Not sure the file is a JPG file at all? Check it with our script.
What Did We Find in 1,052 Public JPG Files? Six readers, and 16 files that only djpeg accepted
Six readers saw our 1,052 public JPG files in nearly the same way. All six accepted 904 files, which is 86 percent, and none accepted 69. The other 79 files split the readers, and 16 passed in djpeg alone.
We searched 13 public sources for JPG files and found 1,052 different ones. Many are made to be odd, so this study shows what readers do with sample and test JPG files and not what servers send. The figure shows how many files every reader accepted.
The six readers
Our script is the 139 line Python program in the check part. It walks the markers, reads the Exif values and notes every rule that a JPG file breaks, and it decodes no pixels.
The other five readers decode the picture. They are djpeg from libjpeg-turbo 2.1.5, Pillow 12.3.0, the Go 1.24.7 standard library, the Rust crate zune-jpeg 0.4.21 and jpeg-js 0.4.4 on Node 22.
Pillow and djpeg use the same C library, so they differ only in the policy of the wrapper. We counted JPG files as accepted when readers returned pictures, even with warnings. For the decoders we saved hashes of the pixels, to see if copies gave the same picture as the intact JPG file.
Where the readers agree
All six readers accepted 904 JPG files. In every file that two or more decoders accepted, they reported the same width and height. We found no case where one decoder saw a larger picture than another. The exact pixel values differ in more files, but we compared them only against the same reader, never across readers.
All six readers refused 69 JPG files. Most are broken on purpose. Forty seven come from the test suite of the metadata extractor project, and 21 come from the PHP exif tests. Nearly all have no frame header or no start marker. One comes from the sharp tests.
Where the readers split
Strictness differs widely. The djpeg tool accepted 958 files and Rust 922. Go accepted 915 and jpeg-js 916.
Pillow accepted 930, which is 28 fewer than djpeg, although both call the same library. Pillow refused 29 files that djpeg accepted, and accepted 1 that djpeg refused. Pillow refuses a cut file by default, while djpeg decodes what it can and prints a warning.
The 16 JPG files that only djpeg accepted all have extra bytes before a marker, from 4 to 42 bytes. The djpeg tool skips them and warns. Rust refuses 65 files for the same reason and names its strict mode. Go refuses 25 because a byte sequence is missing.
Go refused 43 JPG files that djpeg accepted, and accepted none that djpeg refused. Rust accepted 18 that Go refused, and Go accepted 11 that Rust refused. Go also refused 26 files with the words unsupported JPEG feature, like an unusual chroma ratio or an unknown marker, which is not the same as broken.
Read the study with care. It shows what six readers do with 1,052 public JPG files. We opened no JPG file in browsers, phones or photo apps, and we did not check how any file was made. Readers that agree on these files can still differ on yours.
What Do Readers Do With a Damaged JPG File? 50 broken copies, six readers and nine silent edits
We damaged one JPG file in 50 ways and read every copy with six readers. Only 2 of the 50 copies gave the same answer in all six, and 6 were refused by all six. Nine edits to the metadata passed all five decoders with no warning, and so did a zip after the end marker.
The base file, base.jpg, has 15,745 bytes. We changed one thing at a time and sorted the 50 damaged JPG files into 6 groups. The groups cover the start and the segments, the tables and the frame, and what the JPG file says about itself. They also cover the picture data and the size, with the wrapping and extras last.
Green means the reader gave the same answer as for the intact JPG file. For the five decoders that means the same pixels. Amber means it gave another answer with no warning. Red means a warning or a refusal. Our script compares the summary it prints, so it also sees changes to the metadata.
Edits to the metadata pass every decoder
Nine copies change only what the JPG file says about itself. Two remove the JFIF segment (A3) and the Exif segment (A4).
The other seven set orientation to rotated (C1) and move GPS latitude from 12 to 48 (C2). They also remove the GPS block (C3), add an XMP packet (C5) and remove the ICC profile (C6). The last two set the date to 2001 (C7) and change camera make (C9).
All five decoders returned the same pixels for all nine copies, and none warned. Only our script showed a change, because it prints the Exif values. A picture viewer that obeys the orientation tag would show C1 sideways, even though every decoder returned the same pixels.
The same is true for extra bytes. Four copies add data after the end marker. They are a zip archive (F2), a second small JPEG (F3), 1 KB of zeros (F1) and 1 MB of zeros (F8). All four passed the five decoders with the same pixels.
Python and unzip read the zip inside F2 and listed its note.txt file. Our script warned about the extra bytes.
What the groups show
| Group | What we found |
|---|---|
| A. Start and segments | A changed first byte (A1) and a repeated start marker (A7) were refused by all six. Three junk bytes in front (A2) stopped all five decoders, and only our script read past them. Wrong JFIF lengths (A5, A6) split the readers three ways. Removing JFIF or Exif (A3, A4) changed no pixel. |
| B. Tables and frame | With no quantization tables (B1), four decoders refused and Go gave a different picture with no warning. With no Huffman tables (B2), djpeg and Pillow decoded the same pixels and the other three refused. Setting every quantization value to 1 (B4) changed the picture in all five decoders with no warning. A width of 481 (B7) and a doubled height (B6) gave different pictures in Pillow and Rust. |
| C. What the file says about itself | All nine edits changed the metadata only, and no decoder noticed any. Our script showed seven of them. A new comment (C4) and a density of 300 dots per inch (C8) changed no answer in any of the six readers, our script included, because it prints neither. |
| D. Picture data | One flipped byte (D1) made djpeg warn and Go and jpeg-js refuse. Pillow and Rust gave a different picture. Ten flipped bytes (D2) were refused by four decoders, and Rust returned a picture. Moving or deleting bytes (D3 to D7) split the readers in similar ways. A marker inside the scan (D8) made djpeg warn and Go and jpeg-js refuse. Pillow gave another picture. |
| E. Size | A file cut at 90, 50 or 10 percent (E1 to E3) made djpeg warn and Pillow, Go and jpeg-js refuse. Rust gave a different picture with no warning. With no end marker (E4), Rust returned the same pixels. An empty file (E6) and a JPG file with only start and end markers (E7) were refused by all six. |
| F. Wrapping and extras | Base64 text (F4) and gzip (F5) were refused by all six. A PNG with a JPG name (F6) was refused by four decoders, and Pillow opened it, because it goes by content and not by the ending. HTTP headers on top (F7) stopped all five decoders. Extra bytes after the end passed all five. |
Which readers warn
On the 50 copies, only djpeg and our script warned. Pillow, Go, Rust and jpeg-js warned on none. Go refused 31 copies and jpeg-js refused 33, which makes them the strictest. Rust returned another picture for 13 copies and Pillow for 11, and neither said so.
The djpeg tool warned on 18 copies and refused 14. Our script flagged 16, refused 9 and passed 14 with another summary. A reader that forgives more is not safer, because it can hide the damage from you.
Read the grid with care. Green cells mean only that a reader gave the same answer as for the intact JPG file. We opened no copy in browsers or photo apps, so the grid shows decoding and nothing else. Every test changed one thing in a single JPG file that we made.
What Does Our Expert Say About JPG Files? A note from Neeraj Singh on reading before sharing
Read the Exif data of a JPG file yourself before you share it. Nine edits to the metadata and four kinds of extra bytes passed five decoders with no warning, and only a script that reads the metadata saw them. A scan of the first bytes takes seconds.
Expert insight
JPG files look like only pictures, but they also store where and when you took them. In our tests, nine edits to the metadata, like a new place, another date or a new rotation, changed no pixel, and none of five decoders warned. Read the Exif data yourself and look for bytes after the end marker. Remove the metadata before you share a photo.
Why a decoder proves little
A decoder that opens a JPG file shows that the picture data makes sense to that decoder. It does not show what else the JPG file stores. In our tests the five decoders returned the same pixels for nine copies with other metadata, so a JPG file that opens everywhere can still name a place.
Two decoders that agree on a JPG file can both miss the tail. Neither says whether a zip follows the end marker or whether the Exif block names a street. The script from the check part reads the markers, the Exif values and the end of the JPG file. Compare its answer with a second reader.
Hash the file first. The SHA256 hash of a JPG file shows which copy you have, and it shows if a transfer changed it later. Keep the original read only, and work on copies of JPG files.
How Do You Open a JPG File? Every viewer opens it, so look inside with a second tool
Open a JPG file by double clicking it. Every browser, phone and photo app opens JPG files, and Windows, macOS, Android and iOS include viewers. Viewers show the picture and hide what else the JPG file stores, so read the metadata and the end of the file with a second tool.
Opening JPG files is the easy part. The FileInfo entry lists Microsoft Photos and Paint for Windows, and Apple Preview and Photos for Mac. For phones it lists Google Photos and Apple Photos. For the web it lists Google Drive, OneDrive and iCloud Photos. Chrome opens JPG files too.
We opened no JPG file in these apps. Our tests used decoders and a script, so do not read our grid as a test of Photos, Preview or Chrome. The sections below cover what to use when a double click fails or when you want to look inside.
Look inside with a command line
The four commands below hash the JPG file, show its first 16 bytes, decode it with djpeg and use our script. The first two work on any computer with a shell. The djpeg tool comes with libjpeg-turbo, and the script in the check part is ours.
sha256sum base.jpg
head -c 16 base.jpg | xxd
djpeg -ppm base.jpg > /dev/null
python3 jpg_peek.py base.jpg
The djpeg tool prints warnings on any trouble and writes the picture to a PPM file, which is why we sent it to the null device. A silent result is good for the picture, but it says nothing about the metadata and the tail. Our script covers those two.
Two Windows viewers
These cards link to the two Windows programs that name JPG files on their own pages. Both pages say the free version has a limit, and we did not install either one.
SysTools JPG Converter
Batch converts JPG files on Windows
Freeviewer Image Viewer
Opens and previews image files on Windows
The SysTools page describes a Windows converter. It adds single files or folders, previews and resizes the pictures and writes one file or one file per picture. It lists PDF, DOC, DOCX, HTML, TXT, PNG, BMP, TIFF, TGA, APNG, WEBP, GIF, JPEG and JPEG 2000 as outputs.
Its free version converts the first 5 JPG files, and PDF, DOC and DOCX outputs get a watermark.
The Freeviewer page describes a Windows viewer that opens and previews image files of many types, among them JPG, JPEG, PNG, GIF, BMP and TIFF. It lists bulk adding, reordering and printing. Its free demo opens and reads files, and its export is limited to the first 5 images. The page says there are no limits on file size.
How the readers did on our 50 damaged copies
| Reader | Result |
|---|---|
| Our script | Same answer on 11 copies, another summary with no warning on 14, flags on 16 and refusals on 9. It reads the metadata, so it sees edits that decoders miss. |
| djpeg | Same on 17 copies, another picture on 1, a warning on 18 and a refusal on 14. It is the only decoder that warns. |
| Pillow | Same on 20 copies, another picture with no warning on 11 and a refusal on 19. |
| Go | Same on 17 copies, another picture on 2 and a refusal on 31. |
| Rust | Same on 17 copies, another picture with no warning on 13 and a refusal on 20. |
| jpeg-js | Same on 16 copies, another picture on 1 and a refusal on 33. |
How Do You Check a JPG File? Six steps, and a 139 line script
Check a JPG file in six steps. Hash it and read its first bytes. Then decode it, use a script that prints the markers and the Exif values, read the end of the JPG file and compare with a second decoder. No decoder shows what the metadata says.
The six steps
- Copy the JPG file, write down its SHA256 hash and work on the copy.
-
Read the first bytes with
head -c 16 file.jpg | xxd. JPG files start withFF D8 FF. Anything else, like text or89 50 4E 47, means the ending is wrong. -
Decode the JPG file with
djpeg -ppm file.jpg > /dev/null. It prints a warning when the data is cut or damaged. - Use the script below. It prints the kind, size, sampling and metadata, and then the GPS position, camera make and date.
- Read the notes at the end of its output. Bytes after the end marker, a missing end marker and a damaged Exif block get a line of their own.
- Open the JPG file in a second reader and compare the answers. If you plan to share the JPG file, remove the metadata first with the steps in the privacy part.
A 139 line script that reads the JPG file
The Python script below does steps 4 and 5 in about a second. It walks the segments, reads the Exif values and compares the JPG file with the rules we listed. It prints REFUSED when it cannot go on, and a note for every smaller worry. It decodes no pixels, and it changes no JPG file.
import sys, struct
SOF = {0xC0: 'baseline', 0xC1: 'extended', 0xC2: 'progressive', 0xC3: 'lossless',
0xC9: 'arithmetic', 0xCA: 'arithmetic progressive', 0xCB: 'arithmetic lossless'}
ROT = {1: 'upright', 2: 'mirrored', 3: 'rotated 180', 4: 'flipped', 5: 'transposed',
6: 'rotated 90 right', 7: 'transversed', 8: 'rotated 90 left'}
def ifd(t, off, e):
out = {}
for i in range(struct.unpack(e + 'H', t[off:off + 2])[0]):
tag, typ, cnt, val = struct.unpack(e + 'HHI4s', t[off + 2 + 12 * i:off + 14 + 12 * i])
at = struct.unpack(e + 'I', val)[0]
if typ == 3:
out[tag] = struct.unpack(e + 'H', val[:2])[0]
elif typ == 4:
out[tag] = at
elif typ == 2:
out[tag] = (val[:cnt] if cnt <= 4 else t[at:at + cnt]).split(b'\0')[0].decode('latin1')
elif typ == 5 and cnt == 3:
r = struct.unpack(e + '6I', t[at:at + 24])
out[tag] = r[0] / r[1] + r[2] / r[3] / 60 + r[4] / r[5] / 3600
return out
def exif_facts(seg):
t = seg[6:]
e = '<' if t[:2] == b'II' else '>'
a = ifd(t, struct.unpack(e + 'I', t[4:8])[0], e)
b = ifd(t, a[0x8769], e) if 0x8769 in a else {}
g = ifd(t, a[0x8825], e) if 0x8825 in a else {}
info = []
if a.get(0x0112, 1) != 1:
info.append('orientation tag says ' + ROT.get(a[0x0112], str(a[0x0112])))
if 2 in g and 4 in g:
info.append('GPS %.4f %s, %.4f %s' % (g[2], g.get(1, ''), g[4], g.get(3, '')))
if 0x010F in a:
info.append('camera make: ' + a[0x010F])
if 0x9003 in b:
info.append('taken ' + b[0x9003])
return info
def scan_end(d, pos):
"""Step over the entropy coded data and return the offset of the next marker."""
while pos + 1 < len(d):
j = d.find(b'\xff', pos)
if j < 0 or j + 1 >= len(d):
return len(d)
if d[j + 1] == 0 or 0xD0 <= d[j + 1] <= 0xD7:
pos = j + 2
continue
return j
return pos
def peek(path):
d = open(path, 'rb').read()
notes, meta, info = [], [], []
i = d.find(b'\xff\xd8')
if i < 0:
return None, ['REFUSED: no start marker FF D8'], []
if i > 0:
notes.append('%d bytes before the start marker' % i)
pos, sof, dqt, dht, eoi = i + 2, None, 0, 0, None
while pos + 4 <= len(d):
if d[pos] != 0xFF:
notes.append('data where a marker should be at byte %d' % pos)
break
m = d[pos + 1]
if m == 0xFF or m == 0x01 or 0xD0 <= m <= 0xD7:
pos += 1 if m == 0xFF else 2
continue
if m == 0xD9:
eoi = pos
break
ln = struct.unpack('>H', d[pos + 2:pos + 4])[0]
seg = d[pos + 4:pos + 2 + ln]
if pos + 2 + ln > len(d):
notes.append('a segment goes past the end of the file')
break
if m in SOF:
sof = (SOF[m], seg[0], struct.unpack('>HH', seg[1:5]), seg[5], seg[6:15])
elif m == 0xDB:
dqt += 1
elif m == 0xC4:
dht += 1
elif m == 0xFE:
meta.append('comment')
elif m == 0xE0 and seg[:4] == b'JFIF':
meta.append('JFIF %d.%02d' % (seg[5], seg[6]))
elif m == 0xE1 and seg[:4] == b'Exif':
meta.append('Exif')
try:
info += exif_facts(d[pos + 4:pos + 2 + ln])
except Exception:
notes.append('the Exif block is damaged')
elif m == 0xE1 and b'ns.adobe.com/x' in seg[:40]:
meta.append('XMP')
elif m == 0xE2 and seg[:11] == b'ICC_PROFILE':
meta.append('ICC profile')
elif m == 0xEE and seg[:5] == b'Adobe':
meta.append('Adobe')
pos += 2 + ln
if m == 0xDA:
pos = scan_end(d, pos)
if d[pos:pos + 2] == b'\xff\xd9':
eoi = pos
break
if sof is None:
return None, ['REFUSED: no frame header, so no size and no pixels'], []
if eoi is None:
notes.append('no end marker FF D9, the file looks cut')
elif len(d) - eoi - 2 > 0:
notes.append('%d bytes after the end marker' % (len(d) - eoi - 2))
if dqt == 0:
notes.append('no quantization table')
if dht == 0 and sof[0] in ('baseline', 'extended', 'progressive'):
notes.append('no Huffman table')
(h, w), n = sof[2], sof[3]
if w == 0 or h == 0:
notes.append('width or height is zero')
sub = '-'
if n == 3:
s = (sof[4][1] >> 4, sof[4][1] & 15)
sub = {(2, 2): '4:2:0', (2, 1): '4:2:2', (1, 1): '4:4:4', (1, 2): '4:4:0'}.get(s, 'other')
return '|'.join([sof[0], '%dx%d' % (w, h), str(n), sub, str(sof[1]), ', '.join(meta) or 'none']), notes, info
for p in sys.argv[1:]:
s, n, inf = peek(p)
print(p)
if s is None:
print(' ' + n[0])
continue
kind, size, comps, sub, bits, meta = s.split('|')
print(' %s JPEG, %s, %s components, subsampling %s, %s bit' % (kind, size, comps, sub, bits))
print(' metadata: ' + meta)
for x in inf:
print(' info: ' + x)
for x in n:
print(' note: ' + x)
if not n:
print(' no complaint')
On the intact JPG file it prints the kind, size, sampling and metadata, then the place, the camera and the date, with no complaint. On damaged files it names the problem. F2 has 143 bytes after the end marker, E4 has no end marker and A2 has 3 bytes before the start.
python3 jpg_peek.py base.jpg
base.jpg
baseline JPEG, 480x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: GPS 12.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
no complaint
python3 jpg_peek.py C1.jpg C2.jpg F2.jpg E4.jpg B7.jpg A2.jpg
C1.jpg
baseline JPEG, 480x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: orientation tag says rotated 90 right
info: GPS 12.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
no complaint
C2.jpg
baseline JPEG, 480x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: GPS 48.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
no complaint
F2.jpg
baseline JPEG, 480x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: GPS 12.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
note: 143 bytes after the end marker
E4.jpg
baseline JPEG, 480x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: GPS 12.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
note: no end marker FF D9, the file looks cut
B7.jpg
baseline JPEG, 481x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: GPS 12.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
no complaint
A2.jpg
baseline JPEG, 480x320, 3 components, subsampling 4:2:0, 8 bit
metadata: JFIF 1.01, Exif, ICC profile, comment
info: GPS 12.0000 N, 77.0000 E
info: camera make: ExampleCam
info: taken 2026:10:25 09:00:00
note: 3 bytes before the start marker
What the script cannot see
The script reads structure and values, and it cannot say if they are the ones you meant. It passed C1, C2 and B7 with no complaint, although they change orientation, position and width. It prints the new values, and you must compare them with what you expect.
It also decodes nothing. It cannot say whether the picture data is intact, because flipped bytes in the scan (D1) change no marker. For that, use a decoder. A script refusal is a reason to look closer and not a verdict. djpeg decoded 27 public JPG files that the script refused.
Want the causes of an error? Go to the causes.
Is a JPG File Safe to Open? Viewing is safe, and sharing can give away a place
JPG files are safe to view, and they can still reveal more than you meant. Exif can store the camera, the time and GPS positions, and 66 of our 956 parsed public JPG files store one. IANA says the format has no privacy or integrity mechanism of its own.
Looking at a picture is not the risk. IANA says that JPEG data is not inherently executable and that parsers of untrusted data need the usual precautions. It says the format has no built in privacy or integrity mechanism, and it adds that Exif data can contain privacy sensitive information like a geolocation.
We scanned no JPG file for malware and opened none in an app. What we can say is what the public JPG files store. Of 547 Exif files, 409 name a camera make and 404 store a time. Another 66 store GPS positions. Before you post a photo, assume it names more than the picture shows.
file -b base.jpg
JPEG image data, JFIF standard 1.01, ... manufacturer=ExampleCam, ... GPS-Data], comment: "made by us for the JPG page", baseline, precision 8, 480x320, components 3
The ending proves nothing about content. The file command read the markers and named the maker and the GPS block. Our F6 copy, a PNG with a JPG name, showed the other side. Five readers refused it, and the ending alone did not stop anyone from sending it.
What a JPG file can give away
Bytes after the end marker
A decoder stops at FF D9 and never reads what follows. In our F2 copy, a zip archive followed the end marker. All five decoders returned the same pixels with no warning, and Python and unzip listed the note inside the zip. Of 956 public JPG files, 83 have bytes after the end marker.
Most of those are second pictures, padding or text from cameras, which is normal. The point is that JPG files can hide data and still look normal. If a JPG file came from someone you do not trust, treat any tail as a reason to look. The ZIP file page covers the container that F2 used.
Remove the metadata before you share
The jpegtran tool from libjpeg-turbo rewrites the segments without decoding the picture. With -copy none it dropped the Exif, XMP, ICC and comment segments and any bytes after the end marker. The pixels stayed identical, and our script found nothing but JFIF afterwards.
# keep the picture, drop Exif, XMP, ICC, the comment and any bytes after the end marker
jpegtran -copy none -outfile out.jpg photo.jpg
# keep the color profile and drop the rest
jpegtran -copy icc -outfile out.jpg photo.jpg
base.jpg 15745 bytes JFIF 1.01, Exif, ICC profile, comment
out.jpg (none) 14823 bytes JFIF 1.01
out.jpg (icc) 15429 bytes JFIF 1.01, ICC profile
F2.jpg (zip tail) 15888 bytes 143 bytes after the end marker
F2 after none 14823 bytes no complaint
Two cautions. Removing the color profile can change how colors look, so -copy icc keeps it. Removing the orientation tag leaves a sideways picture sideways, so rotate the pixels first. The tool lists an option for 90 degree steps. We tested the strip, the profile copy and one rotate.
Use a copy. Work on copies of JPG files, and write down their SHA256 hashes. The hash shows which file you received, and it shows if a transfer changed it later. Keep the original read only.
Why Will Your JPG File Not Open? Eight causes, and what to do about every one
JPG files stop opening for eight reasons. The file can be another type. Extra bytes can come first, or the file can be cut. End markers, tables or lengths can be missing or wrong. Picture data can be damaged, or your reader can lack support for the kind.
The table lists what our readers did with every kind of damage and what to do about it. The fixes undo one of the edits we made, so they restore a copy by construction. We opened no copy in a browser or a photo app, and an app can behave differently from our five decoders.
| Cause | What we saw and what to do |
|---|---|
| The file is not a JPG (1) |
Base64 text (F4) and a gzip (F5) were refused by all six readers. A PNG with a JPG name (F6) was refused by four decoders, and Pillow opened it by content. Fix: Read the first bytes with head -c 16 file | xxd. If they are not FF D8 FF, unwrap the JPG file or give it the right ending.
|
| Bytes in front of the start marker (2) |
Three junk bytes (A2) and HTTP headers on top (F7) stopped all five decoders. Only our script read past them. Fix: Cut everything before the first FF D8, or download the JPG file again from the source.
|
| The file is cut (3) | Copies cut at 90, 50 or 10 percent (E1 to E3), at the scan header (E5) or by 8 bytes (E8) made djpeg warn and Pillow, Go and jpeg-js refuse. Rust returned another picture with no warning. Fix: Download the JPG file again and compare its size with the source. |
| The end marker is missing (4) |
With no FF D9 at the end (E4), djpeg warned and Pillow, Go and jpeg-js refused. Rust returned the same pixels. Fix: Fetch a fresh copy. We did not test adding the marker by hand, so do not trust a JPG file you repaired this way.
|
| Wrong segment lengths (5) | A JFIF length that was 20 too long (A5) or 4 too short (A6) made Rust and jpeg-js refuse the JPG file. The djpeg tool warned, and Pillow and Go accepted it. The same file opens in one app and fails in another. Fix: Save the JPG file again in an editor that opens it, or get a fresh copy. |
| Missing or changed tables and frame (6) | Removed quantization tables (B1), changed bit depth (B8), four components (B9) and changed frame type (B10) were refused by most decoders. Go returned another picture for B1. Fix: Compare the JPG file with a good copy, and use a decoder that prints warnings, like djpeg. |
| Damaged picture data (7) | One flipped byte in the scan (D1) made djpeg warn and Go and jpeg-js refuse. Pillow and Rust gave another picture. Deleting 100 bytes (D5) split the readers the same way. Fix: Download the JPG file again. A partial picture is not a repair. |
| A kind your reader does not support (8) | Our arithmetic coded file (m13) opened in djpeg and Pillow, and Go, Rust and jpeg-js refused it. Pillow also refused one public file for its size limit. Fix: Open the JPG file in djpeg, or convert it with a tool that supports the kind. Not every kind works in every app. |
Fix a JPG file in seven steps
- Copy the JPG file, write its SHA256 hash and work on the copy.
-
Read the first bytes as in the check steps. Look for
FF D8 FF. -
Decode the JPG file with
djpeg -ppm file.jpg > /dev/null, and read the warnings it prints. - Read the JPG file with the check script. Notes or REFUSED lines name the point where the structure stops making sense.
- Compare the size of the JPG file with the source. A smaller file is cut, and a larger file can have a tail.
- Open the JPG file with a second decoder, and compare the answers.
- If it still fails, get a fresh copy from the source and compare the hashes.
We did not test repairs on files from other people. A file that decodes can still fail in an app that is stricter, and a JPG file that fails in one decoder can open in another. For example, 29 public JPG files that djpeg accepted were refused by Pillow.
What to send when you ask for help
If friends, shops or support teams gave you a JPG file that fails, send them these lines. They show what was read and where it stopped, and they save a round of questions.
Subject: JPG file will not open
File: name, size in bytes and SHA256 hash
First bytes: the output of head -c 16 file | xxd
Decoder: the output of djpeg -ppm file > /dev/null
Script: the output of jpg_peek.py
App: the name and version of the app that fails, and what it printed
Source: where the JPG file came from and how it was copied
Not damage, but a question about other file types? Go to the versus part.
How Do You Make or Convert a JPG File? What 15 files we made show, and what converting costs
Convert JPG files only when you must, because every save in a lossy mode loses detail. Wikipedia calls this generation loss. To make JPG files, save pictures at a quality setting and keep the originals. Rotating and stripping metadata can be lossless.
Making JPG files differs from opening them. You choose a quality and the sampling, and a library writes the segments. We made 15 JPG files, one base and 14 variants, to see what the format allows. Every decoder accepted all but one, the arithmetic coded file.
Make a JPG file
The Python line below saves a picture at quality 85 with 4:2:0 sampling, like our base file. Pillow wrote base.jpg from a drawn picture of 480 by 320 pixels. Other tools write the same segments with other defaults, so compare the output of your tool with the figure in the inside part.
from PIL import Image
im = Image.open('picture.png').convert('RGB')
im.save('picture.jpg', 'JPEG', quality=85, subsampling=2)
The 14 files we made
| File | What it shows |
|---|---|
m01 4:4:4
|
Full color detail at quality 85. 23,265 bytes, against 15,745 for the base file. |
m02 4:2:2
|
Half the color detail across. 18,505 bytes. |
| m03 progressive | The same quantized data in several scans. The file has 14,227 bytes and gives the same pixels in all five decoders. |
| m04 optimized | Huffman tables fitted to the picture. The file has 14,480 bytes and gives the same pixels. |
| m05 quality 10 | Strong compression. The file has 5,827 bytes and gives another picture in every decoder. |
| m06 quality 100 |
Almost no loss, with 4:4:4. 89,616 bytes.
|
| m07 orientation 6 | The Exif tag says rotate 90 right. The file has 15,714 bytes and gives the same pixels. |
| m08 no metadata | No Exif, no profile, no comment. The file has 14,823 bytes and gives the same pixels. |
| m09 no GPS | Exif with the GPS block removed. 15,604 bytes. |
| m10 grayscale | One component. 10,264 bytes. |
| m11 CMYK | Four components. 31,828 bytes. All six readers accepted it. |
| m12 restart markers | A restart marker after every 8 blocks. The file has 15,169 bytes and gives the same pixels. |
| m13 arithmetic | Arithmetic coding instead of Huffman. 11,462 bytes. Go, Rust and jpeg-js refused it. |
| m14 RGB with Adobe | Red, green and blue stored directly, with an Adobe segment. 30,034 bytes. |
Pillow, djpeg and our script accepted all 15 JPG files. Go, Rust and jpeg-js accepted 14. Progressive coding, fitted tables and restart markers changed the size and no pixel, which fits the claim on Wikipedia that progressive files can compress a little better. Quality and sampling changed the picture in every decoder.
Convert a JPG file to another format
Converters decode the picture and write it again, so PNG files made from JPG files keep the loss that the JPG files already had. They do not bring back detail. JPG files made from PNG files lose detail once more. Keep the first file, and convert copies.
Some steps change no pixel. The jpegtran tool strips metadata without decoding, and its help lists crop, flip and rotate in 90 degree steps. Wikipedia says rotates and crops are lossless when the size fits the block grid. Our jpegtran rotate made a 480 by 320 file into a 320 by 480 file.
| To | Result |
|---|---|
| PNG | Lossless from the decoded pixels. The file grows, and the detail lost earlier stays lost. See the PNG file guide. |
| A page with the picture inside. Converters can add a watermark in a free version. See our PDF file page. | |
| ZIP | A zip does not make a JPG file smaller by much, because the data is already compressed. See ZIP files. |
| JPG with no metadata |
Use jpegtran -copy none. The pixels stay the same, and the Exif, the profile and the tail go.
|
How Does a JPG File Differ From PNG, GIF and BMP Files? Told apart by their first bytes
JPG files are lossy and made for photos, and PNG files are lossless and made for sharp edges. GIF files have 256 colors, and BMP files store raw pixels. They are told apart by their first bytes, which are FF D8 FF for JPG and 89 50 4E 47 for PNG.
People mix these types up, and the ending does not stop them. A PNG with a JPG name, our F6 copy, was refused by four of six readers. Tools that read one type refuse or misread another, so read the first bytes before you choose a tool.
We saved the same drawn picture of 480 by 320 pixels in every format with Pillow and counted the bytes. The picture is gradients, a circle and text, so it favors PNG more than a photo would. Photos are where JPG files shine, and our numbers do not test that.
| Format | First bytes | Bytes |
|---|---|---|
| JPG |
FF D8 FF
|
14,823 |
| PNG |
89 50 4E 47
|
16,950 |
| GIF |
47 49 46 38
|
26,569 |
| BMP |
42 4D
|
460,854 |
| WebP |
52 49 46 46
|
6,218 |
The JPG here is 14,823 bytes because it has no metadata. Our base.jpg has 15,745 bytes with its Exif, profile and comment. Wikipedia says JPEG reaches about 10 to 1 with an acceptable loss. On this drawn picture the PNG was only 14 percent larger, and a lossy WebP was far smaller.
The PNG file page covers lossless pictures, and the GIF file page covers palettes and animation. The BMP file page covers raw bitmaps. Other pages on this site cover ICO files and PSD files, which also store pictures.
What Is the JPG File Format Made Of? The facts we could check, and where they came from
The JPG file format is a chain of segments between the start marker and the end marker, with frame, tables, scan and optional metadata. The facts below come from Wikipedia, the Library of Congress, IANA, FileInfo and our checks of 1,052 public JPG files, 15 made files and 50 damaged copies.
| Part | What we could check |
|---|---|
| Name | JPEG, for the Joint Photographic Experts Group. Files end in .jpg or .jpeg. (Wikipedia, FileInfo) |
| Standard | ISO/IEC 10918, also published as ITU-T T.81. First public release on 18 September 1992. (Wikipedia) |
| MIME type |
image/, defined in RFC 2046 and registered with IANA. (IANA)
|
| Magic number |
FF D8 FF. The start marker alone is FF D8. (IANA, Library of Congress)
|
| Wrapper | JFIF 1.02 of 1 September 1992 is the common wrapper. It uses the APP0 segment. (Wikipedia) |
| Camera data | Exif uses the APP1 segment, which is limited to 64 KB. (Wikipedia) |
| Size limit | 65,535 by 65,535 pixels for JPEG and JFIF. (Wikipedia) |
| Modes | Baseline, progressive, lossless and arithmetic. Baseline is the most common. (Wikipedia) |
| Frame markers |
FF C0 for baseline and FF C2 for progressive. (Library of Congress)
|
| Security | Not inherently executable, and no privacy or integrity mechanism of its own. (IANA) |
| Our counts | 1,052 public JPG files, 15 made files, 50 damaged copies and six readers. |
About the values we list
Facts about the standard and wrappers come from sources named in every row. Counts were checked against our own JPG files. FileInfo is a directory, so we use it for the name and programs only. Wikipedia and the Library of Congress give the same start marker.
Wikipedia and the JFIF article disagree slightly on the JFIF date. One text gives 1 September 1992 and a reference list gives 12 September 1992. We use the first and call it version 1.02. The Exif standard has newer versions than those we read about, so we name no current version.
What we left out
We tested on Linux only. We used five decoders and one script. We opened no JPG file in Windows Photos, Apple Preview, a browser or a phone, and we scanned no file for malware. We read the Exif values our script prints, and no others.
Of our 1,052 public JPG files, 98 are test cases made to break readers. Our damaged copies change one thing at a time in copies of one base file made with Pillow. Readers that fail on our JPG files can work on yours, and the reverse is true as well.
Sources checked
For the format and its history, see the JPEG article on Wikipedia. For the wrapper, see the JFIF article. For camera data, see the Exif article.
For the markers, see the Library of Congress format record. For the MIME type and the security notes, see the IANA registration. For the ending, see the FileInfo page on JPG files.
We read the pages of SysTools and Freeviewer for the two tool cards. We could not read the Exif specification from CIPA, because its site blocks automated reads. Every count came from the files and scripts in our research folder.
Frequently Asked Questions About JPG Files
What is a JPG file?
JPG files are pictures saved with lossy JPEG compression. They start with the bytes FF D8 FF and keep the pixels in marked segments. Besides the pixels they can store the camera make, the time, GPS positions and small previews, and even other files after the end marker.
How do I open a JPG file?
Double click it. Every browser, phone and photo app opens JPG files, and Windows Photos, Apple Preview and Paint do too. To look inside, use a command like djpeg or a script that prints the markers and the Exif values. We opened no JPG file in a photo app.
Why does my JPG file not open?
Eight causes cover what we found. The file can be another type. Extra bytes can come first, or the file can be cut. End markers, tables or lengths can be missing or wrong. Picture data can be damaged, or your reader can lack support for the kind, like arithmetic coding.
Is a JPG file the same as a JPEG file?
Yes. FileInfo says both endings name the same format. The ending .jpg is more common because early versions of Windows allowed only three letters after the dot. Wikipedia also lists .jpe, .jif, .jfif and .jfi.
Is a JPG file safe to open?
Looking at pictures is low risk, and IANA says JPEG data is not inherently executable. The privacy risk is higher, because Exif can store GPS positions, times and cameras. We scanned no JPG file for malware and opened none in an app. Remove the metadata before you share a photo.
What does a JPG file store besides the picture?
It can store JFIF segments and Exif blocks with the camera make, the time and GPS positions. It can also store XMP packets, color profiles, comments and bytes after the end marker. Of 956 public JPG files that parse, 547 store Exif and 66 store GPS positions.
Can a JPG file hide a zip archive?
Yes. In our test, a zip placed after the end marker passed five decoders with the same pixels and no warning, and Python and unzip listed the file inside it. Our script warned about the extra bytes. Of 956 public JPG files, 83 have bytes after the end marker.
How can I remove the location from a JPG file?
Remove the Exif data. The jpegtran tool with the option copy none rewrote our test JPG file with the same pixels and no Exif, XMP, profile or comment. It also dropped extra bytes after the end marker. Rotate the pixels first, because removing the orientation tag leaves a sideways picture sideways.
How can I check if a JPG file is damaged?
Hash the JPG file, read its first bytes, decode it with djpeg and use a script that prints the markers and the Exif values. Then open it with a second decoder. Five decoders and our script gave the same answer on only 2 of 50 damaged copies, so one reader proves little.
Does saving a JPG file again lose quality?
Yes, when you save again in a lossy mode. Wikipedia calls this generation loss. Some steps are lossless, like stripping metadata and rotating in 90 degree steps with a tool that works on the segments. In our test, jpegtran kept every pixel when it stripped the metadata.
What is the MIME type of a JPG file?
The MIME type is image/jpeg, registered with IANA. IANA lists jpg and jpeg as endings and FF D8 FF as the magic number. Older Internet Explorer versions sent image/pjpeg when they uploaded files, per Wikipedia. A server sends the type so that browsers show the picture.
Should I use JPG, PNG or WebP?
Use JPG files for photos when small size matters more than perfect edges. Use PNG files for sharp lines and transparency. In our test of one drawn picture, the JPG took 14,823 bytes, the PNG 16,950 and the lossy WebP 6,218. Drawn pictures favor PNG more than photos do.
Still Stuck With a JPG File? Two routes out, and where to look next
Read the first bytes and follow the six steps. The cause table lists what to try in order.
Read what a JPG file can give away, and then how to strip it.
Is it another picture type? The page on PNG files covers lossless pictures, and the page on GIF files covers palettes and animation.
Is it a raw bitmap? The page on BMP files covers raw pixels, and the page on ICO files covers icons.
Is it a zip in disguise? Zips can follow the end marker. The page on ZIP files covers the container, and the tail check shows how to look for one.
Is it an unknown file type? Browse the file extension resource library, or send it on the submit a file type page.