Извикването, което поставя текст на PDF страница, е просто. Подавате на AddText низ, шрифт, размер и позиция, и глифовете се появяват. Това, което не прави, е да ви каже колко широк ще бъде този низ, след като бъде изчертан, и не пренася дълъг низ на няколко реда. Едно извикване изчертава една поредица от текст на една позиция. Ако поредицата е по-широка от колоната, в която сте искали да се побере, тя просто преминава отвъд ръба, и нищо в извикването за изчертаване не ви предупреждава. В момента, в който искате абзац, а не единичен етикет, липсващото парче е ширината на низа в избрания шрифт и размер, измерена преди да го ангажирате към страницата
Това е класическият проблем с оформлението. За да пренесете абзац в колона, трябва да знаете, дума по дума, колко хоризонтално пространство ще заеме всеки потенциален ред, и трябва да го знаете преди да изчертаете каквото и да било. Пренасянето на думи е цикъл на измерване, обвит около извикване за изчертаване, а връзка, която само изчертава, ви дава втората половина. Поддръжката за измерване на текст в компонента PDFium затваря тази празнина с две функции, MeasureText и MeasureTextWidth, които отчитат рендерирания обхват на низ, без да поставят маркер на нито една страница
Защо измерването е помощен клас, а не нов метод в TPdf
Поддръжката за измерване пристига като помощен клас (class helper) за Delphi за TPdf, живеещ в собствен unit, а не като нови методи, завинтени в класа TPdf. Помощният клас е езикова функция, която ви позволява да прикачвате методи към съществуващ тип извън неговата декларация. След като unit-ът е в обхват, новите методи се извикват точно сякаш принадлежат на класа, така че помощният метод се чете като Pdf.MeasureTextWidth(...) без отделен обект, който да се конструира или предава
Причината да се структурира по този начин е разделянето. Основният тип TPdf остава какъвто е, без добавено поле и без засегнат съществуващ подпис, така че проект, който никога не се нуждае от оформление, никога не носи кода за измерване. Проект, който се нуждае от него, добавя един unit към клаузата uses и методите светват. Възможността става по желание с гранулярността на един unit, което е най-чистият начин да разширите тип, който не притежавате или не искате да смущавате
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
Измерване без докосване на страницата
Измерването трябва да е без странични ефекти. То трябва да отчита ширина, без да оставя нищо след себе си, защото го извиквате многократно, докато решавате оформлението, а страницата трябва да изглежда точно така, както би изглеждала, ако никога не бяхте измервали изобщо. Техниката, която прави това възможно, е да се изгради текстов обект, да се попита за неговия размер и да се изхвърли, преди изобщо да бъде прикачен към страница
Последователността се състои от четири PDFium извиквания. FPDFPageObj_NewTextObj създава текстов обект спрямо документа, като се имат предвид името и размерът на шрифта. FPDFText_SetText задава низа, който този обект носи. FPDFPageObj_GetBounds прочита обратно ограничителната рамка на обекта. FPDFPageObj_Destroy освобождава обекта. От решаващо значение е, че нищо в тази последователност не извиква API-то за вмъкване на страница. Обектът се създава, заявява и унищожава в изолация, така че документът остава непроменен, когато функцията се върне. Това е пробна сонда за еднократна употреба, чийто единствен изход са четирите числа на нейната ограничителна рамка
Това е надеждният начин да го направите, защото PDFium не разкрива удобна ширина на изместване (advance width) за всеки глиф, която бихте могли да сумирате сами. Метриките на глифовете зависят от програмата на шрифта, от кодирането и от това как PDFium зарежда начертанието, и няма публично извикване, което да ви дава изместването на всеки знак в низ. Ограничителната рамка на реален текстов обект, от друга страна, се изчислява от същата машина, която би оформила глифовете за изчертаване, така че отразява действителния рендериран обхват, а не приближение. Изграждането на един обект за еднократна употреба и прочитането на неговите граници е най-надеждното измерване, което библиотеката може да даде
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Координати и мерни единици на резултата
Ограничителната рамка се връща като четири ръба – ляв, долен, десен и горен, а двете измерения се получават чрез изваждане. Ширината е десен минус ляв, а височината е горен минус долен. И двете са изразени в PDF потребителски единици (user units), където една единица е една седемдесет и втора от инча – същото координатно пространство, в което позиционирате текст на страницата. На този етап няма скрита единица на устройството и не е включен пиксел. Ширина 36 означава половин инч от страницата, независимо от крайната резолюция на рендериране
Вертикалната ос върви по начина, по който я дефинира PDF, с Y, нарастващо нагоре, което е причината височината да е горен минус долен, а не обратното. Тази подробност има значение, когато премествате курсор надолу по колона. Измервате височината на реда, след което я изваждате от текущата базова линия, за да намерите следващата, защото преместването надолу по страницата означава преместване към по-малко Y. Ако вашата дестинация е екран, а не хартия, конвертирате потребителските единици в пиксели на устройството чрез резолюцията на дисплея: стойност в потребителски единици, умножена по DPI и разделена на 72, дава пиксели, така че ширина на колона, която сте задали в точки, може да бъде сравнена с измерван фрагмент, преди да решите къде да бъде пренасянето
Какво се случва при дегенериран вход
Функциите са написани така, че да се провалят тихо. Ако няма отворен документ или ако текстовият обект не може да бъде създаден, резултатът е нулев обхват, а не предизвикано изключение (exception). Ширината и височината се инициализират до нула в началото и се презаписват само след като ограничителната рамка бъде успешно прочетена обратно. Празен низ, липсващ документ, шрифт, който библиотеката не може да разреши до обект – всяко от тях връща нула, вместо да хвърля грешка
Този избор поддържа цикъла на измерване прост, защото цикъл, който се изпълнява върху хиляди думи, не е мястото за обработка на изключения при всяка итерация. Цената е, че извикващият носи отговорността за проверката. Нулевата ширина е сигнален маркер, а не факт за текста, така че код, който дели на измерена ширина или предполага положителна стойност, трябва да се пази от нула, преди да ѝ се довери. Третирайте нулата като "не можа да се измери" и договорът е ясен; игнорирайте го и дегенерираният вход тихо ще се превърне в оформление с колона от припокриващи се глифове
Алчен (greedy) алгоритъм за пренасяне на думи, изграден върху измерването
С функция за ширина под ръка, пренасянето на думи е кратък алчен цикъл. Разделяте абзаца на думи, поддържате текущ ред и за всяка дума измервате какъв би бил редът, ако добавите тази дума. Докато пробният ред все още се побира в ширината на колоната, продължавате да добавяте; когато прелее, извеждате текущия ред с AddText и започвате нов с думата, която не се е побрала. Натрупването се извършва изцяло с MeasureTextWidth, а единственото нещо, което някога достига до страницата, е ред, за който вече сте потвърдили, че се побира
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<WideString>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := WideString(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the final line
end;
Цикълът измерва пробния ред, вместо да измерва всяка дума и да сумира, защото ширината на реда не е сумата от ширините на неговите думи. Интервалите между думите допринасят и измерваният ред улавя това директно. Алчното правило – поберете колкото думи позволява колоната и прекъснете при последната, която се побира – е същото правило, което запълва празнината между сурово извикване на AddText и реален абзац. Извикването за изчертаване никога не е било трудната част. Измерването, което трябва да го предхожда, е трудното, и точно това предоставя помощният клас
Къде се вписва това
Измерването е слоят между генерирането на съдържание и неговото рендериране, така че се съчетава естествено с останалата част от работния процес за създаване на документ от нулата. Ако сглобявате страници и поставяте текст на първо място, основата е в създаването на PDF документи от нулата с компонента PDFium в Delphi, където AddText и настройката на страницата са разгледани изцяло. Когато шрифтът, който измервате, има толкова значение, колкото и низът, защото метриките зависят от начертанието, анализирането на свойствата на PDF шрифта с компонента PDFium в Delphi показва как библиотеката отчита информацията за шрифта, която управлява тези ограничителни рамки. И двете се надграждат върху същата връзка, PDFium Component за Delphi и Lazarus, където помощният клас за измерване се доставя заедно с API-тата за документи, страници и текст, описани в този блог