Bài viết kỹ thuật

Flexbox, CSS Grid và Chú Thích Cuối Trang trong PDF từ Delphi

PDF Library for Delphi render HTML vào một trang PDF với bố cục hai chiều thực sự: display: flexdisplay: grid được đo đạc và đặt vị trí thay vì bị hạ cấp thành các khối xếp chồng, và chú thích cuối trang được dành sẵn chỗ ở đáy hộp mang tham chiếu của chúng, với đánh số liên tục xuyên suốt các cột và trang. Các điểm vào là những cái quen thuộc, DrawHTMLTextBox cho một hộp đơn và DrawHTMLStory cho luồng nhiều cột

Điều này quan trọng vì HTML hiện là cách phần lớn nội dung báo cáo đến. Mẫu được soạn bởi những người viết CSS, dashboard được thiết kế dưới dạng thẻ (card), và một renderer âm thầm sụp một hàng flex thành bốn khối xếp chồng sẽ tạo ra một tài liệu hoàn toàn không giống với thiết kế. Cho đến khi khả năng này tồn tại, container hai chiều duy nhất mà bộ máy đo đạc được là bảng, nên mọi bố cục dạng thẻ phải được viết lại bằng tay thành bảng

Điều gì đã thay đổi trong mô hình bố cục?

Vòng lặp chính trước đây duy trì một hộp dòng duy nhất và tiến dần xuống trang. Mô hình đó xử lý nội dung inline và các khối xếp chồng hoàn hảo và không thể diễn tả một container mà các phần tử con của nó được định cỡ tương quan với nhau. Bảng là ngoại lệ duy nhất, với phép đo hai lượt riêng của chúng

Flex và grid mỗi thứ đều thêm một lượt đo bị giới hạn qua các phần tử con của một container, và từ quan trọng là bị giới hạn. Một container flex đo tối đa 256 phần tử con trực tiếp vào một mảng cố định. Một grid dùng một ma trận chiếm dụng tối đa 64 nhân 64 ô để đặt tự động có thể xác định trước. Những trần đó tồn tại để một stylesheet mang tính đối kháng hoặc được sinh tự động không thể gây ra đệ quy vô hạn hay bộ nhớ đặt vị trí bậc hai, đây là một mối lo thực sự khi HTML đến từ một mẫu mà khách hàng chỉnh sửa

Các phần tử flex nhận kích thước của chúng như thế nào

Theo hướng hàng, container cộng gộp cơ sở (basis) của mỗi phần tử cùng với trọng số grow và shrink của nó, rồi phân bổ khoảng trống còn lại, dương hoặc âm, theo những trọng số đó. Với flex-wrap, mỗi dòng được giải quyết độc lập, nên một hàng ngắt thành hai dòng sẽ gán khoảng trống tự do theo từng dòng thay vì trên toàn container. Theo hướng cột, cùng phép phân bổ trục chính đó chạy dựa trên hoặc một chiều cao tường minh hoặc chiều cao nội dung

justify-content, align-items, gap và các hướng đảo ngược vận hành trên hình học đã được đo trước đó. Chúng di chuyển các hộp; chúng không bao giờ kích hoạt việc đo lại nội dung phần tử. Sự tách biệt đó chính là điều giữ cho một dashboard phức tạp không phải đo các phần tử con của nó nhiều lần

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Html, Remainder: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    Html :=
      '<div style="display:flex; gap:12px;">' +
      '  <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Revenue</b><br/>EUR 4,182,300</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Margin</b><br/>18.4%</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Backlog</b><br/>92 days</div>' +
      '</div>';

    Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
    if Remainder <> '' then
      Log('content did not fit - carry the remainder to the next box');

    Lib.SaveToFile('dashboard.pdf');
  finally
    Lib.Free;
  end;
end;

Giá trị trả về là chuỗi tiếp nối, đây là cách mọi điểm vào vẽ HTML báo cáo những gì chưa vừa. Truyền nó cho hộp tiếp theo hoặc trang tiếp theo và luồng tiếp tục từ nơi nó dừng lại

Đặt vị trí grid, và một track có thể là gì

Track của grid chấp nhận độ dài cố định, phần trăm, đơn vị fr, các biểu thức repeat() đơn giản và minmax(). Việc đặt tự động lấp đầy ma trận chiếm dụng một cách xác định, nên cùng một HTML luôn tạo ra cùng một cách sắp xếp. Tọa độ tường minh được phép chồng lấn, đây là điều có chủ đích: một thiết kế xếp một huy hiệu lên trên một thẻ đang thể hiện chủ ý, không phải một lỗi. Khi chỉ một trục được cho tường minh, việc đặt chỉ tìm kiếm trên trục còn lại

Các phần tử trải dài qua nhiều hàng đóng góp chiều cao đã đo của chúng trở lại các hàng mà chúng bao phủ, được lấy trung bình qua các hàng đó, giữ cho một phần tử trải dài cao không ép một hàng đơn lẻ trong khi để các hàng lân cận bị thấp:

Html :=
  '<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
  '            gap:10px;">' +
  '  <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
  '  <div>Inspector</div>' +
  '  <div>Date</div>' +
  '  <div style="grid-column:2 / span 2;">Findings summary</div>' +
  '</div>';

Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);

Các phần tử con flex và grid được render qua cùng bộ render HTML như mọi thứ khác, đây chính là thuộc tính khiến tính năng này dùng được thay vì trở thành một thế giới riêng biệt. Font, cascade CSS, liên kết, hình ảnh, bảng và các container flex hoặc grid lồng nhau tiếp theo đều hoạt động bên trong một phần tử flex đúng như chúng hoạt động ở cấp cao nhất, và kế hoạch bố cục bên ngoài ghi lại các lệnh văn bản và hình chữ nhật cuối cùng để việc vẽ lặp lại tái sử dụng bộ nhớ đệm đo đạc đã có

Vì sao chú thích cuối trang lại là một bài toán phân trang?

Một chú thích cuối trang không phải nội dung chảy sau đoạn văn chứa tham chiếu của nó; nó là nội dung phải xuất hiện ở đáy của cùng hộp với tham chiếu của nó. Điều đó đảo ngược thứ tự đo lường thông thường, vì khoảng trống dành cho văn bản chính giờ đây phụ thuộc vào nội dung chưa được bố cục

Vì vậy renderer đo chú thích khi gặp tham chiếu, và trừ diện tích chú thích khỏi ngân sách chiều cao phần thân của hộp bị giới hạn hiện tại. Nếu tham chiếu, phần văn bản chính đến thời điểm đó và chú thích không thể cùng vừa, dấu hiệu chú thích và mọi thứ sau nó sẽ cùng chuyển sang chuỗi tiếp nối. Quy tắc đó chính là thứ ngăn hai thất bại kinh điển: một chú thích đè lên văn bản chính, và một chú thích mắc kẹt trên một trang mà tham chiếu của nó nằm ở trang trước

Trong một hộp bị giới hạn, vùng chú thích được ghim ở đáy với một quy tắc phân cách phía trên nó. Trong phép đo không giới hạn, nơi không có chiều cao hộp để ghim vào, vùng chú thích theo ngay sau phần thân. Việc đánh số được mang trong một trường mở rộng trên stack tiếp nối, nên DrawHTMLTextBoxDrawHTMLStory giữ chuỗi đánh số chạy liên tục qua các cột và trang, và một chuỗi tiếp nối được tạo ra trước khi trường đó tồn tại vẫn tiếp tục đúng

// Chú thích cuối trang bên trong một story nhiều cột giữ một
// chuỗi đánh số liên tục duy nhất
Html := LoadTemplate('chapter.html');    // dùng đánh dấu float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // số cột
  16,       // rãnh cột tính bằng điểm
  20,       // số trang tối đa cho story này
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Hướng dẫn thực tế cho người soạn mẫu

Hãy thiết kế trong phạm vi các trần đã được ghi lại. Một container flex có hơn 256 phần tử con trực tiếp gần như luôn là một bảng dữ liệu khoác lớp áo flex, và đường xử lý bảng đo nó tốt hơn dù sao đi nữa. Một grid lớn hơn 64 nhân 64 là một bảng tính, và cùng lời khuyên đó áp dụng. Với văn bản phần thân nhiều cột, hành vi cột và ngắt từ được mô tả trong ngắt từ và các cột văn bản cân bằng chi phối cách luồng trông ra sao bên trong mỗi cột

Hãy đo trước khi vẽ khi một bố cục phải vừa khít. GetHTMLTextHeight báo cáo chiều cao mà một độ rộng cho trước sẽ cần, đây là cách rẻ để quyết định giữa bố cục này và bố cục khác trước khi cam kết mực in. Và hãy coi một chuỗi tiếp nối không rỗng là bình thường thay vì ngoại lệ: đó là cơ chế nhờ đó nội dung dài được phân trang, không phải một tín hiệu lỗi

Khi HTML đến từ một bộ máy báo cáo thay vì từ mẫu viết tay, tuyến đường theo hướng dữ liệu trong bộ máy báo cáo dataset kết hợp tốt với điều này, tạo ra mã đánh dấu mà flex và grid sau đó sắp xếp. Và khi cùng nội dung cũng phải rời khỏi PDF một lần nữa, đường xuất ngữ nghĩa trong xuất PDF sang Markdown và DOCX khép kín vòng khứ hồi

Bố cục HTML, tạo báo cáo và xuất ngữ nghĩa là một phần của một thư viện cho Delphi, C++Builder và Free Pascal; danh sách tính năng đầy đủ có trên trang PDF Library for Delphi