Я хочу показать пример разработки приложения с затратой небольших усилий на стыке технологий создания десктопных приложений и веб-программирования.
Несколько недель тому я искал способ конвертации специфических PDF документов в изображения с учетом возможности автоматизации и скриптования в будущем. Конечно есть старожил — пакет ImageMagic с утилитой convert, но к сожалению я столкнулся с тем что этот инструмент не так хорош как я ожидал именно на этих файлах — не рендерит корректно многие файлы и что совсем не радовало — многие иллюстрации были испорчены.
Я стал искать другие инструменты и хотя всевозможных утилит очень много но у каждой есть свои особенности так что я так и не выбрал какую использовать.
Вместо этого у меня появилась идея, может ли Qt как довольно зрелая технология помочь мне? В Qt очень просто создать PDF документ с помощью QPrinter, но как насчет обратной функциональности - сделать изображение из PDF страницы? А ведь есть ещё одна хорошо проработаная технология — PDF.js.
Можно ли совместить эти две технологии? Конечно! Qt имеет компонент QWebEngineView. Продемонстрируем в коде:
По быстрому на основе QMainWindow:
m_webView = new QWebEngineView(this);
m_webView->load(url);
setCentralWidget(m_webView);
Результатом будет окно с веб страницей внутри. Теперь очередь PDF.js. Проект имеет довольно большой объём кода, но есть возможность собрать компактную (minified) версию которую можно легко встроить в вебсайт. Пример сборки:
$ git clone git://github.com/mozilla/pdf.js.git
$ cd pdf.js
$ brew install npm
$ npm install -g gulp-cli
$ npm install
$ gulp minified
Результатом будет «скомпилированая» версия pdf.js в папке build/minified, который копируем в наш проект. Установим стартовый URL на локальный файл minified/web/viewer.html
auto url = QUrl::fromLocalFile(app_path+"/minified/web/viewer.html");
Соберём и запустим:

Работает отлично, подход правильный, но показывает PDF файл по умолчанию. Как можно передать имя файла в среду javascript? Для этого у Qt есть другой отличный модуль QWebChannel. Идея в том, что на стороне C++/Qt создаётся QWebChannel объект и он устанавливается каналом(channel) для загружаемой веб страницы. С этим каналом мы можем зарегистрировать объекты которые будут доступны уже внутри JavaScript кода. Из JavaScript будут видны Q_PROPERTY свойства:
auto url = QUrl::fromLocalFile(app_path+"/minified/web/viewer.html");
m_communicator = new Communicator(this);
m_communicator->setUrl(pdf_path);
m_webView = new QWebEngineView(this);
QWebChannel * channel = new QWebChannel(this);
channel->registerObject(QStringLiteral("communicator"), m_communicator);
m_webView->page()->setWebChannel(channel);
m_webView->load(url);
setCentralWidget(m_webView);
Приведённый код позволяет получить доступ к объекту communicator из JavaScript. Теперь необходимо внести изменения в viewer.html/viewer.js, добавить стандартный qwebchannel.js чтобы заработала коммуникация — довольно просто:
Для viewer.html просто добавим включение qwebchannel.js. Для viewer.js добавим инициализацию QWebChannel и получим из канала имя файла который будет использоваться вместо файла по-умолчанию:
Код:
var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf';
new QWebChannel(qt.webChannelTransport
,function(channel) {
var comm = channel.objects.communicator;
DEFAULT_URL = comm.url;
...
Вот как это работает: Перед загрузкой страницы, прикрепляется web channel и регистрируется communicator объект. Потом, когда viewer.html грузится в первый раз, определяется QWebChannel JS класс. После определения DEFAULT_URL создается JS QWebChannel объект и как только коммуникация установлена, будет вызвана прикреплённая js функция которая читает URL из объекта communicator. Новый URL и будет пользоваться вместо файла примера. Таким же образом можно передать отрендериную страницу как изображение из JavaScript в C++/Qt часть приложения.
Закончив изменения в PDF.js просто пересоберем minified:
$ gulp minified
Скопируем minified версию в папку проекта. Доделаем получение списка файлов из аргументов командной строки итд.

Готово, рабочее десктопное приложение PDF viewer за пару часов.
> GitHub
Комментарии (17)
Alex_ME
28.05.2017 18:04+1С PDF вообще все сложно. Однажды мне понадобился просмотр PDF в Android. Вариантов не густо — один на Java, жутко тормозящий, гугловский просмоторщик, который еще надо как-то собрать и прикрутить и pdf.js.
Недавно понадобился просмотр документов в C#. Поискал, поискал — тоже не густо, какие-то сомнительные поделки. Остановился на просмотре XPS в WPF (pdf был не критичен).
Никаких официальных SDK на этот счет не существует?
apro
28.05.2017 18:27+4Я бы использовал mupdf в вашем случае, он на С, поэтому можно и из Java и из C# использовать,
плюс скорее всего для Java уже есть обертка, т.к. демо доступно в Android market.
3aicheg
29.05.2017 05:26+1Недавно приходилось делать под UWP нечто похожее на то, что описывает автор поста (рендеринг PDF в PNG-картинку). Использовал официальный API от Microsoft, Windows::Data::Pdf::PdfDocument, Windows::Data::Pdf::PdfPage, RenderToStreamAsync, вот это вот всё. Рендерил довольно сложные документы (на японском, форматирование, картинки и т. п.), проблем не заметил. Только под Винду, разумеется, ну так и XPS в WPF тоже.
Tantrido
28.05.2017 18:11+1Спасибо, полезно. Похоже таким же образом можно будет отображать xls, doc, odt и т.п.?
yshurik
28.05.2017 18:25+1Вполне, например посмотреть на внутренности Chrome Office Viewer extension и далее всё также как и для pdf.js
gudvinr
28.05.2017 19:04+3А почему JS? Есть же poppler, ?PDF.
monah_tuk
29.05.2017 05:46+1PDF.js это, по сути, готовый компонент. К примеру, на Linux сейчас ровно две читалки умеют историю перехода по локальным ссылкам: встроенный в Firefox на PDF.js и Okular. А при наличии технической документации в PDF с кучей ссылок и переходов — такой функционал просто обязан быть.
lexa
28.05.2017 19:52+7Вообще, в начале года Qt Labs выпустили QtPDF
http://blog.qt.io/blog/2017/01/30/new-qtpdf-qtlabs-module/
Который
а) работает (в Qt 5.4 загоняется легкими пинками, рассчитан на 5.6+)
б) Не требует тянуть за собой WebView/WebChannel/интерпретатор яваскрипта
в) Ну и вообще ничего так.
г) использует PDFium (BSD License) и сам модуль под LGPL
Это не говоря о других помянутых выше (но большинство — GPL, что может мешать)yshurik
28.05.2017 23:28+1К сожалению те мои pdf, из-за который этот проект и родился, qtpdf модуль не смог правильно отрендерить. Хотя там вроде и ничего сложного, сканы с какой-то системы. Из всего перепробованого (либы и утилиты) то как раз только PDF.js рендерит правильно. (ну и конечно последний acrobat reader, но толку)
herr_kaizer
29.05.2017 00:10+4Ожидал увидеть статью про рендеринг PDF — слои там, шрифты, вот это всё. А тут JS-дрисня :(
monah_tuk
29.05.2017 05:48+1Пока попробовать не могу: требуется OpenGL и на VirtualBox тупо не работает (собственно всё, что сделано на электроне — тоже). Поэтому вопрос: историю переходов по локальным ссылкам умеет, как оно сделано в Firefox?
CyberSoft
29.05.2017 09:15+1Поскольку в Firefox PDF.js-просмотрщик, хотел заскриншотить плохо отрендеренный pdf, да видимо за год хорошо подняли — не нашёл у себя таких. В то время начал искать альтернативы на джаве — глаза зацепились за Apache PDFBox: попробовал 1.8 — рендерил также проблемно. В тот момент уже вышла 2.0 RC2, попробовал её — вот она отрендерила прекрасно. Теперь видимо разницы нет.
hamnsk
29.05.2017 11:19+1Судя по скринам делалось на маке, не знаю как там в других современных ос, в убунте точно есть, а на маке так вообще пробел почти по любому типа файла и наслождаемся. А за попытку сделать что то полезное и закрепить материал написав статью дело хорошее, за это конечно зачет.
al_sh
29.05.2017 18:47делал на poppler https://people.freedesktop.org/~aacid/docs/qt5/. Нормально работает и на форточках и под линукс
maniacscientist
А потом люди удивляются — чего это приложения жрут столько?
eugenebabichenko
А потом люди удивляются, почему тормозит даже текстовый редактор :)
В этом плане самым большим злом является платформа Electron. И если VS Code на моём стареньком ноуте ещё сносно работает, то на Atom без слёз не взглянешь. Особенно это удивительно после
vimSublime Text. Тормозящие кошельки от всякой околокриптовалютной истории (привет разработчикам Ethereum) тоже доставляют радости. Особенно в свете того, что их бэкэнд всё равно написан на C++ или Go. И самое страшное/отвратительное — всё это не выглядит и не пытается выглядеть нативным.Уф, накипело.