SDK для підтримки впровадження електронних книг у форматі FB2



Ви знаєте, що «нобелівку» з наукової фантастики отримав китайський автор Лю Цысинь (Liu Cixin, 劉慈欣) з твором The Three-Body Problem ( 三體). На цю книгу звернули увагу Барак Обама (пруф) і Марк Цукерберг (пруф).

image
Ольга Браатхен з власної ініціативи переклала книгу на російську (ось тут можна качнути fb2), за що їй велике спасибі.

Ще один кандидат на «нобелівку» у 2016 — це Ніл Стівенсон (написав «Лавину» і «Криптономикон») з твором Seveneves качнути англійською можна тут, шкода, що на російську ніхто не взявся перекладати).

Розробники компанії EDISON створили програму Управління доступом до електронних документів, про що я писав пару років тому, а сьогодні мова піде про SDK для підтримки впровадження електронних книг у форматі FB2.


Введення

Застосування інформаційних технологій у бібліотечній сфері призвело до появи інтернет-сервісів, що надають читачам видалений доступ до багатого набору художньої, наукової і технічної літератури. Такі сервіси виводять бібліотечну справу на новий рівень. Бібліотеки можуть об'єднуватися в єдину мережу, формуючи величезну географічно розподілену базу оцифрованого контенту, а надання бібліотечних послуг інтернет розширює цільову аудиторію і дає додатковий дохід. Ключові користувачі також отримують переваги: не потрібно витрачати час на поїздку в бібліотеку, брати книги в особисте користування і дбати про своєчасну здачу; можливий доступ до рідкісної літератури, відсутньої в конкретному населеному пункті. Правовласники можуть отримувати роялті, надаючи контент на взаємовигідних умовах.

Особливу увагу в бібліотечних сервісах відводиться дотриманню авторських прав і захисту від копіювання. На транспортному рівні використовуються пропрієтарні формати, а програмне забезпечення на стороні клієнта не повинне зберігати отриманий контент на диску.

Основна частина контенту формується шляхом ручної оцифровки фізичних носіїв, із застосуванням сканерів і подальшим розпізнаванням тексту для забезпечення можливості пошуку, але це не єдине джерело. Існує безліч цифрових форматів, призначених для зберігання електронних книг, і вже оцифрованих книг в форматах даних теж не мало. Відповідно, потрібна підтримка різних електронних форматів в бібліотечних сервісах. Розповім про розробку SDK (інструментарію розробника) для підтримки впровадження електронних книг у форматі Fiction Book в один із сервісів.

Завдання

Сервіс доставки контенту надає користувачам посторінковий доступ до літератури у віддаленому сховищі. Доступ до кожній сторінці фіксується і тарифікується. У сервісі реалізований повнотекстовий пошук, результати якого підсвічуються напівпрозорими прямокутниками. Трафік між сервером і клієнтом захищений і являє собою закритий бінарний формат.

Перед програмістами EDISON стояло завдання створити SDK, що надає кінцевому розробнику набір готових до використання функцій, що сприяють спрощенню процедури впровадження підтримки електронних книг у форматі FB2, а також використанню загальної кодової бази при побудові серверної і клієнтської частини рішення.

Виходячи з функціональних можливостей сервісу, вимоги до набору функцій SDK були визначені заздалегідь:
  1. отримання бібліографічної інформації;
  2. отримання кількості сторінок електронної книги;
  3. отримання результатів повнотекстового пошуку з посиланнями на відповідні сторінки;
  4. отримання координат прямокутників для підсвічування результатів повнотекстового пошуку;
  5. отримання контенту довільної сторінки в двійковому форматі і рендеринг сторінок.
Реалізація і технології: З++ / Qt

Рішення

Електронна книга в FB2 — односторінковий. У форматі не передбачено інформації як повинен виглядати документ. В першу чергу належало вирішити проблему з розбиттям FB2-документу на сторінки, не дублюючи при цьому вміст документа. В результаті був спроектований формат індексного файлу, який зберігає мета-дані про вихідний FB2-документ, отримані в результаті парсинга оригінального документа, рендеринга і розбиття документу на сторінки.
Індексний файл містить місцезнаходження фрагментів XML документа у вигляді зміщення від початку документа і довжини фрагмента у кількості знаків, а також XML-префікс.

Структура індексного файлу включає три розділу:
  • description — фрагмент з описом документа;
  • binary — фрагменти з картинками в оригінальному документі;
  • page — фрагменти документа, де починаються і закінчуються сторінки, отримані в результаті рендеринга з заданими параметрами розміру сторінки, відступів і шрифту.


Інформація про місцезнаходження фрагмента документа XML дозволяє віднімати потрібний шматок інформації з оригінального документа без необхідності його парсингу XML-префікс дозволяє побудувати мініатюрний XML-документ, що містить розмітку тільки потрібної сторінки, розпарсити її і тут же отрендерить потрібну сторінку за запитом користувача.

Приклад індексного файлу з розбиттям документа на сторінки.
<document>
<description>
<fragment>
<offset>418</offset>
<length>5230</length>
<prefix><![CDATA [<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">]]></prefix>
</fragment>
</description>
<binary id="cover.jpg" >
<fragment>
<offset>43034</offset>
<length>48151</length>
</fragment>
</binary>
<page number="1" >
<fragment>
<offset>5657</offset>
<length>1779</length>
<prefix><![CDATA [<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink"><body>]]></prefix>
</fragment>
</page>
<page number="2" >
<fragment>
<offset>7436</offset>
<length>2366</length>
<prefix><![CDATA [<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink"><body><section><section><p>]]></prefix>
</fragment>
</page>
</document>


Коли з'явилося уявлення про алгоритм розбиття документу на сторінки, ми приступили до його реалізації. Для формування індексу був обраний метод потокового парсинга оригінального документа з використанням стандартних класів бібліотеки Qt, завдяки можливості послідовного читання XML-файлу та збереження інформації про зсув у файлі у кількості знаків, за допомогою методу QXmlStreamReader::characterOffset.

У процесі парсинга FB2-документа по мірі просування від тега до тегу, параграфи документа розбираються на набори слів, які потім знову збираються в рядки. У відповідності з файлом налаштувань кожної рядку задається максимальна ширина з урахуванням заданої ширини полів сторінки і відступу для параграфів. Для рядків також задається міжрядковий інтервал, зазначений у файлі налаштувань. Залежно від тегів XML-документа задаються параметри шрифту, розмір, накреслення, вирівнювання. Для заголовків і підзаголовків задається вирівнювання по центру, для епіграфів — вирівнювання по правому краю, за замовчуванням — вирівнювання по лівому краю. По мірі додавання слів у рядок довжина рядка перераховується шляхом додавання довжини всіх доданих слів. Якщо довжина рядка перевищує задану ширину сторінки, то рядок додається до об'єкта сторінки; слово, яке не влізло в рядок, додається в чергову рядок. По мірі додавання рядків на сторінці, перераховується висота всіх рядків з урахуванням міжрядкового інтервалу. При впровадженні картинок за висоту рядка довелося вважати максимальну висоту об'єкта, доданого до рядку. Якщо висота всіх доданих рядків перевищує задану висоту сторінки з урахуванням відступів, в індексний файл додається черговий фрагмент. Описаний алгоритм застосовується як при розбитті FB2-документу на сторінки, так і при довільному доступі до сторінки за засобом використання індексного файлу.

Так як метод QXmlStreamReader::characterOffset повертає зсув у кількості знаків, а не в байтах, то при довільному доступі до сторінок документа довелося вичитувати початок оригінального файлу, і тільки потім вичитувати цікаву частину документа, так як документ може містити кирилицю і латиницю, і використання одного лише зміщення по файлу у байтах, використовуючи метод seek, неминуче призвело б до помилок.
QString Document::documentFragment(uint offset, uint length)
{
QString fragment;
QFile file(m_fileName);

if (!file.open(QIODevice::ReadOnly))
{
m_error = IOError;
return fragment;
}

QTextStream fileStream(&file); 
fileStream.setCodec("UTF-8");
fileStream.setAutoDetectUnicode(true);
fileStream.seek(0); 
fileStream.read(offset);
fragment = fileStream.read(length); 
file.close(); 

if ((uint) fragment.size() < length)
{
m_error = IOError;
fragment = QString();
return fragment;
}

return fragment;
}


Незважаючи на це, втрати в продуктивності ні, доступ до останньої сторінки документа здійснюється так само швидко, як і на першій сторінці, і займає менше секунди. Справа в тому, що середній обсяг книжки без картинок у форматі FB2 рідко перевищує 10 Мб.

Розбиття 7 Мб файлу на 998 сторінок і підготовка індексу займають близько 10 секунд. Розбиття 9 Мб файлу на 1576 сторінок займає близько 15 секунд. У середньому за одну секунду рендерится близько 100 сторінок. При наявності індексу документ відкривається за 50 мілісекунд.

Далі належало вирішити завдання повнотекстового пошуку з прив'язкою до сторінок документа. І тут для забезпечення швидкодії все-таки довелося дублювати вміст документа, але вже без XML розмітки, а у вигляді звичайного текстового файлу. У текстовий індекс вставляються маркери початку і закінчення сторінок у вигляді нульового байта. Для прив'язки до результатів пошуку сторінок і зберігання координат прямокутників, знадобилося організувати два допоміжних індексу в бінарному форматі. Допоміжний індекс для прив'язки результатів пошуку на сторінках зберігає номер сторінки, порядковий номер початкового і кінцевого байта маркера сторінки в текстовому індексі.

Повнотекстовий пошук здійснювався за допомогою бібліотеки, наданої замовником, яка повертає всі словоформи по заданому слову. Пошуковий запит розбивається на набір слів, потім по кожному слову знаходяться всі словоформи, потім по сформованого масиву знайдених словоформ здійснюється пошук в текстовому індексі. З допомогою допоміжного індексу і маркерів початку/закінчення сторінок у текстовому індексі визначаються номери сторінок, до яких належать результати пошуку. Допоміжний індекс з координатами прямокутників формується лише при запиті сторінки. На кожну сторінку формується окремий індексний файл. В процесі отримання координат прямокутників задіяний все той же алгоритм розбиття оригінального документа на сторінки завдяки розподіленню рядків на слова і обчисленню меж кожного слова, а індекс слугує для того, щоб не викликати цей алгоритм повторно при переході на ту ж сторінку.

Створення індексів для повнотекстового пошуку займає вже близько хвилини на документах обсягом близько 10 Мб. Пошук же, при наявності індексів, працює близько однієї секунди на документі з 1576 сторінками.

Черговим сюрпризом було відображення напівпрозорих прямокутників над знайденими фрагментами тексту. Так як спочатку математика з розрахунку меж слів була в пікселях, це викликало неточності в кілька пікселів при масштабуванні сторінок документа. Рішення було знайдено: довелося всього лише перевести всі обчислення дюйми з урахуванням DPI-пристрої виведення, використовуючи при цьому значення з плаваючою крапкою замість чисел, виправивши при цьому істотну частину коду.
m_dpiX = (qreal) QApplication::desktop()->physicalDpiX();
m_dpiY = (qreal) QApplication::desktop()->physicalDpiY();
QFontMetricsF fm(m_font); 
m_rect = fm.boundingRect(m_text);
m_textDescent = fm.descent() / m_dpiY;

qreal width = m_rect.width() / m_dpiX;
qreal height = m_rect.height() / m_dpiY;

m_rect.setSize(QSizeF(width, height)); 

На фінішній прямій залишалося вирішити питання з серіалізацією подання сторінки, включаючи набір прямокутників, в бінарний формат і зворотного читання з нього для передачі вмісту сторінки на клієнта і подальшого візуалізації за допомогою все тієї ж SDK. Тут виявилося все досить просто: на допомогу прийшов стандартний клас бібліотеки QT, QDataStream.

У процесі декомпозиції при вирішенні завдання були виділені наступні класи.
  • Fb2Document — документ FB2, основний клас, що інкапсулює логіку парсинга документа, розбиття на сторінки, формування індексу, надання доступу до довільній сторінці з використанням сформованого індексу, а також повнотекстовий пошук.
  • Fb2Page — сторінка FB2-документа, що інкапсулює логіку заповнення сторінки набором рядків документа і рендеринга сторінок, визначення ознаки закінчення сторінки. Надає інтерфейс для завдання розміру сторінки в дюймах по ширині і висоті, а також відступи від країв сторінки.
  • Fb2Word — слово, інкапсулює логіку обчислення меж слова в дюймах на канві документа, у відповідності з заданими параметрами шрифту, серіалізацію слів сторінки документа в бінарний формат, читання слів з бінарного формату.
  • Fb2String — рядок набору слів (Fb2Word), інкапсулює логіку заповнення рядків списком слів, визначення ознаки закінчення рядка, вирівнювання рядка по лівому, правому краю, по центру, облік міжрядкового інтервалу заданого у файлі налаштувань, сериалиазацию рядків сторінки документа в бінарний формат, читання рядків з бінарного формату.
  • Fb2Image — зображення, інкапсулює логіку рендеринга зображень документа, серіалізацію картинок в бінарний формат, читання картинок з бінарного формату.
  • Fb2Index — індекс, що інкапсулює логіку формування індексного файлу і зчитування з нього.
  • Fb2Fragment — фрагмент FB2-документа, що являє собою основну структуру індексного файлу.
  • Fb2Settings — файл налаштувань, інкапсулює логіку роботи з файлом налаштувань читання/запис.
  • Fb2Func — клас обгортка, надає набір функцій SDK у відповідності з інтерфейсом, заданим при постановці завдання.
  • Словник — клас обгортка над морфологічним словником.




Методи всіх класів SDK були покриті модульними тестами, щоб гарантувати коректність розбиття FB2-документу на сторінки, а також що користувач в клієнтській програмі побачить рівно ту ж картинку при запиті сторінки, що буде спочатку відрендерена на стороні сервера при підготовці індексного файлу.

В результаті кінцева мета була досягнута. Перевага розробки SDK порівняно з коробковим рішенням полягає в гнучкості. Все найскладніше приховано за викликом простих функцій, а розробник, що використовує SDK може самостійно приймати рішення, як використовувати: наприклад, будувати індекси заздалегідь для забезпечення швидкодії функцій системи і більш комфортної роботи користувачів чи будувати індекси за першим зверненням до документа і першому пошуковому запиту і видаляти їх при рідкісному використанні для економії дискового простору.

Для демонстрації працездатності SDK замовнику, на її основі було реалізовано два Desktop-додатки. FictionBookReader надає функціонал примітивного рідера FB2-документів з можливістю посторінкового перегляду та повнотекстового пошуку з підсвічуванням результатів пошуку.



FB2SDK Demo наочно показує функціонал серверної і клієнтської частини SDK. Функціонал серверної частини виділений у вкладку Server, яка демонструє парсинг документа і формування багатосторінкового індексу, а також формування файлів з прямокутниками і повнотекстового індексу. Функціонал клієнтської частини виділений у вкладку Client, яка демонструє рендеринг сторінок документа по сформованому бінарним файлом.







Більше проектів:
Як за 5233 людино-години створити софт для микротомографа
Управління доступом до електронних документів. Від DefView до Vivaldi
Розробка простого плагіна для JIRA для роботи з базою даних
Розробка простого плагіна для JIRA для роботи з базою даних: надаємо нашому плагіну нормальний зовнішній вигляд
Автооновлення служби Windows через AWS для бідних

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.