Teknik Makale

Delphi'de PDF İçeriğini Duyarlı HTML'ye Yeniden Akıtma

PDFium Component, BuildReflowDocument'ı kullanarak sabit düzenli bir PDF'yi yeniden akıtılabilen (reflow) anlamsal bir modele dönüştürür ve o modeli ToHtml üzerinden kendi kendine yeten HTML olarak dışa aktarır. Başlıklar başlık olarak kalır, liste öğeleri liste öğesi olarak kalır ve sayfada tespit edilen tablolar, üstbilgi hücreleri ve birleştirmeleri (span) korunmuş gerçek tablo işaretlemesi olarak çıkar. Çıktıda hiçbir şey harici bir betiğe (script) veya stil sayfasına başvurmaz

Bunu istemenin nedeni, bir PDF sayfasının konumlandırılmış gliflerden oluşan bir küme olmasıdır — bu, bir telefon ekranı, bir ekran okuyucu veya bir arama dizini için tam olarak yanlış olan şeydir. Bunu düz metin çıkararak çözmeye yönelik her girişim, belgeyi okunabilir kılan yapıyı kaybeder ve sayfaları görüntüye dönüştürerek çözmeye yönelik her girişim de metni tamamen kaybeder. Bir yeniden akış modeli her ikisini de korur: sözcükleri ve aralarındaki ilişkileri

Anlamsal bilgi nereden gelir?

Her şey, bileşendeki tek metin ve anlambilim kaynağı olan GetStructuredText'ten başlar. PDF bir yapı ağacı taşıdığında — ISO 32000-1'in 14.7 maddesinde tanımlanan etiketli PDF — model, üreticinin kaydettiği mantıksal hiyerarşiyi izler. Taşımadığında, ki gerçek dünyadaki çoğu PDF taşımaz, model, okuma sırası amaçları için zaten hesaplanmış fiziksel yerleşim sırasına geri döner

Bu seçim katı bir sınır korur: mevcut olanın yanıtlayabileceği sorulara yanıt vermek için ikinci bir PDF ayrıştırıcısı veya ikinci bir render motoru getirilmez. Altındaki okuma sırası mekaniği, yapılandırılmış metin blokları ve okuma sırası yazısında anlatılmıştır ve yeniden akış modeli, onun yerini almak yerine üzerine bir anlamsal katmandır

Her düğüm, bilgisinin nereden geldiğini kaydeder, bu yüzden bir tüketici, belgenin bildirdiği bir başlığı, yerleşim sezgisel yöntemlerinin (heuristic) çıkarsadığı bir başlıktan ayırt edebilir. Güvene duyarlı işlem hatları, tüm düğümleri eşit derecede yetkili saymak yerine bu alanı okumalıdır

Düz bir ağaç ve neden bir nesne ağacı değil?

Model, önce-sıra (pre-order) düzleştirilmiş bir ağaçtır: özyinelemeli bir kayıt veya sahiplikli bir nesne grafiği yerine, her düğümün bir ParentIndex ve bir Depth taşıdığı bir düğüm dizisi. Sayfalar, başlıklar, paragraflar, listeler, liste öğeleri, figürler, altyazılar, tablolar, satırlar ve hücrelerin hepsi o tek doğrusal dizide yaşar

İki fayda ortaya çıkar. Tüketiciler diziyi özyineleme olmadan sırayla akıtabilir, bu da HTML, Markdown veya bir ağaç görünümü üretmeyi basit bir döngü haline getirir. Ve düzen, bir ABI sınırı boyunca özyinelemeli yönetilen türleri ele alma biçimleri farklı olan Delphi, C++Builder ve Free Pascal arasında taşınabilir kalır. Dinamik dizilerden oluşan özyinelemeli bir kayıt, tam olarak her yerde derlenen ama her birinde ince bir biçimde farklı davranan türden bir yapıdır

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // satır içi stil bloğu, harici dosya yok
    Options.MaxNodes := 200000;          // kapalıya-hata (fail-closed) bütçe
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

Tablolar iki kez görünmekten nasıl alıkonur?

Tablo tespiti, bir sayfa için yapılandırılmış metin toplandıktan sonra çalışır; bu, belirgin bir tehlike yaratır: aynı hücre içeriği hem metin bloklarında hem de tespit edilen tabloda bulunur. İkisini de yayınlamak, her tablonun kendi içeriğini gevşek paragraflar olarak yeniden takip ettiği bir HTML üretir

Bunu çözen kural geometriktir. Tespit edilen bir tablo, bir metin bloğunun alanının yarısından fazlasını kapladığında, tablo düğümü ona katılmak yerine o bloğun yerini alır. Bir satır içindeki hücre indeksleme, kovalara (bucket) sayarak oluşturulur, bu yüzden modeli oluşturmak, her satır için her hücreyi yeniden taramak yerine hücre artı satır sayısında doğrusal kalır; bu, tek bir sayfanın yüzlerce hücre taşıyabildiği finansal belgelerde önemlidir

Tespit edilen yapı, tespit olduğu konusunda dürüsttür. Çizgili bir tablo, yalnızca boşlukla hizalanmış bir tablodan daha güvenilir bir şekilde tanınır ve düğümün güven değeri bunu yansıtır. Yanlış bir tablonun tablo olmamasından daha iyi olduğu içerikler için tespiti açık tutun; yanlış bir tablonun daha kötü olduğu arşiv dönüşümü için güven değerine göre kapı koyun

Kendi kendine yeten kalan HTML dışa aktarmak

ToHtml, zaten oluşturulmuş modelde gezinir ve hiçbir zaman PDFium'a geri dönmez, bu yüzden iki kez dışa aktarmak ekstra bir maliyete yol açmaz ve aynı modelden farklı bir sonuç üretemez. Metin ve öznitelik değerleri tek tip olarak kaçışlanır, başlık seviyeleri HTML'nin gerçekten tanımladığı h1'den h6'ya kadar olan aralığa sıkıştırılır ve üstbilgi hücreleri, RowSpan ve ColumnSpan yazıldığı gibi geçer

İsteğe bağlı CSS, düz bir satır içi stil bloğudur. Hiçbir betik, hiçbir web yazı tipi ve hiçbir türden harici kaynak yoktur; çıktıyı bir e-postaya, bir yardım görüntüleyicisine veya kum havuzuna alınmış (sandboxed) bir tarayıcı denetimine gömmeyi güvenli kılan şey de budur:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // sayfa sınırlarını görünür tut
  Options.PreserveLineBreaks := False;   // paragrafları tarayıcının sarmasına izin ver

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

PreserveLineBreaks, üzerinde düşünmeye en değer seçenektir. Bir PDF satır sonu, sabit bir sayfa genişliği için verilmiş bir dizgi kararıdır, bu yüzden onu dar bir ekranda korumak, yeniden akışın çözmek için var olduğu sorunun tam da kendisini yeniden üretir. Şiir, kod listeleri ve adresler için satır sonlarını koruyun; düz yazı için bırakın

Bütçeler, iptal ve sayfa durumu

Karakterlerin, düğümlerin, tabloların ve hücrelerin her birinin bir tavanı vardır ve her biri ayırmadan sonra değil önce denetlenir, bu yüzden bozuk veya kötü niyetli bir belge, başka bir şey durana kadar belleği tüketmek yerine temiz bir şekilde başarısız olur. İptal belirteci; sayfa, blok, tablo, satır ve hücre sınırlarında denetlenir, bu da bin sayfalık bir belgenin iptal edilen bir taramasını duyarlı tutar

Özellikle GUI uygulamaları için önemli bir davranış vardır: tüm belge taraması, etkin sayfayı geri yükleyen bir kapsam içinde çalışır, bu yüzden başarı, bütçe hatası ve iptal, çağıranın geçerli sayfasını dokunulmadan bırakır. Kullanıcının 340. sayfaya bakarken dışa aktarmasına izin veren bir görüntüleyici, sonrasında kendini hâlâ 340. sayfada bulur

Yeniden akış ne için iyidir, ne için değildir?

Yeniden akış çıktısı, arama dizinleme, erişilebilir okuma görünümleri, mobil görüntüleme ve içerik geçişi için mükemmel bir girdidir. Sadakati koruyan bir dönüştürücü değildir: mutlak konumlar, tam yazı tipleri, vektör görsel içerik ve kesin sayfa geometrisi, tasarım gereği amacının dışındadır. Bir işin sayfanın aynı görünmesine ihtiyacı olduğunda onu render edin; başka bir yerde okunabilir olmasına ihtiyacı olduğunda onu yeniden akıtın

Özellikle yardımcı teknoloji için, yeniden akış modeli, erişilebilir bir okuyucu oluşturma yazısında anlatılan okuma özellikleriyle eşleşir ve gerçek bir yapı ağacı taşıyan belgeler belirgin şekilde daha iyi modeller üretir; bu, PDF/UA yapı ağacı doğrulaması yazısında anlatıldığı gibi etiketlemeyi kaynağında doğrulamak için iyi bir gerekçedir

Yeniden akış, yapılandırılmış metin, etiketleme doğrulaması ve render etme, Delphi, C++Builder ve Lazarus genelinde tek bir belge nesnesini paylaşır; tam API Delphi için PDFium Component sayfasında anlatılmıştır