CollateDocumentsEx בספריית PDFlibPas Delphi PDF ממזג כמה מסמכים פתוחים למסמך משוזר אחד. הפונקציה מוסיפה GroupSize עמודים מכל מקור בכל סבב, מקבלת רשימת טווחי עמודים לכל מקור, ומתייחסת לטווח יורד כמו 3-1 כהיפוך של אותו מקור. קריאה אחת הופכת ערימה קדמית וערימה אחורית הפוכה לסדר קריאה
התרחיש שמאחורי ה-API הזה שגרתי ונפוץ ביותר: סורק גיליונות עם מסלול חד-צדדי מריץ את כל הערימה כשהפנים כלפי מטה, ואז המפעיל הופך את הערימה ומריץ אותה שוב. התוצאה היא שני קבצי PDF: הצד הקדמי בסדר, הצד האחורי בסדר הפוך. הקובץ שהמשתמש רוצה הוא קובץ אחד, עמוד 1 קדמי, עמוד 1 אחורי, עמוד 2 קדמי וכן הלאה. המאמר הזה עוסק בבעיית הסידור ובמלכודת שכפול המשאבים שמסתתרת מתחתיה. אם העניין שלך הוא תפוקת שרשור גולמית במקום זאת, ראה מיזוג PDF מהיר באמצעות הזזת הפניות ברמת הבייט; אם קלטי הקלט גדולים מכדי להחזיק אותם בזיכרון כלל, ראה מיזוג ופיצול קובצי PDF בגודל גיגה-בייט עם גישה ישירה
הסורק מייצר שתי ערימות, אחת מהן הפוכה
איסוף אינו מיזוג. מיזוג משרשר טווחי עמודים; איסוף שוזר אותם, ותבנית השזירה היא תכונה של המכשיר הפיזי שהפיק את הקלט. טעות בתבנית לא הופכת את הקובץ למעט שגוי — היא הופכת אותו לבלתי קריא: כל עמוד שני שייך לגיליון אחר. שלושה משתנים מתארים כמעט כל מקרה אמיתי: כמה מקורות בסבב, כמה עמודים מגיעים מכל מקור בכל סבב, והאם צריך לקרוא מקור כלשהו לאחור. CollateDocuments מכסה את שני הראשונים עם מערך פשוט של מזהי מסמכים ומספר שלם GroupSize. CollateDocumentsEx מוסיף את השלישי בכך שהיא מקבלת רשימת טווחי עמודים מופרדת בנקודה-פסיק, קטע אחד לכל מקור, כאשר קטע ריק פירושו כל עמודי אותו מקור וטווח יורד הופך אותו. שתי הפונקציות מוסיפות בסוף המסמך הנבחר כרגע ומחזירות 1 בהצלחה, 0 בכל דחייה
מדוע איסוף נאיבי מכפיל את גודל הקובץ?
מפני שמפת הייבוא שממפה מספרי אובייקטים במקור למספרי אובייקטים ביעד נבנית מחדש בכל קריאת העתקה, וכל דבר שנגיש מיותר מנתח אחד מיובא פעם אחת עבור כל נתח. בתוך PDFlibPas, TPDFDocument.CopyPagesFromDoc מאפסת את NewIndObjList שלה בתחילת כל הפעלה. הרשימה הזו היא הזיכרון היחיד שיש למעתיק לגבי מה שכבר הועבר. קרא לה פעם אחת עם טווח של עשרה עמודים וגופן משותף לכל עשרת העמודים משוטמע פעם אחת. קרא לה עשר פעמים עם עמוד אחד בכל פעם ואותו גופן משוטמע עשר פעמים. זה משנה הרבה יותר בסריקות מאשר במסמכי טקסט, כי עמוד סרוק הוא XObject תמונה גדול אחד והאובייקטים המשותפים הם אלה בעלי המשקל האמיתי: פרופיל ICC משוטמע, שרשרת /DecodeParms משותפת, XObject טופס של חותמת או סימן מים המוחל על כל גיליון, גופן שכבת טקסט ה-OCR. הדרך הברורה לכתוב איסוף round-robin היא לולאה על סבבים, ולולאה זו היא בדיוק המקרה הפתולוגי
// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
RoundIndex: Integer;
begin
for RoundIndex := 1 to 12 do
begin
PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
end;
end;
שנים עשר סבבים, שני מקורות, עשרים וארבע מפות ייבוא. שום דבר לא מזהיר אותך. סדר העמודים נכון, כל עמוד נרנדר, והתסמין היחיד הוא קובץ גדול פי כמה מסכום קלטיו. בעבודת אצווה של 300 עמודים המכפיל הזה אינו שגיאת עיגול — הוא ההבדל בין ארכיון שמתאים לתקציב השמירה לבין כזה שלא
ייבוא פעם אחת, ואז סידור מחדש של עץ העמודים
התיקון הוא להפריד בין שני העניינים שהלולאה הנאיבית מיזגה. העתקה קובעת אילו אובייקטים קיימים ביעד; סידור קובע איפה העמודים יושבים בעץ העמודים. CollateDocumentsEx מעתיקה כל מקור בדיוק פעם אחת, בקריאת CopyPagesFromDoc יחידה עם הטווח המלא של אותו מקור, כך שכל מקור מקבל מפת ייבוא אחת ומשאבים משותפים נכתבים פעם אחת. רק אחרי שכל מקור נחת מתרחשת השזירה, וזה קורה כולו דרך TPDFPageTree.MovePage
הזזות עמודים חינמיות במובן שחשוב כאן. תקן ISO 32000-1 §7.7.3 מגדיר את עץ העמודים כמבנה מאוזן של מילוני צמתים שמערכי /Kids שלהם מחזיקים הפניות עקיפות, כאשר /Count נושא את סך העלים בכל צומת. הזזת עמוד פירושה הסרת הפניה עקיפה אחת ממערך /Kids אחד, הכנסתה לאחר, התאמת שני ערכי /Count, וכיוון מחדש של /Parent של העמוד. אף זרם תוכן לא נגע, אף משאב לא שוכפל, אף אובייקט לא נוצר. אובייקט העמוד שומר על מספר האובייקט שלו, וזו גם הסיבה שמספרי האובייקטים נשארים יציבים כמו בהחלפת עמודים ששומרת על מספרי אובייקטים. יש עוד פרט אחד שהזזת עמוד נאיבית מפספסת ו-MovePage לא. תקן ISO 32000-1 §7.7.3.4 מאפשר ל-/Resources, /MediaBox, /CropBox ו-/Rotate לרשת מצומת אב ולא להיות מוצהרים על העמוד עצמו. עמוד שיורש את משאביו מצומת A ואז מוזז מתחת לצומת B יורש בשקט משהו אחר, או כלום. MovePage לכן פותרת את הערך המורש וכותבת אותו למילון העמוד לפני ההזזה, כך שהעמוד נושא את התכונות שלו עצמו לאורך ההזזה
מה בדיוק עושה מעבר הסידור מחדש?
הוא מריץ מיון בחירה כנגד סמנטיקת הכנסה-במיקום. הסדר היחסי-לבלוק הרצוי מחושב תחילה: הליכה על המקורות בסבב, לקיחת עד GroupSize אינדקסים מכל אחד, דילוג על מקור שהתרוקן, חזרה עד שכל עמוד ממוקם. זה מייצר תמורה מעל הבלוק שהתווסף. יישום התמורה הוא החלק המסורבל, כי MovePage היא פעולת הכנסה, לא החלפה, כך שכל הזזה מזיזה כל מה שביניים ובין המיקום הישן והחדש באחד
המימוש שומר מערך Current שממדל היכן כל עמוד שהתווסף נמצא כעת, סורק קדימה ממיקום K לחיפוש העמוד ששייך ל-K, מבצע את ההזזה, ואז מזיז את ערכי המערך כדי לשקף מה שההזזה עשתה לעץ. זו סיבוכיות O(n בריבוע) בפעולות מערך ואפס בהעתקות אובייקטים, וזה הטרייד-אוף הנכון לעומס העבודה הזה: איסוף של 500 עמודים הוא רבע מיליון ערבובי מספרים שלמים ולא בייט אחד של נתוני תמונה משוכפלים. טווחים יורדים ועמודים חוזרים לא דורשים טיפול מיוחד במעבר הזה כי PLParsePageRangeList נקראת עם מיון מושבת ועם היתר לכפילויות, כך שהסדר המבוקש שורד את הפענוח שלם
טווחים הפוכים ומיזוג הדופלקס בקריאה אחת
כשהיפוך מבוטא כטווח, מקרה המעבר הכפול של הסורק השטוח מתכווץ לקריאה אחת. הצד הקדמי רוצה את הסדר הטבעי שלו והצד האחורי רוצה 12-1, והקטע הריק הראשון לפני נקודה-הפסיק אומר שהמקור הראשון תורם את כל עמודיו
var
PDF: TPDFlib;
Target, Fronts, Backs: Integer;
begin
PDF := TPDFlib.Create;
try
Target := PDF.NewDocument;
if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
Exit;
Fronts := PDF.SelectedDocument;
if PDF.LoadFromFile('backs.pdf', '') <> 1 then
Exit;
Backs := PDF.SelectedDocument;
PDF.SelectDocument(Target);
// fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
PDF.SaveToFile('duplex.pdf');
finally
PDF.Free;
end;
end;
שתי התנהגויות בקטע הזה ראויות לציון מפורש. העמודים המאוספים מתווספים למסמך הנבחר, כך שמסמך שנוצר עם NewDocument תורם את עמוד הריק הראשוני שלו לפניהם ויש למחוק אותו אם אינך רוצה בו. והמקורות עשויים להיות לא אחידים: עם GroupSize 2 מעל מקור בן שלושה עמודים ומקור בן חמישה עמודים, הסבבים יוצאים A1 A2 B1 B2, ואז A3 B3 B4 כש-A כמעט מתרוקן, ואז B5 לבד, כי מקור שהתרוקן פשוט מדולג ולא מרופד
שחזור, שדות טופס, ומה לא מגיע איתם
כל ארגומנט מאומת לפני שנוגעים ביעד. מזהה מסמך חסר, המסמך הנבחר רשום כמקור של עצמו, GroupSize מתחת לאחד, מספר קטעים שלא תואם למספר המקורות, טווח שמציין עמוד שאין למקור: כל אלה מחזירים 0 כשהיעד ללא שינוי. כשל בזמן ההעתקה הוא המקרה הקשה יותר, והוא מטופל דרך DeletePages הציבורית ולא דרך PageTree.DeletePages הגולמית. הסיבה ספציפית. ההעתקה רצה עם MergeFormData מופעל, כך ששדות הטופס של המקור כבר נוספו למערך /AcroForm /Fields של היעד עד שמקור מאוחר יותר נכשל. מחיקת העמודים ברמת עץ העמודים הייתה מפשיטה את עמודי הווידג'ט ומשאירה את הפניות השדה האלה תלויות; הנתיב הציבורי מנתק את הפניות השדה, המתאר וחוט המאמרים לצד העמודים
if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
// Nothing was appended and the target is byte-identical to before.
// 412 is the copy failure; 0 means the arguments were rejected
// during validation, before any page was touched.
Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));
היה כן עם המשתמשים שלך בנוגע לגבולות. האיסוף נושא עמודים, ההערות שלהם ושדות הטופס שלהם, והוא ממזג את רשימת שדות ה-AcroForm, מערך סדר החישוב ומילון המשאבים הברירת מחדל. הוא לא נושא סימניות מקור: עץ המתאר של ערימה קדמית סרוקה הוא כמעט תמיד ריק, כך שדבר לא הולך לאיבוד במקרה הדופלקס, אבל אם אתה מאסף שני מסמכים מחוברים המתארים שלהם נשארים מאחור ואתה בונה מחדש את הניווט בעצמך. יעדים בעלי שם שחיו רק בקטלוג המקור נמצאים באותו מצב. תכנן לכך לפני שתבטיח ללקוח איסוף חסר אובדן
PDFlibPas משלוחה את פונקציות האיסוף יחד עם שאר משטח הרכבת העמודים שלה, כך שתהליך עבודת הסורק, החילוץ מבוסס-הטווח והנתיבים לקבצים גדולים כולם יושבים מאחורי רכיב אחד בדלפי וב-C++Builder. הפניית ה-API המלאה וגרסת ניסיון נמצאות בעמוד המוצר losLab Delphi PDF library