Експортираният PDF поставя всяка граница на колона половин знак вляво от мястото, където Excel я чертае, а всяка пренесена клетка вече се пречупва на друго място. Ширината на колоните в Excel не се мери в знаци или точки. Мери се в единици Max Digit Width (MDW) на Normal шрифта на работната книга, а HotXLS измерва този шрифт с GDI преди всяко изграждане на страничното разделяне. Режимът на отказ е тих: нищо не хвърля изключение, съхранените ширини се връщат байт по байт, а геометрията пак е сгрешена с няколко процента на колона, докато натрупаното отклонение не изтласка една таблица от една страница на две
В каква единица е ширината на колона в Excel?
Ширината на колона в работен лист е брой цифрови знаци от Normal шрифта на работната книга, а не абсолютна мярка. ECMA-376 §18.3.1.13 дефинира атрибута width на <col> чрез Maximum Digit Width на този шрифт при 96 dpi и дава преобразуването от съхранена ширина обратно в пиксели като отрязващ израз върху MDW. За Calibri 11, който Excel доставя като Normal стил, MDW мери 7 пиксела. Прекарайте ширината по подразбиране от 8.43 единици през формулата от спецификацията с MDW 7 и получавате точно 64 пиксела, което е 48 точки при 96 dpi. Това са числата, които самият Excel докладва, така че те са полезна проверка: ако вашето преобразуване възпроизвежда 8.43 единици в 64 пиксела, аритметиката е вярна и само входът MDW може още да е грешен
const
// Максимална ширина на цифра (MDW) на стандартния шрифт в пиксели при 96 dpi.
// Calibri 11 мери 7 px, което възпроизвежда точните пикселни ширини,
// които Excel съхранява (8.43 единици -> 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 пиксели -> точки
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS държи тази аритметика в точно една функция, в модула lxPagination, така че има само едно място, където линийката може да е грешна. + 5 е отстъпът, който Excel добавя за мрежовите линии и полетата на клетката, * 0.75 преобразува пиксели при 96 dpi в PostScript точки, а долната граница MinimumColumnWidth съществува, за да остави дори патологично тясна колона с ивица, в която рендерът да може да нарисува рамка. Публичната входна точка ColumnWidthToPoints запазва стария си подпис с един аргумент и подава измерен MDW на тази функция, което позволи промяната в поведението да влезе, без да се пипне нито едно място на извикване
Защо различен от Calibri Normal шрифт мести всяка граница
Отклонението е мултипликативно и точно затова изглежда като бъг в рендирането, а не като бъг в мерните единици. MDW е множител върху ширината, не отместване. Вдигнете MDW от 7 на 8 и колоната по подразбиране от 8.43 единици отива от 64 пиксела на 72, скок от 8 пиксела или 6 точки на една колона. Десет такива колони и десният ръб на таблицата се е преместил почти с инч. Работните книги, които попадат на това, са напълно обикновени: всичко, генерирано от инструмент за отчети, който набива Arial или Segoe UI в Normal стила, всичко, записано от шаблон за експорт на ERP, всичко, което клиент е престилизирал веднъж и е забравил
Две свързани системи за оформление наследяват грешката, вместо да я причиняват. Обединените области сумират ширините в точки на съставящите ги колони, така че обединение, което се е побирало на една страница в Excel, може да прелее след отклонение в MDW, което си струва да помните, когато изграждате шаблони за отчети с обединени клетки. Shrink-to-fit сравнява измерената ширина на текста със същата ширина на колоната, така че грешният MDW променя и кои клетки се смаляват и с колко. Същото семейство объркване на единици се появява и при котвите на графиките, където геометрията на изображенията и мащабирането в EMU има собствена верига от преобразувания, която може да сгреши
Как HotXLS измерва MDW по време на изпълнение
HotXLS определя MDW от самата работна книга, вместо да приема константа, и работата вършат две процедури. PaginationApplyNormalFont чете шрифта на Normal стила от работната книга и се изпълнява в началото на изграждането на страничното разделяне, преди да е изчислена каквато и да е геометрия на колони; първо връща стойностите към Calibri 11, така че работна книга без таблица с шрифтове не може да наследи остаряло състояние от предишно изграждане. Шрифтът на Normal стила е fonts[0] в styles.xml, изложен от компонента като Workbook.Fonts[0]
// Чете fonts[0] (шрифта на Normal стила) от работната книга на листа.
// Класически листове без таблица с шрифтове пазят подразбирането 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;
Втората процедура, PaginationMeasureMdW, пита GDI за размерите на единичния знак '0' чрез GetTextExtentPoint32W върху споделено извънекранно bitmap платно, връща се към tmAveCharWidth от GetTextMetricsW, когато извикването за размери се провали, и към DefaultMDW, когато нито едното не е налично. Кешът ѝ е един-единствен слот с ключ (name, size), което звучи грубо, докато не погледнете модела на достъп: едно изграждане на странично разделяне иска същия Normal шрифт за всяка колона на всяка страница, така че един слот има почти перфектен процент попадения и струва три сравнения на извикване
Какво става без таблица с шрифтове, без GUI или с липсващ шрифт?
HotXLS се връща към константата Calibri 11 във всеки случай, в който истинският Normal шрифт не може да бъде определен, и го прави тихо по замисъл. Класическите BIFF листове са честият случай: наследените формати не носят XLSX пул от шрифтове, към който fonts[0] да сочи, така че проверката на типа излиза рано и MDW по подразбиране от 7 остава в сила. Това не е поправка, а предишното поведение, запазено умишлено, така че добавянето на измерване към XLSX пътя да не може да влоши изхода за класическите формати
Зависимостта от GDI е честната уговорка. Измерването се изпълнява спрямо контекст на устройство на Windows, така че този път предполага Windows хост с инсталиран шрифт. В услуга или в безглав build агент текстовите метрики на GDI обикновено пак се разрешават, но шрифт, който не е инсталиран на тази машина, се замества от font mapper-а и вие мерите заместителя. Никога не се проваля шумно; връща правдоподобно число за грешната гарнитура. Ако експортите от сървъра трябва да съвпадат с настолен еталон, инсталирайте на хоста за експорт шрифтовете, които вашите шаблони назовават, или фиксирайте Normal шрифта, преди да извикате пътя за PDF експорт на работен лист
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Фиксира Normal шрифта, така че измереният на този хост MDW да е
// този, за който е проектирано оформлението, а не заместител.
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;
Кешове за измерване и този, който забиваше на Win64
Щом измерването на текст е обиколка до GDI, а не умножение, то трябва да се кешира, и кеширането вътре в проход на рендиране е мястото, където тази работа пусна кръв. Циклът shrink-to-fit намалява размера на шрифта на стъпки от 0.5 pt и премерва след всяка стъпка, така че една клетка може да извика PaginationMeasureTextWidth дузина пъти със същия низ, а пренасянето на думи го вика отново за всеки кандидат-ред. Мемоизация с ключ име на шрифт, размер и текст свива това до едно GDI извикване на отделен низ, съхранена в TStringList като двойки име/стойност
Другият кеш, добавен покрай него, не беше толкова спретнат. Проход 5 на рендиране разрешава пула от шрифтове за всяка клетка по FontIndex, а неговата мемоизация използваше паралелни динамични масиви с ръчно поддържан FontMemoCount. Първата версия забрави да вика ResetFontMemo в началото на всяка страница, така че броячът продължаваше да расте през страниците, а масивите не, и кодът пишеше отвъд края на всички тях. На Win32 това тихо драскаше в съседната купчина и приключваше; на Win64 веднага вдигаше access violation при запис на 0x538. Обобщимата поука: кеш, изграден върху масиви и държан в променлива на ниво модул, трябва да се нулира при входа на всеки проход, който го използва, защото списък от низове или речник прощава липсващото нулиране, като расте, а паралелните масиви не
Проверка на собственото ви преобразуване
Не ви трябва компонентът, за да проверите каквото и да е от това. Вземете работна книга, чийто Normal шрифт не е Calibri 11, прочетете ширина от <col width="..."/> и я прекарайте през формулата от спецификацията два пъти, веднъж с MDW 7 и веднъж с MDW, който вашият рендер реално мери за този шрифт; ако отговорите се различават, а вашият изход съвпада с първия, сте намерили отклонението. Геометрията на колоните е една от онези части на електронната таблица, които са или невидими, или единственото нещо, което всички забелязват, и да я направите правилно означава да третирате Normal шрифта като вход за оформлението, а не като детайл от стила. Ако разработвате Delphi или C++Builder приложения, които четат, пишат, рендират и печатат Excel работни книги без инсталиран Office, компонентът HotXLS Delphi Excel поема измерването на MDW, модела на странично разделяне и PDF конвейера зад един набор от VCL класове