Articol tehnic

Preflight versiune PDF în Delphi: reguli Measure RL vs GEO

PDFlibPas (PDF Library for Delphi) verifică fiecare obiect împotriva unui tabel de reguli de versiune PDF înainte să scrie un fișier, și până de recent acel preflight de versiune confunda dicționarele obișnuite de măsurare CAD cu cele geospatiale. Un desen CAD cu o singură pagină se încărca bine, apoi SaveToFile întorcea 0 cu LastErrorCode 602 și cerea 1.7 ExtensionLevel 3. Regulile corectate tratează dicționarele /Measure rectilinii (/Subtype /RL) ca PDF 1.6 simplu și rezervă poarta de extensie pentru markerii geospatiali adevărați

Fișierul a venit prin admiterea de corpus: o pagină, un grup optional-content, două viewports de măsurare rectilinii, genul de output pe care îl scrie un pachet CAD de arhitectură pentru ca un vizualizator să poată citi distanțe de pe planul etajului. Nimic exotic, ceea ce e exact motivul pentru care refuzul a contat. Un preflight care blochează un fișier valid e mai rău decât unul lent, pentru că apelantul primește un diagnostic cu aspect autoritar care arată spre o funcționalitate pe care documentul nu o conține. Reparația a avut două părți: lectura specificației din spatele unei reguli și realizarea că regula nu putea deosebi două tipuri de dicționar la nivelul la care se uita

Cum funcționează preflight-ul de versiune din PDFlibPas la salvare?

Poarta de salvare, PrepareAndCheckSaveVersion, compară fiecare obiect indirect cu PDFFeatureRules și eșuează la prima regulă care se potrivește și care cere mai mult decât ținta permite. Ținta este versiunea documentului (sau versiunea blocată prin LockSaveVersion), plus nivelul de extensie Adobe declarat sub /Extensions /ADBE. Fiecare înregistrare TPDFFeatureRule cară un MinVersion, un MinExtensionLevel, un MatchKind precum fmkDictKey sau fmkDictSubtype, un șir Match, un nume de Feature lizibil pentru om și un callback opțional. AddRule înregistrează o regulă de versiune simplă; AddExtensionRule fixează mereu MinVersion la 17 și adaugă un nivel de extensie peste, deci o regulă de extensie poate fi satisfăcută doar de PDF 1.7 plus intrarea /Extensions corectă. Când poarta se declanșează, versiunea cerută și numele funcționalității sunt păstrate pentru apelant, iar cheile 311, 312 și 313 din GetInformation le expun

Preflight-ul de versiune la salvare în PDFlibPas: PrepareAndCheckSaveVersion compară fiecare obiect cu înregistrări PDFFeatureRules care cară MinVersion, un nivel de extensie și un fel de potrivire, AddExtensionRule fixează cerința la PDF 1.7 plus un nivel de extensie, iar prima potrivire pe care ținta nu o poate satisface oprește salvarea cu eroarea 602
Cheile 311, 312 și 313 din GetInformation transformă un refuz într-un diagnostic, raportând versiunea cerută, funcționalitatea care a declanșat-o și ținta blocată, deci apelantul poate repara fișierul sau regula în loc să ghicească
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: versiunea cerută, 312: funcționalitatea care a declanșat-o,
        // 313: versiunea la care e blocată ținta de salvare ('' când nu e blocată)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

De ce un desen CAD obișnuit eșua cu eroarea 602?

Tabelul de reguli conținea AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), care se declanșa pe orice dicționar care avea pur și simplu o cheie /Measure, iar fiecare viewport de măsurare are una. Tabloul /VP al paginii ține dicționare de viewport, fiecare viewport arată spre dicționarul său de măsurare prin /Measure, iar potrivirea pe prezența cheii se oprea acolo, fără să se uite ce era de fapt dicționarul de măsurare. Scanul de funcționalități de la încărcare putea apoi ridica numărul versiunii documentului la 1.7, dar nu scrie niciodată o declarație /Extensions în numele unui fișier de intrare, deci poarta de salvare vedea PDF 1.7 la nivel de extensie 0 și raporta 1.7 ExtensionLevel 3. Refuzul acela de a inventa o declarație de extensie e deliberat: biblioteca nu promovează în tăcere un fișier de intrare ca să acopere o regulă greșită

Specificația e neechivocă în cazul rectiliniu. Dicționarele Measure au sosit în PDF 1.6, iar ISO 32000-1 §12.9 dă lui /Subtype un implicit RL, un sistem de coordonate rectiliniu descris de propriul său set de intrări: raport de scară, formate de numere X și Y, distanță și arie. Măsurarea geospatială e adaosul ulterior din Adobe Extension Level 3 peste PDF 1.7, identificat prin /Subtype /GEO și carând tablouri de puncte geografice, dicționare de sisteme de coordonate și unități de afișare, structurile parcurse în citirea viewports GeoPDF și a tablourilor GPTS și LPTS în Delphi. Ambele dicționare atârnă de aceeași cheie /Measure, deci orice regulă care se oprește la cheie nu poate fi corectă pentru ambele. Informația distinctivă stă un nivel mai jos, în dicționarul de măsurare însuși

O cheie /Measure, două dicționare în PDFlibPas: măsurarea rectilinie, cu /RL sau cu subtipul omis, cere doar PDF 1.6, în timp ce un dicționar geospatial cere 1.7 ExtensionLevel 3, deci CB_GeospatialDictionary decide după conținut acolo unde vechea regulă de cheie nu le putea deosebi
Un preflight care blochează un fișier valid e mai rău decât unul lent, pentru că apelantul primește un diagnostic autoritar despre o funcționalitate pe care documentul n-a conținut-o niciodată, motiv pentru care verificarea distinctivă a coborât un nivel

Ce impune în continuare setul de reguli corectat?

Reparația șterge regula necondiționată de cheie și lasă porțile care descriu cerințe reale de versiune. O pagină care cară /VP sau /UserUnit mai are nevoie de PDF 1.6 prin CB_PagePDF16Entries, o cheie /PtData mai are nevoie de nivelul de extensie 3, iar CB_GeospatialDictionary decide dacă un dicționar de măsurare e geospatial după conținutul lui, nu după cheia prin care a ajuns la el

// Eliminat: orice dicționar cu o cheie /Measure era numărat ca geospatial
// 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;

Regresiile partajate Delphi și FPC fixează granița aceea din ambele părți. Un viewport al cărui dicționar de măsurare omite /Subtype și unul care scrie explicit /RL trec amândouă la PDF 1.6, aceeași pagină e respinsă în continuare la PDF 1.5, iar detecția de funcționalități nu mai raportează o extensie pentru ea. Adăugarea unui tablou /GPTS întoarce verdictul înapoi la 1.7 ExtensionLevel 3, care trece odată ce nivelul de extensie e declarat, iar un dicționar gol cu /Subtype /GEO e refuzat fără el. Callback-ul e conservator din principiu: un dicționar rectiliniu care cară și o cheie rătăcită /GCS sau /PDU e tratat ca geospatial, pentru că acele chei nu au sens în modelul RL

LockSaveVersion e locul în care schimbarea devine vizibilă pentru apelanți. TPDFlib.LockSaveVersion acceptă '1.0' până la '1.7', întoarce 0 pentru orice altceva, blochează versiunea documentului și împiedică apelurile din partea scriitorului să o ridice în tăcere, dar poarta de salvare rulează în continuare împotriva valorii blocate. Cu regulile corectate, un fișier CAD blocat la 1.6 se salvează curat. Un GeoPDF autentic blocat la 1.6 primește în continuare 602, care e răspunsul corect, iar apelurile de autorat geospatial precum SetMeasureDictCoordinateSystem declară ele însele nivelul de extensie 3 când construiți acel conținut prin 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
    // Conținut real peste 1.6, de exemplu un dicționar measure GEO
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

De ce scanul regulilor de versiune era mai lent decât trebuia?

Scanul copia fiecare TPDFFeatureRule într-un record local înainte să o testeze, și pentru că recordul ține două câmpuri AnsiString, fiecare copie ajusta două contoare de referințe și elibera valorile anterioare. Preflight-ul vizitează fiecare nod al fiecărui arbore de obiecte, scalarii incluși, deci acel cost multiplica numărul de obiecte cu numărul de reguli, iar regulile care nici măcar nu se aplicau versiunii țintă erau copiate mai întâi și sărite apoi. Întrucât PDFFeatureRules e umplut o dată la inițializarea unit-ului și tratat ca read-only, v3.539.17 trece intrările tabelei direct spre MatchSingleRule și RuleExceedsTarget, ale căror parametri const Rule iau o referință fără să atingă șirurile

Accelerarea scanului de reguli în PDFlibPas: preflight-ul copia înainte fiecare înregistrare TPDFFeatureRule înainte de testare, ajustând contoarele de referințe AnsiString pentru fiecare obiect vizitat, în timp ce parametrii const citesc acum tabela read-only în loc, reducând medianul rundei de potrivire a regulilor de la 0,711 s la 0,203 s
Câștigul e real dar îngust: o salvare completă plătește și detecția întârziată de funcționalități, decodarea obiectelor și serializarea, deci raportul măsurat aparține căii de potrivire a regulilor, nu timpului total de salvare
// Înainte: o copie de record gestionat per regulă, per obiect vizitat
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// După: parametrii const citesc intrarea imuabilă a tabelei în loc
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;

Efectul măsurat e îngust și ar trebui citat ca atare. Benchmark-ul verifică un tablou de 20.000 de obiecte numerice împotriva unei ținte PDF 1.4 de zece ori per rundă; compilat cu FPC Win64 la -O2, medianul a cinci runde a coborât de la 0,711 s la 0,203 s, iar rularea celor două build-uri în ordine inversă a dat 0,459 s față de 0,150 s. E aproximativ un câștig de 3x doar pe calea de potrivire a regulilor. O salvare reală plătește și detecția întârziată de funcționalități, decodarea obiectelor și serializarea, deci raportul nu se transpune pe timpul total de salvare. Ordinea regulilor, callback-urile, pragurile de versiune și diagnosticul primei eșecări sunt neschimbate, iar nicio regulă n-a fost ținută în cache peste salvări sau sărită ca să se ajungă aici

Ce verificați când un PDF încărcat pică la preflight-ul de versiune?

Citiți cheile 311 și 312 înainte să atingeți versiunea. Dacă funcționalitatea numește un dicționar geospatial și fișierul doar desenează măsurători rectilinii, acesta era falsul pozitiv de față, iar un build curent salvează fișierul neschimbat. Dacă funcționalitatea e autentică, ori declarați extensia, ori blocați o versiune care conține onest conținutul; ridicarea versiunii doar ca să amuțească poarta ascunde întrebarea dacă consumatorii din aval pot citi ce livrați. Același principiu al verificărilor mărginite și susținute de dovezi guvernează preflight-ul PDF/E-1 în mod autor pentru documente de inginerie, unde desenele CAD întâlnesc un standard de conformitate, nu un număr de versiune

Verificările de conformitate de versiune, dicționarele de măsurare și geospatiale și blocarea versiunii de salvare fac toate parte din PDF Library for Delphi, kit-ul PDFlibPas pentru dezvoltatorii Delphi, C++Builder și Lazarus