جدول بحث /Indexed يخزن مكوّنات فضاء الألوان الأساسي، لا ثلاثيات RGB. قاعدة DeviceGray تحزم بايتاً واحداً لكل إدخال، و DeviceCMYK أربعة، و Lab ثلاثة تحتاج تحجيم نطاق، و ICCBased ما يعلنه ملف التعريف /N. تطبّع HotPDF كل ذلك وقت التحليل إلى لوحة RGB العامة التي يكشف عنها THPDFColorSpace
تلك الجملة هي فئة العيوب كلها. تقريباً كل عارض لوحات يبدأ حياته بافتراض خطوة من ثلاثة بايتات، لأن الغالبية الساحقة من فضاءات /Indexed الواقعية تقع على /DeviceRGB ويصمد الافتراض لسنوات. ثم تصل فاتورة ممسوحة تحمل [/Indexed /DeviceCMYK 15 <...>]، فيمشي القارئ سلسلة البحث ثلاثة بايتات في كل مرة، ويقع كل فهرس بعد الصفر في منتصف لون شخص آخر. المخرج ليس فارغاً ولا يتعطل. إنه قمامة تبدو معقولة، وهو أسوأ بكثير، لأن أحداً لا يفتح بلاغ عيب ضد ألوان تبدو خاطئة فحسب
لماذا تخرج لوحة مفهرسة خاطئة؟
الخطوة خاطئة لأن الخطوة ليست ثابتاً. يعرّف ISO 32000-1 §8.6.6.3 جدول البحث كـ m × (hival + 1) بايتاً، حيث m هو عدد المكوّنات في الفضاء الأساسي، ولا يقول شيئاً عن كون ذلك الأساس RGB. يحظر القسم نفسه قاعدتين فقط صراحة: /Indexed نفسها و /Pattern. كل شيء آخر مباح، بما فيه العائلات الأربع المبنية على CIE من §8.6.5.2 حتى §8.6.5.5
لذا فالعمل الحقيقي ليس قراءة البايتات، بل حلّ الأساس أولاً وسؤاله كم عرض الإدخال. يفعل ResolveIndexedPalette في HotPDF ذلك بالضبط: يحل الأساس إلى THPDFColorSpace، ويأخذ ComponentCount من السجل المُحل، ويرفض المصفوفة فوراً إذا عاد الأساس كـ csfIndexed أو csfPattern أو csfUnsupported. عندها فقط يحدد ميزانية البايتات كـ PaletteCount * Count، التي تبلغ 1024 بايتاً عند الحد الأقصى القانوني hival 255 وأساس من أربعة مكوّنات. تفصيلان في ذلك التحديد هما المكان الذي سيختبرك فيه ملف عدائي. تقصّ HotPDF hival إلى 0..255 قبل استخدامه، بموجب حد §8.6.6.3، لذا فإن مصفوفة بحث تدّعي hival 100000 لا تتحول إلى تخصيص غيغابايت؛ وتقصّ الميزانية إلى ما تحتويه بيانات البحث فعلاً، ثم تتوقف عن ملء اللوحة عند أول إدخال تنفد بايتاته قبل النهاية. جدول قصير يعطي لوحة قصيرة، لا قراءة تتجاوز المخزن
تطبيع أربعة فضاءات أساسية مختلفة إلى جدول RGB واحد
تحوّل HotPDF اللوحات إلى RGB أثناء التحليل لا وقت الرسم، وهذا القرار يقود بقية التصميم. حقل THPDFColorSpace.Palette العام هو TBytes من ثلاثيات RGB بطول 3 * PaletteCount، وبقي على هذا الشكل منذ زمن طويل قبل دعم القواعد المبنية على CIE. إعادة كتابة تخطيط السجل لحمل المكوّنات الأساسية الأصلية كانت ستكسر كل مستهلك للحقل. التحويل وقت التحليل يبقي التخطيط سليماً، وكإضافة، يمرر إدخالات Gray و CMYK و Lab و CalGray و CalRGB و ICCBased عبر نفس مسار HPDFResolveColor الذي يستخدمه العارض بالفعل لعوامل الألوان المباشرة
خطوة التحجيم هي المكان الذي تتوقف فيه الفضاءات الأساسية عن كونها قابلة للتبادل. بايت البحث دائماً 0..255، لكن ما يعنيه ذلك البايت يعتمد على نطاق المكوّن الذي يعلنه الأساس. المكوّنات الموصولة والمعايرة تخطّط على 0..1. أما Lab فلا: يعمل المكوّن L* على 0..100، بينما يعمل a* و b* على نطاق /Range الذي يعلنه الفضاء. تسحب HotPDF تلك الحدود من حقول AMin و AMax و BMin و BMax في THPDFCIEParams، التي يملؤها محلل Lab من إدخال /Range في القاموس ويعرّفها افتراضياً على −108..101 عندما يكون الإدخال غائباً، لذا لا ترمّز نطاقاً متماثلاً من الذاكرة. تضيف القواعد ICCBased مصدر حقيقة ثالثاً: عدد المكوّنات يأتي من /N في تدفق الملف التعريفي، ويُسطح كـ ICCN، وقد يحمل تدفق ICCBased نفسه مصفوفة /Range تقرؤها HotPDF عبر HPDFReadNumericArrayUpTo في نفس جدول حدود المكوّنات. لذلك تمشي لوحة ICC من ثلاثة مكوّنات ولوحة Lab من ثلاثة مكوّنات سلسلة البحث بخطوات متطابقة وحساب مختلف تماماً، وهو بالضبط المخرج الذي لا تستطيع خطوة RGB ثابتة إنتاجه
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj هو إدخال /ColorSpace من XObject صورة أو من قاموس موارد؛
// قد يكون اسماً أو مصفوفة أو مرجعاً غير مباشر
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// اللوحة أصبحت RGB بالفعل مهما كان الفضاء الأساسي
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // جدول بحث قصير، توقف بنظافة
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
عندما يعيش جدول البحث في تدفق
يسمح ISO 32000-1 §8.6.6.3 لجدول البحث بأن يكون نصاً أو تدفقاً، والشكل التدفقي هو الذي يعض. يمكن للتدفق أن يحمل /FlateDecode، ويمكن أن يكون كائناً غير مباشر مشتركاً مع أجزاء غير ذات صلة من الملف، ويمكن أن يكون TStream الخاص به في أي موضع لحظة وصول المحلل إليه. تتعامل HotPDF مع الحالة المضغوطة بالتفويض إلى استدعاء البايتات المفككة المسجل عبر HPDFSetColorStreamReader، وهو نفس الاستدعاء الذي يستخدمه محرك دالة Type 0 المأخوذة بالعينات
عند عدم تسجيل قارئ، يكون المسار الاحتياطي ضيقاً عمداً لا مجرد جهد أفضل. تفحص HotPDF قاموس التدفق بحثاً عن /Filter وترفض اللوحة بالكامل إذا وُجد واحد، بدلاً من إطعام بايتات مضغوطة للبحث وإنتاج جدول خاطئ بثقة. بدون فلتر تقرأ بايتات خام، مقصوصة عند ميزانية البايتات المحسوبة من hival وعدد مكوّنات الأساس. فرع التدفق كله يجلس داخل try..finally يعيد Stream.Position الأصلي على كل مسار خروج، بما فيه حالات الرفض، لأن الكائن مشترك ولن يشكرك المستهلك التالي على مؤشر متحرك. يثبّت LookupStreamUsesDecodedReaderAndRestoresPosition في مجموعة الانحدار نصفي ذلك السلوك
// سجّل الاستدعاءين قبل حل أي فضاء لون، وإلا فستُرفض
// القواعد غير المباشرة وتدفقات البحث المضغوطة ببساطة
HPDFSetColorSpaceResolver(MyResolveIndirectObject);
HPDFSetColorStreamReader(MyDecodedStreamReader);
try
CS := HPDFResolveColorSpace(ColorSpaceObj);
if (CS.Family = csfIndexed) and (Length(CS.Palette) = 0) then
LogWarning('lookup table unreadable: filtered stream or no reader');
finally
HPDFSetColorStreamReader(nil);
HPDFSetColorSpaceResolver(nil);
end;
ما الذي يمنع فضاء ألوان مفهرساً من التكرار إلى الأبد؟
رفض /Indexed كأساس مباشر ضروري وغير كافٍ إطلاقاً. الهجوم المثير يذهب جانبياً. خذ [/Indexed /Separation ...] صالحاً، وتذكر أن §8.6.6.4 يتيح لـ Separation تسمية فضاء بديل وأن §8.6.6.5 يتيح لـ DeviceN الشيء نفسه. اجعل ذلك البديل مرجعاً غير مباشر يشير عائداً إلى مصفوفة Indexed الأصلية. لا شيء في السلسلة ينتهك قاعدة الأساس المباشر عند أي قفزة واحدة: الأساس Separation، وبديل Separation مصفوفة، والمصفوفة تصادف أنها التي بدأت منها. حلّها بسذاجة وسيمشي المحلل الحلقة حتى ينفد المكدس. آلية تحويل الصبغة التي تعتمد عليها تلك الفضاءات موضوع مستقل بذاته، مغطى في المقال عن عرض ألوان Separation و DeviceN؛ تهمّ هنا فقط كحلقة في الدائرة
تسد HotPDF هذا عند نقطة الدخول الوحيدة التي تمر بها كل المسارات. يلف HPDFResolveColorSpace المحلل الداخلي في عدّاد عمق محدود بـ HPDFColorSpaceMaxDepth، وهو 64. اعبر ذلك الحد ويضبط الحارس علماً، والعلم هو المهم: عند التراجع، أي مستوى تداخل رأى العلم مضبوطاً يتخلى عن سجله المُحل جزئياً ويعيد csfUnsupported، لذا يُرفض البيان كله بدلاً من الاحتفاظ بنصفه، ويعاد تصفير العدّادين عندما يعود الاستدعاء الخارجي. تلك العدّادات معلنة كـ threadvar لا متغيرات وحدة عامة، وهذا ليس ترتيباً عارضاً: يعيش GResolveObj و GReadStream في تخزين محلي للخيط لنفس السبب، لأن الصفحات تُرسم بالتوازي. المتغير العام المشترك كان سيترك خيطاً غارقاً في سلسلة DeviceN متداخلة يعثر حد الخيط الآخر الذي يحلل [/Indexed /DeviceRGB 255 <...>] عادياً تماماً، منتجاً فشلاً غير حتمي معتمداً على الحِمل — وهو أقل الأنواع قابلية للتصحيح. وستون مساحة مريحة للمحتوى المشروع، الذي نادراً ما يتداخل أعمق من أربعة أو خمسة مستويات، لذا عامل csfUnsupported القادم من مصفوفة تبدو صالحة بنيوياً كشيء يستحق التسجيل: عملياً يعني حلقة أو عمقاً لا ينتجه أي منتج حقيقي. يغطي IndirectAlternateCycleIsUnsupported، المُميْثَم على Separation و DeviceN، و ExcessivePatternDepthIsUnsupported الحالتين
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported هنا يغطي عائلة غير معروفة فعلاً، وحلقة عبر
// بديل غير مباشر، وعمقاً يتجاوز 64 مستوى
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray، الفضاء الابتدائي في PDF
end;
حد واجهة واحد يستحق التوثيق لمتصلاتك
تطبّق HotPDF إعداد تحويل ICC النافذ وقت التحليل عندما تبني لوحة مفهرسة، والاستدعاءات اللاحقة لـ HPDFSetColorSpaceRenderingIntent لا تعيد توليد لوحة حُلّت بالفعل. تمشي تلك الدالة تحويلات ICCBased والبديلة للسجل الذي تسلمه إياها وتعيد بناء خطوط الأنابيب تلك؛ بايتات RGB المسطحة في Palette أثر وقت التحليل وتبقى كما كانت. هذه نتيجة التطبيع المبكر لا سهو، وتستحق الكتابة في توثيق واجهتك الخاصة بدلاً من ترك عميل يكتشفها. إذا كان تطبيقك يتيح للمستخدم تبديل نية العرض في منتصف الجلسة، فأعد حل فضاءات الألوان بعد التبديل؛ إذا ضبط النية مرة واحدة أثناء إعداد خط الأنابيب، وهي الحالة الشائعة بأغلبية ساحقة، فلا شيء لتفعله. الترتيب نفسه ينطبق على ملفات التدقيق المثبتة عبر HPDFSetICCColorPipeline: اضبطها قبل أن تبدأ الحل، لا بعده
العيب المرآتي في جهة الكتابة
كل ما سبق عن القراءة. العيب المتماثل في جهة الكتابة أسهل شحناً وأسهل تفويتاً بكثير. عندما تبني HotPDF مصفوفة /Indexed من صورة Windows ذات 8 بتات تستدعي GetPaletteEntries، وتلك الواجهة تملأ فقط عدد الإدخالات التي تحملها اللوحة فعلاً وتعيد ذلك العدد. اختبر الكود الأصلي النتيجة مقابل = 0 ثم كرر على 256 إدخالاً ثابتاً فوق مصفوفة مكدس. بالنسبة لصورة من 16 لوناً كتب ذلك 240 إدخالاً من ذاكرة مكدس غير مهيأة مباشرة في سلسلة البحث لملف PDF تسلّمه بعد ذلك لعميل، مع hival مكتوباً ثابتاً 255 بحيث عالج كل قارئ البقايا بأمانة كبيانات ألوان حقيقية. إنه عيب صحة وعيب كشف معلومات يرتديان نفس الثياب. الإصلاح غير براق ومكتمل: اصفّر المصفوفة قبل الاستدعاء، ثم احترم العدد المعاد لكل من حد الحلقة و hival. إذا ولّدت صور /Indexed في أي مكان من كودك، فاذهب وتحقق من hival الآن
قطعتان متجاورتان تكملان الصورة: تتشارك اللوحات المفهرسة ودوال Type 0 المأخوذة بالعينات قارئ التدفق المفكك وانضباط القص نفسه، نوقش في القطعة عن جداول بحث ألوان دوال Type 0، وقواعد الفلاتر الحاكمة لتدفقات البحث هي نفسها الحاكمة لبيانات الصور في استخراج الصور المحملة عبر فلاتر فك الترميز. محلل فضاء الألوان الموصوف هنا يشحن كجزء من مكوّن HotPDF لـ Delphi لـ Delphi و C++Builder، مع مرجع THPDFColorSpace الكامل في صفحة المنتج