tiff
A Dart library for reading and writing TIFF and BigTIFF image files: tags and IFDs, strip/tile pixel data, common compression schemes, color conversion, and GeoTIFF/EXIF metadata — for files ranging from a few kilobytes to multi-gigabyte BigTIFF rasters.
Features
- Classic TIFF (32-bit offsets) and BigTIFF (64-bit offsets), with automatic promotion to BigTIFF on write when the pixel data would exceed Classic's 4 GiB offset limit
- Full IFD/tag parsing, including every baseline TIFF 6.0 tag type plus BigTIFF's LONG8/SLONG8/IFD8
- Strip and tile pixel data, 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 Limitations)
- Horizontal differencing predictor, read and write
- RGBA8 color conversion: WhiteIsZero/BlackIsZero, RGB(+alpha), Palette/ColorMap, CMYK, and non-subsampled YCbCr
- Brightness/contrast/gamma adjustment on decoded RGBA8 pixel data
(
ImageAdjustments) - File-backed decoding that streams strips/tiles from disk instead of
loading a whole file into memory (
package:tiff/tiff_io.dart), plus region-of-interest decoding that skips chunks outside a requested crop TiffInitialView.forViewportpicks a centered region and zoom level sized for a viewer's screen and decode budget, so a first frame never requires decoding a whole multi-gigapixel pageTiffDisplayOptimizer.optimizerewrites a page as tiled (and optionally pyramided) RGB ahead of time, so a strip-organized and/or single-resolution source no longer forces a viewer to decode more than it needs toTiffChunkPlanplans memory-bounded, tile-aligned decode chunks for a page too large to decode whole, andTiffParallelDecoder.decodeBanded(package:tiff/tiff_io.dart) uses it to decode a page in bands across a pool of isolates — every budget/worker-count knob is a caller-supplied parameter, not something read from the OS- Multi-page reading and writing via IFD chains
- GeoTIFF, EXIF, and GPS metadata parsing
- An optional
package:imagebridge (package:tiff/tiff_image_adapter.dart) for converting to/fromimage.Imageand for decoding JPEG-compressed pages - An optional Flutter minimap widget (
package:tiff/tiff_minimap.dart,TiffMinimap) — a decode-agnostic overview-with-viewport-rectangle for panning/zooming a large page - An optional Flutter viewer (
package:tiff/tiff_viewer.dart,TiffImageView) that pans and zooms a page of any size smoothly: only on-screen tiles are decoded, on background isolates, at the pyramid rung matching the zoom
Limitations
- CCITT Group 3/4 fax (Compression 2/3/4) is decode-only. Virtually no modern software writes new Group 3/4 data, so encoding it isn't implemented.
- JPEG-in-TIFF (Compression 6/7) decoding requires the optional adapter
(
package:tiff/tiff_image_adapter.dart) — TIFF's baseline spec has no bundled JPEG codec, so this package borrowspackage:image's. Writing JPEG-compressed TIFF is not supported. Normally each strip/tile is its own self-contained JPEG (TIFF Technical Note 2) and only the strips/tiles a region actually touches get decoded; a page whose encoder instead split one continuous JPEG scan across chunk boundaries still decodes correctly, but anydecodeRegioncall against it costs as much as decoding the whole page — there's no way to decode part of one continuous scan. - Only chunky (interleaved)
PlanarConfiguration, and a singleBitsPerSamplevalue uniform across channels, are supported.
Getting started
dependencies:
tiff: ^1.0.0
Usage
import 'dart:io';
import 'package:tiff/tiff.dart';
void main() {
final bytes = File('image.tif').readAsBytesSync();
final document = TiffDecoder.decode(bytes);
for (final page in document.images) {
print('${page.metadata.width}x${page.metadata.height}, '
'${page.metadata.samplesPerPixel} samples/pixel');
final raster = page.decode(); // raw samples, no color interpretation
print(raster.sampleAt(0, 0, 0));
final rgba = page.decodeRgba8(); // interleaved 8-bit RGBA
}
}
For large (BigTIFF) files, decode directly from disk instead of loading the whole file into memory, and optionally decode just a crop:
import 'dart:io';
import 'package:tiff/tiff.dart';
import 'package:tiff/tiff_io.dart'; // dart:io-backed, not available on web
void main() {
final document = decodeTiffFile(File('huge.tif'));
try {
final page = document.images.first;
final crop = page.decodeRegion(TiffRegion(x: 0, y: 0, width: 512, height: 512));
print(crop.sampleAt(0, 0, 0));
} finally {
document.close(); // releases the file handle
}
}
For a viewer's first frame, decode a region sized for the screen instead of
the whole page — TiffInitialView.forViewport picks a region centered on
the page plus the zoom to display it at, capped by a decode budget you tune
per device:
final page = document.images.first;
final initialView = TiffInitialView.forViewport(
page.metadata,
viewportWidth: 1080,
viewportHeight: 2280,
devicePixelRatio: 3.0, // from the platform, e.g. MediaQuery in Flutter
maxDecodedPixels: 4000000, // lower this on memory-constrained devices
);
final preview = page.decodeRegionRgba8(initialView.region);
// Display `preview` scaled by initialView.zoom, then let the user pan/zoom
// further, decoding new regions on demand.
Preparing a file ahead of time so a later viewer never has to decode more
than it needs to — this is a deliberate one-off rewrite, not something to
run during interactive display, and it decodes the whole page into memory
to do it (see the TiffDisplayOptimizer dartdoc for the memory caveat on a
very large page):
final page = document.images.first;
final optimized = TiffDisplayOptimizer.optimize(
page,
mode: TiffOptimizationMode.tiledPyramid, // or .tiledOnly for just re-tiling
tileSize: 512,
minPyramidDimension: 512,
);
File('optimized.tif').writeAsBytesSync(optimized);
// optimized.tif is tiled RGB with progressively halved rungs appended as
// extra pages — open it the normal way and a viewer can decode by tile at
// whichever rung matches the current zoom, instead of the whole page.
Decoding a huge page in bands, bounded by a memory budget you choose and spread across a pool of isolates — the way to build something like a progressive viewer's own cache without decoding the whole page into memory or paying for redundant tile/strip redecoding at a tight budget:
import 'package:tiff/tiff.dart';
import 'package:tiff/tiff_io.dart'; // needed for TiffParallelDecoder
await TiffParallelDecoder.decodeBanded(
filePath: 'huge.tif',
pageIndex: 0,
bandHeight: 256, // height of each delivered band
maxBytesPerChunk: 200 * 1024 * 1024, // your own memory budget, however computed
workerCount: 4, // your own choice — e.g. Platform.numberOfProcessors - 1
onBand: (band) {
// Called back on your isolate — write it out, feed a pyramid builder,
// whatever you need. Bands from different workers can interleave.
print('band at y=${band.y}, ${band.height} rows, ${band.rgba.length} bytes');
},
);
Writing a TIFF:
import 'dart:io';
import 'package:tiff/tiff.dart';
void main() {
final spec = TiffImageSpec(
width: 256,
height: 256,
samplesPerPixel: 3,
bitsPerSample: 8,
photometric: TiffPhotometric.rgb,
samples: myRgbSamples, // length == width * height * samplesPerPixel
compression: 5, // LZW; see TiffTagId-style compression codes in the docs
predictor: 2, // horizontal differencing (pairs well with LZW/Deflate)
);
final bytes = TiffEncoder.encode([spec]); // pass multiple specs for a multi-page file
File('output.tif').writeAsBytesSync(bytes);
}
Reading GeoTIFF/EXIF metadata (present on the decoded page's metadata, no
extra setup needed):
final metadata = page.metadata;
final geo = metadata.geoTiff; // null if the file has no GeoTIFF tags
if (geo != null) {
print(geo.modelPixelScale); // [scaleX, scaleY, scaleZ]
print(geo.geoKeys[GeoTiffKeyId.gtModelType]);
}
print(metadata.exifTags?[ExifTagId.dateTimeOriginal]?.asString());
Adjusting brightness/contrast/gamma on decoded pixels:
final rgba = page.decodeRgba8();
final adjusted = ImageAdjustments.apply(
rgba,
brightness: 15, // additive, sample units; negative darkens
contrast: 1.2, // 1.0 = no change, around mid-gray
gamma: 1.1, // 1.0 = no change; >1 brightens midtones
);
Optional: package:image bridge
package:tiff/tiff.dart never depends on package:image — that stays a
lightweight import for anyone who only needs raw TIFF pixels. Import
package:tiff/tiff_image_adapter.dart as well to convert to/from
image.Image, or to decode JPEG-compressed TIFF pages (Compression 6/7),
which TIFF's baseline spec has no bundled codec for:
import 'package:tiff/tiff.dart';
import 'package:tiff/tiff_image_adapter.dart';
void main() {
TiffImageAdapter.enableJpegSupport(); // needed once, only for Compression 6/7 pages
final page = TiffDecoder.decode(bytes).images.first;
final image = TiffImageAdapter.toImage(page); // an image.Image, ready for
// cropping/resizing/PNG export/etc. via package:image
final spec = TiffImageAdapter.toTiffImageSpec(image); // back to TIFF
File('roundtrip.tif').writeAsBytesSync(TiffEncoder.encode([spec]));
}
Note: package:image is still a normal (if rarely large) entry in this
package's pubspec.yaml — Dart has no per-file-optional dependency
mechanism, so dart pub get fetches it for every consumer regardless of
whether tiff_image_adapter.dart is ever imported. "Optional" here means
your own code never has to touch package:image's API (or pay for importing
it) unless you choose to.
Optional: Flutter minimap widget
Every other entry point in this package works from plain Dart — no Flutter
SDK needed. Import package:tiff/tiff_minimap.dart as well, from a Flutter
app, for TiffMinimap: a ready-made overview-with-viewport-rectangle for
panning/zooming a large page, driven by the same TransformationController
an InteractiveViewer uses. It's decode-agnostic — hand it whatever
already-decoded ui.Image overview bitmap and native page dimensions your
own tiled/banded/isolate-based loading strategy already produces:
import 'package:flutter/material.dart';
import 'package:tiff/tiff_minimap.dart';
Widget buildMinimap(ui.Image? overview, int baseWidth, int baseHeight, TransformationController controller, Size viewportSize) {
return TiffMinimap(
overview: overview, // null shows a small placeholder spinner
baseWidth: baseWidth,
baseHeight: baseHeight,
controller: controller,
viewportSize: viewportSize,
// Optional: the caption shows the zoom, plus this on its right.
levelLabel: (scale) => scale < 0.5 ? 'overview' : 'full',
);
}
The visible region is tinted, with the rest of the page dimmed; far zoomed
in, it's still drawn as a small marker. showZoomLabel: false hides the
caption.
Note: unlike package:image, adding package:tiff/tiff_minimap.dart's
flutter dependency to this package's own pubspec.yaml means a plain-Dart
project (no Flutter SDK) can no longer depend on package:tiff at all, even
if it never imports this entry point — Flutter SDK dependencies can't be
made conditional the way a large but ordinary package like package:image
can.
Optional: Flutter viewer for large pages
Import package:tiff/tiff_viewer.dart for TiffImageView, a pannable,
zoomable view of a TIFF/BigTIFF page of any size — the same approach as
package:svs's slide viewer:
- Only the tiles on screen are decoded, on a pool of background isolates, and kept in a byte-bounded LRU cache.
- Only the pyramid rung matching the current zoom and the screen's pixel
density is loaded, box-filtered in the worker to match the screen (pages
that are smaller copies of the base page are detected automatically —
including rungs padded to whole tiles — plus an optional sidecar from
TiffDisplayOptimizer.optimizeLargeSourcePyramidLevels). Already-cached rungs and a small preview are painted underneath while it loads, so zooming sharpens a preview instead of flashing blank. - Zoomed out on a page with too shallow a pyramid, tiles are merged into composites downscaled in the worker, so memory follows the screen size.
- JPEG-tiled pages (whole-slide scans) are decoded by the platform codec at
up to 1/8 scale (via
TiffImage.readTileJpeg), so even a pyramid-less multi-gigapixel scan fills a zoomed-out view quickly. - Strip-organized pages are served as strip-aligned bands.
import 'package:flutter/material.dart';
import 'package:tiff/tiff_image_adapter.dart';
import 'package:tiff/tiff_viewer.dart';
Widget buildViewer(String path, TransformationController controller) {
return SizedBox(
height: 480,
child: TiffImageView(
filePath: path,
controller: controller, // optional: read/set the view for overlays
setUpIsolate: TiffImageAdapter.enableJpegSupport, // JPEG-compressed files
pyramidLevelsPath: null, // optional sidecar with extra, smaller rungs
brightness: 0, // contrast/gamma too; re-rendered when they change
onError: (error) => debugPrint('$error'),
),
);
}
TiffImageView reads files with dart:io, so it isn't available on the web.
Example app
See tiff_tester for a full Flutter app example built on this package.
Support
If this package is useful to you, consider supporting its development on Ko-fi:
License
Apache License 2.0. See LICENSE.
Libraries
- tiff
- Read and write TIFF and BigTIFF image files.
- tiff_image_adapter
- Optional bridge to
package:image. - tiff_io
- Optional file-based decoding entry point.
- tiff_minimap
- Optional Flutter widget entry point.
- tiff_viewer
- Optional Flutter viewer for TIFF/BigTIFF pages of any size.