Дмитрий
Sept. 22, 2018, 2:16 p.m.

Reader fb2-files on Qt Creator

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:

  1. QFile f(file);
  2. if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
  3. {
  4. qDebug() << "файл не открыт";
  5. return false;
  6. }
  7. bool ok = true;
  8. QString special;
  9. QString description; // описание видео
  10. // настройки отображения
  11. int fontSize = 20;
  12. if( QSysInfo::productType() == "android" )
  13. fontSize *= 1.8;
  14.  
  15. QXmlStreamReader sr(&f);
  16. QString rId;
  17. QString rType;
  18.  
  19. QString opt;
  20.  
  21. QStringList thisToken;
  22.  
  23. while( !sr.atEnd() )
  24. {
  25. switch( sr.readNext() )
  26. {
  27. case QXmlStreamReader::NoToken:
  28. qDebug() << "QXmlStreamReader::NoToken";
  29. break;
  30. case QXmlStreamReader::StartDocument:
  31. *book = "<!DOCTYPE HTML><html><body style=\"font-size:%1px; font-family:Sans, Times New Roman;\">";
  32. *book = book->arg(fontSize);
  33. break;
  34. case QXmlStreamReader::EndDocument:
  35. book->append("</body></html>");
  36. break;
  37. case QXmlStreamReader::StartElement:
  38. thisToken.append( sr.name().toString() );
  39.  
  40. if(thisToken.contains("description")) // ОПИСАНИЕ КНИГИ
  41. {
  42. if( thisToken.back() != "image" ) //пропускаем всё кроме обложки
  43. break; // не выводим
  44. }
  45.  
  46. if(sr.name().toString() == "title")
  47. {
  48. content->append(""); // добавляем пункт содержания
  49. break;
  50. }
  51.  
  52. if( sr.name().toString() == "body" )
  53. if( !sr.attributes().isEmpty()
  54. && sr.attributes().first().value().toString() == "notes")
  55. special = "notes"; // режим примечаний
  56.  
  57. if(special == "notes")
  58. {
  59. if( sr.name().toString() == "section" )
  60. {
  61. if( sr.attributes().count() > 0 )
  62. {
  63. rId = sr.attributes().at(0).value().toString(); // ссылка на текст
  64. rType = "";
  65. }
  66. }
  67. }
  68.  
  69. opt = " align=\"justify\"";
  70. if(thisToken.contains("title") )
  71. {
  72. opt = " align=\"center\" style=\"font-size:" +QString::number(int(fontSize * 1.5)) + "px\" ";
  73. if(special == "notes")
  74. {
  75. opt += (" id=\"" + rId + "\"");
  76. }
  77. }
  78. if(thisToken.contains("subtitle") )
  79. {
  80. opt = " align=\"center\" style=\"font-size:" +QString::number(int(fontSize * 1.2)) + "px\" ";
  81. }
  82. if(thisToken.contains("annotation") )
  83. {
  84. opt = " align=\"left\" ";
  85. }
  86.  
  87. if(sr.name().toString() == "p"
  88. || sr.name().toString() == "subtitle")
  89. {
  90. book->append("<p"+opt +" >");
  91. break;
  92. }
  93.  
  94. if( sr.name().toString() == "table" )
  95. {
  96. QString text;
  97. for(int i = 0; i < sr.attributes().count(); i++)
  98. {
  99. if(sr.attributes().at(i).name() == "id")
  100. qDebug() << sr.attributes().at(i).value().toString();
  101. if(sr.attributes().at(i).name() == "style")
  102. text.append( "style=\"" +sr.attributes().at(i).value().toString()+ ";\"" );
  103. }
  104. book->append("<table border=1 align=\"center\" style=\"border:solid;\" " + text + ">");
  105. break;
  106. }
  107. if( sr.name().toString() == "tr" )
  108. {
  109. QString text;
  110. if(!thisToken.contains("table"))
  111. qDebug() << "ошибка в таблице";
  112. for(int i = 0; i < sr.attributes().count(); i++)
  113. {
  114. if(sr.attributes().at(i).name() == "aling")
  115. text.append( "aling=\"" +sr.attributes().at(i).value().toString()+ "\"" );
  116. else
  117. qDebug() << "<tr>" << sr.attributes().at(i).name() << sr.attributes().at(i).value().toString();
  118. }
  119. book->append("<tr " + text + ">");
  120. break;
  121. } //
  122. if( sr.name().toString() == "td"
  123. || sr.name().toString() == "th" )
  124. {
  125. if(!thisToken.contains("table"))
  126. qDebug() << "ошибка в таблице";
  127. QString text;
  128. for(int i = 0; i < sr.attributes().count(); i++)
  129. {
  130. if(sr.attributes().at(i).name() == "aling")
  131. text.append( "aling=\"" +sr.attributes().at(i).value().toString()+ "\" " );
  132. else if(sr.attributes().at(i).name() == "valing")
  133. text.append( "valing=\"" +sr.attributes().at(i).value().toString()+ "\" " );
  134. else if(sr.attributes().at(i).name() == "colspan")
  135. text.append( "colspan=" +sr.attributes().at(i).value().toString()+ " " );
  136. else if(sr.attributes().at(i).name() == "rowspan")
  137. text.append( "rowspan=" +sr.attributes().at(i).value().toString()+ " " );
  138. else
  139. qDebug() << "<td th>" << sr.attributes().at(i).name() << sr.attributes().at(i).value().toString();
  140. }
  141. book->append( "<"+sr.name().toString()+ " " + text +">" );
  142. break;
  143. }
  144. if( sr.name().toString() == "empty-line" )
  145. {
  146. book->append("<br/>");
  147. break;
  148. }
  149. if(sr.name().toString() == "strong"
  150. || sr.name().toString() == "sup"
  151. || sr.name().toString() == "sub"
  152. || sr.name().toString() == "code"
  153. || sr.name().toString() == "cite")
  154. {
  155. book->append( "<" + sr.name().toString() + ">");
  156. break;
  157. }
  158. if(sr.name().toString() == "emphasis")
  159. {
  160. book->append( "<i>" );
  161. break;
  162. }
  163. if( sr.name().toString() == "v" )
  164. {
  165. book->append("<p align=\"left\" style=\"margin-left:25px;\">");
  166. break;
  167. }
  168. if(sr.name().toString() == "strikethrough")
  169. {
  170. book->append( "<strike>" );
  171. break;
  172. }
  173.  
  174. if( sr.name().toString() == "a" ) // метка примечания
  175. {
  176. rId = "";
  177. for(int i = 0; i < sr.attributes().count(); i++)
  178. {
  179. if(sr.attributes().at(i).name() == "type" )
  180. {
  181. //rType = sr.attributes().at(i).value().toString();
  182. }
  183. if(sr.attributes().at(i).name() == "href")
  184. {
  185. rId = sr.attributes().at(i).value().toString();
  186. }
  187. }
  188. book->append("<a href=\"" + rId + "\"> ");
  189. //qDebug() << "a" << rId;
  190. }
  191.  
  192. if(sr.name().toString() == "poem"
  193. || sr.name().toString() == "stanza"
  194. || sr.name().toString() == "epigraph")
  195. {
  196. break;
  197. }
  198.  
  199. if(sr.name().toString() == "text-author" ) // автор текстта
  200. {
  201. book->append( "<p align=\"justify\" style=\"margin-left:45px;\">" );
  202. break;
  203. }
  204. if(sr.name().toString() == "date" ) // автор текстта
  205. {
  206. book->append( "<p align=\"justify\" style=\"margin-left:45px;\">" );
  207. break;
  208. }
  209.  
  210. if( sr.name().toString() == "image" ) // расположение рисунков
  211. {
  212. if(sr.attributes().count() > 0)
  213. book->append("<p align=\"center\">"+sr.attributes().at(0).value().toString() + "#" + "</p>");
  214. }
  215. if(sr.name() == "binary") // хранилище рисунков
  216. {
  217. if(sr.attributes().at(0).name() == "id")
  218. {
  219. rId = sr.attributes().at(0).value().toString();
  220. rType = sr.attributes().at(1).value().toString();
  221. }
  222. if(sr.attributes().at(1).name() == "id")
  223. {
  224. rId = sr.attributes().at(1).value().toString();
  225. rType = sr.attributes().at(0).value().toString();
  226. }
  227. }
  228. break;
  229. case QXmlStreamReader::EndElement:
  230. if( thisToken.last() == sr.name().toString() )
  231. {
  232. thisToken.removeLast();
  233. }
  234. else
  235. qDebug() << "error token";
  236.  
  237. if(thisToken.contains("description")) // ОПИСАНИЕ КНИГИ
  238. {
  239. break; // не выводим
  240. }
  241.  
  242. if( sr.name().toString() == "p"
  243. || sr.name().toString() == "subtitle"
  244. || sr.name().toString() == "v"
  245. || sr.name().toString() == "date"
  246. || sr.name().toString() == "text-author")
  247. {
  248. book->append("</p>");
  249. break;
  250. }
  251.  
  252. if(sr.name().toString() == "td"
  253. || sr.name().toString() == "th"
  254. || sr.name().toString() == "tr"
  255. || sr.name().toString() == "table"
  256. || sr.name().toString() == "sup"
  257. || sr.name().toString() == "sub"
  258. || sr.name().toString() == "strong"
  259. || sr.name().toString() == "code"
  260. || sr.name().toString() == "cite")
  261. {
  262. book->append( "</"+sr.name().toString()+">" );
  263. break;
  264. }
  265.  
  266. if( sr.name().toString() == "a" )
  267. {
  268. rId.remove("#");
  269. book->append( "</a><span id=\"" + rId + "___" + "\"></span>" );
  270. qDebug() << "id" << rId + "___";
  271. break;
  272. }
  273.  
  274. if(sr.name().toString() == "emphasis")
  275. {
  276. book->append( "</i>" );
  277. break;
  278. }
  279. if(sr.name().toString() == "strikethrough")
  280. {
  281. book->append( "</strike>" );
  282. break;
  283. }
  284.  
  285. if(sr.name().toString() == "stanza") // конец строфы
  286. {
  287. //book->append("<br/>");
  288. break;
  289. }
  290. if(sr.name().toString() == "epigraph"
  291. || sr.name().toString() == "poem")
  292. {
  293. break;
  294. }
  295.  
  296. if(special == "notes") // режим извлечения примечаний
  297. {
  298. if( sr.name().toString() == "body" )
  299. {
  300. special = "";
  301. }
  302. if( sr.name().toString() == "section" )
  303. {
  304. book->insert(book->lastIndexOf("<"), "<a href=\"#" + rId + "___" + "\"> назад</a>");
  305. }
  306. }
  307. break;
  308. case QXmlStreamReader::Characters:
  309. if( sr.text().toString() == "" )
  310. {
  311. //qDebug() << "isEmpty";
  312. break;
  313. }
  314. if( sr.text().toString() == "\n" )
  315. {
  316. //qDebug() << "isEmpty";
  317. break;
  318. }
  319.  
  320. if(thisToken.contains("description")) // ОПИСАНИЕ КНИГИ
  321. {
  322. description.append(sr.text().toString() + " "); // не выводим
  323. break;
  324. }
  325.  
  326. if(thisToken.contains( "binary" ) ) // для рисунков
  327. {
  328. QString image = "<img src=\"data:"
  329. + rType +";base64,"
  330. + sr.text().toString()
  331. + "\"/>";
  332. book->replace("#"+rId +"#", image);
  333. rId = "";
  334. rType = "";
  335.  
  336. break;
  337. }
  338. if(thisToken.contains("div"))
  339. {
  340. qDebug() << "div" << sr.text().toString();
  341. break;
  342. }
  343. if(thisToken.back() == "FictionBook")
  344. {
  345. qDebug() << "FictionBook" << sr.text().toString();
  346. break;
  347. }
  348.  
  349. if( thisToken.contains("title") ) // формируем содержание
  350. {
  351. content->back() += " " + sr.text().toString();//content->back()=="" ? "" : " " +
  352. // qDebug() << "title" << sr.text().toString();
  353. }
  354.  
  355. if(special == "notes" && !thisToken.contains("title") ) // добавление текста примечания
  356. {
  357. rType += " ";
  358. rType += sr.text().toString();
  359. //break;
  360. }
  361.  
  362. if(thisToken.back() == "p"
  363. || thisToken.back() == "subtitle"
  364. || thisToken.back() == "v"
  365. || thisToken.back() == "emphasis"
  366. || thisToken.back() == "strong"
  367. || thisToken.back() == "strikethrough"
  368. || thisToken.back() == "sup"
  369. || thisToken.back() == "sub"
  370. || thisToken.back() == "td"
  371. || thisToken.back() == "th"
  372. || thisToken.back() == "code"
  373. || thisToken.back() == "cite"
  374. || thisToken.back() == "text-author" // ??
  375. || thisToken.back() == "date"
  376. )
  377. {
  378. book->append( sr.text().toString() );
  379. break;
  380. }
  381.  
  382. if(thisToken.back() == "section")
  383. {
  384. break;
  385. }
  386. if(thisToken.back() == "body")
  387. {
  388. break;
  389. }
  390. if(thisToken.back() == "table"
  391. || thisToken.back() == "tr"
  392. || thisToken.back() == "title"
  393. || thisToken.back() == "poem"
  394. || thisToken.back() == "stanza")
  395. {
  396. //book->append( sr.text().toString() );
  397. break;
  398. }
  399. if(thisToken.back() == "annotation")
  400. {
  401. qDebug() << "annotation" << sr.text().toString();
  402. break;
  403. }
  404.  
  405. if(thisToken.back() == "a")
  406. {
  407. book->append( sr.text().toString() );
  408. break;
  409. }
  410. //все прочие тэги
  411. if( !sr.text().toString().isEmpty() )
  412. {
  413. qDebug() << thisToken.back() << "исключение" ;
  414. book->append("<span> " + sr.text().toString() + "</span>");
  415. }
  416. break;
  417. }
  418. }
  419. f.close();

The file open function is launched by pressing the button:

  1. ui->textBrowser->clear();
  2. QString text;
  3. QStringList content;
  4.  
  5. if(name.endsWith(".fb2"))
  6. {
  7. openerTextFiles otf;
  8. otf.openFB2File(name, &text, &content);
  9. ui->textBrowser->setText(text);
  10. ui->textBrowser->verticalScrollBar()->setValue(0);
  11. }

Now the generated html page can be easily saved

  1. QFile file(name);
  2. if ( !file.open(QIODevice::WriteOnly | QIODevice::Text) )
  3. return;
  4.  
  5. QTextStream out(&file);
  6. out << ui->textBrowser->toHtml();
  7. file.close();

Our reader is ready.

The source code can be downloaded [here] (https://cloud.mail.ru/public/2aN1/5GQW1s6Rb).

Recommended articles on this topic

By article asked0question(s)

4
ВТ
  • Feb. 6, 2019, 9:58 p.m.
  • (edited)

добавил функцию чтения из fb2.zip чеоез QZipReader

  1. if (name.endsWith(".zip")) {
  2. QZipReader unzip(name);
  3. QByteArray ba;
  4. QVector<QZipReader::FileInfo> files = unzip.fileInfoList();
  5. QZipReader::FileInfo fi = files.at(0);
  6. if (fi.isFile) {
  7. ba = unzip.fileData(fi.filePath);
  8. if (fi.size != ba.size()) qDebug() << "unzip error";
  9. }
  10. unzip.close();
  11. openerTextFiles otf;
  12. otf.openFB2File(ba, &text, &content);
  13. ui->textBrowser->document()->clear();
  14. ui->textBrowser->setHtml(text);
  15. ui->textBrowser->verticalScrollBar()->setValue(0);
  16. }

в.pro добавить: QT += gui-private,
в .h :

  1. #include <QtGui/private/qzipwriter_p.h>
  2. #include <QtGui/private/qzipreader_p.h>

ридер через QByteArray сделал

  1. bool openFB2File(QByteArray ba, QString *book, QStringList *content);
ВТ
  • Feb. 9, 2019, 2:24 p.m.

ну у меня несколько компактней получилось

  1.  
  2. tags = {'poem':'i','emphasis':'i', 'stanza':'i','title':'p','title':'p','empty-line':'br',
  3. 'subtitle':'p','section' : 'p','strong':'b','v':'p','coverpage':'br',
  4. }
  5.  
  6. attrs = {
  7. 'title':{'align':'center','style':'font-size:24px;'},
  8. 'subtitle':{'align':'center','style':'font-size:22px;'},
  9. 'table':{'width':'100%','align':'center','border':'1'},
  10. 'td':{'align':'center'},
  11. 'p':{'align':'justify','style':'font-size:20px;'},
  12. }
  13.  
  14. class Fb2Reader(QObject):
  15. def __init__(self,fbdata = None):
  16. super().__init__()
  17. self.fbdata = fbdata
  18.  
  19. def read(self):
  20. sr = QXmlStreamReader(self.fbdata)
  21. ba = QByteArray()
  22. wr = QXmlStreamWriter(ba)
  23. wr.setAutoFormatting(True)
  24. wr.setAutoFormattingIndent(1)
  25. tokens = []
  26. name = ''
  27. d = {}
  28. images = []
  29. while not sr.atEnd():
  30. elem = sr.readNext()
  31. if sr.hasError() :
  32. err = sr.errorString()+':'+str(sr.lineNumber())
  33. return err
  34. if elem == QXmlStreamReader.StartDocument:
  35. wr.writeStartDocument()
  36. elif elem == QXmlStreamReader.EndDocument:
  37. wr.writeEndDocument()
  38. elif elem == QXmlStreamReader.StartElement:
  39. name = sr.name()
  40. tokens.append(name)
  41. if 'description' in tokens and name != 'image':
  42. continue
  43. d = {i.name():i.value() for i in sr.attributes()}
  44. if name == 'image':
  45. link = d.get('href')
  46. wr.writeStartElement('p')
  47. wr.writeAttribute('align','center')
  48. wr.writeCharacters(link)
  49. wr.writeEndElement()
  50. continue
  51. tag = tags.get(name,name)
  52. wr.writeStartElement('',tag)
  53. attr = attrs.get(name)
  54. if attr:
  55. for i in attr:
  56. wr.writeAttribute('',i,attr[i])
  57. else:
  58. for i in d:
  59. wr.writeAttribute('',i,d[i])
  60. elif elem == QXmlStreamReader.EndElement:
  61. tokens.pop()
  62. wr.writeEndElement()
  63. elif elem == QXmlStreamReader.Characters:
  64. if 'description' in tokens: continue
  65. text = sr.text()
  66. if name == 'binary':
  67. link,typ = d.get('id'),d.get('content-type')
  68. content = '<img src=\"data:' + typ +';base64,' + text +'\"/>'
  69. images.append(('#'+link,content,))
  70. else: wr.writeCharacters(text)
  71. s = bytes(ba).decode()
  72. for i in images:
  73. s = s.replace(i[0],i[1])
  74. del images
  75. return s
  76.  

```

ВТ
  • Feb. 9, 2019, 2:27 p.m.

и с навигацией вы круто заморочилить - QTextBrowser отслеживает нажатие на линки

  1. self.edit.anchorClicked.connect(self.on_anchor)
  2. self.btnBack.setEnabled(False)
  3. self.btnBack.clicked.connect(self.back_ward)
  4.  
  5. def on_anchor(self,args):
  6. self.cursor_pos = self.edit.textCursor().position()
  7. self.btnBack.setEnabled(True)
  8.  
  9. def back_ward(self):
  10. self.btnBack.setEnabled(False)
  11. self.edit.moveCursor(self.cursor_pos)
  12. self.edit.viewport().repaint()

```

Дмитрий
  • Feb. 26, 2019, 9:37 p.m.

Спасибо за комментарии, а zip-архивация пригодится для открытия книг fb3 и epub. Это работает на Qt без дополнительных библиотек? Я сделал архиватор, но qzipwriter_p.h и qzipreader_p.h, а ещё zlib качал отдельно.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
  • Last comments
  • Evgenii Legotckoi
    March 9, 2025, 9:02 p.m.
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    March 9, 2025, 4:14 p.m.
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…
  • ИМ
    Nov. 22, 2024, 9:51 p.m.
    Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
  • Evgenii Legotckoi
    Oct. 31, 2024, 11:37 p.m.
    Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
  • A
    Oct. 19, 2024, 5:19 p.m.
    Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html