Articol tehnic

Randare PDF multi-motor în Delphi: integrat, Cairo și PDFium cu PDF Library for Delphi

Trei rasterizatoare pot citi același PDF și pot fi în dezacord cu privire la ce spune. Motorul integrat din PDF Library for Delphi este cel care vine fără fișiere suplimentare și randează totul competent, motiv pentru care ocupă poziția implicită. Cairo aduce un pipeline diferit de transparență și anti-aliasing și tinde să fie cel la care oamenii apelează atunci când măștile soft (soft masks) sau modurile de blend ies greșit în altă parte. PDFium poartă codul de randare al Chrome, așa că o pagină care arată corect într-un browser arată de obicei corect și sub PDFium, cu prețul unui DLL de dimensiuni considerabile și al unei arhitecturi (bitness) pe care insistă să o respecte. Niciunul dintre cele trei nu este corect în abstract. Corectitudinea este per document, iar singura modalitate onestă de a afla care motor gestionează bine un anumit corpus este să rulezi acel corpus prin fiecare dintre ele

Acesta este argumentul pentru a trata motorul ca pe o alegere la runtime, nu la build. PDF Library for Delphi, biblioteca PDF pentru Delphi și C++Builder de la losLab, pune toate cele trei în spatele unei singure suprafețe de randare, astfel încât decizia costă un întreg, nu o ramificație de cod. Restul acestui articol se rezumă la selectarea în siguranță între ele, confirmarea motoarelor pe care le poartă efectiv un binar implementat și evitarea situației în care starea de randare otrăvește pe tăcute următoarea lucrare

Trei rasterizatoare în spatele unei singure suprafețe de apel

Biblioteca își numerotează motoarele. Motorul 1 este randorul integrat, cel implicit, cu opțiuni de netezire GDI+ pe Windows. Motorul 2 este Cairo, iar motorul 3 este PDFium, ambele selectate la runtime prin SelectRenderer. Cele două motoare externe se încarcă din DLL-uri ale căror căi le furnizezi cu SetCairoFileName și SetPDFiumFileName înainte de a le selecta. Indiferent care motor este activ, lucrul trece prin aceleași apeluri: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Schimbarea motorului mută un singur număr; restul codului tău de randare nu observă nimic

Modelul de destinație depășește cu mult bitmapurile. Clasa de randare țintește de asemenea metafișiere (WMF, EMF, EMF+), EPS, contexte de dispozitiv directe, imprimante și HTML5, Cairo și PDFium apărând ca destinații suplimentare doar atunci când au fost compilate. Ieșirea raster este locul unde cele trei motoare diverg cel mai vizibil, așa că aceasta este ceea ce folosesc exemplele de aici

Trei motoare de redare PDF în spatele unei singure suprafețe de apel: SelectRenderer comută între motorul integrat, Cairo și PDFium, în timp ce codul aplicației continuă să apeleze aceleași funcții de redare
SelectRenderer schimbă un singur întreg pentru a muta munca între motoarele încorporat, Cairo și PDFium. Codul aplicației continuă să apeleze RenderPageToFile și rudele sale indiferent ce motor a produs pixelii

Nu presupune niciodată că un motor există: investighează la pornire

Cairo și PDFium sunt caracteristici de compilare condiționată, ceea ce înseamnă că un binar poate fi construit complet fără ele. Când se întâmplă asta, cererea motorului 2 sau 3 nu generează nimic. SelectRenderer pur și simplu returnează o altă valoare decât ID-ul cerut, iar codul care ignoră valoarea returnată continuă să randeze cu orice motor era deja activ. Apărarea este o investigare la pornire care cere fiecărui motor să se identifice și înregistrează răspunsul:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // motorul 1 este întotdeauna prezent
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // restaurează implicitul înainte de lucrul real
end;

Rulează acea investigare o dată la pornire și scrie rezultatul ei în jurnal alături de fiecare lucrare de randare. Cea mai frecventă întrebare atunci când un client raportează o diferență de randare este ce motoare are efectiv instalarea sa, iar un răspuns de o singură linie aflat în jurnal o rezolvă fără o sesiune de acces la distanță (remote-desktop). Un efect secundar util: dacă SetPDFiumFileName însuși returnează 0, știi deja că DLL-ul este problema (cale greșită, arhitectură greșită, o dependență lipsă), nu un binar compilat fără suport pentru PDFium, pentru că apelul de cale nu a rezolvat nimic înainte ca SelectRenderer să ruleze vreodată

Zece formate de ieșire în spatele unui singur întreg Options

Parametrul Options al apelurilor de randare selectează codificarea de ieșire: 0 este BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+ și 9 HTML5. PNG (5) este alegerea implicită rezonabilă pentru previzualizări și imagini de pagină pentru arhivare. JPEG (1), combinat cu SetJPEGQuality, este alegerea mai bună pentru scanări fotografice unde dimensiunea fișierului contează mai mult decât marginile clare

Un format ascunde o cerință privind fluxul țintă. Calea BMP scrie mai întâi datele imaginii, apoi caută înapoi la offsetul 0x26 pentru a corecta câmpurile de rezoluție din antet. Îndreaptă asta către un flux doar-înainte, un wrapper de compresie sau un socket de rețea, iar apelul eșuează într-un mod care pare o eroare de motor, dar nu este. Când o țintă nesecvențiabilă (non-seekable) este inevitabilă, randează în schimb PNG, sau eșalonează BMP-ul printr-un flux de memorie și copiază-l mai departe odată ce este complet

DPI-ul pe care îl transmiți nu este DPI-ul pe care îl obții

Fiecare apel de randare ia un argument DPI, dar rezoluția pe care o obții de fapt este acea valoare înmulțită cu scara globală de randare. SetRenderScale pornește de la 1.0, iar odată ce îl modifici, noul factor se aplică pe tăcute la fiecare randare ulterioară pe acea instanță:

PDF.SetRenderScale(2.0);                    // fiecare randare ulterioară este dublată
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // practic 300 DPI
PDF.SetRenderScale(1.0);                    // resetează, altfel miniaturile tale ajung uriașe

Aceeași persistență se aplică lui SetRenderCropType și setării calității JPEG. Într-un serviciu care produce miniaturi, previzualizări și imagini la rezoluție de tipar dintr-o singură instanță partajată, aceste setări rămase în urmă sunt ceea ce se află de fapt în spatele ocazionalului tichet „miniaturile au ajuns brusc la 40 MB”. Două căi curate de ieșire din asta: resetează starea relevantă la începutul fiecărei operații, sau dedică o instanță separată fiecărui profil de ieșire, astfel încât nimic să nu se scurgă între ele

PDF Library for Delphi: organigramă a sondei de motor la pornire: fiecare renderer își confirmă calea DLL și răspunsul SelectRenderer înainte ca un rezumat de disponibilitate să fie jurnalizat lângă fiecare job de redare
Un apel de cale eșuat acuză DLL-ul, în timp ce un rezultat nepotrivit al SelectRenderer înseamnă că binarul nu a compilat niciodată motorul înăuntru. Sonda rulează o singură dată, iar rezumatul ei de o linie rezolvă majoritatea întrebărilor clienților despre randare

Reglarea motorului implicit înainte de a apela la altul

O proporție surprinzătoare din cererile de tipul „avem nevoie de un alt motor” se dovedesc a fi probleme de setări deghizate. Randorul integrat își expune comportamentul de netezire prin SetGDIPlusOptions și familia mai largă SetRenderOptions, iar SetGDIPlusFileName îți permite să-l îndrepți către un runtime GDI+ specific atunci când un mediu de implementare vine cu unul neobișnuit. Linii zimțate la DPI scăzut, text neclar în miniaturi, benzi (banding) în gradiente: toate acestea răspund la aceste butoane, iar activarea lor nu costă nimic în instalator. Adăugarea Cairo sau PDFium, dimpotrivă, înseamnă livrarea mai multor DLL-uri, urmărirea unei a doua sau a treia variante de arhitectură (bitness) și asumarea obligației de a le actualiza

Așadar, o reclamație privind calitatea are o ordine naturală a operațiilor. Reproduce-o mai întâi la DPI-ul și scara exacte ale clientului, deoarece în jumătate din cazuri diferența dispare odată ce acestea se potrivesc. Încearcă apoi opțiunile de netezire ale motorului integrat. Abia atunci pune pagina alăturat, comparată între motoare, cu orice altă variabilă menținută constantă: randeaz-o în PNG prin motoarele 1, 2 și 3 la DPI identic și atașează toate cele trei. De obicei, două dintre cele trei sunt de acord, iar acea majoritate îți spune dacă valoarea aberantă este documentul interpretat diferit sau propria ta așteptare de referință care este greșită. Trei imagini concrete rezolvă o dispută de tipul „se randează greșit” mult mai repede decât un paragraf de adjective

Un lanț de rezervă (fallback) care se explică singur

Odată ce investigarea și disciplina stării sunt puse la punct, lanțul de rezervă propriu-zis este scurt. Detectarea unui eșec se bazează pe LastRenderError, care păstrează textul mesajului propriu al motorului pentru cea mai recentă randare și este gol atunci când randarea a reușit:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // integrat mai întâi
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium ca rezervă puternică (heavy fallback)
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

Două aspecte de design contează aici. Lanțul înregistrează de ce s-a produs fiecare comutare, deoarece o linie de jurnal care spune „această pagină a trecut pe PDFium din versiunea 3.7” este un semnal de regresie pe care vrei să-l urmărești în monitorizare, nu să-l pierzi. Ordinea de rezervă în sine este o politică ce merită aleasă per sarcină de lucru. Motorul integrat se implementează fără DLL-uri suplimentare, ceea ce îl face prima încercare corectă în majoritatea instalărilor, în timp ce documentele încărcate cu grupuri de transparență sau umbrire neobișnuită sunt motivul obișnuit pentru care o echipă conectează deloc un motor alternativ. Niciun motor nu este cel mai rapid în general, ceea ce este întregul rost al alegerii per apel: testează-le pe fiecare în raport cu un eșantion din documentele tale reale, la DPI-ul tău real, și revizuiește acea măsurătoare de fiecare dată când DLL-urile motorului sau amestecul de documente se schimbă. Corpusul câștigă argumentul de fiecare dată

Lanțul de rezervă al redării PDF: motorul integrat încearcă primul, eșecurile sunt jurnalizate, PDFium reia, iar o excepție semnalează când toate motoarele disponibile eșuează o pagină
Fiecare încercare verifică LastRenderError și înregistrează motivul înainte de a comuta motoarele. Doar când fiecare motor instalat a eșuat lanțul ridică excepția, cu cauzele colectate deja așezate în log

Dincolo de paginile individuale: loturi TIFF și contexte de dispozitiv live

Doi vecini ai apelurilor per pagină completează setul de instrumente. RenderAsMultipageTIFFToFile randează o expresie de interval de pagini direct într-un TIFF multi-pagină, forma naturală pentru predările de arhivare către sisteme de management al documentelor anterioare PDF-ului. RenderPageToDC pictează direct pe un context de dispozitiv Windows pentru controale de previzualizare, guvernat de propriul trio de setări persistente (SetRenderDCOffset, SetRenderDCErasePage, plus tipul de decupare), care necesită aceeași disciplină de resetare ca factorul de scară. Randarea de previzualizare pe ecran și cea pe calea de tipărire poartă suficiente capcane proprii pentru a merita un articol dedicat, legat mai jos

Unde continui de aici

Un obicei care merită dus mai departe: deoarece SelectRenderer are efect pentru fiecare apel ulterior pe instanță, o singură pagină încăpățânată poate fi reîncercată pe un alt motor, în timp ce restul documentului rămâne pe cel implicit. Pentru pictarea previzualizării, selecția imprimantei și gestionarea DevMode, continuă cu articolul despre previzualizarea tipăririi și contextul de dispozitiv. Când randările alimentează un pipeline de volum mare pentru fișiere foarte mari, abordarea bazată pe handle din ghidul de acces direct se combină natural cu randarea per pagină prin DARenderPageToFile

Ambalarea motoarelor, formatele acceptate și build-urile de probă sunt detaliate pe pagina produsului PDF Library for Delphi