מאמר טכני

קריאת מאפייני גופנים של PDF עם PDFium Component ב-Delphi

כל תו גלוי ב-PDF נושא הפניה לגופן שצייר אותו, ו-PDFium Component מאפשר לך לעקוב אחר ההפניה הזו חזרה לאובייקט הגופן ולקרוא מה הוא יודע. יחידת הגישה היא התו, לא המסמך: אתה בוחר תו לפי האינדקס שלו בטקסט העמוד ומבקש את שם המשפחה, שם הבסיס, המשקל, זווית הנטוי (italic angle), והאם הגופן (face) שמאחוריו אכן נישא בתוך הקובץ. המאפיין האחרון הזה הוא מה שרוב הניתוחים מחפשים באמת, מכיוון שגופן מוטמע נוסע עם המסמך וגופן שאינו מוטמע הוא הבטחה שבמכונה של הקורא מותקן במקרה אותו גופן

המרכיב (component) חושף את אלה דרך אותם אובייקטי TPdf ו-TPdfView שבהם אתה משתמש עבור רינדור וחילוץ טקסט. אין אובייקט "טבלת גופנים" נפרד לפתוח. ברגע שהטקסט של העמוד נותח, מאפייני הגופן תלויים באינדקס התו, ואתה קורא אותם גליף אחד בכל פעם. עיצוב זה מתאים לאופן שבו PDF שומר את המידע מלכתחילה: עמוד בודד יכול להחליף גופנים עשרות פעמים, והתשובה הכנה היחידה ל"באיזה גופן המסמך הזה" היא "זה תלוי למי מתווים התכוונת."

קריאת הגופן מאחורי תו אחד

הפעולה השימושית הקטנה ביותר היא לקחת אינדקס תו ולפלוט (dump) את כל מה ש-PDFium יכול להגיד לך על הגופן שלו. כל מאפיין גופן ב-TPdf ו-TPdfView מדורג (indexed) לפי מיקום תו, כך שהאינדקס נשזר דרך כולם. העמוד גם חייב להיות העמוד הנוכחי כדי שהאינדקס ייפתר (resolve) מול הטקסט הנכון, וזה חשוב ברגע שאתה עובר מעבר לעמוד אחד

procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Character : ' + Pdf.Character[CharIndex]);
    Report.Add('Family    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Weight    : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Italic    : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
    Report.Add('Size      : ' + FormatFloat('0.0', PtSize) + ' pt');
    Report.Add('Ascent    : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Descent   : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Embedded  : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

כמה מהחתימות מפתיעות אנשים שמגיעים מספריות אחרות. FontAscent ו-FontDescent לוקחים שני ארגומנטים, אינדקס התו וגודל נקודה, כי PDFium מדווח על המדדים הללו ביחידות של מרחב-גליף (glyph-space) שהופכות לפיקסלים רק כשאתה קנה-מידה אותן (scale) לפי הגודל שבו הטקסט הוגדר. העבר את הערך שכבר קראת מ-FontSize[CharIndex] ותקבל טיפוס (ascent) וירידה (descent) באותן נקודות כמו שאר הפריסה. הירידה חוזרת כשלילית, כיוון שהיא נמדדת מתחת לקו הבסיס. שם המשפחה ושם הבסיס הם מחרוזות נפרדות בכוונה: שם הבסיס הוא רשומת ה-/BaseFont הגולמית מה-PDF, ולעתים קרובות נושאת קידומת תת-קבוצה (subset) כמו ABCDEF+, בעוד ששם המשפחה הוא השם המנוקה שאליו המרונדר (renderer) פותר אותו

הפיכת לחיצה לאינדקס תו

בקרא (viewer), נדיר שאתה יודע את האינדקס מראש. המשתמש לוחץ על גליף ואתה צריך לתרגם את קואורדינטת הפיקסל לתו שתחתיו. CharacterIndexAtPos עושה בדיוק את זה, לוקח את מיקום העכבר וסיבולת (tolerance) ומחזיר את אינדקס התו הקרוב ביותר, או ערך שלילי כאשר הלחיצה נחתה על שטח לבן או עמוד ריק

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 פיקסלים של מרווח בכל כיוון כדי שהחטאה-קרובה עדיין תפגע בגליף.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // לחיצה בין גליפים; עזוב את החלונית (panel)

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

שווה לכוונן את הסיבולת. הדוק מדי ומשתמשים מרגישים שהם צריכים לנחות בדיוק על הקו של האות; רופף מדי ולחיצה בשוליים נצמדת (snaps) לתו רחוק שאין לו שום קשר למה שהם התכוונו. שלושה עד חמישה פיקסלים של מכשיר (device pixels) הם נקודת התחלה סבירה לצפייה במסך. האינדקס המוחזר הוא לתוך הטקסט המנותח של העמוד הנוכחי, אותו מרחב אינדקס שכל מאפיין גופן מצפה לו, כך שאתה יכול להעביר אותו ישירות לשגרה שלמעלה. שמירתו ב-CurrentCharIndex היא אופציונלית אך נוחה: התצוגה (view) שומרת את זה כמושג שלה לגבי הגליף הממוקד, שזה שימושי אם חלקים אחרים של ממשק המשתמש רוצים לקרוא את הבחירה מבלי לגזור אותה מחדש

הטמעה היא המאפיין שקובע

עבור רוב העבודה האמיתית, השאלה היחידה ששווה לענות עליה היא האם כל גופן מוטמע. מסמך שכל הגופנים שלו רוכבים בתוכו מרונדר אותו הדבר ב-RIP של לשכת דפוס, במחשב הנייד של עמית, ובשרת ללא ממשק משתמש כלל. מסמך שנשען על Helvetica לא-מוטמע מהמר שבכל אחת מהמכונות הללו מותקן גופן תואם, וכשההימור נכשל הקורא מחליף משהו קרוב, המדדים משתנים, וטופס שסודר בקפידה זורם מחדש (reflows) בדיוק מספיק כדי להישבר. הליכה על טקסט העמוד וקיבוץ גופנים לפי סטטוס הטמעה נותנים לך את התשובה הזו בזול

procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
  Embedded, External: TStringList;
  I: Integer;
  Name: string;
begin
  Embedded := TStringList.Create;
  External := TStringList.Create;
  try
    Embedded.Sorted := True;
    Embedded.Duplicates := dupIgnore;
    External.Sorted := True;
    External.Duplicates := dupIgnore;

    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      Name := Pdf.FontBaseName[I];
      if Name = '' then
        Continue;              // רווחים שנוצרו וכדומה אין להם גופן
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' non-embedded font(s):' + sLineBreak + External.Text)
    else
      ShowMessage('All ' + IntToStr(Embedded.Count) +
        ' font(s) on this page are embedded.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

שני פרטים שומרים על זה ישר. ראשית, CharacterCount הוא לפי עמוד, כך שביקורת של מסמך שלם פירושה הגדרת Pdf.PageNumber לכל עמוד בתורו והפעלת הלולאה מחדש, תוך מיזוג התוצאות. שנית, שכבת הטקסט מכילה תווים שנוצרו כגון הרווחים שקורא מסיק בין מילים, ולאלו אין אובייקט גופן מאחוריהם; בדיקת שם-בסיס ריק מדלגת עליהם במקום לרשום פנטום (phantom). שם הבסיס הוא המפתח הנכון למניעת כפילויות (de-duplication) כאן מכיוון שקידומת התת-קבוצה שהוא נושא מבחינה בין שתי תת-קבוצות שונות של אותה משפחה, שזה בדרך כלל מה שאתה רוצה לדעת

שליפת הגופן המוטמע החוצה

כאשר גופן מוטמע אתה יכול לקרוא את הבתים שלו ישירות. FontData מחזיר את תוכנית הגופן הגולמית, אותם נתוני TrueType או CFF שה-PDF נושא, שזה מספיק כדי לכתוב קובץ גופן עצמאי או כדי ליצור טביעת אצבע של הגופן מול ספריה מוכרת. הוא מחזיר מערך ריק כאשר הגופן אינו מוטמע, כך שבדיקת ההטמעה ובדיקת האורך יחד שומרות על הכתיבה

procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
  const OutputFile: string);
var
  Data: TBytes;
  Stream: TFileStream;
begin
  if not Pdf.FontIsEmbedded[CharIndex] then
  begin
    ShowMessage('That glyph''s font is not embedded; nothing to extract.');
    Exit;
  end;

  Data := Pdf.FontData[CharIndex];
  if Length(Data) = 0 then
    Exit;

  Stream := TFileStream.Create(OutputFile, fmCreate);
  try
    Stream.WriteBuffer(Data[0], Length(Data));
  finally
    Stream.Free;
  end;
  ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;

הבתים הם תת-הקבוצה המוטמעת, לא הגופן הקמעונאי המקורי, ולכן מה שאתה מקבל בחזרה מכסה בדרך כלל רק את הגליפים שהמסמך השתמש בהם בפועל. זה בדיוק נכון לזיהוי פלילי ואימות, והתאמה גרועה לשימוש חוזר; תת-קבוצה של Times New Roman המכילה שלושים גליפים אינה גופן שאתה יכול להתקין ולהקליד איתו. התייחס לחילוץ כדרך לבדוק מה נשלח, לא ככלי לשחזור גופנים. אם אתה צריך את שם הבסיס התואם כדי לתייג את הפלט, קרא את FontBaseName[CharIndex] לצד הנתונים, והסר את תגית תת-הקבוצה המובילה אם אתה רוצה את המשפחה החשופה

להבין את מספר המשקל

FontWeight מחזיר את מחלקת המשקל המספרית, אותו סולם 100-עד-900 ש-CSS משתמש בו, כאשר 400 הוא רגיל ו-700 הוא מודגש. PDFium מדווח על כל מה שהגופן מצהיר, וזה לא תמיד מאה עגול; גופן יכול להכריז על 350 או 650, והתייחסות לכל דבר השווה או גבוה מ-600 כ"מודגש מספיק כדי לשנות" מחזיקה מעמד טוב יותר מאשר בדיקה עבור 700 בדיוק. זווית הנטוי (italic angle) היא אות מלווה: ערך שאינו אפס, בדרך כלל שלילי, פירושו שהגופן הוא עיצוב נטוי (oblique) או איתליק (italic) אמיתי, ואפס פירושו זקוף (upright). יחד הם מאפשרים לך להבחין ברצף מודגש-נטוי מרצף רגיל מבלי לרנדר כלום, וזה סוג הבדיקה שמעבר הכנה (preflight) או ביקורת נגישות רוצים לבצע באופן גורף

אף אחת מהקריאות הללו אינה דורשת מפת-סיביות (bitmap) מרונדרת. הן מגיעות משכבת הטקסט המנותחת (parsed text layer), ולכן מסמך פתוח בעמוד הנכון הוא כל ההגדרה שאתה צריך, מה שהופך את בדיקת הגופנים לזולה להפעלה על פני ארכיון שלם. אם אתה מצמד את זה עם חילוץ טקסט, אותם אינדקסים של תווים מסתדרים עם הטקסט שאתה שולף, כך שגופן של גליף וערך ה-Unicode שלו הם שתי קריאות מול אינדקס אחד. המאמר המלווה בנושא חילוץ טקסט ממסמכי PDF עם PDFium Component מכסה את הצד הזה של שכבת הטקסט לעומק רב יותר

מאפייני הגופנים המוצגים כאן הם חלק מ-PDFium Delphi VCL Component