מאמר טכני

עקביות דיו גושים ב־NChannel ב־Delphi עם HotPDF

בית דפוס מחזיר את העבודה: אותו דיו גוש הופרד לשני לוחות. HotPDF מונע זאת בזמן יצירה כשהוא כותב NChannel בצורת DeviceN בת חמשת היסודות של ISO 32000-2 ומחזיק מרחב חלופי קנוני אחד והמרת גוון אחת לכל שם גוש לכל אורך המסמך, ודוחה את ההגדרה השנייה הסותרת במקום לפלוט אותה

NChannel אינו שם משפחה של מרחב צבע

הדבר הראשון להתיר הוא השם עצמו. NChannel אינו משפחה של מרחבי צבע כפי שהן המשפחות Separation ו־DeviceN. ISO 32000-2 §8.6.6.5 מתאר אותו כ־תת־סוג של DeviceN, ולכן מרחב NChannel תקני נכתב כמערך בן חמישה יסודות [/DeviceN names alternateSpace tintTransform attributes], ומילון התכונות נושא /Subtype /NChannel. אין מערך [/NChannel ...] במפרט. אם בניתם פעם אחת ביד וראיתם RIP מהנהן כלפיו, זו הסיבה

HotPDF כותב מרחב NChannel כמערך ה־DeviceN בן חמשת היסודות שמילון התכונות שלו נושא את Subtype NChannel עם רשומות Process, Colorants ו־MixingHints, כי אין בכלל מערך שם־משפחה NChannel במפרט
מרחב NChannel תקני הוא מערך DeviceN עם מילון תכונות, ולכן הכותב פולט רק את הצורה הזאת בזמן שהקורא עדיין מקבל את האסימון הישן

HotPDF טעה בזה פעם אחת ואז תיקן, ושווה לומר זאת בגלוי כי זה מעצב את התנהגות הרכיב כיום. גרסאות HotPDF ישנות יותר פלטו את צורת שם־המשפחה. THotPDF.RegisterNChannelColorSpace מפליט כיום רק את הצורה הסטנדרטית של DeviceN עם תכונות, וכיוון שתת־הסוג NChannel הגיע ב־PDF 1.6, נקודת הכניסה של המייצר שוערת על RequirePDFVersion(pdf16, ...) ופשוט נמנעת ביעדים ישנים יותר. צד הרינדור סלחני בכוונה יותר מהכותב: HPDFResolveColorSpace עדיין מקבל את האסימון הישן /NChannel כמשפחת DeviceN כדי שקבצים מהכותב הישן ימשיכו להתעבד, אבל כל דבר ש־HotPDF כותב בחזרה משתמש בקידוד הסטנדרטי. סלחנות בקלט וקפדנות בפלט היא אסימטריה נכונה כאן, כי הקורא שלכם חייב להתמודד עם קבצים שהוא לא יצר בעוד שלכותב שלכם אין תירוץ כזה

איך שם גוש אחד מגיע לשני לוחות?

כי שם של דיו גוש הוא זהות לוח ברמת המסמך כולו, ולא ארגומנט מקומי. שתי קריאות ששתיהן קוראות לצבע Orange אבל מעבירות מרחב חלופי אחר, או את אותו מרחב חלופי עם טרנספורמציית גוון אחרת, מתארות שני דיוים שונים שמקרית חולקים תווית. ל־RIP שבונה הפרדות אין דרך ליישב את זה, ולכן הוא עושה את הדבר הישר היחיד ונותן לכם שני לוחות. לכן HotPDF מחזיק חתימה קנונית לכל שם colorant ברמת המסמך. RegisterSpotColorantDefinition מרכיב את החתימה הזאת ממרחב הצבע החלופי ומצורת פונקציית הגוון, וכל קריאה ל־RegisterSeparation, RegisterSeparationFunc, RegisterSeparationLUT ולהגדרת גוש ב־NChannel עוברת דרכה. כשהגדרה שנייה חולקת עליה, הקריאה מעלה חריגה במקום לרשום בשקט וריאציה שנייה, וההודעה מתמקדת בכוונה במצב הכשל המדויק, כי האלטרנטיבה היא לגלות את זה בפרוף לוח שלושה שבועות אחר כך

כל קריאת רישום גוש ב־HotPDF עוברת דרך RegisterSpotColorantDefinition, שרושם חתימת מרחב חלופי וגוון קנונית אחת לכל שם colorant במסמך ומעלה חריגה כשמגיעה הגדרה שנייה סותרת של אותו שם
שם אחד, חתימה אחת: ההגדרה השנייה הסותרת של גוש נדחית בזמן כתיבה ולא מתגלה בפרוף לוח
// Orange נרשם כבר מול DeviceCMYK עם טרנספורמציית הגוון
// 0 / 0.55 / 1 / 0 בגוון מלא.
Conflicting := Pdf.RegisterExponentialFunction(
  Domain1, NoInk, OtherOrangeCMYK, 1, []);
try
  Pdf.RegisterSeparationFunc('Orange', 'DeviceCMYK', Conflicting);
except
  on E: Exception do
    // 'Spot colourant "Orange" has inconsistent alternate colour
    //  space or tint definition in this document'
    LogPrepressWarning(E.Message);
end;

פיצול השמות הראשיים לרכיבי תהליך ולגוש

NChannel שלם צריך להחשיב כל אחד משמות ה־colorant הראשיים שלו בדיוק פעם אחת, כרכיב תהליך או כדיו גוש. העמסת היתר המתקדמת של RegisterNChannelColorSpace מקבלת את ColorantNames הראשיים, את ProcessColorantNames, את המרחב החלופי, את טרנספורמציית הגוון הכוללת, מערך של רשומות THPDFNChannelSpotColorant וסדר דפוס אופציונלי. כל רשומת גוש נושאת שם משלה, טרנספורמציית גוון Separation משלה עם קלט בודד, מדד solidity אופציונלי ופונקציית dot-gain אופציונלית. טרנספורמציית הגוון הכוללת צריכה למפות N קלטים למספר רכיבי המרחב החלופי; כל גוון גוש צריך למפות קלט אחד לאותו מספר

const
  Colorants: array[0..4] of AnsiString =
    ('Cyan', 'Magenta', 'Yellow', 'Black', 'Orange');
  ProcessNames: array[0..3] of AnsiString =
    ('Cyan', 'Magenta', 'Yellow', 'Black');
  Order: array[0..4] of AnsiString =
    ('Yellow', 'Magenta', 'Cyan', 'Orange', 'Black');
  Domain5: array[0..9] of Single = (0, 1, 0, 1, 0, 1, 0, 1, 0, 1);
  Range4: array[0..7] of Single = (0, 1, 0, 1, 0, 1, 0, 1);
  Domain1: array[0..1] of Single = (0, 1);
  NoInk: array[0..3] of Single = (0, 0, 0, 0);
  OrangeCMYK: array[0..3] of Single = (0, 0.55, 1, 0);
  GainC0: array[0..0] of Single = (0);
  GainC1: array[0..0] of Single = (1);
var
  Pdf: THotPDF;
  Spots: array[0..0] of THPDFNChannelSpotColorant;
  CSName: AnsiString;
begin
  Pdf.Version := pdf20;
  Pdf.BeginDoc;
  Spots[0].Name := 'Orange';
  Spots[0].TintTransform := Pdf.RegisterExponentialFunction(
    Domain1, NoInk, OrangeCMYK, 1, []);
  Spots[0].HasSolidity := True;
  Spots[0].Solidity := 0.82;
  Spots[0].DotGainFunction := Pdf.RegisterExponentialFunction(
    Domain1, GainC0, GainC1, 1, []);
  CSName := Pdf.RegisterNChannelColorSpace(Colorants, ProcessNames,
    'DeviceCMYK',
    Pdf.RegisterPostScriptFunction(Domain5, Range4,
      '{ pop pop pop pop pop 0 0 0 0 }'),
    Spots, Order);

מהקריאה הזאת HotPDF פולט את מילון התכונות שהמפרט דורש: /Subtype /NChannel, מילון /Process שבו ה־/ColorSpace הוא מרחב התהליך וה־/Components מונה את שמות התהליך לפי סדר רכיבי המרחב, מילון /Colorants שמחזיק מערך [/Separation name alternate tintfn] אמיתי לכל גוש, ומילון /MixingHints שנושא את /Solidities, /PrintingOrder ו־/DotGain כשסיפקתם אותם. את שם המשאב שמוחזר מעבירים ל־SetFillColorSpace או ל־SetStrokeColorSpace בדיוק כמו במרחבים הפשוטים יותר שכוסו במאמר על עיבוד צבעי גוש Separation ו־DeviceN

HotPDF מפצל את שמות ה־colorant הראשיים של מרחב NChannel למילון Process עבור ארבעת רכיבי ה־CMYK ולמילון Colorants שמחזיק מערך Separation אחד לכל גוש, כשכל אריתיות נבדקת לפני הכתיבה
כל שם ראשי נתבע בדיוק פעם אחת, וטרנספורמציית הגוון הכוללת, הגוונים לכל גוש וסדר הדפוס מאומתים כולם לפני פליטת כל אובייקט

מה בדיקת העקביות דוחה בפועל?

היא דוחה חוסר עקביות מבני בתוך המרחב, ועושה זאת לפני שנכתב אפילו אובייקט אחד. שמות ה־colorant חייבים להיות ייחודיים ואסור שיהיו ריקים, All או None. הגדרות התהליך והגוש יחד צריכות לכסות את השמות הראשיים במדויק, בלי ש־colorant יופיע בשני התפקידים ובלי שאף אחד יישאר לא מוגדר. כשהמרחב החלופי הוא DeviceCMYK, רכיבי התהליך חייבים להיות Cyan, Magenta, Yellow, Black בסדר הזה, ומספר שמות התהליך חייב להתאים למספר רכיבי המרחב החלופי. כל טרנספורמציית גוון וכל פונקציית dot-gain חייבות להיות אובייקט פונקציה עקיף עם האריתיות הנכונה של קלט ופלט. Solidity חייב להיות ערך סופי בתחום 0..1. סדר הדפוס חייב להיות ריק או תמורה מלאה של השמות הראשיים, לעולם לא רשימה חלקית. מה שהיא לא עושה זה לשפוט צבע: כאן לא בודקים שטרנספורמציית הגוון של ה־Orange שלכם דומה באמת לדיו בפחית, שהרכב ה־CMYK שלה הוא פרוקסי סביר, או שה־solidity שסיפקתם תואם להתנהגות שנמדדה על המצע. אלה שאלות של מכונת דפוס ומדידה, ולרכיב אין מעמד לענות עליהן. העמסת היתר הפשוטה, זו של תהליך בלבד, מחמירה עוד יותר במכוון: היא מפיקה NChannel של תהליך בלבד ומסרבת בכוונה לקבל שמות גוש, כי כתיבת גוש למערך השמות בלי רשומת /Colorants מתאימה תפיק קובץ שאינו תואם מבנית, והמצאת הגדרת ברירת מחדל תהיה גרועה מכשל

התכוונויות פלט של PDF/X-6n חייבות לכסות כל גוש רשום

קובץ PDF/X-6n עם N צבעים מכריז על ה־colorants שלו פעמיים, ושתי ההצהרות חייבות להסכים. AddPDFX6ExternalOutputIntent כותב את הפניית פרופיל ה־ICC החיצוני עם ה־ColorantTable שלו, ולפני שהוא עושה זאת, ValidateRegisteredSpotOutputColorants עובר על כל גוש שהמסמך רשם ומעלה חריגה אם אחד חסר בטבלה. הבדיקה רצה בשני הכיוונים: ברגע שהתכוונות פלט פרסמה את רשימת ה־colorants שלה, גם רישום גוש מאוחר לשם שמחוץ לרשימה נדחה. AddPDFX6ExternalOutputIntentSpotData מוסיף מעל זה את מטא־הנתונים לכל דיו, ואוכף כללים משלו, ובעיקר ש־colorant נושא ערך solidity או נתוני ספקטרום CxF/X-4, לעולם לא את שניהם. זהו אותו משטח תאימות שנדון בכתבה על תיקוף PDF/A, PDF/X ו־PDF/UA

Spectral := TMemoryStream.Create;
try
  LoadCxFForInk('Orange', Spectral);   // מטען ISO 17972-4
  Pdf.AddPDFX6ExternalOutputIntentSpotData(
    'ECG-5', 'Five-colour output condition',
    'https://profiles.example.com/ecg-5.icc', '5CLR',
    Colorants,
    '00112233445566778899AABBCCDDEEFF', #4#3#0#0,
    ['Cyan'], [0.70],                  // solidity עבור colorant אחד
    Order,
    ['Orange'], [Spectral]);           // נתוני ספקטרום עבור אחר
finally
  Spectral.Free;
end;

שני פרטים תפעוליים קל לפספס. הרישום שמגבה את כל זה הוא פר מסמך ומנוקה בגבולות המסמך, ולכן טעינת קובץ חדש לאותו מופע THotPDF אינה יורשת את זהויות הגוש של המסמך הקודם; הבידוד הזה הוא כל העניין, כי חתימה שדלפה הייתה דוחה עבודה תקפה לחלוטין בעבודה הבאה. וגם לצד הפרופיל יש מגבלות קשות משלו: התכוונות פלט חיצונית דורשת יעד PDF 2.0, כתובת פרופיל מוחלטת עם HTTP או HTTPS, חתימת מרחב צבע ICC בת ארבעה בתים, ועבור PDF/X-6n בין 2 ל־15 colorants עם חתימה מתאימה מ־2CLR ועד FCLR

איפה הבדיקה נעצרת

טיפול ה־CxF/X-4 הוא החלק שצריך להיות כנים לגביו. HotPDF מחיל על זרם הספקטרום בדיקות בטיחות מבניות חסומות ומאשר שזהות הדיו בתוכו תואמת את ה־colorant שקראתם לו. הוא תוחם את גודל המטען, דוחה בתים אפסים מוטמעים, מסרב לכל זרם שמכיל הצהרת DOCTYPE או ENTITY, דורש שורש CxF שניתן לזהות, ודורש בדיוק אלמנט SpotInkCharacterisation אחד שנושא בדיוק SpotInkName אחד השווה לשם ה־colorant שלכם. זהו שער נגד קלט פגום ועוין, לא מתקף סכמות. זו אינה מימוש מלא של ISO 17972-4, היא אינה מאמתת את מדידות הספקטרום שלכם, והיא אינה מבקרת לאחור גרפי אובייקטים של צד שלישי קיימים מראש או את טבלאות ה־colorant הפנימיות בתוך פרופיל ICC מוטמע. אם זרימת העבודה שלכם תלויה בתאימות CxF מלאה, תקפו את הקובץ בכלי ייעודי לפני שאתם מעבירים אותו לרכיב

מגבלה סמוכה אחת נושכת אנשים שמעולם לא ציפו לפגוש אותה. מסכה רכה של בהירות (luminosity) אינה יכולה להשתמש ב־Separation, DeviceN או NChannel כ־/CS של קבוצת השקיפות שלה; מרחב הערבוב של הקבוצה חייב להיות מרחב מבוסס התקן או CIE, ולכן צבע הגוש בתוך הקבוצה נפתר דרך טרנספורמציית הגוון שלו אל המרחב החלופי לפני חישוב הבהירות. RegisterLuminositySoftMaskState בונה קבוצת DeviceGray בדיוק כדי שזה לא ישתבש בטעות. התוצאה המעשית היא שעיצוב עתיר גושים שממוסך כך נשפט דרך הפרוקסי של ה־CMYK שלו, לא דרך הדיו שלו, וזה חשוב כשמשווים אותו מול הוכחה מופרדת כפי שמתואר בהערות על הוכחות overprint והתקני עיבוד

כל זה לא מבטל את הצורך בפרוף לוח, אבל הוא מעביר סוג שלם של דחיות קדם דפוס מחדר מכונות הדפוס בחזרה אל הבנייה. ממשקי ה־API ל־NChannel, Separation והתכוונויות פלט PDF/X-6n שתוארו כאן מגיעים ב־HotPDF Delphi Component הרגיל עבור Delphi ו־C++Builder, שם התיעוד מפרט את מבני הרשומות המלאים ואת התנאים המדויקים שבהם כל קריאה נכשלת בבטחה