Teknik Makale

Delphi'de Düzen (Layout) ve Sözcük Kaydırma (Word Wrap) için PDF Metnini Ölçme

PDF sayfasına metin koyan çağrı (call) basittir (straightforward). AddText'e bir dize (string), bir yazı tipi (font), bir boyut ve bir konum (position) verirsiniz ve glifler (glyphs) belirir. Ne yapmadığı (What it does not do) ise o dize çizildiğinde (drawn) ne kadar geniş olacağını (how wide) size söylememesi (tell you) ve uzun bir dizeyi (long string) birkaç (several) satıra (lines) bölmemesidir (does not break). Tek (single) bir çağrı, tek (one) bir metin dizisini (run of text) tek bir konumda (one position) çizer (paints). Dizi, sığdırmak (to fit) istediğiniz sütundan (column) daha genişse, basitçe kenarı aşar (runs past the edge) ve çizim (drawing) çağrısındaki hiçbir şey sizi (you) uyarmaz (warns). Tek (single) bir etiket (label) yerine (rather than) bir paragraf istediğiniz (you want) anda (moment), eksik parça (missing piece), seçtiğiniz yazı tipinde ve boyutunda bir dizenin sayfaya işlemeyi (commit) yapmadan önce (before) ölçülen (measured) genişliğidir (width)

Bu klasik (classic) bir mizanpaj sorunudur (layout problem). Bir paragrafı (paragraph) bir sütuna sarmak (To wrap) için, kelime kelime, her (each) aday (candidate) satırın ne kadar (how much) yatay (horizontal) alan kaplayacağını bilmeniz gerekir ve hiçbir şey (anything) çizmeden (drawing) önce (ahead of) bilmeniz gerekir. Kelime kaydırma (Word wrap), bir çizim çağrısının etrafına (around) sarılmış (wrapped) bir ölçüm döngüsüdür (measurement loop) ve yalnızca (only) çizen (draws) bir bağlama (binding) size ikinci yarıyı verir. PDFium bileşenindeki (component) metin (text) ölçüm (measurement) desteği, hiçbir sayfaya (any page) bir iz (a mark) bırakmadan (without putting) bir dizenin işlenmiş (rendered) kapsamını bildiren (report) iki işlev (functions), MeasureText ve MeasureTextWidth ile bu boşluğu (gap) kapatır (closes)

Ölçüm neden TPdf'de yeni bir yöntem (method) değil (not) de bir sınıf yardımcısıdır (class helper)

Ölçüm desteği (measurement support), TPdf sınıfına (class) cıvatalanmış (bolted) yeni yöntemler (new methods) olarak gelmek (arrives) yerine (rather than), TPdf için kendi (its own) biriminde (unit) yaşayan bir Delphi sınıf yardımcısı (class helper) olarak gelir. Bir sınıf yardımcısı, kendi (its) bildirimi (declaration) dışından mevcut bir türe (type) yöntemler eklemenize (attach) izin veren (lets) bir dil özelliğidir (language feature). Birim (unit) kapsama (scope) girdikten sonra (Once), yeni (new) yöntemler (methods) tıpkı sınıfa aitoldukları (belonged to) gibi (exactly as if) çağrılır, bu nedenle (so) yardımcı bir yöntem (helper method), inşa (construct) edilecek (to) veya etrafta geçirilecek (pass around) ayrı (separate) bir nesne (object) olmadan (with no) Pdf.MeasureTextWidth(...) olarak (as) okunur

Bunu bu (this) şekilde (way) katmanlaştırmanın (layer) nedeni (reason) ayırmadır (separation). Çekirdek (core) TPdf tipi, eklenen (added) hiçbir (no) alan (field) ve dokunulan hiçbir (no) mevcut (existing) imza (signature) olmadan olduğu gibi kalır, bu nedenle hiçbir (never) zaman düzene (layout) ihtiyaç duymayan (needs) bir proje hiçbir (never) zaman ölçüm kodunu (measurement code) taşımaz (carries). Buna ihtiyaç duyan (does need) bir proje (project) bir uses cümlesine (clause) bir birim (unit) ekler (adds) ve yöntemler aydınlanır (light up). Yetenek, size ait olmayan (you do not own) veya bozmak (disturb) istemediğiniz (do not want to) bir türü (type) genişletmenin en temiz (cleanest) yolu (way) olan tek (single) bir birimin tanecikliği seviyesinde isteğe bağlı (opt-in) hale gelir

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;

Sayfaya (page) dokunmadan (without touching) ölçüm (Measuring)

Ölçüm yan etkilerden (side effects) arınmış olmalıdır. Hiçbir (nothing) şeyi (anything) geride bırakmadan (without leaving) bir genişlik (width) raporlamalıdır (must report), çünkü bir düzene (a layout) karar verirken onu (it) defalarca (many times) ararsınız ve sayfa (page) tam olarak (exactly) hiç (never) ölçmemiş (measured) olsaydınız görüneceği gibi görünmelidir (must look). Bunu mümkün kılan teknik, bir metin nesnesi (text object) oluşturmak (build), ondan boyutunu (size) istemek (ask) ve bir sayfaya eklenmeden önce onu atmaktır (throw it away)

Dizi, dört (four) PDFium çağrısıdır. FPDFPageObj_NewTextObj, verilen yazı tipi (font) adı (name) ve boyutuna (size) göre belgeye (document) karşı (against) bir metin nesnesi (text object) oluşturur (creates). FPDFText_SetText, nesnenin (object) taşıdığı dizeyi (string) belirler (sets). FPDFPageObj_GetBounds, nesnenin (object's) sınırlayıcı kutusunu (bounding box) okur. FPDFPageObj_Destroy nesneyi serbest (frees) bırakır (frees). Önemli bir nokta (Crucially), dizideki (sequence) hiçbir (nothing) şey (anything) sayfa (page) ekleme (insertion) API'sini çağırmaz. Nesne (object) yalıtım (isolation) içinde oluşturulur, sorgulanır (queried) ve yok edilir, bu nedenle işlev (function) geri döndüğünde (returns) belge değişmeden (unchanged) kalır. Bu, tek çıkışı (only output) sınırlayıcı kutusunun dört sayısı (four numbers) olan kullan at (throwaway) bir sondadır (probe)

PDFium, (PDFium) kendi başınıza toplayabileceğiniz kullanışlı (convenient) bir glif başına avans genişliği (per-glyph advance width) sunmadığı için (does not expose), bu işi (it) yapmanın sağlam (robust) yolu budur. Glif ölçümleri (Glyph metrics) yazı tipi (font) programına, kodlamaya (encoding) ve PDFium'un yüzü (face) nasıl yüklediğine bağlıdır ve size (you) bir dizedeki her (each) karakterin ilerleyişini ileten halka açık (public) bir çağrı (call) yoktur. Gerçek bir metin nesnesinin (text object) sınırlayıcı kutusu, diğer yandan (on the other hand), glifleri çizim için (for drawing) yerleştirecek olan aynı mekanizma (same machinery) tarafından hesaplanır, bu nedenle bir yaklaşım (approximation) yerine gerçekte işlenmiş (rendered) kapsamı yansıtır (reflects). Bir tane atılabilir (disposable) nesne oluşturmak (Building) ve sınırlarını (bounds) okumak (reading), kitaplığın (library) verebileceği (can give) en (most) güvenilir (reliable) ölçümdür

// 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;

Sonucun koordinatları (Coordinates) ve birimleri (units)

Sınırlayıcı kutu dört kenar (edges), sol (left), alt (bottom), sağ (right) ve üst (top) olarak geri gelir ve iki (two) boyut çıkarma işlemi ile düşer. Genişlik, sağ eksi (minus) soldur ve yükseklik üst eksi (minus) alttır. Her ikisi de, bir birimin (unit) bir inçin (inch) yetmiş ikide (seventy-second) birine karşılık geldiği PDF kullanıcı (user) birimlerinde (units), yani metni sayfada (page) konumlandırdığınız aynı koordinat alanında (coordinate space) ifade edilir (expressed). Bu aşamada gizli bir aygıt (device) birimi ve dahil (involved) piksel yoktur. 36 genişlik (width), nihai oluşturma çözünürlüğü ne olursa olsun sayfanın yarım inçi anlamına gelir

Dikey (vertical) eksen (axis), PDF'in onu (it) tanımladığı şekilde (way) ilerler ve Y yukarı doğru (upward) artar (increasing), bu nedenle yükseklik (height) alt (bottom) eksi (minus) üst (top) değil de üst (top) eksi (minus) alttır. İmleci bir sütunda (column) aşağı doğru ilerlettiğinizde (advance) bu (that) ayrıntı (detail) önemlidir. Bir çizginin yüksekliğini (height) ölçersiniz, sonra bir sonrakini bulmak için mevcut taban çizgisinden çıkarırsınız (subtract), çünkü (because) sayfa (page) aşağı inmek daha küçük Y'ye doğru ilerlemek anlamına gelir. Hedefiniz (destination) kağıt değil bir ekransa (screen), ekran çözünürlüğü ile kullanıcı birimlerini cihaz piksellerine (pixels) dönüştürürsünüz (convert): kullanıcı (user) birimlerindeki (units) bir değer, DPI ile çarpıldığında (multiplied) ve 72'ye bölündüğünde (divided) piksel (pixels) elde edilir, bu nedenle kesmenin (break) nereye gideceğine (goes) karar vermeden (decide) önce noktalarda ayarladığınız bir sütun (column) genişliği (width) ölçülen (measured) bir yürütme (run) ile (against) eşleştirilebilir

Yozlaşmış (degenerate) girdide (input) ne olur

İşlevler sessizce başarısız (fail quietly) olacak şekilde (to) yazılmıştır. Açık (open) bir belge (document) yoksa (no) veya metin nesnesi oluşturulamıyorsa, sonuç, atılan (raised) bir istisna (exception) yerine sıfır kapsamdır (zero extent). Genişlik ve yükseklik tepede (at the top) sıfıra (zero) ilklenir (initialised) ve yalnızca (only) bir sınırlayıcı kutu (bounding box) başarılı (successfully) bir şekilde (successfully) okunursa (read back) üzerine yazılır (overwritten). Boş (empty) bir dizi (string), eksik (missing) bir belge, kitaplığın nesneye çözümleyemediği bir yazı tipi, bunların (these) her biri istisna atmak (throwing) yerine sıfır (zero) döndürür

Bu (That) seçim (choice) bir ölçüm döngüsünü basit tutar, çünkü binlerce (thousands) kelime (words) üzerinden (over) çalışan (runs) bir döngü, her yinelemede (iteration) istisna işlemesi (exception handling) için bir yer değildir. Maliyeti, denetimi (check) çağıranın (caller) taşımasıdır (carries). Sıfır (zero) genişlik (width) metinle (text) ilgili (about) bir gerçek (fact) değil, bir gözcüdür (sentinel), bu nedenle ölçülen bir genişliğe bölünen veya pozitif bir değer varsayan (assumes) kodun (code), ona (it) güvenmeden (trusting) önce sıfıra (zero) karşı (against) koruma (guard) sağlaması gerekir. Sıfırı (zero) "ölçülemedi" (could not measure) olarak (as) ele alın ve sözleşme açıktır (clear); onu görmezden (ignore) gelin ve bozuk bir girdi sessizce üst üste binen gliflerden oluşan bir sütunla (column) bir düzene dönüşür (becomes)

Ölçüm üzerine (on) inşa edilmiş (built) açgözlü (greedy) bir kelime sarması (word wrap)

Elde (in hand) bir genişlik (width) işlevi varken (With), kelime kaydırma (word wrap) kısa (short) açgözlü (greedy) bir döngüdür (loop). Paragrafı kelimelere (words) ayırırsınız (split), mevcut (current) bir satır (line) tutarsınız (keep) ve her (each) kelime (word) için (for) o (that) kelimeyi eklerseniz satırın ne olacağını ölçersiniz (measure). Deneme çizgisi (trial line) sütun genişliğine (column width) hala uyarken (still fits) eklemeye devam edersiniz; taşacağı (overflow) zaman, AddText ile (with) mevcut satırı (current line) temizlersiniz (flush) ve sığmayan kelimeyle (word) yenisini başlatırsınız (start). Birikim (accumulation) tamamen (entirely) MeasureTextWidth ile yapılır (is done) ve (and) sayfaya (page) ulaşan tek şey (only thing), halihazırda (already) onayladığınız bir satırdır

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;

Döngü, (The loop) her (each) kelimeyi (word) ölçmek ve toplamak yerine (instead of) deneme satırını (trial line) ölçer (measures), çünkü (because) bir satırın genişliği (width) kelimelerinin (words) genişliklerinin (widths) toplamı (sum) değildir. Kelimeler arasındaki (between words) boşluklar katkıda bulunur ve ölçülen (measured) bir çalışma (run) bunu (that) doğrudan (directly) yakalar (captures). Sütunun izin verdiği (allows) kadar çok (many) kelime (words) sığdırma (fit) ve sığan son kelimede kırma (break) şeklindeki açgözlü kural, ham (raw) bir AddText ile (with) gerçek bir paragraf arasındaki (between) boşluğu (gap) dolduran (fills) kuralın (rule) aynısıdır (same). Çizim (drawing) çağrısı (call) asla zor bölüm (part) değildi (never). Ondan önce gelmesi (precede) gereken ölçümdür (measurement that has to) ve (and) yardımcının (helper) sağladığı (provides) tam olarak (exactly) budur (what)

Bunun nerede uyduğu (Where this fits)

Ölçüm, içerik (content) oluşturma ve onu (it) işleme (rendering) arasındaki katmandır, dolayısıyla sıfırdan oluşturulan bir belge (document) iş akışının (workflow) geri kalanıyla (rest of) doğal olarak (naturally) eşleşir (pairs). İlk etapta sayfaları birleştiriyor (assembling) ve metinleri yerleştiriyorsanız, temel (groundwork), AddText ve sayfa (page) kurulumunun tamamının (in full) kapsandığı Delphi'deki PDFium bileşeniyle sıfırdan PDF belgeleri oluşturma makalesindedir. Ölçümlediğiniz (measuring) yazı tipinin (font) en az dizi (string) kadar (as much as) önemli olduğu durumlarda (When), çünkü (because) ölçümler (metrics) yüze (face) bağlıdır (depend on), Delphi'de PDFium VCL bileşeniyle PDF yazı tipi özelliklerini analiz etme, kütüphanenin o (those) sınırlayıcı kutuları süren yazı tipi bilgisini nasıl (how) raporladığını (reports) gösterir. Her ikisi de, (Both) ölçüm yardımcısının bu blogda açıklanan (described) belge (document), sayfa (page) ve metin API'leriyle birlikte gönderildiği, Delphi ve Lazarus için PDFium Bileşeni olan aynı bağlama (binding) üzerine inşa edilir