Technisch artikel

PDF-versie-preflight in Delphi: Measure RL versus GEO

PDFlibPas (PDF Library for Delphi) toetst elk object aan een tabel met PDF-versieregels voordat hij een bestand wegschrijft, en tot voor kort zag die PDF-versie-preflight gewone CAD-measure-dictionaries aan voor geospatiale. Een CAD-tekening van één pagina laadde prima, waarna SaveToFile 0 teruggaf met LastErrorCode 602 en 1.7 ExtensionLevel 3 eiste. De gecorrigeerde regels behandelen rectilineaire /Measure-dictionaries (/Subtype /RL) als gewone PDF 1.6 en reserveren de extension-gate voor echte geospatiale markeringen

Het bestand kwam binnen via corpus-toelating: één pagina, één optional-content-groep, twee rectilineaire meetviewports, het soort uitvoer dat een architectuur-CAD-pakket schrijft zodat een viewer afstanden van een plattegrond kan aflezen. Er was niets exotisch aan, en dat is precies waarom de weigering ertoe deed. Een preflight die een geldig bestand blokkeert is erger dan een trage, want de aanroeper krijgt een diagnose die autoriteit uitstraalt en wijst naar een feature die het document helemaal niet bevat. De fix bestond uit twee delen: de specificatie-lezing achter één regel, en het besef dat de regel op het niveau waar hij keek twee dictionarytypes niet uit elkaar kon houden

Hoe werkt de save-time-versiepreflight van PDFlibPas?

De save-gate, PrepareAndCheckSaveVersion, vergelijkt elk indirect object met PDFFeatureRules en faalt op de eerste regel die zowel matcht als meer nodig heeft dan het doel toestaat. Het doel is de documentversie (of de versie die LockSaveVersion heeft vastgezet), plus het Adobe extension level dat onder /Extensions /ADBE is aangegeven. Elk TPDFFeatureRule-record draagt een MinVersion, een MinExtensionLevel, een MatchKind zoals fmkDictKey of fmkDictSubtype, een Match-string, een mensleesbare Feature-naam en een optionele callback. AddRule registreert een gewone versieregel; AddExtensionRule zet MinVersion altijd op 17 en stapelt er een extension level bovenop, dus een extension-regel kan uitsluitend worden vervuld door PDF 1.7 plus de juiste /Extensions-entry. Als de gate af gaat worden de vereiste versie en de featurenaam voor de aanroeper bewaard, en de GetInformation-sleutels 311, 312 en 313 leggen ze bloot

Save-time-versiepreflight in PDFlibPas: PrepareAndCheckSaveVersion vergelijkt elk object met PDFFeatureRules-records met MinVersion, een extension level en een match-kind, AddExtensionRule zet de eis vast op PDF 1.7 plus een extension level, en de eerste match die het doel niet kan vervullen stopt de save met fout 602
De GetInformation-sleutels 311, 312 en 313 maken van een weigering een diagnose, met de vereiste versie, de feature die haar afdwong en het vastgezette doel, zodat de aanroeper het bestand of de regel kan fixen in plaats van te gokken
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: vereiste versie, 312: feature die hem afdwong,
        // 313: versie waarop het save-doel is vastgezet ('' indien vrij)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Waarom faalde een gewone CAD-tekening met fout 602?

De regeltabel bevatte AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), die afging op elke dictionary die maar een /Measure-sleutel had, en elke meetviewport heeft er een. De /VP-array van de pagina bevat viewport-dictionaries, elke viewport wijst via /Measure naar zijn measure-dictionary, en de sleutel-aanwezigheid-match stopte daar zonder te kijken wat de measure-dictionary nu eigenlijk was. De feature-scan bij het laden kon daarmee het documentversienummer naar 1.7 verhogen, maar hij schrijft nooit namens een invoerbestand een /Extensions-declaratie, dus de save-gate zag PDF 1.7 op extension level 0 en meldde 1.7 ExtensionLevel 3. Die weigering om een extension-declaratie te verzinnen is opzettelijk: de library promoveert een invoerbestand niet stilletjes om een foutieve regel te verhullen

De specificatie is ondubbelzinnig over het rectilineaire geval. Measure-dictionaries kwamen met PDF 1.6, en ISO 32000-1 §12.9 geeft /Subtype een default van RL, een rectilineair coördinatensysteem beschreven door zijn eigen set entries: schaalverhouding, X- en Y-getalnotaties, afstand en oppervlakte. Geospatiale meting is de latere toevoeging uit Adobe Extension Level 3 bovenop PDF 1.7, te herkennen aan /Subtype /GEO en dragend geografische puntarrays, coördinatensysteem-dictionaries en weergave-eenheden, de structuren die in het lezen van GeoPDF-viewports, GPTS- en LPTS-arrays in Delphi worden doorlopen. Allebei de dictionaries hangen aan dezelfde /Measure-sleutel, dus elke regel die bij de sleutel stopt kan niet voor allebei kloppen. De onderscheidende informatie zit één niveau lager, in de measure-dictionary zelf

Eén /Measure-sleutel, twee dictionaries in PDFlibPas: rectilineaire meting, met /RL of zonder subtype, heeft alleen PDF 1.6 nodig, terwijl een geospatiale dictionary 1.7 ExtensionLevel 3 nodig heeft, dus CB_GeospatialDictionary beslist op inhoud waar de oude sleutelregel ze niet kon onderscheiden
Een preflight die een geldig bestand blokkeert is erger dan een trage, want de aanroeper krijgt een autoritaire diagnose over een feature die het document nooit bevatte, en daarom verschoof de onderscheidende controle één niveau omlaag

Wat handhaaft de gecorrigeerde regelset nog wel?

De fix schrapt de onvoorwaardelijke sleutelregel en laat de gates staan die echte versie-eisen beschrijven. Een pagina met /VP of /UserUnit heeft nog steeds PDF 1.6 nodig via CB_PagePDF16Entries, een /PtData-sleutel heeft nog steeds extension level 3 nodig, en CB_GeospatialDictionary beslist op basis van de inhoud of een measure-dictionary geospatiaal is, niet op basis van de sleutel waarmee hij werd bereikt

// Verwijderd: elke dictionary met een /Measure-key telde als geospatiaal
// 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 gedeelde Delphi- en FPC-regressies pinnen die grens aan beide kanten vast. Een viewport waarvan de measure-dictionary /Subtype weglaat en eentje die /RL uitschrijft komen allebei door bij PDF 1.6, dezelfde pagina wordt bij PDF 1.5 nog steeds geweigerd, en de featuredetectie meldt er geen extension meer voor. Een /GPTS-array toevoegen draait het oordeel terug naar 1.7 ExtensionLevel 3, wat passeert zodra het extension level is gedeclareerd, en een kale /Subtype /GEO-dictionary wordt zonder dat geweigerd. De callback is bewust conservatief: een rectilineaire dictionary die ook nog een verdwaalde /GCS- of /PDU-sleutel draagt wordt als geospatiaal behandeld, want die sleutels hebben in het RL-model geen betekenis

LockSaveVersion is waar deze verandering zichtbaar wordt voor aanroepers. TPDFlib.LockSaveVersion accepteert '1.0' tot en met '1.7', geeft 0 terug voor al het andere, zet de documentversie vast en verhindert dat writer-zijde aanroepen haar geruisloos verhogen, maar de save-gate draait nog steeds tegen de vastgezette waarde. Met de gecorrigeerde regels slaat een CAD-bestand dat op 1.6 is vastgezet vlekkeloos op. Een echte GeoPDF vastgezet op 1.6 krijgt nog steeds 602, en dat is het juiste antwoord, en de geospatiale authoring-aanroepen zoals SetMeasureDictCoordinateSystem declareren extension level 3 zelf wanneer u die inhoud via de API bouwt

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
    // Echte inhoud boven 1.6, bijvoorbeeld een GEO-measure-dictionary
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

Waarom was de versieregel-scan trager dan nodig?

De scan kopieerde elke TPDFFeatureRule naar een lokaal record voordat hij hem toetste, en omdat het record twee AnsiString-velden draagt hield elke kopie twee reference counts bij en gaf de vorige waarden vrij. De preflight bezoekt elk knooppunt van elke objectboom, scalars incluis, dus die kost vermenigvuldigde objectaantal met regelaantal, en regels die niet eens op de doelversie van toepassing waren werden eerst gekopieerd en daarna overgeslagen. Omdat PDFFeatureRules één keer bij de unit-initialisatie wordt gevuld en als read-only wordt behandeld, geeft v3.539.17 table-entries rechtstreeks door aan MatchSingleRule en RuleExceedsTarget, wier const Rule-parameters een referentie nemen zonder de strings aan te raken

Versnelling van de regelscan in PDFlibPas: de preflight kopieerde vroeger elk TPDFFeatureRule-record vóór het toetsen, met bijstelling van AnsiString-reference counts per bezocht object, terwijl const-parameters nu de read-only tabel in place lezen, wat de mediaanronde van regel-matching van 0.711 s naar 0.203 s bracht
De winst is echt maar smal: een volledige save betaalt ook nog deferred featuredetectie, objectdecodering en serialisatie, dus de gemeten verhouding hoort bij het regel-matchpad en niet bij de totale savetijd
// Vroeger: een managed record-kopie per regel, per bezocht object
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// Nu: const-parameters lezen de immutabele table-entry in place
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;

Het gemeten effect is smal en moet ook zo geciteerd worden. De benchmark toetst een array van 20.000 numerieke objecten tien keer per ronde aan een PDF 1.4-doel; gebouwd met FPC Win64 op -O2 zakte de mediaan van vijf rondes van 0.711 s naar 0.203 s, en de twee builds in omgekeerde volgorde draaien gaf 0.459 s tegenover 0.150 s. Dat is grofweg een winst van 3x op alleen het regel-matchpad. Een echte save betaalt daarnaast nog deferred featuredetectie, objectdecodering en serialisatie, dus de verhouding doorschuift niet naar de totale savetijd. Regelvolgorde, callbacks, versiedrempels en de eerste-falen-diagnose zijn onveranderd, en geen enkele regel is over saves heen gecached of overgeslagen om dit te halen

Wat controleert u wanneer een geladen PDF de versiepreflight niet haalt?

Lees de sleutels 311 en 312 voordat u aan de versie zit. Noemt de feature een geospatiale dictionary en tekent het bestand alleen rectilineaire metingen, dan was dit die false positive, en een actuele build slaat het bestand onveranderd op. Is de feature echt, dan declareert u ofwel de extension ofwel zet u vast op een versie die de inhoud eerlijk bevat; de versie verhogen enkel om de gate de mond te snoeren verbergt de vraag of downstream-verbruikers kunnen lezen wat u uitlevert. Hetzelfde principe van begrensde, op bewijs gebaseerde controles drijft de PDF/E-1 author-mode-preflight voor engineeringdocumenten, waar CAD-tekeningen een conformiteitsnorm ontmoeten in plaats van een versienummer

Versiecompliance-controles, measure- en geospatiale dictionaries en save-versielocking horen allemaal bij PDF Library for Delphi, de PDFlibPas-toolkit voor Delphi-, C++Builder- en Lazarus-ontwikkelaars