Wyeksportowany PDF kładzie każdą granicę kolumny pół znaku na lewo od tego, gdzie rysuje ją Excel, a każda zawijana komórka łamie się teraz w innym miejscu. Szerokość kolumny Excela nie jest mierzona w znakach ani punktach. Jest mierzona w jednostkach Max Digit Width (MDW) czcionki Normal skoroszytu, a HotXLS mierzy tę czcionkę przez GDI przed każdym budowaniem paginacji. Tryb awarii jest cichy: nic nie rzuca, zapisane szerokości robią round-trip bajt za bajt, a geometria jest i tak skrzywiona o kilka procent na kolumnę, aż skumulowany dryf przepchnie tabelę z jednej strony na dwie
W jakiej jednostce jest szerokość kolumny Excela?
Szerokość kolumny w arkuszu to liczba znaków cyfry czcionki Normal skoroszytu, nie pomiar absolutny. ECMA-376 §18.3.1.13 definiuje atrybut width elementu <col> przez Maximum Digit Width tej czcionki przy 96 dpi i daje konwersję ze zapisanej szerokości z powrotem na piksele jako wyrażenie obcinające po MDW. Dla Calibri 11, którą Excel wysyła jako styl Normal, MDW mierzy 7 pikseli. Przepuść domyślną szerokość 8.43 jednostek przez formułę specyfikacji z MDW 7 i dostaniesz dokładnie 64 piksele, czyli 48 punktów przy 96 dpi. To liczby, które sam Excel raportuje, więc robią użyteczną kontrolę: jeśli twoja konwersja odtwarza 8.43 jednostki na 64 piksele, arytmetyka jest dobra i tylko wejście MDW może jeszcze być złe
const
// Maximum digit width (MDW) domyślnej czcionki tekstu w pikselach przy 96 dpi.
// Calibri 11 mierzy 7 px, co odtwarza dokładne szerokości pikselowe
// zapisywane przez Excela (8.43 jednostek -> 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; // piksele 96 dpi -> punkty
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS trzyma tę arytmetykę w dokładnie jednej funkcji, w jednostce lxPagination, więc jest jedno miejsce, w którym linijka może się mylić. + 5 to wypełnienie, które Excel dodaje na linie siatki i marginesy komórek, * 0.75 konwertuje piksele 96 dpi na punkty PostScript, a podłoga na MinimumColumnWidth istnieje po to, by patologicznie wąska kolumna nadal zostawiała pasek, w który renderer może narysować obramowanie. Publiczny punkt wejścia ColumnWidthToPoints zachowuje starą sygnaturę jednoargumentową i przekazuje zmierzone MDW do tej funkcji, co pozwoliło zmianie zachowania wylądować bez dotykania choćby jednego miejsca wywołania
Dlaczego Normal font inny niż Calibri przesuwa każdą granicę
Dryf jest multiplikatywny, dlatego czyta się go jako bug renderowania, a nie bug jednostek. MDW to czynnik na szerokości, nie offset. Pchnij MDW z 7 na 8, a domyślna kolumna 8.43 jednostek idzie z 64 pikseli na 72, skok o 8 pikseli albo 6 punktów na jednej kolumnie. Dziesięć takich kolumn i prawa krawędź tabeli przesunęła się o większość cala. Skoroszyty, które na tym wykładają się, są całkiem zwyczajne: cokolwiek wygenerowanego przez narzędzie raportowe, które wpiętuje Arial albo Segoe UI w styl Normal, cokolwiek zapisanego z szablonu eksportu ERP, cokolwiek, co klient raz przystylował i zapomniał
Dwa pokrewne systemy układu dziedziczą błąd, zamiast go powodować. Regiony scalone sumują szerokości punktowe swoich kolumn członkowskich, więc scalenie, które mieściło się na jednej stronie w Excelu, może przepełnić się po dryfie MDW, co warto pamiętać, gdy budujesz szablony raportów ze scalonymi komórkami. Shrink-to-fit porównuje zmierzoną szerokość tekstu z tą samą szerokością kolumny, więc złe MDW zmienia też to, które komórki się kurczą i o ile. Ta sama rodzina pomieszania jednostek wychodzi w kotwicach rysunków, gdzie geometria obrazów i skalowanie EMU ma własny łańcuch konwersji, w którym łatwo się pomylić
Jak HotXLS mierzy MDW w czasie działania
HotXLS rozwiązuje MDW z samego skoroszytu, zamiast zakładać stałą, i dwie procedury robią robotę. PaginationApplyNormalFont czyta czcionkę stylu Normal ze skoroszytu i działa na szczycie budowania paginacji, zanim jakakolwiek geometria kolumn jest liczona; najpierw resetuje do Calibri 11, więc skoroszyt bez tabeli czcionek nie może odziedziczyć nieaktualnego stanu po poprzednim budowaniu. Czcionka stylu Normal to fonts[0] w styles.xml, eksponowana przez komponent jako Workbook.Fonts[0]
// Czyta fonts[0] (czcionkę stylu Normal) ze skoroszytu arkusza.
// Klasyczne arkusze bez tabeli czcionek trzymają domyślne 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;
Druga procedura, PaginationMeasureMdW, pyta GDI o rozmiar pojedynczego znaku '0' przez GetTextExtentPoint32W na współdzielonym bitmapowym kanwie poza ekranem, cofa się do tmAveCharWidth z GetTextMetricsW, gdy wywołanie rozmiaru zawiedzie, i cofa się do DefaultMDW, gdy ani jedno nie jest dostępne. Jej bufor to pojedyncze gniazdo kluczowane przez (nazwa, rozmiar), co brzmi topornie, dopóki nie spojrzysz na wzorzec dostępu: budowanie paginacji pyta o tę samą czcionkę Normal przy każdej kolumnie każdej strony, więc jedno gniazdo ma niemal doskonałą skuteczność trafień i kosztuje trzy porównania na wywołanie
Co się dzieje bez tabeli czcionek, bez GUI albo przy brakującej czcionce?
HotXLS degraduje do stałej Calibri 11 w każdym przypadku, w którym prawdziwa czcionka Normal nie da się ustalić, i robi to po cichu z założenia. Klasyczne arkusze BIFF to częsty przypadek: stare formaty nie niosą puli czcionek XLSX, do której fonts[0] mogłoby się odwołać, więc straż typu wychodzi wcześnie i domyślne MDW 7 obstaje. To nie jest poprawka, to poprzednie zachowanie zachowane celowo, tak by dodanie pomiaru do ścieżki XLSX nie mogło zregresować wyjścia formatu klasycznego
Zależność od GDI to szczere zastrzeżenie. Pomiar działa na kontekście urządzenia Windows, więc ścieżka zakłada hosta Windows z zainstalowaną czcionką. W usłudze albo bezgłowym agencie budowania metryki tekstu GDI na ogół nadal się rozwiązują, ale czcionka niezainstalowana na tej maszynie zostaje podstawiona przez maper czcionek i mierzysz podstawienie. Nigdy nie zawodzi głośno; zwraca wiarygodną liczbę dla złego kroju pisma. Jeśli eksporty po stronie serwera mają pasować do referencji desktopowej, zainstaluj czcionki, które nazywają twoje szablony, na hoście eksportu, albo przypnij czcionkę Normal przed wywołaniem ścieżki eksportu PDF arkusza
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Przypnij czcionkę Normal, by MDW zmierzone na tym hoście było tym,
// pod którym projektowano układ, a nie podstawieniem mapera czcionek.
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;
Bufory pomiaru i ten, który wywalił się na Win64
Skoro pomiar tekstu to round-trip GDI, a nie mnożenie, musi być buforowany, a buforowanie wewnątrz przebiegu renderowania to miejsce, gdzie ta praca ociekła krwią. Pętla shrink-to-fit schodzi z rozmiarem czcionki o 0.5 pt na krok i mierzy ponownie po każdym kroku, więc jedna komórka może wywołać PaginationMeasureTextWidth tuzin razy z tym samym ciągiem, a zawijanie słów wywołuje ją znowu dla każdej kandydackiej linii. Memo kluczowane nazwą czcionki, rozmiarem i tekstem zgniata to do jednego wywołania GDI na odrębny ciąg, zapisywane w TStringList jako pary nazwa/wartość
Drugi bufor dodany obok nie był tak schludny. Przebieg renderowania 5 rozwiązuje pulę czcionek per komórka przez FontIndex, a jego memo używało równoległych tablic dynamicznych z ręcznie utrzymywanym FontMemoCount. Pierwsza wersja zapomniała wywołać ResetFontMemo na początku każdej strony, więc licznik dalej wspinał się między stronami, podczas gdy tablice nie, i kod pisał za końcem wszystkich. Na Win32 po cichu bazgroliło w sąsiednią stertę i kończyło; na Win64 rzuciło naruszenie dostępu przy zapisie do 0x538 natychmiast. Uogólnialna lekcja: bufor podparty tablicą trzymany w zmiennej poziomu jednostki musi być resetowany na wejściu każdego przebiegu, który go używa, bo lista ciągów albo słownik wybacza brak resetu rosnąc, a równoległe tablice nie
Sprawdzanie własnej konwersji
Nie potrzebujesz komponentu, by to wszystko zweryfikować. Weź skoroszyt, którego czcionka Normal nie jest Calibri 11, odczytaj szerokość z <col width="..."/> i przepuść ją przez formułę specyfikacji dwa razy, raz z MDW 7 i raz z MDW, które twój renderer faktycznie mierzy dla tej czcionki; jeśli odpowiedzi się różnią i twoje wyjście pasuje do pierwszej, znalazłeś dryf. Geometria kolumn to jedna z tych części silnika arkusza, które są albo niewidzialne, albo jedyną rzeczą, jaką ktokolwiek zauważa, a dopięcie jej na dobre znaczy traktowanie czcionki Normal jako wejścia układu, a nie detalu stylistycznego. Jeśli budujesz aplikacje Delphi lub C++Buildera, które czytają, zapisują, renderują i drukują skoroszyty Excela bez zainstalowanego Office, komponent Excel HotXLS Delphi obsługuje pomiar MDW, model paginacji i potok PDF za jednym zestawem klas VCL