מאמר טכני

מציג PDF עבור Lazarus ו-Free Pascal עם PDFium

Delphi ו-Lazarus מהדרים את אותו Object Pascal, ודמיון פני השטח הזה הוא בדיוק מה שהופך את העברת מציג ביניהם למטעה. שתי סביבות הפיתוח מתפצלות בשלושה מקומות החשובים לעבודת PDF: סוג ה-string הטבעי הוא UTF-16 ב-Delphi ו-UTF-8 ביישום LCL; VCL ו-LCL הם מסגרות ויזואליות שונות עם פקדים, דיאלוגים ופורמטי שידור טפסים משלהן; וקובץ בינארי של Delphi מכוון ל-Windows בזמן שקובץ בינארי FPC עשוי להיות מיועד ל-Linux או macOS. אף אחד מהבדלים אלה לא מופיע בזמן הידור. מציג הבנוי על PDFium Component, שמגיע עם מהדורות VCL ו-LCL מעץ קוד מקור אחד, יהדר בנקייה תחת Lazarus לאחר קומץ החלפות שמות יחידות ומספר בלוקי {$IFDEF FPC}. הכישלונות מגיעים מאוחר יותר, כשנתונים אמיתיים ופריסה אמיתית חושפים את ההנחות שהבנייה ב-Delphi עשתה בשקט

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

אותו Pascal, מטענים שונים של מחרוזות

ה-string הטבעי של Delphi הוא UTF-16 מאז 2009. Lazarus ו-Free Pascal ברירת מחדל ל-UTF-8 ביישומי LCL. ממשקי ה-API של הרכיב לטקסט מדברים UTF-16 דרך הסוג WString, שמהדורת FPC מכנה בשם WideString, כך שכל גבול שבו טקסט עובר בין ממשק המשתמש LCL שלך לבין מנוע ה-PDF הוא נקודת המרה

ההמרות קורות אוטומטית בהקצאות פשוטות, ורוב הקוד לעולם לא צריך לחשוב עליהן. שתי הרגלים מרחיקים את באגי הקידוד. העבר טקסט ישירות ללא מניפולציה ברמת הבייט: קוד שפורס מונח חיפוש לפי הסטת בייט עובד ב-Delphi, שבו Char אחד הוא יחידת UTF-16 אחת, ומשחית UTF-8 רב-בייטי ב-LCL. ובדוק עם נתונים שאינם ASCII מהריצה הראשונה. שם קובץ גרמני, מונח חיפוש קירילי, שם מחבר עם אקצנט במטא-נתוני המסמך: נתוני בדיקה טהורי-ASCII מסתירים כל פגם קידוד, כי ASCII הוא הטווח היחיד שבו UTF-8 ו-UTF-16 מסכימים בייט לתו. הבאג אמיתי כל הזמן; ASCII פשוט שומר אותו בלתי נראה עד שלקוח במינכן פותח קובץ שמעולם לא ניסית

בלוק תנאי אחד, לא fork לכל IDE

אחרי תריסר ה-IFDEF הראשונים בסיס הקוד מתחיל להרגיש כמו שני פרויקטים הלובשים מאגר אחד, ו-forking לכל IDE נראה מפתה. זה הצעד הלא נכון. ההבדלים האמיתיים קורסים לבלוק הצהרה משותף אחד, ו-fork מכפיל את עלות כל תיקון באג מכאן ואילך. שמור את שכבת התנאים קטנה כזאת:

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // component text APIs are UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

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

בנה את הטופס בקוד, לא בשני מעצבים

שידור טפסים הוא המקום שבו פרויקטים דו-IDE נרקבים בשקט. .dfm ו-.lfm שטוענים לתאר את אותו טופס מתסחפים זה מזה מאפיין אחר מאפיין עד שהשניים בנויים מתנהגים שונה מסיבות שאף אחד לא יכול ל-diff, כי שני הקבצים אינם אפילו באותו פורמט. בנייה של המציג בזמן ריצה עוקפת את כל הבעיה. יש רצף constructor אחד, גרסה נשלטת כקוד רגיל, והוא קורא אותו דבר בשתי הפלטפורמות:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // opens the document; PageCount valid after this
  end;
end;

הסדר המדויק של ההקצאות הללו חשוב פחות מהשורה האחת שעושה את העבודה האמיתית. PdfView.Pdf := Pdf קושר את פקד הוויזואלי לרכיב המסמך, ומאותה נקודה ניווט דפים דרך PageNumber והתנהגות ההתאמה דרך FitMode מגיבים בצורה זהה תחת VCL ו-LCL. quirk חוצה-מסגרות אחד שכדאי לדעת לפני שמשתמש מדווח עליו כבאג: הקצאת Zoom ידנית מאפסת את FitMode חזרה ל-pfmNone בשתי המסגרות. כך שאם סרגל הכלים שלך מתייחס ל"התאמת רוחב" כהעדפה דביקה, אתה צריך להקצות מחדש את מצב ההתאמה לאחר כל zoom תכנותי, או ההעדפה מפסיקה בשקט להיות דביקה בפעם הראשונה שקוד נוגע ברמת ה-zoom

הקובץ הבינארי שה-IDE לא אזהר עליו

הרכיב עוטף את מנוע PDFium, שמגיע כקובץ בינארי פלטפורמה טבעי, וקובץ בינארי זה הוא מקור כמעט כל דוח "עובד ב-IDE, נכשל מהקיצור המותקן". שלושה כללים מסבירים את רובם. סיביות חייבת להתאים בצורה מדויקת. קובץ הרצה 32 סיביות לא יכול לטעון ספריית pdfium של 64 סיביות, והמסר שמערכת ההפעלה מחזירה ("המודול לא נמצא" בחלק מגרסאות Windows) מטעה באופן פעיל, כי הקובץ יושב ממש שם ליד קובץ ההרצה. פתור את נתיב הספרייה ביחס לקובץ ההרצה, לא לתיקיית העבודה; השקה מ-IDE והשקת shell שונות בדיוק בנקודה הזו, וזו הסיבה שהבאג מסתתר במהלך הפיתוח. ותפוס טעינה כושלת לפני שהמסמך הראשון נפתח, ואז דווח עליה עם הנתיב הצפוי והארכיטקטורה מאויתים. כרטיס תמיכה שקורא "PDFium binary 64-bit חסר ב-<path>" נסגר תוך דקות. כזה שקורא "מציג קורס בהפעלה" הופך לשבוע של הלוך ושוב

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

רישום רכיבים ב-IDE של Lazarus

בנייה בזמן ריצה אינה דורשת רישום בזמן עיצוב כלל, שזו ההגדרה הנקייה ביותר למציג שבונה את ממשק המשתמש שלו בקוד. כשאתה רוצה את הרכיבים בפלטה של Lazarus לעבודת עיצוב, התקן את החבילה ואפשר ליחידת הרישום הייעודית שלה, PDFiumLazReg ב-Lib/FPC/PDFiumLaz.lpk, לטפל בה. יחידה זו מסומנת בזמן עיצוב במכוון: היא מפנה לממשקי עורך מאפיינים של IDE שאסור להם להיקשר לקובץ ההרצה שלך לשחרור

טעה בזה והסימפטום הוא יישום שתלוי בצורה בלתי מוסברת בחבילות IDE, שמופיע ככישלון פריסה על המכונה הראשונה של הלקוח שמעולם לא הותקן עליה Lazarus

דיבור וקוראי מסך מחוץ ל-Windows

המרת טקסט לדיבור היא התכונה האחת שבה הסיפור החוצה-פלטפורמות נשבר, והיא נשברת במערכת ההפעלה, לא ברכיב. SAPI, ה-backend הרגיל ל-TTS ב-Windows, קיים רק ב-Windows. בנייה ב-Lazarus שעדיין מכוונת ל-Windows שומרת על פלט SAPI מלא ועל אותה התנהגות תואמת-NVDA שהמקור ב-Delphi סיפק, כך שהעברה מ-Windows ל-Windows לא מאבדת דבר כאן, ומשתמש NVDA לא יכול להבדיל בין שתי הבנייות

יעד Linux או macOS הוא עניין אחר. אין SAPI לקרוא, לכן פלט השמע חייב להיות מחווט מחדש לשירות דיבור טבעי בזמן שממשקי ה-API של הקריאה מעליו נשארים במקומם. הפיצול הזה הוא הטיעון לשים דיבור מאחורי ממשק מ-commit הראשון: ניתוח סדר הקריאה וסמן המעקב ברמת המילה הם ניטרליים-פלטפורמה ועוברים ללא שינוי, ורק השכבה הדקה שאכן מייצרת קול צריכה להשתנות לכל פלטפורמה. מאמר הקורא הנגיש מכסה את מנגנון הקריאה הזה לעומק

רשימת בדיקת שוויון לפני שתכריז על ההעברה גמורה

המעבר הבא תפס רגרסיות אמיתיות, המפורטות בגסות בסדר שבו כישלונות נוטים לצוץ. פתח מסמך שנתיבו מכיל תווים שאינם ASCII. חפש מונח עם תווים שאינם ASCII ואשר שהלחצים מודגשים היכן שצריך. הפעל גלילת גלגל עכבר, בחירת גרירה וניווט דפים במקלדת על כל קבוצת וידג'טים שאתה משחרר, כי טיפול פוקוס והתנהגות גלגל הם הפינות התלויות ביותר בקבוצת-וידג'טים ב-LCL. בדוק עיבוד בקנה מידה של תצוגה 100%, 150% ו-200%. לבסוף, הרץ את הבנייה המותקנת, לא את בנייית ה-IDE, על מכונה שמעולם לא היה עליה ה-IDE, כי זו הבדיקה היחידה שמפעילה את פתרון הבינארי בכנות. כל השאר יכול לעבור בזמן שזה נכשל בשקט

תפוקת העיבוד עוברת בין שתי המהדורות ללא שינוי, כך שגישת הקישור ממאמר cache העיבוד וביצועי ה-zoom חלה על המציג LCL בדיוק כפי שנכתבה למציג VCL

אף אחד מהדברים הללו לא הופך את מהדורת LCL לנחותה. פני השטח המרכזיים זהים בשני הצדדים: TPdf, TPdfView, עיבוד, טפסים, חילוץ טקסט וממשקי ה-API של נגישות מתנהגים אותו דבר ללא קשר לאיזה IDE הידר אותם. כל הבדל שכדאי לעקוב אחריו קשור לפלטפורמה ולא למהדורה. דיבור SAPI הוא ל-Windows בלבד, דיאלוגים עוקבים אחרי המוסכמות של כל מסגרת, והקובץ הבינארי חייב להתאים לארכיטקטורה שנטענת לתוכה. קבל את גבולות הקידוד, טופס זמן הריצה ופתרון הבינארי נכון, ושאר ההעברה הוא העבודה המכנית שהמהדר כבר טיפל בה עבורך

מהדורות VCL ו-LCL המתוארות כאן מגיעות יחד כ-PDFium Component, עם קוד מקור וממשקי API ציבוריים זהים עבור Delphi, C++Builder ו-Lazarus/FPC