PDF Library for Delphi מסירה אופרטור מטריצת טקסט identity, 1 0 0 1 0 0 Tm, במהלך אופטימיזציית ה-peephole של זרם התוכן בזמן השמירה רק כשמטריצת הטקסט ומטריצת שורת הטקסט כבר identity: מיד אחרי BT, או מיד אחרי Tm identity קודם. cm identity עדיין תמיד נזרק, כי cm מכפיל את ה-CTM בזמן ש-Tm מחליף את שתי מטריצות הטקסט מקצה לקצה. מאז v3.539.28 כל Tm identity אחר נשאר בזרם
הבאג שזה מתקן הוא מהסוג השקט. מחולל דוחות פולט BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET וסומך על ה-Tm ה-identity כדי לשלוח את המחרוזת השנייה חזרה אל מקור מרחב הטקסט לפני שהוא מיישם את לוגיקת המיקום שלו. האופטימייזר הישן ראה שישה מספרים שמאייתים את המטריצה ה-identity, החליט שהאופרטור לא יכול בשום אופן לשנות משהו, ומחק אותו. שום דבר לא נכשל, שום דבר לא רשם אזהרה, והעמוד הנשמר צייר את "Total" מיד אחרי "Invoice" על אותו baseline, וזו בדיוק המחלקה של פגמים שאף אחד לא מבחין בה עד שלקוח מדפיס את ה-PDF
למה 1 0 0 1 0 0 Tm לא תמיד no-op?
Tm identity הוא no-op רק כשהוא יחליף שתי מטריצות שכבר מחזיקות את ה-identity, וזו תכונה של האופרטורים שלפניו, לא של האופרנדים שלו עצמם. ISO 32000-1 §9.4.1 אומר ש-BT מאתחל את מטריצת הטקסט (Tm) ואת מטריצת שורת הטקסט (Tlm) שתיהן ל-identity, ו-§9.4.2 מגדיר את Tm כקובע את שתיהן לערכים הנתונים, לא כמכפיל עליהן. השוו זאת ל-cm (§8.4.4), שמכפיל מימין את מטריצת הטרנספורמציה הנוכחית: הכפלה ב-identity לא משנה שום CTM, ולכן 1 0 0 1 0 0 cm בטוח למחיקה בכל מקום. בתוך אובייקט טקסט התמונה שונה. Td, TD, T* ו-Tm לא-identity כולם מזיזים את ה-Tlm, וכל אופרטור מציג-טקסט (Tj, TJ, ', ") מקדם את ה-Tm ברוחב ה-glyph-ים שצייר. אחרי כל אחד מאלה, Tm identity הוא איפוס אמיתי אל המקור. אם אי-פעם עקבתם אחר מיקומי טקסט ביד עם עוקב מצב ה-CTM ומטריצת הטקסט של זרם התוכן, זו אותה הבחנה בין הכפלת מצב לבין החלפתו
איך הסריקה האחורית מחליטה איזה Tm identity להשיל
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices הולך עכשיו אחורה מכל Tm identity ומשיל אותו רק אם הסריקה מגיעה קודם אל BT או אל Tm identity אחר. ה-Tm ה-identity הקודם סופר בין אם הוא נשמר ובין אם הוא עצמו רק תוזמן למחיקה, כי בשני המקרים הוא השאיר את שתי המטריצות ב-identity, בדיוק כמו ש-BT עושה. הכלל ממיין כל אופרטור שהוא עלול לפגוש לאחת משתי קבוצות:
- עוצרים ושומרים את ה-Tm:
Td,TD,T*,Tmלא-identity,Tj,TJ,',",ET, כל אופרטור שהפרסר אינו מכיר, או תחילת הזרם - עוקפים וממשיכים לסרוק: אופרטורים שלעולם לא נוגעים ב-Tm או ב-Tlm, כמו
Tf,Tc, קובעי צבע,gs, אופרטורי marked-content ו-cm
המקרים השמרניים מכוונים. אופרטור לא מוכר יכול להיות כל דבר, ולכן הסריקה מסרבת להסיק מעבר לו. ה-ET סוגר את אובייקט הטקסט, ולכן ל-Tm אחריו אין BT שמעיד לטובת ערכי המטריצה. הסריקה גם עובדת על זרם תוכן אחד בכל פעם, וזה חשוב לעמודים שה-/Contents שלהם מערך: שכבה שמתחילה באמצע אובייקט טקסט, בלי BT משלה, שומרת על ה-Tm ה-identity שלה גם כשהשכבה הקודמת הייתה הופכת אותו למיותר. זה עולה כמה בייטים על קבצים מוזרים ולעולם לא מזיז glyph. אם אתם עורכים טקסט עמוד ברמת ההוראה, כמו בהליכת מיפוי תו-אל-בייט-תוכן, אותו מודל TPDFContentProgram המפורסר הוא מה שהאופטימייזר כותב מחדש
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // זרם פגום: משאירים את הבייטים במנוחה
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // מחזיר את מספר ההוראות שהוסרו
finally
Optimizer.Free;
end;
Result := Prog.Emit; // הוראה אחת לכל שורה
finally
Prog.Free;
end;
end;
// הוסר: Tm ישירות אחרי BT, השני מתוך שני identity Tm ברצף
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// נשמר: Tm אחרי Td, אחרי Tj, אחרי Tm לא-identity, או מחוץ ל-BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
הריצו את ה-helper על זרם החשבונית מהפתיחה וה-Tm ה-identity שורד, כי הסריקה האחורית פוגעת ב-Tj לפני שהיא מגיעה ל-BT. שימו /F1 12 Tf, 2 Tc ו-0 g בין ה-BT ל-Tm ה-identity והוא עדיין הולך, כי אף אחד מאלה לא נוגע במטריצות הטקסט. רצף כמו BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm מאבד בדיוק אופרטור אחד: ה-Tm ה-identity הראשון מאפס את המטריצה שה-Td הזיז, ורק השני הוא מיותר
מתי ה-peephole optimizer באמת רץ?
האופטימייזר רץ רק במהלך מעבר הדחיסה, בתוך TPDFPageTree.Compress, ורק על זרמי תוכן שאינם כבר דחוסי Flate. TPDFlib.SetOptimizeContentStreams(1) הוא ברירת המחדל, ואותו מתג חשוף גם כשדה OptimizeContentStreams של TPDFlibSaveOptions; גם CompressContent וגם CompressPage מכבדים אותו. זרם שה-/Filter שלו כבר /FlateDecode נדלג לגמרי, כך שטעינת PDF דחוס קיים ושמירתו שוב לא כותבת מחדש את האופרטורים שלו. אם הזרם נכשל בפרסור, הבייטים המפוענחים המקוריים נדחסים בלי שינוי. TPDFlib.NormalizeContentStreams מפרסר ופולט מחדש תוכן עם רווחים ומספרים קנוניים אבל לעולם לא קורא לאופטימייזר, מה שהופך אותו ל-baseline שימושי כשרוצים לראות כמה מהפרש הגודל כללי ה-peephole תורמים, לצד הרווחים הגדולים יותר המכוסים באופטימיזציה של גודל קובץ PDF עם subsetting של פונטים
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// זרמים לא דחוסים עוברים את כללי ה-peephole, ואז Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// אותה בחירה דרך אפשרויות השמירה המצורפות; False מכבה את זה
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
מה בדיקת הרגרסיה הישנה באמת הבטיחה?
בדיקת הרגרסיה הישנה הבטיחה צורה אחת בלבד: Tm identity ישירות אחרי BT מוסר. ה-Peephole_RemovesIdentityTextMatrix מזין BT 1 0 0 1 0 0 Tm (hello) Tj ET אל האופטימייזר ומצהיר שלא נותר שום Tm. מהדורה מוקדמת כבר ציינה שהשלת Tm identity אינה בטוחה כשה-Tlm אינו identity, ובכל זאת שמרה על ההתנהגות כי הבדיקה "נעלה" אותה. בקריאה חוזרת מרוכזת, הבדיקה לא אומרת דבר על Tm identity אחרי Td או אחרי טקסט מוצג; להתייחס אל הכיסוי של דוגמה אחת כאל החוזה של הכלל כולו היה הטעות האמיתית. התיקון משאיר את המקרה המקורי עובר ומוסיף שישה מקרים שנועצים גם בצורות הניתנות להסרה וגם באלה שנשמרות, כולל Tm מחוץ לכל אובייקט טקסט ואחד שבא אחרי ET
הפשרה קלה לקבל ברגע שכותבים אותה על הדף. מחוללים שעוטפים כל אובייקט טקסט כ-BT 1 0 0 1 0 0 Tm ... עדיין מקבלים את האופרטור המיותר הזה מוסר, ושם הגיע כמעט כל החיסכון. מה שהאופטימייזר מוותר עליו הוא ה-Tm ה-identity המזדמן באמצע אובייקט טקסט, כמה בייטים לעמוד לפני ש-Flate בכלל רואה אותם, בתמורה לערובה שכותרת המודול אומרת במפורש: כל טרנספורמציה שקולה בפלט ולעולם לא משנה את העמוד הנראה. אופטימייזר גודל שמזיז טקסט אינו אופטימייזר, הוא באג רינדור עם יחסי דחיסה טובים
פרסר זרם התוכן, ה-peephole optimizer ואפשרויות הדחיסה בזמן השמירה המתוארים כאן כולם מגיעים בPDF Library for Delphi and C++Builder, שחושפת גם את NormalizeContentStreams, CompressContent ו-TPDFlibSaveOptions לכיוונון איך כל מסמך נכתב