Teknik Makale

Delphi'de Erişilebilir PDF için Otomatik Yapı Etiketleme

Üretilmiş bir raporu bir ekran okuyucuyla açın; alışılmış başarısızlıklar bir dakika içinde ortaya çıkar: her sayfada "Sayfa 3 / 40" sesli okunur, şirket logosu arkasından hiçbir şey gelmeyen "grafik" olarak duyurulur ve bir tablo, sütun adları olmayan uzun bir sayı dizisi olarak çıkar. PDF Library for Delphi üçünü de otomatik yapı etiketlemeyle ortadan kaldırır: SetAutoTagMode'u bir kez çağırın ve sıradan çizim çağrıları, belge çizilirken paragrafları, başlıkları, şekilleri, tabloları ve okuyucunun atlayacağı sayfa gereçlerini üretsin

Elle etiketleme yolu hâlâ duruyor. Her çizim çağrısını BeginTag ile EndTag içine sarabilirsiniz ve olağandışı yapılı belgeler için doğru araç da bu. Sıradan bir rapor, fatura veya ekstre içinse elle etiketleme, erişilebilirliği ürünün ömrü boyunca bir şey çizen her kod yolunda kimsenin bir çifti unutmamasına bağlar. Bir uyumluluk gereksinimini buna oynamayız

SetAutoTagMode neyi açar

SetAutoTagMode bir bit maskesi alır ve çağrıdan önce yürürlükte olan modu döndürür. Her bit bir içerik türünü kapsar:

  • AUTOTAG_TEXT (1): metin paragraf olur, kaydedilmiş bir başlık varsa başlık olur
  • AUTOTAG_FURNITURE (2): sabit üst ve alt başlıklar ile sayfa numaraları, okuyucunun atladığı yapı öğelerine dönüşür
  • AUTOTAG_FIGURE (4): çizilen bir görsel şekil olur; dekoratif olarak bildirdiyseniz yapı öğesi olur
  • AUTOTAG_TABLE (8): çizilen tablolar satırlarını ve hücrelerini yapı ağacına taşır
  • AUTOTAG_DEFAULT (15): dördü birden; yeni her rapor kodu için önerdiğimiz değer

Modu açmak aynı zamanda belgeyi etiketli olarak işaretler ve bu adım göründüğünden daha önemlidir. ISO 32000-1 §14.7.1'e göre bir okuyucu, kataloğun /MarkInfo sözlüğü /Marked true demedikçe dosyayı etiketsiz sayar. Bir dosya tam ve doğru bir yapı ağacı taşıyıp yine de hiç yapısı yokmuş gibi duyurulabilir; çünkü yardımcı teknolojiye ona bakmasını söyleyen tek bayrak eksiktir. Ağaç oradadır. Onu okuyan hiçbir şey yoktur

Metin etiketleme çağrı başına çalışır. Her DrawText, DrawWrappedText ya da DrawTextBox çağrısı tek bir öğe olur; böylece beş DrawText çağrısıyla satır satır çizilen bir paragraf, okuyucuya beş tek satırlık paragraf olarak ulaşır. Yerleşim kodunuz satırları zaten kendisi bölüyorsa bunu değiştirmeye değer: bütün paragrafı DrawWrappedText'e verin ve tek çağrının tek paragraf üretmesini sağlayın

SetAutoTagMode bit maskelerinin düz Delphi çizim çağrılarını etiketli bir PDF yapı ağacına eşlediği, Marked bayrağının gerekli olarak vurgulandığı şema
SetAutoTagMode'un her etkin biti, sıradan çizim çağrılarını yapı ağacındaki eşleşen bir öğeye yönlendirir
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // sol üst köken, y aşağı doğru büyür
    Lib.SetDocumentLanguage('en-US');
    Lib.SetAutoTagMode(AUTOTAG_DEFAULT);       // herhangi bir RegisterHeading çağrısından önce
    Lib.AddStandardFont(4);

    Lib.SetTextSize(18);
    Lib.RegisterHeading(1, 'Annual service report');
    Lib.DrawText(72, 96, 'Annual service report');        // H1 olarak etiketlenir

    Lib.SetTextSize(11);
    Lib.DrawWrappedText(72, 130, 450,
      'Every unit installed before 2024 was inspected. ' +
      'Units that failed the torque check were serviced on site.');  // tek P

    // ... kalan sayfalar ...

    // Var olan her sayfaya uygulanır; sayfalar çizildikten sonra çağırın
    Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1);  // Pagination yapı öğesi
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

Üçüncü satırdaki yorum boşa değil. RegisterHeading bir başlık düzeyini yalnızca metin etiketleme hâlâ açıkken kuyruğa alır ve SetAutoTagMode çalıştığında kuyruktaki düzeyi temizler. Önce başlığı kaydedip modu sonra açarsanız başlık, hiçbir yerde sizi uyaran bir hata olmadan sıradan bir paragraf olarak çıkar

Başlık, kendinden önce çizilen görseli neden bekler?

RegisterHeading düzeyi sıradaki metin parçasına adlandırır ve onu yalnızca metin tüketebilir. Araya bir görsel çizilirse görsel şekil olur ve başlık, ardından gelecek metin için kuyrukta kalır. Bariz uygulama, kuyruktaki düzeyi herhangi bir etiketin tüketmesine izin verir; o zaman bölüm başlığını kaydetme, bir banner görseli çizme ve başlığı çizme sıralaması, H2 olarak etiketlenmiş bir banner ve paragrafa düşürülmüş gerçek bir başlık üretir. Başlıklara göre gezinen bir okuyucu bir resme konar

Kuyruğun çalışma biçiminden iki şey daha çıkar. Başlık düzeyi, RegisterHeading'e hangi dizeyi geçirdiğinizden bağımsız olarak sıradaki hangi metin gelirse ona yapışır: yerleşim kodunuz başlığın üstünde küçük bir "Bölüm 3" vurgusu çiziyorsa başlık o vurgu olur. Altıdan derin düzeyler H6 olarak yazılır; çünkü okuyucu ötesini bilmez. Title bağımsız değişkeni kendi işini yapmaya devam eder: DrawTOC'nin sonra içindekiler tablosuna çevireceği ana hattı besler

Görseller aynı "tek öğede harcanır" muamelesini görür. RegisterFigure sıradaki görselin taşıyacağı açıklamayı sağlar; RegisterDecoration ise sıradaki görseli anlamı olmayan bir çizgi, kenarlık ya da arka plan olarak bildirir ve bu, Layout yapı öğesi olarak yazılır (ISO 32000-1 §14.8.2.2). Her bildirim tam olarak tek bir görsel tarafından tüketilir; böylece sonraki bir görsel, önceki bir görsel için yazılmış açıklamayı asla miras almaz. Yanlış resme yapışan alt metin, hiç olmamasından daha kötüdür; çünkü okuyucu yanlış bir şeyi tam bir kendinden eminlikle söyler

Hangi görsellerin dekoratif olduğunu çıkarsamaya bilerek girişmedik. Bir görsel ya okuyucunun tanımlaması gereken içeriktir ya atlaması gereken; hangisi olduğunu yalnızca çağıran kod bilir. Kaçınılacak durum üçüncüsüdür: hiçbir bildirim olmadan çizilen bir görsel, açıklaması olmayan bir şekil olur; bu PDF/UA'yı geçmez ve ekran okuyucuyu "grafik" deyip ardından susacak şekilde durdurur. Rapor kodunuzda DrawImage için grep çalıştırırsanız her eşleşmenin hemen üstünde bir RegisterFigure ya da RegisterDecoration olmalı

PDF Library for Delphi şeması: kayıtlı bir PDF başlığının araya giren bir görsel boyunca beklediğini, RegisterFigure ve RegisterDecoration ise her birinin tam olarak tek bir görselde harcandığını gösterir
RegisterHeading, bu arada başka ne çizilirse çizilsin sıradaki metni bekler
Lib.RegisterHeading(2, 'Gearbox inspection');   // sıradaki metin için kuyrukta

Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0);       // görseli yükler ve seçer
Lib.DrawImage(72, 160, 360, 270);             // etiketli bir Figure olur

Lib.RegisterDecoration;                       // anlamsız çizgi
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4);               // bir layout yapı öğesinin içinde çizilir

Lib.DrawText(72, 480, 'Gearbox inspection');  // H2 ancak şimdi belirir

Açıklama, belgedeki başka herhangi bir dizeden fazla özen hak eder. Görme engelli bir okuyucu resmin yerine açıklamayı alır ve aldığı şeyin tamamı budur. "Grafik" ona hiçbir şey söylemez; "Bölgelere göre üç aylık gelir, üçüncü çeyrekte doğu bölgesi en yüksek" ise gören bir okuyucunun resimden iki saniyede aldığını söyler

Tablolar: başlık satırları bildirilir, asla tahmin edilmez

Tablo biti açıkken DrawTableRows tabloyu, satırlarını ve hücrelerini yapı ağacına yazar; böylece okuyucu, ızgarayı birbiriyle ilgisiz metinler olarak okumak yerine bir değerin hangi sütunda durduğunu söyleyebilir. SetTableHeaderRowCount baştaki kaç satırın başlık olduğunu söyler. O satırlar sütun kapsamı taşıyan TH hücreleri olarak yazılır; okuyucunun, kullanıcının üzerine geldiği değerden önce "Tork" duyurabilmesini sağlayan da budur

Başlık satırı sayısı varsayılan olarak sıfırdır ve bu bir tercihti. Bir tablonun ilk satırı çoğu zaman gerçekten başlıktır; tahmin etmek çoğu zaman doğru olurdu. Yanlış tahminin maliyeti ise tek taraflıdır: başlık sanılan bir veri satırı, sütunundaki her değerin önünde sesli okunur; bu da hiç başlığı olmayan bir tablodan çok daha kafa karıştırıcıdır. Sizden tek satır kod, o hata modundan daha ucuzdur

Başlık satırlarını bildirmek ile onları her sayfada yinelemek ayrı kararlardır. SetTableHeaderRowCount anlamsaldır ve sayfada hiçbir şeyi değiştirmez. Yineleme yerleşimdir ve DrawTaggedTableRows'un RepeatHeaderRows bağımsız değişkenine aittir. İkisi tek ayar olsaydı doğru başlık etiketleri istemek, hiç istemediğiniz yinelenen başlık bantlarını çizmeye de başlardı ve sayfa sayfa denetlenmiş bir yerleşim kayardı. RepeatHeaderRows olarak geçilen satırlar da başlık olarak etiketlenir; aynı şeyi iki kez söylemek zorunda kalmazsınız

PDF Library for Delphi şeması: tablo başlık hücreleri yapı ağacında sütun kapsamı taşırken RepeatHeaderRows, sayfa sonunda tekrarlamayı yalnızca düzeye bağlı bir karar olarak tutar
Başlık satırları yapı ağacında bir kez bulunur ve her TH hücresine sütun kapsamı eklenir
var
  TableID: Integer;
begin
  TableID := Lib.CreateTable(40, 3);
  Lib.SetTableHeaderRowCount(TableID, 1);       // satır 1 başlık bandı
  Lib.SetTableCellContent(TableID, 1, 1, 'Part');
  Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
  Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
  // ... veri satırlarını doldur ...
  // 1..40 satırlarını 600pt'lik banda çiz, her sayfada bir başlık satırını tekrarla
  Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;

Otomatik bir belgenin içinde elle etiketlenmiş bölümler

Otomatik etiketleme, elle açtığınız bir etiket hâlâ açıkken kenara çekilir. Bu, kodunuzun bir belgenin bir bölümünü kendisinin tanımlayıp geri kalanını kütüphaneye bırakmasını, ikisinin birbirinin içine yerleşmeden sağlar; çoğu gerçek belgenin istediği düzen de budur. Kapak sayfası ile imza bloğu yalnızca sizin anladığınız bir yapı taşır; aralarındaki iki yüz sayfalık gövde metni taşımaz

Hangi karışımı kullanırsanız kullanın üç kural çıktıyı geçerli tutar. Bir yapı öğesinin içinde hiçbir şey etiketlenmez; çünkü bir yapı öğesinin marked-content dizisi aynı zamanda yapı öğesi taşıyamaz ve bunu deneyen dosya PDF/UA doğrulamasını geçemez. Boş metin hiçbir öğe açmaz; böylece boş dizeyle yapılan başıboş bir DrawText, okuyucunun boş olarak duyuracağı bir öğe bırakamaz. Ve içeride başka çizim çağrılarıyla çizen bir kütüphane çağrısı dış düzeyde tek bir etiket açar; parçaları asla kendilerine ait ikinci bir öğenin içine gömülüp kalmaz

Kaydettikten sonra, güvenmeden önce ağacı geri okuyun. Okuma API'si 1 tabanlıdır ve bu bizi de yakaladı: ilk denetim döngümüz 0'dan StructTreeItemCount - 1'e kadar döndü, başta boş bir kayıt aldı, son öğeye hiç ulaşmadı ve bir süre bir paragrafın kayıp olduğuna ikna olduk. Hiçbir şey kayıp değildi. Döngü iki uçta da bir kaymıştı, hepsi bu

var
  I: Integer;
begin
  // Elle etiketlenmiş bir alıntı; otomatik etiketleme buna karışmaz
  Lib.BeginTag('BlockQuote', '', '');
  Lib.DrawWrappedText(72, 520, 450,
    'All readings were taken with the unit at operating temperature.');
  Lib.EndTag;
  Lib.SaveToFile('report.pdf');

  // Ağacı geri okuyun: indeksler 1'den StructTreeItemCount'a kadar gider
  if Lib.LoadFromFile('report.pdf', '') = 1 then
    for I := 1 to Lib.StructTreeItemCount do
      Writeln(Lib.GetStructTreeItemText(I, 2), '  ',    // çözümlenmiş tür
              Lib.GetStructTreeItemText(I, 4));         // varsa /Alt
end;

Bu tür bir döküm hızlı bir sağlamlık sınamasıdır, karar değil. Görmek istediğiniz şey, ilk paragrafın önünde bir H1, her şeklin açıklamasını taşıması ve boş /Alt'lı bir şekil olmamasıdır

Çizim sırası ile okuma sırasının ayrıştığı yer

Otomatik etiketleme öğeleri, içeriğin çizildiği sırayla kaydeder. Yerleşim kodunuz gövdeden önce kenar çubuğunu çiziyorsa ağacın tuttuğu sıra odur ve ekran okuyucunun okuduğu sıra da odur. Görsel sıra ile okuma sırasının gerçekten ayrıştığı ya da başka yapı türlerine ya da öğe başına dile ihtiyaç duyan belgeler için elle etiketleme API'si daha uygundur; etiketli PDF ve erişilebilirlik yapısı gezinmesi rolleri, kapsamları ve başlık bağlarını ayrıntılı olarak ele alır

Kendinizin çizmediği sayfalar için, işaretli içeriği olmayan içe aktarılmış sayfalar gibi, AutoTagPages sonradan çalışır: kendisine verilen sayfalardaki medyan metin boyutunu gövde metni sayar, belirgin biçimde büyük dizileri başlık sayar ve diğer boyamaları yapı öğesi olarak işaretler. Yeniden üretemeyeceğiniz içerik için doğru araç budur. Kendi kodunuzun çizdiği her şey için çizim anında etiketlemeyi tercih ederiz; çünkü orada kütüphane, başlığın ne olduğunu font boyutundan tahmin etmek yerine açıkça öğrenir

Belge tamamlandığında varsaymak yerine bir denetleyiciden geçirin: PDF/A ve PDF/UA preflight notları ürettiğiniz yapı hakkında nasıl karar alacağınızı gösterir; veri kümesi tabanlı rapor dışa aktarımı yazısı ise bu çağrıların, yerleşimini veriden üreten bir rapor motorunda nerede durduğunu gösterir

PDF Library for Delphi, Delphi, C++Builder ve Lazarus için doğal Pascal ile yazılmıştır ve dağıtılacak harici bir PDF çalışma zamanı yoktur; dolayısıyla etiketli çıktı, sayfayı çizen kodun kendisinden gelir. Eksiksiz API başvurusu ve platform listesi PDF Library for Delphi ürün sayfasındadır