Το PDFlibPas (PDF Library for Delphi) ελέγχει κάθε object απέναντι σε πίνακα κανόνων έκδοσης PDF πριν γράψει αρχείο, και μέχρι πρόσφατα εκείνο το preflight έκδοσης PDF μπέρδευε συνηθισμένα CAD measurement dictionaries με geospatial. Ένα CAD σχέδιο μίας σελίδας φορτωνόταν καλά, μετά το SaveToFile επέστρεφε 0 με LastErrorCode 602 και απαιτούσε 1.7 ExtensionLevel 3. Οι διορθωμένοι κανόνες μεταχειρίζονται rectilinear dictionaries /Measure (/Subtype /RL) ως σκέτο PDF 1.6 και κρατούν την πύλη extension για πραγματικά geospatial σημάδια
Το αρχείο ήρθε μέσα από corpus admission: μία σελίδα, μία ομάδα optional-content, δύο rectilinear measurement viewports, το είδος εξόδου που γράφει ένα αρχιτεκτονικό CAD πακέτο ώστε ένας viewer να διαβάζει αποστάσεις από κάτοψη. Τίποτα σε αυτό δεν ήταν εξωτικό, που είναι ακριβώς ο λόγος που η άρνηση είχε σημασία. Ένα preflight που μπλοκάρει έγκυρο αρχείο είναι χειρότερο από ένα αργό, επειδή ο καλών παίρνει μια διαγνωστική που μοιάζει αυθεντική και δείχνει χαρακτηριστικό που το έγγραφο δεν περιέχει. Το fix πήρε δύο μέρη: την ανάγνωση της προδιαγραφής πίσω από έναν κανόνα, και τη συνειδητοποίηση ότι ο κανόνας δεν μπορούσε να ξεχωρίσει δύο τύπους dictionaries στο επίπεδο που κοιτούσε
Πώς δουλεύει το save-time preflight έκδοσης του PDFlibPas;
Η πύλη αποθήκευσης, PrepareAndCheckSaveVersion, συγκρίνει κάθε indirect object απέναντι στο PDFFeatureRules και αποτυγχάνει στον πρώτο κανόνα που και ταιριάζει και θέλει περισσότερα από όσα επιτρέπει ο στόχος. Ο στόχος είναι η έκδοση εγγράφου (ή η έκδοση καρφωμένη με LockSaveVersion), συν το Adobe extension level δηλωμένο κάτω από /Extensions /ADBE. Κάθε εγγραφή TPDFFeatureRule κουβαλά MinVersion, MinExtensionLevel, MatchKind όπως fmkDictKey ή fmkDictSubtype, string Match, ανθρώπινα ευανάγνωστο όνομα Feature και προαιρετικό callback. Το AddRule καταχωρεί σκέτο κανόνα έκδοσης· το AddExtensionRule καρφώνει πάντα MinVersion στο 17 και προσθέτει extension level από πάνω, οπότε κανόνας extension μπορεί μόνο να ικανοποιηθεί από PDF 1.7 συν τη σωστή εγγραφή /Extensions. Όταν η πύλη πέσει, η απαιτούμενη έκδοση και το όνομα χαρακτηριστικού κρατιούνται για τον καλούντα, και τα κλειδιά 311, 312 και 313 του GetInformation τα εκθέτουν
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: απαιτούμενη έκδοση, 312: χαρακτηριστικό που την πυροδότησε,
// 313: έκδοση στην οποία είναι καρφωμένος ο στόχος save ('' όταν ξεκλείδωτος)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Γιατί ένα σκέτο CAD σχέδιο απέτυχε με error 602;
Ο πίνακας κανόνων περιείχε AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), που πυροδοτούνταν σε κάθε dictionary που απλώς είχε κλειδί /Measure, και κάθε measurement viewport έχει ένα. Ο πίνακας /VP της σελίδας κρατά viewport dictionaries, κάθε viewport δείχνει το measure dictionary του μέσω /Measure, και το ταίριασμα παρουσίας κλειδιού σταματούσε εκεί χωρίς να κοιτάξει τι ήταν πραγματικά το measure dictionary. Το load-time feature σκαν μπορούσε τότε να ανεβάσει τον αριθμό έκδοσης του εγγράφου στο 1.7, αλλά δεν γράφει ποτέ δήλωση /Extensions εκ μέρους αρχείου εισόδου, οπότε η πύλη αποθήκευσης έβλεπε PDF 1.7 σε extension level 0 και ανέφερε 1.7 ExtensionLevel 3. Εκείνη η άρνηση να επινοήσει δήλωση extension είναι σκόπιμη: η βιβλιοθήκη δεν προβιβάζει ήσυχα αρχείο εισόδου για να καλυφθεί κανόνας που είναι λάθος
Η προδιαγραφή είναι κατηγορηματική για την rectilinear περίπτωση. Τα Measure dictionaries ήρθαν στο PDF 1.6, και το ISO 32000-1 §12.9 δίνει στο /Subtype default RL, ένα rectilinear σύστημα συντεταγμένων που περιγράφεται από το δικό του σύνολο εγγραφών: λόγος κλίμακας, μορφές αριθμών X και Y, απόσταση και εμβαδόν. Η geospatial μέτρηση είναι η μεταγενέστερη προσθήκη από το Adobe Extension Level 3 πάνω στο PDF 1.7, αναγνωρίσιμη από /Subtype /GEO και κουβαλώντας geographic point πίνακες, dictionaries συστημάτων συντεταγμένων και μονάδες εμφάνισης, τις δομές που περπατά το ανάγνωση GeoPDF viewports, GPTS και LPTS πινάκων στο Delphi. Και τα δύο dictionaries κρέμονται από το ίδιο κλειδί /Measure, οπότε κανένας κανόνας που σταματά στο κλειδί δεν μπορεί να είναι σωστός και για τα δύο. Η διαχωριστική πληροφορία κάθεται ένα επίπεδο πιο κάτω, μέσα στο ίδιο το measure dictionary
Τι εξακολουθεί να επιβάλλει το διορθωμένο σύνολο κανόνων;
Το fix σβήνει τον άνευ όρων κανόνα κλειδιού και αφήνει τις πύλες που περιγράφουν πραγματικές απαιτήσεις έκδοσης. Σελίδα που κουβαλά /VP ή /UserUnit θέλει ακόμα PDF 1.6 μέσω CB_PagePDF16Entries, κλειδί /PtData θέλει ακόμα extension level 3, και το CB_GeospatialDictionary αποφασίζει αν ένα measure dictionary είναι geospatial από το περιεχόμενό του και όχι από το κλειδί από το οποίο έφτασε
// Αφαιρέθηκε: κάθε dictionary με κλειδί /Measure μετρούσε ως 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;
Τα κοινά regressions Delphi και FPC καρφώνουν εκείνο το όριο και από τις δύο πλευρές. Ένα viewport του οποίου το measure dictionary παραλείπει /Subtype και ένα που γράφει ρητά /RL περνάνε και τα δύο σε PDF 1.6, η ίδια σελίδα εξακολουθεί να απορρίπτεται σε PDF 1.5, και το feature detection δεν αναφέρει πλέον extension για αυτήν. Προσθήκη πίνακα /GPTS αναποδογυρίζει την ετυμηγορία πίσω στο 1.7 ExtensionLevel 3, που περνά μόλις δηλωθεί το extension level, και ένα γυμνό dictionary /Subtype /GEO απορρίπτεται χωρίς αυτό. Το callback είναι συντηρητικό εκ σχεδιασμού: rectilinear dictionary που κουβαλά και ένα ξένο κλειδί /GCS ή /PDU μεταχειρίζεται ως geospatial, αφού εκείνα τα κλειδιά δεν έχουν νόημα στο μοντέλο RL
Το LockSaveVersion είναι εκεί που αυτή η αλλαγή γίνεται ορατή στους καλούντες. Το TPDFlib.LockSaveVersion δέχεται '1.0' έως '1.7', επιστρέφει 0 για οτιδήποτε άλλο, καρφώνει την έκδοση εγγράφου, και σταματά τις writer-side κλήσεις από το να την ανεβάζουν σιωπηλά, όμως η πύλη αποθήκευσης εξακολουθεί να τρέχει απέναντι στην καρφωμένη τιμή. Με τους διορθωμένους κανόνες, ένα CAD αρχείο καρφωμένο στο 1.6 αποθηκεύεται καθαρά. Ένα γνήσιο GeoPDF καρφωμένο στο 1.6 εξακολουθεί να παίρνει 602, που είναι η σωστή απάντηση, και οι geospatial authoring κλήσεις όπως το SetMeasureDictCoordinateSystem δηλώνουν μόνες τους extension level 3 όταν χτίζετε εκείνο το περιεχόμενο μέσω του API
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
// Πραγματικό περιεχόμενο πάνω από 1.6, για παράδειγμα 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;
Γιατί το σκαν κανόνων έκδοσης ήταν πιο αργό από όσο χρειαζόταν;
Το σκαν αντέγραφε κάθε TPDFFeatureRule σε τοπική εγγραφή πριν τη τεστάρει, και επειδή η εγγραφή κρατά δύο πεδία AnsiString, κάθε αντίγραφο ρύθμιζε δύο reference counts και απελευθέρωνε τις προηγούμενες τιμές. Το preflight επισκέπτεται κάθε node κάθε δέντρου objects, συμπεριλαμβανομένων των scalars, οπότε εκείνο το κόστος πολλαπλασίαζε πλήθος objects επί πλήθος κανόνων, και κανόνες που δεν ισχύαν καν για την έκδοση στόχο αντιγράφονταν πρώτα και παραλείπονταν μετά. Επειδή το PDFFeatureRules γεμίζει μία φορά στην αρχικοποίηση της μονάδας και μεταχειρίζεται read-only, το v3.539.17 περνά εγγραφές πίνακα κατευθείαν στο MatchSingleRule και στο RuleExceedsTarget, των οποίων οι παράμετροι const Rule παίρνουν αναφορά χωρίς να αγγίξουν τα strings
// Πριν: ένα managed αντίγραφο εγγραφής ανά κανόνα, ανά επισκεπτόμενο object
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Μετά: οι παράμετροι const διαβάζουν την αμετάβλητη εγγραφή πίνακα στη θέση της
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;
Το μετρημένο αποτέλεσμα είναι στενό και πρέπει να παρατίθεται έτσι. Το benchmark τεστάρει πίνακα 20.000 αριθμητικών objects απέναντι σε στόχο PDF 1.4 δέκα φορές ανά γύρο· χτισμένο με FPC Win64 σε -O2, η διάμεσος πέντε γύρων έπεσε από 0.711 s σε 0.203 s, και το τρέξιμο των δύο builds σε αντίστροφη σειρά έδωσε 0.459 s απέναντι σε 0.150 s. Αυτό είναι περίπου κέρδος 3x στο μονοπάτι rule matching μόνο. Ένα πραγματικό save πληρώνει επίσης deferred feature detection, αποκωδικοποίηση objects και serialization, οπότε ο λόγος δεν μεταφέρεται στον συνολικό χρόνο save. Σειρά κανόνων, callbacks, όρια εκδόσεων και η διαγνωστική πρώτης αποτυχίας μένουν αμετάβλητα, και κανένας κανόνας δεν κρατήθηκε σε cache ανάμεσα σε saves ή παραλείφθηκε για να φτάσουμε εκεί
Τι πρέπει να ελέγξετε όταν ένα φορτωμένο PDF αποτυγχάνει το preflight έκδοσης;
Διαβάστε τα κλειδιά 311 και 312 πριν αγγίξετε την έκδοση. Αν το χαρακτηριστικό κατονομάζει geospatial dictionary και το αρχείο σχεδιάζει μόνο rectilinear μετρήσεις, αυτό ήταν το false positive, και ένα τρέχον build αποθηκεύει το αρχείο αμετάβλητο. Αν το χαρακτηριστικό είναι γνήσιο, είτε δηλώστε το extension είτε κλειδώστε σε έκδοση που ειλικρινά περιέχει το περιεχόμενο· το να ανεβάσετε την έκδοση μόνο και μόνο για να σιγήσει η πύλη κρύβει το ερώτημα αν οι κατάντη καταναλωτές μπορούν να διαβάσουν ό,τι παραδίδετε. Η ίδια αρχή των οριακών, τεκμηριωμένων ελέγχων κινεί το preflight author-mode PDF/E-1 για μηχανικά έγγραφα, όπου CAD σχέδια συναντούν πρότυπο συμμόρφωσης αντί για αριθμό έκδοσης
Οι έλεγχοι συμμόρφωσης έκδοσης, τα measurement και geospatial dictionaries, και το κλείδωμα έκδοσης αποθήκευσης είναι όλα μέρος του PDF Library for Delphi, του PDFlibPas toolkit για προγραμματιστές Delphi, C++Builder και Lazarus