HotPDF có thể xây dựng một tài liệu phân trang từ một cây khai báo thay vì từ tọa độ. Bạn lắp ráp một THPDFDOMDocument từ các section, stack, văn bản, danh sách và bảng, giao nó cho THPDFDOMRenderer, và renderer sẽ đo đạc, phân trang, vẽ phần khung trang, và khi được yêu cầu, phát ra cây cấu trúc PDF/UA giúp kết quả trở nên khả năng tiếp cận được. Mã bố cục không bao giờ phải tự tính tọa độ y
Bất kỳ ai từng duy trì một bộ tạo báo cáo dựa trên tọa độ đều hiểu vì sao điều này quan trọng. Phiên bản đầu tiên hoạt động tốt. Rồi địa chỉ khách hàng dài ra thành ba dòng, một bảng có thêm hàng, một tiêu đề được bản địa hóa bị xuống dòng, và mọi tọa độ y phía sau đều sai. Các bản sửa dồn tích lại thành những đoạn kiểm tra ngắt trang thủ công rải rác khắp logic nghiệp vụ, và yêu cầu PDF gắn thẻ đến hai năm sau không thể được gắn thêm vào đoạn mã vốn không hề biết đoạn văn là gì
Cây sở hữu những gì, và vì sao quyền sở hữu lại nghiêm ngặt
DOM áp dụng quyền sở hữu duy nhất ở mọi cấp: tài liệu sở hữu các section của nó, một section sở hữu phần thân, header và footer của nó, và stack, container cùng bảng sở hữu các phần tử con của chúng. Việc tái sử dụng diễn ra qua Clone hoặc qua một factory đã đăng ký, chứ không bao giờ bằng cách gắn cùng một đối tượng vào hai đối tượng cha. Quy tắc đó không phải hình thức. Một thành phần xuất hiện hai lần trong cây sẽ bị đo hai lần với ràng buộc khác nhau và bị giải phóng hai lần khi dọn dẹp
Hệ quả thực tế cho mã gọi là các hàm hỗ trợ trả về các thực thể mới. Đăng ký một factory bằng RegisterComponent và gọi CreateComponent cho bạn một công thức được đặt tên tạo ra một thành phần mới mỗi lần, đây là cách mà các phần khung lặp lại như khối chữ ký hay footer pháp lý thuộc về trong cây
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // phát ra cây cấu trúc PDF/UA
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 tính bằng điểm (points)
Section.PageHeight := 842;
Section.MarginLeft := 56;
Section.MarginTop := 56;
Section.MarginRight := 56;
Section.MarginBottom := 56;
Section.Style.FontName := 'Helvetica';
Section.Style.FontSize := 10;
Section.Body.AddHeading('Annual maintenance report', 1);
Section.Body.AddText('Every asset inspected during the reporting ' +
'period is listed below, grouped by site.');
Section.Body.AddSpacer(12);
Table := THPDFDOMTable.Create('assets');
Table.AddColumn(3); // trọng số, không phải độ rộng tuyệt đối
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // hàng tiêu đề
Row[0].Text := 'Asset';
Row[1].Text := 'Last service';
Row[2].Text := 'Status';
for I := 0 to High(Assets) do
begin
Row := Table.AddRow(16);
Row[0].Text := Assets[I].Name;
Row[1].Text := Assets[I].ServiceDate;
Row[2].Text := Assets[I].Status;
end;
Section.Body.Add(Table);
end;
Làm sao việc phân trang tránh được chi phí bậc hai?
Cách phân trang ngây thơ cho một cây là nhân bản bất cứ thứ gì không vừa trang và mang nó sang trang tiếp theo. Trên một bảng có mười nghìn hàng, cách này nhân bản các hàng còn lại một lần cho mỗi trang, biến một tài liệu tuyến tính thành một tài liệu bậc hai
HotPDF chia tách một cách hẹp hơn thay vào đó. Renderer cấp cao nhất duyệt các phần tử con của phần thân theo chỉ mục và không bao giờ nhân bản toàn bộ một section hay body. Chỉ các stack và container lồng nhau thực sự trải dài qua ranh giới trang mới bị nhân bản phần cây con bị ảnh hưởng, và hai loại lá nặng nề mang theo một con trỏ vị trí (cursor) thay vì một bản sao: một phần tiếp nối văn bản lưu dải ký tự nguồn mà nó vẫn còn nợ, còn một phần tiếp nối bảng lưu lát cắt hàng mà nó chưa đặt xong. Tài liệu dài giữ được tính tuyến tính, và các đoạn văn dài tốn chi phí như nhau bất kể chúng ngắt một lần hay năm lần
Việc đo đạc luôn trung thực về tác dụng phụ. THPDFLayoutElement.Measure bắt buộc phải không có tác dụng phụ vẽ, và việc đặt thực tế luôn chạy qua THotPDF.PlaceLayoutElement, cùng một hàm trung tâm đo lại đoạn đã đặt, thiết lập quyền sở hữu tràn trang và ghi lại thông tin chẩn đoán. Renderer DOM chỉ quyết định chính sách trang mới, khung trang, khoảng cách và vòng đời của các phần tiếp nối
Các quy tắc tiêu đề bảng ngăn một tài liệu vô hạn
Lặp lại tiêu đề bảng qua các trang nghe có vẻ đơn giản nhưng ẩn chứa hai kiểu thất bại. HotPDF yêu cầu rằng hàng tiêu đề chỉ xuất hiện trong lượt hàng liên tiếp đầu tiên, và lần chia đầu tiên phải chứa vừa tất cả hàng tiêu đề cộng thêm ít nhất một hàng nội dung. Nếu không có quy tắc thứ hai, một tiêu đề cao hơn khoảng không gian còn lại sẽ tạo ra một trang chỉ chứa tiêu đề, tiếp theo là một trang giống hệt khác, cứ thế mãi mãi
Các trang tiếp nối vẽ lại tiêu đề, và bản sao được vẽ lại đó được đánh dấu là artifact chứ không phải nội dung, đây là câu trả lời đúng cho cả khả năng tiếp cận lẫn trích xuất văn bản. Hàng tiêu đề gốc vẫn ở trong cấu trúc bảng logic đúng một lần duy nhất. Bỏ qua điều này và một trình đọc màn hình sẽ đọc lại tiêu đề cột giữa chừng dữ liệu, còn một bộ trích xuất văn bản chèn một hàng tiêu đề trùng lặp giữa các hàng nội dung
Cũng có một trần phòng vệ về độ sâu tiếp nối, vì một thành phần tùy chỉnh có thể tự do triển khai Split theo cách luôn trả về một đuôi tương đương. Renderer kiểm tra giới hạn này sau khi tách đuôi và trước khi bắt đầu trang tiếp theo, và vòng lặp hiện tại giải phóng đuôi trong khối finally của riêng nó, nên một thành phần bên thứ ba hoạt động sai sẽ thất bại với một lỗi có thể chẩn đoán được thay vì làm đầy ổ đĩa
Một phần tử logic, nhiều mảnh trang
Gắn thẻ tự động là nơi mô hình phân trang và mô hình cấu trúc phải đồng thuận với nhau. Một đoạn văn bị chia qua hai trang vẫn là một đoạn văn logic duy nhất, nên nó phải vẫn là một phần tử cấu trúc duy nhất. Nhưng định danh nội dung được đánh dấu (marked content identifier) lại theo từng trang, nên mỗi mảnh hiển thị cần MCID riêng của nó trên trang mà nó xuất hiện
HotPDF giải quyết điều này bằng cách giữ một phần tử cấu trúc duy nhất và thêm một tham chiếu nội dung được đánh dấu vào mảng /K của nó cho mỗi mảnh, với cặp /Pg và /MCID định danh trang và định danh đó. Khe ParentTree cho MCID đó trỏ ngược lại về cùng một phần tử. Đây chính xác là điều ISO 14289 kỳ vọng, và đó là lý do bản nhân bản tiếp nối khác với bản nhân bản thông thường: một Clone thông thường có nghĩa là nội dung logic mới và nhận một định danh ngữ nghĩa mới, còn bản nhân bản tiếp nối nội bộ kế thừa định danh của thành phần mà nó tiếp nối
Việc tái sử dụng phần tử được tra cứu qua một chỉ mục các định danh ngữ nghĩa được sắp xếp theo con trỏ thành phần và tìm kiếm bằng so sánh nhị phân, giữ cho việc tra cứu có độ phức tạp logarit trên các cây lớn. Chỉ mục chỉ giữ tham chiếu không sở hữu; vòng đời của bản thân các đối tượng cấu trúc vẫn thuộc về đồ thị đối tượng PDF
Các quy tắc cấu trúc mà renderer thực thi ngay từ đầu
Khi GenerateStructure được bật, một số quy tắc PDF/UA được kiểm tra trong khi cây đang được render thay vì sau khi file đã tồn tại. Tiêu đề bắt đầu từ cấp 1 và không được bỏ cấp. LI chỉ được xuất hiện bên trong L, còn Lbl và LBody chỉ bên trong LI. TR thuộc về một bảng, còn TH và TD thuộc về một hàng. Một hình ảnh không có văn bản thay thế bị từ chối ở chế độ PDF/UA
Từ chối sớm là lựa chọn có chủ đích ở đây. Một trình kiểm định báo cáo thiếu văn bản thay thế sau khi tài liệu đã được ghi ra cho bạn biết rằng một lô mười nghìn bản kê cần được tạo lại; một renderer từ chối thành phần đó cho bạn biết chính xác thành phần nào, trong khi dữ liệu đã tạo ra nó vẫn còn trong phạm vi. Việc xác minh tuân thủ vẫn thuộc về pipeline như một bước riêng biệt, và cơ chế của bước đó được đề cập trong xác thực PDF/A, PDF/X và PDF/UA
var
Pdf: THotPDF;
Renderer: THPDFDOMRenderer;
Stats: THPDFDOMRenderStatistics;
begin
Pdf := THotPDF.Create(nil);
Renderer := THPDFDOMRenderer.Create;
try
Pdf.FileName := 'maintenance-report.pdf';
Pdf.BeginDoc;
Stats := Renderer.Render(Doc, Pdf);
Pdf.EndDoc;
Writeln(Format('%d page(s), %d placement(s), %d split(s)',
[Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
[Stats.StructureElementCount, Stats.MarkedContentCount,
Stats.ArtifactCount]));
Writeln(Format('deepest continuation chain: %d',
[Stats.MaximumContinuationDepth]));
finally
Renderer.Free;
Doc.Free;
Pdf.Free;
end;
end;
Bản ghi thống kê hữu ích hơn thoạt nhìn. SplitCount tăng vọt sau khi đổi mẫu thường có nghĩa là một thành phần bắt đầu đo cao hơn container của nó. MaximumContinuationDepth tăng dần là dấu hiệu cảnh báo sớm cho một thành phần mà Split của nó đạt được quá ít tiến triển trên mỗi trang. Và so sánh ArtifactCount với số trang tiếp nối xác nhận rằng các tiêu đề lặp lại thực sự đã được gắn thẻ là artifact
DOM nằm ở đâu bên cạnh API trực tiếp
DOM không thay thế việc vẽ trực tiếp; nó nằm trên cùng các đối tượng trang đó. Bất cứ thứ gì renderer đặt vào đều có thể xen kẽ với các lệnh gọi trực tiếp trên THotPDF, điều này quan trọng khi một báo cáo cần một phần tử đặt vị trí thủ công như một hình ảnh chữ ký tại một vị trí chính xác. Việc đóng trang vẫn nằm dưới sự kiểm soát của AddPage và EndDoc, nên chế độ flush ngay lập tức không giữ trang hoàn chỉnh nào trong bộ nhớ và bộ nhớ thường trú vẫn được chi phối bởi các phần tiếp nối hiện tại, tài nguyên font và đồ thị đối tượng tài liệu thông thường
Hãy chọn DOM khi nội dung theo hướng dữ liệu và bố cục theo hướng quy tắc, và giữ vẽ trực tiếp cho tác phẩm nghệ thuật cố định. Nếu vấn đề hiện tại của bạn cụ thể là phân trang bảng, cách tiếp cận hẹp hơn trong tạo bảng trong PDF đáng đọc trước, và hành vi cấp văn bản như canh đều được mô tả trong canh đều văn bản
Bố cục khai báo, gắn thẻ tự động và API vẽ trực tiếp đều nằm trong cùng thành phần cho Delphi và C++Builder; danh sách tính năng đầy đủ có trên trang thành phần HotPDF Delphi PDF