Some time ago I wrote an article in which I showed how to open a fb2 file with Qt tools. After some time, I noticed a number of shortcomings in it, which I decided to eliminate. Moreover, I found that some fb2 readers also have disadvantages (namely, incorrect display of tables), which prompted me to write this article. For starters, you can read the last article . We will act on the same principle: we form the book string in html format and place it in the QTextBrowser object.
Let me remind you that in order to create an html document, you need to perform 3 actions: open the tag, fill it with content and close it. Therefore, there are 4 options for us: we rewrite from the source file, rewrite with corrections, do nothing (ignore), and conduct special processing.
Introduction
All fb2 format tags can be divided into 3 groups identical to html, similar to html, and others.
Identical tags are shown in table 1.
Table 1 - fb2 and html tags
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 |
Similar tags are identical to the html element by purpose, but have different names. All correspondences are given in table 2.
Table 2 - Correspondence between fb2 and html tags
description | fb2 | html |
---|---|---|
Empty line | empty-line | br |
Italics | emphasis | i |
Strikethrough | strikethrough | strike |
Algorithm
The file opening algorithm is implemented as the openFB2File function. Other tags do not have clear correspondence with html elements or are similar, but they need special processing (<a>, <body>, <image>). Some of them can simply be ignored (<poem>). To convert the main part (<v>, <date>, <text-author>, <title>, <subtitle>), I suggest that they correspond <p> with some additional attributes. The algorithm is implemented as follows:
- 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);
- }
Now the generated html page can be easily saved
- QFile file(name);
- if ( !file.open(QIODevice::WriteOnly | QIODevice::Text) )
- return;
- QTextStream out(&file);
- out << ui->textBrowser->toHtml();
- file.close();
Our reader is ready.
The source code can be downloaded [here] (https://cloud.mail.ru/public/2aN1/5GQW1s6Rb).
добавил функцию чтения из fb2.zip чеоез QZipReader
в.pro добавить: QT += gui-private,
в .h :
ридер через QByteArray сделал
ну у меня несколько компактней получилось
```
и с навигацией вы круто заморочилить - QTextBrowser отслеживает нажатие на линки
```
Спасибо за комментарии, а zip-архивация пригодится для открытия книг fb3 и epub. Это работает на Qt без дополнительных библиотек? Я сделал архиватор, но qzipwriter_p.h и qzipreader_p.h, а ещё zlib качал отдельно.