Technický článek

PDF version preflight v Delphi: pravidla Measure RL vs GEO

PDFlibPas (PDF Library for Delphi) kontroluje každý objekt proti tabulce pravidel PDF verzí, než zapíše soubor, a ještě donedávna si tenhle PDF version preflight pletl obyčejné CAD měřicí slovníky s geospatial. Jednostránková CAD kresba se načetla v pohodě a pak SaveToFile vrátilo 0 s LastErrorCode 602 a žádal si 1.7 ExtensionLevel 3. Opravená pravidla berou rectilinear slovníky /Measure (/Subtype /RL) jako obyčejné PDF 1.6 a extension bránu nechávají pro opravdové geospatial markery

Soubor přišel přes corpus přijetí: jedna stránka, jedna optional-content skupina, dva rectilinear měřicí viewporty, takový výstup, jaký píše architektonický CAD balík, aby si prohlížeč mohl přečíst vzdálenosti z půdorysu. Nic na něm nebylo exotické, a přesně proto to odmítnutí bolelo. Preflight, který blokuje validní soubor, je horší než pomalý, protože volající dostane diagnostiku vypadající autoritativně a ukazující na funkci, kterou dokument neobsahuje. Oprava měla dvě části: čtení specifikace za jedním pravidlem a uvědomění, že pravidlo na úrovni, kde se dívalo, nedokázalo rozlišit dva typy slovníků

Jak funguje save-time version preflight PDFlibPas?

Save brána PrepareAndCheckSaveVersion srovnává každý nepřímý objekt proti PDFFeatureRules a padá na prvním pravidle, které jak sedne, tak vyžaduje víc, než cíl dovoluje. Cílem je verze dokumentu (nebo verze připíchnutá LockSaveVersion) plus Adobe extension level deklarovaný pod /Extensions /ADBE. Každý záznam TPDFFeatureRule nese MinVersion, MinExtensionLevel, MatchKind jako fmkDictKey nebo fmkDictSubtype, řetězec Match, lidsky čitelné jméno Feature a volitelný callback. AddRule registruje obyčejné version pravidlo; AddExtensionRule vždy připíchne MinVersion na 17 a navrch přidá extension level, takže extension pravidlo může uspokojit jen PDF 1.7 plus správná položka /Extensions. Když brána zaklapne, požadovaná verze a jméno funkce se nechají pro volajícího a klíče GetInformation 311, 312 a 313 je vystaví

Save-time version preflight v PDFlibPas: PrepareAndCheckSaveVersion srovnává každý objekt proti záznamům PDFFeatureRules nesoucím MinVersion, extension level a match kind, AddExtensionRule připíchne požadavek na PDF 1.7 plus extension level a první shoda, kterou cíl nemůže uspokojit, zastaví save s chybou 602
Klíče GetInformation 311, 312 a 313 mění odmítnutí na diagnózu, hlásí požadovanou verzi, funkci, která ji spustila, a zamčený cíl, takže volající může opravit soubor nebo pravidlo, místo aby hádal
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: požadovaná verze, 312: funkce, která ji spustila,
        // 313: verze, na kterou je save cíl zamčený ('' když odemčeno)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Proč obyčejná CAD kresba selhala s chybou 602?

Tabulka pravidel obsahovala AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), které se spustilo na jakémkoli slovníku, který měl pouze klíč /Measure, a každý měřicí viewport ho má. Pole /VP stránky drží viewport slovníky, každý viewport ukazuje na svůj measure slovník přes /Measure a shoda na přítomnost klíče tam zastavila, aniž by se podívala, čím ten measure slovník doopravdy je. Feature sken při načítání pak mohl zvýšit číslo verze dokumentu na 1.7, ale nikdy nepíše deklaraci /Extensions na jméno vstupního souboru, takže save brána viděla PDF 1.7 na extension level 0 a nahlásila 1.7 ExtensionLevel 3. Tohle odmítnutí si deklaraci vymyslet je záměrné: knihovna nepovýšuje potichu vstupní soubor, aby zametla pod koberec špatné pravidlo

Specifikace je v rectilinear případě jednoznačná. Measure slovníky přišly s PDF 1.6 a ISO 32000-1 §12.9 dává /Subtype default RL, rectilinear souřadnicový systém popsaný vlastní sadou položek: poměr měřítka, číselné formáty X a Y, vzdálenost a plocha. Geospatial měření je pozdější přírůstek z Adobe Extension Level 3 navrch na PDF 1.7, identifikovaný přes /Subtype /GEO a nesoucí geografická pole bodů, slovníky souřadnicových systémů a display jednotky, struktury, kterými prochází článek o čtení GeoPDF viewportů, polí GPTS a LPTS v Delphi. Oba slovníky visí na tom samém klíči /Measure, takže pravidlo, které se zastaví na klíči, nemůže sedět pro oba. Rozlišující informace sedí o úroveň níž, v samotném measure slovníku

Jeden klíč /Measure, dva slovníky v PDFlibPas: rectilinear měření, s /RL nebo vynechaným subtypem, potřebuje jen PDF 1.6, zatímco geospatial slovník potřebuje 1.7 ExtensionLevel 3, takže CB_GeospatialDictionary rozhoduje podle obsahu tam, kde staré klíčové pravidlo je nerozeznalo
Preflight, který blokuje validní soubor, je horší než pomalý, protože volající dostane autoritativní diagnostiku o funkci, kterou dokument nikdy neobsahoval, a proto se rozlišující kontrola přesunula o úroveň níž

Co opravená sada pravidel pořád vynucuje?

Oprava maže nepodmíněné klíčové pravidlo a nechává brány popisující opravdové požadavky verzí. Stránka nesoucí /VP nebo /UserUnit pořád potřebuje PDF 1.6 přes CB_PagePDF16Entries, klíč /PtData pořád potřebuje extension level 3 a CB_GeospatialDictionary rozhoduje, jestli je measure slovník geospatial, podle svého obsahu, ne podle klíče, přes který dorazil

// Odstraněno: každý slovník s klíčem /Measure se počítal jako 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;

Sdílené Delphi a FPC regrese připichují tu hranici z obou stran. Viewport, jehož measure slovník vynechá /Subtype, i ten, který /RL napíše naplno, projdou na PDF 1.6, tatáž stránka se pořád odmítne na PDF 1.5 a feature detekce už pro ni extension nehlásí. Přidání pole /GPTS otočí verdikt zpátky na 1.7 ExtensionLevel 3, což projde, jakmile se extension level deklaruje, a holý slovník /Subtype /GEO se bez něj odmítne. Callback je záměrně konzervativní: rectilinear slovník, který taky nese zbloudilý klíč /GCS nebo /PDU, se bere jako geospatial, protože ty klíče v RL modelu nemají význam

LockSaveVersion je místo, kde se tahle změna ukáže volajícím. TPDFlib.LockSaveVersion přijímá '1.0' až '1.7', na cokoli jiného vrátí 0, připíchne verzi dokumentu a zastaví volání na straně writeru, aby ji potichu nezvyšovala, a přesto save brána běží proti zamčené hodnotě. S opravenými pravidly se CAD soubor zamčený na 1.6 uloží čistě. Skutečný GeoPDF zamčený na 1.6 dostane pořád 602, což je správná odpověď, a geospatial autorská volání, jako SetMeasureDictCoordinateSystem, deklarují extension level 3 sama, když tohle obsah stavíte přes 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
    // Skutečný obsah nad 1.6, například GEO measure slovník
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

Proč byl sken version pravidel pomalejší, než musel být?

Sken kopíroval každý TPDFFeatureRule do lokálního záznamu, než ho otestoval, a protože záznam drží dvě pole AnsiString, každá kopie upravila dva reference county a uvolnila předchozí hodnoty. Preflight navštíví každý uzel každého objektového stromu, scalars nevyjímaje, takže ta cena se násobila počet objektů krát počet pravidel a pravidla, která se na cílovou verzi ani nevztahovala, se nejdřív zkopírovala a pak přeskočila. Protože PDFFeatureRules se plní jednou při inicializaci unity a bere se jako read-only, v3.539.17 předává položky tabulky rovnou do MatchSingleRule a RuleExceedsTarget, jejichž parametry const Rule berou referenci, aniž by sahaly na řetězce

Zrychlení skenu pravidel v PDFlibPas: preflight dřív kopíroval každý záznam TPDFFeatureRule, než ho otestoval, a upravoval AnsiString reference county pro každý navštívený objekt, zatímco const parametry teď čtou read-only tabulku na místě, čímž median kola rule-matching klesl z 0.711 s na 0.203 s
Zisk je skutečný, ale úzký: plný save platí taky za odloženou feature detekci, dekódování objektů a serializaci, takže naměřený poměr patří cestě rule-matching, ne celkovému času save
// Před: kopie managed záznamu na pravidlo, na navštívený objekt
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// Po: const parametry čtou neměnnou položku tabulky na místě
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;

Naměřený efekt je úzký a měl by se citovat takhle. Benchmark kontroluje pole 20 000 numerických objektů proti cíli PDF 1.4 desetkrát za kolo; build s FPC Win64 na -O2, median pěti kol klesl z 0.711 s na 0.203 s a puštění obou buildů v obráceném pořadí dalo 0.459 s proti 0.150 s. To je zhruba 3× zisk jen na cestě rule-matching. Skutečný save platí taky za odloženou feature detekci, dekódování objektů a serializaci, takže ten poměr se nepřenáší na celkový čas save. Pořadí pravidel, callbacky, prahy verzí a first-failure diagnostika zůstávají nezměněné a žádné pravidlo se necachovalo přes save ani nepřeskakovalo

Co zkontrolovat, když načtené PDF neprojde version preflight?

Přečtěte si klíče 311 a 312, než sáhnete na verzi. Pokud funkce jmenuje geospatial slovník a soubor kreslí jen rectilinear měření, byl tohle false positive a aktuální build uloží soubor beze změny. Pokud je funkce skutečná, buď deklarujte extension, nebo se zamkněte na verzi, která obsah upřímně obsahuje; zvýšení verze jen proto, abyste umlčeli bránu, schovává otázku, jestli downstream konzumenti přečtou to, co dodáváte. Týž princip ohraničených, důkazy podložených kontrol řídí PDF/E-1 author-mode preflight pro inženýrské dokumenty, kde CAD kresby potkávají konformance standard místo čísla verze

Kontroly version compliance, měřicí a geospatial slovníky a zamykání save verze jsou všechny součástí PDF Library for Delphi, PDFlibPas toolkitu pro vývojáře v Delphi, C++Builderu a Lazarusu