"Not a JPEG file: starts with 0x..."

This message comes straight from libjpeg the instant it reads your file and the first two bytes are not FF D8. In most cases the picture is still in there — a handful of stray bytes have been pushed in front of the real start, or the markers are damaged. Realigning to the true SOI is a structural fix, and it does not require sending your photo anywhere.

Almost every JPEG decoder — Photoshop, ImageMagick, Python's Pillow, countless viewers — is built on libjpeg, and its very first act is to check that a file begins with the Start-of-Image marker FF D8. If the two leading bytes are anything else, it aborts before decoding a single pixel and prints Not a JPEG file: starts with 0x## 0x##. The bytes it reports are the clue. Drop the photo above and the tool scans past whatever junk is in front, resyncs to the real FF D8, re-validates the marker segments, and salvages the scan — entirely inside your browser tab.

Your file never leaves your device — repair runs in your browser. 0 bytes uploaded.

Your data is the big block — usually intact. What breaks is the small index. Repair rebuilds it.

Drop a file here or browseTap to choose a file

ZIP · Office · PDF · video · JPG · PNG · RAR/7z · SQLite — repaired right here in your browser. Nothing is uploaded

0 bytes uploadedFree: 3 repairs/day — up to 500 MB video, 100 MB docs & archives, 50 MB photos. Free download. Single repair €5.90. No account required.

Fix it now

  1. Drop the file
  2. Repair runs locally
  3. Download the result

The error is a two-byte check, not a verdict on your image

A JPEG is a stream of markers, each a 0xFF byte followed by a type byte. The file must open with the Start-of-Image marker, FF D8, and end with End-of-Image, FF D9. Between them sit the metadata and table segments — APP0 (JFIF) or APP1 (Exif), one or more DQT quantization tables, a SOF0/SOF2 frame header that declares width, height and components, the DHT Huffman tables, then SOS (Start of Scan) followed by the entropy-coded pixel data.

When libjpeg's jpeg_read_header() runs, its first_marker routine reads exactly two bytes and compares them to FF D8. If they do not match it raises JERR_NO_SOI, whose text is literally Not a JPEG file: starts with 0x%02x 0x%02x — the two hex values are just whatever it actually found. Notice what this means: the decoder never looked at your photo. It rejected the file on its first two bytes. So the message does not tell you the image is destroyed — it tells you the file does not begin the way a JPEG must. That distinction is the whole reason recovery is often possible.

What the leading bytes are telling you

The two hex values in the message are a genuine diagnostic. If they are close to FF D8 but shifted, or if the file is a valid JPEG with garbage bolted onto the front, resyncing recovers it. If they are the signature of a different format, you are looking at a mislabelled file, not a broken JPEG. Common leading bytes:

  • 0x89 0x50 — a PNG (‰PNG). The file was saved or renamed as .jpg but is genuinely a PNG; it needs PNG repair, not JPEG resync.
  • 0x00 0x00 — often a HEIC/MP4-family file whose ftyp box starts with a length field, or a truncated download that begins in zeroes.
  • 0x52 0x49 (RI) — a RIFF container, usually WebP.
  • 0x49 0x49 or 0x4D 0x4DTIFF byte-order marks, typical of camera RAW files.
  • 0xEF 0xBB — a UTF-8 BOM: text (an HTML error page, a base64 blob) was saved with a .jpg name.

The most recoverable case is a real JPEG with a garbage prefix: bytes shoved ahead of the true FF D8. This happens constantly — a recovery/undelete tool that prepends its own header, an interrupted download that captured stray HTTP bytes, a card reader that returned junk at the start of the sector, or two files concatenated by a botched copy. The genuine FF D8 is still present a little further in; the decoder just never scanned for it.

How the repair realigns and salvages the file

The tool does not trust the first two bytes. It scans the buffer for the true SOI — the FF D8 that begins a real JPEG stream — and strips everything before it, so the file starts where libjpeg expects. From there it walks the marker chain forward and re-validates each segment: it checks that APPn, DQT, SOF, DHT and SOS markers are well-formed and that segment lengths are sane, repairing or resynchronising markers that were mangled and re-anchoring the scan that follows SOS.

Then it salvages the entropy-coded scan — the compressed pixel data itself — and writes out a clean, well-ordered JPEG that decoders will accept. The rebuilt file is validated against Apple's ImageIO, the same decoder your Mac and iPhone use, so a file that opens here opens in Preview, Photos and everywhere else. Every byte of this runs in your browser: the photo is read from disk, realigned and re-emitted locally, and you can confirm in the DevTools Network tab that zero bytes are uploaded — which matters when the image is personal, evidentiary or not yet published.

Where a JPEG genuinely can't be brought back

Resyncing only helps if there is a JPEG to resync to. If the message reports the signature of another format — 0x89 0x50 for PNG, a RIFF/TIFF/HEIC mark — then the file is not a JPEG at all, and no amount of JPEG repair changes that. It may still be a perfectly good image in its real format (try JPEG repair only after confirming it is one), but a PNG has to be treated as a PNG. Renaming a file never changes its bytes, and neither can we.

The second hard limit is truncation. If the scan after SOS was cut short — a download or card copy that stopped partway — the top of the image is recoverable but the rows past the cut were never written; they come back as gray or repeated blocks, and no tool can invent pixels that were never saved. Finally, if the quantization or Huffman tables are missing or corrupt beyond reconstruction, or the entropy data itself is genuinely scrambled rather than merely misframed, the scan cannot be decoded — realigning the container does not regenerate destroyed compressed data. This is the honest ceiling: we fix how the file is framed and salvage what was actually recorded; we do not fabricate image content.

What this can and can't fix

Can fix

  • A real JPEG with junk bytes in front of the FF D8 SOI marker (recovery-tool headers, stray download bytes, concatenated files)
  • Damaged or out-of-order APPn/DQT/SOF/DHT/SOS markers that a strict decoder refuses
  • A scan that is intact but unreachable because the header the decoder read was wrong
  • Files that error in libjpeg-based apps (Photoshop, ImageMagick, Pillow) but still contain a valid stream
  • Output validated against Apple ImageIO, so a fixed file opens in Preview and Photos too

Can't fix

  • A file that is actually another format (PNG 0x89 0x50, WebP, HEIC, TIFF/RAW) — that is not a JPEG and needs its own repair
  • Image rows past a truncation point — those pixel bytes were never written to disk
  • A scan whose Huffman/quantization tables are gone or whose entropy data is genuinely scrambled, not just misframed
  • A 0-byte file, or one with no FF D8 marker anywhere in it

If a repair fails, we tell you why (missing data versus broken structure), and you are never charged for a failed repair.

FAQ

What does the '0x...' part of the message actually mean?

It is the two bytes libjpeg found where it expected the FF D8 Start-of-Image marker. They are a diagnostic: 0x89 0x50 means the file is really a PNG, 0x49 0x49 or 0x4D 0x4D points to TIFF/RAW, and values near FF D8 usually mean a genuine JPEG with a few stray bytes in front that can be stripped.

The bytes are 0x89 0x50 — can you still fix it?

Not as a JPEG. 0x89 0x50 is the PNG signature, so the file was saved or renamed with the wrong extension. It may be a perfectly healthy PNG — see corrupt screenshot PNG or a PNG CRC error if it will not open — but JPEG resync cannot apply to bytes that were never a JPEG.

Why does one broken byte at the start reject the whole photo?

Because libjpeg checks the Start-of-Image marker before it decodes anything. Its first_marker routine reads two bytes, and if they are not FF D8 it aborts immediately with this error — even when the real image data further in is completely intact. Realigning to the true marker lets the decoder proceed.

Is my photo uploaded to be repaired?

No. The file is read from your disk, realigned and re-emitted entirely in your browser; nothing is transmitted. You can watch the DevTools Network tab and confirm 0 bytes leave — which matters for personal photos, legal evidence and unpublished work.

Related: Repair a JPEG · PNG CRC error · Corrupt screenshot PNG