Teknisk artikel

PDF-versionspreflight i Delphi: Measure RL- och GEO-regler

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

Versionspreflight vid sparande i PDFlibPas: PrepareAndCheckSaveVersion jämför varje objekt mot PDFFeatureRules-poster med MinVersion, en extension level och en matchkind, AddExtensionRule låser kravet vid PDF 1.7 plus en extension level, och den första match målet inte kan uppfylla stoppar sparandet med fel 602
GetInformation-nycklarna 311, 312 och 313 gör en vägran till en diagnos, och rapporterar den krävda versionen, funktionen som utlöste den och det låsta målet, så anroparen kan fixa filen eller regeln i stället för att gissa
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

En /Measure-nyckel, två ordböcker i PDFlibPas: rectilinear-mätning, med /RL eller subtypen utelämnad, kräver bara PDF 1.6, medan en geospatial ordbok kräver 1.7 ExtensionLevel 3, så CB_GeospatialDictionary avgör utifrån innehåll där den gamla nyckelregeln inte kunde skilja dem åt
En preflight som blockerar en giltig fil är värre än en långsam, för att anroparen får en auktoritativ diagnostik om en funktion dokumentet aldrig innehöll, vilket är varför den skiljande kontrollen flyttades en nivå ned

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

Regelskanningssnabbning i PDFlibPas: preflighten brukade kopiera varje TPDFFeatureRule-post innan den testades, med justering av AnsiString-referensräknare för varje besökt objekt, medan const-parametrar nu läser den skrivskyddade tabellen på plats, vilket sänkte medianen för en regelmatchningsrunda från 0,711 s till 0,203 s
Vinsten är verklig men smal: ett fullt sparande betalar också för uppskjuten funktionsdetektering, objektavkodning och serialisering, så den uppmätta kvoten gäller regelmatchningsvägen och inte den totala sparningstiden
// 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