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
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
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
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