Bài viết kỹ thuật

Đọc và ghi marked content PDF trong Delphi

Marked content là cơ chế mà ISO 32000-1 §14.6 định nghĩa để gắn thẻ nội dung trang, và cả tagged PDF lẫn PDF/UA đều xây trên nó. PDFium Component phơi bày nó trực tiếp: PageObjectMarks đọc mọi thẻ BDC và property list của nó từ một page object, AddPageObjectMark ghi một thẻ, RemovePageObjectMark xóa một thẻ, còn PageObjectMarkedContentID báo cáo MCID liên kết nội dung với cây cấu trúc

Cho đến khi cây cấu trúc có thể nối lại được với nội dung mà nó mô tả, công cụ khả năng tiếp cận chỉ là phép đoán. Cây cấu trúc nói "đây là một tiêu đề"; MCID nói mark nào trên trang nào thực sự là tiêu đề đó. Cả hai nửa đều phải đọc được trước khi một ứng dụng có thể kiểm tra, sửa chữa hoặc báo cáo về gắn thẻ

Một mark, trong byte, là gì?

Một toán tử BDC với một tag name và một property list tùy chọn, đóng bởi EMC. Trong content stream nó trông như /P <</MCID 3>> BDC ... EMC: thẻ /P đặt tên vai trò, dictionary mang property, và mọi thứ giữa hai toán tử là marked content. Một page object bên trong khoảng đó mang mark, đó là thứ PDFium trả lại và thứ PDFium Component biến thành một bản ghi

TPdfContentMark giữ một handle, thẻ Name, và một mảng TPdfContentMarkParam. Mỗi tham số có một Key, một Kind và một trường giá trị ý nghĩa được chọn bởi kind đó: pmpInt, pmpFloat, pmpString hoặc pmpBlob. Kind đến từ báo cáo kiểu riêng của PDFium chứ không phải từ getter nào tình cờ thành công, đó là khác biệt giữa đọc một property list và đoán một property list

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Vì sao pmpUnknown lại có nghĩa hai thứ khác nhau

pmpUnknown được trả về khi PDFium báo FPDF_OBJECT_UNKNOWN, và PDFium cũng trả về giá trị đó cho một key không tồn tại. Hai trường hợp không thể phân biệt tại lớp này, và giả vờ ngược lại thì tệ hơn nói ra

Hệ quả thực tế cho mã của bạn: hãy coi pmpUnknown như "không có giá trị dùng được ở đây" thay vì như một kiểu bạn có thể giải mã bừa. Nếu một property quan trọng với quy trình của bạn, hãy xác minh nó hiện diện với một kind bạn nhận ra, và đừng suy luận sự vắng mặt từ một unknown — một mark mà property list bạn không đọc được là một mark bạn nên báo cáo, không phải một mark bạn âm thầm chấp nhận

Một bản ghi mark là một snapshot, không phải handle bạn sở hữu

Trường Handle thuộc về thư viện. Nó cũ đi ngay khi mark bị gỡ, page object bị phá hủy hoặc trang bị dỡ, nên bản ghi là một snapshot chỉ đọc với tuổi thọ ngắn. Cache nó qua một lần chuyển trang và bạn đang cầm một con trỏ vào bộ nhớ mà engine đã thu hồi

Đó là cùng kỷ luật áp dụng cho handle page object nói chung trong PDFium, và nó bắt người ta ở cùng một chỗ: một điều khiển danh sách được nạp bằng các bản ghi mark, một người dùng điều hướng sang trang khác, và một sự cố trông không liên quan đến điều hướng. Hãy sao chép ra các giá trị bạn cần — tên, các key, các số — và buông handle. Các ghi chú về handle page object cũ đi sau biến đổi trình bày quy tắc chung và cách nó cắn ở nơi khác

Thêm một mark, và bước lưu dễ bị bỏ sót

AddPageObjectMark nhận chỉ số page object, một tag name và một tập tham số hoàn chỉnh. Tham số được ghi dưới dạng một tập hợp thay vì vá từng key một, đó là lý do TPdfContentMarkParam không có sentinel Has* — trường hợp "cập nhật một trường của một bản ghi sẵn có" mà những sentinel đó bảo vệ không phát sinh

Phần đáng nói rõ: thêm một mark dựng lại content stream của trang để thẻ sống sót qua một lần lưu. Điều này phải tường minh vì SaveAs không tự tái sinh nội dung — một thay đổi chỉ tồn tại trong object model sẽ bị vứt, và file lưu ra sẽ trông y hệt cái bạn bắt đầu. Nếu bạn từng thêm thứ gì vào một trang PDFium và thấy nó thiếu trong đầu ra, thường đây là lý do

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Điều này làm và không làm cho một tài liệu điều gì

Mark một mình không tạo ra một tagged PDF. Một tài liệu đã gắn thẻ tuân thủ cần một cây cấu trúc mà các phần tử tham chiếu các MCID này, một mục /MarkInfo khai báo tài liệu đã đánh dấu, và tên vai trò có nghĩa đúng như chuẩn nói chúng nên có. Ghi một mark /P với một MCID mà không phần tử cấu trúc nào trỏ tới cho bạn nội dung tuyên bố là đã gắn thẻ cùng một cây cấu trúc không bao giờ nhắc tới nó

Nơi marked content thực sự chứng tỏ giá trị ở cấp độ này là kiểm tra và sửa chữa: kiểm toán page object nào đã gắn thẻ, tìm các artifact đáng lẽ phải được đánh dấu như vậy, hoặc khớp MCID với một cây cấu trúc để tìm các orphan. Về nửa cây cấu trúc của công việc đó, hãy xem bài viết về xác thực cây cấu trúc PDF/UA, còn về trải nghiệm đọc mà các thẻ rốt cuộc phục vụ, các ghi chú về xây dựng một trình đọc PDF dễ tiếp cận trong Delphi

PDFium Component mang cho các ứng dụng Delphi, C++Builder và Lazarus một API VCL cấp cao trên engine PDFium, với marked content, cây cấu trúc và xác thực khả năng tiếp cận tiếp cận được từ mã Pascal thường — xem trang sản phẩm PDFium Component để biết bề mặt API đầy đủ