Teknik Makale

Vuruş Koordinatlarıyla Delphi PDF Metin Araması: PDFlibPas

Bir sayfanın metnini çıkarmak işin kolay yarısıdır. Kullanıcı arama kutusuna bir kelime yazıp görüntüleyicinin oraya sıçramasını ve etrafına sarı kutu çizmesini beklediği anda, düz metin dizgesinin veremediği bir şeye ihtiyacınız olur: her eşleşmenin bulunduğu sayfa ve PDF koordinatlarındaki kapladığı dikdörtgen. Sayfa boyunca birleştirilmiş bir dizge bu geometriyi kaybetmiştir. Alt dizgeyi bulabilirsiniz ama onu işaret edemezsiniz

PDFlibPas, Delphi ve C++Builder için yerel bir Object Pascal PDF kütüphanesidir ve v3.78.0'dan itibaren tam olarak bu soruya cevap verir. Mevcut text-block çıkarıcısının üstüne oturan üç sorgu API'si vardır: SearchText, sayfa aralığında yürür ve her vuruşu sayfası ile eksene hizalı dikdörtgeniyle döndürür; EnumPageElements, tek sayfadaki her şeyi listeler; buna metin blokları ve gömülü görseller de dahildir; GetTextInAreaEx ise bir bölge içindeki her bloğun dikdörtgenini, bunları düz dizge listesine ezmeden rapor eder. Bunların hiçbiri yazma yoluna dokunmaz; kütüphanenin zaten sahip olduğu makinenin üstüne kurulmuş saf okuma tarafı ekleridir

Geometri neden hunide değil, text-block listesinde yaşar

Doğal içgüdü, içeride ne çalışıyorsa GetPageText onu yeniden kullanmaktır. Bu yol, sayfa dizgesini üreten ve çağrı dönmeden önce kendini serbest bırakan geçici bir extraction "funnel" içinden geçer. Sonucu elinize aldığınızda blok başına koordinatlar gitmiştir. Onları saklama hakkı zaten hiç sizin olmamıştır

Koordinatlar farklı bir yapıda yaşamaya devam eder. ExtractPageTextBlocks(3), her öğesi sekiz double'lık bounding quad, font adı, font boyutu ve bloğun metnini taşıyan bir text-block list handle'ı döndürür. Çıkarma işleminden sonra geometrinin elde kaldığı tek yer bu handle'dır; bu yüzden yeni sorgu API'lerinin her biri funnel yerine bunun üzerine kuruludur. Block list'i yeniden kullanmak, aramanın, numaralandırmanın ve bölge sorgularının tek extraction geçişini ve bloğun nerede olduğuna dair tek tanımı paylaşması anlamına gelir

Bu yüzden SearchText yönteminin şekli bu kısıttan doğar. Aralıktaki her sayfa için block list'i çıkarır, her bloğun metnini GetTextBlockText ile okur, sorguya karşı dener ve eşleşen bloklarda quad'ı dikdörtgene indirger. Döndürdüğü vuruş küçük bir kayıttır:

type
  TPDFlibSearchHit = record
    Page: Integer;                       // 1-based page of the match
    Left, Top, Right, Bottom: Double;    // axis-aligned hit rectangle
    MatchText: WideString;               // the block text that contained the query
  end;

Bound dizisi dört köşe değil, X/Y iç içe dizilidir

İlk can yakan ayrıntı budur. GetTextBlockBound(ListID, Index, BoundIndex), 1 ile 8 arasında bir BoundIndex alır ve bu sekiz değer, tahmin edebileceğiniz gibi iki alanlı "köşe 1, köşe 2, köşe 3, köşe 4" biçiminde gruplu değildir. Bunlar X, Y, X, Y, X, Y, X, Y düzenindedir: tek sayılı indisler X koordinatlarıdır, çift sayılı indisler Y koordinatlarıdır; toplam dört nokta vardır. Bunları yanlış eşlerseniz dikdörtgeniniz anlamsız olur

Düz dikdörtgen yerine quad bulunmasının nedeni dönüş açısıdır. Açılı yerleştirilmiş metin bloğu gerçek bir dört noktalı sınırlayıcı çokgene sahiptir ve bu sekiz double onu sadakatle açıklar. Highlight-and-jump kullanımı için ise neredeyse her zaman dik duran kutu istersiniz; bu yüzden kütüphane, dört noktanın minimum ve maksimum X ile Y değerlerini süpürerek quad'ı eksene hizalı dikdörtgene indirger. Döndürülmüş metin, onu içine alan dik kutuya çöker; highlight katmanının ihtiyaç duyduğu şey de tam budur:

var
  Pdf: TPDFlib;
  Hits: array[0..255] of TPDFlibSearchHit;
  Found, I: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('contract.pdf', '');
    // Search pages 1 to 10, case-insensitive, substring match.
    Found := Pdf.SearchText('indemnity', [], '1-10', Hits);
    for I := 0 to Found - 1 do
      if I <= High(Hits) then
        WriteLn(Format('p%d: [%.1f %.1f %.1f %.1f] %s',
          [Hits[I].Page, Hits[I].Left, Hits[I].Top,
           Hits[I].Right, Hits[I].Bottom, Hits[I].MatchText]));
  finally
    Pdf.Free;
  end;
end;

Dikdörtgenin, sayfanın sol alt köşesini başlangıç kabul eden PDF user-space point koordinatlarında olduğunu not edin; bu, çizim ve annotation çağrılarına verdiğiniz koordinat sistemiyle aynıdır. Bu bilinçli bir seçimdir: arama vuruşundan geri aldığınız dikdörtgeni, dönüştürme yapmadan doğrudan highlight annotation'a veya "buraya kaydır" komutuna verebilirsiniz

Büyük-küçük harf duyarlılığı, tam sözcük ve CJK farkı

İkinci parametre, TPDFlibSearchOptions ile soCaseSensitive ve soWholeWord içinden seçilen bir [] kümesidir. Boş küme soCaseSensitive yaygın durumdur: büyük-küçük harf duyarsız alt dizge araması. Indemnity ekleyerek indemnity ile soWholeWord öğelerini farklı kılın, sign ekleyerek signature değerinin

Tam sözcük eşleşmesi için sözcük sınırının ne olduğuna dair tanım gerekir ve burada kuralın, tasarım gereği ASCII merkezli olduğunu açıkça söylemekte fayda var. Karakter; ASCII harfi, ASCII rakamı veya alt çizgiyse sözcüğün parçası sayılır: tanımlayıcı kurallarından tanıdığınız [A-Za-z0-9_] sınıfı. Eşleşme ancak hemen önündeki ve arkasındaki karakterler değilse sözcük karakteri sayılır; ya da eşleşme bloğun kenarında duruyorsa tam sözcük kabul edilir

Latin dışı betikler için sonucu, çok dilli arama kutusu göndermeden önce bilmek gerekir. Han karakterleri, kana ve ASCII dışı diğer harfler bu sınıfın dışında kaldığı için yanlarındaki her sınır sözcük dışı kenar gibi okunur. Pratikte bunun anlamı, CJK metni üzerinde tam sözcük aramasının her konumu geçerli sözcük sınırıymış gibi davranması ve bayrağın orada fiilen alt dizge eşleşmesine düşmesidir. Bu belgelenmiş sınırlamadır, hata değildir ve özelliğin modellendiği davranışla örtüşür. Korpusunuz ağırlıklı olarak CJK ise tam sözcük modu, özel bir tokenizer'ın vereceği bölümlemeyi sağlamaz; buna güvenmek yerine tasarımınızı buna göre kurun

Başka yerlerdeki ince arıza sınıfını açıklayan bir uygulama dipnotu daha var: büyük-küçük harf duyarsız karşılaştırma, UpperCase yöntemini WideString üzerinde kullanır, AnsiUpperCase üzerinde değil. Ansi varyantı bir AnsiString döndürür; bu, yolun geri kalanında kullanılan WideString ile hizalanmaz ve ikisini karıştırmak tür uyuşmazlıkları, daha kötüsü etkin kod sayfası dışındaki karakterlerde kayıplı harf katlama üretir. Baştan sona Unicode girer, Unicode çıkar

Tüm kütüphane için tek sayfa aralığı ayrıştırıcısı

Üçüncü parametre, "1,3,5-9" gibi bir sayfa aralığı dizgesidir. Bunun ayrıştırılmasında özel hiçbir şey yoktur: burada da, PLParsePageRangeList ile PrintPages ve sayfa kopyalama yordamlarının arkasındaki aynı SearchText kullanılır; dolayısıyla doğru yazdırılan aralık doğru aranır. Boş aralık dizgesi "tüm sayfalar" için işaret değeridir; bu durumda

Maliyet açısından kapsam önemlidir. Bin sayfalık belgede on sayfalık dilimde arama yaparsanız bin değil, on sayfa için blok çıkarılır; çünkü döngü yalnızca aralığın adını verdiği sayfaları seçip çıkarır. Maddenin ekte olduğunu zaten biliyorsanız, bunu aralıkta belirtin ve dosyanın geri kalanını atlayın

İçeride arama ve numaralandırma, yineleme sırasında seçili sayfayı değiştirir; bu yüzden her biri girişte çağıranın seçili sayfasını kaydeder ve finally bloğunda geri yükler. SearchText çağrısını sayfa inşa sürecinin ortasında yaparsanız, çağrı döndüğünde seçiminiz bıraktığınız yerdedir. Bu save-and-restore sözleşmesi, eksik olduğunda fark edilen türden bir şeydir; orada olmasının nedeni de tam olarak budur

Tüm sayfayı numaralandırma: metin ve görseller tek listede

Arama, "bu sözcük nerede" sorusunu cevaplar. İç gözlemin diğer yarısı ise "bu sayfada toplamda ne var" sorusudur ve bunun cevabı EnumPageElements yöntemidir. Her öğesi ya metin bloğu ya da gömülü görsel olan tekil bir birleşik liste döndürür; ayırım Kind alanıyla yapılır:

type
  TPDFlibPageElementKind = (ekText, ekImage);

  TPDFlibPageElement = record
    Kind: TPDFlibPageElementKind;
    Page: Integer;
    Left, Top, Right, Bottom: Double;
    Text: WideString;        // ekText
    FontName: WideString;    // ekText
    FontSize: Double;        // ekText
    ImageID: Integer;        // ekImage; usable with SelectImage / GetImageID
  end;

Metin öğeleri aynı ExtractPageTextBlocks geçişinden gelir; bu yüzden her biri dikdörtgeni, font adı ve font boyutu doldurulmuş halde ulaşır. Görsel öğeleri, sayfanın embedded image list'inden FindImages ve GetImageID ile gelir; taşıdıkları ImageID değeri, görseli daha derin incelemek için SelectImage içine vereceğiniz handle'dır. İki tür tek diziye düşer; böylece sayfa üzerinde tek yürüyüşle üzerindeki her şeyi görürsünüz

var
  Pdf: TPDFlib;
  Elems: array[0..511] of TPDFlibPageElement;
  Total, I: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf', '');
    Total := Pdf.EnumPageElements(1, Elems);
    for I := 0 to Total - 1 do
      if I <= High(Elems) then
        if Elems[I].Kind = ekText then
          WriteLn(Format('text  %s/%.1f  "%s"',
            [Elems[I].FontName, Elems[I].FontSize, Elems[I].Text]))
        else
          WriteLn(Format('image id=%d', [Elems[I].ImageID]));
  finally
    Pdf.Free;
  end;
end;

Burada, kütüphanenin geri kalanını izleyen ve saygı göstermediğinizde ilklendirilmemiş bellek okumanıza yol açacak bir sayım kuralı vardır. Dönüş değeri, sizin verdiğiniz diziden daha büyük olabilen toplam öğe sayısıdır. Fonksiyon yalnızca sığan kadarını doldurur ve geri kalanını saymaya devam eder; tıpkı imza numaralandırmasında olduğu gibi. Bu yüzden koruma her zaman aynıdır: döngüyü, dönen sayı ile High(array) değerinin küçüğüyle sınırlandırın; asla körü körüne sayıya kadar gitmeyin. Yukarıdaki örnekler I <= High(...) denetimini bu yüzden gösterir. Dönüş değeri tamponunuzu aşıyorsa, daha büyük dizi ayarlayıp yeniden çağırın

Kütüphanenin daha düşük seviyeli text-block çağrılarını kullandıysanız, bu onların typed ve geometri farkındalıklı katmanıdır; alttaki extraction, PDFlibPas ile Delphi PDF metni, görseli ve font çıkarımı yazısında anlatılan makinenin aynısıdır. Ama hedef "bu metin nerede" değil, "bu belge yardımcı teknoloji için nasıl yapılandırılmış" ise, okuma tarafındaki paralel hikâye etiketli PDF yapı ağacıdır; fiziksel blok yerleşimi yerine mantıksal okuma sırasını açığa çıkarır

Nereye bakacağınızı zaten biliyorsanız bölge sorguları

Bazen hiç arama teriminiz olmaz; elinizde bir dikdörtgen vardır. Form şablonu fatura numarasını hep sağ üst köşeye koyar ya da taranmış düzen bir tablo için sabit bant ayırır. GetTextInAreaEx bu duruma hizmet eder. Bu, GetTextInArea yönteminin sınır taşıyan karşılığıdır: eski çağrı bölge için düz dizge listesi verirken, yeni olan tutulan her bloğun dikdörtgenini metniyle birlikte döndürür; böylece yalnızca kutuda ne olduğunu değil, her satırın kutu içinde nereye düştüğünü de öğrenirsiniz

var
  Pdf: TPDFlib;
  Hits: array[0..63] of TPDFlibSearchHit;
  Found, I: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf', '');
    Pdf.SelectPage(1);
    // Left, Top, Width, Height in PDF points on the selected page.
    Found := Pdf.GetTextInAreaEx(360, 720, 180, 60, Hits);
    for I := 0 to Found - 1 do
      if I <= High(Hits) then
        WriteLn(Hits[I].MatchText);
  finally
    Pdf.Free;
  end;
end;

İki şeyi net tutun. GetTextInAreaEx, o anda seçili sayfa üzerinde çalışır; bu yüzden önce SelectPage çağırın; SearchText aksine aralık almaz. Ve blok, sorgu dikdörtgeniyle tam içinde kalıyorsa değil, onunla kesişiyorsa tutulur; dolayısıyla sınırı taşan satır yine gelir. Elle çizilmiş seçim kutusu için genelde istediğiniz budur; ama katı kapsama gerekiyorsa, artık dikdörtgenler elinizde olduğuna göre dönen sonuçları kendiniz filtreleyebilirsiniz

Bunu işe koşmak

Bu üç çağrıdaki ortak hat şudur: geometri artık sonradan yeniden kurduğunuz bir şey değildir. Arama vuruşu kendi sayfasını ve kutusunu bilir. Sayfa öğesi kendi dikdörtgenini ve metinse fontunu bilir. Bölge sorgusu her satırın nereye düştüğünü bildirir. Bu, kamuya açık API'nin altına inmeden veya text-extraction hattını elle yeniden kurmadan gerçek bir find-and-highlight özelliği, tıkla-bul indeksi ya da yerleşim farkındalıklı çıkarıcı inşa etmeye yeter

Bu sorgu API'leri, üzerine kurulu oldukları tam text-block extraction katmanı ve Delphi ile C++Builder için okuma tarafı introspection yüzeyinin geri kalanıyla birlikte PDFlibPas Delphi PDF Library ürününün parçası olarak gelir