JetsonPDF.Tiff
1.1.0
dotnet add package JetsonPDF.Tiff --version 1.1.0
NuGet\Install-Package JetsonPDF.Tiff -Version 1.1.0
<PackageReference Include="JetsonPDF.Tiff" Version="1.1.0" />
<PackageVersion Include="JetsonPDF.Tiff" Version="1.1.0" />
<PackageReference Include="JetsonPDF.Tiff" />
paket add JetsonPDF.Tiff --version 1.1.0
#r "nuget: JetsonPDF.Tiff, 1.1.0"
#:package JetsonPDF.Tiff@1.1.0
#addin nuget:?package=JetsonPDF.Tiff&version=1.1.0
#tool nuget:?package=JetsonPDF.Tiff&version=1.1.0
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
- Quick start
- Reading TIFFs
- Writing TIFFs
- Compression matrix
- Photometric and bit-depth coverage
- Predictor support
- Interop with other JetsonPDF assemblies
- Out of scope
- Limitations
- Targets
- License
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 (ConvertingXaml → Rendering → Encoding → Completed). |
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/ 324TileOffsets) — 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
JPEGTablestag) — 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.Decodereads 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.0netstandard2.0net462
License
MIT.
| Product | Versions 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. |
-
.NETFramework 4.6.2
- JetsonPDF.Reader (>= 1.1.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- System.Buffers (>= 4.5.1)
- System.IO.Compression (>= 4.3.0)
- System.Memory (>= 4.5.5)
- System.ValueTuple (>= 4.5.0)
-
.NETStandard 2.0
- JetsonPDF.Reader (>= 1.1.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- System.Buffers (>= 4.5.1)
- System.IO.Compression (>= 4.3.0)
- System.Memory (>= 4.5.5)
-
net8.0
- JetsonPDF.Reader (>= 1.1.0)
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 |