Technický článek

Vykreslování PDF z více zdrojů v Delphi: Cairo, PDFium a PDF Library for Delphi

Tři rasterizéry mohou přečíst totéž PDF a neshodnout se na tom, co v něm stojí. Vestavěný engine v PDF Library for Delphi je ten, který se dodává bez jediného souboru navíc a všechno vykresluje kompetentně, a právě proto si zaslouží pozici výchozí volby. Cairo přináší jinou pipeline pro průhlednost a vyhlazování hran a bývá tím, po čem lidé sáhnou, když jinde vycházejí měkké masky nebo režimy prolnutí špatně. PDFium nese vykreslovací kód Chrome, takže stránka, která vypadá správně v prohlížeči, vypadá obvykle správně i pod PDFium, a to za cenu objemné DLL a bitové šířky, na jejíž shodě trvá. Ani jeden z těch tří není správný v abstraktní rovině. Správnost je záležitostí konkrétního dokumentu a jediný poctivý způsob, jak zjistit, který engine daný korpus zvládá, je ten korpus prohnat každým z nich

To je argument pro to brát engine jako volbu za běhu, ne při sestavení. PDF Library for Delphi, knihovna PDF pro Delphi a C++Builder od losLab, staví všechny tři za jediné vykreslovací rozhraní, takže rozhodnutí stojí jedno celé číslo místo větve v kódu. Zbytek se scvrkne na bezpečné přepínání mezi nimi, na ověření, které enginy nasazená binárka skutečně nese, a na udržení vykreslovacího stavu tak, aby tiše neotrávil další úlohu

Tři rasterizéry za jedním rozhraním volání

Knihovna své enginy čísluje. Engine 1 je vestavěný renderer, tedy výchozí volba, na Windows s možnostmi vyhlazování přes GDI+. Engine 2 je Cairo a engine 3 je PDFium, oba se vybírají za běhu přes SelectRenderer. Oba externí enginy se načítají z DLL, jejichž cesty dodáte pomocí SetCairoFileName a SetPDFiumFileName ještě před jejich výběrem. Ať je aktivní kterýkoli engine, práce jde přes tatáž volání: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Přepnutí enginu posune jedno číslo; zbytek vašeho vykreslovacího kódu si toho vůbec nevšimne

Model cílů sahá daleko za bitmapy. Třída rendereru cílí také na metasoubory (WMF, EMF, EMF+), EPS, přímé kontexty zařízení, tiskárny a HTML5, přičemž Cairo a PDFium se objevují jako další cíle jen tehdy, když byly zakompilovány. Nejviditelněji se ty tři enginy rozcházejí u rastrového výstupu, a proto jej používají i příklady zde

Tři render enginy PDF za jedním volacím povrchem: SelectRenderer přepíná mezi vestavěným enginem, Cairem a PDFium, zatímco kód aplikace dál volá stejné render funkce
SelectRenderer vymění jedno celé číslo za přesun práce mezi vestavěným enginem, Cairo a PDFium. Aplikační kód dál volá RenderPageToFile a společníky bez ohledu na to, který engine pixely vyprodukoval

Nikdy nepředpokládejte, že engine existuje: sondujte při startu

Cairo a PDFium jsou funkce podmíněného překladu, což znamená, že binárku lze sestavit zcela bez nich. Když k tomu dojde, požadavek na engine 2 nebo 3 nic nevyvolá. SelectRenderer jednoduše vrátí jinou hodnotu, než jaké ID jste žádali, a kód, který návratovou hodnotu ignoruje, vykresluje dál tím enginem, jenž byl aktivní už předtím. Obranou je startovní sonda, která každý engine požádá, aby se identifikoval, a odpověď zaznamená:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // engine 1 je přítomen vždy
  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);                       // před skutečnou prací obnovte výchozí volbu
end;

Spusťte tu sondu jednou při startu a její výsledek zapište do logu vedle každé vykreslovací úlohy. Naprosto nejčastější otázkou ve chvíli, kdy zákazník hlásí rozdíl ve vykreslení, je, které enginy jeho instalace vlastně má, a jednořádková odpověď ležící v logu to vyřeší bez relace vzdálené plochy. Užitečný vedlejší efekt: pokud vrátí 0 samo SetPDFiumFileName, víte hned, že problémem je DLL (špatná cesta, špatná bitová šířka, chybějící závislost), a nikoli binárka přeložená bez podpory PDFium, protože volání s cestou nic nenašlo ještě dřív, než se SelectRenderer vůbec spustil

Deset výstupních formátů za jedním celým číslem Options

Parametr Options u vykreslovacích volání vybírá kódování výstupu: 0 je BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+ a 9 HTML5. PNG (5) je rozumnou výchozí volbou pro náhledy a archivní obrázky stránek. JPEG (1) ve dvojici se SetJPEGQuality je lepší volbou pro fotografické skeny, kde na velikosti souboru záleží víc než na ostrých hranách

Jeden formát skrývá požadavek na cílový stream. Cesta pro BMP nejprve zapíše obrazová data a pak se vrátí na offset 0x26, aby v hlavičce opravila pole s rozlišením. Namiřte to na stream, který umí jen dopředu, tedy na kompresní obálku nebo síťový soket, a volání selže způsobem, jenž se čte jako chyba enginu, ale není jí. Když se cíli bez možnosti skákat na pozici nelze vyhnout, vykreslujte místo toho PNG, nebo BMP připravte přes paměťový stream a zkopírujte jej dál, až bude hotové

DPI, které předáte, není DPI, které dostanete

Každé vykreslovací volání bere argument DPI, jenže rozlišení, které skutečně dostanete, je tato hodnota vynásobená globálním měřítkem vykreslení. SetRenderScale začíná na 1.0 a jakmile jej změníte, nový faktor se tiše uplatní na každé další vykreslení na dané instanci:

PDF.SetRenderScale(2.0);                    // každé další vykreslení je zdvojnásobené
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // fakticky 300 DPI
PDF.SetRenderScale(1.0);                    // resetujte, jinak vám náhledy vyjdou obří

Táž lepivost platí pro SetRenderCropType i pro nastavení kvality JPEG. Ve službě, která z jedné sdílené instance vyrábí náhledy, previewy i obrázky v tiskovém rozlišení, stojí právě tato zbytková nastavení za občasným ticketem „náhledy najednou mají 40 MB“. Ven vedou dvě čisté cesty: buď příslušný stav resetovat na začátku každé operace, nebo každému výstupnímu profilu vyhradit vlastní instanci, aby mezi nimi nic neprosakovalo

PDF Library for Delphi: Vývojový diagram startup sondy engine: každý renderer potvrdí svou cestu DLL a svou odpověď SelectRenderer dřív, než se shrnutí dostupnosti zaloguje vedle každé render úlohy
Neúspěšné volání path obžaluje DLL, zatímco neodpovídající výsledek SelectRenderer znamená, že binárka engine nikdy nezahrnula do kompilace. Sonda běží jednou a její jednořádkové shrnutí urovná většinu zákaznických otázek kolem vykreslování

Doladění výchozího enginu dřív, než sáhnete po jiném

Překvapivý podíl požadavků typu „potřebujeme jiný engine“ se nakonec ukáže být problémem nastavení v přestrojení. Vestavěný renderer zpřístupňuje své chování při vyhlazování přes SetGDIPlusOptions a širší rodinu SetRenderOptions a SetGDIPlusFileName vám dovolí namířit jej na konkrétní běhové prostředí GDI+, když nasazovací prostředí dodává nějaké neobvyklé. Zubatá čárová grafika při nízkém DPI, rozmazaný text v náhledech, pruhování v přechodech: na tohle všechno zmíněné ovladače reagují a jejich otočení nestojí v instalátoru nic. Přidání Cairo nebo PDFium naproti tomu znamená dodávat další DLL, sledovat druhou či třetí variantu bitové šířky a převzít závazek je aktualizovat

Stížnost na kvalitu má tedy přirozené pořadí kroků. Nejprve ji reprodukujte při přesně témž DPI a měřítku, jaké má zákazník, protože v polovině případů se rozdíl po jejich srovnání vypaří. Dále vyzkoušejte volby vyhlazování vestavěného enginu. Teprve pak dejte stránku vedle sebe napříč enginy se všemi ostatními proměnnými zafixovanými: vykreslete ji do PNG enginy 1, 2 i 3 při totožném DPI a přiložte všechny tři. Obvykle se dva ze tří shodnou a tato většina vám řekne, zda je odchylka v tom, že se dokument interpretuje jinak, nebo v tom, že je posunuté vaše vlastní očekávání. Tři konkrétní obrázky vyřeší spor o „vykresluje se to špatně“ mnohem rychleji než odstavec přídavných jmen

Řetěz záložních variant, který se vysvětlí sám

Jakmile jsou sondování a disciplína stavu na místě, je samotný záložní řetěz krátký. Detekce selhání se opírá o LastRenderError, které nese vlastní text zprávy enginu k poslednímu vykreslení a je prázdné, když vykreslení uspělo:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // nejprve vestavěný
  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 jako těžká záloha
  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;

Dva návrhové body tu mají váhu. Řetěz zaznamenává, proč k jednotlivým přepnutím došlo, protože řádek v logu znějící „tato stránka od vydání 3.7 spadá zpět na PDFium“ je regresní signál, který chcete sledovat v monitoringu jako trend, ne jej ztratit. Samo pořadí záloh je politika, kterou stojí za to volit podle typu zátěže. Vestavěný engine se nasazuje bez jediné DLL navíc, což z něj ve většině instalací dělá správný první pokus, zatímco dokumenty nabité skupinami průhlednosti nebo neobvyklým stínováním bývají vůbec tím důvodem, proč tým alternativní engine zapojí. Žádný engine není nejrychlejší obecně, což je celý smysl volby na úrovni jednotlivého volání: proměřte každý z nich na vzorku svých skutečných dokumentů při svém skutečném DPI a k tomu měření se vracejte pokaždé, když se změní DLL enginů nebo skladba dokumentů. Spor vždycky vyhraje korpus

Řetěz fallbacku renderu PDF: vestavěný engine zkouší první, selhání se logují, PDFium zkouší znovu a vyvolaná výjimka hlásí, že všechny dostupné enginy selhaly na straně
Každý pokus zkontroluje LastRenderError a zaznamená důvod dřív, než přepne engine. Až když selže každý nainstalovaný engine, řetěz vyvolá výjimku, se shromážděnými příčinami už zapsanými v logu

Za hranicí jednotlivých stránek: dávky TIFF a živé kontexty zařízení

Sadu nástrojů dotvářejí dva sousedé volání pro jednu stránku. RenderAsMultipageTIFFToFile vykreslí výraz s rozsahem stránek rovnou do vícestránkového TIFF, což je přirozený tvar pro archivní předávky do systémů správy dokumentů starších než PDF. RenderPageToDC kreslí přímo do kontextu zařízení Windows pro náhledové ovládací prvky a řídí se vlastní trojicí lepivých nastavení (SetRenderDCOffset, SetRenderDCErasePage a typ ořezu), která vyžadují stejnou resetovací disciplínu jako faktor měřítka. Náhled na obrazovce a vykreslování pro tisk mají dost vlastních pastí na to, aby si zasloužily samostatný článek, odkazovaný níže

Kam dál

Jeden zvyk stojí za to si odnést: protože SelectRenderer platí pro každé další volání na instanci, lze jedinou zatvrzelou stránku zkusit znovu jiným enginem, zatímco zbytek dokumentu zůstane na výchozím. K malování náhledů, výběru tiskárny a práci s DevMode pokračujte článkem o náhledu tisku a kontextu zařízení. Když vykreslování napájí vysokoobjemovou pipeline nad velmi rozsáhlými soubory, snoubí se s vykreslováním po stránkách přes DARenderPageToFile přirozeně handlový přístup popsaný v průvodci přímým přístupem

Balení enginů, podporované formáty a zkušební sestavení jsou podrobně popsány na produktové stránce PDF Library for Delphi