Technical Article

Извличане на изображения от PDF с PDFium VCL в Delphi

PDF съхранява изображенията като първокласни обекти в своите потоци от съдържание. Когато страница препраща към снимка, сканиран документ или диаграма, данните за пикселите се намират в XObject речник паралелно с геометрията на страницата. PDFium VCL предоставя достъп до тях чрез две свойства на TPdf: BitmapCount, което връща броя на вградените растерни изображения на текущата страница, и Bitmap[Index], което декодира едно от тях в TBitmap, който вие притежавате и трябва да освободите. Това е целият модел на извличане. Цикълът се състои от четири реда; това, което изисква преценка, е заобикалящата го архитектура.

Отваряне на документа

Първото нещо, което трябва да знаете за TPdf, е че Active := True никога не предизвиква изключение. Неуспешно зареждане, грешна парола, повреден файл â€?всички тези грешки се улавят вътрешно и компонентът просто остава неактивен. Трябва сами да проверите флага след присвояването, в противен случай ще преминете към цикъла за страници с 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 и няма изключение, което да прихванете. При инструмент за пакетна обработка на стотици файлове това тихо поведение всъщност е полезно: натрупвате неуспешните опити в списък, вместо да разгръщате стека на извикванията (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 като грешка и спирането на извличането е грешен рефлекс: пропуснете го, запишете в лога страницата и индекса, ако имате нужда от одитна следа, и продължете. Останалата част от страницата все още може да съдържа валидни изображения.

Обърнете внимание, че външният цикъл задава Pdf.PageNumber при всяка итерация. Това присвояване зарежда страницата във вътрешното състояние на компонента и прави BitmapCount смислено свойство. Пропуснете го и ще четете броя на изображенията на една и съща страница многократно. Този шаблон изглежда излишен при писане, но такъв е дизайнът на API: страницата е курсор, а не колекция.

Избор на изходен формат

Форматът BMP е без загуба на качество (lossless) и е винаги наличен без допълнителни модули, което го прави надежден избор по подразбиране, когато все още не знаете какво съдържа изображението. Когато размерът на файла е от значение, пикселният формат на върнатия TBitmap ви подсказва кой кодек е подходящ. 32-битовият bitmap съдържа алфа канал; 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: lossless, no extra units
  end;
end;

На практика изборът на формат се определя от Bmp.PixelFormat и размерите на изображението. Ако PixelFormat = pf32bit, имате нужда от формат, който поддържа алфа канал; PNG е очевидният избор, въпреки че изисква модула PNGImage в по-старите версии на Delphi. За 24-битови изображения, по-широки от приблизително 300 пиксела, JPEG с качество 85 предлага трикратно намаление на размера спрямо BMP без забележима загуба в качеството при повечето снимки. Под този праг BMP е съпоставим по размер и напълно спестява нуждата от вземане на решения за качеството.

Какво отчита и какво не отчита BitmapCount

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

Броят също така не включва вградени (inline) изображения â€?рядко използвана PDF структура, при която данните за изображението са вградени директно в потока от съдържание на страницата, а не като именуван XObject. Те остават извън възможностите на този API; те се срещат толкова рядко в реални документи, че повечето инструменти за извличане просто не ги поддържат.

Детайл, който си струва да помните: стойността на BitmapCount, която четете, се отнася за текущата страница към момента на последното задаване на PageNumber. Ако вашият код се разклонява или извиква функция, която променя PageNumber между преброяването и извличането, може да прочетете по-малко изображения от планираните или да излезете извън границите на индекса. Дръжте четенето на броя и цикъла Bitmap[] на една и съща страница, без да променяте PageNumber междувременно.

Използване на TPdfView във VCL приложение

Памет и производителност при пакетна обработка

При големи архиви най-важното нещо, което трябва да следите, е бюджетът на паметта. Всяко извикване на Bitmap[] заделя нов TBitmap в купчината (heap), което при сканирана страница с 300 DPI е лесно 25 MB необработени пикселни данни преди всякакво кодиране. Ако обработвате страниците в затворен цикъл без освобождаване на паметта между итерациите, обемът на заетата памет ще нараства линейно с броя на изображенията. Правилният подход винаги е: извличате един bitmap, извършвате необходимите операции, освобождавате го и извличате следващия. Ако трябва да пазите препратки към няколко растерни изображения едновременно за сравнение, първо ги пребройте с BitmapCount, разпределете съответно контейнера си и след това освобождавайте всяко едно веднага щом приключите с него, вместо да отлагате почистването за края на документа. При документ от 500 сканирани страници тази разлика може да бъде между 25 MB и 12 GB пикова заетост на паметта (peak RSS).

Компонентът TPdfView предлага същите свойства BitmapCount и Bitmap[], но страницата, от която чете, е текущо показаната страница на изгледа, а не TPdf.PageNumber. Двата указателя за страници са независими; промяната на единия не премества другия. Във VCL приложение с преглед на живо можете да извикате Pdf.PageNumber := N, за да управлявате извличането чрез TPdf, докато изгледът остава на страницата, която потребителят последно е прелистил. Това разделение е умишлено и запазва състоянието на дисплея на четеца чисто, докато извличането се изпълнява във фонов режим.

Свойствата BitmapCount и Bitmap[], показани тук, са част от компонента PDFium VCL Component за Delphi и C++Builder.