tiff 1.0.0
tiff: ^1.0.0 copied to clipboard
Dart library for reading and writing TIFF and BigTIFF: tags, IFDs, strips/tiles, compression, GeoTIFF/EXIF metadata, plus optional image bridge and Flutter viewer/minimap widgets.
1.0.0 #
-
TiffImage.readTileJpeg(tileX, tileY): a JPEG-tiled page's tile as one standalone JPEG stream (shared JPEGTables merged in, an Adobe marker added for RGB photometric), for handing to a platform codec without decoding. -
New optional Flutter viewer,
TiffImageView(package:tiff/tiff_viewer.dart): a pannable, zoomable view of a TIFF/BigTIFF page of any size, modeled onpackage:svs's slide viewer. Only on-screen tiles are decoded, on a pool of background isolates (workerCount), into a byte-bounded LRU cache (cacheBytes); painting happens in screen space and visits only on-screen tiles, so a multi-gigapixel page pans as smoothly as a small one.- Only the pyramid rung matching the zoom is loaded, picked against
physical pixels (
devicePixelRatio) so high-density screens stay sharp, and shrunk in the worker with a box filter to match the screen. Pages that are smaller copies of the base page are detected automatically, including rungs padded out to whole tiles (as Philips scanners write them), which are aligned by their true power-of-two downsample rather than stretched; label/macro images are ignored.pyramidLevelsPathadds a sidecar of extra rungs such asTiffDisplayOptimizer.optimizeLargeSourcePyramidLevelsoutput. While a rung loads, a small preview and every cached rung in view are painted underneath, coarsest first, so zooming sharpens step by step instead of reloading. A pyramid that stops short of 4096 pixels gets a box-filtered overview as its coarsest level. - Zoomed out on a page with too shallow a pyramid, tiles are merged into composites and downscaled in the worker before reaching the GPU, so tile count and texture memory follow the screen, not the page.
- JPEG-tiled rungs (8-bit YCbCr/RGB/grayscale, as whole-slide scanners
write them) are decoded like svs does: workers only read each tile's
JPEG stream, and
dart:ui's platform codec decodes it at up to 1/8 scale — about 20x faster than a full Dart decode — with composites assembled row by row, as many in parallel as there are CPU cores. A pyramid-less multi-gigapixel JPEG page first shows a soft color-map preview built from sampled tiles, then sharpens from the center out. - Strip-organized pages are served as strip-aligned bands (at most 4096 pixels per side, inside GPU texture limits), so no strip is decompressed more often than needed.
brightness/contrast/gammaare baked into decoded tiles; on change, old pixels stay visible until the adjusted ones arrive.- The view is held in a
TransformationController(controller) that works withTiffMinimapand app overlays;initialViewstarts from the whole page or a centeredTiffInitialViewregion;TiffImageViewState.resetViewfits the page again;setUpIsolateenables JPEG decoding in workers;onErrorreports unreadable files. Pinch, drag, and mouse-wheel zoom are built in, and decoded tiles are dropped on OS memory pressure. - The built-in minimap sharpens as tiles load: its thumbnail is redrawn
(at most every 400ms) from the preview plus every tile loaded so far,
keeping detail from tiles already evicted, and its caption shows the
zoom and the pyramid level on screen (e.g.
50%·L1 · 1/2).
- Only the pyramid rung matching the zoom is loaded, picked against
physical pixels (
-
TiffMinimap: a caption with the zoom percentage (showZoomLabel, on by default) and optionallevelLabel; the visible region is tinted with the rest of the page dimmed, and never shrinks below a visible marker when zoomed far in. NewTiffMinimap.formatZoom.
0.5.2 #
BandedDownsampler.downsampleParallelandTiffDisplayOptimizer.optimizeLargeSourcePyramidLevelsParallel'sworkerCountandmaxBandBytesare now optional (int?) instead of required — and the library itself resolves them when left unset, viaTiffAutoDecodeBudget.recommendreading actual idle system memory and CPU count internally, rather than leaving that to be reimplemented by every caller. Pass either explicitly to override just that one; the other still comes from the same recommended budget unless it's overridden too.- New
levelCountparameter onTiffDisplayOptimizer.optimize,optimizeLargeSourcePyramidLevels, andoptimizeLargeSourcePyramidLevelsParallel: build exactly this many pyramid rungs instead of deriving the count fromminPyramidDimension(minPyramidDimensionis still what picks the count whenlevelCountis left unset, the existing default behavior — already a sensible "auto" choice, since it stops at a rung small enough to display smoothly without any further downsampling at read time). ForTiffOptimizationMode.tiledPyramidthis includes the base resolution itself; forTiffOptimizationMode.pyramidLevelsOnlyand the two large-source functions (which never re-encode the base) it counts only the smaller rungs — either way, the same numberTiffOptimizeProgress.levelCountreports. ThrowsArgumentErrorif it isn't> 0, or if it's small enough to leave no smaller rung to build where a mode requires at least one.
0.5.1 #
- Faster
TiffDisplayOptimizer.optimize/optimizeLargeSourcePyramidLevels, aimed squarely at the pyramid-cache-building path (seeTiffOptimizationMode.pyramidLevelsOnly):TileWriter's per-tile scratch buffer was a generic, boxedList<int>(8 bytes/element) filled one sample at a time even for the common 8-bit-RGB case, where 1 byte/element and a bulksetRangecopy is all that's needed;ChunkEncoderandPixelPackercopied and repacked each row through more per-element loops than an already-byte-shaped 8-bit row needs. All three now use typed buffers and bulk copies instead, which is where most of a large pyramid's build time was actually going — decoding and downsampling the source itself is comparatively fast.ImageResampler.downsampleRgba8also gets a dedicated fast path for an exact 2x2 box filter (every pyramid rung after the first is exactly half the size of the one before it) and no longer recomputes each output column's source span on every single output row. TiffOptimizeProgress(returned viaoptimize's/optimizeLargeSourcePyramidLevels'sonProgress) is richer and far more granular — this is a breaking change to its shape. It used to fire once per pyramid rung plus once at the very end, with the (often largest) final tile-and-compress pass reported as a single opaque step. It now carries aTiffOptimizeStage(decoding,downsampling, orencoding), alevel/levelCountlocating which pyramid rung an update belongs to, and astepIndex/stepCountfor progress within that stage — e.g. one update per row-band whileoptimizeLargeSourcePyramidLevelsbanded-decodes its first rung, and one update per tile while every rung is compressed.fractionis now weighted by how many pixels each phase actually touches rather than by rung count, so it tracks real elapsed time much more closely — a caller driving a progress bar or status message off it gets both a smoother bar and a much better idea of what's actually happening (e.g. "decoding source, band 12/40" vs. an unmoving bar during what used to be the single biggest, totally silent step).- New
BandedDownsampler.downsampleParallelandTiffDisplayOptimizer.optimizeLargeSourcePyramidLevelsParallel: the same banded-decode-then-box-filter downsample asBandedDownsampler.downsample/optimizeLargeSourcePyramidLevels, but spreading the decode of a huge source's first pyramid rung across multiple isolates instead of one band at a time on a single core — real multi-core parallelism for what's usually the dominant cost of building a pyramid for a truly large source. Bit-for-bit identical output to the sequential versions given the samemaxBandBytes; only which isolate decodes each band (and how long it takes) differs. Both are purely additive — the existing synchronousdownsample/optimizeLargeSourcePyramidLevelsare unchanged and still the right choice for a source small enough that isolate-spawn overhead wouldn't pay for itself. - Small, safe decode-side speedups in the same spirit as the encode-side
ones above:
ChunkDecoder's per-row unpack now bulk-copies into its output buffer instead of a per-element loop;PixelUnpacker.unpackRowbulk-copies the common 8-bit case instead of oneByteData.getUint8call per byte;RgbTransform/GrayscaleTransformskip their scale-to-0..255 math entirely for 8-bit sources, where it's always the identity; andBandedDownsampler/ImageResampler.downsampleRgba8's box-filter finalization uses integer round-half-up instead of a double divide and.round()per channel per pixel.
0.5.0 #
- New
package:tiff/tiff_minimap.dartentry point, adding aTiffMinimapwidget: a ready-made overview-with-viewport-rectangle for panning/zooming a large TIFF page (tap or drag it to jump the main viewer there). It's decode-agnostic — hand it whatever already-decodedui.Imageoverview bitmap and native page dimensions your own tiled/banded/isolate-based loading strategy already produces, and it handles the layout, the viewport-rect overlay, and tap/drag navigation. This is the only library entry point that importspackage:flutter— every other one (tiff.dart,tiff_io.dart,tiff_image_adapter.dart) stays usable from plain Dart with no Flutter SDK involved. Addingflutteras apackage:tiffdependency (needed so this entry point resolves at all, even for a consumer that never imports it) means a plain-Dart project (no Flutter SDK available) can no longer depend onpackage:tiff— a breaking change for that case.
0.4.0 #
TiffDisplayOptimizer.optimize's newTiffOptimizationMode.pyramidLevelsOnlybuilds the same progressively-halved, tiled rungs astiledPyramid, but without re-encoding the base resolution itself — the output holds only the smaller rungs, a small fraction of whattiledPyramidwrites, since the (by far largest) base-resolution copy is never duplicated. Meant for a caller that wants extra zoom-out levels as a disposable, sidecar cache next to a source that already serves its own base resolution well (e.g. it's already tiled), rather than a full standalone replacement file for it. ThrowsArgumentErrorif the page's longest side is already at or belowminPyramidDimension, since there'd be nothing smaller to build.- New
TiffDisplayOptimizer.optimizeLargeSourcePyramidLevelsbuilds the same output aspyramidLevelsOnly, but for a source too large to safely decode as one whole RGBA8 buffer —optimizealways decodes the whole page first regardless of mode, which for a real multi-gigapixel page can itself be too large to hold in memory before any downsampling even starts. This instead derives the first rung at or belowmaxDirectDecodePixelsvia the newBandedDownsampler, which decodes the source in row bands (bounded bymaxBandBytes) rather than all at once — bit-for-bit equivalent to whatImageResampler.downsampleRgba8would produce given the whole source at once, just computed in bounded memory. Every rung after that first one reuses the same in-memory halvingoptimizeitself uses, since each is smaller than the last and therefore safe by construction. A rung abovemaxDirectDecodePixelsis never produced at all (not even via banding) — this exists to help only the far zoomed-out end a viewer's own bounded-memory region/tile decode of the source doesn't serve well.
0.3.0 #
- Decoded, unpacked samples (
TiffRasterBuffer.samples, and the equivalent intermediate buffer inChunkDecoder.decodeChunk/PixelUnpacker.unpackRow) are now backed byUint8List/Uint16List/Uint32List(picked bybitsPerSamplevia the newallocateSampleBuffer) instead of a genericList<int>, which costs a full 8-byte machine word per element in Dart regardless of how few bits the value actually needs. For the common case — 8-bit samples, e.g. any RGB whole-slide-image scanner file — this is a 4x memory reduction per decoded chunk (7 bytes/pixel instead of 28 for 3-channel RGB, once the final RGBA8 conversion buffer is counted too).TiffChunkPlan's own per-pixel cost estimate is updated to match, so a memory budget computed from it (directly, or via theTiffAutoDecodeBudgetbelow) now reflects what a decode actually allocates — previously the inflated estimate could force chunks well below a page's native tile/strip height purely because of how the intermediate buffer happened to be represented, not how much data it actually held, which is exactly what forces redundant redecoding (see 0.2.0's note onTiffChunkPlan.forBudget) and collapsesTiffChunkPlan.recommendedWorkerCountto a single worker.samples's type staysList<int>(the public field/sampleAtAPI is unchanged) — only what backs it changed. TiffAutoDecodeBudget.recommend(package:tiff/tiff_io.dart) picks a(maxBytesPerChunk, workerCount)pair forTiffParallelDecoder.decodeBandedsized to both a page's metadata and the machine actually running it, so a caller no longer has to hardcode a number that's either too small for a big, idle multi-core machine or too large for a small/mobile one —TiffChunkPlan/TiffChunkPlan.recommendedWorkerCountneed a caller to already know both by design (see 0.2.0's note on why neither reads OS/CPU state itself). Backed by the newSystemMemoryInfo.probe()(a best-effort system memory reading viasysctl/vm_staton macOS,/proc/meminfoon Linux, orwmicon Windows —nullon mobile, or any probe failure, since none of those expose system-wide memory to an app) andPlatform.numberOfProcessors. That memory reading is a one-time snapshot, not a promise it stays free for the whole decode — a real machine keeps allocating elsewhere the entire time a large decode runs — sorecommenddeliberately spends less than the snapshot reports available: a reserve (reserveFraction/reserveBytes, whichever is larger) is set aside for the rest of the machine up front, and what's left is further divided bydoubleBufferSafetyFactorto cover a worker's previous chunk not necessarily being garbage-collected before its next chunk is decoded. Skipping either margin let a generous reading, taken on an already-loaded machine, size a decode large enough to push the whole system into memory pressure. The resulting aggregate budget is divided across up tocpuCountchunks beforeTiffChunkPlan.forBudgetsizes them, rather than handed toforBudgetwhole — otherwise, on a budget too small to fit more than one full tile/strip-aligned chunk at a time (a wide whole-slide-image page easily),recommendedWorkerCountalways came back 1 regardless of how many cores were free, since chunk size (not worker count) was what used up the budget. Dividing first only shrinks chunks below the native tile/strip height (trading some redundant redecoding of the same tile/strip for spreading that work across otherwise-idle cores concurrently, rather than paying for it serially on one core) when the budget genuinely can't fitcpuCountfull-sized chunks; it has no effect once it can, sinceforBudgetalready caps chunk height at the native tile/strip height on its own.
0.2.0 #
TiffChunkPlan.forBudgetcomputes how to decode a page in horizontal chunks that are both bounded by a caller-supplied byte budget and aligned to a whole number of tile/strip rows — decoding one row at a time (the natural choice for a tight memory budget) meansTiffImage.decodeRegionRgba8redecodes the same underlying tile/strip once per row that overlaps it (500x-plus for a real whole-slide-image file's 512px tiles), since it has no cross-call cache;forBudgetavoids that by only shrinking the chunk below one tile/strip if the budget truly forces it.TiffChunkPlan.recommendedWorkerCountderives how many such chunks can run concurrently within an aggregate memory budget and a CPU-count cap. Both are pure — no I/O, no process/OS memory reading — the budget itself is always an explicit parameter the caller computes however it likes.TiffParallelDecoder.decodeBanded(package:tiff/tiff_io.dart) decodes a page in horizontal bands across a pool of isolates, usingTiffChunkPlaninternally — each worker opens its own file handle (a decoded page can't cross an isolate boundary) and delivers bands back to the caller's own isolate as they finish, so the caller can write them out, feed a pyramid builder, or anything else per band without that work needing to be isolate-safe itself. Worker count, per-chunk budget, and band height are all caller-supplied.TiffInitialView.forViewportcomputes a centered region and display zoom sized for a viewer's viewport and a per-device decode budget, so a viewer's first frame decodes a screen-sized crop instead of the whole page.TiffDisplayOptimizer.optimizerewrites a page as tiled (and optionally pyramided) RGB, as a deliberate one-off "prepare this file" step run before a viewer opens it — not during interactive display — so a source TIFF that's strip-organized, single-resolution, or both no longer forces a viewer to decode more than it needs to just to pan or zoom out. ItsonProgresscallback reports aTiffOptimizeProgress(concretecompletedSteps/totalStepscounts, plus the same as afraction) once per pyramid rung and a final time after encoding, so a caller can show real "step 2 of 5" progress instead of an indefinite spinner for a large page.ImageResampler.downsampleRgba8(box filtering) is the pyramid-level resampler behind it, and is usable standalone too.- Fixed: JPEG-in-TIFF (Compression 6/7) decoding now handles a page whose
JPEG scan is split across strip/tile boundaries instead of each
strip/tile being a self-contained JPEG the way TIFF Technical Note 2
describes and this package otherwise assumes — some encoders write pages
this way, and even the first chunk (SOF and all) is just as much a
fragment as the others, since its own entropy data has nowhere near an
EOI. Previously this surfaced as
package:image'sImageException: Only single frame JPEGs supportedwhen decoding the second and later chunks. Each chunk is now checked upfront for its own SOF and trailing EOI — a chunk missing either is treated as part of one continuous scan and the whole page's chunks are reassembled into a single JPEG stream and decoded together instead; a chunk that has both but still fails to decode (corrupt data, a dimension mismatch, ...) surfaces that error directly rather than being incorrectly merged with unrelated, perfectly valid chunks (which used to be possible before this check was added, and would surface as a confusingImageException: Duplicate JPG frame data found.on a page where every chunk actually is independently self-contained, e.g. a real whole-slide- image scanner file). - Fixed: a strip/tile with a byte count of 0 (a "sparse" chunk — some
whole-slide-image scanners never store background tiles at all, and rely
on the reader leaving that area at its default fill value) is now skipped
outright instead of being treated as a JPEG chunk that failed the
self-contained check above. Previously a single sparse chunk anywhere in
a JPEG-compressed page made every chunk get funneled into the
whole-page stitched-JPEG fallback — including chunks that were perfectly
valid, independent JPEGs on their own — which then failed with
ImageException: Duplicate JPG frame data found.once more than one real frame ended up concatenated together. This is the shape a real Philips/Aperio-style whole-slide-image TIFF commonly takes.
0.1.0 #
Initial release.
- Read and write Classic TIFF (32-bit offsets) and BigTIFF (64-bit offsets), with automatic promotion to BigTIFF when a written image's pixel data would exceed the 4 GiB Classic offset limit.
- Full IFD/tag parsing, covering every baseline TIFF 6.0 tag type plus BigTIFF's LONG8/SLONG8/IFD8.
- Strip- and tile-organized pixel data, both reading and writing (edge tiles are cropped on read and zero-padded on write automatically).
- Compression: None, PackBits, LZW, and Deflate/ZIP, read and write; CCITT Group 3/4 fax and JPEG, read only (see the README for details on both). LZW decoding also transparently handles "old-style" (pre-TIFF6, LSB-first) data from legacy encoders, auto-detected the same way libtiff does. JPEG-in-TIFF decoding correctly treats the JPEG codec's own YCbCr->RGB conversion as final, rather than re-applying the PhotometricInterpretation tag's YCbCr transform on top of already-RGB samples. CCITT Group 4/2D decoding correctly preserves zero-length runs (some encoders pair a zero-length white run with a zero-length black run), which previously desynced the reference-line tracking used by later rows.
- Horizontal differencing predictor, read and write.
- RGBA8 color conversion covering WhiteIsZero/BlackIsZero, RGB(+alpha), Palette/ColorMap, CMYK, and non-subsampled YCbCr.
- Brightness/contrast/gamma adjustment for decoded RGBA8 pixel data
(
ImageAdjustments). - File-backed decoding (
package:tiff/tiff_io.dart) that streams strips/tiles from disk instead of loading a whole file into memory, plus region-of-interest decoding (decodeRegion/decodeRegionRgba8) that skips chunks outside a requested crop — the way to preview or tile through a multi-gigabyte page without materializing the whole image in memory. - Multi-page reading and writing via IFD chains.
- GeoTIFF, EXIF, and GPS metadata parsing.
- Optional
package:imagebridge (package:tiff/tiff_image_adapter.dart) for converting to/fromimage.Imageand for decoding JPEG-compressed pages.