Bài viết kỹ thuật

Bật tắt optional content layer của PDF trong Delphi

PDFium Component điều khiển PDF optional content layer (OCG) trong Delphi qua hai method của TPdf: InspectOptionalContent liệt kê mọi layer cùng với visibility mà PDFium thật sự sẽ render, và SaveAsOptionalContentConfigured ghi một bản sao đã kiểm chứng trong đó các layer bạn chọn được bật hay tắt. Method thứ hai còn vô hiệu hóa các luật Usage và /AS mà nếu không sẽ lặng lẽ Undo mất chỉnh sửa của bạn. Cả hai làm việc trên tài liệu đang mở sẵn trong TPdf, nên không có parser thứ hai nào phải giữ đồng bộ với những gì viewer hiển thị

Yêu cầu thường đến từ một xưởng CAD hay GIS: bộ bản vẽ đi kèm kích thước, chú thích và khối tiêu đề trên các layer riêng, và khách muốn một bản sao ẩn phần kích thước trước khi chuyển cho nhà cung cấp. PDFium render optional content chuẩn xác, nhưng ABI công khai của nó không có hàm nào để liệt kê OCG, chọn một configuration hay lật trạng thái một layer. Vậy bạn xuống cấp object, sửa /OCProperties, lưu, nạp lại, và layer vẫn nguyên ở đó. Nguyên nhân nằm ở logic visibility của PDFium, và đáng hiểu nó trước khi đụng vào bất kỳ byte nào

Vì sao sửa /ON và /OFF không đổi những gì PDFium render?

Sửa hai mảng /ON và /OFF của dictionary configuration là chưa đủ, vì PDFium cho một trạng thái tường minh nằm trong dictionary /Usage của chính OCG thắng hai mảng đó, và một luật auto-state /AS lại có thể đè lên cả hai. ISO 32000-1 §8.11.4 mô tả configuration và usage dictionary như hai cơ chế tách rời; renderer của PDFium gộp chúng vào một quyết định duy nhất, và InspectOptionalContent tái hiện nó theo thứ tự này:

  • Xuất phát từ /BaseState của configuration, nơi /ON và /Unchanged cùng được tính là hiện hữu và chỉ /OFF là ẩn
  • Áp mảng /ON của configuration, rồi tới mảng /OFF, nên một group có mặt trong cả hai kết thúc bằng ẩn
  • Áp trạng thái Usage tường minh của group cho usage được yêu cầu, chẳng hạn /Usage << /View << /ViewState /OFF >> >>, thứ đè lên mọi thứ phía trên
  • Coi một group mà /Intent không chứa /View lẫn /All là hiện hữu, vì nó không tham gia vào visibility theo view intent
  • Cuối cùng chạy mảng /AS của configuration được chọn, mà các entry của nó cho sự kiện khớp đặt trạng thái của các group mà chúng liệt kê
Quyết định visibility năm bước mà PDFium Component tái hiện cho mọi PDF optional content group trong Delphi: BaseState đặt điểm xuất phát, hai mảng ON và OFF của configuration áp theo thứ tự, một entry Usage ViewState hay PrintState tường minh đè lên cả hai, Intent không tham gia vẫn được tính là hiện hữu, và mảng AS chạy sau cùng
Sửa hai mảng ON và OFF là chưa đủ vì PDFium gộp BaseState, cả hai mảng, trạng thái Usage của group và cuối cùng là các luật auto-state AS vào một phán quyết mà InspectOptionalContent tái hiện từng bước

Bước thứ ba là bước làm người ta bỏng tay. Một file được công cụ layout lưu thường mang /ViewState /ON trên mọi OCG, và PDFium khi đó phớt lờ mảng /OFF mà bạn chỉnh công phu: lần lưu thành công, file mở lại sạch sẽ, và layer vẫn tô lên trang. Với Print và Export, OcExplicitUsageState đọc PrintState hay ExportState trước và fallback về ViewState khi entry riêng không có mặt, nên một mình ViewState /ON cũng ghim layer cho cả in ấn. Marked content tham chiếu một OCMD (§8.11.2.2) sau đó được phân giải chống lại các kết quả per-group này, qua chính sách /P hay, khi có mặt, biểu thức visibility /VE

Làm sao liệt kê các layer mà PDFium thật sự hiển thị?

TPdf.InspectOptionalContent trả về một TPdfOptionalContentInventory trong đó mảng Groups mang số object, tên, intents, ba trạng thái Usage, ngôn ngữ, vùng zoom, cờ Locked, chỉ mục radio-group và EffectiveVisible đã tính của từng OCG. Method trước hết nhờ PDFium lưu tài liệu in-memory hiện tại, trải các object stream, rồi quét kết quả, nên các chỉnh sửa trước đó trong phiên cũng được phản ánh. Chỉ mục configuration 0 luôn là dictionary /D mặc định và các entry của /Configs theo từ chỉ mục 1; đối số mặc định -1 chọn chỉ mục 0. Một tài liệu thiếu /OCProperties khiến method trả False với lý do nằm trong ErrorMessage thay vì raise

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage mặc định là ocuView; -1 chọn configuration 0, dictionary /D
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

Mảng Memberships báo mọi OCMD cùng Policy của nó (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), text VisibilityExpression thô và EffectiveVisible riêng của nó. Vài luật biên là có chủ ý. /P mặc định là /AnyOn, và một OCMD không có group nào được tính là hiện hữu. Một tham chiếu tới số object không phải OCG đã biết được coi là hiện hữu thay vì làm hỏng cả biểu thức. Phép đánh giá /VE dừng ở độ sâu lồng 32 và coi mọi thứ sâu hơn là ẩn, điều giữ một biểu thức thù địch hay tự tham chiếu khỏi biến việc soi vào một stack overflow

Ghi trạng thái layer mới bằng SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured nhận một mảng record TPdfOptionalContentStateChange (số object group cộng Visible) và ghi một tài liệu mà configuration được chọn sinh ra đúng trạng thái đó. Configuration được chọn nhận /BaseState /ON cùng các mảng /ON và /OFF trọn vẹn phủ mọi group, và mỗi OCG đã có sẵn Usage dictionary nhận một ViewState tường minh (hay PrintState / ExportState, theo Options.Usage) khớp trạng thái mới của nó. Với TPdfOptionalContentConfigureOptions.Default, khóa /AS của configuration được chọn bị gỡ bỏ để một sự kiện open, print hay export không lật các layer quay lại

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  SetLength(Changes, 2);
  Changes[0].GroupObjectNumber := DimensionsObj;
  Changes[0].Visible := False;
  Changes[1].GroupObjectNumber := NotesObj;
  Changes[1].Visible := True;

  // Configuration 0, ocuView, DisableAutomaticState và EnforceRadioGroups True
  Options := TPdfOptionalContentConfigureOptions.Default;

  if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
    Changes, Options, Report) then
    raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);

  Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
    [Report.ChangedGroupCount, Report.GroupCount,
     Report.UpdatedUsageStateCount,
     BoolToStr(Report.RemovedAutomaticState, True)]));
end;

Đường ghi giữ output tự lưu của PDFium làm tiền tố từng-byte-một và chỉ nối thêm phần chủ configuration được viết lại cùng các object OCG mang Usage dictionary, theo sau là một section xref và trailer mới. Trước khi một byte nào chạm đích, kết quả được mở lại trong một TPdf riêng rẽ dưới chính sách nạp nghiêm ngặt, và method thất bại nếu bảng cross-reference không kiểm chứng được. Overload file đi xa hơn một bước: nó ghi vào một file tạm bên cạnh đích và chỉ thay thế đích sau khi kiểm chứng thành công, nên một cập nhật bị từ chối chẳng bao giờ để lại một bản vẽ viết dở. Đó là cùng cách sửa đổi tăng dần có kiểm chứng mà trình soạn name tree và number tree PDF trong PDFium Component dùng

SaveAsOptionalContentConfigured trong PDFium Component ghi một PDF Delphi với các layer đã bật tắt thế nào: các thay đổi trạng thái và options đi vào, configuration được chọn được viết lại với các mảng ON và OFF trọn vẹn cùng trạng thái Usage, bản sửa đổi tăng dần có kiểm chứng được nối thêm, và một lần mở lại nghiêm ngặt phải kiểm chứng được trước khi bất cứ thứ gì được ghi
Lần lưu configured giữ chính bản re-save của PDFium làm tiền tố byte, nối phần chủ configuration viết lại cộng một section xref mới, và mở lại kết quả trong một TPdf riêng rẽ trước khi đích bị đụng tới

Lần lưu configured từ chối làm những gì?

Lần lưu configured từ chối mọi thay đổi mà chính tài liệu cấm hay không thể biểu diễn an toàn, và mọi lần từ chối xảy ra trước khi đích bị đụng tới. Một số object không nằm trong /OCGs thất bại trắng trợn. Việc đổi một group nằm trong mảng /Locked của configuration thất bại, dù việc nhắc lại giá trị hiện tại của nó là được phép. Với EnforceRadioGroups bật, bất kỳ bộ /RBGroups nào sẽ kết thúc với nhiều hơn một thành viên hiện hữu bị từ chối thay vì lặng lẽ tắt các thành viên còn lại. Tài liệu được mã hóa bị từ chối vì các object tăng dần dạng plaintext không thể mang security handler đang kích hoạt. Tài liệu có chữ ký raise EPdfError trừ khi bạn truyền AllowSignedDocument = True, vì thay đổi những gì một trang hiển thị có thể làm vỡ vùng phủ chữ ký hay một chính sách chứng nhận

Các cổng từ chối mà SaveAsOptionalContentConfigured áp dụng trong PDFium Component trước khi ghi một PDF Delphi configured: một số object ngoài OCGs thất bại, các group bị khóa thất bại, các bộ RBGroups có nhiều hơn một thành viên hiện hữu bị từ chối, tài liệu mã hóa không mang nổi object tăng dần plaintext, và file có chữ ký đòi AllowSignedDocument
Mọi lần từ chối xảy ra trước khi đích bị đụng tới, và lý do thất bại nằm trong Report.ErrorMessage thay vì để lại một bản vẽ viết dở
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // ghi /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // entry đầu của /Configs, không phải /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument vẫn là False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // file có chữ ký: chẳng gì được ghi vào Target
      Result := False;
    end;
  end;
end;

Hãy nắm các đánh đổi trước khi nối nó vào một batch job. Bản sửa đổi được nối nằm trên đỉnh lần re-save trọn vẹn của PDFium, chứ không phải các byte file gốc của bạn, và đó chính là lý do input có chữ ký cần sự đồng ý tường minh. Phép viết lại cũng chuẩn hóa configuration được chọn về /BaseState /ON, nên một baseline /Unchanged hay /OFF của tác giả bị thay bằng các mảng tường minh cho cùng visibility kết quả. Việc bỏ /AS xóa luôn các mẹo chỉ-in-ấn như một layer watermark chỉ xuất hiện trên giấy; đặt DisableAutomaticState thành False để giữ các luật đó, chấp nhận rằng chúng có thể đè trạng thái bạn yêu cầu cho sự kiện đó. Mặt tích cực thì PDF/A-2 (ISO 19005-2 clause 6.9) và PDF/UA (ISO 14289-1 clause 7.10) đều cấm /AS trong configuration dictionary, nên output mặc định gỡ luôn một vấn đề mà PDF/A preflight validation với PDFium Component của bạn lẽ ra phải báo

Điều khiển layer đứng ở đâu trong một PDF viewer Delphi

Trong một viewer, điều khiển layer là một checklist dẫn dắt bởi inventory cộng một lần nạp lại kết quả đã lưu. Đổ checklist từ Groups, vô hiệu các entry đang Locked, coi các thành viên chung RadioGroupIndex là loại trừ lẫn nhau, và khi apply thì ghi ra một TMemoryStream rồi nạp ngược stream đó vào TPdf để view tô trạng thái mới. Phần nối giữa TPdf và TPdfView được trình bày trong dựng một PDF viewer giàu tính năng với PDFium VCL trong Delphi. Giấy phép, bản tải dùng thử và phần còn lại của bộ tính năng nằm trên trang sản phẩm PDFium Component for Delphi