מרנדר העמודים של רכיב HotPDF ל-Delphi מקדם כעת טקסט על ידי חישוב ההזחה של כל glyph במרחב טקסט, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th כפי ש-ISO 32000-1 §9.4.4 מגדיר, ואז הזזת מטריצת הטקסט דרך החלק הליניארי שלה עם HPDFTranslateTextMatrix. החיתוך נשמר לכל מסגרת q ומשוחזר ב-Q, אבל אזור GDI נלכד רק כשהמסגרת הזאת באמת משנה את ה-clip. שני התיקונים נחתו ב-HotPDF 2.754.0, ושניהם הגיעו מעמודים מהעולם האמיתי שרנדרו עם מילים מכווצות או אזורי clip שדלפו מעבר ל-Q שלהם. הבאג הראשון הוא אריתמטיקה שנראית נכונה עד שמפיק כותב את גודל הפונט שלו אל המטריצה. השני הוא תיקון נכונות שכמעט עלה לנו בהאצת הרנדור המקבילי, והדרך שבה החזרנו את המהירות שווה ידיעה אם אתם כותבים מכשיר PDF כלשהו שנשען על GDI
למה טקסט מתכווץ לגוש כש-PDF משתמש ב-Tf 1?
כי הקוד הישן של ההתקדמות הוסיף מרחק במרחב טקסט ישר אל רכיב ההזזה של Tm, כאילו מרחב הטקסט ומרחב המשתמש תמיד באותו קנה מידה. שפע מפיקים אמיתיים מגדירים את גודל הפונט ל-1 עם Tf ונושאים את הגודל האמיתי במטריצת הטקסט. עם /F1 1 Tf ו-12 0 0 12 72 700 Tm, glyph ברוחב 500 יחידות מתקדם 0.5 במרחב טקסט, שהם 6 נקודות על העמוד ברגע שה-Tm מכפיל אותו. המרנדר הישן ביצע Tm.e := Tm.e + Adv והזיז את העט 0.5 נקודות. כל glyph נחת שתים-עשרית של תו אחרי הקודם, ולכן שורת טקסט רגילה רונדרה כמשיכה כהה בשוליים השמאליים בזמן שאותו קובץ נראה מושלם בכל מציג אחר
// זרם תוכן ממפיק שקודד את הגודל ב-Tm ולא ב-Tf:
// BT
// /F1 1 Tf
// 12 0 0 12 72 700 Tm
// [(Hel) 30 (lo) -250 (world)] TJ
// ET
// התקדמות ישנה (מפושט): מרחק שנוסף אל Tm.e כאילו היה מרחב משתמש
Adv := W * FontSize / 1000; // 0.5 עבור glyph בן 500 יחידות
if (HorizScale <> 0) and (HorizScale <> 100) then
Adv := Adv * HorizScale / 100; // Th על הרוחב בלבד
Adv := Adv + CharSpace; // Tc לא מוכפל ב-Th
if Code = 32 then
Adv := Adv + WordSpace * FontSize / 1000; // Tw מוכפל בטעות ב-Tfs
Tm.e := Tm.e + Adv; // מתעלם מ-Tm.a, Tm.b, Tm.c, Tm.d
// התאמת TJ ישנה: בלי Th, ושוב רק Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;
הקיצור של ה-Tm.e לא היה הליקוי היחיד באותו בלוק. ריווח מילים Tw מבוטא ביחידות מרחב טקסט לא מוכפלות, אבל הקוד הישן כפל אותו ב-FontSize / 1000, כך שתחת Tf 12 שורה מיושרת איבדה כמעט את כל הרווח שבין המילים שלה. קנה המידה האופקי Th חל על רוחב ה-glyph אבל לא על Tc או Tw, והתאמת ה-kerning של ה-TJ דילגה עליו לגמרי. הנתיב שאינו מצייר שמקדם טקסט בלתי נראה במצב רינדור 3 — הסוג ששכבות טקסט OCR משתמשות בו — וטקסט בתוך optional content נסתר נשאו עותק פרטי של אותה אריתמטיקה, כך שכל דבר שצויר אחרי ריצה בלתי נראית התחיל ממיקום שגוי. באגי מצב טקסט במרנדר לעיתים נדירות נכשלים בקול רם: כמו באגי אינדקס האופרנד ושם המשאב שאיפסו פעם את Tc, Tw ו-Tz בלי שגיאה אחת, אלה הניבו עמודים משכנעים על הפלט של הספרייה עצמה ונשברו רק על קבצים של מפיקים אחרים
איך ISO 32000-1 §9.4.4 מגדיר את התקדמות ה-glyph?
ISO 32000-1 §9.4.4 מגדיר את ההתקדמות לגמרי במרחב טקסט ומחיל אותה על מטריצת הטקסט כמטריצת הזזה, ולכן התשובה היא לחשב את tx קודם ולתת ל-Tm לעשות את קנה המידה, הסיבוב וההטיה. לכתיבה אופקית, tx שווה ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, כאשר w0 הוא רוחב ה-glyph באלפיות של em, Tj הוא ההתאמה של ה-TJ, ו-Th הוא Tz חלקי 100. ה-Tm החדש הוא [1 0 0 1 tx 0] × Tm, שב-HotPDF הוא ה-helper HPDFTranslateTextMatrix: הוא מוסיף X ו-Y דרך מקדמי המטריצה a, b, c ו-d במקום לכתוב ישירות אל e ו-f. לפי §9.3.3, ה-Tw חל רק על קוד התו החד-בייתי 32, ולכן קודי CID מרובי בתים לעולם לא קולטים ריווח מילים בנתיב האופקי. אותו helper מניע עכשיו את Td, TD, T*, האופרטורים ' ו-", התאמות TJ ואת נתיב הטקסט הנסתר, מה שאומר שפונקציה אחת מחזיקה בכלל
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;
// התקדמות glyph אופקית, ISO 32000-1 9.4.4
W := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
Adv := Adv + State.Text.WordSpace; // Tw במרחב טקסט, לא מוכפל
Adv := Adv * State.Text.HorizScale / 100; // Th חל על הסכום כולו
HPDFTranslateTextMatrix(Tm, Adv, 0);
// אלמנט מספר של TJ: אותו מרחב, אותו Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);
מיקום ה-glyph היה צריך לעקוב אחר אותה לוגיקה. כשאין קו מתאר מוטמע זמין והמרנדר חוזר אל TextOutW של GDI, הוא כעת בונה את מטריצת ה-glyph המלאה מתוך CTM × Tm × rise × קנה מידה של em, כולל ה-Th, ומתקין אותה עם SetWorldTransform במצב GM_ADVANCED בתוך זוג SaveDC / RestoreDC. פונט ה-GDI נוצר בגובה קבוע של 1000 יחידות והטרנספורמציה עושה את התמחור, כך שטקסט מסובב ומוטה שומר על הכיוון שלו במקום להיצייר זקוף בנקודת מקור מוטרנספורמת. מצב כתיבה אנכי הוא אי-הסימטריה המכוונת היחידה: פונט WMode 1 מתקדם במורד ציר ה-y לפי המדד האנכי שלו, וקנה מידה אופקי לא חל על הציר הזה
מה q/Q באמת שומר במצב גרפי של PDF?
ISO 32000-1 §8.4.2 מפרט את נתיב החיתוך הנוכחי כחלק ממצב הגרפיקה, ולכן ה-Q חייב לשחזר את ה-clip בדיוק כפי שהיה ב-q המתאים, ולא רק את הפרמטרים המספריים. HotPDF כבר החזיק מחסנית מצב גרפיקה עם ה-CTM, הצבעים, פרמטרי הקו ומצב הטקסט, אבל GDI שומר את ה-clip בהקשר המכשיר, מחוץ למחסנית הזאת. עותק של המצב המספרי לכן שיחזר הכול חוץ מה-clip, ו-clip שהותקן עם W n בתוך בלוק q ... Q המשיך לקצץ כל פעולה מאוחרת יותר בעמוד. Form XObjects הוסיפו נתיב שני אל אותו כשל, כי §8.10 נותן לטופס save ו-restore סמויים סביב התוכן שלו, ותוכן טפסים אמיתי לפעמים משאיר את ה-q של עצמו לא מאוזנים גם כשהמפרט דורש שיזדווגו. המרנדר קורא עכשיו ל-CaptureClipBeforeChange ול-SaveDC לפני הרצת טופס, ואז אחרי שהטופס מסיים הוא משליך כל אזור שמור עמוק יותר מעומק הכניסה וקורא ל-RestoreDC, כך שלכל HRGN שמור יש בדיוק נתיב שחרור אחד
לכידת clip עצלנית עם THPDFSavedClipState
התיקון שנשלח שומר רשומת THPDFSavedClipState אחת לכל q, אבל דוחה את החלק היקר עד שהמסגרת תשנה לראשונה את ה-clip. הרשומה מחזיקה את handle האזור, את עומק המחסנית שאליו הוא שייך, את הקשר המכשיר שממנו נלקח ודגל Captured. ה-DevPushState ממלא רק את העומק וה-DC ומגדל את מערך המסגרות בהכפלה מ-16, כך שזרם תוכן מלא ב-q 1 0 0 1 x y cm ... Q לא מקצה אף אובייקט GDI בכלל. אופרטורים שעומדים לשנות חיתוך — כלומר ציור נתיב עם W או W* ממתין, האופרטור n, מילויי תבניות וכניסה לטופס — קוראים ל-CaptureClipBeforeChange קודם
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
Index, ClipResult: Integer;
Region: HRGN;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index < 0) or FSavedClips[Index].Captured or
(FSavedClips[Index].StackDepth <> FGSStack.Count) or
(FSavedClips[Index].DC <> FDC) then Exit; // כבר נשמר, או לא שלנו
Region := CreateRectRgn(0, 0, 0, 0);
if Region = 0 then RaiseLastOSError;
ClipResult := GetClipRgn(FDC, Region); // 0 פירושו אין clip בכלל
if ClipResult <= 0 then
begin
DeleteObject(Region);
Region := 0;
if ClipResult < 0 then RaiseLastOSError;
end;
FSavedClips[Index].Region := Region;
FSavedClips[Index].Captured := True;
end;
procedure THPDFPageRenderer.DevPopState;
var
Index: Integer;
begin
ClearSavedClipRegions(FGSStack.Count);
Index := FSavedClipCount - 1;
if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
begin
if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
SelectClipRgn(FDC, FSavedClips[Index].Region); // Region 0 מסיר את ה-clip
if FSavedClips[Index].Region <> 0 then
DeleteObject(FSavedClips[Index].Region);
Dec(FSavedClipCount);
end;
FGSStack.Pop;
end;
העלות הנמדדת של הגרסה הלהוטת היא הסיבה לקיום העיצוב הזה. המימוש הנכון הראשון יצר וקרא אזור GDI בכל q, ועל עמודים שעשויים בעיקר מטרנספורמציות מספריות תהליכוני הרנדור בילו את זמנם בהתחרות על אובייקטי אזור של GDI במקום לרסטר. pipeline הרנדור המקביל צנח מהרווח הצפוי שלו אל בערך 1.13 עד 1.20 מתפוקת תהליכון יחיד ונכשל בשער ההאצה של פי 1.5 בסוויטת הבנצ'מרק. עם לכידה עצלנית וקיבולת מסגרות ממוחזרת, אותו בנצ'מרק עובר שוב את שער הפי 1.5 המקורי. אנטי-אליאסינג ל-glyph-ים של TrueType קטנים נחת באותה מהדורה והיה החשוד הברור, אבל הרגרסיה נעקבה אחורה אל הקצאת אזורים, מה שמזכיר טוב למדוד לפני שמאשימים את התכונה החדשה ביותר
איפה הגבולות של הגישה הזאת?
ה-clip השמור הוא אזור GDI בפיקסלי מכשיר, ולכן הוא מדויק עבור ה-bitmap שרונדר וחסר משמעות עבור כל יעד אחר. זו הסיבה שכל מסגרת רושמת את הקשר המכשיר שלה וה-DevPopState מדלג על השחזור כשה-DC השתנה, למשל בזמן שקבוצת שקיפות מרונדרת אל bitmap שכבה משלה. החזרת אפס מ-GetClipRgn היא תוצאה לגיטימית שפירושה אין clip, ושחזורה עם SelectClipRgn(FDC, 0) הוא מה שמסיר נכון clip שלא התקיים ב-q המתאים. בצד הטקסט, התיקון מתקן לאן כל glyph הולך, אבל הוא לא ממציא רוחבים: אם פונט משמיט את מערך ה-/Widths שלו והתוכנית המוטמעת אינה זמינה, ההתקדמות עדיין טובה ככל fallback הרוחב. כשבודקים רגרסיה באזור הזה, שמרו לפחות fixture אחד עם Tf 1 ו-Tm מוכפל, אחד עם Tz ו-Tw שאינם אפס, ואחד עם clip בתוך q ... Q ואחריו תוכן מחוצה לו, כי אף אחד מאלה לא מופיע במסמכים שהספרייה עצמה מייצרת
אם מניעים את המרנדר מקוד יישום, שום דבר לא משתנה בתבנית הקריאה שמתוארת ברנדור עמוד PDF אל bitmap, ועמודים שהציגו בעבר שורות מוכתמות או תוכן קטום פשוט צריכים לרנדר נכון על 2.754.0 ומעלה. פרטים על הרכיב, גרסאות Delphi ו-C++Builder הנתמכות והרישוי נמצאים בעמוד המוצר של רכיב HotPDF ל-PDF