Извикването, което поставя текст на PDF страница, е просто. Давате на AddText низ, шрифт, размер и позиция и глифовете се появяват. Това, което не прави, е да ви каже колко широк ще бъде този низ, след като бъде нарисуван, и не разделя дълъг низ на няколко реда. Еднократно извикване рисува един цикъл от текст (run of text) на една позиция. Ако цикълът е по-широк от колоната, в която сте искали да се побере, той просто преминава отвъд ръба и нищо в извикването за рисуване не ви предупреждава. В момента, в който искате параграф, а не един етикет, липсващото парче е ширината на низ в избрания шрифт и размер, измерена преди да го ангажирате (commit) към страницата
Това е класическият проблем на оформлението. За да пренесете (wrap) параграф в колона, трябва да знаете, дума по дума, колко хоризонтално пространство ще отнеме всеки кандидат-ред, и трябва да го знаете, преди да нарисувате каквото и да било. Пренасянето на думи (word wrap) е измервателен цикъл, обвит около извикване за рисуване, и биндинг, който само рисува, ви дава втората половина. Поддръжката за измерване на текст в PDFium Component затваря тази празнина с две функции, MeasureText и MeasureTextWidth, които отчитат рендирания размер (rendered extent) на низ, без да поставят маркер на нито една страница
Защо измерването е помощник на клас (class helper), а не нов метод в TPdf
Поддръжката за измерване пристига като помощник на клас (class helper) в Delphi за TPdf, живеещ в собствен unit, вместо като нови методи, завинтени в класа TPdf. Помощникът на клас е езикова функция, която ви позволява да прикачвате методи към съществуващ тип извън неговата декларация. След като unit-ът е в обхват, новите методи се извикват точно както ако принадлежаха на класа, така че помощният метод се чете като Pdf.MeasureTextWidth(...) без отделен обект за конструиране или предаване наоколо
Причината да се наслои по този начин е разделянето (separation). Основният тип TPdf остава такъв, какъвто е, без добавено поле и без докосване на съществуваща сигнатура, така че проект, който никога не се нуждае от оформление (layout), никога не носи кода за измерване. Проект, който се нуждае от него, добавя един unit към клаузата uses и методите светват. Способността става по избор (opt-in) при гранулираност от един единствен unit, което е най-чистият начин да разширите тип, който не притежавате или не искате да нарушавате
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // помощният unit; въвежда MeasureText в обхват (scope) на TPdf
// С unit-а в обхват методите се четат като членове на TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W и H вече са рендираните ширина и височина в PDF потребителски единици
end;
Измерване без докосване на страницата
Измерването трябва да е без странични ефекти. То трябва да докладва ширина, без да оставя нищо след себе си, защото го извиквате много пъти, докато вземате решение за оформление, и страницата трябва да изглежда точно така, както би изглеждала, ако изобщо не бяхте измервали. Техниката, която прави това възможно, е да се изгради текстов обект, да се попита за неговия размер и да се изхвърли, преди изобщо да бъде прикрепен към страница
Последователността е четири PDFium извиквания. FPDFPageObj_NewTextObj създава текстов обект спрямо документа, като са дадени името и размерът на шрифта. FPDFText_SetText задава низа, който този обект носи. FPDFPageObj_GetBounds прочита обратно ограничаващата кутия на обекта. FPDFPageObj_Destroy освобождава обекта. От решаващо значение е, че нищо в тази последователност не извиква API-то за вмъкване на страница. Обектът се създава, запитва и унищожава в изолация, така че документът е непроменен, когато функцията се върне. Това е сонда за еднократна употреба (throwaway probe), чийто единствен изход са четирите числа на нейната ограничаваща кутия
Това е стабилният (robust) начин да го направите, защото PDFium не излага удобна ширина на напредък на глиф (per-glyph advance width), която бихте могли да сумирате сами. Метриките на глифовете зависят от програмата за шрифтове, от кодирането и от това как PDFium зарежда лицето (face), и няма публично извикване, което да ви дава напредъка на всеки символ в даден низ. Ограничаващата кутия на реален текстов обект, от друга страна, се изчислява от същата машинария, която би оформила глифовете за рисуване, така че отразява действителния рендиран размер, а не приближение. Изграждането на един обект за еднократна употреба и прочитането на неговите граници е най-надеждното измерване, което библиотеката може да даде
// Формата на MeasureText, изразена спрямо проверените PDFium извиквания.
// Текстов обект се изгражда, измерва и унищожава; не е включена страница.
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); // сондата е изхвърлена, страницата е непокътната
end;
end;
Координати и единици на резултата
Ограничаващата кутия се връща като четири ръба, ляв, долен, десен и горен, а двете измерения падат (fall out) чрез изваждане. Ширината е дясно минус ляво, а височината е горе минус долу. И двете са изразени в потребителски единици на PDF (PDF user units), където една единица е една седемдесет и втора от инча, същото координатно пространство, в което позиционирате текст на страницата. На този етап няма скрита единица за устройство (device unit) и не участва пиксел. Ширина от 36 означава половин инч от страницата, независимо от крайната разделителна способност на рендиране
Вертикалната ос върви по начина, по който я дефинира PDF, с Y, нарастващо нагоре, което е причината височината да е горе минус долу, а не обратното. Този детайл има значение, когато придвижвате курсор надолу по колона. Измервате височината на един ред, след това я изваждате от текущата базова линия (baseline), за да намерите следващия, защото придвижването надолу по страницата означава движение към по-малко Y. Ако дестинацията ви е екран, а не хартия, конвертирате потребителските единици в пиксели на устройството с разделителната способност на дисплея: стойност в потребителски единици, умножена по DPI и разделена на 72, дава пиксели, така че ширина на колона, която сте задали в точки, може да бъде съпоставена с измерен цикъл, преди да решите къде отива прекъсването
Какво се случва при дегенериран вход
Функциите са написани да се провалят тихо. Ако няма отворен документ или ако текстовият обект не може да бъде създаден, резултатът е нулев размер (zero extent) вместо хвърлено изключение. Ширината и височината са инициализирани на нула в началото и се презаписват само след като ограничителната кутия е прочетена успешно обратно. Празен низ, липсващ документ, шрифт, който библиотеката не може да разреши (resolve) в обект, всяко от тези връща нула, вместо да хвърля грешка
Този избор поддържа цикъла на измерване прост, защото цикъл, който минава през хиляди думи, не е мястото за обработка на изключения при всяка итерация. Цената е, че извикващият (caller) носи отговорността за проверката. Нулевата ширина е страж (sentinel), а не факт за текста, така че код, който дели на измерена ширина или предполага положителна стойност, трябва да се пази от нула, преди да ѝ се довери. Третирайте нулата като "не можа да измери" и договорът е ясен; игнорирайте го и дегенерираният вход тихо се превръща в оформление с колона от припокриващи се глифове
Алчно пренасяне на думи, изградено върху измерването
С функция за ширина в ръка, пренасянето на думи е кратък алчен цикъл (greedy loop). Разделяте параграфа на думи, запазвате текущ ред и за всяка дума измервате какъв би бил редът, ако добавите тази дума. Докато пробният (trial) ред все още се побира в ширината на колоната, продължавате да добавяте; когато прелее, вие изчиствате (flush) текущия ред с AddText и започвате нов с думата, която не се е побрала. Натрупването се извършва изцяло с MeasureTextWidth и единственото нещо, което някога достига до страницата, е ред, за който вече сте потвърдили, че се побира
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<string>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := string(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Измерете кандидат-реда, преди да нарисувате каквото и да било.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // изчистете реда, който се побра
Y := Y - LineHeight; // Y намалява при слизане надолу
Line := Words[I]; // преливащата дума започва на следващия ред
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // изчистете последния ред
end;
Цикълът измерва пробния ред, вместо да измерва всяка дума и да сумира, защото ширината на реда не е сумата от ширините на неговите думи. Интервалите между думите допринасят и един измерен цикъл (run) улавя това директно. Алчното правило, поберете толкова думи, колкото колоната позволява, и прекъснете на последната, която се побира, е същото правило, което запълва празнината между суровото AddText и истински параграф. Извикването за рисуване никога не е било трудната част. Измерването, което трябва да го предхожда, е трудната част и точно това предоставя помощникът
Къде се вписва това
Измерването е слоят между генерирането на съдържание и рендирането му, така че естествено се съчетава с останалата част от работния процес за документ от нулата. Ако сглобявате страници и поставяте текст на първо място, основата е в създаване на PDF документи от нулата с PDFium component в Delphi, където AddText и настройката на страницата са разгледани изцяло. Когато шрифтът, който измервате, има също толкова голямо значение, колкото и низът, тъй като метриките зависят от лицето (face), анализирането на свойствата на PDF шрифта с PDFium component в Delphi показва как библиотеката отчита информацията за шрифта, която задвижва тези ограничаващи кутии. И двете се надграждат върху един и същи биндинг, PDFium Component за Delphi и Lazarus, където помощникът за измерване се доставя заедно с API за документи, страници и текст, описани в този блог