Bài viết kỹ thuật

Tái Bố Cục Nội Dung PDF thành HTML Responsive trong Delphi

PDFium Component biến một PDF bố cục cố định thành một mô hình ngữ nghĩa có thể tái bố cục, dùng BuildReflowDocument, và xuất mô hình đó thành HTML độc lập thông qua ToHtml. Tiêu đề vẫn là tiêu đề, mục danh sách vẫn là mục danh sách, và các bảng được phát hiện trên trang được xuất ra thành mã đánh dấu bảng thật với ô tiêu đề và span được giữ nguyên. Không có gì trong đầu ra tham chiếu đến một script hay stylesheet bên ngoài

Lý do cần điều này là một trang PDF là một tập hợp glyph được định vị, đây chính xác là điều không phù hợp cho màn hình điện thoại, trình đọc màn hình hay chỉ mục tìm kiếm. Mọi nỗ lực giải quyết vấn đề này bằng cách trích xuất văn bản thuần đều mất đi cấu trúc khiến tài liệu dễ đọc, và mọi nỗ lực giải quyết bằng cách chuyển trang thành hình ảnh đều mất hoàn toàn văn bản. Một mô hình tái bố cục giữ được cả hai: các từ và mối quan hệ giữa chúng

Thông tin ngữ nghĩa đến từ đâu?

Mọi thứ bắt đầu từ GetStructuredText, nguồn duy nhất của văn bản và ngữ nghĩa trong thành phần này. Khi PDF mang cây cấu trúc, tức PDF có gắn thẻ như được định nghĩa trong ISO 32000-1 điều khoản 14.7, mô hình đi theo cấp bậc logic mà nhà sản xuất đã ghi lại. Khi không có, và phần lớn PDF ngoài thực tế không có, mô hình quay về thứ tự bố cục vật lý vốn đã được tính sẵn cho mục đích thứ tự đọc

Lựa chọn đó giữ một ranh giới cứng: không có bộ phân tích PDF thứ hai và không có động cơ render thứ hai nào được đưa vào để trả lời những câu hỏi mà bộ máy hiện có đã trả lời được. Cơ chế thứ tự đọc bên dưới được mô tả trong khối văn bản có cấu trúc và thứ tự đọc, và mô hình tái bố cục là một lớp ngữ nghĩa nằm trên đó chứ không phải một sự thay thế

Mỗi nút ghi lại nguồn gốc thông tin của nó, nên một bên tiêu thụ có thể phân biệt một tiêu đề mà tài liệu khai báo với một tiêu đề mà thuật toán bố cục suy luận ra. Các pipeline nhạy cảm với độ tin cậy nên đọc trường đó thay vì coi mọi nút đều có thẩm quyền như nhau

Một cây phẳng, và vì sao nó không phải cây đối tượng

Mô hình là một cây được làm phẳng theo thứ tự duyệt trước: một mảng các nút mà mỗi nút mang ParentIndexDepth, thay vì một bản ghi đệ quy hay một đồ thị đối tượng có quyền sở hữu. Trang, tiêu đề, đoạn văn, danh sách, mục danh sách, hình minh họa, chú thích, bảng, hàng và ô đều nằm trong cùng một mảng tuyến tính đó

Hai lợi ích theo sau. Bên tiêu thụ có thể streaming mảng theo thứ tự mà không cần đệ quy, giúp việc phát ra HTML, Markdown hoặc một view dạng cây trở thành một vòng lặp đơn giản. Và bố cục vẫn có tính di động qua Delphi, C++Builder và Free Pascal, những trình biên dịch khác nhau về cách xử lý kiểu quản lý đệ quy qua ranh giới ABI. Một bản ghi đệ quy chứa mảng động chính là loại cấu trúc biên dịch được ở mọi nơi nhưng lại hành xử khác nhau tinh vi ở mỗi nơi

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // khối style nội tuyến, không file ngoài
    Options.MaxNodes := 200000;          // ngân sách fail-closed
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

Làm sao để bảng không bị xuất hiện hai lần?

Việc phát hiện bảng chạy sau khi văn bản có cấu trúc đã được thu thập cho một trang, điều này tạo ra một rủi ro rõ ràng: cùng nội dung ô tồn tại vừa trong các khối văn bản vừa trong bảng đã phát hiện. Phát ra cả hai sẽ tạo ra HTML mà mỗi bảng lại theo sau bởi chính nội dung của nó lần nữa dưới dạng các đoạn văn rời rạc

Quy tắc giải quyết điều này mang tính hình học. Khi một bảng đã phát hiện bao phủ hơn một nửa diện tích của một khối văn bản, nút bảng sẽ thay thế khối đó thay vì nhập chung vào. Việc đánh chỉ mục ô bên trong một hàng được xây bằng cách đếm vào các nhóm, nên việc xây mô hình vẫn tuyến tính theo số ô cộng số hàng thay vì quét lại từng ô cho mỗi hàng, điều này quan trọng trên các tài liệu tài chính nơi một trang duy nhất có thể mang theo hàng trăm ô

Cấu trúc đã phát hiện trung thực về việc nó là phát hiện. Một bảng có đường kẻ được nhận diện đáng tin cậy hơn một bảng chỉ căn chỉnh bằng khoảng trắng, và độ tin cậy của nút phản ánh điều đó. Với nội dung mà một bảng sai còn hơn không có bảng, hãy giữ việc phát hiện bật; với chuyển đổi lưu trữ nơi một bảng sai còn tệ hơn, hãy chặn theo độ tin cậy

Xuất HTML luôn giữ tính độc lập

ToHtml duyệt qua mô hình đã được xây dựng từ trước và không bao giờ quay lại PDFium, nên xuất hai lần không tốn thêm gì và không thể cho ra kết quả khác nhau từ cùng một mô hình. Văn bản và giá trị thuộc tính được thoát ký tự đồng nhất, cấp tiêu đề bị giới hạn trong khoảng h1 đến h6 mà HTML thực sự định nghĩa, và ô tiêu đề, RowSpanColumnSpan được truyền qua nguyên trạng như đã viết

CSS tùy chọn là một khối style nội tuyến đơn giản. Không có script, không có web font và không có tài nguyên bên ngoài dưới bất kỳ hình thức nào, đây chính là điều khiến đầu ra an toàn để nhúng vào một email, một trình xem trợ giúp hoặc một điều khiển trình duyệt trong sandbox:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // giữ ranh giới trang hiển thị
  Options.PreserveLineBreaks := False;   // để trình duyệt tự ngắt dòng đoạn văn

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

PreserveLineBreaks là tùy chọn đáng cân nhắc nhất. Một lần ngắt dòng trong PDF là một quyết định dàn trang được đưa ra cho một độ rộng trang cố định, nên giữ lại nó trên một màn hình hẹp tái tạo đúng vấn đề mà việc tái bố cục vốn tồn tại để giải quyết. Giữ ngắt dòng cho thơ, mã nguồn liệt kê và địa chỉ; bỏ chúng đối với văn xuôi

Ngân sách, hủy bỏ và trạng thái trang

Ký tự, nút, bảng và ô mỗi thứ đều có một trần, và mỗi trần được kiểm tra trước khi cấp phát chứ không phải sau đó, nên một tài liệu bị hỏng hoặc mang tính đối kháng thất bại một cách gọn gàng thay vì tiêu tốn bộ nhớ cho đến khi thứ khác gặp sự cố. Token hủy bỏ được kiểm tra tại ranh giới trang, khối, bảng, hàng và ô, giúp một lượt quét bị hủy của một tài liệu nghìn trang vẫn phản hồi nhanh

Một hành vi quan trọng riêng cho các ứng dụng GUI: toàn bộ lượt quét tài liệu chạy bên trong một phạm vi khôi phục lại trang đang hoạt động, nên thành công, thất bại vì ngân sách và hủy bỏ đều để nguyên trang hiện tại của bên gọi không bị chạm vào. Một trình xem cho phép người dùng xuất trong khi đang xem trang 340 sẽ thấy mình vẫn ở trang 340 sau đó

Tái bố cục tốt cho việc gì, và không tốt cho việc gì

Đầu ra tái bố cục là đầu vào xuất sắc cho lập chỉ mục tìm kiếm, chế độ đọc trợ năng, hiển thị di động và di chuyển nội dung. Nó không phải là một bộ chuyển đổi giữ độ trung thực: vị trí tuyệt đối, font chính xác, tác phẩm vector và hình học trang chính xác nằm ngoài mục đích của nó theo thiết kế. Khi một tác vụ cần trang trông giống hệt, hãy render nó; khi cần trang có thể đọc được ở nơi khác, hãy tái bố cục nó

Riêng đối với công nghệ trợ năng, mô hình tái bố cục kết hợp với các tính năng đọc được mô tả trong xây dựng một trình đọc trợ năng, và các tài liệu mang cây cấu trúc thực sự sẽ tạo ra mô hình tốt hơn rõ rệt, đây là lý do chính đáng để xác thực việc gắn thẻ ở đầu nguồn như được mô tả trong xác thực cây cấu trúc PDF/UA

Tái bố cục, văn bản có cấu trúc, xác thực gắn thẻ và render dùng chung một đối tượng tài liệu qua Delphi, C++Builder và Lazarus; toàn bộ API được mô tả trên trang PDFium Component cho Delphi