מאמר טכני

kernels להקטנת תמונות ו-dithering להדפסה ב-HotPDF

HotPDF חושף שלושה kernels להקטנת תמונות דרך המאפיין ImageDownsampleKernel ומעבר Floyd-Steinberg נפרד דרך RenderOutputDither. הראשון שולט איך תמונות נראות אחרי שמקטינים אותן כדי לעמוד בתקציב גודל, השני שולט איך הן נראות אחרי שעמוד הופחת לשחור-לבן. אף אחד מהם לא מופעל כברירת מחדל, ושניהם opt-in מאותה סיבה: הם עולים בזמן אמיתי

הלחץ שמוביל אנשים לכאן מוכר. חוזה סרוק של 60 MB חייב לצאת דרך שער דואר שדוחה כל דבר מעל 10 MB, או אצוות מצהירים חייבת להגיע להתקן חד-גוני בסגנון פקס שמעבד כל פיקסל אפור כנייר או כטונר. שני הבעיות הן בעיות resampling, ולשתיהן יש תשובה מהירה שנראית רע ותשובה איטית שנראית נכון

במה שלושת ה-kernels באמת נבדלים

ל-THPDFResampleKernel שלושה ערכים, והם יושבים בנקודות שונות באמת על עקומת המהירות והאיכות. rkHalftone מאציל לנתיב ה-GDI ההיסטורי של StretchBlt עם מצב HALFTONE, שחרף שמו הוא סינון ברמת bilinear: מהיר, מספק ל-line art ולצילומי מסך, ונוטה לקצוות גרגיריים שמזהים מיד על תמונות מוקטנות. rkBicubic מריץ kernel Catmull-Rom נפרד, ו-rkLanczos3 מריץ sinc מוחלון נפרד עם תמיכה של שלושה אונות

שני ה-kernels הנפרדים רצים כשני מעברים, אופקי ואז אנכי, עם 6 עד 12 taps לכל פיקסל יעד בפסקל טהור. זה בערך סדר גודל איטי יותר מהנתיב של GDI, וזו בדיוק הסיבה ש-rkHalftone נשאר ברירת המחדל. באצווה לילית של אלפי עמודים ההפרש הוא החלטת תזמון, לא העדפה. במסמך יחיד שמשתמש מחכה לו, Lanczos3 כמעט חינם וטוב יותר בעין

עקומות המשקל של שלושת kernels ההקטנה של HotPDF: rkHalftone מאציל לנתיב GDI מבוסס HALFTONE ברמת bilinear עם תמיכה של אחד, rkBicubic מריץ עקומה שלישונית Catmull-Rom נפרדת עם תמיכה של שתיים, ו-rkLanczos3 מריץ sinc מוחלון עם תמיכה של שלוש, בתמורה של כסדר גודל במהירות לתמונות טובות יותר בעין
שלושת ערכי ה-kernel יושבים בנקודות שונות באמת על עקומת המהירות והאיכות: נתיב GDI ברמת bilinear, עקומה שלישונית Catmull-Rom ו-sinc מוחלון של שלוש אונות, וה-kernels הנפרדים מנרמלים משקלים כך ששום דבר לא מצלצל מעבר לשחור או ללבן

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

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // קובעים לפני הקריאה
    Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
    if Changed > 0 then
    begin
      Writeln('resampled images: ', Info.DownsampledImageCount);
      Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
      Pdf.SaveToFile('scanned-contract-150dpi.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

הארגומנט MinimumSavingsBytes, 4096 למעלה, הוא המגן ששומר על הפעולה כנה. קידוד מחדש של תמונה שכבר נדחסה ביעילות יכול להניב stream גדול מהמקור, ומקטין שמחליף כל תמונה בעיוורון יגדיל לפעמים את הקובץ שהתבקש לצמצם. הסף אומר: מאשרים את ההחלפה רק כשהיא חוסכת לפחות את מספר הבייטים הזה. PreservedCalibratedImageCount מדווח את ההחלטה השמרנית השנייה, תמונות שהושארו ללא מגע כי הן נושאות מרחב צבע מכויל ש-resampling היה מפגע בו

מדוע מקדם פולינום שגוי כה קשה לאתר?

כי kernel אינטרפולציה שבור לא קורס ולא זורק, הוא פשוט מניב תמונה שנראית שגויה בעדינות באופן שאף אחד לא יכול לייחס. ה-kernel של Catmull-Rom הוא שלישוני לחתיכות, והענף החיצוני שלו בצורת Horner מקוננת הוא ((-0.5t + 2.5)t - 4)t + 2. כותבים את המקדם האמצעי כ--5 במקום -4 והפונקציה עדיין מחושבת, עדיין מחזירה מספרים בטווח סביר, ועדיין מייצרת תמונה

הנזק מופיע כ-W(1) שמחושב ל--1 שם שחייב להיות 0. משקלים שליליים מצטברים, הסכום נחתך באפס, והסימפטום הנראה הוא מעבר גוון שקצהו השמאלי מתקרר וקצה מדרגה שמאבד את הגוונים הביניים שלו. שום דבר בכישלון לא מצביע על פולינום. הבדיקה שתופסת את זה בשניות היא אריתמטית ולא ויזואלית: kernel מאנדק חייב לקיים W(0) = 1 ו-W(±1) = W(±2) = 0, וכל kernel שמחמיץ את שלוש הנקודות האלה מסתיר טעות מקדם, נקודה. טוענים על שלושת הערכים האלה בבדיקת יחידה ומעמד הפגמים של הקלדה נעלם כולו

גרף הענף החיצוני של ה-bicubic Catmull-Rom של HotPDF שמראה מדוע מקדם שגוי מסתתר: ה-typo שכותב -5 במקום -4 בצורת ה-Horner המקוננת עדיין מחושב ומשאיר את W(1) ב--1 ואת W(2) ב--2 שם נדרש אפס, ולכן טענה ש-W(0) שווה 1 בתוספת שני אילוצי האפס תופסת זאת בשניות
kernel שבור לעולם לא קורס, הוא פשוט מחזיר מספרים שנראים סבירים, וזו הסיבה שהעין לא תופסת typo של מקדם. W(0) = 1 עם אפסים בפלוס ובמינוס אחד ושתיים הוא בדיקת יחידה של שלוש שורות

dithering של Floyd-Steinberg, ואיפה שהוא יושב ב-pipeline

מעבר ה-dithering הוא בעיה אחרת מ-resampling ויושב בנקודה אחרת ב-pipeline. RenderOutputDither מחיל הפצת שגיאה של Floyd-Steinberg אחרי ההרכבה של העמוד, וזה המקום היחיד שהגיוני לתצוגה מקדימה של הדפסה חד-גונית או לייצוא בסגנון פקס: הפעולה היא על הפחתת raster מוגמר לסיבית אחת לפיקסל, ולא על איך תמונות בודדות עובדו בכניסה

האלגוריתם עצמו קצר. הבהירות נחתכת ב-50 אחוז, ושגיאת הקוונטיזציה מופצת לארבעה שכנים עם המשקלים הקלאסיים 7/16, 3/16, 5/16 ו-1/16, ימינה, שמאלה-מטה, מטה וימינה-מטה. הפיקסל בפלט הוא 0 או 255 בכל ערוץ. מה שהאלטרנטיבה הנאיבית נותנת במקום, חיתוך חד בלי הפצה, הופך תמונה לצללית ומאבד כל גוון ביניים שנשא את התוכן

מיקום ב-pipeline העיבוד של dithering ה-Floyd-Steinberg של HotPDF: RenderOutputDither רץ אחרי הרכבת העמוד על ה-raster המוגמר בן 24 הסיביות, חותך בהירות ב-50 אחוז ומפיץ כל שגיאת קוונטיזציה ימינה ומטה עם משקלי 7/16, 3/16, 5/16 ו-1/16 דרך חוצץ שורות שחייב לצבור, ומניב פלט חד-גוני של סיבית אחת
ל-dithering יש מקום אחרי ההרכבה כי הוא מפחית raster מוגמר לסיבית אחת, ולא בגלל איך התמונות עובדו. משקלי ההפצה מסתכמים לאחד, וחוצץ השורות חייב לצבור ולא לדרוס
// dithering בזמן העיבוד עבור התקן תצוגה מקדימה חד-גוני
Pdf.RenderOutputDither := True;

// או מחילים את אותו מעבר על bitmap שכבר בבעלותך. ה-bitmap חייב
// להיות pf24bit; הפונקציה מחזירה False במקום לנחש
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// גישה ישירה ל-kernel כשמדגימים מחדש מחוץ ל-pipeline של המסמך
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

יש פרט מימוש אחד בהפצת שגיאות שנושך כל אחד פעם אחת. חוצץ השגיאות משורה לשורה חייב לצבור. כל פיקסל בשורה הבאה מקבל תרומות משלושה פיקסלים שונים בשורה הנוכחית, ה-taps של 3/16, 5/16 ו-1/16, ואם הקוד משייך במקום להוסיף, כל כתיבה משליך את התרומה הקודמת ורק ה-tap האחרון שורד. התמונה עדיין נראית דיתרה, וזה מה שמקשה לשים לב, אבל הטקסטורה שגויה והשבת הגוונים סוטה. הבדיקה שתופסת את זה כמותית: דיתר שדה אפור אחיד בינוני ודרוש שהכיסוי הפנימי ינח בין 40 ל-60 אחוז

איזו שילוב צריך pipeline של הקטנת גודל להשתמש בו?

מתאימים את ה-kernel למה שהתמונות באמת הן, ומתייחסים ל-dithering כעניין של התקן ולא כעניין של דחיסה. לסריקות צילום שחייבות לעמוד בתקציב גודל, rkLanczos3 ב-150 או 200 DPI שומר על הפרטים שאנשים מבחינים בהם תוך חיתוך מספר הפיקסלים פי ארבעה ויותר. לצילומי מסך, דיאגרמות ו-line art, rkHalftone באמת מספיק והרבה יותר מהיר, כי לתמונות האלה מעט מעברי גוון לשמר. לאצווה מעורבת שלא ניתן לבחון בה כל תמונה, rkBicubic הוא אמצע הדרך הסביר: טוב יותר מ-bilinear, בערך חצי מספר ה-taps של Lanczos3

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

אם בונים את משטח התצוגה המקדימה שמציג את התוצאה, אותו נתיב עיבוד שמתועד בעיבוד עמוד PDF ל-bitmap הוא המקום שבו RenderOutputDither נכנס לפעולה, כך שהתצוגה המדותרת והפלט המדותר מגיעים מנתיב קוד אחד ולא משני מימושים שמתפצלים

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