אם אי פעם ניסיתם לשלוף טקסט מודגש (bold) או נטוי (italic) מתוך PDF, סביר להניח שהתחלתם עם regex. זה נראה כמו הצעד המתבקש. לחפש את המילה "Bold" בשם הפונט, לסמן את הטקסט, ולהמשיך הלאה. האסטרטגיה הזו עובדת בדיוק מספיק זמן כדי לתת לכם ביטחון שווא. ואז מישהו מייצא את אותו המסמך מגרסה אחרת של Acrobat, או מ-LibreOffice, או מדרייבר של "הדפסה ל-PDF", וכל ההנחות שכתבתם בקוד קורסות.
למה שמות פונטים משקרים
PDF.js יגיש לכם מחרוזות כמו ABCDEF+TimesNewRomanPS-BoldMT. ששת התווים הראשונים הם תחילית (prefix) אקראית שמוזרקת במהלך הייצוא, והם משתנים בכל פעם שהקובץ נוצר מחדש. להמר על ה-parser שלכם על התחילית הזו זה להמר על רעש. יצואנים (exporters) אחרים אפילו פחות מועילים. חלקם מוציאים מזהים פשוטים כמו Font12 או F1. התוויות הללו אינן נושאות שום משמעות סמנטית; הן תגיות משאבים פנימיות שפשוט היו בקרבת מקום כשנכתב הקובץ.
מכיוון שפורמט ה-Portable Document Format לא תוכנן מעולם כדי להקל על חילוץ טקסט בהמשך התהליך, שמות פונטים הם פשוט הפניות למשאבים מוטמעים או לתתי-קבוצות (subsets). הם מעולם לא נועדו לשמש כ-API יציב לזיהוי סגנון. כשאתם כותבים regex שמחפש את תת-המחרוזת Bold או Italic, אתם מגרדים תווית שאפליקציית היוצר הייתה חופשייה לעצב כרצונה. אתם לא קוראים את המאפיינים הטיפוגרפיים האמיתיים. אתם קוראים מוסכמת שיום קבצים, ומוסכמות אינן חוזים.
קראו את ה-Descriptor, לא את התווית
האמת המוחלטת נמצאת במקום אחר. ב-PDF.js, כל אובייקט דף חושף את commonObjs, מפה שמחזיקה את ה-font descriptors האמיתיים הדרושים לרינדור הדף. כאשר הספרייה מנתחת דף, היא ממלאת את המפה הזו באובייקטי פונט אמיתיים. האובייקטים הללו חושפים מאפיינים בוליאניים עבור bold ו-italic. הבוליאנים הללו אינם מוסקים ממחרוזת השם. הם נובעים מה-font descriptor המוטמע בתוך ה-PDF, ונגזרים ממדדי glyph, דגלי טבלת OS/2, וה-symbolic descriptors שהדפוס כתב לתוך המסמך.
זה אומר שאפשר להפסיק לנחש. לפני שאתם עוברים על פריטי הטקסט בדף, צרו fontStyleMap על ידי מעבר על page.commonObjs. עבור כל font ID, שמרו אובייקט המתעד את מאפייני ה-.bold וה-.italic האמיתיים המסופקים על ידי אובייקט הפונט. מאוחר יותר, כשאתם מעבדים כל פריט טקסט, חפשו את הפנייה לפונט שלו במפה שלכם וקראו את הדגלים שחושבו מראש. פתאום יש לכם תשובה דטרמיניסטית שאינה משתנה בין ייצואים.
אתם יכולים לשמור מנגנון גיבוי (fallback). אם ה-descriptor חסר איכשהו או חלקי, נקו את שם הפונט – הסירו את התחילית, השליכו את התגיות האקראיות, והריצו regex שמרני על מה שנותר. אך זה צריך להיות מוצא אחרון, לא הלוגיקה העיקרית שלכם. ההבדל באמינות הוא דרמטי. בעוד שפענוח מבוסס שם נשבר בין יצואנים שונים, פענוח מבוסס descriptor נשאר יציב כי הוא שואל את הקובץ מה הוא באמת מכיל.
כשגם הפונט משקר: סגנונות סינתטיים
אפילו ה-descriptor יכול לפספס. חלק מייצאי ה-PDF לא טורחים להטמיע גופן נטוי (italic) נפרד. במקום זאת, הם לוקחים את הפונט הרומאני הזקוף ומטים אותו באמצעות מטריצת טרנספורמציה (transform matrix). זה נפוץ בקבצים שנוצרו על ידי כלי עיצוב או מעבדי תמלילים ישנים המעדיפים גודל קובץ על פני טוהר טיפוגרפי.
כל פריט טקסט ב-PDF.js נושא מערך transform, מטריצת affine בעלת שישה איברים הממפה את מערכת קואורדינטות ה-glyph למערכת קואורדינטות הדף. האיבר השלישי במערך הזה שולט ב-horizontal shear (גזירה אופקית). כאשר הערך הזה אינו אפס, הטקסט מוטה מכנית על ידי הרינדרר. אם תסמכו רק על ה-font descriptor, תסווגו את הטקסט הזה כרומאני זקוף. אם תבדקו את המטריצה, תתפסו את ה-italic הסינתטי ותסמנו אותו נכון. אותה לוגיקה חלה על bold סינתטי שנוצר על ידי הדפסה כפולה (overprinting), אם כי זה קשה יותר לזיהוי מתוך גיאומטריה בלבד. עבור טקסט מוטה, ערך ה-shear הוא ה"אקדח המעשן" שלכם.
קווים תחתונים מצוירים, לא מוצהרים
bold ו-italic הם מאפייני פונט. underline אינו כזה. ב-PDF, קו תחתון הוא נתיב וקטורי (vector path). הרינדרר מנפיק פקודת ציור עבור מקטע אופקי דק הממוקם ליד קו הבסיס (baseline) של הטקסט. זהו אלמנט גרפי שפשוט נמצא מת
ההבחנה הזו חשובה מכיוון שקריאת תיאורי גופן (font descriptors) לא תחשוף קו תחתי. עליך לבחון את אופרטורי הציור הגולמיים (raw drawing operators) בדף, או את הפלט ברמת הגיאומטריה, כדי למצוא מקטעי קווים אופקיים קצרים המקבילים לקו הבסיס (baseline) במרחק המתאים. כאשר מנוע החילוץ שלך מזהה מקטע כזה מתחת לרצף טקסט, אתה מסמן את הרצף הזה כקו תחתי. התייחסות לכך כשכבת זיהוי נפרדת שומרת על מודל הנתונים שלך אמין: bold ו-italic הם תכונות מובנות של הגופן, בעוד שקו תחתי (underline) הוא עיטור חיצוני המרונדר על ידי המסמך.
תהליך עבודה (Pipeline) פעיל
מערכת חילוץ נקייה מפרידה בין תחומי אחריות לשכבות נפרדות. ראשית, "עובד גיאומטריה" (geometry worker) עובר על הדף. הוא מבצע שאילתה ל-page.commonObjs כדי להרכיב את ה-fontStyleMap שלך, בוחן את מערך ה-transform של כל פריט טקסט כדי לתפוס כתב נטוי סינתטי (synthetic italics), וסורק נתיבי וקטור (vector paths) סמוכים כדי לאתר קווים תחתונים. הפלט שלו הוא מבנה ביניים נקי, נקרא לו textMeta, שבו כל רצף טקסט נושא שלושה ערכים בוליאניים פשוטים: bold, italic, ו-underline.
לאחר מכן, "בונה טקסט מחדש" (text rebuilder) צורך את המבנה הזה ומוציא פלט עם סימון (markup). סדר הקינון (nesting) חשוב כאן. ההיררכיה הנכונה מציבה את הקו התחתון בשכבה החיצונית ביותר, לאחר מכן את הנטוי, ואז את המודגש בשכבה הפנימית ביותר. המשמעות היא שרצף מעוצב במלואו יהפוך ל-<u><i><b>text</b></i></u>. סדר זה מונע חפיפות HTML לא תקינות ושומר על רינדור עקבי בדפדפנים ובממירים של מסמכים. הוא גם משקף את הלוגיקה הטיפוגרפית: עיטור עוטף דגש סמנטי, ודגש סמנטי עוטף משקל מבני.
העוצמה של גישה זו היא שהיא משתמשת רק במה שה-PDF כבר יודע על עצמו. אין כאן שימוש ב-OCR, בשירות ראייה מבוסס ענן (cloud vision service), ובמודל למידת מכונה המנחש סגנונות מפיקסלים רסטריים. אתה קורא את השכבה הסמנטית של הקובץ עצמו, החושפת דרך הגיאומטריה והמטא-דאטה שיישום היוצר כבר חישב. התוצאה היא מהירה, דטרמיניסטית ומדויקת לאורך הנוף הכאוטי של מחוללי PDF.
השורה התחתונה
שימוש ב-Regex נגד שמות גופנים הוא מלכודת. זה מרגיש כמו קיצור דרך כי זה עובד על הקובץ היחיד שבדקת, אבל זה קורס תחת הלחץ הקל של ייצוא שני. המידע האמיתי כבר נמצא בתוך ה-PDF, טמון ב-descriptors, במטריצות ובנתיבי וקטור. בנה את ה-pipeline שלך סביב העובדות הללו. שאל את אובייקט הגופן אם הוא bold. בדוק את מטריצת ה-transform עבור shear. חפש מקטעי קווים מצוירים עבור underlines. אם תבצע שאילתה על ההנדסה של המסמך עצמו במקום לגרד את התוויות השטחיות שלו, תקבל סגנונות שישרדו ממחולל PDF אחד למשנהו.
