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

Text advance PDF και επαναφορά clip q/Q σε renderer Delphi

Ο renderer σελίδων του HotPDF Delphi Component πλέον προχωρά κείμενο υπολογίζοντας κάθε μετατόπιση glyph σε text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th όπως το ορίζει το ISO 32000-1 §9.4.4, και μετά μετακινώντας τη μήτρα κειμένου μέσα από το γραμμικό της μέρος με HPDFTranslateTextMatrix. Το clipping αποθηκεύεται ανά frame q και επαναφέρεται στο Q, αλλά μια περιοχή GDI συλλαμβάνεται μόνο όταν εκείνο το frame αλλάζει πραγματικά το clip. Και τα δύο fixes έφτασαν στο HotPDF 2.754.0, και τα δύο ήρθαν από σελίδες πραγματικού κόσμου που αποδίδονταν με μαζεμένες λέξεις ή περιοχές clip που διέρρεαν πέρα από το Q τους. Το πρώτο bug είναι αριθμητική που φαίνεται σωστή μέχρι ένας παραγωγός να γράψει το μέγεθος γραμματοσειράς του στη μήτρα. Το δεύτερο είναι διόρθωση ορθότητας που σχεδόν μας κόστισε το παράλληλο speedup απόδοσης, και ο τρόπος που ξαναπήραμε την ταχύτητα αξίζει να τον ξέρετε αν γράφετε οποιαδήποτε PDF συσκευή βασισμένη σε GDI

Γιατί το κείμενο μαζεύεται σε σωρό όταν ένα PDF χρησιμοποιεί Tf 1;

Επειδή ο παλιός κώδικας advance πρόσθετε απόσταση text space ευθέως στη συνιστώσα μετάφρασης του Tm, σαν text space και user space να είχαν πάντα την ίδια κλίμακα. Αρκετοί παραγωγοί πραγματικού κόσμου θέτουν το μέγεθος γραμματοσειράς στο 1 με Tf και κουβαλάνε το πραγματικό μέγεθος στη μήτρα κειμένου. Με /F1 1 Tf και 12 0 0 12 72 700 Tm, glyph πλάτους 500 μονάδων προχωρά 0.5 σε text space, που είναι 6 πόντοι στη σελίδα μόλις το Tm το κλιμακώσει. Ο παλιός renderer εκτελούσε Tm.e := Tm.e + Adv και μετακινούσε την πένα 0.5 πόντους. Κάθε glyph προσγειωνόταν ένα δωδέκατο χαρακτήρα μετά το προηγούμενο, οπότε γραμμή κειμένου σώματος αποδιδόταν ως σκούρα κηλίδα στο αριστερό περιθώριο ενώ το ίδιο αρχείο έδειχνε τέλειο σε κάθε άλλον viewer

Γιατί το κείμενο μαζεύεται κάτω από Tf 1 στον renderer του HotPDF: με 12 0 0 12 72 700 Tm ένα glyph 500 μονάδων πρέπει να προχωρήσει 0.5 μονάδες text space, που το Tm κλιμακώνει σε 6 πόντους, ενώ ο παλιός κώδικας πρόσθετε 0.5 ευθέως στο Tm.e και αποδιδε γραμμή κειμένου σώματος ως κηλίδα ενός δωδέκατου χαρακτήρα ανά glyph
Παραγωγοί που κωδικοποιούν το μέγεθος γραμματοσειράς στη μήτρα κειμένου έκαναν κάθε glyph να προσγειώνεται ένα δωδέκατο χαρακτήρα μετά το προηγούμενο, ελάττωμα αόρατο στην ίδια την έξοδο της βιβλιοθήκης
// Content stream από παραγωγό που κωδικοποιεί μέγεθος σε Tm, όχι Tf:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// Παλιό advance (απλοποιημένο): απόσταση προστεθειμένη στο Tm.e σαν να ήταν user space
Adv := W * FontSize / 1000;                  // 0.5 για glyph 500 μονάδων
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // Th μόνο στο πλάτος
Adv := Adv + CharSpace;                      // Tc όχι κλιμακωμένο από Th
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // Tw λανθασμένα κλιμακωμένο από Tfs
Tm.e := Tm.e + Adv;                          // αγνοεί Tm.a, Tm.b, Tm.c, Tm.d

// Παλιά ρύθμιση TJ: χωρίς Th, και πάλι μόνο Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;

Η συντόμευση Tm.e δεν ήταν το μόνο ελάττωμα σε εκείνο το block. Το word spacing Tw εκφράζεται σε μονάδες text space χωρίς κλίμακα, όμως ο παλιός κώδικας τον πολλαπλασίαζε με FontSize / 1000, οπότε κάτω από Tf 12 μια στοιχισμένη γραμμή έχανε σχεδόν όλο το διάλειμμα ανάμεσα στις λέξεις της. Η οριζόντια κλίμακα Th εφαρμοζόταν στο πλάτος glyph αλλά όχι στο Tc ή στο Tw, και η ρύθμιση kerning TJ την παραλείπει εντελώς. Το μη-ζωγραφικό μονοπάτι που προχωρά αόρατο κείμενο render mode 3, το είδος που χρησιμοποιούν οι στρώσεις κειμένου OCR, και κείμενο μέσα σε κρυμμένο optional content κουβαλούσαν ιδιωτικό αντίγραφο της ίδιας αριθμητικής, οπότε οτιδήποτε ζωγραφιζόταν μετά από αόρατο run ξεκινούσε από λάθος θέση. Bugs κατάστασης κειμένου σε renderer σπάνια αποτυγχάνουν δυνατά: όπως τα bugs δείκτη operand και ονόματος πόρου που κάποτε μηδένιζαν Tc, Tw και Tz χωρίς ούτε ένα error, αυτά παρήγαν πειστικές σελίδες στην ίδια την έξοδο της βιβλιοθήκης και έσπαζαν μόνο σε αρχεία από άλλους παραγωγούς

Πώς ορίζει το ISO 32000-1 §9.4.4 το glyph advance;

Το ISO 32000-1 §9.4.4 ορίζει το advance εξ ολοκλήρου σε text space και το εφαρμόζει στη μήτρα κειμένου ως μήτρα μετάφρασης, οπότε η απάντηση είναι να υπολογίσετε πρώτα το tx και να αφήσετε το Tm να κάνει την κλίμακα, την περιστροφή και τη στρέβλωση. Για οριζόντια γραφή, tx ισούται με ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, όπου w0 είναι το πλάτος glyph σε χιλιοστά του em, το Tj είναι η ρύθμιση TJ, και το Th είναι το Tz διαιρεμένο με 100. Το νέο Tm είναι [1 0 0 1 tx 0] × Tm, που στο HotPDF είναι ο helper HPDFTranslateTextMatrix: προσθέτει X και Y μέσα από τους συντελεστές μήτρας a, b, c και d αντί να γράφει στο e και στο f ευθέως. Κατά το §9.3.3, το Tw ισχύει μόνο για τον κωδικό χαρακτήρα ενός byte 32, οπότε multibyte κωδικοί CID δεν παίρνουν ποτέ word spacing στο οριζόντιο μονοπάτι. Ο ίδιος helper τώρα οδηγεί τα Td, TD, T*, τους operators ' και ", τις ρυθμίσεις TJ και το κρυφό μονοπάτι κειμένου, που σημαίνει ότι μία μόνο συνάρτηση κατέχει τον κανόνα

Glyph advance του ISO 32000-1 9.4.4 στον renderer του HotPDF: tx υπολογίζεται σε text space από w0, Tj, Tfs, Tc, Tw και Th, μετά εφαρμόζεται μέσω HPDFTranslateTextMatrix ώστε η μετατόπιση να περνά πάνω από τους συντελεστές μήτρας a, b, c και d και Td, TD, TJ και το κρυφό μονοπάτι κειμένου να μοιράζονται έναν κανόνα
Η πρόσθεση του advance στο Tm.e ευθέως δουλεύει μόνο όταν το text space ισούται με το user space· η δρομολόγησή του μέσω των συντελεστών μήτρας κρατά κλιμακωμένο, περιστραμμένο και στρεβλωμένο κείμενο σωστό
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
  Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
  Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;

// Οριζόντιο glyph advance, ISO 32000-1 9.4.4
W   := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
  Adv := Adv + State.Text.WordSpace;         // Tw σε text space, χωρίς κλίμακα
Adv := Adv * State.Text.HorizScale / 100;    // Th ισχύει για ολόκληρο το άθροισμα
HPDFTranslateTextMatrix(Tm, Adv, 0);

// Αριθμητικό στοιχείο TJ: ίδιος χώρος, ίδιο Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);

Η τοποθέτηση glyphs έπρεπε να ακολουθήσει την ίδια λογική. Όταν δεν είναι διαθέσιμο ενσωματωμένο outline και ο renderer γυρνά σε GDI TextOutW, τώρα χτίζει την πλήρη μήτρα glyph από CTM × Tm × rise × em κλίμακα, συμπεριλαμβανομένου του Th, και την εγκαθιστά με SetWorldTransform σε κατάσταση GM_ADVANCED μέσα σε ζευγάρι SaveDC / RestoreDC. Η γραμματοσειρά GDI δημιουργείται σε σταθερό ύψος 1000 μονάδων και ο μετασχηματισμός κάνει το μέγεθος, οπότε περιστραμμένο και στρεβλωμένο κείμενο κρατά τον προσανατολισμό του αντί να ζωγραφίζεται όρθιο σε μετασχηματισμένο σημείο αρχής. Η κατακόρυφη κατάσταση γραφής είναι η μία σκόπιμη ασυμμετρία: γραμματοσειρά WMode 1 προχωρά κάτω στον άξονα y κατά την κατακόρυφη μετρική της, και η οριζόντια κλίμακα δεν ισχύει για εκείνον τον άξονα

Τι αποθηκεύει πραγματικά το q/Q σε κατάσταση γραφικών PDF;

Το ISO 32000-1 §8.4.2 απαριθμεί το τρέχον μονοπάτι clipping ως μέρος της κατάστασης γραφικών, οπότε το Q πρέπει να επαναφέρει το clip ακριβώς όπως ήταν στο αντίστοιχο q, όχι μόνο τις αριθμητικές παραμέτρους. Το HotPDF κρατούσε ήδη στοίβα κατάστασης γραφικών με το CTM, χρώματα, παραμέτρους γραμμής και κατάσταση κειμένου, αλλά το GDI κρατά το clip στο device context, έξω από εκείνη τη στοίβα. Ένα αντίγραφο της αριθμητικής κατάστασης λοιπόν επανέφερε τα πάντα εκτός από το clip, και clip εγκατεστημένο με W n μέσα σε block q ... Q εξακολουθούσε να κόβει κάθε μεταγενέστερη πράξη στη σελίδα. Τα Form XObjects πρόσθεσαν δεύτερη διαδρομή προς την ίδια αποτυχία, επειδή το §8.10 δίνει σε φόρμα έμμεσο save και restore γύρω από το περιεχόμενό της, και περιεχόμενο φορμών πραγματικού κόσμου μερικές φορές αφήνει τους δικούς του operators q ανισσοροπούμενους παρόλο που η προδιαγραφή απαιτεί να ζευγαρώνουν. Ο renderer τώρα καλεί CaptureClipBeforeChange και SaveDC πριν τρέξει φόρμα, μετά αφού τελειώσει η φόρμα πετάει οποιεσδήποτε αποθηκευμένες περιοχές βαθύτερες από το βάθος εισόδου και καλεί RestoreDC, οπότε κάθε αποθηκευμένο HRGN έχει ακριβώς ένα μονοπάτι απελευθέρωσης

Τεμπέλικη σύλληψη clip με THPDFSavedClipState

Το fix που παραδόθηκε αποθηκεύει μία εγγραφή THPDFSavedClipState ανά q, αλλά αναβάλλει το ακριβό μέρος μέχρι το frame να τροποποιήσει πρώτη φορά το clip. Η εγγραφή κρατά το handle περιοχής, το βάθος στοίβας στο οποίο ανήκει, το device context από το οποίο πάρθηκε και σημαία Captured. Το DevPushState γεμίζει μόνο το βάθος και το DC και μεγαλώνει τον πίνακα frames διπλασιάζοντας από 16, οπότε content stream γεμάτο q 1 0 0 1 x y cm ... Q δεν εκχωρεί καθόλου GDI object. Operators που πρόκειται να αλλάξουν clipping, εννοώντας ζωγραφική μονοπατιού με εκκρεμές W ή W*, τον operator n, γεμίσματα patterns και είσοδο φόρμας, καλούν πρώτα CaptureClipBeforeChange

Τεμπέλικη σύλληψη clip GDI στον renderer του HotPDF: το DevPushState καταγράφει μόνο βάθος και DC ανά q, το CaptureClipBeforeChange διαβάζει την περιοχή ακριβώς πριν W, n ή είσοδος φόρμας αλλάξουν clipping, το DevPopState επαναφέρει και σβήνει στο Q, και η πρόθυμη εκδοχή που σύλλαβε σε κάθε q έριξε την παράλληλη απόδοση στα περίπου 1.15 επί τη μονόνημη
Η δημιουργία GDI περιοχής σε κάθε q άφηνε πεινασμένα τα threads απόδοσης, οπότε η σύλληψη τώρα γίνεται μόνο όταν operator πρόκειται να αλλάξει clipping και η πύλη speedup 1.5 επί περνά ξανά
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
  Index, ClipResult: Integer;
  Region: HRGN;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index < 0) or FSavedClips[Index].Captured or
     (FSavedClips[Index].StackDepth <> FGSStack.Count) or
     (FSavedClips[Index].DC <> FDC) then Exit;   // ήδη αποθηκευμένο, ή όχι δικό μας
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // 0 σημαίνει καθόλου clip
  if ClipResult <= 0 then
  begin
    DeleteObject(Region);
    Region := 0;
    if ClipResult < 0 then RaiseLastOSError;
  end;
  FSavedClips[Index].Region := Region;
  FSavedClips[Index].Captured := True;
end;

procedure THPDFPageRenderer.DevPopState;
var
  Index: Integer;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
  begin
    if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
      SelectClipRgn(FDC, FSavedClips[Index].Region);  // Region 0 αφαιρεί το clip
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

Το μετρημένο κόστος της πρόθυμης εκδοχής είναι ο λόγος που υπάρχει αυτός ο σχεδιασμός. Η πρώτη σωστή υλοποίηση δημιουργούσε και διάβαζε μια GDI περιοχή σε κάθε q, και σε σελίδες φτιαγμένες κυρίως από αριθμητικούς μετασχηματισμούς τα threads renderer περνούσαν τον χρόνο τους ανταγωνιζόμενα για GDI region objects αντί να ραστερίζουν. Το παράλληλο pipeline απόδοσης έπεσε από το αναμενόμενο κέρδος του σε περίπου 1.13 έως 1.20 επί τη μονόνημη απόδοση και απέτυχε την πύλη speedup 1.5 επί στη σουίτα benchmarks. Με τεμπέλικη σύλληψη και επαναχρησιμοποιούμενη χωρητικότητα frames, το ίδιο benchmark περνά την αρχική πύλη 1.5 επί ξανά. Η μικρή antialiasing glyphs TrueType έφτασε στην ίδια έκδοση και ήταν ο προφανής ύποπτος, αλλά το regression ιχνηλατήθηκε πίσω στην εκχώρηση περιοχών, που είναι καλή υπενθύμιση να μετράτε πριν κατηγορήσετε το νεότερο χαρακτηριστικό

Πού είναι τα όρια αυτής της προσέγγισης;

Το αποθηκευμένο clip είναι GDI περιοχή σε pixels συσκευής, οπότε είναι ακριβές για το bitmap που αποδίδεται και ανούσιο για οποιονδήποτε άλλο στόχο. Γι' αυτό κάθε frame καταγράφει το device context του και το DevPopState παραλείπει την επαναφορά όταν το DC έχει αλλάξει, για παράδειγμα ενώ ομάδα διαφάνειας αποδίδεται στο δικό της bitmap στρώσης. Το GetClipRgn να επιστρέφει μηδέν είναι νόμιμο αποτέλεσμα που σημαίνει κανένα clip, και η επαναφορά του με SelectClipRgn(FDC, 0) είναι ό,τι σωστά αφαιρεί clip που δεν υπήρχε στο αντίστοιχο q. Στην πλευρά κειμένου, η διόρθωση ανακρούει πού πηγαίνει κάθε glyph, αλλά δεν επινοεί πλάτη: αν μια γραμματοσειρά παραλείπει τον πίνακα /Widths της και το ενσωματωμένο πρόγραμμα είναι μη διαθέσιμο, το advance εξακολουθεί να είναι μόνο τόσο καλό όσο το fallback πλάτους. Όταν τεστάρετε regression αυτή την περιοχή, κρατήστε τουλάχιστον ένα fixture με Tf 1 και κλιμακωμένο Tm, ένα με μη μηδενικό Tz και Tw, και ένα με clip μέσα σε q ... Q ακολουθούμενο από περιεχόμενο έξω από αυτό, επειδή κανένα από εκείνα δεν εμφανίζεται σε έγγραφα παραγμένα από την ίδια τη βιβλιοθήκη

Αν οδηγείτε τον renderer από κώδικα εφαρμογής, τίποτα δεν αλλάζει στο πρότυπο κλήσης που περιγράφεται στο απόδοση σελίδας PDF σε bitmap, και σελίδες που έδειχναν προηγουμένως μουτσιασμένες γραμμές ή κομμένο περιεχόμενο θα πρέπει απλώς να αποδίδονται σωστά στο 2.754.0 και μεταγενέστερα. Λεπτομέρειες για το component, υποστηριζόμενες εκδόσεις Delphi και C++Builder και αδειοδότηση βρίσκονται στη σελίδα προϊόντος HotPDF Delphi PDF Component