מאמר טכני

Identity Tm בזרמי תוכן של PDF: הסרת peephole בטוחה

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 ומטריצת הטקסט של זרם התוכן, זו אותה הבחנה בין הכפלת מצב לבין החלפתו

PDFlibPas מתייחס אחרת אל 1 0 0 1 0 0 cm ואל 1 0 0 1 0 0 Tm: ה-cm מכפיל את ה-CTM מימין והוא no-op בכל מקום, בזמן שה-Tm מחליף את ה-Tm וה-Tlm מקצה לקצה, וכל Tj מקדם את ה-Tm ברוחב שצייר, ולכן Tm identity אחרי טקסט מוצג הוא איפוס אמיתי
מחולל הדוחות סמך על האיפוס הזה: מחיקת ה-Tm ה-identity ציירה את Total מיד אחרי Invoice על אותו baseline, ושום דבר לא נכשל, נרשם או הזהיר בדרך אל המדפסת של הלקוח

איך הסריקה האחורית מחליטה איזה 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
ה-RemoveIdentityMatrices של PDFlibPas הולך אחורה מכל Tm identity: את Tf, Tc, קובעי הצבע, gs ו-cm עוקפים, בזמן ש-Td, TD, T*, Tm לא-identity, Tj, TJ, אופרטור לא מוכר או ET עוצרים את הסריקה ושומרים את ה-Tm, ו-BT מעיד לטובת השלתו
גם Tm identity קודם עוצר את הסריקה, כי שמור או כבר מתוזמן למחיקה הוא השאיר את שתי המטריצות ב-identity — בכל מקרה האופטימייזר לעולם לא מזיז glyph

המקרים השמרניים מכוונים. אופרטור לא מוכר יכול להיות כל דבר, ולכן הסריקה מסרבת להסיק מעבר לו. ה-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 של פונטים

PDFlibPas מריץ את ה-peephole optimizer רק בתוך מעבר הדחיסה בזמן השמירה: ה-TPDFPageTree.Compress מכבד את SetOptimizeContentStreams, זרם שכבר מסונן ב-/FlateDecode נדלג לגמרי, זרם שלא ניתן לפרסור נדחס עם הבייטים המקוריים שלו ללא שינוי, וה-NormalizeContentStreams לעולם לא קורא לאופטימייזר בכלל
זרמים דחוסים שנדלגו הם החלק השקט: טענו PDF קיים, שמרו אותו שוב, והאופרטורים שלו יוצאים ללא מגע כי האופטימייזר כותב מחדש רק זרמים שפענח קודם
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 לכיוונון איך כל מסמך נכתב