Tekninen artikkeli

Excel-sarakkeen leveys ja Max Digit Width (MDW) Delphissä

Viety PDF asettaa jokaisen sarakerajan puoli merkkiä vasemmalle siitä, missä Excel sen piirtää, ja jokainen rivitetty solu katkeaa nyt eri kohtaa. Excel-sarakkeen leveyttä ei mitata merkeissä eikä pisteissä. Se mitataan työkirjan Normal-fontin Max Digit Width (MDW) -yksiköinä, ja HotXLS mittaa kyseisen fontin GDI:llä ennen jokaista sivutusrakennetta. Vikatila on hiljainen: mikään ei heitä, tallennetut leveydet round-tripataan tavu tavulta, ja geometria on silti muutaman prosentin väärin saraketta kohden, kunnes kertynyt ajautuminen työntää yhden sivun taulukon kahdelle

Mikä yksikkö Excel-sarakkeen leveys on?

Arkin sarakeleveys on laskuri työkirjan Normal-fontin numeromerkeistä, ei absoluuttinen mittaus. ECMA-376 §18.3.1.13 määrittelee <col>-elementin width-attribuutin kyseisen fontin Maximum Digit Widthin kautta 96 dpi:ssä, ja antaa muunnoksen tallennetusta leveydestä takaisin pikseleiksi katkaisevana lausekkeena MDW:n yli. Calibri 11:lle, jonka Excel toimittaa Normal-tyylinä, MDW mittaa 7 pikseliä. Syötä oletusleveys 8.43 yksikköä määrittelykaavan läpi MDW 7:llä ja saat täsmälleen 64 pikseliä, joka on 48 pistettä 96 dpi:ssä. Nuo ovat luvut, joita Excel itse raportoi, joten ne tekevät hyödyllisen tarkistuksen: jos muunnosesi toistaa 8.43 yksikköä 64 pikseliksi, aritmetiikka on oikein ja vain MDW-syöte voi vielä olla väärin

const
  // Oletusrunkofontin maksiminumeroleveys (MDW) pikseleinä 96 dpi:ssä.
  // Calibri 11 mittaa 7 px, mikä toistaa täsmälliset pikselileveydet,
  // jotka Excel tallentaa (8.43 yksikköä -> 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:n pikselit -> pisteet
  if Result < MinimumColumnWidth then
    Result := MinimumColumnWidth;
end;

HotXLS pitää kyseisen aritmetiikan täsmälleen yhdessä funktiossa, lxPagination-yksikössä, joten on yksi paikka, jossa mittanauha voi olla väärin. + 5 on täyte, jonka Excel lisää ruudukon viivoille ja solujen marginaaleille, * 0.75 muuntaa 96 dpi:n pikselit PostScript-pisteiksi, ja lattia MinimumColumnWidthissa on olemassa, jotta patologisesti kapea sarake jättää silti kaistan, johon rendereri voi piirtää reunan. Julkinen sisääntulopiste ColumnWidthToPoints pitää vanhan yksiargumenttisen allekirjoituksensa ja välittää mitatun MDW:n tälle funktiolle, mikä antoi käyttäytymismuutoksen laskeutua koskettamatta yhtään kutsupaikkaa

HotXLS:n sarakeleveysmuunnosketju Delphissä, syöttäen työkirjan Normal-fontin mitatun Max Digit Widthin määrittelykaavaan, jolloin tallennettu leveys 8.43 yksikköä muuttuu 64 pikseliksi ja sitten 48 pisteeksi
Tallennettu leveys on numerolaskuri, joten Normal-fontin mitattu MDW on kaavan syöte tyylittelyn yksityiskohdan sijaan, ja 8.43:n 64:ksi ja 48:ksi round trip tarkistaa aritmetiikan

Miksi muu kuin Calibri Normal -fonti siirtää jokaisen rajan

Ajautuminen on multiplikatiivista, minkä vuoksi se luetaan renderöintivirheeksi eikä yksikkövirheeksi. MDW on kerroin leveydellä, ei siirtymä. Työnnä MDW arvosta 7 arvoon 8, ja oletusarvoinen 8.43-yksikön sarake kasvaa 64 pikselistä 72:een, hyppy 8 pikseliä eli 6 pistettä yhdellä sarakkeella. Kymmenellä tällaisella sarakkeella taulukon oikea reuna on liikkunut lähes tuuman verran. Tähän kompastuvat työkirjat ovat täysin tavallisia: mikä tahansa raportointityökalun tuottama, joka leimaa Arial- tai Segoe UI -fontin Normal-tyyliin, mikä tahansa ERP-vientimallista tallennettu, mikä tahansa, jonka asiakas kerran uudelleentyylitteli ja unohti

Kaksi toisiinsa liittyvää asettelujärjestelmää perii virheen sen sijaan, että aiheuttaisivat sen. Yhdistetyt alueet summaavat jäsensarakkeidensa pisteleveydet, joten yhdistetty alue, joka mahtui yhdelle sivulle Excelissä, voi vuotaa yli MDW-ajautumisen jälkeen, mikä kannattaa muistaa, kun rakennat yhdistettyjen solujen raporttipohjia. Shrink-to-fit vertaa mitattua tekstin leveyttä samaan sarakeleveyteen, joten väärä MDW muuttaa myös sen, mitkä solut kutistuvat ja kuinka paljon. Sama yksikkösekavennusten perhe ilmestyy esiin piirrosankkureissa, joissa kuvageometrialla ja EMU-skaalauksella on oma muunnosketjunsa, jonka voi saada väärin

Kaksi HotXLS:n sarakeviivainta rinnakkain, toinen mitattu MDW 7 pikselillä ja toinen 8:lla, näyttäen kuinka sarakkeittainen hyppy 64:stä 72 pikseliin kertyy kymmenen sarakkeen yli, kun yhdistetyt alueet ja shrink-to-fit perivät virheen
Koska MDW kertoo sen sijaan, että siirtäisi, yksi väärä mittaus siirtää jokaisen sarakerajan, ja yhdistetyt alueet ja shrink-to-fit perivät ajautumisen ilman että mikään heittää

Kuinka HotXLS mittaa MDW:n ajonaikaisesti

HotXLS ratkaisee MDW:n itse työkirjasta vakion olettamisen sijaan, ja kaksi proseduuria tekee työn. PaginationApplyNormalFont lukee Normal-tyylin fontin työkirjasta ja ajaa sivutusrakennuksen alussa, ennen kuin mitään sarakegeometriaa lasketaan; se nollautuu ensin Calibri 11:een, joten fonttitauluton työkirja ei voi periä vanhentunutta tilaa edellisestä rakennuksesta. Normal-tyylin fontti on fonts[0] kohteessa styles.xml, jonka komponentti näyttää muodossa Workbook.Fonts[0]

// Lukee fonts[0]:n eli Normal-tyylin fontin työarkin työkirjasta.
// Klassiset fonttitauluttomat työarkit säilyttävät Calibri 11 -oletuksen.
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;

Toinen proseduuri, PaginationMeasureMdW, kysyy GDI:ltä yksittäisen merkin '0' laajuuden funktiolla GetTextExtentPoint32W jaetulla näytön ulkopuolisella bittikarttakankaalla, palautuu kohteen GetTextMetricsW tmAveCharWidth-arvoon, kun laajuuskutsu epäonnistuu, ja palautuu DefaultMDW:hen, kun kumpikaan ei ole käytettävissä. Sen välimuisti on yksi paikka, jonka avain on (name, size), mikä kuulostaa karkealta, kunnes katsot käyttömallia: sivutusrakennus kysyy samaa Normal-fonttia jokaisen sivun jokaiselle sarakkeelle, joten yhdellä paikalla on lähes täydellinen osumaprosentti ja hinta on kolme vertailua per kutsu

Mitä tapahtuu, kun fonttitaulua, GUI:a tai fonttia ei ole?

HotXLS degradeaa Calibri 11 -vakioon jokaisessa tapauksessa, jossa todellista Normal-fonttia ei voi määrittää, ja se tekee niin hiljaisesti suunnitellusti. Klassiset BIFF-työarkit ovat yleisin tapaus: perintämuodoissa ei ole XLSX-fonttipoolia, johon fonts[0] voisi viitata, joten tyyppitarkistus poistuu aikaisin ja oletusarvoinen MDW 7 pysyy voimassa. Se ei ole korjaus, vaan aiempi käyttäytyminen, joka on tarkoituksella säilytetty, jotta mittauksen lisääminen XLSX-polkuun ei voisi heikentää klassisen muodon tuotosta

GDI-riippuvuus on rehellinen varaus. Mittaus ajaa Windows-laitekontekstia vasten, joten polku olettaa Windows-isäntän, jossa fontti on asennettuna. Palvelussa tai headless-rakennusagentissa GDI-tekstimetriikat ratkeavat yleensä yhä, mutta koneelta puuttuva fontti korvataan fonttimaaparilla ja mittaat korviketta. Se ei koskaan epäonnistu äänekkäästi; se palauttaa uskottavan luvun väärälle kirjasintyypille. Jos palvelinpuolen vientien on vastattava työpöytäreferenssiä, asenna vienti-isäntään fontit, joita mallisi nimeävät, tai kiinnitä Normal-fontti ennen työarkin PDF-vientipolun kutsumista

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

    // Kiinnitä Normal-fontti, jotta tällä isännällä mitattu MDW on se,
    // jota vasten asettelu suunniteltiin, ei fonttimaaparikorviketta.
    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;

Mittausvälimuistit ja se, joka kaatui Win64:llä

Kun tekstin mittaus on GDI round trip kertolaskun sijaan, se pitää välimuistittaa, ja välimuistitus renderöintikierroksen sisällä on kohta, jossa tämä työ vuoti verta. Shrink-to-fit-silmukka laskee fonttikokoa 0.5 pisteen askelin ja mittaa uudelleen jokaisen askeleen jälkeen, joten yksi solu voi kutsua PaginationMeasureTextWidth:ia tusina kertaa samalla merkkijonolla, ja sanojen rivitys kutsuu sitä uudelleen jokaista kandidaattiriviä kohden. Muistio, jonka avain on fontin nimi, koko ja teksti, kutistaa tämän yhteen GDI-kutsuun per erillinen merkkijono, tallennettuna TStringList:iin nimi/arvo-pareina

Toinen sen rinnalle lisätty välimuisti ei ollut yhtä siisti. Renderöintikierros 5 ratkaisee fonttipoolin per solu FontIndex:llä, ja sen muistio käytti rinnakkaisia dynaamisia taulukoita käsin ylläpidetyllä FontMemoCount:illa. Ensimmäinen versio unohti kutsua ResetFontMemo:a jokaisen sivun alussa, joten laskuri jatkoi kiipeämistä sivujen yli, vaikka taulukot eivät, ja koodi kirjoitti kaikkien niiden loppujen yli. Win32:lla se raaputti hiljaa viereiseen kekoon ja loppui; Win64:llä se nosti access violationin heti kirjoituksessa kohteeseen 0x538. Yleistettävä oppitunti: yksikkötason muuttujassa pidetty taulukkopohjainen välimuisti pitää nollata jokaisen sen käyttävän kierroksen alussa, koska merkkijonolista tai sanakirja antaa puuttuvan nollauksen anteeksi kasvamalla, ja rinnakkaiset taulukot eivät

Kuinka HotXLS ratkaisee työkirjan Normal-fontin, mittaa sen Max Digit Widthin GDIn kautta kahdella varatoiminnolla ja välimuistittaa tuloksen, vieressä kaksi renderöintikierroksen muistiota ja se nollaussääntö, jonka rinnakkaindeksitaulukkovälimuisti tarvitsee
MDW ratkaistaan työkirjasta ja mitataan GDIn kautta kerran per fontti, sitten välimuistitetaan avaimen mukaan, kun taas renderöintikierrosten muistiot näyttävät, miksi rinnakkaindeksitaulukkovälimuisti pitää nollata jokaisen kierroksen alussa

Oman muunnosesi tarkistaminen

Et tarvitse komponenttia varmentaaksesi mitään tästä. Ota työkirja, jonka Normal-fontti ei ole Calibri 11, lue leveys kohteesta <col width="..."/> ja aja se määrittelykaavan läpi kahdesti, kerran MDW 7:llä ja kerran sillä MDW:llä, jonka renderöijäsi oikeasti mittaa kyseiselle fontille; jos vastaukset eroavat ja tulostesi täsmää ensimmäiseen, olet löytänyt ajautumisen. Sarakegeometria on yksi niistä taulukkolaskentamoottorin osista, joka on joko näkymätön tai ainoa, jonka joku huomaa, ja sen saaminen oikein tarkoittaa Normal-fontin käsittelyä asettelun syötteenä tyylittelyn yksityiskohdan sijaan. Jos rakennat Delphi- tai C++Builder-sovelluksia, jotka lukevat, kirjoittavat, renderöivät ja tulostavat Excel-työkirjoja ilman asennettua Officea, HotXLS Delphi Excel -komponentti hoitaa MDW-mittauksen, sivutusmallin ja PDF-putken yhden VCL-luokkajoukon takana