Articolo tecnico

Salvataggi PDF a Versione Esatta in Delphi: Conformità di PDFiumPas

PDFiumPas, il wrapper Delphi e C++Builder attorno al motore PDFium di Google, salva un documento a una versione PDF esatta da 1.3 a 1.7 tramite il parametro PdfVersion del metodo TPdf.SaveAs. La chiamata FPDF_SaveWithVersion propria di PDFium riscrive solo l'intestazione %PDF-M.m, senza verificare se il contenuto effettivo del documento sia legale a quella versione. PDFiumPas colma questa lacuna con un passaggio di verifica di conformità post-salvataggio che percorre la catena di revisioni cross-reference attive e controlla le dichiarazioni di Adobe Extension Level prima che il file lasci il metodo

Questa distinzione conta soprattutto nella produzione tipografica, dove un profilo PDF/X nomina una versione PDF esatta e uno strumento di preflight o un RIP rifiuta qualsiasi cosa sia silenziosamente in disaccordo con la propria intestazione, uno scenario trattato dal lato output in validare documenti PDF/X pronti per la stampa con PDFiumPas. SaveAs espone il target come l'enum TPdfVersion, pv13 fino a pv17 insieme ai valori più vecchi da pv10 a pv12, più un TSaveOption indipendente per riscritture incrementali o complete. Passa PdfVersion e PDFiumPas svolge due compiti in un'unica chiamata: chiede a PDFium di stampigliare l'intestazione richiesta, poi rilegge i byte appena scritti e rifiuta di restituire un file il cui contenuto attivo non può legalmente esistere a quella versione

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

Perché l'ultima definizione di oggetto nel file è la cosa sbagliata di cui fidarsi?

L'ultimo oggetto fisico con un dato numero in un file PDF non è necessariamente l'oggetto che un lettore conforme risolverebbe oggi per quel numero. Un PDF che è passato attraverso diversi aggiornamenti incrementali non ha un unico grafo di oggetti, ha una storia di essi stratificati dentro un singolo file, e ogni ciclo di append può liberare un oggetto, ridefinirlo sotto un nuovo numero di generazione, o lasciare il suo vecchio corpo fisico seduto tra due marcatori endobj senza più alcuna voce cross-reference che vi punti

PDFiumPas ha incontrato esattamente quella modalità di fallimento prima di tracciare esplicitamente le revisioni xref: un'annotazione Redact orfana per via di una successiva riscrittura dell'oggetto pagina, o un dizionario /MarkInfo lasciato fisicamente presente senza alcuna voce xref che vi punti, poteva ancora comparire in una scansione di byte e ancora far scattare un controllo di funzionalità dipendente dalla versione che non si applicava più al documento che un lettore avrebbe realmente aperto. La direzione del fallimento era il rifiuto falso, non l'accettazione falsa: un file che era genuinamente andato oltre una funzionalità nella sua revisione corrente poteva comunque essere bloccato dal salvarsi a una versione inferiore a causa di contenuto che nessuno poteva più raggiungere

Come determina PDFiumPas quali definizioni di oggetto sono realmente attive?

PDFiumPas risolve l'insieme di oggetti attivi nello stesso modo di un lettore conforme, percorrendo la catena cross-reference invece di scansionare i byte alla ricerca di intestazioni di oggetto. Il resolver parte dall'ultimo offset startxref nel file e segue ogni link /Prev all'indietro attraverso le revisioni più vecchie, analizzando lungo il percorso tabelle cross-reference classiche, stream ibridi collegati a /XRefStm, e stream cross-reference puri. Il percorso va dal più recente al più vecchio e stabilisce ogni numero di oggetto la prima volta che viene visto, cosicché una voce libera in una revisione successiva oscuri correttamente un corpo di oggetto scritto in una precedente, e una ridefinizione sotto un nuovo offset o generazione vinca sempre su ciò che ha sostituito

I membri di object-stream ricevono un controllo aggiuntivo che una semplice ricerca di offset non può fornire da sola, un meccanismo trattato più in profondità in validare object stream e cross-reference stream con PDFiumPas. Un oggetto compresso recuperato da un /ObjStm deve avere il proprio stream genitore confermato attivo nello stesso percorso, e il suo indice deve concordare con la posizione propria del membro dentro l'intestazione di quello stream prima che PDFiumPas lo tratti come contenuto vivo. ISO 32000-1 sezione 7.5.8.4 descrive persino un caso di riferimento ibrido dove una tabella di compatibilità classica marca un oggetto libero mentre la voce /XRefStm del trailer definisce simultaneamente quello stesso oggetto come un membro compresso altrove; PDFiumPas fonde lo stream xref supplementare nella stessa revisione prima che le voci classiche vengano applicate, cosicché la definizione compressa vinca nel modo inteso dalla specifica

Adobe Extension Level: il cancello sopra il numero di versione

Un'intestazione %PDF-1.7 promette solo l'insieme di funzionalità che ISO 32000-1 ha standardizzato nel 2008, mentre diverse capacità su cui i produttori PDF fanno affidamento oggi sono state distribuite in seguito come supplementi esclusivi Adobe stratificati sopra quello stesso numero di versione. Adobe ha registrato ogni supplemento come una coppia BaseVersion ed ExtensionLevel registrata nel dizionario /Extensions del catalogo documento sotto un prefisso sviluppatore, ADBE per le estensioni proprie di Adobe, cosicché un lettore possa distinguere un semplice file PDF 1.7 da uno che implementa anche un livello di estensione numerato. Salvare a pv17 senza quella dichiarazione non è di per sé un errore; lo diventa solo nel momento in cui il contenuto attivo dipende realmente da una funzionalità che la dichiarazione dovrebbe coprire

Quali funzionalità ad alta versione fanno scattare il cancello a versione esplicita?

PDFiumPas verifica un elenco specifico, guidato dalla specifica, invece di indovinare dal solo numero di versione. I dizionari immagine che portano una voce esplicita /SMaskInData o un valore /BitsPerComponent di 16 richiedono entrambi PDF 1.5, con il caso a sedici bit che segue direttamente le regole sui componenti immagine della sezione 4.8 del PDF Reference 1.5. Le annotazioni RichMedia e le azioni RichMediaExecute richiedono /BaseVersion /1.7 con /ExtensionLevel 3 o superiore. Gli stream 3D PRC, identificati da un dizionario che porta sia /Type /3D sia /Subtype /PRC, richiedono la stessa versione base ma solo /ExtensionLevel 1. I dizionari Measure geospaziali e le annotazioni Projection richiedono /BaseVersion /1.7 con /ExtensionLevel 3, lo stesso supplemento Adobe da cui dipende RichMedia

Il controllo geospaziale porta un dettaglio di lettura della specifica utile da conoscere se mai costruisci una tua logica dipendente dalla versione sopra PDFiumPas. La Tabella 254 di ISO 32000-1 marca la voce /Type del dizionario Measure come opzionale, notando solo che "se presente, deve essere Measure", mentre la Tabella 311 rende /Type obbligatorio per il dizionario stream 3D in cui vive il contenuto PRC. L'output GeoPDF del mondo reale proveniente da strumenti di mappatura omette abitualmente /Type sul dizionario Measure e scrive solo /Subtype /GEO, quindi il rilevatore geospaziale di PDFiumPas fa corrispondere solo su /Subtype invece di richiedere entrambe le chiavi come può fare in sicurezza il suo rilevatore 3D PRC. Richiedere /Type su entrambi i dizionari avrebbe lasciato passare inosservato il contenuto GeoPDF conforme attraverso il cancello, finendo in un semplice file PDF 1.7 senza alcuna dichiarazione di livello di estensione a sostenerlo

PDFiumPas declassa automaticamente le funzionalità non supportate?

Non come capacità generale, e presupporre il contrario è l'errore da evitare qui. SaveAs incanala la versione target attraverso una routine interna, ValidatePdfVersionCompliance, e quando quella routine trova una funzionalità che la versione target o la sua dichiarazione di livello di estensione non può supportare, SaveAs solleva un'eccezione che porta il testo di errore della routine invece di scrivere il file; il chiamante riceve indietro una ragione precisa, nominata per funzionalità, mai un documento silenziosamente riscritto. L'unico punto in cui PDFiumPas riscrive automaticamente il contenuto è un target PDF 1.3, dove elimina i default di trasparenza semanticamente neutri /BM /Normal, /CA 1 e /ca 1 che PDFium scrive sempre nei dizionari ExtGState indipendentemente dalla versione target, perché quei valori specifici non portano alcun significato visivo e PDF 1.3 precede del tutto quelle chiavi

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

Trasparenza genuinamente non-default e soft mask immagine falliscono comunque apertamente a un target PDF 1.3, perché rimuoverle cambierebbe come la pagina appare realmente, e PDFiumPas non prenderà quella decisione per tuo conto. Due limiti correlati vale la pena pianificare prima che una versione esatta entri in una pipeline batch. L'output a versione esplicita non porta mai un dizionario /Encrypt; il salvataggio fallisce immediatamente se la sorgente è protetta, il che per caso si allinea con i profili PDF/X e PDF/A che comunque vietano la crittografia, ma significa che la decrittazione è un passaggio separato nel tuo flusso di lavoro piuttosto che qualcosa che SaveAs fa per tuo conto. PDFiumPas non ha nemmeno alcun metodo pubblico per scrivere una dichiarazione /Extensions /ADBE su un catalogo, quindi un file sorgente che contiene contenuto RichMedia, 3D PRC o geospaziale ma manca di quella dichiarazione non supererà il cancello indipendentemente da quale PdfVersion tu richieda; la dichiarazione deve già esistere nella sorgente, tipicamente perché lo strumento di creazione l'ha scritta, oppure la funzionalità deve essere rimossa prima del salvataggio. La proprietà di sola lettura TPdf.PdfVersion vale la pena controllarla prima ancora di tentare un salvataggio a versione esatta, poiché risolve la stessa versione effettiva consapevole del catalogo, intestazione o override /Version, qualunque sia corrente, su cui si basa il validatore stesso al momento del salvataggio

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

Tratta un'eccezione di SaveAs su un target a versione esatta come un report di preflight piuttosto che come un bug: il messaggio nomina la clausola esatta che il documento sorgente sta violando, che è esattamente l'informazione di cui una tipografia o una pipeline di archiviazione hanno bisogno prima che un file proceda oltre. Il percorso di salvataggio a versione esplicita, il resolver di revisione xref attiva e i controlli Adobe Extension Level descritti qui fanno parte del componente PDFiumPas standard per Delphi e C++Builder; la pagina del prodotto porta il riferimento completo di TPdf.SaveAs insieme al resto dell'API di conformità e moduli