The complete script draws the same RGB pixels three times: Flate/Paeth, LZW/TIFF and adaptive LZW/PNG.
php examples/filters-predictors.php filters-predictors.pdf
Bytes are PDF samples, three eight-bit components per pixel, row by row. They
are not a PNG or TIFF file. PredictorLayout expresses Colors, Columns and
BitsPerComponent.
TIFF subtracts the preceding sample of the same component, including at depths of 1, 2, 4 and 16 bits. PNG works on bytes and prefixes each row with the selected filter. Paeth uses the left, upper and upper-left neighbors; adaptive selection examines possibilities for each row.
FlateDecode or LZWDecode composes prediction before compression.
StreamObject uses those same objects to write /Filter, /DecodeParms and
length. LZW grows codes from 9 to 12 bits, then resets its dictionary.
EarlyChange selects boundaries according to the two permitted conventions;
decompressed pixels remain identical.
Changing depth requires adapting data, packing and the image dictionary together.
Changing BitsPerComponent alone does not convert bytes. CI compares a large
corpus against an independent oracle and qpdf: width changes, resets, depths
and partial rows.
Reference: ISO 7.4.4, physical pages 28–33.
View the PDF result
This file was generated with the code in this documentation. All pages appear below; open the PDF to use its links and bookmarks.
Use the browser PDF viewer
This viewer depends on your browser. If the document does not appear, use “Open the PDF” or “Download”.
Complete source code
Code from roukmoute/pdf · View the source at the documented commit ↗
Read the commented script: filters-predictors.php
<?php
declare(strict_types=1);
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Resources\XObject;
use PDF\FileStructure;
use PDF\Graphics\CoordinateSystems\Matrix;
use PDF\Specification\Dictionary\ImageDictionaryTable89;
use PDF\Specification\Dictionary\ImageDictionaryTable89\BitsPerComponentEntry;
use PDF\Specification\Dictionary\ImageDictionaryTable89\ColorSpaceEntry;
use PDF\Specification\Dictionary\ImageDictionaryTable89\HeightEntry;
use PDF\Specification\Dictionary\ImageDictionaryTable89\SubtypeEntry;
use PDF\Specification\Dictionary\ImageDictionaryTable89\TypeEntry;
use PDF\Specification\Dictionary\ImageDictionaryTable89\WidthEntry;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\ConcatenateMatrix;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\InvokeXObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\RestoreGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SaveGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Filters\EarlyChange;
use PDF\Syntax\Filters\FixedPngFilterSelection;
use PDF\Syntax\Filters\FlateDecode;
use PDF\Syntax\Filters\LZWDecode;
use PDF\Syntax\Filters\PngPredictor;
use PDF\Syntax\Filters\PngRowFilter;
use PDF\Syntax\Filters\PredictorLayout;
use PDF\Syntax\Filters\TiffPredictor;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// 1. Build the same RGB pixels for all three images. Rows are arranged
// from top to bottom; each pixel contains three eight-bit components.
// These bytes are raw PDF samples, not an imported PNG or TIFF file.
$width = 64;
$height = 32;
$pixels = '';
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
$pixels .= chr(intdiv($x * 255, $width - 1)) . chr(intdiv($y * 255, $height - 1)) . chr(intdiv(($x + $y) * 255, $width + $height - 2));
}
}
$imageDictionary = new ImageDictionaryTable89(
new TypeEntry(new NameType('XObject')),
new SubtypeEntry(new NameType('Image')),
new WidthEntry(new IntegerType($width)),
new HeightEntry(new IntegerType($height)),
new ColorSpaceEntry(new NameType('DeviceRGB')),
new BitsPerComponentEntry(new IntegerType(8)),
);
// 2. Describe prediction rows with the PDF values in Table 8:
// Colors=3, Columns=64, BitsPerComponent=8 by default. TIFF subtracts the
// preceding sample of the same component. PNG predicts each byte
// and prefixes each row with the number of the filter used.
$layout = new PredictorLayout(new IntegerType(3), new IntegerType($width));
$paeth = new PngPredictor($layout, new FixedPngFilterSelection(PngRowFilter::Paeth));
$tiff = new TiffPredictor($layout);
$adaptive = new PngPredictor($layout);
// 3. The filter composes prediction before compression. StreamObject produces
// /Filter, /DecodeParms and encoded length from these same objects.
// EarlyChange determines only the 9/10/11/12-bit boundaries of LZW codes.
$images = [
new StreamObject($pixels, $imageDictionary, new FlateDecode(6, $paeth)),
new StreamObject($pixels, $imageDictionary, new LZWDecode(new EarlyChange(new IntegerType(0)), $tiff)),
new StreamObject($pixels, $imageDictionary, new LZWDecode(new EarlyChange(new IntegerType(1)), $adaptive)),
];
$font = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(
new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))),
new XObject(new DictionaryType(
new Entry(new NameType('FlatePaeth'), new IndirectReference($images[0])),
new Entry(new NameType('LzwTiff'), new IndirectReference($images[1])),
new Entry(new NameType('LzwPng'), new IndirectReference($images[2])),
)),
);
$operators = [
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(22)),
new MoveTextPosition(new NumericType(42), new NumericType(784)),
new ShowText(new LiteralString('Les memes pixels, trois encodages PDF')),
new SetTextFontAndSize(new NameType('F1'), new NumericType(11)),
new MoveTextPosition(new NumericType(0), new NumericType(-38)),
new ShowText(new LiteralString('La prediction transforme les octets avant LZW ou zlib.')),
new MoveTextPosition(new NumericType(0), new NumericType(-22)),
new ShowText(new LiteralString('Le lecteur inverse la compression, puis la prediction declaree.')),
new EndTextObject(),
];
// 4. Matrices change only display size and position.
// All images retain 64 × 32 pixels; only their encoded bytes differ.
foreach ([['FlatePaeth', 'Flate + PNG Paeth'], ['LzwTiff', 'LZW EarlyChange=0 + TIFF'], ['LzwPng', 'LZW EarlyChange=1 + PNG adaptatif']] as $index => [$name, $label]) {
$top = 636 - $index * 170;
array_push(
$operators,
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
new MoveTextPosition(new NumericType(42), new NumericType($top + 16)),
new ShowText(new LiteralString($label)),
new EndTextObject(),
new SaveGraphicsState(),
new ConcatenateMatrix(new Matrix(new NumericType(256), new NumericType(0), new NumericType(0), new NumericType(128), new NumericType(42), new NumericType($top - 128))),
new InvokeXObject(new NameType($name)),
new RestoreGraphicsState(),
);
}
$content = new StreamObject(new ContentStreams(...$operators));
$pages = new PageTreeNode();
$pages->addKid(new PageObject(new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842)), $resources, $content));
$catalog = new DocumentCatalog($pages);
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/filters-predictors.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;
