מאמר טכני

מיזוג ופיצול קובצי PDF בגודל ג'יגה-בייט ב-Delphi עם PDFlibPas Direct Access

מיזוג או פיצול של PDF בגודל שני ג'יגה-בייט בדרך הברורה עולה לכם שני דברים בו-זמנית: זמן ריצה ומרחב כתובות. הדרך הברורה היא לטעון כל קלט, לבצע את העבודה, לכתוב את הפלט. הטעינה היא המקום שבו זה נשבר. ארכיון סריקה שעובר מ-300 ל-600 DPI מכפיל את הרזולוציה הלינארית שלו ומגדיל את גודל הדיסק בערך פי ארבעה, כך שאותה עבודת הרכבה שטיפלה בקבצים של 400 MB כל השנה מתחילה להגרד ברגע שקלט עולה על ג'יגה-בייט, לרוב בזמן שלא עושה יותר מאשר ספירת עמודים. המשימה מעולם לא הפכה לקשה יותר. פתיחה, ספירה, בחירת טווחים, שרשור הוא כל העניין. טעינה מלאה של עץ פשוט הפסיקה להיות ברירת מחדל נאותה בגודל הזה. PDFlibPas, ספריית ה-PDF של losLab עבור Delphi ו-C++Builder, עונה לזאת עם שכבת Direct Access שלה: משפחת פונקציות עם קידומת DA שמגובות בקורא סטרימינג שמהלך על טבלת ה-xref במקומה במקום לבנות את כל המסמך בזיכרון

לאן הולך הזיכרון בטעינה מלאה

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

שכבת Direct Access הופכת את המודל. DAOpenFile ו-DAOpenFileReadOnly מנתחים את ה-trailer וה-xref, כמה קילובייטים בסוף הקובץ, ומחזירים מזהה קובץ. אובייקטים נשלפים באופן עצלני כאשר קריאה צריכה אותם. התוצאה המעשית היא שפתיחת קובץ של מספר ג'יגה-בייטים אורכת בערך כמו פתיחת קובץ קטן, והזיכרון עוקב אחרי מה שגישתם אליו ולא אחרי מה שהקובץ מכיל

בדיקת קובץ עצום מבלי לטעון אותו

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

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

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

PageRef הוא מזהה אובייקט, לא מספר עמוד

הטעות הנפוצה ביותר עם ה-DA API היא העברת מספר עמוד כשפונקציה מצפה ל-PageRef. כמעט כל קריאת DA לעמוד מקבלת מזהה הפניה לאובייקט העמוד ולא מספר עמוד: DAExtractPageText, DARenderPageToFile, DARotatePage ו-DACapturePage כולם מצפים ל-ref. אתם מקבלים אחד על ידי תרגום המספר הנגיש לאדם דרך DAFindPage:

PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

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

מיזוג מאות קבצים עם רשימה בשם

עבור שני קבצים, MergeFiles(First, Second, Output) מספיק. הרכבת אצווה מתרחבת טוב יותר דרך רשימות קבצים: רשמו קלטים תחת שם רשימה, ואז מזגו את הרשימה במעבר אחד

Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Verify the result the cheap way: direct access again
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

למשפחת המיזוג שלושה גרסאות, וההבדל אינו מהירות בלבד. MergeFileListFast מדלגת על שמירת עץ המבנה; MergeFileListStrict אוכפת מצב מחמיר; הגרסה ללא סיומת היא ברירת המחדל המאוזנת. הכלל התפעולי שנובע: אם אחד מהקלטים הוא PDF מתויג שמבנה הנגישות שלו חייב לשרוד, המקרה הברור הוא כל דבר שיוצר ל-PDF/UA, השתמשו בגרסה ברירת המחדל או Strict, כיוון שגרסה Fast מוסירה בשקט את עץ המבנה. עבור ארכיוני סריקה פשוטים ללא תיוג, Fast הוא ביצועים חינמיים. החליטו לפי צינור, לא לפי מצב רוח המפתח, ורשמו את הגרסה שנעשה בה שימוש ביומן העבודה

פיצול ללא טעינה: חילוץ טווחים

פיצול עוקב אחרי אותה פילוסופיה של ללא-טעינה. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) מושך טווח עמודים ישירות מקובץ לקובץ, עם רשימת טווח כמו '1-500', '501-1000', או בחירות מופרדות-בפסיק, והמקור לעולם אינו הופך לעץ מסמך. כאשר מסמך כבר טעון מסיבות אחרות, ExtractPageRanges מייצר מסמך חדש בזיכרון מהמסמך הנוכחי, ו-CopyPageRanges מושך טווחים ממסמך טעון אחר לפי ID. לפיצול לפי-דף-כרטיס של זרמי הדפסה מאוחדים, הצורה מקובץ-לקובץ היא זו ששומרת על קלט של 4 GB מלא שלעולם לא מתנפח לזיכרון RAM

קבצים שמשקרים על הגאומטריה שלהם

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

ראשית, כותרות שהוזזו. שערי דואר ומזרמי הדפסה לפעמים מוסיפים בתים לפני PDF, כך שסימן ה-%PDF כבר לא יושב בהיסט 0 וכל היסט xref בקובץ שגוי באותה כמות. קורא הסטרימינג מזהה זאת וחושף אותה (DAShiftedHeader ברמה השטוחה, ShiftedHeader על TSmartPDFReader), ואז מפצה עליה בזמן הקריאות. אריתמטיקת היסט ביתית בדרך כלל אינה עושה זאת, וזו הסיבה ש-"עובד על כל קובץ שאנחנו מייצרים, נכשל על קבצים מלקוח X" הוא הסימפטום הקלאסי

שנית, טבלאות xref שבורות. DACopyFile(InputFileName, OutputFileName, PageCount) מסטרים את כל הקובץ לעותק חדש תוך כדי בניית ה-xref מחדש, ומחזיר את ספירת העמודים כתוצר לוואי. הרצת זה כשלב נורמליזציה לפני צרכן downstream דקדקני ממיר מחלקה של כשלי ניתוח לסירוגין לשלב תיקון צפוי אחד. וכשהעריכות שלכם עצמכם צריכות שמירה, DAAppendFile כותב אותן כעדכון מצטבר, מוסיף גרסה חדשה במקום לכתוב מחדש ג'יגה-בייטים, מה ששומר על עלות השמירה פרופורציונלית לשינוי ולא לקובץ

פרטי אספקה: ליניאריזציה והרכבה

שתי יכולות סמוכות משלימות צינור קבצים גדולים. כאשר הפלט המורכב מוגש דרך HTTP לצפייה בדפדפן, LinearizeFile מארגן מחדש אותו לסטרימינג על טווח בתים כך שהעמוד הראשון מוצג לפני שסיום הורדת מנת 500 MB, הריצו אותו כשלב אחרון, לאחר כל המיזוג, כיוון שכל שינוי מאוחר יותר מדא-ליניאריז את הקובץ שוב. וכאשר מנות זקוקות להרכבה ולא לשרשור פשוט, נניח דף כיסוי שמודפס מאחורי כל דף-כרטיס או שני עמודי מקור מושמים על גבי דף פלט אחד, DACapturePage הופך כל עמוד לתבנית לשימוש חוזר ש-DADrawCapturedPage ממקם על עמוד יעד במלבן שרירותי, עדיין ללא טעינת מסמך מלאה על המקור בגודל מרובה-ג'יגה-בייט

מגבלות ומה נשאר לקריאה בלבד

הפורמט עצמו נגמר הרבה לפני שה-Direct Access נגמר. היסטים הם Int64 לאורך כל שכבת DA, כך שהתקרות האמיתיות הן דיסק זמין ושדה ה-xref offset של 10 ספרות של טבלאות xref קלאסיות (לא-סטרימינג). ארכיוני סריקה של מרובה-ג'יגה-בייט אינם מיוחדים בפועל, והזיכרון נשאר מוגבל ללא תלות בגודל הקובץ כיוון שאובייקטים נקראים רק כאשר קריאה מבקשת אותם

שתי שאלות עולות לעתים קרובות מספיק כדי לענות עליהן ישירות. מיזוג דרך הנתיב ברירת המחדל מעביר מבנה מסמך, אז סימניות וקישורים שורדים; גרסת Fast היא זו שמחליפה את עץ המבנה במהירות, שזו הסיבה השלמה להשמר אותה לקלטים ללא תיוג. ההרגל הבטוח הוא לפתוח את פלט המיזוג, ללכת על קווי המתאר שלו, ולבדוק נקודתית כמה קישורים פנימיים לפני שחרורו. לגבי עריכה: יש קרקע אמצעית שימושית בין בדיקה לקריאה-בלבד לבין טעינה מלאה. פעולות ברמת-עמוד עובדות ישירות על ה-handle, DARotatePage, DAMovePage ו-DAHidePage ביניהם, יחד עם קריאות שדה-טופס, ו-DAAppendFile משמר את העריכות האלה כגרסה מצטברת. עריכה ברמת-תוכן, כל דבר שכותב מחדש את מפעילי הסימון בתוך עמוד, עדיין שייכת לשכבת המסמך המלא

מאמרים קשורים

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

רשימת הפונקציות המלאה של Direct Access נשלחת עם הספרייה; מהדורות והורדות לניסיון נמצאות בדף המוצר PDFlibPas