An invoice archive can render cleanly, print, embed every font and carry a valid output intent, and a PDF/A validator will still reject it because one resource dictionary holds 4,096 entries. The ceiling is 4,095. PDF/A inherits a small table of implementation limits from the PDF Reference, and a file one unit over any of them fails conformance even though no viewer complains
The other failure that lands in the same "but it looks fine" bucket is a symbolic TrueType font that carries an /Encoding entry. Checkboxes, barcodes and dingbats come out right on screen, and the validator still flags the font dictionary. Both problems live in the object graph rather than in anything a person can see on the page, so they are worth checking mechanically before a file goes into an archive
The numbers PDF/A inherits, and where the boundary sits
PDFium Component checks four container limits on every PDF/A validation: a name token may be at most 127 bytes long, an array may hold at most 8,191 elements, a dictionary may hold at most 4,095 entries, and arrays and dictionaries may nest at most 28 levels deep. All four boundaries are inclusive. A 127-byte name passes and a 128-byte name fails; an array of exactly 8,191 elements passes and one more element fails it
The name, array and dictionary limits come from the era when a conforming reader was only required to handle structures up to those sizes. A current desktop viewer may open a far larger array without complaint, but an archival file is supposed to open in readers nobody has tested against yet, including old ones a records system may still be running. That is why PDF/A keeps the historical ceilings instead of relaxing them
Inclusive versus exclusive is where hand-rolled checks usually go wrong. A test written as Count >= 8191 instead of Count > 8191 rejects files that conform, and a validator that rejects conforming files tends to be believed anyway, so the generator gets "fixed" for a problem it never had. The function ValidatePdfACompliance in the FPdfPdfa unit counts strictly past the limit, and it reads the file bytes directly, so the check runs without loading the PDFium DLL
uses
System.Classes, FPdfPdfa;
procedure CheckContainerLimits(const FileName: string; Log: TStrings);
var
Src: TFileStream;
Res: TPdfAValidationResult;
begin
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfACompliance(Src); // byte scan, no PDFium DLL needed
if pvaiNameOverLimit in Res.Issues then
Log.Add('A name token is longer than 127 bytes');
if pvaiArrayOverLimit in Res.Issues then
Log.Add('An array holds more than 8191 elements');
if pvaiDictOverLimit in Res.Issues then
Log.Add('A dictionary holds more than 4095 entries');
if pvaiNestingOverLimit in Res.Issues then
Log.Add('Arrays and dictionaries nest deeper than 28 levels');
if Res.IsCompliant then
Log.Add('No PDF/A findings');
finally
Src.Free;
end;
end;
The four flags are members of the Issues set on TPdfAValidationResult, next to every other PDF/A finding, and IsCompliant returns True only when a PDF/A identification was found and the set is empty. A file that carries no PDF/A identification at all is checked against the strict PDF/A-1 rule set, so the container limits still apply to it
Which generators run into these limits?
Almost always the ones that build structure in a loop. A form with several thousand fields produces an AcroForm /Fields array, or a page /Annots array, that walks past 8,191 entries. A report engine that registers one font or image resource per generated object, and never reuses an existing entry, grows a /Resources sub-dictionary past 4,095 keys. A tagged document whose structure tree is produced by recursing over a nested data model can pass 28 levels without anyone looking
Long names have a different origin: data being packed into name tokens. A separation colorant named after a customer reference, an optional-content group named after a file path, a field label assembled from six levels of hierarchy. Names cost nothing to generate, and 127 bytes goes quickly once a UTF-8 label with accented characters is involved, because each of those characters takes two or three bytes. The 127 bytes are counted after the / introducer, up to the next delimiter or whitespace
The remedy is structural every time. Split the array, split the dictionary, flatten the recursion, shorten the name. Injecting PDF/A markers into the file cannot help with any of these findings, since the problem is the shape of the object graph and the XMP metadata says nothing about it
How the scan counts elements and entries
The counting rule matters when you try to reproduce a finding in a hex editor. A nested array or dictionary counts as one element of its parent, which is how the limit is stated: a 10-element array of 1,000-element arrays is fine, because no single array exceeds 8,191. Inside a dictionary only the keys are counted, so a value that happens to be a name does not inflate the entry total. Hex strings such as <FEFF0041> are skipped as single tokens, so their angle brackets are never mistaken for dictionary nesting
The scan covers the object structure of the whole file, including objects stored inside compressed object streams, which are expanded before counting. Stream bodies themselves, such as page content and image data, are blanked out first, so a run of binary bytes that happens to contain a / or a [ cannot trigger a false name or array finding
There is an adjacent numeric limit that the same validation reports under its own flag. When the file declares PDF/A-2 or a later part, pvaiIntegerImplementationLimit is raised for any integer token outside the 32-bit signed range, that is outside -2,147,483,648 to 2,147,483,647. Generators that write byte offsets or large counters as plain integers are the usual source
Why must a symbolic TrueType font drop /Encoding?
ISO 19005-1 §6.3.7 admits only the font program's built-in cmap as the character mapping for a symbolic TrueType font. A symbolic font maps codes to glyphs on its own terms; that is what the Symbolic flag declares. Add an /Encoding entry and the file now gives two answers to the question "which glyph does byte 0x41 select", with nothing in the file saying which one wins. Readers resolve the conflict differently, and the same checkbox can come out as a tick in one viewer and as the letter A in another
ValidatePdfACompliance finds every font dictionary with /Subtype /TrueType, checks whether it has an /Encoding key, and then reads bit 3 (value 4) of the /Flags entry in the font descriptor. The descriptor is followed whether it is written inline or referenced as an indirect object. A non-symbolic TrueType font that keeps /WinAnsiEncoding or /MacRomanEncoding is not flagged by this check, because for non-symbolic fonts PDF/A expects exactly that encoding
// Res comes from ValidatePdfACompliance as in the first example
if pvaiSymbolicTrueTypeEncoding in Res.Issues then
Log.Add('A symbolic TrueType font carries /Encoding; PDF/A admits only ' +
'the font''s built-in cmap (ISO 19005-1 6.3.7)');
// A neighbouring font rule with a different trigger: a NON-symbolic
// TrueType font whose Encoding dictionary carries /Differences
if pvaiTrueTypeDifferencesForbidden in Res.Issues then
Log.Add('A non-symbolic TrueType Encoding dictionary carries /Differences');
The fonts that trip this rule are predictable: Symbol, Wingdings, barcode fonts, icon fonts. They show up in business documents as checkboxes, rating stars and barcodes, and a subsetting step that treats every TrueType font the same way writes an encoding for them too. They are also the fonts nobody looks at when a validator says "font problem", because the body text fonts get checked first
The opposite mistake also appears in the wild: a producer sets the Symbolic flag on an ordinary text font that really is WinAnsi-encoded. The PDF/A check follows the clause as written and reports that font, and here the repair belongs in the descriptor: correct the flag in the descriptor rather than delete the encoding. Removing /Encoding from a text font that was mislabelled would leave the text dependent on whatever cmap the subset happens to contain, which is a worse file than the one you started with. Look at the font's glyph set before choosing which half of the contradiction to remove
Reading the findings in a preflight report
The preflight report unit ships with the Enterprise edition, while ValidatePdfACompliance is in every edition. The report in the FPdfPreflightReport unit sorts the four container limits into the Structure category and the symbolic TrueType encoding issue into the Content category. That split is deliberate. Structure findings usually go back to whoever maintains the generator, while content findings usually go to whoever supplied the fonts and templates, and a report that mixes them sends both people chasing problems that are not theirs
Each issue carries its enum name as Code and a recommendation that names the remedy in concrete terms, for example "Split dictionaries so none carries more than 4095 entries". Note that the Structure category also holds two other findings, a missing tagged structure on a Level A claim and PDF 1.5 cross-reference or object streams in a PDF/A-1 file, so filter by code when you only want the four limits
uses
System.SysUtils, System.Classes, PDFium, FPdfPreflightReport;
procedure ReportStructureFindings(const FileName: string; Log: TStrings);
var
Pdf: TPdf;
Report: TPdfPreflightReport;
Issues: TPdfPreflightIssues;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // this path loads the PDFium DLL
if not Pdf.Active then
Exit;
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA]);
Issues := Report.FilterByCategory('Structure').FlattenIssues;
for I := 0 to High(Issues) do
Log.Add(Issues[I].Code + ': ' + Issues[I].Recommendation);
Report.SaveJsonToFile(ChangeFileExt(FileName, '.preflight.json'));
finally
Pdf.Free;
end;
end;
The two paths answer slightly different questions. BuildPdfPreflightReport goes through TPdf.ValidatePdfA, which saves the open document to a temporary copy, runs the same byte scan on that copy and then adds checks that need the PDFium engine, such as fonts that are used on a page but not embedded. ValidatePdfACompliance judges the exact bytes on disk. For implementation limits, the byte path on the original file is the one to trust: it reports on the file you are about to archive, and it runs in a build step or a locked-down service where a native DLL cannot be deployed. Use the report path when you also want the engine-level checks and the JSON, SARIF or JUnit output for a pipeline
Where to go from here
Running these checks across a folder, deciding which findings block a release and turning the result into a report people will read are covered in the walkthrough of PDF/A preflight validation in Delphi and in the batch preflight report CLI. Which PDF/A part and conformance level to target in the first place is the subject of the notes on PDF/A archival compliance; that choice decides, among other things, whether the integer limit applies
The validators shown here ship with PDFium Component for Delphi, C++Builder and Lazarus, alongside rendering, form filling and editing on top of the PDFium engine. The PDFium Component product page lists the supported conformance standards, editions and compiler versions