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.