Technisch artikel

PDF 2.0-, PDF/A-4- en PDF/UA-2-uitvoer met HotPDF in Delphi

Een PDF/A-3-checklist zegt meestal dat het XMP-pakket twee identificatie-entries moet dragen, pdfaid:part en pdfaid:conformance. Een correct geschreven PDF/A-4-bestand draagt pdfaid:part 4 en pdfaid:rev 2020, en voor het basisprofiel is er helemaal geen conformance-entry. Een letter toevoegen om het te "repareren" maakt het bestand fout: ISO 19005-4 identificeert het basisdeel op nummer en revisiejaar, en alleen de twee extensies houden een letter over

PDF/UA-2 heeft één niveau lager een vergelijkbare valkuil, binnen de structuurboom. Een eigen rol declareren in een eigen namespace lijkt de hele klus, want namespaces zijn precies wat deel 2 als feature toevoegde. Het is de helft. Een rol als WidgetGroup in https://example.com/ns/widgets moet nog steeds naar een standaardrol mappen voordat een conformerende checker haar accepteert, en niets op de gerenderde pagina hint dat de mapping ontbreekt

PDF/A-4 kiezen met PDFACompliance

In HotPDF zijn de PDF/A-4-profielen drie waarden van de bestaande property PDFACompliance: '4' voor het basisdeel, '4E' voor technisch documenten en '4F' voor bestanden die embedded files meedragen. Met '4' gekozen krijgt het XMP-pakket pdfaid:part 4 en pdfaid:rev 2020 en verder niets uit de pdfaid-namespace. '4E' en '4F' voegen pdfaid:conformance met E of F toe. De decoderfuncties melden dezelfde splitsing: PDFAPart geeft 4 terug voor alle drie de waarden, en PDFAConformanceLevel geeft E, F of #0 voor het basisprofiel

Matrix van PDF/A-4-selectiewaarden in HotPDF met basis 4 zonder conformiteitsletter naast 4E engineering en 4F embedded-file-profielen boven gedeelde PDF 2.0-constraints
basis '4' schrijft part 4 rev 2020 zonder enige letter; alleen 4E en 4F houden er een

Alle drie forceren een PDF 2.0-header. De upgrade gebeurt binnen EndDoc, dus een document dat op PDF 1.4 begon komt als 2.0 uit de bus zonder aparte aanroep. Het ene geval dat dat tegenhoudt is StrictVersionLock. Die vlag verbiedt automatische upgrades, dus EndDoc geeft "PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade" in plaats van een 1.7-bestand weg te schrijven dat in zijn metadata deel 4 claimt. Een codebase die de versie vastzet voor een oude afdrukworkflow ziet precies die melding zodra iemand voor het eerst het profiel omschakelt

Twee PDF/A-4-regels veranderen wat er in de trailer en de Catalog belandt. ISO 19005-4 §6.1.3 staat een document information dictionary alleen toe wanneer de Catalog een PieceInfo-entry heeft, en beperkt haar zelfs dan tot ModDate, dus HotPDF schrijft voor deel 4 geen Info-dictionary en de titel reist als dc:title in XMP. Title en Lang blijven verplicht. Laat een van beide leeg en EndDoc geeft een exception. Die komt er ook wanneer er geen output intent met AddPDFAOutputIntent is toegevoegd, en EnableEncrypt weigert te draaien zolang een PDF/A-profiel is gekozen, want een versleuteld archiefbestand is een contradictie die de standaard niet toestaat

Associated files onder PDF/A-4f en 4e

Associated files zijn de voornaamste reden om PDF/A-4f te kiezen: een XML-factuur achter de gerenderde pagina, een JSON-payload die een stroomafwaarts systeem parseert. AddPDFAssociatedFile neemt een bestandsnaam, een MIME-type, een beschrijving, een AFRelationship-waarde en de bytes, en geeft de indirecte FileSpec-dictionary terug zodat u optionele sleutels van uzelf kunt toevoegen

var
  Pdf: THotPDF;
  Icc: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  Icc := TFileStream.Create('sRGB2014.icc', fmOpenRead or fmShareDenyWrite);
  try
    Pdf.FileName := 'invoice-archive.pdf';
    Pdf.PDFACompliance := '4F';        // part 4, rev 2020, conformance F
    Pdf.Title := 'Invoice 2026-0731';  // alleen naar XMP dc:title geschreven
    Pdf.Lang := 'en-US';
    Pdf.BeginDoc;
    Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', 'sRGB IEC61966-2.1',
      Icc, 3, 'DeviceRGB');
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
    // LoadInvoiceBytes is uw eigen functie die TBytes teruggeeft
    Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
      'Structured invoice data', 'Data', LoadInvoiceBytes);
    Pdf.EndDoc;
  finally
    Icc.Free;
    Pdf.Free;
  end;
end;

Elke aanroep doet vier dingen. Ze schrijft een EmbeddedFile-stream waarvan de /Subtype het MIME-type als PDF-naam bevat, met een /Params-dictionary met daarin de grootte, een wijzigingsdatum en een MD5-checksum. Ze bouwt een FileSpec met zowel /F als /UF, de beschrijving als /Desc, en de relatienaam. Daarna registreert ze die FileSpec tweemaal, in de /AF-array van de Catalog en in de EmbeddedFiles-naamboom. Beide registraties zijn verplicht. Een bestand dat op maar één plek is geregistreerd, kan nog steeds openen vanuit de bijlagenlijst van een viewer, en daarom bewijst een visuele steekproef heel weinig over een archief

De relatiestring moet een van Source, Data, Alternative, Supplement of Unspecified zijn; al het andere geeft een exception voordat er ook maar een byte is weggeschreven, en een leeg MIME-type doet datzelfde. Ook het profiel wordt gecontroleerd. AddPDFAssociatedFile accepteert PDF/A-3, PDF/A-4e en PDF/A-4f, en het basisprofiel '4' geeft een exception omdat hij geen associated files toestaat. In de omgekeerde richting weigert AddDocumentAttachment te draaien onder PDF/A-3, 4e en 4f, want een kale bijlage mist de /AF-registratie die die profielen nodig hebben. Code die voor PDF/A-3 tegen AddPDFA3AssociatedFile is geschreven, compileert gewoon en gedraagt zich hetzelfde; AddPDFAssociatedFile is de profielneutrale naam voor die methode

De twee extensies verdelen de overgebleven permissies tussen elkaar. Onder '4F' zorgt EndDoc ervoor dat de Catalog een EmbeddedFiles-naamboom draagt, en AddFileAttachmentAnnotation wordt geaccepteerd, terwijl het basisprofiel en 4e haar afwijzen. Onder '4E' zijn Add3DAnnotation en AddRichMediaAnnotation toegestaan, samen met de acties SetOCGState en GoTo3DView. Elk ander PDF/A-profiel weigert alle vier

Waarom faalt een eigen rol met namespace alsnog voor PDF/UA-2?

Een eigen rol komt door PDF/UA-2 alleen wanneer twee voorwaarden gelden: zijn /NS-entry wijst naar een namespace-dictionary die in de structuurboomwortel is opgenomen, en de rol lost via de RoleMapNS van die namespace op naar een rol in een standaardnamespace. HotPDF verdeelt de klus over drie methodes. RegisterStructureNamespace maakt een indirecte /Type /Namespace-dictionary, voegt haar toe aan StructTreeRoot /Namespaces en geeft haar terug; een tweede aanroep met dezelfde URI geeft de bestaande dictionary terug in plaats van er nog een toe te voegen. AddStructureElementNS maakt het element en zet zijn /NS, onderweg registrerend. AddStructRoleMapNS schrijft de mapping

Diagram dat de platte rolwoordenschat van PDF/UA-1 afzet tegen PDF/UA-2-namespaces gebonden via RegisterStructureNamespace en AddStructureElementNS in Delphi
één URI hergebruikt altijd een enkele namespace-dictionary — geen dubbele stapels
const
  PDF2NS   = 'http://iso.org/pdf2/ssn';
  WidgetNS = 'https://example.com/ns/widgets';
var
  DocElem, Group: THPDFDictionaryObject;
begin
  Pdf.PDFUACompliance := True;
  Pdf.PDFUAPart := 2;                // PDF/UA-2 vereist PDF 2.0
  Pdf.Title := 'Widget catalog';     // lege Title geeft een exception bij EndDoc
  Pdf.Lang := 'en-US';
  Pdf.BeginDoc;

  DocElem := Pdf.AddStructureElement('Document', nil);  // gebonden aan PDF2NS
  Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
  // zonder deze mapping geeft EndDoc ua2.rolemap.unresolved
  Pdf.AddStructRoleMapNS('WidgetGroup', WidgetNS, 'Div', PDF2NS);

  Pdf.CurrentPage.SetFont('Arial', [], 18);
  Pdf.CurrentPage.BeginMarkedContentMCID('H1', 0);
  Pdf.CurrentPage.TextOut(50, 760, 0, 'Widgets');
  Pdf.CurrentPage.EndMarkedContent;
  Pdf.AddStructureElement('H1', Group, 0, 0);  // pagina-index 0, MCID 0
  Pdf.EndDoc;
end;

Een deel van die registratie gebeurt ongevraagd. Met PDFUAPart op 2 bindt AddStructureElement('Document', nil) het element aan de PDF 2.0-standaardnamespace http://iso.org/pdf2/ssn, want de wortel van een PDF/UA-2-boom moet een Document in die namespace zijn. Dezelfde binding geldt voor elke rol in de PDF 2.0-woordenschat, dus H1 en P hebben geen namespace-argument nodig. Rollen die alleen in de PDF 1.7-set bestaan, zoals Note, Code of BlockQuote, blijven in de 1.7-namespace

Het mappingdoel is een echte keuze. Een lege doelnamespace aan AddStructRoleMapNS geven schrijft de korte vorm, een kale rolnaam opgelost tegen de PDF 1.7-set. De PDF 2.0-URI meegeven schrijft de vorm met twee items, [rol namespace]. Map in een PDF/UA-2-document naar de PDF 2.0-namespace: de hele boom lost dan in één woordenschat op, en doelen als Aside of FENote, die geen 1.7-tegenhanger hebben, komen beschikbaar. Twee mappingvormen worden bij EndDoc ronduit afgewezen: een keten die in zichzelf terugloopt, en een rol in een expliciete namespace die op een andere rol in diezelfde namespace mapt

De aanroepvolgorde is losser dan hij eruitziet. De mapping kan vóór of ná het ontstaan van het element worden toegevoegd, want niets controleert haar totdat de boom af is. Wat wél meteen faalt is AddStructureElementNS terwijl PDFUAPart 1 is. Deel 1 heeft geen namespaces, en de aanroep geeft ter plekke "structure namespaces require PDFUAPart=2"

Wat EndDoc controleert voordat het bestand wordt weggeschreven

Met PDFUACompliance aan en PDFUAPart op 2 draait EndDoc een volledige PDF/UA-2-structuurvalidatie voordat er iets wordt geserialiseerd, en de eerste fout wordt een exception. Vier van zijn controles bewaken het skelet van de boom. De wortel moet exact één Document-element op topniveau bevatten. Elke namespace-dictionary moet indirect zijn, getypeerd Namespace, en een niet-lege URI dragen die slechts een keer voorkomt. Elke /NS-verwijzing moet oplossen naar een dictionary die in de /Namespaces-array van de wortel staat. En elke rol moet oplossen naar een standaardnamespace, direct of via RoleMap en RoleMapNS. Elementen die vanuit het Document-element niet bereikbaar zijn, worden ook gemeld

Stroom van vier structuurboompoorten tussen de controles die HotPDF bij EndDoc draait: één Document-wortel, schone namespace-dictionaries, oplosbare NS-verwijzingen, standaard of gemapte rollen
de poorten eerder afvangen zou geldige tussenstanden afwijzen

Dezelfde pas dekt de vereisten op documentniveau: een niet-lege, geldige Lang; MarkInfo /Marked op true; DisplayDocTitle aan; en een XMP-pakket met een niet-lege dc:title en pdfuaid:part 2. HotPDF zet de marked-vlag, de viewerpreferentie en de XMP-identificatie zelf zodra PDF/UA aan staat, en schrijft pdfuaid:rev 2024 naast het deelnummer. Titel en taal bent u zelf om aan te leveren, en een lege waarde voor een van beide geeft een exception voordat de validator überhaupt begint. Koppen, lijsten, tabellen, annotaties en formulieren worden in dezelfde run gecontroleerd

Wachten tot EndDoc is met opzet. Halverwege de generatie is een boom legitiem incompleet: het Document-element heeft misschien nog geen kinderen, een namespace kan zijn geregistreerd voordat een element haar gebruikt, een mapping kan na zijn element binnenkomen. Bij elke aanroep controleren zou die toestanden afwijzen. Bij EndDoc is de boom af en nog in het geheugen, en de exception benoemt een regelcode en het pad van het element dat haar brak

try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    // E.Message heeft de vorm
    //   PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
    //   Heading level skips from H2 to H4
    LogBuildFailure(Pdf.FileName, E.Message);  // uw eigen logger
    raise;
  end;
end;

PDF/A-4-uitvoer krijgt dezelfde behandeling van een eigen validator. Wanneer PDFACompliance een deel-4-waarde bevat, toetst EndDoc de gegenereerde graaf aan ISO 19005-4 en geeft een exception met de regelcode en de clausule, bijvoorbeeld pdfa4.filespec.relationship voor een FileSpec zonder geldige relatie of pdfa4.trailer.info_entries voor een verdwaalde Info-sleutel. De Enterprise-editie stelt beide validators bloot als ValidatePDFUA2Structure en ValidatePDFA4, die een issues-array vullen met elke vastgehouden diagnose in plaats van bij de eerste fout te stoppen. Voor een CI-stap die een complete lijst moet printen, is dat het betere ingangspunt

PDF 2.0-rollen en koppen voorbij H6

De getypeerde enum THPDFStandardStructureType wint sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong en sstArtifact. Drie ervan veranderen hoe gewone zakelijke documenten worden getagd. Aside geeft zijbalken en uitgelichte citaten een thuis die geen misbruikte Sect is. FENote markeert voet- en eindnoten als noten, zodat een schermlezer ze op verzoek kan aanbieden in plaats van ze halverwege een zin voor te lezen. Em en Strong dragen nadruk als structuur, waar een PDF 1.7-boom haar hooguit in een Span kon vangen

Koppen zijn openeindig in PDF 2.0. De enum stopt bij sstH6, maar de string-overload van AddStructureElement accepteert H7, H8 en verder, want de PDF 2.0-set definieert Hn voor elke n vanaf 1. PDF 1.7 stopte bij H6, wat diepe technische documenten dwong hun hiërarchie af te vlakken of niveaus te hergebruiken. Voor standaarddocumenten, wetboeken en onderdeelcatalogi kan dat alleen al rechtvaardigen om de uitvoer naar PDF 2.0 te verhuizen

Diepere koppen komen met een strengere regel. De PDF/UA-2-pas binnen EndDoc eist dat kopniveaus opeenvolgend zijn: de eerste kop moet H1 zijn, en een sprong van H2 naar H4 geeft een exception met ua2.heading.skipped. Een hiërarchie mag alleen lager beginnen binnen een DocumentFragment, de rol voor content die uit een groter document is geëxtraheerd. Een generator die een Markdown- of HTML-hiërarchie rechtstreeks op Hn-rollen mapt, erft elk niveau dat de auteur voor de visuele grootte oversloeg, dus normaliseer de hiërarchie voordat u haar tagt

Voordat u productie-uitvoer omschakelt naar PDF 2.0

PDF 2.0 begint met een headerwijziging, en de header is wat oudere verbruikers eerst lezen. Een ingestservice, afdrukworkflow of bedrijfsviewer die slechts tot PDF 1.7 accepteert, stopt bij %PDF-2.0, hoe schoon de rest van het bestand ook is. Elk PDF/A-4-profiel of PDF/UA-deel 2 kiezen, kiest PDF 2.0 of u er nu om vroeg of niet, dus bevestig elk verbruikend systeem vóór de omschakeling, niet erna

De uitrol met het laagste risico houdt PDF/A-3 aan voor documenten die naar lezers gaan die u niet beheert, gebruikt PDF/A-4f voor interne archieven waarvan u de ingest-zijde beheert, en neemt PDF/UA-2 aan waar een toegankelijkheidsbeleid die noemt. StrictVersionLock doet tijdens de overgang tevens dienst als wachter: zet Version op pdf17 en de lock in de codepaden die op 1.7 moeten blijven, en een per ongeluk gekozen PDF/A-4 faalt bij EndDoc met een melding in plaats van een systeem te bereiken dat het bestand niet kan openen. De gidsen over PDF/A-, PDF/X- en PDF/UA-validatie en over ZUGFeRD- en Factur-X-hybride facturen op PDF/A-3 behandelen de profielbeslissingen die vóór het versienummer komen, en de notities over geautomatiseerde preflight-rapportage laten zien hoe u het oordeel tot een bouwstap maakt

Dit alles draait als native VCL-code binnen de component voor Delphi en C++Builder, dus PDF/A-4- en PDF/UA-2-uitvoer reist mee met uw executable en heeft geen externe engine nodig. Ondersteunde profielen en RAD Studio-versies staan vermeld op de HotPDF Delphi-componentpagina