Teknik Makale

Delphi ile PDFiumPas: Operatör Düzeyinde PDF Sansürü

Biri bir ismin üzerine siyah bir kutu boyar, hiçbir şeyi düzleştirmez, dosyayı gönderir ve gözden geçiren kişi dikdörtgeni seçip ismi bir e-postaya yapıştırır. PDFiumPas buna operatör düzeyinde sansürle yanıt verir: SaveAsRedacted yalnızca karakter kutusu bir sansür dikdörtgenine değen Unicode skalerleri siler; hayatta kalanları özgün yazı tipinden, boyuttan, matristen, gösterim kipinden ve renkten yeniden kurar; eksene hizalı yolları ve görselleri tümüyle bırakmak yerine kırpar

Boyalı bir dikdörtgen neden sansür değildir?

Bir içerik akışının üstüne eklenen bir çizim işlemi hiçbir şeyi gizlemez, çünkü altındaki metin gösterim işleçleri hâlâ akıştadır ve hâlâ kod noktalarıyla eşleşir. ISO 32000-1 §9.4 bir metin nesnesini BT ve ET arasındaki konumlandırma ve gösterim işleçleri dizisi olarak tanımlar; sonradan çizilen dolgulu bir dikdörtgen aynı akıştaki yalnızca başka bir işlemdir. Çıkarım pikselleri değil işleçleri dolaşır; bu yüzden örtülü dörtlü olduğu gibi geri gelir. Gerçek sansür çıktıyı karartmak değil işleneni kaldırmak zorundadır

Bariz güvenli uygulama acımasızdır: sınırlayıcı kutusu bir sansür dikdörtgeniyle kesişen her sayfa nesnesini bulup tüm nesneyi siler. Önceki PDFiumPas sürümlerinin yaptığı buydu ve doğruydu ama pahalıydı. Tek bir Tj bütün bir tablo satırını taşıyabilir; bu yüzden tek bir hesap numarasını karartmak tarihi, açıklamayı ve tutarı da beraberinde götürüyordu. Tam genişlikte bir tablo bandı olan dikdörtgen dolgu tüm sayfa boyunca yok oluyordu. Bir fatura logosu, sansür bir köşesini kestiği için kayboluyordu. Sürüm 3.101.0 kararı bir düzey aşağı, sayfa nesnesinden işlenene indirir

Operatör düzeyinde sansür gerçekte neyi siler?

PDFiumPas metin nesnelerini değil Unicode skalerleri siler. SaveAsRedacted sırasında bileşen, yüklü metin sayfasından karakterden sayfa nesnesine bir eşleme kurar; sonra sınanan nesnenin sahibi olduğu her karakter için karakter kutusunu okur ve o kutuyu her sansür dikdörtgeniyle kesiştirir. Bir dikdörtgene değen karakterler kaldırılmak üzere işaretlenir; gerisi hayatta kalan olarak işaretlenir. Hiçbir şey kesişmiyorsa nesne tamamen olduğu gibi bırakılır. Her karakter kesişiyorsa nesne eskisi gibi tümüyle kaldırılır. Yalnızca karışık durum bir bölmeyi tetikler

PDFiumPas operatör düzeyinde sansürün Delphi içinde tüm nesne silmeyle karşılaştırılması: eski yol bir hesap numarası örtüldüğünde bütün bir metin nesnesini düşürürken, bölme yolu yalnızca kesişen karakterleri siler ve her hayatta kalanı kendi metin nesnesi olarak yeniden yayar
Yalnızca karışık durum bir bölmeyi tetikler: hiçbir şeyin kesişmemesi nesneyi olduğu gibi bırakır, her şeyin kesişmesi onu tümüyle kaldırır

Her hayatta kalan bundan sonra, özgün yazı tipi tanıtıcısından, özgün yazı tipi boyutundan, karakter başına metin matrisinden, özgün metin gösterim kipinden ve vuruş genişliği, uç birleştirme, uç şekli ve tire dizisi dahil üst nesnenin dolgu ve vuruş durumundan kurulu kendi metin nesnesi olarak yeniden yayar. Glifleri metrik olarak özdeş tutan şey, yeni bir yazı tipi çözmek yerine yazı tipi tanıtıcısını yeniden kullanmaktır; kerningi ve sözcük aralığını düzeni yeniden çalıştırmadan yerinde tutan şey ise karakter başına matrisi yeniden kullanmaktır. Bedeli nesne sayısıdır: tutulan bir karakter bir metin nesnesine dönüşür; TPdfRedactionOptions.MaxSplitObjects değerinin üretilen parçalara sert bir tavan olarak var olmasının nedeni budur

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // dosya zaten /Redact notlarını taşıyor
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // operatör düzeyinde bölme (varsayılan)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // üretilen parçalara tavan

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // kapalı başarısız ol, gönderme
  finally
    Pdf.Free;
  end;
end;

Dikdörtgenler kırpılır, döndürülmüş geometri kırpılmaz

Yollar yalnızca PDFiumPas yolun eksene hizalı bir dikdörtgen olduğunu kanıtlayabildiğinde bölünür. Kanıt bilinçli olarak dardır: nesne matrisinin her iki kayma terimi de 0,0001 değerinin altında olmalı, yol bir MOVETO ile başlayıp yalnızca LINETO ile devam eden dört ila altı parçadan oluşmalı ve dönüştürülen noktalar 0,01 hoşgörü içinde nesne sınırlarının dört köşesine de oturmalıdır. Bu denetimi geçen bir yol, art arda dikdörtgen çıkarmasıyla indirgenir; her sansür dikdörtgeni hayatta kalan kümesini sol, sağ, alt ve üst şeritlere oyar ve ortaya çıkan her şerit özgün dolgu kipi, vuruş bayrağı ve boyama durumuyla yeniden oluşturulur. Eğriler, üçgenler, kırpılmış şekiller ve döndürülmüş her şey denetimi geçemez ve tüm nesne kaldırılır

Görseller ISO 32000-1 §8.9 uyarınca işler; görsel örnekleri, geçerli dönüştürme matrisiyle eşlenen birim kareyi kaplar. PDFiumPas bu eşlemeyi ters çevirerek hayatta kalan her sayfa uzayı parçasını yeniden normalleştirilmiş görsel koordinatlarına çevirir, onları birim aralığa kıstırır ve sonra içeri doğru yuvarlayarak piksel dizinlerine dönüştürür: sol ve üst kenarlar Ceil işleminden, sağ ve alt kenarlar Floor işleminden geçer. Bu yön önemlidir. Dışa doğru yuvarlama, sansürlenen yandan kaynak piksellerin kısmi bir sütununun parça kenarında hayatta kalmasına izin verirdi. Tamsayı piksel sınırları bundan sonra yeniden normalleştirilmiş koordinatlara dönüştürülür ve parça matrisini türetmek için kullanılır; böylece kırpılan bit eşlem, kesildiği piksel sınırına tam olarak oturur. Kırpmanın kendisi Gray, BGR, BGRx ve BGRA biçimleri arasında satır aralığını bilen bir satır kopyasıdır. Yollarda olduğu gibi, döndürülmüş ya da eğik bir görsel ya da matrisinde dejenere ölçek terimi bulunan bir görsel tümüyle kaldırılır

PDFiumPasin Delphi içinde kısmen sansürlenmiş bir görseli nasıl kırptığı: hayatta kalan sayfa uzayı parçası ters çevrilen CTM üzerinden normalleştirilmiş görsel koordinatlarına geri eşlenir, birim aralığa kıstırılır ve içeri doğru yuvarlanır; böylece sansürlenmiş hiçbir piksel sütunu hayatta kalmaz
Solda ve üstte Ceil, sağda ve altta Floor; böylece kesim bütün bir piksel sınırına oturur
// After a successful SaveAsRedacted call
Writeln(Format('applied %d redaction(s) on %d page(s)',
  [Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
  [Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
  [Report.SplitTextObjectCount, Report.SplitPathObjectCount,
   Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
  [Report.ResourcePruneReport.RemovedNameCount,
   Report.ResourcePruneReport.RemovedObjectCount]));

if Report.PreservedFragmentCount = 0 then
  // bölünebilen hiçbir şey yoktu: kesişen her nesne tümüyle düşürüldü
  LogWholeObjectFallback(SourcePdf);

PDFiumPas eşlenmemiş karakterlerde neden kapalı başarısız olur?

Çünkü yeniden üretilebilir Unicode skaleri olmayan bir glif dürüstçe yeniden kurulamaz. Bir hayatta kalanı yeniden oluşturmak, metin ayarlama API sini bir dizeyle çağırmak demektir ve bu, tutulan her karakter için kararlı bir kod noktası gerektirir. ToUnicode verisi bozuk ya da eksik olan simgesel alt küme yazı tipleri boş bir eşleme verebilir ve tahminle yeniden kodlama, ekranda doğru görünürken altında farklı bir karakter taşıyan çıktı üretirdi. PDFiumPas reddeder: tutulan karakter denetimi bir istisna yükseltir, istisna SaveAsRedacted içinde yakalanır, TPdfRedactionReport.Succeeded değeri ErrorMessage içindeki iletiyle birlikte False döner ve işlev False döndürür. Aynı kural, parça kümesini sessizce kesmek yerine istisna yükselten bölme bütçesi için de geçerlidir. Güvenmediğiniz yazı tiplerine sahip bir belge olduğunda ve belirleyici eski davranışı istediğinizde, Options.PreservePartialObjects := False ayarlayın; kesişen her nesne tümüyle ortadan kalkar

Paylaşılan kapsamlar arasında kaynak ayıklama

Nesneleri bölmek arkada yetimler bırakır ve onları ayıklamak, sayfa düzeyi /Resources sözlüğünü karşılaştırmak kadar basit değildir. ISO 32000-1 §7.8.3 aynı kaynak sözlüğünün birden çok sayfa, Form XObject, desen ve not görünüm akışı tarafından aynı anda başvurulmasına izin verir. Bir yazı tipi adını bir sayfa onu kullanmayı bıraktığı için silmek, onu hâlâ kullanan başka bir sayfayı bozar. PruneUnusedPdfResources bu yüzden kapsam başına çalışır: /Contents öğesini doğrudan bir dizi, bir diziye dolaylı başvuru ya da tek bir akış olması fark etmeksizin çözer; sonra kaynak kullanımını gerçekte kaynak adlandıran işleçlerden toplar: yazı tipleri için Tf, XObjectler için Do, grafik durumu için gs, renk uzayları ve desenler için CS, cs, SCN ve scn, gölgelendirmeler için sh, işaretli içerik özellikleri için BDC ve DP; buna satır içi görsellerin /CS girdisi de dahildir. Bir sözlük birden çok kapsam tarafından paylaşıldığında, hiçbir şey kaldırılmadan önce kullanılan ad kümeleri kategori başına birleştirilir

PDFiumPasta kaynak ayıklama: üç kapsam tek bir paylaşılan kaynak sözlüğüne başvurur, kullanılan ad kümeleri kategori başına birleştirilir ve yalnızca hiçbir kapsamın başvurmadığı adlar kaldırıldıktan sonra erişilemeyen nesneler süpürülür
Tek bir sözlük birkaç sayfaya, Form XObjecte ve görünüm akışına hizmet edebilir; bu yüzden PDFiumPas tek bir adı bile düşürmeden önce tüm kullanılan ad kümelerini birleştirir

Yalnızca sözlüğe işaret eden her kapsamda başvurulmadığı doğrulanan adlar düşürülür. Güvenle ayrıştırılamayan bir kapsam olduğu gibi bırakılır ve bu muhafazakâr yöndür: ayıklanmamış bir dosya yalnızca daha büyüktür, yanlış ayıklanmış bir dosya bozuktur. Hayatta kalan sözlükler, kesin nesil numaralarını taşıyan seyrek bir artımlı güncelleme olarak geri yazılır ve bir erişilebilirlik yeniden yazımı, adlar ortadan kalktığında erişilemez duruma gelen nesneleri bundan sonra süpürür. TPdfResourcePruneReport ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, bayt sayılarını ve bir Succeeded bayrağını bildirir. SaveAsRedacted bu adımı temizlenmiş çıktı üzerinde otomatik çalıştırır; bu yüzden sansür yolu onu zaten içerir, ancak işlev bunu tek başına isteyen işlem hatları için akış düzeyinde dışa aktarılır

uses
  FPdfCompress;

procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
  Source, Dest: TFileStream;
  Report: TPdfResourcePruneReport;
begin
  Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
  try
    Dest := TFileStream.Create(TargetPdf, fmCreate);
    try
      // AllowSignedDocument False kalır: artımlı bir yeniden yazım,
      // bir imzanın kapsadığı bayt aralıklarını geçersiz kılar
      PruneUnusedPdfResources(Source, Dest, Report);
      if not Report.Succeeded then
        raise Exception.Create(Report.ErrorMessage);
      Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
        [Report.RemovedNameCount, Report.UpdatedScopeCount,
         Report.SourceByteCount, Report.OutputByteCount]));
    finally
      Dest.Free;
    end;
  finally
    Source.Free;
  end;
end;

Bir belge işlem hattına bağlamak

Sansür yolu yüklediğiniz belgeyi hiçbir zaman değiştirmez. SaveAsRedacted yalıtılmış bir anlık görüntü yakalar, /Redact notlarını oraya uygular, ekleri kaldırır, açılış eylemini, katalog eylemlerini, ad ağaçlarını, ilişkili dosyaları, AcroFormu ve üst veriyi kaldıran temizleme geçişini çalıştırır, kaynakları ayıklar ve ancak ondan sonra çıktı akışını yazar. Bu çıktıyı bağımsız bir belge olarak yeniden açıp metni yeniden çıkarmak, kendi test takımınızda tutmaya değer doğrulama adımıdır; çünkü asıl soruyu yanıtlayan tek denetim budur: bir okuyucu dörtlüyü hâlâ elde edebilir mi. Planlamaya değer bir sonuç: bölme sayfa nesnelerini değiştirir, bu yüzden elinizde tuttuğunuz herhangi bir FPDF_PAGEOBJECT tanıtıcısı sonrasında ölmüştür; bu, bir dönüşümden sonra bayat sayfa nesnesi tanıtıcıları bölümünde anlatılan ömür tuzağının aynısıdır

İki komşu parça iş akışını tamamlar. Sansür dikdörtgenlerinin nereye gideceğine karar vermek genellikle çıkarılan geometriyle başlar ve yapısal metin blokları ve okuma düzeni bölümündeki blok ve okuma düzeni modeli, ham karakter dizilerinden daha iyi bir aday kutu kaynağıdır. Sonucu bir gözden geçirene sunmak, güvenli bir PDF önizlemesi oluşturma bölümündeki sağlamlaştırma kurallarına aittir; orada form doldurma ve JavaScript varsayılan olarak kapalı kalır. Birlikte, uyumluluk iş akışlarının çoğunun gereksindiği döngüyü kapsarlar: bul, operatör düzeyinde sansürle, yeniden açarak doğrula, güvenli önizle. Bileşenin tam API yüzeyi, deneme indirmesi ve lisans koşulları PDFium Delphi Bileşeni ürün sayfasında yer alır