Bài viết kỹ thuật

In ấn dữ liệu biến đổi PDF/VT trong Delphi với PDFium VCL

Một xưởng in giao dịch trả lại lô sao kê 80.000 trang của bạn với một dòng từ chối: "không phải PDF/VT, RIP không thể cache." Tệp vẫn mở bình thường trong mọi trình xem trên bàn làm việc của bạn, màu sắc đúng, dữ liệu đã gộp chính xác. Nhưng đó không phải điều máy in số yêu cầu. In dữ liệu biến đổi tốc độ cao sống hay chết ở chỗ máy in nhận ra được rằng khối logo khách hàng ở trang 1 chính là cùng một đối tượng, từng byte một, với khối ở trang 40.000, kết xuất một lần rồi tái sử dụng. PDF/VT là tiêu chuẩn biến lời hứa đó thành thứ máy có thể kiểm tra, và "trông đúng" chính là cái bẫy, vì cấu trúc mà RIP đọc được không hiện ra trên màn hình

PDFiumPas bộc lộ cấu trúc đó qua một bề mặt nhỏ trên TPdf: SaveAsPdfVT ghi nó ra, ValidatePdfVT kiểm tra nó. Bài này nói về việc hai phương thức đó thực sự đặt gì lên đĩa và đọc gì ra, chỗ nào ISO 16612-2 chặt hơn vẻ ban đầu, và phần nào chỉ là mốc cấu trúc trung thực chứ không phải một lượt preflight đầy đủ để tính tiền cho khách

PDF/VT chuẩn hóa gì, và vì sao PDF/X phải đi trước

PDF/VT (ISO 16612-2:2010) không phải là một định dạng tệp mới. Nó là một lớp siêu dữ liệu tối ưu hóa gắn lên trên tệp PDF/X, và thứ tự đó là phần chịu lực. Tiêu chuẩn định nghĩa ba mức tuân thủ, nhưng chỉ hai mức trong số đó gọi tên một tệp PDF: PDF/VT-1, một tài liệu đơn lẻ tự chứa, và PDF/VT-2, mô hình bộ tệp trong đó các trang tham chiếu tới tài nguyên chia sẻ bên ngoài. Mã thứ ba bạn có thể thấy, PDF/VT-2s, hoàn toàn không phải là giá trị ở cấp tệp; nó nằm trong tiêu đề luồng MIME được mô tả ở Phụ lục A. Nếu bạn thấy mã ghi GTS_PDFVTVersion = "PDF/VT-2s" vào XMP của một tài liệu, mã đó sai

Quy tắc không thể thương lượng đối với một tệp đơn là nền PDF/X. ISO 16612-2 §6.2.1 yêu cầu mọi tệp PDF/VT-1 cũng phải là một tệp PDF/X-4 hợp lệ. Bộ tệp PDF/VT-2, theo §6.2.2, thay vào đó phải nằm trên PDF/X-4p, PDF/X-5g, hoặc PDF/X-5pg. Vì vậy một trình ghi PDF/VT không thể chỉ nối thêm vài khóa nhận dạng: nó phải mang theo toàn bộ bộ đánh dấu PDF/X-4, nghĩa là một OutputIntent, một hồ sơ đích ICC nhúng, các mục XMP và Info của tài liệu khớp nhau, một /ID ở trailer, và không được mã hóa. Bỏ sót bất kỳ thứ nào trong số đó là bạn có một tệp tự nhận PDF/VT nhưng sẽ thất bại ngay khi một bộ tiêu thụ tuân thủ kiểm tra nền tảng. PDFiumPas xem lớp PDF/X-4 như một phần của lần lưu PDF/VT, nên bạn không gọi SaveAsPdfX riêng trước; bộ chèn ghi cả hai lớp trong một lượt

Ghi tệp bằng SaveAsPdfVT

Lời gọi tối thiểu không cần gì ngoài một tài liệu đang hoạt động, vì TPdfVTSaveOptions.Default cung cấp sẵn một hồ sơ ICC sRGB tích hợp và mức tuân thủ pvc1. Quy trình lưu chạy ba bước bên trong: nó gỡ bỏ mọi bảo vệ (đặt các dấu thuần văn bản vào một luồng đối tượng đã mã hóa sẽ làm hỏng luồng đó), nó nối từ điển Info hiện có của tài liệu và trailer /ID vào bộ đánh dấu để giá trị XMP và Info khớp nhau, rồi nó nối thêm các đối tượng PDF/X-4 và PDF/VT thông qua một cập nhật tăng dần

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    if Pdf.LoadFromFile('statements-merged.pdf') then
    begin
      // Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
      if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
        Writeln('PDF/VT-1 written')
      else
        Writeln('Save failed (document not active?)');
    end;
  finally
    Pdf.Free;
  end;
end;

Với đầu ra sản xuất thực sự, bạn gần như luôn muốn ghi đè OutputIntent bằng đặc tính của máy in thay vì phương án dự phòng sRGB chung chung. Hãy cung cấp các byte ICC và các định danh điều kiện qua TPdfVTSaveOptions:

var
  Pdf: TPdf;
  Opt: TPdfVTSaveOptions;
  Icc: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('directmail-merged.pdf');
    Icc := LoadIccProfile('GRACoL2013_CRPC6.icc');  // your own loader

    Opt := TPdfVTSaveOptions.Default;
    Opt.Conformance := pvc1;            // pvc2 is normalised to pvc1 on write
    Opt.IccProfileData := Icc;
    Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
    Opt.OutputCondition := 'Commercial print, coated, CRPC6';
    Opt.RegistryName := 'http://www.color.org';
    Opt.Title := 'Spring 2026 Direct Mail Run';
    Opt.Trapped := ptvFalse;           // PDF/X Info /Trapped state

    Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

Một chi tiết trong đoạn đó là rào chắn có chủ ý chứ không phải giới hạn để tranh cãi. Đặt Opt.Conformance := pvc2 không tạo ra tệp PDF/VT-2. Bộ ghi chuẩn hóa mọi yêu cầu khác pvc1 về lại pvc1, vì PDF/VT-2 là định dạng bộ tệp và một trình ghi một tệp duy nhất chỉ nối thêm một tài liệu đầu ra không thể tự mình dựng bộ tài nguyên bên ngoài mà §6.2.2 đòi hỏi. Giá trị pvc2 tồn tại cho đường đọc, để ValidatePdfVT có thể nhận ra và báo một tài liệu bộ tệp đã tồn tại; nó không phải mục tiêu ghi

Cây DPart: cấu trúc mà RIP thực sự đọc

Trái tim của PDF/VT là hệ thống phân cấp Document Part (DPart). Nó cho phép một máy in chia một lô dài thành các bản ghi, nhóm bản ghi thành người nhận hoặc túi thư, và gắn Document Part Metadata để thiết bị phía sau có thể định tuyến và tính phí cho từng phần. ISO 16612-2 §6.5 vạch ra sơ đồ nối dây: catalog mang /DPartRoot, nút DPart gốc mang /DPartRootNode/NodeNameList đặt tên cho từng cấp của hệ thống phân cấp, các DPart lá bao phủ phạm vi của cây trang, và mọi trang thuộc về một phần đều trỏ ngược lại lá của nó qua mục /DPart ở cấp trang

Khi tài liệu nguồn của bạn đã có sẵn một hệ thống phân cấp dùng được, SaveAsPdfVT sẽ giữ nguyên nó. Khi không có, bộ ghi tạo ra một hệ tối thiểu: một DPart cấp tài liệu duy nhất phủ theo thứ tự cây trang hiện tại, với một tham chiếu ngược /DPart được thêm vào từng đối tượng trang còn sống và một /NodeNameList [/Document] một cấp. Hãy thành thật về bản chất của cây tối thiểu đó. Nó là một mốc cấu trúc thỏa yêu cầu hình dạng của §6.5; nó không phải siêu dữ liệu nghiệp vụ. Nó không thể tự bịa ra người nhận, ranh giới từng bưu phẩm, hay các lô sản phẩm, vì thông tin đó chưa từng có trong nguồn. Nếu bạn có dữ liệu theo từng người nhận, bạn được kỳ vọng tự dựng một cây DPart sâu hơn và mở rộng /NodeNameList cho khớp với các cấp bạn tạo

Xác thực vượt xa việc chỉ có khóa

ValidatePdfVT trả về một bản ghi TPdfVTValidationResult với ba thứ: Conformance được phát hiện, một tập Issues, và một trợ giúp IsCompliant chỉ đúng khi mức tuân thủ là một mức thật và tập lỗi trống. Danh sách lỗi được đặt riêng rất cụ thể, nên kết quả thất bại sẽ cho bạn biết bạn bỏ sót điều khoản nào thay vì chỉ nói "không hợp lệ":

var
  Pdf: TPdf;
  Res: TPdfVTValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('statements-pdfvt.pdf');
    Res := Pdf.ValidatePdfVT;

    if Res.IsCompliant then
      Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
    else
    begin
      if pvviMissingDPartRoot in Res.Issues then
        Writeln('DPart hierarchy missing or unusable');
      if pvviMissingPdfXIdentifier in Res.Issues then
        Writeln('PDF/X-4 base identifier absent');
      if pvviMissingOutputIntent in Res.Issues then
        Writeln('OutputIntent / ICC profile missing');
      if pvviEncryptionPresent in Res.Issues then
        Writeln('Encrypted - PDF/X forbids this');
    end;
  finally
    Pdf.Free;
  end;
end;

Hai kiểm tra đáng hiểu sâu là cặp tuân thủ và phép đi qua DPart, vì cả hai từng quá dễ dãi và đã được siết lại để khớp tiêu chuẩn. Ở phía ghép cặp, bộ xác thực so khớp chính xác chứ không phải "bất kỳ PDF/X nào cũng được": tệp PDF/VT-1 chỉ được chấp nhận trên nền PDF/X-4, và tệp PDF/VT-2 chỉ trên PDF/X-4p, PDF/X-5g, hoặc PDF/X-5pg. Một dấu PDF/VT-1 đặt trên nền PDF/X-1a sẽ bị báo lỗi, không được cho qua

Phép đi qua DPart là nơi phần lớn tính nghiêm ngặt nằm ở đó. Không đủ chỉ để catalog có khóa /DPartRoot, vì một đối tượng rỗng giả mạo hoặc một đối tượng không có liên kết trang vẫn không thể được tiêu thụ. HasValidDPartHierarchy và hàm đệ quy ValidateDPartNode truy vết toàn bộ cấu trúc: chúng theo các liên kết cha, từ chối con trùng lặp và chu trình, buộc /Start/DParts loại trừ lẫn nhau, và yêu cầu phạm vi trang lá phải bao phủ cây trang theo thứ tự duyệt sâu trước với mỗi /DPart của trang trỏ tới lá chứa nó. Tất cả các lỗi nội bộ đó gộp về cùng một bit lỗi pvviMissingDPartRoot thay vì mở rộng enum công khai, vì vậy hãy xem cờ đó như "hệ thống phân cấp DPart không dùng được", chứ không phải theo nghĩa đen là "thiếu khóa gốc"

Ba bẫy cú pháp mà bộ xác thực nay áp dụng

Các lượt kiểm tra liên tiếp theo §6.5 Bảng 4 đã phát hiện những dạng mà các phiên bản trước chấp nhận nhưng tiêu chuẩn không cho phép. Đây là những kiểu mà một cây DPart dựng tay rất dễ làm sai, nên đáng nói riêng ra:

  • /DParts là một mảng các mảng, không phải một mảng phẳng. Mỗi phần tử của mảng ngoài cũng phải là một mảng tham chiếu gián tiếp. Một /DParts [9 0 R] phẳng bị từ chối; dạng đúng là /DParts [[9 0 R] [10 0 R]]. Điều này ngăn một cấu trúc không phân cấp giả dạng thành một cấp hợp lệ
  • /End chỉ đánh dấu một phạm vi nhiều trang thực sự. Một DPart lá chỉ được mang /End khi nó cũng có /Start, và /End phải nằm sau /Start trong thứ tự cây trang. Một trường hợp thoái hóa /Start 3 0 R /End 3 0 R giờ làm hệ thống phân cấp không dùng được thay vì bị đọc như một phần một trang
  • Tên /NodeNameList phải sống sót qua giải mã tên PDF dưới dạng XML NMTOKEN. Một tên như /Bad#20Name khi bung ra sẽ chứa dấu cách, vốn không phải token hợp lệ. Cách triển khai làm một kiểm tra ASCII nhẹ (chữ cái, chữ số, ., -, _, :, cộng với byte không phải ASCII) để bắt lỗi khoảng trắng và dấu phân tách mà không loại nhầm các tên bản địa hóa hoặc do nhà cung cấp đặt

Dấu XMP: hai cách ghi cùng một thuộc tính

Việc nhận dạng PDF/VT nằm trong XMP dưới không gian tên pdfvtid, cụ thể là GTS_PDFVTVersionGTS_PDFVTModDate, cạnh các chuẩn xmp:CreateDatexmp:ModifyDate. Một tinh tế dễ gây báo "thiếu" sai trong các bộ đọc ngây thơ là bất kỳ trường nào trong số này cũng có thể được tuần tự hóa theo hai cách: dưới dạng văn bản phần tử (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) hoặc dưới dạng thuộc tính RDF trên phần tử mô tả. PDFiumPas đọc cả hai dạng, nên một tệp do công cụ khác ghi theo kiểu thuộc tính sẽ không bị chấm phạt. Nó cũng thực thi quy tắc nhất quán ở §6.3 rằng GTS_PDFVTModDate phải bằng xmp:ModifyDate; lệch nhau sẽ sinh pvviModDateMismatch

Một quy tắc nữa từ cùng điều khoản: một giá trị GTS_PDFVTVersion không xác định sẽ được giữ nguyên là pvcUnknown thay vì bị kéo ngược về pvcNone. Phân biệt đó rất quan trọng trong vận hành. pvcNone nghĩa là "không có dấu PDF/VT nào cả, một PDF thông thường", còn pvcUnknown nghĩa là "có thứ gì đó gắn một phiên bản mà bộ xác thực này không nhận ra" (trong đó có trường hợp PDF/VT-2s). Gộp hai thứ này lại sẽ giấu một tệp lỗi trong cùng một ngăn với một tài liệu thuần

Phạm vi của bảo đảm

Cần nói thật chính xác về ranh giới những gì các phương thức này cam kết, vì tuân thủ in dữ liệu biến đổi gắn với tiền thật. Các kiểm tra DPart và ghép cặp là xác thực cấu trúc ở mức byte. Chúng xác nhận rằng bộ khung tối ưu hóa, các dấu nền PDF/X-4, OutputIntent, và XMP đều hiện diện và nhất quán nội bộ. Chúng không phải là preflight PDF/X-4 ở mức nội dung: chúng không kiểm tra mọi màu có nằm trong điều kiện đầu ra đã khai báo hay không, mọi phông chữ đã được nhúng hay chưa, hoặc có lọt vào một trường hợp pha trộn trong suốt bị cấm nào không. Với một công việc bạn đem đi chạy ở nhà in theo hợp đồng, hãy ghép xác thực cấu trúc của PDFiumPas với một bộ preflight PDF/X chuyên dụng và một bản in thử, giống như cách bạn tự kiểm tra bất kỳ tuyên bố tuân thủ nào khác. Lớp cấu trúc bắt những lỗi âm thầm làm hỏng cache của RIP; nó là một nửa của phép kiểm tra đầy đủ, không phải toàn bộ

Nếu bạn đang đưa các kiểm tra này vào một cổng phát hành rộng hơn, cùng cách quét ở mức byte này cũng là nền cho các công việc tiêu chuẩn khác của thư viện, bao gồm kiểm tra các luồng object và cross-reference trước khi một tệp chạm vào preflight, và kỷ luật đối tượng chia sẻ phía sau các con dấu trang tái sử dụng với Form XObjects giúp một tài liệu thân thiện với RIP ngay từ đầu. Các API lưu và xác thực PDF/VT và PDF/X được mô tả ở đây là một phần của thành phần PDFium VCL cho Delphi và C++Builder, nơi trang sản phẩm có tham chiếu tuân thủ đầy đủ