Image decode + source I/O¶
The contract for how internal/image reads a source and decodes only the
resolution and bytes a request needs. Three transparent, output-equivalent
optimisations sit on the decode/probe path, each with a guaranteed fallback
to the plain full-decode behaviour:
- Resolution-aware decode — decode at (or near) the output resolution instead of decoding full-res and shrinking.
- Memory-mapped source I/O —
mmapfilesystem sources so libvips faults in only the pages it reads. - Header-only dimension probe — read
info.jsonwidth/height from the file header in pure Go, without constructing a libvips image.
Operator-facing performance notes live in the package's behaviour; this is the durable contract a re-implementation must satisfy.
What it does¶
A render or info request carries its region and size up front, and only
needs a fraction of a large master. Rather than io.ReadAll the whole
compressed source and decode every pixel, the pipeline:
- picks a decode-time reduction the source codec supports (JPEG DCT shrink-on-load, JP2/HTJ2K and pyramidal-TIFF level reduction) and remaps the crop onto that reduced grid;
- memory-maps the source file so only the touched pages are resident;
- for
info.json, parses dimensions from the header (no decode).
All three are invisible to callers: identical bytes are served, the same
cache keys are used, and any case the fast path doesn't cleanly handle
takes the previous full-decode / io.ReadAll / libvips-probe path.
Surface¶
Internal to internal/image — no public API, config field, or env var is
added by these optimisations. Entry points and helpers:
Pipeline.Execute(ctx, req, source)— render path; callsdecode.Pipeline.Probe(ctx, source)/image.Probe(source)—info.jsondimension path.- Resolution-aware decode (
reduce.go):planThumbnail/loadThumbnail(full-region downscale),loadReducedRegion/loadReduced+remapRegion/remapSize(arbitrary-region downscale),cleanReduction(the proportional-reduction guard). - Source I/O (
sourcebytes.go+ build-taggedmmap_unix.go/mmap_windows.go/mmap_other.go):sourceBytes(r) ([]byte, cleanup, error)— mmaps an eligible file, elseio.ReadAll. - Header dimensions (
dimensions.go):dimensions(buf) (w, h, ok)—image.DecodeConfig(JPEG/PNG/GIF), TIFF IFD walk, JP2ihdrbox / raw-codestreamSIZ(the latter viajp2box.go).
Contracts¶
- Output equivalence. Rendered output is byte-for-byte identical to the full-decode path, within resampling tolerance for shrink-on-load (both Lanczos). Render and info cache keys are unchanged — a cached result must not depend on which path produced it. These are optimisations, not behaviour changes.
- Conservative reduction. Never decode below the resolution the output
needs (
source / R ≥ target); the reduction factor is the largest power-of-two ≤ a cap.cleanReductionrequires the loader to return a clean proportional power-of-two reduction, else the request falls back to the full decode. - Header parse equals libvips raw. The pure-Go header parser
(
dimensions()) must return exactly whatvips.NewImageFromBuffer(...).Width()/Height()would on the raw header — no EXIF autorotate applied. Guarded by a corpus equivalence test; any unrecognised format or unparseable header falls back to the libvips probe. This is an internal helper invariant and is independent of the metadata policy below. - Probe reports the dimensions the render will produce.
Pipeline.Probefeedsinfo.json, so it must agree with what the render pipeline outputs for the same identifier: image.metadata: preserve— no autorotate; Probe returns raw header dimensions via the fast path (equal to libvips raw, as above).image.metadata: strip(default) — the render pipeline bakes a non-normal EXIF orientation into the pixels (autorotate), so Probe returns oriented dimensions (width/height swapped for the 90°/270° orientations 5–8). In this mode Probe reads the orientation via libvips (img.GetOrientation()) rather than the orientation-blind fast path, so it resolves orientation the same way the render side does.info.jsonis cached, so the cold-path cost is amortized.
Probe and the render must never disagree on dimensions for a given
identifier, or IIIF region/tile math breaks. See
docs/briefs/METADATA-PRIVACY.md.
- mmap eligibility + lifetime. mmap applies only to a regular on-disk
file (the filesystem source's *os.File) whose bytes prepareBuffer
won't rewrite (not a raw J2K codestream), on a platform with a real mmap
(unix, windows). The mapping stays valid until after encode — the
image may re-read the source during downstream ops — so cleanup is
deferred to run after the image is closed.
- Fallback is always safe, never the only path. Unreducible format /
full-res / upscale → full decode. Non-file source (HTTP/S3 stream, the
Cached wrapper's bytes.Reader), mmap failure, or non-Unix/Windows
platform → io.ReadAll. Unknown format / bad header → libvips probe.
- Decode-only. No encoder, output format, or format-support change; the
htj2k decode contract is intact.
- Bounded concurrency. Execute and Probe are each gated by a
separate semaphore sized to image.max_concurrent, so an info.json
burst can't starve renders (and vice-versa) or hold an unbounded number of
in-flight source buffers.
Test coverage¶
In internal/image/:
reduce_test.go— pixel-equivalence of shrink-on-load and reduced-region decode against a full-decode reference; reduction math.sourcebytes_test.go— file-vs-reader byte-equivalence, idempotent mmap cleanup, raw-J2K fallback, and end-to-endExecuteequivalence (mmap vs ReadAll source).dimensions_test.go— header-parsed dims match libvips across jpeg/png/tiff/jp2/jph; garbage input falls back to the libvips probe.pipeline_test.go— theExecute/Probeconcurrency gates.
Out of scope¶
- Pre-generating pyramids for flat sources (a flat JPEG/PNG with no pyramid). iiiris reads the resolutions a source already provides; JPEG shrink-on-load already helps.
- Region/tile-level spatial decode. Not needed — libvips already reads only the tiles covering a crop from a tiled source on demand.
- mmap / partial I/O for non-filesystem sources (HTTP, S3) and for the origin cache. Network sources are latency-bound and keep buffering; a filesystem origin-cache mmap is a possible later win.
- Encoder choices (baseline vs progressive JPEG, quality). Those are
config and live in
docs/configuration.md.