Вызов, помещающий текст на страницу PDF, прост. Вы даете AddText строку, шрифт, размер и позицию, и глифы появляются. Чего он не делает, так это не сообщает вам, какой ширины будет эта строка после ее рисования, и он не разбивает длинную строку на несколько строк. Один вызов рисует один проход текста в одной позиции. Если проход шире, чем столбец, в который вы хотели его вписать, он просто выходит за край, и ничто в вызове рисования не предупреждает вас об этом. В тот момент, когда вам нужен абзац, а не одна метка, недостающей частью является ширина строки в выбранном шрифте и размере, измеренная до того, как вы зафиксируете ее на странице
Это классическая проблема компоновки. Чтобы перенести абзац в столбец, вам нужно знать слово за словом, сколько горизонтального пространства займет каждая строка-кандидат, и вы должны знать это до того, как что-либо нарисовать. Перенос слов — это цикл измерения, обернутый вокруг вызова рисования, и привязка, которая только рисует, дает вам вторую половину. Поддержка измерения текста в компоненте PDFium закрывает этот пробел двумя функциями, MeasureText и MeasureTextWidth, которые сообщают об отображаемом размере строки, не оставляя следов ни на какой странице
Почему измерение — это помощник класса (class helper), а не новый метод в TPdf
Поддержка измерений реализована как помощник класса (class helper) Delphi для TPdf, находящийся в отдельном модуле (unit), а не как новые методы, прикрепленные к классу TPdf. Помощник класса — это языковая функция, позволяющая прикреплять методы к существующему типу за пределами его объявления. Как только модуль находится в области видимости, новые методы вызываются так, как если бы они принадлежали классу, поэтому метод-помощник читается как Pdf.MeasureTextWidth(...) без необходимости создавать отдельный объект или передавать его
Причина такого расслоения — разделение. Базовый тип TPdf остается таким, какой он есть, без добавления каких-либо полей и изменения существующих сигнатур, поэтому проект, которому никогда не нужен макет, никогда не содержит код для измерений. Проект, которому это нужно, добавляет один модуль в раздел uses, и методы загораются. Возможность становится опциональной на уровне отдельного модуля, что является самым чистым способом расширения типа, которым вы не владеете или не хотите беспокоить
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 не предоставляет удобную ширину продвижения для каждого глифа, которую вы могли бы суммировать сами. Метрики глифов зависят от программы шрифта, кодировки и от того, как 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, где одна единица равна 1/72 дюйма — то самое координатное пространство, в котором вы позиционируете текст на странице. На этом этапе нет никаких скрытых единиц устройства или пикселей. Ширина 36 означает половину дюйма страницы, независимо от конечного разрешения рендеринга
Вертикальная ось проходит так, как определяет PDF: ось Y увеличивается вверх, поэтому высота равна верхнему минус нижнему, а не наоборот. Эта деталь имеет значение, когда вы перемещаете курсор вниз по столбцу. Вы измеряете высоту строки, затем вычитаете ее из текущей базовой линии, чтобы найти следующую, потому что движение вниз по странице означает движение к меньшему Y. Если вашим местом назначения является экран, а не бумага, вы конвертируете пользовательские единицы в пиксели устройства с учетом разрешения экрана: значение в пользовательских единицах, умноженное на DPI и разделенное на 72, дает пиксели, поэтому ширину столбца, которую вы устанавливаете в точках, можно сопоставить с измеренным проходом перед тем, как вы решите, где будет перенос
Что происходит при неверном вводе
Функции написаны так, чтобы тихо давать сбой. Если нет открытого документа или если текстовый объект не может быть создан, результатом будет нулевой размер, а не вызванное исключение. Ширина и высота инициализируются нулем в верхней части и перезаписываются только после успешного чтения ограничивающей рамки. Пустая строка, отсутствующий документ, шрифт, который библиотека не может преобразовать в объект, — каждый из этих вариантов возвращает ноль вместо генерации исключения
Этот выбор упрощает цикл измерений, поскольку цикл, выполняющийся над тысячами слов — не место для обработки исключений на каждой итерации. Цена этого заключается в том, что проверка ложится на вызывающую сторону. Нулевая ширина является сигнальным значением, а не фактом о тексте, поэтому код, который делит на измеренную ширину или предполагает положительное значение, должен защититься от нуля, прежде чем доверять ему. Воспринимайте ноль как «не удалось измерить», и контракт будет ясен; проигнорируйте это, и неверный ввод тихо превратится в макет со столбцом перекрывающихся глифов
Жадный перенос слов на основе измерения
Имея функцию определения ширины, перенос слов — это короткий жадный цикл. Вы разбиваете абзац на абзац на слова, сохраняете текущую строку, и для каждого слова вы измеряете, какой была бы строка, если бы вы добавили это слово. Пока тестовая строка все еще вписывается в ширину столбца, вы продолжаете добавлять; когда она выйдет за пределы, вы сбрасываете текущую строку с помощью 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];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // 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(Line, Font, FontSize, X, Y); // flush the final line
end;
Цикл измеряет пробную строку, а не измеряет каждое слово и суммирует, потому что ширина строки не равна сумме ширин ее слов. Пробелы между словами вносят свой вклад, и измеренный проход захватывает это напрямую. Жадное правило, вписывающее столько слов, сколько позволяет столбец, и прерывающее на последнем, которое подходит, — это то же самое правило, которое заполняет разрыв между необработанным AddText и реальным абзацем. Вызов отрисовки никогда не был сложной частью. Измерение, которое должно ему предшествовать — вот в чем сложность, и это именно то, что обеспечивает помощник
Где это применяется
Измерение — это слой между генерацией контента и его визуализацией, поэтому оно естественно сочетается с остальной частью рабочего процесса создания документа с нуля. Если вы собираете страницы и размещаете текст с самого начала, то основа заложена в статье Создание PDF-документов с нуля с компонентом PDFium в Delphi, где полностью рассматриваются AddText и настройка страницы. Когда измеряемый шрифт имеет такое же значение, как и строка, потому что метрики зависят от начертания, анализ свойств шрифтов PDF с помощью компонента PDFium в Delphi показывает, как библиотека сообщает информацию о шрифте, которая управляет этими ограничивающими рамками. Оба метода основаны на одной и той же привязке, компоненте PDFium для Delphi и Lazarus, где помощник измерения поставляется вместе с API документа, страницы и текста, описанными в этом блоге