Технічна стаття

Вилучення зображень з PDF за допомогою компонента PDFium у Delphi

PDF зберігає зображення як об'єкти першого класу у своїх потоках вмісту. Коли сторінка посилається на фотографію, скан або діаграму, дані пікселів знаходяться у словнику XObject поряд із геометрією сторінки. PDFium Component надає доступ до цього через дві властивості TPdf: BitmapCount, яка повертає кількість вбудованих растрових зображень на поточній сторінці, та Bitmap[Index], яка декодує одне з них у TBitmap, яким ви володієте і який повинні звільнити. Це і є вся модель вилучення. Цикл складається з чотирьох рядків; що вимагає обдуманого підходу — це супутній код

Відкриття документа

Перше, що потрібно знати про TPdf, це те, що Active := True ніколи не викликає винятків (exceptions). Помилки завантаження, неправильні паролі, пошкоджені файли: всі вони поглинаються всередині, і компонент просто залишається неактивним. Ви повинні самостійно перевірити прапорець після присвоєння, інакше ви перейдете до циклу сторінок, де 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');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

Файли, захищені паролем, дотримуються того ж шаблону: призначте Pdf.Password перед встановленням Active := True. Якщо пароль неправильний, Active залишається False, і ви не отримуєте винятків для перехоплення. В інструменті пакетної обробки сотень файлів така "тиха" поведінка насправді корисна: ви накопичуєте невдачі у списку замість того, щоб розкручувати стек викликів (unwinding the call stack) для кожного з них

Перебір сторінок та витягнення растрових зображень

BitmapCount є властивістю на рівні сторінки, тому ви встановлюєте Pdf.PageNumber перед її читанням. Нумерація сторінок починається з 1; значення за замовчуванням — 0, що означає, що жодна сторінка не завантажена. Властивість Bitmap[Index] має індексацію з 0 і повертає TBitmap, яким володіє викликаюча сторона. Ви повинні звільнити його. Якщо знехтувати звільненням у довгому циклі під час обробки великого документа, споживання пам'яті швидко зросте, оскільки кожне растрове зображення може містити кілька мегабайт необроблених піксельних даних до будь-якого стиснення

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;

Перевірка Assigned має значення. Невелика кількість генераторів PDF записує XObjects зображень з нульовими розмірами пікселів або іншими некоректними даними; у таких випадках компонент повертає nil замість порожнього растрового зображення. Сприймати повернення nil як помилку та зупиняти вилучення — це неправильна реакція: пропустіть це, зафіксуйте сторінку та індекс у журналі, якщо вам потрібен контрольний слід (audit trail), і продовжуйте. Решта сторінки все ще може містити дійсні зображення

Зверніть увагу, що зовнішній цикл встановлює Pdf.PageNumber на кожній ітерації. Саме це присвоєння завантажує сторінку у внутрішній стан компонента і робить BitmapCount значущим. Пропустіть це, і ви будете постійно зчитувати кількість для тієї самої сторінки. Такий шаблон може здаватися надлишковим під час написання, але саме так спроєктовано API: сторінка є курсором, а не колекцією

Вибір вихідного формату

BMP працює без втрат якості і завжди доступний без додаткових модулів (units), що робить його надійним варіантом за замовчуванням, коли ви ще не знаєте, що містить зображення. Коли розмір файлу має значення, формат пікселів повернутого TBitmap підказує, який кодек є відповідним. 32-бітове растрове зображення містить альфа-канал; PNG зберігає його без втрат. Велике 24-бітове зображення з безперервним тоном (continuous tone) є кандидатом для JPEG. Менші зображення або ті, що намальовані з обмеженою палітрою, зазвичай краще залишати у форматі BMP, ніж пропускати через JPEG, який додає блокові артефакти при низьких налаштуваннях якості і мало що заощаджує при високих

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: lossless, no extra units
  end;
end;

На практиці вибір формату залежить від Bmp.PixelFormat та розмірів. Якщо PixelFormat = pf32bit, вам потрібен формат, який підтримує альфа-канал; PNG є очевидним вибором, хоча в старих версіях Delphi для цього потрібен модуль PNGImage. Для 24-бітових зображень шириною понад 300 пікселів JPEG із якістю 85 дає трикратне зменшення розміру порівняно з BMP без відчутної втрати якості у більшості фотографічного вмісту. Нижче цього порогу BMP є порівнянним за розміром і дозволяє взагалі уникнути будь-яких рішень щодо якості

Що BitmapCount враховує, а що ні

PDF розрізняє зображення XObjects і векторну графіку, намальовану за допомогою операторів контурів. Сторінка, яка візуально виглядає складною, може повернути BitmapCount рівним нулю, якщо кожен її елемент є векторним. Відскановані сторінки майже завжди повертають рівно одиницю: сканер записує весь скан як одне єдине повносторінкове зображення XObject з тією роздільною здатністю, на яку був налаштований сканер. Сторінки, де набірний текст поєднується з вбудованими фотографіями, повертають по одному запису на кожну фотографію. Декоративні лінії, затінені фони та межі таблиць зазвичай взагалі не відображаються у кількості растрових зображень

Кількість також не включає вбудовані зображення (inline images) — рідко використовувану конструкцію PDF, де дані зображення вбудовуються безпосередньо у потік вмісту сторінки, а не як іменований XObject. Вони виходять за рамки того, що охоплює цей API; вони настільки рідко зустрічаються в реальних документах, що більшість інструментів для вилучення просто не обробляють їх

Одна деталь, про яку варто пам'ятати: BitmapCount, який ви зчитуєте, стосується поточної сторінки станом на останнє присвоєння PageNumber. Якщо ваш код розгалужується або викликає будь-яку функцію, яка змінює PageNumber між підрахунком та отриманням, ви можете зчитати менше зображень, ніж виділили місця, або вийти за межі індексу (index past the end). Тримайте зчитування кількості та цикл перебору Bitmap[] на одній сторінці, не змінюючи PageNumber між ними

Використання TPdfView у віконному (form) додатку

Компонент TPdfView надає ті ж самі властивості BitmapCount та Bitmap[], але сторінка, з якої він читає, — це поточна відображувана сторінка переглядача, а не TPdf.PageNumber. Ці два вказівники на сторінки є незалежними; встановлення одного не переміщує інший. У віконному додатку VCL з активним переглядачем ви можете викликати Pdf.PageNumber := N, щоб керувати вилученням через TPdf, тоді як переглядач залишається на тому місці, куди востаннє прокрутив користувач. Цей поділ є навмисним і зберігає стан відображення переглядача чистим, поки виконується фонове вилучення

Пам'ять та продуктивність у пакетних завданнях

При роботі з великим архівом бюджет пам'яті — це головне, за чим слід стежити. Кожен виклик Bitmap[] виділяє новий TBitmap у купі (heap), і на відсканованій сторінці з роздільною здатністю 300 DPI це легко може скласти 25 МБ необроблених піксельних даних ще до будь-якого кодування. Якщо ви обробляєте сторінки у щільному циклі без звільнення пам'яті між ітераціями, робочий набір лінійно зростатиме пропорційно кількості зображень. Правильна форма завжди така: отримати одне растрове зображення, зробити те, що потрібно, звільнити його, отримати наступне. Якщо вам потрібно тримати посилання на кілька растрових зображень одночасно для етапу порівняння, спершу підрахуйте їх за допомогою BitmapCount і відповідним чином виділіть ваш контейнер, а потім звільняйте кожне зображення відразу після завершення роботи з ним, замість того, щоб відкладати очищення до кінця документа. Для документа на 500 відсканованих сторінок ця відмінність може означати різницю між піковим RSS у 25 МБ та 12 ГБ

Властивості BitmapCount та Bitmap[], показані тут, є частиною PDFium Component для Delphi та C++Builder