מאמר טכני

חילוץ תמונות מ-PDF עם PDFium Component ב-Delphi

PDF מאחסן תמונות כאובייקטים מסוג ראשון בתוך זרמי התוכן שלו. כאשר עמוד מפנה לתצלום, סריקה או תרשים, נתוני הפיקסלים יושבים במילון XObject לצד הגיאומטריה של העמוד. PDFium Component מציף זאת דרך שני מאפיינים ב-TPdf: BitmapCount, המחזיר כמה מפות-סיביות (bitmaps) מוטמעות יש בעמוד הנוכחי, ו-Bitmap[Index], אשר מפענח אחת מהן ל-TBitmap שנמצא בבעלותך ועליך לשחררו. זהו כל מודל החילוץ (extraction model). הלולאה היא ארבע שורות; מה שדורש שיקול דעת היא הצנרת (plumbing) מסביב

פתיחת המסמך

הדבר הראשון שיש לדעת על TPdf הוא ש-Active := True לעולם אינו מעלה חריגה (raises). כשלונות טעינה, סיסמאות שגויות, קבצים פגומים: כולם נבלעים פנימה והרכיב פשוט נשאר לא פעיל. עליך לבדוק את הדגל בעצמך לאחר ההקצאה, או שתמשיך ללולאת העמודים עם PageCount שמחזיר אפס ותתפלא מדוע שום דבר לא חולץ

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // המשך לחילוץ
  finally
    Pdf.Free;
  end;
end;

קבצים המוגנים בסיסמה פועלים לפי אותו דפוס: הקצה Pdf.Password לפני הגדרת Active := True. אם הסיסמה שגויה, Active נשאר False ואינך מקבל שום חריגה לתפוס. בכלי אצווה (batch tool) המעבד מאות קבצים, ההתנהגות השקטה הזו למעשה שימושית: אתה צובר את הכשלים ברשימה במקום לפרוק (unwinding) את מחסנית הקריאות (call stack) עבור כל אחד מהם

איטרציה על עמודים ושליפת מפות-סיביות

BitmapCount הוא פר-עמוד, לכן עליך להגדיר Pdf.PageNumber לפני קריאתו. מספרי העמודים מבוססים-1; בררת-המחדל היא 0, כלומר שום עמוד אינו טעון. המאפיין Bitmap[Index] מבוסס-0 ומחזיר TBitmap בבעלות-הקורא (caller-owned). עליך לשחרר אותו. הזנחת השחרור (free) בתוך לולאה ארוכה על מסמך גדול גורמת לזיכרון לטפס מהר, משום שכל מפת-סיביות (bitmap) יכולה להיות כמה מגה-בתים של נתוני פיקסלים גולמיים לפני כל דחיסה

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

הגנת (guard) ה-Assigned חשובה. מספר קטן של מחוללי PDF כותבים אובייקטי XObject לתמונה עם אפס ממדי פיקסל או נתונים פגומים אחרים; במקרים אלו הרכיב מחזיר nil במקום מפת-סיביות ריקה. התייחסות להחזרת nil כאל שגיאה והפסקת החילוץ היא רפלקס שגוי: דלג על זה, תעד ביומן (log) את העמוד והאינדקס אם אתה זקוק לנתיב ביקורת (audit trail), והמשך. שאר העמוד עשוי עדיין להניב תמונות תקינות

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

בחירת פורמט פלט

BMP הוא חסר-אובדן (lossless) ותמיד זמין ללא יחידות (units) נוספות, מה שהופך אותו לבררת-מחדל מבוססת כאשר אינך יודע עדיין מה התמונה מכילה. כאשר גודל הקובץ חשוב, פורמט הפיקסלים (pixel format) של ה-TBitmap המוחזר אומר לך איזה קודק מתאים. מפת-סיביות (bitmap) של 32-ביט נושאת ערוץ אלפא; PNG שומר על כך ללא אובדן. תמונה גדולה של 24-ביט עם גוון רציף היא מועמדת ל-JPEG. תמונות קטנות יותר או כאלו שצוירו עם פלטה מוגבלת עדיף בדרך כלל להשאיר כ-BMP מאשר להריץ דרך JPEG, מה שמוסיף ארטיפקטים של חסימה (blocking artifacts) בהגדרות איכות נמוכות וחוסך מעט בהגדרות גבוהות

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: ללא אובדן, ללא יחידות נוספות
  end;
end;

בפועל, בחירת הפורמט מונעת על ידי Bmp.PixelFormat וממדים. אם PixelFormat = pf32bit אתה זקוק לפורמט שנושא אלפא; PNG הוא הבחירה הברורה, אם כי הוא דורש את היחידה PNGImage בגרסאות Delphi ישנות יותר. עבור תמונות 24-ביט הרחבות בערך מ-300 פיקסלים, JPEG באיכות 85 נותן הקטנת גודל של שלוש-לאחד לעומת BMP ללא אובדן מורגש ברוב התוכן הצילומי. מתחת לסף זה, BMP ניתן להשוואה בגודל ונמנע לחלוטין מכל החלטת איכות

מה BitmapCount סופר ומה לא

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

הספירה גם אינה כוללת תמונות בתוך-השורה (inline images), מבנה PDF בשימוש נדיר שבו נתוני התמונה מוטמעים ישירות בזרם תוכן העמוד ולא כ-XObject בעל שם. אלו נופלים מחוץ למה שה-API הזה מציף; הם נדירים מספיק במסמכים אמיתיים עד שרוב כלי החילוץ פשוט אינם מטפלים בהם

פרט אחד ששווה לזכור: ה-BitmapCount שאתה קורא הוא עבור העמוד הנוכחי נכון להקצאת ה-PageNumber האחרונה. אם הקוד שלך מסתעף או קורא לפונקציה כלשהי שמשנה את PageNumber בין הספירה לשליפה, אתה עלול לקרוא פחות תמונות ממה שהקצית להן מקום, או לאנדקס (index) אל מעבר לסוף. שמור על קריאת הספירה ועל לולאת ה-Bitmap[] באותו עמוד מבלי לגעת ב-PageNumber ביניהם

שימוש ב-TPdfView באפליקציית טופס

הרכיב TPdfView חושף את אותם מאפייני BitmapCount ו-Bitmap[], אבל העמוד שממנו הוא קורא הוא העמוד המוצג כעת בתצוגה, לא TPdf.PageNumber. שני מצביעי-העמודים הם עצמאיים; הגדרת האחד אינה מזיזה את האחר. באפליקציית טופס VCL עם מציג (viewer) חי, אתה יכול לקרוא ל-Pdf.PageNumber := N כדי לדחוף חילוץ דרך TPdf בעוד המציג נשאר על כל מה שהמשתמש גלל אליו לאחרונה. ההפרדה הזו היא מכוונת ושומרת על מצב התצוגה של המציג נקי בזמן שחילוץ ברקע רץ

זיכרון וביצועים בעבודות אצווה

על פני ארכיון גדול תקציב הזיכרון הוא הדבר העיקרי שיש לעקוב אחריו. כל קריאת Bitmap[] מקצה TBitmap חדש בערימה (heap), ובעמוד סרוק ב-300 DPI זה בקלות 25 מגה-בתים של נתוני פיקסלים גולמיים לפני כל קידוד. אם תעבד עמודים בלולאה הדוקה (tight loop) מבלי לשחרר בין איטרציות, ה-working set (סט העבודה) יגדל ליניארית עם מספר התמונות. הצורה הנכונה היא תמיד: שלוף מפת-סיביות אחת, עשה מה שאתה צריך, שחרר אותה, שלוף את הבאה. אם עליך להחזיק הפניות לכמה מפות-סיביות בו-זמנית עבור שלב השוואה, ספור אותן תחילה עם BitmapCount והקצה את המכל שלך בהתאם, ואז שחרר כל אחת ברגע שסיימת איתה במקום לדחות זאת לניקוי בסוף-המסמך. במסמך עם 500 עמודים סרוקים, ההבחנה הזו יכולה להיות ההבדל בין שיא RSS של 25 מגה-בתים לבין 12 ג'יגה-בתים

מאפייני BitmapCount ו-Bitmap[] המוצגים כאן הם חלק מ-PDFium Component עבור Delphi ו-C++Builder