How image formats are detected

The first thing every tool on FileConverters does with a file is decide what it actually is. That decision is made by a small pure function, detectFormat, that reads the file's leading bytes and compares them with known signatures — never by trusting the filename. This guide walks through what that function sees for each of the 7 formats it recognizes, how it parses the HEIF container that HEIC and AVIF share, how an extension mismatch is reported, and how the animation checks decide when the "first frame only" caveat applies. Every verdict below was produced by the detector itself, at build time, on the illustrative byte arrays described beside it. The methodology page states the rules; this page shows them running.

Bytes, not names

A filename is a label somebody attached; the bytes are the file. A file can arrive as .png while holding JPEG data, or carry .jpg over a HEIF container. A converter that trusted the label would hand those bytes to the wrong decoder or stamp the wrong extension on the output. So the detector runs first, the extension is compared against its verdict afterwards, and the conversion proceeds from what the file is. The output extension comes from the engine's canonical table: a JPEG output is always written as .jpg, a WebP as .webp.

Format MIME type Output extension
PNG image/png .png
JPEG image/jpeg .jpg
WebP image/webp .webp
GIF image/gif .gif
BMP image/bmp .bmp
HEIC image/heic .heic
AVIF image/avif .avif

The flat signatures: PNG, JPEG, WebP, GIF, BMP

Five formats announce themselves with fixed bytes at the very start of the file, and the detector checks them in order. PNG is a fixed prefix: the illustrative buffer built here begins 89 50 4E 47 0D 0A 1A 0A and is PNG; truncating that buffer byte by byte, the detector answers PNG from 8 bytes up and nothing shorter, so the first 3 bytes alone are unrecognized, because a partial signature is not a match. JPEG is the 3-byte prefix FF D8 FF: the bytes FF D8 FF E0 00 10 4A 46 49 46 are JPEG, but FF D8 00 10, which has the first two bytes and not the third, is unrecognized.

WebP lives inside a RIFF container, so the check has two parts: RIFF at the start and WEBP at byte 8, after the container's size field. That position is not typed on this page; it was found by sliding WEBP along a RIFF header until the detector answered WebP. A RIFF file carrying the form type WAVE instead is unrecognized — the container alone proves nothing. GIF matches either GIF87a or GIF89a; both header variants built here come back GIF and GIF. BMP is the shortest signature, BM, so the detector also enforces a minimum length, found here by probing: a 14-byte BM header built here is BMP, and the same bytes cut to 13 are unrecognized. Plain text matches nothing: the bytes of "hello world, not an image" are unrecognized.

HEIF containers: parsing the ftyp box

HEIC and AVIF have no fixed signature at byte zero. Both are ISO base media files, and what identifies them is the brands declared in the ftyp box at the start of the file. The box is laid out as fixed 4-byte fields — size, type, major_brand, minor_version — followed by any number of 4-byte compatible brands up to the box's declared size. The detector reads the FourCC at byte 4, the major brand at byte 8, skips the minor version at byte 12, and collects compatible brands from byte 16 to the end of the box. The engine publishes none of these offsets, widths or minimums as constants, so this page found each one the way it finds every verdict: by running the detector over a sweep of candidate inputs at build time and recording where its answer changes — sliding a brand along a box, truncating a valid box byte by byte, counting a declared size down. The methodology page's phrase for the parser is "bounded by the declared box size, never by substring-searching the file", and the bounds are what make it safe on hostile input:

  • Input shorter than the size, type and major-brand fields together (12 bytes) is rejected before any brand is read: a bare ftyp header of 8 bytes is unrecognized, a valid heic box cut to 11 bytes is unrecognized, and the same box cut to 12 bytes — exactly those three fields — is HEIC.
  • A declared size below the 16 bytes the fixed fields occupy is invalid: a box declaring 15 bytes while spelling heic as its major brand is unrecognized; the same bytes declaring 16 are HEIC.
  • A declared size larger than the data is clamped to the bytes present, so a box claiming FF FF FF FF bytes still reads its one brand, heic, and is HEIC.
  • The minor-version field is never a brand: a box whose minor version spells heic yields the brand list abcd and is unrecognized.
  • Bytes after the box are never read: an isom box followed by the text heicheic yields isom, iso2 and is unrecognized.
  • A file cut off mid-brand stops cleanly: a box declaring two compatibles but truncated to 18 bytes yields mif1.
  • A leading box that is not ftyp ends the attempt: a moov box with heic inside it is unrecognized.

HEIC or AVIF: how the brands decide

Once the brand list exists, two rules settle the verdict. Any explicitly HEVC-coded brand — heic, heix, hevc, hevx — makes the file HEIC, and that check runs first, so a file declaring both codecs is identified as HEIC and is handled as HEIC by every tool that accepts it. With no HEVC brand present, any avif or avis brand, major or compatible, makes the file AVIF. The structural brands mif1 and msf1 count as HEIC unless an avif or avis brand is also declared: the box built here declaring only mif1 and miaf is HEIC, and the same major brand with avif among its compatibles is AVIF. Brand lists that belong to neither family fall through to unrecognized, because the detector does not guess. The table shows the verdict on each illustrative box, with the image-sequence flag that isAnimatedAvif derives from the avis brand. After the verdict, the families part: HEIC goes to the self-hosted libheif decoder, AVIF to the browser's own codec behind a runtime feature probe — see the HEIC and AVIF guide.

Illustrative box Brands read Verdict Image sequence
HEVC-coded still, heic major heic, mif1, heic HEIC no
Structural major with HEVC compatibles mif1, mif1, heic, hevc HEIC no
Structural brands only mif1, mif1, miaf HEIC no
AV1-coded still, avif major avif, avif, mif1, miaf AVIF no
AV1 image sequence, avis major avis, avis, msf1, miaf AVIF yes
Structural major, avif compatible mif1, mif1, avif AVIF no
Both avif and heic declared avif, avif, heic HEIC no
Brands outside both families isom, iso2, avc1, mp41 unrecognized no

The extension notice

After detection, checkExtension compares the verdict with what the filename claims. A mismatch is reported only when both sides are known and disagree; an unknown extension or unrecognized bytes produce no notice. The claim comes from formatFromExtension, which lowercases the extension and treats aliases as one claim: .jpg .jpeg .jfif all claim JPEG, and .heif claims HEIC. Extensions the engine does not know — .tiff .txt — claim nothing. The filenames below are invented; the bytes are the fixtures built above.

Illustrative filename Bytes are Name claims Mismatch notice
screenshot.png JPEG PNG yes
IMG_0001.jpg HEIC JPEG yes
photo.heic AVIF HEIC yes
photo.jpeg JPEG JPEG no
scan.JFIF JPEG JPEG no
IMG_0002.HEIF HEIC HEIC no
download.bin PNG nothing no
README WebP nothing no
note.png unrecognized PNG no
archive.png unrecognized PNG no

The extension itself is read by fileExtension with plain rules: only the text after the last dot counts, so archive.tar.png reads as png; case is folded; directory names are ignored even when a folder in the path has a dot; and a name with no extension, a name ending in a dot, or a dotfile such as .gitignore reads as none and claims nothing.

Animation: when the first-frame caveat appears

Canvas decoding keeps one frame, so the tools state "first frame only" before converting an animated file — and only then. The container says whether animation is possible; a per-file check says whether this file is animated. For WebP, isAnimatedWebP walks the RIFF chunk list: a VP8X chunk whose flags byte has the animation bit set, or any ANIM or ANMF chunk, means animated. A plain VP8 still built here reports no; a VP8X chunk with flags 10 and no animation chunk reports no; flags 02 report yes; and a still-flagged VP8X followed by an ANIM chunk reports yes.

For GIF, isAnimatedGif walks the block structure past the header and logical screen descriptor, skipping extension blocks and counting image descriptors; more than one is animation. One image descriptor reports no, two report yes, five report yes. An unparseable structure stops the walk and reports not animated rather than guessing. For AVIF, the avis brand in the ftyp box is the signal, as the table above shows. The planner carries the container-level fact separately: planConversion sets animationDropsToFirstFrame to true for GIF to PNG and true for AVIF to JPEG, but false for HEIC to PNG, because HEIC is not in the engine's list of animatable containers.

What the verdict is used for

The detected format is the source the planner receives, and from it the plan decides flattening, quality and the output name — the subject of the conversion pipeline guide and the transparency guide. AVIF to JPEG plans a background flatten (true) because a HEIF image can carry an alpha plane and JPEG cannot; GIF to PNG does not (false). The detector also runs a second time, on the output: the produced bytes are sniffed before the download button appears. A PNG-to-WebP plan asks for image/webp; if the bytes that came back begin with the PNG signature, the detector reports PNG, the mismatch is reported as an error, and no mislabeled file is offered.

What detection cannot tell you

A matched signature means the file begins the way that format begins. It does not mean the rest of the file is intact, and it says nothing about the pixels: dimensions and transparency come from decoding, and nothing in this step measures or judges quality. Formats outside the engine's table have no signature to match and no extension claim, so a .tiff is reported as unrecognized rather than mis-identified. Detection changes nothing: it reads, reports, and hands the verdict on; your original bytes are never rewritten.

Common misreadings

  • "The tool renamed my file wrong." The output extension follows the tool's target format, not the input's name or its detected format. An illustrative screenshot.png holding JPEG dropped on a to-PNG tool is a JPEG-to-PNG conversion, and the row's notice says so; because the swapped name would be unchanged, the engine appends its suffix — screenshot-converted.png — so the download does not shadow the original. The same file on a to-JPG tool is written as screenshot.jpg, with no suffix needed.
  • "It calls my .heif a HEIC." The engine uses one label, HEIC, for HEVC-coded HEIF regardless of extension; .heif is an alias for the same claim. AV1-coded HEIF is the separate AVIF format.
  • "The animation notice is missing." It appears only when the per-file check finds animation. A single-frame GIF or a still WebP with a VP8X chunk is not animated.
  • "Unrecognized means corrupt." It means the leading bytes matched no supported signature — a format outside the table, a truncated header, or a file that is not an image. The detector reports what it can prove, not why the match failed.

Frequently asked questions

Why does the tool say my .png is a JPEG?

Because its bytes are. The detector reads the file's leading bytes before looking at the name; an illustrative file called screenshot.png, built here, whose bytes begin FF D8 FF is identified as JPEG, the name claims PNG, and the row shows a notice. The conversion then runs from the actual format.

Does the detector read the whole file?

No. detectFormat matches the leading bytes against the format signatures and, for HEIF containers, parses only the ftyp box at the start of the file, bounded by its declared size. The animation checks walk further — the RIFF chunk list for WebP, the block structure for GIF — but only to answer that one question. Decoding is a separate step.

How does it tell HEIC and AVIF apart when both are HEIF files?

By the brands declared in the ftyp box. Explicitly HEVC-coded brands (heic, heix, hevc, hevx) win: an illustrative box built here declaring both avif and heic is HEIC. With no HEVC brand present, any avif or avis brand makes the file AVIF, as the avif box built here shows. The structural brands mif1 and msf1 on their own are treated as HEIC: the box built here declaring only mif1 and miaf comes back that way.

Why is a RIFF file not always a WebP?

RIFF is a generic container. The detector requires both RIFF at the start and WEBP at byte 8; an illustrative RIFF file built here whose form type is WAVE is unrecognized.

When does the "first frame only" notice appear?

When the per-file animation check says so: a WebP with the VP8X animation flag or an ANIM chunk, a GIF with more than one image descriptor, or an AVIF declaring the avis brand. A single-frame GIF built here reports animated: no; the same structure with two frames reports yes.

Does detection send any part of my file anywhere?

No. The detector is a pure function over a byte array, running in your browser tab with the rest of the pipeline. The site is static files with no upload endpoint, and its Content-Security-Policy restricts connections to its own origin — the methodology page shows how to confirm this.

More in this series: all guides, including the conversion pipeline and HEIC and AVIF decoding and licensing. Tools that lean on detection: HEIC to JPG, AVIF to PNG, WebP to PNG and PNG to JPG.

Local processing: detection reads a file's leading bytes inside your browser tab, and every step that follows stays there — files are decoded, converted, and verified entirely in your browser and never transmitted. See the methodology page for how to verify the no-upload claim yourself.