Technischer Artikel

PDF-Versions-Preflight in Delphi: RL- gegen GEO-Regeln

PDFlibPas (PDF Library for Delphi) prüft jedes Objekt vor dem Schreiben einer Datei gegen eine Tabelle von PDF-Versionsregeln, und bis vor Kurzem verwechselte dieser PDF-Versions-Preflight gewöhnliche CAD-Mess-Dictionarys mit geodätischen. Eine einseitige CAD-Zeichnung lud einwandfrei, dann gab SaveToFile 0 mit LastErrorCode 602 zurück und verlangte 1.7 ExtensionLevel 3. Die korrigierten Regeln behandeln rectilinear-/Measure-Dictionarys (/Subtype /RL) als schlichtes PDF 1.6 und reservieren die Extension-Schranke für echte geodätische Marker

Die Datei kam über die Korpus-Aufnahme herein: eine Seite, eine Optional-Content-Gruppe, zwei rectilinear-Mess-Viewports – die Art von Ausgabe, die ein Architektur-CAD-Paket schreibt, damit ein Viewer Entfernungen direkt vom Grundriss ablesen kann. Nichts an ihr war exotisch, genau deshalb zählte die Verweigerung. Ein Preflight, der eine gültige Datei blockiert, ist schlimmer als ein langsamer, denn der Aufrufer bekommt eine autoritativ aussehende Diagnose, die auf ein Feature zeigt, das das Dokument gar nicht enthält. Der Fix bestand aus zwei Teilen: der Spec-Lesart hinter einer Regel und der Erkenntnis, dass die Regel zwei Dictionary-Typen auf der Ebene, auf der sie suchte, gar nicht unterscheiden konnte

Wie funktioniert der Save-Versions-Preflight von PDFlibPas?

Die Save-Schranke, PrepareAndCheckSaveVersion, vergleicht jedes indirekte Objekt gegen PDFFeatureRules und scheitert an der ersten Regel, die sowohl matcht als auch mehr braucht, als das Ziel erlaubt. Das Ziel ist die Dokumentversion (oder die von LockSaveVersion festgenagelte Version), plus dem unter /Extensions /ADBE deklarierten Adobe-Extension-Level. Jeder TPDFFeatureRule-Record trägt ein MinVersion, ein MinExtensionLevel, ein MatchKind wie fmkDictKey oder fmkDictSubtype, einen Match-String, einen menschenlesbaren Feature-Namen und einen optionalen Callback. AddRule registriert eine schlichte Versionsregel; AddExtensionRule nagelt MinVersion immer auf 17 und legt einen Extension-Level oben drauf, eine Extension-Regel ist also nur durch PDF 1.7 plus den richtigen /Extensions-Eintrag zu erfüllen. Trippt die Schranke, werden die nötige Version und der Feature-Name für den Aufrufer vorgehalten, und GetInformation-Keys 311, 312 und 313 legen sie offen

Save-Versions-Preflight in PDFlibPas: PrepareAndCheckSaveVersion vergleicht jedes Objekt gegen PDFFeatureRules-Records mit MinVersion, einem Extension-Level und einem MatchKind, AddExtensionRule nagelt die Anforderung auf PDF 1.7 plus Extension-Level, und der erste Match, den das Ziel nicht erfüllen kann, stoppt den Save mit Fehler 602
GetInformation-Keys 311, 312 und 313 machen aus einer Verweigerung eine Diagnose und melden die geforderte Version, das auslösende Feature und das festgenagelte Ziel, sodass der Aufrufer die Datei oder die Regel reparieren kann, statt zu raten
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: geforderte Version, 312: auslösendes Feature,
        // 313: Version, auf die das Save-Ziel festgenagelt ist ('' wenn offen)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Warum scheiterte eine schlichte CAD-Zeichnung mit Fehler 602?

Die Regeltabelle enthielt AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), was auf jedes Dictionary feuerte, das nur einen /Measure-Key hatte – und jedes Mess-Viewport hat einen. Das /VP-Array der Seite hält Viewport-Dictionarys, jeder Viewport zeigt über /Measure auf sein Measure-Dictionary, und der Key-Präsenz-Match hörte dort auf, ohne danach zu schauen, was das Measure-Dictionary eigentlich war. Der Feature-Scan zur Ladezeit konnte dann die Dokumentversionsnummer auf 1.7 heben, aber er schreibt nie im Namen einer Eingabedatei eine /Extensions-Deklaration, also sah die Save-Schranke PDF 1.7 auf Extension-Level 0 und meldete 1.7 ExtensionLevel 3. Diese Weigerung, eine Extension-Deklaration zu erfinden, ist Absicht: Die Bibliothek stuft eine Eingabedatei nicht still hinauf, um eine falsche Regel zu übertünchen

Die Spec ist beim rectilinear-Fall eindeutig. Measure-Dictionarys kamen mit PDF 1.6, und ISO 32000-1 §12.9 gibt /Subtype den Default RL, ein rectilineares Koordinatensystem, beschrieben durch seinen eigenen Satz von Einträgen: Skalierungsverhältnis, X- und Y-Zahlenformate, Distanz und Fläche. Geodätische Messung ist der spätere Zuwachs aus Adobe Extension Level 3 auf PDF 1.7, identifiziert über /Subtype /GEO und mit geografischen Punkt-Arrays, Koordinatensystem-Dictionarys und Anzeigeeinheiten – den Strukturen, die in GeoPDF-Viewports, GPTS- und LPTS-Arrays in Delphi lesen abgelaufen werden. Beide Dictionarys hängen am selben /Measure-Key, also kann keine Regel, die am Key stehen bleibt, für beide richtig sein. Die unterscheidende Information sitzt eine Ebene tiefer, im Measure-Dictionary selbst

Ein /Measure-Key, zwei Dictionarys in PDFlibPas: Rectilinear-Messung, mit /RL oder weggelassenem Subtype, braucht nur PDF 1.6, ein geodätisches Dictionary braucht 1.7 ExtensionLevel 3, also entscheidet CB_GeospatialDictionary nach Inhalt, wo die alte Key-Regel sie nicht auseinanderhalten konnte
Ein Preflight, der eine gültige Datei blockiert, ist schlimmer als ein langsamer, denn der Aufrufer bekommt eine autoritative Diagnose über ein Feature, das das Dokument nie enthielt, deshalb ist die unterscheidende Prüfung eine Ebene tiefer gerückt

Was setzt das korrigierte Regelset weiterhin durch?

Der Fix löscht die bedingungslose Key-Regel und lässt die Schranken stehen, die echte Versionsanforderungen beschreiben. Eine Seite mit /VP oder /UserUnit braucht weiterhin PDF 1.6 über CB_PagePDF16Entries, ein /PtData-Key braucht weiterhin Extension-Level 3, und CB_GeospatialDictionary entscheidet anhand des Inhalts, ob ein Measure-Dictionary geodätisch ist, statt anhand des Keys, über den es erreicht wurde

// Entfernt: jedes Dictionary mit /Measure-Key galt als geodätisch
// 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;

Die gemeinsamen Delphi- und FPC-Regressionen nageln diese Grenze von beiden Seiten fest. Ein Viewport, dessen Measure-Dictionary /Subtype weglässt, und einer, der /RL ausschreibt, bestehen beide bei PDF 1.6, dieselbe Seite wird weiterhin bei PDF 1.5 zurückgewiesen, und die Feature-Erkennung meldet dafür keine Extension mehr. Ein hinzugefügtes /GPTS-Array kippt das Urteil zurück auf 1.7 ExtensionLevel 3, was besteht, sobald der Extension-Level deklariert ist, und ein nacktes /Subtype /GEO-Dictionary wird ohne ihn verweigert. Der Callback ist bewusst konservativ: Ein rectilineares Dictionary, das zusätzlich einen verirrten /GCS- oder /PDU-Key trägt, wird als geodätisch behandelt, denn diese Keys haben im RL-Modell keine Bedeutung

LockSaveVersion ist die Stelle, an der diese Änderung für Aufrufer sichtbar wird. TPDFlib.LockSaveVersion nimmt '1.0' bis '1.7', gibt für alles andere 0 zurück, nagelt die Dokumentversion fest und hält Writer-seitige Aufrufe davon ab, sie still anzuheben, doch die Save-Schranke läuft weiterhin gegen den festgenagelten Wert. Mit den korrigierten Regeln speichert eine auf 1.6 festgenagelte CAD-Datei sauber. Eine echte GeoPDF auf 1.6 bekommt weiterhin 602 – die richtige Antwort –, und die geodätischen Authoring-Aufrufe wie SetMeasureDictCoordinateSystem deklarieren Extension-Level 3 selbst, wenn Sie diesen Inhalt über die API bauen

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
    // Echter Inhalt über 1.6, etwa ein 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;

Warum war der Versionsregel-Scan langsamer als nötig?

Der Scan kopierte jeden TPDFFeatureRule-Record in einen lokalen Record, bevor er ihn testete, und weil der Record zwei AnsiString-Felder hält, justierte jede Kopie zwei Referenzzähler und ließ die vorherigen Werte frei. Der Preflight besucht jeden Knoten jedes Objektbaums, Skalare eingeschlossen, also multiplizierte sich diese Kosten mit Objektzahl mal Regelzahl, und Regeln, die auf die Zielversion nicht einmal anwendbar waren, wurden zuerst kopiert und danach übersprungen. Weil PDFFeatureRules einmal bei der Unit-Initialisierung gefüllt und als read-only behandelt wird, reicht v3.539.17 die Tabelleneinträge direkt an MatchSingleRule und RuleExceedsTarget durch, deren const Rule-Parameter eine Referenz nehmen, ohne die Strings anzufassen

Regel-Scan-Beschleunigung in PDFlibPas: Der Preflight kopierte vormals jeden TPDFFeatureRule-Record vor dem Test und justierte AnsiString-Referenzzähler für jedes besuchte Objekt, während const-Parameter die read-only-Tabelle jetzt an Ort und Stelle lesen, womit der Median der Regel-Matching-Runde von 0,711 s auf 0,203 s fiel
Der Gewinn ist echt, aber eng: Ein voller Save zahlt zusätzlich für verzögerte Feature-Erkennung, Objekt-Dekodierung und Serialisierung, das gemessene Verhältnis gehört also zum Regel-Matching-Pfad und nicht zur gesamten Save-Zeit
// Vorher: eine verwaltete Record-Kopie pro Regel, pro besuchtem Objekt
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// Nachher: const-Parameter lesen den unveränderlichen Tabelleneintrag an Ort und Stelle
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;

Die gemessene Wirkung ist eng und sollte auch so zitiert werden. Der Benchmark prüft ein Array von 20.000 numerischen Objekten zehnmal pro Runde gegen ein PDF-1.4-Ziel; gebaut mit FPC Win64 bei -O2, fiel der Median aus fünf Runden von 0,711 s auf 0,203 s, und in umgekehrter Reihenfolge gelaufen ergaben die beiden Builds 0,459 s gegen 0,150 s. Das ist rund eine 3-fache Beschleunigung auf dem Regel-Matching-Pfad allein. Ein echter Save zahlt zusätzlich für verzögerte Feature-Erkennung, Objekt-Dekodierung und Serialisierung, das Verhältnis überträgt sich also nicht auf die gesamte Save-Zeit. Regelreihenfolge, Callbacks, Versionsschwellen und die Erste-Fehler-Diagnose sind unverändert, und keine Regel wurde über Saves hinweg gecacht oder übersprungen, um dorthin zu kommen

Was sollten Sie prüfen, wenn eine geladene PDF den Versions-Preflight nicht besteht?

Lesen Sie die Keys 311 und 312, bevor Sie die Version anfassen. Nennt das Feature ein geodätisches Dictionary und die Datei zeichnet nur rectilinear-Messungen, war das dieser False Positive, und ein aktueller Build speichert die Datei unverändert. Ist das Feature echt, deklarieren Sie entweder die Extension oder nageln Sie auf eine Version fest, die den Inhalt ehrlich enthält; die Version nur anzuheben, um die Schranke zum Verstummen zu bringen, vergräbt die Frage, ob downstream-Konsumenten lesen können, was Sie ausliefern. Dasselbe Prinzip begrenzter, beleggestützter Prüfungen treibt den PDF/E-1-Author-Mode-Preflight für Ingenieurdokumente an, wo CAD-Zeichnungen auf einen Konformitätsstandard treffen statt auf eine Versionsnummer

Versions-Konformitätsprüfungen, Mess- und geodätische Dictionarys und Save-Versions-Locking sind alles Teil der PDF Library for Delphi, dem PDFlibPas-Toolkit für Delphi-, C++Builder- und Lazarus-Entwickler