Technický článek

Barevné filtry PDF pro slabozraké v Delphi s PDFium

Uživatel se zbytky zraku nedokáže při výchozím kontrastu rozeznat černý text na bílé stránce, takže požádá o tmavý režim. Naivní odpovědí je invertovat každý pixel vykreslené stránky. Odešle se to za týden a rozbije se to hned další den: naskenované fotografie se vrátí a vypadají jako filmové negativy, žluté zvýrazňovače uživatele se promění v nečitelnou modrou šmouhu a někdo se zeptá, proč výtisk vyšel jednolitě černý. Tato funkce stojí za sestavení a je opravdu snadné ji udělat napůl špatně, a rozdíl mezi oběma výsledky je jedna myšlenka: každé barevné rozhodnutí patří na konkrétní místo ve vykreslovacím pipeline, a inverze je špatný nástroj použitý ve špatné fázi. Kód zde používá PDFium Component, prohlížečku postavenou na PDFiu pro Delphi, C++Builder a Lazarus, jejíž vykreslovací API tyto fáze vystavuje odděleně

Filtry jsou stav prezentace, nikdy stav dokumentu

Jedno pravidlo tu zabraňuje nejhorší kategorii chyb: režim čtení mění, jak se bitmapa vyprodukuje nebo následně zpracuje, a nic jiného. Bajty PDF zůstávají nedotčené, každý režim je vratný opětovným vykreslením, a „uložení" nikdy nezapíše filtrovaný vzhled zpátky do souboru. To zní samozřejmě, dokud právní recenzent nevytiskne smlouvu pod aktivním filtrem a nezaloží invertovanou verzi. V tu chvíli si otázka „používá tisk vlastní vzhled dokumentu, nebo vzhled obrazovky" zaslouží explicitní odpověď ve vaší specifikaci, ne náhodu cesty kódu. Nastavení filtru držte ve stavu prohlížečky, aplikujte ho při vykreslování a u každé exportní cesty deklarujte, jaký vzhled používá

Toto pravidlo se vyplatí dvakrát. Vratnost přijde zadarmo, protože přepnutí režimu znovu vykreslí z nezměněného zdroje: není potřeba udržovat žádný zásobník pro undo a série změn režimu nemá jak stránku poškodit. Scénáře s více okny zůstávají koherentní ze stejného důvodu. Dva pohledy na jeden dokument mohou běžet v různých režimech, protože každý pohled vlastní svůj stav prezentace, zatímco objekt dokumentu zůstává sdílený

Nejdřív vykreslit, pak transformovat

Podporovaný vzor je zpracování bitmapy po vykreslení: RenderPage vyprodukuje rastr stránky, pak ho upraví transformační průchod. Komponenta dodává tři transformace jako operace na bitmapě na místě, InvertPdfBitmap, DuotonePdfBitmap a GrayscalePdfBitmap, což dělá z přepnutí režimu čistou dvoufázovou funkci:

Diagram pipeline režimů čtení prohlížeče PDF v Delphi, kde jedno volání PDFium RenderPage živí čtyři režimy čtení, každý jako transformace bitmapy na místě jako InvertPdfBitmap nebo DuotonePdfBitmap
RenderPage vyprodukuje raster jednou a aktivní režim čtení vybere jednu bitmapovou transformaci na místě
function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // tmavé pozadí, jantarový text
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal propadne dál: dokument si ponechá vlastní barvy
end;

Z tohoto návrhu plynou dvě věci. Za prvé, cena transformace je úměrná velikosti bitmapy, takže tato práce patří tam, kde jsou vaše vykreslovací výsledky kešované: filtrujte kešovanou bitmapu jednou, ne při každém vykreslení. Za druhé, protože transformace běží na hotovém rastru, zasáhne text, vektorovou grafiku, obrázky i vzhledy anotací stejným způsobem. Tato jednotnost je přesně to, co obyčejná inverze u fotografií pokazí. Je to důvod, proč je duotónová transformace lepším výchozím nastavením pro textově náročné dokumenty, protože mapuje jas na zvolenou barevnou škálu od tmavé po světlou místo negace odstínů; inverze zůstává k dispozici jako explicitní volba pro čtenáře, kteří ji chtějí. Ostřejší hrany glyfů jsou samostatná páka. Vykreslovací volba reNoSmoothText vypne vyhlazování textu při vykreslování a dobře se páruje s režimem vysokého kontrastu při velkém zoomu

Dva odstíny šedi, které si odporují

Mezi vykreslovací volby patří reGrayscale, což vypadá jako zkratka kolem kroku následného zpracování. Není to stejná operace:

Diagram srovnávající render volbu PDFium reGrayscale, která odbarví obrázky, ale nechá barevné nadpisy, proti post-processu Delphi GrayscalePdfBitmap převádějícím celou stranu
Volba enginu odsytí obsah obrázku, zatímco post-process mění každý pixel hotové bitmapy
// Na úrovni enginu: odstíny šedi aplikované během rasterizace
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Následné zpracování: vykreslit barevně, převést hotovou bitmapu
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

Volba na úrovni enginu se aplikuje na rastrový výstup obrazového obsahu, ale nedosáhne na vektorové výplně ani barvy textu, takže stránka s barevnými nadpisy se může vrátit se šedými fotografiemi a tvrdohlavě modrými nadpisy. GrayscalePdfBitmap na hotové bitmapě převede úplně vše, bezpodmínečně. Vykreslovací volba si přesto zaslouží místo, když chcete desaturovat obrázky a zároveň zachovat barvu textu jako signál, což někteří uživatelé se zbytky zraku výslovně preferují. Ale pokud požadavek zní „stránka v odstínech šedi", je to následné zpracování, které ho splní. Ať zvolíte kteroukoli cestu, mějte na paměti oba styly přetížení RenderPage. Funkční forma vrací bitmapu, kterou vlastní volající a musí ji uvolnit, a na tom záleží ve chvíli, kdy filtry znásobí počet vykreslených bitmap v oběhu

Pozadí, značky výběru a past jménem PageColor

Ne každá úprava pohodlí je transformace. Nahrazení bílého pozadí stránky teplým odstínem samo o sobě často stačí pro čtenáře citlivé na oslnění, a existuje pro to vyhrazená vlastnost. Tato vlastnost nese pravidlo rozsahu, o které lidé zakopávají:

Diagram pasti rozsahu PageColor v prohlížeči PDF v Delphi: tón se ukáže na obrazovce, zatímco výstup RenderPage zůstane bílý, dokud se barva nepředá explicitně
PageColor tónuje jen zobrazení na obrazovce a RenderPage drží bílou stránku, pokud se barva nepředá explicitně
// Ovlivňuje jen zobrazení na obrazovce
PdfView.PageColor := $00D9EDF2;  // teplý odstín papíru za obsahem stránky

// Výstup RenderPage ignoruje PageColor; barvu předejte explicitně
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor mění to, co zobrazuje TPdfView, ale bitmapy vyprodukované přes RenderPage si drží výchozí bílou, pokud jinak neřekne parametr Color. Příznak je spolehlivý: obrazovka ukazuje tónovanou stránku, uživatel exportuje nebo tiskne, a výstup se vrátí k bílé. Zařaďte to pod stejné rozhodnutí o exportní politice z první sekce

Zbývající barevné vlastnosti definují překryvné značky: HighlightColor pro nálezy vyhledávání, SelectionColor pro uživatelský výběr textu, ReadingWordColor pro kurzor mluveného slova. Každou z nich je nutné znovu zkontrolovat pod každým filtrem, který nabízíte. Jantarový kurzor čtení, který funguje na bílé, po inverzi zmizí; bledě modrý výběr zmizí v pozadí s vysokým kontrastem. Udržujte překryvné palety podle režimu, ne jednu globální sadu, a kombinace testujte záměrně. Filtry plus převod textu na řeč je pro čtenáře, kterým tato funkce slouží, běžná konfigurace, ne okrajový případ. Samotnou mechaniku překryvů pokrývá článek o přístupné čtečce

Čísla, ověření a otázka tisku

WCAG 2.1 dělá z této funkce něco měřitelného. Kritérium úspěchu 1.4.3 požaduje poměr kontrastu 4,5:1 pro text těla, a 1.4.6 ho zvedá na 7:1 pro zvýšený kontrast. Namátkově prověřte svůj režim vysokého kontrastu proti těmto poměrům pomocí analyzátoru kontrastu spuštěného na skutečném vykresleném výstupu. Text přes obrázky a text ve formulářových polích jsou místa, kde poměry potichu selžou, i když text těla projde

Tisk si zaslouží vlastní rozhodnutí, a obhajitelný výchozí stav je vlastní vzhled dokumentu, přičemž „tisknout tak, jak je zobrazeno" nabídnete jako explicitní volbu uživatele. Vytištěná stránka je důkazem ve víc workflow, než autoři prohlížeček obvykle čekají, a invertovaný výtisk smlouvy je supportní incident s právní příchutí. Ještě jedno spárování má vliv na výkon: filtrované vykreslování zdvojnásobí práci s bitmapou při každém přepnutí režimu, takže neaplikujte transformaci při každé zprávě o vykreslení. Kešujte filtrovanou bitmapu a transformaci spusťte znovu jen tehdy, když se skutečně změní stránka, zoom nebo režim. Strategie kešování, která toto zlevní, žije v článku o vykreslovací mezipaměti a výkonu zoomu

Jedna věc, kterou vyřešit ve vašem UI, ne v kódu: který režim je ten správný výchozí. Neexistuje jediná odpověď, takže nabídněte celou sadu a nechte čtenáře, ať si vybere. Vysoký kontrast sedí většině textově náročného čtení, inverze sedí čtenářům, kteří výslovně chtějí světlé na tmavém, odstíny šedi omezí barevný šum a tónování pozadí řeší citlivost na oslnění. Volbu ukládejte pro každého uživatele zvlášť, obnovujte ji při spuštění a udržujte cestu zpátky do normálu na jeden stisk klávesy, protože čtenář, který se ocitne v režimu, který nedokáže přečíst, potřebuje rychlou cestu ven

Vykreslovací volby, transformace bitmap a barevné vlastnosti pohledu použité zde jsou součástí PDFium Component pro Delphi, C++Builder a Lazarus/FPC, s plným zdrojovým kódem, takže implementace transformací lze auditovat nebo rozšířit