Teknik Makale

Delphi'de PDFium Karakter Kutularıyla PDF Metin Satırı Seçimi

Bir PDF metin sayfası karakterleri ve kutuları açığa vurur, satırları asla. PDFium Component, dikey merkezleri tohum karakter yüksekliğinin yarısı içine düşen karakter kutularını kümeleyerek, tıklanan karakterden dışa doğru tarayarak ve tolerans aşılana kadar devam ederek görsel bir satır inşa eder. Görüntüleyicideki her seçim yolu o tek yardımcıyı çağırır, dolayısıyla fare, klavye ve kod anlaşır

Sizi bunu aramaya gönderen belirti özel ve nahoştur. Bir kullanıcı iki sütunlu bir raporda bir paragrafa üç kez tıklar ve sayfanın yarısını alır. Ya da bir tablo hücresine üç kez tıklar ve seçim tüm satırı artı altbilgideki sayfa numarasını yutar. Görüntüleyici bozuk değildir; dosyanın yanıtlayamayacağı bir soru soruyordur. Bir PDF'de seçilecek bir satır yoktur ve aksini iddia eden herhangi bir uygulama tahmin ediyordur. Bu makale tahmini kasıtlı ve tutarlı kılmak hakkındadır. Gerçekten ihtiyacınız olan şey bir belgeden metin çekmekse, PDFium ile PDF belgelerinden metin çıkarmaya bakın; metni düzenliyorsanız ve genişliklere ihtiyacınız varsa, metin ölçümü ve kelime kaydırmaya bakın. Burada konu daha dardır: bir görsel satırın nerede başlayıp bittiğine karar vermek ve tam olarak onu seçmek

Bir PDF metin sayfasının neden hiç satır nesnesi yok?

Çünkü bir PDF içerik akışı çizimi tanımlar, yapıyı değil. ISO 32000-1 §9.4, bir metin nesnesini konumlandırma ve gösterme operatörleri içeren bir BT / ET çifti olarak tanımlar. §9.4.2'nin konumlandırma operatörleri (Td, TD, Tm, T*) bir metin matrisini sayfa etrafında hareket ettirir ve §9.4.3'ün gösterme operatörleri (Tj, TJ, ', ") o matrisin o an işaret ettiği her yerde glifler boyar. Bu modelde hiçbir şey "bu glif çalıştırması bir satırdır" demez. Bir satır, boyama bittikten sonra bir insanın gördüğü şeydir

Üreticiler bunu kontrol edemeyeceğiniz şekillerde daha da kötüleştirir. Yaslanmış bir paragraf, satır başına bir TJ dizisi olarak, veya öncesinde açık bir Tm olan kelime başına bir Tj olarak, veya boşluğu taşıyan tirlem ayarlamalarıyla tek bir gösterme işlemi olarak yayılabilir. İki sütunlu bir düzen, sol sütunu yukarıdan aşağıya sonra sağ sütunu yayabilir, veya üretici kendi dahili nesne listesini farklı bir sırada dolaştıysa onları iç içe geçirebilir. PDFium'un size verdiği karakter dizisi, içerik akışını izler ve içerik akışı, üreten uygulamanın canının ne istediğini izler. Dolayısıyla gerçekte aldığınız iki fonksiyon, sayfanın kaç karakter tuttuğunu bildiren FPDFText_CountChars ve sayfa uzayında bir karakterin sınır kutusunu döndüren FPDFText_GetCharBox'dır. Tüm ham kelime dağarcığı budur. Onun üzerindeki her şey, kelimeler, satırlar, paragraflar, sütunlar, geometri üzerinde yaptığınız çıkarımdır

CR ve LF tespiti neden yanlış bir test?

Çünkü karşı test edeceğiniz karakterler güvenilir bir şekilde mevcut değildir ve mevcut olduklarında güvenilir bir şekilde sizin değildir. PDFium, çıkarılan metni okunabilir kılmak için metin sayfasına sentetik karakterler enjekte eder: iki çalıştırma görsel olarak ayrıldığında bir boşluk, bir sonraki çalıştırma yeni bir taban çizgisinde başladığında bir CR veya LF. FPDFText_IsGenerated tam olarak bunları dosyadan çıkan karakterlerden ayırt edebilmeniz için vardır ve PDFium Component bunu CharacterGenerated özelliği olarak açığa vurur

O karakterlerde bölün ve PDFium'un onları sentezlerken verdiği her yargı kararını miras alırsınız. Sarmalanmış bir paragraf içindeki sert bir satır sonu ve yumuşak bir kaydırma, sentezden sonra aynı görünür. Üreticinin hücre hücre yaydığı bir tablo satırı, taban çizgileri yeterince yakın olduğu için son hücre ile bir sonraki satırın ilk hücresi arasında hiç kesinti almayabilir. Bu arada farklı bir boyutta gövde metnini takip eden bir başlık, bir insanın bir tane gördüğü yerde iki kesinti alabilir. Üretilen karakterler, tüm sayfa çıkarma için bir render kolaylığıdır; bir satır modeli değildir ve tam olarak seçimin en çok önemli olduğu belgelerde bozulurlar

Karakter kutularını dikey merkeze göre kümelemek

Güvenilir sinyal geometridir. Kullanıcının tıkladığı karakteri tohum olarak alın, kutusunun dikey merkezini hesaplayın ve komşu kutular dikey merkezlerini tolerans içinde tuttukça her iki yönde dışa doğru yürüyün. PDFium Component, o toleransı tohum kutu yüksekliğinin yarısı olarak kullanır, sayfa birimi 0,5'lik bir tabanla, böylece dejenere kutular — bir nokta, ince bir boşluk, neredeyse sıfır yükseklikli kutulu bir glif — toleransı hiçe indirmez ve satırı bir karakterden sonra kesmez

function TPdfView.LineRangeAt(TxtPage: FPDF_TEXTPAGE; CharIndex: Integer;
  out StartIndex, Count: Integer): Boolean;
var
  Lo, Hi, Total: Integer;
  SeedBox, Box: TPdfRectangle;
  SeedYMid, BoxYMid, HalfH: Double;
begin
  Result := False;
  StartIndex := -1;
  Count := 0;
  Total := FPDFText_CountChars(TxtPage);
  if (CharIndex < 0) or (CharIndex >= Total) then
    Exit;

  if FPDFText_GetCharBox(TxtPage, CharIndex, SeedBox.Left, SeedBox.Right,
    SeedBox.Bottom, SeedBox.Top) = 0 then
    Exit;
  SeedYMid := (SeedBox.Top + SeedBox.Bottom) / 2;
  HalfH := Abs(SeedBox.Top - SeedBox.Bottom) / 2;
  if HalfH < 0.5 then          // floor for degenerate boxes
    HalfH := 0.5;

  Lo := CharIndex;
  Hi := CharIndex;
  while Lo > 0 do
  begin
    if FPDFText_GetCharBox(TxtPage, Lo - 1, Box.Left, Box.Right,
      Box.Bottom, Box.Top) = 0 then
      Break;
    BoxYMid := (Box.Top + Box.Bottom) / 2;
    if Abs(BoxYMid - SeedYMid) > HalfH then
      Break;
    Dec(Lo);
  end;
  while Hi < Total - 1 do
  begin
    if FPDFText_GetCharBox(TxtPage, Hi + 1, Box.Left, Box.Right,
      Box.Bottom, Box.Top) = 0 then
      Break;
    BoxYMid := (Box.Top + Box.Bottom) / 2;
    if Abs(BoxYMid - SeedYMid) > HalfH then
      Break;
    Inc(Hi);
  end;
  StartIndex := Lo;
  Count := Hi - Lo + 1;
  Result := True;
end;

Bu döngüdeki üç ayrıntı yerini hak eder. Tolerans, bir sabitten değil tohumdan türetilir, dolayısıyla 24pt'lik bir başlık geniş bir bant alır ve 7pt'lik dipnot metni dar bir bant alır ve hiçbiri diğerinden karakter çalmaz. Karşılaştırma, taban çizgileri veya kutu üstleri yerine dikey merkezleri kullanır, ki bu bir üst simgeyi, satır içi farklı boyutlu bir çalıştırmayı veya karma fontlu bir cümleyi komşularıyla aynı satırda tutar. Ve başarısız bir FPDFText_GetCharBox, atlanmak yerine taramayı sonlandırır, çünkü alınamayan geometriye sahip bir karakter size hiçbir yönde kanıt vermez ve onun ötesine devam etmek, yürüyüşün daha ileride bir karakterin gücüyle gerçek bir sınırı atlamasına izin verirdi

Her seçim yolu neden bir yardımcıyı paylaşmalı?

Çünkü her biri "satırı" ayrı ayrı uygulayan üç kod yolu birbirinden ayrılır ve sessizce ayrılırlar. PDFium Component'te, üç tıklama genişletmesi, Shift+Home, Shift+End ve genel SelectLineAt yöntemi, sınırlarını aynı LineRangeAt çağrısı üzerinden çözer. Üç tıklama onu seçim çıpasından tohumlar; shift tuşları onu seçim imlecinden tohumlar ve yalnızca o ucu hareket ettirir; SelectLineAt onu çağıranın sağladığı bir karakter indeksinden tohumlar ve sonucu, fare yolunun kullandığı aynı aralık doğrulayıcısı olan SelectTextRange'e verir. Bunun yerine mantığı çoğaltın ve hata bir çökme değil, yavaş bir sürüklenmedir. Birisi sıkı satır aralığına sahip bir raporu düzeltmek için üç tıklama toleransını ayarlar ve şimdi Shift+End, aynı paragrafta üç tıklamanın durduğu yerden bir karakter kısa duruyor. Bir kullanıcı fareyle bir satırı seçer, klavyeyle genişletir ve seçimin küçüldüğünü izler. SelectLineAt, sıradan seçim boru hattını beslediği için, programatik seçim de fare girdisinin etkin olup olmadığından bağımsız kalır ve yine de bedavaya aralık doğrulaması, yeniden boyama ve OnSelectionChange bildirimini alır

// Select the visual line under a client-space point, then read it back
procedure TForm1.SelectLineUnderCursor(X, Y: Integer);
var
  CharIndex: Integer;
begin
  CharIndex := PdfView1.CharacterIndexAtPos(X, Y, 6.0, 6.0);
  if CharIndex < 0 then
    Exit;
  if PdfView1.SelectLineAt(PdfView1.CurrentPage, CharIndex) then
    Memo1.Lines.Add(PdfView1.SelectedText);
end;

CharacterIndexAtPos'taki tolerans argümanlarını not edin. İsabet testinin kendi gevşekliği vardır, sayfa birimlerinde ifade edilir ve satır toleransından ayrı bir kaygıdır. İki satır arasındaki satır aralığına inen bir tıklama, o kutu içinde en yakın olan karaktere çözülür; satır taraması sonra o karakter her ne çıktıysa ondan çalışır. Tohuma çok cömert bir isabet toleransı beslemek, kullanıcının işaret etmediği bir satırı seçmenin daha kolay yollarından biridir

İki indeks uzayı: karakter indeksi ve metin indeksi

Bir aralığa sahip olduğunuzda, onu bir dize ofseti olarak kullanma dürtüsüne direnin. FPDFText_GetText, sayfa metnini bir UTF-16 arabelleği olarak döndürür, ama indeksleri FPDFText_GetCharBox ve FPDFText_CountChars'ın kullandığı karakter indeksleriyle aynı indeks uzayı değildir. Daha önce ele alınan üretilen karakterler, metin arabelleğinde otururken kullanılabilir geometrisi olmayan karakter yuvalarını işgal eder ve iki numaralandırma sayfa boyunca birbirinden uzaklaşır. Köprüler, PDFium Component tarafından CharacterIndexToTextIndex ve TextIndexToCharacterIndex olarak sarılan FPDFText_GetTextIndexFromCharIndex ve FPDFText_GetCharIndexFromTextIndex'tir

var
  TextStart, TextEnd: Integer;
begin
  // char-index range from LineRangeAt -> offsets into the page text buffer
  TextStart := Pdf.CharacterIndexToTextIndex(StartIndex);
  TextEnd   := Pdf.CharacterIndexToTextIndex(StartIndex + Count - 1);
  if (TextStart >= 0) and (TextEnd >= TextStart) then
    Caption := Pdf.Text(TextStart, TextEnd - TextStart + 1);
end;

En sert ısıran yön ters yöndür. Çıkarılan dize üzerinde uygulanan bir arama size metin indeksleri verir ve bunları doğrudan bir kutu veya seçim API'sine geçirmek, sayfada ne kadar aşağı inerseniz o kadar büyüyen bir hatayla, sessizce yanlış karakterleri adresler. Geometrik bir şey sayıya dokunmadan önce TextIndexToCharacterIndex ile dönüştürün. Vekil çiftler, bunun üzerine emoji, CJK ve vekil çiftler üzerine makalede ele alınan ikinci, bağımsız bir ofset problemi ekler

Sezgisel yöntemin büküldüğü yer

Sınırlar hakkında kendinize karşı dürüst olun, çünkü gerçekler ve ulaşılabilirler. Döndürülmüş metin en açık durumdur: bir karakter kutusu, sayfa uzayında eksen hizalı bir dikdörtgendir, dolayısıyla 90 derece döndürülmüş metin için bir görsel satırın kutuları sayfa boyunca yayılmış dikey merkezlere sahiptir ve tarama neredeyse hemen durur. Elde ettiğiniz kısa bir seçimdir, yanlış bir seçim değil, ki bu daha iyi bir hata modudur, ama yine de bir hatadır. Dikey yazma modları aynı nedenle aynı şekilde davranır. İki sütunlu düzenler, sütunlar birbirinden dikey olarak kaydırıldığında çalışır ve kaydırılmadığında bozulur. Her iki sütun da bir taban çizgisi ızgarasını paylaşıyorsa, sağ sütundaki karakterler sol sütun satırının toleransı içinde oturur ve tarama, saf geometride durulacak hiçbir şey olmadığı için, tam olarak oluğun karşısına geçer. Bunu tespit etmek, dikey kümeleme üzerine bir yatay boşluk testine ihtiyaç duyar ve boşluk eşiğini seçmek, hangi belgelerde yanılmaya razı olduğunuz hakkında kendi başına bir yargı kararıdır. Karma font boyutları, tohuma göreli toleransın iyi ele aldığı durumdur: 11pt gövde metni içinde satır içi 8pt'lik bir kod aralığı, merkezini bant içinde tutar ve bir sonraki taban çizgisindeki 24pt'lik bir başlık, gövde satırını kendine çekmez

Burada anlatılan satır-seçim semantiği, örneklerde kullanılan isabet testi, seçim aralığı ve metin indeksi API'lerinin yanında, Delphi ve C++Builder için PDFium Component'te gönderilir; ürün sayfası, metin sayfası ve seçim modeli için tam referansı taşır