JetsonPDF.Tiff 1.1.0

dotnet add package JetsonPDF.Tiff --version 1.1.0
                    
NuGet\Install-Package JetsonPDF.Tiff -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JetsonPDF.Tiff" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JetsonPDF.Tiff" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="JetsonPDF.Tiff" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JetsonPDF.Tiff --version 1.1.0
                    
#r "nuget: JetsonPDF.Tiff, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JetsonPDF.Tiff@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JetsonPDF.Tiff&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=JetsonPDF.Tiff&version=1.1.0
                    
Install as a Cake Tool

JetsonPDF.Tiff

Pure-managed TIFF reader and writer for .NET. No GDI+, no System.Drawing, no native dependencies — just decompressed RGBA8888 pixels and a small writer that emits baseline TIFF strips. Built originally so JetsonPDF.OpenSilver could display .tif files inside a WebAssembly browser app (where neither GDI+ nor <img src="*.tif"> works), but the surface is general-purpose — any .NET app that needs to read or write TIFF frames without a native codec can use it directly.

using JetsonPDF.Tiff;

// Read: decode every page, hand the first one to anything that takes PNG.
TiffImage image = TiffImage.Decode(File.ReadAllBytes("scan.tif"));
File.WriteAllBytes("page0.png", image.Frames[0].EncodePng());

// Write: encode RGBA buffers back out as multi-page TIFF.
byte[] tiffBytes = TiffWriter.Encode(
    image.Frames.Select(TiffEncodeFrame.From),
    new TiffWriteOptions { Compression = TiffCompression.Deflate });
File.WriteAllBytes("round-tripped.tif", tiffBytes);

Contents


Overview

JetsonPDF.Tiff is a baseline TIFF (Rev 6) implementation written entirely in managed C#. The reader produces row-major RGBA8888 frames; the writer accepts the same RGBA layout and emits a strip-based, little-endian (II) TIFF. Decoding is eager — TiffImage.Decode reads the whole file and decodes every IFD up front, because TIFF is offset-driven random access and not stream- friendly by nature.

The library reuses the heavy-lifting filter implementations from JetsonPDF.Reader via InternalsVisibleTo:

  • CCITT (CcittFaxFilter) for compression codes 2 / 3 / 4
  • LZW (LzwFilter) for compression code 5
  • Flate / Deflate for codes 8 and 32946

A small managed PNG encoder (PngEncoder) ships in the assembly so any frame can be converted to a standalone PNG without taking on a third-party image dependency — TiffFrame.EncodePng() and TiffFrame.ToDataUri() are how the OpenSilver TiffViewer puts a .tif on a web page.

Quick start

using JetsonPDF.Tiff;

// Decode from bytes (e.g. an in-memory blob, an HTTP response, etc.).
TiffImage img = TiffImage.Decode(bytes);

// Or from a Stream — TIFF needs random access, so the stream is copied to
// memory up front. The stream is NOT disposed by Decode.
using FileStream fs = File.OpenRead("scan.tif");
TiffImage img2 = TiffImage.Decode(fs);

Console.WriteLine($"{img.Frames.Count} page(s)");
foreach (TiffFrame frame in img.Frames)
{
    Console.WriteLine(
        $"  {frame.Width}x{frame.Height} " +
        $"{frame.Photometric} compression={frame.Compression} " +
        $"{frame.BitsPerSample} bpp x {frame.SamplesPerPixel} samples");
}

// Hand a frame to any consumer that takes PNG.
byte[] png = img.Frames[0].EncodePng();

Reading TIFFs

Decoding

TiffImage TiffImage.Decode(byte[] data);
TiffImage TiffImage.Decode(Stream stream);

Both overloads return a TiffImage whose Frames collection has been populated eagerly — one TiffFrame per IFD, in source order.

Decode can throw:

Exception Why
ArgumentNullException data / stream is null.
InvalidDataException The bytes aren't a well-formed baseline TIFF (bad header, missing required tag, strip length mismatch, etc.).
NotSupportedException The file uses a feature this decoder doesn't implement — see Out of scope.

TiffFrame

Each decoded page exposes:

Member Type What it is
Width / Height int Pixel dimensions (tags 256 / 257).
Compression TiffCompression The source compression code (preserved from the IFD).
Photometric TiffPhotometric Source photometric interpretation.
BitsPerSample int Source bits-per-sample (1, 2, 4, or 8).
SamplesPerPixel int Source sample count per pixel.
Rgba8888 byte[]? Row-major decoded RGBA, length = W * H * 4. Null when this frame is a JPEG passthrough.
PassthroughJpeg byte[]? Raw JPEG bytes for compression-7 frames whose strip is a self-contained JPEG. Null otherwise.

Compression, Photometric, BitsPerSample, and SamplesPerPixel describe the source layout — they're not what the writer will produce if you re- encode. After decode, pixels are always in 8-bit RGBA regardless of the source bit depth or photometric interpretation.

Encoding frames to PNG / data URI

byte[] EncodePng();      // RGBA -> PNG. Throws if this frame is a JPEG passthrough.
byte[] EncodeImage();    // PNG, or the original JPEG bytes if passthrough.
string ToDataUri();      // "data:image/png;base64,..." or "data:image/jpeg;base64,..."

EncodePng() produces a standalone PNG with full RGBA. EncodeImage() is the JPEG-aware variant — for compression-7 passthrough frames it returns the original JPEG bytes (no re-encode), and for everything else it produces PNG. ToDataUri() wraps the bytes in a data: URI suitable for an HTML <img> tag or an OpenSilver BitmapImage. Building the URI involves a base64 encode of the payload, so cache the result if you call it repeatedly.

Writing TIFFs

public static class TiffWriter
{
    public static byte[] Encode(
        IEnumerable<TiffEncodeFrame> frames,
        TiffWriteOptions? options = null,
        Action<int>? onFrameEncoded = null);

    public static void Encode(
        Stream output,
        IEnumerable<TiffEncodeFrame> frames,
        TiffWriteOptions? options = null,
        Action<int>? onFrameEncoded = null);

    public static byte[] Encode(
        int width, int height, byte[] rgba8888,
        TiffWriteOptions? options = null);
}

The byte-returning overload is the primary one — it does the full encode in memory and hands back the result. The Stream overload is a thin wrapper that calls the byte overload and writes the result to the supplied stream (the stream is not closed). The (width, height, rgba) overload is a convenience for the common "one frame, no options" case.

onFrameEncoded fires with a 1-based frame index after each frame's strips have been compressed — useful for driving a progress bar on a multi-page encode.

TiffEncodeFrame

One page of input. Pixels are row-major RGBA8888 — the same shape TiffFrame.Rgba8888 produces, so an encoder → decoder round trip is trivially symmetric.

new TiffEncodeFrame(int width, int height, byte[] rgba8888);
TiffEncodeFrame.From(TiffFrame frame);  // convenience: build from a decoded frame

The constructor validates that rgba8888.Length == width * height * 4. From throws if the source TiffFrame is a JPEG passthrough (no RGBA payload to hand to the encoder).

TiffWriteOptions

Property Default Meaning
Compression Deflate Strip compression. See the compression matrix for what the writer accepts.
PixelFormat Rgb How RGBA input is mapped onto TIFF samples on disk.
BlackAndWhiteThreshold 128 Luminance threshold (0..255) used when PixelFormat = BlackAndWhite. Pixels below the threshold become black (BlackIsZero "1").
DpiX 72 Tag 282 (XResolution).
DpiY 72 Tag 283 (YResolution).
RowsPerStrip 64 Approximate rows per strip. Multi-strip output keeps memory pressure bounded on large pages. Set to 0 to put the whole page in a single strip.

TiffEncodePixelFormat

Value On-disk layout
Rgb 24 bpp RGB (3 samples x 8 bits), alpha dropped. Photometric = Rgb.
Rgba 32 bpp RGBA (4 samples x 8 bits), alpha kept. ExtraSamples = 2 (unassociated).
Grayscale 8 bpp greyscale, BlackIsZero. Luminance = 0.299*R + 0.587*G + 0.114*B.
BlackAndWhite 1 bpp, BlackIsZero, thresholded against BlackAndWhiteThreshold.

The pixel-format choice is independent of the compression choice — any of the four pixel formats can be combined with any supported write compression.

Multi-frame output

TiffWriter.Encode takes an IEnumerable<TiffEncodeFrame> and emits a single TIFF file whose IFDs are chained via the next-IFD pointer in source order. Strip data for every frame is written first, then each frame's IFD payloads and the IFD itself; the chain pointers are patched at the end. The result is a single file that opens as a multi-page document in any viewer that supports baseline TIFF.

var frames = new[]
{
    new TiffEncodeFrame(w, h, page0Rgba),
    new TiffEncodeFrame(w, h, page1Rgba),
    new TiffEncodeFrame(w, h, page2Rgba),
};

byte[] multiPage = TiffWriter.Encode(frames, new TiffWriteOptions
{
    Compression = TiffCompression.Deflate,
    PixelFormat = TiffEncodePixelFormat.Rgb,
    RowsPerStrip = 64,
});

Compression matrix

The reader is permissive (it handles every baseline strip compression and JPEG passthrough); the writer is intentionally narrow.

Code TiffCompression enum Read Write
1 None yes yes
2 CcittRle (T.4 1D / modified Huffman) yes no
3 CcittGroup3Fax (T.4, 1D and 2D via T4Options bit 0) yes no
4 CcittGroup4Fax (T.6) yes no
5 Lzw yes no
6 JpegOldStyle (TIFF 6 "old JPEG") no no
7 JpegNew (RFC 2301 JPEG-in-TIFF) passthrough only* no
8 Deflate yes yes
32773 PackBits yes yes
32946 AdobeDeflate yes yes (same wire as code 8)

* Compression 7 is read as a single self-contained JPEG strip — the original JPEG bytes come back via TiffFrame.PassthroughJpeg. Split-JPEG frames using the JPEGTables tag (split tables / per-strip entropy coding) throw NotSupportedException.

Asking the writer for an unsupported compression throws NotSupportedException with a message listing the supported set:

TIFF writer does not yet support compression Lzw.
Supported: None, PackBits, Deflate, AdobeDeflate.

Photometric and bit-depth coverage

Photometric Code Read Notes
WhiteIsZero 0 yes Greyscale, inverted.
BlackIsZero 1 yes Greyscale.
Rgb 2 yes 3 or 4 samples per pixel; alpha via ExtraSamples.
Palette 3 yes Requires tag 320 ColorMap; 8- or 16-bit ranges auto-detected.
TransparencyMask 4 no Throws NotSupportedException.
Cmyk 5 yes Naive (1-C)(1-K) → sRGB conversion (no ICC).
YCbCr 6 no Throws NotSupportedException.
CieLab 8 no Throws NotSupportedException.

Supported bits-per-sample on read: 1, 2, 4, 8. Anything else (notably 16-bit and 32-bit / floating-point samples) throws.

Supported sample arrangements: chunky only. PlanarConfig = 2 (separate planes per sample) throws.

ExtraSamples is honoured for both unassociated (2) and associated / premultiplied (1) alpha — premultiplied alpha is un-premultiplied on the way to RGBA.

FillOrder = 2 (LSB-first bit order, common on CCITT fax data) is reversed byte-by-byte before decoding.

Predictor support

Predictor Value Read Notes
None 1 yes Default.
Horizontal differencing 2 yes 8-bit samples only. The undo pass runs after strip decompression and before photometric conversion.
Floating-point 3 no Throws.

The writer does not emit a predictor (predictor = 1 is the default and isn't written). For files that compress well with Deflate + predictor 2, encoding through this library trades a bit of ratio for predictor-free output.

Interop with other JetsonPDF assemblies

Consumer How it uses JetsonPDF.Tiff
JetsonPDF.OpenSilver The TiffViewer control accepts a TIFF as byte[] or Stream, decodes it with TiffImage.Decode, and binds each frame's ToDataUri() to an <Image>. This is the original motivation for the assembly — browsers can't decode TIFF natively, and OpenSilver apps can't call GDI+.
JetsonPDF.PdfToTiffConverter Drives the WPF render pipeline page-by-page (so Main needs [STAThread]), then hands the rasterised RGBA buffers to TiffWriter.Encode. The shared TiffConversionProgress struct in this assembly reports progress through IProgress<T> at each TiffConversionStage transition (ConvertingXamlRenderingEncodingCompleted).
JetsonPDF.Reader JetsonPDF.Tiff consumes the reader's CcittFaxFilter, LzwFilter, and Flate machinery via InternalsVisibleTo("JetsonPDF.Tiff") declared in JetsonPDF.Reader's AssemblyInfo.cs. This avoids reimplementing three non-trivial bit-level decoders.

Out of scope

The following are intentionally unsupported. The decoder throws NotSupportedException with an explanatory message when it encounters them, rather than producing wrong pixels:

  • BigTIFF (8-byte offsets, magic 43 instead of 42).
  • Tiled images (tag 322 TileWidth / 324 TileOffsets) — strip-based layouts only.
  • 16-bit and 32-bit samples, including IEEE float — only 1 / 2 / 4 / 8 bpp.
  • Planar configurations (PlanarConfig = 2) — chunky only.
  • YCbCr (photometric 6), CIELab (photometric 8), TransparencyMask (photometric 4), and LogLuv (photometrics 32844 / 32845).
  • Old-style JPEG (compression code 6) — the obsolete TIFF 6 JPEG mode.
  • JPEG-in-TIFF with split JPEG tables (compression 7 with the JPEGTables tag) — only single-strip self-contained JPEG passthrough is supported.
  • JBIG (compressions 34661 / 34715) and JPEG 2000-in-TIFF (33003 / 33005).
  • Floating-point predictor (predictor 3).
  • CCITT T.6 "uncompressed mode" (T6Options bit 1) — almost no encoders produce this; throws.
  • GeoTIFF, EXIF, XMP, and other auxiliary metadata IFDs — the reader reads the baseline image IFDs and ignores extension tags.

Limitations

  • Decode is eager. TiffImage.Decode reads the entire file and decodes every IFD up front; there is no lazy / on-demand frame path. TIFF needs random access across the whole file, so streamed decoding would buffer internally anyway. For huge multi-page documents where you only want one page, an external pre-split is still cheaper than feeding the whole file through this decoder.
  • The writer is narrower than the reader. Encoding CCITT or LZW would duplicate codecs that mostly exist as legacy compatibility — for a modern general-purpose ratio use Deflate, and for a guaranteed-round-trip option use PackBits.
  • CMYK → RGB is naive. Pure (1 - C)(1 - K) math, no ICC profile, no black-point compensation. Acceptable for display previews; not for colour- managed workflows.
  • No metadata round-trip. Tags outside the baseline image-data set (XMP, EXIF, GeoTIFF, IPTC, custom IFDs) are read and discarded. The writer emits only the minimum required tags plus resolution.
  • Resolution unit is fixed at inches (tag 296 = 2). Centimetre-unit output isn't exposed in TiffWriteOptions.

Targets

  • net8.0
  • netstandard2.0
  • net462

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on JetsonPDF.Tiff:

Package Downloads
JetsonPDF.OpenSilver

OpenSilver integration for JetsonPDF. Bundles both directions of the XAML pipeline. Authoring (XAML→PDF): the same xmlns:jetsonpdf="http://schemas.jetsonpdf.com/authoring/2025" dialect as the WPF integration, driven by the OpenSilver runtime so the same XAML compiles to a PDF from inside a WebAssembly app, an Edge WebView2 simulator, or a Playwright-driven Chromium CLI. Viewer (PDF→XAML): emits XAML that OpenSilver's XamlReader.Load renders in the browser — text via vector paths (no font cache), images via base64 data URIs, AcroForm widgets as live OpenSilver controls. Walker + image encoder + widget-action dispatch are OpenSilver-specific; emission code, snapshot model, and PDF emission are shared with the WPF flavour. Authoring types live under JetsonPDF.OpenSilver.Authoring; viewer/markup types under JetsonPDF.OpenSilver.

JetsonPDF.PdfToTiffConverter

Rasterizes a parsed JetsonPDF document to a multipage TIFF. Reuses the JetsonPDF.Wpf PdfToXamlConverter pipeline (XamlReader.Parse + WPF Measure/Arrange) for layout, captures each page via RenderTargetBitmap, and encodes the multipage TIFF through JetsonPDF.Tiff's managed TiffWriter (no GDI+ dependency). Windows-only because of WPF rasterisation (net8.0-windows + WPF); must be invoked from an STA thread.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 182 6/6/2026
1.0.0 152 5/23/2026
0.2.0-preview 152 5/23/2026