PDF/A
PDF/A is the archival subset of PDF: self-contained files that will still render the
same way in twenty years. UniPDF implements it in model/pdfa as a set of profiles,
one per part and conformance variant. Every profile does two separate jobs. Handed to
a PdfWriter, it rewrites the document toward the standard as the file is written.
Handed a CompliancePdfReader, it checks an existing file and reports the rules it
breaks.
The pdfa package is documented as experimental, and its API may change in a minor
release.
Profiles
| Part | Profiles | Constructors | Base PDF version |
|---|---|---|---|
| PDF/A-1 (ISO 19005-1) | 1A, 1B | NewProfile1A, NewProfile1B | 1.4 |
| PDF/A-2 (ISO 19005-2) | 2A, 2B, 2U | NewProfile2A, NewProfile2B, NewProfile2U | 1.7 |
| PDF/A-3 (ISO 19005-3) | 3A, 3B, 3U | NewProfile3A, NewProfile3B, NewProfile3U | 1.7 |
| PDF/A-4 (ISO 19005-4) | 4, 4E, 4F | NewProfile4, NewProfile4E, NewProfile4F | 2.0 |
Every constructor takes a pointer to that part’s options struct and accepts nil,
which fills in the defaults. All of them satisfy pdfa.Profile, which is
model.StandardImplementer (ApplyStandard, ValidateStandard, StandardName) plus
Part() and Conformance().
Choosing a conformance level
For parts 1 to 3 the trailing letter is the conformance level, and it is cumulative:
| Level | Requires |
|---|---|
| B | Reliable visual reproduction. Fonts embedded, color unambiguous. |
| U | Everything in B, plus text that maps to Unicode through ToUnicode. |
| A | Everything in U, plus a tagged logical structure and reading order. |
Validation honors that hierarchy through the pdfaid:conformance check: a file marked
A passes the B and U checks, and a file marked U passes the B check. The reverse is
not true, which is why running one file against all the profiles of a part usually
reports failures for the stricter ones.
Part 4 drops the letters and uses a different scheme. Plain PDF/A-4 always requires
Unicode-mappable text but does not require tagging, so it sits roughly where 2U and 3U
did. 4E targets engineering content, and 4F targets files carrying embedded
attachments. There is no hierarchy between them: the pdfaid:conformance value has to
match the profile exactly, so a plain PDF/A-4 file always reports a violation of rule
6.7.3-3 when validated against Profile4E or Profile4F.
Picking a part is mostly about what the document contains. PDF/A-1 is the most restrictive and the most widely demanded by archives and regulators; it forbids transparency, layers, LZW and embedded files outright. PDF/A-2 lifts those restrictions, allowing transparency, optional content and JPEG 2000, and permits attachments as long as they are themselves PDF/A. PDF/A-3 is PDF/A-2 with that last restriction removed, which is the reason it exists: it is the part used for hybrid invoices such as ZUGFeRD and Factur-X, where an XML payload rides along inside the PDF. PDF/A-4 is built on PDF 2.0 and is the one to pick for new documents when nothing downstream insists on an older part.
Applying a standard
PdfWriter.ApplyStandard registers the profile. Nothing happens at that point; the
document is rewritten during Write, after the writer has assembled all its parts.
reader, file, err := model.NewPdfReaderFromFile(inputPath, nil)
if err != nil {
return err
}
defer file.Close()
pdfWriter, err := reader.ToWriter(nil)
if err != nil {
return err
}
pdfWriter.ApplyStandard(pdfa.NewProfile2B(nil))
return pdfWriter.WriteToFile(outputPath)What every profile’s applier does, regardless of part: raises the PDF version to the
part’s minimum, fills in CreationDate and ModDate on the document information
dictionary, switches the trailer to a content-hash-based /ID when the input has none,
writes an XMP packet carrying the pdfaid part and conformance, adds an output intent,
embeds fonts that were only referenced, rewrites annotation flags, strips forbidden
actions, and clears NeedAppearances on the AcroForm dictionary.
Three of those are worth knowing about before running it on a batch.
Font substitution reads the host system. When a font is referenced but not
embedded, the applier looks for a replacement among the fonts installed on the machine
doing the conversion, trying the original name first and then Times New Roman, Arial
and DejaVu Sans. Only .ttf and .ttc files are considered. If none of those turn up,
ApplyStandard fails with no matching font found in the system and the error
surfaces from Write. The substitute is not metrically matched to the original, so
line breaks can move. Fonts using Identity-H or Identity-V, and CIDFontType2
fonts that carry a CIDToGIDMap, are left alone.
Annotation flags are rewritten on every annotation on every page, not only on the ones that break a rule. The Print bit is set, and Invisible, Hidden, NoView and ToggleNoView are cleared. Annotations that were deliberately hidden become visible and printable in the output.
The output intent is only added when the catalog does not already have one. Which
intent gets added depends on the device color spaces the document uses, and when it
uses more than one, the applier converts everything to a single space first.
CMYKDefaultColorSpace in the options struct selects the target for that conversion:
false, the default, converts to RGB and adds an sRGB output intent, and true
converts to CMYK. Gray counts as present in every document, so any file that also uses
RGB or CMYK content takes this conversion path.
The rest of the options struct is small. Now overrides the clock, which is what you
want for reproducible output. Xmp is an XmpOptions value carrying copyright, the
document and instance identifiers, and the XMP marshaling indent.
Validating a standard
Validation needs a CompliancePdfReader rather than a plain PdfReader. It parses the
file with compliance mode set, which records the low-level syntactic detail the rules
about headers, trailers and cross-reference formatting depend on. A regular reader
throws that away and will not type-check here.
detailedReader, err := model.NewCompliancePdfReader(inputFile)
if err != nil {
return err
}
if err := pdfa.NewProfile2B(nil).ValidateStandard(detailedReader); err != nil {
fmt.Printf("not conforming: %v\n", err)
}A nil return means the profile found nothing wrong. Otherwise the error is a
pdfa.VerificationError, whose ViolatedRules field holds one ViolatedRule per
failed check, with the clause number in RuleNo and the requirement text in Detail.
Rules are sorted by clause number. The error’s Error() method prints the standard
name followed by every violation, which is what the examples show.
One reader can be validated against several profiles in turn, which is what the example
programs do. In v5, NewCompliancePdfReader takes an io.ReaderAt and derives the
size itself; NewCompliancePdfReaderAt is the explicit-size form for sources where
that fails.
Limitations
The implementation is explicitly experimental, and validation is not complete. A number
of clauses are present in the source as unimplemented placeholders, among them parts of
the object syntax checks, the rendering intent check, some interactive form appearance
rules, and the graphics state nesting and CID range implementation limits. A file that
passes ValidateStandard has passed the rules UniPDF checks, which is a subset of the
standard. For a conformance claim, run the output through an independent validator such
as veraPDF.
Applying a standard is best effort. It fixes what can be fixed by rewriting structure
and metadata, and it cannot invent content that was never there. The clearest case is
the A conformance levels: the applier sets MarkInfo Marked to true and inserts an
empty StructTreeRoot when the catalog has none, but it does not tag the page content.
An untagged document run through NewProfile1A or NewProfile2A comes out with the
catalog entries a tagged file would have and no tags. Genuine A-level conformance means
producing tagged content in the first place, which is what the
accessibility guides cover.
Applying a profile and then validating with the same profile is not a guarantee of a clean result, and is worth doing as a check rather than assuming.
Where to look
| Guide | Covers |
|---|---|
| PDF/A-1 | The most restrictive part. No transparency, no layers, no attachments. |
| PDF/A-2 | Transparency, optional content, JPEG 2000, PDF/A attachments. |
| PDF/A-3 | PDF/A-2 with arbitrary embedded files, for hybrid invoices. |
| PDF/A-4 | The PDF 2.0 part, with the 4E and 4F variants. |
For accessible tagged output and PDF/UA, see accessibility. For reducing file size, which is a separate concern from conformance, see PDF optimization.