23 апреля 2021

Извлечение изображений из файла формата PDF

    Я уже писал о создании документов в формате PDF. Он представляет собой контейнер, который состоит из объектов различного типа: текста, изображений, шрифтов и так далее. Иногда может потребоваться операция обратная созданию – разобрать этот контейнер на составные части. Сегодня я рассмотрю извлечение изображений из PDF файлов и сохранение их на диск.
    Добавленные в документ формата PDF изображения хранятся как отдельные объекты. Это позволяет рисовать их на страницах документа сколько угодно раз без дальнейшего увеличения размера файла. Сначала рассмотрим функцию, которая извлекает из PDF-файла эти объекты. Для этого я воспользуюсь библиотекой Foxit Quick PDF Library (бывшая Debenu Quick PDF Library).
procedure ExtractImages(const sFileName, sPath: String);
var
  pdf: TDebenuPDFLibrary;
  iImageCount, iImageIndex,
  iImageID, iImageWidth, iImageHeight: Integer;
  sImageExt, sImageFileName: String;
begin
  pdf := TDebenuPDFLibrary.Create;
  try
    if pdf.LoadFromFile(sFileName, '') = 1
      then begin
        pdf.SetFindImagesMode(2);
        iImageCount := pdf.FindImages;
        for iImageIndex := 1 to iImageCount do
          begin
            // Идентификатор изображения
            iImageID := pdf.GetImageID(iImageIndex);
            if pdf.SelectImage(iImageID) = 1 then
              begin
                // Тип изображения
                case pdf.ImageType of
                  1: sImageExt := 'jpg';
                  2: sImageExt := 'bmp';
                  3: sImageExt := 'tif';
                  4: sImageExt := 'png';
                  else sImageExt := 'Unknown';
                end;
                // Размер изображения
                iImageWidth  := pdf.ImageWidth;
                iImageHeight := pdf.ImageHeight;

                sImageFileName := ExpandFileName(
                  Format('%s%s ObjNo=%d %dx%d.%s',
                        [sPath,
                         iImageIndex.ToString.PadLeft
                           (iImageCount.ToString.Length, '0'),
                         pdf.ImageObjectNumber,
                         iImageWidth, iImageHeight, sImageExt]));
                WriteLn(sImageFileName);
                pdf.SaveImageToFile(sImageFileName);
              end;
          end;
      end
      else WriteLn('File open error')
  finally
    pdf.Free;
  end;
end;
В исходном коде этой функции я отмечу два момента. Во-первых, вызов функции SetFindImagesMode, которая устанавливает режим поиска изображений в словаре ресурсов для функции FindImages. Он может принимать следующие значения:
  • 1 = "Default search" – рекурсивный поиск изображений только в ресурсах страниц и аннотаций в документе. Это самый быстрый метод и требует меньше всего памяти, но неиспользуемые изображения не будут найдены.
  • 2 = "Full search" – исследуется каждый объект в документе. Это занимает больше времени и требует больше памяти, но будут найдены все изображения, даже если они не используются ни одной из страниц или аннотаций.
  • 3 = "Default search, full convert" – отличается от режима 1 тем, что изображение конвертируется в bmp-формат.
  • 4 = "Full search, full convert" – отличается от режима 2 тем, что изображение конвертируется в bmp-формат.
Во-вторых, обратите внимание на используемую при формировании имени файла функцию ImageObjectNumber. Она возвращает номер объекта, в котором хранится изображение. Ее я добавил сам, т.к. подобного метода у Quick PDF нет (далее будет понятно, для чего он может быть полезен). Функция ImageObjectNumber обращается к приватным свойствам класса, поэтому ее нужно добавить в исходный код библиотеки или в class helper (об использовании class helper для доступа к private членам класса я писал год назад). Для проверки запустим эту функцию на одном из рекламных документов от Embarcadero (21 страница, 6.5МБ):
Извлечение изображений из файла формата PDF
    Следующая функция, извлекает изображения из PDF файла с привязкой к странице документа:
procedure ExtractImages(const sFileName, sPath: String);
var
  pdf: TDebenuPDFLibrary;
  iPageCount, iPageIndex,
  iImageListID, iImageCount, iImageIndex,
  iImageWidth, iImageHeight,
  iImageBitsPerPixel, iImageObjectNumber: Integer;
  sColorSpaceType,
  sImageExt, sImageFileName: String;
begin
  pdf := TDebenuPDFLibrary.Create;
  try
    if pdf.LoadFromFile(sFileName, '') = 1
      then begin
        iPageCount := pdf.PageCount;
        for iPageIndex := 1 to iPageCount do
          begin
            pdf.SelectPage(iPageIndex);
            // Список изображений на странице
            iImageListID := pdf.GetPageImageList(0);
            if iImageListID <> 0 then
            try
              iImageCount  := pdf.GetImageListCount(iImageListID);
              for iImageIndex := 1 to iImageCount do
                begin
                  // Тип изображения
                  case pdf.GetImageListItemIntProperty(iImageListID,
                                                       iImageIndex,
                                                       400) of
                    1: sImageExt := 'jpg';
                    2: sImageExt := 'bmp';
                    3: sImageExt := 'tif';
                    4: sImageExt := 'png';
                    else sImageExt := 'Unknown';
                  end;
                  // Размер изображения
                  iImageWidth := 
                    pdf.GetImageListItemIntProperty(iImageListID, 
                                                    iImageIndex,
                                                    401);
                  iImageHeight := 
                    pdf.GetImageListItemIntProperty(iImageListID, 
                                                    iImageIndex,
                                                    402);
                  // Глубина цвета
                  iImageBitsPerPixel := 
                    pdf.GetImageListItemIntProperty(iImageListID,
                                                    iImageIndex,
                                                    403);
                  // Тип цветового пространства
                  case pdf.GetImageListItemIntProperty(iImageListID, 
                                                       iImageIndex,
                                                       404) of
                    1: sColorSpaceType := 'Gray';
                    2: sColorSpaceType := 'RGB';
                    3: sColorSpaceType := 'CMYK';
                    else sColorSpaceType := '-';
                  end;
                  // Номер объекта изображения
                  iImageObjectNumber := 
                    pdf.GetImageListItemIntProperty(iImageListID,
                                                    iImageIndex, 407);

                  sImageFileName := ExpandFileName(
                    Format('%spage#%s img#%s ObjNo=%d %s bpp=%d %dx%d.%s',
                           [sPath,
                            iPageIndex.ToString.PadLeft
                              (iPageCount.ToString.Length, '0'),
                            iImageIndex.ToString.PadLeft
                              (iImageCount.ToString.Length, '0'),
                            iImageObjectNumber, sColorSpaceType, 
                            iImageBitsPerPixel, iImageWidth,
                            iImageHeight, sImageExt]));
                  WriteLn(sImageFileName);
                  pdf.SaveImageListItemDataToFile(iImageListID, iImageIndex, 
                                                  0, sImageFileName);
                end;
            finally
              pdf.ReleaseImageList(iImageListID);
            end;
          end;
      end
      else WriteLn('File open error')
  finally
    pdf.Free;
  end;
end;
Она обрабатывает документ по страницам. Для каждой страницы составляется список нарисованных на ней изображений, а затем они извлекаются и сохраняются на диск. Обратите внимание на свойство номер 407. Это номер объекта изображения, который я получил в первом варианте функции с помощью ImageObjectNumber. По этому номеру можно сгруппировать дублирующиеся изображения или сопоставить с результатами работы первой функции. Кроме использованных мной целочисленных свойств (GetImageListItemIntProperty) у элементов списка изображений есть еще вещественные свойства (GetImageListItemDblProperty). В текущей версии библиотеки вещественные свойства имеют ID от 501 до 508 и позволяют получить доступ к координатам изображения на странице.
    Извлечем изображения из документа по страницам:
Извлечение изображений из файла формата PDF по страницам
и сопоставим результаты работы этих функций:
  • первая извлекла из документа 169 изображений, а вторая по ссылкам на страницах 220 изображений;
  • изображение "002 ObjNo=43 1454x1122.jpg" в документе ни где не используется и только увеличивает его размер;
  • изображение "001 ObjNo=37 2559x1629.jpg" найдено на 7-й странице документа "page#07 img#1 ObjNo=37 RGB bpp=24 2559x1629.jpg";
  • изображение "ObjNo=222" является хорошим примером повторного использования объектов в документе. Только на 17-й странице оно используется 5 раз.
    Третий вариант функции, тоже извлекает изображения из PDF файла с привязкой к странице документа, но использует функции "прямого доступа" (Direct Access). Функции прямого доступа, в отличие от стандартных функций, не считывают весь PDF-файл в память, а загружая только то, что необходимо. Это позволяет ускорить обработку файла и снизить потребление оперативной памяти. Эти функции в основном используются для очень больших PDF-документов, содержащих тысячи страниц и размером более 500 МБ.
procedure ExtractImages(const sFileName, sPath: String);
var
  pdf: TDebenuPDFLibrary;
  iFileHandle,
  iPageCount, iPageIndex, iPageRef,
  iImageListID, iImageCount, iImageIndex,
  iImageWidth, iImageHeight, iImageBitsPerPixel,
  iImageObjectNumber: Integer;
  sColorSpaceType,
  sImageExt, sImageFileName: String;
begin
  pdf := TDebenuPDFLibrary.Create;
  try
    iFileHandle := pdf.DAOpenFileReadOnly(sFileName, '');
    if iFileHandle <> 0 then
      begin
        iPageCount := pdf.DAGetPageCount(iFileHandle);
        for iPageIndex := 1 to iPageCount do
          begin
            iPageRef := pdf.DAFindPage(iFileHandle, iPageIndex);
            if iPageRef <> 0 then
              begin
                // Список изображений на странице
                iImageListID := pdf.DAGetPageImageList(iFileHandle, iPageRef);
                if iImageListID <> 0 then
                 try
                   iImageCount  := pdf.DAGetImageListCount(iFileHandle,
                                                           iImageListID);
                   for iImageIndex := 1 to iImageCount do
                     begin
                       // Тип изображения
                       case pdf.DAGetImageIntProperty(iFileHandle,
                                                      iImageListID,
                                                      iImageIndex,
                                                      400) of
                         1: sImageExt := 'jpg';
                         2: sImageExt := 'bmp';
                         3: sImageExt := 'tif';
                         4: sImageExt := 'png';
                         else sImageExt := 'Unknown';
                       end;
                       // Размер изображения
                       iImageWidth  := 
                         pdf.DAGetImageIntProperty(iFileHandle, 
                                                   iImageListID,
                                                   iImageIndex,
                                                   401);
                       iImageHeight := 
                         pdf.DAGetImageIntProperty(iFileHandle,
                                                   iImageListID,
                                                   iImageIndex,
                                                   402);
                       // Глубина цвета
                       iImageBitsPerPixel := 
                         pdf.DAGetImageIntProperty(iFileHandle,
                                                   iImageListID,
                                                   iImageIndex,
                                                   403);
                       // Тип цветового пространства
                       case pdf.DAGetImageIntProperty(iFileHandle,
                                                      iImageListID,
                                                      iImageIndex, 
                                                      404) of
                         1: sColorSpaceType := 'Gray';
                         2: sColorSpaceType := 'RGB';
                         3: sColorSpaceType := 'CMYK';
                         else sColorSpaceType := '-';
                       end;
                       // Номер объекта изображения
                       iImageObjectNumber := 
                         pdf.DAGetImageIntProperty(iFileHandle,
                                                   iImageListID,
                                                   iImageIndex,
                                                   407);

                       sImageFileName := ExpandFileName(
                         Format('%spage#%s img#%s ObjNo=%d %s bpp=%d %dx%d.%s',
                                [sPath,
                                 iPageIndex.ToString.PadLeft
                                   (iPageCount.ToString.Length, '0'),
                                 iImageIndex.ToString.PadLeft
                                   (iImageCount.ToString.Length, '0'),
                                 iImageObjectNumber, sColorSpaceType,
                                 iImageBitsPerPixel, iImageWidth,
                                 iImageHeight, sImageExt]));
                       WriteLn(sImageFileName);
                       pdf.DASaveImageDataToFile(iFileHandle, iImageListID,
                                                 iImageIndex, sImageFileName);
                     end;
                 finally
                   pdf.DAReleaseImageList(iFileHandle, iImageListID);
                 end;
             end;
          end;
      end
      else WriteLn('File open error')
  finally
    pdf.Free;
  end;
end;
Эта функция работает аналогично предыдущей, только немного быстрее. Но есть небольшая проблема. Метод DAGetImageIntProperty не умеет получать номер объекта изображения (свойство номер 407 у GetImageListItemIntProperty). Значит эту возможность реализуем сами. Аналогично ImageObjectNumber, для этого нам необходим доступ к приватным членам класса. Что бы не изменять исходный код библиотеки, я сделал небольшой class helper:
unit ahQuickPDF;

interface

uses
  DebenuPDFLibrary1811;

type
  TQuickPDFHelper = class helper for TQuickPDF
    function ImageObjectNumber: Integer;
    function DAGetImageIntProperty(const FileHandle, 
                                         ImageListID,
                                         ImageIndex, 
                                         PropertyID: Integer): Integer;
  end;

implementation

uses
   DebenuPDFLibrarySmartAccess, DebenuPDFLibraryStructure,
   DebenuPDFLibraryImageList;

{ TQuickPDFHelper }

function TQuickPDFHelper.ImageObjectNumber: Integer;
begin
  with Self do
    if Assigned(FSelImage)
      then Result := FSelImage.IndRef.ObjNum
      else Result := 0;
end;

function TQuickPDFHelper.DAGetImageIntProperty
                     (const FileHandle, ImageListID,
                            ImageIndex, PropertyID: Integer): Integer;
var
  Link: TSmartPDFLink;
  ImageList: TPDFImageList;
begin
  Result := 0;
  with Self do
    begin
      Link := ValidFileHandle(FileHandle);
      if Assigned(Link) then
        begin
         ImageList := ValidImageListID(Link, ImageListID);
         if Assigned(ImageList) then
           if PropertyID = 407
             then begin
               var ImgRef: TPDFIndRef := ImageList.ImageRef(ImageIndex);
               if Assigned(ImgRef) then
                 Result := ImgRef.ObjNum;
             end
             else Result := Link.Doc.GetImageIntProperty(ImageList,
                                                         ImageIndex,
                                                         PropertyID);
        end;
    end;
end;

end.

Комментариев нет:

Отправить комментарий