PDFlibPas (PDF Library for Delphi) controlla ogni oggetto contro una tabella di regole di versione PDF prima di scrivere un file, e fino a poco fa quel preflight di versione PDF scambiava normali dizionari di misura CAD per geospaziali. Un disegno CAD di una pagina caricava bene, poi SaveToFile restituiva 0 con LastErrorCode 602 e pretendeva 1.7 ExtensionLevel 3. Le regole corrette trattano i dizionari /Measure rettilinei (/Subtype /RL) come normale PDF 1.6 e riservano il gate di extension ai veri marker geospaziali
Il file era arrivato tramite ammissione al corpus: una pagina, un gruppo optional-content, due viewport di misura rettilinei, il tipo di output che un pacchetto CAD di architettura scrive perché un viewer possa leggere le distanze da una pianta. Non c'era niente di esotico, ed è esattamente per questo che il rifiuto contava. Un preflight che blocca un file valido è peggiore di uno lento, perché chi chiama riceve una diagnostica dall'aria autorevole che punta a una feature che il documento non contiene. La correzione è passata da due parti: la lettura della spec dietro una regola, e la resa del conto che la regola non poteva distinguere due tipi di dizionario al livello a cui guardava
Come funziona il preflight di versione al salvataggio in PDFlibPas?
Il gate di save, PrepareAndCheckSaveVersion, confronta ogni oggetto indiretto con PDFFeatureRules e fallisce alla prima regola che sia combacia sia richieda più di quanto il target consente. Il target è la versione del documento (o la versione fissata da LockSaveVersion), più il livello di extension Adobe dichiarato sotto /Extensions /ADBE. Ogni record TPDFFeatureRule porta un MinVersion, un MinExtensionLevel, un MatchKind come fmkDictKey o fmkDictSubtype, una stringa Match, un nome di Feature leggibile e una callback opzionale. AddRule registra una regola di versione semplice; AddExtensionRule fissa sempre MinVersion a 17 e aggiunge sopra un livello di extension, quindi una regola di extension può essere soddisfatta solo da PDF 1.7 più la giusta voce /Extensions. Quando il gate scatta, la versione richiesta e il nome della feature vengono conservati per chi chiama, e le chiavi 311, 312 e 313 di GetInformation li espongono
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: feature che l'ha innescata,
// 313: versione a cui il target di save è bloccato ('' se sbloccato)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Perché un disegno CAD qualunque falliva con errore 602?
La tabella delle regole conteneva AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), che scattava su qualunque dizionario con una semplice chiave /Measure, e ogni viewport di misura ne ha una. L'array /VP della pagina contiene dizionari di viewport, ogni viewport punta al suo dizionario di misura attraverso /Measure, e il match per presenza della chiave si fermava lì senza guardare che cosa fosse davvero quel dizionario di misura. La scansione delle feature al load poteva così alzare il numero di versione del documento a 1.7, ma non scrive mai una dichiarazione /Extensions per conto di un file di input, quindi il gate di save vedeva PDF 1.7 a livello di extension 0 e riportava 1.7 ExtensionLevel 3. Quel rifiuto di inventarsi una dichiarazione di extension è voluto: la libreria non promuove in silenzio un file di input per coprire una regola sbagliata
La spec è inequivocabile sul caso rettilineo. I dizionari Measure sono arrivati con il PDF 1.6, e ISO 32000-1 §12.9 dà a /Subtype un default RL, un sistema di coordinate rettilineo descritto dal suo insieme di voci: rapporto di scala, formati numerici X e Y, distanza e area. La misura geospaziale è l'aggiunta successiva di Adobe Extension Level 3 sopra il PDF 1.7, identificata da /Subtype /GEO e con array di punti geografici, dizionari di sistema di coordinate e unità di visualizzazione, le strutture percorse in leggere viewport GeoPDF e array GPTS e LPTS in Delphi. Entrambi i dizionari pendono dalla stessa chiave /Measure, quindi una regola che si ferma alla chiave non può andare bene per entrambi. L'informazione discriminante sta un livello più giù, nel dizionario di misura stesso
Che cosa fa rispettare ancora il set di regole corretto?
La correzione cancella la regola incondizionata sulla chiave e lascia i gate che descrivono veri requisiti di versione. Una pagina con /VP o /UserUnit continua a richiedere PDF 1.6 attraverso CB_PagePDF16Entries, una chiave /PtData continua a richiedere livello di extension 3, e CB_GeospatialDictionary decide se un dizionario di misura è geospaziale dal suo contenuto anziché dalla chiave per cui è arrivato
// Rimosso: ogni dizionario con una chiave /Measure contava come geospaziale
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
Le regression condivise Delphi e FPC fissano quel confine da entrambi i lati. Un viewport il cui dizionario di misura omette /Subtype e uno che lo scrive esplicitamente come /RL passano entrambi a PDF 1.6, la stessa pagina viene ancora rifiutata a PDF 1.5, e il rilevamento delle feature non riporta più una extension per essa. Aggiungere un array /GPTS ribalta il verdetto di nuovo a 1.7 ExtensionLevel 3, che passa una volta dichiarato il livello di extension, e un dizionario nudo /Subtype /GEO viene rifiutato senza. La callback è conservativa per progetto: un dizionario rettilineo che porta anche una chiave /GCS o /PDU smarrita viene trattato come geospaziale, perché quelle chiavi non hanno senso nel modello RL
LockSaveVersion è il punto in cui questo cambiamento diventa visibile per chi chiama. TPDFlib.LockSaveVersion accetta da '1.0' a '1.7', restituisce 0 per qualunque altra cosa, fissa la versione del documento, e impedisce alle chiamate lato writer di alzarla in silenzio, e il gate di save gira comunque contro il valore bloccato. Con le regole corrette, un file CAD bloccato a 1.6 salva pulito. Un GeoPDF autentico bloccato a 1.6 prende comunque 602, che è la risposta giusta, e le chiamate di authoring geospaziale come SetMeasureDictCoordinateSystem dichiarano da sole il livello di extension 3 quando costruisci quel contenuto attraverso la API
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Contenuto vero sopra il 1.6, per esempio un dizionario di misura GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Perché la scansione delle regole di versione era più lenta del necessario?
La scansione copiava ogni TPDFFeatureRule in un record locale prima di testarlo, e poiché il record contiene due campi AnsiString, ogni copia aggiustava due reference count e rilasciava i valori precedenti. Il preflight visita ogni nodo di ogni object tree, scalari inclusi, così quel costo moltiplicava numero di oggetti per numero di regole, e le regole che non si applicavano nemmeno alla versione target venivano copiate prima e saltate dopo. Dato che PDFFeatureRules viene riempito una volta sola all'inizializzazione della unit e trattato come read-only, la v3.539.17 passa le voci della tabella direttamente a MatchSingleRule e RuleExceedsTarget, i cui parametri const Rule prendono un riferimento senza toccare le stringhe
// Prima: una copia managed del record per regola, per oggetto visitato
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Dopo: i parametri const leggono sul posto la voce immutabile della tabella
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
L'effetto misurato è stretto e va citato così. Il benchmark confronta un array di 20.000 oggetti numerici contro un target PDF 1.4 dieci volte per round; compilato con FPC Win64 a -O2, la mediana di cinque round è scesa da 0.711 s a 0.203 s, e facendo girare le due build in ordine inverso si sono avuti 0.459 s contro 0.150 s. È più o meno un guadagno di 3x sul solo percorso di match delle regole. Un save reale paga anche il rilevamento differito delle feature, la decodifica degli oggetti e la serializzazione, quindi il rapporto non si riporta sul tempo totale di save. Ordine delle regole, callback, soglie di versione e diagnostica sul primo fallimento sono invariati, e nessuna regola è stata messa in cache tra i save né saltata per arrivarci
Che cosa controllare quando un PDF caricato fallisce il preflight di versione?
Leggi le chiavi 311 e 312 prima di toccare la versione. Se la feature nomina un dizionario geospaziale e il file disegna solo misure rettilinee, era questo falso positivo, e una build corrente salva il file invariato. Se la feature è genuina, dichiara la extension o blocca su una versione che contiene onestamente il contenuto; alzare la versione solo per ammutolire il gate nasconde la domanda se i consumatori a valle riescano a leggere quello che spedisci. Lo stesso principio di controlli limitati e supportati da evidenze guida il preflight author-mode PDF/E-1 per i documenti di ingegneria, dove i disegni CAD incontrano uno standard di conformità anziché un numero di versione
I controlli di conformità di versione, i dizionari di misura e geospaziali, e il blocco della versione di save sono tutti parte di PDF Library for Delphi, il toolkit PDFlibPas per sviluppatori Delphi, C++Builder e Lazarus