Table of Contents

TIFF

Egami reads and writes TIFF and BigTIFF, written from the TIFF 6.0 specification, Adobe's technical notes and supplements and the BigTIFF design note, starting from the reader in Lars's ClassicMac. TiffFormat.Info is registered in EgamiContext.Default, so Raster.Load recognizes TIFF from its first bytes and Save("x.tif") picks the writer. Every page of libtiff's own sample images and of Egami's corpus decodes to the samples libtiff gives.

Reading

// Load reads the first full-resolution page in the pixel type that fits it: Rgb16 for 16-bit color, Cmyk8 for CMYK.
using Raster page = Raster.Load("scan.tif");
Console.WriteLine($"{page.Width} × {page.Height}, {page.Layout.ChannelOrder} {page.Layout.ChannelType}");

// Probe says how many pages LoadAll would return, without decoding any.
Console.WriteLine($"{Raster.Probe("scan.tif").ImageCount} pages");

Load reads one page: the first full-resolution page, or the one TiffReaderOptions.Page names. Reduced-resolution pages, SubIFD previews and transparency masks are never counted as pages.

// Pick another page by its index among the full-resolution pages.
using Raster<Rgba8> third = Raster.Load<Rgba8>("scan.tif", options: new TiffReaderOptions { Page = 2 });

LoadAll returns every full-resolution page, each at its own size and pixel type; with IncludeReducedPages the reduced-resolution pages and SubIFDs come too, each after the page it belongs to.

// Every page at its own size and pixel type. The list owns the rasters.
using RasterList pages = Raster.LoadAll("scan.tif");
foreach (Raster page in pages)
{
    Console.WriteLine($"{page.Width} × {page.Height}");
}

// Keep one page after the list is gone.
using Raster first = pages.Detach(0);
Compression Read
None, LZW (and TIFF 5.0's bit-reversed LZW), Deflate, PackBits, ZSTD, LZMA Yes, with the horizontal and floating-point predictors
CCITT modified Huffman, Group 3 (1-D and 2-D), Group 4 1-bit pages
JPEG, old-style JPEG 8-bit pages, through Egami's JPEG decoder; old-style JPEG is best effort
Lossless JPEG (code 7 with SOF3 or SOF11 units) Gray, RGB and CMYK pages of 2 to 16 bits, each strip or tile decoded to its samples as they are, including DNG's layout of a row as two components half as wide
ThunderScan 4-bit gray
SGILog, SGILog24 LogL and LogLuv pages, as linear GrayF32 or RgbF32
WebP Lossless and lossy, through Egami's WebP decoder
JPEG XL (50002, and DNG 1.7's 52546) Chunky gray, RGB and YCbCr pages of 8 or 16-bit or float samples, lossless and lossy, through Egami's JPEG XL decoder; each strip or tile as its codestream decodes alone. DNG's camera RAW pages (CFA, LinearRaw) are not read
LERC Not yet: NotSupportedException names the code. Added on real demand

Each page loads in the pixel type that fits it. Depths Egami has no type for widen to the next: 2 and 4 bits to 8, 10 to 14 bits to 16, scaled so the maximum stays the maximum; 24 and 32-bit integers and 16 to 64-bit floats load as float types.

Photometric 1 to 8 bits 9 to 16 bits Wider or float
Gray (either polarity) Gray8, GrayAlpha8 Gray16, GrayAlpha16 GrayF32; RgbaF32 with alpha
RGB Rgb8, Rgba8 Rgb16, Rgba16 RgbF32, RgbaF32; RgbaF16 for 16-bit float
Palette Rgb8, or Rgb16 when the color map needs 16 bits
CMYK Cmyk8; Rgba8 with alpha Cmyk16; Rgba16 with alpha RgbaF32
YCbCr, at any subsampling Rgb8 Rgb16
CIE Lab, ICC Lab, ITU Lab Rgb8 Rgb16 RgbF32

Associated (premultiplied) alpha loads as straight alpha, and TiffDetails.ExtraSamples says which kind the file had; extra samples that are not alpha are dropped with a warning. Signed samples are offset so 0 becomes the middle value. Lab is relative to the page's white point, D50 when the file gives none (as Adobe's note and libtiff have it), and converted to sRGB with Bradford adaptation, as ICC does. Float pages are linear light. CMYK pages keep their ICC profile, so ConvertColorTo converts them through it.

Writing

// Each pixel type is written at its own depth: 16-bit color stays 16-bit. Deflate with the predictor is the default.
photo.Save("duck.tif");

// Tiles, LZW, BigTIFF, or the smallest exact form the pixels allow.
photo.Save("tiled.tif", new TiffWriterOptions { Compression = TiffCompression.Lzw, TileSize = 256, Layout = TiffLayout.Auto });
photo.Save("small.tif", new TiffWriterOptions { Reduce = true });

// JPEG inside TIFF: RGB is written as YCbCr 4:2:0 with the tables shared by every strip.
photo.Save("photo.tif", new TiffWriterOptions { Compression = TiffCompression.Jpeg, Quality = 90 });
Option Default Does
Compression the TIFF source's, else Deflate None, Lzw, Deflate, Zstd, Lzma, PackBits, CcittGroup3 and CcittGroup4 (1-bit), Jpeg (8-bit gray, RGB and CMYK; lossless JPEG at Quality 100, 8 and 16-bit), Webp (lossless at Quality 100, else lossy)
CompressionLevel 5 Deflate level, 0 to 9
ZstdLevel 9 Zstandard level, 1 to 22, as libtiff's ZSTD_LEVEL
LzmaPreset 6 LZMA preset, 0 to 9, with xz's meanings, as libtiff's LZMA_PRESET
Predictor the source's, else Horizontal for integers and FloatingPoint for floats For LZW, Deflate, Zstd and LZMA; None turns it off
Quality 75 JPEG and lossy WebP quality; 100 writes lossless JPEG or lossless WebP
TileSize the source's, else strips of about 64 KB A multiple of 16 writes square tiles; 0 forces strips
Layout Classic Big writes BigTIFF; Auto switches when the pixels pass 4 GB; Classic throws before writing when they would
Reduce false Writes the smallest exact form: 1 bit, gray, a palette of up to 256 colors, or 8 bits from 16
AssociatedAlpha false Writes straight alpha premultiplied (ExtraSamples 1)
ParallelCompression false Compresses strips and tiles on several threads; the file is the same

Every pixel type is written at its own depth: gray and RGB at 8 and 16 bits with or without alpha, GrayF32, RgbF32, RgbaF32 and RgbaF16 as floats, Cmyk8 and Cmyk16 as separated, premultiplied types as associated alpha; packed types widen to 8 or 16 bits. The writer is deterministic and little-endian, except that re-saving a big-endian TIFF keeps its byte order, as it keeps the source's compression, predictor and tiling. A JPEG, WebP or CCITT source keeps its compression only while no operation has changed the pixels, so a lossy file is never compressed twice by accident; a lossless JPEG source keeps it always. Raster.SaveAll writes several rasters as pages.

// A page where every pixel is black or white saves as a 1-bit CCITT Group 4 fax.
page.Save("fax.tif", new TiffWriterOptions { Compression = TiffCompression.CcittGroup4 });

Group 4 is the usual choice for scanned text; it takes 1-bit pages only, so threshold first or set Reduce.

// Zstandard with the predictor: lossless, read by libtiff and GDAL. Level 1 writes fastest, 19 writes smallest.
photo.Save("zstd.tif", new TiffWriterOptions { Compression = TiffCompression.Zstd });
photo.Save("zstd-small.tif", new TiffWriterOptions { Compression = TiffCompression.Zstd, ZstdLevel = 19 });

ZSTD (code 50000, libtiff's and GDAL's) is lossless like Deflate. In Egami's benchmarks a TIFF loads about as fast as with Deflate (the predictor and the pixel conversion take most of the time); level 3 writes about as fast as Deflate's default and level 9 (the default) a little smaller than it, while on flat images such as screenshots the files are a third smaller. Higher levels write smaller and slower. Egami's Zstandard codec is its own managed code from RFC 8878: one frame a strip or tile, with the content size and no checksum, as libtiff writes. Up to level 15 it follows libzstd's settings and stays within 5 % of its size; from 16, where libzstd switches to optimal parsing, Egami's files are a few percent larger. libtiff and GDAL read Egami's ZSTD files and Egami reads theirs.

// Lossless JPEG in each strip or tile: quality 100 asks for it, as it asks for WebP's lossless form. 16 bits stay 16.
photo.Save("lossless-jpeg.tif", new TiffWriterOptions { Compression = TiffCompression.Jpeg, Quality = 100 });

Lossless JPEG (ITU-T T.81 annex H) keeps 8 and 16-bit gray, RGB and CMYK as they are: RGB is not turned into YCbCr, and inks are stored as they are, not inverted. Each strip or tile is a whole SOF3 stream with its own Huffman tables and the predictor that codes it smallest, so ParallelCompression applies and there are no JPEGTables. It is the coding DNG uses for camera RAW data; libtiff's JPEG codec does not read it, so choose Deflate, ZSTD or LZMA for files other TIFF readers must open. Float pages are written at 16 bits and alpha is refused, as with baseline JPEG.

// LZMA with the predictor: the smallest lossless TIFF, slow to write; preset 6 is libtiff's default, 9 the smallest.
photo.Save("archive.tif", new TiffWriterOptions { Compression = TiffCompression.Lzma });
photo.Save("archive-fast.tif", new TiffWriterOptions { Compression = TiffCompression.Lzma, LzmaPreset = 1 });

LZMA (code 34925) writes the smallest lossless files, at a cost: on Egami's 2048 by 1536 photo benchmark, preset 6 is 18 % smaller than Deflate and 16 % smaller than ZSTD 19, but loads about eight times slower and writes about twenty times slower than Deflate. It suits archives that are written once and read rarely. Each strip or tile is an xz stream, as libtiff writes it: the delta filter over whole samples, then LZMA2, with no check; Egami's codec is its own managed code from the xz format specification. Presets 0 to 9 follow xz, with the dictionary cut to the strip, so a 64 KB strip never asks for a 64 MB dictionary; Egami's files are within 4 % of liblzma's size at every preset. libtiff and GDAL read Egami's LZMA files and Egami reads theirs. Some libtiff builds (Alpine's, for one) leave LZMA out and cannot open them.

Files are checked against libtiff (no tiffinfo warnings, identical samples), ImageMagick and Pillow, and open in Windows' WIC except for BigTIFF and tiled gray-with-alpha or 16-bit CMYK, which WIC does not read from any writer.

Format details and metadata

The tags EXIF defines (ImageDescription, Make, Model, Orientation, Software, DateTime, Artist, Copyright and the rest) and the EXIF, GPS and Interoperability sub-IFDs become RasterMetadata.Exif, so converting a TIFF to JPEG keeps them. ICC profiles, XMP, IPTC (or the IPTC in Photoshop's resources) and the resolution are read too. TiffDetails describes each page: its TiffCompression, TiffPredictor, TiffPhotometric, TiffSampleFormat, TiffByteOrder and TiffLayout, strips or tiles, and TiffExtraSample kinds. Tags Egami does not map, such as GeoTIFF's and private ones, are kept as TiffTag values in OtherTags and written back by the TIFF writer, except tags that hold offsets to structures Egami cannot move.

using Raster page = Raster.Load("scan.tif");
TiffDetails tiff = (TiffDetails)page.Metadata.FormatDetails!;
Console.WriteLine($"page {tiff.PageIndex}: {tiff.Photometric}, {tiff.BitsPerSample}-bit, {tiff.Compression}, {tiff.ByteOrder}, {tiff.Layout}");
Console.WriteLine(tiff.TileWidth is int tile ? $"tiles of {tile}" : $"{tiff.RowsPerStrip} rows a strip");

// Tags Egami does not map are kept as they were read.
foreach (TiffTag tag in tiff.OtherTags)
{
    Console.WriteLine($"tag {tag.Id}: {tag.Count} × {tag.Type}");
}

Memory and damaged files

Only the strips or tiles a read touches are decoded, a band at a time, and rows are converted from the band as they are asked for, so a region or a reduced decode of a large tiled file reads only the tiles it needs. Uncompressed strips in memory are read where they lie. A stream that cannot seek is read whole first, since a TIFF's index often comes last; the bytes count against SafetyLimits.MaxTotalBytes. Every IFD offset is checked against the file and against loops, and a strip or tile that decodes short is padded with zero samples and noted in one warning per page (RecoveryMode.Strict throws instead).

Recipes

Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.

Split a multi-page scan into PNGs

using RasterList pages = Raster.LoadAll("scan.tif");
for (int i = 0; i < pages.Count; i++)
{
    pages[i].Save($"page-{i + 1}.png");
}

Each page keeps its own size and pixel type; see Reading.

Combine pages into one TIFF

using RasterList pages = Raster.LoadAll("scan.tif");

// Several rasters make a multi-page TIFF; one raster saves to any format.
Raster.SaveAll(pages, "copy.tif");
Raster.SaveAll([pages[1]], "second.png");