HotPDF เปรียบเทียบเอกสาร PDF สองฉบับจาก Delphi ผ่าน THPDFDocComparison ซึ่งไล่เดินกราฟอ็อบเจกต์ของทั้งสองไฟล์จากแคตตาล็อกออกไปด้านนอก และเมื่อได้รับคำสั่งก็จะเรนเดอร์แต่ละคู่หน้าและวัดพิกเซลที่แตกต่างกันด้วย ผลลัพธ์คือรายงาน JSON ที่ระบุความแตกต่างทุกจุดที่พบ งบประมาณที่ใช้ไป และการเปรียบเทียบดำเนินไปจนจบหรือไม่ ทั้งสองรอบสำคัญด้วยกันทั้งคู่ เพราะการเทียบความต่างเชิงโครงสร้างและการเทียบความต่างเชิงภาพตอบคำถามที่ต่างกัน
คำถามเบื้องหลังคุณสมบัตินี้มักเป็นคำถามเกี่ยวกับการออกรุ่นใหม่ เอนจินสร้างรายงานได้รับการเปลี่ยนแปลง ผลลัพธ์ถูกสร้างขึ้นใหม่ และมีคนต้องตัดสินใจว่ามีอะไรเปลี่ยนไปหรือไม่ การเปิดไฟล์สองไฟล์เทียบกันใช้ได้ดีจนถึงราวสามหน้าก่อนที่ความสนใจจะเริ่มล้า การเปรียบเทียบไบต์ดิบล้มเหลวทันที เพราะการรันตัวสร้างเดียวกันสองครั้งให้ไบต์ที่ต่างกันด้วยเหตุผลที่ไม่เกี่ยวข้องเลยกับสิ่งที่ผู้อ่านมองเห็น
เหตุใด PDF จึงมีไบต์ต่างกันแต่มองเห็นเหมือนกันได้
PDF สองไฟล์ที่สร้างขึ้นแยกกันแต่พิมพ์ออกมาเหมือนกันทุกประการ มักมีไบต์ที่ต่างกันเป็นเรื่องปกติ และเหตุผลก็เป็นเรื่องเชิงโครงสร้างมากกว่าเรื่องความสวยงาม หมายเลขอ็อบเจกต์ถูกกำหนดตามลำดับที่อ็อบเจกต์ถูกเขียนออกมา ชุดย่อยฟอนต์จัดสรร CID ตามลำดับที่พบกลิฟครั้งแรก ดังนั้นชุดย่อยที่สร้างขึ้นระหว่างการไล่เดินที่ต่างกันเล็กน้อยจะสร้างไบต์สตรีมเนื้อหาที่ต่างกันสำหรับข้อความที่มองเห็นเหมือนกัน ออฟเซ็ตของตารางอ้างอิงข้ามจะเลื่อนทุกครั้งที่มีสิ่งใดต้นทางเปลี่ยนความยาว
นี่คือเหตุผลที่หมายเลขอ็อบเจกต์ใช้เป็นเอกลักษณ์ข้ามเอกสารไม่ได้ HotPDF จึงสร้างสแนปช็อตแต่ละชุดด้วยการไล่เดินจากแคตตาล็อก ขยายดิกชันนารีตามลำดับไบต์ของคีย์ และขยายอาร์เรย์ตามดัชนี ดังนั้นทุกอ็อบเจกต์จึงถูกตั้งชื่อตามเส้นทางที่ไปถึงมัน อ็อบเจกต์ที่การไล่เดินไปไม่ถึงจากราก จะถอยกลับไปใช้เส้นทางสังเคราะห์ $Unreachable[...] ที่พกหมายเลขอ็อบเจกต์และรุ่นไว้ ซึ่งทำให้เนื้อหาที่ถูกทิ้งไม่มีเจ้าของยังคงมองเห็นได้ในรายงานแทนที่จะหายไปอย่างเงียบๆ
สตรีมไม่ได้ถูกเปรียบเทียบด้วยการคัดลอก แต่ละสตรีมสร้างลายเซ็น SHA-256 แบบสะสม โดยคำนวณไปพร้อมกับการเรียกคืนตำแหน่งเดิมของสตรีมในภายหลัง ดังนั้นการเปรียบเทียบไฟล์ขนาดสองร้อยเมกะไบต์จึงไม่ได้หมายความว่าต้องสร้างข้อมูลสองร้อยเมกะไบต์ขึ้นมาซ้ำสองครั้ง
การจัดเรียงหน้าให้ตรงกันเมื่อเอกสารหนึ่งมีการแทรกหน้า
การเปรียบเทียบหน้า 1 กับหน้า 1 หน้า 2 กับหน้า 2 และเรื่อยไปเช่นนี้ ถูกต้องก็ต่อเมื่อไม่มีการแทรกหน้าใดเลย หากแทรกหน้าปกเข้าไป การเปรียบเทียบแบบไร้เดียงสาจะรายงานว่าทุกหน้าเปลี่ยนแปลง ซึ่งถูกต้องในทางเทคนิคแต่ไม่มีประโยชน์ในทางปฏิบัติ
HotPDF จัดเรียงหน้าให้ตรงกันก่อนเทียบความต่าง โดยสร้างลายเซ็นต่อหน้าจากข้อความที่สกัดได้ ถอยกลับไปใช้ลายเซ็นเชิงโครงสร้างสำหรับหน้าที่ไม่มีข้อความ แล้วคำนวณลำดับย่อยที่เพิ่มขึ้นยาวที่สุดเหนือดัชนีเป้าหมายที่จับคู่กันได้ หน้าที่อยู่ในลำดับย่อยนั้นคือหน้าที่แค่ขยับตำแหน่ง ส่วนหน้าที่อยู่นอกลำดับย่อยคือการเคลื่อนย้ายที่แท้จริง ความแตกต่างนี้เองที่ทำให้การเทียบความต่างของคู่มือ 400 หน้าอ่านเข้าใจได้ เพราะรายงานจะบอกว่ามีการแทรกหนึ่งหน้า แทนที่จะบอกว่าสี่ร้อยหน้าเปลี่ยนแปลง
การรันการเปรียบเทียบเชิงโครงสร้าง
การเรียกที่ง่ายที่สุดรับเอกสารที่โหลดไว้สองฉบับและโหมดหนึ่ง cmStructural ทำการไล่เดินกราฟอ็อบเจกต์ cmRenderedImage ทำการเปรียบเทียบพิกเซล cmFull ทำทั้งสองอย่าง ส่วนโหมดที่เบากว่าอย่าง cmPageCount, cmPageText และ cmObjectCount มีไว้สำหรับการตรวจสอบเบื้องต้นแบบประหยัด:
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;
รายงานแยกแยะสามสถานะที่ค่าบูลีนทำไม่ได้ identical บอกว่ามีสิ่งใดแตกต่างหรือไม่ comparisonComplete บอกว่าการไล่เดินเสร็จสิ้นหรือไม่ และ comparisonBudget ระบุขีดจำกัดที่หยุดการทำงานหากมี การเปรียบเทียบที่ใช้งบประมาณจนหมดจะรายงาน comparisonComplete=false และ identical=false พร้อมกัน เพราะการไล่เดินที่ถูกตัดตอนไม่มีพื้นฐานให้อ้างความเท่ากันได้ ระบบอัตโนมัติใดๆ ที่อ่านเฉพาะ identical ในที่สุดจะปฏิบัติต่อการหยุดเพราะหมดงบประมาณเหมือนเป็นความแตกต่างจริง ดังนั้นควรอ่านทั้งสามค่า
ขีดจำกัดใดบ้างที่ควบคุมการไล่เดินให้อยู่ในขอบเขต
ค่าเริ่มต้นใน THPDFStructuralCompareLimits.Default ถูกกำหนดขนาดไว้สำหรับเอกสารจริง ไม่ใช่สำหรับเอกสารที่ประสงค์ร้าย และงบประมาณที่มีนัยสำคัญเชิงความหมายทุกตัวมีเพดานของตัวเอง ได้แก่ อ็อบเจกต์ 250,000 ชิ้น ขอบ 2,000,000 เส้น ความลึก 128 ความแตกต่างที่รายงาน 10,000 รายการ สตรีมละ 64 MB และไบต์สตรีมรวม 512 MB ค่าละ 1 MB และเส้นทางละ 4,096 ไบต์ ปรับเพิ่มค่าเหล่านี้อย่างจงใจเมื่อคุณรู้จักชุดข้อมูลของตัวเองดี และปรับลดลงเมื่อเปรียบเทียบไฟล์ที่มาจากภายนอก:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // ล้มเหลวเร็วในระบบ CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // ค่าเริ่มต้นคือ 72
Options.ColorTolerance := 2; // เพิกเฉยต่อสัญญาณรบกวนจากการปัดเศษระดับ 1-2
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // เขียนภาพซ้อนทับสำหรับตรวจสอบ
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
รอบการเรนเดอร์จะประมาณจำนวนพิกเซลจากขนาดหน้าและ DPI ที่ร้องขอก่อนที่จะจัดสรรบิตแมปใดๆ แล้วตรวจสอบบิตแมปจริงอีกครั้งในภายหลัง ดังนั้นเรขาคณิตของหน้าที่ผิดรูปจึงไม่สามารถแอบผ่านงบประมาณไปได้ด้วยการโกหกเรื่องขนาดของตัวเอง การเพิ่ม DPI จะเพิ่มความละเอียดและต้นทุนแบบยกกำลังสอง 150 DPI มีพิกเซลมากกว่า 72 ถึงสี่เท่า และเพดานพิกเซลต่อหน้าและรวมทั้งหมดมีไว้ก็เพราะงานแบบแบตช์ที่ 300 DPI จะจัดสรรจนเกิดปัญหาหากไม่มีเพดานนี้
ความคล้ายกันระดับใดจึงถือว่าใกล้เคียงพอ
สองหน้าจะนับว่าคล้ายกันก็ต่อเมื่อเงื่อนไขทั้งสองข้อเป็นจริงพร้อมกัน คืออัตราส่วนพิกเซลที่เปลี่ยนแปลงต้องอยู่ที่หรือต่ำกว่า MaxChangedPixelRatio และความคล้ายคลึงต้องอยู่ที่หรือสูงกว่า MinimumSimilarity การใช้เกณฑ์สองค่าแทนที่จะเป็นค่าเดียว เพราะพิกเซลผิดพลาดร้ายแรงจำนวนหยิบมือกับการเปลี่ยนสีเล็กน้อยกระจายทั่วบริเวณเป็นความล้มเหลวคนละแบบกัน และแต่ละแบบเพียงลำพังอาจยอมรับได้ในเวิร์กโฟลว์หนึ่งแต่ตัดสิทธิ์ในอีกเวิร์กโฟลว์หนึ่ง การทดสอบเกณฑ์ใช้ค่าที่ไม่ปัดเศษ ทศนิยมหกตำแหน่งใน JSON มีไว้เพื่อให้รายงานคงที่และเทียบความต่างได้ ไม่ใช่เพื่อกำหนดนิยามการเปรียบเทียบ
พิกเซลที่เปลี่ยนแปลงจะถูกจัดกลุ่มเป็นบริเวณโดยใช้ไทล์ขนาดคงที่เป็นโหนดที่มีความติดกันแบบสี่ทิศทาง แทนที่จะใช้ flood fill ต่อพิกเซล วิธีนี้ทำให้หน่วยความจำถูกจำกัดขอบเขตและรายการบริเวณคงที่ในทุกการรัน การตัดรายละเอียดบริเวณที่เก็บไว้มีผลเฉพาะกับรายการเท่านั้น ไม่กระทบจำนวนบริเวณที่รายงาน ดังนั้นหน้าที่มีบริเวณเปลี่ยนแปลงมากกว่า MaxChangedRegions ก็ยังคงรายงานจำนวนที่แท้จริงได้
พฤติกรรมหนึ่งควรกล่าวให้ชัดเจน เพราะมันสวนทางกับสัญชาตญาณปกติ ความล้มเหลวของตัวเรนเดอร์ ความล้มเหลวของการจัดสรร และความล้มเหลวของการซ้อนทับ จะไม่มีทางถูกกลืนหายไปเฉยๆ สิ่งใดก็ตามในลักษณะนี้จะถูกบันทึกเป็น renderError หรือ renderBudget และบังคับให้ renderComparisonComplete=false เพราะหน้าที่เรนเดอร์ไม่สำเร็จคือหน้าที่ไม่มีใครเปรียบเทียบเลย และการรายงานว่ามันเหมือนกันย่อมเลวร้ายกว่าการไม่รายงานอะไรเลย
แต่ละโหมดควรอยู่ตรงไหนในไปป์ไลน์
การเปรียบเทียบเชิงโครงสร้างตอบคำถามว่าอะไรเปลี่ยนไป และเป็นค่าเริ่มต้นที่เหมาะสมสำหรับชุดทดสอบถดถอย มันระบุเส้นทาง ดัชนีหน้า และหมายเลขอ็อบเจกต์ที่เกี่ยวข้อง ดังนั้นความล้มเหลวจึงชี้ไปที่โค้ดที่สร้างมันขึ้นมาได้ การเปรียบเทียบเชิงภาพที่เรนเดอร์แล้วตอบคำถามว่าจะมีใครสังเกตเห็นหรือไม่ ซึ่งเป็นคำถามสำหรับการอนุมัติและการตรวจสอบว่ารอบการปรับให้เหมาะสมนั้นไม่สูญเสียข้อมูลจริงๆ
ทั้งสองแบบผสานกันได้ดี รัน cmStructural ในทุกรอบบิลด์และปล่อยให้มันล้มเหลวอย่างชัดเจนเมื่อพบการเปลี่ยนแปลงระดับอ็อบเจกต์ที่ไม่คาดคิด รัน cmFull พร้อมฮีตแมปก่อนออกรุ่นใหม่ เมื่อมีคนพร้อมดูภาพซ้อนทับ สำหรับไปป์ไลน์ที่สร้างมาร์กอัปหน้าอยู่แล้วด้วยเหตุผลอื่น ผลลัพธ์ข้อความที่อธิบายไว้ในการส่งออกหน้า PDF เป็น SVG ให้มุมมองที่สามที่มนุษย์เทียบความต่างได้ และการตรวจสอบอัตโนมัติในระบบอัตโนมัติสำหรับรายงานพรีไฟลต์ ครอบคลุมคำถามด้านความสอดคล้องตามมาตรฐานที่โหมดเทียบความต่างทั้งสองแบบไม่ได้ออกแบบมาให้ตอบ
การเปรียบเทียบ พรีไฟลต์ และการเรนเดอร์ ใช้แบบจำลองอ็อบเจกต์ของเอกสารที่โหลดไว้ร่วมกัน ดังนั้นการไล่ผ่านไฟล์เพียงรอบเดียวก็ป้อนข้อมูลให้ทั้งสามอย่างได้ รายการคุณสมบัติทั้งหมดสำหรับ Delphi และ C++Builder อยู่ที่หน้าคอมโพเนนต์ PDF ของ HotPDF สำหรับ Delphi