Техническа статия

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

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

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

Първото нещо, което трябва да знаете за TPdf, е, че Active := True никога не хвърля изключение (never 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 и не получавате изключение, което да хванете. В инструмент за пакетна обработка, обработващ стотици файлове, това тихо поведение всъщност е полезно: натрупвате неуспехите в списък, вместо да развивате стека на извикванията (call stack) за всеки един от тях

Итериране през страници и изтегляне на растерни графики

BitmapCount е за всяка страница, така че задавате Pdf.PageNumber, преди да го прочетете. Номерата на страниците са базирани на 1; по подразбиране е 0, което означава, че не е заредена страница. Свойството Bitmap[Index] е базирано на 0 и връща притежаван от извикващия (caller-owned) 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-битово изображение с непрекъснат тон е кандидат за 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: без загуби, без допълнителни модули
  end;
end;

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

Какво брои и какво не брои BitmapCount

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

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

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

Използване на TPdfView във формулярно (form) приложение

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

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

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

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