Articolo tecnico

Modificare i metadati PDF caricati in Delphi senza riscriverli

Hai diecimila PDF contrattuali provenienti da una dozzina di generatori diversi, e l'ufficio legale vuole che ognuno riporti il giusto Author, una stringa Author corretta, e una modalità di lettura che apra il pannello dei segnalibri all'avvio. ProducerLa soluzione ingenua è caricare ogni file, ridisporre le pagine e scrivere un documento nuovo. Se lo fai, butti via ogni numero di oggetto esistente, la cronologia degli aggiornamenti incrementali, eventuali firme digitali e il cross-reference accuratamente tarato emesso dallo strumento originale. Le pagine sembrano identiche e il file, strutturalmente, è un estraneo. Per una modifica dei metadati, è la scelta sbagliata in assoluto

La scelta giusta è trattare il documento caricato come un grafo di oggetti da mutare in place: entra nel dizionario Info, nello /Metadatastream XMP e nel Catalog, cambia le poche voci che ti interessano e riscrivi il risultato. HotPDF, il componente PDF VCL nativo per Delphi e C++Builder, espone esattamente questa superficie tramite la sua API di scrittura per i documenti caricati. Questo articolo spiega come usarla correttamente e qual è l'unico errore che quasi tutti fanno: modificare il dizionario Info e dimenticare che una seconda copia degli stessi metadati vive in XMP

Due punti archiviano gli stessi metadati, e non concordano

PDF conserva le informazioni del documento in due posizioni parallele, ed è questa la radice della maggior parte dei ticket del tipo "ho cambiato il titolo ma Acrobat mostra ancora quello vecchio". La prima è il dizionario delle informazioni del documento, il classico /Info oggetto con /Title, /Author, /Subject, /Keywords, /Creator, e /Producerchiavi, definite in ISO 32000-1 §14.3.3. La seconda è un pacchetto XMP, un documento XML memorizzato come stream agganciato al Catalog sotto /Metadata, definito in §14.3.2 e costruito sul modello dati Adobe XMP

Entrambi possono contenere un titolo. Nulla nello standard li obbliga a coincidere. I visualizzatori moderni e la maggior parte dei validatori PDF/A preferiscono il pacchetto XMP quando è presente e tornano al dizionario Info quando non lo è. Quindi, se aggiorni solo /Info che è ciò che fa la grandissima maggioranza del codice che imposta i metadati PDF, un lettore che si fida di XMP continuerà a mostrare il valore obsoleto, e un controllo PDF/A segnalerà la discrepanza. L'operazione corretta su qualsiasi file che abbia già un pacchetto XMP è una doppia scrittura: cambia la voce Info erigenera l'XMP, così i due restano coerenti. HotPDF ti offre entrambe le metà, ma la disciplina di usarle insieme spetta a te

Modificare il dizionario Info

Gli helper lato Info sono sottili e prevedibili. SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator, e SetLoadedProducerciascuno accetta una singola AnsiStringstringa e scrive la chiave corrispondente nel dizionario Info caricato, sostituendo il valore se la chiave esiste e aggiungendolo se non esiste. Per rimuovere del tutto una chiave, per esempio un /Creator che nomina i tuoi strumenti interni, chiama RemoveLoadedInfoKeycon il nome nudo della chiave. Nessuno di questi tocca XMP; agiscono solo sull'/Infooggetto che LoadFromFileha individuato quando ha analizzato il file

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-in.pdf', '') > 0 then
    begin
      Pdf.SetLoadedTitle('Master Services Agreement 2026');
      Pdf.SetLoadedAuthor('Legal Department');
      Pdf.SetLoadedSubject('Executed contract, retention 7 years');
      Pdf.SetLoadedKeywords('contract; MSA; 2026; executed');
      Pdf.SetLoadedProducer('Acme Document Pipeline');
      Pdf.RemoveLoadedInfoKey('Creator');  // drop the originating tool name
      Pdf.SaveLoadedDocument('contract-out.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Un dettaglio da tenere in ordine: questi metodi accettano AnsiStringstringhe. Per i titoli ASCII non c'è alcun problema, ma le stringhe PDF che devono contenere caratteri non latini vanno codificate come richiede lo standard, UTF-16BE con byte-order mark oppure PDFDocEncoding, prima di passarle. La libreria scrive i byte che le fornisci in un oggetto stringa; non indovina alcuna codifica al posto tuo. Se i tuoi titoli sono in inglese semplice, ignora questa nota. Se contengono caratteri accentati o CJK, codificali con intenzione e prova il risultato in un visualizzatore reale

Riscrivere il pacchetto XMP

SetLoadedXMPMetadataè l'altra metà della doppia scrittura. Passagli il pacchetto XMP completo come un AnsiStringe fa una di due cose: se il Catalog fa già riferimento a uno /Metadatastream, ne sostituisce il contenuto in place, mantenendo lo stesso numero di oggetto; se non esiste alcuno stream di metadati, ne crea uno, lo marca /Type /Metadatae /Subtype /XML, assegna un numero di oggetto e lo collega dal Catalog. In entrambi i casi finisci con un oggetto di metadati valido che i visualizzatori leggeranno

Fornisci tu l'XML, quindi controlli lo schema: dc:title, dc:creator, xmp:CreatorTool, e così via. Questo è potere e responsabilità insieme: la libreria non analizza né valida il tuo pacchetto, e scrive i byte non compressi, senza applicare alcun filtro di stream. Un pacchetto malformato passerà attraverso la chiamata e riemergerà più tardi come un errore di metadati danneggiati. Costruisci l'XML con cura e rispecchia esattamente i valori che hai scritto nel dizionario Info, così le due viste non si contraddicono mai

const
  XMP_TEMPLATE =
    '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' +
    '<x:xmpmeta xmlns:x="adobe:ns:meta/">' +
    '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
    '<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">' +
    '<dc:title><rdf:Alt><rdf:li xml:lang="x-default">%s</rdf:li></rdf:Alt></dc:title>' +
    '<dc:creator><rdf:Seq><rdf:li>%s</rdf:li></rdf:Seq></dc:creator>' +
    '</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
begin
  // After setting the Info dictionary, mirror the same values into XMP:
  Pdf.SetLoadedTitle('Master Services Agreement 2026');
  Pdf.SetLoadedAuthor('Legal Department');
  Pdf.SetLoadedXMPMetadata(
    AnsiString(Format(XMP_TEMPLATE,
      ['Master Services Agreement 2026', 'Legal Department'])));
  Pdf.SaveLoadedDocument('contract-out.pdf');
end;

Quest'ordine, prima Info, poi XMP, quindi salvataggio, è il modello da interiorizzare. Le due chiamate sono indipendenti; la coerenza esiste solo perché hai passato loro le stesse stringhe. Se salti la chiamata XMP su un file che ha un pacchetto XMP, torni al bug di obsolescenza silenziosa che tutta questa sezione serve a prevenire

Diagram showing a PDF Info dictionary and an XMP metadata stream both holding title and author, edited in place alongside the bookmark outline tree
I metadati vivono in due posti, il dizionario Info e lo stream XMP, più gli indizi di lettura a livello Catalog e l'albero dei segnalibri. Una modifica in place tocca ciascuno di essi senza ricostruire il documento.

Guidare il modo in cui il visualizzatore apre il file

Tre voci del Catalog decidono ciò che un lettore vede nell'istante in cui il documento si apre, e tutte e tre sono modifiche di una sola riga sul grafo caricato. SetLoadedPageModescrive /PageModecome oggetto name: passa 'UseOutlines'per aprire il pannello dei segnalibri, 'UseThumbs'per la barra delle miniature, 'FullScreen'per la modalità presentazione, oppure 'UseAttachments'per mostrare il riquadro degli allegati (ISO 32000-1 §7.7.3.1, Tabella 28). SetLoadedPageLayoutscrive /PageLayoutallo stesso modo, 'SinglePage', 'OneColumn', 'TwoColumnLeft'e il resto. Entrambi accettano il nome senza la barra iniziale; la libreria la aggiunge in output

SetLoadedLanguagescrive la voce /Langdel Catalog, il tag in linguaggio naturale per l'intero documento, 'en-US', 'de-DE', un tag BCP 47. Nota la differenza di tipo che manda fuori strada molti: /PageModee /PageLayoutsono oggetti name PDF, mentre nomeè una /Langstringa. HotPDF gestisce correttamente questo aspetto al suo interno, ma se mai ispezioni l'output vedrai rispetto a /PageMode /UseOutlines, e ora sai perché. La voce /Lang (en-US)conta più di quanto sembri: è ciò che la tecnologia assistiva legge per scegliere la pronuncia, ed è un requisito obbligatorio per la conformità PDF/UA all'accessibilità./LangRinominare i segnalibri senza disturbare l'albero

if Pdf.LoadFromFile('handbook.pdf', '') > 0 then
begin
  Pdf.SetLoadedPageMode('UseOutlines');     // /PageMode, a name
  Pdf.SetLoadedPageLayout('TwoColumnLeft'); // /PageLayout, a name
  Pdf.SetLoadedLanguage('en-US');           // /Lang, a string
  Pdf.SaveLoadedDocument('handbook-tagged.pdf');
end;

I titoli dei segnalibri sono manutenzione ordinaria, un refuso in un'intestazione, un capitolo rinumerato dopo la costruzione dell'outline

I titoli dei segnalibri sono manutenzione ordinaria, un refuso in un'intestazione, un capitolo rinumerato dopo che l'outline è stato costruito. SetLoadedOutlineTitleprende un indice zero-based delle voci di outline di primo livello e un nuovo titolo, percorre la catena Catalog → /Outlines/First/Nextfino a quella posizione e sostituisce la /Titlestringa della voce. Cambia solo il titolo; la destinazione, lo stato aperto/chiuso e la struttura figlia restano intatti

if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
  Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
  Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
  Pdf.SaveLoadedDocument('report-renamed.pdf');
end;

Rinominare è sicuro proprio perché non tocca mai i contatori strutturali. Eliminare una voce di outline è il caso che morde, e vale la pena capirlo anche quando stai solo rinominando, perché ti mostra che cosa non devi modificare a mano. Ogni nodo dell'outline porta un non da modificare a mano. Ogni nodo dell'outline porta un /Count, e, secondo ISO 32000-1 §12.3.3, quel conteggio non è il numero dei figli immediati. È il numero totale dei discendenti visibili: un /Count di N significa che N discendenti sono attualmente espansi, mentre un valore negativo significa che il nodo ha discendenti ma è chiuso. Quando si rimuove una voce di primo livello, il /Outlines conteggio radice non può essere semplicemente decrementato di uno; va ricalcolato sommando, per ogni nodo di primo livello sopravvissuto, "uno per il nodo stesso più il suo /Count", saltando i discendenti di qualsiasi nodo chiuso (conteggio negativo). Sbaglia questo calcolo e il totale dei segnalibri che un lettore mostra deraglia, perché cambia di più di uno per ogni cancellazione. La rinomina aggira tutto questo, ed è un motivo in più per preferire l'helper mirato invece di mettere mano da soli al dizionario

Come il salvataggio resta in place

Ogni modifica sopra muta oggetti in memoria; niente arriva su disco finché SaveLoadedDocumentnon viene eseguito. Il motivo per cui questo approccio costa poco è che il salvataggio non rigenera il documento, ma conserva i numeri di oggetto esistenti e la struttura che HotPDF ha analizzato in caricamento, riscrivendo lo stesso grafo con il piccolo insieme di oggetti modificati e appena allocati. È questo che impedisce a un passaggio di metadati di riscrivere l'intero file, ed è lo stesso meccanismo di aggiornamento in place che rende operativi i flussi di oggetti e gli aggiornamenti incrementali. Se i file sorgente provengono da Word o da un'altra suite per ufficio, il loro layout degli oggetti ha peculiarità proprie che vale la pena conoscere prima di modificarli; l'articolo su flussi di cross-reference ibridi nei PDF di Officespiega come sono strutturati quei file e cosa sopravvive a un round trip

Due limiti da rispettare. Primo, questo è un modello di modifica in place, non uno strumento di redazione o sanitizzazione: rimuovere una chiave Info rimuove quella chiave, ma non elimina i valori precedenti che potrebbero persistere in una precedente generazione di aggiornamento incrementale dello stesso file. Se il tuo requisito è la rimozione vera di metadati sensibili, è un'operazione diversa e più pesante. Secondo, la scrittura XMP è letterale, la libreria si fida del tuo XML e non lo valida, quindi per tutto ciò che deve finire in PDF/A o in un validatore rigoroso, genera il pacchetto da un template noto come buono e verifica l'output. Usato entro questi limiti, l'editing dei metadati in place è lo strumento giusto: corregge i pochi byte sbagliati e lascia esattamente com'era il novantanove per cento del file già corretto scritto dal produttore originale

L'API di scrittura per documenti caricati mostrata qui è inclusa nel Componente HotPDFper Delphi e C++Builder, insieme all'intero set di metodi per metadati, outline e Catalog