מאמר טכני

רינדור צבעי Separation ו-DeviceN Spot ב-Delphi

התוכנה HotPDF מרנדרת צבעי Separation ו-DeviceN spot בדפי PDF טעונים על ידי פענוח מרחב הצבעים באמצעות HPDFResolveColorSpace, הערכת פונקציית טרנספורמציית הגוון (tint-transform) באמצעות HPDFEvalTintTransform והמרת התוצאה דרך מרחב הצבעים החלופי ל-RGB עבור המסך. מאז גרסה v2.375.0, צינור רינדור זה מעריך את כל ארבעת סוגי הפונקציות של PDF, כולל מחשבוני PostScript מסוג Type 4, כך שקובץ מוכן לדפוס המכיל דיו של Pantone מציג את צבעיו האמיתיים במקום סימני מקום. מאמר זה עובר על אופן פעולת צינור הרינדור וכן על האופן שבו הוא נכשל בזמן שבנינו אותו

מקרה הטריגר הוא תמיד זהה. לקוח מקבל קובץ PDF מבית דפוס: הכותרת מוגדרת בדיו spot בעל שם, עיצוב האריזה משתמש בשילוב DeviceN של שני סוגי דיו, וטקסט הגוף הוא שחור פשוט. ב-Acrobat זה נראה מושלם. באפליקציית Delphi שלכם הכותרת מרונדרת בשחור, או גרוע מכך, אינה מרונדרת כלל, והלקוח מגיש דוח באג נגד התוכנה שלכם ולא נגד הקובץ. הקובץ תקין. המרנדר פשוט אינו מפרש את מרחבי הצבעים בהם הקובץ משתמש

מדוע צבעי spot מרונדרים בשחור במציג PDF?

צבעי spot מרונדרים בשחור, או נעלמים, כאשר המרנדר מיישם רק את אופרטורי צבעי ההתקן (rg,‏ g,‏ k) ומתעלם מהאופרטורים הגנריים. תקן ISO 32000-1 §8.6 מגדיר שלוש קבוצות של מרחבי צבעים: מרחבי התקן (DeviceGray, DeviceRGB, DeviceCMYK), מרחבים מבוססי CIE (CalGray, CalRGB, Lab, ICCBased) ומרחבים מיוחדים (Indexed, Separation, DeviceN, Pattern). כל דבר מחוץ לקבוצת ההתקן נבחר עם האופרטורים הגנריים: cs ו-CS בוחרים מרחב לפי שם ממילון המשאבים של הדף, ואז sc,‏ SC,‏ scn ו-SCN מספקים את ערכי הרכיבים. מרנדר שמדלג על אופרטורים אלה שומר על הצבע האחרון שהוגדר, אשר עבור דף שנפתח בכותרת צבע ה-spot שלו הוא השחור הראשוני של DeviceGray

התוכנה HotPDF הוסיפה את קבוצת האופרטורים הגנריים למרנדר הדפים שלה בגרסה v2.333.0, יחד עם נתיב פענוח מאוחד: כל רשומת /ColorSpace במשאבים, בין אם היא שם פשוט, מערך פנימי או הפניה עקיפה, מנותחת לרשומת THPDFColorSpace יחידה, וכל בקשת צבע מילוי או מתווה מתועלת דרך קריאת HPDFResolveColor אחת. פירוט המשפחות מציג את כיסוי התמיכה במבט מהיר

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

החלטת עיצוב אחת מוכיחה את עצמה שוב ושוב: csfUnsupported היא משפחה מדרגה ראשונה, ולא שגיאה. מרחב שהמרנדר אינו יכול לפרש נסוג למצב גיבוי מוגדר במקום לבטל את רינדור הדף, דבר התואם את האופן שבו מציגים מובילים מתנהגים ומונע ממילוי אקזוטי בודד להשבית מסמך שניתן לרינדור

כיצד טרנספורמציית גוון הופכת ערך דיו יחיד לצבע אמיתי?

מרחב Separation נושא שלושה פיסות מידע: שם הדיו, מרחב צבעים חלופי ופונקציית טרנספורמציית גוון. המערך [/Separation /PANTONE485 /DeviceCMYK f] אומר: כאשר זרם התוכן כותב 0.8 scn, הזן את ערך הגוון 0.8 לפונקציה f וצייר את רביעיית ה-CMYK המתקבלת. מרחב DeviceN מכליל זאת ל-N סוגי דיו עם פונקציה בעלת N קלטים. שם הדיו עצמו הוא רק בגדר המלצה על המסך; טרנספורמציית הגוון היא כל סמנטיקת הרינדור, ולכן מרנדר שמנתח את המרחב אך מדלג על הפונקציה לא עשה דבר מועיל עדיין

הפונקציה HPDFEvalTintTransform היא מנוע הפונקציות שמאחורי שלב זה. היא הושקה בגרסה v2.334.0 ומעריכה פונקציות מעריכיות מסוג Type 2 (הנוסחה C0 + x^N * (C1 - C0) עם הגבלת Domain ו-Range), פונקציות תפר מסוג Type 3 (רקנסטרוקציה של תת-פונקציות שנבחרו לפי Bounds עם מיפוי מחדש של Encode), ופונקציות דגימה מסוג Type 0 עם דגימות של 8, 16 ו-32 סיביות. Type 0 הוא אותו מנגנון טבלת חיפוש (look-up table) שכיסינו מצד היצירה במאמר על בניית טבלאות חיפוש צבעים מסוג Type 0; צד הרינדור עובר על אותו מבנה בסדר הפוך, מבייטים של דגימות מפוענחות חזרה לערכי הרכיבים

פונקציות מחשבון PostScript מסוג Type 4 היו האחרונות להצטרף. עד גרסה v2.375.0 הן נסוגו בצורה חלקה לסימן מקום ניטרלי; מאז גרסה v2.375.0, הפונקציה HPDFEvalPostScriptCalculator מריצה את ערכת האופרטורים המלאה של תקן ISO 32000-1 §7.10.5 על גבי מחסנית אופרנדים מוגבלת: מתמטיקה, השוואה, אופרטורים בוליאניים ואופרטורים של סיביות, מניפולציה של מחסנית כולל roll, ותנאי if/ifelse. סמנטיקת מקרי הקצה היא קשיחה יותר מכפי שהיא נראית. פקודת round של PostScript מעגלת חצאים לכיוון הערך הגדול יותר, ולכן לא ניתן להשתמש בעיגול הבנקאי של Delphi באמצעות Round; האופרטורים הטריגונומטריים עובדים במעלות, כאשר atan מחזירה ערכים בטווח [0, 360); ו-exp הוא חזקה של שני אופרנדים, ולא המעריך הטבעי. אותו מעריך מניע גם מילוי מעברי צבע מבוססי פונקציה, והיא הסיבה שרינדור הצללה צירית ורדיאלית קיבל מעברים מונחי מחשבון באותה גרסה

// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

כנות לגבי דיוק: מחשבון תוכנה המוערך בדיוק כפול (double precision) לא יתאים ל-RIP ביט לביט, והגבלה בגבול ה-Range יכולה להיות שונה בסיבית הפחות משמעותית ממימוש אחר. עבור תצוגת מסך ורינדור בדיקות זה חסר משמעות; אם אתם בונים מערכת הגהה מנוהלת צבע, טרנספורמציית הגוון היא רק השלב הראשון ואתם עדיין זקוקים למנוע CMM אמיתי בהמשך הדרך

CalGray, CalRGB, Lab ו-ICCBased ללא מנוע ICC

המשפחות המבוססות על CIE לוקחות את הענף השני של אותו מפענח. HotPDF ממירה ערכי Lab דרך שרשרת Lab ל-XYZ ל-sRGB הסטנדרטית, כולל נקודת השבירה של 6/29 בפונקציית ההעברה ההפוכה, ומטפלת ב-CalRGB עם הגמא לכל ערוץ בתוספת מטריצה ליניארית של 3x3 וב-CalGray עם הגמא הבודדת שלה. הפרט הרלוונטי לביצועים הוא הטיפול בנקודה הלבנה: מטריצת המרת XYZ ל-sRGB מותאמת בשיטת Bradford לנקודה הלבנה המוצהרת במרחב הצבעים ונשמרת במטמון ברשומת ה-THPDFColorSpace המפוענחת, כך שהעבודה לכל פיקסל נשארת כפל 3x3 יחיד ללא קשר לנקודת המקור המוצהרת

מרחבי ICCBased מקבלים טיפול פרגמטי במכוון. מפרט ה-PDF מחייב כל זרם ICCBased להצהיר על מרחב /Alternate (חלופי) או לרמוז עליו דרך ספירת הרכיבים שלו /N, בדיוק כדי שמציגים ללא מנוע ניהול צבע יוכלו עדיין לרנדר בצורה סבירה. HotPDF מפענחת את ICCBased דרך אותו ערוץ חלופי, או מנחשת DeviceGray, DeviceRGB או DeviceCMYK לפי ערך /N של 1, 3 או 4 כאשר הרשומה אינה קיימת, ולעולם אינה מנתחת את בייטי הפרופיל. פירוש הדבר הוא שאין תלות ב-lcms ואין עלות חיפוש פרופיל, במחיר של דיוק צבעוני: מרחב ICCBased שהפרופיל שלו שונה משמעותית מהחלופי שלו יציג את הרינדור החלופי. עבור תצוגה על המסך ותמונות ממוזערות זהו אותו פשרה שכל מציג קל משקל עושה, וזהו הגבול שיש לציין בבירור בתיעוד שלכם

הבאג שגרם לכל חיפוש מרחב צבעים בעל שם להיכשל

החיבור של אופרטור ה-v2.333.0 נשלח עם פגם שנשאר סמוי במשך ארבעים ושתיים גרסאות: המטפלים של cs ו-CS חיפשו את האופרנד שלהם כשהלוכסן המוביל שלו שלם (/CS0), מול מפתחות מילון המשאבים שנשמרו ללא הלוכסן (CS0). כל חיפוש מרחב צבעים בעל שם נכשל, מאה אחוז מהזמן, והקוד נסוג בשקט לברירת המחדל של DeviceGray שחור. הסימפטום הנראה לעין היה מעודן בדרך הגרועה ביותר: מילוי Separation של 1 scn הפך ל-DeviceGray 1.0, שמצייר בלבן, ודיו לבן על דף לבן אינו שגיאת רינדור שמישהו מצלם לה מסך. התיקון, בגרסה v2.375.0, היה פונקציית עזר משותפת לנרמול שמות שהוחלה בכל חיפוש של אופרנד למשאב

שני פגמים קשורים התגלו באותה חקירה. ראשית, הפניות עקיפות לאובייקטים מסוג מערך הוחזרו מבלי שפוענחו: לגישת המסמך של המרנדר היו מפענחים מוגדרי-סוג עבור זרמים ומילונים בלבד, ולכן /CS0 5 0 R המצביע על מערך [/Separation ...] עצמאי הוחזר כהפניה הגולמית והמרחב פוענח כלא נתמך. שנית, הפונקציה HPDFReadNumericArray אכפה סמנטיקת אורך קשיחה, שדרשה ממערך ה-PDF להיות ארוך לפחות כמו הבאפר שסופק. קריאת ערכי /C0 ו-/C1 של פונקציית Type 2 לתוך באפר של ארבעה אלמנטים נכשלה עבור חלופות בעלות רכיב אחד או שלושה רכיבים, מה שהשאיר את שני המערכים מאופסים, וכל גוון מעריכי שאינו CMYK רונדר בשחור עד שגרסה v2.376.0 הציגה את הקורא המקל HPDFReadNumericArrayUpTo. הלקח: כל מפתח PDF המתועד כנושא מערך מספרי באורך משתנה חייב להיקרא עם קורא הממלא את מה שקיים, מכיוון שבאפר בגודל קבוע עם התאמה קשיחה הופך קבצים תקינים לאפסים שקטים

כיצד בודקים רינדור צבעי spot מבלי להוליך את עצמכם שולל?

השאלה המטרידה היא מדוע מערך הבדיקות נשאר ירוק לאורך כל זה. הבדיקה המקורית, SeparationRendersDistinguishable, אימתה רק שהמפת סיביות המרונדרת אינה שחורה לחלוטין. צינור רינדור שהוריד כל צבע spot ל-DeviceGray הפיק פלט אפור ולבן, שאינו שחור, ולכן האימות עבר בהצלחה בעוד שהתכונה כולה הייתה מושבתת. אימותים חלשים מהצורה "אינו ריק", "אינו שחור לחלוטין" או "האש אינו אפס" אינם יכולים להבדיל בין מרנדר עובד למקולקל, מכיוון שכמעט כל מצב כשל עדיין מפיק כמה פיקסלים

סגנון האימות שאכן תופס כשלים אלה נועל את הצבע הצפוי: רנדרו קובץ PDF מינימלי שנבנה ידנית שבו צבע ה-spot מפוענח לגוון ידוע, ואז ספרו את הפיקסלים שבהם גוון זה דומיננטי. רינדור למפת סיביות לצורך בדיקה משתמש באותה נקודת כניסה RenderLoadedPageToBitmap המתוארת במדריך לרינדור דפים למפת סיביות

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Lock the expected ink: demand a real area of red-dominant
        // pixels, never settle for "not all black".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

קובץ הבדיקה (fixture) חשוב בדיוק כמו האימות. קובץ PDF שנבנה ידנית באורך של כמה מאות בייטים, המכיל מילוי Separation אחד ולא שום דבר אחר, אינו משאיר מקום לספק לגבי הפלט הצפוי; קובץ מהעולם האמיתי מפעיל קוד נוסף אך אינו יכול לומר לכם איזה שלב נכשל. אנו מתייחסים כעת לספירת פיקסלים של צבע צפוי כרף המינימלי לכל בדיקת רינדור, מכיוון שזהו סגנון האימות היחיד שאילץ פגמים אלה להיחשף

רינדור צבעי spot ומרחבי צבע CIE הוא חלק מצינור המסמכים הטעונים ב-רכיב HotPDF עבור Delphi ו-C++Builder, לצד מנוע הפונקציות, רינדור הצללה ונתיבי יצוא מפות הסיביות המוצגים למעלה