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
ftypheader of 8 bytes is unrecognized, a validheicbox 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
heicas 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 FFbytes still reads its one brand, heic, and is HEIC. -
The minor-version field is never a brand: a box whose minor version spells
heicyields the brand list abcd and is unrecognized. -
Bytes after the box are never read: an
isombox followed by the textheicheicyields 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
ftypends the attempt: amoovbox withheicinside 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.pngholding 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 asscreenshot.jpg, with no suffix needed. -
"It calls my .heif a HEIC." The engine uses one label, HEIC,
for HEVC-coded HEIF regardless of extension;
.heifis 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
VP8Xchunk 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.