PDF hasil ekspor menaruh setiap batas kolom setengah karakter ke kiri dari tempat Excel menggambarnya, dan setiap sel yang membungkus teks kini patah di tempat yang berbeda. Lebar kolom Excel tidak diukur dalam karakter atau poin. Ia diukur dalam unit Max Digit Width (MDW) dari font Normal milik workbook, dan HotXLS mengukur font itu dengan GDI sebelum setiap build paginasi. Mode gagalnya sunyi: tak ada yang melempar, lebar tersimpan round-trip byte-per-byte, dan geometrinya tetap meleset beberapa persen per kolom sampai akumulasi hanyut mendorong tabel satu halaman menjadi dua
Dalam satuan apa lebar kolom Excel diukur?
Lebar kolom di sebuah worksheet adalah hitungan karakter digit dari font Normal workbook, bukan pengukuran absolut. ECMA-376 §18.3.1.13 mendefinisikan atribut width dari <col> dalam terminologi Maximum Digit Width dari font itu pada 96 dpi, dan memberikan konversi dari lebar tersimpan kembali ke piksel sebagai ekspresi pemotongan atas MDW. Untuk Calibri 11, yang dikirim Excel sebagai gaya Normal, MDW terukur 7 piksel. Masukkan lebar bawaan 8.43 unit ke rumus spesifikasi dengan MDW 7 dan Anda mendapat tepat 64 piksel, yaitu 48 poin pada 96 dpi. Itu angka yang Excel sendiri laporkan, jadi mereka jadi pemeriksaan berguna: jika konversi Anda mereproduksi 8.43 unit menjadi 64 piksel, aritmetikanya benar dan hanya input MDW yang masih bisa salah
const
// Maximum digit width (MDW) of the default body font in pixels at 96 dpi.
// Calibri 11 measures 7 px, which reproduces the exact pixel widths
// Excel stores (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; // 96 dpi pixels -> points
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS menahan aritmetika itu dalam tepat satu fungsi, di unit lxPagination, jadi ada satu tempat di mana penggaris bisa salah. + 5 adalah padding yang Excel tambahkan untuk gridline dan margin sel, * 0.75 mengonversi piksel 96 dpi ke poin PostScript, dan lantai di MinimumColumnWidth ada agar kolom yang patologis sempit masih menyisakan strip yang bisa diisi border oleh renderer. Titik masuk publik ColumnWidthToPoints menahan signature satu-argumen lamanya dan meneruskan MDW terukur ke fungsi ini, dan itulah yang membuat perubahan perilaku mendarat tanpa menyentuh satu pun call site
Mengapa font Normal non-Calibri menggeser setiap batas
Hanyutnya itu multiplikatif, dan itulah mengapa ia terbaca sebagai bug rendering alih-alih bug satuan. MDW adalah faktor pada lebar, bukan offset. Dorong MDW dari 7 ke 8 dan kolom bawaan 8.43 unit berpindah dari 64 piksel ke 72, lompatan 8 piksel atau 6 poin pada satu kolom. Sepuluh kolom seperti itu dan tepi kanan tabel sudah berpindah hampir seinci. Workbook yang menjebak ini sepenuhnya biasa saja: apa pun yang dihasilkan alat pelaporan yang mengecap Arial atau Segoe UI ke gaya Normal, apa pun yang tersimpan dari templat ekspor ERP, apa pun yang sekali diubah gayanya pelanggan lalu dilupakan
Dua sistem tata letak terkait mewarisi error itu alih-alih menimbulkannya. Wilayah merge menjumlahkan lebar poin kolom-kolom anggotanya, jadi merge yang muat satu halaman di Excel bisa meluap setelah hanyut MDW, yang layak diingat saat Anda membangun templat laporan sel-merge. Shrink-to-fit membandingkan lebar teks terukur terhadap lebar kolom yang sama, jadi MDW yang salah juga mengubah sel mana yang menyusut dan seberapa banyak. Keluarga kebingungan satuan yang sama muncul di anchor gambar, tempat geometri gambar dan penskalaan EMU punya rantai konversinya sendiri untuk disalahkan
Bagaimana HotXLS mengukur MDW saat run time
HotXLS me-resolve MDW dari workbook itu sendiri alih-alih mengasumsikan konstanta, dan dua prosedur mengerjakannya. PaginationApplyNormalFont membaca font gaya Normal dari workbook dan berjalan di puncak build paginasi, sebelum geometri kolom mana pun dihitung; ia me-reset ke Calibri 11 lebih dulu, sehingga workbook tanpa tabel font tak bisa mewarisi state basi dari build sebelumnya. Font gaya Normal adalah fonts[0] di styles.xml, diekspos komponen sebagai Workbook.Fonts[0]
// Reads fonts[0] (the Normal style font) off the worksheet workbook.
// Classic worksheets without a font table keep the Calibri 11 default.
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;
Prosedur kedua, PaginationMeasureMdW, meminta GDI extent karakter tunggal '0' lewat GetTextExtentPoint32W di bitmap canvas off-screen bersama, jatuh ke tmAveCharWidth dari GetTextMetricsW saat panggilan extent gagal, dan jatuh ke DefaultMDW saat keduanya tak tersedia. Cache-nya satu slot berkunci (name, size), yang terdengar kasar sampai Anda melihat pola aksesnya: satu build paginasi meminta font Normal yang sama pada setiap kolom setiap halaman, jadi satu slot punya hit rate nyaris sempurna dan berbiaya tiga perbandingan per panggilan
Apa yang terjadi tanpa tabel font, tanpa GUI, atau font yang hilang?
HotXLS terdegradasi ke konstanta Calibri 11 di setiap kasus di mana font Normal sungguhan tak dapat ditentukan, dan melakukannya senyap secara desain. Worksheet BIFF klasik adalah kasus yang umum: format warisan tak membawa kolam font XLSX untuk fonts[0] merujuk, jadi type guard keluar lebih awal dan MDW bawaan 7 berdiri. Itu bukan perbaikan, itu perilaku sebelumnya yang dipertahankan sengaja, agar penambahan pengukuran pada jalur XLSX tak bisa meregres output format klasik
Ketergantungan GDI adalah peringatan yang jujur. Pengukuran berjalan terhadap device context Windows, jadi jalurnya mengasumsikan host Windows dengan font terpasang. Di sebuah service atau build agent headless, metrik teks GDI umumnya masih ter-resolve, tetapi font yang tak terpasang di mesin itu digantikan font mapper dan Anda mengukur penggantinya. Ia tak pernah gagal dengan nyaring; ia mengembalikan angka yang masuk akal untuk typeface yang salah. Jika ekspor sisi-server harus cocok dengan referensi desktop, pasang font yang dinamai templat Anda di host ekspor, atau pin font Normal sebelum memanggil jalur ekspor PDF worksheet
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Pin the Normal font so the MDW measured on this host is the one
// the layout was designed against, not a font-mapper substitute.
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;
Cache pengukuran, dan satu yang crash di Win64
Begitu pengukuran teks menjadi round trip GDI alih-alih perkalian, ia harus di-cache, dan caching di dalam pass render adalah tempat kerja ini menarik darah. Loop shrink-to-fit menurunkan ukuran font dalam kenaikan 0.5 pt dan mengukur ulang setelah tiap langkah, jadi satu sel bisa memanggil PaginationMeasureTextWidth sedosen kali dengan string yang sama, dan word wrap memanggilnya lagi per baris kandidat. Memo berkunci nama font, ukuran, dan teks melunturkan itu menjadi satu panggilan GDI per string berbeda, disimpan dalam TStringList sebagai pasangan nama/nilai
Cache lain yang ditambahkan bersamanya tak serapi itu. Render pass 5 me-resolve kolam font per sel lewat FontIndex, dan memo-nya memakai dynamic array paralel dengan FontMemoCount yang dirawat tangan. Versi pertama lupa memanggil ResetFontMemo di awal setiap halaman, jadi hitungan terus menanjak melintasi halaman sementara array tidak, dan kode menulis melewati ujung semuanya. Di Win32 itu diam-diam mencorat-coret heap sebelah lalu selesai; di Win64 ia melempar access violation pada penulisan ke 0x538 seketika. Pelajaran yang bisa digeneralisasi: cache bertumpu-array yang disimpan di variabel level-unit harus di-reset di masuk setiap pass yang memakainya, karena string list atau kamus memaafkan reset yang hilang dengan bertumbuh dan array paralel tidak
Memeriksa konversi Anda sendiri
Anda tak butuh komponen untuk memverifikasi semua ini. Ambil workbook yang font Normalnya bukan Calibri 11, baca lebar dari <col width="..."/>, dan jalankan lewat rumus spesifikasi dua kali, sekali dengan MDW 7 dan sekali dengan MDW yang renderer Anda benar-benar ukur untuk font itu; jika jawabannya berbeda dan output Anda cocok dengan yang pertama, Anda telah menemukan hanyutnya. Geometri kolom adalah salah satu bagian mesin spreadsheet yang tak terlihat atau satu-satunya hal yang semua orang perhatikan, dan mengadopsinya dengan benar berarti memperlakukan font Normal sebagai input tata letak alih-alih detail gaya. Jika Anda membangun aplikasi Delphi atau C++Builder yang membaca, menulis, merender, dan mencetak workbook Excel tanpa Office terpasang, komponen Excel Delphi HotXLS menangani pengukuran MDW, model paginasi, dan pipeline PDF di balik satu set kelas VCL