Technical Article

Филтри за цвят при лошо зрение в PDF с Delphi и PDFium

Читател с лошо зрение не може да различи черен текст на бяла страница при стандартния контраст, затова изисква тъмен режим. Лесното решение е да се инвертира всеки пиксел от рендираната страница. То се разработва за седмица, но се проваля на следващия ден: сканираните снимки започват да изглеждат като фотографски негативи, жълтите маркери на читателя се превръщат в нечетливо синьо петно и някой пита защо разпечатката е излязла изцяло черна. Функцията наистина си струва да бъде изградена и е изключително лесна за половинчато изпълнение, а разликата между двата резултата се крие в една идея: всяко решение за цвета трябва да се взема в конкретна точка от процеса на рендиране, а инверсията е грешният инструмент, приложен в грешния етап. Кодът тук използва PDFium Component â€?визуализаторът на базата на PDFium за Delphi, C++Builder и Lazarus, чийто API за рендиране излага тези етапи поотделно.

Филтрите са състояние на представяне, а не състояние на документа

Едно правило предотвратява най-неприятните бъгове тук: режимът на четене променя начина, по който растерното изображение се генерира или обработва допълнително, и нищо друго. PDF байтовете остават непокътнати, всеки режим е обратим чрез повторно рендиране и операцията по запазване никога не записва филтриран външен вид обратно във файла. Това звучи очевидно, докато някой юридически рецензент не отпечата договор при активен филтриран изглед и не изпрати инвертираната версия. В този момент въпросът „далÐ?печатът използва собствения външен вид на документа или този на екранаâ€?се оказва изключително важен и изисква изричен отговор във вашите спецификации, а не случайно поведение на кодовата нишка. Дръжте настройките на филтъра в състоянието на визуализатора, прилагайте ги при рендиране и изисквайте от всеки експортен път да декларира кой външен вид използва.

Това правило се отплаща многократно. Обратимостта се получава безплатно, тъй като превключването на режимите рендира отново от непроменения източник â€?няма списък с отмени (undo stack) за поддържане и няма как поредица от промени в режимите да влоши качеството на страницата. Сценариите с няколко прозореца остават съгласувани по същата причина. Два изгледа на един документ могат да работят в различни режими, тъй като всеки изглед притежава собствено състояние на представяне, докато обектът на документа остава споделен.

Първо рендиране, след това трансформиране

Поддържаният модел е обработка на растерното изображение след рендиране: методът RenderPage генерира растера на страницата, след което трансформиращ алгоритъм го променя. Компонентът се доставя с три трансформации като директни операции върху растерното изображение: InvertPdfBitmap, DuotonePdfBitmap и GrayscalePdfBitmap, което прави превключването на режимите лесна двуетапна функция:

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);  // dark bg, amber text

    rmGrayscale:    GrayscalePdfBitmap(Result);

  end;

  // rmNormal falls through: the document keeps its own colors

end;

От този дизайн следват две неща. Първо, цената на трансформацията е пропорционална на размера на изображението, така че работата трябва да се извършва там, където се кешират резултатите от рендирането â€?филтрирайте кешираното изображение веднъж, а не при всяко изчертаване. Второ, тъй като трансформацията се изпълнява върху готовия растер, тя засяга по един и същ начин текста, векторната графика, изображенията и външния вид на анотациите. Именно тази еднородност е това, което обикновената инверсия прави грешно при снимките. Това е причината дуотонната (duotone) трансформация да е по-добър избор по подразбиране за текстови документи, тъй като тя картографира яркостта върху избрана цветова гама от тъмно към светло, вместо да обръща цветовите нюанси; инверсията остава достъпна като изричен избор за читателите, които я предпочитат. По-острите краища на символите са друг инструмент. Опцията за рендиране reNoSmoothText изключва изглаждането на текста при изчертаване и се съчетава добре с режима на висок контраст при голямо увеличение.

Два режима за нива на сивото, които се различават

Опциите за рендиране включват reGrayscale, което изглежда като лесен път за заобикаляне на стъпката за допълнителна обработка. Това обаче не е същата операция:

// Engine-level: grayscale applied during rasterization

GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);



// Post-process: render in color, convert the finished bitmap

GrayB := Pdf.RenderPage(0, 0, W, H);

GrayscalePdfBitmap(GrayB);

Опцията на ниво енджин се прилага към растера на съдържанието на изображенията, но не достига до векторните запълвания или цветовете на текста, така че страница с цветни заглавия може да се върне с цветни текстове и сиви снимки. Методът GrayscalePdfBitmap върху готовото растерно изображение преобразува всичко без изключение. Опцията за рендиране все пак има своето място, когато искате изображенията да бъдат обезцветени, но да запазите цвета на текста като маркер, което някои читатели с лошо зрение специално предпочитат. Но ако изискването е за изцяло сива страница, то версията с допълнителна обработка е тази, която го изпълнява. Който и път да изберете, имайте предвид и двата стила на претоварване на RenderPage. Вариантът като функция връща растерно изображение, което извикващият притежава и трябва да освободи, а това става важно веднага щом филтрите умножат броя на рендираните изображения в паметта.

Фонове, маркировки за селекция и клопката PageColor

Не всяка корекция за удобство е трансформация. Замяната на белия фон на страницата с топъл тон често е достатъчна сама по себе си за читатели, чувствителни към отблясъци, и за това има специално свойство. Свойството обаче носи правило за обхват, което може да обърка разработчиците:

// Affects the on-screen view only

PdfView.PageColor := $00D9EDF2;  // warm paper tone behind page content



// RenderPage output ignores PageColor; pass the color explicitly

Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

Свойството PageColor променя това, което TPdfView показва, но растерните изображения, създадени чрез RenderPage, запазват подразбиращия се бял цвят, освен ако параметърът Color не укаже друго. Симптомът е ясен: екранът показва тонираната страница, потребителят я експортира или отпечатва, а изходният файл се връща към бяло. Отнесете това към същото решение за политика за експортиране от първия раздел.

Остарелите свойства за цвят определят наслагваните маркери: HighlightColor за резултати от търсене, SelectionColor за селекция на текст от потребителя, ReadingWordColor за курсора на четените думи. Всеки от тях трябва да бъде проверен повторно под всеки филтър, който предлагате. Кехлибарен курсор за четене, който работи добре на бяло, изчезва след инверсия; бледосиня селекция се губи на висококонтрастен фон. Поддържайте палитри от наслагвания за всеки режим, а не един глобален набор, и тествайте комбинациите целенасочено. Филтрите плюс преобразуването на текст в реч е нормална конфигурация за читателите, които тази функция обслужва, а не частен случай. Самият механизъм за наслагване е разгледан в статията за достъпен четец на PDF.

Числа, валидация и въпросът за печата

Стандартът WCAG 2.1 превръща тази функция в нещо, което можете да измерите. Критерият за успех 1.4.3 изисква съотношение на контраст 4.5:1 за основния текст, а 1.4.6 го повишава до 7:1 за подобрен контраст. Проверете режима си на висок контраст спрямо тези съотношения с анализатор на контраст върху реално рендирано изображение. Текстът върху изображения и в полетата на формуляри са местата, където съотношенията тихо не се спазват, дори когато основният текст преминава проверката.

Печатът изисква собствено решение и разумното поведение по подразбиране е използването на оригиналния външен вид на документа, като се предложи „печаÑ?на показания изгледâ€?като изричен избор за потребителя. Отпечатаната страница служи за доказателство в много повече работни процеси, отколкото разработчиците на визуализатори обикновено очакват, а инвертиран печат на договор е повод за проблем с юридически нюанси. Още една комбинация е от значение за производителността: филтрираното рендиране удвоява работата с растерни изображения при всяко превключване на режима, така че не прилагайте трансформация при всяко съобщение за изчертаване (paint). Кеширайте филтрираното изображение и изпълнявайте трансформацията отново само когато страницата, мащабът или режимът действително се променят. Стратегията за кеширане, която прави това икономично, се намира в статията за кеш на рендиране и производителност при мащабиране.

Едно нещо, което трябва да решите във вашия потребителски интерфейс, а не в кода: кой режим е правилният по подразбиране. Няма единствен отговор, така че предложете целия набор и оставете читателя да избере. Високият контраст е подходящ за четене на текстови документи, инверсията е за читатели, които изрично искат светъл текст на тъмен фон, сивата скала намалява цветовия шум, а цветният фон помага при чувствителност към отблясъци. Запазете избора за всеки потребител, възстановете го при стартиране и осигурете бърз начин за връщане към нормален изглед, тъй като читател, който попадне в нечетлив режим, се нуждае от бърз изход.

Опциите за рендиране, трансформациите на растерни изображения и цветовите свойства на изгледа, използвани тук, се доставят с PDFium Component за Delphi, C++Builder и Lazarus/FPC, с пълен изходен код, така че имплементациите на трансформациите могат да бъдат одитирани или разширявани.