-
Notifications
You must be signed in to change notification settings - Fork 0
PDF A Conformance
Produce archival-grade PDF files that comply with ISO 19005 (PDF/A-1, PDF/A-2, PDF/A-3, or the PDF 2.0-based PDF/A-4). Call $doc->enablePdfA() before save() / output() and the library enforces all required constraints, embeds the necessary metadata and colour profile, and generates a file that passes veraPDF validation.
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font\Unit;
use DragonOfMercy\PhpPdf\PdfA\PdfALevel;
$doc = new Document(Unit::MM);
$doc->enablePdfA(PdfALevel::A2B);
// All fonts must be embedded - register a custom font family
$doc->registerFontFamily('DejaVu', 'fonts/DejaVuSans.ttf');
$page = $doc->addPage();
$page->setFont('DejaVu', 12);
$page->cell(0, 10, 'Archival document');
$doc->save('archive.pdf');| Case | Standard | Description |
|---|---|---|
PdfALevel::A1B |
ISO 19005-1, level B | Basic: correct visual reproduction. PDF 1.4-based. Transparency (alpha PNG / SVG opacity / mask) is forbidden. |
PdfALevel::A1A |
ISO 19005-1, level A | Accessible: A-1b plus a tagged logical structure tree. Requires a catalog language. Auto-enables tagging. |
PdfALevel::A2B |
ISO 19005-2, level B | Basic: correct visual reproduction. |
PdfALevel::A2U |
ISO 19005-2, level U | Unicode: A-2b plus a valid ToUnicode map on every font (satisfied automatically by custom embedded fonts). |
PdfALevel::A2A |
ISO 19005-2, level A | Accessible: A-2u plus a tagged logical structure tree. Auto-enables tagging. |
PdfALevel::A3B |
ISO 19005-3, level B | A-2b plus support for embedded associated files (e.g. Factur-X XML). |
PdfALevel::A3U |
ISO 19005-3, level U | A-2u plus support for embedded associated files. |
PdfALevel::A3A |
ISO 19005-3, level A | Accessible: A-3u plus a tagged logical structure tree. Auto-enables tagging. |
PdfALevel::A4 |
ISO 19005-4, PDF 2.0 | PDF/A-4: PDF 2.0-based archival. Unicode mapping mandatory, tagging not required, no conformance letter. Emits a %PDF-2.0 header and no /Info dictionary. |
PdfALevel::A4F |
ISO 19005-4, flavour F | A-4 plus support for embedded associated files (e.g. Factur-X XML). Adds pdfaid:conformance F. |
PDF/A level A adds a tagged logical structure tree on top of the Unicode (level U) requirements. The document must have a catalog language, and all content must be drawn through the high-level tagged API (cell(), markdown(), table(), image()). enablePdfA() with a level-A variant calls enableTagging() for you - you do not call it separately.
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font;
use DragonOfMercy\PhpPdf\Font\Unit;
use DragonOfMercy\PhpPdf\PdfA\PdfALevel;
$doc = new Document(Unit::MM);
$doc->metadata()->title('Archival report');
$doc->registerFontFamily('Body', regular: 'fonts/DejaVuSans.ttf', bold: 'fonts/DejaVuSans-Bold.ttf');
$doc->enablePdfA(PdfALevel::A2A, 'en-US'); // second arg sets the catalog /Lang
$page = $doc->addPage();
$page->setFont(Font::custom('Body'), 12);
$page->markdown("# Archival report\n\nContent drawn through the tagged high-level API.");
$page->image('chart.png', x: 20, y: 80, w: 60, h: 40, alt: 'Revenue chart');
$doc->save('archive-level-a.pdf');For PDF/A-3a (with an embedded associated file):
use DragonOfMercy\PhpPdf\PdfA\AFRelationship;
$doc->enablePdfA(PdfALevel::A3A, 'en-US');
$doc->attachFile(file_get_contents('factur-x.xml'), 'factur-x.xml', AFRelationship::Data, 'text/xml');Calling both enablePdfA(PdfALevel::A2A, 'en-US') and enablePdfUA('en-US') on the same document produces a single file that is simultaneously PDF/A-2a and PDF/UA-1. The XMP stream carries both the pdfaid schema (conformance A) and the pdfuaid schema, plus a PDF/A extension-schema declaration for pdfuaid, so veraPDF validates it under both the 2a and ua1 profiles.
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font;
use DragonOfMercy\PhpPdf\PdfA\PdfALevel;
$doc = new Document();
$doc->metadata()->title('Accessible archival report');
$doc->registerFontFamily('Body', regular: 'fonts/DejaVuSans.ttf', bold: 'fonts/DejaVuSans-Bold.ttf');
$doc->enablePdfA(PdfALevel::A2A, 'en-US');
$doc->enablePdfUA('en-US');
$page = $doc->addPage();
$page->setFont(Font::custom('Body'), 12);
$page->markdown("# Accessible archival report\n\nAll content drawn through the tagged API.");
$page->image('chart.png', x: 20, y: 80, w: 60, h: 40, alt: 'Revenue chart');
$doc->save('pdfa2a-and-pdfua1.pdf');The same combination works for PDF/A-3a + PDF/UA-1 using PdfALevel::A3A.
PDF/A-1 (ISO 19005-1:2005) is the oldest and strictest archival profile. It is built on PDF 1.4 and emits a %PDF-1.4 header.
Transparency is forbidden. ISO 19005-1 predates the PDF 1.4 soft-mask / transparency model and disallows it entirely. At output() time the conformance guard throws a PdfException if the document contains:
- A PNG with an alpha channel (alpha transparency).
- An SVG element that uses
fill-opacityorstroke-opacitybelow 1.0, oropacitybelow 1.0. - An SVG element that uses a
<mask>, soft-mask (maskType), or<filter>.
Remedy: flatten the image against a solid background before passing it to image(), remove opacity from the SVG source, or switch to PDF/A-2 or higher (which support transparency).
Embedded files are forbidden. There is no part-1 equivalent of PDF/A-3's associated-file mechanism; calling attachFile() on a PDF/A-1 document throws immediately.
Levels 1b and 1a only. ISO 19005-1 has no 'U' (Unicode) level. Level A adds a tagged structure tree (auto-enables tagging) and requires a catalog language.
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font\Unit;
use DragonOfMercy\PhpPdf\PdfA\PdfALevel;
$doc = new Document(Unit::MM);
$doc->metadata()->title('Archival document (PDF 1.4)');
$doc->registerFontFamily('Body', 'fonts/DejaVuSans.ttf');
$doc->enablePdfA(PdfALevel::A1B);
$page = $doc->addPage();
$page->setFont('Body', 12);
$page->cell(0, 10, 'PDF/A-1b archival document');
$doc->save('archive-pdfa1b.pdf');For level A (tagged, with catalog language):
$doc->enablePdfA(PdfALevel::A1A, 'en-US');-
Forced metadata path. The XMP packet and the document
/IDpair are always written. Normally/IDand full XMP are optional. For parts 2-3 the/Infodictionary is also written; PDF/A-4 omits it (see below). -
sRGB output intent. An
[OutputIntent]array is added to the catalog referencing a bundled 588-byte littleCMS sRGB ICC profile (/OutputConditionIdentifier (sRGB IEC61966-2.1)). -
pdfaidXMP schema. For parts 2-3, apdfaid:part(2 or 3) andpdfaid:conformance(B / U / A) block is prepended to the XMP packet. For PDF/A-4 the identification ispdfaid:part4 pluspdfaid:rev2020, withpdfaid:conformanceFonly for the A-4f flavour.
PDF/A-1 emits a %PDF-1.4 header (ISO 19005-1 is PDF 1.4-based) and uses a bundled ICC v2 sRGB output-intent profile (parts 2-4 use the v4 profile). PDF/A-1 also requires a /CIDSet stream in every embedded CIDFont subset, which the subsetter emits automatically. For PDF/A-2 and PDF/A-3 the version header is %PDF-1.7, satisfying their version requirement. PDF/A-4 is PDF 2.0-based, so it emits a %PDF-2.0 header instead.
PDF/A-4 (ISO 19005-4:2020) is built on PDF 2.0 rather than PDF 1.7 and differs from parts 2-3 in three observable ways:
-
%PDF-2.0header instead of%PDF-1.7. -
No
/Infotrailer dictionary. PDF 2.0 deprecates the document information dictionary, so PDF/A-4 forbids it; all metadata lives in the XMP stream. (Set values via$doc->metadata()exactly as before - they are written to XMP only.) -
Identification by revision. XMP carries
pdfaid:rev2020 in place of a part-2/3 conformance letter. There is noA/U/Blevel: Unicode mapping is always mandatory and tagging is never required.
The A4F flavour additionally permits embedded associated files and adds pdfaid:conformance F.
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font\Unit;
use DragonOfMercy\PhpPdf\PdfA\AFRelationship;
use DragonOfMercy\PhpPdf\PdfA\PdfALevel;
$doc = new Document(Unit::MM);
$doc->metadata()->title('Archival document (PDF 2.0)');
$doc->registerFontFamily('Body', 'fonts/DejaVuSans.ttf');
$doc->enablePdfA(PdfALevel::A4);
$page = $doc->addPage();
$page->setFont('Body', 12);
$page->cell(0, 10, 'PDF/A-4 archival document');
$doc->save('archive-pdfa4.pdf');
// A-4f with an embedded Factur-X invoice
$doc->enablePdfA(PdfALevel::A4F);
$doc->attachFile(file_get_contents('factur-x.xml'), 'factur-x.xml', AFRelationship::Data, 'text/xml');PDF/A forbids non-embedded fonts. All standard-14 fonts (Helvetica, Times, Courier, etc.) are not embedded and will cause a conformance guard error. Register every font via Document::registerFontFamily() before using it.
$doc->enablePdfA(PdfALevel::A2B);
// Register and use a custom embedded font
$doc->registerFontFamily('OpenSans', 'fonts/OpenSans-Regular.ttf', 'fonts/OpenSans-Bold.ttf');
$page->setFont('OpenSans', 12);PDF/A-2u and A-3u require a valid ToUnicode CMap on every font. Custom embedded fonts already carry /ToUnicode streams produced during subsetting, so the U variant is essentially free.
PdfAConformanceGuard is called by output() before serialization. It throws a PdfException in any of these cases:
| Condition | Error |
|---|---|
| A non-embedded standard font (Helvetica, Times, Courier, etc.) is in use. | Register all fonts with registerFontFamily(). |
| Encryption is configured on the same document. | PDF/A forbids encryption. |
Document JavaScript (addDocumentScript()) is present. |
PDF/A forbids embedded scripts. |
Appended revisions (addSignature(), addDocumentTimestamp(), enableLtv()) are present. |
PDF/A forbids incremental updates of this kind. |
Additionally, calling attachFile() on a PDF/A level that forbids embedded files (PDF/A-2 and the base PDF/A-4) throws immediately. Embedded associated files are only permitted at part 3 (A3B / A3U / A3A) or with the PDF/A-4f flavour (A4F).
PDF/A-3 allows embedding machine-readable files alongside the human-readable PDF. The most common use case is a Factur-X or ZUGFeRD e-invoice XML.
use DragonOfMercy\PhpPdf\PdfA\AFRelationship;
$doc->enablePdfA(PdfALevel::A3B);
$xml = file_get_contents('factur-x.xml');
$doc->attachFile(
$xml,
'factur-x.xml',
AFRelationship::Data,
'text/xml',
'Factur-X invoice data',
);
$doc->save('invoice-pdfa3.pdf');| Parameter | Type | Default | Description |
|---|---|---|---|
$bytes |
string |
- | Raw file content. |
$name |
string |
- | Filename (used as the key in the /EmbeddedFiles name tree and as /F / /UF). |
$relationship |
AFRelationship |
Data |
Semantic relationship of the file to the document. |
$mime |
string |
'application/octet-stream' |
MIME type of the embedded file. |
$description |
string|null |
null |
Optional human-readable description. |
$modDate |
DateTimeImmutable|null |
now | Modification date. Pass explicitly for deterministic output. |
| Case | PDF name | Typical use |
|---|---|---|
AFRelationship::Source |
Source |
File from which this PDF was generated. |
AFRelationship::Data |
Data |
Machine-readable data represented in the PDF (Factur-X default). |
AFRelationship::Alternative |
Alternative |
Alternative representation of the document content. |
AFRelationship::Supplement |
Supplement |
Supplementary material. |
AFRelationship::Unspecified |
Unspecified |
Relationship not specified. |
Attachments work on documents without enablePdfA() as well; in that case they are plain PDF attachments with no conformance restriction.
The library ships golden tests that pipe rendered output through veraPDF and assert isCompliant="true". They auto-skip when a JRE or the veraPDF CLI jar are absent.
-
tests/Golden/PdfA1bTest.php- byte-identity golden for PDF/A-1b, veraPDF-validated with--flavour 1b. -
tests/Golden/PdfA1aTest.php- byte-identity golden for PDF/A-1a, veraPDF-validated with--flavour 1a. -
tests/Golden/PdfA2ConformanceTest.php- covers levels B, U, and A for parts 2 and 3, including the combined PDF/A-2a + PDF/UA-1 scenario (run against both--flavour 2aand--flavour ua1), the PDF/A-4 / A-4f flavours (--flavour 4and--flavour 4f), and the PDF/A-1b / A-1a flavours (--flavour 1band--flavour 1a). -
tests/Golden/PdfA4Test.phpandtests/Golden/PdfA4fTest.php- byte-identity goldens for the two PDF/A-4 flavours.
cd build/
vendor/bin/phpunit tests/Golden/PdfA2ConformanceTest.php
vendor/bin/phpunit tests/Golden/PdfA1bTest.php tests/Golden/PdfA1aTest.phpMIT licensed. Source on GitHub - if phppdf helps you, you can buy me a coffee.