PDF/A validation and repair
Added in version 10.14.
PDF/A (ISO 19005) is the archival subset of PDF. The pikepdf.pdfa
module can prepare a document for PDF/A, check whether it would conform when
saved, and save it with a guarantee that the bytes written were validated.
What this is, and what it is not
pikepdf.pdfa is an allowlist validator for the level B
(“basic”) conformance of PDF/A-1, PDF/A-2 and PDF/A-3, that is
PDF/A-1b, PDF/A-2b and PDF/A-3b. It approves a document only if every construct
it encounters is one it recognizes and knows to conform. Anything it does not
recognize is reported, rather than guessed at. A denial may therefore be a false
alarm; an approval should never be wrong.
It is not:
A certification. veraPDF is the reference validator for PDF/A. The rule identifiers used here are veraPDF’s, and pikepdf’s validator was developed by comparing its verdicts with veraPDF’s on the veraPDF test corpus. If you need an authoritative answer, use veraPDF.
A converter. pikepdf repairs structure and metadata. It does not convert colour, embed or subset fonts, rasterize transparency, or rewrite page content. A document that needs any of those will not pass, and you will need a tool such as Ghostscript to convert it first.
A validator for levels A or U, or for PDF/A-4. Only 1b, 2b and 3b are supported.
Constructs that PDF/A permits but that the validator does not examine are
reported as not_checked instead of passing. The list of such constructs is in
Unsupported constructs; it will shrink over time without changes to the API.
Installing
pikepdf.pdfa needs a few pure Python dependencies (jsonschema, referencing
and fontTools) that the rest of pikepdf does not. They are an optional extra:
pip install 'pikepdf[pdfa]'
import pikepdf never loads pikepdf.pdfa or its dependencies. The module
is imported on first use, either explicitly or as the attribute pikepdf.pdfa.
If the dependencies are missing, importing it raises ImportError naming
the extra.
Quick start
For most uses, pikepdf.pdfa.save() is the only call you need:
import pikepdf
from pikepdf import pdfa
with pikepdf.open('input.pdf') as pdf:
report = pdfa.save(pdf, 'output.pdf', '2b')
for line in report.prepared.describe():
print(line)
save repairs what it can, writes the file, reads back what it wrote and
validates it. It returns only if the written file passed. Otherwise it raises
pikepdf.pdfa.PdfaError, whose report explains why (and, as on
success, report.prepared records what was repaired), and the destination is
left untouched:
try:
with pikepdf.open('form.pdf') as pdf:
pdfa.save(pdf, 'form-pdfa.pdf', '2b')
except pdfa.PdfaError as e:
print(e.report.summary())
The flavour can be given as a string ('1b', '2b', '3b') or as a
pikepdf.pdfa.Flavour.
Prepare, check and save
The module has three verbs, which differ in what they change and what they promise.
pikepdf.pdfa.prepare()Applies repairs to the open document, in memory, and returns a
pikepdf.pdfa.PrepareResultrecording what it changed. Calling it again makes no further changes. It does not validate anything. See What prepare changes for the list of repairs.pikepdf.pdfa.check()Predicts the verdict for the file that
savewould write, without writing anything and without changing the document. It is pure and cheap, and returns apikepdf.pdfa.Report.pikepdf.pdfa.save()Runs
prepare(unlessrepair=False), writes the document to a temporary file, reopens the file and validates the bytes written, and only then moves it into place. It is the only call that promises anything.
Use save when you want a PDF/A file. Use prepare and check when you want
to know whether a document can become PDF/A, or to decide what to do before
saving, for example whether to fall back to a converter:
>>> from pikepdf import pdfa
>>> pdf = pikepdf.open('../tests/resources/pdfa/francais.pdf')
>>> pdfa.check(pdf, '2b').verdict
'fail'
>>> result = pdfa.prepare(pdf, '2b')
>>> result.changed
True
>>> report = pdfa.check(pdf, '2b')
>>> print(report.summary())
PDF/A-2b: pass
check describes the document at the moment of the call. pikepdf cannot
tell whether a document changed since it was checked, so a stored report is
only a hint. In particular, run check after your last metadata edit:
pikepdf.Pdf.open_metadata() writes the XMP packet when its context
manager exits, not while you are editing.
with pikepdf.open('input.pdf') as pdf:
pdfa.prepare(pdf, '2b')
with pdf.open_metadata() as meta:
meta['dc:title'] = 'Un document en français'
report = pdfa.check(pdf, '2b') # after the metadata was written
if report.passed:
pdfa.save(pdf, 'output.pdf', '2b')
check is advisory. If you save the document yourself with
pdf.save(path, **report.save_kwargs), the file should get the same verdict,
but nothing verifies that it does. pikepdf.pdfa.validate_written()
validates a file that is already written, if you need to confirm it.
Verdicts and findings
A pikepdf.pdfa.Report has a verdict with one of three values:
'pass'Nothing was found that prevents approval.
'fail'At least one finding is a violation: a construct that breaks PDF/A.
'not_checked'There are findings, but all of them are constructs the validator does not check (unsupported). The document may well be valid PDF/A; pikepdf cannot say.
savetreats this like'fail'and raises.
report.passed is true only for 'pass'. report.violations and
report.unsupported split the findings by kind.
Each pikepdf.pdfa.Finding is a named tuple of rule, where,
message and kind ('violation' or 'unsupported'):
>>> pdf = pikepdf.open('../tests/resources/pdfa/francais.pdf')
>>> report = pdfa.check(pdf, '2b')
>>> finding = report.violations[0]
>>> finding.rule
'ISO_19005_2:6.2.4.3-4'
>>> finding.kind
'violation'
>>> print(finding)
[violation] ISO_19005_2:6.2.4.3-4 at obj 10 0 (Page) content Do /Im0: DeviceGray used without a PDF/A OutputIntent
Rule identifiers follow these conventions:
ISO_19005_2:6.2.8-3: a veraPDF rule, named by the part of ISO 19005 (ISO_19005_1for PDF/A-1,ISO_19005_2andISO_19005_3for the others), the clause and veraPDF’s test number. The rule’s description is in the veraPDF validation profiles.pikepdf:<name>: a local policy, or a construct the validator does not support, such aspikepdf:shadingorpikepdf:pattern.pikepdf:schema-<Role>: a key or value the validator does not recognize in an object of a particular role, such aspikepdf:schema-Pagefor an unknown key in a page dictionary. PDF permits private and future keys, so these are usually unsupported rather than violations. They are violations only where PDF/A itself forbids unlisted keys.pikepdf:internal: an unexpected error while checking. Problems with the document never raise fromcheck; they become findings like this one.
report.summary() gives a readable description, listing up to 20 findings by
default:
PDF/A-2b: not_checked (0 violation(s), 5 unsupported)
[unsupported] pikepdf:colour-space at obj 8 0 (Page) content cs /Pattern: pattern colour is not supported
[unsupported] pikepdf:pattern at obj 8 0 (Page) content: scn with a pattern /p5
[unsupported] pikepdf:shading at obj 8 0 (Page) content: shadings (sh) are not supported
[unsupported] pikepdf:schema-Resources at obj 2 0 (Resources) /Pattern: patterns are not supported
[unsupported] pikepdf:schema-Resources at obj 2 0 (Resources) /Shading: shadings are not supported
Output intents
PDF/A requires an output intent, an ICC profile that says how device colours
are to be interpreted. prepare and save install one, replacing any output
intents the document had:
output_intent='sRGB'(the default) installs the sRGB profile shipped with pikepdf.output_intent=<bytes>installs the ICC profile you supply: an RGB, CMYK or gray output profile. PDF/A-1 requires an ICC version 2 profile; PDF/A-2 and PDF/A-3 accept up to version 4. An unusable profile raisesValueErrorbefore the document is changed.output_intent=Nonekeeps the document’s output intents as they are.
output_condition_identifier sets the intent’s /OutputConditionIdentifier.
It defaults to 'sRGB' for the built-in profile, and otherwise to the
profile’s description.
with open('press.icc', 'rb') as f:
icc = f.read()
with pikepdf.open('input.pdf') as pdf:
report = pdfa.save(
pdf, 'output.pdf', '2b',
output_intent=icc, output_condition_identifier='Press',
)
print(report.output_intent) # 'CMYK' for a CMYK profile
report.output_intent is the colour space of the document’s PDF/A output
intent ('RGB', 'CMYK' or 'GRAY') as the validator found it, and
result.output_intent on a PrepareResult is the colour space of the intent
that was requested. The validator checks that device colours used in the
document are compatible with it; it does not convert them.
Save settings
Some pikepdf.Pdf.save() settings change the written bytes in ways that
would make a verdict meaningless, or produce files PDF/A forbids. The PDF/A
functions therefore pin them, and refuse a conflicting value with
ValueError. Passing a pinned setting with its pinned value is allowed.
Setting |
Pinned value |
Reason |
|---|---|---|
|
|
the PDF/A version constraints must be kept |
|
|
PDF/A forbids encryption |
|
|
QDF mode writes uncompressed, annotated output |
|
|
it rewrites content streams after the check |
|
|
the validator evaluates filters as qpdf rewrites them at this level |
|
|
it rewrites the XMP packet after the check |
For PDF/A-1, which is based on PDF 1.4, two more are pinned:
Setting |
Pinned value |
Reason |
|---|---|---|
|
|
PDF 1.4 has no object streams |
|
|
PDF/A-1 is based on PDF 1.4 |
fix_metadata_version defaults to true in pikepdf.Pdf.save(), where it
updates the PDF version recorded in the XMP metadata while saving. That is a
change to the XMP packet made after the check, so the verdict would describe
different metadata from what was written. It is pinned to false, and prepare
writes the metadata the flavour needs instead.
The remaining settings are yours to choose: compress_streams,
recompress_flate, linearize, progress, deterministic_id, static_id,
min_version, and for PDF/A-2 and PDF/A-3 object_stream_mode and
force_version. Versions may not exceed 1.4 for PDF/A-1 or 1.7 for PDF/A-2
and PDF/A-3. An Adobe extension level, such as min_version=('1.7', 8), is
accepted for PDF/A-2 and PDF/A-3 and refused for PDF/A-1, since PDF 1.4 has
none. An unknown keyword raises TypeError.
qpdf rewrites the catalog’s Adobe developer extensions (/Extensions /ADBE)
whenever it saves, to match the version and extension level it writes: the
entry is kept or updated when the extension level is nonzero, and removed when
it is zero, which it always is for PDF/A-1. The check predicts this, so a file
saved by Acrobat with /ADBE extension level 8 passes, and its PDF/A-1
output carries no /ADBE entry.
pikepdf.pdfa.resolve_save_kwargs() returns the complete settings for a
flavour, with your choices applied, and report.save_kwargs holds the settings
a verdict assumed:
>>> kwargs = pdfa.resolve_save_kwargs('2b', linearize=True)
>>> kwargs['linearize'], kwargs['fix_metadata_version']
(True, False)
>>> pdfa.resolve_save_kwargs('2b', fix_metadata_version=True)
Traceback (most recent call last):
...
ValueError: fix_metadata_version=True is not allowed for PDF/A-2b: it must be False because it rewrites the XMP packet after the check
pdf.save(path, **kwargs) works directly with the result.
Attachments in PDF/A-3
PDF/A-3 permits files of any kind to be embedded, provided each is an
associated file: its file specification states its MIME type and its
relationship to the document (/AFRelationship), and is listed in an /AF
array. pikepdf.Pdf.attachments writes all of these, so a file attached
through it can be saved as PDF/A-3b:
import pikepdf
from pikepdf import AttachedFileSpec, Name, pdfa
with pikepdf.open('in.pdf') as pdf:
pdf.attachments['invoice.xml'] = AttachedFileSpec.from_filepath(
pdf, 'invoice.xml', relationship=Name.Data
)
pdfa.save(pdf, 'out.pdf', '3b')
prepare (and so save) supplies what an attachment made some other way
lacks, as described in What prepare changes. The validator checks the file
specifications of the /EmbeddedFiles name tree and of the catalog’s /AF
array, and their embedded file streams. It does not look inside the embedded
files, which PDF/A-3 does not require to be PDF/A.
PDF/A-1 forbids embedded files. PDF/A-2 permits them only if they are
themselves PDF/A-1 or PDF/A-2 files, which the validator does not check, so an
attachment makes a PDF/A-2 document 'not_checked'.
What prepare changes
prepare changes structure and metadata, never page content. It:
replaces the output intents with a single PDF/A output intent, unless the requested intent is already the only one, or
output_intent=None;removes
/Interpolatefrom images;removes annotations that are hidden, invisible or not viewable (the Hidden, Invisible, NoView and ToggleNoView flags), along with their popup annotations, and sets the Print flag on the annotations that remain (see below to run this step on its own);
removes
/AlternatePresentations(slideshows) and/JavaScript(document-level JavaScript) from the/Namesdictionary;adds a
/CIDSetto subset CIDFonts (PDF/A-1 only);gives the encoding of a non-symbolic TrueType font that has
/Differencesbut no/BaseEncodingthe base encoding/WinAnsiEncoding(PDF/A-2 and PDF/A-3 only). Without one, the encoding is based on StandardEncoding, which PDF/A does not permit. The font is changed only if every character code shown in it is either assigned by/Differencesor has the same glyph name in both encodings, so no glyph drawn changes; if the content streams cannot be read completely, no font is changed;for embedded files (PDF/A-3 only), gives an embedded file without a MIME type the MIME type
application/octet-stream, gives a file specification without/AFRelationshipthe value/Unspecified, and lists an embedded file that no/AFarray lists in the catalog’s/AFarray;rewrites the XMP metadata to keep only the properties predefined for the flavour, replacing a packet that cannot be read. The properties of every
rdf:Descriptionare kept when all of them have the samerdf:about, even a non-empty one such asuuid:...(pikepdf’s own metadata editor writes one); when theirrdf:aboutvalues differ, only the Descriptions with an emptyrdf:aboutare kept. The rewritten packet has an emptyrdf:about;gives document dates that have no time zone the local time zone of the machine running pikepdf;
declares PDF/A conformance in the XMP metadata, and sets the DocInfo entries that have XMP equivalents to agree with the XMP;
records pikepdf as the producer (
pdf:Producer) and the current time asxmp:MetadataDate.
pikepdf.pdfa.PrepareResult.describe() returns a sentence for each kind of
change, and pikepdf.pdfa.PrepareResult.messages() the same sentences
paired with a suggested log level: 'warning' for annotations removed,
'info' for Print flags set, XMP properties removed and an unreadable XMP
packet replaced (PrepareResult.xmp_problem says why it could not be read),
and 'debug' for the rest. PrepareResult.changed is false if nothing but the
producer and metadata date was updated.
import logging
log = logging.getLogger(__name__)
for level, sentence in report.prepared.messages():
log.log(logging.getLevelName(level.upper()), sentence)
Removing hidden annotations and pruning XMP properties discard information. If
that matters, inspect the PrepareResult or call check first.
The annotation step is also available on its own as
pikepdf.pdfa.repair_annotation_flags(). This is useful before handing a
file to another PDF/A converter such as Ghostscript, whose PDF/A mode silently
deletes every annotation that lacks the Print flag, including /Link
annotations that producers often write without any flags:
import pikepdf
from pikepdf import pdfa
with pikepdf.open('in.pdf') as pdf:
result = pdfa.repair_annotation_flags(pdf)
pdf.save('for-ghostscript.pdf')
Guarantees and limits
savevalidates the written bytes. The report it returns always describes the file at the destination. On failure it raisesPdfaError; an existing destination file is left unchanged and a new one is not created. A stream destination is written only on success. The cost is one extra parse of the output.checkmodels the writer. qpdf changes some things between the objects in memory and the bytes it writes: stream filters, the trailer, the PDF version, encryption, the cross-reference format, and which objects are written at all (unreachable objects are dropped).checkevaluates the document as it will be written, not as it is in memory.Write-time stream failures cannot be predicted. If a stream’s data cannot be decoded when qpdf writes it,
checkcannot know in advance.savecatches this when it validates the written file.Conversion mode does not matter. The validator and
preparegive the same results whether the document is read in implicit or explicit conversion mode, however that mode is chosen:pikepdf.explicit_conversion(), the document’sconversion_mode, orpikepdf.set_object_conversion_mode(). They work in explicit mode internally, switching to it for the calling thread only while they run.Files are replaced atomically where possible.
savewrites a temporary file beside the destination and renames it into place. An existing file keeps its permissions; a new file gets the default permissions of your umask. A destination that is a symbolic link is replaced by a regular file. If the destination’s directory is not writable, the temporary file is created elsewhere and the verified bytes are copied into place, which is not atomic. To save over the input file, open it withpikepdf.open(..., allow_overwriting_input=True)and pass the same path, orNone, as the destination.preparechanges the in-memory document; those changes remain whether or not a subsequentsavesucceeds.
Unsupported constructs
These constructs are reported as unsupported, so a document containing them
gets 'not_checked' (or 'fail', if something else is wrong). Most are allowed
in PDF/A under conditions the validator does not yet verify.
/OpenActionin the document cataloginteractive forms (
/AcroForm) and widget annotationsthe
/Namesdictionary entries other than named destinations (/Dests), named appearance streams (/AP), document-level JavaScript (/JavaScript, a violation),/AlternatePresentations(a violation in PDF/A-2 and PDF/A-3), and embedded files in PDF/A-3 (see Attachments in PDF/A-3)Type 3 fonts
optional content (
/OCProperties) and marked-content propertiespatterns and shadings
Separation and DeviceN colour spaces
JPEG 2000 (JPX) images
halftones
/Fontin graphics state parameter dictionaries (ExtGState)/CIDSetin TrueType-based CIDFont subsetsembedded files and associated files, other than PDF/A-3 embedded files listed in the
/EmbeddedFilesname tree or the catalog’s/AFarray; in particular file attachment annotations, and/AFarrays of pages, annotations, XObjects and structure elementsreferences to external files
annotation types other than Text, Link, Popup and the markup annotations
/Metadatastreams attached to objects other than the catalogXMP extension schemas and
xmpMMstructuresdeveloper extensions other than Adobe’s (
/ADBE) in the catalog’s/Extensions, and Adobe extensions to PDF versions after 1.7 (after 1.4 for PDF/A-1)output intents other than sRGB are accepted, but less exercised than sRGB
Threads
Independent pikepdf.Pdf objects may be checked, prepared and saved
concurrently from different threads. As with the rest of pikepdf, do not modify
one Pdf from several threads at once.
Credits
The validator and repairs were developed in OCRmyPDF, which needs to know whether its output is PDF/A before it can promise that it is, and moved to pikepdf so that other applications can use them. Because pikepdf owns the writer, it can validate the bytes that are actually written.
The rule catalogue is derived from the veraPDF validation profiles, Copyright © veraPDF Consortium, used under the Creative Commons Attribution 4.0 International License. The veraPDF Consortium does not endorse pikepdf or this validator.