Articolo tecnico

Preflight versione PDF in Delphi: regole Measure RL vs GEO

Un piano di un edificio esportato da un pacchetto CAD per architettura tipicamente ha un array /VP sulla sua prima pagina con due viewport, e ogni viewport porta una voce /Measure così un viewer può leggere i metri direttamente dal disegno. Un foglio di mappa GeoPDF ha lo stesso array /VP e la stessa chiave /Measure. Il piano è PDF 1.6 ordinario. Il foglio di mappa ha bisogno di PDF 1.7 più Adobe Extension Level 3. Un controllo di versione che si ferma al nome della chiave chiederà l'estensione per entrambi, e il piano viene rifiutato al salvataggio con un messaggio su contenuto geospaziale che non ha mai contenuto

Una chiave /Measure, due dizionari diversi

Il nome della chiave vi dice che un viewport è misurabile; solo il dizionario dietro di essa vi dice come. I dizionari Measure sono arrivati nel PDF 1.6, e ISO 32000-1 §12.9 definisce il tipo rettilineo: una stringa di rapporto di scala in /R, array di formato numerico in /X e /Y, e ulteriori array per la distanza (/D) e l'area (/A). La misurazione geospaziale è arrivata dopo, come parte di Adobe Extension Level 3 sopra il PDF 1.7. È marcata da /Subtype /GEO e porta un sistema di coordinate geografico in /GCS, un sistema di coordinate di visualizzazione opzionale in /DCS, array di punti accoppiati in /GPTS e /LPTS, e unità di visualizzazione preferite in /PDU — le strutture percorse in leggere viewport GeoPDF e array GPTS e LPTS in Delphi

Il dettaglio che fa inciampare la gente è il default. Il §12.9 dà a /Subtype un valore predefinito di RL, e parecchi esportatori CAD ne approfittano e lasciano fuori la voce. Un checker che legge un subtype mancante come "sconosciuto, pensa al peggio" trasforma ognuno di quei disegni in un documento geospaziale. Un checker che fa matching sulla chiave /Measure non arriva nemmeno alla questione del subtype, perché il dizionario viewport che contiene la chiave è un livello sopra il dizionario measure che la risponde

Una chiave /Measure, due dizionari in PDFlibPas: la misurazione rettilinea, con /RL o subtype omesso, richiede solo PDF 1.6, mentre un dizionario geospaziale richiede 1.7 ExtensionLevel 3, così il preflight decide dal contenuto del dizionario measure dove una regola a livello di chiave non sa distinguerli
Un preflight che blocca un file valido è peggiore di uno lento, perché il chiamante riceve una diagnosi autorevole su una funzionalità che il documento non ha mai contenuto, ed è per questo che il controllo distintivo siede un livello sotto

Come il preflight di salvataggio di PDF Library for Delphi arriva a un verdetto

PDF Library for Delphi (PDFlibPas) esegue un preflight di versione dentro SaveToFile prima che qualsiasi output venga scritto. Visita ogni oggetto indiretto, scendendo nei dizionari e negli array annidati, e prova ogni nodo contro una tabella di regole sulle funzionalità. Ogni regola nomina una versione PDF minima e, per le funzionalità di estensione, un livello di estensione Adobe; una regola di estensione può essere soddisfatta solo da PDF 1.7 insieme a una voce /Extensions /ADBE corrispondente nel catalogo. Il target contro cui controlla è la versione del documento, o la versione che avete bloccato con LockSaveVersion, più il livello di estensione che il catalogo dichiara. La prima regola che insieme fa matching e supera quel target termina il salvataggio: SaveToFile restituisce 0 e LastErrorCode diventa 602, il valore di PDFLIB_ERROR_VERSION_COMPLIANCE

Preflight di versione al salvataggio in PDFlibPas: SaveToFile controlla ogni oggetto contro una tabella fissa di regole sulle funzionalità, ciascuna con una versione PDF minima e, per le funzionalità di estensione, un livello di estensione Adobe che solo PDF 1.7 con una voce /Extensions corrispondente può soddisfare, e il primo match che il target non può soddisfare ferma il salvataggio con errore 602
Le chiavi GetInformation 311, 312 e 313 trasformano un rifiuto in una diagnosi, riportando la versione richiesta, la funzionalità che l'ha innescata e il target bloccato, così il chiamante può sistemare il file o la regola invece di tirare a indovinare

Un codice di errore da solo non vi dice cosa sistemare. Tre chiavi GetInformation portano il resto: 311 è la versione di cui la funzionalità colpevole ha bisogno (per esempio 1.7 ExtensionLevel 3), 312 è il nome della funzionalità, e 313 è la versione a cui il target di salvataggio è bloccato, vuota quando nulla è bloccato

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
      if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
        // 311: versione richiesta, 312: funzionalità che l'ha innescata,
        // 313: versione a cui il target di salvataggio è bloccato ('' quando sbloccato)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Per il contenuto di misurazione la tabella contiene tre regole e deliberatamente nessuna regola sulla nuda chiave /Measure. Una pagina con /VP o /UserUnit richiede PDF 1.6. Un dizionario /PtData richiede il livello di estensione 3. Un dizionario measure richiede il livello di estensione 3 quando dice /Subtype /GEO o contiene uno qualsiasi di /GCS, /DCS, /GPTS, /LPTS o /PDU. Così un viewport il cui dizionario measure omette /Subtype e uno che scrive esplicitamente /RL passano entrambi a 1.6, mentre la stessa pagina a 1.5 viene rifiutata per colpa di /VP. Aggiungete un array /GPTS e la risposta diventa 1.7 ExtensionLevel 3

Il test del contenuto è conservativo di proposito. Un dizionario rettilineo che porta anche un /GCS o /PDU vagante viene trattato come geospaziale. Preferiamo rifiutare quel file che lasciarlo passare: quelle chiavi non significano nulla nel modello rettilineo, quindi chi le ha scritte era o uno strumento geospaziale o qualcosa che ha danneggiato il file, e un viewer consapevole di GEO potrebbe bene provare a interpretarle. Il loader è altrettanto attento nell'altra direzione. La sua scansione delle funzionalità può alzare il numero di versione del documento a 1.7, ma non scrive mai una dichiarazione /Extensions per conto di un file di input — dichiarare un'estensione è un'affermazione sul contenuto, e solo il vostro codice o le chiamate di authoring dovrebbero farla

Controllare i viewport di un disegno prima di salvare

Quando 312 nomina una funzionalità geospaziale e siete sicuri che il file è un semplice disegno, guardate voi stessi i dizionari measure. Le chiamate pubbliche sui viewport bastano. GetMeasureDictCoordinateSystem legge /Subtype e restituisce 1 per RL, 2 per GEO, e 0 quando la voce è assente — il che per il §12.9 significa comunque rettilineo. I conteggi degli array di punti vi dicono se ci sono dati di georeferenziazione

// Riporta ogni dizionario measure nel documento caricato.
// Tutte le chiamate qui fanno solo letture; nessuna aggiunge chiavi al file.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
  Page, VP, VPID, MD, Kind: Integer;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.SelectPage(Page);
    for VP := 1 to Pdf.GetPageViewPortCount do
    begin
      VPID := Pdf.GetPageViewPortID(VP);
      MD := Pdf.GetViewPortMeasureDict(VPID);
      if MD = 0 then
        Continue;
      Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = omesso (RL), 1 = RL, 2 = GEO
      Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
        [Page, string(Pdf.GetViewPortName(VPID)), Kind,
         Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
    end;
  end;
end;

Due chiamate mancano da quel ciclo di proposito: GetMeasureDictGCSDict e GetMeasureDictDCSDict. Quelle due sono accessor get-or-create costruiti per l'authoring. Su un dizionario measure senza voce /GCS, GetMeasureDictGCSDict ne aggiunge una vuota e dichiara il livello di estensione 3 per il documento. Chiamatelo come sonda su un piano rettilineo e il dizionario ora contiene davvero /GCS; un salvataggio successivo bloccato a 1.6 viene rifiutato, giustamente, per contenuto che la vostra diagnosi ha appena scritto. Se dovete sapere se esiste un sistema di coordinate, il subtype e i conteggi degli array di punti rispondono senza effetti collaterali

Bloccare l'output a PDF 1.6

Una versione bloccata tiene l'output onesto quando i vostri consumatori hanno un tetto. TPDFlib.LockSaveVersion accetta le stringhe da '1.0' a '1.7' e restituisce 0 per qualsiasi altra cosa. Fissa la versione del documento e impedisce alle chiamate lato scrittura di alzarla da sole, ma il preflight di salvataggio gira comunque contro il valore bloccato. Un disegno CAD bloccato a 1.6 si salva pulito. Un vero GeoPDF bloccato a 1.6 si prende 602, ed è la risposta che volete, perché l'alternativa è un file la cui intestazione promette meno di ciò che il suo contenuto richiede

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
try
  if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
    if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
      // Contenuto reale sopra 1.6, per esempio un dizionario measure GEO
      raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
        [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
finally
  Pdf.UnlockSaveVersion;
end;

Leggete 311 e 312 prima di toccare la versione. Se la funzionalità è genuino contenuto geospaziale, avete due scelte oneste: lasciare il documento sbloccato a 1.7 con l'estensione dichiarata, o togliere i dati geospaziali dalla copia che mandate a un consumatore solo-1.6. Alzare il blocco solo per far sparire l'errore è l'opzione da evitare. Nasconde la domanda vera — il reader a valle può gestire ciò che state spedendo? Quando costruite contenuto geospaziale tramite l'API, i setter SetMeasureDict* come SetMeasureDictCoordinateSystem e SetMeasureDictGPTSCount dichiarano da soli il livello di estensione 3, così i file prodotti arrivano al preflight già etichettati. La stessa abitudine di controlli delimitati e sostenuti da prove ricompare in il preflight author-mode PDF/E-1 per documenti di ingegneria, dove i disegni CAD incontrano uno standard di conformità anziché un numero di versione

Perché la scansione delle regole legge la tabella in loco

Il costo del preflight è all'incirca nodi di oggetto per regole, e il conteggio dei nodi include ogni numero e nome dentro ogni array. Questo rende l'overhead per regola più importante di quanto sembri. Ogni regola nella tabella è un record con due campi AnsiString, la stringa di match e il nome della funzionalità. Copiare un tale record in una variabile locale fa sì che Delphi o FPC incrementi due reference count e rilasci ciò che la locale teneva prima. Fatto una volta per regola per nodo, significa pagare la contabilità delle stringhe anche per regole che non si applicano alla versione target e vengono buttate alla riga successiva

Scansione delle regole in PDFlibPas: copiare un record di regola che contiene campi stringa aggiusta i reference count a ogni visita, mentre passare la voce della tabella di sola lettura come parametro const la legge in loco senza toccare le stringhe
Leggere la tabella di sola lettura in loco toglie il lavoro sui reference count dal percorso di match delle regole, mentre un salvataggio completo paga comunque per la rilevazione delle funzionalità, la decodifica degli oggetti e la serializzazione

La tabella delle regole viene riempita una volta all'inizializzazione dell'unità e mai modificata in seguito, così la scansione consegna le voci della tabella al suo codice di match tramite parametri record const e le legge dove vivono. Un parametro record const è passato per riferimento, quindi provare una regola non tocca nessun campo stringa e i reference count restano dove erano. L'effetto è concentrato sul percorso di match delle regole. Un salvataggio vero decodifica anche gli oggetti, esegue la rilevazione differita delle funzionalità e serializza l'output, e quelle fasi non sono toccate da come le regole vengono passate. L'ordine delle regole, le soglie e la diagnosi del primo fallimento in 311 e 312 non dipendono da questo, e nessun risultato di regola viene messo in cache tra i salvataggi, quindi un documento che modificate e salvate due volte viene controllato per intero entrambe le volte

Lo stesso pattern vale la pena controllarlo nel vostro codice Delphi ovunque un ciclo percorra una tabella di record che contengono stringhe, interfacce o array dinamici:

type
  TMyRule = record
    Name: string;      // campo gestito: le copie costano lavoro sui reference count
    Threshold: Integer;
  end;

// Codice proprio del lettore, mostrato come pattern
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
  Result := Value >= Rule.Threshold;  // const: nessuna copia, nessun traffico di refcount
end;

// Nel ciclo caldo, passate la voce della tabella direttamente invece di
// assegnarla prima a un record locale
for I := 0 to High(Rules) do
  if Applies(Rules[I], V) then
    Inc(Hits);

Viewport di misurazione, dizionari geospaziali, blocco della versione e il controllo di conformità al salvataggio viaggiano tutti in PDF Library for Delphi, disponibile per progetti Delphi, C++Builder e Lazarus