File indexing completed on 2024-05-12 16:06:34

0001 /*
0002     SPDX-FileCopyrightText: 2006 Pino Toscano <toscano.pino@tiscali.it>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kdjvu.h"
0008 
0009 #include <QByteArray>
0010 #include <QDomDocument>
0011 #include <QFile>
0012 #include <QHash>
0013 #include <QPainter>
0014 #include <QQueue>
0015 #include <QString>
0016 
0017 #include <KLocalizedString>
0018 #include <QDebug>
0019 
0020 #include <libdjvu/ddjvuapi.h>
0021 #include <libdjvu/miniexp.h>
0022 
0023 #include <stdio.h>
0024 
0025 QDebug &operator<<(QDebug &s, const ddjvu_rect_t r)
0026 {
0027     s.nospace() << "[" << r.x << "," << r.y << " - " << r.w << "x" << r.h << "]";
0028     return s.space();
0029 }
0030 
0031 static void which_ddjvu_message(const ddjvu_message_t *msg)
0032 {
0033 #ifdef KDJVU_DEBUG
0034     qDebug() << "which_djvu_message(...):" << msg->m_any.tag;
0035     switch (msg->m_any.tag) {
0036     case DDJVU_ERROR:
0037         qDebug().nospace() << "ERROR: file " << msg->m_error.filename << ", line " << msg->m_error.lineno;
0038         qDebug().nospace() << "ERROR: function '" << msg->m_error.function << "'";
0039         qDebug().nospace() << "ERROR: '" << msg->m_error.message << "'";
0040         break;
0041     case DDJVU_INFO:
0042         qDebug().nospace() << "INFO: '" << msg->m_info.message << "'";
0043         break;
0044     case DDJVU_CHUNK:
0045         qDebug().nospace() << "CHUNK: '" << QByteArray(msg->m_chunk.chunkid) << "'";
0046         break;
0047     case DDJVU_PROGRESS:
0048         qDebug().nospace() << "PROGRESS: '" << msg->m_progress.percent << "'";
0049         break;
0050     default:;
0051     }
0052 #else
0053     Q_UNUSED(msg);
0054 #endif
0055 }
0056 
0057 /**
0058  * Explore the message queue until there are message left in it.
0059  */
0060 static void handle_ddjvu_messages(ddjvu_context_t *ctx, int wait)
0061 {
0062     const ddjvu_message_t *msg;
0063     if (wait) {
0064         ddjvu_message_wait(ctx);
0065     }
0066     while ((msg = ddjvu_message_peek(ctx))) {
0067         which_ddjvu_message(msg);
0068         ddjvu_message_pop(ctx);
0069     }
0070 }
0071 
0072 /**
0073  * Explore the message queue until the message \p mid is found.
0074  */
0075 static void wait_for_ddjvu_message(ddjvu_context_t *ctx, ddjvu_message_tag_t mid)
0076 {
0077     ddjvu_message_wait(ctx);
0078     const ddjvu_message_t *msg;
0079     while ((msg = ddjvu_message_peek(ctx)) && msg && (msg->m_any.tag != mid)) {
0080         which_ddjvu_message(msg);
0081         ddjvu_message_pop(ctx);
0082     }
0083 }
0084 
0085 /**
0086  * Convert a clockwise coefficient \p r for a rotation to a counter-clockwise
0087  * and vice versa.
0088  */
0089 static int flipRotation(int r)
0090 {
0091     return (4 - r) % 4;
0092 }
0093 
0094 static miniexp_t find_second_in_pair(miniexp_t theexp, const char *which)
0095 {
0096     miniexp_t exp = theexp;
0097     while (exp) {
0098         miniexp_t cur = miniexp_car(exp);
0099         if (!miniexp_consp(cur) || !miniexp_symbolp(miniexp_car(cur))) {
0100             exp = miniexp_cdr(exp);
0101             continue;
0102         }
0103 
0104         const QString id = QString::fromUtf8(miniexp_to_name(miniexp_car(cur)));
0105         if (id == QLatin1String(which)) {
0106             return miniexp_cadr(cur);
0107         }
0108         exp = miniexp_cdr(exp);
0109     }
0110     return miniexp_nil;
0111 }
0112 
0113 static bool find_replace_or_add_second_in_pair(miniexp_t theexp, const char *which, miniexp_t replacement)
0114 {
0115     miniexp_t exp = miniexp_cdddr(theexp);
0116     while (exp) {
0117         miniexp_t cur = miniexp_car(exp);
0118         if (!miniexp_consp(cur) || !miniexp_symbolp(miniexp_car(cur))) {
0119             exp = miniexp_cdr(exp);
0120             continue;
0121         }
0122 
0123         const QString id = QString::fromUtf8(miniexp_to_name(miniexp_car(cur)));
0124         if (id == QLatin1String(which)) {
0125             miniexp_t reversed = miniexp_reverse(cur);
0126             miniexp_rplaca(reversed, replacement);
0127             cur = miniexp_reverse(reversed);
0128             return true;
0129         }
0130         exp = miniexp_cdr(exp);
0131     }
0132     // TODO add the new replacement ad the end of the list
0133     return false;
0134 }
0135 
0136 // ImageCacheItem
0137 
0138 class ImageCacheItem
0139 {
0140 public:
0141     ImageCacheItem(int p, int w, int h, const QImage &i)
0142         : page(p)
0143         , width(w)
0144         , height(h)
0145         , img(i)
0146     {
0147     }
0148 
0149     int page;
0150     int width;
0151     int height;
0152     QImage img;
0153 };
0154 
0155 // KdjVu::Page
0156 
0157 KDjVu::Page::Page()
0158 {
0159 }
0160 
0161 KDjVu::Page::~Page()
0162 {
0163 }
0164 
0165 int KDjVu::Page::width() const
0166 {
0167     return m_width;
0168 }
0169 
0170 int KDjVu::Page::height() const
0171 {
0172     return m_height;
0173 }
0174 
0175 int KDjVu::Page::dpi() const
0176 {
0177     return m_dpi;
0178 }
0179 
0180 int KDjVu::Page::orientation() const
0181 {
0182     return m_orientation;
0183 }
0184 
0185 // KDjVu::Link
0186 
0187 KDjVu::Link::~Link()
0188 {
0189 }
0190 
0191 KDjVu::Link::LinkArea KDjVu::Link::areaType() const
0192 {
0193     return m_area;
0194 }
0195 
0196 QPoint KDjVu::Link::point() const
0197 {
0198     return m_point;
0199 }
0200 
0201 QSize KDjVu::Link::size() const
0202 {
0203     return m_size;
0204 }
0205 
0206 QPolygon KDjVu::Link::polygon() const
0207 {
0208     return m_poly;
0209 }
0210 
0211 // KDjVu::PageLink
0212 
0213 KDjVu::PageLink::PageLink()
0214 {
0215 }
0216 
0217 int KDjVu::PageLink::type() const
0218 {
0219     return KDjVu::Link::PageLink;
0220 }
0221 
0222 QString KDjVu::PageLink::page() const
0223 {
0224     return m_page;
0225 }
0226 
0227 // KDjVu::UrlLink
0228 
0229 KDjVu::UrlLink::UrlLink()
0230 {
0231 }
0232 
0233 int KDjVu::UrlLink::type() const
0234 {
0235     return KDjVu::Link::UrlLink;
0236 }
0237 
0238 QString KDjVu::UrlLink::url() const
0239 {
0240     return m_url;
0241 }
0242 
0243 // KDjVu::Annotation
0244 
0245 KDjVu::Annotation::Annotation(miniexp_t anno)
0246     : m_anno(anno)
0247 {
0248 }
0249 
0250 KDjVu::Annotation::~Annotation()
0251 {
0252 }
0253 
0254 QPoint KDjVu::Annotation::point() const
0255 {
0256     miniexp_t area = miniexp_nth(3, m_anno);
0257     int a = miniexp_to_int(miniexp_nth(1, area));
0258     int b = miniexp_to_int(miniexp_nth(2, area));
0259     return QPoint(a, b);
0260 }
0261 
0262 QString KDjVu::Annotation::comment() const
0263 {
0264     return QString::fromUtf8(miniexp_to_str(miniexp_nth(2, m_anno)));
0265 }
0266 
0267 void KDjVu::Annotation::setComment(const QString &comment)
0268 {
0269     miniexp_t exp = m_anno;
0270     exp = miniexp_cdr(exp);
0271     exp = miniexp_cdr(exp);
0272     miniexp_rplaca(exp, miniexp_string(comment.toUtf8().constData()));
0273 }
0274 
0275 QColor KDjVu::Annotation::color() const
0276 {
0277     return QColor();
0278 }
0279 
0280 void KDjVu::Annotation::setColor(const QColor &)
0281 {
0282 }
0283 
0284 // KDjVu::TextAnnotation
0285 
0286 KDjVu::TextAnnotation::TextAnnotation(miniexp_t anno)
0287     : Annotation(anno)
0288     , m_inlineText(true)
0289 {
0290     const int num = miniexp_length(m_anno);
0291     for (int j = 4; j < num; ++j) {
0292         miniexp_t curelem = miniexp_nth(j, m_anno);
0293         if (!miniexp_listp(curelem)) {
0294             continue;
0295         }
0296 
0297         QString id = QString::fromUtf8(miniexp_to_name(miniexp_nth(0, curelem)));
0298         if (id == QLatin1String("pushpin")) {
0299             m_inlineText = false;
0300         }
0301     }
0302 }
0303 
0304 QSize KDjVu::TextAnnotation::size() const
0305 {
0306     miniexp_t area = miniexp_nth(3, m_anno);
0307     int c = miniexp_to_int(miniexp_nth(3, area));
0308     int d = miniexp_to_int(miniexp_nth(4, area));
0309     return QSize(c, d);
0310 }
0311 
0312 int KDjVu::TextAnnotation::type() const
0313 {
0314     return KDjVu::Annotation::TextAnnotation;
0315 }
0316 
0317 QColor KDjVu::TextAnnotation::color() const
0318 {
0319     miniexp_t col = find_second_in_pair(m_anno, "backclr");
0320     if (!miniexp_symbolp(col)) {
0321         return Qt::transparent;
0322     }
0323 
0324     return QColor(QString::fromUtf8(miniexp_to_name(col)));
0325 }
0326 
0327 void KDjVu::TextAnnotation::setColor(const QColor &color)
0328 {
0329     const QByteArray col = color.name().toLatin1();
0330     find_replace_or_add_second_in_pair(m_anno, "backclr", miniexp_symbol(col.constData()));
0331 }
0332 
0333 bool KDjVu::TextAnnotation::inlineText() const
0334 {
0335     return m_inlineText;
0336 }
0337 
0338 // KDjVu::LineAnnotation
0339 
0340 KDjVu::LineAnnotation::LineAnnotation(miniexp_t anno)
0341     : Annotation(anno)
0342     , m_isArrow(false)
0343     , m_width(miniexp_nil)
0344 {
0345     const int num = miniexp_length(m_anno);
0346     for (int j = 4; j < num; ++j) {
0347         miniexp_t curelem = miniexp_nth(j, m_anno);
0348         if (!miniexp_listp(curelem)) {
0349             continue;
0350         }
0351 
0352         QString id = QString::fromUtf8(miniexp_to_name(miniexp_nth(0, curelem)));
0353         if (id == QLatin1String("arrow")) {
0354             m_isArrow = true;
0355         } else if (id == QLatin1String("width")) {
0356             m_width = curelem;
0357         }
0358     }
0359 }
0360 
0361 int KDjVu::LineAnnotation::type() const
0362 {
0363     return KDjVu::Annotation::LineAnnotation;
0364 }
0365 
0366 QColor KDjVu::LineAnnotation::color() const
0367 {
0368     miniexp_t col = find_second_in_pair(m_anno, "lineclr");
0369     if (!miniexp_symbolp(col)) {
0370         return Qt::black;
0371     }
0372 
0373     return QColor(QString::fromUtf8(miniexp_to_name(col)));
0374 }
0375 
0376 void KDjVu::LineAnnotation::setColor(const QColor &color)
0377 {
0378     const QByteArray col = color.name().toLatin1();
0379     find_replace_or_add_second_in_pair(m_anno, "lineclr", miniexp_symbol(col.constData()));
0380 }
0381 
0382 QPoint KDjVu::LineAnnotation::point2() const
0383 {
0384     miniexp_t area = miniexp_nth(3, m_anno);
0385     int c = miniexp_to_int(miniexp_nth(3, area));
0386     int d = miniexp_to_int(miniexp_nth(4, area));
0387     return QPoint(c, d);
0388 }
0389 
0390 bool KDjVu::LineAnnotation::isArrow() const
0391 {
0392     return m_isArrow;
0393 }
0394 
0395 int KDjVu::LineAnnotation::width() const
0396 {
0397     if (m_width == miniexp_nil) {
0398         return 1;
0399     }
0400 
0401     return miniexp_to_int(miniexp_cadr(m_width));
0402 }
0403 
0404 void KDjVu::LineAnnotation::setWidth(int width)
0405 {
0406     find_replace_or_add_second_in_pair(m_anno, "width", miniexp_number(width));
0407 }
0408 
0409 // KDjVu::TextEntity
0410 
0411 KDjVu::TextEntity::TextEntity()
0412 {
0413 }
0414 
0415 KDjVu::TextEntity::~TextEntity()
0416 {
0417 }
0418 
0419 QString KDjVu::TextEntity::text() const
0420 {
0421     return m_text;
0422 }
0423 
0424 QRect KDjVu::TextEntity::rect() const
0425 {
0426     return m_rect;
0427 }
0428 
0429 class KDjVu::Private
0430 {
0431 public:
0432     Private()
0433         : m_djvu_cxt(nullptr)
0434         , m_djvu_document(nullptr)
0435         , m_format(nullptr)
0436         , m_docBookmarks(nullptr)
0437         , m_cacheEnabled(true)
0438     {
0439     }
0440 
0441     QImage generateImageTile(ddjvu_page_t *djvupage, int &res, int width, int row, int xdelta, int height, int col, int ydelta);
0442 
0443     void readBookmarks();
0444     void fillBookmarksRecurse(QDomDocument &maindoc, QDomNode &curnode, miniexp_t exp, int offset = -1);
0445 
0446     void readMetaData(int page);
0447 
0448     int pageWithName(const QString &name);
0449 
0450     ddjvu_context_t *m_djvu_cxt;
0451     ddjvu_document_t *m_djvu_document;
0452     ddjvu_format_t *m_format;
0453 
0454     QVector<KDjVu::Page *> m_pages;
0455     QVector<ddjvu_page_t *> m_pages_cache;
0456 
0457     QList<ImageCacheItem *> mImgCache;
0458 
0459     QHash<QString, QVariant> m_metaData;
0460     QDomDocument *m_docBookmarks;
0461 
0462     QHash<QString, int> m_pageNamesCache;
0463 
0464     bool m_cacheEnabled;
0465 
0466     static unsigned int s_formatmask[4];
0467 };
0468 
0469 unsigned int KDjVu::Private::s_formatmask[4] = {0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000};
0470 
0471 QImage KDjVu::Private::generateImageTile(ddjvu_page_t *djvupage, int &res, int width, int row, int xdelta, int height, int col, int ydelta)
0472 {
0473     ddjvu_rect_t renderrect;
0474     renderrect.x = row * xdelta;
0475     renderrect.y = col * ydelta;
0476     int realwidth = qMin(width - renderrect.x, xdelta);
0477     int realheight = qMin(height - renderrect.y, ydelta);
0478     renderrect.w = realwidth;
0479     renderrect.h = realheight;
0480 #ifdef KDJVU_DEBUG
0481     qDebug() << "renderrect:" << renderrect;
0482 #endif
0483     ddjvu_rect_t pagerect;
0484     pagerect.x = 0;
0485     pagerect.y = 0;
0486     pagerect.w = width;
0487     pagerect.h = height;
0488 #ifdef KDJVU_DEBUG
0489     qDebug() << "pagerect:" << pagerect;
0490 #endif
0491     handle_ddjvu_messages(m_djvu_cxt, false);
0492     QImage res_img(realwidth, realheight, QImage::Format_RGB32);
0493     // the following line workarounds a rare crash in djvulibre;
0494     // it should be fixed with >= 3.5.21
0495     ddjvu_page_get_width(djvupage);
0496     res = ddjvu_page_render(djvupage, DDJVU_RENDER_COLOR, &pagerect, &renderrect, m_format, res_img.bytesPerLine(), (char *)res_img.bits());
0497     if (!res) {
0498         res_img.fill(Qt::white);
0499     }
0500 #ifdef KDJVU_DEBUG
0501     qDebug() << "rendering result:" << res;
0502 #endif
0503     handle_ddjvu_messages(m_djvu_cxt, false);
0504 
0505     return res_img;
0506 }
0507 
0508 void KDjVu::Private::readBookmarks()
0509 {
0510     if (!m_djvu_document) {
0511         return;
0512     }
0513 
0514     miniexp_t outline;
0515     while ((outline = ddjvu_document_get_outline(m_djvu_document)) == miniexp_dummy) {
0516         handle_ddjvu_messages(m_djvu_cxt, true);
0517     }
0518 
0519     if (miniexp_listp(outline) && (miniexp_length(outline) > 0) && miniexp_symbolp(miniexp_nth(0, outline)) && (QString::fromUtf8(miniexp_to_name(miniexp_nth(0, outline))) == QLatin1String("bookmarks"))) {
0520         m_docBookmarks = new QDomDocument(QStringLiteral("KDjVuBookmarks"));
0521         fillBookmarksRecurse(*m_docBookmarks, *m_docBookmarks, outline, 1);
0522         ddjvu_miniexp_release(m_djvu_document, outline);
0523     }
0524 }
0525 
0526 void KDjVu::Private::fillBookmarksRecurse(QDomDocument &maindoc, QDomNode &curnode, miniexp_t exp, int offset)
0527 {
0528     if (!miniexp_listp(exp)) {
0529         return;
0530     }
0531 
0532     int l = miniexp_length(exp);
0533     for (int i = qMax(offset, 0); i < l; ++i) {
0534         miniexp_t cur = miniexp_nth(i, exp);
0535 
0536         if (miniexp_consp(cur) && (miniexp_length(cur) > 0) && miniexp_stringp(miniexp_nth(0, cur)) && miniexp_stringp(miniexp_nth(1, cur))) {
0537             QString title = QString::fromUtf8(miniexp_to_str(miniexp_nth(0, cur)));
0538             QString dest = QString::fromUtf8(miniexp_to_str(miniexp_nth(1, cur)));
0539             QDomElement el = maindoc.createElement(QStringLiteral("item"));
0540             el.setAttribute(QStringLiteral("title"), title);
0541             if (!dest.isEmpty()) {
0542                 if (dest.at(0) == QLatin1Char('#')) {
0543                     dest.remove(0, 1);
0544                     bool isNumber = false;
0545                     dest.toInt(&isNumber);
0546                     if (isNumber) {
0547                         // it might be an actual page number, but could also be a page label
0548                         // so resolve the number, and get the real page number
0549                         int pageNo = pageWithName(dest);
0550                         if (pageNo != -1) {
0551                             el.setAttribute(QStringLiteral("PageNumber"), QString::number(pageNo + 1));
0552                         } else {
0553                             el.setAttribute(QStringLiteral("PageNumber"), dest);
0554                         }
0555                     } else {
0556                         el.setAttribute(QStringLiteral("PageName"), dest);
0557                     }
0558                 } else {
0559                     el.setAttribute(QStringLiteral("URL"), dest);
0560                 }
0561             }
0562             curnode.appendChild(el);
0563             if (!el.isNull() && (miniexp_length(cur) > 2)) {
0564                 fillBookmarksRecurse(maindoc, el, cur, 2);
0565             }
0566         }
0567     }
0568 }
0569 
0570 void KDjVu::Private::readMetaData(int page)
0571 {
0572     if (!m_djvu_document) {
0573         return;
0574     }
0575 
0576     miniexp_t annots;
0577     while ((annots = ddjvu_document_get_pageanno(m_djvu_document, page)) == miniexp_dummy) {
0578         handle_ddjvu_messages(m_djvu_cxt, true);
0579     }
0580 
0581     if (!miniexp_listp(annots) || miniexp_length(annots) == 0) {
0582         return;
0583     }
0584 
0585     miniexp_t exp = miniexp_nth(0, annots);
0586     int size = miniexp_length(exp);
0587     if (size <= 1 || qstrncmp(miniexp_to_name(miniexp_nth(0, exp)), "metadata", 8)) {
0588         return;
0589     }
0590 
0591     for (int i = 1; i < size; ++i) {
0592         miniexp_t cur = miniexp_nth(i, exp);
0593         if (miniexp_length(cur) != 2) {
0594             continue;
0595         }
0596 
0597         QString id = QString::fromUtf8(miniexp_to_name(miniexp_nth(0, cur)));
0598         QString value = QString::fromUtf8(miniexp_to_str(miniexp_nth(1, cur)));
0599         m_metaData[id.toLower()] = value;
0600     }
0601 }
0602 
0603 int KDjVu::Private::pageWithName(const QString &name)
0604 {
0605     const int pageNo = m_pageNamesCache.value(name, -1);
0606     if (pageNo != -1) {
0607         return pageNo;
0608     }
0609 
0610     const QByteArray utfName = name.toUtf8();
0611     const int fileNum = ddjvu_document_get_filenum(m_djvu_document);
0612     ddjvu_fileinfo_t info;
0613     for (int i = 0; i < fileNum; ++i) {
0614         if (DDJVU_JOB_OK != ddjvu_document_get_fileinfo(m_djvu_document, i, &info)) {
0615             continue;
0616         }
0617         if (info.type != 'P') {
0618             continue;
0619         }
0620         if ((utfName == info.id) || (utfName == info.name) || (utfName == info.title)) {
0621             m_pageNamesCache.insert(name, info.pageno);
0622             return info.pageno;
0623         }
0624     }
0625     return -1;
0626 }
0627 
0628 KDjVu::KDjVu()
0629     : d(new Private)
0630 {
0631     // creating the djvu context
0632     d->m_djvu_cxt = ddjvu_context_create("KDjVu");
0633     // creating the rendering format
0634 #if DDJVUAPI_VERSION >= 18
0635     d->m_format = ddjvu_format_create(DDJVU_FORMAT_RGBMASK32, 4, Private::s_formatmask);
0636 #else
0637     d->m_format = ddjvu_format_create(DDJVU_FORMAT_RGBMASK32, 3, Private::s_formatmask);
0638 #endif
0639     ddjvu_format_set_row_order(d->m_format, 1);
0640     ddjvu_format_set_y_direction(d->m_format, 1);
0641 }
0642 
0643 KDjVu::~KDjVu()
0644 {
0645     closeFile();
0646 
0647     ddjvu_format_release(d->m_format);
0648     ddjvu_context_release(d->m_djvu_cxt);
0649 
0650     delete d;
0651 }
0652 
0653 bool KDjVu::openFile(const QString &fileName)
0654 {
0655     // first, close the old file
0656     if (d->m_djvu_document) {
0657         closeFile();
0658     }
0659 
0660     // load the document..., use UTF-8 variant to work on Windows, too, see bug 422500
0661     d->m_djvu_document = ddjvu_document_create_by_filename_utf8(d->m_djvu_cxt, fileName.toUtf8().constData(), true);
0662     if (!d->m_djvu_document) {
0663         return false;
0664     }
0665     // ...and wait for its loading
0666     wait_for_ddjvu_message(d->m_djvu_cxt, DDJVU_DOCINFO);
0667     if (ddjvu_document_decoding_error(d->m_djvu_document)) {
0668         ddjvu_document_release(d->m_djvu_document);
0669         d->m_djvu_document = nullptr;
0670         return false;
0671     }
0672 
0673     qDebug() << "# of pages:" << ddjvu_document_get_pagenum(d->m_djvu_document);
0674     int numofpages = ddjvu_document_get_pagenum(d->m_djvu_document);
0675     d->m_pages.clear();
0676     d->m_pages.resize(numofpages);
0677     d->m_pages_cache.clear();
0678     d->m_pages_cache.resize(numofpages);
0679 
0680     // get the document type
0681     QString doctype;
0682     switch (ddjvu_document_get_type(d->m_djvu_document)) {
0683     case DDJVU_DOCTYPE_UNKNOWN:
0684         doctype = i18nc("Type of DjVu document", "Unknown");
0685         break;
0686     case DDJVU_DOCTYPE_SINGLEPAGE:
0687         doctype = i18nc("Type of DjVu document", "Single Page");
0688         break;
0689     case DDJVU_DOCTYPE_BUNDLED:
0690         doctype = i18nc("Type of DjVu document", "Bundled");
0691         break;
0692     case DDJVU_DOCTYPE_INDIRECT:
0693         doctype = i18nc("Type of DjVu document", "Indirect");
0694         break;
0695     case DDJVU_DOCTYPE_OLD_BUNDLED:
0696         doctype = i18nc("Type of DjVu document", "Bundled (old)");
0697         break;
0698     case DDJVU_DOCTYPE_OLD_INDEXED:
0699         doctype = i18nc("Type of DjVu document", "Indexed (old)");
0700         break;
0701     }
0702     if (!doctype.isEmpty()) {
0703         d->m_metaData[QStringLiteral("documentType")] = doctype;
0704     }
0705     // get the number of components
0706     d->m_metaData[QStringLiteral("componentFile")] = ddjvu_document_get_filenum(d->m_djvu_document);
0707 
0708     // read the pages
0709     for (int i = 0; i < numofpages; ++i) {
0710         ddjvu_status_t sts;
0711         ddjvu_pageinfo_t info;
0712         while ((sts = ddjvu_document_get_pageinfo(d->m_djvu_document, i, &info)) < DDJVU_JOB_OK) {
0713             handle_ddjvu_messages(d->m_djvu_cxt, true);
0714         }
0715         if (sts >= DDJVU_JOB_FAILED) {
0716             qDebug().nospace() << "\t>>> page " << i << " failed: " << sts;
0717             return false;
0718         }
0719 
0720         KDjVu::Page *p = new KDjVu::Page();
0721         p->m_width = info.width;
0722         p->m_height = info.height;
0723         p->m_dpi = info.dpi;
0724 #if DDJVUAPI_VERSION >= 18
0725         p->m_orientation = flipRotation(info.rotation);
0726 #else
0727         p->m_orientation = 0;
0728 #endif
0729         d->m_pages[i] = p;
0730     }
0731 
0732     // reading the metadata from the first page only should be enough
0733     if (numofpages > 0) {
0734         d->readMetaData(0);
0735     }
0736 
0737     return true;
0738 }
0739 
0740 void KDjVu::closeFile()
0741 {
0742     // deleting the old TOC
0743     delete d->m_docBookmarks;
0744     d->m_docBookmarks = nullptr;
0745     // deleting the pages
0746     qDeleteAll(d->m_pages);
0747     d->m_pages.clear();
0748     // releasing the djvu pages
0749     QVector<ddjvu_page_t *>::Iterator it = d->m_pages_cache.begin(), itEnd = d->m_pages_cache.end();
0750     for (; it != itEnd; ++it) {
0751         ddjvu_page_release(*it);
0752     }
0753     d->m_pages_cache.clear();
0754     // clearing the image cache
0755     qDeleteAll(d->mImgCache);
0756     d->mImgCache.clear();
0757     // clearing the old metadata
0758     d->m_metaData.clear();
0759     // cleaning the page names mapping
0760     d->m_pageNamesCache.clear();
0761     // releasing the old document
0762     if (d->m_djvu_document) {
0763         ddjvu_document_release(d->m_djvu_document);
0764     }
0765     d->m_djvu_document = nullptr;
0766 }
0767 
0768 QVariant KDjVu::metaData(const QString &key) const
0769 {
0770     QHash<QString, QVariant>::ConstIterator it = d->m_metaData.constFind(key);
0771     return it != d->m_metaData.constEnd() ? it.value() : QVariant();
0772 }
0773 
0774 const QDomDocument *KDjVu::documentBookmarks() const
0775 {
0776     if (!d->m_docBookmarks) {
0777         d->readBookmarks();
0778     }
0779     return d->m_docBookmarks;
0780 }
0781 
0782 void KDjVu::linksAndAnnotationsForPage(int pageNum, QList<KDjVu::Link *> *links, QList<KDjVu::Annotation *> *annotations) const
0783 {
0784     if ((pageNum < 0) || (pageNum >= d->m_pages.count()) || (!links && !annotations)) {
0785         return;
0786     }
0787 
0788     miniexp_t annots;
0789     while ((annots = ddjvu_document_get_pageanno(d->m_djvu_document, pageNum)) == miniexp_dummy) {
0790         handle_ddjvu_messages(d->m_djvu_cxt, true);
0791     }
0792 
0793     if (!miniexp_listp(annots)) {
0794         return;
0795     }
0796 
0797     if (links) {
0798         links->clear();
0799     }
0800     if (annotations) {
0801         annotations->clear();
0802     }
0803 
0804     int l = miniexp_length(annots);
0805     for (int i = 0; i < l; ++i) {
0806         miniexp_t cur = miniexp_nth(i, annots);
0807         int num = miniexp_length(cur);
0808         if ((num < 4) || !miniexp_symbolp(miniexp_nth(0, cur)) || (qstrncmp(miniexp_to_name(miniexp_nth(0, cur)), "maparea", 7) != 0)) {
0809             continue;
0810         }
0811 
0812         QString target;
0813         QString type;
0814         if (miniexp_symbolp(miniexp_nth(0, miniexp_nth(3, cur)))) {
0815             type = QString::fromUtf8(miniexp_to_name(miniexp_nth(0, miniexp_nth(3, cur))));
0816         }
0817         KDjVu::Link *link = nullptr;
0818         KDjVu::Annotation *ann = nullptr;
0819         miniexp_t urlexp = miniexp_nth(1, cur);
0820         if (links && (type == QLatin1String("rect") || type == QLatin1String("oval") || type == QLatin1String("poly"))) {
0821             if (miniexp_stringp(urlexp)) {
0822                 target = QString::fromUtf8(miniexp_to_str(miniexp_nth(1, cur)));
0823             } else if (miniexp_listp(urlexp) && (miniexp_length(urlexp) == 3) && miniexp_symbolp(miniexp_nth(0, urlexp)) && (qstrncmp(miniexp_to_name(miniexp_nth(0, urlexp)), "url", 3) == 0)) {
0824                 target = QString::fromUtf8(miniexp_to_str(miniexp_nth(1, urlexp)));
0825             }
0826             if (target.isEmpty() || ((target.length() > 0) && target.at(0) == QLatin1Char('#'))) {
0827                 KDjVu::PageLink *plink = new KDjVu::PageLink();
0828                 plink->m_page = target;
0829                 link = plink;
0830             } else {
0831                 KDjVu::UrlLink *ulink = new KDjVu::UrlLink();
0832                 ulink->m_url = target;
0833                 link = ulink;
0834             }
0835         } else if (annotations && (type == QLatin1String("text") || type == QLatin1String("line"))) {
0836             if (type == QLatin1String("text")) {
0837                 KDjVu::TextAnnotation *textann = new KDjVu::TextAnnotation(cur);
0838                 ann = textann;
0839             } else if (type == QLatin1String("line")) {
0840                 KDjVu::LineAnnotation *lineann = new KDjVu::LineAnnotation(cur);
0841                 ann = lineann;
0842             }
0843         }
0844         if (link /* safety check */ && links) {
0845             link->m_area = KDjVu::Link::UnknownArea;
0846             miniexp_t area = miniexp_nth(3, cur);
0847             int arealength = miniexp_length(area);
0848             if ((arealength == 5) && (type == QLatin1String("rect") || type == QLatin1String("oval"))) {
0849                 link->m_point = QPoint(miniexp_to_int(miniexp_nth(1, area)), miniexp_to_int(miniexp_nth(2, area)));
0850                 link->m_size = QSize(miniexp_to_int(miniexp_nth(3, area)), miniexp_to_int(miniexp_nth(4, area)));
0851                 if (type == QLatin1String("rect")) {
0852                     link->m_area = KDjVu::Link::RectArea;
0853                 } else {
0854                     link->m_area = KDjVu::Link::EllipseArea;
0855                 }
0856             } else if ((arealength > 0) && (arealength % 2 == 1) && type == QLatin1String("poly")) {
0857                 link->m_area = KDjVu::Link::PolygonArea;
0858                 QPolygon poly;
0859                 for (int j = 1; j < arealength; j += 2) {
0860                     poly << QPoint(miniexp_to_int(miniexp_nth(j, area)), miniexp_to_int(miniexp_nth(j + 1, area)));
0861                 }
0862                 link->m_poly = poly;
0863             }
0864 
0865             if (link->m_area != KDjVu::Link::UnknownArea) {
0866                 links->append(link);
0867             }
0868         } else if (ann /* safety check */ && annotations) {
0869             annotations->append(ann);
0870         }
0871     }
0872 }
0873 
0874 const QVector<KDjVu::Page *> &KDjVu::pages() const
0875 {
0876     return d->m_pages;
0877 }
0878 
0879 QImage KDjVu::image(int page, int width, int height, int rotation)
0880 {
0881     if (d->m_cacheEnabled) {
0882         bool found = false;
0883         QList<ImageCacheItem *>::Iterator it = d->mImgCache.begin(), itEnd = d->mImgCache.end();
0884         for (; (it != itEnd) && !found; ++it) {
0885             ImageCacheItem *cur = *it;
0886             if ((cur->page == page) && (rotation % 2 == 0 ? cur->width == width && cur->height == height : cur->width == height && cur->height == width)) {
0887                 found = true;
0888             }
0889         }
0890         if (found) {
0891             // taking the element and pushing to the top of the list
0892             --it;
0893             ImageCacheItem *cur2 = *it;
0894             d->mImgCache.erase(it);
0895             d->mImgCache.push_front(cur2);
0896 
0897             return cur2->img;
0898         }
0899     }
0900 
0901     if (!d->m_pages_cache.at(page)) {
0902         ddjvu_page_t *newpage = ddjvu_page_create_by_pageno(d->m_djvu_document, page);
0903         // wait for the new page to be loaded
0904         ddjvu_status_t sts;
0905         while ((sts = ddjvu_page_decoding_status(newpage)) < DDJVU_JOB_OK) {
0906             handle_ddjvu_messages(d->m_djvu_cxt, true);
0907         }
0908         d->m_pages_cache[page] = newpage;
0909     }
0910     ddjvu_page_t *djvupage = d->m_pages_cache[page];
0911 
0912     /*
0913         if ( ddjvu_page_get_rotation( djvupage ) != flipRotation( rotation ) )
0914         {
0915     // TODO: test documents with initial rotation != 0
0916     //        ddjvu_page_set_rotation( djvupage, m_pages.at( page )->orientation() );
0917             ddjvu_page_set_rotation( djvupage, (ddjvu_page_rotation_t)flipRotation( rotation ) );
0918         }
0919     */
0920 
0921     static const int xdelta = 1500;
0922     static const int ydelta = 1500;
0923 
0924     int xparts = width / xdelta + 1;
0925     int yparts = height / ydelta + 1;
0926 
0927     QImage newimg;
0928 
0929     int res = 10000;
0930     if ((xparts == 1) && (yparts == 1)) {
0931         // only one part -- render at once with no need to auxiliary image
0932         newimg = d->generateImageTile(djvupage, res, width, 0, xdelta, height, 0, ydelta);
0933     } else {
0934         // more than one part -- need to render piece-by-piece and to compose
0935         // the results
0936         newimg = QImage(width, height, QImage::Format_RGB32);
0937         QPainter p;
0938         p.begin(&newimg);
0939         int parts = xparts * yparts;
0940         for (int i = 0; i < parts; ++i) {
0941             const int row = i % xparts;
0942             const int col = i / xparts;
0943             int tmpres = 0;
0944             const QImage tempp = d->generateImageTile(djvupage, tmpres, width, row, xdelta, height, col, ydelta);
0945             p.drawImage(row * xdelta, col * ydelta, tempp);
0946             res = qMin(tmpres, res);
0947         }
0948         p.end();
0949     }
0950 
0951     if (res && d->m_cacheEnabled) {
0952         // delete all the cached pixmaps for the current page with a size that
0953         // differs no more than 35% of the new pixmap size
0954         int imgsize = newimg.width() * newimg.height();
0955         if (imgsize > 0) {
0956             for (int i = 0; i < d->mImgCache.count();) {
0957                 ImageCacheItem *cur = d->mImgCache.at(i);
0958                 if ((cur->page == page) && (abs(cur->img.width() * cur->img.height() - imgsize) < imgsize * 0.35)) {
0959                     d->mImgCache.removeAt(i);
0960                     delete cur;
0961                 } else {
0962                     ++i;
0963                 }
0964             }
0965         }
0966 
0967         // the image cache has too many elements, remove the last
0968         if (d->mImgCache.size() >= 10) {
0969             delete d->mImgCache.last();
0970             d->mImgCache.removeLast();
0971         }
0972         ImageCacheItem *ich = new ImageCacheItem(page, width, height, newimg);
0973         d->mImgCache.push_front(ich);
0974     }
0975 
0976     return newimg;
0977 }
0978 
0979 bool KDjVu::exportAsPostScript(const QString &fileName, const QList<int> &pageList) const
0980 {
0981     if (!d->m_djvu_document || fileName.trimmed().isEmpty() || pageList.isEmpty()) {
0982         return false;
0983     }
0984 
0985     QFile f(fileName);
0986     f.open(QIODevice::ReadWrite);
0987     bool ret = exportAsPostScript(&f, pageList);
0988     if (ret) {
0989         f.close();
0990     }
0991     return ret;
0992 }
0993 
0994 bool KDjVu::exportAsPostScript(QFile *file, const QList<int> &pageList) const
0995 {
0996     if (!d->m_djvu_document || !file || pageList.isEmpty()) {
0997         return false;
0998     }
0999 
1000     FILE *f = fdopen(file->handle(), "w+");
1001     if (!f) {
1002         qDebug() << "error while getting the FILE*";
1003         return false;
1004     }
1005 
1006     QString pl;
1007     for (const int p : pageList) {
1008         if (!pl.isEmpty()) {
1009             pl += QLatin1String(",");
1010         }
1011         pl += QString::number(p);
1012     }
1013     pl.prepend(QStringLiteral("-page="));
1014 
1015     // setting the options
1016     static const int optc = 1;
1017     const char **optv = (const char **)malloc(1 * sizeof(char *));
1018     QByteArray plb = pl.toLatin1();
1019     optv[0] = plb.constData();
1020 
1021     ddjvu_job_t *printjob = ddjvu_document_print(d->m_djvu_document, f, optc, optv);
1022     while (!ddjvu_job_done(printjob)) {
1023         handle_ddjvu_messages(d->m_djvu_cxt, true);
1024     }
1025 
1026     free(optv);
1027 
1028     return fclose(f) == 0;
1029 }
1030 
1031 QList<KDjVu::TextEntity> KDjVu::textEntities(int page, const QString &granularity) const
1032 {
1033     if ((page < 0) || (page >= d->m_pages.count())) {
1034         return QList<KDjVu::TextEntity>();
1035     }
1036 
1037     miniexp_t r;
1038     while ((r = ddjvu_document_get_pagetext(d->m_djvu_document, page, nullptr)) == miniexp_dummy) {
1039         handle_ddjvu_messages(d->m_djvu_cxt, true);
1040     }
1041 
1042     if (r == miniexp_nil) {
1043         return QList<KDjVu::TextEntity>();
1044     }
1045 
1046     QList<KDjVu::TextEntity> ret;
1047 
1048     int height = d->m_pages.at(page)->height();
1049 
1050     QQueue<miniexp_t> queue;
1051     queue.enqueue(r);
1052 
1053     while (!queue.isEmpty()) {
1054         miniexp_t cur = queue.dequeue();
1055 
1056         if (miniexp_listp(cur) && (miniexp_length(cur) > 0) && miniexp_symbolp(miniexp_nth(0, cur))) {
1057             int size = miniexp_length(cur);
1058             QString sym = QString::fromUtf8(miniexp_to_name(miniexp_nth(0, cur)));
1059             if (sym == granularity) {
1060                 if (size >= 6) {
1061                     int xmin = miniexp_to_int(miniexp_nth(1, cur));
1062                     int ymin = miniexp_to_int(miniexp_nth(2, cur));
1063                     int xmax = miniexp_to_int(miniexp_nth(3, cur));
1064                     int ymax = miniexp_to_int(miniexp_nth(4, cur));
1065                     QRect rect(xmin, height - ymax, xmax - xmin, ymax - ymin);
1066                     KDjVu::TextEntity entity;
1067                     entity.m_rect = rect;
1068                     entity.m_text = QString::fromUtf8(miniexp_to_str(miniexp_nth(5, cur)));
1069                     ret.append(entity);
1070                 }
1071             } else {
1072                 for (int i = 5; i < size; ++i) {
1073                     queue.enqueue(miniexp_nth(i, cur));
1074                 }
1075             }
1076         }
1077     }
1078 
1079     return ret;
1080 }
1081 
1082 void KDjVu::setCacheEnabled(bool enable)
1083 {
1084     if (enable == d->m_cacheEnabled) {
1085         return;
1086     }
1087 
1088     d->m_cacheEnabled = enable;
1089     if (!d->m_cacheEnabled) {
1090         qDeleteAll(d->mImgCache);
1091         d->mImgCache.clear();
1092     }
1093 }
1094 
1095 bool KDjVu::isCacheEnabled() const
1096 {
1097     return d->m_cacheEnabled;
1098 }
1099 
1100 int KDjVu::pageNumber(const QString &name) const
1101 {
1102     if (!d->m_djvu_document) {
1103         return -1;
1104     }
1105 
1106     return d->pageWithName(name);
1107 }