Teknik Makale

Delphi'de PDFium ile PDF/UA Yapı Ağacı Doğrulaması

Preflight aracınız dosyayı PDF/UA açısından temiz raporluyor. veraPDF aynı dosyayı açıyor ve 7.3 maddesi altında alternate text olmayan bir Figure işaretliyor. İki araç da haklı; aradaki boşluk, erişilebilirliği bayt tarayarak denetlemenin bütün problemidir. Bayt düzeyindeki geçiş, dosyanın söylediğini doğrular: /StructTreeRoot değerini, /MarkInfo /Marked true değerini, XMP paketi içindeki pdfuaid:part işaretini, belge başlığını ve dili bulur. Bunlar biçim işaretleridir ve gereklidir. Ama dördüncü sayfadaki gerçek figürün, ekran okuyucunun yüksek sesle okuyabileceği bir açıklama taşıyıp taşımadığını size söylemezler. Bu cevap etiket ağacında yaşar ve onu almak için ağacı yürümek gerekir

PDFium Component, Delphi ve C++Builder için yerel bir VCL PDF kütüphanesidir ve içindeki ValidatePdfUa her iki geçişi de yapar. Bayt düzeyindeki geçiş biçim işaretlerini ele alır. Bunun üstünde, canlı etiket ağacını yükleyen, her öğeyi gezen ve eksik özniteliğin stil tercihi değil gerçek erişilebilirlik kusuru anlamına geldiği yüksek güvenli küçük içerik kuralları kümesini denetleyen yapı ağacı geçişi oturur. Bu makale işte o ikinci geçiş üzerinedir: neyi denetlediği, neden kural mantığının altında DLL bulunmayan saf bir fonksiyon olduğu ve nerede bilerek kısa kesildiği

Bayt taraması eksik Alt'ı neden göremez

ISO 14289-1 (PDF/UA-1), ISO 32000 üzerine ek gereksinimler katmanıdır. Bu gereksinimlerin bir kısmı yapısaldır ve ham dosyada görülebilir: catalog bir structure tree bildirmelidir, viewer preferences içinde DisplayDocTitle ayarlanmış olmalıdır, fontlar gömülü olmalıdır. Stream gövdelerini temizleyip ad token'larını ayraç sınırlarıyla eşleyen bir token tarayıcısı bunların hepsini doğrulayabilir ve PDFium'un ValidatePdfUaCompliance yordamı 7.1, 7.18 ve 7.21 gibi maddeler için tam olarak bunu yapar

Ama "her Figure alternate text taşır" ifadesi dosyanın sözdiziminin özelliği değildir. Bu, mantıksal yapının — içeriği anlama eşleyen etiketli öğe ağacının — özelliğidir. Figure'ın Alt girdisi yapı öğesi sözlüğünde bulunabilir, bir /ActualText span üzerinden sağlanabilir ya da role-mapped özel türden gelebilir. Bunu bayt akışı içinde /Alt arayarak güvenilir biçimde bulamazsınız; çünkü bu dizge ilgisiz bağlamlarda da görünür, object stream içinde sıkıştırılmış olabilir ve hangi yapı öğesine ait olduğunu size hiç söylemez. Bu soruyu dürüstçe cevaplamanın yolu, belgenin kendi yapı ağacına, öğe öğe, veraPDF ve PAC'in değerlendirdiği aynı yüzeye sormaktır. PDFium'un Tier-1 denetimleri tam olarak bu çizgi etrafında kuruludur: biçim için bayt taraması, içerik için ağaç yürüyüşü

Canlı etiket ağacını okuma

Ham malzeme TPdf.GetStructureElements (ayrıca StructureElements özelliği olarak da sunulur) değeridir; bu, belge sırasına göre düz bir TPdfStructureElements — yani TPdfStructureElement kayıtlarından oluşan düz dizi — döndürür. Her kayıt, PDFium erişimci işlevleri üzerinden tek bir yapı öğesinin yansımasıdır ve erişilebilirlik kurallarının gerçekten ihtiyaç duyduğu alanları taşır:

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

Doğrulayıcının eksen aldığı alan TypeName alanıdır. Bu alan FPDF_StructElement_GetType içinden gelir; PDFium role map'i çözdükten sonra öğenin standart yapı türünü, yani /S adını döndürür. AlternateText, FPDF_StructElement_GetAltText üzerinden; ActualText, FPDF_StructElement_GetActualText üzerinden; ID ise FPDF_StructElement_GetID üzerinden gelir. Dizi düz ve sıralı olduğu için doğrulayıcı tüm belge üzerinde bir kerede akıl yürütebilir; özyinelemeye ihtiyaç duymaz. Bu, öğe başına değil belge genelinde çalışan tek kural için önemlidir

Denetleyici saf fonksiyondur ve bunun nedeni vardır

Kural mantığı, DLL ile konuşan yöntemin içine gömülü değildir. Ayrı duran, herkese açık, saf bir fonksiyondur:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Bir düz öğe dizisini alır ve issue kümesi döndürür. Hiçbir PDFium işlevi çağırmaz, hiçbir belge açmaz, hiçbir global duruma dokunmaz. Bu ayrım bilinçlidir ve iki kez karşılığını verir. Birincisi test edilebilirliktir: birim testinde sentetik TPdfStructureElements dizisi kurabilirsiniz — Alt'sız Figure, erişilebilir metni yalnızca ActualText içinde olan Formula, aynı ID'yi paylaşan iki Note — ve pdfium.dll hiç mevcut olmadan sonuç kümesi üzerinde doğrulama yapabilirsiniz. Kural mantığı çevrimdışı doğrulanır; DLL dolaşımı ise kütüphane yoksa atlanan canlı belge smoke testiyle ayrı doğrulanır

İkincisi sorumluluğun berraklığıdır. TPdf.ValidatePdfUa dağınık kısmın sahibidir — her sayfayı yüklemek, öğeleri çekmek, bunları biriktirmek — ve sonra temiz bir diziyi saf denetleyiciye teslim eder. "Veriyi getir" (DLL, yan etkiler, yaşam süresi) ile "kuralları yargıla" (saf, deterministik) asla birbirine dolanmaz. Bir kural değişmesi gerektiğinde, içinde I/O bulunmayan fonksiyonu değiştirirsiniz

Üç kural gerçekte neyi denetler

Yapı ağacı geçişi, TPdfUaValidationIssues enum'unun sonuna eklenmiş üç issue değeri yükseltir; böylece enum mevcut çağıranlar için ABI kararlı kalır: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt ve pvuaiNoteMissingId. Gövdesi tamamen kavranabilecek kadar küçüktür:

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

7.3 maddesi figürleri düzenler: Figure öğesi metin alternatifi sağlamalıdır. Bu denetimin ilk sürümü yalnızca Alt girdisine bakıyordu; bu da onu referans doğrulayıcılardan daha katı yapıyordu. PDF/UA, erişilebilir metni bunun yerine ActualText üzerinden sağlanan Figure'ı kabul eder — replacement text geçerli alternatif temsildir — bu yüzden kural, yalnızca her ikisi de boşsa Figure'ı işaretler; yani Alt da ActualText de boşsa. 7.7 maddesi formülleri kapsar ve aynı düzeltmeden sonra Figure koluyla aynı Alt-veya-ActualText testini kullanır; erişilebilir metni yalnızca ActualText üzerinden veren bir Formula içeren uygunluk örneği, Formula kolu Figure koluyla hizalanana kadar yanlış reddediliyordu

7.9 maddesi doğası gereği farklıdır. Note öğesinin bir /ID değeri olmalıdır ve bu ID belge genelinde benzersiz olmalıdır. Eksik ID, öğe başına bir hatadır. Yinelenen ID ise iki öğe arasındaki ilişkidir; bu yüzden düz dizi önemlidir: her Note için denetleyici daha önce görülen öğeler üzerinde geriye tarama yapar ve aynı ID'yi taşıyan önceki Note ile çakışma bulursa işaretler. Maliyeti Note sayısı üzerinden bariz O(n²)'dir; bu gerçek belgeler için önemsizdir ve işlevi, senkron tutulacak yardımcı dizin gerektirmeyen tek okunabilir döngü halinde bırakır

Benzersizliğin genel olması için sayfalar boyunca biriktirme

PDFium yapı öğelerini belge başına değil sayfa başına verir; bu yüzden ValidatePdfUa içindeki orkestrasyon, kurallar çalışmadan önce bunları toplamak zorundadır. FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage ile her sayfayı gezer; bileşenin o anda açık tuttuğu sayfadan bağımsızdır ve her sayfanın öğelerini tek bir diziye ekler. Saf denetleyiciyi ancak ondan sonra çağırır:

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

Bu biriktirme, 7.9 benzersizlik denetimini doğru kılan şeydir. Farklı sayfalardaki iki Note aynı ID'yi paylaşabilir; sayfa sayfa doğrulama yapsaydınız çakışmayı hiç göremezdiniz, çünkü her sayfanın öğe kümesi kendi içinde tutarlı görünür. Belge genelinde tek dizi kurmak, yinelemenin görünür olmasının tek yoludur. Baştaki koruma da not edilmeye değer: ağaç yürüyüşü yalnızca bayt düzeyindeki geçiş şunu rapor etmemişse çalışır: pvuaiMissingStructTreeRoot. Etiketsiz belgenin yürüyecek ağacı yoktur ve zaten eksik yapı kökü için işaretlenmiştir; bu yüzden sayfa başına yüklemeler tamamen atlanır. Derin geçiş, yarar sağlamayacak belgelerde hiçbir maliyet oluşturmaz

Tasarım gereği ihtiyatlı: sessizce kaçır, asla boş yere bağırma

Bu doğrulayıcının en önemli özelliği, yapmayı reddettiği şeydir. Yalnızca /S içinden doğrudan dönen standart FPDF_StructElement_GetType tür adlarını eşler — Figure, Formula, Note. Özel tür tanımlayıp bunu Figure'a role-map eden belge, PDFium'un türü nasıl çözdüğüne bağlı olarak kendi adını döndürebilir. Böyle olduğunda denetleyici bunu tanımaz ve sessiz kalır. Bu bir false negative'dir ve istenen davranış da budur. Tasarım kuralı asla false positive üretmektense eksik raporlamaktır; çünkü uyumlu dosyalarda sürekli yanlış alarm veren preflight aracı kullanıcılarını onu görmezden gelmeye alıştırır ve görmezden gelinen doğrulayıcı hiç olmamasından daha kötüdür. Dekoratif görseller, yapı ağacında değil artifact stream içinde yaşar; dolayısıyla baştan Figure olarak yüzeye çıkmazlar ve doğru biçimde artifact olarak işaretlenmiş arka plan çizgisi için "eksik Alt" şikâyeti almazsınız

Kapsamın üç kuralla sınırlı tutulmasının nedeni de budur. Heading-level nesting (7.4), table header scope (7.5) ve role-map cycle detection (7.1) elbette meşru PDF/UA gereksinimleridir; ancak bunları iyi denetlemek gerçek grafik ve öznitelik analizi gerektirir, bunları safça denetlemek ise tasarımın yasakladığı false positive'leri üretir — PDF/UA, basit "sıkı biçimde artmalı" kuralının yanlış reddedeceği H1, H2, H3, H3 gibi başlık düzenlerine izin verir. Bu denetimler özel uygunluk araçlarına bırakılmıştır. Tier-1 kümesi, eksik özniteliğin yoruma kapalı olduğu alt kümedir

Sınırı açıkça ifade etmek gerekirse

Bunu yayın kapısına bağlamadan önce iki sınırı bilmekte yarar var. İlki, denetleyicinin yalnızca PDFium'un yapı öğesinden okuyabildiği kadar iyi olmasıdır. Referans doğrulayıcıların geçirdiği bazı uygunluk derlemi dosyaları, PDFium'un yüzeye çıkarmadığı alternate-text mekanizması kullanır; bu yüzden FPDF_StructElement_GetAltText boş döner, dosya ise gerçekte uyumludur. Saf denetleyici sonra eksik veri üzerinden eksik Alt işaretler — bu, kural mantığından değil DLL erişim kapsamasından kaynaklanan false positive'dir. Bu durumları soğuracak şekilde kuralı gevşetmek, yakalanması amaçlanan gerçek hatalara karşı da onu körleştirirdi; bu yüzden üzeri örtülmez, bilinen PDFium sınırlaması olarak belgelenir

İkincisi, bunun bir preflight olduğu, sertifikasyon olmadığıdır. Tier-1, bayt taramasının yapısal olarak yakalayamayacağı yüksek güvenli içerik hatalarını yanlış alarm vermeden yakalar; ama başlık semantiği, tablo yapısı ve okuma sırası doğruluğu da dahil olmak üzere tam PDF/UA uyumluluğu yine de eksiksiz doğrulayıcıya ve sonunda insan incelemecisine aittir. ValidatePdfUa öğesini kendi hattınızda belirgin kusurları hızlı ve ucuz biçimde reddetmek için kullanın; son sözü ise veraPDF veya PAC söylesin. Aynı yapı ağacı dolaşımı Delphi'de erişilebilir PDF okuyucu oluşturma işinin temelini de oluşturur; orada etiket ağacı okuma sırasını ve seslendirilen metni yönlendirir ve Delphi'den PDF annotation inceleme içindeki metadata düzeyi çalışmayı tamamlar

Burada gösterilen yapı ağacı API'leri ile ValidatePdfUa doğrulayıcısı, Delphi ve C++Builder (VCL) ile Lazarus/FPC (LCL) için PDFium Component ile birlikte sunulur. Ürün sayfası, bu denetimlerin arkasındaki eksiksiz TPdfStructureElement kayıt düzeni ve issue enum'u da dahil olmak üzere tam API başvurusuna bağlantı verir