Τεχνικό Άρθρο

Έλεγχος PDF/UA σε Delphi: Widths, CharSet, CIDSet

Μια αναφορά veraPDF που λέει ότι ένα glyph width διαφωνεί με το ενσωματωμένο font program δεν σας λέει σχεδόν τίποτα για ποιο glyph, ή γιατί. Το PDFlibPas απαντά το ερώτημα επιλύοντας κάθε character code μέσα από το ενσωματωμένο cmap σε glyph index, κανονικοποιώντας το metric του προγράμματος σε 1000 μονάδες ανά em, και συγκρίνοντας εκεί

Γιατί διαφωνούν τα glyph widths;

Επειδή οι δύο αριθμοί που συγκρίνονται ζουν σε διαφορετικά συστήματα συντεταγμένων, και τίποτα στο PDF dictionary δεν σας λέει τη μετατροπή. Ένα font dictionary γράφει /Widths σε glyph space, που το PDF ορίζει στο ένα χιλιοστό του em (ISO 32000-1 §9.2.4). Ο πίνακας hmtx μέσα στο ενσωματωμένο πρόγραμμα TrueType γράφει advances σε font design units, και ο πίνακας head αποφασίζει πόσες από αυτές κάνουν ένα em: 2048 για τις περισσότερες γραμματοσειρές TrueType, 1000 για CFF-παραγώμενες, κατά καιρούς κάτι εντελώς άλλο. Συγκρίνετε τις ακατέργαστες τιμές και κάθε γραμματοσειρά 2048-upem στο σώμα σας φαίνεται χαλασμένη. Αυτή είναι η παγίδα που στήνει το ISO 14289-1 §7.21.5 για όποιον προσπαθεί να ελέγξει widths διαβάζοντας πεδία dictionary

Έλεγχος glyph width του PDFlibPas σε Delphi: μια εγγραφή /Widths σε glyph space και ένα advance hmtx σε font design units φέρονται στο ίδιο σύστημα συντεταγμένων με κλιμάκωση του metric του προγράμματος σε 1000 μονάδες ανά em πριν γίνει οποιαδήποτε σύγκριση
Το PDFlibPas κλιμακώνει κάθε advance hmtx στο ένα χιλιοστό του em πριν το συγκρίνει με το width του dictionary, και αναφέρει μόνο ό,τι απέχει πάνω από μία μονάδα

Το PDFlibPas κανονικοποιεί στο load. Το TPDFTrueTypeParser αποθηκεύει Advance * 1000 div unitsPerEm στον πίνακα widths του, ώστε το Parser.GetWidth(GID) απαντά ήδη στα ίδια χιλιοστά του em που χρησιμοποιεί το PDF, και το GetRawWidth μένει διαθέσιμο όταν χρειάζεστε design units πίσω. Αυτό αφήνει ακόμα το δυσκολότερο μισό: το πέρασμα από character code σε glyph index. Για μια απλή γραμματοσειρά TrueType η διαδρομή εξαρτάται από τη σημαία Symbolic στο FontDescriptor, bit 3 του /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Οι συμβολικές γραμματοσειρές απευθύνονται απευθείας μέσω του cmap
    // του προγράμματος, με τη σύμβαση (3,0) high-byte ως εφεδρεία
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Μη συμβολική: code -> όνομα glyph μέσω της encoding, όνομα -> Unicode
    // μέσω της Adobe Glyph List, Unicode -> GID μέσω του cmap του προγράμματος
    UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
    if UnicodeValue = 0 then
      Continue;
    GID := Parser.GetGlyphIndex(UnicodeValue);
  end;
  if (GID > 0) and (GID < Parser.GlyphCount) then
    if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
      Inc(MismatchCount);
finally
  Parser.Free;
end;

Δύο λεπτομέρειες σε εκείνο το απόσπασμα ζυγίζουν. Η ανοχή είναι μία μονάδα, όχι μηδέν, γιατί η κανονικοποίηση είναι ακέραια διαίρεση και ένα νόμιμα παραγόμενο αρχείο μπορεί να πέσει μία μονάδα εκτός· αυτό ακριβώς είναι η διατύπωση «μέσα σε ένα χιλιοστό του em» που αναφέρει το diagnostic 10036. Και ο φρουρός GID < Parser.GlyphCount δεν είναι διακόσμηση. Η GetWidth είναι γραμμένη να συγχωρεί για καλούντες rendering, σφίγγοντας δείκτη εκτός εύρους στην τελευταία εγγραφή του hmtx και πέφτοντας στο 750 όταν ο πίνακας απουσιάζει. Το συγχωρητικό είναι σωστό για rendering και λάθος για έλεγχο, οπότε ο έλεγχος απορρίπτει τον δείκτη πριν ζητήσει width αντί να εμπιστευτεί το clamp

Το CIDFontType2 προσθέτει ακόμα μία έμμεση στάθμη

Το PDFlibPas διατρέχει composite fonts με τον ίδιο τρόπο, με /CIDToGIDMap παρεμβαλόμενη ανάμεσα στο CID και το glyph. Τα widths έρχονται στον πίνακα /W, που το ISO 32000-1 §9.7.4.3 του δίνει δύο σχήματα που εναλλάσσονται ελεύθερα σε έναν πίνακα: ένα αρχικό CID ακολουθούμενο από πίνακα διαδοχικών widths, ή ένα πρώτο CID, ένα τελευταίο CID, και ένα μόνο width εφαρμοσμένο σε όλη τη σειρά. Ο έλεγχος κάνει parse και τα δύο, μετά δίνει κάθε προκύπτον ζεύγος στην ίδια σύγκριση, και αναφέρει το σύνολο στο diagnostic 10037. Το βήμα αντιστοίχισης είναι εκεί όπου διαφέρουν τα composite fonts, και είναι ο λόγος που το diagnostic 10021 για χάρτη που λείπει μετράει πριν διαβάσετε καθόλου width — μια απούσα ή κακοσχηματισμένη /CIDToGIDMap δεν απλώς παραβιάζει το §7.21.3.2, κάνει το ερώτημα width αναπάντητο

Τρεις διαδρομές που παίρνει το PDFlibPas από character code σε glyph index σε Delphi: το cmap του προγράμματος για συμβολικές TrueType, μια παράκαμψη encoding και Adobe Glyph List για μη συμβολικές, και βήμα CMap συν /CIDToGIDMap για CIDFontType2
Η σύγκριση width δεν μπορεί να ξεκινήσει μέχρι ο character code να επιλυθεί σε glyph index, και κάθε είδος γραμματοσειράς φτάνει σε εκείνον τον δείκτη με διαφορετική διαδρομή
// Το /CIDToGIDMap είναι το όνομα /Identity ή ροή big-endian 16-bit
// glyph indices, ένα ανά CID (ISO 32000-1 section 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
  GID := CID;
  Result := True;
end
else if Obj is TPDFStream then
begin
  Data := TPDFStream(Obj).GetDecodedStream;
  P := CID * 2 + 1;                       // τα Pascal strings είναι βάσης 1
  if (P >= 1) and (P + 1 <= Length(Data)) then
  begin
    GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
    Result := True;
  end;
end;

Τι πρέπει να κάνει ένας auditor όταν το font program δεν αποκωδικοποιεί;

Να μην πει τίποτα. Οι έλεγχοι πληρότητας /CharSet και /CIDSet που απαιτεί το ISO 14289-1 §7.21.4.2 — diagnostics 10038 και 10039 — είναι το σημείο όπου ένας υπερζηλότυπος validator γίνεται βάρος, γιατί μια αναφορά «το CharSet σας είναι ελλιπές» είναι αδιαχώριστη, για αυτόν που τη διαβάζει, από «ο decoder Type 1 μας παραδόθηκε». Το PDFlibPas λοιπόν αναφέρει ελλείπουσα εγγραφή μόνο όταν τρία πράγματα πετύχουν όλα: το font program αποκωδικοποιεί, η αντιστοίχιση code-to-glyph επιλύεται, και το ίδιο το set αποκωδικοποιεί. Η TPDFType1Decoder.LoadPFBFromString πρέπει να επιστρέψει True και να δώσει πλήθος charstrings πριν οποιοδήποτε όνομα glyph ελεγχθεί απέναντι στο string /CharSet· η διαδρομή /CIDSet θέλει τη ροή να αποσυμπιεστεί και το πλήθος glyphs να γυρίσει θετικό πριν ελεγχθεί έστω ένα bit. Οποιαδήποτε exception στην πορεία καταλήγει σε «κανένα εύρημα», όχι σε ελάττωμα

Ο συντηρητικός κανόνας αναφοράς στο audit PDF/UA του PDFlibPas: μια ελλιπής εγγραφή /CharSet ή /CIDSet αναφέρεται μόνο όταν το font program αποκωδικοποιεί, η αντιστοίχιση code-to-glyph επιλύεται και το ίδιο το set αποκωδικοποιεί, και κάθε αποτυχία παράγει σιωπή
Τρεις ανεξάρτητες επιτυχίες απαιτούνται πριν εκδοθεί εύρημα ελλιπούς εγγραφής, ώστε ένας decoder που παραδίδεται να σας κοστίζει ψευδώς αρνητικό και όχι ψευδή κατηγορία

Αυτό είναι σκόπιμη μεροληψία προς τα ψευδώς αρνητικά, και αξίζει να δηλωθεί ευθέως αντί να θαφτεί. Ένας κατεστραμμένος πίνακας CFF, μια ανυποστήρικτη παραλλαγή Type 1, ή μια /CIDSet κοντύτερη από το εύρος glyphs παράγουν όλα σιωπή αντί για diagnostic. Η λογική είναι ότι τα audits PDF/UA προωθούνται σε συγγραφείς που δεν έχτισαν τα εργαλεία, και μια ψευδής κατηγορία κοστίζει περισσότερο από ένα χαμένο εύρημα: ο συγγραφέας καίει μια μέρα αποδεικνύοντας ότι ένα compliant αρχείο είναι compliant, και παύει να εμπιστεύεται ολόκληρη την αναφορά. Το Matterhorn Protocol κάνει τον ίδιο διαχωρισμό σε άλλη μορφή όταν ξεχωρίζει τους ελέγχους που μπορεί να αποφανθεί μηχανή από αυτούς που πρέπει άνθρωπος, και το checkpoint Fonts του (31) είναι εκεί που ζουν αυτοί. Αν θέλετε την αυστηρότερη ανάγνωση, τρέξτε το PDFlibPas ως γρήγορη πύλη και έναν αφιερωμένο validator ως δεύτερη γνώμη — αυτό το ζευγάρι είναι το ίδιο που περιγράφει το PDF/A και PDF/UA preflight walkthrough

Το /Contents της σελίδας είναι λίστα, όχι stream

Το μοναδικό ακριβότερο λάθος στον έλεγχο content streams είναι να μεταχειριστείτε το /Contents ως ένα stream. Το ISO 32000-1 §7.7.3.3 αφήνει μια σελίδα να κρατά πίνακα streams της οποίας η συνένωση, με κενά ανάμεσα στα μέρη, είναι το πρόγραμμα της σελίδας· οι producers κόβουν σε αυθαίρετα σημεία, και ένα BT μπορεί να κάθεται σε ένα μέλος με το αντίστοιχο ET στο επόμενο. Ένας content processor κρατά κατάσταση — το βάθος εμφώλευσης marked-content, τη γραμματοσειρά που επέλεξε το τελευταίο Tf, τη σημαία text-object — και το Process μηδενίζει αυτή την κατάσταση στην είσοδο. Καλέστε το μία φορά ανά μέλος πίνακα και κάθε stream μετά το πρώτο ξεκινά χωρίς τρέχουσα γραμματοσειρά, οπότε κείμενο που ήταν άψογα επισημασμένο διαβάζεται ως θόρυβος χωρίς επισήμανση και χωρίς γραμματοσειρά. Το PDFlibPas συνενώνει πρώτα και επεξεργάζεται μία φορά

function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
  I: Integer;
begin
  Result := '';
  Obj := DerefIndRef(FDoc, Obj);
  if Obj is TPDFStream then
    Result := TPDFStream(Obj).GetDecodedStream
  else if Obj is TPDFArray then
    for I := 0 to TPDFArray(Obj).Count - 1 do
      Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;

// Μία κλήση Process σε όλη τη συνένωση, ποτέ μία κλήση ανά μέλος
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Ποια Form XObjects μετρούν πραγματικά ως αδόμητα;

Μόνο εκείνα που καλεί πραγματικά μια σελίδα, από σημείο κλήσης εκτός marked content, και των οποίων το ίδιο το περιεχόμενο δείχνει κείμενο. Το diagnostic 10040 επιβάλλει το ISO 14289-1 §7.20 καταγράφοντας τρία ανεξάρτητα γεγονότα ανά object number — έχει κείμενο, κλήθηκε, κλήθηκε μέσα σε marked content — και αναφέροντας μόνο την τομή των δύο πρώτων μείον το τρίτο. Κάθε μία από τις δύο συντομεύσεις είναι λάθος με τρόπο που θα στέλνατε: η επισήμανση κάθε Form με κείμενο στα /Resources τιμωρεί μια βιβλιοθήκη templates απ' όπου δεν σχεδιάζει κανείς, και η επισήμανση κάθε κληθέντος Form τιμωρεί vector logos που δεν κουβαλούν κείμενο και δεν θέλουν επισήμανση. Το σημείο κλήσης επιλύεται με object number και όχι με όνομα πόρου, αφού το ίδιο Form φτάνει συχνά μέσω διαφορετικών ονομάτων σε διαφορετικές σελίδες. Το συνοδευτικό diagnostic 10041 διατρέχει το ίδιο συνενωμένο πρόγραμμα για το §7.21.8, επιλύοντας κάθε operand εμφάνισης κειμένου μέσω της γραμματοσειράς σε ισχύ και μετρώντας τους codes που πέφτουν σε .notdef, που απαγορεύεται ανεξαρτήτως της κατάστασης text rendering — συμπεριλαμβανομένης της αόρατης κατάστασης πίσω από σαρωμένες εικόνες. Πώς πρέπει να τυλιχτούν τα Forms που επιβιώνουν είναι ερώτημα δομικού δέντρου, καλυμμένο στο άρθρο για την κατασκευή δομής tagged PDF

Γραμματοσειρές χωρίς καθόλου FontDescriptor

Μια μη ενσωματωμένη γραμματοσειρά είναι νόμιμη είσοδος για αυτό το audit, όχι κατάσταση σφάλματος, και κάθε helper κάτω από τον έλεγχο ενσωμάτωσης πρέπει να επιβιώνει από αυτήν. Όταν το PDFlibPas δεν βρίσκει /FontDescriptor, ή descriptor χωρίς FontFile, FontFile2, ή FontFile3, καταγράφει diagnostic 10020 — ή 10022 όταν το όνομα είναι ένα από τα Standard 14, που το §7.21.4 NOTE 5 αρνείται εμφαντικά να εξαιρέσει — και μετά συνεχίζει μέσα από το υπόλοιπο του αρχείου. Αυτό είναι όλο το νόημα μιας αναφοράς: ένας συγγραφέας θέλει κάθε εύρημα σε ένα πέρασμα, όχι ένα εύρημα ανά εκτέλεση. Οπότε η αναφορά descriptor που δίνεται στους helpers width, cmap, CharSet και CIDSet μπορεί να είναι Nil, και καθένας τους τεστάρει γι αυτό στην είσοδο αντί να υποθέσει ότι προγενέστερος έλεγχος διέκοψε το audit. Αν η διόρθωση είναι να ενσωματωθεί ό,τι λείπει, η μηχανική είναι στη σημείωση για την ενσωμάτωση ελλιπών γραμματοσειρών σε υπάρχον PDF

Τρέχοντας το audit

Μία κλήση, πάνω σε αρχείο που δεν παράγατε απαραίτητα εσείς. Η TPDFlib.CheckFileCompliance παίρνει επιλογέα compliance test — 2 για PDF/UA-1 κατά ISO 14289-1:2014 — και επιστρέφει είτε μηδέν είτε handle λίστας strings οι εγγραφές του οποίου είναι ένας αριθμητικός code, άνω-κάτω τελεία, και ευανάγνωστο μήνυμα. Τα ευρήματα γραμματοσειρών και content streams που συζητιούνται εδώ καταλαμβάνουν τα 10020 έως 10041 σε εκείνο το εύρος, κρατημένα αριθμητικά μακριά από τους codes 00xxx του PDF/A ώστε ένα μεικτό log μένει ευανάγνωστο. Περνώντας 1 στο Options γίνεται βραχυκύκλωμα στο πρώτο εύρημα, που θέλετε σε build gate και όχι σε εργαλείο συγγραφής. Για έγγραφο ακόμα ανοιχτό στη μνήμη, η GetPDFUADiagnostics τρέχει την ισοδύναμη επιθεώρηση χωρίς ταξίδι μέσω δίσκου

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 επιλέγει PDF/UA-1; Options = 0 αναφέρει κάθε εύρημα
  Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
  if Issues = 0 then
    WriteLn('delivery.pdf: PDF/UA-1 conformant')
  else
  begin
    Count := PDF.GetStringListCount(Issues);
    for I := 1 to Count do
      WriteLn('  ', PDF.GetStringListItem(Issues, I));   // π.χ. 10037 CIDFontType2 ...
  end;
end;

Κανένα από αυτά δεν θέλει εξωτερικό validator binary στη μηχανή, που είναι η διαφορά ανάμεσα σε έλεγχο που τρέχει σε κάθε build και έλεγχο που τρέχει όταν θυμηθεί κανείς. Τα API συμμόρφωσης και diagnostics που περιγράφονται εδώ έρχονται στο standard PDFlibPas Delphi PDF Library, της οποίας η σελίδα προϊόντος κουβαλά τον πλήρη πίνακα diagnostic codes για PDF/UA-1 πλάι στις σουίτες τεστ PDF/A, PDF/X και PDF/E