Teknisk artikel

Render PDF-sider til JPEG-billeder i Delphi med PDFium Component

At rendere en PDF-side til en JPEG er to operationer, som folk har tendens til at køre sammen og derefter fejlsøge separat. Først rasteriserer du siden til et pixelbitmap i en opløsning, du vælger. Derefter giver du dette bitmap til en JPEG-indkoder og vælger en kvalitet. PDFium Component ejer den første halvdel gennem RenderPage; den anden halvdel er ren VCL, TJPEGImage fra Vcl.Imaging.jpeg. Sømmen mellem dem er der, hvor de interessante beslutninger lever, fordi den opløsning, du vælger på rendersiden, og den kvalitet, du vælger på indkodersiden, afvejes mod hinanden og mod filstørrelse på måder, der er lette at få forkerte

Det, man skal internalisere før nogen kode: en PDF-side har ingen pixels. Den er beskrevet i punkter, hvor ét punkt er 1/72 tomme, og siden er en vektortegning målt i disse punkter. Når du beder PDFium om at rendere, vælger du, hvor mange pixels den tegning skal projiceres over på, og det valg er DPI'en. Får du aritmetikken forkert, render du enten en sløret miniature, når du ønskede en printmaster, eller du allokerer et 200-megapixel bitmap til noget, der er bestemt til at være en 120-pixel forhåndsvisning

Fra DPI til pixeldimensioner

RenderPage ønsker heltalspixels for Width og Height, ikke en DPI. Så den første opgave er at konvertere. En side rapporterer sin størrelse i punkter gennem PageWidth og PageHeight (begge Double), og konverteringen er den samme, som enhver rasteriserer bruger: pixels er lig punkter gange mål-DPI divideret med 72. En US Letter-side er 612 gange 792 punkter. Ved 150 DPI bliver det 1275 gange 1650 pixels; ved 72 DPI forbliver det 612 gange 792, én pixel pr. punkt, hvilket er det tilfælde, folk glemmer, blot er identiteten

// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free;   // the function-form RenderPage hands you ownership

To detaljer i disse fire linjer bestemmer, om koden er korrekt. Den første er, at funktionsformen af RenderPage returnerer en TBitmap, som du ejer. PDFium allokerede den og gik sin vej; hvis du ikke kalder Free på den i hver iteration, vil en batch over et par hundrede sider lække et par hundrede bitmaps, og processen svulmer op, indtil noget vælter. Den anden er argumentet Color, her clWhite. PDF-sider tegnes normalt under forudsætning af et uigennemsigtigt hvidt underlag, og en side med gennemsigtighed renderet på den forkerte baggrundsfarve producerer grumsede kanter eller vildfarne mørke glorier. Hvid er den rigtige standard for næsten ethvert dokument; parameteren eksisterer til det sjældne tilfælde, hvor den ikke er

0, 0 er Left og Top forskydningerne ind på siden, i det skalerede koordinatrum, og du lader dem stå på nul, medmindre du beskærer. ro0 er rotation: lad den stå på nul, og PDFium ærer den rotation, siden allerede deklarerer i sin /Rotate-post, så en side, der er forfattet i landskab (landscape), kommer ud i landskab, uden at du gør noget

Indkodning af bitmappet som JPEG

Når bitmappet eksisterer, er JPEG den nemme del, og det er ren Delphi. TJPEGImage.Assign kopierer bitmappet ind, CompressionQuality sætter kvaliteten på en 1 til 100 skala, og SaveToFile skriver filen. Den eneste rækkefølgeregel er, at kvaliteten skal indstilles før du gemmer, fordi den styrer den indkodning, som SaveToFile udløser

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

Den indlejrede try/finally ser kræsen ud for en enkeltsides-hjælper, og den er præcis rigtig til en batch-kørsel. Den indre blok frigiver indkoderen, den ydre blok frigiver bitmappet, og hvis en af dem udløser en undtagelse, frigiver den stadig, hvad den ejer. Slår du dem sammen til én, kan en undtagelse under indkodningen strande bitmappet. Over et langt forløb er det forskellen mellem en konverter, der bliver færdig, og en, der dør på side 300 med en korrupt fil og en hukommelsesfejl-dialog (out-of-memory)

Valg af DPI og kvalitet sammen

De to knapper er ikke uafhængige af outputtets formål, og den almindelige fejl er at skrue op for begge af forsigtighed. En web-miniature renderet ved 300 DPI og gemt i kvalitet 95 er flere hundrede kilobytes, der lader som om, det er et 120-pixel billede; browseren smider næsten det hele væk ved nedskaleringen. Match opløsningen til de pixels, outputtet faktisk behøver, og vælg derefter en kvalitet, der overlever JPEG's tabsgivende komprimering uden synlige artefakter

OutputDPIJPEG quality
Liste-miniature7260-70
Skærmvisning96-15080-85
Højdetaljevisning200-30085-95
Printmaster300-60090-100

JPEG-kvalitet fortjener et par advarende ord i sig selv. Det er ikke en lineær skala. Springet fra 70 til 85 køber en reel visuel forbedring for en beskeden filvækst; springet fra 95 til 100 fordobler stort set filen for en forskel, som næsten ingen kan se, fordi kvalitet 100 stadig ikke er tabsfri, den holder bare op med at kassere meget. For teksttunge sider udtværer JPEGs blokbaserede komprimering de skarpe kanter af glyphs til en svag ringen, hvilket er grunden til, at kvalitet under ca. 80 giver et scannet udseende af tekst på det, der burde være sprødt output. Hvis siderne overvejende er tekst, og du kan skifte format, render PNG denne tekst uden ringen; JPEG fortjener sin plads på fotografisk og blandet indhold, hvor dens komprimering er ægte mindre

Hurtigere, mindre miniaturer

Når målet er en miniature frem for en trofast reproduktion, kan du fortælle renderen, at den skal udføre mindre arbejde. Parameteren Options tager et sæt TRenderOption-flag, og et par af dem bytter troskab for hastighed på nøjagtig den måde, en lille forhåndsvisning ønsker. reGrayscale smider farverne, hvilket både renderes hurtigere og producerer et mindre bitmap at indkode. reNoSmoothImage og reNoSmoothPath springer anti-aliasing over, som alligevel er usynlig på miniatureskala

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Fit the page inside MaxW x MaxH while preserving aspect ratio.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

Miniaturetilfældet viser også den renere måde at tænke på størrelsesændring på. I stedet for at gå gennem DPI, beregner du en enkelt skaleringsfaktor, der tilpasser siden inden for en afgrænsningsboks og bevarer billedformatet, hvilket er, hvad Min af de to forhold gør. Både en portrætside og en landskabsside ender inde i den samme boks uden forvrængning, og du behøver aldrig at ræsonnere over, hvilken DPI der svarer til "tilpas til 200 gange 280." Et forbehold ved reGrayscale: den konverterer rasterbilledindhold til gråt, men vektorfyldninger og tekst beholder deres farveværdier i maskinen, så en side, der for det meste er vektorkunst, kan komme tilbage mindre monokrom, end flagets navn antyder. For et ægte fuldt gråtoneresultat er konvertering af det renderede bitmap med GrayscalePdfBitmap den pålidelige vej

Batch-behandling af et helt dokument

At sammensætte det til et helt dokument er en løkke over PageCount, hvor PageNumber flyttes én side ad gangen. Sider er 1-baserede: side et er PageNumber := 1, og løkken kører til og med PageCount, ikke PageCount - 1. Den anden ting, batchen skal respektere, er kontrakten om lydløs indlæsning. At sætte Active := True kaster aldrig en undtagelse på en beskadiget fil eller en forkert adgangskode; det efterlader bare ActiveFalse. Tjek den, før du render en enkelt side, ellers virker den første RenderPage mod et dokument, der aldrig blev åbnet

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // zero-pad so files sort right
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Nulfyldningen via Digits er en lille ting, der sparer en eftermiddag senere. Navngiver du filerne page_1.jpg til og med page_10.jpg, vil ethvert værktøj, der sorterer dem som strenge, placere page_10 lige efter page_1, hvilket forpurrer rækkefølgen. Udfyldning til bredden af det højeste sidenummer, så et dokument på 300 sider giver page_001.jpg, holder den leksikalske rækkefølge og siderækkefølgen identisk overalt i næste led

For dokumenter, der er store nok til, at konverteringen tager mærkbar tid, bør du køre den væk fra UI-tråden eller pumpe beskeder mellem siderne, så applikationen forbliver responsiv, og give brugeren en måde at stoppe på. Hvis du render meget store sider og ønsker annullering, der bider midt på siden frem for kun mellem siderne, har PDFium Component en progressiv render-sti med en annulleringstoken (cancellation token); det er en tungere mekanisme, end de fleste batch-eksporter behøver, men den er der, når en enkelt side ved 600 DPI i sig selv er langsom nok til at blokere

En sidste parring, der er værd at kende. Når du rasteriserer en side, kasseres dens tekstlag: JPEGen er pixels, og ordene i den er ikke længere valgbare eller søgbare. Når du har brug for både et billede og den underliggende tekst, skal du rendere billedet og trække teksten ud separat, hvilket ledsagestykket om udtrækning af tekst fra PDF-dokumenter med PDFium Component dækker. De RenderPage-overloads og render-indstillinger, der vises her, er en del af PDFium Component for Delphi og C++Builder