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