מאמר טכני

פונקציות PDF מסוג 2/3/4 ב-Delphi: אקספוננציאלי, PostScript

HotPDF, רכיב ה-PDF הילידי מסוג VCL עבור Delphi ו-C++Builder, מעריכה את שלושת סוגי הפונקציות של PDF שבנויים מנוסחאות ולא מרשתות דגימה: אינטרפולציה אקספוננציאלית מסוג 2, תפירה (stitching) מסוג 3, ופונקציות מחשבון PostScript מסוג 4, בהתאמה ל-ISO 32000-1 סעיפים 7.10.3, ‏7.10.4, ו-7.10.5. סוג 2 ממזג בין שני וקטורי פלט לאורך עקומה, סוג 3 משרשר כמה תת-פונקציות על פני תחום קלט אחד, וסוג 4 מריץ תוכנית PostScript מוגבלת שיכולה להסתעף, להשוות, ולחשב כמעט כל דבר שזרם תוכן צריך מהקלטים שלו. טעות קלה באחד משלושתם והכשל אף פעם לא מכריז על עצמו כבאג — הוא מופיע כגרדיאנט עם רצועה שטוחה מתה, צבע spot שמעובד כשחור טהור, או פונקציית מחשבון שסוטה בדיוק באחד בקלטים שחבילת בדיקות במקרה לא ניסתה

שלושת אלה יושבים לצד רביעי, סוג 0, ששומר רשת דוגמאות במקום נוסחה ומתואר בנפרד בהמאמר הנלווה על טבלאות חיפוש צבע (LUT) מסוג 0. שתי המשפחות פותרות אותה בעיה, מיפוי קלט לפלט, אבל סוג 0 הם נתונים שמחושבים פעם אחת ואפויים לתוך הקובץ, בעוד סוגים 2, 3, ו-4 הם קוד שהקורא מעריך בכל קריאה. כל ארבעת הסוגים חולקים נקודת שיגור (dispatch) אחת במנוע העיבוד של HotPDF, מופתחת על פי רשומת ה-/FunctionType של מילון הפונקציה, כך שהצללה, טרנספורמציית גוון, או פונקציית spot של הלפטון אף פעם לא צריכה לדעת איזה מהארבעה היא קיבלה לפני שהיא יכולה לבקש צבע

איך עובדת פונקציית PDF מסוג 2 אקספוננציאלית?

פונקציית PDF מסוג 2 מחשבת נוסחה אחת — y = C0 + x^N × (C1 − C0), מיושמת רכיב-רכיב — כאשר x הוא הקלט היחיד של הפונקציה, מנורמל מול ה-/Domain שלו לפני שהנוסחה רצה (ISO 32000-1 סעיף 7.10.3). ‏/C0 ו-/C1 הם וקטורי הפלט בשני קצוות הטווח הזה, מספר אחד לכל רכיב פלט, ו-/N הוא המעריך שמעצב את העקומה ביניהם: N = 1 נותן את הרמפה הליניארית הישרה שמאחורי רוב עצירות הגרדיאנט וההמרות דו-גוניות, N מעל 1 מושך את העקומה לעבר C0, ו-N בין 0 ל-1 דוחף אותה לעבר C1. RegisterExponentialFunction בונה את המילון הזה מחמישה ארגומנטים ומוסרת בחזרה אובייקט פונקציה מוכן לחיבור להצללה, לפונקציית spot של לפטון, או לכל מקום אחר שהמפרט מקבל בו מפתח /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 או פונקציית spot של לפטון עם סף מותנה. המעריך של HotPDF, ‏HPDFEvalPostScriptCalculator, מבצע טוקניזציה לתוכנית פעם אחת — מספרים, אופרטורים, ובלוקי הליך { } — ואז עובר על מחסנית אופרנדים בת 100 רשומות, העומק ש-ISO 32000-1 סעיף 7.10.5 קורא לו, מאחורי תקרה קשה של 50,000 אופרטורים מוערכים כגיבוי הגנתי מפני תוכניות פתולוגיות או כתובות-ביד

אופרטור ה-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 לא: היא מעגלת חצי-לזוגי, מוסכמת עיגול-הבנקאי שמחליפה לאיזה כיוון תיקו של .5 נופל כך שעיגול חוזר לא צובר הטיה. השניים מסכימים כמעט בכל מקום ולא מסכימים בדיוק על הגבול שחשוב כאן — ‏Round(0.5) של Delphi מחזירה 0 ו-Round(2.5) מחזירה 2, בעוד ה-round של מפרט ה-PDF רוצה 1 ו-3 עבור אותם קלטים בדיוק — כך שאי-ההתאמה מתחבאת דרך בדיקה מזדמנת ואז משתחזרת כסטייה-של-אחד עקבית בכל מקום שהחשבון הביניים של תוכנית מחשבון נוחת בדיוק על חצי-שלם. ‏ISO 32000-1 סעיף 7.10.5 טבלה 42 מפורש בכך ש-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 המוצהר, לפני שאובייקט הפונקציה אי-פעם נכתב לתוך המסמך. בלוקי { } לא מאוזנים, אופרטור לא-מזוהה, underflow של מחסנית, או מספר פלט שלא תואם ל-/Range כולם נכשלים בהרצת-הניסיון הזו ומעלים חריגה מיד, עם מחסנית קריאות שמצביעה על קריאת RegisterPostScriptFunction במקום על ארטיפקט עיבוד שהתגלה במהלך QA על קובץ שכבר יצא לדרך. הניסיון באמצע לא מוכיח שהתוכנית נכונה על פני כל ה-/Domain שלה — הסתעפות מותנית שמתנהגת רע רק ליד קצה אחד של טווח הקלט עדיין יכולה לחמוק על פני נקודת דגימה בודדת — אבל הוא סוגר את כל המחלקה של תוכניות שפגומות מבנית ולא רק שגויות בפינה אחת

איפה גרדיאנטים וצבעי spot מפעילים את הפונקציות האלה

סוגים 2, 3, ו-4 לעיתים רחוקות מופיעים בבידוד ב-PDF אמיתי; הם מופיעים בכל מקום שהמפרט מקבל מפתח /Function, ושני הצרכנים הנפוצים ביותר הם הצללות וטרנספורמציות גוון של צבעי spot. אופרטור ה-sh (ISO 32000-1 סעיף 8.7.4.5) של גרדיאנט צירי או רדיאלי מעריך את ה-/Function שלו פעם אחת לכל מיקום לאורך ציר הגרדיאנט, וזה בדיוק מקרה מרובה-העצירות שתפירת סוג 3 קיימת בשבילו. טרנספורמציית גוון של מרחב צבע Separation או DeviceN הוא הבית הנפוץ האחר לשלושת הסוגים האלה, וזה המקום שבו סוג 4 מרוויח את מקומו: דיו spot בודד בדרך כלל מצטמצם לעקומה מסוג 2 או 0, אבל תערובת DeviceN של כמה דיו עם התנהגות טרפינג ו-overprint אמיתית לעיתים קרובות זקוקה ללוגיקה מותנית שרק מחשבון PostScript יכול לבטא, המקרה המתואר במאמר על עיבוד צבעי spot מסוג Separation ו-DeviceN. ‏RegisterSeparationFunc היא קריאת ההתאמה בצד הכתיבה: היא מקבלת שם דיו, מרחב צבע חלופי, וכל אובייקט שמשפחת ה-Register*Function מחזירה, ומחווטת את טרנספורמציית הגוון הזו למשאב מרחב-צבע Separation ששאר העמוד יכול לבחור עם scn/SCN

יחד, רשתות הדגימה של סוג 0 ושלושת הסוגים מונעי-הנוסחה האלה מכסים כל /Function ש-PDF יכול להצהיר, ובחירת הנכון בעיקר עניין של מה כבר יש לך: טבלת חיפוש שחושבה במקום אחר הופכת לסוג 0, מיזוג דו-נקודתי הופך לסוג 2, כמה מיזוגים משורשרים על פני תחום הופכים לסוג 3, וכל דבר עם לוגיקה מותנית אמיתית הופך לסוג 4. ‏RegisterExponentialFunction, ‏RegisterStitchingFunction, ו-RegisterPostScriptFunction הם חלק מרכיב HotPDF הסטנדרטי עבור Delphi ו-C++Builder, לצד שאר ה-API של פונקציות והצללות ISO 32000-1 שלו