Bài viết kỹ thuật

Xác thực PDF/X trong Delphi với PDFium Component

Thành phần PDFium Component dành cho Delphi xác thực các tài liệu PDF/X sẵn sàng in thông qua TPdf.ValidatePdfX, vốn triển khai việc kiểm tra tiêu chuẩn ISO 15930 theo hai lớp: tám phép kiểm tra nội dung ở cấp độ byte (cấm nén LZW, JavaScript, các trường biểu mẫu, các tham chiếu OPI, thiếu TrimBox, khóa Trapped chưa được thiết lập, v.v.) cùng với một lượt duyệt mô hình đối tượng PDFium sử dụng FPDFFont_GetIsEmbedded để xác minh việc nhúng phông chữ trên mọi đối tượng văn bản của mọi trang. Kết quả là một bản ghi TPdfXValidationResult nêu tên cấp độ tuân thủ phát hiện được và liệt kê từng lỗi vi phạm dưới dạng một kiểu liệt kê được định kiểu, nhờ đó ứng dụng Delphi của bạn có thể cho khách hàng biết chính xác lý do tại sao một tệp tin sẽ bị từ chối tại xưởng in trước khi có ai đó thực hiện ghi bản kẽm

Nếu bạn đã từng chuyển phát nhanh một sản phẩm đến một nhà in thương mại và nhận lại nó với một dòng từ chối duy nhất — "không có TrimBox", "phông chữ không được nhúng", "chưa thiết lập Trapped" — bạn sẽ biết chi phí của việc phát hiện ra lỗi quá muộn. Tiêu chuẩn PDF/X là phần đối ứng trước khi in (prepress) của PDF/A: trong khi PDF/A lưu trữ đảm bảo một tài liệu kết xuất giống hệt nhau nhiều thập kỷ sau, PDF/X đảm bảo một tài liệu phân tách màu, tạo ảnh và cắt xén giống hệt nhau trên hệ thống RIP của người khác vào sáng ngày mai. Hai tiêu chuẩn này chia sẻ chung cơ chế (định danh XMP, OutputIntents, các profile ICC nhúng sẵn) nhưng trả lời các câu hỏi khác nhau, đó là lý do tại sao thành phần này phân phối các bộ xác thực riêng biệt cho từng loại — khía cạnh PDF/A được đề cập trong bài viết xác thực trước khi bay PDF/A với PDFium Component

Tiêu chuẩn ISO 15930 thực sự yêu cầu những gì từ một tệp PDF sẵn sàng in?

Tiêu chuẩn ISO 15930 tồn tại để hiện thực hóa việc trao đổi mù (blind exchange): một nhà thiết kế giao một tệp tin cho một nhà in mà họ chưa từng nói chuyện cùng, và nhà in vẫn có thể tạo ra sản phẩm đầu ra chính xác mà không cần cuộc gọi điện thoại nào, không cần email báo thiếu phông chữ, và không có hình ảnh liên kết nào bị bỏ quên trên máy tính xách tay của nhà thiết kế. Mọi quy tắc trong tiêu chuẩn đều phục vụ mục tiêu đó. Các phông chữ bắt buộc phải được nhúng bởi vì hệ thống RIP nhận tệp không thể giả định là sở hữu chúng. Các tham chiếu bên ngoài bị cấm vì tệp tin phải tự hoàn chỉnh. Các tính năng tương tác bị cấm vì mực in không có trình xử lý sự kiện onclick

Thành phần PDFium Component nhận diện ba nhóm tuân thủ (conformance families) và báo cáo chúng thông qua kiểu liệt kê TPdfXConformance trong kết quả xác thực: pxc1a cho PDF/X-1a:2001 (ISO 15930-1, mức cơ sở CMYK-cộng-màu-pha nghiêm ngặt trên nền PDF 1.3/1.4), pxc3 cho PDF/X-3:2002 (ISO 15930-3, chấp nhận màu RGB, Lab và màu được quản lý bởi ICC), và pxc4 cho PDF/X-4:2010 (ISO 15930-7, cuối cùng đã cho phép độ trong suốt thực tế và các lớp trên nền PDF 1.6). Một tệp hoàn toàn không mang định danh PDF/X nào sẽ trả về kết quả là pxcNone, đây bản thân nó là một câu trả lời hữu ích: tài liệu chưa từng tuyên bố là sẵn sàng để in, và mọi thứ khác mà bộ xác thực báo cáo giải thích những gì cần làm để đạt được điều đó

Các lệnh cấm sẽ trở nên hợp lý một khi bạn suy nghĩ giống như một nhà cung cấp hệ thống RIP. Bộ lọc /LZWDecode bị cấm trong mọi biến thể PDF/X để cho một bên tiêu dùng tuân thủ không bao giờ phụ thuộc vào một bộ lọc có lịch sử về khả năng tương thích và cấp phép; Flate thực hiện cùng công việc mà không mang theo gánh nặng đó. JavaScript, các trường AcroForm và các từ điển hành động bổ sung /AA bị cấm bởi vì một tệp in phải là một mô tả cố định các vết mực trên giấy — bất kỳ thứ gì có thể đột biến diện mạo tại thời điểm mở tệp đều phá vỡ sự đảm bảo rằng những gì được duyệt thử (proofed) là những gì được in ra. Các trình giữ chỗ OPI (Open Prepress Interface) bị cấm bởi vì theo thiết kế, chúng là các tham chiếu đến các hình ảnh độ phân giải cao được lưu trữ ở một nơi khác, và "somewhere else" chính là điều mà việc trao đổi mù cấm đoán

Tại sao các xưởng in lại từ chối các tệp PDF không có TrimBox?

TrimBox là trang thành phẩm — hình chữ nhật còn lại sau khi máy cắt giấy thực hiện nhát cắt. MediaBox, thứ mà mọi trang PDF đều có, chỉ đơn thuần là tờ giấy in: nó bao gồm phần tràn lề (bleed), các dấu cắt xén, các mục tiêu đăng ký (registration targets) và các thanh màu sắc. Phần mềm bình trang (imposition software) định vị các trang trên một tờ in dựa theo các TrimBox của chúng; nếu không có TrimBox, người vận hành phải đoán xem danh thiếp của bạn thực sự kết thúc ở đâu, và một phỏng đoán sai sẽ cắt mất phần tràn lề của bạn hoặc để lại một vệt trắng ở một cạnh. Đó là lý do tại sao tiêu chuẩn ISO 15930 yêu cầu một TrimBox (hoặc một ArtBox) trên mỗi trang, và tại sao ValidatePdfX nâng lỗi pvxiMissingTrimBox khi không tìm thấy khóa /TrimBox trên bất kỳ trang nào của tài liệu

Khóa /Trapped trả lời cho một câu hỏi sản xuất khác. Bẫy màu (trapping) là kỹ thuật trước khi in nhằm chồng lấn nhẹ các màu liền kề để sự lệch đăng ký máy in nhỏ không tạo ra các khoảng trống màu trắng giữa chúng. Nhà in cần biết liệu công việc đó đã được thực hiện hay chưa: bẫy màu một tệp tin đã được bẫy màu sẽ nhân đôi các phần chồng lấn, và bỏ qua bẫy màu trên một tệp tin chưa được bẫy màu có nguy cơ xuất hiện các khoảng trống nhìn thấy được. Do đó, tiêu chuẩn PDF/X yêu cầu từ điển Info phải nêu rõ /Trapped /True hoặc /Trapped /False một cách rõ ràng — một khóa bị thiếu hoặc giá trị /Unknown buộc con người phải kiểm tra tệp tin theo cách thủ công, đây chính xác là cuộc hội thoại mà việc trao đổi mù được sinh ra để loại bỏ. Thành phần này gắn cờ vấn đề này dưới mã lỗi pvxiTrappedNotSet

Chạy xác thực hai lớp bằng TPdf.ValidatePdfX

Hàm TPdf.ValidatePdfX không nhận đối số nào và trả về một bản ghi TPdfXValidationResult với ba thành viên: Conformance (dạng PDF/X phát hiện được), Issues (a Pascal set of TPdfXValidationIssue values), and an IsCompliant helper. Internally it serializes the loaded document to a memory stream, runs the byte-level inspector over it, and then walks the PDFium object model for the per-font embedding check. A minimal preflight gate looks like this:

uses PDFium, FPdfPdfx;

procedure CheckPrintReadiness(const FileName: string);
var
  Pdf: TPdf;
  Res: TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;

    Res := Pdf.ValidatePdfX;
    Writeln('Detected conformance: ',
      Ord(Res.Conformance)); // pxc1a, pxc3, pxc4, pxcNone...

    if Res.IsCompliant then
      Writeln('PDF/X checks passed')
    else
    begin
      if pvxiMissingTrimBox in Res.Issues then
        Writeln('REJECT: no /TrimBox on the pages');
      if pvxiTrappedNotSet in Res.Issues then
        Writeln('REJECT: /Trapped missing or /Unknown');
      if pvxiPdfiumFontNotEmbedded in Res.Issues then
        Writeln('REJECT: a page uses a non-embedded font');
      if pvxiLzwForbidden in Res.Issues then
        Writeln('REJECT: LZWDecode filter present');
    end;
  finally
    Pdf.Free;
  end;
end;

Bởi vì Issues is an ordinary Pascal set, you can partition it however your workflow needs — treat structural problems as hard rejects, treat pvxiMissingTitle (a SHOULD in the standard, not a MUST) as a warning, and log the rest. The same record type also feeds the component's report generator, so if you would rather emit a human-readable document than branch on enums, the pattern in building a batch preflight report CLI with the PDFium Component applies to PDF/X unchanged

Những gì lớp cấp độ byte bắt được — và những gì nó bỏ lỡ

Lớp cấp độ byte là một lượt quét mã thông báo (token scan) trên các byte cấu trúc của tài liệu với phần thân của các luồng (stream bodies) được làm trống, nhờ đó một tệp JPEG ngẫu nhiên chứa mẫu byte /JavaScript sẽ không thể kích hoạt một cảnh báo giả. Bên cạnh các phép kiểm tra dấu hiệu (XMP pdfxid:GTS_PDFXVersion, OutputIntent với một profile ICC được nhúng, trailer /ID, lệnh cấm mã hóa), lượt duyệt nội dung thêm vào tám phép kiểm tra, mỗi phép có giá trị enum riêng:

  • pvxiLzwForbidden — một bộ lọc /LZWDecode xuất hiện ở bất kỳ vị trí nào trong tệp (bị cấm trong tất cả các biến thể PDF/X)
  • pvxiJavaScriptForbidden — một hành động hoặc cây tên /JavaScript hiện diện
  • pvxiFormFieldsForbidden — một từ điển /AcroForm hoặc mục nhập /XFA tồn tại
  • pvxiAdditionalActions — một từ điển các hành động bổ sung /AA hiện diện
  • pvxiEmbeddedFilesForbidden — mục /EmbeddedFiles hoặc chú thích /FileAttachment hiện diện
  • pvxiOpiForbidden — một mục nhập /OPI hoặc /Alternates tham chiếu đến nội dung hình ảnh có thể thay thế
  • pvxiMissingTrimBox — không tìm thấy TrimBox trên bất kỳ trang nào
  • pvxiTrappedNotSet — thiếu /Trapped hoặc được thiết lập thành /Unknown

Quét byte rất nhanh và không cần công cụ kết xuất, nhưng nó có một điểm mù cố hữu với phông chữ: ở cấp độ đó, bộ kiểm tra chỉ có thể áp dụng một heuristic thô — nó gắn cờ một tài liệu khi hoàn toàn không tìm thấy chương trình phông chữ được nhúng nào. Một tệp tin có chín phông chữ được nhúng và một phông chữ hệ thống bị lọt vào trông vẫn ổn đối với một lượt quét byte. Khoảng trống duy nhất đó là lý do tại sao lớp thứ hai tồn tại

Nhúng phông chữ cho từng phông chữ thông qua mô hình đối tượng PDFium

Lớp mô hình đối tượng của PDFium Component trả lời câu hỏi về phông chữ một cách chính xác. Sau lượt duyệt cấp độ byte, TPdf.ValidatePdfX lặp qua mọi trang, yêu cầu FPDFPage_CountObjects cung cấp danh sách đối tượng, và đối với mỗi đối tượng văn bản, nó phân giải handle phông chữ qua FPDFTextObj_GetFont và truy vấn FPDFFont_GetIsEmbedded. Một phông chữ không được nhúng ở bất kỳ vị trí nào trong tài liệu sẽ thêm lỗi pvxiPdfiumFontNotEmbedded vào tập hợp vấn đề. Quá trình duyệt cắt ngắn mạch (short-circuits) ở hai cấp độ — nó dừng quét các đối tượng trên một trang, và dừng tải các trang tiếp theo, ngay tại thời điểm vấn đề được xác nhận — do đó trên một danh mục vi phạm dài 300 trang, phán quyết thường xuất hiện ngay sau trang đầu tiên

Có hai lưu ý ranh giới đáng biết. Thứ nhất, lớp này cần thư viện PDFium được tải và yêu cầu các bản dựng xuất bản hàm FPDFFont_GetIsEmbedded; khi thiếu export này, phép kiểm tra sẽ bị bỏ qua thay vì báo lỗi, do đó một DLL cũ hơn không bao giờ tạo ra các từ chối ma. Thứ hai, phép kiểm tra trả lời câu hỏi "có nhúng hay không" và không có gì hơn — nó không phân biệt việc nhúng đầy đủ với việc nhúng tập hợp con (subsetting), cũng như không kiểm tra mức độ bao phủ của glyph. Khi một tệp tin bị lỗi và bạn cần biết phông chữ nào trên trang nào, các kỹ thuật liệt kê trong bài viết phân tích các thuộc tính phông chữ PDF với PDFium trong Delphi sẽ tiếp quản chính xác nơi giá trị boolean của bộ xác thực dừng lại

Xác thực các luồng mà không cần tải tài liệu — hoặc DLL

Bộ kiểm tra cấp độ byte cũng được phơi bày dưới dạng một hàm độc lập, ValidatePdfXCompliance(Source: TStream) trong đơn vị mã nguồn FPdfPdfx, và nó là mã Object Pascal thuần túy không có phụ thuộc vào tệp DLL PDFium. Điều đó giúp nó có thể triển khai ở những nơi công cụ kết xuất không được chào đón: một cổng tải lên dung lượng nhẹ trên một máy chủ web, một tác vụ CI kiểm duyệt các tác phẩm nghệ thuật được tạo ra, hoặc một dịch vụ Lazarus trên một nền tảng mà bạn không muốn phân phối các tệp nhị phân gốc. Cung cấp cho nó bất kỳ luồng dữ liệu hỗ trợ tìm kiếm (seekable stream) nào:

uses Classes, FPdfPdfx;

function QuickPdfXGate(const FileName: string): Boolean;
var
  Fs: TFileStream;
  Res: TPdfXValidationResult;
begin
  Fs := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfXCompliance(Fs);
    Result := Res.IsCompliant and (Res.Conformance <> pxcNone);
  finally
    Fs.Free;
  end;
end;

Sự đánh đổi ở đây là rõ ràng: đường dẫn độc lập chạy các phép kiểm tra dấu hiệu và tất cả tám phép kiểm tra nội dung, nhưng không có lớp PDFium cho từng phông chữ, do đó phán quyết về phông chữ của nó quay về heuristic thô. Một kiến trúc hợp lý sử dụng ValidatePdfXCompliance làm cổng đầu tiên giá rẻ và dành riêng tính năng TPdf.ValidatePdfX đầy đủ cho các tệp tin đã vượt qua cổng đó

Nơi bộ xác thực này dừng lại và một lượt kiểm tra trước khi in đầy đủ bắt đầu

Sự trung thực rất quan trọng trong các công cụ preflight, do đó đây là ranh giới. ValidatePdfX xác minh các dấu hiệu định danh, các lệnh cấm cấu trúc, các khóa hình học trang, khai báo Trapped và việc nhúng phông chữ xuống tận các đối tượng văn bản riêng lẻ. Nó không đo lường tổng độ phủ mực (total ink coverage), xác thực xem mọi không gian màu có hợp lệ đối với biến thể được tuyên bố hay không (chẳng hạn quy tắc chỉ CMYK của X-1a), kiểm tra độ phân giải hình ảnh đối chiếu với màn hình dòng (line screen), hoặc đánh giá hành vi in đè (overprint) và làm phẳng độ trong suốt (transparency flattening) — những công việc đó cần một công cụ preflight quản lý màu sắc, và tài liệu hướng dẫn của chính đơn vị mã nguồn này khuyên bạn nên ghép đôi nó với một công cụ như vậy để chứng nhận cuối cùng. Những gì phép kiểm tra hai lớp cung cấp cho bạn là 80% các lỗi từ chối thuộc về cấu trúc và có thể phát hiện sớm, được bắt trong vài phần nghìn giây ngay trong mã Delphi của chính bạn thay vì nhận email báo lỗi từ nhà in vào ngày mai

Cả hai lớp xác thực, các API tiêm dấu hiệu PDF/X để tạo ra tài liệu đầu ra tuân thủ, cùng với các bộ xác thực PDF/A, PDF/UA, PDF/E và PDF/VT chia sẻ chung kiến trúc đều được phân phối trong PDFium Component dành cho Delphi và C++Builder — một thành phần duy nhất, từ kết xuất đến kiểm duyệt trước khi in