Skip to content

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:

  1. Resolution-aware decode — decode at (or near) the output resolution instead of decoding full-res and shrinking.
  2. Memory-mapped source I/Ommap filesystem sources so libvips faults in only the pages it reads.
  3. Header-only dimension probe — read info.json width/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; calls decode.
  • Pipeline.Probe(ctx, source) / image.Probe(source)info.json dimension 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-tagged mmap_unix.go / mmap_windows.go / mmap_other.go): sourceBytes(r) ([]byte, cleanup, error) — mmaps an eligible file, else io.ReadAll.
  • Header dimensions (dimensions.go): dimensions(buf) (w, h, ok)image.DecodeConfig (JPEG/PNG/GIF), TIFF IFD walk, JP2 ihdr box / raw-codestream SIZ (the latter via jp2box.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. cleanReduction requires 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 what vips.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.Probe feeds info.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.json is 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-end Execute equivalence (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 — the Execute / Probe concurrency 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.