PDFページにテキストを配置する呼び出しは簡単です。AddTextに文字列、フォント、サイズ、位置を指定すると、グリフが表示されます。しかし、描画された後の文字列の幅を教えてくれるわけでも、長い文字列を複数行に分割してくれるわけでもありません。単一の呼び出しで、ある位置に一連のテキストを描画します。そのテキストが、収めたかった列よりも広い場合、単に端を越えてしまい、描画の呼び出しでは警告されません。単一のラベルではなく段落が必要になった瞬間に欠けている部分は、ページにコミットする前に測定された、選択したフォントとサイズでの文字列の幅です
これは古典的なレイアウトの問題です。段落を列に折り返すには、単語ごとに各候補行がどれくらいの水平スペースを占めるかを知る必要があり、何かを描画する前にそれを知っておく必要があります。ワードラップは、描画呼び出しの周囲にラップされた測定ループであり、描画のみを行うバインディングは後半部分しか提供しません。PDFiumコンポーネントのテキスト測定サポートは、ページにマークを付けることなく文字列のレンダリングされた範囲を報告する2つの関数、MeasureTextおよびMeasureTextWidthでそのギャップを埋めます
測定がTPdfの新しいメソッドではなくクラスヘルパーである理由
測定サポートは、TPdfクラスに新しいメソッドとして組み込まれるのではなく、独自のユニットに存在するTPdfのDelphiクラスヘルパーとして提供されます。クラスヘルパーは、宣言の外部から既存の型にメソッドをアタッチできる言語機能です。ユニットがスコープ内に入ると、新しいメソッドはあたかもそのクラスに属しているかのように正確に呼び出されるため、ヘルパーメソッドはPdf.MeasureTextWidth(...)と読め、構築したり持ち回ったりする個別のオブジェクトは必要ありません
このように階層化する理由は分離です。コアのTPdf型はそのまま維持され、フィールドは追加されず、既存のシグネチャも変更されないため、レイアウトを必要としないプロジェクトには測定コードが含まれません。それを必要とするプロジェクトは、uses句に1つのユニットを追加すると、メソッドが利用可能になります。機能は単一のユニットの粒度でオプトインになり、これは所有していない型や、干渉したくない型を拡張する最もクリーンな方法です
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;
ページに触れずに測定する
測定は副作用がないものでなければなりません。レイアウトを決定する間に何度も呼び出し、ページはまったく測定しなかった場合とまったく同じに見える必要があるため、何も残さずに幅を報告する必要があります。これを可能にする技術は、テキストオブジェクトを構築し、そのサイズを尋ね、ページにアタッチされる前にそれを破棄することです
シーケンスは4つのPDFium呼び出しです。FPDFPageObj_NewTextObjは、フォント名とサイズを与えられて、ドキュメントに対してテキストオブジェクトを作成します。FPDFText_SetTextは、そのオブジェクトが保持する文字列を設定します。FPDFPageObj_GetBoundsは、オブジェクトのバウンディングボックスを読み取ります。FPDFPageObj_Destroyはオブジェクトを解放します。重要なのは、そのシーケンスのどれもページ挿入APIを呼び出さないということです。オブジェクトは隔離されて作成、クエリ、および破棄されるため、関数が戻ったとき、ドキュメントは変更されていません。これは、バウンディングボックスの4つの数値のみを出力する使い捨てのプローブです
PDFiumは、自分で合計できるような便利なグリフごとの前進幅を公開していないため、これが堅牢な方法です。グリフメトリクスは、フォントプログラム、エンコーディング、およびPDFiumがフェイスをロードする方法に依存し、文字列内の各文字の前進を手渡すパブリックな呼び出しはありません。一方、実際のテキストオブジェクトのバウンディングボックスは、描画のためにグリフをレイアウトするのと同じメカニズムによって計算されるため、近似ではなく実際のレンダリングされた範囲を反映します。使い捨てのオブジェクトを1つ構築し、その境界を読み取ることは、ライブラリが提供できる最も信頼性の高い測定です
// 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;
結果の座標と単位
バウンディングボックスは、左、下、右、上の4つのエッジとして返され、2つの寸法は引き算によって算出されます。幅は右マイナス左で、高さは上マイナス下です。どちらもPDFのユーザー単位で表され、1単位は1インチの72分の1であり、ページ上にテキストを配置するのと同じ座標空間です。この段階では、隠されたデバイス単位やピクセルは関与しません。最終的なレンダリング解像度がどうであれ、幅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と実際の段落の間のギャップを埋めるルールと同じです。描画呼び出しは決して難しい部分ではありませんでした。その前に必要な測定がそうであり、それこそがヘルパーが提供するものです
これの適合先
測定はコンテンツの生成とレンダリングの間のレイヤーであるため、ゼロからのドキュメントワークフローの他の部分と自然にペアになります。そもそもページを組み立ててテキストを配置している場合、基礎となるのはDelphiのPDFiumコンポーネントを使用してゼロからPDFドキュメントを作成するであり、ここでAddTextとページ設定が完全にカバーされています。メトリクスはフェイスに依存するため、測定しているフォントが文字列と同じくらい重要な場合、DelphiのPDFiumコンポーネントでPDFフォントプロパティを分析するは、ライブラリがそれらのバウンディングボックスを駆動するフォント情報をどのように報告するかを示しています。どちらも同じバインディングであるDelphiおよびLazarus用のPDFiumコンポーネントに基づいており、測定ヘルパーは、このブログ全体で説明されているドキュメント、ページ、およびテキストAPIとともに出荷されています