مقاله فنی

توابع PDF از نوع ۲/۳/۴ در Delphi: نمایی، الحاقی، PostScript

HotPDF، کامپوننت بومی VCL برای PDF در Delphi و C++Builder، سه نوع تابع PDF ساخته‌شده از فرمول به‌جای شبکه‌ی نمونه را ارزیابی می‌کند: درون‌یابی نمایی نوع ۲، الحاق نوع ۳، و توابع ماشین‌حساب PostScript نوع ۴، که به‌ترتیب با ISO 32000-1 §7.10.3، §7.10.4، و §7.10.5 متناظرند. نوع ۲ بین دو بردار خروجی در امتداد یک منحنی ترکیب می‌کند، نوع ۳ چندین زیرتابع را در یک دامنه‌ی ورودی به‌هم زنجیر می‌کند، و نوع ۴ یک برنامه‌ی محدودشده‌ی PostScript اجرا می‌کند که می‌تواند تقریباً هر چیزی را که یک جریان محتوا از ورودی‌هایش نیاز دارد شاخه‌بندی، مقایسه، و محاسبه کند. هرکدام از این سه را کمی اشتباه بگیرید و خرابی هرگز خودش را به‌عنوان یک باگ اعلام نمی‌کند — به‌شکل یک گرادیان با یک باند کاملاً تخت، یک رنگ نقطه‌ای که کاملاً سیاه رندر می‌شود، یا یک تابع ماشین‌حساب که دقیقاً یکی کم روی ورودی‌هایی که یک مجموعه تست اتفاقاً امتحان نکرده نمایان می‌شود

این سه در کنار یک چهارمی می‌نشینند، نوع ۰، که به‌جای یک فرمول یک شبکه‌ی نمونه‌برداری‌شده ذخیره می‌کند و جداگانه در مقاله‌ی همراه درباره‌ی جدول‌های جستجوی رنگ نوع ۰ پوشش داده شده. این دو خانواده همان مسئله را حل می‌کنند، نگاشت یک ورودی به یک خروجی، اما نوع ۰ داده‌ای است که یک‌بار محاسبه و درون فایل پخته شده، درحالی‌که نوع ۲، ۳، و ۴ کدی هستند که خواننده در هر فراخوانی ارزیابی می‌کند. هر چهار در یک نقطه‌ی dispatch در رندرر HotPDF مشترک هستند، که با ورودی /FunctionType دیکشنری تابع کلیددهی شده، پس یک سایه‌زنی، یک تبدیل رنگ، یا یک تابع نقطه‌ی هافتون هرگز نیازی ندارد بداند کدام‌یک از آن چهار را پیش از خواستن یک رنگ دریافت کرده

یک تابع نمایی PDF نوع ۲ چطور کار می‌کند؟

یک تابع PDF نوع ۲ یک فرمول را محاسبه می‌کند — y = C0 + x^N × (C1 − C0)، اعمال‌شده مؤلفه‌به‌مؤلفه — که در آن x تنها ورودی تابع است، نرمال‌شده در برابر /Domain آن پیش از اجرای فرمول (ISO 32000-1 §7.10.3). /C0 و /C1 بردارهای خروجی در دو انتهای آن بازه هستند، یک عدد به‌ازای هر مؤلفه‌ی خروجی، و /N نمایی است که منحنی میان آن‌ها را شکل می‌دهد: N = ۱ رمپ خطی ساده‌ای می‌دهد که پشت اغلب توقف‌های گرادیانی و تبدیل‌های duotone قرار دارد، N بالاتر از ۱ منحنی را به‌سمت C0 می‌کشد، و N بین ۰ و ۱ آن را به‌سمت C1 هُل می‌دهد. RegisterExponentialFunction آن دیکشنری را از پنج آرگومان می‌سازد و یک شیء تابع پس می‌دهد آماده برای وصل‌شدن به یک سایه‌زنی، یک تابع نقطه‌ی هافتون، یا هر جای دیگری که مشخصات یک کلید /Function می‌پذیرد

رابطه‌ی تعداد مؤلفه بین C0 و C1 دو بار اهمیت پیدا می‌کند: یک‌بار وقتی یک تابع نوع ۲ می‌نویسید، و بار دیگر هر زمانی که HotPDF مجبور است یکی را که خودش نساخته رندر کند. در سمت نویسندگی، RegisterExponentialFunction، C0 و C1 را در برابر یکدیگر بررسی می‌کند و اگر ناسازگار باشند raise می‌کند، پس فراخوانی‌ای که به BeginDoc می‌رسد از پیش یک شیء تابع خودسازگار است. اما در سمت رندر، ارزیاب باید به هر آرایه‌ی /C0 و /C1ای که یک فایل منبع واقعاً اعلام کرده اعتماد کند — مثلاً یک فایل چاپخانه‌ای که برای پیش‌نمایش باز شده، یا یک سند امضاشده که به کاربر نشان داده می‌شود — و نسخه‌های پیش از 2.376.0 آن آرایه‌ها را در یک بافر با اندازه‌ی چهار مؤلفه، حالت CMYK، می‌خواندند. یک tint نمایی از نوع DeviceGray یا DeviceRGB، با یک /C0 و /C1 یک یا سه‌عنصری، آن خواندن را بی‌سروصدا شکست می‌داد و هر دو آرایه را در صفر رها می‌کرد، پس tint به‌جای رنگ مورد نظرش سیاه تخت رنگ می‌شد. نسخه‌ی 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;

الحاق نوع ۳: زنجیرکردن زیرتوابع در سرتاسر یک آرایه‌ی Bounds

یک تابع PDF نوع ۳ k زیرتابع را در یک نگاشت تکه‌ای واحد روی /Domain یک ورودی تکی به‌هم می‌دوزد، و دو آرایه‌ای که آن را ممکن می‌کنند /Bounds و /Encode هستند (ISO 32000-1 §7.10.4). /Bounds شامل k − 1 نقطه‌ی تقسیم داخلی است که /Domain را به k بازه‌ی متوالی می‌بُرد؛ ارزیاب اولین بازه‌ای که کران بالایش از ورودی فراتر رود را انتخاب می‌کند، یا آخرین بازه را وقتی ورودی به کران نهایی برسد، و به زیرتابع آن بازه واگذار می‌کند. سپس /Encode ورودی را از موقعیتش درون آن بازه به هر بازه‌ی ورودی‌ای که خودِ زیرتابع انتخاب‌شده انتظار دارد — معمولاً [۰، ۱] اگر زیرتابع یک قطعه‌ی نمایی دیگر باشد — دوباره نگاشت می‌کند پیش از اینکه ارزیابی، یک فراخوانی عمیق‌تر، به درون /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 نوع ۴ چه کاری می‌تواند بکند که نوع ۲ و ۳ نمی‌توانند؟

یک تابع PDF نوع ۴ یک برنامه‌ی واقعی، هرچند عمداً محدودشده، اجرا می‌کند: یک ماشین‌حساب PostScript که ورودی‌هایش را روی یک پشته‌ی عملوند فشار می‌دهد، عملگرهای محاسباتی، مقایسه‌ای، دستکاری پشته، و بولی به‌علاوه‌ی شرطی‌های if/ifelse را اجرا می‌کند، و خروجی‌هایش را روی پشته می‌گذارد وقتی تمام شود (ISO 32000-1 §7.10.5، جدول ۴۲). هیچ سازه‌ی حلقه‌ای و هیچ ذخیره‌ی متغیر نام‌داری وجود ندارد، فقط پشته، که استدلال درباره‌ی یک برنامه‌ی مطابق را آسان نگه می‌دارد — اما درون آن مجموعه‌ی عملگر محدودشده، نوع ۴ می‌تواند چیزهایی را بیان کند که نوع ۲ و نوع ۳ نمی‌توانند، مثل یک فرمول ترکیب چندجوهری واقعی برای یک جداسازی DeviceN یا یک تابع نقطه‌ی هافتون با یک آستانه‌ی شرطی. ارزیاب HotPDF، HPDFEvalPostScriptCalculator، برنامه را یک‌بار توکنایز می‌کند — اعداد، عملگرها، و بلوک‌های رویه‌ی { } — سپس یک پشته‌ی عملوند صد-ورودی‌ای را می‌پیماید، همان عمقی که ISO 32000-1 §7.10.5 می‌خواهد، پشت یک سقف سخت پنجاه‌هزار عملگر ارزیابی‌شده به‌عنوان یک سد دفاعی در برابر برنامه‌های پاتولوژیک یا دستی‌نوشته

عملگر roll: جهتش به‌راحتی می‌تواند برعکس شود

roll عملگری است که بیشترین احتمال را دارد در اولین تلاش برعکس دربیاید، چون هم ترتیب آرگومانش و هم جهت چرخشش هر دو خلاف آنچه انگلیسی توصیفشان می‌کند اجرا می‌شوند. n j roll یک شمارنده‌ی n و یک مقدار چرخش j را pop می‌کند، سپس 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 این کار را نمی‌کند: آن نیم-به-زوج گرد می‌کند، قرارداد گردکردن بانکی که جهت افتادن یک تساوی .5 را متناوب می‌کند تا گردکردن تکراری بایاس انباشته نکند. این دو تقریباً همه‌جا توافق دارند و دقیقاً روی همان مرزی که اینجا اهمیت دارد اختلاف دارند — Round(0.5) در Delphi ۰ برمی‌گرداند و Round(2.5) عدد ۲ را، درحالی‌که round در مشخصات PDF برای همان ورودی‌ها ۱ و ۳ می‌خواهد — پس این ناهماهنگی در طول تست معمولی پنهان می‌ماند و بعد به‌عنوان یک خطای ثابت یکی-کم هرجایی که محاسبات میانی یک برنامه‌ی ماشین‌حساب دقیقاً روی یک نیم-عدد‌صحیح بنشیند بازتولید می‌شود. ISO 32000-1 §7.10.5 جدول ۴۲ صریح است که round یک کسر .5 را به‌سمت عدد صحیح بزرگ‌تر می‌فشارد، پس HotPDF این عملگر را به‌جای فراخوانی Round در Delphi، به‌صورت Floor(x + 0.5) پیاده‌سازی می‌کند، و هر کدی که محاسبات یک برنامه‌ی نوع ۴ را دستی بازسازی یا نقطه‌ای بررسی می‌کند به همین جایگزینی نیاز دارد

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;

اعتبارسنجی در زمان ثبت، یک برنامه‌ی ماشین‌حساب بد را زودتر می‌گیرد

یک برنامه‌ی نوع ۴ بدشکل، گرفتنش در زمان نویسندگی ارزان و در هر جای دیگری گران است، پس RegisterPostScriptFunction فقط متن منبع را ذخیره نمی‌کند: این برنامه را یک‌بار، در نقطه‌ی میانی /Domain اعلام‌شده، آزمایشی ارزیابی می‌کند، پیش از اینکه شیء تابع اصلاً درون سند نوشته شود. بلوک‌های نامتوازن { }، یک عملگر ناشناخته، یک underflow پشته، یا تعداد خروجی‌ای که با /Range مطابقت ندارد همگی آن اجرای آزمایشی را شکست می‌دهند و بلافاصله یک استثنا raise می‌کنند، با پشته‌ی فراخوانی‌ای که به فراخوانی RegisterPostScriptFunction اشاره می‌کند به‌جای یک مصنوع رندر که در طول QA روی یک فایلی که از پیش عرضه شده کشف می‌شود. آزمایش نقطه‌ی میانی ثابت نمی‌کند برنامه در سرتاسر کل /Domain آن درست است — یک شاخه‌ی شرطی که فقط نزدیک یک لبه از بازه‌ی ورودی بدرفتاری می‌کند همچنان می‌تواند از یک نقطه‌ی نمونه‌ی تکی بگذرد — اما کل رده‌ی برنامه‌هایی را که از نظر ساختاری خراب هستند، نه صرفاً در یک گوشه اشتباه، می‌بندد

گرادیان‌ها و رنگ‌های نقطه‌ای این توابع را کجا به کار می‌گیرند

نوع ۲، ۳، و ۴ به‌ندرت در یک PDF واقعی به‌تنهایی ظاهر می‌شوند؛ آن‌ها هرجایی که مشخصات یک کلید /Function می‌پذیرد ظاهر می‌شوند، و دو مصرف‌کننده‌ی رایج‌ترین سایه‌زنی‌ها و تبدیل‌های tint رنگ نقطه‌ای هستند. عملگر sh یک گرادیان محوری یا شعاعی (ISO 32000-1 §8.7.4.5) /Function خودش را یک‌بار به‌ازای هر موقعیت در امتداد محور گرادیان ارزیابی می‌کند، که دقیقاً همان حالت چندتوقفی است که الحاق نوع ۳ برایش وجود دارد. تبدیل tint یک فضای رنگ Separation یا DeviceN، خانه‌ی مکرر دیگر این سه نوع است، و همان‌جایی است که نوع ۴ ارزشش را ثابت می‌کند: یک جوهر نقطه‌ای تکی معمولاً به یک منحنی نوع ۲ یا نوع ۰ تقلیل می‌یابد، اما یک ترکیب DeviceN از چند جوهر با رفتار واقعی trapping و overprint اغلب به منطق شرطی‌ای نیاز دارد که فقط یک ماشین‌حساب PostScript می‌تواند بیان کند، حالتی که در مقاله‌ی رندر رنگ‌های نقطه‌ای Separation و DeviceN پوشش داده شده. RegisterSeparationFunc فراخوانی همتاشده در سمت نویسندگی است: یک نام colorant، یک فضای رنگ جایگزین، و هر شیئی که خانواده‌ی Register*Function برگرداند را می‌گیرد، و آن تبدیل tint را به یک منبع فضای رنگ Separation سیم‌کشی می‌کند که بقیه‌ی صفحه می‌تواند با scn/SCN انتخاب کند

شبکه‌های نمونه‌برداری‌شده‌ی نوع ۰ و این سه نوع فرمول‌محور با هم، هر /Functionای را که یک PDF می‌تواند اعلام کند پوشش می‌دهند، و انتخاب درست‌ترین آن‌ها اغلب سؤالی است درباره‌ی اینکه از پیش چه چیزی دارید: یک جدول جستجو که جای دیگری محاسبه شده به نوع ۰ تبدیل می‌شود، یک ترکیب دو-نقطه‌انتهایی به نوع ۲ تبدیل می‌شود، چند ترکیب زنجیرشده در سرتاسر یک دامنه به نوع ۳ تبدیل می‌شود، و هر چیزی با منطق شرطی واقعی به نوع ۴ تبدیل می‌شود. RegisterExponentialFunction، RegisterStitchingFunction، و RegisterPostScriptFunction بخشی از کامپوننت استاندارد HotPDF برای Delphi و C++Builder هستند، در کنار بقیه‌ی API تابع و سایه‌زنی ISO 32000-1 آن