Table of Contents

CCITT fax

Egami decodes raw CCITT fax data: modified Huffman and Group 3 (ITU-T T.4, one- and two-dimensional) and Group 4 (ITU-T T.6), the bi-level coding of fax machines, of many scanned PDFs and of TIFF compressions 2, 3 and 4. Raw data has no header, so its parameters come from outside: in a PDF they are the CCITTFaxDecode filter's DecodeParms dictionary, and CcittFaxParameters holds the same eight values with the same defaults. CcittFax.Encode writes Group 4 and one-dimensional Group 3 for PDF images; inside TIFF CCITT data is read and written by the TIFF format. Every CCITT stream of a 4,000-file PDF corpus, byte-aligned Group 4 among them, and of a matrix of every parameter combination decodes bit for bit as PDFium and pdf.js decode it.

As a PDF filter

CcittFax.Decode gives what the filter gives: one bit a pixel, most significant first, each row padded to whole bytes, black 0 and white 1 unless BlackIs1 is set. The PDF library then applies the image's /Decode array, /ImageMask and color space, as it does after every other filter.

// A PDF image with /Filter /CCITTFaxDecode: its DecodeParms become the parameters, and the result is the filter's
// output, packed rows with black 0 and white 1, ready for the image's /Decode array and color space.
CcittFaxParameters parameters = new() { K = -1, EncodedByteAlign = true, Columns = 301, Rows = 48 };
byte[] rows = CcittFax.Decode(stream, parameters, out int rowCount);
Parameter Default Meaning
K 0 Below 0 Group 4; 0 one-dimensional Group 3; above 0 mixed Group 3, a bit after each EOL picking the next row's coding
EndOfLine false EOLs are required before each row; they are accepted where present either way
EncodedByteAlign false Group 4 rows start on byte boundaries; Group 3 EOLs end on one, after fill bits
Columns 1728 Row width in pixels
Rows 0 Row count; 0 decodes until the data or its end-of-block code ends
EndOfBlock true The data ends with RTC (Group 3) or EOFB (Group 4); decoding stops there either way
BlackIs1 false The output's bit sense: 1 for black instead of 0
DamagedRowsBeforeError 0 With EOLs and K 0 or above, how many damaged rows are skipped to the next EOL

Writing

CcittFax.Encode is the filter's inverse: it takes packed rows in the same form Decode gives (black 0 unless BlackIs1) and writes them with the parameters' coding. K below 0 writes Group 4, which is usually the smallest coding for scanned text, often half of Flate's size; K 0 writes one-dimensional Group 3. EndOfLine puts an EOL before each Group 3 row and EndOfBlock ends the data with RTC or EOFB. Mixed Group 3 (K above 0) and EncodedByteAlign are not written. The same parameters then go in the image's DecodeParms, and Decode gives back the rows exactly.

// A bi-level scan as a PDF image with /Filter /CCITTFaxDecode: pack its rows (0 black, 1 white), then write Group 4.
int rowBytes = (scan.Width + 7) / 8;
byte[] rows = new byte[rowBytes * scan.Height];
for (int y = 0; y < scan.Height; y++)
{
    Span<Gray8> row = scan.Row(y);
    for (int x = 0; x < scan.Width; x++)
    {
        if (row[x].L >= 128)
        {
            rows[(y * rowBytes) + (x >> 3)] |= (byte)(0x80 >> (x & 7));
        }
    }
}

// These parameters are the image's DecodeParms: << /K -1 /Columns 301 /Rows 48 >>.
CcittFaxParameters parameters = new() { K = -1, Columns = scan.Width, Rows = scan.Height };
byte[] g4 = CcittFax.Encode(rows, parameters);

As an image

CcittFormat loads the data as Gray8, black 0 and white 255, as 1-bit TIFF and JBIG2 load, whatever BlackIs1 says. The parameters go in CcittReaderOptions. With Rows given, Raster.Probe reads nothing and RasterDecoder decodes a row at a time, holding only the current row and the one above it. The format has no signature, so detection never picks it.

// The same data as an image: Gray8, black 0 and white 255. The data has no header, so name the format.
CcittReaderOptions options = new() { Parameters = new CcittFaxParameters { K = -1, EncodedByteAlign = true, Columns = 301, Rows = 48 } };
using Raster image = Raster.Load(stream, CcittFormat.Info, options: options);

Damaged data

A row that does not decode ends decoding: the rows before it are kept, the rest are white, and the warning Ccitt.Damaged goes to EgamiContext.Diagnostics (or to RasterMetadata.Warnings for a load). With EOLs and K 0 or above, up to DamagedRowsBeforeError damaged rows are first skipped to the next EOL, each repeating the row before it (Ccitt.RowsSkipped). Data that ends before Rows rows warns Ccitt.Truncated. Only data in which not one row decodes throws CorruptDataException; a strict load throws on any of these. PDFium and pdf.js carry on decoding after damage instead, so the later rows of a damaged stream differ from theirs.

Columns and Rows are checked against SafetyLimits before memory is taken, and with Rows 0 the output grows only up to MaxPixels, so a stream of EOLs cannot use up memory.

Recipes

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

Decode a PDF's CCITT image

// An image XObject with /Filter /CCITTFaxDecode: DecodeParms keys not given keep PDF's defaults. The image's /Height
// stands in for a missing Rows, so the decoder stops where the image does.
CcittFaxParameters parameters = new() { K = k, EncodedByteAlign = aligned, Columns = columns, Rows = rows > 0 ? rows : height };
byte[] bits = CcittFax.Decode(data, parameters, out int decoded);

// The filter's samples: 0 black, 1 white. /Decode [1 0] inverts them, and an /ImageMask paints where a sample is 0.
bool invert = decode is [> 0.5, ..];
int rowBytes = (columns + 7) / 8;
using Raster<Gray8> image = Raster.Create<Gray8>(width, decoded);
for (int y = 0; y < decoded; y++)
{
    Span<Gray8> row = image.Row(y);
    for (int x = 0; x < Math.Min(width, columns); x++)
    {
        bool sample = ((bits[(y * rowBytes) + (x >> 3)] >> (7 - (x & 7))) & 1) != 0;
        row[x] = new Gray8(sample != invert ? (byte)255 : (byte)0);
    }
}

CcittFax.Decode gives what the CCITTFaxDecode filter gives, byte-aligned Group 4 included; the image's /Decode, /ImageMask and color space then apply as after any filter. To load the data straight to Gray8, use CcittFormat.