مقاله فنی

مقایسه دو فایل PDF در دلفی: ساختار و پیکسل

HotPDF دو سند PDF را از دلفی از طریق THPDFDocComparison مقایسه می‌کند، که گراف اشیا هر دو فایل را از کاتالوگ به بیرون می‌پیماید و، در صورت درخواست، هر جفت صفحه را نیز رندر کرده و پیکسل‌های متفاوت را اندازه‌گیری می‌کند. نتیجه یک گزارش JSON است که هر تفاوتی را که یافته، بودجه‌ای که مصرف کرده، و اینکه آیا مقایسه تا پایان اجرا شده یا نه، نام می‌برد. هر دو گذر اهمیت دارند، زیرا یک دیف ساختاری و یک دیف بصری به پرسش‌های متفاوتی پاسخ می‌دهند

پرسشی که پشت این ویژگی است معمولاً یک پرسش انتشار (release) است. یک موتور گزارش‌سازی تغییری می‌کند، خروجی دوباره تولید می‌شود، و کسی باید تصمیم بگیرد آیا چیزی جابه‌جا شده یا نه. بازکردن هر دو فایل کنار هم تا حدود سه صفحه مقیاس‌پذیر است، پیش از آنکه توجه از کار بیفتد. مقایسه بایت‌های خام فوراً شکست می‌خورد، زیرا دو اجرای یک تولیدکننده واحد به دلایلی که هیچ ربطی به آنچه یک خواننده می‌بیند ندارد، بایت‌های متفاوتی تولید می‌کنند

چرا PDFها می‌توانند از نظر بایتی متفاوت اما از نظر بصری یکسان باشند؟

دو PDF که به‌صورت مستقل تولید شده‌اند و یکسان چاپ می‌شوند، به‌طور معمول در بایت‌های خود متفاوت‌اند، و دلایل آن ساختاری هستند نه ظاهری. شماره‌های شیء بر اساس ترتیبی که اشیا به‌طور اتفاقی نوشته می‌شوند، تخصیص می‌یابند. زیرمجموعه‌های فونت، CIDها را بر اساس ترتیبی که گلیف‌ها برای اولین بار مواجه می‌شوند تخصیص می‌دهند، بنابراین زیرمجموعه‌ای که طی یک پیمایش کمی متفاوت ساخته شده، بایت‌های جریان محتوای متفاوتی برای همان متن قابل‌مشاهده تولید می‌کند. افست‌های جدول ارجاع متقابل (cross-reference) هرگاه هر چیز پیش از آن طول تغییر کند، جابه‌جا می‌شوند

به همین دلیل شماره‌های شیء نمی‌توانند به‌عنوان هویت بین‌سندی استفاده شوند. در عوض HotPDF هر نماگرفت را با پیمایش از کاتالوگ می‌سازد، دیکشنری‌ها را بر اساس ترتیب بایتی کلیدهایشان و آرایه‌ها را بر اساس اندیس بسط می‌دهد، بنابراین هر شیء با مسیری که به آن می‌رسد نام‌گذاری می‌شود. اشیایی که پیمایش نمی‌تواند از ریشه به آن‌ها برسد، به یک مسیر مصنوعی $Unreachable[...] بازمی‌گردند که شماره شیء و نسل (generation) را حمل می‌کند، که محتوای بی‌صاحب را در گزارش قابل‌مشاهده نگه می‌دارد به‌جای آنکه به‌طور خاموش غایب باشد

جریان‌ها (streams) با کپی‌کردن مقایسه نمی‌شوند. هر جریان یک امضای SHA-256 افزایشی مشارکت می‌دهد، که هنگام محاسبه، موقعیت اصلی جریان پس از آن بازگردانده می‌شود، بنابراین مقایسه دو فایل صدمگابایتی به‌معنای مادی‌سازی (materialise) دوباره صد مگابایت به‌صورت دوبرابر نیست

همتراز کردن صفحات وقتی یک سند دارای یک درج است

مقایسه صفحه ۱ در برابر صفحه ۱، صفحه ۲ در برابر صفحه ۲ و به همین ترتیب، فقط زمانی درست است که چیزی درج نشده باشد. یک صفحه جلد اضافه کنید و یک مقایسه ساده‌لوحانه هر صفحه را تغییریافته گزارش می‌دهد، که از نظر فنی درست اما از نظر عملیاتی بی‌فایده است

HotPDF پیش از دیف‌کردن، صفحات را همتراز می‌کند. برای هر صفحه یک امضا از متن قابل‌استخراج می‌سازد، برای صفحات بدون متن به یک امضای ساختاری بازمی‌گردد، و سپس طولانی‌ترین زیردنباله افزایشی (longest increasing subsequence) را روی اندیس‌های هدف تطبیق‌یافته محاسبه می‌کند. صفحاتی که درون آن زیردنباله هستند، صفحاتی هستند که صرفاً جابه‌جا شده‌اند؛ صفحاتی که بیرون آن هستند، جابه‌جایی‌های واقعی‌اند. همین تمایز است که دیفِ یک راهنمای ۴۰۰صفحه‌ای را قابل‌خواندن می‌کند، زیرا گزارش می‌گوید یک صفحه درج شده، نه اینکه چهارصد صفحه تغییر کرده

اجرای یک مقایسه ساختاری

ساده‌ترین فراخوانی دو سند بارگذاری‌شده و یک حالت می‌گیرد. cmStructural پیمایش گراف اشیا را انجام می‌دهد، cmRenderedImage مقایسه پیکسلی را انجام می‌دهد، cmFull هر دو را انجام می‌دهد، و حالت‌های سبک‌تر cmPageCount، cmPageText و cmObjectCount برای بررسی‌های دودی (smoke check) ارزان وجود دارند:

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 برای اسناد واقعی اندازه‌گذاری شده‌اند نه برای اسناد خصمانه، و هر بودجه معنایی مرتبط سقف مخصوص به خود را دارد: ۲۵۰٬۰۰۰ شیء، ۲٬۰۰۰٬۰۰۰ یال، عمق ۱۲۸، ۱۰٬۰۰۰ تفاوت گزارش‌شده، ۶۴ مگابایت به ازای هر جریان و ۵۱۲ مگابایت بایت جریان در مجموع، ۱ مگابایت به ازای هر مقدار و ۴٬۰۹۶ بایت به ازای هر مسیر. این‌ها را به‌عمد افزایش دهید وقتی مجموعه اسنادتان را می‌شناسید، و آن‌ها را کاهش دهید هنگام مقایسه فایل‌هایی که از بیرون آمده‌اند:

var
  Limits: THPDFStructuralCompareLimits;
  Options: THPDFRenderedCompareOptions;
begin
  Limits := THPDFStructuralCompareLimits.Default;
  Limits.MaxDifferences := 200;        // شکست سریع در CI
  Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;

  Options := THPDFRenderedCompareOptions.Default;
  Options.DPI := 150;                  // پیش‌فرض ۷۲ است
  Options.ColorTolerance := 2;         // نادیده‌گرفتن نویز گرد‌شدگی سطح ۱ تا ۲
  Options.MinimumSimilarity := 0.9995;
  Options.MaxChangedPixelRatio := 0.0005;
  Options.GenerateHeatmaps := True;    // نوشتن تصاویر همپوشان برای بازبینی

  Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
    Limits, Options);
end;

گذر رندرشده، تعداد پیکسل را از ابعاد صفحه و DPI درخواستی پیش از تخصیص هر بیت‌مپی تخمین می‌زند، و پس از آن بیت‌مپ واقعی را دوباره بررسی می‌کند، بنابراین یک هندسه صفحه معیوب نمی‌تواند با دروغ‌گفتن درباره اندازه خود از بودجه عبور کند. افزایش DPI وفاداری و هزینه را به‌صورت درجه‌دوم افزایش می‌دهد: ۱۵۰ DPI چهار برابر پیکسل‌های ۷۲ است، و سقف‌های پیکسلی به‌ازای هر صفحه و کل، دقیقاً به این دلیل وجود دارند که یک کار دسته‌ای در ۳۰۰ DPI در غیر این صورت راه خود را به سمت مشکل تخصیص می‌دهد

چقدر شباهت، شباهت کافی است؟

دو صفحه فقط زمانی مشابه به‌حساب می‌آیند که هر دو شرط برقرار باشند: نسبت پیکسل‌های تغییریافته در یا زیر MaxChangedPixelRatio باشد و شباهت در یا بالای MinimumSimilarity باشد. دو آستانه به‌جای یک آستانه، زیرا تعداد اندکی پیکسل به‌طرز فاجعه‌باری نادرست و یک موج گسترده از تغییرات رنگی جزئی، شکست‌های متفاوتی هستند، و هر یک به‌تنهایی می‌تواند در یک جریان‌کار قابل‌قبول و در جریان‌کاری دیگر رد‌کننده باشد. آزمون‌های آستانه از مقادیر گردنشده استفاده می‌کنند؛ شش رقم اعشار در JSON برای پایدار و قابل‌دیف نگه‌داشتن گزارش‌ها وجود دارند، نه برای تعریف‌کردن مقایسه

پیکسل‌های تغییریافته با استفاده از کاشی‌های اندازه‌ثابت به‌عنوان گره با مجاورت چهارطرفه در مناطقی گروه‌بندی می‌شوند، به‌جای پرشدگی سیلابی (flood fill) به ازای هر پیکسل. این کار حافظه را کران‌دار و فهرست مناطق را در بین اجراها پایدار نگه می‌دارد. کوتاه‌کردن جزئیات منطقه نگه‌داشته‌شده فقط روی فهرست تأثیر می‌گذارد، نه روی تعداد منطقه گزارش‌شده، بنابراین صفحه‌ای با مناطق تغییریافته بیشتر از MaxChangedRegions همچنان تعداد آن‌ها را گزارش می‌دهد

یک رفتار ارزش بیان صریح دارد زیرا غریزه معمول را وارونه می‌کند. شکست‌های رندرکننده، شکست‌های تخصیص حافظه و شکست‌های همپوشانی هرگز بلعیده نمی‌شوند. هر چیزی از این نوع به‌صورت renderError یا renderBudget ثبت می‌شود و renderComparisonComplete=false را اجباری می‌کند، زیرا صفحه‌ای که رندر آن شکست خورده، صفحه‌ای است که هیچ‌کس آن را مقایسه نکرده، و گزارش‌کردن آن به‌عنوان یکسان بدتر از گزارش‌نکردن هیچ‌چیز است

هر حالت کجای یک خط‌لوله جای می‌گیرد؟

مقایسه ساختاری به این پاسخ می‌دهد که چه چیزی تغییر کرده و پیش‌فرض درست برای مجموعه‌های رگرسیون است: مسیر، اندیس صفحه و شماره‌های شیء درگیر را نام می‌برد، بنابراین یک شکست به کدی اشاره می‌کند که آن را تولید کرده. مقایسه رندرشده به این پاسخ می‌دهد که آیا کسی متوجه خواهد شد، که پرسش تأییدیه‌ها و تأیید این است که آیا یک گذر بهینه‌سازی واقعاً بدون افت (lossless) بوده یا نه

این دو خوب با هم ترکیب می‌شوند. cmStructural را روی هر ساخت اجرا کنید و بگذارید در برابر تغییرات غیرمنتظره سطح شیء با صدای بلند شکست بخورد؛ cmFull را همراه با نقشه‌های حرارتی (heatmap) پیش از یک انتشار اجرا کنید، وقتی یک انسان برای نگاه‌کردن به همپوشانی‌ها در دسترس است. برای خط‌لوله‌هایی که به دلایل دیگری از قبل نشانه‌گذاری صفحه تولید می‌کنند، خروجی متنی که در صادرات صفحات PDF به SVG شرح داده شده، دیدگاه سومِ قابل‌دیف برای انسان را می‌دهد، و بررسی‌های خودکار در اتوماسیون گزارش پیش‌پرواز (preflight) پرسش‌های انطباق را پوشش می‌دهند که هیچ‌یک از دو حالت دیف قرار نیست به آن‌ها پاسخ دهد

مقایسه، پیش‌پرواز (preflight) و رندر، همان مدل شیء سند بارگذاری‌شده را به اشتراک می‌گذارند، بنابراین یک گذر واحد روی یک فایل می‌تواند هر سه را تغذیه کند. فهرست کامل ویژگی‌ها برای دلفی و C++Builder در صفحه مؤلفه HotPDF PDF برای دلفی قرار دارد