أصلح PDFlibPas خللين مستقلين في مفكك halftone لـ JBIG2 الأصلي عنده: في v3.539.37 تُفهرَس قناع التخطي HSKIP كقيمة HSKIP[ng, mg] كما يعرّفها ITU-T T.88 §6.6.5.1، وفي v3.539.38 الشبكات التي تبلغ إحداثيات سالبة، عبر HGX أو HGY سالب أو عبر دوران، توضع بإزاحة أرضية (floor) حقيقية. قبل هذين الإصدارين كانت مناطق halftone المتأثرة تخرج مشوشة أو مزاحة دون رفع أي خطأ. واختبأ كلا الخللين خلف بيانات اختبار صادف أنها متناظرة أو غير سالبة، والثاني يشتغل على خاصية في Delphi و Free Pascal تعضّ خارج JBIG2 بعيداً: shr على عدد صحيح موقّع إزاحة منطقية، لا الإزاحة الحسابية >> التي تفترضها المواصفة
مناطق halftone أقل أنواع مناطق JBIG2 شيوعاً، فيمكن لمفكك أن يعالج آلاف المستندات الممسوحة قبل أن يقابل صورة منقّاة مشفرة كذلك. وحين يحدث يكون الفشل قبيحاً: الملف يُحلَّل، وأطوال المقاطع تجمع صحيحة، والصفحة بمقاسها الصحيح، والمنطقة خردة
ماذا يفك منطقة halftone في JBIG2 فعلاً؟
منطقة halftone في JBIG2 شبكة من صور bitmap صغيرة تُلتقط من قاموس أنماط، والعمل الحقيقي للمفكك هو حساب فهرس لكل خلية شبكة والموضع البكسلي الذي تهبط فيه تلك الخلية. يحمل قاموس الأنماط HNUMPATS نمطاً بمقاس HPW × HPH بكسل. ثم يصف مقطع منطقة halftone شبكة من HGW عموداً في HGH صفاً وصورة تدرج رمادي بالحجم نفسه، مشفرة بمستويات بت Gray-coded. ويُفك كل مستوى بت بالتقسيم الإقليمي العام (generic region) فوق bitmap بمقاس HGW × HGH، المستوى الأعلى معنوياً أولاً، والمستويات معاً تعطي كل خلية فهرس نمطها
ويستخدم وضع الخلايا حساب نقطة ثابتة بكسر من 8 بتات. أصل الشبكة HGX و HGY زوج من قيم 32-بت، ومتجه الشبكة HRX و HRY يصف الخطوة بين الخلايا المتجاورة، وهو ما يسمح بشبكة مدورة. ولصف الشبكة mg وعمودها ng يحسب T.88 §6.6.5 الموضع البكسلي هكذا:
x = (HGX + mg × HRY + ng × HRX) >> 8y = (HGY + mg × HRX − ng × HRY) >> 8
ويدخل قناع التخطي عبر علم HENABLESKIP الاختياري. حين يُضبط العلم يبني §6.6.5.1 صورة bitmap بمقاس HGW × HGH اسمها HSKIP ويضبط HSKIP[ng, mg] إلى 1 لكل خلية يقع نمطها كلياً خارج المنطقة: x + HPW <= 0، أو x >= HBW، أو y + HPH <= 0، أو y >= HBH. ثم تُفك مستويات التدرج الرمادي بذلك القناع بوصفه bitmap التخطي للتقسيم الإقليمي العام، فلا يقرأ المفكك السياقي الحسابي ولا يحدّثه لخلية متخطاة. ويجب أن يتوافق المفكك والمرمّز على كل بت في HSKIP وإلا خرج المُرمِّزان الحسابيان عن التزامن
لماذا لم يكسر قناع HSKIP المبادَل إلا الشبكات غير المربعة؟
كُتب قناع التخطي بإحداثياته معكوسة، ولم يكشفه إلا شبكة غير مربعة، لأن الشبكة المربعة تبقي كل إحداثي مبادَل داخل القناع. تخزن PDFlibPas صور bitmap بوصولي بكسل بصيغة (column, row)، وكان الكود الذي بنى القناع يمرر (mg, ng)، الصف أولاً. ومفكك مستويات التدرج الرمادي يقرأ القناع صحيحاً بصيغة (ng, mg). وحلقة وضع الأنماط كانت تقرأه بترتيب الباني المعكوس، فاتفقا، ومراجعة منطق الوضع وحده كانت ستجتاز. وفخ تسمية زاد الأمر سوءاً: في حلقة الوضع يمرّ المتغير المسمى col على صفوف الشبكة و Row على أعمدتها
خذ شبكة 5 × 3 من أنماط 4 × 4 على منطقة 16 × 8 التي يستخدمها v3.539.37 حالة انحدار له. مع HRX = 1024 و HRY = 0 يهبط عمود الشبكة 4 عند x = 16 وصف الشبكة 2 عند y = 8، وكلاهما خارج المنطقة. والقناع الصحيح يعلم سبع خلايا: عمود 4 كله وصف 2 كله. أما الكتابات المبادَلة فحاولت ضبط بكسلات عند فهارس صفوف 3 و 4 في قناع ارتفاعه ثلاثة صفوف فقط، وماضِع bitmap تجاهل تلك الكتابات الخارجة عن المدى بصمت. فما نجا كان العمود 2، الصفوف من 0 إلى 2. فتخطى المفكك خليتين كان المرمّز قد شفرهما، وفك ست خلايا كان المرمّز قد تخطاها
المفكك الحسابي لا يفشل حين يحدث ذلك. إنه يفك بكسلات زائدة من بتات تعود لخلايا لاحقة، وسياقاته تقرأ جيراناً خطأ، وكل فهرس نمط بعد أول اختلاف ضجيج، ولهذا كان العرض شوشةَ منطقة لا بضعَ خلايا مزاحة. وعلى شبكة مربعة يكون الخلل نفسه غالباً غير مرئي: لا يخرج أي إحداثي مبادَل من القناع، وحين تكون الخلايا خارج المنطقة متناظرة حول القطر، شبكة تتجاوز الحافتين اليمنى والسفلى بعدد خلايا متساوٍ مثلاً، يكون القناع المبادَل بتاً ببت هو الصحيح. كما أن HENABLESKIP اختياري، ويجب أن يكون 0 حين تكون صورة التدرج الرمادي مشفرة MMR، ونادراً ما يضبطه المرمّزون، فكان للخلل طرق ظهور قليلة جداً. ومنذ v3.539.37 يكتب الباني HSKIP[ng, mg] وتقرأ حلقة الوضع الترتيب نفسه
لماذا تفشل إزاحات شبكة halftone السالبة في ثلاث طبقات؟
شبكة halftone تبدأ يسارَ منطقتها أو فوقها كسرت PDFlibPas في ثلاثة مواضع منفصلة، وكل خلل أخفى التالي له. ويسمح T.88 بهذه الهندسة عمداً. فمرمّز يوائم منقّاته مع الصفحة لا مع المنطقة، أو يستخدم شبكة مدورة، ينتج طبيعياً زوايا خلايا سالبة تقصّها المنطقة. أصلح v3.539.38 الطبقات الثلاث معاً، لأن إصلاح أي واحدة وحدها لم يكن سيغير إلا العرض
الطبقة 1: حقل موقّع يُقرأ غير موقّع
يعرّف T.88 §7.4.5.1.2 قيمتَي HGX و HGY بوصفهما 32-بت موقّعتين، لكن المفكك قرأهما بالمُعِدّ ذاته ذي 32-بت الذي يستخدمه للحقول غير الموقعة، وكان ذلك المُعِدّ يقيّد كل نتيجة سالبة إلى 0. فشبكة مقصود أن تبدأ عند HGX = -900 نُقلت بهدوء إلى أصل المنطقة. وفي حالة انحدار v3.539.38 خرجت الصورة كلها صفين أخفض من المكان. والقيد يفسر أيضاً لماذا نجا الخللان الآخران طويلاً: مع أصل مفروض غير سالب لم يكن الإحداثي السالب يظهر إلا عبر شبكة مدورة ذات HRY > 0، حيث تهبط y = HGY + mg × HRX − ng × HRY تحت الصفر لأعمدة الشبكة اللاحقة
الطبقة 2: shr ليست >> 8
يكتب T.88 >> 8 ويقصد إزاحة حسابية تقرب نحو سالب ما لا نهاية. وترجمها المفكك كـ shr 8. وفي Delphi و Free Pascal فإن shr على عدد صحيح موقّع إزاحة منطقية: بت الإشارة يُزاح داخل الصفر. فالقيمة Integer الحاملة -512 تعطي shr 8 قيمة 16777214 بدل -2. فنمط كان ينبغي رسمه عند y = -2 وقصه إلى نصفه السفلي أُرسل 16 مليون صف نزولاً وأُسقط خارج المنطقة. لا شيء انهار؛ الصف الأعلى من الـ halftone اختفى ببساطة
الطبقة 3: مقارنة نقطة ثابتة بدل البكسلات
قارن اختبار التخطي قيم نقطة ثابتة لا مواضع بكسلية، والاثنتان لا تتكافآن متى كان الكسر غير صفري. وكان الكود الأصلي يتحايل على الإزاحة المنطقية باختبار xx + HPW × 256 <= 0 على القيمة غير المزاحة، وهو ما ظنَّه مكافئاً لاختبار T.88. مع HGX = -900 ونمط من 4 بكسلات يعطي ذلك -900 + 1024 = 124، وهو موجب، فلا تُخطَّى الخلية. لكن المواصفة تزيح أولاً: floor(-900 / 256) = -4، و -4 + 4 = 0 يحقق x + HPW <= 0، فتقع الخلية كلياً خارج المنطقة ويجب تخطيها. فالمرمّز خطاها والمفكك فكها، وانحرفت صورة التدرج الرمادي تماماً كما في حالة القناع المبادَل
حالة الانحدار من v3.539.38 تستخدم شبكة 4 × 3 من أنماط 4 × 4 عند HGX = -900 و HGY = -512 و HRX = 1024 على منطقة 12 × 10. تهبط أعمدة الشبكة عند x = -4 و 0 و 4 و 8، فالعمود 0 كله خارج المنطقة ومكانه HSKIP؛ وتهبط صفوف الشبكة عند y = -2 و 2 و 6، فالصف 0 يجب قصّه إلى صفّي بكسله السفليين لا إسقاطه. وإصلاح الطبقات واحدة تلو الأخرى يعيد إنتاج التراكم:
| الأعطال المصلحة | المنطقة المفكوكة |
|---|---|
| لا شيء (قبل v3.539.38) | الشبكة تُسحب إلى الأصل، والصورة كلها صفين أخفض |
| قراءة HGX / HGY الموقّعة فقط | صف الشبكة الأول مفقود، والبقية مشوشة بانحراف اختبار التخطي |
| القراءة الموقعة وإزاحة الأرضية واختبار التخطي في فضاء البكسل | متطابقة بكسلاً ببكسل مع الصفحة المحسوبة من T.88 §6.6.5 ومع مفككَي مرجع مستقلين |
والإصلاح مساعد واحد، HalftoneGridPixel، مشترك بين باني قناع التخطي وحلقة الوضع. يجمّع الإحداثي في Int64 فلا يستطيع جداء mg × HRX كبير أن يلتف، ويقسم على 256 بالتقريب نحو سالب ما لا نهاية، ويقيّد إلى ±MaxInt div 2 فلا تستطيع شبكة تالفة أن تفيض لاحقاً في حساب bitmap. ويقارن اختبار التخطي الآن قيم البكسل تلك مقابل HPW و HPH و HBW و HBH، تماماً كما يصوغه §6.6.5.1
كيف تكتب إزاحة يمين حسابية في Delphi؟
لا يملك Delphi معامل إزاحة حسابية، فالإزاحة اليمينية الموقعة الصحيحة يجب أن تُكتب قسماً أرضياً، و div العادية ليست ذلك القسم. فـ div تبتر نحو الصفر. للقيم غير السالبة يتفق البتر والأرضية، ويتفقان أيضاً للقيم السالبة مضاعفات القاسم بالضبط، ولهذا تبدو -512 div 256 = -2 سليمة في اختبار سريع. ويختلفان في كل ما عداهما: -900 div 256 تساوي -3 بينما الأرضية -4، و -1 div 256 تساوي 0 بينما الأرضية -1. وإحداثي JBIG2 بكسر غير صفري هو بالضبط الحالة التي تعطي فيها div البكسل الخطأ
على مترجمَي Delphi Win32 و Win64، متغير Integer حامل -512 مزاحاً يميناً بـ 8 يعطي 16777214، و Int64 حامل -512 يعطي 72057594037927934. ويعرّف Free Pascal shr أيضاً إزاحةً منطقية ويوفر SarLongint و SarInt64 في وحدة System عنده للنسخة الحسابية، لكن تلك التوابع غير موجودة في Delphi، فالكود المشترك بين المترجمين يحتاج مساعده الخاص:
// قسم أرضي: يقرّب نحو سالب ما لا نهاية لأي إشارة لـ A و B.
// يجب ألا يساوي B صفراً، و FloorDiv(Low(Integer), -1) يفيض مثل div تماماً
function FloorDiv(A, B: Integer): Integer;
begin
Result := A div B;
if (A mod B <> 0) and ((A < 0) <> (B < 0)) then
Dec(Result);
end;
// إزاحة يمين حسابية (الـ ">>" في C و T.88 على القيم الموقعة).
// للقيمة السالبة تكون not Value = -Value - 1 غير سالبة، فالـ
// shr المنطقية آمنة هناك، و not الخارجية تعيد النتيجة إلى مكانها
function SarInt32(Value: Integer; Shift: Integer): Integer; // Shift 0..31
begin
if Value >= 0 then
Result := Value shr Shift
else
Result := not ((not Value) shr Shift);
end;
function SarInt64(Value: Int64; Shift: Integer): Int64; // Shift 0..63
begin
if Value >= 0 then
Result := Value shr Shift
else
Result := not ((not Value) shr Shift);
end;
وحيلة not لا تزيح عدداً سالباً أبداً، فلا تعتمد على كيفية معاملة المترجم لبت الإشارة، ولا تفيض قط، بما في ذلك Low(Integer). وطابق كلا المساعدَين مرجعَ أرضية Int64 عبر عدة ملايين قيمة، وكل إزاحة من 0 إلى 31 وحدّي Low(Integer) و High(Integer) على Delphi Win32 و Delphi Win64 و Free Pascal x86_64. وفحص عقلي يستحق البقاء في أي اختبار وحدة يلمس الإحداثيات:
var
V: Integer;
begin
V := -900;
Writeln(V shr 8); // 16777212 إزاحة منطقية، الخلل القديم
Writeln(V div 256); // -3 بتر نحو الصفر
Writeln(FloorDiv(V, 256)); // -4 ما يعنيه T.88 بـ >> 8
Writeln(SarInt32(V, 8)); // -4
end;
كما تعيد Math.Floor(V / 256) القيمة -4، لكن التفافها عبر Double يفقد الدقة لقيم Int64 فوق 253، فهندسة الأعداد الصحيحة ينبغي أن تبقى في الأعداد الصحيحة
أي نداءات PDFlibPas تشغّل مفكك halftone؟
يعمل مفكك halftone لـ JBIG2 حين تعرض PDFlibPas صفحة بالمحرك المدمج، لأن العرض يحتاج بكسلات. فـ RenderPageToFile و RenderPageToStream كلاهما يبلغ إليه عبر تدفقات صور JBIG2Decode الخاصة بالصفحة، فإعادة عرض صفحة halftone هي الطريقة المباشرة للتأكد أن v3.539.38 تغيّر مخرجاتك. ويتولى المفكك نفسه بقية أنواع مناطق JBIG2، تغطيها مقالتا جداول Huffman المخصصة في JBIG2 لدى المفكك البحت Pascal و فك ملفات JBIG2 عشوائية الوصول في Delphi، والـ bitmap المعروض يغذي تحويلات مثل عرض صفحات PDF إلى أحادي اللون 1-بت
uses
SysUtils, PDFlibrary;
var
Lib: TPDFlib;
Page: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('scanned-halftone.pdf', '') <> 1 then
raise Exception.CreateFmt('Load failed, error %d', [Lib.LastErrorCode]);
for Page := 1 to Lib.PageCount do
// العرض يفك كل منطقة JBIG2، والـ halftones منها
if Lib.RenderPageToFile(150, Page, PDF_RENDER_PNG,
Format('page-%.3d.png', [Page])) <> 1 then
Writeln('Page ', Page, ' was not rendered');
finally
Lib.Free;
end;
end.
استخراج الصور يسلك عادة مساراً مختلفاً. تعيد GetPageImageList صور JBIG2 بصيغتها الأصلية، و SaveImageListItemDataToFile أو GetImageListItemDataToString تسلّمك ملف JBIG2 مستقلاً مبنياً من بايتات التدفق: ترويسة الملف، وبيانات JBIG2Globals، ومقطع نهاية ملف حول بيانات الصفحة. ويبلّغ الخاصية 400 من GetImageListItemIntProperty قيمة 6 لمثل ذلك المدخل. ولا يُفك شيء في ذلك المسار، فملف .jb2 مستخرج يبدو صحيحاً في عارض آخر بينما تعرض الصفحة المعروضة ضجيجاً كان علامة نمطية على خللَي halftone هذين:
var
ListID, I: Integer;
begin
Lib.SelectPage(1);
ListID := Lib.GetPageImageList(0);
if ListID = 0 then
Exit;
try
for I := 1 to Lib.GetImageListCount(ListID) do
if Lib.GetImageListItemIntProperty(ListID, I, 400) = 6 then // JBIG2 مستقل
Lib.SaveImageListItemDataToFile(ListID, I, 0,
Format('page1-image%d.jb2', [I]));
finally
Lib.ReleaseImageList(ListID);
end;
end;
وحين تفرض الأقنعة أو تحويل الألوان مساراً بديلاً معروضاً، يعود المدخل bitmap مفكوكاً ويعمل مفكك halftone فعلاً. والمزيد عن قوائم الصور في استخراج النص والصور والخطوط في PDF لدى Delphi
مرجع سريع: قواعد شبكة halftone في JBIG2
- فهرس قناع التخطي بصيغة
HSKIP[ng, mg]، عمود الشبكة أولاً، واقرأه بالترتيب نفسه أينما وُضعت الخلايا (T.88 §6.6.5.1، صُحح في PDFlibPas v3.539.37) - اختبر أي كود halftone أو شبكة بشبكة غير مربعة ومجموعة غير متناظرة من الخلايا خارج المنطقة، لأن شبكة مربعة تستطيع أن تخفي فهرساً مبادَلاً كلياً
- اقرأ
HGXوHGYقيمتين موقعتين ذواتي 32-بت (T.88 §7.4.5.1.2)، لا عبر مُعِدّ غير موقّع يقيّد السوالب أبداً - ترجم
>> 8الواردة في المواصفة قسماً أرضياً على 256، لاshr 8ولاdiv 256 - أجرِ اختبار التخطي على مواضع بكسلية مزاحة؛ فصيغة النقطة الثابتة تختلف متى كان الكسر غير صفري، كما تُظهر
HGX = -900مع نمط من 4 بكسلات - جمّع إحداثيات الشبكة في
Int64وقيّدها قبل تسليمها لكود bitmap، فلا تستطيع شبكة تالفة أن تفيض - رقِّ إلى v3.539.38 أو أحدث إن كانت مستنداتك تحوي مناطق halftone ذات
HENABLESKIPأو أصول شبكة سالبة أو شبكات مدورة
يعرض PDFlibPas ويستخرج ويحرر مستندات PDF من Delphi و C++Builder بمفكك JBIG2 أصلي بلغة Pascal يعالج الآن أقنعة تخطي halftone وأصول الشبكات السالبة والشبكات المدورة كما يحدد T.88. وانظر PDFlibPas Delphi PDF library للميزات والإصدارات وتنزيل التجربة