Documentation menu
Drop-to-render pipeline
How a dropped file becomes an on-screen analysis: the exact resolution order from handleFile through classification, content sniffing, and format catalog lookups, down to the unknown.js fallback. For engineers adding a new format or debugging misrouted files.
On this page
Overview flow#
File dropped/selected
-> app.js: handleFile(file)
-> resolveKind(file) (app.js:120)
-> classifyFile(file) (classify.js) name/MIME only
-> SPICE .raw disambiguation (sniffSpiceRaw, for ext === 'raw')
-> VARIANT_REROUTE check (ambiguous extensions, e.g. .ts)
-> resolveByContent(file) (file-sniff.js) only if kind is
'unknown'/'extensionless'
-> ROUTES[kind] (app.js) -> lazy-loaded renderer
-> renderer(file, container) draws the result cards
Every stage after classifyFile() exists to correct or fill in a kind that extension/MIME alone got wrong or couldn't determine - a lying extension, an ambiguous one, or none at all.
classifyFile() (name/MIME only, no byte reads)#
web/assets/js/core/classify.js exports classifyFile(file), which maps a file to a ROUTES key purely from its file.name extension and file.type MIME string - it never reads bytes (byte sniffing is layered on top by the caller). It is a long if-chain, roughly in this precedence order:
- SVG (MIME or
SVG_EXTS) - checked first so it doesn't fall through to a generic image MIME. - Dozens of specific extensions mapped to dedicated renderer kinds (
docx,xlsx,epub,dwg,kicad_*,aep,stl,gcode,psd,mobi, ...) - ordered deliberately so a format with a misleading vendor MIME (.dwgis often served asimage/vnd.dwg,.midasaudio/midi) is caught by its extension before the genericimage//audio//video/MIME fallback below would misroute it. - Generic MIME fallback:
image/*->photo,audio/*->audio,video/*->video. PHOTO_EXTS/AUDIO_EXTS/VIDEO_EXTS(fromformats.js) as an extension-only fallback for files with no informative MIME.isProprietaryExt(ext)(fromproprietary.js'sFORMATStable) ->proprietary.- Licence/marker files with no real extension (
LICENSE,COPYING,py.typed,CACHEDIR.TAG) ->plaintext. - No extension and nothing else matched ->
extensionless(treated as text with a hex fallback, not flagged "unknown"). - Otherwise ->
unknown.
classifyFile is also exposed as window._anrClassify for the folder scan and the /compare page.
Content-based sniffing (file-sniff.js)#
web/assets/js/core/file-sniff.js determines what a file actually is from its leading bytes, independent of its name, so a lying or missing extension doesn't produce a wrong or "unknown" result. Three exports:
sniffFileType(file)- reads the first 264 bytes and checks magic numbers for ~25 common containers (PNG, JPEG, GIF, BMP, TIFF, PSD, RIFF-based WEBP/WAVE/AVI, ISO-BMFFftypbrands -> HEIC/AVIF/M4A/3GP/MP4, Matroska, Ogg, ID3/MPEG audio, FLAC, ZIP, RAR, 7z, ar, SQLite, gzip, XZ, Zstandard, bzip2, LZ4,.Zcompress, ELF, Windows EXE, PostScript/EPS, DICOM, TAR, legacy LZMA, and a trailing SVG-tag heuristic). Returns{ kind, ext, label }ornull.resolveByContent(file)- the single source of truth used by both the drop path and the folder-analysability scan. It layers, in order: a handful of niche proprietary/game-format magic checks (Raise3D ideaMaker, DJI firmware, Unity manifests, Valve.res, Android backups, ...), an HTML/SVG text heuristic, then falls through tosniffFileType()'s broad magic table, thensniffGitObject()(loose objects/packfiles), then a CSV/TSV heuristic (consistent comma/tab counts across the first lines). Returns{ kind, sniffedExt }, wherekindstays'unknown'if nothing matched.isReadableText(file)- true if the first 4KB has a UTF-16 BOM or is >85% printable ASCII; decides whetherextensionless/unknownfiles show as text or a hex dump. Shared with the folder scan viawindow._anrReadableText.
resolveKind() in app.js (web/assets/js/core/app.js:120) only calls resolveByContent when classifyFile returned 'unknown' or 'extensionless' - a recognised extension is trusted and never re-sniffed.
Ambiguous-extension reroutes#
Two mechanisms in app.js correct a classifyFile result using bytes for extensions that legitimately name more than one format:
VARIANT_REROUTE(app.js:75) - for extensions whoseclassifyFileroute is tuned for the common variant (.ts-> MPEG transport stream vs. TypeScript source;.dts-> DTS audio vs. Device Tree Source;.key-> Keynote vs. PEM key;.obj/.nc/.md/.mat/.modsimilarly ambiguous). Each entry names theprimaryvariant and a safe fallbackkind('plaintext'for a text/source variant,'unknown'for a binary one).resolveKind()reads the file's first 1024 bytes and callsdetectVariant()(see below); if the detected variant differs fromprimary, the file is rerouted to the fallback kind.- SPICE
.raw-classifyFileroutes.rawtophoto(camera RAW);resolveKind()separately callssniffSpiceRaw()to catch SPICE simulation output sharing the same extension and reroutes to'spice'.
formats.js: the central catalog#
web/assets/js/core/formats.js is the single source of truth for supported file types. It exports the lowercase classification sets (PHOTO_EXTS, AUDIO_EXTS, VIDEO_EXTS, CSV_EXTS, SVG_EXTS, ...) that classify.js and other modules read to route files, plus a curated display catalog (FULL_ANALYSIS / IDENTIFICATION, consumed by catalogGrouped()) that drives the format overlay, the about page's format tables, and the overlay search box. Adding an extension to an existing category is usually a one-line change here; see the file's own header comment and the add-file-format skill for the full workflow.
EXT_VARIANTS (formats.js:255) is a table of ambiguous extensions with a human-readable summary and an optional machine detect rule (a magic string/hex at a byte offset, or { default: true } as the fallback). detectVariant(ext, bytes, text, opts) (formats.js:428) looks up the extension, evaluates each variant's rule in order, and returns the first matching variant's name (or the default) - this is what backs both the in-app "this looks like X, not Y" readout and the VARIANT_REROUTE correction in app.js described above.
Proprietary-format dispatch (proprietary.js)#
For files classified as 'proprietary', renderProprietary(file, container, extOverride) (web/assets/js/renderers/proprietary.js:3944) looks the extension up in the FORMATS table (imported from proprietary-formats.js, the large reference table of 200+ formats: app name, icon, optional magic check, optional chunk name). If the format entry names a chunk, the renderer lazily imports ../parsers/parsers-<chunk>.js (proprietary.js:3981) and calls that chunk's PARSERS[ext] function with the file's header bytes - so the ~200 format-specific binary parsers are split into per-domain lazy chunks (parsers-audio.js, parsers-video.js, parsers-image.js, parsers-docs.js, parsers-dev.js, parsers-archive.js, parsers-gaming.js, parsers-threed.js, parsers-geodata.js, parsers-sci.js, parsers-security.js, parsers-email.js, parsers-disk.js, parsers-osmisc.js, parsers-raw.js) rather than one huge always-loaded module. isProprietaryExt(ext) (proprietary.js:4224) is simply ext in FORMATS, used by classifyFile()'s fallback. See parsers-and-libs.md for the chunk/lib inventory.
The unknown.js fallback#
web/assets/js/renderers/unknown.js is the last resort for a 'unknown' or 'extensionless' kind (and is also reused as the generic "identify this blob" helper elsewhere, e.g. carved/recovered files). guessFormat(bytes) independently re-checks a similar magic-number table (PDF, PNG, JPEG, GIF, WAV/WebP/AVI via RIFF, Ogg, FLAC, ID3/MPEG audio, ftyp-brand MP4/MOV/M4A, ZIP, 7z, gzip, RAR, ELF, MZ EXE/DLL, XML, SQLite, BMP, ICO, TIFF, Matroska, Java class/Mach-O fat binary, plus a couple of 3D-printer-specific text signatures) purely to produce a human-readable label for display, then falls back to a UTF-16/printable-ASCII heuristic to decide between a text preview and a raw hex/ASCII dump. It also computes SHA-256 and offers enhanced previews for plain text, JSON and XML, and can hand off to photo-recover.js's carveImages/repairJpeg when an embedded or truncated image is detected inside an otherwise-unknown blob.
Keeping the page alive during heavy analysis (yieldToMain)#
Analysis runs on the main thread, so a chain of unbroken synchronous passes freezes the tab outright - scrolling stops and clicks queue until the last one returns. yieldToMain() in core/util.js is the shared escape hatch: it awaits a requestAnimationFrame (falling back to setTimeout when the tab is hidden, where rAF never fires) so the browser gets to paint and service input between slices of work.
Two conventions build on it, both currently modelled by renderAudio in renderers/audio.js:
- Yield between heavy passes. Whole-file sweeps (loudness, true peak, DTMF, each whole-file STFT) are separated by
await yieldToMain(). This costs about a frame per pass and in exchange the page stays responsive throughout. - Show the card first, fill the slow rows after.
pendingRow(label, help, atStep)builds a readout row whose value is aasciiBar(); the card is appended immediately with its cheap rows complete and its expensive ones pending, then each row'sfill(value, extra)(ordrop(), when there turned out to be nothing to report) runs as its pass lands. Row order and card shape are identical to a synchronous build - only the arrival is staged. The bar is determinate, not a looping one:atStepis the index of the pass that produces that row's value, and a shared step counter advanced after each pass fills every outstanding bar in proportion, so each reaches full exactly as its value lands. - Finish the headline visual before starting anything else. Audio builds the spectrogram and awaits its
firstPaintbefore the forensic passes begin, so the main visual gets the thread to itself rather than competing with analysis the reader isn't waiting on.
The same shape applies to panels that are collapsed by default: build them empty and compute on the <details> toggle event rather than on load. The ELA and LSB panels in renderers/photo.js and the OCR canvas both do this, so a forensic read that most visitors never open costs nothing until it is opened.