Teknisk artikel

Extrahera bilder från PDF:er med PDFium Component i Delphi

PDF lagrar bilder som förstklassiga objekt inuti sina innehållsströmmar (content streams). När en sida refererar till ett fotografi, en skanning eller ett diagram, lever pixeldatan i en XObject-ordbok (XObject dictionary) vid sidan av sidgeometrin. PDFium Component synliggör (surfaces) det genom två egenskaper på TPdf: BitmapCount, som returnerar hur många inbäddade bitmappar som finns på den aktuella sidan, och Bitmap[Index], som avkodar (decodes) en av dem till en TBitmap som du äger och måste frigöra. Det är hela extraheringsmodellen. Loopen är fyra rader; vad som kräver omdöme (judgment) är de omgivande rördragningarna (plumbing)

Att öppna dokumentet

Den första saken att veta om TPdf är att Active := True aldrig utlöser (raises). Laddningsfel, fel lösenord, korrupta filer: alla sväljs internt och komponenten förblir helt enkelt inaktiv. Du måste själv kontrollera flaggan efter tilldelningen, annars fortsätter du in i sid-loopen (page loop) med att PageCount returnerar noll och undrar varför ingenting extraherades

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;

Lösenordsskyddade filer följer samma mönster: tilldela Pdf.Password innan du sätter Active := True. Om lösenordet är fel stannar ActiveFalse och du får inget undantag att fånga. I ett satsverktyg (batch tool) som bearbetar hundratals filer är det tysta beteendet faktiskt användbart: du ackumulerar (accumulate) felen i en lista i stället för att nysta upp anropsstacken (unwinding the call stack) för varje enskilt fel

Att iterera sidor och dra bitmappar

BitmapCount är per sida, så du sätter Pdf.PageNumber innan du läser den. Sidnummer är 1-baserade; standardvärdet är 0, vilket innebär att ingen sida är laddad. Egenskapen Bitmap[Index] är 0-baserad och returnerar en uppringare-ägd (caller-owned) TBitmap. Du måste frigöra den. Försumma (neglect) frigörelsen inuti en lång loop över ett stort dokument, och minnet klättrar snabbt, eftersom varje bitmapp kan vara flera megabyte av rå pixeldata före eventuell komprimering

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-skyddet spelar roll. Ett litet antal PDF-generatorer skriver bild-XObjects med nollpixeldimensioner eller annan felformaterad data (malformed data); i de fallen returnerar komponenten nil snarare än en tom bitmapp. Att behandla en nil-retur som ett fel och stoppa extraheringen är fel reflex: hoppa över den, logga sidan och indexet om du behöver granskningsspåret (audit trail), och fortsätt. Resten av sidan kan fortfarande ge giltiga bilder

Lägg märke till att den yttre loopen sätter Pdf.PageNumber i varje iteration. Den tilldelningen är vad som laddar sidan in i komponentens interna tillstånd och gör BitmapCount meningsfull. Hoppa över den och du läser samma sidas antal upprepade gånger. Mönstret känns överflödigt när du skriver det, men det är så API:et är designat: sidan är en markör (cursor), inte en samling

Att välja ett utdataformat

BMP är förlustfritt och alltid tillgängligt utan extra enheter (units), vilket gör det till en sund standard när du ännu inte vet vad bilden innehåller. När filstorleken spelar roll berättar pixelformatet på den returnerade TBitmap vilken kodek (codec) som är lämplig. En 32-bitars bitmapp bär en alfakanal (alpha channel); PNG bevarar den utan förlust. En stor 24-bitars bild med kontinuerlig ton (continuous tone) är en kandidat för JPEG. Mindre bilder eller sådana som är ritade med en begränsad palett (palette) lämnas i allmänhet bättre som BMP än att köras genom JPEG, vilket lägger till blockeringsartefakter (blocking artifacts) vid låga kvalitetsinställningar och sparar lite vid höga

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;

I praktiken drivs formatvalet av Bmp.PixelFormat och dimensioner. Om PixelFormat = pf32bit behöver du ett format som bär alfa; PNG är det uppenbara valet, även om det kräver PNGImage-enheten (unit) i äldre Delphi-versioner. För 24-bitars bilder bredare än ungefär 300 pixlar ger JPEG med kvalitet 85 en tre-till-ett-storleksminskning över BMP utan märkbar förlust i det mesta fotografiska innehållet. Under den tröskeln är BMP jämförbart i storlek och undviker helt alla kvalitetsbeslut

Vad BitmapCount räknar och inte räknar

PDF skiljer på bild-XObjects och vektorgrafik ritad med banoperatörer (path operators). En sida som ser visuellt komplex ut kan returnera en BitmapCount på noll om varje element är vektor. Skannade sidor returnerar nästan alltid exakt en: skannern skriver hela skanningen som ett enda fullsides bild-XObject med vilken upplösning skannern än var inställd på. Sidor som blandar typsatt (typeset) text med inbäddade fotografier returnerar en post per fotografi. Dekorativa linjer (rule lines), skuggade bakgrunder och tabellramar (table borders) visas vanligtvis inte alls i bitmappsantalet

Antalet inkluderar inte heller inbäddade bilder (inline images), en sällan använd PDF-konstruktion där bilddata bäddas in direkt i sidans innehållsström snarare än som ett namngivet XObject. Dessa faller utanför vad detta API synliggör (surfaces); de är tillräckligt ovanliga i riktiga dokument för att de flesta extraheringsverktyg helt enkelt inte hanterar dem

En detalj värd att komma ihåg: den BitmapCount du läser är för den aktuella sidan enligt den senaste PageNumber-tilldelningen. Om din kod grenar (branches) eller anropar någon funktion som ändrar PageNumber mellan räknandet och hämtningen, kan du läsa in färre bilder än vad du allokerade utrymme för, eller indexera (index) förbi slutet. Håll räkningen (the count read) och Bitmap[]-loopen på samma sida utan att röra PageNumber däremellan

Att använda TPdfView i en formulärapplikation

TPdfView-komponenten exponerar (exposes) samma egenskaper, BitmapCount och Bitmap[], men sidan den läser från är visarens för närvarande visade sida, inte TPdf.PageNumber. De två sidpekarna (page pointers) är oberoende; att ställa in den ena flyttar inte den andra. I en VCL-formulärapplikation med en live-visare (live viewer) kan du anropa Pdf.PageNumber := N för att driva extrahering genom TPdf medan visaren stannar på vad användaren senast skrollade till. Denna separation är avsiktlig och håller visarens visningstillstånd (display state) rent medan en bakgrundsextrahering körs

Minne och prestanda i satsjobb

Över ett stort arkiv är minnesbudgeten det viktigaste (the main thing) att hålla koll på. Varje Bitmap[]-anrop allokerar en ny TBitmap på heapen (the heap), och på en 300 DPI skannad sida är det lätt 25 MB av rå pixeldata före eventuell kodning. Om du bearbetar sidor i en snäv loop (tight loop) utan att frigöra mellan iterationerna, växer arbetsmängden linjärt med antalet bilder. Den korrekta formen (shape) är alltid: hämta en bitmapp, gör vad du behöver, frigör den, hämta nästa. Om du behöver hålla referenser till flera bitmappar samtidigt för ett jämförelsesteg, räkna dem först med BitmapCount och allokera din behållare (container) därefter, och frigör sedan var och en så fort du är klar med den snarare än att skjuta upp det till dokumentets slutstädning. På ett dokument med 500 skannade sidor kan den distinktionen innebära skillnaden mellan 25 MB och 12 GB maximal RSS (peak RSS)

Egenskaperna BitmapCount och Bitmap[] som visas här är en del av PDFium Component för Delphi och C++Builder