Articol tehnic

Randarea paginilor PDF în JPEG în Delphi cu PDFium

Randarea unei pagini PDF într-un JPEG înseamnă două operații pe care lumea tinde să le ruleze împreună și apoi să le depaneze separat. Întâi rasterizați pagina într-o imagine în pixeli, la o rezoluție aleasă de dumneavoastră. Apoi dați acea imagine unui codificator JPEG și alegeți o calitate. PDFium Component deține prima jumătate prin RenderPage; a doua jumătate este VCL curat, TJPEGImage din Vcl.Imaging.jpeg. Cusătura dintre ele este locul în care stau deciziile interesante, pentru că rezoluția aleasă la randare și calitatea aleasă la codare se compensează reciproc și se compensează cu dimensiunea fișierului în feluri ușor de greșit

Lucrul de interiorizat înaintea oricărui cod: o pagină PDF nu are pixeli. Ea este descrisă în puncte, unde un punct înseamnă 1/72 dintr-un țol, iar pagina este un desen vectorial măsurat în acele puncte. Când îi cereți lui PDFium să randeze, alegeți pe câți pixeli să proiecteze acel desen, iar acea alegere este DPI-ul. Greșiți aritmetica și fie randați o miniatură neclară când voiați un original de tipar, fie alocați o imagine de 200 de megapixeli pentru ceva sortit să fie o previzualizare de 120 de pixeli

Apelul RenderPage din PDFium Component rasterizează o pagină PDF într-un TBitmap pe care codul Delphi VCL îl codează ca JPEG cu TJPEGImage
RenderPage deține jumătatea de rasterizare și vă dă un TBitmap; codificatorul VCL îl transformă în JPEG odată ce calitatea este setată

De la DPI la dimensiuni în pixeli

RenderPage vrea Width și Height întregi, în pixeli, nu un DPI. Așa că prima treabă este conversia. O pagină își raportează dimensiunea în puncte prin PageWidth și PageHeight (ambele Double), iar conversia este aceeași pe care o folosește orice rasterizor: pixelii sunt egali cu punctele înmulțite cu DPI-ul țintă și împărțite la 72. O pagină US Letter are 612 pe 792 de puncte. La 150 DPI devine 1275 pe 1650 de pixeli; la 72 DPI rămâne 612 pe 792, un pixel per punct, cazul despre care lumea uită că este doar identitatea

Formula de la puncte la pixeli înmulțește punctele paginii cu DPI împărțit la 72 pentru a dimensiona ieșirea RenderPage în Delphi
RenderPage vrea pixeli întregi, așa că punctele paginii sunt înmulțite cu DPI-ul și împărțite la 72 înaintea fiecărui apel
// Pdf.PageNumber trebuie să indice deja pagina dorită.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... folosește Bitmap ...
Bitmap.Free;   // forma-funcție a lui RenderPage îți predă proprietatea

Două detalii din acele patru rânduri decid dacă acest cod este corect. Primul este că forma-funcție a lui RenderPage returnează un TBitmap pe care îl dețineți dumneavoastră. PDFium l-a alocat și a plecat; dacă nu îl eliberați cu Free la fiecare iterație, un lot de câteva sute de pagini pierde câteva sute de imagini, iar procesul se umflă până cedează ceva. Al doilea este argumentul Color, aici clWhite. Paginile PDF sunt de obicei desenate presupunând un substrat alb opac, iar o pagină cu transparență randată peste culoarea de fundal greșită produce margini tulburi sau halouri întunecate rătăcite. Albul este valoarea implicită potrivită pentru aproape orice document; parametrul există pentru cazul rar în care nu este

Perechea 0, 0 reprezintă deplasările Left și Top în pagină, în spațiul de coordonate scalat, iar dumneavoastră le lăsați la zero dacă nu decupați. ro0 este rotirea: lăsați-o pe zero și PDFium respectă orice rotire declară deja pagina în intrarea ei /Rotate, așa că o pagină creată în format vedere iese în format vedere fără să faceți nimic

Codarea imaginii bitmap ca JPEG

Odată ce imaginea bitmap există, JPEG-ul este partea ușoară și este Delphi pur. TJPEGImage.Assign copiază imaginea înăuntru, CompressionQuality setează calitatea pe o scală de la 1 la 100, iar SaveToFile scrie fișierul. Singura regulă de ordine este că trebuie să setați calitatea înainte de salvare, pentru că ea guvernează codarea pe care o declanșează SaveToFile

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;

Acel try/finally imbricat pare pedant pentru o rutină de o pagină și este exact ce trebuie pentru un lot. Blocul interior eliberează codificatorul, cel exterior eliberează imaginea, iar oricare dintre ele, declanșat de o excepție, tot eliberează ce deține. Contopiți-le într-unul singur și o excepție în timpul codării poate abandona imaginea. Pe o rulare lungă, asta este diferența dintre un convertor care ajunge la capăt și unul care moare la pagina 300 cu un fișier corupt și o casetă de memorie insuficientă

Alegerea împreună a DPI-ului și a calității

Cele două butoane nu sunt independente de scopul rezultatului, iar greșeala obișnuită este să le dați pe amândouă la maximum, din prudență. O miniatură pentru web randată la 300 DPI și salvată la calitatea 95 înseamnă câteva sute de kiloocteți care se prefac a fi o imagine de 120 de pixeli; browserul aruncă aproape tot la micșorare. Potriviți rezoluția cu pixelii de care are nevoie efectiv rezultatul, apoi alegeți o calitate care supraviețuiește compresiei cu pierderi a JPEG-ului fără artefacte vizibile

RezultatDPICalitate JPEG
Miniatură în listă7260-70
Previzualizare pe ecran96-15080-85
Vizionare cu detaliu mare200-30085-95
Original pentru tipar300-60090-100

Calitatea JPEG merită o avertizare separată. Nu este un buton liniar. Saltul de la 70 la 85 cumpără o îmbunătățire vizuală reală la o creștere modestă a fișierului; saltul de la 95 la 100 aproape dublează fișierul pentru o diferență pe care aproape nimeni nu o vede, pentru că nici calitatea 100 nu este fără pierderi, doar încetează să mai arunce mult. La paginile pline de text, compresia pe blocuri a JPEG-ului întinde marginile ascuțite ale glifelor într-o umbră slabă, motiv pentru care o calitate sub aproximativ 80 face ca textul să pară scanat acolo unde rezultatul ar trebui să fie clar. Dacă paginile sunt mai ales text și puteți schimba formatul, PNG randează acel text fără umbră; JPEG își merită locul pe conținut fotografic și mixt, unde compresia lui este într-adevăr mai mică

Miniaturi mai rapide și mai mici

Când ținta este o miniatură, nu o reproducere fidelă, îi puteți spune motorului de randare să muncească mai puțin. Parametrul Options primește un set de indicatori TRenderOption, iar câțiva dintre ei schimbă fidelitatea pe viteză exact așa cum vrea o previzualizare mică. reGrayscale renunță la culoare, ceea ce și randează mai repede, și produce o imagine mai mică de codat. reNoSmoothImage și reNoSmoothPath sar peste antialiasingul oricum invizibil la scară de miniatură

O pagină PDF în format vedere este încadrată într-o casetă de miniatură de 200 pe 280 cu opțiunile reGrayscale și reNoSmoothImage din PDFium Component
Min-ul celor două rapoarte de potrivire ține paginile în format vedere și portret în aceeași casetă de încadrare, iar indicatorii de randare schimbă fidelitatea pe viteză
function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Încadrează pagina în MaxW x MaxH păstrând raportul de aspect.
  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;

Cazul miniaturii arată și modul mai curat de a gândi dimensionarea. În loc să treceți prin DPI, calculați un singur factor de scalare care încadrează pagina într-o casetă și păstrează raportul de aspect, adică exact ce face Min-ul celor două rapoarte. O pagină portret și una în format vedere ajung amândouă în aceeași casetă fără distorsiune, iar dumneavoastră nu trebuie niciodată să vă gândiți ce DPI corespunde lui „încape în 200 pe 280”. O rezervă la reGrayscale: el convertește conținutul de imagine raster în gri, dar umplerile vectoriale și textul își păstrează valorile de culoare în motor, așa că o pagină formată mai ales din grafică vectorială poate ieși mai puțin monocromă decât sugerează numele indicatorului. Pentru un rezultat cu adevărat integral în tonuri de gri, conversia imaginii randate cu GrayscalePdfBitmap este drumul de încredere

Prelucrarea în lot a unui document întreg

Punerea laolaltă pentru un document complet înseamnă o buclă peste PageCount, cu PageNumber mutat câte o pagină pe rând. Paginile sunt indexate de la 1: prima pagină este PageNumber := 1, iar bucla merge până la PageCount inclusiv, nu până la PageCount - 1. Celălalt lucru pe care lotul trebuie să îl respecte este contractul de încărcare tăcută. Setarea lui Active := True nu ridică niciodată excepție la un fișier deteriorat sau la o parolă greșită; pur și simplu lasă Active pe False. Verificați-l înainte să randați vreo pagină, altfel primul RenderPage lucrează pe un document care nu a fost deschis niciodată

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));   // completează cu zerouri, ca fișierele să se sorteze corect
    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;

Completarea cu zerouri prin Digits este un fleac care vă scutește de o după-amiază pierdută mai târziu. Numiți fișierele de la page_1.jpg la page_10.jpg și orice instrument care le sortează ca șiruri pune page_10 imediat după page_1, încurcând ordinea. Completarea până la lățimea celui mai mare număr de pagină, așa încât un document de 300 de pagini să producă page_001.jpg, păstrează ordinea lexicală identică cu ordinea paginilor peste tot mai departe

Pentru documentele destul de mari cât conversia să dureze sensibil, rulați-o în afara firului de interfață sau pompați mesaje între pagini, ca aplicația să rămână receptivă, și dați utilizatorului o cale de a opri. Dacă randați pagini foarte mari și vreți o anulare care mușcă în mijlocul paginii, nu doar între pagini, PDFium Component are un drum de randare progresivă cu un jeton de anulare; acela este un mecanism mai greu decât are nevoie majoritatea exporturilor în lot, dar există pentru când o singură pagină la 600 DPI este ea însăși destul de lentă cât să blocheze

O ultimă împerechere care merită știută. Rasterizarea unei pagini îi aruncă stratul de text: JPEG-ul este pixeli, iar cuvintele din el nu mai pot fi selectate sau căutate. Când aveți nevoie și de imagine, și de textul de dedesubt, randați pentru imagine și extrageți textul separat, ceea ce acoperă articolul însoțitor despre extragerea textului din documente PDF cu PDFium Component. Supraîncărcările RenderPage și opțiunile de randare arătate aici fac parte din PDFium Component pentru Delphi și C++Builder