Articolo tecnico

Output PDF 2.0, PDF/A-4 e PDF/UA-2 con HotPDF in Delphi

Una checklist PDF/A-3 di solito dice che il pacchetto XMP deve portare due voci di identificazione, pdfaid:part e pdfaid:conformance. Un file PDF/A-4 scritto correttamente porta pdfaid:part 4 e pdfaid:rev 2020, e per il profilo base non c'è affatto una voce di conformance. Aggiungere una lettera per «sistemarlo» rende il file sbagliato: ISO 19005-4 identifica la parte base con numero e anno di revisione, e solo le due estensioni conservano una lettera

PDF/UA-2 ha una trappola simile un livello sotto, dentro l'albero di struttura. Dichiarare un ruolo personalizzato nel proprio namespace sembra tutto il lavoro, visto che i namespace sono la funzionalità che la parte 2 ha aggiunto. Ne è metà. Un ruolo come WidgetGroup in https://example.com/ns/widgets deve comunque mappare a un ruolo standard prima che un checker conforme lo accetti, e nulla nella pagina renderizzata accenna che la mappatura manca

Selezionare PDF/A-4 con PDFACompliance

In HotPDF i profili PDF/A-4 sono tre valori della già esistente proprietà PDFACompliance: '4' per la parte base, '4E' per i documenti tecnici e '4F' per i file che portano file incorporati. Con '4' selezionato, il pacchetto XMP riceve pdfaid:part 4 e pdfaid:rev 2020 e nient'altro dal namespace pdfaid. '4E' e '4F' aggiungono pdfaid:conformance con E o F. Le funzioni di decodifica riportano la stessa ripartizione: PDFAPart restituisce 4 per tutti e tre i valori, e PDFAConformanceLevel restituisce E, F, oppure #0 per il profilo base

Matrice dei valori di selettore PDF/A-4 in HotPDF che mostra la base 4 senza lettera di conformità accanto ai profili 4E engineering e 4F embedded-file sopra vincoli PDF 2.0 condivisi
la base '4' scrive parte 4 rev 2020 senza alcuna lettera; solo 4E e 4F la conservano

Tutti e tre forzano un'intestazione PDF 2.0. L'aggiornamento avviene dentro EndDoc, quindi un documento partito a PDF 1.4 esce come 2.0 senza una chiamata separata. L'unico caso che lo ferma è StrictVersionLock. Quel flag vieta gli aggiornamenti automatici di versione, quindi EndDoc solleva «PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade» anziché scrivere un file 1.7 che rivendica la parte 4 nei suoi metadati. Una codebase che blocca la versione per un vecchio flusso di stampa vedrà esattamente quel messaggio la prima volta che qualcuno commuta il profilo

Due regole PDF/A-4 cambiano cosa atterra nel trailer e nel Catalog. ISO 19005-4 §6.1.3 ammette un dizionario information del documento solo quando il Catalog ha una voce PieceInfo, e anche lì lo limita a ModDate, quindi HotPDF non scrive alcun dizionario Info per la parte 4 e il titolo viaggia come dc:title in XMP. Title e Lang restano obbligatori. Lasciate vuoto uno dei due e EndDoc solleva. Solleva anche quando non è stato aggiunto alcun output intent con AddPDFAOutputIntent, e EnableEncrypt rifiuta di partire mentre un profilo PDF/A è selezionato, perché un file archivistico cifrato è una contraddizione che lo standard non ammette

File associati sotto PDF/A-4f e 4e

I file associati sono la ragione principale per scegliere PDF/A-4f: una fattura XML dietro la pagina renderizzata, un payload JSON che un sistema a valle analizza. AddPDFAssociatedFile prende un nome file, un tipo MIME, una descrizione, un valore AFRelationship e i byte, e restituisce il dizionario FileSpec indiretto così potete attaccare chiavi opzionali vostre

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';        // parte 4, rev 2020, conformance F
    Pdf.Title := 'Invoice 2026-0731';  // scritto solo in XMP dc:title
    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 è una funzione vostra che restituisce TBytes
    Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
      'Structured invoice data', 'Data', LoadInvoiceBytes);
    Pdf.EndDoc;
  finally
    Icc.Free;
    Pdf.Free;
  end;
end;

Ogni chiamata fa quattro cose. Scrive uno stream EmbeddedFile il cui /Subtype contiene il tipo MIME come nome PDF, con un dizionario /Params che porta la dimensione, una data di modifica e un checksum MD5. Costruisce una FileSpec con sia /F sia /UF, la descrizione come /Desc, e il nome della relazione. Poi registra quella FileSpec due volte, nell'array /AF del Catalog e nel name tree EmbeddedFiles. Entrambe le registrazioni sono obbligatorie. Un file registrato in un solo posto può ancora aprirsi dalla lista allegati di un visualizzatore, ecco perché un controllo a campione visivo prova ben poco su un archivio

La stringa di relazione deve essere una di Source, Data, Alternative, Supplement o Unspecified; qualsiasi altra cosa solleva prima che un byte sia scritto, e così un tipo MIME vuoto. Viene controllato anche il profilo. AddPDFAssociatedFile accetta PDF/A-3, PDF/A-4e e PDF/A-4f, e il profilo base '4' solleva perché non ammette file associati. Nella direzione opposta, AddDocumentAttachment rifiuta di partire sotto PDF/A-3, 4e e 4f, dato che un allegato semplice manca della registrazione /AF che quei profili richiedono. Il codice scritto per PDF/A-3 contro AddPDFA3AssociatedFile continua a compilare e si comporta uguale; AddPDFAssociatedFile è il nome profile-neutral di quel metodo

Le due estensioni si spartiscono i permessi rimanenti. Sotto '4F', EndDoc si assicura che il Catalog porti un name tree EmbeddedFiles, e AddFileAttachmentAnnotation è accettata, mentre il profilo base e 4e la rifiutano. Sotto '4E', Add3DAnnotation e AddRichMediaAnnotation sono permesse, insieme alle azioni SetOCGState e GoTo3DView. Ogni altro profilo PDF/A rifiuta tutte e quattro

Perché un ruolo personalizzato con namespace fallisce comunque PDF/UA-2?

Un ruolo personalizzato passa PDF/UA-2 solo quando valgono due condizioni: la sua voce /NS punta a un dizionario namespace elencato nella radice dell'albero di struttura, e il ruolo si risolve attraverso la RoleMapNS di quel namespace verso un ruolo in un namespace standard. HotPDF sparge il lavoro su tre metodi. RegisterStructureNamespace crea un dizionario indiretto /Type /Namespace, lo aggiunge a StructTreeRoot /Namespaces e lo restituisce; una seconda chiamata con lo stesso URI restituisce il dizionario esistente invece di aggiungerne un altro. AddStructureElementNS crea l'elemento e imposta il suo /NS, registrando il namespace lungo la strada. AddStructRoleMapNS scrive la mappatura

Diagramma che contrappone il vocabolario di ruoli piatto di PDF/UA-1 ai namespace PDF/UA-2 vincolati tramite RegisterStructureNamespace e AddStructureElementNS in Delphi
un URI riusa sempre un unico dizionario di namespace — nessun mucchio di duplicati
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 richiede PDF 2.0
  Pdf.Title := 'Widget catalog';     // un Title vuoto solleva a EndDoc
  Pdf.Lang := 'en-US';
  Pdf.BeginDoc;

  DocElem := Pdf.AddStructureElement('Document', nil);  // legato a PDF2NS
  Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
  // senza questa mappatura EndDoc solleva 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);  // indice pagina 0, MCID 0
  Pdf.EndDoc;
end;

Parte di quell'elenco avviene senza che lo chiediate. Con PDFUAPart impostato a 2, AddStructureElement('Document', nil) lega l'elemento al namespace standard PDF 2.0, http://iso.org/pdf2/ssn, perché la radice di un albero PDF/UA-2 deve essere un Document in quel namespace. Lo stesso legamento avviene per ogni ruolo del vocabolario PDF 2.0, quindi H1 e P non hanno bisogno di un argomento namespace. I ruoli che esistono solo nell'insieme PDF 1.7, come Note, Code o BlockQuote, restano nel namespace 1.7

Il target della mappatura è una scelta vera. Passare un namespace target vuoto a AddStructRoleMapNS scrive la forma corta, un nome ruolo nudo risolto contro l'insieme PDF 1.7. Passare l'URI PDF 2.0 scrive la forma a due elementi [role namespace]. In un documento PDF/UA-2, mappate al namespace PDF 2.0: l'intero albero allora si risolve in un unico vocabolario, e target come Aside o FENote, che non hanno corrispondente 1.7, diventano disponibili. Due forme di mappatura vengono rifiutate a prescindere a EndDoc, una catena che rimanda su se stessa e un ruolo in un namespace esplicito mappato a un altro ruolo in quello stesso namespace

L'ordine delle chiamate è più lasco di quanto sembri. La mappatura può essere aggiunta prima o dopo che l'elemento esista, perché nulla la controlla finché l'albero non è finito. Ciò che fallisce immediatamente è AddStructureElementNS mentre PDFUAPart è 1. La parte 1 non ha namespace, e la chiamata solleva «structure namespaces require PDFUAPart=2» sul posto

Cosa controlla EndDoc prima che il file sia scritto

Con PDFUACompliance acceso e PDFUAPart a 2, EndDoc esegue una validazione strutturale PDF/UA-2 completa prima di serializzare qualsiasi cosa, e il primo errore diventa un'eccezione. Quattro dei suoi controlli proteggono lo scheletro dell'albero. La radice deve contenere esattamente un elemento Document di primo livello. Ogni dizionario namespace deve essere indiretto, tipizzato Namespace, e portare un URI non vuoto che compare una volta sola. Ogni riferimento /NS deve risolvere a un dizionario elencato nell'array /Namespaces della radice. E ogni ruolo deve risolvere a un namespace standard, direttamente o attraverso RoleMap e RoleMapNS. Anche gli elementi irraggiungibili dall'elemento Document vengono riportati

Flusso dei quattro gate dell'albero della struttura che HotPDF controlla a EndDoc: radice Document unica, dizionari namespace puliti, riferimenti NS risolvibili, ruoli standard o mappati
intercettare i gate prima farebbe rifiutare stati intermedi validi

La stessa passata copre i requisiti a livello documento: un Lang non vuoto e valido; MarkInfo /Marked impostato a true; DisplayDocTitle acceso; e un pacchetto XMP con un dc:title non vuoto e pdfuaid:part 2. HotPDF imposta da sé il flag marked, la preferenza del visualizzatore e l'identificazione XMP una volta che PDF/UA è attivo, e scrive pdfuaid:rev 2024 accanto al numero di parte. Titolo e lingua sono roba che fornite voi, e un valore vuoto per uno dei due solleva prima ancora che il validatore parta. Intestazioni, liste, tabelle, annotazioni e form vengono controllati nella stessa esecuzione

Aspettare EndDoc è deliberato. A metà generazione un albero è legittimamente incompleto: l'elemento Document può non avere ancora figli, un namespace può essere registrato prima che un elemento lo usi, una mappatura può arrivare dopo il suo elemento. Controllare a ogni chiamata rifiuterebbe quegli stati. A EndDoc l'albero è finito e ancora in memoria, e l'eccezione nomina un codice di regola e il percorso dell'elemento che l'ha rotta

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

L'output PDF/A-4 riceve lo stesso trattamento dal suo validatore. Quando PDFACompliance contiene un valore di parte 4, EndDoc controlla il grafo generato contro ISO 19005-4 e solleva con il codice di regola e la clausola, per esempio pdfa4.filespec.relationship per una FileSpec senza una relazione valida o pdfa4.trailer.info_entries per una chiave Info smarrita. L'edizione Enterprise espone entrambi i validatori come ValidatePDFUA2Structure e ValidatePDFA4, che riempiono un array di problemi con ogni diagnostica trattenuta invece di fermarsi al primo errore. Per un passo di CI che deve stampare un elenco completo, quello è il punto d'ingresso migliore

Ruoli PDF 2.0 e intestazioni oltre H6

L'enum tipizzato THPDFStandardStructureType guadagna sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong e sstArtifact. Tre di loro cambiano come vengono taggati i normali documenti aziendali. Aside dà a barre laterali e citazioni in evidenza una casa che non è un Sect usato male. FENote marca note a piè di pagina e note di chiusura come note, così un lettore schermo può offrirle su richiesta invece di leggerle in mezzo a una frase. Em e Strong portano l'enfasi come struttura, dove un albero PDF 1.7 poteva solo avvolgerla in un Span

Le intestazioni sono aperte in PDF 2.0. L'enum si ferma a sstH6, ma l'overload stringa di AddStructureElement accetta H7, H8 e oltre, perché l'insieme PDF 2.0 definisce Hn per qualsiasi n da 1 in su. PDF 1.7 si fermava a H6, il che costringeva i documenti tecnici profondi ad appiattire la propria outline o a riutilizzare i livelli. Per documenti di standard, codici legali e cataloghi di parti, questo da solo può giustificare lo spostamento dell'output a PDF 2.0

Le intestazioni profonde arrivano con una regola più severa. La passata PDF/UA-2 dentro EndDoc richiede livelli di intestazione sequenziali: la prima intestazione deve essere H1, e un salto da H2 a H4 solleva con ua2.heading.skipped. Una gerarchia può partire più in basso solo dentro un DocumentFragment, il ruolo pensato per contenuto estratto da un documento più grande. Un generatore che mappa una outline Markdown o HTML dritta sui ruoli Hn eredita ogni livello che l'autore ha saltato per dimensione visiva, quindi normalizzate l'outline prima di taggarla

Prima di commutare l'output di produzione a PDF 2.0

PDF 2.0 comincia con un cambio di intestazione, e l'intestazione è ciò che i consumer più vecchi leggono per primi. Un servizio di ingest, un flusso di stampa o un visualizzatore line-of-business che accetta solo fino a PDF 1.7 si fermerà a %PDF-2.0 non importa quanto pulito sia il resto del file. Selezionare qualsiasi profilo PDF/A-4 o PDF/UA parte 2 seleziona PDF 2.0 che l'abbiate chiesto o no, quindi confermate ogni sistema consumatore prima della commutazione, non dopo

Il rollout a rischio più basso mantiene PDF/A-3 per i documenti che escono verso lettori che non controllate, usa PDF/A-4f per gli archivi interni dove l'ingest è vostro, e adotta PDF/UA-2 dove una policy di accessibilità lo nomina. StrictVersionLock fa anche da guardia durante la transizione: impostate Version a pdf17 e il blocco nei percorsi di codice che devono restare a 1.7, e una selezione PDF/A-4 accidentale fallisce a EndDoc con un messaggio invece di raggiungere un sistema che non sa aprire il file. Le guide alla validazione PDF/A, PDF/X e PDF/UA e alle fatture ibride ZUGFeRD e Factur-X su PDF/A-3 coprono le decisioni di profilo che vengono prima del numero di versione, e le note sul report di preflight automatizzato mostrano come trasformare il verdetto in un passo di build

Tutto questo gira come codice VCL nativo dentro il componente per Delphi e C++Builder, quindi l'output PDF/A-4 e PDF/UA-2 viaggia con il vostro eseguibile e non richiede alcun motore esterno. I profili supportati e le versioni RAD Studio sono elencati sulla pagina del componente HotPDF Delphi