Дмитрий
ДмитрийSept. 22, 2018, 4:16 a.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:

    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).

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

ВТ
  • Feb. 6, 2019, 10:58 a.m.
  • (edited)

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

    if (name.endsWith(".zip")) {
        QZipReader unzip(name);
        QByteArray ba;
            QVector<QZipReader::FileInfo> files = unzip.fileInfoList();
            QZipReader::FileInfo fi = files.at(0);
                if (fi.isFile) {
                    ba = unzip.fileData(fi.filePath);
                    if (fi.size != ba.size()) qDebug() << "unzip error";
            }
            unzip.close();
            openerTextFiles otf;
            otf.openFB2File(ba, &text, &content);
            ui->textBrowser->document()->clear();
            ui->textBrowser->setHtml(text);
            ui->textBrowser->verticalScrollBar()->setValue(0);
 }

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

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

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

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

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


tags = {'poem':'i','emphasis':'i', 'stanza':'i','title':'p','title':'p','empty-line':'br',
'subtitle':'p','section' : 'p','strong':'b','v':'p','coverpage':'br',
}

attrs = {
'title':{'align':'center','style':'font-size:24px;'},
'subtitle':{'align':'center','style':'font-size:22px;'},
'table':{'width':'100%','align':'center','border':'1'},
'td':{'align':'center'},
'p':{'align':'justify','style':'font-size:20px;'},
}

class Fb2Reader(QObject):
    def __init__(self,fbdata = None):
        super().__init__()
        self.fbdata = fbdata

    def read(self):
        sr = QXmlStreamReader(self.fbdata)
        ba = QByteArray()
        wr = QXmlStreamWriter(ba)
        wr.setAutoFormatting(True)
        wr.setAutoFormattingIndent(1)
        tokens = []
        name = ''
        d = {}
        images = []
        while not sr.atEnd():
            elem = sr.readNext()
            if sr.hasError() : 
                err = sr.errorString()+':'+str(sr.lineNumber())
                return err
            if elem == QXmlStreamReader.StartDocument:
                wr.writeStartDocument()
            elif elem == QXmlStreamReader.EndDocument:
                wr.writeEndDocument()
            elif elem == QXmlStreamReader.StartElement:
                name = sr.name()
                tokens.append(name)
                if 'description' in tokens and name != 'image':
                    continue
                d = {i.name():i.value() for i in sr.attributes()}
                if name == 'image':
                    link = d.get('href')
                    wr.writeStartElement('p')
                    wr.writeAttribute('align','center')
                    wr.writeCharacters(link)
                    wr.writeEndElement()
                    continue
                tag = tags.get(name,name)
                wr.writeStartElement('',tag)
                attr = attrs.get(name)
                if  attr:
                    for i in attr: 
                        wr.writeAttribute('',i,attr[i])
                else:
                    for i in d: 
                        wr.writeAttribute('',i,d[i])            
            elif elem == QXmlStreamReader.EndElement:
                tokens.pop()
                wr.writeEndElement()
            elif elem == QXmlStreamReader.Characters:
                if 'description' in tokens: continue
                text = sr.text()
                if name == 'binary':
                    link,typ = d.get('id'),d.get('content-type')
                    content = '<img src=\"data:' + typ +';base64,' + text +'\"/>'
                    images.append(('#'+link,content,)) 
                else: wr.writeCharacters(text)
        s = bytes(ba).decode()
        for i in images: 
            s = s.replace(i[0],i[1])
        del images
        return s

```

ВТ
  • Feb. 9, 2019, 3:27 a.m.

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

        self.edit.anchorClicked.connect(self.on_anchor)
        self.btnBack.setEnabled(False)
        self.btnBack.clicked.connect(self.back_ward)

    def on_anchor(self,args):
        self.cursor_pos = self.edit.textCursor().position()
        self.btnBack.setEnabled(True)

    def back_ward(self):
        self.btnBack.setEnabled(False)
        self.edit.moveCursor(self.cursor_pos)
        self.edit.viewport().repaint()

```

Дмитрий
  • Feb. 26, 2019, 10:37 a.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
AD

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:20points,
  • Rating points-10
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 11:51 a.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiOct. 31, 2024, 2:37 p.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 8:19 a.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 7:51 a.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 11:02 a.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
Evgenii Legotckoi
Evgenii LegotckoiJune 24, 2024, 3:11 p.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 6:04 a.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 3:49 a.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimOct. 25, 2024, 9:10 a.m.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Follow us in social networks