Teknik Makale

Form XObject Özyinelemesi: Delphi'de PDFlibPas Döngü Tespiti

PDFlibPas, Delphi PDF içerik akışlarındaki özyinelemeli Form XObject çağrılarını, küresel bir ziyaret edilenler kümesi değil, aktif çağrı zincirini takip ederek çözer; bu yüzden TPDFlib.EnumPageContentStatesEx, bir sayfada birkaç kez çağrılan aynı Form'u, meşru yeniden kullanımı bir döngüyle karıştırmadan dolaşabilir. Bir fatura şablonundaki bir damga Form XObject'i tipik durumdur: aynı nesne, bir sayfadaki başlıktan, alt bilgiden ve bir filigran katmanından çağrılır ve yalnızca kendi üzerine döngü yapan bir çağrı zinciri gerçek bir döngüdür

ISO 32000-1 §8.10, bir Form XObject'i, bir sayfanın veya başka bir Form'un Do operatörüyle çağırdığı, /Matrix'te kendi koordinat sistemine, o koordinat sistemindeki /BBox'ta bir kırpma sınırına ve isteğe bağlı olarak kendi kaynak sözlüğüne sahip, kendi kendine yeten bir içerik akışı olarak tanımlar. Spesifikasyonda bir Form'un kaç kez çağrılabileceğine veya Form'ların birbirini ne kadar derinlikte çağırabileceğine dair bir sınır yoktur; bu yüzden uyumlu bir ayrıştırıcı, meşru yeniden kullanımı ve meşru iç içe geçmeyi kabul etmek zorundadır, aynı zamanda spesifikasyonun gerçekten yasakladığı tek düzenlemeye karşı kendini savunurken: içerik akışı doğrudan veya geçişli olarak kendisini çağıran bir Form. PDFlibPas bu ayrımı, her Do anlık görüntüsüne eklenen TPDFlibContentFormTraversalStatus değerleri aracılığıyla bildirir; en dikkat çekici olanları, başarılı bir iniş için ftsEnumerated ve gerçekte bir döngü olan tek durum için ftsCycle'dır

Aynı Form XObject'i yeniden kullanmak neden yanlış bir döngüyü tetiklemiyor?

Tekrarlanan bir Form XObject referansı, kendi başına yanlış giden bir şeyin kanıtı değildir. ISO 32000-1, aynı Form nesnesinin yazarın istediği kadar çok yerden bir içerik akışında çağrılmasına izin verir ki bu, bir logo damgasının, bir antetli kağıt şablonunun veya bir sayfa numarası alt bilgisinin, içerik akışını birkaç kez çoğaltmadan bir sayfa genelinde yeniden kullanılmasının tam olarak nedenidir. Kaçak özyinelemeye karşı naif koruma, nesne numarasıyla anahtarlanan tek bir ziyaret edilenler kümesidir: bir dolaşıcı Form nesnesi 12'yi ilk kez gördüğünde, 12'yi görülmüş olarak işaretler ve ağacın başka herhangi bir yerinde ona tekrar girmeyi reddeder. Bu yaklaşım, aynı damga bir sayfanın iki ilgisiz köşesinde göründüğü an bozulur, çünkü ikinci, tamamen meşru çağrı, nesne numarası zaten görülmüş olarak işaretlendikten sonra gelir ve sanki bir döngüymüş gibi reddedilir

PDFlibPas, döngü tespitini tüm belge yerine mevcut çağrı zinciriyle kapsamlandırarak bu yanlış pozitiften kaçınır. EnumPageContentStatesEx, çözümlenmiş Form akışını içine inmeden hemen önce aktif çağrı zincirine iter, ardından iniş başarılı olsun olmasın geri döndüğü anda aynı girdiyi tekrar çıkarır. Aynı akışın bir kardeş çağrısı, yalnızca birincisi zaten çıkarıldıktan sonra başlar; bu yüzden kardeş çağrı onu kontrol ettiğinde çağrı zinciri o akıştan temizdir ve dolaşıcı onu başka herhangi bir Form gibi tam olarak numaralandırır. Gerçek bir döngü, aynı zincirde farklı görünür: Form A, Form B'yi çağırır, B'nin kendi içeriği A'ya geri çağırdığında B hâlâ zincirde açıktır ve A, henüz dönmemiş dış çağrıdan hâlâ zincirde oturmaktadır -bu, ftsCycle'ın bildirdiği tek şekildir, mevcut çağrı zincirinde daha önce bir yerde hâlâ açık olan bir Form akışı, yalnızca sayfada başka bir yerde mevcut olan değil

PDFlibPas onu durdurmadan önce Form XObject özyinelemesi ne kadar derine gidebilir?

Döngü tespiti ve derinlik sınırlaması iki farklı sorunu çözer ve PDFlibPas onları tam olarak bu nedenle iki farklı TPDFlibContentFormTraversalStatus sonucu olarak tutar. Her biri bir sonrakini çağıran ve hiçbiri tekrarlamayan yirmi ayrı Form'dan oluşan bir zincir, hiçbir tanıma göre bir döngü değildir -aktif zincir kontrolü hiçbir zaman tekrarlanan bir akış bulmaz- ama yirmi dürüst iç içe geçme düzeyi yine de yirmi düzeyde ayrıştırma, matris birleştirme ve kaynak çözümlemedir ki başka bir şey durdurmazsa bozuk veya kötü niyetli bir PDF bunu keyfi olarak daha yükseğe itebilir. EnumPageContentStatesEx, tam olarak bu nedenle bir MaxFormDepth parametresi alır ve çağıran ne isterse istesin geçirilen değeri en fazla 64'e sıkıştırır. Sıfır derinlik, kendi başına bilinmeye değer özel bir durumdur: Form özyinelemesini tamamen devre dışı bırakır ve daha eski EnumPageContentStates metodunun düz, yalnızca-sayfa davranışını yeniden üretir; bu yüzden o moddaki her Do anlık görüntüsü, herhangi bir şey denemek yerine ftsNotRequested bildirir

var
  Lib: TPDFlib;
  States: array of TPDFlibContentGraphicsState;
  Count, I: Integer;
begin
  Lib:= TPDFlib.Create;
  try
    if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
      Exit;
    Lib.SelectPage(1);
    Count:= Lib.EnumPageContentStatesEx(True, 8, States);  // count only
    SetLength(States, Count);
    Lib.EnumPageContentStatesEx(True, 8, States);          // fill
    for I:= 0 to Count- 1 do
      if States[I].FormTraversalStatus= ftsCycle then
        LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
  finally
    Lib.Free;
  end;
end;

Çağrı başına bir alt-takipçi: grafik durumunu yalıtmak

Bir Form XObject'e her iniş, sayfayı zaten dolaşan takipçiyi paylaşmak yerine kendi grafik-durumu takipçisini alır, çünkü bir Form'un içerik akışının grafik durumunu tam olarak bulduğu gibi bırakması gerekir ve PDFlibPas açtığı her PDF'in bu gerekliliğe gerçekten uyduğunu varsayamaz. Çocuk takipçi, çağıran Do talimatında aktif olan CTM, renk durumu ve metin parametrelerinin ne olursa olsun bir anlık görüntüsünden başlar, ardından Form'un tek bir talimatını çalıştırmadan önce kendi kaydetme-ve-geri-yükleme yığınını ve mevcut yol takibini boşa sıfırlar. Dikkatsiz veya hasarlı bir Form içinde eşleşen bir Q olmayan dengesiz bir q -daha eski araçların ürettiği PDF'lerde bulunması nadir bir şey değil- o tek çağrının takipçisinin içinde tutulur ve hiçbir zaman sayfa takipçisine veya içerik akışında bir satır sonra oturan aynı damganın bir kardeş çağrısına sızmaz

Form /Matrix, Do'da yürürlükte olan CTM ile bir cm operatörünün yaptığı aynı şekilde birleşir, onun yerine geçmek yerine mevcut dönüşüme sol-çarpılır ve PDFlibPas kasıtlı olarak ikinci bir formül tutmak yerine bu tek kod yolunu yeniden kullanır, çünkü aynı matris cebrinin iki bağımsız uygulaması, birkaç ölçekleme, döndürme ve eğme birleştirme turundan sonra sessizce birbirinden ayrılan tam olarak bu türden bir yinelemedir. /BBox daha sonra matris zaten uygulandıktan sonra Form'un kendi koordinat uzayında kırpar ve o kutunun dört köşesinin de tek tek dönüştürülür, yalnızca karşıt köşeler değil, çünkü döndürülmüş veya eğilmiş bir Form, aksi takdirde dönüşüm onu başka bir yere taşımadan önce aşırı bir köşede oturan gerçek içeriği kaçıran bir sınırlayıcı kutu bildirebilir. Önceki örnekten döngüyü aynı States dizisi üzerinde genişletmek bu alanları doğrudan okur

for I:= 0 to Count- 1 do
  if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
    States[I].FormBBoxKnown then
    Writeln('Form ', States[I].XObjectResource, ' matrix ',
      States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
      ' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);

Aynı kaynak adına sahip iki Form aynı yazı tipini paylaşır mı?

Hayır. /F1 gibi bir kaynak adı, yalnızca kullanıldığı noktada aktif olan kaynak sözlüğüne göre bir anlam ifade eder ve iki farklı Form XObject, o özdeş ad altında tamamen iki farklı yazı tipini tanımlamakta özgürdür. PDFlibPas bunu, her kaynak adının yanında bir kaynak kapsamını takip ederek çözer: bir Form kendi /Resources sözlüğünü taşıdığında, o sözlük içindeki her şey için tam kaynak kapsamı olur; Form'un kendi sözlüğünün atladığı her ne olursa olsun sayfaya veya çağıran sözlüğüne anahtar bazında bir yedek yoktur. Yalnızca hiç /Resources anahtarı olmayan bir Form -bazı daha eski PDF üreticilerinin hâlâ ürettiği bir örüntü- çağıran sözlüğü toptan miras alır ve bu, yeni çıktıda dayanılmaya değer genel bir kural değil, kasıtlı bir uyumluluk istisnasıdır. Bu nedenle bir TPDFlibContentGraphicsState anlık görüntüsünde yazı tipi kimliği, yalnızca ad değil, FontResource ve FontResourceScope çiftidir; belirli bir /F1'in o belirli kapsamda tam olarak hangi dolaylı nesneye çözüldüğünü doğrulamak için FontObjectNumber mevcuttur

Aynı kapsamlama, altta yatan çözümleme mekanizması yazı tiplerini özel bir durum olarak ele almadığından, ExtGState girdileri ve iç içe geçmiş XObject girdileri dahil, bir Form'un taşıyabileceği her adlandırılmış kaynak için de geçerlidir -yazı tipi durumu yalnızca en çok önem taşır, çünkü uyumsuz bir yazı tipi kimliği, bariz bir başarısızlık yerine sessizce yanlış glifler üretir. Metin çalışmalarını yalnızca yazı tipi adına göre gruplandıran, kaynak kapsamına göre de gruplandırmayan çıkarma kodu, adı paylaşan iki görsel olarak farklı yazı tipini birleştirir ve hata, biri tek tutarlı bir yazı tipi olarak okunması gereken şeyin içinde yanlış yazı tipinden rakamlar oturduğunu fark edene kadar yüzeye çıkmaz

for I:= 0 to Count- 1 do
  if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
    RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
      States[I].FontObjectNumber, States[I].ContentDepth);

Kendi boru hattınızda FormTraversalStatus'u okumak

FormTraversalStatus, her Do anlık görüntüsünü kendi başına küçük bir tanılama raporuna dönüştürür ve onu göz ardı eden bir boru hattı, eksik bir çıkarmayı açıklayacak tam bilgiyi çöpe atıyordur. ftsNotApplicable, talimatın başından beri hiç çözümlenmiş bir Form çağrısı olmadığı anlamına gelir; ftsNotRequested, bu çağrı için özyinelemenin kapatıldığı anlamına gelir; ftsEnumerated, Form'un başarıyla ayrıştırıldığı ve dolaşıldığı anlamına gelir; ftsDepthLimit ve ftsCycle, bir inişin kasıtlı olarak kısa kesildiği iki yolu işaretler; ve ftsMalformed, dolaşımı durduran her şeyi kapsar -çözülemeyen bir akış referansı, ayrıştırılamayan bir /Matrix veya /BBox veya Form'un kendi içeriği çalıştırılırken fırlatılan bir istisna. Bu son durum operasyonel olarak önemlidir, çünkü başarısız bir iç içe dolaşım, o dal için zaten ürettiği kısmi çıktı her ne ise onu geri alır; bu yüzden bir çağıran, bir Form'un gerçekten boş mu olduğunu yoksa içerik akışına iki talimat girdikten sonra mı patladığını asla tahmin etmek zorunda kalmaz

var
  Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
  Status: TPDFlibContentFormTraversalStatus;
begin
  for Status:= Low(Tally) to High(Tally) do
    Tally[Status]:= 0;
  for I:= 0 to Count- 1 do
    Inc(Tally[States[I].FormTraversalStatus]);
  if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
    FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;

Sınırlar, maliyetler ve bunun nereye oturduğu

Bir Form kaç kez çağrılırsa çağrılsın, bir numaralandırma çağrısı başına içerik akışı tam olarak bir kez kod çözülür ve ayrıştırılır, çünkü PDFlibPas, ayrıştırılmış talimat listesini her kardeş çağrıda yeniden ayrıştırmak yerine altta yatan akış nesnesine karşı önbelleğe alır -girişteki üç köşeli damga bir kez kod çözülür ve üç kez dolaşılır, üç kez kod çözülmez. Her tek çağrıda yeniden kurulan şey, bir çağrı yeri ile diğeri arasında meşru olarak farklılık gösteren her şeydir: çocuk takipçisi, birleştirilmiş CTM, kesişen kırpma ve kaynak kapsamı. Bu çağrı başına CTM ve kırpma muhasebesi, PDFlibPas'ın içerik akışı CTM ve kırpma durumu takipçisinin arkasındaki aynı makinedir; Form özyinelemesinin kendisinin ötesine geçen herhangi bir içerik akışı dolaşımı için bunun yanında okumaya değer

Bu API daha büyük bir boru hattına girmeden önce, etrafında beklenti belirlemeye değer iki sınır vardır. 64 düzeylik derinlik tavanı, meşru olarak derin belgeler için bir ayar düğmesi değildir, çünkü gerçek faturalar, hesap özetleri ve rapor şablonları Form'ları esas olarak hiçbir zaman üç veya dört düzeyden daha derin iç içe geçirmez -ftsDepthLimit'e gerçekten çarpan bir belge, alışılmadık derecede ayrıntılı olmaktan çok bozuk veya kötü niyetli olma olasılığı çok daha yüksektir ve daha büyük bir sayıyla sessizce yeniden denenmek yerine bir veri kalitesi sinyali olarak günlüğe kaydedilmeye değer. EnumPageContentStatesEx aynı zamanda okuma tarafı bir analiz API'sidir: bir içerik akışının ne yaptığını bildirir, bir Form'un görünür olması gerekip gerekmediğini değil, ki bu, bir damga veya filigran Form'u bir görüntüleyicinin kapatmış olabileceği bir katmanın arkasında oturduğunda İsteğe Bağlı İçerik Grubu görünürlük durumu tarafından yanıtlanan ayrı bir sorudur. Çağrı-zinciri döngü tespiti, çağrı başına yalıtma ve kaynak kapsamlama, birlikte Delphi ve C++Builder için PDFlibPas bileşenindeki içerik akışı inceleme yüzeyinin bir köşesini oluşturur