Teknik Makale

PDFium ile Otomatik PDF Ön Kontrolü ve Risk Denetimi

Bir üretim sınırına — bir yazdırma kuyruğuna, bir arşive, bir müşteri yükleme portalına — gelen bir PDF, hiçbir şey onu işlemeden önce denetlenmelidir. Dosya, dış bir programı başlatmak üzere bağlanmış bir Launch eylemi, baskıdan sağ çıkamayacak kadar kaba görüntüler, tam da gönderildiği yazdırma işini yasaklayan bir şifreleme sözlüğü ya da karşılayamadığı bir PDF/A etiketi taşıyor olabilir. Bir belgeyi bir iş akışına girmeden önce bu tür kurallara karşı incelemeye ön kontrol denir ve PDFium C API, tek bir sayfa bile işlemeden bu denetimleri doğrudan gerçeklemek için Delphi tarafına gereken her şeyi verir

Bu yazı denetimlerin kendisini kuruyor: dört denetim sınıfı, her biri bulguları paylaşılan bir sonuç listesine ekleyen küçük bir yordam. Etkileşimli ögeler, kaynak ölçümleri, güvenlik durumu ve standart işaretleri, aritmetik dahil olmak üzere çalışan kodla ele alınıyor. İhtiyacınız olan şey denetimlerin çevresindeki düzenekse — yığın klasör döngüleri, JSON ve HTML rapor dosyaları, dosya başına yalıtım — PDFium Component hazır bir ön kontrol motoruyla gelir ve yığın ön kontrol CLI yazısı o tesisatı anlatır. İkisi bilerek tek bir çıkış kodu sözlüğünü paylaşır; böylece burada yazılan bir denetleyici doğrudan o yığın sürücüsünün altına oturur

PDF hattı şeması: bir girdi PDF dosyası, bulguları eşik tabanlı bir çıkış koduna eşlenen tek bir TPreflightFinding kaydında toplanan dört denetim sınıfına dağılır
Denetim, güvenilmeyen bir dosyayı dört denetim sınıfına dağıtır — etkileşimli ögeler, kaynak ölçümleri, güvenlik durumu ve standart işaretleri — her sonucu sayılabilir tek bir bulgu kaydında toplar ve bunu tek bir çıkış koduna çevirir

Bulgu kaydı ve çıkış kodu sözleşmesi

Her denetim tek bir düz kayıt türüne yazar; çünkü alternatifi, yani her denetimin kendi düzyazısını basması, sonradan sayılamaz, süzülemez ve eşiklenemez. Dört alan yeterlidir

uses
  System.SysUtils, System.Math, System.IOUtils,
  System.Generics.Collections, pdfium_lib;

type
  TFindingSeverity = (fsInfo, fsWarning, fsError);

  TPreflightFinding = record
    Severity: TFindingSeverity;
    Code: string;       // kararlı makine anahtarı, örn. 'ACT-LAUNCH'
    Page: Integer;      // 1 tabanlı; 0 belge düzeyi demektir
    Message: string;    // insanlar için; sürümler arasında yeniden yazılabilir
  end;

  TFindings = TList<TPreflightFinding>;

procedure Add(Findings: TFindings; Severity: TFindingSeverity;
  const Code: string; Page: Integer; const Msg: string);
var
  F: TPreflightFinding;
begin
  F.Severity := Severity;
  F.Code := Code;
  F.Page := Page;
  F.Message := Msg;
  Findings.Add(F);
end;

Aşağı akıştaki araçlar Code alanına göre anahtarlanır, hiçbir zaman değişmekte özgür olan Message metnine göre değil. Süreç çıkış kodu, yığın yazısındaki aynı üç değerli sözleşmeyi izler: 0 dosyanın hiç bulgu üretmediği, 1 bulgu bulunduğu ve 2 dosya ayrıştırılamadığı ya da parola istediği için denetimin kendisinin çalışamadığı anlamına gelir. Kod 2 değerini ayrı tutmak önemlidir. Bozuk taramalarla dolu bir klasör, ani bir uyum çöküşü değil yukarı akışta bozuk bir tarayıcıdır ve ikisini birleştirmek birini yanlış sorunun peşine düşürür

Etkileşimli ögeler: betikler, launch hedefleri, dış bağlantılar

PDFium bulduğu her eylemi bir tamsayı türüne göre sınıflandırır ve fpdf_doc.h başlığındaki sabitleri tam olarak saptamak gerekir; çünkü yanlış kopyalanmış değerler bir tarayıcıyı sessizce kör eder. Gerçek numaralandırma şudur: PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 ve PDFACTION_EMBEDDEDGOTO = 5. Neyin eksik olduğuna dikkat edin: bir JavaScript üyesi yoktur. Belge düzeyindeki betikler bağlantı eylemi değildir ve FPDFAction_GetType üzerinden asla görünmez; ayrı bir çağrı ailesiyle numaralandırılırlar. Eylem türlerini hayali bir JavaScript sabitine karşı sınayan bir denetleyici derlenir, çalışır ve sonsuza dek hiçbir şey bulmaz

const
  PDFACTION_GOTO         = 1;   // belge içi atlama: zararsız
  PDFACTION_REMOTEGOTO   = 2;   // başka bir yerel dosyaya atlama
  PDFACTION_URI          = 3;   // dış bir URL açar
  PDFACTION_LAUNCH       = 4;   // dış bir program başlatır
  PDFACTION_EMBEDDEDGOTO = 5;   // gömülü bir dosyaya atlama

function ActionTarget(Doc: FPDF_DOCUMENT; Action: FPDF_ACTION;
  AType: ULONG): string;
var
  Buf: array[0..2047] of AnsiChar;
begin
  FillChar(Buf, SizeOf(Buf), 0);
  if AType = PDFACTION_URI then
    FPDFAction_GetURIPath(Doc, Action, @Buf, SizeOf(Buf))
  else
    FPDFAction_GetFilePath(Action, @Buf, SizeOf(Buf));
  Result := string(UTF8String(PAnsiChar(@Buf)));
end;

procedure AuditPageActions(Doc: FPDF_DOCUMENT; Page: FPDF_PAGE;
  PageNo: Integer; Findings: TFindings);
var
  StartPos: Integer;
  Link: FPDF_LINK;
  Action: FPDF_ACTION;
  AType: ULONG;
begin
  StartPos := 0;
  while FPDFLink_Enumerate(Page, @StartPos, @Link) <> 0 do
  begin
    Action := FPDFLink_GetAction(Link);
    if Action = nil then
      Continue;                 // yalnızca hedef içeren bağlantı, işaretlenecek şey yok
    AType := FPDFAction_GetType(Action);
    case AType of
      PDFACTION_LAUNCH:
        Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
          'Launch action targets "' + ActionTarget(Doc, Action, AType) + '"');
      PDFACTION_URI:
        Add(Findings, fsWarning, 'ACT-URI', PageNo,
          'link opens ' + ActionTarget(Doc, Action, AType));
      PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
        Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
          'cross-file destination "' + ActionTarget(Doc, Action, AType) + '"');
    end;                        // PDFACTION_GOTO tasarımı gereği sessiz kalır
  end;
end;

procedure AuditDocumentBehaviors(Doc: FPDF_DOCUMENT; Findings: TFindings);
var
  N: Integer;
begin
  N := FPDFDoc_GetJavaScriptActionCount(Doc);
  if N > 0 then
    Add(Findings, fsError, 'JS-DOC', 0,
      Format('%d document-level JavaScript action(s) run on open', [N]));
  N := FPDFDoc_GetAttachmentCount(Doc);
  if N > 0 then
    Add(Findings, fsWarning, 'ATT-EMB', 0,
      Format('%d embedded file attachment(s)', [N]));
end;

Önem derecesi ayrımı politikayı kodlar. Bir Launch eylemi hatadır; çünkü rastgele bir programı başlatmak, bir PDF içindeki bir tıklamanın yapabileceği en tehlikeli şeydir ve hiçbir faturanın buna ihtiyacı yoktur. Dış URI adresleri uyarıdır: meşru belgelerde yaygındır ama bir inceleyici hedefi tıklamadan görmelidir; çünkü görünen bağlantı metni ile gerçek hedefin uyuşması gerekmez. Belge içi GoTo atlamaları davranış değil yapıdır ve rapordan tümüyle uzak durur — her içindekiler girdisinde kurt masalı anlatan bir ön kontrol, insanları onu yok saymaya alıştırır. JavaScript sayısının ardındaki betik gövdelerini okumak, ayrıca imza MDP düzeyleri ve XFA tespiti için güvenlik riski denetimi yazısı aynı yüzeyi bileşenin nesne sarmalayıcısı üzerinden dolaşıyor

Kaynak ölçümleri: etkin görüntü DPI değeri

Bir PDF içindeki görüntünün kendine ait bir DPI değeri yoktur. Pikselleri vardır ve sayfa, bu pikselleri punto cinsinden ölçülen bir dikdörtgene yerleştirir; 72 punto bir inç eder. Çözünürlük yalnızca bu ikisinin oranı olarak vardır; aynı 600 çarpı 400 fotoğrafın küçük resim olarak jilet gibi keskin, tam sayfa kapak görseli olarak bulanık bir karmaşa olmasının nedeni budur. Bu yüzden denetimin her görüntü için iki sayıya birden ihtiyacı vardır: görüntü meta verisinden gelen kaynak piksel boyutları ve nesne sınırlarından gelen yerleşim dikdörtgeni

procedure AuditPageImages(Page: FPDF_PAGE; PageNo: Integer;
  Findings: TFindings);
var
  I, ObjCount: Integer;
  Obj: FPDF_PAGEOBJECT;
  Meta: FPDF_IMAGEOBJ_METADATA;
  L, B, R, T: Single;
  WidthPt, HeightPt, DpiX, DpiY, EffDpi: Double;
begin
  ObjCount := FPDFPage_CountObjects(Page);
  for I := 0 to ObjCount - 1 do
  begin
    Obj := FPDFPage_GetObject(Page, I);
    if FPDFPageObj_GetType(Obj) <> FPDF_PAGEOBJ_IMAGE then
      Continue;
    if FPDFImageObj_GetImageMetadata(Obj, Page, @Meta) = 0 then
      Continue;
    if FPDFPageObj_GetBounds(Obj, @L, @B, @R, @T) = 0 then
      Continue;

    WidthPt  := R - L;              // sayfadaki yerleşim boyutu, punto cinsinden
    HeightPt := T - B;
    if (WidthPt <= 0) or (HeightPt <= 0) or
       (Meta.Width = 0) or (Meta.Height = 0) then
      Continue;

    // 72 punto = 1 inç, yani yerleşim inçi = punto / 72 ve
    // etkin DPI = kaynak piksel / yerleşim inçi.
    DpiX := Meta.Width  / (WidthPt  / 72.0);
    DpiY := Meta.Height / (HeightPt / 72.0);
    EffDpi := Min(DpiX, DpiY);      // baskı kalitesini kötü olan eksen belirler

    if EffDpi < 150.0 then
      Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
        Format('image %dx%d px placed at %.1fx%.1f pt = %.0f DPI effective',
          [Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
    else if EffDpi > 600.0 then
      Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
        Format('image is %.0f DPI at placed size; resampling would ' +
          'shrink the file with no visible loss', [EffDpi]));
  end;
end;

Eşikler fizik değil politikadır: 150 DPI, altında ofis baskısının gözle görülür biçimde pikselleştiği bir tabandır, 300 olağan ticari hedeftir ve 600 üstündeki her şey görünür kalite kazandırmadan dosya boyutunu şişirir; bu yüzden bir kusur olarak değil bilgilendirici bir şişkinlik olarak raporlanır. Dürüst bir çekince: FPDFPageObj_GetBounds eksene hizalı kutuyu döndürür; dolayısıyla döndürülerek yerleştirilmiş bir görüntüde hesaplanan değer gerçek yoğunluğu olduğundan düşük gösterir. FPDF_IMAGEOBJ_METADATA yapısı ayrıca PDFium'un tam dönüşüm matrisinden türettiği horizontal_dpi ve vertical_dpi alanlarını taşır; iki sonucu karşılaştırmak döndürülmüş yerleşimleri saptamanın ucuz bir yoludur. Aynı puntodan piksele aritmetiği ters yönde işlemeyi de sürer ve bu JPEG dışa aktarma yazısında ele alınıyor

Güvenlik durumu: şifreleme ve izin bitleri

PDF şifrelemesi işleri farklı iki parola tanımlar. Kullanıcı parolası şifre çözmeye kapı tutar: onsuz dosya hiç açılmaz ve FPDF_LoadDocument, FPDF_GetLastError çağrısı FPDF_ERR_PASSWORD bildirirken nil döndürür. Sahip parolası izinlere kapı tutar: yalnızca bir sahip parolasıyla korunan bir dosya kimlik bilgisi olmadan açılır ama uyumlu bir okuyucunun uyması gereken kısıtlama bitleri taşır. Bu yüzden yükleme denemesinin kendisi ilk güvenlik yoklamasıdır ve bu ayrım çıkış kodunu belirler — kullanıcı parolalı bir dosya denetlenemez (kod 2), sahip parolalı bir dosya ise normal biçimde denetlenir ve yalnızca bulgu biriktirir

const
  FPDF_ERR_PASSWORD = 4;

function AuditSecurity(const FileName: string;
  Findings: TFindings): FPDF_DOCUMENT;
var
  Perms: ULONG;
  Revision: Integer;
begin
  Result := FPDF_LoadDocument(PAnsiChar(AnsiString(FileName)), nil);
  if Result = nil then
  begin
    if FPDF_GetLastError() = FPDF_ERR_PASSWORD then
      Add(Findings, fsError, 'SEC-USERPW', 0,
        'user (open) password required; audit cannot proceed')
    else
      Add(Findings, fsError, 'DOC-BROKEN', 0, 'file failed to parse');
    Exit;
  end;

  Revision := FPDF_GetSecurityHandlerRevision(Result);
  if Revision >= 0 then       // -1 dosyanın şifreli olmadığı anlamına gelir
  begin
    // Boş parolayla açıldı ama şifreli: yalnızca sahip parolası var.
    // Herkes okuyabilir, ancak izin bitleri uyumlu bir okuyucunun
    // neye izin verdiğini kısıtlar. Şifresiz dosyalar tüm bitleri
    // ayarlı bildirir; revizyon kapısının önce gelmesinin nedeni budur.
    Perms := FPDF_GetDocPermissions(Result);
    Add(Findings, fsInfo, 'SEC-ENC', 0,
      Format('encrypted, security handler revision %d', [Revision]));
    if (Perms and 4) = 0 then      // bit 3: yazdırma
      Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
        'printing is not permitted');
    if (Perms and 16) = 0 then     // bit 5: içerik kopyalama / çıkarma
      Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
        'content extraction is not permitted');
    if (Perms and 2048) = 0 then   // bit 12: yüksek çözünürlüklü yazdırma
      Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
        'only low-resolution printing is permitted');
  end;
end;

Maskeler, bitleri 1 numaradan başlatarak numaralandıran ISO 32000-1 Tablo 22 kaynaklıdır: /P değerinin 3. biti 4 maskesidir, 5. bit 16, 12. bit 2048. Belirli bir bulgunun önemli olup olmadığı bir yönlendirme kararıdır. Bir baskı bürosu SEC-NOPRINT taşıyan bir dosyayı, teslim tarihinden üç saat önce RIP aşamasında değil, gönderenin net bir mesaj aldığı girişte geri çevirmelidir. Bir arşiv ise SEC-ENC bulgusunun kendisini engelleyici saymalıdır; çünkü şifreleme ile uzun süreli saklama bir arada yürümez — standart denetiminin birazdan resmî olarak dile getireceği bir nokta

Standart işaretleri: bir PDF/A beyanını okumak

Bir dosya PDF/A uyumluluğunu XMP meta veri paketinde, pdfaid:part özelliği (1 ile 4 arası) ve pdfaid:conformance (düzey harfi; görsel sadakat için b, tam yapısal etiketleme için a gibi) üzerinden bildirir. PDFium C API hiçbir XMP erişimcisi sunmaz; FPDF_GetMetaText yalnızca Info sözlüğünü okur ve tanımlama orada yaşamaz. Kaçış kapısı standardın kendisindeki bir kuraldır: ISO 19005, XMP meta veri akışının sıkıştırılmamış saklanmasını şart koşar; tam da araçlar onu bir PDF ayrıştırıcısı olmadan bulabilsin diye. Bu yüzden ham bir bayt taraması meşru bir beyan dedektörüdür — ve beyanı sıkıştırılmış bir akışın içine gizlenmiş bir dosya, iddia ettiği standardı zaten çiğnemiştir

function PdfAClaim(const FileName: string): string;
var
  Bytes: TBytes;
  S: RawByteString;
  P, Limit: Integer;
begin
  Result := '';                     // boş = PDF/A beyanı yok
  Bytes := TFile.ReadAllBytes(FileName);
  if Length(Bytes) = 0 then
    Exit;
  SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
  P := Pos('pdfaid:part', S);       // XMP tanımlama şeması
  if P = 0 then
    Exit;
  // Hem <pdfaid:part>2</pdfaid:part> hem de pdfaid:part="2" biçimini işler:
  // özellik adından sonraki ilk rakamı alır.
  Limit := Min(P + 32, Length(S));
  Inc(P, Length('pdfaid:part'));
  while (P <= Limit) and not (S[P] in ['1'..'4']) do
    Inc(P);
  if P <= Limit then
    Result := 'PDF/A-' + Char(S[P]);
end;

Bunun ürettiği bulgu bilerek bilgilendiricidir; çünkü bir beyan, dosyanın bir özelliği değil bir bildirimdir. XMP girdisi, bozuk olanlar dahil herhangi bir üreticinin yazabileceği tek satır XML'dir; uyumluluk ise dosyanın gömülü yazı tipleri, aygıttan bağımsız renk ve yasaklı özellikler hakkındaki yüzlerce kuralı gerçekten karşılamasıdır. Beyanı tespit etmek size hangi dosyaların gerçek doğrulamaya yönlendirileceğini söyler, fazlasını değil. Bileşenin yerleşik ön kontrol motoru bu doğrulamayı PDF/A, PDF/UA ve PDF/X profilleri boyunca yapar ve yığın CLI yazısı onu, bir denetçinin sonradan açabileceği raporlarla birlikte bir hatta nasıl bağlayacağınızı gösteriyor

Sorunlu bir dosya üzerinde bir çalıştırma

Sürücü denetimleri birbirine dizer: önce güvenlik, çünkü denetimin hiç çalışıp çalışmayacağına o karar verir; sonra belge düzeyindeki davranışlar ile standart beyanı; sonra eylemler ve görüntüler için bir sayfa döngüsü

function AuditFile(const FileName: string; Findings: TFindings): Integer;
var
  Doc: FPDF_DOCUMENT;
  Page: FPDF_PAGE;
  I: Integer;
  Claim: string;
begin
  Doc := AuditSecurity(FileName, Findings);
  if Doc = nil then
    Exit(2);                        // denetim başarısızlığı, bir karar değil
  try
    AuditDocumentBehaviors(Doc, Findings);
    Claim := PdfAClaim(FileName);
    if Claim <> '' then
      Add(Findings, fsInfo, 'STD-PDFA', 0,
        Claim + ' conformance claimed (declaration only, not validated)');
    for I := 0 to FPDF_GetPageCount(Doc) - 1 do
    begin
      Page := FPDF_LoadPage(Doc, I);
      if Page = nil then
      begin
        Add(Findings, fsError, 'PAGE-BROKEN', I + 1, 'page failed to parse');
        Continue;
      end;
      try
        AuditPageActions(Doc, Page, I + 1, Findings);
        AuditPageImages(Page, I + 1, Findings);
      finally
        FPDF_ClosePage(Page);
      end;
    end;
  finally
    FPDF_CloseDocument(Doc);
  end;
  if Findings.Count > 0 then
    Result := 1
  else
    Result := 0;
end;

Dış bir ajanstan geri gelen bir broşür üzerinde çıktı şöyle görünür

> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 finding(s)
  [ERROR]   ACT-LAUNCH   page 3   Launch action targets "..\tools\setup.exe"
  [ERROR]   JS-DOC       doc      2 document-level JavaScript action(s) run on open
  [WARNING] IMG-LOWRES   page 7   image 412x287 px placed at 396.0x275.8 pt = 75 DPI effective
  [WARNING] SEC-NOPRINT  doc      printing is not permitted
  [INFO]    STD-PDFA     doc      PDF/A-2 conformance claimed (declaration only, not validated)
exit code 1

Her satır kendi başına eyleme dönüştürülebilir ama asıl karar bileşimdedir. Bu dosya bir şifreleme sözlüğü ve canlı JavaScript taşırken PDF/A-2 iddiasında bulunuyor; PDF/A ise ikisini de düpedüz yasaklar — yani beyan, derin bir doğrulayıcı çalışmadan önce kanıtlanabilir biçimde yanlıştır. Düz bir bulgu listesinin yüzeye çıkardığı, ikili geç ya da kal kararının ise sakladığı çelişki tam da budur

Bu denetimin size söyleyemedikleri

Bir ön kontrol aracına duyulan güveni ayakta tutan şey kapsam konusundaki dürüstlüktür. Yukarıdaki her şey, dosyanın kendisi hakkında beyan ettiğini okur: PDFium yapıyı ayrıştırır ve bu denetim onun envanterini çıkarır. PDF/A doğrulaması yapmaz — gömülü yazı tiplerine karşı glif kapsamı denetimi yok, çıktı hedeflerine karşı renk uzayı çözümlemesi yok, bir beyanı uyumluluktan ayıran madde düzeyindeki kuralların hiçbiri yok; bunun için bileşenin ön kontrol motoru ya da veraPDF gibi adanmış bir doğrulayıcı gerekir. İzin bitleri, uyumlu okuyucuların uyduğu bildirimlerdir, kriptografik duvarlar değil; dolayısıyla SEC-NOPRINT zorlamayı değil niyeti betimler. Eylem taraması bağlantı açıklamalarını ve belge düzeyindeki betikleri kapsar; form alanı olay sözlüklerine gömülü betikler için üstüne form API çağrıları gerekir. Denetimi bir imza denetimiyle genişletirseniz o da beyan edilen niyeti bildirir, doğrulanmış kriptografiyi değil — sertifika zinciri doğrulaması ayrı bir iştir. Bir ön kontrol denetimi giriş görüşmesidir, duruşma değil: işi, yönlendirme kararını bilgili, hızlı ve yinelenebilir kılmaktır

Not: Bu denetim boyunca kullanılan belge, sayfa, açıklama ve görüntü nesnesi API çağrıları, üst düzey bir Delphi sarmalayıcısı ve tam bir standart doğrulama ön kontrol motoruyla birlikte PDFium Component ile gelir