Raster Format Dispatch#
When WISER is handed a file path, something has to decide which of the raster
formats it knows about should open it. That decision is format dispatch, and
it lives in src/wiser/raster/format_registry.py and
RasterDataLoader.load_from_file.
This page documents how a path becomes a RasterDataSet, and — the part most
developers come here for — how to add support for a new file
type.
Overview#
Dispatch is deliberately split into three stages, so that the cheap decisions happen before the expensive ones and nothing irreversible happens while WISER is still guessing.
Stage |
What it does |
Cost |
|---|---|---|
Order |
Rank the registered formats for this path, most likely first. |
String comparison only. |
Identify |
Ask each candidate, in order, whether it recognizes the file. |
A few bytes read per candidate. No handles opened. |
Open |
Open the file with the format that won. |
One file handle, once. |
flowchart TB
Path["load_from_file(path)"]
Override{"format= given?"}
One["Use that format only.<br/>Fail loudly if it doesn't work."]
Order["candidates_for(path)<br/>extension-claimants first, then<br/>everything else, then the catch-all"]
Ident["identify() down the list"]
Yes{"Confidence.YES?"}
Win["Winner"]
Maybe["Highest-priority MAYBE"]
Open["try_load_file() on the winner"]
Build["FormatSpec.loader -> RasterDataSet(s)"]
Path --> Override
Override -- yes --> One --> Open
Override -- no --> Order --> Ident --> Yes
Yes -- "stop immediately" --> Win --> Open
Yes -- "none were certain" --> Maybe --> Open
Open --> Build
Why not just try every format?#
That is what WISER used to do, and it caused three problems worth remembering, because each one is a rule the current design exists to enforce:
The winner was whichever format was tried last. The old loop had no
break, so every format was attempted and the last success overwrote the earlier ones. Which implementation opened a file therefore depended on the declaration order of a dict.Every successful attempt opened a file handle, and all but one were discarded.
Identification could block on the user. The NetCDF sub-dataset dialog ran inside the attempt, so it could be shown for a file that a later format then claimed, throwing away what the user chose.
Hence: exactly one format wins, identify() never opens or prompts, and only
the winner is opened.
The registry#
Every format WISER can open is one FormatSpec entry in RASTER_FORMATS:
FormatSpec(
name="ENVI", # the override token
impl=ENVI_GDALRasterDataImpl, # class that opens it
extensions=frozenset({"", ".hdr", ".img", ".dat"}),
priority=70,
loader=load_normal_dataset,
interactive_step=False,
)
Field |
Meaning |
|---|---|
|
Stable identifier. Used by the |
|
The |
|
Extensions this format claims. An ordering hint only (see below). |
|
Higher is tried earlier. Breaks ties between equally confident formats. |
|
How to turn the opened impl into datasets. Usually |
|
True if opening can block on a dialog. Informational today; marks what must stay on the main thread when loading moves to a worker. |
|
True only for the GDAL catch-all, which always sorts last. |
Extensions order, they do not gate#
This is the single most important thing to understand about the registry.
A format is never excluded for failing to claim the path’s extension. The
extension only decides where in the queue a format sits. candidates_for()
returns every registered format for every path — extension-claimants first
by priority, then everything else by priority, then the catch-all.
The reason is that extensions in this domain are genuinely ambiguous:
Extension |
Could be |
|---|---|
|
ENVI raster or PDS3 |
|
ENVI raster header or ENVI spectral library |
|
PDS4 label or any other XML |
|
PDS3 label |
(none) |
ENVI data file |
A strict extension-to-format map would be wrong on real data immediately, and would make a file with an unusual extension unloadable even when WISER could read it perfectly well. Ordering gives the speed and determinism of extension dispatch without the brittleness.
identify()#
RasterDataImpl.identify(path) returns a Confidence:
Value |
Meaning |
Effect on the search |
|---|---|---|
|
Positive content match. |
Wins immediately; the search stops. |
|
Plausible, no positive signal. |
Kept as a fallback if nothing is certain. |
|
Positively excluded. |
Dropped. |
The base implementation returns MAYBE — “no opinion” — so an implementation
that doesn’t override it stays in the running but never outranks a format that
is actually sure.
identify() must be cheap and side-effect-free. Specifically it must not:
open a lasting handle or allocate significant memory,
show a dialog or otherwise block on the user,
raise for an unreadable or missing file — return
NOinstead.
It delegates to GDAL#
For GDAL-backed formats, identify() does not hand-roll magic-byte checks.
It calls gdal.IdentifyDriverEx, which runs the driver’s own Identify()
without opening the dataset:
@classmethod
def identify(cls, path: str) -> Confidence:
drv = gdal.IdentifyDriverEx(
cls.get_load_filename(path), allowed_drivers=list(cls.get_gdal_drivers())
)
return Confidence.YES if drv is not None else Confidence.NO
GDAL already maintains the magic-byte and label checks for every driver it ships; duplicating them in WISER would only create something to drift.
Note
The Python binding keyword is allowed_drivers, not allowedDrivers, and
passing None raises rather than meaning “any driver”. Omit the argument if you
want every driver considered.
priority still matters#
It is reasonable to ask why priority matters if the first YES wins. Because
identify() is not a perfect oracle: an .img with a PDS label sitting beside
an ENVI header will get a confident yes from both formats. identify()
decides where the walk stops; priority decides the order it walks, and
therefore which of two equally confident formats is asked first.
Sidecar files: DATA_EXTENSIONS and SIDECAR_EXTENSIONS#
Some formats are a header file plus a separately-named data file, and the user
may legitimately select either one. The file-open dialog offers *.hdr and
*.tfw filters, so this is a normal thing to happen, not an edge case.
Two class attributes describe that relationship:
class ENVI_GDALRasterDataImpl(GDALRasterDataImpl):
DATA_EXTENSIONS = ("", ".img", ".dat") # the raster itself, in resolution order
SIDECAR_EXTENSIONS = (".hdr",) # what the user might click instead
DATA_EXTENSIONS does two jobs:
Resolution.
get_load_filename("scene.hdr")triesscene, thenscene.img, thenscene.dat, and returns the first that exists. This runs beforeidentify()— GDAL will not identify the header file itself, so skipping it would make WISER reject a dataset the user validly selected.Exclusion.
identify()returnsNOfor a path whose own extension is not in the list. This matters because GDAL’s ENVI driver claims any file with a same-stem.hdrbeside it — without the restriction, an unrelatedscene.tifsitting next toscene.hdrwould be identified as ENVI.
Set DATA_EXTENSIONS = None (the default) for a self-describing single-file
format, which imposes no extension restriction. That is the right choice when
the driver’s own content check is strong and extensions vary in the wild — PDS3
and NetCDF both do this.
Adding a new format#
Adding support for a new file type is normally two steps.
1. Write the implementation#
Subclass GDALRasterDataImpl in src/wiser/raster/dataset_impl.py and declare
which GDAL drivers it uses:
class MyFormat_GDALRasterDataImpl(GDALRasterDataImpl):
GDAL_DRIVERS = ["MYDRIVER"]
# Only if the format has a header/data split, or you need to stop it
# claiming unrelated files that share a basename:
# DATA_EXTENSIONS = (".dat",)
# SIDECAR_EXTENSIONS = (".hdr",)
@classmethod
def try_load_file(cls, path: str, **kwargs) -> ["MyFormat_GDALRasterDataImpl"]:
gdal.UseExceptions()
gdal_dataset = gdal.OpenEx(
cls.get_load_filename(path),
nOpenFlags=gdalconst.OF_READONLY | gdalconst.OF_VERBOSE_ERROR,
allowed_drivers=cls.GDAL_DRIVERS,
)
if gdal_dataset is None:
raise ValueError(f"Unable to open file: {path}")
return [cls(gdal_dataset)]
Declaring GDAL_DRIVERS is all that is needed to get a working identify() —
the inherited implementation uses it. Pass the same list to OpenEx so that
identification and opening can never disagree about which driver is in play.
If the usable driver set can only be determined at run time (JPEG2000 support
varies by GDAL build), override get_gdal_drivers() instead of setting
GDAL_DRIVERS.
2. Register it#
Add one FormatSpec to RASTER_FORMATS in format_registry.py:
FormatSpec(
name="MyFormat",
impl=MyFormat_GDALRasterDataImpl,
extensions=frozenset({".myf"}),
priority=45,
),
Priorities are spaced by 5 so a format can be slotted between two existing ones without renumbering. Choose one relative to formats it might be confused with: if your format and an existing one can both open the same file, the higher priority is asked first.
That is the whole registration. There is no second table to update — the spec carries the impl, the ordering, and the dataset-construction strategy together.
Non-GDAL formats#
If the format is not read through GDAL — the pdr planetary-data library, for
instance — subclass RasterDataImpl directly and write identify() by hand.
Keep to the contract: cheap, no handles, no dialogs, NO rather than an
exception on unreadable input.
If opening needs to ask the user something#
Set interactive_step=True on the spec and give it a loader that runs the
dialog, as load_FITS_dataset does. Respect the interactive keyword in
try_load_file: when it is False — project restore, tests, headless use —
choose a sensible default instead of prompting.
Checklist#
GDAL_DRIVERSdeclared (oridentify()written by hand)Same driver list used by
try_load_fileDATA_EXTENSIONSset if the format has sidecars or a greedy driverFormatSpecadded toRASTER_FORMATSwith a consideredpriorityinteractive=Falseproduces a non-prompting defaultFile-open dialog filter added in
show_open_file_dialogif users should be able to force this format (see below)Tests in
src/tests/test_format_registry.py
Overrides#
Detection is only used when nobody has told WISER what the file is. Three
callers can state the format outright, and all three funnel into the same
format= parameter on load_from_file:
Source |
Where |
|---|---|
Programmatic callers |
|
The user’s file-dialog filter |
|
A saved project |
|
An override tries only that format and raises if it fails, rather than quietly falling back to guessing — a wrong override is a mistake worth surfacing, not one worth hiding.
The file-open dialog#
Filters are declared as (text, format_name) pairs. QFileDialog returns which
filter the user selected, and that selection is treated as a statement of what
the file is:
supported_formats = [
(self.tr("All supported files (...)"), None), # None = detect
(self.tr("ENVI raster files (*.img *.hdr *.dat)"), "ENVI"),
...
]
A filter that cannot name a single format — “All supported files”, “Try luck
with GDAL” — maps to None, meaning ordinary detection. When adding a format,
add a filter for it only if a user could plausibly need to force it.
Project files#
dataset_to_pyrep records the format that opened each referenced dataset, and
the load path passes it back as the override. Without this, a project would
re-detect on every open, and a registry change or a new file appearing beside
the data could silently resolve the same path to a different implementation than
the one in use when the project was saved.
Both directions degrade gracefully: a manifest with no format field (written
by an older WISER) falls back to detection, and a format naming something no
longer registered logs a warning and does the same.
Future work#
Splitting the interactive step out of try_load_file. The NetCDF
sub-dataset dialog still runs inside try_load_file. Because only the winning
format is ever opened, it can no longer be shown speculatively or have its
result discarded — but it does mean the GUI step and the I/O are still in one
call, with no seam between them.
Separating them into open() (worker-thread safe) and configure() (main
thread only) is what would allow dataset loading to move off the UI thread:
identify and open on a worker, then marshal only configure back to the main
thread for the formats whose spec sets interactive_step. The registry already
records which formats those are.