مقاله فنی

چرا برخی جریان‌های شیء PDF در Delphi به زباله رمزگشایی می‌شوند

یک جریان شیء PDF که بدون خطا inflate می‌شود اما همچنان به‌عنوان نویز خوانده می‌شود، معمولاً یک گام را کم دارد: معکوس‌کردن Predictor مطابق ISO 32000-1. وقتی دیکشنری /DecodeParms یک جریان /Predictor برابر ۲ یا بالاتر حمل می‌کند، بایت‌هایی که FlateDecode پس می‌دهد داده‌ی اصلی نیستند — آن‌ها مقادیر تفاضل‌گرفته‌شده‌ی ردیفی به‌سبک PNG یا تفاضل‌گرفته‌شده‌ی افقی به‌سبک TIFF هستند که به یک پاس بازسازی دوم پیش از اینکه هر جستجوی دیکشنری معنا پیدا کند نیاز دارند. PDFiumPas، کتابخانه‌ی بومی کامپوننت VCL برای PDF در Delphi و C++Builder، آن پاس بازسازی را در v2.16.0 اضافه کرد، به‌طور مشخص چون جریان‌های شیء PDF 1.5+ به بایت‌های تفاضل‌گرفته‌شده گسترش می‌یافتند که هیچ تجزیه‌گر دیکشنری‌ای نمی‌توانست بخواند

چرا FlateDecode به‌تنهایی کافی نیست

خودِ FlateDecode فقط رمزگشایی DEFLATE است (ISO 32000-1 §7.4.4.1): هر بایتی که رمزگذار به فشرده‌ساز داده را بازتولید می‌کند، نه بیشتر. Predictor یک لایه بالاتر زندگی می‌کند، در دیکشنری /DecodeParms جریان، و یک تبدیل را توصیف می‌کند که رمزگذار پیش از فشرده‌سازی اعمال کرده — تفاضل‌گیری دنباله‌های طولانی از مقادیر ساختاریافته‌ی مشابه، مثل اعداد صحیح متراکم درون یک جریان cross-reference یا یک جریان شیء را به دنباله‌های طولانی از اعداد کوچک تبدیل می‌کند که DEFLATE به‌مراتب بهتر فشرده می‌کند. ISO 32000-1 §7.4.4.3 (جدول ۸) صریح است که لغوکردن این تبدیل بخشی از رمزگشایی یک جریان فیلترشده است، نه یک پاس پاک‌سازی اختیاری، اما به‌راحتی می‌توان یک کمکی FlateDecode نوشت که فقط inflate را فرا می‌خواند و همان‌جا متوقف می‌شود

علامت به‌محض اینکه بدانید به‌دنبال چه چیزی بگردید متمایز است. بایت‌های تفاضل‌گرفته‌شده-با-Predictor نویز تصادفی نیستند — همچنان شکل یک جریان فشرده را حمل می‌کنند، پس یک تجزیه‌گر ساده‌لوح اغلب از چند توکن معتبر-به‌نظر رسیده عبور می‌کند پیش از برخورد به یک دنباله‌ی بایتی که هیچ‌طور نمی‌تواند یک نام، عدد، یا جداکننده‌ی PDF باشد، و ردیف‌های متفاوت در افست‌های متفاوتی بسته به اینکه مقادیر زیرین چقدر اتفاقاً با همسایه‌هایشان فرق می‌کردند شکست می‌خورند. آن ناسازگاری چیزی است که این باگ را از یک فایل شکست‌خورده‌ی تکی سخت برای تشخیص می‌کند: دو PDF از همان تولیدکننده می‌توانند فقط در اینکه کدام مقادیر اتفاقاً تکرار می‌شوند فرق داشته باشند، پس یکی تقریباً به‌طور تصادفی تجزیه می‌شود درحالی‌که دیگری کاملاً شکست می‌خورد

پارامتر Predictor در PDF واقعاً چه کاری انجام می‌دهد؟

ورودی /Predictor در /DecodeParms به یک خواننده‌ی مطابق می‌گوید کدام معکوس‌سازی را اجرا کند، و جدول ۸ از ISO 32000-1 مقادیری را که در عمل اهمیت دارند تعریف می‌کند: ۱ یعنی هیچ پیش‌بینی‌ای اعمال نشده، ۲ TIFF Predictor 2 (تفاضل‌گیری افقی) را انتخاب می‌کند، و هر مقدار از ۱۰ تا ۱۵ پیش‌بینی به‌سبک PNG را انتخاب می‌کند. سه کلید دیگر همراهش سفر می‌کنند — /Colors، /BitsPerComponent، و /Columns — و با هم هندسه‌ی ردیفی را توصیف می‌کنند که تفاضل‌گیری در برابرش محاسبه شده، حتی زمانی که جریان هیچ داده‌ی تصویری نگه نمی‌دارد: یک جریان شیء یک تصویر نیست، اما نویسندگان PDF همان ماشین‌آلات predictor مبتنی-بر-ردیف را برای آن دوباره استفاده می‌کنند چون delta-سپس-deflate اعداد صحیح متراکم و افست‌های شیء را بهتر از deflate‌کردن خامشان فشرده می‌کند

TIFF Predictor 2 ساده‌تر از این دو طرح است: هر مؤلفه به‌عنوان تفاضل از همان مؤلفه در پیکسل قبلی روی همان ردیف ذخیره می‌شود، و هر ردیف در لبه‌ی چپش بازنشانی می‌شود به‌جای اینکه یک تفاضل را از ردیف بالا حمل کند. پیش‌بینی PNG خاص‌تر است، چون فیلتر واقعی می‌تواند از ردیف به ردیف تغییر کند: هر ردیف با یک بایت تگ تکی شروع می‌شود — ۰ برای None، ۱ برای Sub، ۲ برای Up، ۳ برای Average، ۴ برای Paeth — و آن تگ، نه مقدار اعلان‌شده‌ی /Predictor، تصمیم می‌گیرد آن ردیف مشخص چطور بازسازی می‌شود. یک /Predictor برابر ۱۲ واقعاً فقط اشاره‌ی رمزگذار است که فیلتر Up را ترجیح داده، جایی که هر بایت با افزودن بایتی که مستقیم بالای آن در ردیف قبلی است بازیابی می‌شود، اما یک رمزگشای درست همچنان باید تگ را روی هر ردیف بخواند به‌جای اینکه فرض کند Up در سراسر آن است

چرا جریان‌های شیء یک Predictor گم‌شده را نامرئی می‌کنند؟

جریان‌های شیء این مسئله را ترکیب می‌کنند به‌جای صرفاً تکرار آن. ISO 32000-1 §7.5.7 اجازه می‌دهد یک نویسنده‌ی PDF 1.5+ چندین شیء غیرمستقیم را در یک کانتینر فشرده‌ی تکی، یک /ObjStm، بسته‌بندی کند، و رایج است که دقیقاً همان اشیائی که یک اعتبارسنج بیشترین نیاز را به آن‌ها دارد — کاتالوگ، /OutputIntents، یا یک جریان /Metadata از نوع XMP — با /Predictor برابر ۱۲ پیوست‌شده از آن کانتینر سفر کنند، چون آن اشیاء به‌اندازه‌ی کافی کوتاه و تکراری‌اند که از تفاضل‌گیری ردیفی سود ببرند. وقتی گام predictor گم شود، گسترش جریان شیء خطایی raise نمی‌کند: یک دنباله‌ی بایتی تولید می‌کند که سطحی باورپذیر به‌نظر می‌رسد اما به اشیاء مورد انتظار توکنایز نمی‌شود، پس هر چیزی که آن درون بسته‌بندی شده صرفاً نمایان نمی‌شود. رندر به‌ندرت متوجه می‌شود، چون یک موتور رندر مطابق از پیش داده‌ی تفاضل‌گرفته‌شده-با-predictor را پیش از اینکه اصلاً به layout برسد بازسازی می‌کند؛ کدی که متوجه می‌شود دقیقاً همان نوعی است که این باگ درونش پنهان شده — یک اعتبارسنج، امضاکننده، یا بررسی‌کننده‌ی نسخه که خودش بایت‌های خام PDF را برای پاسخ‌دادن به یک سؤال ساختاری می‌پیماید، بدون هیچ fallbackای به‌محض اینکه نمای خودش از جریان شیء اشتباه برگردد

PDFiumPas دقیقاً به همین شکست پیش از v2.16.0 برخورد کرد. جریان‌های شیء ساخته‌شده با /Predictor برابر ۱۲، حالت رایج برای نویسندگان PDF 1.5+، از طریق PdfExpandObjectStreams به بایت‌های تفاضل‌گرفته‌شده گسترش می‌یافتند که اسکنر ساختاری نمی‌توانست تجزیه کند، پس کاتالوگ، /OutputIntents، و اشیاء /Metadata بسته‌بندی‌شده درون آن عملاً برای اسکن‌های مطابقت نامرئی بودند — هیچ استثنایی، هیچ هشداری، فقط یک اسکن که بی‌سروصدا طوری رفتار می‌کرد انگار آن اشیاء غایب بودند. مکانیک‌های عمیق‌تر اینکه PDFiumPas چطور یک جریان شیء را در برابر جدول cross-reference فعال حل می‌کند، از جمله حالت‌های ترکیبی و xref-stream خالص، جداگانه در مقاله‌ی اعتبارسنجی جریان‌های شیء و xref با PDFiumPas پوشش داده شده؛ گام predictor توصیف‌شده در اینجا پس از آن حل اجرا می‌شود، روی بایت‌هایی که هر شیء فشرده واقعاً حمل می‌کند

معکوس‌کردن ردیف‌های Predictor از نوع PNG و TIFF در Pascal

PDFiumPas تفاضل‌گیری را در یک روال تکی، PdfApplyPredictor، معکوس می‌کند، و محاسبات هندسی‌اش ارزش دانستن دارد چه آن را فراخوانی کنید چه ایده را در کد Delphi خودتان دوباره پیاده‌سازی کنید. عرض ردیف به بایت ceil(Columns × Colors × BitsPerComponent ÷ 8) است و عرض بایت به‌ازای هر پیکسل که هر دو الگوریتم استفاده می‌کنند ceil(Colors × BitsPerComponent ÷ 8) است — هرکدام از این گردکردن‌ها را اشتباه بگیرید و بازسازی در سراسر یک مرز ردیف می‌خواند به‌جای درون آن. یک /Predictor زیر ۲ دست‌نخورده رها می‌شود، چون ۱ یعنی رمزگذار اصلاً هیچ تبدیلی اعمال نکرده؛ ۲ شاخه‌ی TIFF نشان‌داده‌شده در زیر را انتخاب می‌کند، و هر چیزی از ۱۰ به بالا به بازسازی فیلتر-ردیف PNG سقوط می‌کند، جایی که بایت تگ در ابتدای هر ردیف — نه مقدار اعلان‌شده‌ی /Predictor — تصمیم می‌گیرد آن ردیف مشخص چطور لغو می‌شود

function PdfApplyPredictor(const Src: TBytes;
  Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
  RowLen, Bpp, R, I: Integer;
begin
  Result:= Src;
  if Predictor< 2 then
    Exit;                                   // 1 = no prediction, nothing to undo
  if Colors<= 0 then Colors:= 1;
  if Bpc<= 0 then Bpc:= 8;
  if Columns<= 0 then Columns:= 1;
  if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
    Exit;                                   // reject hostile row geometries
  RowLen:= (Columns* Colors* Bpc+ 7) div 8;  // ceil(), per ISO 32000-1 Table 8
  Bpp:= (Colors* Bpc+ 7) div 8;
  if Predictor= 2 then
  begin
    if Bpc<> 8 then
      Exit;                                 // only the 8-bit layout is reconstructed
    Result:= Copy(Src, 0, Length(Src));
    R:= 0;
    while R+ RowLen<= Length(Result) do
    begin
      for I:= R+ Bpp to R+ RowLen- 1 do
        Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
      Inc(R, RowLen);
    end;
    Exit;
  end;
  // Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
  SrcOfs:= R* (RowLen+ 1);
  DstOfs:= R* RowLen;
  Tag:= Src[SrcOfs];
  Inc(SrcOfs);
  for I:= 0 to RowLen- 1 do
  begin
    if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0;   // byte to the left
    if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0;   // byte above
    case Tag of
    1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A);            // Sub
    2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B);            // Up
    3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
    // Paeth (tag 4) adds whichever of A, B or the byte above-left sits
    // closest to the linear predictor A+ B- C; tag 0 (None) copies the
    // filtered byte through unchanged
    else Result[DstOfs+ I]:= Src[SrcOfs+ I];
    end;
  end;
end;

PDFiumPas در v2.16.0 چه چیزی را تغییر داد

رفع اشکالی که در PDFiumPas v2.16.0 عرضه شد درون PdfReadAndDecodeStream می‌نشیند، روالی که بایت‌های خام یک جریان را می‌خواند و آن‌ها را برای هر فراخواننده‌ای که نیاز دارد ساختار PDF را در سطح بایت بازرسی کند، از جمله گسترش جریان شیء، رمزگشایی می‌کند؛ این فقط پس از تأیید اینکه /Filter یک FlateDecode ساده است، هرگز یک زنجیره، بازسازی را امتحان می‌کند، چون یک فیلتر زنجیره‌ای نمی‌تواند ایمن در این لایه با predictor تصحیح شود. خواندن /Predictor، /Colors، /BitsPerComponent، و /Columns از دیکشنری جریان هم به یک تجزیه‌گر دیکشنری عمومی نیاز ندارد: PdfDictRefNum هر کلید را با جستجوی مستقیم توکن-نام درون بازه‌ی بایتی همان یک دیکشنری پیدا می‌کند، که اینجا دقیقاً به این دلیل امن است که آن چهار کلید نمی‌توانند درون یک دیکشنری جریان تکی تکرار یا تودرتو شوند. همان جستجوی توکن-نام به‌محض اینکه به یک ناحیه‌ی بزرگ‌تر یا کمتر-محدود از یک فایل PDF اشاره کند بسیار پرخطرتر است، که موضوع مقاله‌ی همراه درباره‌ی تجزیه‌ی ایمن دیکشنری‌های PDF است

// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
  Inflated:= PdfInflate(Raw);
  Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
  if Predictor>= 2 then
  begin
    PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
    PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
    PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
    Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
  end
  else
    Result:= Inflated;
end;

پیش از v2.16.0، یک جریان شیء ساخته‌شده با /Predictor برابر ۱۲ بدون raise‌شدن هیچ خطایی به بایت‌های تفاضل‌گرفته‌شده گسترش می‌یافت، پس هر شیء کاتالوگ، /OutputIntents، یا /Metadata بسته‌بندی‌شده درون آن بدون هیچ هشداری از اسکن‌های ساختاری PDFiumPas گم می‌شد. پس از رفع اشکال، همان جریان شیء inflate و سپس به‌درستی بازسازی می‌شود، و اشیاء بسته‌بندی‌شده درون آن دوباره برای آن اسکن‌ها قابل‌مشاهده می‌شوند. حدود دفاعی همراه رفع اشکال سفر کردند: PdfApplyPredictor حالا /Colors بالای ۶۴، /BitsPerComponent بالای ۳۲، و /Columns بالای ۲^۲۴ را کاملاً رد می‌کند، چون آن ترکیب‌ها هندسه‌های ردیفی را توصیف می‌کنند که هیچ تولیدکننده‌ی واقعی PDF به آن‌ها نیاز ندارد و عمدتاً وجود دارند تا یک رمزگشا را وادار کنند بسیار بیش از آنچه بایت‌های ورودی توجیه می‌کند حافظه تخصیص دهد

var
  Pdf: TPdf;
  Report: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'incoming.pdf';
    Pdf.Active := True;
    Report := Pdf.ValidatePdfA;
    if not Report.IsCompliant then
      LogNonCompliance(Report); // caller-supplied handler
  finally
    Pdf.Free;
  end;
end;

محدودیت‌های ارزش دانستن

بازسازی predictor در PDFiumPas دو لبه دارد که ارزش دانستن دارند پیش از اینکه به آن‌ها اعتماد کنید. بازسازی TIFF Predictor 2 فقط حالت ۸-بیت-به-ازای-هر-مؤلفه را پوشش می‌دهد؛ PDF بسته‌بندی‌های باریک‌تر را مجاز می‌کند، اما داده‌ی تفاضل‌گرفته‌شده‌ی زیر-بایتی TIFF بدون بازسازی عبور می‌کند به‌جای اینکه حدس زده شود، پس یک جریان که /Predictor برابر ۲ با /BitsPerComponent برابر ۱، ۲، یا ۴ اعلان می‌کند امروز از طریق این مسیر به‌درستی رمزگشایی نمی‌شود. پیش‌بینی PNG چنین محدودیتی ندارد — هر ردیف تگ فیلتر خودش را می‌دهد، و هر پنج نوع تعریف‌شده صرف‌نظر از اینکه مقدار اعلان‌شده‌ی /Predictor بین ۱۰ تا ۱۵ اتفاقاً چیست بازسازی می‌شود، که با اینکه فیلترکردن به‌سبک PNG واقعاً چطور کار می‌کند مطابقت دارد: مقدار اعلان‌شده به یک اشاره‌ی درباره‌ی اینکه رمزگذار عمدتاً از چه چیزی استفاده کرده نزدیک‌تر است تا یک وعده درباره‌ی هر ردیف

موتور رندر بومی PDFium از پیش داده‌ی تصویری و جریان-محتوای تفاضل‌گرفته‌شده-با-predictor را به‌درستی بازسازی می‌کند، که دقیقاً همان دلیلی است که یک فایل می‌تواند در هر نمایشگر معمولی کاملاً رندر شود درحالی‌که یک اعتبارسنج، امضاکننده، یا بررسی‌کننده‌ی نسخه‌ی سطح-بایت ساخته‌شده روی آن، همان بایت‌ها را اشتباه می‌خواند. رمزگشایی آگاه-از-predictor توصیف‌شده در اینجا اعتبارسنجی PDF/A، اسکن ساختاری، و ویژگی‌های امضای PDFiumPas، کامپوننت بومی VCL برای PDFium در Delphi و C++Builder، را پشتیبانی می‌کند