Odborný článok

Šírka stĺpca Excelu a Max Digit Width (MDW) v Delphi

Exportované PDF kladie každú hranicu stĺpca o pol znaku ľavejšie, než ju kreslí Excel, a každá zalomená bunka sa teraz láme na inom mieste. Šírka stĺpca Excelu sa nemeria v znakoch ani bodoch. Meria sa v jednotkách Max Digit Width (MDW) Normal fontu zošita a HotXLS meria ten font cez GDI pred každým zostavením stránkovania. Režim zlyhania je tichý: nič nespadne, uložené šírky prežijú round-trip bajt po bajte a geometria je stále vedľa o pár percent na stĺpec, kým akumulovaný drift nepotlačí jednostranovú tabuľku na dve

V akej jednotke je šírka stĺpca Excelu?

Šírka stĺpca v pracovnom liste je počet znakov číslice Normal fontu zošita, nie absolútne meranie. ECMA-376 §18.3.1.13 definuje atribút width elementu <col> v termínoch Maximum Digit Width tohto fontu pri 96 dpi a dáva konverziu z uloženej šírky späť na pixely ako skracujúci výraz nad MDW. Pre Calibri 11, ktorý Excel dodáva ako Normal štýl, MDW meria 7 pixelov. Prehnite predvolenú šírku 8.43 jednotiek cez špecifikačný vzorec s MDW 7 a dostanete presne 64 pixelov, čo je 48 bodov pri 96 dpi. To sú čísla, ktoré Excel sám hlási, takže tvoria užitočnú kontrolu: ak vaša konverzia reprodukuje 8.43 jednotiek na 64 pixelov, aritmetika je správna a len vstup MDW môže byť ešte zlý

const
  // Maximálna šírka číslice (MDW) predvoleného textového fontu v pixeloch pri 96 dpi.
  // Calibri 11 meria 7 px, čo reprodukuje presné pixelové šírky,
  // ktoré Excel ukladá (8.43 jednotiek -> 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; // pixely 96 dpi -> body
  if Result < MinimumColumnWidth then
    Result := MinimumColumnWidth;
end;

HotXLS drží tú aritmetiku v presne jednej funkcii, v jednotke lxPagination, takže existuje jediné miesto, kde môže byť pravítko zlé. + 5 je padding, ktorý Excel pridáva pre mriežku a okraje buniek, * 0.75 prevádza pixely 96 dpi na PostScript body a podlaha pri MinimumColumnWidth existuje preto, aby patologicky úzky stĺpec stále nechal prúžok, do ktorého renderer dokáže nakresliť orámovanie. Verejný vstupný bod ColumnWidthToPoints drží svoj starý jednoargumentový podpis a preposiela merané MDW tejto funkcii, čo umožnilo správanie zmeniť bez dotknutia jediného miesta volania

Konverzný reťazec šírky stĺpca HotXLS v Delphi, kŕmiaci merané Max Digit Width Normal fontu zošita do špecifikačného vzorca, takže uložená šírka 8.43 jednotiek sa stane 64 pixelmi a potom 48 bodmi
Uložená šírka je počet číslic, takže merané MDW Normal fontu je vstupom do vzorca, nie štylistický detail, a round-trip 8.43 na 64 na 48 kontroluje aritmetiku

Prečo ne-Calibri Normal font posúva každú hranicu

Drift je multiplikatívny, čo je dôvod, prečo sa číta ako renderovacia chyba, nie chyba jednotiek. MDW je faktor na šírke, nie ofset. Potlačte MDW z 7 na 8 a predvolený stĺpec 8.43 jednotiek pôjde z 64 pixelov na 72, skok o 8 pixelov alebo 6 bodov na jednom stĺpci. Desať stĺpcov toho a pravý okraj tabuľky sa pohnul o takmer palec. Zošity, ktoré to spustia, sú úplne obyčajné: čokoľvek vygenerované reportovacím nástrojom, ktorý pečiatkuje Arial alebo Segoe UI do Normal štýlu, čokoľvek uložené z ERP exportnej šablóny, čokoľvek, čo zákazník raz prestylizoval a zabudol

Dva súvisiace systémy rozloženia zdede chybu, namiesto aby ju spôsobovali. Zlúčené regióny sčítavajú bodové šírky svojich členských stĺpcov, takže merge, ktorý sa zmestil na jednu stranu v Exceli, môže po MDW drifte pretekať, čo stojí za zapamätanie, keď staviate šablóny reportov so zlúčenými bunkami. Shrink-to-fit porovnáva meranú šírku textu s tou istou šírkou stĺpca, takže zlé MDW tiež mení, ktoré bunky sa zmenšia a o koľko. Rovnaká rodina zmatenia jednotiek sa objavuje v kresbových ukotveniach, kde geometria obrázkov a škálovanie EMU má vlastný konverzný reťazec, ktorý sa dá pokaziť

Dve pravítka stĺpcov HotXLS porovnané, jedno merané s MDW 7 pixelov a druhé s 8, ukazujúce, ako sa skok na stĺpec z 64 na 72 pixelov akumuluje naprieč desiatimi stĺpcami, zatiaľ čo zlúčené regióny a shrink-to-fit zdede chybu
Keďže MDW násobí namiesto posúvania, jedno zlé meranie pohnie každú hranicu stĺpca a zlúčené regióny a shrink-to-fit zdede drift bez toho, aby čokoľvek spadlo

Ako HotXLS meria MDW za behu

HotXLS rozlíši MDW zo samotného zošita namiesto predpokladu konštanty a dve procedúry robia prácu. PaginationApplyNormalFont prečíta Normal štýl fontu zo zošita a beží na vrchole zostavenia stránkovania, pred vypočítaním akejkoľvek geometrie stĺpcov; najprv sa resetuje na Calibri 11, takže zošit bez fontovej tabuľky nemôže zdediť zastaraný stav z predchádzajúceho zostavenia. Font Normal štýlu je fonts[0] v styles.xml, vystavený komponentom ako Workbook.Fonts[0]

// Číta fonts[0] (font Normal štýlu) z pracovného listu zošita.
// Klasické listy bez fontovej tabuľky držia predvolené 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;

Druhá procedúra, PaginationMeasureMdW, pýta GDI rozsah jediného znaku '0' cez GetTextExtentPoint32W na zdieľanom off-screen bitmapovom canvas, pada späť na tmAveCharWidth z GetTextMetricsW, keď volanie rozsahu zlyhá, a pada späť na DefaultMDW, keď ani jedno nie je dostupné. Jeho cache je jediný slot kľúčovaný (name, size), čo znie hrubo, kým sa nepozriete na prístupový vzor: zostavenie stránkovania pýta ten istý Normal font na každom stĺpci každej strany, takže jeden slot má takmer dokonalú mieru zásahov a stojí tri porovnania na volanie

Čo sa stane bez fontovej tabuľky, bez GUI alebo s chýbajúcim fontom?

HotXLS degraduje na konštantu Calibri 11 v každom prípade, keď sa skutočný Normal font nedá určiť, a robí to mlčky zámerne. Klasické pracovné listy BIFF sú ten bežný: legacy formáty neniosu XLSX fontový fond, na ktorý by fonts[0] odkazovalo, takže typová stráž skončí skoro a predvolené MDW 7 platí. To nie je oprava, je to predchádzajúce správanie zachované zámerne, takže pridanie merania na XLSX cestu nemohlo regresovať klasicko-formátový výstup

Závislosť GDI je úprimná výhrada. Meranie beží proti Windows device contextu, takže cesta predpokladá Windows hostiteľa s nainštalovaným fontom. V službe alebo headless build agentovi GDI textové metriky sa spravidla stále rozlíšia, ale font, ktorý nie je nainštalovaný na tom stroji, sa substituuje font mapperom a meriate substitút. Nikdy nespadne nahlas; vráti presvedčivé číslo pre zlý rez písma. Ak serverové exporty musia zodpovedať desktop referencii, nainštalujte fonty, ktoré vaše šablóny menujú, na exportný hostiteľ, alebo pripnite Normal font pred vyvolaním cesty PDF exportu pracovného listu

var
  Book: TXLSXWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSXWorkbook.Create;
  Exporter := TXLSPDFExport.Create;
  try
    Book.Open('quarterly-report.xlsx');

    // Pripnite Normal font, aby MDW merané na tomto hostiteľovi bolo to,
    // proti ktorému bolo rozloženie navrhnuté, nie substitút font mappera.
    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 meraní a tá jedna, ktorá spadla na Win64

Keď je textové meranie GDI okruh namiesto násobenia, musí sa cachovať a cachovanie vo vnútri renderovacej passy je miesto, kde táto práca pustila krv. Shrink-to-fit slučka krokuje veľkosť fontu nadol v prírastkoch 0.5 pt a znova meria po každom kroku, takže jedna bunka môže zavolať PaginationMeasureTextWidth tuctokrát s tým istým reťazcom a word wrap ho volá znova na každý kandidátsky riadok. Memo kľúčované názvom fontu, veľkosťou a textom zrúti to na jedno GDI volanie na odlišný reťazec, uložené v TStringList ako páry názov/hodnota

Druhá cache pridaná popri nej nebola taká uprataná. Render pass 5 rozlíši fontový fond na bunku cez FontIndex a jeho memo používalo paralelné dynamické polia s ručne udržiavaným FontMemoCount. Prvá verzia zabudla zavolať ResetFontMemo na začiatku každej strany, takže počet stále stúpal naprieč stranami, zatiaľ čo polia nie, a kód zapisoval za koniec všetkých. Na Win32 to mlčky škriabalo do susednej heapu a skončilo; na Win64 to okamžite vyvolalo access violation pri zápise na 0x538. Zovšeobecniteľná lekcia: cache podložená poľom držaná v variable na úrovni jednotky musí byť resetovaná na vstupe každej passy, ktorá ju používa, pretože string list alebo slovník odpúšťa chýbajúci reset rastom a paralelné polia nie

Ako HotXLS rozlíši Normal font zošita, meria jeho Max Digit Width cez GDI s dvoma zálohami a cachuje výsledok, vedľa dvoch render-pass memos a resetovacieho pravidla, ktoré paralelno-polová cache potrebuje
MDW sa rozlíši zo zošita a meria sa cez GDI raz na font, potom sa cachuje podľa kľúča, zatiaľ čo render-pass memos ukazujú, prečo paralelno-polová cache musí byť resetovaná na vstupe každej passy

Kontrola vlastnej konverzie

Nepotrebujete komponent na overenie čohokoľvek z toho. Vezmite zošit, ktorého Normal font nie je Calibri 11, prečítajte šírku z <col width="..."/> a prehnite ju špecifikačným vzorcom dvakrát, raz s MDW 7 a raz s MDW, ktoré váš renderer skutočne meria pre ten font; ak sa odpovede líšia a váš výstup zhoduje s prvou, našli ste drift. Geometria stĺpcov je jedna z tých častí spreadsheet enginu, ktorá je buď neviditeľná, alebo jediná vec, ktorú ktokoľvek zaznamená, a dostať ju správne znamená považovať Normal font za vstup rozloženia, nie štylistický detail. Ak staviate Delphi alebo C++Builder aplikácie, ktoré čítajú, zapisujú, renderujú a tlačia Excel zošity bez nainštalovaného Office, HotXLS Delphi Excel component zvláda MDW meranie, stránkovací model a PDF pipeline za jednou sadou VCL tried