Technical Article

PDF рендериране с няколко ядра в Delphi: Вградено, Cairo и PDFium с PDFlibPas

Три различни растерни ядра могат да заредят един и същ PDF документ и да го покажат по различен начин. Вграденото ядро в PDFlibPas се разпространява без допълнителни външни файлове и се справя отлично с изобразяването на всичко, което е и причината да бъде избрано по подразбиране. Cairo предлага различна обработка на прозрачност и заглаждане (anti-aliasing) и обикновено се избира, когато меки маски (soft masks) или режими на смесване (blend modes) не се изобразяват правилно с други инструменти. PDFium съдържа кода за рендериране на Chrome, така че страница, която изглежда правилно в браузър, обикновено се изобразява добре и през PDFium, но срещу цената на голям DLL файл и строго съобразяване с неговата разрядност. Никое от трите ядра не е перфектно в общия случай. Коректността зависи от конкретния документ и единственият сигурен начин да разберете кое ядро се справя най-добре с вашите документи е да ги тествате и с трите.

Това налага ядрото за рендериране да се избира по време на изпълнение на програмата, а не при нейното компилиране. PDFlibPas, PDF библиотеката за Delphi и C++Builder от losLab, поставя трите ядра зад един общ интерфейс, така че изборът изисква промяна само на едно цяло число вместо сложни клонове в кода. Всичко останало се свежда до безопасен избор на ядро, проверка кои ядра съдържа инсталираната програма и предотвратяване на това настройките на текущата задача да повлияят безшумно на следващата.

Три растерни ядра зад общ интерфейс за извикване

Библиотеката номерира своите ядра. Ядро 1 е вграденият рендерер по подразбиране, използващ опции за изглаждане на GDI+ под Windows. Ядро 2 е Cairo, а ядро 3 - PDFium, като и двете се избират по време на изпълнение чрез SelectRenderer. Двете външни ядра се зареждат от DLL библиотеки, чиито пътища задавате чрез SetCairoFileName и SetPDFiumFileName перед избора им. Независимо кое ядро е активно, операциите преминават през еднакви извиквания: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Смяната на ядра изисква промяна само на едно число; останалата част от кода ви не се променя.

Списъкът от изходни формати се простира далеч отвъд растерните изображения. Класът на рендерера поддържа метафайлове (WMF, EMF, EMF+), EPS, директни контексти на устройства (device contexts), принтери и HTML5, като Cairo и PDFium се появяват като допълнителни изходни цели само когато са включени при компилацията. Растерният изход е мястото, където трите ядра се различават най-видимо, затова примерите тук използват растерни изображения.

Никога не приемайте, че дадено ядро е налично: тествайте при стартиране

Cairo и PDFium са функции за условна компилация, което означава, че дадена програма може да бъде компилирана напълно без тях. Когато това се случи, заявката за използване на ядро 2 или 3 няма да предизвика изключение. Методът SelectRenderer просто ще върне стойност, различна от поискания ID, а кодът, който игнорира върнатата стойност, ще продължи да рендерира с досега активното ядро. Защитата срещу това е тест при стартиране на програмата, който изисква всяко ядро да се идентифицира и записва резултата:

function ProbeEngines(PDF: TPDFlib): string;

begin

  Result := 'built-in';                        // engine 1 is always present

  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);                       // restore the default before real work

end;

Изпълнете този тест веднъж при стартиране и запишете резултата в дневника (log) до всяка задача за рендериране. Най-честият въпрос, когато клиент съобщи за разлика при изобразяването, е кои ядра действително са налични в неговата инсталация, а наличието на тази информация в дневника решава въпроса веднага, без необходимост от сесия за отдалечен достъп. Полезен страничен ефект: ако самата функция SetPDFiumFileName върне 0, вече знаете, че проблемът е в DLL файла (грешен път, разрядност или липсваща зависимост), а не в липсата на PDFium поддръжка при компилацията на бинарния файл, тъй като определянето на пътя се е провалило още преди стартирането на SelectRenderer.

Десет изходни формата зад едно цяло число в Options

Параметърът Options при извикванията за рендериране определя изходния формат: 0 е BMP, 1 - JPEG, 2 - WMF, 3 - EMF, 4 - EPS, 5 - PNG, 6 - GIF, 7 - TIFF, 8 - EMF+ и 9 - HTML5. PNG (5) е логичният избор по подразбиране за визуализация и архивни изображения на страници. JPEG (1), комбиниран със SetJPEGQuality, е по-добрият избор за сканирани снимки, където размерът на файла е по-важен от острите контури.

Един от форматите съдържа скрито изискване към изходния поток. BMP форматът записва първо изображението, след което се връща на отместване 0x26, за да коригира полетата за разделителна способност в заглавната част. Ако насочите това към поток, който поддържа само превъртане напред (като компресиран поток или мрежов сокет), извикването ще се провали по начин, който изглежда като системна грешка в ядрото, но не е. Когато не можете да избегнете поток без поддръжка на произволен достъп, рендерирайте в PNG или прекарайте BMP данните през поток в паметта (memory stream) и ги копирайте напред след завършването им.

Подадената стойност за DPI не е получената разделителна способност

Всяко извикване за рендериране приема аргумент за DPI, но реалната разделителна способност на изхода е тази стойност, умножена по глобалния мащаб на рендериране. Свойството SetRenderScale запова от 1.0 и след като го промените, новият коефициент се прилага безшумно към всяко следващо извикване за тази инстанция:

PDF.SetRenderScale(2.0);                    // every later render is doubled

PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // effectively 300 DPI

PDF.SetRenderScale(1.0);                    // reset, or your thumbnails arrive huge

Същото запазване на състоянието се отнася за SetRenderCropType и настройките за качество на JPEG. В услуга, която генерира миниатюри (thumbnails), визуализации и изображения за печат от една споделена инстанция, именно тези останали настройки водят до оплаквания от типа "миниатюрите изведнъж станаха по 40 MB". Има два лесни начина за решаване на проблема: нулирайте съответните параметри в началото на всяка операция или използвайте отделна инстанция за всеки профил на изходните данни, за да не се допуска смесване на параметрите.

Настройка на вграденото ядро преди избор на друго

Изненадващо голяма част от заявките за използване на друго ядро всъщност се дължат на неправилни настройки. Вграденият рендерер разкрива поведението си на изглаждане чрез SetGDIPlusOptions и фамилията настройки SetRenderOptions, а SetGDIPlusFileName ви позволява да го насочите към специфична среда за изпълнение на GDI+, ако средата на внедряване съдържа необичайна версия. Накъсани линии при нисък DPI, замъглен текст в миниатюрите, стъпаловидни градиенти: всичко това се коригира чрез тези настройки, като използването им не оскъпява инсталационния пакет. За разлика от това, добавянето на Cairo или PDFium изисква разпространение на допълнителни DLL файлове, следене на разрядността на хоста и ангажимент за тяхната поддръжка и актуализация.

Затова оплакванията за качество имат естествен ред на анализ. Първо възпроизведете проблема при точните параметри за DPI и мащаб на клиента, тъй като в половината от случаите разликата изчезва при еднакви параметри. След това опитайте опциите за изглаждане на вграденото ядро. Едва след това сравнете изобразяването на страницата при трите ядра с фиксирани други променливи: рендерирайте я в PNG през ядра 1, 2 и 3 при еднакъв DPI и ги сравнете. Обикновено две от трите ядра дават еднакъв резултат и това мнозинство ви показва дали изключението се дължи на специфична интерпретация на документа или на неправилни очаквания от ваша страна. Три реални изображения решават спорове за лошо изобразяване много по-бързо от всякакви текстови описания.

Верига от резервни варианти с отчитане на грешките

След като тестът за наличност и изчистването на настройките са реализирани, самата верига от резервни варианти е кратка. Откриването на грешка разчита на свойството LastRenderError, което съхранява съобщението за грешка от последното рендериране и е празно при успех:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);

begin

  PDF.SelectRenderer(1);                            // built-in first

  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 as the 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;

Два архитектурни детайла са важни тук. Веригата записва защо се е случило превключването, тъй като ред в дневника като "тази страница премина към PDFium след версия 3.7" е важен сигнал за регресия, който трябва да се проследява в системата за мониторинг. Самият ред на превключване е избор, който се прави спрямо натоварването. Вграденото ядро се разпространява без допълнителни библиотеки, което го прави отличен първи избор за повечето инсталации, докато документи със сложна прозрачност или специфични сенки са основната причина разработчиците да добавят външни ядра. Никое ядро не е най-бързо в общия случай, което е и смисълът от избор при всяко извикване: тествайте всяко ядро с реални документи при реалните параметри на вашата система и актуализирайте тези измервания при подмяна на DLL библиотеките или промяна в типа на документите. Реалните файлове са най-добрият аргумент.

Отвъд отделните страници: TIFF пакети и контекст на устройство в реално време

Две допълнителни функции допълват инструментите за рендериране. RenderAsMultipageTIFFToFile рендерира диапазон от страници директно в многостраничен TIFF файл, което е стандартният формат за прехвърляне на данни към по-стари системи за управление на документи. RenderPageToDC изобразява страницата директно върху Windows контекст на устройство (device context) за контроли за визуализация, като се управлява от три настройки (SetRenderDCOffset, SetRenderDCErasePage и типа изрязване), които изискват същото нулиране, както и мащабът. Прегледът на екрана и печатът съдържат специфични особености, които са разгледани в отделна статия по-долу.

Към какво да преминете нататък

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

Разпространението на ядрата, поддържаните формати и пробни версии са описани подробно на продуктовата страница на PDFlibPas.