Создаём PDF просмотрщик за пару часов

  • Tutorial
Давно про Qt не писали, потому сделаем что-то простое но мощное. Фреймворк был создан уже более десяти лет тому (скоро и 20), но всё ещё продолжает нас радовать и удивлять благодаря усилиям Qt сообщества.

Я хочу показать пример разработки приложения с затратой небольших усилий на стыке технологий создания десктопных приложений и веб-программирования.

Несколько недель тому я искал способ конвертации специфических 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");

Соберём и запустим:
image
Работает отлично, подход правильный, но показывает 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 версию в папку проекта. Доделаем получение списка файлов из аргументов командной строки итд.
image
Готово, рабочее десктопное приложение PDF viewer за пару часов.

GitHub
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 18
    +18
    А потом люди удивляются — чего это приложения жрут столько?
      +7

      А потом люди удивляются, почему тормозит даже текстовый редактор :)


      В этом плане самым большим злом является платформа Electron. И если VS Code на моём стареньком ноуте ещё сносно работает, то на Atom без слёз не взглянешь. Особенно это удивительно после vim Sublime Text. Тормозящие кошельки от всякой околокриптовалютной истории (привет разработчикам Ethereum) тоже доставляют радости. Особенно в свете того, что их бэкэнд всё равно написан на C++ или Go. И самое страшное/отвратительное — всё это не выглядит и не пытается выглядеть нативным.


      Уф, накипело.

      +1

      С PDF вообще все сложно. Однажды мне понадобился просмотр PDF в Android. Вариантов не густо — один на Java, жутко тормозящий, гугловский просмоторщик, который еще надо как-то собрать и прикрутить и pdf.js.


      Недавно понадобился просмотр документов в C#. Поискал, поискал — тоже не густо, какие-то сомнительные поделки. Остановился на просмотре XPS в WPF (pdf был не критичен).


      Никаких официальных SDK на этот счет не существует?

        +4

        Я бы использовал mupdf в вашем случае, он на С, поэтому можно и из Java и из C# использовать,
        плюс скорее всего для Java уже есть обертка, т.к. демо доступно в Android market.

          +1
          Недавно приходилось делать под UWP нечто похожее на то, что описывает автор поста (рендеринг PDF в PNG-картинку). Использовал официальный API от Microsoft, Windows::Data::Pdf::PdfDocument, Windows::Data::Pdf::PdfPage, RenderToStreamAsync, вот это вот всё. Рендерил довольно сложные документы (на японском, форматирование, картинки и т. п.), проблем не заметил. Только под Винду, разумеется, ну так и XPS в WPF тоже.
          +1
          Спасибо, полезно. Похоже таким же образом можно будет отображать xls, doc, odt и т.п.?
          +3

          А почему JS? Есть же poppler, μPDF.

            +1

            PDF.js это, по сути, готовый компонент. К примеру, на Linux сейчас ровно две читалки умеют историю перехода по локальным ссылкам: встроенный в Firefox на PDF.js и Okular. А при наличии технической документации в PDF с кучей ссылок и переходов — такой функционал просто обязан быть.

            +2

            Похоже на Electron. Только Qt.

              +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, что может мешать)
                +1
                К сожалению те мои pdf, из-за который этот проект и родился, qtpdf модуль не смог правильно отрендерить. Хотя там вроде и ничего сложного, сканы с какой-то системы. Из всего перепробованого (либы и утилиты) то как раз только PDF.js рендерит правильно. (ну и конечно последний acrobat reader, но толку)
                +4
                Ожидал увидеть статью про рендеринг PDF — слои там, шрифты, вот это всё. А тут JS-дрисня :(
                  +1

                  Пока попробовать не могу: требуется OpenGL и на VirtualBox тупо не работает (собственно всё, что сделано на электроне — тоже). Поэтому вопрос: историю переходов по локальным ссылкам умеет, как оно сделано в Firefox?

                    +1
                    Поскольку в Firefox PDF.js-просмотрщик, хотел заскриншотить плохо отрендеренный pdf, да видимо за год хорошо подняли — не нашёл у себя таких. В то время начал искать альтернативы на джаве — глаза зацепились за Apache PDFBox: попробовал 1.8 — рендерил также проблемно. В тот момент уже вышла 2.0 RC2, попробовал её — вот она отрендерила прекрасно. Теперь видимо разницы нет.
                      0
                      Ан нет…
                      Слева PDFBox 2.0 и справа FF 53.0.3 (в консоли показывает (PDF.js: 1.6.467))
                      скрин
                      image
                      +1
                      Судя по скринам делалось на маке, не знаю как там в других современных ос, в убунте точно есть, а на маке так вообще пробел почти по любому типа файла и наслождаемся. А за попытку сделать что то полезное и закрепить материал написав статью дело хорошее, за это конечно зачет.
                        0
                        делал на poppler https://people.freedesktop.org/~aacid/docs/qt5/. Нормально работает и на форточках и под линукс

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое