Некоторое время назад я написал статью, в которой показал, как открыть файл fb2 с помощью инструментов Qt. Спустя какое-то время я заметил в нем ряд недостатков, которые решил устранить. Более того, я обнаружил, что у некоторых fb2-ридеров есть и недостатки (а именно, некорректное отображение таблиц), что и побудило меня написать эту статью. Для начала можете прочитать последнюю статью . Будем действовать по тому же принципу: формируем строку книги в формате html и размещаем ее в объекте QTextBrowser.
Напомню, что для создания html документа нужно выполнить 3 действия: открыть тег, заполнить его содержимым и закрыть. Поэтому у нас есть 4 варианта: переписываем из исходного файла, переписываем с исправлениями, ничего не делаем (игнорируем) и проводим спецобработку.
Вступление
Все тэги формата fb2 можно разделить на 3 группы идентичные html, похожие на html и другие.
Идентичные теги показаны в таблице 1.
Таблица 1 - теги fb2 и html
description | tag |
---|---|
Paragraph | p |
Link | a |
Table | table |
Table row | tr |
Table cell | td |
Table header cell | th |
Superscript | sup |
Subscript | sub |
Monospace font | code |
Bold font | strong |
Quote | cite |
Подобные теги идентичны элементу html по назначению, но имеют разные названия. Все соответствия приведены в таблице 2.
Таблица 2 - Соответствие тегов fb2 и html
description | fb2 | html |
---|---|---|
Empty line | empty-line | br |
Italics | emphasis | i |
Strikethrough | strikethrough | strike |
Алгоритм
Алгоритм открытия файлов реализован в виде функции openFB2File. Остальные теги не имеют четкого соответствия с элементами html или похожи, но требуют специальной обработки (<a>, <body>, <image>). Некоторые из них можно просто игнорировать (<poem>). Чтобы преобразовать основную часть (<v>, <date>, <text-author>, <title>, <subtitle>), я предлагаю им сопоставить <p> с некоторыми дополнительными атрибутами. Алгоритм реализуется следующим образом:
QFile f(file); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "файл не открыт"; return false; } bool ok = true; QString special; QString description; // описание видео // настройки отображения int fontSize = 20; if( QSysInfo::productType() == "android" ) fontSize *= 1.8; QXmlStreamReader sr(&f); QString rId; QString rType; QString opt; QStringList thisToken; while( !sr.atEnd() ) { switch( sr.readNext() ) { case QXmlStreamReader::NoToken: qDebug() << "QXmlStreamReader::NoToken"; break; case QXmlStreamReader::StartDocument: *book = "<!DOCTYPE HTML><html><body style=\"font-size:%1px; font-family:Sans, Times New Roman;\">"; *book = book->arg(fontSize); break; case QXmlStreamReader::EndDocument: book->append("</body></html>"); break; case QXmlStreamReader::StartElement: thisToken.append( sr.name().toString() ); if(thisToken.contains("description")) // ОПИСАНИЕ КНИГИ { if( thisToken.back() != "image" ) //пропускаем всё кроме обложки break; // не выводим } if(sr.name().toString() == "title") { content->append(""); // добавляем пункт содержания break; } if( sr.name().toString() == "body" ) if( !sr.attributes().isEmpty() && sr.attributes().first().value().toString() == "notes") special = "notes"; // режим примечаний if(special == "notes") { if( sr.name().toString() == "section" ) { if( sr.attributes().count() > 0 ) { rId = sr.attributes().at(0).value().toString(); // ссылка на текст rType = ""; } } } opt = " align=\"justify\""; if(thisToken.contains("title") ) { opt = " align=\"center\" style=\"font-size:" +QString::number(int(fontSize * 1.5)) + "px\" "; if(special == "notes") { opt += (" id=\"" + rId + "\""); } } if(thisToken.contains("subtitle") ) { opt = " align=\"center\" style=\"font-size:" +QString::number(int(fontSize * 1.2)) + "px\" "; } if(thisToken.contains("annotation") ) { opt = " align=\"left\" "; } if(sr.name().toString() == "p" || sr.name().toString() == "subtitle") { book->append("<p"+opt +" >"); break; } if( sr.name().toString() == "table" ) { QString text; for(int i = 0; i < sr.attributes().count(); i++) { if(sr.attributes().at(i).name() == "id") qDebug() << sr.attributes().at(i).value().toString(); if(sr.attributes().at(i).name() == "style") text.append( "style=\"" +sr.attributes().at(i).value().toString()+ ";\"" ); } book->append("<table border=1 align=\"center\" style=\"border:solid;\" " + text + ">"); break; } if( sr.name().toString() == "tr" ) { QString text; if(!thisToken.contains("table")) qDebug() << "ошибка в таблице"; for(int i = 0; i < sr.attributes().count(); i++) { if(sr.attributes().at(i).name() == "aling") text.append( "aling=\"" +sr.attributes().at(i).value().toString()+ "\"" ); else qDebug() << "<tr>" << sr.attributes().at(i).name() << sr.attributes().at(i).value().toString(); } book->append("<tr " + text + ">"); break; } // if( sr.name().toString() == "td" || sr.name().toString() == "th" ) { if(!thisToken.contains("table")) qDebug() << "ошибка в таблице"; QString text; for(int i = 0; i < sr.attributes().count(); i++) { if(sr.attributes().at(i).name() == "aling") text.append( "aling=\"" +sr.attributes().at(i).value().toString()+ "\" " ); else if(sr.attributes().at(i).name() == "valing") text.append( "valing=\"" +sr.attributes().at(i).value().toString()+ "\" " ); else if(sr.attributes().at(i).name() == "colspan") text.append( "colspan=" +sr.attributes().at(i).value().toString()+ " " ); else if(sr.attributes().at(i).name() == "rowspan") text.append( "rowspan=" +sr.attributes().at(i).value().toString()+ " " ); else qDebug() << "<td th>" << sr.attributes().at(i).name() << sr.attributes().at(i).value().toString(); } book->append( "<"+sr.name().toString()+ " " + text +">" ); break; } if( sr.name().toString() == "empty-line" ) { book->append("<br/>"); break; } if(sr.name().toString() == "strong" || sr.name().toString() == "sup" || sr.name().toString() == "sub" || sr.name().toString() == "code" || sr.name().toString() == "cite") { book->append( "<" + sr.name().toString() + ">"); break; } if(sr.name().toString() == "emphasis") { book->append( "<i>" ); break; } if( sr.name().toString() == "v" ) { book->append("<p align=\"left\" style=\"margin-left:25px;\">"); break; } if(sr.name().toString() == "strikethrough") { book->append( "<strike>" ); break; } if( sr.name().toString() == "a" ) // метка примечания { rId = ""; for(int i = 0; i < sr.attributes().count(); i++) { if(sr.attributes().at(i).name() == "type" ) { //rType = sr.attributes().at(i).value().toString(); } if(sr.attributes().at(i).name() == "href") { rId = sr.attributes().at(i).value().toString(); } } book->append("<a href=\"" + rId + "\"> "); //qDebug() << "a" << rId; } if(sr.name().toString() == "poem" || sr.name().toString() == "stanza" || sr.name().toString() == "epigraph") { break; } if(sr.name().toString() == "text-author" ) // автор текстта { book->append( "<p align=\"justify\" style=\"margin-left:45px;\">" ); break; } if(sr.name().toString() == "date" ) // автор текстта { book->append( "<p align=\"justify\" style=\"margin-left:45px;\">" ); break; } if( sr.name().toString() == "image" ) // расположение рисунков { if(sr.attributes().count() > 0) book->append("<p align=\"center\">"+sr.attributes().at(0).value().toString() + "#" + "</p>"); } if(sr.name() == "binary") // хранилище рисунков { if(sr.attributes().at(0).name() == "id") { rId = sr.attributes().at(0).value().toString(); rType = sr.attributes().at(1).value().toString(); } if(sr.attributes().at(1).name() == "id") { rId = sr.attributes().at(1).value().toString(); rType = sr.attributes().at(0).value().toString(); } } break; case QXmlStreamReader::EndElement: if( thisToken.last() == sr.name().toString() ) { thisToken.removeLast(); } else qDebug() << "error token"; if(thisToken.contains("description")) // ОПИСАНИЕ КНИГИ { break; // не выводим } if( sr.name().toString() == "p" || sr.name().toString() == "subtitle" || sr.name().toString() == "v" || sr.name().toString() == "date" || sr.name().toString() == "text-author") { book->append("</p>"); break; } if(sr.name().toString() == "td" || sr.name().toString() == "th" || sr.name().toString() == "tr" || sr.name().toString() == "table" || sr.name().toString() == "sup" || sr.name().toString() == "sub" || sr.name().toString() == "strong" || sr.name().toString() == "code" || sr.name().toString() == "cite") { book->append( "</"+sr.name().toString()+">" ); break; } if( sr.name().toString() == "a" ) { rId.remove("#"); book->append( "</a><span id=\"" + rId + "___" + "\"></span>" ); qDebug() << "id" << rId + "___"; break; } if(sr.name().toString() == "emphasis") { book->append( "</i>" ); break; } if(sr.name().toString() == "strikethrough") { book->append( "</strike>" ); break; } if(sr.name().toString() == "stanza") // конец строфы { //book->append("<br/>"); break; } if(sr.name().toString() == "epigraph" || sr.name().toString() == "poem") { break; } if(special == "notes") // режим извлечения примечаний { if( sr.name().toString() == "body" ) { special = ""; } if( sr.name().toString() == "section" ) { book->insert(book->lastIndexOf("<"), "<a href=\"#" + rId + "___" + "\"> назад</a>"); } } break; case QXmlStreamReader::Characters: if( sr.text().toString() == "" ) { //qDebug() << "isEmpty"; break; } if( sr.text().toString() == "\n" ) { //qDebug() << "isEmpty"; break; } if(thisToken.contains("description")) // ОПИСАНИЕ КНИГИ { description.append(sr.text().toString() + " "); // не выводим break; } if(thisToken.contains( "binary" ) ) // для рисунков { QString image = "<img src=\"data:" + rType +";base64," + sr.text().toString() + "\"/>"; book->replace("#"+rId +"#", image); rId = ""; rType = ""; break; } if(thisToken.contains("div")) { qDebug() << "div" << sr.text().toString(); break; } if(thisToken.back() == "FictionBook") { qDebug() << "FictionBook" << sr.text().toString(); break; } if( thisToken.contains("title") ) // формируем содержание { content->back() += " " + sr.text().toString();//content->back()=="" ? "" : " " + // qDebug() << "title" << sr.text().toString(); } if(special == "notes" && !thisToken.contains("title") ) // добавление текста примечания { rType += " "; rType += sr.text().toString(); //break; } if(thisToken.back() == "p" || thisToken.back() == "subtitle" || thisToken.back() == "v" || thisToken.back() == "emphasis" || thisToken.back() == "strong" || thisToken.back() == "strikethrough" || thisToken.back() == "sup" || thisToken.back() == "sub" || thisToken.back() == "td" || thisToken.back() == "th" || thisToken.back() == "code" || thisToken.back() == "cite" || thisToken.back() == "text-author" // ?? || thisToken.back() == "date" ) { book->append( sr.text().toString() ); break; } if(thisToken.back() == "section") { break; } if(thisToken.back() == "body") { break; } if(thisToken.back() == "table" || thisToken.back() == "tr" || thisToken.back() == "title" || thisToken.back() == "poem" || thisToken.back() == "stanza") { //book->append( sr.text().toString() ); break; } if(thisToken.back() == "annotation") { qDebug() << "annotation" << sr.text().toString(); break; } if(thisToken.back() == "a") { book->append( sr.text().toString() ); break; } //все прочие тэги if( !sr.text().toString().isEmpty() ) { qDebug() << thisToken.back() << "исключение" ; book->append("<span> " + sr.text().toString() + "</span>"); } break; } } f.close();
The file open function is launched by pressing the button:
ui->textBrowser->clear(); QString text; QStringList content; if(name.endsWith(".fb2")) { openerTextFiles otf; otf.openFB2File(name, &text, &content); ui->textBrowser->setText(text); ui->textBrowser->verticalScrollBar()->setValue(0); }
Теперь сгенерированную html-страницу можно легко сохранить
QFile file(name); if ( !file.open(QIODevice::WriteOnly | QIODevice::Text) ) return; QTextStream out(&file); out << ui->textBrowser->toHtml(); file.close();
Наша читалка готова.
Исходный код можно скачать здесь .
добавил функцию чтения из fb2.zip чеоез QZipReader
в.pro добавить: QT += gui-private,
в .h :
ридер через QByteArray сделал
ну у меня несколько компактней получилось
```
и с навигацией вы круто заморочилить - QTextBrowser отслеживает нажатие на линки
```
Спасибо за комментарии, а zip-архивация пригодится для открытия книг fb3 и epub. Это работает на Qt без дополнительных библиотек? Я сделал архиватор, но qzipwriter_p.h и qzipreader_p.h, а ещё zlib качал отдельно.