Όταν ένα PDF δεν ενσωματώνει γραμματοσειρά, το HotPDF component αποδίδει εκείνο το κείμενο με μια εγκατεστημένη γραμματοσειρά Windows που επιλέγει το HPDFMapBaseFontToSystem: αποκωδικοποιεί το όνομα /BaseFont, κόβει το μέρος του στυλ, δοκιμάζει αρκετές συλλαβές μέχρι το GDI να επιβεβαιώσει ότι η οικογένεια είναι εγκατεστημένη, μετρά τα λείποντα widths standard 14 από γραμματοσειρές metric-συμβατές, και μετατρέπει κωδικούς ενός byte σε Unicode πριν τη σχεδίαση. Κάθε ένα από αυτά τα βήματα υπάρχει επειδή η αφελής έκδοση απέτυχε σε πραγματικά αρχεία. Ο page renderer RenderLoadedPageToBitmap τα πηγαίνει καλά με ενσωματωμένα programs· αυτή είναι η ιστορία των γραμματοσειρών που δεν είναι καθόλου μέσα στο αρχείο
Γιατί το GDI σχεδιάζει αθόρυβα λάθος typeface για μη ενσωματωμένη γραμματοσειρά;
Το GDI δεν αναφέρει ποτέ γραμματοσειρά που λείπει: δώστε στο CreateFontIndirect όνομα face που δεν ξέρει και επιλέγει αθόρυβα υποκατάστατο, συχνά ένα διαφορετικό serif χωρίς βάρος bold. Ο πρώιμος renderer περνούσε το PDF όνομα σχεδόν αναλλοίωτο, οπότε τα TimesNewRoman,Bold, TimesNewRomanPS-BoldMT και SegoeUI-Semibold δεν ταίριαζαν τίποτα και βγηνόταν σε ό,τι διάλεγε το GDI. Τα ονόματα μπορεί να είναι χειρότερα. Το ISO 32000-1 §7.3.5 αφήνει ένα όνομα να γράψει οποιοδήποτε byte ως #xx, και οι CJK producers συχνά γράφουν ονόματα γραμματοσειρών ως escaped UTF-8 ή bytes legacy code page· πριν το v2.766.69 τα ίδια τα escapes γινόταν το όνομα face. Το HPDFMapBaseFontToSystem τώρα αποκωδικοποιεί πρώτα τα escapes, επιστρέφει έγκυρη ακολουθία bytes UTF-8 ως χαρακτήρες του, και διαβάζει τυχόν άλλα υψηλά bytes στην code page του συστήματος
Το κόψιμο του στυλ είναι εκεί που δαγκώνουν οι ευρετικές μέθοδοι. Ένα κόμμα πάντα τελειώνει την οικογένεια (το Arial,Bold δίνει Arial), αλλά μια παύلة μόνο όταν η λέξη μετά από αυτήν είναι στυλ: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra ή Condensed. Ο κανόνας αυτός κρατά το MS-Mincho ολόκληρο ενώ γυρνά το Calibri-Light σε Calibri. Το HotPDF μετά δοκιμάζει συλλαβές της οικογένειας, ακολουθούμενες από συλλαβές της οικογένειας με αφαίρεση suffix PSMT, MT ή PS. Από το v2.768.18 εκείνες οι συλλαβές καλύπτουν κάθε επιλογή κενών στα σημεία όπου μπορεί να ξεκινήσει λέξη: πριν από κεφαλαίο που έπεται πεζού (MyriadPro γίνεται Myriad Pro), στο τελευταίο κεφαλαίο μιας σειράς που ακολουθείται από πεζό (UIGothic), και μετά από αρχικό MS (MSPGothic), από την πλήρως αραιή μορφή ως το όνομα όπως γράφτηκε· πέρα από τέσσερα τέτοια σημεία δοκιμάζονται μόνο η πλήρως αραιή και το ενωμένο όνομα. Το αραιό κενών δεν μπορεί να εφαρμοστεί τυφλά, επειδή τα Windows κρατούν κάποιες λέξεις ενωμένες: η SimSun είναι εγκατεστημένη με ακριβώς εκείνη τη συλλαβή, ενώ τα MicrosoftYaHei, MicrosoftJhengHei και MSPGothic ανήκουν στα Microsoft YaHei, Microsoft JhengHei και MS PGothic. Πριν το v2.768.18 ο mapper έβαζε κενό πριν από κάθε εσωτερικό κεφαλαίο, οπότε το MicrosoftYaHei ψαχνόταν ως Microsoft Ya Hei και δεν βρισκόταν ποτέ. Ένας υποψήφιος μετράει ως εγκατεστημένος όταν CreateFontIndirect ακολουθούμενο από GetTextFace επιστρέφει το ζητούμενο όνομα ή, από το v2.768.18, όταν ο πίνακας name της επιλεγμένης γραμματοσειράς το απαριθμεί ως family, full ή typographic family name σε οποιαδήποτε γλώσσα· η απάντηση κρατιέται σε cache ανά όνομα, οπότε έγγραφα με πολλές γραμματοσειρές που δεν είναι εγκατεστημένες δεν ρωτούν πια τα Windows για κάθε όνομα σε κάθε σελίδα
Επειδή η συνάρτηση αντιστοίχισης είναι public στη μονάδα HPDFRenderFontMetrics, μια αναφορά preflight μπορεί να δείξει με ποια εγκατεστημένη οικογένεια θα αποδοθεί κάθε μη ενσωματωμένη γραμματοσειρά, χρησιμοποιώντας την απαρίθμηση γραμματοσειρών που το THotPDF εκθέτει ήδη για φορτωμένα έγγραφα:
uses
HPDFDoc, HPDFRenderFontMetrics;
procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
Page, I: Integer;
Info: THPDFLoadedFontInfo;
begin
for Page := 0 to Pdf.LoadedPageCount - 1 do
for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
Log.Add(Format('page %d /%s %s -> %s',
[Page + 1, string(Info.ResourceName), string(Info.FontName),
HPDFMapBaseFontToSystem(Info.FontName)]));
end;
Πώς μετρά το HotPDF τις γραμματοσειρές standard 14 χωρίς /Widths;
Το HotPDF μετρά τις λείπουσες προωθήσεις πάνω στην εγκατεστημένη γραμματοσειρά με τα ίδια metrics, επειδή το ISO 32000-1 §9.6.2.2 επιτρέπει στις γραμματοσειρές standard 14 να παραλείπουν /Widths και η βιβλιοθήκη δεν κουβαλά πίνακες AFM. Η Arial κουβαλά metrics Helvetica, η Times New Roman κουβαλά Times και η Courier New κουβαλά Courier, οπότε το HPDFMeasureBaseFontWidths δημιουργεί το αντίστοιχο face σε lfHeight = -1000 και καλεί το GetCharWidth32W· σε εκείνο το ύψος το αποτέλεσμα είναι ήδη στις μονάδες 1/1000 em που χρησιμοποιούν τα PDF widths. Ο renderer πρώτα γυρνά κάθε κωδικό σε Unicode μέσω /Encoding, /BaseEncoding και /Differences, με default το StandardEncoding. Το SVG export και η εξαγωγή κειμένου πέφτουν σε μία ακόμη παγίδα: μια standard γραμματοσειρά Type 1 χωρίς καθόλου /Encoding παρήγαγε decoder χωρίς πληροφορία κωδικοποίησης, το SVG export δεν τον κατέγραφε ποτέ, και κάθε μετρημένο πλάτος πήγαινε χαμένο. Η προμήθεια του υπονοούμενου StandardEncoding το διόρθωσε, υπό τον όρο να σημειώνεται ως predefined encoding· η δρομολόγησή του στη διαδρομή ονόματος CMap αποκωδικοποιεί κάθε κωδικό ως 0 και κάθε πλάτος ακολουθεί
Bold, italic και ένα off-by-one στα flags του font descriptor
Η εγγραφή /Flags ενός font descriptor αριθμεί τα bits της από το 1, όχι από το 0, οπότε το ForceBold είναι bit 19 ($40000) και το Italic bit 7 ($40), κατά το ISO 32000-1 Table 123. Ο παλιός κώδικας τεστάρε $20000, που είναι bit 18, SmallCap. Το λάθος επέζησε από το v2.345.0 ως το v2.766.53 επειδή το /FontDescriptor είναι σχεδόν πάντα έμμεση αναφορά και ο builder γραμματοσειρών διάβαζε μόνο άμεσα objects, οπότε όλος ο κλάδος flags δεν έτρεχε ποτέ, και η ίδια τύφλωση αγνοούσε το /Widths 12 0 R και στοιθούσε κείμενο σε fallback advance 500 μονάδων. Μόλις το v2.766.53 άρχισε να επιλύει έμμεσες αναφορές μέσω του renderer, το bit έπρεπε να διορθωθεί στην ίδια αλλαγή, αλλιώς κάθε face small-caps θα αποδιδόταν ξαφνικά bold:
const
// Το ISO 32000-1 Table 123 μετρά θέσεις bit από το 1
FD_ITALIC = $00040; // bit 7
FD_SMALLCAP = $20000; // bit 18, όχι βάρος
FD_FORCEBOLD = $40000; // bit 19
procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
if (Flags and FD_FORCEBOLD) <> 0 then
LF.lfWeight := FW_BOLD;
if (Flags and FD_ITALIC) <> 0 then
LF.lfItalic := 1;
end;
Γιατί γραμματοσειρές CJK με UCS2 CMap παίρνουν λάθος πλάτη;
CJK κείμενο με predefined UCS2 CMap σχεδιάζει τα σωστά glyphs αλλά λάθος διάσταση όταν ένας renderer μεταχειρίζεται τον κωδικό ως CID, επειδή το /W δεικτοδοτείται από CID, όχι από κωδικό χαρακτήρα. Με STSong-Light και UniGB-UCS2-H, ο κωδικός τυχαίνει να ισούται με την τιμή Unicode, οπότε το GDI σχεδιάζει τους σωστούς χαρακτήρες και το bug κρύβεται στις προωθήσεις: τα πεζά γράμματα φτάνουν ως κωδικοί 97 και άνω, πέφτουν έξω από εγγραφή /W όπως [1 95 500], και όλα παίρνουν το default πλάτος /DW του 1000. Από το v2.766.56 ο renderer του HotPDF διαβάζει κωδικούς μέσω των εύρων codespace του CMap (ISO 32000-1 §9.7.6.2) και τους χαρτογραφεί σε CIDs πριν ψάξει πλάτη. Χρησιμοποιούνται μόνο οι ενσωματωμένοι πίνακες UCS2 και UTF16 και τα embedded CMap streams· μια προσέγγιση identity για κάτι όπως GBK-EUC-H θα έμοιαζε απλώς υποστηριζόμενη ενώ παράγει λάθος έξοδο, οπότε ο renderer δεν προσποιείται
Γιατί οι τονισμένοι χαρακτήρες γίνονται ερωτηματικά σε κινεζικά Windows;
Κωδικοί ενός byte δεν πρέπει ποτέ να φτάσουν τις GDI συναρτήσεις ANSI («A»), επειδή οι GetGlyphOutlineA και GetGlyphIndicesA ερμηνεύουν bytes στην code page του συστήματος ενώ η TextOutA χρησιμοποιεί τον character set της επιλεγμένης γραμματοσειράς. Σε κινεζικό σύστημα, το byte $A9 της Arial (το σύμβολο copyright στα Windows-1252) έγινε lead byte GBK και αποδιδόταν ως «?», μια παγίδα στην οποία έπεσε κατευθείαν η διαδρομή unhinted outline που προστέθηκε στο v2.766.83. Το v2.767.3 ρωτά την πραγματοποιημένη γραμματοσειρά για τον character set της με GetTextCharset, τον μετατρέπει σε code page μέσω TranslateCharsetInfo, περνά το byte από MultiByteToWideChar και καλεί τις συναρτήσεις W· οι γραμματοσειρές symbol χρησιμοποιούν U+F000 συν τον κωδικό αντ' αυτού. Κωδικοποιήσεις που διαφωνούν με τα Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — χαρτογραφούνται σε Unicode πριν τις δει οποιαδήποτε γραμματοσειρά συστήματος
Ποια είναι τα όρια της σχεδίασης με γραμματοσειρές συστήματος;
Η απόδοση με γραμματοσειρές συστήματος είναι προσέγγιση, και το HotPDF component είναι ειλικρινές ως προς το πού σταματά. Πριν το v2.768.18 ο έλεγχος εγκατάστασης σύγκρινε μόνο το όνομα που επιστρέφει το GetTextFace, και σε τοπικοποιημένα Windows εκείνη η συνάρτηση αναφέρει το όνομα οικογένειας στη γλώσσα του συστήματος, οπότε το Microsoft YaHei σε κινεζικά Windows ή το Yu Mincho σε ιαπωνικά Windows κρινόταν ως λείπον και σχεδιαζόταν σε υποκατάστατο GDI· από το v2.768.18 ένα face που γυρνά υπό άλλο όνομα ψάχνεται επίσης στον πίνακα name της γραμματοσειράς, και τέτοιες γραμματοσειρές βρίσκονται. Η metric συμβατότητα εγγυάται μόνο για τις οικογένειες Helvetica, Times και Courier· το Symbol χαρτογραφείται σε Symbol και το ZapfDingbats σε Wingdings, που είναι πρόσκαιρη λύση και όχι ταίριασμα. Το preflight παραπάνω βλέπει επίσης μόνο γραμματοσειρές στο dictionary /Resources κάθε σελίδας, όχι εκείνες που αναφέρονται μέσα από form XObjects. Όταν ένας κωδικός δεν μπορεί ακόμα να σχεδιαστεί, το tracking ανεπίλυτων glyphs στον χρόνο σχεδίασης το αναφέρει, που είναι καλύτερο σήμα από το να κοιτάτε thumbnails
Η ανθεκτική διόρθωση κάθεται στην πλευρά της συγγραφής. Το ίδιο το HotPDF γράφει με FontEmbedding σε True by default, υποκαθιστώντας μια ενσωματωμένη Arial ακόμα κι όταν ο κώδικας καλεί SetFont με Helvetica, και το ενσωματωμένο κείμενο περνά από τον renderer glyphs ενσωματωμένων γραμματοσειρών αντί για οποιαδήποτε από τις μαντέψικες λύσεις παραπάνω. Μια φτηνή ασφάλεια για εισερχόμενα αρχεία είναι να προειδοποιείτε πριν την απόδοση όταν μια χαρτογραφημένη οικογένεια δεν είναι στη λίστα γραμματοσειρών οθόνης:
// VCL: το Screen.Fonts απαριθμεί ονόματα εγκατεστημένων οικογενειών (unit Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;
Για το πλήρες component, συμπεριλαμβανομένης της απόδοσης σελίδων, της εξαγωγής κειμένου και του font subsetting στην πλευρά συγγραφής, δείτε τη σελίδα προϊόντος HotPDF Delphi PDF component