HotPDF so sánh hai tài liệu PDF từ Delphi thông qua THPDFDocComparison, hàm này duyệt đồ thị đối tượng của cả hai file từ catalog trở ra ngoài, và khi được yêu cầu, còn render từng cặp trang rồi đo các điểm ảnh khác nhau. Kết quả là một báo cáo JSON nêu tên mọi khác biệt tìm được, ngân sách đã tiêu thụ, và liệu quá trình so sánh có chạy đến hoàn tất hay không. Cả hai bước đều quan trọng, vì so sánh cấu trúc và so sánh trực quan trả lời hai câu hỏi khác nhau
Câu hỏi đằng sau tính năng này thường là câu hỏi về phát hành. Một bộ máy tạo báo cáo nhận một thay đổi, đầu ra được tạo lại, và ai đó phải quyết định xem có gì đã thay đổi hay không. Mở cả hai file cạnh nhau chỉ khả thi với khoảng ba trang trước khi sự chú ý không còn theo kịp. So sánh byte thô thất bại ngay lập tức, vì hai lần chạy của cùng một bộ tạo cho ra byte khác nhau vì những lý do chẳng liên quan gì đến những gì người đọc nhìn thấy
Vì sao hai file PDF có thể khác byte nhưng giống hệt nhau về mặt trực quan?
Hai file PDF được tạo ra độc lập mà in ra giống hệt nhau thường xuyên khác nhau về byte, và lý do mang tính cấu trúc chứ không phải thẩm mỹ. Số đối tượng được gán theo thứ tự các đối tượng tình cờ được ghi ra. Tập con font phân bổ CID theo thứ tự glyph được gặp lần đầu, nên một tập con được xây dựng trong một lượt duyệt hơi khác sẽ tạo ra byte luồng nội dung khác nhau cho cùng một đoạn văn bản hiển thị. Offset bảng tham chiếu chéo dịch chuyển bất cứ khi nào có gì đó ở phía trước thay đổi độ dài
Đây là lý do vì sao số đối tượng không thể dùng làm định danh xuyên tài liệu. Thay vào đó, HotPDF xây dựng mỗi ảnh chụp bằng cách duyệt từ catalog, mở rộng dictionary theo thứ tự byte của khóa và mảng theo chỉ mục, nên mọi đối tượng được đặt tên bằng đường dẫn tới nó. Những đối tượng mà quá trình duyệt không thể tiếp cận từ gốc sẽ quay về đường dẫn tổng hợp $Unreachable[...] mang theo số đối tượng và thế hệ (generation), giúp nội dung mồ côi vẫn hiển thị trong báo cáo thay vì âm thầm biến mất
Các luồng (stream) không được so sánh bằng cách sao chép. Mỗi luồng đóng góp một chữ ký SHA-256 tính theo kiểu tăng dần, được tính trong khi khôi phục lại vị trí luồng gốc sau đó, nên so sánh hai file trăm megabyte không có nghĩa là phải hiện thực hóa hai trăm megabyte hai lần
Căn chỉnh trang khi một tài liệu có phần chèn thêm
So sánh trang 1 với trang 1, trang 2 với trang 2 và cứ thế chỉ đúng khi không có gì bị chèn thêm. Chèn một trang bìa và một phép so sánh ngây thơ sẽ báo mọi trang đều thay đổi, điều này về mặt kỹ thuật là đúng nhưng vô dụng trong thực tế vận hành
HotPDF căn chỉnh các trang trước khi so sánh khác biệt. Nó xây dựng một chữ ký cho mỗi trang từ văn bản có thể trích xuất được, quay về chữ ký cấu trúc cho các trang không có văn bản, rồi tính dãy con tăng dài nhất (longest increasing subsequence) trên các chỉ mục đích đã khớp. Các trang nằm trong dãy con đó chỉ đơn thuần bị dịch chuyển; các trang nằm ngoài dãy con đó thực sự đã di chuyển. Sự phân biệt này chính là điều giúp bản so sánh khác biệt của một tài liệu hướng dẫn 400 trang có thể đọc được, vì báo cáo nói rằng một trang đã được chèn thêm thay vì bốn trăm trang đã thay đổi
Chạy một phép so sánh cấu trúc
Lệnh gọi đơn giản nhất nhận hai tài liệu đã tải và một chế độ. cmStructural thực hiện duyệt đồ thị đối tượng, cmRenderedImage thực hiện so sánh điểm ảnh, cmFull thực hiện cả hai, và các chế độ nhẹ hơn cmPageCount, cmPageText và cmObjectCount tồn tại cho các kiểm tra nhanh giá rẻ:
uses
HPDFDoc, HPDFDocCompare;
var
DocA, DocB: THotPDF;
Report: AnsiString;
begin
DocA := THotPDF.Create(nil);
DocB := THotPDF.Create(nil);
try
if (DocA.LoadFromFile('baseline.pdf') <= 0) or
(DocB.LoadFromFile('candidate.pdf') <= 0) then
Exit;
Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
with TFileStream.Create('diff.json', fmCreate) do
try
WriteBuffer(Report[1], Length(Report));
finally
Free;
end;
finally
DocB.Free;
DocA.Free;
end;
end;
Báo cáo phân biệt ba trạng thái mà một giá trị boolean không thể diễn tả. identical cho biết có bất kỳ điều gì khác biệt hay không, comparisonComplete cho biết quá trình duyệt đã hoàn tất hay chưa, và comparisonBudget nêu tên giới hạn đã dừng nó lại nếu có. Một phép so sánh làm cạn ngân sách sẽ báo cáo comparisonComplete=false và identical=false cùng nhau, vì một lượt duyệt bị cắt ngắn không có cơ sở để khẳng định sự đồng nhất. Bất kỳ hệ thống tự động nào chỉ đọc identical cuối cùng sẽ coi việc dừng vì ngân sách là một khác biệt thực sự, nên hãy đọc cả ba giá trị
Những giới hạn nào giữ cho quá trình duyệt luôn bị chặn?
Các giá trị mặc định trong THPDFStructuralCompareLimits.Default được định cỡ cho tài liệu thực tế chứ không phải cho tài liệu mang tính đối kháng, và mọi ngân sách có ý nghĩa ngữ nghĩa đều có trần riêng: 250.000 đối tượng, 2.000.000 cạnh, độ sâu 128, 10.000 khác biệt được báo cáo, 64 MB cho mỗi luồng và 512 MB tổng số byte luồng, 1 MB cho mỗi giá trị và 4.096 byte cho mỗi đường dẫn. Hãy nâng chúng một cách có chủ đích khi bạn hiểu rõ kho dữ liệu của mình, và hạ chúng xuống khi so sánh các file đến từ bên ngoài:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // thất bại nhanh trong CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // mặc định là 72
Options.ColorTolerance := 2; // bỏ qua nhiễu làm tròn 1-2 mức
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // ghi ảnh phủ (overlay) để soát xét
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Bước so sánh render ước tính số điểm ảnh từ kích thước trang và DPI được yêu cầu trước khi bất kỳ bitmap nào được cấp phát, và kiểm tra lại bitmap thực tế sau đó, nên một hình học trang bị lỗi không thể lọt qua ngân sách bằng cách nói dối về kích thước của nó. Tăng DPI làm tăng độ trung thực và chi phí theo cấp số bậc hai: 150 DPI có số điểm ảnh gấp bốn lần so với 72, và trần điểm ảnh trên mỗi trang cũng như tổng thể tồn tại chính xác vì một tác vụ hàng loạt ở 300 DPI nếu không sẽ tự cấp phát mình vào rắc rối
Giống đến mức nào thì được coi là đủ giống?
Hai trang chỉ được tính là giống nhau khi cả hai điều kiện đều đúng: tỷ lệ điểm ảnh thay đổi bằng hoặc thấp hơn MaxChangedPixelRatio và độ giống nhau bằng hoặc cao hơn MinimumSimilarity. Hai ngưỡng thay vì một, vì một vài điểm ảnh sai lệch nghiêm trọng và một dải rộng những thay đổi màu nhỏ nhặt là hai kiểu lỗi khác nhau, và mỗi kiểu riêng lẻ có thể chấp nhận được trong quy trình này nhưng bị loại trong quy trình khác. Kiểm tra ngưỡng dùng giá trị chưa làm tròn; sáu chữ số thập phân trong JSON tồn tại để giữ báo cáo ổn định và có thể so sánh khác biệt được, không phải để định nghĩa phép so sánh
Các điểm ảnh thay đổi được nhóm thành vùng bằng cách dùng các ô (tile) kích thước cố định làm nút với kề bốn hướng, thay vì flood fill theo từng điểm ảnh. Điều đó giữ bộ nhớ bị chặn và danh sách vùng ổn định qua các lần chạy. Cắt bớt chi tiết vùng được giữ lại chỉ ảnh hưởng đến danh sách hiển thị, không ảnh hưởng đến số vùng được báo cáo, nên một trang có nhiều vùng thay đổi hơn MaxChangedRegions vẫn báo cáo được có bao nhiêu vùng thực sự tồn tại
Một hành vi đáng nói thẳng vì nó đảo ngược bản năng thông thường. Lỗi renderer, lỗi cấp phát và lỗi phủ ảnh không bao giờ bị nuốt chửng. Bất kỳ lỗi loại nào như vậy đều được ghi lại là renderError hoặc renderBudget và buộc renderComparisonComplete=false, vì một trang render thất bại là một trang không ai so sánh được, và báo cáo nó là giống hệt còn tệ hơn là không báo cáo gì cả
Mỗi chế độ thuộc về đâu trong một pipeline?
So sánh cấu trúc trả lời câu hỏi có gì đã thay đổi và là mặc định đúng đắn cho các bộ kiểm thử hồi quy: nó nêu tên đường dẫn, chỉ số trang và số đối tượng liên quan, nên một thất bại chỉ thẳng vào đoạn mã đã tạo ra nó. So sánh render trả lời câu hỏi liệu có ai nhận ra hay không, đây là câu hỏi dành cho việc phê duyệt và cho việc xác minh rằng một bước tối ưu hóa thực sự không mất mát dữ liệu
Chúng kết hợp tốt với nhau. Chạy cmStructural trên mọi bản build và để nó thất bại rõ ràng khi có thay đổi bất ngờ ở cấp đối tượng; chạy cmFull kèm heatmap trước một đợt phát hành, khi có người sẵn sàng xem qua các lớp phủ. Với các pipeline đã phát ra mã đánh dấu trang vì lý do khác, đầu ra văn bản được mô tả trong xuất trang PDF sang SVG mang lại góc nhìn thứ ba, có thể đọc và so sánh khác biệt bằng con người, còn các kiểm tra tự động trong tự động hóa báo cáo preflight bao phủ các câu hỏi về tuân thủ chuẩn mà không chế độ so sánh khác biệt nào ở trên nhắm tới trả lời
So sánh khác biệt, preflight và render dùng chung một mô hình đối tượng tài liệu đã tải, nên một lượt duyệt duy nhất qua một file có thể cấp dữ liệu cho cả ba. Danh sách tính năng đầy đủ cho Delphi và C++Builder có trên trang thành phần HotPDF Delphi PDF