يُقيّم HotPDF، مكوّن PDF الأصلي القائم على VCL لـDelphi وC++Builder، أنواع دوال PDF الثلاثة المبنية من صيغ رياضية بدلًا من شبكات عينات: الاستيفاء الأسّي من النوع 2، والتماس من النوع 3، ودوال حاسبة PostScript من النوع 4، المطابقة لـISO 32000-1 §7.10.3 و§7.10.4 و§7.10.5. يمزج النوع 2 بين متجهي مخرجات على طول منحنى، ويسلسل النوع 3 عدة دوال فرعية عبر نطاق إدخال واحد، ويشغّل النوع 4 برنامج PostScript مقيّدًا يمكنه التفرع والمقارنة وحساب أي شيء تقريبًا يحتاجه تدفق محتوى من مدخلاته. أخطئ في أي من الثلاثة قليلًا ولن يعلن الفشل نفسه كخطأ برمجي أبدًا — سيظهر كتدرج بشريط مسطح ميت، أو لون تعريفي (spot color) يُرسم أسودًا خالصًا، أو دالة حاسبة تنحرف بمقدار واحد بالضبط عند مدخلات صادف ألا تجربها مجموعة اختبارات
تقف هذه الأنواع الثلاثة إلى جانب نوع رابع، النوع 0، الذي يخزّن شبكة عينات بدلًا من صيغة رياضية ويُشرح بشكل منفصل في المقالة المرافقة حول جداول بحث الألوان من النوع 0. تحل العائلتان المشكلة نفسها، تحويل مدخل إلى مخرج، لكن النوع 0 بيانات مُحسبة مرة واحدة ومخبوزة في الملف، بينما الأنواع 2 و3 و4 شيفرة يُقيّمها القارئ عند كل استدعاء. تتشارك الأنواع الأربعة نقطة توزيع واحدة في عارض HotPDF، مفتاحها إدخال /FunctionType في قاموس الدالة، بحيث لا يحتاج تظليل، أو تحويل درجة لون، أو دالة نقطة نصفية (halftone spot) إلى معرفة أي من الأربعة تلقّته قبل أن يطلب لونًا
كيف تعمل دالة PDF الأسّية من النوع 2؟
تحسب دالة PDF من النوع 2 صيغة واحدة — y = C0 + x^N × (C1 − C0)، مطبَّقة عنصرًا بعنصر — حيث x هو المدخل الوحيد للدالة، مُطبَّعًا مقابل /Domain الخاص به قبل تشغيل الصيغة (ISO 32000-1 §7.10.3). /C0 و/C1 هما متجها المخرجات عند طرفي ذلك النطاق، رقم واحد لكل مكوّن مخرج، و/N هو الأس الذي يشكّل المنحنى بينهما: N = 1 يعطي المنحدر الخطي المستقيم خلف معظم توقفات التدرج وتحويلات الدرجتين اللونيتين (duotone)، وN أعلى من 1 يجذب المنحنى نحو C0، وN بين 0 و1 يدفعه نحو C1. تبني RegisterExponentialFunction ذلك القاموس من خمس وسائط وتُعيد كائن دالة جاهزًا للتوصيل في تظليل، أو دالة نقطة نصفية، أو أي مكان آخر يقبل فيه المعيار مفتاح /Function
علاقة عدد المكوّنات بين C0 وC1 تهم مرتين: مرة عندما تؤلف دالة من النوع 2، ومرة أخرى كلما اضطر HotPDF لرسم دالة لم ينشئها هو. من جانب التأليف، تتحقق RegisterExponentialFunction من C0 وC1 مقابل بعضهما وتُطلق استثناءً إذا اختلفا، بحيث يكون استدعاء يصل إلى BeginDoc بالفعل كائن دالة متسقًا ذاتيًا. أما من جانب الرسم، فيتعيّن على المُقيِّم أن يثق بأي مصفوفتي /C0 و/C1 يعلنهما ملف مصدر فعليًا — ملف من مطبعة فُتح للمعاينة مثلًا، أو مستند موقَّع يُعرض مرة أخرى لمستخدم — وقرأت الإصدارات قبل 2.376.0 تلك المصفوفات إلى مخزن مؤقت بحجم أربعة مكوّنات، حالة CMYK. درجة لون أسّية بنمط DeviceGray أو DeviceRGB، بمصفوفتي /C0 و/C1 من عنصر واحد أو ثلاثة عناصر، فشلت في تلك القراءة بصمت وتركت كلتا المصفوفتين عند صفر، فرُسمت الدرجة اللونية أسودًا مسطحًا بدلًا من لونها المقصود. أعاد الإصدار 2.376.0 ضبط حجم القارئ إلى عدد المخرجات المُعلَن فعليًا للدالة بدلًا من مخزن مؤقت ثابت — وهذا بالضبط نوع الخلل الذي لا تكشفه إلا حالة اختبار غير CMYK، بما أن مجموعة الاختبارات القائمة شغّلت CMYK طوال الوقت، حيث يلائم أربعة في أربعة دائمًا
var
EaseIn: THPDFDictionaryObject;
begin
// Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
EaseIn := Pdf.RegisterExponentialFunction(
[0,1], // Domain: single input, clamped to [0,1]
[0, 0, 0], // C0: output at x = 0
[0.8, 0, 0], // C1: output at x = 1
3, // N: exponent, 1 = linear, > 1 eases toward C0
[]); // Range omitted: defaults to a [0,1] clamp per output
end;
التماس من النوع 3: تسلسل الدوال الفرعية عبر مصفوفة Bounds
تُلحم دالة PDF من النوع 3 عدد k من الدوال الفرعية في تحويل واحد متعدد القطع فوق /Domain لمدخل واحد، والمصفوفتان اللتان تُنجزان ذلك هما /Bounds و/Encode (ISO 32000-1 §7.10.4). تحمل /Bounds عدد k − 1 من نقاط التقسيم الداخلية التي تقسّم /Domain إلى k فترة متتالية؛ يختار المُقيِّم أول فترة يتجاوز حدّها الأعلى المدخل، أو الفترة الأخيرة بمجرد وصول المدخل إلى الحد النهائي، ويسلّم إلى الدالة الفرعية الخاصة بتلك الفترة. تعيد /Encode بعد ذلك تخطيط المدخل من موضعه داخل تلك الفترة إلى أي نطاق مدخل تتوقعه الدالة الفرعية المختارة نفسها — عادة [0, 1] إذا كانت الدالة الفرعية قطعة أسّية إضافية — قبل أن يستمر التقييم، خطوة استدعاء أعمق، إلى /Domain و/Range الخاصين بتلك الدالة الفرعية نفسها
كان مُقيِّم التماس في HotPDF يعالج فقط دالتين فرعيتين بالضبط، وكان قارئ /Bounds الخاص به يتطلب مصفوفة كاملة من ثمانية عناصر، لذا فإن نقطة التقسيم الواحدة التي يحتاجها تدرج من قطعتين فعليًا — رقم واحد في /Bounds — كانت تفشل دائمًا في التحليل وتُعيد الدالة لا شيء. لم تُطبَّق /Encode إطلاقًا. أعاد الإصدار 2.376.0 كتابة الاختيار كبحث عام لعدد k من الدوال الفرعية كما يصفه المعيار وبدأ بقراءة /Bounds مقابل طولها المُعلَن الحقيقي، بحيث يُحل الآن تدرج من ثلاثة أو أربعة أو خمسة توقفات مُلحَم من ذلك العدد من القطع الأسّية بالطريقة نفسها التي كان يدّعي بها تدرج من قطعتين أنه يُحل دائمًا. يبني المثال أدناه منحدرًا من قطعتين أسود إلى أحمر إلى أبيض، وهو الشكل الذي يلجأ إليه تظليل محوري أو شعاعي كلما عجز منحنى أسّي واحد عن حمل كل توقف لوني يتطلبه تصميم
var
ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
// Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
ToRed := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0], [0.8, 0, 0], 1, []);
ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1], 1, []);
Ramp := Pdf.RegisterStitchingFunction(
[0,1], // Domain: the stitched function's own input range
[ToRed, ToWhite], // Functions: k = 2 sub-functions
[0.5], // Bounds: k - 1 = 1 split point
[0,1, 0,1], // Encode: 2 numbers per sub-function
[]); // Range omitted: inherited from each sub-function
end;
ما الذي يمكن لدالة حاسبة PostScript من النوع 4 فعله ولا يستطيعه النوعان 2 و3؟
تُشغّل دالة PDF من النوع 4 برنامجًا حقيقيًا، وإن كان مقيَّدًا عمدًا: حاسبة PostScript تدفع مدخلاتها إلى مكدس معاملات، وتنفّذ عوامل حسابية ومقارنة وتلاعب بالمكدس وعوامل منطقية بالإضافة إلى شروط if/ifelse، وتترك مخرجاتها على المكدس عند الانتهاء (ISO 32000-1 §7.10.5، الجدول 42). لا يوجد بناء حلقة ولا تخزين متغيرات مسمّاة، فقط المكدس، وهذا ما يبقي برنامجًا متوافقًا سهل التفكير فيه — لكن ضمن مجموعة العوامل المقيَّدة تلك، يمكن للنوع 4 التعبير عن أشياء لا يستطيعها النوعان 2 و3، مثل صيغة مزج حقيقية متعددة الأحبار لفصل DeviceN أو دالة نقطة نصفية بعتبة شرطية. يُرمّز مُقيِّم HotPDF، HPDFEvalPostScriptCalculator، البرنامج مرة واحدة — أرقامًا، وعوامل، وكتل إجراءات { } — ثم يجتاز مكدس معاملات من 100 مدخل، وهو العمق الذي يدعو إليه ISO 32000-1 §7.10.5، خلف سقف صارم من 50000 عامل مُقيَّم كخط دفاع احترازي ضد برامج مرضية أو مكتوبة يدويًا
عامل roll: من السهل الحصول على الاتجاه معكوسًا
roll هو العامل الأكثر عرضة للخروج معكوسًا في محاولة أولى، لأن ترتيب وسيطته واتجاه دورانه كلاهما يعملان عكس ما تصفه اللغة الإنجليزية. يسحب n j roll عددًا n ومقدار دوران j، ثم يزيح دوريًا أعلى n مدخل في المكدس بمقدار j موضعًا، ملتفًّا بالعناصر التي تسقط من طرف واحد عائدًا إلى الطرف الآخر؛ المثال الكلاسيكي، مباشرة من المعيار، هو a b c 3 1 roll الذي ينتج c a b — العنصر العلوي ينتقل إلى قاع المجموعة، لا العكس، وكل عنصر آخر يتحرك للأعلى بمقدار واحد لإفساح المجال. يحسب مُقيِّم HotPDF الموضع الجديد لمدخل المكدس i كـ(i + j) mod n، وهو ما يطابق ذلك المثال تمامًا، لكنها حلقة من سطرين يسهل بالقدر نفسه كتابتها بدوران معكوس، ودالة roll معكوسة لا تزال تنتج لونًا يبدو معقولًا — إنه فقط ليس اللون الذي طلبه مؤلف الملف
const
Prog = '{ 3 1 roll }'; // (a b c) -> (c a b): the third input moves to the front
var
Reorder: THPDFStreamObject;
begin
// Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
Reorder := Pdf.RegisterPostScriptFunction(
[0,1, 0,1, 0,1], // Domain: 2 numbers per input
[0,1, 0,1, 0,1], // Range: 2 numbers per output (required for Type 4)
Prog);
end;
round ليست Round في Delphi: التقريب لأعلى مقابل تقريب المصرفي
يحسم عامل round في PostScript تعادل .5 دائمًا نحو العدد الصحيح الأكبر، بينما دالة Round المدمجة في Delphi لا تفعل ذلك: فهي تقرّب نصف-إلى-زوجي، اصطلاح تقريب المصرفي (banker's rounding) الذي يبدّل الاتجاه الذي يقع فيه تعادل .5 كي لا يتراكم تحيّز عبر تقريبات متكررة. تتفق الدالتان في كل مكان تقريبًا وتختلفان بالضبط عند الحد الفاصل المهم هنا — تُعيد Round(0.5) في Delphi القيمة 0 وتُعيد Round(2.5) القيمة 2، بينما تريد round في معيار PDF القيمتين 1 و3 للمدخلين نفسيهما — لذا يختبئ عدم التطابق عبر اختبار عرضي ثم يتكرر كانحراف ثابت بمقدار واحد أينما تقع الحسابات الوسيطة لبرنامج حاسبة تمامًا على عدد نصف صحيح. الجدول 42 في ISO 32000-1 §7.10.5 صريح بأن round تدفع كسر .5 نحو العدد الصحيح الأكبر، لذا ينفّذ HotPDF العامل كـFloor(x + 0.5) بدلًا من استدعاء Round في Delphi، وأي شيفرة تعيد تنفيذ أو تتحقق يدويًا من حسابات برنامج من النوع 4 تحتاج الاستبدال نفسه
function PostScriptRound(const X: Double): Double;
begin
// ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
// integer. Delphi's Round() is banker's rounding and disagrees here:
// Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
Result := Floor(X + 0.5);
end;
التحقق عند التسجيل يلتقط برنامج حاسبة سيئًا مبكرًا
برنامج من النوع 4 مشوَّه رخيص الاكتشاف وقت التأليف ومكلف الاكتشاف في أي مكان آخر، لذا لا تكتفي RegisterPostScriptFunction بتخزين النص المصدري: فهي تُقيّم البرنامج تجريبيًا مرة واحدة، عند نقطة المنتصف لـ/Domain المُعلَن، قبل أن يُكتب كائن الدالة إلى المستند إطلاقًا. كتل { } غير المتوازنة، وعامل غير معروف، ونقص في المكدس، وعدد مخرجات لا يطابق /Range، كلها تفشل في ذلك التشغيل التجريبي وتُطلق استثناءً فورًا، مع إشارة مكدس الاستدعاءات إلى استدعاء RegisterPostScriptFunction بدلًا من عيب رسم يُكتشف أثناء ضمان الجودة على ملف شُحن بالفعل. لا يثبت التشغيل التجريبي عند المنتصف صحة البرنامج عبر /Domain بأكمله — فرع شرطي لا يسيء التصرف إلا بالقرب من حافة واحدة من نطاق الإدخال لا يزال يمكنه الإفلات من نقطة عينة واحدة — لكنه يغلق الفئة الكاملة من البرامج المعطوبة بنيويًا بدلًا من كونها خاطئة فقط في زاوية واحدة
أين تضع التدرجات والألوان التعريفية هذه الدوال موضع العمل
نادرًا ما تظهر الأنواع 2 و3 و4 معزولة في ملف PDF حقيقي؛ فهي تظهر أينما يقبل المعيار مفتاح /Function، وأكثر المستهلكين شيوعًا هما التظليلات وتحويلات درجة اللون التعريفي. يُقيّم عامل sh الخاص بتدرج محوري أو شعاعي (ISO 32000-1 §8.7.4.5) /Function الخاص به مرة واحدة لكل موضع على طول محور التدرج، وهذه بالضبط حالة التوقفات المتعددة التي وُجد التماس من النوع 3 من أجلها. تحويل درجة اللون لفضاء لون Separation أو DeviceN هو الموطن المتكرر الآخر لهذه الأنواع الثلاثة، وهو حيث يستحق النوع 4 مكانته: عادة ما يختزل حبر تعريفي واحد إلى منحنى من النوع 2 أو النوع 0، لكن مزج DeviceN لعدة أحبار بسلوك تراكب (trapping) وطباعة فوقية (overprint) حقيقي كثيرًا ما يحتاج المنطق الشرطي الذي لا تستطيع التعبير عنه إلا حاسبة PostScript، وهي الحالة المشروحة في مقالة رسم ألوان Separation وDeviceN التعريفية. RegisterSeparationFunc هو استدعاء الإقران من جانب التأليف: يأخذ اسم مادة تلوين، وفضاء لون بديل، وأي كائن تعيده عائلة Register*Function، ويربط ذلك التحويل اللوني بمورد فضاء لون Separation يمكن لبقية الصفحة اختياره عبر scn/SCN
معًا، تغطي شبكات العينات في النوع 0 وهذه الأنواع الثلاثة المدفوعة بالصيغ كل /Function يمكن لملف PDF إعلانه، واختيار النوع الصحيح هو في الغالب سؤال عمّا لديك بالفعل: جدول بحث مُحسب في مكان آخر يصبح النوع 0، ومزج بنقطتي نهاية يصبح النوع 2، وعدة مزجات مسلسلة عبر نطاق تصبح النوع 3، وأي شيء بمنطق شرطي حقيقي يصبح النوع 4. RegisterExponentialFunction، وRegisterStitchingFunction، وRegisterPostScriptFunction جزء من مكوّن HotPDF القياسي لـDelphi وC++Builder، إلى جانب بقية واجهته البرمجية للدوال والتظليل وفق ISO 32000-1