مقاله فنی

رمزگشایی Predictor و LZWDecode در PDF با HotPDF Delphi

یک جریان با علامت /Predictor 12 به این معنا نیست که هر ردیف از فیلتر ۲ PNG استفاده می‌کند. HotPDF، مؤلفه بومی VCL برای PDF در Delphi و C++Builder، مقادیر پیش‌بین ۱۰ تا ۱۵ را به‌عنوان یک خانواده در نظر می‌گیرد: برچسب فیلتر واقعی، از ۰ تا ۴، اولین بایت هر ردیف رمزگذاری‌شده است، و HPDFDecodePredictor آن برچسب را ردیف‌به‌ردیف می‌خواند و تأیید می‌کند. این تمایز شکل تقریباً هر باگ در این گوشه از PDF است، چون وقتی اشتباه می‌کنید هیچ‌چیز خطا پرتاب نمی‌کند. زنجیره فیلتر اجرا می‌شود، رستر اندازه‌ای است که انتظار داشتید، و تصویر به‌صورت نویز مورب یا گرادیانی که با هر خط اسکن بیشتر منحرف می‌شود بیرون می‌آید. پنج عدد در /DecodeParms (ISO 32000-1 §7.4.4) بیشتر معنای بایت‌ها را تغییر می‌دهند تا طولشان را، پس یک عدد اشتباه به‌جای یک خطا، آشغال باورپذیر تولید می‌کند

چرا /Predictor 12 به این معنا نیست که فیلتر ۲ PNG روی هر ردیف است؟

چون عدد پیش‌بین فقط می‌گوید «پیش‌بینی PNG در حال استفاده است»، نه اینکه کدام فیلتر. رمزگذارهای PNG یک فیلتر به‌ازای هر خط اسکن انتخاب می‌کنند و فیلتر PDF آن را به ارث می‌برد، پس مقادیر پیش‌بین ۱۰ (None)، ۱۱ (Sub)، ۱۲ (Up)، ۱۳ (Average)، ۱۴ (Paeth) و ۱۵ (Optimum) همه یکسان رمزگشایی می‌شوند: بایت برچسب پیشرو هر ردیف چیزی است که رمزگشا باید از آن اطاعت کند. پیامد چیدمان به‌اندازه معنا اهمیت دارد. هر ردیف رمزگذاری‌شده 1 + RowBytes بایت طول دارد، بنابراین ورودی دقیقاً به‌اندازه تعداد ردیف‌ها از خروجی بیشتر است، و جریانی که طولش مضرب صحیحی از RowBytes + 1 نیست طبق تعریف بریده شده است. HotPDF پیش از لمس‌کردن هر بایت این مرز را بررسی می‌کند، هر برچسبی بالای ۴ را با Invalid PNG predictor row tag رد می‌کند، و ردیف قبلی را مستقیماً از تک بافر خروجی می‌خواند به‌جای ساختن یک آرایه ردیف دوبعدی. فیلترهای ۱ و ۳ به عقب تا BytesPerPixel در ردیف جاری برمی‌گردند، فیلتر ۲ مستقیم به بالا می‌خواند، فیلتر ۴ انتخاب Paeth را روی چپ، بالا و بالا-چپ اجرا می‌کند — و هر چهارتا روی خروجی از پیش بازسازی‌شده عمل می‌کنند، به همین دلیل ردیف بالا باید همان ردیف رمزگشایی‌شده باشد و هرگز ورودی فیلترشده نباشد

uses
  HPDFPredictor;

var
  Filtered, Raster: AnsiString;
  ErrorText: string;
begin
  // /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
  if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
       Int64(1024) * 3 * 8192, Raster, ErrorText) then
    ConsumeRaster(Raster)
  else
    LogStreamDefect('predictor', ErrorText);   // no exception, no partial raster
end;

آرگومان MaxOutputBytes تزئین نیست. یک مرحله پیش‌بین در واقع یک مرحله فشرده‌گشایی پنهان‌شده است، و یک مقدار /Columns خصمانه یا صرفاً خراب چند کیلوبایت ورودی را به یک درخواست تخصیص چندگیگابایتی تبدیل می‌کند. HotPDF بیت به‌ازای ردیف، بایت به‌ازای ردیف و اندازه کل رستر را ابتدا در Int64 محاسبه می‌کند، هندسه‌ای که سرریز می‌کند را رد می‌کند، و سقف تعیین‌شده توسط فراخوان‌کننده را رعایت می‌کند. یک کران واقعی برآمده از دیکشنری تصویر منتقل کنید و حالت شکست یک پیام لاگ‌شده می‌شود نه یک دیالوگ حافظه‌ناکافی روی ماشین یک مشتری

چرا Predictor 2 در TIFF تصاویر ۴بیتی را خراب می‌کند؟

چون Predictor 2 تفاضل‌گیری افقی به‌ازای هر نمونه است، نه هر بایت، و در ۱، ۲ یا ۴ بیت به‌ازای مؤلفه، چند نمونه یک بایت را به اشتراک می‌گذارند. پیاده‌سازی رایج بایت N-Colors را به بایت N اضافه می‌کند، که تصادفاً در ۸ بیت به‌ازای مؤلفه درست است و در هر جای دیگر بی‌صدا اشتباه است. یک اسکن RGB هشت‌بیتی کامل رمزگشایی می‌شود، سپس همان کد یک تصویر چهاربیتی نمایه‌شده را همان اولین باری که یکی در تولید ظاهر شود نابود می‌کند

حساب درست درون فیلد بیت کار می‌کند. HotPDF نمونه‌ها را از نمایه Colors تا Colors * Columns - 1 می‌پیماید، نمونه و همسایه چپ همان مؤلفه‌اش را با یک ماسک از (1 shl BitsPerComponent) - 1 در شیفت مناسب استخراج می‌کند، آن‌ها را پیمانه آن ماسک جمع می‌کند، و نتیجه را بدون اختلال در دیگر نمونه‌های بسته‌بندی‌شده در همان بایت بازمی‌نویسد. دنباله هم اهمیت دارد: یک ردیف تا مرز بایت پد می‌شود، پس بیت‌های پد پس از آخرین نمونه باید دست‌نخورده بمانند به‌جای اینکه در حساب تا شوند. در ۱۶ بیت به‌ازای مؤلفه هر نمونه یک جفت بایت بزرگ‌اندین است و جمع در $FFFF در سراسر جفت می‌پیچد نه اینکه بین بایت‌ها مستقلاً حمل کند؛ در ۸ بیت بازگشت ساده بایتی درست است، با گام Colors که قرمز در برابر قرمز و آلفا در برابر آلفا انباشته می‌شود. در هر نوع، اولین پیکسل یک ردیف یک مقدار تحت‌اللفظی است، هرگز یک تفاضل، و بازگشت در هر مرز ردیف دوباره شروع می‌شود — پیش‌بینی TIFF هرگز ردیف بالا را نمی‌خواند، که کل تفاوت میان آن و خانواده PNG است

EarlyChange در LZWDecode واقعاً چه چیزی را کنترل می‌کند؟

این تابع کنترل می‌کند چه زمانی خواننده اندازه کدش را یک بیت گشادتر می‌کند، و یک کد عقب‌ماندن هرچیز پس از آن را فاسد می‌کند. HotPDF قاعده را به‌صورت یک تغییرناپذیر واحد بیان می‌کند: پس از افزودن یک مدخل دیکشنری، خواندن بعدی وقتی NextCode به (1 shl CodeSize) - Ord(EarlyChange) برسد گشادتر می‌شود. با /EarlyChange 1، پیش‌فرض ISO 32000-1 §7.4.4، این تعویض یک کد زودتر رخ می‌دهد؛ با /EarlyChange 0 دقیقاً روی مرز رخ می‌دهد. هر دو در فایل‌های واقعی ظاهر می‌شوند و هیچ‌چیز در جریان بیتی نمی‌گوید کدام یک را رمزگذار استفاده کرده. بقیه ماشین حالت باید هماهنگ حرکت کند: یک کد پاک‌سازی اندازه کد، ماسک بیت، کد آزاد بعدی و انبار عبارت را با هم بازنشانی می‌کند، و کد پایان-اطلاعات با هر عرضی که در آن لحظه جاری است خوانده می‌شود، نه با ۹ بیت اولیه. HotPDF از InitialCodeSize برابر ۹ شروع می‌کند، اندازه کد را در ۱۲ و دیکشنری را در ۴۰۹۶ مدخل سقف‌بندی می‌کند، و FillOrder را به‌طور پیش‌فرض foTop می‌کند چون PDF کدها را با بیت باترتیب بالا اول بسته‌بندی می‌کند — foBottom برای جریان‌های سبک‌TIFF وجود دارد که این‌طور نیستند

uses
  HPDFLZW;

var
  Decoder: TPDFLZWDecompressor;
  Parms: TPDFLZWParms;
  Plain: AnsiString;
begin
  Decoder := TPDFLZWDecompressor.Create;
  try
    Decoder.EarlyChange := True;      // /EarlyChange 1 is the PDF default
    Decoder.FillOrder := foTop;       // high-order bit first
    Decoder.MaxOutputBytes := 256 * 1024 * 1024;
    Decoder.RequireInitialClear := False;
    Decoder.RequireEndOfInformation := False;

    Parms.Predictor := 12;
    Parms.Colors := 3;
    Parms.BitsPerComponent := 8;
    Parms.Columns := 1024;
    Parms.ExpandedTo8Bit := False;
    Parms.ColorSpace := 'DeviceRGB';

    if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
      LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
        Decoder.KwKwKExpansions, Decoder.OutputBytes)
    else
      LogStreamDefect('lzw', Decoder.LastError);
  finally
    Decoder.Free;
  end;
end;

این آمارها برای تشخیص وجود دارند، نه به‌خاطر خودنمایی. وقتی یک فایل به طول درست اما پیکسل‌های اشتباه رمزگشایی می‌شود، PeakCodeSize و DictionaryAdds فوراً می‌گویند آیا خواننده هرگز جایی که نویسنده گشادتر کرده، گشادتر کرده یا نه. EarlyChange را برعکس کنید، دوباره رمزگشایی کنید، دو تا را مقایسه کنید: اگر اعداد جابه‌جا شوند، پاسخ را در یک اجرا دارید به‌جای گام‌به‌گام رفتن در یک خواننده بیتی

شاخه KwKwK، و وقتی یک جریان باید صرفاً شکست بخورد

یک مورد قانونی که غیرقانونی به‌نظر می‌رسد Code = NextCode است، و HotPDF آن را با ساختن مدخل پیش از انتشارش مدیریت می‌کند. یک رمزگذار ممکن است کدی را برای عبارتی که در همان گام تعریف می‌کند منتشر کند، که هرجا ورودی شامل الگویی از شکل K w K w K باشد رخ می‌دهد؛ رمزگشا نمی‌تواند آن کد را جست‌وجو کند چون هنوز وجود ندارد، پس باید Previous + First(Previous) را بسازد، آن را به‌عنوان مدخل جدید بیفزاید، و مدخلی که تازه ساخته را منتشر کند. HotPDF این‌ها را در KwKwKExpansions می‌شمارد و متقابلاً بررسی می‌کند که کدی که افزوده همان کدی است که خواسته شده. هرچیز بالاتر از NextCode فساد است، و آنجا یک رمزگشا باید متوقف شود نه بداهه‌نوازی کند: HotPDF روی یک کد آینده، روی یک پیشوند دیکشنری که به بیرون از عرصه عبارت اشاره می‌کند، روی یک دیکشنری پر، و روی یک کد اول که تحت‌اللفظی نیست، خطا پرتاب می‌کند. دو سوییچ سخت‌گیری عمداً به‌طور پیش‌فرض خاموش‌اند، RequireInitialClear و RequireEndOfInformation، چون بسیاری PDF‌های تولیدی کد پاک‌سازی پیشرو را حذف می‌کنند یا بدون یک ترمینیتور اطلاعات را تمام می‌کنند. آن‌ها را وقتی خروجی خودتان را اعتبارسنجی می‌کنید روشن کنید، وقتی فایل‌ها را از دنیای بیرون مصرف می‌کنید خاموش بگذارید

جایی که /DecodeParms واقعاً در سمت سند بارگذاری‌شده خوانده می‌شود

HotPDF /DecodeParms یا اختصارش /DP را روی دیکشنری جریان تصویر ریزالو می‌کند، یا یک دیکشنری یا یک آرایه را می‌پذیرد و وقتی آرایه باشد آخرین عنصر را می‌گیرد، سپس Predictor، Colors، BitsPerComponent، Columns و EarlyChange را به مسیر رستر می‌برد. مورد آرایه چیزی است که مردم فراموش می‌کنند: جریانی که با [/ASCII85Decode /FlateDecode] فیلتر شده یک آرایه پارامتر موازی حمل می‌کند، و تنظیمات پیش‌بین به آخرین فیلتر تعلق دارند، نه اولی

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
      for I := 0 to Pdf.GetLoadedImageCount - 1 do
        if Pdf.GetLoadedImageInfo(I, Info) then
        begin
          Bmp := Pdf.ExtractLoadedImage(I);   // nil when the raster is unusable
          if Bmp <> nil then
          try
            Bmp.SaveToFile(Format('image-%d.bmp', [I]));
          finally
            Bmp.Free;
          end;
        end;
  finally
    Pdf.Free;
  end;
end;

یک نقص تاریخی روی آن مسیر ارزش نام‌بردن دارد، چون کلاس باگ تکرار می‌شود. روال قدیمی Flate-با-پارامتر یک جریان فشرده‌گشایی می‌ساخت و سپس از ورودی فشرده اصلی کپی می‌کرد، پس مرحله پیش‌بین بایت‌های فشرده دریافت می‌کرد و با وفاداری آن‌ها را ناپیش‌بین می‌کرد: همیشه اشتباه، هرگز خطا پرتاب‌نشده. کد فعلی فقط از رمزگشا می‌خواند پیش از سپردن نتیجه به پیش‌بین مشترک، و یک رستر کوتاه‌تر از اندازه محاسبه‌شده را رد می‌کند به‌جای بازگشت به بایت‌های هنوز فشرده — یک بازگشتی که قبلاً یک شکست رمزگشایی را به یک بیت‌مپ فاسد تبدیل می‌کرد. همان پیاده‌سازی پیش‌بین اکنون جریان‌های مرجع متقابل را نیز خدمت می‌کند، که سازگاری مفیدی است اگر با جریان‌های آبجکت و به‌روزرسانی‌های افزایشی نیز کار کنید، و ماشین استخراج پیرامون آن در مقاله همراه درباره استخراج تصاویر بارگذاری‌شده و فیلترهای رمزگشایی آن‌ها پوشش داده شده است. تصاویری که به‌صورت DCTDecode یا JPXDecode می‌رسند هرگز به پیش‌بین نمی‌رسند؛ آن‌ها مدل پیکسل فشرده خودشان را حمل می‌کنند

توان عملیاتی: عرصه عبارت پیوسته در برابر رشته‌های به‌ازای مدخل

جایگزین‌کردن دیکشنری رشته به‌ازای مدخل با یک عرصه عبارت پیوسته حدود ۱.۶۱ برابر سریع‌تر روی یک ورودی آسیب‌شناسانه اندازه‌گیری شد: ۱۵۵۸ مگابایت بر ثانیه در برابر ۹۶۹ مگابایت بر ثانیه روی یک بنچمارک که طولانی‌ترین عبارت منفردش به ۷,۳۷۰,۸۸۰ بایت می‌رسد. شکل آن ورودی این شکاف را توضیح می‌دهد، چون پیاده‌سازی‌های کلاسیک یکی از دو معامله بد را انتخاب می‌کنند. یک دیکشنری از مقادیر AnsiString برای هرکدام از تا ۴۰۹۶ مدخل یک رشته تازه تخصیص و کپی می‌کند، هر مدخل جدید والدش را کامل کپی می‌کند؛ یک پشته پیشوند/پسوند از این حافظه اجتناب می‌کند اما هر عبارت را با پیمایش زنجیره به عقب یک بایت در یک زمان و معکوس‌کردنش بازسازی می‌کند، که برای متن معمولی خوب است و برای عبارتی که به مگابایت‌ها می‌رسد دردناک است. HotPDF هر عبارت را پیوسته به یک عرصه که هندسی رشد می‌کند می‌چسباند، مدخل‌ها را با آفست و طول نمایه می‌کند، و یک عبارت را با یک Move واحد به بافر خروجی منتشر می‌کند. هزینه صادقانه حافظه است: عرصه‌ای که هر عبارت را کامل نگه می‌دارد با مجموع طول‌های هر عبارت محدود می‌شود نه با تعداد مدخل، که دقیقاً چرا MaxOutputBytes هم روی فشرده‌گشا و هم روی پیش‌بین وجود دارد. آن کران را از آنچه دیکشنری تصویر ادعا می‌کند رستر باید باشد استخراج کنید، و یک جریان دروغین سریع شکست می‌خورد

فشرده‌گشای LZW، پیش‌بین مشترک و مسیر استخراج تصویر بارگذاری‌شده که در اینجا نشان داده شد به‌عنوان بخشی از مؤلفه HotPDF استاندارد برای Delphi و C++Builder ارائه می‌شود، با مرجع کامل فیلتر و DecodeParms روی صفحه محصول