PDFlibPas (PDF Library for Delphi) kontrollerar varje objekt mot en tabell av PDF-versionsregler innan den skriver en fil, och fram till nyligen förväxlade den versionspreflighten vanliga CAD-mätningsordböcker med geospatiala. En CAD-ritning på en sida lästes in fint, och sedan returnerade SaveToFile 0 med LastErrorCode 602 och krävde 1.7 ExtensionLevel 3. De korrigerade reglerna behandlar rectilinear /Measure-ordböcker (/Subtype /RL) som vanlig PDF 1.6 och reserverar extension-grinden för riktiga geospatiala markörer
Filen kom in via corpusantagning: en sida, en optional content-grupp, två rectilinear-mätviewports, den typ av utdata ett arkitektur-CAD-paket skriver så att en viewer kan läsa av mått från en planritning. Ingenting med den var exotiskt, vilket är precis varför vägran spelade roll. En preflight som blockerar en giltig fil är värre än en långsam, för att anroparen får en diagnostik som ser auktoritativ ut och pekar på en funktion dokumentet inte innehåller. Fixen tog två delar: specifikationsläsningen bakom en regel, och insikten att regeln inte kunde skilja två ordbokstyper åt på den nivå den tittade
Hur fungerar PDFlibPas versionspreflight vid sparande?
Spargrinden, PrepareAndCheckSaveVersion, jämför varje indirekt objekt mot PDFFeatureRules och misslyckas på den första regel som både matchar och kräver mer än målet tillåter. Målet är dokumentversionen (eller versionen låst av LockSaveVersion), plus Adobe extension level deklarerad under /Extensions /ADBE. Varje TPDFFeatureRule-post bär en MinVersion, en MinExtensionLevel, en MatchKind som fmkDictKey eller fmkDictSubtype, en Match-sträng, ett människoläsbart Feature-namn och en valfri callback. AddRule registrerar en enkel versionsregel; AddExtensionRule låser alltid MinVersion vid 17 och lägger till en extension level ovanpå, så en extensionsregel bara kan uppfyllas av PDF 1.7 plus rätt /Extensions-post. När grinden utlöses sparas den krävda versionen och funktionsnamnet för anroparen, och GetInformation-nycklarna 311, 312 och 313 exponerar dem
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: krävd version, 312: funktionen som utlöste den,
// 313: versionen som sparmålet är låst vid ('' när olåst)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Varför misslyckades en vanlig CAD-ritning med fel 602?
Regeltabellen innehöll AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), som utlöses av varje ordbok som bara hade en /Measure-nyckel, och varje mätviewport har en. Sidans /VP-array innehåller viewportordböcker, varje viewport pekar på sin measure-ordbok genom /Measure, och nyckeltillvaromatchen stannade där utan att titta på vad measure-ordboken faktiskt var. Funktionsgenomgången vid inläsning kunde sedan höja dokumentversionsnumret till 1.7, men den skriver aldrig en /Extensions-deklaration å en infils vägnar, så spargrinden såg PDF 1.7 på extension level 0 och rapporterade 1.7 ExtensionLevel 3. Den vägran att hitta på en extensionsdeklaration är medveten: biblioteket befordrar inte tyst en infil för att kleta över en regel som är fel
Specifikationen är entydig om rectilinear-fallet. Measure-ordböcker kom i PDF 1.6, och ISO 32000-1 §12.9 ger /Subtype standardvärdet RL, ett rectilinear-koordinatsystem beskrivet av sin egen uppsättning poster: skalförhållande, X- och Y-talformat, längd och yta. Geospatial mätning är det senare tillägget från Adobe Extension Level 3 ovanpå PDF 1.7, identifierat av /Subtype /GEO och bärande geografiska punktarrayer, koordinatsystemordböcker och visningsenheter, de strukturer som gås igenom i att läsa GeoPDF-viewports, GPTS- och LPTS-arrayer i Delphi. Båda ordböckerna hänger på samma /Measure-nyckel, så en regel som stannar vid nyckeln kan inte vara rätt för båda. Den skiljande informationen ligger en nivå ned, i measure-ordboken själv
Vad fortsätter det korrigerade regelset att kräva?
Fixen tar bort den ovillkorade nyckelregeln och låter grindarna som beskriver riktiga versionskrav vara kvar. En sida som bär /VP eller /UserUnit kräver fortfarande PDF 1.6 genom CB_PagePDF16Entries, en /PtData-nyckel kräver fortfarande extension level 3, och CB_GeospatialDictionary avgör om en measure-ordbok är geospatial utifrån sitt innehåll snarare än nyckeln den nåddes genom
// Borttaget: varje ordbok med en /Measure-nyckel räknades som 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;
De gemensamma Delphi- och FPC-regressionerna spikar den gränsen från båda håll. En viewport vars measure-ordbok utelämnar /Subtype och en som stavar ut /RL passerar båda vid PDF 1.6, samma sida avvisas fortfarande vid PDF 1.5, och funktionsdetekteringen rapporterar inte längre en extension för den. Att lägga till en /GPTS-array vänder omdömet tillbaka till 1.7 ExtensionLevel 3, vilket passerar så fort extension level är deklarerad, och en naken /Subtype /GEO-ordbok vägras utan den. Callbacken är konservativ av design: en rectilinear-ordbok som också bär en vilsekommen /GCS- eller /PDU-nyckel behandlas som geospatial, eftersom de nycklarna inte har någon mening i RL-modellen
LockSaveVersion är där den här ändringen blir synlig för anropare. TPDFlib.LockSaveVersion accepterar '1.0' genom '1.7', returnerar 0 för allt annat, låser dokumentversionen och hindrar skrivarsidans anrop från att tyst höja den, men spargrinden körs ändå mot det låsta värdet. Med de korrigerade reglerna sparar en CAD-fil låst vid 1.6 rent. En äkta GeoPDF låst vid 1.6 får fortfarande 602, vilket är det rätta svaret, och de geospatiala författaranropen som SetMeasureDictCoordinateSystem deklarerar själva extension level 3 när du bygger det innehållet genom API:t
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
// Verkligt innehåll över 1.6, till exempel en GEO-measure-ordbok
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Varför var versionsregelskanningen långsammare än den behövde vara?
Skanningen kopierade varje TPDFFeatureRule till en lokal post innan den testades, och eftersom posten håller två AnsiString-fält justerade varje kopia två referensräknare och släppte de tidigare värdena. Preflighten besöker varje nod i varje objektträd, skalärer inkluderade, så den kostnaden multiplicerade objektantal med regelantal, och regler som inte ens gällde målversionen kopierades först och hoppades över sedan. Eftersom PDFFeatureRules fylls en gång vid unitinitiering och behandlas som skrivskyddad, skickar v3.539.17 tabellposter rakt till MatchSingleRule och RuleExceedsTarget, vars const Rule-parametrar tar en referens utan att röra strängarna
// Före: en hanterad postkopia per regel, per besökt objekt
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Efter: const-parametrar läser den oföränderliga tabellposten på plats
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;
Den uppmätta effekten är smal och bör citeras på det sättet. Benchmarket kontrollerar en array med 20 000 numeriska objekt mot ett PDF 1.4-mål tio gånger per runda; byggt med FPC Win64 vid -O2 föll medianen av fem rundor från 0,711 s till 0,203 s, och att köra de två byggena i omvänd ordning gav 0,459 s mot 0,150 s. Det är ungefär en 3x-vinst på bara regelmatchningsvägen. Ett verkligt sparande betalar också för uppskjuten funktionsdetektering, objektavkodning och serialisering, så kvoten överförs inte till total sparningstid. Regelordning, callbacks, versionströsklar och första-fel-diagnostiken är oförändrade, och ingen regel cachades över sparanden eller hoppades över för att nå dit
Vad bör du kontrollera när en inläst PDF faller på versionspreflighten?
Läs nycklarna 311 och 312 innan du rör versionen. Om funktionen namnger en geospatial ordbok och filen bara ritar rectilinear-mätningar var det här falska positivet, och ett aktuellt bygge sparar filen oförändrad. Om funktionen är äkta, deklarera antingen extensionen eller lås till en version som ärligt innehåller innehållet; att höja versionen bara för att tysta grinden gömmer frågan om nedströmskonsumenter kan läsa det du levererar. Samma princip om avgränsade, evidensbaserade kontroller driver PDF/E-1-författarlägespreflighten för ingenjörsdokument, där CAD-ritningar möts av en konformitetsstandard i stället för ett versionsnummer
Versionskompatibilitetskontroller, measure- och geospatiala ordböcker och sparningsversionslåsning ingår alla i PDF Library for Delphi, PDFlibPas-verktygslådan för Delphi-, C++Builder- och Lazarus-utvecklare