מאמר טכני

שילוב תמונות סרוקות ל-PDF אחד עם PDFium Component

לצוות עיבוד-תביעות היו שלושים שנות תיקי נייר שעברו דרך סורק מוזן-דפים. הסורק ירק JPEG אחד לכל עמוד לתוך תיקייה, בשמות 0001.jpg, 0002.jpg וכן הלאה. מה שהארכיון היה באמת צריך זה PDF אחד לכל תיק, עם העמודים לפי הסדר, כך שסוקר יוכל לפתוח מסמך יחיד במקום ללחוץ על מאה תמונות ממוזערות של תמונות. הצעד האחרון הזה, הפיכת ערימה ממוספרת של סריקות ל-PDF יחיד מסודר, היא העבודה כאן

PDFium Component מטפל בזה ישירות. מעבר לרינדור וחילוץ טקסט, המרכיב יכול לבנות PDF מאפס: ליצור מסמך ריק, להוסיף עמוד ריק באיזה גודל שתרצה, לזרוק תמונה על העמוד הזה בקואורדינטות של מרחב-משתמש (user-space), ואז לשמור. כל צינור העיבוד (pipeline) יושב על רכיב TPdf, כך שממיר אצווה (batch) הוא לולאה על שמות קבצים פלוס קומץ קריאות

צורת ההמרה

שלושה דברים צריכים לקרות עבור כל סריקה. אתה מחליט על גודל העמוד, אתה ממקם את התמונה בתוך העמוד תוך השארת שוליים, ואתה מתקדם לעמוד הבא. PDFium Component נותן לך מתודה אחת לכל אחד: AddPage יוצר עמוד ריק בגודל נתון, AddImage (או AddPicture אם אתה כבר מחזיק ב-TPicture) מצייר את מפת-הסיביות (bitmap) לתוך העמוד הנוכחי, ו-PageNumber אומר לרכיב לאיזה עמוד מכוונות קריאות הציור הבאות

הפרט היחיד שמכשיל אנשים הוא מערכת הקואורדינטות. מרחב המשתמש של PDF שם את המקור (origin) בפינה השמאלית-תחתונה של העמוד, כאשר Y גדל כלפי מעלה, ההפך מקואורדינטות המסך שמפתחי Delphi מושיטים אליהן יד כרפלקס. ה-X, Y שאתה מעביר ל-AddImage הם הפינה השמאלית-תחתונה של מלבן התמונה, ו-Width, Height הם גודל המיקום בנקודות, לא גודל הפיקסלים של קובץ המקור. אם תתבלבל בזה, הסריקות שלך ינחתו מחוץ לעמוד או הפוכות יחסית למה שציפית להן

יצירת המסמך ועמוד לכל סריקה

התחל עם מסמך ריק. CreateDocument מקצה PDF טרי ומשאיר את המרכיב (component) פעיל, כך שאין צעד פתיחה נפרד. משם אתה עובר על רשימת הקבצים הסרוקים, ועבור כל אחד אתה מוסיף עמוד, הופך אותו לנוכחי וממקם את התמונה. ממדי העמוד כאן הם A4 בנקודות (595 × 842 לרוחב/דיוקן - portrait), גודל הגיליון הסטנדרטי להתכתבויות בארכיון

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // רוחב A4 בנקודות
  PageH = 842.0;   // גובה A4 בנקודות
  Margin = 36.0;   // גבול של חצי אינץ' סביב כל סריקה
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // חדש, ריק, כבר פעיל
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // אינדקס עמודים מבוסס-1
      Pdf.PageNumber := I + 1;                // הפוך את העמוד החדש לנוכחי
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

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

הנחה אחת מסתתרת בלולאה הזו: הסדר של Files. סורק קורא לעמודים בשמות 0001.jpg עד 0100.jpg, אך ספירת תיקייה (directory enumeration) לא תמיד מחזירה אותם ממוינים, וברגע שאתה פוגע ב-page9.jpg ליד page10.jpg, מיון מחרוזות פשוט שם את עמוד 10 לפני עמוד 9. מיין את הרשימה במפורש לפני הלולאה, והעדף שמות מרופדים באפסים (zero-padded) בזמן הסריקה כך שסדר לקסיקלי יתאים לסדר העמודים. רצף עמודים הוא הדבר היחיד שסוקר מבחין בו מיד, וזו הטעות הזולה ביותר למניעה

מיקום סריקה ושמירה על יחס הרוחב-גובה שלה

סריקה היא לעתים רחוקות באותה צורה כמו העמוד. אם תמתח אותה כדי למלא את הגיליון תעוות את הטקסט; אם תמקם אותה בגודל פיקסל מלא היא תגלוש החוצה. התיקון הוא לשנות קנה מידה (scale) לפי הקטן מבין שני היחסים, התאמה-לרוחב או התאמה-לגובה, ולמרכז את מה שנשאר. מכיוון שהמקור (origin) יושב בשמאל-למטה, מרכוז פירושו פיצול החלל הנותר שווה בשווה והוספתו גם ל-X וגם ל-Y

procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG וכו' דרך יחידות הגרפיקה של VCL
    
    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;
    
    // התאם לתוך השוליים מבלי לעוות את הסריקה.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;
    
    // מרכז: החלל הנותר מחולק שווה בשווה. Y נמדד מתחתית העמוד.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;
    
    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

זה טוען את הקובץ פעם אחת כדי לקרוא את ממדי הפיקסלים שלו, מחשב קנה מידה אחיד יחיד, ומעביר את מלבן המיקום ל-AddImage. הפקודה AddImage מקבלת נתיב קובץ ישירות ומנתבת אותו דרך אותו צינור תמונות כמו AddPicture, כך שכל פורמט שיחידות הגרפיקה של VCL מזהות פועל ללא טיפול במקרה-מיוחד (special-casing). אם התמונה כבר פוענחה ב-TPicture מחלונית תצוגה מקדימה, קרא ל-AddPicture(Pic, X, Y, DrawW, DrawH) עם אותו מלבן ודלג על הקריאה השנייה לקובץ

דילוג על הפענוח עבור סריקות JPEG

סורקים כמעט תמיד פולטים JPEG. טעינת JPEG לתוך TPicture מפענחת אותו למפת-סיביות (bitmap), ואז PDFium מקודד אותו מחדש בשמירה, שני סבבי התלויה מאבדת-נתונים (lossy round trips) שאינך צריך. AddJpegImage מטמיע את הבתים הדחוסים המקוריים ישר לתוך העמוד מתוך זרם (stream), וזה גם מהיר יותר וגם נקי יותר חזותית עבור אצווה (batch) בנפח גבוה

var
  Stream: TFileStream;
begin
  // ... לאחר AddPage + PageNumber עבור העמוד הנוכחי ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // מטמיע את בתי ה-JPEG כפי שהם; אין מחזור פענוח/קידוד-מחדש.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

אתה עדיין מחשב את X, Y, DrawW, ו-DrawH באותה דרך, מאחר ואתה זקוק לממדי הפיקסלים לשם קנה-מידה. קרא אותם מהקובץ או מניתוח (parse) מהיר של הכותרת, ואז העבר את הזרם הגולמי ל-AddJpegImage. עבור סריקות PNG או TIFF, הנתיב של AddImage הוא הנכון; שמור את קיצור הדרך ל-JPEG לפורמט שעליו הוא אכן חל

תיוג כל עמוד

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

// כיתוב מתחת לסריקה: Y קטן כלפי תחתית העמוד.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

נקודה אחרונה לגבי שמירה. SaveAs היא פונקציה שמחזירה Boolean, לכן בקוד ייצור (production code) בדוק את התוצאה שלה במקום להניח שהכתיבה הצליחה; אחרת, דיסק מלא או נתיב פלט נעול ייכשלו בשקט. ברגע שהלולאה מסתיימת והקובץ נכתב, יש לך בדיוק את מה שהארכיון היה צריך: PDF מסודר אחד לכל תיק, עמודים בקנה-מידה מותאם (scaled to fit), ומוכנים לקריאה בכל מציג

אותן אבני בניין מכסות עבודות קשורות. החלף את כלל שינוי-הגודל לכל-עמוד-בנפרד ותקבל ספר תמונות עם תמונה אחת בכל גיליון; שמור את הלולאה אך קרא ממקור מרובה-עמודים (multi-page) מסוג TIFF ויש לך ממיר ארכיון פקס. אם אתה רוצה תמונה רחבה יותר של בניית קבצי PDF באופן תכנותי, ראה יצירת מסמכי PDF מאפס עם PDFium Component; כדי לרנדר את התוצאה בחזרה למסך מאוחר יותר, ראה המרת עמודי PDF לתמונות JPEG עם PDFium Component

PDFium Component מבית loslab.com מאגד את ממשקי ה-API ליצירת מסמכים, רינדור וטקסט המשמשים לאורך כל סדרה זו