Teknik Makale

PDFium ile Delphi'de Ek Açıklama Görünüm Gidiş Dönüşleri

v3.121.1 öncesi PDFium Component'te, bir ek açıklamayı TPdf.Annotation[] üzerinden okuyup kaydı geri atamak, orijinal yalnızca /N taşıyorken bile /AP görünüm sözlüğüne boş /R ve /D girdileri ekleyebiliyordu. PDF/A doğrulayıcıları o sözlüğü reddeder. v3.121.1'den beri getter yalnızca gerçekten okuduğu bir görünümü bildirir; böylece değişmemiş bir gidiş dönüş hiçbir yeni şey yazmaz. Bu başarısızlığı ayrıntısıyla anlamaya değer; çünkü sıradan tetikleyici, bir dosyayı daha az uyumlu değil daha uyumlu yapmak için tasarlanmış bir düzeltmedir

PDFium Component ek açıklama gidiş dönüşü şeması: TPdf.Annotation[] ve SetAnnotationData üzerinden afPrint eklemek, FPDFAnnot_SetAP ile boş /R ve /D akışları da yazar ve PDF/A temiz görünüm sözlüğünü, v3.121.1 yalnızca gerçekten okuduğu görünümleri bildirene dek veraPDF'in reddettiği sözlüğe çevirir
Bir ek açıklamayı okuyup değiştirmeden geri yazmak eskiden boş rollover ve down görünüm akışları ekliyordu; PDF/A'yı kıran şey de budur, eklemek istediğiniz Print bayrağı değil

Bir ek açıklamayı değiştirmeden geri yazdığınızda ne ters gider?

Kısa cevap: ek açıklama hiç sahip olmadığı görünüm akışları kazanır ve düzenlemenizden önce PDF/A doğrulamasını geçen bir dosya, sonrasında geçemez. Tipik senaryo şöyle işler. Müşteri arşivi Print bayrağı taşımayan square ve text ek açıklamalarıyla gelir; PDF/A her ek açıklamanın yazdırılmasını ister, bu yüzden sayfaları dolaşır, afPrint ekler ve her kaydı geri atarsınız. O kodda görünümlere dokunan hiçbir şey yoktur. TPdf.Annotation[]'dan gelen kayıt bir TPdfAnnotation'dır ve SetAnnotationData, Has* işaretçisi kurulu olan her alanı yazar; HasContents / ContentsText çiftlerinin tasarlanan çalışma biçimi de tam olarak budur. Sorun şuydu: getter, var olmayan modlar için boş dizelerle HasAppearanceRollover ile HasAppearanceDown'u True yapıyordu ve setter da sadakatle iki boş akış yazıyordu:

procedure MarkAnnotationsPrintable(const FileName: string);
var
  Pdf: TPdf;
  PageNo, I: Integer;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    for PageNo := 1 to Pdf.PageCount do
    begin
      Pdf.PageNumber := PageNo;
      for I := 0 to Pdf.AnnotationCount - 1 do
      begin
        A := Pdf.Annotation[I];
        if not (afPrint in A.Flags) then
        begin
          A.Flags := A.Flags + [afPrint] - [afHidden, afInvisible, afNoView];
          // v3.121.1 öncesinde bu atama, kaynak ek açıklamada yalnızca /AP/N
          // varken boş /AP/R ve /AP/D akışları da yazıyordu
          Pdf.Annotation[I] := A;
        end;
      end;
    end;
    Pdf.SaveAs(ChangeFileExt(FileName, '.printable.pdf'));
  finally
    Pdf.Free;
  end;
end;

ISO 32000-1 §12.5.5, görünüm sözlüğünü üç girdiyle tanımlar: normal görünüm için /N, rollover için /R, down için /D. /R ile /D isteğe bağlıdır ve yokluklarında görüntüleyici /N'e döner. Ama boş bir /R akışı yokluk değildir. Hiçbir şey boyamayan geçerli bir akıştır; dolayısıyla rollover görünümlerine uyan bir görüntüleyici, imleç ek açıklamanın üstüne kaydığı anda boş bir dikdörtgen gösterir. PDF/A daha da katıdır: ISO 19005-1 (Corrigendum 2 ile) ve ISO 19005-2 / 19005-3, ek açıklama görünüm sözlüğünde yalnızca /N'e izin verir. veraPDF, gidiş dönüşten geçmiş dosyayı PDF/A-1 için 6.5.3-4 kuralı, PDF/A-2 ve PDF/A-3 için 6.3.3-2 kuralıyla bildirir ve yerleşik TPdf.ValidatePdfA, onu pvaiAnnotationApDictViolation olarak listeler. Standardın bir maddesini tatmin etmek için Print bayrağını ekleyen düzenleme, ötekini kırdı

ISO 32000-1'den ek açıklama görünüm sözlüğü, normal, rollover ve down girdileriyle: PDFium eksik akış için de var olan boş akış için de 2 bayt döndürür; ikisi de TPdf üzerinden içerik yok olarak okunur, PDF/A'nın yasakladığı boş akışı yalnızca TPdf.ValidatePdfA gibi bayt düzeyi bir denetim bulur
Eksik /R, /N'e döner; boş /R boş bir dikdörtgen boyar ve hâlâ PDF/A'yı geçemez; kayıt üzerinden ise ikisi ayırt edilemez

FPDFAnnot_GetAP eksik görünüm için neden 2 döndürür?

PDFium, istenen görünüm akışı var olmasa bile FPDFAnnot_GetAP'ten asla sıfır döndürmez. Fonksiyon, alışılmış PDFium iki çağrı modelini izler: gerekli bayt sayısını almak için nil tampon geçirin, ayırın, sonra UTF-16LE metni kopyalamak için yeniden çağırın. Boyut her zaman UTF-16 sonlandırıcısını içerir; dolayısıyla eksik bir akış 2 bayt, yani boş dize artı sonlandırıcısı bildirir. v3.121.1 öncesi getter ByteLength >= SizeOf(FPDF_WCHAR) sınıyordu; her çağrının geçtiği bir denetim, dolayısıyla herhangi bir görünümü olan her ek açıklama için üç HasAppearance* bayrağı da True dönüyordu. Kayıt üzerinden gidiş dönüş sonra her mod için boş dize saklamayı FPDFAnnot_SetAP'ye soruyor ve PDFium akışı onu tutmak için kuruyordu. İstisna yok, uyarı yok ve görünür sayfa aynı görünüyordu; kusurun bir görüntüleyicide değil bir veraPDF test verisinde ortaya çıkmasının nedeni buydu

FPDFAnnot_GetAP PDFium'da eksik görünüm akışını nasıl bildirir: iki çağrılı model UTF-16 sonlandırıcısı için her zaman en az iki bayt döndürür; SizeOf(FPDF_WCHAR) ile karşılaştıran eski kapı her çağrıyı geçirip tüm HasAppearance işaretçilerini True yapıyordu; v3.121.1 kapısı ise sonlandırıcıdan fazlasını artı çift bayt sayısını ister
İki bayt, kodlanmış boş dizedir; bir görünümün var olduğunun kanıtı değildir; düzeltilmiş getter, sonlandırıcı uzunluğuna eşit ya da ondan az her şeyi içeriksiz sayar ve geri yazma sessiz kalır

v3.121.1'in bir görünümün var olduğuna nasıl karar verdiği

GetPageAnnotation içinde AppearanceNormal, AppearanceRollover ve AppearanceDown'u dolduran yardımcı ReadAppearance, artık bir sonucu yalnızca sonlandırıcının ötesinde en az bir karakter taşıdığında içerik sayar. İlk çağrı SizeOf(FPDF_WCHAR)'dan fazla bayt ve çift bayt sayısı döndürmelidir; tek uzunluk UTF-16 olamaz. Metni gerçekten kopyalayan ikinci çağrı da yeniden doğrulanır: 2 ya da daha az dönen bir uzunluk ya da ayrılan tampondan büyük bir uzunluk, HasValue'yı False'a sıfırlar ve dizeyi boş bırakır. Yazma tarafında hiçbir şey değişmedi. SetAnnotationData hâlâ HasAppearance* bayrağı True olan modlar için FPDFAnnot_SetAP çağırır; böylece yalnızca /N'i olan bir ek açıklamadan okunan kayıt geriye yalnızca /N yazar. Regresyon test verisi her iki yönü de kapsar: normal görünümlü bir square ek açıklaması, okunup değiştirilmeden geri yazıldığında PDF/A-1b, PDF/A-2b ve PDF/A-3b'yi geçer; Print bayrağı çıkarılmış aynı ek açıklama ise beklenen bayrak kuralında ve yalnızca orada başarısız olur

Eksik ve boş akışlar aynı görünür; getter bu yüzden ihtiyatlı kalır

Yerli API, var olan ama boş bir görünüm akışını eksik olandan ayırt edemez ve PDFium Component öyleymiş gibi yapmaz. Her iki durum da FPDFAnnot_GetAP'ten aynı 2 baytı döndürür; böylece ikisi de HasAppearanceRollover = False ve boş bir AppearanceRollover ile okunur. Bunun tasarımınıza yansıtmanız gereken iki sonucu var. Birincisi, False işaretçi "içerik okunmadı, dolayısıyla geri yazma bu moda dokunmaz" demektir, "/R anahtarı sözlükte yok" değil. İkincisi, kayıt dosyada hâlihazırda bulunan boş bir akışı saptayamaz: eski bir derlemin ya da başka bir aracın zarar verdiği belge temiz okunur ve kaydı geri atamak onu ne tamir eder ne kötüleştirir. O dosyaları bulmak için bayt düzeyi bir denetim gerekir; TPdf.ValidatePdfA ile PDFium Component ile PDF/A preflight doğrulama akışı tam bunun içindir

Bir görünümü bilinçli olarak nasıl temizlersiniz?

İşaretçiyi açıkça True yapıp boş bir dize geçirirsiniz; setter onu yazar. SetAnnotationData içinde boş dizeleri engellemek bu hataya karşı künt düzeltme olurdu ama aynı zamanda bir görünümü bilinçli olarak temizleyen çağıranları da kırardı; HasContents ile HasAuthor'ın metin için izlediği sözleşme de budur. Dolayısıyla düzeltmenin tamamı getter'da yaşar ve setter, çağıranın ne isterse onu uygular:

// Rollover görünümünü değiştir, sonra yeniden temizle
A := Pdf.Annotation[0];
A.HasAppearanceRollover := True;
A.AppearanceRollover := 'q Q';
Pdf.Annotation[0] := A;

A := Pdf.Annotation[0];
// A.HasAppearanceRollover True'dur ve metin 'q Q' olarak gidiş dönüş yapar
A.HasAppearanceRollover := True;   // niyeti açıkça yeniden beyan et
A.AppearanceRollover := '';        // bilinçli olarak boş akış yaz
Pdf.Annotation[0] := A;

A := Pdf.Annotation[0];
// HasAppearanceRollover = False olarak ve boş dizeyle geri okunur:
// boş akış ile eksik akış burada ayırt edilemez

Şunu unutmayın: açıkça boşaltılmış bir /R ya da /D, yukarıda anılan PDF/A kuralları altında hâlâ fazladan bir anahtar sayılır. Hedef bir arşiv profiliyse, boş olmayan bir /N yazmak ve öteki iki moda dokunmamak doğrulanan tek biçimdir. PDFium Component ile XFDF dışa ve içe aktarma gibi ek açıklamaları belgeler arasında taşıyan her iş akışı aynı kuralı izlemelidir: kaynağın gerçekten sahip olduğu modları kopyalayın, öteki işaretçileri False bırakın

PDF/A güvenli kalan bir oku-değiştir-yaz örüntüsü

v3.121.1 ya da üzerine yükseltin, görünüm işaretçilerini getter'ın döndürdüğü hâlde bırakın ve göndermeden önce kaydedilen dosyayı doğrulayın. Bayat bir boş akış yok olarak okunduğu için doğrulama adımı kayda değil serileştirilmiş belgeye bakmalıdır ve her partiden sonra koşturmaya değer kadar ucuzdur:

uses
  PDFium, FPdfPdfa;  // FPdfPdfa, TPdfAValidationIssue'yi bildirir

function AnnotationAppearancesAreClean(Pdf: TPdf): Boolean;
var
  Report: TPdfAValidationResult;
begin
  // Pdf'te hâlihazırda yüklü belgeyi doğrular; açıldığından beri
  // Pdf.Annotation[] üzerinden yapılan düzenlemeler dahil
  Report := Pdf.ValidatePdfA;
  Result := not (pvaiAnnotationApDictViolation in Report.Issues);
end;

Aynı disiplin, inceleme için sayfaları yeniden renklendiren ya da notlandıran her panele uygulanır; PDFium Component ile Delphi ek açıklama inceleme akışı kurma makalesinde ele alınan iş akışı: kayıt, motorun okuyabildiğinin anlık görüntüsüdür ve sizin kurmadığınız bir işaretçi değişmeden geri dönmelidir. Tam ek açıklama API'si, PDF/A preflight ve yerli PDFium motoru PDFium Component for Delphi, C++Builder and Lazarus ile birlikte gelir