Идея

Нам необходимо давать пользователям возможность быстрого (беглого) просмотра связанных с клиентом документов, которые хранятся в PDF. Сами по себе они являются отсканированными реальными договорами, заявлениями, доп. соглашениями и т.п. Просмотр должен осуществляться на странице "Карточки клиента" или в плавающем фрейме. Так же первое ограничение — использование в качестве СУБД — PostgreSQL.

Это ограничение оговаривается в силу того, что в последующем необходимо сделать переносимый код, который не будет зависеть от используемой СУБД.

Итак, наш компонент должен:

  1. Обрабатывать загруженные файлы формата pdf (потом возможно и другие).

  2. Сохранять их во внутреннем формате, который подойдёт для быстрого отображения клиенту. Сохранять с порядком следования страниц, потому что изначально формат PDF применялся как контейнер для отсканированных TIFF-файлов.

  3. Отображать запрошенные документы на странице, либо в плавающем фрейме.

  4. Должна быть встроена навигация по документу и возможность вывода на принтер.

Внешний вид

Проектирование было в лучших олд-скульных традициях — почти на салфетке (на листе черновой бумаги ;) ).

Скетч внешнего вида NinjaViewer
Рис 1. Черновой скетч (олд-скул).
Прототип интерфейса NinjaViewer
Рис 2. После этого «в руки был взят» Фотошоп и получился такой прототип, к которому и будем стремится.

Может быть кнопки маловаты, но это решит время и мнение пользователей. (А к тому же ещё масштаб изображений маловат)

Внутреннее устройство

Изначально ставится задача использовать PostgreSQL для хранения этих документов. Хранение файлов на ФС я отмёл сразу, потому что при создании бэкапов очень желательно не «размазывать» данные по разным местам, а делать всё сразу одной командой.

База данных

Структура базы данных должна удовлетворять следующим условиям:

  • хранить бинарные данные с использованием OID (в случае с PostgreSQL), или в полях типа BLOB (в случае с MySQL);

    Хранение в полях типа Bytea (для PostgreSQL) создает дополнительные накладные расходы, а также отличается в 3-4 раза меньшим быстродействием по сравнению с использованием OID.

  • необходимо хранить оригинал загруженного файла и его преобразованные страницы;

  • для страниц нужна нумерация.

Для PostgreSQL нам нужна такая структура:

CREATE TABLE documents (
    id integer NOT NULL,
    filename text NOT NULL,
    filetype character varying(100) NOT NULL,
    additional text,
    creation_date timestamp without time zone,
    last_change timestamp without time zone,
);
COMMENT ON TABLE documents IS 'Таблица различных электронных документов (договоры, факсы и т.п.)';
 
CREATE SEQUENCE documents_id_seq
    INCREMENT BY 1
    NO MAXVALUE
    NO MINVALUE
    CACHE 1;
ALTER SEQUENCE documents_id_seq OWNED BY documents.id;
 
CREATE TABLE documents_blobs (
    document_id integer NOT NULL,
    oid oid
);
COMMENT ON TABLE documents_blobs IS 'Оригиналы документов'; 
 
CREATE TABLE documents_pages (
    document_id integer,
    page_number integer,
    page_oid oid
);
COMMENT ON TABLE documents_pages IS 'Страницы документов';

Изменение формата и разбивка на отдельные страницы

После загрузки на сервер PDF-документ должен быть подвергнут обработке, разбивке на странице и конвертации в иной формат (например, JPEG или PNG).

Полученные изображения должны быть отсортированы по порядку следования страниц и загружены в соответствующую таблицу. А исходный файл должен быть загружен в таблицу оригиналов.

Важно

Всё это сделать во время одной транзакции, которую можно откатить, если добавление в БД пошло не так, как планировалось.

Для преобразования форматов я принял решение использовать проверенные никсовые утилиты: ghostscript, convert, библиотеку imagemagick.

В качестве конечно формата, после теста, мной был выбран PNG, как более качественный, занимающий не намного больше места, чем JPEG, являющий открытой заменой GIF'у, да и просто мне PNG больше импонирует.

Программный интерфейс

Самое основное в этом компоненте - это, конечно же, отображение документов.

Для этого необходимо рендерить саму форму виджета (которую я так бережно рисовал «от руки», а потом в Фотошопе), а также изображения внутри неё.

Нам необходимо реализовать два метода:

public function render() { //... }
public function renderImage($documentId, $pageNumber) { //... }

Первый будет отображать виджет с элементами управления и окном отображения документами. А второй будет вызываться первым, забирать данные из БД и передавать их клиенту.

Вроде бы всё

Программная реализация, а также скриншоты черновых (но уже рабочих!) вариантов я опубликую в следующем посте.