Ein Grundriss, den ein Architektur-CAD-Paket exportiert, hat typischerweise ein /VP-Array auf seiner ersten Seite mit zwei Viewports, und jeder Viewport trägt einen /Measure-Eintrag, damit ein Viewer Meter direkt von der Zeichnung ablesen kann. Ein GeoPDF-Kartenblatt hat dasselbe /VP-Array und denselben /Measure-Key. Der Grundriss ist gewöhnliches PDF 1.6. Das Kartenblatt braucht PDF 1.7 plus Adobe Extension Level 3. Eine Versionsprüfung, die beim Key-Namen stoppt, fordert die Extension für beide, und der Grundriss wird beim Speichern mit einer Meldung über Geodaten abgewiesen, die er nie enthielt
Ein /Measure-Key, zwei verschiedene Dictionarys
Der Key-Name verrät nur, dass ein Viewport messbar ist; das Dictionary dahinter verrät, wie. Measure-Dictionarys kamen mit PDF 1.6, und ISO 32000-1 §12.9 definiert die rectilinear-Variante: ein Skalierungsverhältnis-String in /R, Zahlenformat-Arrays in /X und /Y, dazu weitere Arrays für Distanz (/D) und Fläche (/A). Geodätische Messung kam später, als Teil von Adobe Extension Level 3 auf PDF 1.7. Sie ist markiert durch /Subtype /GEO und trägt ein geografisches Koordinatensystem in /GCS, ein optionales Anzeige-Koordinatensystem in /DCS, gepaarte Punkt-Arrays in /GPTS und /LPTS sowie bevorzugte Anzeigeeinheiten in /PDU — die Strukturen, die der Artikel zum Lesen von GeoPDF-Viewports, GPTS- und LPTS-Arrays in Delphi abgeht
Das Detail, an dem es hakt, ist der Default. §12.9 gibt /Subtype den Default-Wert RL, und viele CAD-Exporter nutzen das und lassen den Eintrag weg. Ein Checker, der einen fehlenden Subtype als "unbekannt, im Zweifel das Schlimmste annehmen" liest, macht aus jedem dieser Drawings ein geodätisches Dokument. Ein Checker, der auf den /Measure-Key matcht, kommt gar nicht erst zur Subtype-Frage, denn das Viewport-Dictionary, das den Key hält, liegt eine Ebene über dem Measure-Dictionary, das sie beantwortet
Wie der Save-Preflight der PDF Library for Delphi zu einem Urteil kommt
PDF Library for Delphi (PDFlibPas) fährt einen Versions-Preflight innerhalb von SaveToFile, bevor irgendeine Ausgabe geschrieben wird. Er besucht jedes indirekte Objekt, steigt in verschachtelte Dictionarys und Arrays hinab, und testet jeden Knoten gegen eine Tabelle von Feature-Regeln. Jede Regel nennt eine Mindest-PDF-Version und, bei Extension-Features, ein Adobe-Extension-Level; eine Extensions-Regel kann nur von PDF 1.7 zusammen mit einem passenden /Extensions /ADBE-Eintrag im Katalog erfüllt werden. Das Ziel, gegen das geprüft wird, ist die Dokumentversion, oder die mit LockSaveVersion festgenagelte Version, plus dem Extension-Level, das der Katalog deklariert. Die erste Regel, die sowohl matcht als auch das Ziel übersteigt, beendet den Save: SaveToFile gibt 0 zurück und LastErrorCode wird zu 602, dem Wert von PDFLIB_ERROR_VERSION_COMPLIANCE
Ein Fehlercode allein sagt nicht, was zu fixen ist. Drei GetInformation-Keys tragen den Rest: 311 ist die Version, die das verletzende Feature braucht (etwa 1.7 ExtensionLevel 3), 312 ist der Name des Features, und 313 ist die Version, auf die das Save-Ziel festgenagelt ist, leer, wenn nichts genagelt ist
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;
Für Messinhalte hält die Tabelle drei Regeln und bewusst keine Regel auf den nackten /Measure-Key. Eine Seite mit /VP oder /UserUnit braucht PDF 1.6. Ein /PtData-Dictionary braucht Extension-Level 3. Ein Measure-Dictionary braucht Extension-Level 3, wenn es /Subtype /GEO sagt oder eines von /GCS, /DCS, /GPTS, /LPTS oder /PDU enthält. Ein Viewport, dessen Measure-Dictionary /Subtype weglässt, und einer, der /RL ausschreibt, bestehen also beide bei 1.6, während dieselbe Seite bei 1.5 wegen /VP abgewiesen wird. Kommt ein /GPTS-Array dazu, wird die Antwort 1.7 ExtensionLevel 3
Der Inhaltstest ist bewusst konservativ. Ein rectilinear-Dictionary, das zusätzlich ein verirrtes /GCS oder /PDU trägt, wird als geodätisch behandelt. Wir verweigern diese Datei lieber, als sie durchzuwinken: Diese Keys bedeuten nichts im rectilinear-Modell, wer sie also schrieb, war entweder ein Geodaten-Tool oder etwas, das die Datei beschädigt hat, und ein GEO-bewusster Viewer versucht durchaus, sie zu interpretieren. Der Loader ist in der anderen Richtung ebenso sorgfältig. Sein Feature-Scan kann die Versionsnummer des Dokuments auf 1.7 heben, aber er schreibt nie im Namen einer Eingabedatei eine /Extensions-Deklaration — eine Extension zu deklarieren ist eine Aussage über den Inhalt, und nur Ihr Code oder die Authoring-Aufrufe sollten sie machen
Die Viewports einer Zeichnung vor dem Speichern prüfen
Nennt 312 ein geodätisches Feature und Sie sind sicher, dass die Datei eine schlichte Zeichnung ist, schauen Sie selbst in die Measure-Dictionarys. Die öffentlichen Viewport-Aufrufe genügen. GetMeasureDictCoordinateSystem liest /Subtype und gibt 1 für RL zurück, 2 für GEO, und 0, wenn der Eintrag fehlt — was laut §12.9 ebenfalls rectilinear bedeutet. Die Punkt-Array-Zähler verraten, ob überhaupt Georeferenzierungsdaten vorhanden sind
// Jedes Measure-Dictionary im geladenen Dokument melden.
// Alle Aufrufe hier lesen nur; keiner fügt der Datei Keys hinzu.
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 = weggelassen (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;
Zwei Aufrufe fehlen in dieser Schleife mit Absicht: GetMeasureDictGCSDict und GetMeasureDictDCSDict. Die beiden sind Get-or-Create-Accessoren, gebaut für Authoring. An einem Measure-Dictionary ohne /GCS-Eintrag fügt GetMeasureDictGCSDict einen leeren hinzu und deklariert Extension-Level 3 für das Dokument. Als Sonde auf einem rectilinearen Grundriss gerufen, enthält das Dictionary jetzt wirklich ein /GCS; ein darauf folgender, auf 1.6 festgenagelter Save wird korrekt abgewiesen, und zwar wegen eines Inhalts, den Ihre Diagnose gerade geschrieben hat. Wer wissen will, ob ein Koordinatensystem existiert, bekommen das Subtype und die Punkt-Array-Zähler, ohne Seiteneffekte
Die Ausgabe bei PDF 1.6 festnageln
Eine festgenagelte Version hält die Ausgabe ehrlich, wenn Ihre Konsumenten eine Decke haben. TPDFlib.LockSaveVersion akzeptiert die Strings '1.0' bis '1.7' und gibt für alles andere 0 zurück. Sie fixiert die Dokumentversion und hält Writer-seitige Aufrufe davon ab, sie eigenmächtig anzuheben, doch der Save-Preflight läuft weiterhin gegen den festgenagelten Wert. Eine auf 1.6 festgenagelte CAD-Zeichnung speichert sauber. Eine echte GeoPDF auf 1.6 bekommt 602, und das ist die Antwort, die Sie wollen, denn die Alternative ist eine Datei, deren Header weniger verspricht, als ihr Inhalt braucht
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
// 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))]);
finally
Pdf.UnlockSaveVersion;
end;
Lesen Sie 311 und 312, bevor Sie die Version anfassen. Ist das Feature echter Geodateninhalt, haben Sie zwei ehrliche Optionen: das Dokument auf 1.7 offen lassen mit deklarierter Extension, oder die Geodaten aus der Kopie entfernen, die an einen reinen-1.6-Konsumenten geht. Die Sperre nur anzuheben, um den Fehler loszuwerden, ist die zu vermeidende Option. Sie verdeckt die eigentliche Frage — kann der downstream-Reader lesen, was Sie ausliefern? Bauen Sie Geodateninhalt über die API, deklarieren die SetMeasureDict*-Setter wie SetMeasureDictCoordinateSystem und SetMeasureDictGPTSCount Extension-Level 3 selbst, authoring-erzeugte Dateien kommen also bereits gelabelt zum Preflight. Dieselbe Gewohnheit begrenzter, beleggestützter Prüfungen zeigt der Artikel zum PDF/E-1-Author-Mode-Preflight für Engineering-Dokumente, wo CAD-Zeichnungen auf einen Konformitätsstandard treffen statt auf eine Versionsnummer
Warum der Regel-Scan die Tabelle an Ort und Stelle liest
Die Kosten des Preflights liegen grob bei Objektknoten mal Regeln, und die Knotenzahl schließt jede Zahl und jeden Namen in jedem Array ein. Das macht den Overhead pro Regel wichtiger, als er aussieht. Jede Regel in der Tabelle ist ein Record mit zwei AnsiString-Feldern, dem Match-String und dem Feature-Namen. Einen solchen Record in eine lokale Variable zu kopieren bringt Delphi oder FPC dazu, zwei Referenzzähler zu justieren und freizugeben, was die lokale vorher hielt. Pro Regel pro Knoten einmal ausgeführt heißt das: Man zahlt die String-Buchhaltung sogar für Regeln, die auf die Zielversion nicht zutreffen und in der allernächsten Zeile weggeworfen werden
Die Regeltabelle wird einmal bei der Unit-Initialisierung gefüllt und danach nie verändert, der Scan reicht Tabelleneinträge seinem Matching-Code also über const-Record-Parameter weiter und liest sie, wo sie liegen. Ein const-Record-Parameter wird per Referenz übergeben, das Testen einer Regel fasst also keines der beiden String-Felder an, und die Referenzzähler bleiben, wo sie waren. Die Wirkung konzentriert sich auf den Regel-Matching-Pfad. Ein echter Save dekodiert außerdem Objekte, fährt verzögerte Feature-Erkennung und serialisiert die Ausgabe, und diese Stufen sind unberührt davon, wie Regeln übergeben werden. Regelreihenfolge, Schwellen und die Erstverletzung-Diagnose in 311 und 312 hängen nicht davon ab, und kein Regelergebnis wird zwischen Saves gecached, ein Dokument, das Sie zweimal speichern, wird also beide Male vollständig geprüft
Dasselbe Muster lohnt einen Blick im eigenen Delphi-Code, wo immer eine Schleife über eine Tabelle von Records läuft, die Strings, Interfaces oder dynamische Arrays enthalten:
type
TMyRule = record
Name: string; // verwaltetes Feld: Kopien kosten Referenzzähler-Arbeit
Threshold: Integer;
end;
// Eigener Code des Lesers, als Muster gezeigt
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const: keine Kopie, kein Referenzzähler-Verkehr
end;
// Im Hot Loop den Tabelleneintrag direkt übergeben, statt ihn
// zuerst einer lokalen Record-Variable zuzuweisen
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
Mess-Viewports, geodätische Dictionarys, Versions-Locking und die Save-Zeit-Konformitätsprüfung shippen alle in der PDF Library for Delphi, verfügbar für Delphi-, C++Builder- und Lazarus-Projekte