Teknik Makale

HotPDF Delphi ile HTML-to-PDF'te CSS Multi-Column Layout

HotPDF Component, THPDFHTMLImporter.RenderHTML5 HTML'i Delphi'de PDF'e dönüştürürken CSS multi-column metnini dizer. Sayfalı HTML motoru, column-count ile column-width'i içerik kutusuna göre çözer, her multicol kapsayıcının son parçasını ikili aramayla dengeler, önceki sayfaları sütun sütun sayfa dibine kadar doldurur ve column-fill, column-span: all ile column-rule destekler

Bunu istemenin olağan nedeni sıkıcı ama gerçektir: iki sütunlu bir ürün bülteni, üç dar sütuna dizilmiş bir hükümler ve koşullar eki ya da pazarlama ekibinin HTML'de tasarladığı basılı bültene benzemesi gereken bir sürüm notları PDF'i. Sütun desteği olmadan içe aktarıcı her paragrafı tek geniş satır ölçüsünde yığar, satır uzunluğu ikiye katlanır ve belge bir günlük dosyası gibi okunur. Desteğiyle, tarayıcı ön izlemesini süren aynı stil sayfası PDF'i de sürer ve zor kısım sayfalara bölmeye dönüşür; tarayıcıların ekranda çoğunlukla hiç düşünmek zorunda kalmadığı kısım

HotPDF kaç sütun kullanacağına nasıl karar verir?

HotPDF sütun sayısını, CSS Multi-column Layout Module Level 1 §3.4'teki sözde-algoritmayla aynı kurallarla çözer; dolgu ve kenarlıklar çıkarıldıktan sonraki içerik kutusu genişliği W'ye uygulanır. Bildirilmiş bir column-count olduğu gibi alınır. Yalnızca column-width verildiğinde motor, en az bir sütun olacak biçimde N = Trunc((W + gap) / (column-width + gap)) hesaplar. İkisi de mevcutken sonuç ikisinin küçüğüdür; bildirilen sayı türetilen sayıyı tavanlayabilir ama hiç şişiremez

Somut bir örnek: 483pt içerik kutusu (56pt yan kenar boşluklu A4, yuvarlanmış) ile column-width: 140pt; column-gap: 12pt, Trunc(495 / 152) = 3 sütun verir; her biri (483 - 2 * 12) / 3 = 153pt genişliğindedir. Aynı kurala column-count: 2 eklemek onu iki sütuna düşürür, çünkü küçük olan kazanır. normal boşluk anahtar sözcüğü, kapsayıcı yazı tipi boyutunun 1em'ine çözülür ve bu çözümleme ayrıştırma zamanında değil düzen zamanında olur; aynı kuralda column-gap'ten sonra bildirilmiş bir font-size bile doğru boşluğu üretir

Delphi'de HTML'den PDF'e HotPDF CSS multi-column sayı çözümlemesi: column-count bildirildiği gibi kullanılır; column-width, N = Trunc((W + gap) / (column-width + gap)) türetir; ikisi de ayarlıyken küçük olan kazanır ve 12pt altındaki her sütun tek sütuna çöker
Motor CSS sözde-algoritmasını içerik kutusuna uygular, türetilen sayıyı bildirilenle tavanlar ve 12pt taban koruması ekler; güvenilmeyen şablonlar böylece binlerce mikroskobik sütun üretemez

Korumalardan biri spesifikasyonda yoktur ve orası bilerek ayarlanmıştır. Sütun genişliği hesaplandıktan sonra 12pt altındaki her değer kapsayıcıyı tek sütuna geri çöker. column-count: 9999 diyen bir stil sayfası — kazayla olsun güvenilmeyen bir şablondan gelmiş olsun — aksi hâlde negatif ya da mikroskobik genişlikli binlerce sütun ve her satıra bir glif sığdırmaya çalışan bir satır kırıcı üretirdi. Tek sütuna düşmek çıktıyı okunur tutar ve düzen maliyetini sınırlı tutar. Sıfır ya da negatif sayılar hiç oraya varamaz: doğrulayıcı column-count için yalnızca auto ya da pozitif bir tamsayı kabul eder; column-width ile column-gap sırasıyla auto ya da normal, ya da nokta ya da em cinsinden bir uzunluk alır

Delphi'de çok sütunlu bir HTML bölümünü PDF'e render etmek

Çok sütunlu çıktı, içe aktarıcıda bir anahtar gerektirmez. THPDFHTMLImporter, içine çizdiği THotPDF belgesiyle kurulur; BeginDoc çoktan çalışmış olmalıdır ve sütun özellikleri normal kaskadla gelir — ya işaretlemeye ya da RenderHTML5'in ikinci argümanı olarak verilen yazar stil sayfasına yazılmıştır

uses
  SysUtils, HPDFDoc, HPDFHTMLImport;

procedure BuildBulletin(const FileName: string);
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;
    Importer.Margin := 56;
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;

    Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
           '  column-rule: 0.5pt solid #999999 } ' +
           'section.news h2 { column-span: all }';

    Html := '<section class="news">' +
            '<p>Release 4.2 ships the new export pipeline...</p>' +
            '<p>Licensing changes take effect on 1 March...</p>' +
            '<h2>Known issues</h2>' +
            '<p>Import of legacy archives &amp; templates...</p>' +
            '</section>';

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('Nothing was emitted');
    Pdf.EndDoc;
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

Son paragraftaki çift kaçışa dikkat: Pascal dizgesi içindeki &amp;, ampersandın HTML varlığıdır ve içe aktarıcı onu tek karaktere geri çözer. Kaskadın geri kalanı, bütçeler ve kaynak callback'leri, HotPDF HTML5 sayfalı medya içe aktarımına genel bakışta anlatıldığı gibi çalışır; sütunlar aynı profildeki bir düzen modundan ibarettir

Sütunları ikili aramayla dengelemek

HotPDF sütunları, açgözlü bir doldurmanın kalan her satırı hâlâ yerleştirdiği en küçük sütun yüksekliğini arayarak dengeler. Açgözlü geçiş basittir: satırları, sıradaki satır aday yüksekliği aşacak kadar ilerleyinceye dek birinci sütuna yürütür, sonra ikinci sütunda sürdürür ve böyle gider. Arama alt sınır olarak en uzun tek satırla, üst sınır olarak bütün satır yüksekliklerinin toplamıyla başlar, her turda aralığı ikiye böler ve sınırlar 0.05pt içine girdiğinde durur

Delphi'de HotPDF multi-column dengeleme: en uzun tek satır ile bütün satır yüksekliklerinin toplamı arasındaki ikili arama, açgözlü doldurmanın her satırı yerleştirdiği en küçük sütun yüksekliğini bulur; sınırlar 0.05pt içinde yakınsar
Her-satır-sığar yüklemi sütun yüksekliğinde monotoniktir; aralığı ikiye bölmek en kısa dengeli banda yakınsar ve son açgözlü doldurma, öksüz bırakmadan her satırı tüketir

İkili arama yalnızca yüklem monotonken çalışır ve burada öyledir: daha uzun bir sütun, daha kısa olandan daha az satır tutamaz; yükseklik büyürken "her satır sığar" yüklemi false'tan true'ya tam bir kez döner. Bu özellik yakınsamayı garanti eder ve bulunan yükseklikteki son açgözlü doldurmanın, uydurma bir ek sayfa tetikleyecek öksüz bir satır bırakmak yerine her satırı tükettiğini garanti eder. Sonucun bir ayrıntısını da açıklar: dengeli bant, satır kutularının izin verdiği kadar kısadır; sütunlar eşit olmak zorunda değildir, çünkü satırlar bölünemez ve son sütun ötekilerden bir iki satır yukarıda bitebilir

Sütunlar bir sayfa sonunu aştığında ne olur?

Geçerli sayfaya sığmayan sütunlar sırayla sayfa dibine kadar doldurulur ve yalnızca son sayfa parçası dengelenir. Bu, CSS Multi-column Layout Level 1 §7.1'in parçalanmış bağlamlarda column-fill: balance için öngördüğü davranıştır ve basılı bir bültenin görünümü de budur: son sayfa dışında her sayfada tam sütunlar; son sayfada kalan metin, boş bir sütunun yanında tek uzun sütun bırakmak yerine eşit bölünür

Her sayfa parçası aynı parçalayıcıdan bağımsız geçer. Kalan satırlar mevcut yüksekliğin N sütununa sığıyorsa parça dengelenir; sığmıyorsa her sütun sayfa dibine kadar dolar ve döngü yeni bir sayfa açar. Motorun break-inside: avoid kararları ve kapsam ölçümü için kullandığı bütün kapsayıcının doğal yüksekliği, sınırsız kapasiteli dengeli bant yüksekliğidir; böylece taze bir sayfada dengeli bir blok olarak sığacak kapsayıcı dilimlenmek yerine bütün hâliyle oraya taşınır. Tek bir satır bütün bir sayfadan uzun olduğunda motor kısıtı gevşetir, satırı yine de bir sayfanın en üstüne yerleştirir ve multi-column özneli bir hdkPaginationConstraint teşhisi raporlar

Delphi'de HTML'den PDF'e HotPDF multi-column sayfalama: son sayfadan önceki parçalar her sütunu sayfa dibine kadar doldurur; son parça sütunlar boyunca dengelenir ve column-span all, bir kapsayıcıyı tam genişlikli bir öğenin çevresinde ayrı ayrı dengelenen bantlara böler
Bir multicol kapsayıcının yalnızca son parçası dengelenir; önceki sayfalar sırayla dolar ve her parça dengeyi, çizgileri ve sayfalamayı bağımsız yineler

column-fill: auto ve sabit yükseklikli kutular

column-fill: auto ile kesin bir height birlikteyken HotPDF sütunları sırayla doldurur ve her sütunun yüksekliği, kalan sayfa alanı ile kapsayıcı yüksekliğinin dolgusu ve kenarlıkları çıkarılmış hâlinin küçüğüdür. Bu minimum göründüğünden daha önemlidir. Yalnızca kalan sayfa alanını kullanmak, sayfada kutudan fazla yer kaldığında birinci sütunun her satırı yutmasına izin verirdi. Kesin bir yükseklik yokken column-fill: auto, tarayıcılar gibi dengelemeye döner; çünkü kısıtsız bir multicol kapsayıcının doldurulacak bir sütun yüksekliği yoktur. İlk uygulama o yedeği kaçırmıştı ve 27 regresyon kontrolünden biri yakalamıştı

column-span: all ve iç içe paragraflar

HotPDF, CSS Multi-column Layout Level 1 §6.1'i izleyerek column-span: all'u çok sütunlu bir kapsayıcının doğrudan çocuklarında destekler. Yayılan bir çocuk kapsayıcıyı parçalara böler: önündeki çocuklar bir sütun bandı kurar, yayılan öğe tam genişlik boyunca render edilir ve arkasındaki çocuklar yeni bir bant başlatır. Her bant kendi içinde dengelenir, doldurulur, çizgilenir ve sayfalanır; üç sütunlu bir bölümün ortasındaki başlık böylece aralarında tam genişlikli bir başlık bulunan iki dengeli bant üretir — insanların taslaklarda gerçekten çizdiği düzen

Oraya varmak önce daha az görünür bir sorunu düzeltmeyi gerektirdi. <div style="column-count:2"> gibi, birkaç <p> çocuğu olan bir kapsayıcı, her paragrafı tek bir üst üste sütunda render ediyordu; çünkü her paragraf kendi metin bloğu olarak diziliyor ve ebevendeki sütun bildirimi hiç uygulanmıyordu. HotPDF artık iç içe blok çocukların metnini kapsayıcı için tek bir akışta topluyor. Toplayıcı bunu yaparken her blok sınırında katı bir satır sonu eklemek zorunda; atlanırsa bir paragrafın son sözcüğüyle sonrakinin ilk sözcüğü aynı satırda birleşir — test metni tesadüfen noktalama ile bitiyorsa kolayca gözden kaçan bir hata

Birleşik akışın bilinçli bir kapsamı vardır; yalnızca bütün alt ağaç düz metin akışıyken kullanılır. Form kontrolleri, görseller, tablolar, listeler ya da herhangi bir yerinde <hr> barındıran bir kapsayıcı sıradan blok yolunu korur ve tek sütunda render edilir; birleşik metin toplayıcısı onların geometrisini ifade edemez. Tabloların özellikle kendi sayfalama kuralları vardır — HotPDF'te rowspan yönetimi ve yinelenen tablo başlıkları makalesinde işlenir — ve onları bir sütun bandına karıştırmak o garantileri bozar

Sütun çizgisi nereye gelir ve nasıl test edilir

HotPDF column-rule'u, CSS Multi-column Layout Level 1 §4'ün anlattığı gibi her boşluğun orta çizgisine — bir sütunun sağ kenarı artı boşluğun yarısı — her sayfa parçasında bandın tepesinden dibine çizer. Genişlik 1pt'ye varsayılandır. Renk, bildirilmemişse ya da currentColor'sa öğenin metin rengine varsayılandır. Stil anahtar sözcüğü olmayan column-rule kısayolu solid sayılır; border kısayolunun yaygın okunuşuyla uyumludur, none ile hidden ise çizgiyi bastırır

Sütun geometrisi, PDF'i ayrıştırmadan gözlenebilir. THPDFHTMLImporter'daki OnLineLayout olayı çizilen her satır için bir THPDFHTMLLineLayout kaydıyla bir kez tetiklenir ve motor her sütunu ayrı çizdiği için her satırın X alanı sütununun sol kenarıdır. Komşu sütunlar tam olarak sütun genişliği artı boşluk kadar farklıdır; regresyon testinde 12pt boşluğu 20pt boşluktan ayırmayı kolaylaştıran da budur

type
  TColumnProbe = class
  public
    Lines: TStringList;
    constructor Create;
    destructor Destroy; override;
    procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
  end;

constructor TColumnProbe.Create;
begin
  inherited Create;
  Lines := TStringList.Create;
end;

destructor TColumnProbe.Destroy;
begin
  Lines.Free;
  inherited;
end;

procedure TColumnProbe.LineDrawn(Sender: TObject;
  const Line: THPDFHTMLLineLayout);
begin
  // PageIndex sıfır tabanlıdır; X, satırın sütununun sol kenarıdır
  Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
    [Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;

// Bağlantı, RenderHTML5'ten önce:
//   Probe := TColumnProbe.Create;
//   Importer.OnLineLayout := Probe.LineDrawn;

Bu olaya karşı test yazmaktan iki pratik not. PageIndex sıfır tabanlıdır; elle yazılan bir assertion'ın ilk yanlış aldığı şey budur. X hizalama kaymalarını da içerir; ortalanmış ya da sağa hizalı metin için sütun konumlarını sola hizalı test verilerinde karşılaştırın. Çizginin kendisi olayda görünmez; konumunu öne sürmek için sıkıştırılmamış içerik akışında dikey m ile l operatörlerini tarayın

Bilmeye değer sınırlar ve tuzaklar

Birkaç davranış yaklaşımdır ve bunları düzen incelemesinden sonra değil önce bilmek iyidir. Birleşik sütun akışının içinde paragraf aralığı, her blok sınırında kapsayıcı satır yüksekliğinin bir satırıyla yaklaşıklandırılır; sütun bandı içindeki paragraf başına kenar boşlukları tam olarak yeniden üretilmez. column-span: all bir kapsayıcıyı böldüğünde her parça kapsayıcı dolgusunu ve arka plan kutusunu yineler; parçalar görsel olarak süreklidir ama katı CSS anlamında tek kutu değildir. none ile hidden dışındaki her column-rule-style değeri doğrulamayı geçer ama çizgi solid olarak konturlanır

Desteklenmeyen değerler sessizce başarısız olmaz. StrictCSS'i True yapmak her teşhisi bir EHPDFHTMLUnsupportedCSS istisnasına çevirir; OnDiagnostic ise toplu işin onları günlüğe yazıp sürdürmesine izin verir. Katı modun sayfalama teşhisine de uygulandığına dikkat edin; bir sayfadan uzun satırı olan belge, kısıtı sessizce gevşetmek yerine StrictCSS altında istisna fırlatır

procedure TReportJob.HtmlDiagnostic(Sender: TObject;
  const Diagnostic: THPDFHTMLDiagnostic);
begin
  case Diagnostic.Kind of
    hdkUnsupportedProperty, hdkUnsupportedValue:
      FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
    hdkPaginationConstraint:
      FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
        Diagnostic.MessageText);
  else
    FLog.Add(Diagnostic.MessageText);
  end;
end;

// Şablon yazımı: motorun yok sayacağı her şeyde hızlı başarısız ol
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;

Katı mod, şablon yazılırken doğru ayardır; column-span: both gibi bir yazım hatası, başlığın sütunların içinde sessizce akmasına izin vermek yerine anında reddedilir. Üretimde OnDiagnostic üzerinden günlükleme genellikle daha iyi takastır; müşteri kaynaklı HTML'deki tek tuhaf bildirim bütün belgeye mal olmamalı. Özdeş teşhisler (aynı tür, özne ve değer) MaxDiagnostics listesine sığdıkları sürece bir kez bildirilir; liste 256 girdiyle varsayılandır, böylece kaskadın yüzlerce öğe için ziyaret ettiği bir bildirim günlüğü boğmaz

SSS

HotPDF, columns kısayolunu destekler mi?

Evet. columns kısayolu, herhangi bir sırada en fazla bir pozitif tamsayı sayı ile bir sütun uzunluğu kabul eder ve auto her iki longhand'ı sıfırlar. Çözülen değerler, tekil özelliklerle aynı sayı ve genişlik kurallarını izler

Çok sütunlu bir kapsayıcı görsel ya da tablo tutabilir mi?

Tutabilir ama HotPDF o zaman o kapsayıcıyı tek sütunda render eder. Sütun akışı yalnızca tüm torunlar düz metin akışıyken uygulanır; çünkü görsellerin, tabloların, listelerin ve form kontrollerinin kendi kutu geometrisi ile sayfalama kuralları gerekir

Son sayfa neden dengeli sütunlar gösterirken önceki sayfalar tamdır?

Bu, sayfalı medyada column-fill: balance için CSS kuralıdır: yalnızca son parça dengelenir. Önceki sayfalar her sütunu sayfa dibine kadar doldurur; belgenin ortasında yer boşa gitmez

Multi-column düzen, HTML5 sayfalı medya profili ve HTML-to-PDF hattının geri kalanı Delphi ve C++Builder için HotPDF Component'in parçasıdır; bileşen doğrudan PDF çizimini, formları, şifrelemeyi ve metin çıkarmayı da kapsar