Просмотр PDF в Rocket.Chat

По долгу работы пришлось столкнуться с замечательным opensource продуктом RocketChat (RC). Это приложение имеет серверную часть, основанную на платформе Meteor, https://github.com/RocketChat/Rocket.Chat, десктоп представление реализованное в Electron, https://github.com/RocketChat/Rocket.Chat.Electron, и, помимо всего прочего, реализации для android и iOS. В заметке рассматриваемое решение затрагивает лишь первые два аспекта данного продукта, а именно основная часть Сервер (Meteor) и Клиент (настольное приложение Electron).

В RC был реализован (причём криво, примечание - в новых версиях уже пофиксили, вывод только первой страницы pdf) просмотр pdf за счёт рендеринга первой страницы в canvas тег на клиенте, что нагружало клиента и не давало нормального просмотра. По умолчанию поведение загруженного pdf в сообщении, при клике на ссылку с названием документа вызывало скачивание файла pdf на компьютер.

Решение, которое последует ниже, открывает окно и выводит файл на просмотр с возможностью скачать, распечатать, просмотреть все страницы, поменять масштаб или просто закрыть окно, что гораздо удобнее поведения по умолчанию. В браузере файл будет загружен в модальное окно, в тег iframe, где будет вызван pdf обзор с помощью встроенной в браузер поддержки pdf (если таковая имеется, но во всех современных браузерах - полет нормальный). В настольном приложении будет создано окно, которое имеет поддержу pdf просмотра с помощью специальной библиотеки, так как по умолчанию в Electron, точнее под его капотом, библиотеке рендеринга из Chromium нет этой поддержки, в отличие от полноценного браузера, чтобы там где не писали в интернетах (например здесь, типа лёгкое решение stackoverflow, которое как оказалось не работает). Сейчас до сих пор актуальна проблема pdf просмотра в самом фреймворке Electron, например здесь https://github.com/electron/electron/issues/11065. Но в инфраструктуре RC, как в принципе и любой другой, в контексте Electron, проблема оказывается решаемой.

Серверная часть, RC Meteor

packages/rocketchat-message-attachments/client/messageAttachment.html:


<!-- ... more code up ... -->

<div class="attachment-title">
    {{#if title_link}}
        {{#if isPDF}}
            <a href="{{fixCordova title_link}}" target="_blank" rel="noopener noreferrer" class="pdf-open-in-modal">{{title}}</a>
        {{else}}
            <a href="{{fixCordova title_link}}" target="_blank" rel="noopener noreferrer">
                {{#if isFile}} {{_ "Attachment_File_Uploaded"}}: {{/if}}
                {{title}}
            </a>
            {{#if title_link_download}}
                <a class="icon-download attachment-download-icon" href="{{fixCordova title_link}}" target="_blank" download="" rel="noopener noreferrer"></a>
            {{/if}}
        {{/if}}
    {{else}}
        {{title}}
    {{/if}}
    {{#if collapsed}}
        <span class="collapse-switch icon-right-dir" data-index="{{index}}" data-collapsed="{{collapsed}}"></span>
    {{else}}
        <span class="collapse-switch icon-down-dir" data-index="{{index}}" data-collapsed="{{collapsed}}"></span>
    {{/if}}
</div>

<!-- ... more code down ... -->

В шаблоне нужно повесить класс pdf-open-in-modal всем элементам вложениям pdf. Классы noopener noreferrer предотвращают вызов окна загрузки файла в десктоп приложении.


packages/rocketchat-ui/client/views/app/pdfViewer.html:

<template name="pdfViewer">
    <style>
        .rc-modal {
            width: 90%;
            height: 100%;
            max-width: none;
        }

        .pdf-viewer {
            position: absolute;
            top: 45px;
            left: 0;
            width: 100%;
            height: 100%;
        }

        .rc-modal__content {
            margin-top: 16px;
            height: 100%;
            max-height: 95%;
            overflow-y: hidden;
        }

        .new-window-panel {
            position: absolute;
            top: 0;
            left: 50%;
            font-size: 35px;
        }
    </style>
    <div class="new-window-panel">
        <a href="{{absPdfLinkInline}}" target="_blank">&#9112;</a>
    </div>
    <iframe class="pdf-viewer" src="{{absPdfLinkInline}}"></iframe>
</template>

Шаблон браузерного pdf просмотрщика, который рендерится в модалку. Динамические стили нужны для перезаписи глобальных стилей стандартного модального окна RC, чтобы увеличить его по ширине и высоте.


packages/rocketchat-file-upload/server/config/FileSystem.js

packages/rocketchat-file-upload/server/config/GridFS.js

if (req.query && req.query.hasOwnProperty('disposition')) {
   res.removeHeader('Content-Security-Policy');
   res.setHeader('Content-Disposition', `${ req.query.disposition }; filename*=UTF-8''${ encodeURIComponent(file.name) }`);
} else {
   res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`);
}

Этот код нужно добавить в соответствующие файлы здесь и здесь, для того чтобы предотвратить скачивание файла, а вместо этого загрузить его в iframe. Поиск лучшего решения по добавлению в одном месте в процессе


packages/rocketchat-ui/client/views/app/room.js:


/* ... more code up ... */

Template.room.events({

/* ... more code up ... */

'click .pdf-open-in-modal'(event) {
		event.preventDefault();
		const isElectron = (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1);
		const relPdfLink = this._arguments[1].attachments[0].title_link;
		const pdfId = `${ relPdfLink.split('/')[2] }.pdf`;
		const absPdfLink = Meteor.absoluteUrl().replace(/\/$/, '') + relPdfLink;
		if (isElectron) {
			const newWindow = window.open(absPdfLink, pdfId, 'width=1,height=1');
			newWindow.close();
		} else {
			const absPdfLinkInline = `${ absPdfLink }?disposition=inline`;
			modal.open({
				showConfirmButton: false,
				text: Blaze.toHTMLWithData(Template.pdfViewer, { absPdfLinkInline }),
				html: true,
			});
		}
	},
});

/* ... more code down ... */

Основная часть на клиенте meteor, которая "понимает" текущее окружение (браузер или десктоп) и в зависимости от этого загружает содержимое либо в iframe (браузерный pdf просмотрщик), либо открывает окно, которое инициирует событие в десктоп приложении Electron, для того чтобы отдать данные и открыть окно с поддержкой pdf просмотра, но уже в Electron, с этими полученными данными (под данными понимается прямая ссылка на pdf).


packages/rocketchat-ui/package.js:


// TEMPLATE FILES

/* ... more code up ... */

api.addFiles('client/views/app/pdfViewer.html', 'client');

/* ... more code down ... */

Регистрация шаблона.

Клиентская часть, Electron

yarn add electron-pdf-window

Добавляем библиотеку.


src/public/preload.js:


/* ... more code up ... */

const { BrowserWindow } = require('electron').remote;
const PDFWindow = require('electron-pdf-window');

/* ... more code down ... */

window.open = ((defaultWindowOpen) => (href, frameName, features) => {

/* ... more code up ... */

if (RegExp(/.*\.pdf$/).test(href)) {
		const pdfWindow = new BrowserWindow({ width: 800, height: 600 });
		PDFWindow.addSupport(pdfWindow);
		pdfWindow.loadURL(href);
	}

/* ... more code down ... return ... */

})(window.open);

Собственно добавление поддержки pdf в electron, здесь мы подписываемся на событие открытие окна, которое происходит в meteor, чтобы получить прямую ссылку на файл pdf и переоткрыть его в электроновском pdf-окне. Вот, собственно, такой хак. Далее стартуем или компилируем приложение.

issue и связанные с ним PR на github.



Перед тем как писать комментарии, рекомендую ознакомиться:

Markdown синтаксис »

Оформление кода »

Нужна аватарка »

Комментарии