HotPDF render HTML table qua profile paged-media HTML5 của nó bằng một occupancy grid thật cho rowspan và colspan, row height đo được chứ không ước lượng theo số ký tự, và các header row lặp lại trên mọi continuation page. Có hai tình huống làm nó từ chối lặp header, và biết trước hai chuyện đó rẻ hơn nhiều so với debug một cell bị nhân đôi về sau
Loại document ép người ta cần tính năng này là thứ mọi reporting team sớm muộn cũng ship: một invoice hay compliance report nơi source of truth là HTML, table chạy dài qua bốn page, và header phải đọc được trên từng page một. Thiếu một layout table thật là có ngay hai lỗi reader nhìn là thấy: header chỉ xuất hiện một lần ở page một, và các row có height đoán từ số ký tự
Vì sao khả năng table lại chuyển vào HTML renderer?
Vì phương án kia đánh mất rich text, mà rich text chính là lý do content là HTML ngay từ đầu. Kế hoạch hiển nhiên trông như tái sử dụng: HotPDF đã có một layout DOM table object với grid đàng hoàng, thì chỉ việc bắc cầu từ HTML parser vào đó và có spanning miễn phí. Vấn đề là cái table object đó vẽ bằng gì. Cell của nó mang text và một style, và đường vẽ của nó emit plain text, nên bất cứ thứ gì HTML thực sự chứa ngoài font và màu — link, superscript, đổi size inline, màu theo từng run — đều biến mất trước khi kịp chạm tới page
Hướng sống sót khi va vào document thật là hướng ngược lại. Mang các khả năng của table engine — occupancy grid, đo đạc thật, lặp header, column weighting — vào HTML renderer, và để rich-text rendering ở lại chỗ nó vốn đã chạy tốt. Đó là một thay đổi lớn hơn cây cầu, và là thay đổi giữ cho một hyperlink nằm trong table cell vẫn là hyperlink
Rowspan mà không cần union-find
Spanning cell tạo ra các atomic row group, nhưng closure trên các group đó không cần một disjoint-set structure tổng quát, vì occupancy luôn là một interval liền mạch. Một cell rowspan="3" bắt đầu ở row K chiếm row K tới K+2 và không gì khác, nên thông tin group rút gọn còn một end marker theo từng row
Thuật toán gói gọn trong hai dòng ý tưởng. Khi đặt một spanning cell bắt đầu ở K và kết thúc ở E, ghi GroupEnd[K] := Max(GroupEnd[K], E). Rồi đi các row một lần theo chiều ngược và áp G[R] := G[G[R]], thứ lan truyền từng row end ngược qua các span chồng lấn và cho ra transitive closure chỉ trong một lượt. Cái bạn nhận được, với mọi row, là row cuối cùng bắt buộc ở cùng page với nó — đúng cái mà bước pagination cần để quyết định break được phép rơi ở đâu
Phân phối height là nửa còn lại. Khi một spanning cell cần nhiều chỗ theo chiều dọc hơn những row nó phủ đang có, phần dư dồn cho row cuối của span, chứ không chia đều. Hãy xử lý spanning cell sau khi row height thường đã ổn định, rồi bù thêm vào row cuối của mỗi span. Chia đều phần dư nghe công bằng hơn và cho ra output sai lộ liễu: những row chỉ chứa cell ngắn một dòng bị phình to vì một cell không liên quan nào đó cách ba row phía trên tình cờ cao
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Stats: THPDFHTMLImportStatistics;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'audit-report.pdf';
Pdf.BeginDoc;
Importer := THPDFHTMLImporter.Create(Pdf);
try
Importer.Margin := 48;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Importer.MaxDOMNodes := 200000;
Importer.MaxLayoutOperations := 2000000;
if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
begin
Stats := Importer.Statistics;
Writeln('tables ', Stats.TableCount,
' page breaks ', Stats.PageBreakCount);
end;
finally
Importer.Free;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
RenderHTML5 nhận một author style sheet tùy chọn làm tham số thứ hai, và đó là nơi các print rule thuộc về. Đừng đem screen style sheet vào đây. Profile được version hóa, và HTML5ProfileMilestones báo nhóm capability nào bản build hiện tại đã triển khai: himParserCascade, himPagedLayout, himTablesForms và himBoundedResources, để application có thể suy giảm một cách chủ ý thay vì phát hiện ra một khoảng trống ngay trên production
Phép đo phải khớp với phép vẽ, tuyệt đối
Row height chỉ đúng khi code đo các dòng wrap wrap chúng theo cùng luật với code vẽ chúng. Nghe hiển nhiên, và đây lại là nguồn phổ biến nhất của những cái bảng có border không thẳng hàng với content. HotPDF đo bằng một greedy line counter, và counter đó phải khớp wrapping semantics của rich-text output path ở ba điểm cụ thể: nó chỉ break tại space, không bao giờ cắt giữa một word, và một word rộng hơn column được nhường cả một dòng
Yêu cầu thứ hai là font. Phép đo phải chạy với font của chính cell, set qua SetFont với đúng tên, style set và size trước khi gọi hàm tính width, chứ không phải với font nào tình cờ đang active. Text đậm thường rộng hơn mười phần trăm so với thường ở cùng size, đủ để biến cell ba dòng thành cell bốn dòng. Một table có header đậm và body không đậm, đo bằng một font duy nhất, sẽ sai đúng ở những row mà reader nhìn đầu tiên
Làm đúng mấy thứ này đổi được những gì bạn assert được trong test. Hiệu ứng quan sát được của phép đo chính xác là line spacing, không phải số glyph: một row một dòng cao khoảng 20 points trong khi ước lượng theo số ký tự cho cùng content đó dự đoán hai dòng và khoảng 35. Hãy assert trên khoảng cách dọc giữa các row. Và nhớ rằng PDF user space có Y tăng lên phía trên, nên một header nằm trên một body row nghĩa là giá trị Y của header lớn hơn — ngược với phản xạ screen coordinate hay viết
HotPDF từ chối lặp header khi nào?
Trong hai trường hợp, và cả hai nếu cứ làm theo đều cho ra output sai lộ liễu. Thứ nhất là một header block chứa spanning cell tràn qua khỏi header vào các body row. Lặp header sẽ vẽ nội dung cell đó lần thứ hai ở một vị trí nó không còn thuộc về, nên header được vẽ một lần và table tiếp tục mà không có nó. Thứ hai là một header cao hơn 90 phần trăm chiều cao page dùng được, nơi việc lặp gần như không chừa chỗ cho data và table sẽ không tiến thêm được chút nào
Cả hai lời từ chối đều có chủ ý và im lặng theo thiết kế, vì phương án thay thế còn tệ hơn. Nếu header của bạn không lặp trong khi bạn kỳ vọng ngược lại, hãy check markup xem có rowspan nào vắt qua ranh giới thead trước khi nghi ngờ engine. Đúng một markup pattern đó giải thích phần lớn các case bất ngờ
// Column weight đến từ markup, nên print style sheet là nơi
// kiểm soát chúng. Width được coi là weight, không phải pixel
const
PrintStyleSheet =
'table { width: 100%; }' +
'thead th { font-weight: bold; background: #eee; }' +
'td.amount { text-align: right; }';
// Một header row mang rowspan tràn sang body sẽ tắt việc lặp header.
// Giữ span trong cùng một section:
// <thead><tr><th rowspan="2">Item</th>...</tr></thead> ổn
// <tr><th rowspan="3">Item</th>... tràn vào tbody, không lặp
Column width hành xử như weight thay vì số đo tuyệt đối, và đó là hành vi giữ cho table vẫn dùng được khi content không khớp ước lượng của author. Một column khai báo 30 phần trăm nhận khoảng 30 phần trăm width khả dụng, nhưng phép phân phối tôn trọng minimum width mà mỗi column thực sự cần, nên một column hẹp chứa một token dài không thể break sẽ không âm thầm tràn ra ngoài table box
Nó nằm ở đâu trong một document pipeline
Phần table nằm trong profile paged-media rộng hơn, và các pagination rule, resource budget cùng cách xử lý CSS mô tả trong đường import paged-media HTML5 áp dụng nguyên vẹn cho document có chứa table. Nếu data của bạn không khởi đầu là HTML, đường dựng trực tiếp trong xây table thẳng vào một PDF né hẳn tầng parsing và cho bạn cùng hành vi grid qua một API. Và vì row height suy cho cùng phụ thuộc vào chỗ các dòng break, phần đo đạc trong text justification và line breaking là mảnh ghép tự nhiên cho ai đang tinh chỉnh output dạng bảng dày
Bài học dùng lại được ở đây chẳng nằm ở table chút nào. Khi một subsystem mới cần một capability mà subsystem cũ đã có, hãy hỏi trong hai bên, bên nào sở hữu thứ khó tái triển khai nhất. Grid arithmetic là vài chục dòng và di chuyển dễ. Rich-text rendering với inline link, superscript và styling theo từng run thì không, nên grid chuyển nhà còn text ở lại. HotPDF ship cả hai đường như một phần của HotPDF Delphi PDF component, để việc chọn giữa HTML input và dựng trực tiếp là quyết định của project chứ không phải của library