Technisch artikel

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

Een plattegrond geëxporteerd door een architectuur-CAD-pakket heeft typisch een /VP-array op zijn eerste pagina met twee viewports, en elke viewport draagt een /Measure-invoer zodat een viewer meters rechtstreeks van de tekening kan aflezen. Een GeoPDF-kaartblad heeft dezelfde /VP-array en dezelfde /Measure-sleutel. De plattegrond is gewone PDF 1.6. Het kaartblad heeft PDF 1.7 plus Adobe Extension Level 3 nodig. Een versiecontrole die bij de sleutelnaam stopt eist de extensie voor allebei op, en de plattegrond wordt bij de save geweigerd met een melding over geospatiale inhoud die hij nooit heeft bevat

Eén /Measure-sleutel, twee verschillende dictionaries

De sleutelnaam zegt dat een viewport meetbaar is; alleen de dictionary erachter zegt hoe. Measure-dictionaries kwamen met PDF 1.6, en ISO 32000-1 §12.9 definieert het rectilineaire soort: een schaalverhoudingstring in /R, getalnotatiearrays in /X en /Y, en verdere arrays voor afstand (/D) en oppervlak (/A). Geospatiale meting kwam later, als deel van Adobe Extension Level 3 bovenop PDF 1.7. Hij wordt gemarkeerd door /Subtype /GEO en draagt een geografisch coördinatensysteem in /GCS, een optioneel weergave-coördinatensysteem in /DCS, gepaarde puntarrays in /GPTS en /LPTS, en geprefereerde weergave-eenheden in /PDU — de structuren die in GeoPDF-viewports, GPTS- en LPTS-arrays lezen in Delphi worden doorlopen

Het detail waar mensen over struikelen is de default. §12.9 geeft /Subtype een defaultwaarde RL, en legio CAD-exporters maken daar gebruik van en laten de invoer weg. Een checker die een ontbrekend subtype als "onbekend, neem het ergste aan" leest, maakt van elk van die tekeningen een geospatiaal document. Een checker die op de /Measure-sleutel matcht komt zelfs nooit bij de subtypevraag, want de viewportdictionary die de sleutel vasthoudt zit één niveau boven de measure-dictionary die hem beantwoordt

Eén /Measure-sleutel, twee dictionaries in PDFlibPas: rectilineaire meting, met /RL of met weggelaten subtype, heeft alleen PDF 1.6 nodig, terwijl een geospatiale dictionary 1.7 ExtensionLevel 3 nodig heeft, dus de preflight beslist op de inhoud van de measure-dictionary waar een sleutelniveau-regel 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 bevat, en daarom zit de onderscheidende controle één niveau dieper

Hoe de save-preflight van PDF Library for Delphi tot een oordeel komt

PDF Library for Delphi (PDFlibPas) draait een versiepreflight binnen SaveToFile voordat er uitvoer wordt geschreven. Hij bezoekt elk indirect object, daalt af in geneste dictionaries en arrays, en toetst elke knoop aan een tabel met feature-regels. Elke regel noemt een minimale PDF-versie en, voor extensiefeatures, een Adobe-extensieniveau; een extensieregel kan alleen worden vervuld door PDF 1.7 samen met een passende /Extensions /ADBE-invoer in de catalogus. Het doel waar hij tegen toetst is de documentversie, of de versie die je met LockSaveVersion hebt vastgezet, plus welk extensieniveau de catalogus aangeeft. De eerste regel die zowel matcht als dat doel overtreft, beëindigt de save: SaveToFile geeft 0 terug en LastErrorCode wordt 602, de waarde van PDFLIB_ERROR_VERSION_COMPLIANCE

Save-time versiepreflight in PDFlibPas: SaveToFile toetst elk object aan een vaste tabel met feature-regels, elk met een minimale PDF-versie en, voor extensiefeatures, een Adobe-extensieniveau dat alleen PDF 1.7 met een passende /Extensions-invoer kan vervullen, en de eerste match die het doel niet kan vervullen stopt de save met error 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 repareren in plaats van te gissen

Een errorcode alleen zegt niet wat je moet repareren. Drie GetInformation-sleutels dragen de rest: 311 is de versie die de overtredende feature nodig heeft (bijvoorbeeld 1.7 ExtensionLevel 3), 312 is de naam van de feature, en 313 is de versie waarop het save-doel is vastgezet, leeg als niets is vastgezet

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;

Voor meetinhoud houdt de tabel drie regels bij en bewust geen regel op de kale /Measure-sleutel. Een pagina met /VP of /UserUnit heeft PDF 1.6 nodig. Een /PtData-dictionary heeft extensieniveau 3 nodig. Een measure-dictionary heeft extensieniveau 3 nodig wanneer hij /Subtype /GEO zegt of een van /GCS, /DCS, /GPTS, /LPTS of /PDU bevat. Een viewport waarvan de measure-dictionary /Subtype weglaat en één die /RL uitschrijft passeren allebei op 1.6, terwijl dezelfde pagina op 1.5 wordt geweigerd vanwege /VP. Voeg een /GPTS-array toe en het antwoord wordt 1.7 ExtensionLevel 3

De inhoudstest is met opzet conservatief. Een rectilineaire dictionary die ook een losse /GCS of /PDU draagt wordt als geospatiaal behandeld. Liever wijzen we dat bestand af dan het doorglijden: die sleutels betekenen niets in het rectilineaire model, dus wie ze schreef was ofwel een geospatiaal gereedschap ofwel iets dat het bestand beschadigde, en een GEO-bewuste viewer gaat ze misschien wel interpreteren. De loader is in de andere richting even zorgvuldig. Zijn featurescan kan het versienummer van het document naar 1.7 tillen, maar hij schrijft nooit een /Extensions-declaratie namens een invoerbestand — een extensie declareren is een claim over inhoud, en alleen jouw code of de authoring-aanroepen zouden die moeten doen

De viewports van een tekening controleren voordat je opslaat

Wanneer 312 een geospatiale feature noemt en je zeker weet dat het bestand een gewone tekening is, kijk dan zelf naar de measure-dictionaries. De openbare viewportaanroepen zijn genoeg. GetMeasureDictCoordinateSystem leest /Subtype en geeft 1 terug voor RL, 2 voor GEO, en 0 wanneer de invoer ontbreekt — wat per §12.9 ook rectilineair betekent. De puntarraytellingen zeggen of er georeferentiedata aanwezig is

// Rapporteer elke measure-dictionary in het geladen document.
// Alle aanroepen hier lezen alleen; geen van ze voegt sleutels toe aan het bestand.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
  Page, VP, VPID, MD, Kind: Integer;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.SelectPage(Page);
    for VP := 1 to Pdf.GetPageViewPortCount do
    begin
      VPID := Pdf.GetPageViewPortID(VP);
      MD := Pdf.GetViewPortMeasureDict(VPID);
      if MD = 0 then
        Continue;
      Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = weggelaten (RL), 1 = RL, 2 = GEO
      Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
        [Page, string(Pdf.GetViewPortName(VPID)), Kind,
         Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
    end;
  end;
end;

Twee aanroepen ontbreken in die lus met opzet: GetMeasureDictGCSDict en GetMeasureDictDCSDict. Die twee zijn get-or-create-accessors gebouwd voor authoring. Op een measure-dictionary zonder /GCS-invoer voegt GetMeasureDictGCSDict een lege toe en declareert extensieniveau 3 voor het document. Roep hem als sonde op een rectilineaire plattegrond aan en de dictionary bevat nu echt /GCS; een daaropvolgende save vastgezet op 1.6 wordt geweigerd, terecht, voor inhoud die jouw diagnose net zelf schreef. Wil je weten of er een coördinatensysteem bestaat, dan beantwoorden het subtype en de puntarraytellingen dat zonder bijeffecten

De uitvoer vastzetten op PDF 1.6

Een vastgezette versie houdt de uitvoer eerlijk wanneer je afnemers een plafon hebben. TPDFlib.LockSaveVersion accepteert de strings '1.0' tot '1.7' en geeft 0 terug voor alles anders. Hij fixeert de documentversie en houdt writer-side aanroepen tegen die hem uit zichzelf willen tillen, maar de save-preflight draait nog steeds tegen de vastgezette waarde. Een CAD-tekening vastgezet op 1.6 slaat schoon op. Een echte GeoPDF vastgezet op 1.6 krijgt 602, en dat is het antwoord waar je op zit te wachten, want het alternatief is een bestand waarvan de header minder belooft dan zijn inhoud vraagt

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
try
  if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
    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))]);
finally
  Pdf.UnlockSaveVersion;
end;

Lees 311 en 312 voordat je aan de versie zit. Is de feature echte geospatiale inhoud, dan heb je twee eerlijke keuzes: laat het document vastgezet op 1.7 met de gedeclareerde extensie, of haal de geospatiale data uit de kopie die je naar een 1.6-only afnemer stuurt. De lock opvliegen om de error te laten verdwijnen is de optie om te vermijden. Hij verbergt de echte vraag — kan de downstream-reader aan wat je verstuurt? Bouw je geospatiale inhoud via de API, dan declareren de SetMeasureDict*-setters als SetMeasureDictCoordinateSystem en SetMeasureDictGPTSCount zelf extensieniveau 3, dus authored bestanden komen al gelabeld bij de preflight aan. Dezelfde gewoonte van begrensde, bewijsgebaseerde controles zie je in de PDF/E-1 author-mode preflight voor engineeringdocumenten, waar CAD-tekeningen tegen een conformiteitstandaard aankomen in plaats van tegen een versienummer

Waarom de regelscan de tabel in place leest

De kosten van de preflight zijn grofweg objectknoopen keer regels, en het knoopaantal omvat elk getal en elke naam in elke array. Daarmee telt de per-regel-overhead zwaarder dan hij lijkt. Elke regel in de tabel is een record met twee AnsiString-velden, de matchstring en de featurenaam. Zo'n record naar een lokale variabele kopiëren laat Delphi of FPC twee reference counts bijwerken en loslaten wat de lokale vasthield. Een keer per regel per knoop betekent dat je de stringboekhouding betaalt zelfs voor regels die niet op de doelversie van toepassing zijn en op de allernexte regel worden weggegooid

Regelscan in PDFlibPas: het kopiëren van een regelrecord met stringvelden stelt bij elk bezoek reference counts bij, terwijl het als const-parameter doorgeven van de read-only tabelinvoer hem in place leest zonder de strings aan te raken
Het in place lezen van de read-only tabel haalt reference-count-werk uit het regel-matchpad, terwijl een volledige save nog steeds betaalt voor featuredetectie, objectdecodering en serialisatie

De regeltabel wordt één keer bij unit-initialisatie gevuld en daarna nooit meer gewijzigd, dus de scan geeft tabelinvoer via const-recordparameters aan zijn matchcode door en leest ze waar ze liggen. Een const-recordparameter wordt by reference doorgegeven, dus het toetsen van een regel raakt geen van beide stringvelden en de reference counts blijven waar ze waren. Het effect concentreert zich op het regel-matchpad. Een echte save decodeert ook objecten, draait uitgestelde featuredetectie en serialiseert de uitvoer, en die fasen worden niet geraakt door hoe regels worden doorgegeven. Regelvolgorde, drempels en de eerste-falen-diagnose in 311 en 312 hangen er niet van af, en geen enkel regelresultaat wordt tussen saves gecachet, dus een document dat je bewerkt en twee keer opslaat wordt allebei de keren volledig gecontroleerd

Hetzelfde patroon loont om in je eigen Delphi-code te checken waar een lus een tabel met records doorloopt die strings, interfaces of dynamische arrays bevatten:

type
  TMyRule = record
    Name: string;      // managed veld: kopieën kosten reference-count-werk
    Threshold: Integer;
  end;

// Eigen code van de lezer, als patroon getoond
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
  Result := Value >= Rule.Threshold;  // const: geen kopie, geen refcount-verkeer
end;

// In de hot loop geeft de tabelinvoer direct door in plaats van
// hem eerst aan een lokaal record toe te wijzen
for I := 0 to High(Rules) do
  if Applies(Rules[I], V) then
    Inc(Hits);

Meet-viewports, geospatiale dictionaries, versielocking en de save-time compliance-controle zitten allemaal in PDF Library for Delphi, beschikbaar voor Delphi-, C++Builder- en Lazarus-projecten