บทความเทคนิค

พรีไฟลท์ PDF แบบอัตโนมัติและการตรวจสอบความเสี่ยงด้วย PDFium

ไฟล์ PDF ที่เดินทางมาถึงเขตแดนการผลิต — คิวการพิมพ์, คลังข้อมูลสำรอง, หรือพอร์ทัลสำหรับให้ลูกค้าอัปโหลด — ควรได้รับการตรวจสอบก่อนที่สิ่งใดก็ตามจะนำมันไปแสดงผล ไฟล์นั้นอาจมีการดำเนินการประเภท Launch ที่ถูกตั้งค่าให้เริ่มต้นโปรแกรมภายนอก, มีรูปภาพที่หยาบเกินกว่าจะรอดจากการพิมพ์, มีพจนานุกรมการเข้ารหัสที่สั่งห้ามงานพิมพ์ชนิดเดียวกับที่ไฟล์ถูกส่งเข้ามา, หรือมีป้ายกำกับ PDF/A ที่ไม่ตรงกับความเป็นจริง การตรวจสอบเอกสารกับกฎเหล่านี้ก่อนที่มันจะเข้าสู่เวิร์กโฟลว์เรียกว่า การทำพรีไฟลท์ (preflighting) และ C API ของ PDFium จะมอบทุกสิ่งใน Delphi ที่จำเป็นต่อการติดตั้งการตรวจสอบเหล่านี้โดยตรง โดยไม่ต้องทำการเรนเดอร์แม้แต่หน้าเดียว

บทความนี้จะสร้างการตรวจสอบต่างๆ ขึ้นมาเอง: คลาสตรวจสอบสี่คลาส โดยแต่ละคลาสจะเป็นรูทีนขนาดเล็กที่จะนำสิ่งที่พบไปต่อท้ายในรายการผลลัพธ์ที่ใช้ร่วมกัน องค์ประกอบเชิงโต้ตอบ ตัวชี้วัดทรัพยากร สถานะด้านความปลอดภัย และเครื่องหมายมาตรฐาน ล้วนเป็นโค้ดที่ใช้งานได้จริง รวมถึงคณิตศาสตร์ด้วย หากสิ่งที่คุณต้องการคือกลไกที่อยู่รอบๆ การตรวจสอบเหล่านั้น — ลูปโฟลเดอร์แบบแบทช์, ไฟล์รายงาน JSON และ HTML, หรือการแยกไฟล์เป็นรายไฟล์ — คอมโพเนนต์ PDFium จะมาพร้อมกับกลไกพรีไฟลท์สำเร็จรูป และ บทความเกี่ยวกับแบทช์พรีไฟลท์ CLI จะครอบคลุมเรื่องท่อทางเหล่านั้น ทั้งสองส่วนนี้จงใจใช้คำศัพท์เกี่ยวกับ exit-code ชุดเดียวกัน เพื่อให้เครื่องมือตรวจสอบที่เขียนขึ้นที่นี่สามารถเสียบเข้าใต้ไดรเวอร์แบทช์นั้นได้โดยตรง

บันทึกสิ่งที่ค้นพบและข้อตกลงของ exit-code

ทุกๆ การตรวจสอบจะเขียนลงในเรคคอร์ดแบบแบน (flat record type) ประเภทเดียว เนื่องจากอีกทางเลือกหนึ่ง คือการให้การตรวจสอบแต่ละรายการพิมพ์คำอธิบายของตัวเอง จะไม่สามารถนับจำนวน กรอง หรือกำหนดเกณฑ์ในภายหลังได้ ฟิลด์สี่ฟิลด์นั้นเพียงพอแล้ว

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

type
  TFindingSeverity = (fsInfo, fsWarning, fsError);

  TPreflightFinding = record
    Severity: TFindingSeverity;
    Code: string;       // stable machine key, e.g. 'ACT-LAUNCH'
    Page: Integer;      // 1-based; 0 means document level
    Message: string;    // for humans; free to reword between releases
  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;

เครื่องมือที่อยู่ดาวน์สตรีมจะกำหนดคีย์ที่ Code โดยไม่ใช้ข้อความ Message ซึ่งมีอิสระที่จะเปลี่ยนแปลงได้ exit-code ของกระบวนการจะเป็นไปตามข้อตกลงสามค่าเช่นเดียวกับบทความแบบแบทช์: 0 หมายถึงไฟล์นั้นไม่พบสิ่งใด, 1 หมายถึงมีสิ่งที่ค้นพบ, และ 2 หมายความว่าการตรวจสอบตัวเองไม่สามารถทำงานได้เนื่องจากไฟล์ล้มเหลวในการแยกวิเคราะห์ (parse) หรือต้องการรหัสผ่าน การแยกโค้ด 2 เอาไว้มีความสำคัญมาก โฟลเดอร์ที่เต็มไปด้วยภาพสแกนที่เสียหายคือเครื่องสแกนเนอร์ที่เสียอยู่ต้นทาง ไม่ใช่การที่ระบบรองรับ (compliance) ล่มสลายลงอย่างกะทันหัน และการรวมทั้งสองอย่างเข้าด้วยกันจะส่งผลให้ผู้คนไปไล่ล่าปัญหาผิดจุด

องค์ประกอบแบบโต้ตอบ: สคริปต์, เป้าหมายการเปิดโปรแกรม, ลิงก์ภายนอก

PDFium จะแยกประเภททุกแอ็คชันที่มันพบด้วยประเภทเลขจำนวนเต็ม และค่าคงที่จาก fpdf_doc.h ก็คุ้มค่าแก่การระบุให้ชัดเจน เพราะค่าที่คัดลอกมาผิดๆ จะทำให้เครื่องสแกนเนอร์บอดสนิทอย่างเงียบๆ การแจงนับที่แท้จริงคือ PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 และ PDFACTION_EMBEDDEDGOTO = 5 โปรดสังเกตสิ่งที่หายไป: ไม่มีสมาชิก JavaScript เลย สคริปต์ระดับเอกสารไม่ใช่ลิงก์แอ็คชันและจะไม่ปรากฏผ่าน FPDFAction_GetType เลย; พวกมันถูกแจงนับโดยชุดการเรียกใช้งานที่แยกต่างหาก เครื่องมือตรวจสอบที่ทดสอบแอ็คชันตามประเภทกับค่าคงที่ของ JavaScript ที่จินตนาการขึ้นเอง เมื่อคอมไพล์ รัน และก็จะไม่พบอะไรเลยตลอดกาล

const
  PDFACTION_GOTO         = 1;   // in-document jump: harmless
  PDFACTION_REMOTEGOTO   = 2;   // jump into another local file
  PDFACTION_URI          = 3;   // opens an external URL
  PDFACTION_LAUNCH       = 4;   // starts an external program
  PDFACTION_EMBEDDEDGOTO = 5;   // jump into an embedded file

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;                 // destination-only link, nothing to flag
    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 stays silent by design
  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;

การแบ่งระดับความรุนแรงเป็นการเข้ารหัสนโยบาย แอ็คชันประเภท Launch ถือเป็นข้อผิดพลาด เนื่องจากการสั่งรันโปรแกรมแบบสุ่ม (arbitrary program) เป็นสิ่งที่อันตรายที่สุดที่การคลิกภายใน PDF จะสามารถทำได้ และคงไม่มีใบแจ้งหนี้ใบไหนที่จะต้องใช้ฟีเจอร์นี้ URI ภายนอกจะถือเป็นคำเตือน: แม้จะเป็นเรื่องปกติสำหรับเอกสารที่ถูกต้องตามกฎหมาย แต่ผู้ตรวจสอบก็ควรจะได้เห็นเป้าหมายโดยไม่ต้องคลิกก่อน เนื่องจากข้อความลิงก์ที่มองเห็นกับปลายทางที่แท้จริงนั้นไม่จำเป็นต้องตรงกัน การข้าม (GoTo) ภายในเอกสารเป็นเรื่องของโครงสร้าง ไม่ใช่พฤติกรรม และควรเก็บมันเอาไว้นอกรายงานโดยสิ้นเชิง — เครื่องมือพรีไฟลท์ที่คอยร้องเตือนเหมือนเด็กเลี้ยงแกะ (cries wolf) ไปกับทุกๆ เอนทรีของสารบัญ จะฝึกให้ผู้คนคุ้นชินกับการเพิกเฉยมัน สำหรับการอ่านเนื้อหาสคริปต์ที่อยู่เบื้องหลังการนับของ JavaScript รวมถึงระดับลายเซ็น MDP และการตรวจจับ XFA บทความเกี่ยวกับการตรวจสอบความเสี่ยงด้านความปลอดภัย ได้อธิบายเกี่ยวกับพื้นผิวการทำงานเดียวกันผ่านออบเจ็กต์แรปเปอร์ของคอมโพเนนต์นี้ไว้แล้ว

ตัวชี้วัดทรัพยากร: DPI ของรูปภาพที่มีผล

รูปภาพภายในเอกสาร PDF จะไม่มี DPI เป็นของตัวเอง แต่มันมีเพียงพิกเซล จากนั้นหน้าเอกสารก็จะจัดวางพิกเซลเหล่านั้นลงในสี่เหลี่ยมผืนผ้าที่วัดหน่วยเป็นพอยต์ (point) โดยที่ 72 พอยต์จะเท่ากับหนึ่งนิ้ว ความละเอียดนั้นมีอยู่เพียงเพื่อเป็นอัตราส่วนระหว่างสองสิ่งนี้ ซึ่งเป็นเหตุผลที่ว่าทำไมรูปภาพขนาด 600 x 400 ภาพเดียวกันจึงคมชัดกริบเมื่อใช้เป็นรูปภาพขนาดย่อ (thumbnail) แต่กลายเป็นความยุ่งเหยิงที่เบลอเมื่อเป็นภาพหลักเต็มหน้า ดังนั้นการตรวจสอบจึงต้องการตัวเลขทั้งสองตัวสำหรับทุกรูปภาพ: มิติพิกเซลของภาพต้นฉบับจากข้อมูลเมตาของภาพ และสี่เหลี่ยมที่จัดวางจากขอบเขตของออบเจ็กต์

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;              // placed size on the page, in points
    HeightPt := T - B;
    if (WidthPt <= 0) or (HeightPt <= 0) or
       (Meta.Width = 0) or (Meta.Height = 0) then
      Continue;

    // 72 points = 1 inch, so placed inches = points / 72, and
    // effective DPI = source pixels / placed inches.
    DpiX := Meta.Width  / (WidthPt  / 72.0);
    DpiY := Meta.Height / (HeightPt / 72.0);
    EffDpi := Min(DpiX, DpiY);      // the worse axis decides print quality

    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;

เกณฑ์เหล่านี้คือนโยบาย ไม่ใช่ฟิสิกส์: 150 DPI คือขั้นต่ำที่การพิมพ์ในสำนักงานจะมองเห็นเป็นพิกเซลได้อย่างชัดเจน 300 คือเป้าหมายปกติในเชิงพาณิชย์ และอะไรก็ตามที่สูงกว่า 600 จะไม่ส่งผลดีต่อคุณภาพที่มองเห็นได้ แต่จะทำให้ไฟล์มีขนาดใหญ่ขึ้น ซึ่งเป็นสาเหตุที่มันรายงานข้อมูลว่าบวมมากกว่าเป็นความบกพร่อง ข้อแม้ประการหนึ่งที่ตรงไปตรงมาคือ: FPDFPageObj_GetBounds จะคืนค่ากล่องที่จัดวางตามแกน ดังนั้นสำหรับภาพที่จัดวางโดยมีการหมุน ตัวเลขที่คำนวณได้จะประเมินความหนาแน่นจริงต่ำเกินไป โครงสร้างข้อมูล FPDF_IMAGEOBJ_METADATA จะพกพา horizontal_dpi และ vertical_dpi ซึ่ง PDFium แตกออกมาจากเมทริกซ์การแปลงเต็มรูปแบบ และการเปรียบเทียบผลลัพธ์ทั้งสองวิธีก็เป็นวิธีการที่คุ้มค่าในการตรวจจับการวางตำแหน่งที่มีการหมุน คณิตศาสตร์แบบ points-to-pixels แบบเดียวกันนี้ ขับเคลื่อนการเรนเดอร์ในทิศทางตรงกันข้าม ครอบคลุมไว้ใน บทความเกี่ยวกับการส่งออก JPEG

สถานะความปลอดภัย: การเข้ารหัสและบิตสิทธิ์การเข้าถึง

การเข้ารหัส PDF จะกำหนดรหัสผ่านสองชุดซึ่งทำหน้าที่ต่างกัน รหัสผ่านผู้ใช้ (user password) จะเป็นประตูกั้นการถอดรหัส: หากไม่มีมัน ไฟล์ก็จะไม่เปิดขึ้นมาเลย และ FPDF_LoadDocument จะส่งคืนค่า nil ในขณะที่ FPDF_GetLastError จะรายงาน FPDF_ERR_PASSWORD รหัสผ่านเจ้าของ (owner password) จะเป็นประตูกั้นสิทธิ์การเข้าถึง: ไฟล์ที่ถูกปกป้องด้วยรหัสผ่านของเจ้าของเท่านั้น จะถูกเปิดด้วย credentials ที่ไม่มีสิทธิ์ แต่จะมีบิตข้อจำกัดติดมาด้วยเพื่อให้ตัวอ่านที่รองรับมาตรฐานต้องเคารพตาม ความพยายามในการโหลดจึงเป็นการตรวจสอบความปลอดภัยครั้งแรก และความแตกต่างนี้จะเป็นตัวตัดสิน exit code — ไฟล์ที่ติดรหัสผ่านผู้ใช้จะไม่สามารถตรวจสอบได้ (code 2) ในขณะที่ไฟล์ที่ติดรหัสผ่านเจ้าของจะตรวจสอบได้ตามปกติและจะเก็บข้อมูลที่ค้นพบเอาไว้เพียงอย่างเดียว

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 means the file is not encrypted
  begin
    // Opened with an empty password yet encrypted: owner-password-only.
    // Anyone may read it, but the permission bits restrict what a
    // conforming reader lets them do. Unencrypted files report all
    // bits set, which is why the revision gate comes first.
    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: print
      Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
        'printing is not permitted');
    if (Perms and 16) = 0 then     // bit 5: copy / extract content
      Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
        'content extraction is not permitted');
    if (Perms and 2048) = 0 then   // bit 12: high-resolution print
      Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
        'only low-resolution printing is permitted');
  end;
end;

หน้ากาก (masks) เหล่านี้มาจากตารางที่ 22 ของมาตรฐาน ISO 32000-1 ซึ่งเรียงลำดับตัวเลขเริ่มจาก 1: บิต 3 ของค่า /P คือ mask 4, บิต 5 คือ 16, และบิต 12 คือ 2048 สิ่งที่ค้นพบแต่ละอย่างจะมีความสำคัญหรือไม่ก็ขึ้นอยู่กับการตัดสินใจในการกำหนดเส้นทาง สำนักพิมพ์ควรจะตีกลับไฟล์ SEC-NOPRINT ไปตั้งแต่ตอนรับเข้า ซึ่งเป็นจุดที่ผู้ส่งจะได้รับข้อความที่ชัดเจน ดีกว่าจะไปตีกลับที่เครื่อง RIP ล่วงหน้าก่อนถึงกำหนดส่งงานเพียงแค่สามชั่วโมง คลังเอกสารควรจะปฏิบัติกับ SEC-ENC ราวกับว่ามันเป็นตัวขัดขวางเสียเอง เนื่องจากการเข้ารหัสและการเก็บรักษาในระยะยาวนั้นไม่สามารถอยู่ร่วมกันได้ — ประเด็นที่การตรวจสอบตามมาตรฐานกำลังจะสร้างขึ้นอย่างเป็นทางการ

เครื่องหมายมาตรฐาน: การอ่านการอ้างสิทธิ์ PDF/A

ไฟล์จะประกาศความสอดคล้องกับมาตรฐาน PDF/A ในแพ็กเก็ตข้อมูลเมตา XMP ผ่านพร็อพเพอร์ตี้ pdfaid:part (1 ถึง 4) และ pdfaid:conformance (อักษรระดับชั้น เช่น b สำหรับความถูกต้องของภาพ (visual fidelity) หรือ a สำหรับการแท็กโครงสร้างทั้งหมด) C API ของ PDFium ไม่มีตัวเข้าถึง XMP เลย; FPDF_GetMetaText จะอ่านเฉพาะพจนานุกรม Info เท่านั้น ซึ่งไม่ใช่ที่ตั้งของการระบุข้อมูลนั้น ช่องทางหนีคือมีกฎอยู่ในตัวมาตรฐานเอง: ISO 19005 กำหนดให้ต้องเก็บสตรีมของข้อมูลเมตา XMP ไว้โดยไม่มีการบีบอัด เพื่อให้เครื่องมือต่างๆ สามารถค้นหามันได้โดยไม่ต้องใช้ตัวแยกวิเคราะห์ PDF เต็มรูปแบบ ดังนั้นการสแกนไบต์ดิบจึงเป็นตัวจับอ้างสิทธิ์ (claim detector) ที่ถูกกฎ — และไฟล์ที่มีการอ้างสิทธิ์ซ่อนอยู่ในสตรีมที่ถูกบีบอัดก็ถือเป็นการละเมิดมาตรฐานที่ตัวมันอ้างสิทธิ์อยู่แล้ว

function PdfAClaim(const FileName: string): string;
var
  Bytes: TBytes;
  S: RawByteString;
  P, Limit: Integer;
begin
  Result := '';                     // empty = no PDF/A claim present
  Bytes := TFile.ReadAllBytes(FileName);
  if Length(Bytes) = 0 then
    Exit;
  SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
  P := Pos('pdfaid:part', S);       // XMP identification schema
  if P = 0 then
    Exit;
  // Handles both <pdfaid:part>2</pdfaid:part> and pdfaid:part="2":
  // take the first digit after the property name.
  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;

สิ่งที่ค้นพบเหล่านี้มีจุดมุ่งหมายเพื่อเป็นข้อมูล (informational) เพียงอย่างเดียว เพราะการอ้างสิทธิ์ก็คือการประกาศ (declaration) ไม่ใช่เป็นพร็อพเพอร์ตี้ของไฟล์ การป้อนข้อมูลลง XMP เป็นคำสั่ง XML แบบบรรทัดเดียวที่ผู้ผลิตรายใดก็สามารถเขียนได้ แม้แต่ผู้ผลิตที่มีข้อบกพร่อง; ความสอดคล้อง (conformance) หมายถึงไฟล์นั้นต้องผ่านกฎหลายร้อยข้อเกี่ยวกับฟอนต์ที่ฝังอยู่, การแสดงสีโดยไม่ขึ้นอยู่กับอุปกรณ์ (device-independent color), และฟีเจอร์ที่ต้องห้าม การตรวจจับการอ้างสิทธิ์จะบอกคุณว่าควรส่งต่อไฟล์ใดไปเพื่อตรวจสอบความถูกต้อง (validation) จริงๆ และก็ไม่มีอะไรมากไปกว่านี้ เอนจินพรีไฟลท์ที่ติดตั้งมาพร้อมกับคอมโพเนนต์จะดำเนินการตรวจสอบในโปรไฟล์ PDF/A, PDF/UA, และ PDF/X อย่างครบถ้วน และ บทความเกี่ยวกับแบทช์ CLI จะแสดงให้เห็นถึงวิธีเชื่อมต่อเข้ากับไปป์ไลน์พร้อมด้วยรายงานต่างๆ ที่ผู้ตรวจสอบสามารถเปิดอ่านได้ในภายหลัง

การรันกับไฟล์ที่มีปัญหา

ไดรเวอร์เรียงร้อยการตรวจสอบเข้าด้วยกัน: ความปลอดภัยมาก่อนเป็นอันดับแรก เนื่องจากมันจะเป็นตัวตัดสินว่าจะดำเนินการตรวจสอบทั้งหมดหรือไม่ ถัดมาก็จะเป็นพฤติกรรมระดับเอกสารและการอ้างสิทธิ์ในมาตรฐาน จากนั้นจึงจะเป็นลูปแบบรายหน้าสำหรับแอ็คชันและรูปภาพต่างๆ

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);                        // audit failure, not a verdict
  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;

เมื่อทดสอบกับโบรชัวร์ที่ส่งกลับมาจากเอเจนซี่ภายนอก ผลลัพธ์ก็จะออกมาหน้าตาแบบนี้

> 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

แต่ละบรรทัดสามารถนำไปใช้งาน (actionable) ได้ด้วยตัวของมันเอง แต่การผสมผสานกันต่างหากที่เป็นคำตัดสินที่แท้จริง ไฟล์นี้อ้างว่าเป็น PDF/A-2 แต่กลับพกเอาพจนานุกรมการเข้ารหัสและ JavaScript แบบไลฟ์มาด้วย และ PDF/A ก็สั่งห้ามทั้งสองสิ่งนี้โดยเด็ดขาด — ดังนั้นการอ้างสิทธิ์จึงสามารถพิสูจน์ได้ว่าเป็นเท็จก่อนที่การตรวจสอบเชิงลึกใดๆ จะเริ่มทำงาน สิ่งนี้คือความขัดแย้งประเภทหนึ่งที่รายการค้นพบแบบแบนสามารถหยิบยกขึ้นมาให้เห็นเด่นชัด ในขณะที่สถานะบูลีน (pass/fail) แบบธรรมดาเลือกที่จะซ่อนมันเอาไว้

สิ่งที่การตรวจสอบนี้ไม่สามารถบอกคุณได้

ความซื่อสัตย์เกี่ยวกับขอบเขต คือสิ่งที่ทำให้เครื่องมือพรีไฟลท์ได้รับความไว้วางใจ ทุกสิ่งที่กล่าวมาข้างต้นคือการอ่านสิ่งที่ไฟล์ได้ประกาศเกี่ยวกับตัวเอง: PDFium แยกวิเคราะห์โครงสร้าง และการตรวจสอบนี้ก็นำมันมาประเมิน (inventory) เครื่องมือนี้ไม่ได้ทำการตรวจสอบมาตรฐาน PDF/A แต่อย่างใด — ไม่มีการตรวจสอบความครอบคลุมของไกลฟ์ (glyph-coverage) กับแบบอักษรที่ฝังอยู่, ไม่มีการวิเคราะห์สเปซสีขัดแย้งกับเจตนาการแสดงผลเอาท์พุต, ไม่มีกฎเกณฑ์ระดับเงื่อนไขหรือระดับข้อย่อยที่แยกการอ้างสิทธิ์ออกจากความสอดคล้องอย่างแท้จริง; สำหรับสิ่งนั้นคุณต้องการเครื่องมือที่สร้างขึ้นมาเพื่อตรวจสอบความถูกต้อง (validator) โดยเฉพาะ เช่น เอนจินพรีไฟลท์ของคอมโพเนนต์ตัวนี้ หรือ veraPDF บิตสิทธิ์การเข้าถึงเป็นการประกาศที่มีให้สำหรับโปรแกรมอ่านที่รองรับเพื่อปฏิบัติตาม ไม่ใช่กำแพงกั้นทางวิทยาการเข้ารหัส ดังนั้น SEC-NOPRINT จึงอธิบายถึงความตั้งใจแทนที่จะเป็นการบังคับ การสแกนแอ็คชัน (action scan) ครอบคลุมถึงคำอธิบายประกอบของลิงก์และสคริปต์ระดับเอกสาร; แต่สำหรับสคริปต์ที่ถูกฝังลึกอยู่ในพจนานุกรมเหตุการณ์ของฟิลด์แบบฟอร์ม (form-field event dictionaries) ก็จำเป็นต้องใช้ form APIs เข้ามาช่วยเพิ่มเติม และการตรวจสอบลายเซ็น หากคุณขยายผลการตรวจสอบด้วยสิ่งนี้ มันจะรายงานเพียงเจตนาที่ประกาศไว้ ไม่ใช่การเข้ารหัสที่ผ่านการรับรอง — การตรวจสอบความถูกต้องของสายโซ่ใบรับรอง (certificate chain validation) เป็นงานที่ต้องทำแยกต่างหาก การทำพรีไฟลท์คือการสัมภาษณ์เพื่อรับเข้าทำงาน ไม่ใช่การพิจารณาคดี: หน้าที่ของมันคือการตัดสินใจในการกำหนดเส้นทางให้ได้รับข้อมูลรวดเร็ว และทำซ้ำได้

หมายเหตุ: API ของออบเจ็กต์เอกสาร หน้าเว็บ คำอธิบายประกอบ และรูปภาพที่ใช้ตลอดการตรวจสอบนี้ เมื่อใช้ร่วมกับ Delphi wrapper ระดับสูงและเอนจินพรีไฟลท์สำหรับตรวจสอบมาตรฐานอย่างสมบูรณ์แบบ จะถูกรวมเข้าไว้ด้วยกันใน PDFium Component