Bài viết kỹ thuật

Preflight phiên bản PDF trong Delphi: luật Measure RL và GEO

PDFlibPas (PDF Library for Delphi) đối chiếu mọi object với một bảng luật phiên bản PDF trước khi ghi tệp, và cho tới gần đây, preflight phiên bản PDF ấy còn nhầm dictionary đo đạc CAD thường với dictionary địa lý không gian. Một bản vẽ CAD một trang nạp ngon lành, rồi SaveToFile trả về 0 với LastErrorCode 602 và đòi 1.7 ExtensionLevel 3. Bộ luật đã sửa coi các dictionary /Measure trực giao (/Subtype /RL) là PDF 1.6 thuần, và giữ cổng extension cho các dấu hiệu địa lý thật sự

Tệp đi vào qua kho admitting corpus: một trang, một nhóm optional-content, hai viewport đo trực giao, kiểu output mà một bộ CAD kiến trúc ghi ra để trình xem có thể đọc khoảng cách từ bản vẽ mặt bằng. Chẳng gì ở nó exotic cả, và chính vì thế mà lời từ chối đáng được chú ý. Một preflight chặn một tệp hợp lệ còn tệ hơn một preflight chậm, vì bên gọi nhận được một chẩn đoán nhìn rất authority, trỏ vào một tính năng mà tài liệu không hề chứa. Bản sửa gồm hai phần: cách đọc đặc tả đứng sau một luật, và nhận ra rằng luật đó ở tầm nhìn của nó thì không thể phân biệt hai kiểu dictionary với nhau

Preflight phiên bản lúc lưu của PDFlibPas hoạt động thế nào?

Cổng lúc lưu, PrepareAndCheckSaveVersion, so sánh mọi indirect object với PDFFeatureRules và thất bại ở luật đầu tiên vừa khớp vừa đòi hỏi nhiều hơn mức đích cho phép. Đích là phiên bản tài liệu (hay phiên bản bị ghim bởi LockSaveVersion), cộng với mức extension Adobe khai báo dưới /Extensions /ADBE. Mỗi record TPDFFeatureRule mang một MinVersion, một MinExtensionLevel, một MatchKind như fmkDictKey hay fmkDictSubtype, một chuỗi Match, một tên Feature dễ đọc và một callback tùy chọn. AddRule đăng ký một luật phiên bản thường; AddExtensionRule luôn ghim MinVersion ở 17 và cộng thêm một mức extension, nên một luật extension chỉ có thể được thỏa mãn bởi PDF 1.7 cộng đúng entry /Extensions. Khi cổng bật, phiên bản yêu cầu và tên tính năng được giữ lại cho bên gọi, và các khóa 311, 312, 313 của GetInformation phơi bày chúng

Preflight phiên bản lúc lưu trong PDFlibPas: PrepareAndCheckSaveVersion so sánh mọi object với các record PDFFeatureRules mang MinVersion, mức extension và kiểu khớp, AddExtensionRule ghim yêu cầu ở PDF 1.7 cộng một mức extension, và phép khớp đầu tiên mà đích không thỏa được sẽ dừng việc lưu với lỗi 602
Các khóa 311, 312 và 313 của GetInformation biến một lời từ chối thành một chẩn đoán, báo phiên bản yêu cầu, tính năng kích hoạt nó và đích đã ghim, để bên gọi sửa tệp hay sửa luật thay vì đoán mò
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
      if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
        // 311: phiên bản yêu cầu, 312: tính năng kích hoạt,
        // 313: phiên bản đích lưu bị ghim ('' khi chưa ghim)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

Vì sao một bản vẽ CAD thuần thất bại với lỗi 602?

Bảng luật chứa AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), thứ nổ với bất kỳ dictionary nào chỉ cần có khóa /Measure, mà mọi viewport đo đạc đều có một cái. Mảng /VP của trang chứa các dictionary viewport, mỗi viewport trỏ tới dictionary đo của nó qua /Measure, và phép khớp theo sự hiện diện của khóa dừng ngay đó mà chẳng nhìn xem dictionary đo đạc thực ra là gì. Phép quét tính năng lúc nạp có thể nâng số phiên bản tài liệu lên 1.7, nhưng nó chưa bao giờ ghi một khai báo /Extensions thay cho tệp đầu vào, nên cổng lưu thấy PDF 1.7 ở extension level 0 và báo 1.7 ExtensionLevel 3. Lời từ chối bịa ra một khai báo extension ấy là có chủ đích: thư viện không âm thầm nâng cấp tệp đầu vào để che lấp một luật đang sai

Đặc tả nói rõ không chút mơ hồ về ca trực giao. Dictionary Measure xuất hiện từ PDF 1.6, và ISO 32000-1 §12.9 cho /Subtype giá trị mặc định RL, một hệ tọa độ trực giao được mô tả bằng bộ entry của riêng nó: tỉ lệ scale, định dạng số X và Y, khoảng cách và diện tích. Đo đạc địa lý là phần bổ sung muộn hơn từ Adobe Extension Level 3 chồng lên PDF 1.7, nhận dạng qua /Subtype /GEO và mang theo các mảng điểm địa lý, dictionary hệ tọa độ và đơn vị hiển thị, những cấu trúc được đi qua trong bài đọc các viewport, mảng GPTS và LPTS của GeoPDF trong Delphi. Cả hai dictionary đều treo trên cùng một khóa /Measure, nên bất kỳ luật nào dừng ở khóa đều không thể đúng cho cả hai. Thông tin phân biệt nằm sâu một tầng, ngay trong dictionary đo đạc

Một khóa /Measure, hai dictionary trong PDFlibPas: đo đạc trực giao, với /RL hay bỏ qua subtype, chỉ cần PDF 1.6, trong khi dictionary địa lý cần 1.7 ExtensionLevel 3, nên CB_GeospatialDictionary quyết định theo nội dung ở nơi luật khóa cũ không thể phân biệt chúng
Một preflight chặn tệp hợp lệ còn tệ hơn một preflight chậm, vì bên gọi nhận về chẩn đoán uy nghiêm về một tính năng tài liệu chưa từng chứa, và đó là lý do phép kiểm tra phân biệt được dời xuống sâu một tầng

Bộ luật đã sửa vẫn thực thi những gì?

Bản sửa xóa luật khóa vô điều kiện và giữ lại các cổng mô tả yêu cầu phiên bản thật sự. Một trang mang /VP hay /UserUnit vẫn cần PDF 1.6 qua CB_PagePDF16Entries, một khóa /PtData vẫn cần extension level 3, còn CB_GeospatialDictionary quyết định một dictionary đo đạc có phải địa lý hay không bằng nội dung của nó thay vì bằng khóa đã đưa nó tới

// Đã xóa: mọi dictionary có khóa /Measure đều bị tính là địa lý
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);

AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);

function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
  Dict: TPDFDictionary;
begin
  Result := False;
  if not (Obj is TPDFDictionary) then
    Exit;
  Dict := TPDFDictionary(Obj);
  Result := (Dict.StringValue('Subtype') = 'GEO') or
    (Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
    (Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
    (Dict.FindIndexByKeyName('PDU') >= 0);
end;

Bộ hồi quy Delphi và FPC dùng chung ghim ranh giới đó từ cả hai phía. Một viewport có dictionary đo bỏ qua /Subtype và một viewport viết rõ /RL đều đạt ở PDF 1.6, cùng trang đó vẫn bị từ chối ở PDF 1.5, và phát hiện tính năng không còn báo extension cho nó nữa. Thêm một mảng /GPTS lật ngược phán quyết về 1.7 ExtensionLevel 3, đạt ngay khi mức extension được khai báo, còn một dictionary /Subtype /GEO trơ trọi thì bị từ chối nếu thiếu nó. Callback được thiết kế theo hướng bảo thủ: một dictionary trực giao mà lại mang thêm khóa lạc /GCS hay /PDU được coi là địa lý, vì những khóa đó vô nghĩa trong mô hình RL

LockSaveVersion là nơi thay đổi này lộ ra với bên gọi. TPDFlib.LockSaveVersion nhận '1.0' tới '1.7', trả về 0 với bất kỳ thứ gì khác, ghim phiên bản tài liệu, và chặn các lệnh gọi phía ghi âm thầm nâng nó lên, nhưng cổng lưu vẫn chạy đối chiếu với giá trị đã ghim. Với bộ luật đã sửa, một tệp CAD ghim ở 1.6 lưu sạch sẽ. Một GeoPDF chính hiệu ghim ở 1.6 vẫn ăn 602, đúng là đáp án như thế, và các lệnh gọi soạn nội dung địa lý như SetMeasureDictCoordinateSystem tự khai báo extension level 3 khi bạn dựng nội dung đó qua API

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
  if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
    // Nội dung thật vượt 1.6, ví dụ một dictionary đo địa lý GEO
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

Vì sao phép quét luật phiên bản chậm hơn mức cần thiết?

Phép quét copy từng TPDFFeatureRule vào một record cục bộ trước khi thử, và vì record chứa hai trường AnsiString, mỗi lần copy chỉnh hai reference count và giải phóng các giá trị cũ. Preflight ghé qua mọi node của mọi cây object, kể cả scalar, nên chi phí đó nhân số object với số luật, còn những luật chẳng liên quan gì tới phiên bản đích vẫn bị copy trước rồi mới bị bỏ qua. Vì PDFFeatureRules chỉ được đổ đầy một lần lúc khởi tạo unit và được coi là read-only, v3.539.17 đưa thẳng entry bảng cho MatchSingleRule và RuleExceedsTarget, hai hàm có tham số const Rule nhận tham chiếu mà không đụng vào chuỗi

Tăng tốc quét luật trong PDFlibPas: preflight từng copy mọi record TPDFFeatureRule trước khi thử, chỉnh reference count AnsiString cho từng object được ghé thăm, trong khi tham số const giờ đọc bảng read-only tại chỗ, cắt trung vị một vòng khớp luật từ 0.711 s xuống 0.203 s
Phần tăng là thật nhưng hẹp: một lần lưu trọn vẹn còn phải trả cho phát hiện tính năng hoãn lại, giải mã object và serialize, nên tỉ số đo được thuộc về đường khớp luật chứ không thuộc tổng thời gian lưu
// Trước: một bản copy record managed mỗi luật, mỗi object được ghé thăm
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// Sau: tham số const đọc entry bảng bất biến tại chỗ
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
  Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
  RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
  FeatureName := PDFFeatureRules[X].Feature;
  Result := False;
  Exit;
end;

Hiệu quả đo được là hẹp và nên được trích dẫn đúng như thế. Benchmark đối chiếu một mảng 20.000 object số với đích PDF 1.4 mười lần mỗi vòng; build bằng FPC Win64 với -O2, trung vị của năm vòng rơi từ 0.711 s xuống 0.203 s, và chạy hai bản build theo thứ tự ngược cho 0.459 s so với 0.150 s. Đó là khoảng 3 lần nhanh hơn trên riêng đường khớp luật. Một lần lưu thật còn phải trả cho phát hiện tính năng hoãn lại, giải mã object và serialize, nên tỉ số không chuyển được sang tổng thời gian lưu. Thứ tự luật, callback, ngưỡng phiên bản và chẩn đoán lỗi đầu tiên đều không đổi, và chẳng luật nào được cache xuyên các lần lưu hay bị bỏ qua để đạt kết quả đó

Nên kiểm tra gì khi một PDF đã nạp trượt preflight phiên bản?

Đọc khóa 311 và 312 trước khi đụng vào phiên bản. Nếu tính năng nêu một dictionary địa lý mà tệp chỉ vẽ các phép đo trực giao, đó chính là false positive này, và một bản build hiện tại sẽ lưu tệp không đổi. Nếu tính năng là thật, hoặc khai báo extension, hoặc ghim về một phiên bản thành thật chứa nội dung đó; nâng phiên bản chỉ để bịt miệng cổng là che giấu câu hỏi liệu những bên tiêu thụ hạ nguồn có đọc được những gì bạn phát hành hay không. Nguyên tắc kiểm tra có chừng mực, dựa trên bằng chứng tương tự thúc đẩy bài preflight author-mode PDF/E-1 cho tài liệu kỹ thuật, nơi các bản vẽ CAD gặp một chuẩn tuân thủ chứ không phải một con số phiên bản

Kiểm tra tuân thủ phiên bản, dictionary đo đạc và địa lý, cùng ghim phiên bản lưu đều là một phần của PDF Library for Delphi, bộ công cụ PDFlibPas cho lập trình viên Delphi, C++Builder và Lazarus