تدفق موسوم بـ/Predictor 12 لا يعني أن كل صف يستخدم مرشِّح PNG رقم 2. تعامل HotPDF، مكوّن VCL الأصلي لملفات PDF في Delphi وC++Builder، قيم predictor من 10 إلى 15 كعائلة واحدة: وسم المرشِّح الحقيقي، من 0 إلى 4، هو أول بايت من كل صف مُرمَّز، ويقرأ HPDFDecodePredictor ذلك الوسم ويتحقق منه صفًا صفًا. هذا التمييز هو شكل شبه كل علّة في هذه الزاوية من PDF، لأنه لا شيء يرفع استثناء حين تخطئه. تعمل سلسلة المرشِّح، وتكون الصورة النقطية raster بالحجم الذي توقعته، وتخرج الصورة كتشويش قطري أو تدرّج ينحرف أكثر مع كل سطر مسح. الأرقام الخمسة في /DecodeParms (ISO 32000-1 §7.4.4) تغيّر في الغالب معنى البايتات لا طولها، فالقيمة الخاطئة تنتج بيانات فاسدة معقولة الشكل بدل خطأ صريح
لماذا لا يعني /Predictor 12 مرشِّح PNG رقم 2 على كل صف؟
لأن رقم predictor يقول فقط "تنبؤ PNG قيد الاستخدام"، لا أي مرشِّح. تختار مرمِّزات PNG مرشِّحًا لكل سطر مسح، ويرث مرشِّح PDF ذلك، فقيم predictor 10 (None) و11 (Sub) و12 (Up) و13 (Average) و14 (Paeth) و15 (Optimum) كلها تُفَك بالطريقة نفسها: بايت الوسم الأول لكل صف هو ما يجب أن يطيعه فاك الترميز. نتيجة التخطيط تهم بقدر الدلالة. طول كل صف مُرمَّز 1 + RowBytes بايتًا، فالمدخل يتجاوز المُخرَج بمقدار عدد الصفوف بالضبط، والتدفق الذي لا يكون طوله مضاعفًا صحيحًا لـRowBytes + 1 مبتور بالتعريف. تتحقق HotPDF من ذلك الحد قبل لمس أي بايت، وترفض أي وسم أعلى من 4 برسالة Invalid PNG predictor row tag، وتقرأ الصف السابق مباشرة من المخزن المؤقت الوحيد للمُخرَج بدل تجسيد مصفوفة صفوف ثنائية الأبعاد. المرشِّحان 1 و3 يرجعان إلى الخلف بمقدار BytesPerPixel داخل الصف الحالي، والمرشِّح 2 يقرأ للأعلى مباشرة، والمرشِّح 4 يشغّل اختيار 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 ليس زخرفة. مرحلة predictor هي مرحلة إلغاء ضغط متنكرة، وقيمة /Columns عدائية أو معطوبة فحسب تحوّل بضع كيلوبايتات من المدخل إلى طلب تخصيص متعدد الغيغابايتات. تحسب HotPDF بتات الصف وبايتات الصف وحجم الصورة النقطية الكلي في Int64 أولًا، وترفض الهندسة التي تفيض، وتحترم السقف الذي يمرّره المستدعي. مرّر حدًا حقيقيًا مشتقًا من قاموس الصورة ويصبح وضع الفشل رسالة مسجَّلة لا مربع حوار نفاد ذاكرة على جهاز عميل
لماذا يُفسد TIFF Predictor 2 الصور ذات 4 بت؟
لأن Predictor 2 هو فرق أفقي لكل عيّنة، لا لكل بايت، وعند 1 أو 2 أو 4 بت لكل مكوّن تشترك عدة عينات في بايت واحد. التطبيق الشائع يضيف البايت N-Colors إلى البايت N، وهذا يصادف كونه صحيحًا عند 8 بت لكل مكوّن وخاطئًا بصمت في كل مكان آخر. مسح RGB بـ8 بت يُفَك ترميزه بشكل مثالي، ثم يدمّر الكود نفسه صورة مفهرَسة بـ4 بت أول مرة تظهر فيها واحدة في الإنتاج
الحساب الصحيح يعمل داخل حقل البتات. تجتاز HotPDF العينات من الفهرس Colors إلى Colors * Columns - 1، وتستخرج العيّنة وجارتها اليسرى من المكوّن نفسه بقناع mask قدره (1 shl BitsPerComponent) - 1 عند الإزاحة المناسبة، وتضيفهما بالباقي modulo ذلك القناع، وتكتب النتيجة مجددًا دون إزعاج العينات الأخرى المعبّأة في البايت نفسه. الذيل مهم أيضًا: يُحشى الصف حتى حد بايت، فبتات الحشو بعد آخر عيّنة يجب أن تنجو دون مساس بدل طيّها في الحساب. عند 16 بت لكل مكوّن كل عيّنة زوج بايتات بترتيب big-endian والجمع يلتف عند $FFFF عبر الزوج لا بترحيل carry بين البايتات باستقلالية؛ عند 8 بت التكرار البسيط بالبايت صحيح، بخطوة Colors بحيث يتراكم الأحمر مقابل الأحمر والألفا مقابل الألفا. في كل صيغة أول بكسل في الصف حرفي literal، لا فرق أبدًا، ويعيد التكرار البدء عند كل حدود صف — تنبؤ TIFF لا يقرأ الصف الأعلى أبدًا، وهذا هو الفارق الكامل بينه وبين عائلة PNG
ماذا يتحكم EarlyChange فعلًا في LZWDecode؟
يتحكم بمتى يوسّع القارئ حجم شيفرته ببت واحد، وأن تكون شيفرة واحدة خارج التزامن يُفسد كل ما يليها. تعبّر HotPDF عن القاعدة كثابت واحد: بعد إضافة مدخل قاموس، توسّع القراءة التالية حين يبلغ NextCode قيمة (1 shl CodeSize) - Ord(EarlyChange). مع /EarlyChange 1، افتراضي ISO 32000-1 §7.4.4، يحدث التبديل شيفرة واحدة مبكرًا؛ ومع /EarlyChange 0 يحدث بالضبط عند الحد. كلاهما يظهر في ملفات حقيقية، ولا شيء في تدفق البتات يخبرك أيهما استخدمه المرمِّز. بقية آلة الحالة يجب أن تتحرك بتزامن: شيفرة المسح clear code تُعيد ضبط حجم الشيفرة وقناع البتات والشيفرة الحرة التالية وتخزين العبارات معًا، وتُقرَأ شيفرة نهاية-المعلومات بأي عرض سائد في تلك اللحظة، لا بـ9 بتات ابتدائية. تبدأ HotPDF عند InitialCodeSize بقيمة 9، وتحدّ حجم الشيفرة عند 12 والقاموس عند 4096 مدخلًا، وتضبط 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;
الإحصائيات موجودة للفرز triage، لا للزينة. حين يُفَك ترميز ملف إلى الطول الصحيح لكن ببكسلات خاطئة، يخبرك PeakCodeSize وDictionaryAdds فورًا ما إذا كان القارئ وسّع في الموضع نفسه الذي وسّع فيه الكاتب. بدّل EarlyChange، وأعد فك الترميز، وقارن الاثنين: إن تحركت الأرقام، فلديك إجابتك في تشغيلة واحدة بدل تتبع قارئ بتات خطوة بخطوة
فرع KwKwK، ومتى يجب أن يفشل تدفق ببساطة
الحالة الشرعية الوحيدة التي تبدو غير شرعية هي Code = NextCode، وتعالجها HotPDF ببناء المدخل قبل إصداره. قد يُصدر مرمِّز شيفرة عبارة يعرّفها في الخطوة نفسها، وهذا يحدث كلما احتوى المدخل نمطًا بصيغة K w K w K؛ لا يستطيع فاك الترميز البحث عن تلك الشيفرة لأنها غير موجودة بعد، فيجب أن يبني Previous + First(Previous)، ويضيفه كمدخل جديد، ويُصدر المدخل الذي بناه للتو. تعدّ HotPDF تلك الحالات في KwKwKExpansions وتتحقق تقاطعيًا أن الشيفرة التي أضافتها هي الشيفرة التي طُلبت منها. كل ما هو أعلى من NextCode فساد، وهناك يجب أن يتوقف فاك الترميز بدل الارتجال: ترفع HotPDF استثناءً عند شيفرة مستقبلية، وعند بادئة قاموس تشير خارج مساحة العبارات arena، وعند قاموس ممتلئ، وعند شيفرة أولى ليست حرفية. مفتاحا صرامة يُعطَّلان افتراضيًا عمدًا، RequireInitialClear وRequireEndOfInformation، لأن كثيرًا من ملفات PDF المُنتَجة تحذف شيفرة المسح الرائدة أو تنفد بياناتها دون مُنهي. فعّلهما عند التحقق من مُخرَجك الخاص، واتركهما معطَّلين عند استهلاك ملفات من العالم الخارجي
أين يُقرَأ /DecodeParms فعلًا في جانب المستند المُحمَّل
تحلّ HotPDF /DecodeParms أو اختصاره /DP على قاموس تدفق الصورة، وتقبل إما قاموسًا أو مصفوفة وتأخذ العنصر الأخير حين تكون مصفوفة، ثم تحمل Predictor وColors وBitsPerComponent وColumns وEarlyChange إلى مسار الصورة النقطية. حالة المصفوفة هي ما ينساه الناس: تدفق مُرشَّح بـ[/ASCII85Decode /FlateDecode] يحمل مصفوفة معاملات موازية، وإعدادات predictor تخص المرشِّح الأخير، لا الأول
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-بمعاملات القديم كان ينشئ تدفق إلغاء ضغط ثم ينسخ من المدخل المضغوط الأصلي، فتلقّت مرحلة predictor بايتات مضغوطة وتنبأت بها عكسيًا بأمانة: خاطئ دومًا، ولم يرفع استثناءً قط. الكود الحالي يقرأ فقط من فاك الترميز قبل تسليم النتيجة إلى predictor المشترك، ويرفض صورة نقطية أقصر من الحجم المحسوب بدل التراجع إلى البايتات ما تزال مضغوطة — وهو تراجع كان يحوّل فشل فك الترميز إلى صورة نقطية فاسدة. تخدم تلك التطبيقة نفسها لـpredictor الآن تدفقات الإحالة المرجعية أيضًا، وهو اتساق مفيد إن كنت تعمل أيضًا مع تدفقات الكائنات والتحديثات التزايدية، وآلية الاستخراج المحيطة مشروحة في المقال المرافق عن استخراج الصور المُحمَّلة ومرشِّحات فك ترميزها. الصور الواردة كـDCTDecode أو JPXDecode لا تصل إلى predictor على الإطلاق؛ فهي تحمل نموذج بكسل مضغوط خاصًا بها
الإنتاجية: مساحة عبارات متلاصقة مقابل سلاسل لكل مدخل
استبدال قاموس السلاسل لكل مدخل بمساحة عبارات متلاصقة قِيس أسرع بنحو 1.61 مرة على مدخل مَرَضي pathological: 1558 ميبيبايت/ثانية مقابل 969 ميبيبايت/ثانية على معيار قياسي أطول عبارة واحدة فيه تبلغ 7,370,880 بايتًا. شكل ذلك المدخل يفسّر الفجوة، لأن التطبيقات الكلاسيكية تختار واحدة من مقايضتين سيئتين. قاموس من قيم AnsiString يخصّص وينسخ سلسلة جديدة لكل مدخل من حتى 4096 مدخلًا، كل مدخل جديد ينسخ والده بالكامل؛ ومكدس بادئة/لاحقة يتجنب ذلك الاستهلاك للذاكرة تمامًا لكنه يعيد بناء كل عبارة باجتياز السلسلة عكسيًا بايتًا بايت وعكسها، وهذا جيد للنص العادي ومؤلم حين تمتد عبارة واحدة إلى ميغابايتات. تُلحق HotPDF كل عبارة متلاصقة إلى مساحة تنمو هندسيًا، وتفهرس المدخلات بالإزاحة والطول، وتُصدر عبارة بعملية Move واحدة إلى المخزن المؤقت للمُخرَج. الكلفة الصادقة هي الذاكرة: مساحة تحمل كل عبارة كاملة محدودة بمجموع أطوال كل العبارات لا بعدد المدخلات، وهذا بالضبط سبب وجود MaxOutputBytes على كل من فاك الضغط وpredictor. اشتق ذلك الحد مما يدّعيه قاموس الصورة أن الصورة النقطية ينبغي أن تكونه، وتدفق كاذب يفشل بسرعة
فاك ضغط LZW وpredictor المشترك ومسار استخراج الصور المُحمَّلة المعروضة هنا تُشحن ضمن HotPDF Component القياسي لـ Delphi وC++Builder، مع المرجع الكامل للمرشِّحات وDecodeParms على صفحة المنتج