Tehnički članak

PDF preflight verzije u Delphi-ju: RL i GEO Measure pravila

PDFlibPas (PDF Library for Delphi) proverava svaki objekt naspram tabele pravila PDF verzije pre nego što upiše fajl, i do skoro je taj PDF version preflight uzimao obične CAD mjerne rečnike za geospatijalne. CAD crtež od jedne stranice uredno se učitao, a onda je SaveToFile vratio 0 sa LastErrorCode 602 i zahtevao 1.7 ExtensionLevel 3. Ispravljena pravila tretiraju rectilinear /Measure rečnike (/Subtype /RL) kao običan PDF 1.6 i ostavljaju extension kapiju za prave geospatijalne markere

Fajl je ušao kroz corpus prijem: jedna stranica, jedna optional-content grupa, dva rectilinear mjerna viewport-a, vrsta izlaza koji arhitektonski CAD paket piše da bi pregledač mogao da očita rastojanja sa osnova plana. Ništa kod njega nije bilo egzotično, i baš zato je odbijanje i bilo važno. Preflight koji blokira validan fajl je gori od sporog, jer pozivalac dobija dijagnostiku koja deluje autoritetno i pokazuje na mogućnost koju dokument uopšte ne sadrži. Popravka je imala dva dela: čitanje specifikacije iza jednog pravila, i spoznaju da pravilo na nivou na kom je gledalo ne može da razlikuje dva tipa rečnika

Kako radi save-time version preflight u PDFlibPas-u?

Save kapija, PrepareAndCheckSaveVersion, poredi svaki indirektan objekt naspram PDFFeatureRules i pada na prvom pravilu koje se i poklopi i traži više nego što target dozvoljava. Target je verzija dokumenta (ili verzija prikovana kroz LockSaveVersion), plus Adobe extension level deklarisan pod /Extensions /ADBE. Svaki TPDFFeatureRule zapis nosi MinVersion, MinExtensionLevel, MatchKind poput fmkDictKey ili fmkDictSubtype, Match string, čoveku čitljivo Feature ime i opcionu callback funkciju. AddRule registruje obično version pravilo; AddExtensionRule uvek pribija MinVersion na 17 i dodaje extension level preko toga, pa extension pravilo može zadovoljiti samo PDF 1.7 plus pravi /Extensions unos. Kada kapija zapne, potrebna verzija i ime mogućnosti čuvaju se za pozivaoca, i GetInformation ključevi 311, 312 i 313 ih izlažu

Save-time version preflight u PDFlibPas-u: PrepareAndCheckSaveVersion poredi svaki objekt sa PDFFeatureRules zapisima koji nose MinVersion, extension level i match kind, AddExtensionRule pribija zahtev na PDF 1.7 plus extension level, i prvo poklapanje koje target ne može da zadovolji zaustavlja čuvanje sa greškom 602
GetInformation ključevi 311, 312 i 313 pretvaraju odbijanje u dijagnozu, prijavljujući potrebnu verziju, mogućnost koja ju je okida i zaključani target, pa pozivalac može da sredi fajl ili pravilo umesto da nagađa
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: potrebna verzija, 312: mogućnost koja ju je okidala,
        // 313: verzija na koju je save target zaključan ('' kad nije zaključan)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Zašto je običan CAD crtež pao sa greškom 602?

Tabela pravila je sadržala AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), koja je pucala na svaki rečnik koji samo ima /Measure ključ, a svaki mjerni viewport ga ima. /VP niz stranice drži viewport rečnike, svaki viewport pokazuje na svoj measure rečnik kroz /Measure, i poklapanje po prisustvu ključa tu je stalo, bez gledanja šta je measure rečnik zapravo bio. Load-time feature sken mogao je tada da podigne broj verzije dokumenta na 1.7, ali nikada ne upisuje /Extensions deklaraciju u ime ulaznog fajla, pa je save kapija videla PDF 1.7 na extension level 0 i prijavila 1.7 ExtensionLevel 3. To odbijanje da izmišlja extension deklaraciju je namerno: biblioteka ne unapređuje tiho ulazni fajl da bi prekriala pogrešno pravilo

Specifikacija je za rectilinear slučaj nedvosmislena. Measure rečnici su stigli sa PDF 1.6, i ISO 32000-1 §12.9 daje /Subtype-u podrazumevanu vrednost RL, rectilinear koordinatni sistem opisan sopstvenim setom unosa: odnos razmere, X i Y formati brojeva, rastojanje i površina. Geospatijalno merenje je kasniji dodatak iz Adobe Extension Level 3 preko PDF 1.7, prepoznatljiv po /Subtype /GEO, sa geografskim nizovima tačaka, rečnicima koordinatnih sistema i jedinicama prikaza, strukturama kroz koje prolazi čitanje GeoPDF viewport-a, GPTS i LPTS nizova u Delphi-ju. Oba rečnika vise sa istog /Measure ključa, pa nijedno pravilo koje stane na ključu ne može biti ispravno za oba. Razlikujući podatak sedi jedan nivo niže, u samom measure rečniku

Jedan /Measure ključ, dva rečnika u PDFlibPas-u: rectilinear merenje, sa /RL ili izostavljenim subtype-om, treba samo PDF 1.6, dok geospatijalni rečnik traži 1.7 ExtensionLevel 3, pa CB_GeospatialDictionary odlučuje po sadržaju tamo gde staro ključ pravilo nije moglo da ih razlikuje
Preflight koji blokira validan fajl je gori od sporog, jer pozivalac dobija autoritetnu dijagnostiku o mogućnosti koju dokument nikada nije imao, pa je razlikujuća provera i premeštena jedan nivo niže

Šta ispravljeni set pravila i dalje nameće?

Popravka briše bezuslovno ključ pravilo i ostavlja kapije koje opisuju stvarne zahteve verzije. Stranica koja nosi /VP ili /UserUnit i dalje traži PDF 1.6 kroz CB_PagePDF16Entries, /PtData ključ i dalje traži extension level 3, a CB_GeospatialDictionary odlučuje da li je measure rečnik geospatijalan po svom sadržaju, a ne po ključu kojim je došao

// Uklonjeno: svaki rečnik sa /Measure ključem računao se kao geospatijalan
// 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;

Zajedničke Delphi i FPC regresije pribijaju tu granicu sa obe strane. Viewport čiji measure rečnik izostavlja /Subtype i onaj koji izričito napiše /RL oba prolaze na PDF 1.6, ista stranica se i dalje odbija na PDF 1.5, a feature detekcija više ne prijavljuje extension za nju. Dodavanje /GPTS niza vraća presudu na 1.7 ExtensionLevel 3, što prolazi čim se extension level deklariše, a gol /Subtype /GEO rečnik se odbija bez toga. Callback je po dizajnu konzervativan: rectilinear rečnik koji nosi i odmetnuti /GCS ili /PDU ključ tretira se kao geospatijalan, jer ti ključevi nemaju značenje u RL modelu

LockSaveVersion je mesto na kom ova promena postaje vidljiva pozivaocima. TPDFlib.LockSaveVersion prihvata '1.0' do '1.7', za sve ostalo vraća 0, pribija verziju dokumenta i sprečava pisane pozive da je tiho podignu, a save kapija i dalje radi protiv zaključane vrednosti. Sa ispravljenim pravilima, CAD fajl zaključan na 1.6 čuva se čisto. Pravi GeoPDF zaključan na 1.6 i dalje dobija 602, što je ispravan odgovor, a geospatijalni autorski pozivi poput SetMeasureDictCoordinateSystem sami deklarišu extension level 3 kad taj sadržaj gradite kroz 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
    // Pravi sadržaj iznad 1.6, na primer GEO measure rečnik
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

Zašto je sken version pravila bio sporiji nego što je trebalo?

Sken je kopirao svaki TPDFFeatureRule u lokalni zapis pre testiranja, i pošto zapis nosi dva AnsiString polja, svaka kopija je korigovala dva brojača referenci i oslobađala prethodne vrednosti. Preflight posećuje svaki čvor svakog objektnog drveta, skalare uključivo, pa se taj trošak množio broj objekata sa brojem pravila, a pravila koja se na ciljnu verziju uopšte ne odnose kopirala su se prvo pa preskakala posle. Pošto je PDFFeatureRules popunjen jednom pri inicijalizaciji jedinice i tretiran kao read-only, v3.539.17 prosleđuje unose tabele pravo u MatchSingleRule i RuleExceedsTarget, čiji const Rule parametri uzimaju referencu bez diranja stringova

Ubrzanje skena pravila u PDFlibPas-u: preflight je kopirao svaki TPDFFeatureRule zapis pre testiranja, korigujući AnsiString brojače referenci za svaki posećeni objekat, dok const parametri sada čitaju read-only tabelu na mestu, spustivši median kruga poklapanja pravila sa 0,711 s na 0,203 s
Dobit je stvarna ali uska: puno čuvanje plaća i odloženu feature detekciju, dekodovanje objekata i serijalizaciju, pa izmerena proporcija pripada putanji poklapanja pravila, a ne ukupnom vremenu čuvanja
// Pre: upravljana kopija zapisa po pravilu, po posećenom objektu
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// Posle: const parametri čitaju nepromenljivi unos tabele na mestu
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;

Izmereni efekat je uzak i tako treba i citirati ga. Benchmark proverava niz od 20.000 numeričkih objekata naspram PDF 1.4 targeta deset puta po krugu; kompajliran sa FPC Win64 na -O2, median od pet krugova pao je sa 0,711 s na 0,203 s, a pokretanje dva builda obrnutim redosledom dalo je 0,459 s naspram 0,150 s. To je otprilike 3x dobitak samo na putanji poklapanja pravila. Pravo čuvanje plaća i odloženu feature detekciju, dekodovanje objekata i serijalizaciju, pa se proporcija ne prenosi na ukupno vreme čuvanja. Redosled pravila, callback-ovi, pragovi verzija i dijagnostika prvog pada su nepromenjeni, i nijedno pravilo nije keširano između čuvanja niti preskočeno da bi se došlo dovde

Šta proveriti kad učitani PDF padne na version preflight-u?

Pročitajte ključeve 311 i 312 pre nego što dirate verziju. Ako mogućnost imenuje geospatijalni rečnik a fajl samo crta rectilinear merenja, to je bio ovaj lažni pozitiv, i aktuelni build čuva fajl nepromenjen. Ako je mogućnost prava, ili deklarišite extension ili se zaključajte na verziju koja sadržaj iskreno sadrži; podizanje verzije samo da se kapija utiša krije pitanje mogu li nizvodni potrošači da pročitaju ono što isporučujete. Isti princip ograničenih, dokazima potkrepljenih provera pokreće PDF/E-1 author-mode preflight za inženjerske dokumente, gde CAD crteži meru sa standardom usaglašenosti, a ne sa brojem verzije

Provere usaglašenosti verzije, mjerni i geospatijalni rečnici i zaključavanje verzije čuvanja deo su PDF Library for Delphi, PDFlibPas alata za Delphi, C++Builder i Lazarus developere