المقارنة المرئية في PDFlibPas تعرض ملفي PDF بالـ DPI نفسه وتقارن ما يراه القارئ فعلاً. TPDFlib.CompareRenderedPagesWith يقارن زوج صفحات واحد ويعيد البكسلات المتغيرة والمناطق وقناعاً وطبقة تركيب (overlay). أما TPDFlib.CompareDocumentVisualsWith و TPDFlib.CompareFileVisuals فيقرنان مستندين كاملين صفحةً صفحة، ويكشفان الصفحات المدرجة والمحذوفة، ويبلّغان عن كل شيء بصيغة JSON
حالة الاستخدام هي اختبار الانحدار المرئي (visual regression testing). أحدهم يعيد كتابة قالب الفاتورة، أو يرفع إصدار حزمة خطوط، أو يستبدل محرك العرض، والسؤال ليس هل تغيّرت البايتات (إنها تتغير دائماً) بل هل سيرى أي عميل فرقاً. المقارنة البنيوية تجيب عن سؤال مختلف. ملفان يمكن أن يختلفا في كل تدفق محتوى ومع ذلك يرسمَان صفحات متطابقة، ومعامل واحد منزاح يمكن أن يحرّك رقماً إجمالياً بمقدار ثلاث نقاط دون أن يلمس أي شيء سيلمحته أداة استخراج النصوص. البكسلات هي الطبقة الوحيدة التي يكون فيها "يبدو مثلها" قابلاً للاختبار
ماذا تقارن مقارنة PDF المرئية فعلاً؟
يقارن PDFlibPas النتيجة المرئية على صفحة بيضاء، لا محتويات المخازن الخام. كل صفحة تُعرض عبر RenderPageToString بصيغة BMP غير مضغوطة، وتُفك إلى صورة نقطية (bitmap) من الأعلى إلى الأسفل، وأي بكسل يحمل قناة alpha يُركَّب فوق الأبيض قبل مقارنة القنوات. لذلك صورة RGB وصورة RGBA تبدوان متطابقتين على الشاشة تُقارنان بوصفهما متطابقتين، وهذا بالضبط ما تريده حين يأتي الجانبان من مساري عرض مختلفين
محلل BMP لديه طابعة مقصودة واحدة يستحق معرفتها. الصورة النقطية 32 بت بنمط BI_RGB تحمل بايتاً رابعاً لكل بكسل، لكن الصيغة تترك ذلك البايت غير معرّف: بعض الكتّاب يملؤونه بـ 0، وبعضهم بـ 255، وبعضهم ببقايا قديمة. يتجاهله PLDecodeVisualBMP ويحتفظ باللون. معاملته بوصفه درجة عتامة (opacity) سيسمح لعارض يكتب أصفاراً بمحو كل بكسل على الصفحة، وستبلّغ المقارنة بسعادة عن مستندين فارغين متطابقين. وإذا غذّيت صورك النقطية بنفسك إلى الواجهة البرمجية الأدنى مستوى، لا يُحترم الـ alpha إلا عندما تعلن الصيغة بنفسك كـ vpfRGBA أو vpfBGRA
يُعد البكسل متغيراً عندما تختلف أي من قنواته الحمراء أو الخضراء أو الزرقاء بأكثر من ColorTolerance. التسامح 0 يعني تطابقاً بتّياً. عملياً، قيمة من خانة الآحاد تمتص رجّة تنعيم الحواف (anti-aliasing) بين إصدارات العارض دون أن تحجب محرفاً تغيّر، لأن تغيّر نص حقيقي يحرك القنوات بأضعاف ذلك. خذ الرقم من تشغيل أساس معروف بالسلامة، لا بالتخمين
مقارنة زوج صفحات واحد عبر CompareRenderedPagesWith
TPDFlib.CompareRenderedPagesWith هو الأداة حين تعرف مسبقاً أي الصفحات تنتمي معاً وتريد التفصيل: صندوق محيط، ومناطق تغيّر متصلة، وقناع اختياري لكل بكسل، وطبقة تركيب اختيارية بصبغة حمراء. سجل الخيارات ونوع النتيجة يعيشان في وحدة PDFlibVisualCompare، ويعطيك PLDefaultVisualCompareOptions تسامحاً 0، ومربعات 32 بكسل، وبلا مناطق تجاهل وبلا قناع أو طبقة تركيب
uses
PDFlibrary, PDFlibVisualCompare;
var
Baseline, Candidate: TPDFlib;
Options: TPDFVisualCompareOptions;
Comparison: TPDFVisualCompareResult;
ErrorText: WideString;
begin
Baseline := TPDFlib.Create;
Candidate := TPDFlib.Create;
try
if (Baseline.LoadFromFile('invoice-v1.pdf', '') = 1) and
(Candidate.LoadFromFile('invoice-v2.pdf', '') = 1) then
begin
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.IncludeOverlay := True;
// حجب طابع وقت الطباعة؛ وحدات بكسل عند الـ DPI أدناه
SetLength(Options.IgnoreRects, 1);
Options.IgnoreRects[0].Left := 600;
Options.IgnoreRects[0].Top := 20;
Options.IgnoreRects[0].Right := 790;
Options.IgnoreRects[0].Bottom := 50;
if Baseline.CompareRenderedPagesWith(Candidate, 1, 1, 96, Options,
Comparison, ErrorText) then
begin
if Comparison.ChangedPixels > 0 then
Writeln(PLVisualCompareResultToJSON(Comparison));
end
else
Writeln('Compare failed: ', ErrorText);
end;
finally
Candidate.Free;
Baseline.Free;
end;
end;
ثلاث تفاصيل تعضّ الناس هنا. مستطيلات التجاهل بوحدات البكسل المعروضة بأصل أعلى-يسار وفترات نصف مغلقة، فقيمتا Right و Bottom حصريتان، والمستطيل نفسه يغطي جزءاً مختلفاً من الصفحة إذا غيّرت الـ DPI. المستطيلات المتداخلة تُحسب مرة واحدة، والبكسلات المتجاهَلة تظهر في IgnoredPixels بدلاً من أن تختفي، وهو ما يبقي ChangedRatio صادقاً لأنه يقسم على البكسلات المقارَنة فقط. أخيراً، يستدعي الاستدعاء ذو الصفحة الواحدة أن يكون لكلا العرضين الأبعاد نفسها؛ الصفحة التي تغيّر مقاسها تفشل بخطأ بدلاً من أن تُمَدّ لتلائم
تأتي المناطق من مرحلة المربعات: تُجمع البكسلات المتغيرة في مربعات بحجم TileSize وتُدمج المربعات المتجاورة في منطقة واحدة بحدودها وعدد بكسلاتها. المربعات الأكبر تعطي مناطق أقل وأخشن، وهذا عادةً ما يريده تقرير الاختبار. طبقة التركيب نسخة RGB من الصفحة الثانية بصبغة حمراء على البكسلات المتغيرة، وتُعاد بايتات خاماً في Comparison.Overlay؛ وكتابتها إلى PNG ضمن مخلفات CI لديك هي مهمة كودك، لا المكتبة
كيف يتعامل إقران الصفحات على مستوى المستند مع الصفحات المدرجة؟
TPDFlib.CompareDocumentVisualsWith يقرن الصفحات عبر أطول متتالية جزئية مشتركة (longest common subsequence) على بصمات بكسل دقيقة، فلا تحوّل صفحة مدرجة واحدة كل الصفحات اللاحقة إلى تغيّر كاذب. الحلقة الساذجة من صفحة 1 إلى صفحة 1 وصفحة 2 إلى صفحة 2 عديمة الفائدة لحظة يكتسب القالب صفحة غلاف: كل صفحة بعد الإدراج منزاحة بمقدار واحد والتقرير يتوهّج أحمر من أعله إلى أسفله
كل صفحة تُعرض وتُختزل إلى مفتاح مكوّن من عرض بكسلاتها وارتفاعها وبصمة SHA-256 للبكسلات المرئية. المقاس أهم مما يبدو: صفحة طولية بيضاء بالكامل وصفحة عرضية بيضاء بالكامل بمساحة واحدة لهما مخزنا بكسل متطابقان بتّياً، ومن دون المقاسين في المفتاح لترسّبت إحداهما إلى الأخرى. المفاتيح المتطابقة على الجانبين تصبح مراسٍ (anchors)، ويلتقط الـ LCS أطول سلسلة مرتبة منها، وتُقرن الصفحات بين مرساة متتاليَين بالترتيب. وعندما يحوي فجوةٌ صفحاتٍ أكثر في جانب واحد، يُبلَّغ عن الفائض بوصفه added أو removed
var
Options: TPDFVisualCompareOptions;
Report: WideString;
begin
// Baseline و Candidate محمّلان كالسابق، بإعدادات عرض متطابقة
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.TileSize := 64;
Report := Baseline.CompareDocumentVisualsWith(Candidate, 96, Options);
if Baseline.LastErrorCode <> 0 then
Writeln('Visual diff failed: ', Report)
else
Writeln(Report);
end;
المراسٍ بتطابق بتّي، وهذا خيار تصميم لا سهو. ينطبق ColorTolerance على مقارنة الصفحات المقرنة فقط، لا على البصمة أبداً. الصفحة التي لا تحمل سوى ضجيج تنعيم حواف لن ترسو، لكنها تهبط في فجوة ومع ذلك، وتُقرن بالترتيب مع نظيرتها، ثم تُقارن بوصفها unchanged تحت التسامح. المراسي الضبابية أكثر تسامحاً وأقل قابلية للتنبؤ بكثير، لأن الصفحات شبه المكررة (فواصل فارغة، صفحات بنود متكررة) ستبدأ في مطابقة بعضها بعضاً
كن واضحاً حول ماهية هذا المحاذاة. تسمّيها الـ JSON بـ exact-raster-lcs-gap-order عن قصد: إنها محاذاة نقطية (raster alignment)، لا تعرّف دلالي للصفحات. إذا احتوت فجوة صفحة معاد كتابتها وصفحة مدرجة، فقد يقارن الإقران بالترتيب إعادة الكتابة مقابل الإدراج ويبلّغ عن الأصلية بوصفها محذوفة. والمستندات المليئة بصفحات متطابقة غامضة بالمثل. حين يكون الإقران مؤثراً في الحكم، انظر إلى الأزواج، لا إلى العدّادات فقط
قراءة تقرير JSON
تقرير المستند ذو شكل ثابت يمكنك وضع تأكيدات (assert) عليه في مشغّل اختبارات. على النجاح يحمل "status":"complete" و "complete":true واسم المحاذاة و firstPageCount و secondPageCount والعدّادات changedPages و addedPages و removedPages و unchangedPages. ثم تسرد مصفوفة pairs كل صف بـ firstPage و secondPage (0 للجانب المفقود) و kind إما unchanged أو changed أو size_changed أو added أو removed
صفوف النوع changed و unchanged تضمّن كائن comparison يضم width و height و comparedPixels و changedPixels و ignoredPixels و changedRatio و boundingBox (null عندما لا يتغير شيء) وقائمة regions من bounds مع changedPixels. صف size_changed لا يحوي مقارنة إطلاقاً؛ يبلّغ عن firstWidth و firstHeight و secondWidth و secondHeight ويُحسب ضمن changedPages. تمديد صفحة غيّر مقاسها لمقارنتها سينتج جداراً من البكسلات المتغيرة عديمة المعنى، لذا تبلّغ المكتبة عن تغيّر الهندسة وتتوقف عنده
تقرير المستند لا يحوي أقنعة ولا طبقات تركيب أبداً. يجبر CompareDocumentVisualsWith الخيارين IncludeMask و IncludeOverlay على الإيقاف حتى لو طلبتهما خياراتك، لذا يبقى استهلاك الذاكرة ثابتاً مع نمو عدد الصفحات بدلاً من الاحتفاظ بمعاينة كاملة الصفحة لكل زوج. استخدم التقرير لإيجاد الصفحات المثيرة للاهتمام، ثم استدعِ CompareRenderedPagesWith على تلك الصفحات فقط للحصول على طبقة التركيب. الإخفاقات تعود بـ "status":"error" و "complete":false مع سلسلة error، ويُضبط LastErrorCode على 401
المقارنة مع ملف عبر CompareFileVisuals
TPDFlib.CompareFileVisuals هو الشكل ذو الاستدعاء الواحد لبيئات CI: يحمل ملف PDF الآخر، وينسخ إعدادات العارض الحالية إليه، ويعيد JSON المستند نفسه. التوقيع هو CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance)، حيث يجب أن يكون التسامح بين 0 و255 وأن يكون الـ DPI محدوداً وموجباً
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('baseline\statement.pdf', '') = 1 then
begin
Report := Lib.CompareFileVisuals('build\statement.pdf', '', 96, 8);
Writeln(Report);
end;
finally
Lib.Free;
end;
end;
وراثة العارض هي سبب تفضيل هذا الاستدعاء على تحميل الملف الثاني بنفسك. يأخذ المستند المحمّل مقياس العرض ونوع القص وإعدادات الرسوميات للنُسخة الحالية، إضافة إلى اختيار محرك Cairo أو PDFium حين يكونان مترجمَين. وإذا بنيت نسختَي TPDFlib يدوياً من أجل CompareDocumentVisualsWith، فتكوين كليهما بشكل متطابق مسؤوليتك، وأي عدم تطابق يظهر بوصفه تغيّر كل صفحة. كما أنه لا يقبل مناطق تجاهل؛ توجه إلى الواجهة البرمجية المعرَّفة بالأنواع حين تحتاجها
الحدود والتكلفة ومكان المقارنة البنيوية
يقيّد PDFlibPas العمل صراحةً حتى يفشل الإدخال المعادٍ أو الضخم بخطأ بدلاً من استنزاف الذاكرة. الصفحة المعروضة قد لا تتجاوز 64 مليون بكسل، وشبكة المربعات محدودة بـ 4 ملايين مربع، ومحاذاة الصفحات ترفض المدخلات التي يتجاوز فيها مجموع عددَي الصفحات 8192 أو كانت مصفوفة الـ LCS ستتجاوز 4 ملايين خانة. كما أن مقارنة المستندات تعرض كل صفحة مرتين، مرة للبصمة ومرة أخرى للمقارنة المقرنة، فاحسب على ذلك عند 150 DPI وما فوق. لأغلب أطقم اختبارات الانحدار تلتقط 72 إلى 96 DPI تغيّرات التخطيط والنص بجزء يسير من الوقت
المقارنة المرئية والمقارنة البنيوية تجيبان عن سؤالين مختلفين، وخط الأنابيب الجاد يريد عادةً كليهما. مقارنة البكسل تخبرك هل سيلمح القارئ شيئاً. وهي لا تستطيع أن تخبرك هل اكتسب مستند موقَّع تعليقاً توضيحياً يُعرض خارج الصفحة، أو هل استبدل تحديث تدريجي بهدوء قيمة حقل نموذج بنص مرئي مطابق. لتلك الطبقة، تصنيف ما تغيّر في PDF بعد توقيعه تستعرض المقارنة على مستوى المراجعات. ولأن اختيار العارض يقود البكسلات، فإن عرض PDF متعدد المحركات في Delphi يستحق القراءة قبل أن تثبّت أساسك على محرك واحد
الأسئلة الشائعة
هل تمدد المقارنة المرئية في PDFlibPas الصفحات التي تغيّر مقاسها؟
لا. يعيد CompareRenderedPagesWith خطأً للصفحات ذات مقاس البكسل المختلف، وتعلّم تقرير المستند تلك الأزواج size_changed بالبُعدين معاً بدلاً من مقارنتها
لماذا لا تُستخدم الصفحات شبه المتطابقة كمراسٍ؟
تستخدم المراسِ بصمة SHA-256 دقيقة للبكسلات المرئية مع مقاس الصفحة، فتبقى المحاذاة قابلة للتنبؤ. يُطبَّق التسامح لاحقاً، عند مقارنة الصفحات المقرنة، فصفحات الضجيج الخالص تنتهي unchanged في النهاية
هل يمكنني الحصول على صورة تركيب لكل صفحة في مستند؟
ليس من استدعاء المستند، الذي يحذف الأقنعة وطبقات التركيب عمداً لإبقاء الذاكرة ثابتة. استخدم الـ JSON لإيجاد الصفحات المتغيرة، ثم استدعِ CompareRenderedPagesWith مع IncludeOverlay على تلك الصفحات
المقارنة المرئية وإقران الصفحات وبقية واجهات العرض والمستندات تأتي ضمن مكتبة PDFlibPas PDF لـ Delphi