Το εξαγόμενο PDF βάζει κάθε όριο στήλης μισό χαρακτήρα αριστερά από εκεί που το σχεδιάζει το Excel, και κάθε αναδιπλούμενο κελί σπάει τώρα σε διαφορετικό σημείο. Το πλάτος στήλης του Excel δεν μετριέται σε χαρακτήρες ούτε σε points. Μετριέται σε μονάδες Max Digit Width (MDW) της γραμματοσειράς Normal του βιβλίου εργασίας, και το HotXLS μετρά αυτή τη γραμματοσειρά με GDI πριν από κάθε χτίσιμο σελιδοποίησης. Ο τρόπος αποτυχίας είναι σιωπηλός: τίποτα δεν εγείρει εξαίρεση, τα αποθηκευμένα πλάτη κάνουν round-trip byte προς byte, και η γεωμετρία εξακολουθεί να αστοχεί κατά λίγα τοις εκατό ανά στήλη, μέχρι η συσσωρευμένη απόκλιση να σπρώξει έναν πίνακα μίας σελίδας σε δύο
Σε ποια μονάδα μετριέται το πλάτος στήλης του Excel;
Το πλάτος μιας στήλης σε ένα φύλλο εργασίας είναι ένα πλήθος χαρακτήρων ψηφίου της γραμματοσειράς Normal του βιβλίου εργασίας, όχι απόλυτη μέτρηση. Το ECMA-376 §18.3.1.13 ορίζει το attribute width του <col> ως προς το Maximum Digit Width εκείνης της γραμματοσειράς στα 96 dpi, και δίνει τη μετατροπή από ένα αποθηκευμένο πλάτος πίσω σε pixels ως μια έκφραση με περικοπή πάνω στο MDW. Για το Calibri 11, που είναι αυτό που στέλνει το Excel ως στυλ Normal, το MDW μετρά 7 pixels. Περάστε το προεπιλεγμένο πλάτος των 8.43 units μέσα από τον τύπο της προδιαγραφής με MDW 7 και παίρνετε ακριβώς 64 pixels, δηλαδή 48 points στα 96 dpi. Αυτοί είναι οι αριθμοί που αναφέρει το ίδιο το Excel, οπότε αποτελούν χρήσιμο έλεγχο: αν η μετατροπή σας αναπαράγει τα 8.43 units σε 64 pixels, η αριθμητική είναι σωστή και μόνο η είσοδος MDW μπορεί ακόμη να είναι λάθος
const
// Μέγιστο πλάτος ψηφίου (MDW) της προεπιλεγμένης γραμματοσειράς κειμένου σε pixels στα 96 dpi.
// Το Calibri 11 μετρά 7 px, που αναπαράγει τα ακριβή πλάτη σε pixels
// που αποθηκεύει το Excel (8.43 units -> 64 px -> 48 pt).
DefaultMDW = 7;
MinimumColumnWidth = 24.0;
function ColumnWidthToPointsMdW(Value: Double; MdW: Integer): Double;
var
Pixels: Integer;
begin
if Value <= 0 then
Value := 8.43;
if MdW <= 0 then
MdW := DefaultMDW;
Pixels := Trunc(((256 * Value + Trunc(128 / MdW)) / 256) * MdW) + 5;
Result := Pixels * 0.75; // pixels 96 dpi -> points
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
Το HotXLS κρατά αυτή την αριθμητική σε ακριβώς μία συνάρτηση, μέσα στη μονάδα lxPagination, ώστε να υπάρχει ένα μοναδικό σημείο όπου ο χάρακας μπορεί να είναι λάθος. Το + 5 είναι το padding που προσθέτει το Excel για τις γραμμές πλέγματος και τα περιθώρια των κελιών, το * 0.75 μετατρέπει τα pixels των 96 dpi σε PostScript points, και το κατώφλι στο MinimumColumnWidth υπάρχει ώστε μια παθολογικά στενή στήλη να αφήνει ακόμη μια λωρίδα όπου ο renderer μπορεί να σχεδιάσει περίγραμμα. Το δημόσιο σημείο εισόδου ColumnWidthToPoints κρατά την παλιά υπογραφή του ενός ορίσματος και προωθεί ένα μετρημένο MDW σε αυτή τη συνάρτηση, κάτι που επέτρεψε στην αλλαγή συμπεριφοράς να προσγειωθεί χωρίς να αγγίξει ούτε ένα σημείο κλήσης
Γιατί μια γραμματοσειρά Normal εκτός Calibri μετακινεί κάθε όριο
Η απόκλιση είναι πολλαπλασιαστική, και γι αυτό διαβάζεται ως σφάλμα απόδοσης παρά ως σφάλμα μονάδων. Το MDW είναι συντελεστής πάνω στο πλάτος, όχι μετατόπιση. Ανεβάστε το MDW από 7 σε 8 και η προεπιλεγμένη στήλη των 8.43 units πάει από 64 pixels σε 72, ένα άλμα 8 pixels ή 6 points σε μία μόνο στήλη. Δέκα στήλες με αυτό και η δεξιά άκρη του πίνακα έχει μετακινηθεί σχεδόν μία ίντσα. Τα βιβλία εργασίας που σκοντάφτουν εδώ είναι απολύτως συνηθισμένα: οτιδήποτε παράγει ένα εργαλείο αναφορών που σφραγίζει Arial ή Segoe UI στο στυλ Normal, οτιδήποτε αποθηκεύεται από πρότυπο εξαγωγής ERP, οτιδήποτε ένας πελάτης ξαναστύλισε κάποτε και το ξέχασε
Δύο συγγενικά συστήματα διάταξης κληρονομούν το σφάλμα αντί να το προκαλούν. Οι συγχωνευμένες περιοχές αθροίζουν τα πλάτη σε points των στηλών που τις απαρτίζουν, οπότε μια συγχώνευση που χωρούσε σε μία σελίδα στο Excel μπορεί να ξεχειλίσει μετά από απόκλιση MDW, κάτι που αξίζει να θυμάστε όταν φτιάχνετε πρότυπα αναφορών με συγχωνευμένα κελιά. Το shrink-to-fit συγκρίνει το μετρημένο πλάτος κειμένου με το ίδιο πλάτος στήλης, οπότε το λανθασμένο MDW αλλάζει επίσης ποια κελιά συρρικνώνονται και κατά πόσο. Η ίδια οικογένεια σύγχυσης μονάδων εμφανίζεται στις άγκυρες σχεδίων, όπου η γεωμετρία εικόνων και η κλιμάκωση EMU έχει τη δική της αλυσίδα μετατροπής για να πάει στραβά
Πώς μετρά το HotXLS το MDW κατά την εκτέλεση
Το HotXLS επιλύει το MDW από το ίδιο το βιβλίο εργασίας αντί να υποθέτει μια σταθερά, και δύο procedures κάνουν τη δουλειά. Το PaginationApplyNormalFont διαβάζει τη γραμματοσειρά του στυλ Normal από το βιβλίο εργασίας και τρέχει στην αρχή του χτισίματος σελιδοποίησης, πριν υπολογιστεί οποιαδήποτε γεωμετρία στηλών· επαναφέρει πρώτα σε Calibri 11, ώστε ένα βιβλίο εργασίας χωρίς πίνακα γραμματοσειρών να μην μπορεί να κληρονομήσει παλιά κατάσταση από προηγούμενο χτίσιμο. Η γραμματοσειρά του στυλ Normal είναι το fonts[0] στο styles.xml, που το component το εκθέτει ως Workbook.Fonts[0]
// Διαβάζει το fonts[0] (τη γραμματοσειρά του στυλ Normal) από το βιβλίο εργασίας του φύλλου.
// Τα classic worksheets χωρίς πίνακα γραμματοσειρών κρατούν την προεπιλογή Calibri 11.
procedure PaginationApplyNormalFont(Worksheet: TObject);
var
Sh: TXLSXWorksheet;
Fnt: TXLSXFont;
begin
PaginationNormalFontName := 'Calibri';
PaginationNormalFontSize := 11;
if not (Worksheet is TXLSXWorksheet) then
Exit;
Sh := TXLSXWorksheet(Worksheet);
if (Sh.Workbook = nil) or (Sh.Workbook.Fonts.Count < 1) then
Exit;
Fnt := Sh.Workbook.Fonts[0];
if Fnt.Name <> '' then
PaginationNormalFontName := Fnt.Name;
if Fnt.Size > 0 then
PaginationNormalFontSize := Fnt.Size;
end;
Η δεύτερη procedure, το PaginationMeasureMdW, ζητά από το GDI την έκταση του μοναδικού χαρακτήρα '0' μέσω GetTextExtentPoint32W σε ένα κοινόχρηστο off-screen bitmap canvas, υποχωρεί στο tmAveCharWidth από το GetTextMetricsW όταν η κλήση έκτασης αποτύχει, και υποχωρεί στο DefaultMDW όταν δεν είναι διαθέσιμο ούτε αυτό. Η cache του είναι μία μοναδική θέση με κλειδί (όνομα, μέγεθος), που ακούγεται πρόχειρο μέχρι να κοιτάξετε το μοτίβο πρόσβασης: ένα χτίσιμο σελιδοποίησης ζητά την ίδια γραμματοσειρά Normal σε κάθε στήλη κάθε σελίδας, οπότε μία θέση έχει σχεδόν τέλειο ποσοστό ευστοχίας και κοστίζει τρεις συγκρίσεις ανά κλήση
Τι γίνεται χωρίς πίνακα γραμματοσειρών, χωρίς GUI ή με γραμματοσειρά που λείπει;
Το HotXLS υποβαθμίζεται στη σταθερά Calibri 11 σε κάθε περίπτωση όπου η πραγματική γραμματοσειρά Normal δεν μπορεί να προσδιοριστεί, και το κάνει σιωπηλά εκ σχεδιασμού. Τα classic BIFF φύλλα εργασίας είναι η συνηθισμένη περίπτωση: οι παλαιές μορφές δεν μεταφέρουν δεξαμενή γραμματοσειρών XLSX στην οποία να αναφέρεται το fonts[0], οπότε ο έλεγχος τύπου βγαίνει νωρίς και ισχύει το προεπιλεγμένο MDW 7. Αυτό δεν είναι διόρθωση, είναι η προηγούμενη συμπεριφορά που διατηρήθηκε σκόπιμα, ώστε η προσθήκη μέτρησης στη διαδρομή XLSX να μην μπορεί να προκαλέσει οπισθοδρόμηση στην έξοδο των classic μορφών
Η εξάρτηση από το GDI είναι η τίμια επιφύλαξη. Η μέτρηση τρέχει πάνω σε ένα device context των Windows, οπότε η διαδρομή προϋποθέτει έναν host Windows με τη γραμματοσειρά εγκατεστημένη. Σε μια υπηρεσία ή σε έναν headless build agent, οι μετρικές κειμένου του GDI συνήθως εξακολουθούν να επιλύονται, αλλά μια γραμματοσειρά που δεν είναι εγκατεστημένη σε εκείνο το μηχάνημα αντικαθίσταται από τον font mapper και μετράτε την αντικαταστάτρια. Ποτέ δεν αποτυγχάνει θορυβωδώς· επιστρέφει έναν εύλογο αριθμό για λάθος τυπογραφικό στοιχείο. Αν οι εξαγωγές από τον server πρέπει να ταιριάζουν με μια αναφορά από desktop, εγκαταστήστε στον host εξαγωγής τις γραμματοσειρές που ονομάζουν τα πρότυπά σας, ή καρφώστε τη γραμματοσειρά Normal πριν καλέσετε τη διαδρομή εξαγωγής φύλλου εργασίας σε PDF
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Καρφώστε τη γραμματοσειρά Normal ώστε το MDW που μετριέται σε αυτόν τον host
// να είναι εκείνο για το οποίο σχεδιάστηκε η διάταξη, όχι υποκατάστατο του font mapper.
if Book.Fonts.Count > 0 then
begin
Book.Fonts[0].Name := 'Calibri';
Book.Fonts[0].Size := 11;
end;
Exporter.UseWorksheetPageSetup := True;
Exporter.SaveAsPDF(Book, 'quarterly-report.pdf');
finally
Exporter.Free;
Book.Free;
end;
end;
Caches μέτρησης, και εκείνη που κατέρρευσε σε Win64
Από τη στιγμή που η μέτρηση κειμένου είναι μια διαδρομή προς το GDI και όχι ένας πολλαπλασιασμός, πρέπει να αποθηκεύεται σε cache, και η αποθήκευση μέσα σε ένα render pass είναι εκεί όπου αυτή η δουλειά μάτωσε. Ο βρόχος shrink-to-fit κατεβάζει το μέγεθος γραμματοσειράς σε βήματα των 0.5 pt και ξαναμετρά μετά από κάθε βήμα, οπότε ένα μόνο κελί μπορεί να καλέσει το PaginationMeasureTextWidth δώδεκα φορές με την ίδια συμβολοσειρά, και η αναδίπλωση λέξεων το καλεί ξανά ανά υποψήφια γραμμή. Ένα memo με κλειδί το όνομα γραμματοσειράς, το μέγεθος και το κείμενο συμπτύσσει όλα αυτά σε μία κλήση GDI ανά διακριτή συμβολοσειρά, αποθηκευμένη σε μια TStringList ως ζεύγη ονόματος/τιμής
Η άλλη cache που προστέθηκε δίπλα της δεν ήταν τόσο τακτική. Το render pass 5 επιλύει τη δεξαμενή γραμματοσειρών ανά κελί μέσω του FontIndex, και το memo του χρησιμοποιούσε παράλληλους δυναμικούς πίνακες με ένα FontMemoCount που συντηρούνταν στο χέρι. Η πρώτη έκδοση ξέχασε να καλέσει το ResetFontMemo στην αρχή κάθε σελίδας, οπότε ο μετρητής ανέβαινε συνεχώς από σελίδα σε σελίδα ενώ οι πίνακες όχι, και ο κώδικας έγραφε πέρα από το τέλος όλων τους. Στα Win32 αυτό μουτζούρωνε αθόρυβα τον διπλανό σωρό και τελείωνε· στα Win64 εγείρει αμέσως access violation σε μια εγγραφή στη διεύθυνση 0x538. Το γενικεύσιμο μάθημα: μια cache που στηρίζεται σε πίνακες και κρατιέται σε μεταβλητή επιπέδου μονάδας πρέπει να μηδενίζεται στην είσοδο κάθε περάσματος που τη χρησιμοποιεί, επειδή μια string list ή ένα dictionary συγχωρεί έναν χαμένο μηδενισμό μεγαλώνοντας, ενώ οι παράλληλοι πίνακες όχι
Πώς να ελέγξετε τη δική σας μετατροπή
Δεν χρειάζεστε το component για να επαληθεύσετε τίποτα από αυτά. Πάρτε ένα βιβλίο εργασίας του οποίου η γραμματοσειρά Normal δεν είναι Calibri 11, διαβάστε ένα πλάτος από το <col width="..."/>, και περάστε το από τον τύπο της προδιαγραφής δύο φορές, μία με MDW 7 και μία με το MDW που πραγματικά μετρά ο renderer σας για εκείνη τη γραμματοσειρά· αν οι απαντήσεις διαφέρουν και η έξοδός σας ταιριάζει με την πρώτη, βρήκατε την απόκλιση. Η γεωμετρία στηλών είναι ένα από εκείνα τα κομμάτια μιας μηχανής υπολογιστικών φύλλων που ή είναι αόρατο ή είναι το μόνο που προσέχει κανείς, και το να το κάνετε σωστά σημαίνει να αντιμετωπίζετε τη γραμματοσειρά Normal ως είσοδο της διάταξης και όχι ως λεπτομέρεια στυλ. Αν φτιάχνετε εφαρμογές Delphi ή C++Builder που διαβάζουν, γράφουν, αποδίδουν και εκτυπώνουν βιβλία εργασίας Excel χωρίς εγκατεστημένο Office, το HotXLS Delphi Excel component αναλαμβάνει τη μέτρηση MDW, το μοντέλο σελιδοποίησης και τη ροή PDF πίσω από ένα ενιαίο σύνολο κλάσεων VCL