PDFlibPas יכולה לפתוח PDF מקומי דרך תצוגת זיכרון ממופה, מוגבלת ולקריאה בלבד: LoadFromMappedFile ו-DAOpenMappedFile שומרות בדיוק חלון נע אחד מעל הקובץ, ממפות אותו מחדש לפי דרישה ומספקות כל פרוסת אובייקט באמצעות קריאות עם offset מוחלט. ספריית ה-PDF של Delphi לעולם אינה מחזיקה את כל המקור בזיכרון, ולכן השימוש במרחב הכתובות נשאר קבוע כשהקובץ גדל. התכנון נועד לעומס עבודה אחד: קובצי PDF בגודל ג׳יגה-בייט שבהם המפענח סיים את הטעינה ועדיין חוזר לדיסק, אובייקט אחר אובייקט וקטע זרם אחר קטע זרם
מדוע קריאות מפוזרות נשארות יקרות אחרי טעינת ה-PDF
טעינת PDF אינה מסיימת את קריאתו, ובקובץ של כמה ג׳יגה-בייט הפער הזה הוא המקום שבו הזמן נעלם. טבלת cross-reference או זרם cross-reference (ISO 32000-1 §7.5.4 ו-§7.5.8) רק מתעדים היכן מתחיל כל אובייקט עקיף. הבתים מגיעים מאוחר יותר, כאשר עמוד עובר רינדור, תוכנית גופן מפוענחת או זרם קובץ מוטמע (ISO 32000-1 §7.11.4) מופק. ארכיון של 2 GB עם עשרות אלפי אובייקטים הופך לעשרות אלפי קריאות קטנות ולא מסודרות, ואף אחת מהן אינה ידועה בזמן הטעינה
המסלול שבו הקריאות האלה עברו בעבר היה Seek משותף ואחריו Read על זרם בעל מיקום יחיד, והוא נכשל בשני כיוונים בבת אחת. כל קטע משלם על קריאת קובץ גם כשהעמוד כבר נמצא במטמון של מערכת ההפעלה, והסמן הוא state משתנה ומשותף, כך שקובץ מקומי ומקור טווח הבתים שמאחורי טעינת טווחים מתקדמת של PDF עם prefetch לא יכלו להריץ את אותו קוד מפענח בלי להילחם על המיקום. PDFlibPas פותרת את שני הדברים בכך שהיא הופכת קריאה לפי offset מוחלט מאופטימיזציה לחוזה
מה מבטיח TPDFReadAtStream
TPDFReadAtStream מבטיח קריאה ב-offset מוחלט שאינה תלויה בסמן הלוגי של הזרם ואינה משנה אותו. זהו יורש מופשט של TStream עם מתודה וירטואלית אחת בדיוק, ושני המקורות שאינם תלויי-סמן בספרייה יורשים ממנו: TReadOnlyMappedFileStream לקבצים מקומיים ו-TByteRangeStream למקורות מרוחקים שמוגשים לפי טווחים. קורא פרוסות האובייקטים בודק פעם אחת אם המקור הוא TPDFReadAtStream, וחוזר לרצף הישן של seek ואז read כאשר אינו כזה, כך שזרם קובץ רגיל או זרם זיכרון ממשיך לעבוד ללא שינוי
type
// זרמים לקריאה בלבד שקריאות ה-offset המוחלטות שלהם נמנעות מ-Seek ו-Read משותפים
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// גישה לחלון לקריאה בלבד של קובץ מקומי אחד
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
ההבדל חשוב יותר ממה שהחתימה מרמזת. ReadAt משתמש ב-offset שקיבל ומשאיר את Position בדיוק כפי שהיה, וזה מה שמאפשר לרמות מקוננות של המפענח לבצע קריאות בלי ריקוד של save-and-restore סביב כל קריאה. TReadOnlyMappedFileStream עדיין מממש Read, Seek ו-Size כמו כל TStream, Seek מגביל את המיקום הלוגי לתוך הקובץ, ו-Write תמיד מחזיר 0 מפני שהמקור נפתח לקריאה בלבד
פתיחת PDF דרך תצוגה ממופה ב-Delphi
שתי נקודות כניסה מפורשות פותחות מקור ממופה, ואף אחת מהן אינה משנה את ההתנהגות של נקודות הכניסה שכבר נמצאות אצלכם. LoadFromMappedFile טוענת ובוחרת מסמך; DAOpenMappedFile מחזירה handle של Direct Access מעל אותו קובץ, וזה המצב הרצוי כאשר ממזגים ומפצלים קובצי PDF בגודל ג׳יגה-בייט דרך Direct Access. LoadFromFile ו-DAOpenFile משמרות את סמנטיקת שיתוף הקבצים, השגיאות והתאימות שלהן ללא שינוי, ולכן דבר אינו משתנה עבור callers שאינם בוחרים בכך. שתי נקודות הכניסה הממופות מקבלות WindowSize מבוקש בבתים ומסכת ביטים Options, ושתיהן מקבלות 0 עבור כל אחד מהם
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 בוחר ברירת מחדל של 64 MiB; כאן המיפוי חובה
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// חילוץ מושהה עובר כעת בחלונות ממופים במקום לבצע seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
מה PDF_MAPPED_FILE_REQUIRE_MAPPING באמת אוכף
PDF_MAPPED_FILE_REQUIRE_MAPPING הופך fallback שקט לכשל מיידי שניתן לאבחן בזמן הפתיחה. כאשר Options נשאר 0, שתי נקודות הכניסה מקבלות fallback לזרם קובץ לקריאה בלבד: אם לפלטפורמה אין קוד מיפוי או שקריאת המיפוי נכשלת, המסמך עדיין נפתח וכל קריאה עוברת דרך זרם קובץ רגיל. כשהדגל מוגדר, PDFlibPas מקבלת את הקלט רק אם התצוגה הראשונה נוצרה, ומדווחת על סירוב דרך LastErrorCode 401 במקום לטעון מסמך שמתנהג בשקט בדיוק כמו הנתיב הישן
ב-Windows הזרם הממופה פותח handle שני לקריאה בלבד עם FILE_SHARE_READ, FILE_SHARE_WRITE ו-FILE_SHARE_DELETE, יחד עם FILE_FLAG_RANDOM_ACCESS, יוצר מעליו מיפוי PAGE_READONLY וממפה את החלון הראשון בתוך הבנאי. מיפוי חמדני הוא כל העניין: כשל של "mapping required" מופיע ב-LoadFromMappedFile, לא בקריאת האובייקט העצלנית הראשונה באמצע עבודת רינדור. עם זאת, צריך להיות ברורים היכן ההבטחה נעצרת. קוד המיפוי מקומפל רק ליעדי Windows, וקובץ בגודל אפס אינו מנסה למפות כלל, לכן PDF_MAPPED_FILE_REQUIRE_MAPPING הוא בקשה שיכולה להיכשל באופן חוקי ולא הבטחה ניידת. WindowSize שלילי, או כל ביט ב-Options מלבד הערך המתועד היחיד, נדחה על הסף עם שגיאה 401 זהה
חלון אחד שממופה מחדש לפי גרנולריות ההקצאה
רק תצוגה אחת נשמרת בכל זמן, וזה מה שמשאיר את השימוש במרחב הכתובות בלתי תלוי בגודל הקובץ. WindowSize של 0 בוחר 64 MiB; ערך שמתחת לגרנולריות ההקצאה של המערכת מועלה אליה; ערך מעל 1 GiB נחתך; והתוצאה מעוגלת כלפי מעלה למספר שלם של יחידות גרנולריות, 65536 בתים ב-Windows אלא אם GetSystemInfo מדווח על dwAllocationGranularity אחר. כאשר קריאה נוחתת מחוץ לתצוגה הנוכחית, PDFlibPas מבטלת אותה, מיישרת את ה-offset המבוקש כלפי מטה לגבול גרנולריות וממפה חלון חדש שם. החלון האחרון נחתך לגודל הפיזי של הקובץ, כך שהתצוגה לעולם אינה נמשכת מעבר לסופו
קריאה יחידה יכולה לחצות מספר כלשהו של חלונות: הלולאה מעתיקה כל מה שהתצוגה הנוכחית יכולה לספק, ממפה מחדש וממשיכה, ובקשה שיוצאת מעבר לסוף מחזירה count קצר במקום להיכשל. מה ש-PDFlibPas אינה עושה בכוונה הוא למסור לכם מצביע לתוך התצוגה, מפני שהקריאה הבאה שחוצה חלון מבטלת אותו ואף caller אינו יכול להתגונן מזה באופן סביר. בתים ממופים מועתקים ישירות למאגרי היעד שבבעלות המפענח, מה שמסיר את מאגר קלט הקובץ הנוסף ואת החלפת המיקום, אך הספרייה אינה טוענת לאחסון סופי ללא העתקה בתוך המפענח. חלונאות לקריאה משתלבת גם עם צד הכתיבה, מפני שהזזת הפניות ברמת הבתים במהלך מיזוג PDF מהיר מזרים החוצה בתים של אובייקטים בזמן שהמקור הממופה מזרים אותם פנימה. פשרת גודל החלון ברורה: חלון קטן יותר מחזיק פחות מרחב כתובות וממפה מחדש בתדירות גבוהה יותר, וזה בדרך כלל הבחירה הנכונה בתוך תהליך של 32 סיביות
על מה המנעול מגן ומה GetMappedFileInfo מדווח
מקטע קריטי אחד מכסה את התצוגה הממופה, את סמן הקובץ החלופי, את המיקום הלוגי ואת הסטטיסטיקות, וההפרדה בין שתי מתודות הקריאה נובעת ישירות ממנו. ReadAt נוטלת את המנעול וקוראת לקורא הפנימי שאינו נעול; Read נוטלת את אותו מנעול, קוראת לאותו קורא פנימי במיקום הלוגי הנוכחי ואז מקדמת אותו. שימוש בפונקציה הפנימית במקום ב-ReadAt הציבורית הוא מה שמונע נעילה רקורסיבית, והחזקת המנעול לאורך כל לולאת ההעתקה היא מה ששומר על נכונות המיפוי מחדש תחת קריאות מקבילות. פרט אחד של Free Pascal כדאי לדעת לפני port: יחידת Windows של FPC מגדירה רשומה משלה בשם TCriticalSection, ולכן את השדה ואת יצירתו חייבים לכתוב כ-SyncObjs.TCriticalSection. Delphi מקמפלת בשמחה את הצורה ללא הסמכה; FPC פותרת אותה לרשומה שאין לה Create, Enter או Leave
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMappedהוא false בכל פעם שה-fallback הנייד לזרם קובץ פעיל, והוא השדה היחיד שמוכיח שמיפוי מעולם לא נוצרwindowSizeהוא החלון האפקטיבי המיושר ולא הערך שביקשתם, ו-mappedBytesקטן ממנו בחלון הזנבmappedOffsetהוא תחילת התצוגה השמורה, מיושרת להקצאה, או -1 כאשר אין כרגע תצוגה פעילהreadCallsסופר בקשות קריאה מוצלחות בתוך הטווח,bytesReadסופר בתים שהועתקו ל-callers, ו-remapCountכולל את התצוגה הראשונית
בדיקות רגרסיה ממוקדות מכסות קריאות מוחלטות החוצות חלונות, שימור הסמן הלוגי, קריאות קצרות בזנב, offsets לא חוקיים, כתיבות שנדחות, מיפוי מחדש בין חלונות מופרדים, חילוץ מושהה של קובץ מצורף בלתי-דחיס בגודל 220 KB וסטטיסטיקות שנעשות לא חוקיות אחרי DACloseFile; חבילות ה-headless של Win32 ו-Win64 גילו כל אחת 1467 בדיקות ועברו את כולן ללא בדיקה שהתעלמו ממנה, כשל, שגיאה או דליפה. אם אתם עובדים עם קובצי PDF בגודל ג׳יגה-בייט ב-Delphi או C++Builder וה-profiler שלכם ממשיך להצביע על קריאות קובץ במקום על parsing, נקודות הכניסה לקובץ הממופה ראויות אחר צהריים של מדידה, ו-GetMappedFileInfo יספר לכם אם באמת קיבלתם מיפוי. תיעוד ה-API המלא ובניית ניסיון נמצאים בדף ספריית ה-PDF של PDFlibPas ל-Delphi