The June 2008 Adobe supplement extends PDF 1.7 to Adobe level 3. An extension retains the base PDF format and declares additional properties a reader may encounter.

This example uses extension declarations and a Bates artifact. Bates numbers are identifiers chosen by the application to index documents, for example DOC-000001 and EXH-000019. They are not necessarily the file's page numbers.

Run the example

php examples/adobe-extensions.php adobe-extensions.pdf

The complete script is commented step by step. The result contains one page and two identifiers displayed in its footer. This tutorial depends only on the core.

Declare the extension

$extensions = new DeveloperExtensions(new DeveloperExtensionEntry(
    new DeveloperPrefix(new NameType('ADBE')),
    new DeveloperExtension(new NameType('1.7'), new IntegerType(3)),
));
$catalog = new DocumentCatalog($pages, new Extensions($extensions));

DeveloperPrefix retains the case-sensitive prefix. ADBE identifies Adobe. DeveloperExtension assembles /BaseVersion /1.7 and /ExtensionLevel 3. DeveloperExtensionEntry associates the prefix with this instance. Extension declarations and all their entries remain direct, as ISO 32000-1, §7.12.1 requires: no indirect objects occur in this part of the catalog. Finally, Extensions places this dictionary in the document catalog.

The declared base version describes the specification being extended. It is not calculated from the file header. The model supports several prefixes with independent base versions and levels. The library does not automatically register prefixes in a registry.

Build the Bates artifact

$numbers = new ArrayType(
    new LiteralString('DOC-000001'),
    new LiteralString('EXH-000019'),
);
$bates = new BatesArtifact($numbers);

The object produces /Type /Pagination, /Subtype /BatesN and /Contents. Contents accepts a string for one identifier or an array of strings for several. Their order remains the application's choice. Values are PDF objects; a generic entry cannot replace them with integers or dictionaries.

Connect properties to drawing

new BeginMarkedContentWithProperties(new NameType('Artifact'), $bates),
// BT, Tf, Td and Tj draw the footer here.
new EndMarkedContent(),

BDC opens the /Artifact marked sequence with its properties; EMC closes it. Ordinary text operators position and draw the identifiers. Bates properties describe that content but create no visible text by themselves. The example therefore supplies numbers both to properties and to displayed text.

ContentStreams checks marked-sequence balance during writing. The independent example check also verifies the /ADBE declaration, /BatesN classification, order of the two strings and their visible presence.

Experiments and references

  • Replace the string array with one string and change the displayed text.
  • Add a developer prefix with its own level to observe both declarations.
  • Move the footer by changing coordinates supplied to MoveTextPosition.

The declaration mechanism appears on pages 23–25 of the supplement and in ISO 32000-1, §7.12. The Bates artifact appears in Table 10.17, page 100 of the supplement preserved in the core. The supplement guide details coverage of other extensions and provenance rules.

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.

PDF result adobe-extensions: page 1 of 1
Page 1 of 1
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: adobe-extensions.php
<?php

declare(strict_types=1);

use PDF\Document\Artifacts\BatesArtifact;
use PDF\Document\Extensions\DeveloperExtension;
use PDF\Document\Extensions\DeveloperExtensionEntry;
use PDF\Document\Extensions\DeveloperExtensions;
use PDF\Document\Extensions\DeveloperPrefix;
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\FileStructure;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\BeginMarkedContentWithProperties;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndMarkedContent;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DocumentStructure\CatalogDictionary\Extensions;
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\Object\ArrayType;
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';

// Tutorial 13: docs/tutorials/13-adobe-extensions.md.
// The catalog declares the Adobe level used; the page stream then realizes
// one of its extensions, Bates numbering (supplement, p.100).
$extensions = new DeveloperExtensions(new DeveloperExtensionEntry(
    new DeveloperPrefix(new NameType('ADBE')),
    new DeveloperExtension(new NameType('1.7'), new IntegerType(3)),
));

$font = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(new Font(new DictionaryType(
    new Entry(new NameType('F1'), new IndirectReference($font)),
)));

// Bates properties describe numbers already chosen by the application.
// They draw nothing. BDC/EMC classify the drawn text as a pagination
// artifact, distinct from document content.
$numbers = new ArrayType(new LiteralString('DOC-000001'), new LiteralString('EXH-000019'));
$bates = new BatesArtifact($numbers);
$contents = new StreamObject(new ContentStreams(
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(24)),
    new MoveTextPosition(new NumericType(64), new NumericType(716)),
    new ShowText(new LiteralString('PDFToolkit - extensions Adobe')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(13)),
    new MoveTextPosition(new NumericType(0), new NumericType(-46)),
    new ShowText(new LiteralString('PDF 1.7, Adobe ExtensionLevel 3')),
    new MoveTextPosition(new NumericType(0), new NumericType(-30)),
    new ShowText(new LiteralString('Le catalogue contient /Extensions /ADBE.')),
    new MoveTextPosition(new NumericType(0), new NumericType(-26)),
    new ShowText(new LiteralString('Chaque numero Bates reste un objet chaine explicite.')),
    new MoveTextPosition(new NumericType(0), new NumericType(-26)),
    new ShowText(new LiteralString('Leur liste est fournie par /Contents dans un artefact.')),
    new EndTextObject(),
    // The same position can carry several document indices.
    new BeginMarkedContentWithProperties(new NameType('Artifact'), $bates),
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(11)),
    new MoveTextPosition(new NumericType(64), new NumericType(48)),
    new ShowText(new LiteralString('DOC-000001  |  EXH-000019')),
    new EndTextObject(),
    new EndMarkedContent(),
));
$page = new PageObject(
    new Rectangle(new NumericType(0), new NumericType(0), new NumericType(612), new NumericType(792)),
    $resources,
    $contents,
);
$pages = (new PageTreeNode())->addKid($page);
$catalog = new DocumentCatalog($pages, new Extensions($extensions));
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/adobe-extensions.pdf';
$file->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;

Guide source at the documented commit ↗