שבוע אחרי שתכונת הדחיסה יוצאת מגיעות שתי תלונות: לחוזה הסרוק יש עכשיו קווי מתאר של אותיות מדורגים ופרוותיים, והלוגו השקוף בעמוד השער יושב בתוך הילה חיוורת. PDFiumPas עונה על שתיהן במקום אחד. TPdf.OptimizeImages מודד כל תמונה לפני שהוא מקטין אותה, ואז בוחר גרעין דגימה מחדש וצובר צבע בצורה מודעת־אלפא
זה לא היה כך תמיד. לפני v3.100.0 אותה שיטה ביצעה דגימת יתר למטה לכל תמונה שאינה דו־מפלסית בצעד שכן קרוב קבוע — בדיוק האלגוריתם שמייצר את שתי התלונות: הוא דוגם בנקודה פיקסל מקור אחד לכל פיקסל פלט, והוא מתייחס ל־RGB שיושב מתחת לפיקסל שקוף לחלוטין כאילו לקורא אי פעם תהיה אפשרות לראות אותו. שכתוב v3.100.0 מחליף את הנתיב הבודד הזה בחמישה גרעינים, כלל בחירה מדוד ותקציב זיכרון עבודה מפורש
למה דגימה מחדש גורמת לטקסט סרוק להיראות משונן?
כי דגימה בנקודה עונה על השאלה הלא נכונה. כשסריקה של 300 DPI מכוונת מחדש ל־150 DPI, כל פיקסל יעד מייצג בלוק שתיים על שתיים של פיקסלי מקור, והשכן הקרוב שומר אחד מהארבעה ומשליך את השאר. מי מהם שורד תלוי בעיגול, ולכן קצה קו שהיה חלק עם אנטי־קצה (antialiasing) במקור הופך להטלת מטבע לכל פיקסל. התוצאה היא מדרגות האליאסינג הקלאסיות לאורך קצוות הגליפים, ובנוסף מוארה (moiré) באזורי הפוטוהלפתון שבהם הדגימות שהושלכו במקרה נשאו את התבנית. זה חשוב יותר ב־PDF מאשר על המסך כי הנזק הוא קבוע. XObject של תמונה נושא את נתוני הדגימה שלו לצד /Width, /Height ו־/BitsPerComponent (ISO 32000-1 §8.9.5), ודגימה מחדש כותבת מחדש את שלושתם בתוך הקובץ. זום גרוע במציג הוא פריים שאפשר לצייר מחדש, ול־PDFiumPas יש מנגנון נפרד לזה במטמון רינדור וביצועי זום. דגימה מחדש גרועה היא מסמך חדש שאתם מוסרים ללקוח
איך PDFiumPas מודד פירוט ובוחר גרעין
PDFiumPas מחליט לכל תמונה, לא לכל מסמך. לפני בחירת גרעין הוא מחשב ציון פירוט לומיננס מנורמל מרשת דגימה חסומה: הצעדים האופקי והאנכי הם (Width + 63) div 64 ו־(Height + 63) div 64, כך שסריקה של 12000 פיקסלים ותמונה ממוזערת של 300 פיקסלים עולים שניהם בערך אותה סריקה של 64 על 64. בכל מיקום דגימה הוא מסכם את ההפרש המוחלט אל השכן מימין ואל השכן מלמטה, על פני עד שלושה ערוצים, ואז מחלק במספר הדגימות כפול 255. הציון נופל בטווח 0 עד 1, כאשר גרפיקה עסקית שטוחה יושבת ליד האפס ומרקם צילומי צפוף מטפס
סולם הבחירה רץ אז בסדר קבוע. אם ResampleFilter הוא כל דבר פרט ל־pirfAdaptive, המסנן ההוא משמש כמות שהוא. אחרת: תוכן בן סיבית אחת מקבל pirfBilevel; ContentClass של piccLineArt מקבל pirfBox; גורם קנה מידה של 4 ומעלה מקבל גם הוא pirfBox, כי בהקטנה כזאת ממוצע שטח הוא גם התשובה הזולה ביותר וגם הנכונה ביותר; piccPhoto, ציון פירוט של 0.08 ומעלה, או PreferredQuality של 0.9 ומעלה מקבלים pirfLanczos עם הגרעין התלת־אונתי שלו; קנה מידה של 2 ומעלה או איכות של 0.7 ומעלה מקבלים pirfBicubic ברדיוס 2; כל השאר מקבל pirfBilinear. מכיוון ש־TPdfImageOptimizeOptions.Default קובע את PreferredQuality ל־0.85, הרצה בברירת מחדל לעולם לא חוזרת ל־bilinear אלא אם ההקטנה עדינה והתוכן שטוח
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// ברירות מחדל: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, איכות 0.85, תקציב 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
תמונה נוגעת רק כאשר הגדול מבין ה־DPI של המיקום האופקי והאנכי שלה, מחולק ב־TargetDpi, מגיע אל MinDpiRatio. השומר הזה קיים כדי שתמונה של 160 DPI המכוונת ליעד של 150 DPI לא תקודד מחדש עבור רווח של שישה אחוזים שעולה דור אחד של איכות. תמונות מתחת ל־MinDimension בכל ציר שהוא, 8 כברירת מחדל, מדולגות בתור אייקונים או קווים
למה לוגואים שקופים מקבלים שוליים לבנים?
כי הצבע מתחת לפיקסל שקוף לחלוטין שרירותי, וממוצע משוקלל פשוט נותן לו זכות הצבעה. מייצאים לוגו מכלי עיצוב והשוליים הבלתי נראים הם לעיתים קרובות לבנים, או שחורים, או כל מה שהיה על הקנבס; ערוץ האלפא מסתיר אותם, וסכום ישר על טביעת הרגל של הגרעין מערבב אותם במהירות בחזרה אל הקצה הנראה. PDFiumPas נמנע מזה בכך שהוא צובר דגימות BGRA בצורה טרומית (premultiplied) ומבטל את ההכפלה הטרומית רק בפיקסל היעד
באופן קונקרטי, כל דגימה תורמת מוסיפה channel * alpha * weight אל צובר הצבע, alpha * weight אל צובר האלפא, ו־weight אל סכום המשקלים. אז צבע היעד מחולק בצובר האלפא ולא בסכום המשקלים, וזה הצעד שחשוב: חלוקה בסכום המשקלים הייתה גוררת את הצבע אל הפיקסלים הבלתי נראים, ואילו חלוקה באלפא הצבורה משחזרת את הצבע שהדגימות הנראות באמת הסכימו עליו. האלפא של היעד היא גודל נפרד, 255 * AlphaSum / WeightSum. פורמטים ללא אלפא מחלקים בסכום המשקלים כרגיל, בית הריפוד של יעד FPDFBitmap_BGRx נכתב כקבוע 255, וכל ערוץ נחתך לטווח 0 עד 255 לפני שמירתו. האלפא הזאת בדרך כלל מקורה ברשומת מסכה רכה במילון התמונה (ISO 32000-1 §11.4), ש־PDFium כבר הרכיב אל חוצץ ה־BGRA שהמדגם המחדש מקבל
// הצורה של לולאת הצבירה הפנימית, לכל דגימת מקור תורמת
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... ובפיקסל היעד, ביטול ההכפלה הטרומית מול סכום האלפא
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
לשמור אמנות קו בת 1 סיבית מחוץ לאזור האפור
כל גרעין רציף שמיושם על סריקה דו־מפלסית מייצר אפור, ואפור הוא בדיוק מה שתמונה בסגנון פקס אסור לה להכיל. לכן PDFiumPas משאיר תמונות בנות סיבית אחת במנוחה כברירת מחדל: PreserveBilevel הוא True ב־TPdfImageOptimizeOptions.Default, ותמונות כאלה נוחתות ב־SkippedCount ללא פגע. מגדירים אותו ל־False ונתיב ה־pirfBilevel נכנס לפעולה במקום גרעין מחליק. הוא הולך על מלבן המקור המדויק שמכסה כל פיקסל יעד, ממצע לומיננס עם המשקלים 0.114, 0.587 ו־0.299 בסדר הזיכרון BGR, וחותך את התוצאה בסף 127.5 ל־0 או 255 שטוחים. לא ניתן לכתוב שום דבר ביניים, ולכן הקצוות נשארים חדים ולא נוצרת הילה אפורה סביב קווים דקים; ערוץ האלפא של מקור BGRA ממוצע כרגיל, ויעד BGRx מקבל את הקבוע 255. אם אתם צריכים את הפיקסלים עצמם ולא מסמך קטן יותר, חילוץ תמונות ממסמכי PDF הוא הנתיב הנפרד
מה קורה כשתמונה חורגת מתקציב זיכרון העבודה?
היא נשארת בדיוק כפי שהייתה, והיא נספרת. MaxWorkingBytes ברירת המחדל שלו 64 MiB והוא נאכף פעמיים. לפני יצירת מפת הסיביות של היעד, PDFiumPas דוחה את התמונה אם רוחב כפול גובה כפול בתים לפיקסל חורג מהתקציב. אחרי ש־FPDFBitmap_CreateEx מצליח הוא בודק שוב לפי המכפלה האמיתית של המרווח (stride) בגובה, כי ריפוד שורות יכול לדחוף הקצאה מעבר למגבלה שאותה מכפלה נאיבית עברה. כל דחייה הורסת את היעד ולא מחזירה דבר. היו ברורים לגבי הידרדרות שזה מרמז עליה: תמונה שחורגת מהתקציב אינה נדגמת מחדש באיכות נמוכה יותר, והיא אינה מפוצלת לאריחים. המקור נשאר במסמך, BudgetExceededCount ו־SkippedCount עולים שניהם, ולכן הרצה יכולה לדווח על הצלחה בזמן שהמסמך אופטמו רק חלקית. זו התנהגות fail-safe מכוונת, אבל המשמעות היא שהדוח אינו קריאה אופציונלית. קיים גם מצב כשל נפרד: תמונות שאת המפה הסיבית שלהן PDFium לא מסוגל לייצר כלל, כמו CMYK, JPX, JBIG2 או מקורות ממוסכים, מעלות את FailedCount במקום וגם נשארות ללא פגע
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // הצבעת שטח דו-מפלסית
Options.ContentClass := piccPhoto; // כפיית Lanczos לקבוצות צילומים
Options.MaxWorkingBytes := 256 * 1024 * 1024; // מרווח לסריקות גדולות
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
לקרוא את הדוח לפני שמשחררים את הקובץ
TPdfImageOptimizeReport נבנה כדי לאבחן אותו, לא רק כדי לרשום אותו. לצד OptimizedCount, SkippedCount ו־FailedCount הוא חושף מונה אחד לכל גרעין, כך ש־BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount ו־BilevelFilterCount מספרים לכם למה הכלל האדפטיבי הגיע באמת לגבי אוסף התמונות שלכם. תוצאה שכולה box פירושה שההקטנות היו תלולות או שהתוכן סווג כאמנות קו; תוצאה שכולה Lanczos על מסמך שהאמנתם שהוא אמנות קו היא סימן שכדאי לקבוע את ContentClass במפורש. AverageDetailScore הוא המספר להשוואה מול סף ה־0.08 של Lanczos בכיול PreferredQuality, ו־PeakWorkingBytes מראה כמה מ־MaxWorkingBytes ההרצה באמת צרכה. אפשרויות לא תקפות נכשלות בקול ולא בשקט: TargetDpi שאינו חיובי, MinDpiRatio מתחת ל־1, PreferredQuality מחוץ לטווח 0 עד 1, או MaxWorkingBytes שאינו חיובי מרימים EPdfError לפני שעמוד כלשהו נוגע. וכן, OptimizeImages עורך רק את המסמך בזיכרון; כל עמוד ששונה מחויב עם FPDFPage_GenerateContent, ואחרי זה אתם עדיין קוראים בעצמכם ל־SaveAs. כדי לראות בעין מה השתנה, הציגו את המסמכים לפני ואחרי כמפות סיביות כמתואר בהמרת עמודי PDF לתמונות JPEG והשוו אותם בזום מלא
דגימה מחדש אדפטיבית היא מאותן תכונות שבלתי נראות כשהן עובדות ומייצרות פניות תמיכה כשהן לא, ולכן המדידה, הטיפול באלפא ותקציב הזיכרון היו חייבים לנחות יחד ולא כשלושה שיפורים נפרדים. אם אתם שוקלים את זה למוצר ב־Delphi, C++Builder או Lazarus, כל משטח ה־API ופרטי הרישוי נמצאים בעמוד המוצר PDFiumPas Delphi PDFium Component