File indexing completed on 2024-04-28 04:32:46

0001 /*
0002     SPDX-FileCopyrightText: 2007 Tobias Koenig <tokoe@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "textdocumentgenerator.h"
0008 #include "textdocumentgenerator_p.h"
0009 
0010 #include <QFile>
0011 #include <QFontDatabase>
0012 #include <QImage>
0013 #include <QMutex>
0014 #include <QPainter>
0015 #include <QPrinter>
0016 #include <QStack>
0017 #include <QTextDocumentWriter>
0018 #include <QTextStream>
0019 #include <QVector>
0020 
0021 #include "action.h"
0022 #include "annotations.h"
0023 #include "document_p.h"
0024 #include "page.h"
0025 #include "textpage.h"
0026 
0027 #include <cmath>
0028 
0029 using namespace Okular;
0030 
0031 /**
0032  * Generic Converter Implementation
0033  */
0034 TextDocumentConverter::TextDocumentConverter()
0035     : QObject(nullptr)
0036     , d_ptr(new TextDocumentConverterPrivate)
0037 {
0038 }
0039 
0040 TextDocumentConverter::~TextDocumentConverter()
0041 {
0042     delete d_ptr;
0043 }
0044 
0045 QTextDocument *TextDocumentConverter::convert(const QString &)
0046 {
0047     return nullptr;
0048 }
0049 
0050 Document::OpenResult TextDocumentConverter::convertWithPassword(const QString &fileName, const QString &)
0051 {
0052     QTextDocument *doc = convert(fileName);
0053     setDocument(doc);
0054     return doc != nullptr ? Document::OpenSuccess : Document::OpenError;
0055 }
0056 
0057 QTextDocument *TextDocumentConverter::document()
0058 {
0059     return d_ptr->mDocument;
0060 }
0061 
0062 void TextDocumentConverter::setDocument(QTextDocument *document)
0063 {
0064     d_ptr->mDocument = document;
0065 }
0066 
0067 DocumentViewport TextDocumentConverter::calculateViewport(QTextDocument *document, const QTextBlock &block)
0068 {
0069     return TextDocumentUtils::calculateViewport(document, block);
0070 }
0071 
0072 TextDocumentGenerator *TextDocumentConverter::generator() const
0073 {
0074     return d_ptr->mParent ? d_ptr->mParent->q_func() : nullptr;
0075 }
0076 
0077 /**
0078  * Generic Generator Implementation
0079  */
0080 Okular::TextPage *TextDocumentGeneratorPrivate::createTextPage(int pageNumber) const
0081 {
0082 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0083     Q_Q(const TextDocumentGenerator);
0084 #endif
0085 
0086     Okular::TextPage *textPage = new Okular::TextPage;
0087 
0088     int start, end;
0089 
0090 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0091     q->userMutex()->lock();
0092 #endif
0093     TextDocumentUtils::calculatePositions(mDocument, pageNumber, start, end);
0094 
0095     {
0096         QTextCursor cursor(mDocument);
0097         for (int i = start; i < end - 1; ++i) {
0098             cursor.setPosition(i);
0099             cursor.setPosition(i + 1, QTextCursor::KeepAnchor);
0100 
0101             QString text = cursor.selectedText();
0102             if (text.length() == 1) {
0103                 QRectF rect;
0104                 TextDocumentUtils::calculateBoundingRect(mDocument, i, i + 1, rect, pageNumber);
0105                 if (pageNumber == -1) {
0106                     text = QStringLiteral("\n");
0107                 }
0108 
0109                 textPage->append(text, Okular::NormalizedRect(rect.left(), rect.top(), rect.right(), rect.bottom()));
0110             }
0111         }
0112     }
0113 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0114     q->userMutex()->unlock();
0115 #endif
0116 
0117     return textPage;
0118 }
0119 
0120 void TextDocumentGeneratorPrivate::addAction(Action *action, int cursorBegin, int cursorEnd)
0121 {
0122     if (!action) {
0123         return;
0124     }
0125 
0126     LinkPosition position;
0127     position.link = action;
0128     position.startPosition = cursorBegin;
0129     position.endPosition = cursorEnd;
0130 
0131     mLinkPositions.append(position);
0132 }
0133 
0134 void TextDocumentGeneratorPrivate::addAnnotation(Annotation *annotation, int cursorBegin, int cursorEnd)
0135 {
0136     if (!annotation) {
0137         return;
0138     }
0139 
0140     annotation->setFlags(annotation->flags() | Okular::Annotation::External);
0141 
0142     AnnotationPosition position;
0143     position.annotation = annotation;
0144     position.startPosition = cursorBegin;
0145     position.endPosition = cursorEnd;
0146 
0147     mAnnotationPositions.append(position);
0148 }
0149 
0150 void TextDocumentGeneratorPrivate::addTitle(int level, const QString &title, const QTextBlock &block)
0151 {
0152     TitlePosition position;
0153     position.level = level;
0154     position.title = title;
0155     position.block = block;
0156 
0157     mTitlePositions.append(position);
0158 }
0159 
0160 void TextDocumentGeneratorPrivate::addMetaData(DocumentInfo::Key key, const QString &value)
0161 {
0162     mDocumentInfo.set(key, value);
0163 }
0164 
0165 QList<TextDocumentGeneratorPrivate::LinkInfo> TextDocumentGeneratorPrivate::generateLinkInfos() const
0166 {
0167     QList<LinkInfo> result;
0168 
0169     for (int i = 0; i < mLinkPositions.count(); ++i) {
0170         const LinkPosition &linkPosition = mLinkPositions[i];
0171 
0172         const QVector<QRectF> rects = TextDocumentUtils::calculateBoundingRects(mDocument, linkPosition.startPosition, linkPosition.endPosition);
0173 
0174         for (int i = 0; i < rects.count(); ++i) {
0175             const QRectF &rect = rects[i];
0176 
0177             LinkInfo info;
0178             info.link = linkPosition.link;
0179             info.ownsLink = i == 0;
0180             info.page = std::floor(rect.y());
0181             info.boundingRect = QRectF(rect.x(), rect.y() - info.page, rect.width(), rect.height());
0182             result.append(info);
0183         }
0184     }
0185 
0186     return result;
0187 }
0188 
0189 QList<TextDocumentGeneratorPrivate::AnnotationInfo> TextDocumentGeneratorPrivate::generateAnnotationInfos() const
0190 {
0191     QList<AnnotationInfo> result;
0192 
0193     for (int i = 0; i < mAnnotationPositions.count(); ++i) {
0194         const AnnotationPosition &annotationPosition = mAnnotationPositions[i];
0195 
0196         AnnotationInfo info;
0197         info.annotation = annotationPosition.annotation;
0198 
0199         TextDocumentUtils::calculateBoundingRect(mDocument, annotationPosition.startPosition, annotationPosition.endPosition, info.boundingRect, info.page);
0200 
0201         if (info.page >= 0) {
0202             result.append(info);
0203         }
0204     }
0205 
0206     return result;
0207 }
0208 
0209 void TextDocumentGeneratorPrivate::generateTitleInfos()
0210 {
0211     QStack<QPair<int, QDomNode>> parentNodeStack;
0212 
0213     QDomNode parentNode = mDocumentSynopsis;
0214 
0215     parentNodeStack.push(qMakePair(0, parentNode));
0216 
0217     for (int i = 0; i < mTitlePositions.count(); ++i) {
0218         const TitlePosition &position = mTitlePositions[i];
0219 
0220         Okular::DocumentViewport viewport = TextDocumentUtils::calculateViewport(mDocument, position.block);
0221 
0222         QDomElement item = mDocumentSynopsis.createElement(position.title);
0223         item.setAttribute(QStringLiteral("Viewport"), viewport.toString());
0224 
0225         int headingLevel = position.level;
0226 
0227         // we need a parent, which has to be at a higher heading level than this heading level
0228         // so we just work through the stack
0229         while (!parentNodeStack.isEmpty()) {
0230             int parentLevel = parentNodeStack.top().first;
0231             if (parentLevel < headingLevel) {
0232                 // this is OK as a parent
0233                 parentNode = parentNodeStack.top().second;
0234                 break;
0235             } else {
0236                 // we'll need to be further into the stack
0237                 parentNodeStack.pop();
0238             }
0239         }
0240         parentNode.appendChild(item);
0241         parentNodeStack.push(qMakePair(headingLevel, QDomNode(item)));
0242     }
0243 }
0244 
0245 void TextDocumentGeneratorPrivate::initializeGenerator()
0246 {
0247     Q_Q(TextDocumentGenerator);
0248 
0249     mConverter->d_ptr->mParent = q->d_func();
0250 
0251     if (mGeneralSettings) {
0252         mFont = mGeneralSettings->font();
0253     }
0254 
0255     q->setFeature(Generator::TextExtraction);
0256     q->setFeature(Generator::PrintNative);
0257     q->setFeature(Generator::PrintToFile);
0258 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0259     q->setFeature(Generator::Threaded);
0260 #endif
0261 
0262     QObject::connect(mConverter, &TextDocumentConverter::addAction, q, [this](Action *a, int cb, int ce) { addAction(a, cb, ce); });
0263     QObject::connect(mConverter, &TextDocumentConverter::addAnnotation, q, [this](Annotation *a, int cb, int ce) { addAnnotation(a, cb, ce); });
0264     QObject::connect(mConverter, &TextDocumentConverter::addTitle, q, [this](int l, const QString &t, const QTextBlock &b) { addTitle(l, t, b); });
0265     QObject::connect(mConverter, &TextDocumentConverter::addMetaData, q, [this](DocumentInfo::Key k, const QString &v) { addMetaData(k, v); });
0266 
0267     QObject::connect(mConverter, &TextDocumentConverter::error, q, &Generator::error);
0268     QObject::connect(mConverter, &TextDocumentConverter::warning, q, &Generator::warning);
0269     QObject::connect(mConverter, &TextDocumentConverter::notice, q, &Generator::notice);
0270 }
0271 
0272 TextDocumentGenerator::TextDocumentGenerator(TextDocumentConverter *converter, const QString &configName, QObject *parent, const QVariantList &args)
0273     : Okular::Generator(*new TextDocumentGeneratorPrivate(converter), parent, args)
0274 {
0275     Q_D(TextDocumentGenerator);
0276     d->mGeneralSettings = new TextDocumentSettings(configName, this);
0277 
0278     d->initializeGenerator();
0279 }
0280 
0281 TextDocumentGenerator::~TextDocumentGenerator()
0282 {
0283 }
0284 
0285 Document::OpenResult TextDocumentGenerator::loadDocumentWithPassword(const QString &fileName, QVector<Okular::Page *> &pagesVector, const QString &password)
0286 {
0287     Q_D(TextDocumentGenerator);
0288     const Document::OpenResult openResult = d->mConverter->convertWithPassword(fileName, password);
0289 
0290     if (openResult != Document::OpenSuccess) {
0291         d->mDocument = nullptr;
0292 
0293         // loading failed, cleanup all the stuff eventually gathered from the converter
0294         d->mTitlePositions.clear();
0295         for (const TextDocumentGeneratorPrivate::LinkPosition &linkPos : std::as_const(d->mLinkPositions)) {
0296             delete linkPos.link;
0297         }
0298         d->mLinkPositions.clear();
0299         for (const TextDocumentGeneratorPrivate::AnnotationPosition &annPos : std::as_const(d->mAnnotationPositions)) {
0300             delete annPos.annotation;
0301         }
0302         d->mAnnotationPositions.clear();
0303 
0304         return openResult;
0305     }
0306     d->mDocument = d->mConverter->document();
0307 
0308     d->generateTitleInfos();
0309     const QList<TextDocumentGeneratorPrivate::LinkInfo> linkInfos = d->generateLinkInfos();
0310     const QList<TextDocumentGeneratorPrivate::AnnotationInfo> annotationInfos = d->generateAnnotationInfos();
0311 
0312     pagesVector.resize(d->mDocument->pageCount());
0313 
0314     const QSize size = d->mDocument->pageSize().toSize();
0315 
0316     QVector<QList<Okular::ObjectRect *>> objects(d->mDocument->pageCount());
0317     for (const TextDocumentGeneratorPrivate::LinkInfo &info : linkInfos) {
0318         // in case that the converter report bogus link info data, do not assert here
0319         if (info.page < 0 || info.page >= objects.count()) {
0320             continue;
0321         }
0322 
0323         const QRectF rect = info.boundingRect;
0324         if (info.ownsLink) {
0325             objects[info.page].append(new Okular::ObjectRect(rect.left(), rect.top(), rect.right(), rect.bottom(), false, Okular::ObjectRect::Action, info.link));
0326         } else {
0327             objects[info.page].append(new Okular::NonOwningObjectRect(rect.left(), rect.top(), rect.right(), rect.bottom(), false, Okular::ObjectRect::Action, info.link));
0328         }
0329     }
0330 
0331     QVector<QList<Okular::Annotation *>> annots(d->mDocument->pageCount());
0332     for (const TextDocumentGeneratorPrivate::AnnotationInfo &info : annotationInfos) {
0333         annots[info.page].append(info.annotation);
0334     }
0335 
0336     for (int i = 0; i < d->mDocument->pageCount(); ++i) {
0337         Okular::Page *page = new Okular::Page(i, size.width(), size.height(), Okular::Rotation0);
0338         pagesVector[i] = page;
0339 
0340         if (!objects.at(i).isEmpty()) {
0341             page->setObjectRects(objects.at(i));
0342         }
0343         QList<Okular::Annotation *>::ConstIterator annIt = annots.at(i).begin(), annEnd = annots.at(i).end();
0344         for (; annIt != annEnd; ++annIt) {
0345             page->addAnnotation(*annIt);
0346         }
0347     }
0348 
0349     return openResult;
0350 }
0351 
0352 bool TextDocumentGenerator::doCloseDocument()
0353 {
0354     Q_D(TextDocumentGenerator);
0355     delete d->mDocument;
0356     d->mDocument = nullptr;
0357 
0358     d->mTitlePositions.clear();
0359     d->mLinkPositions.clear();
0360     d->mAnnotationPositions.clear();
0361     // do not use clear() for the following two, otherwise they change type
0362     d->mDocumentInfo = Okular::DocumentInfo();
0363     d->mDocumentSynopsis = Okular::DocumentSynopsis();
0364 
0365     return true;
0366 }
0367 
0368 bool TextDocumentGenerator::canGeneratePixmap() const
0369 {
0370     return Generator::canGeneratePixmap();
0371 }
0372 
0373 void TextDocumentGenerator::generatePixmap(Okular::PixmapRequest *request)
0374 {
0375     Generator::generatePixmap(request);
0376 }
0377 
0378 QImage TextDocumentGeneratorPrivate::image(PixmapRequest *request)
0379 {
0380     if (!mDocument) {
0381         return QImage();
0382     }
0383 
0384 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0385     Q_Q(TextDocumentGenerator);
0386 #endif
0387 
0388     QImage image(request->width(), request->height(), QImage::Format_ARGB32);
0389     image.fill(Qt::white);
0390 
0391     QPainter p;
0392     p.begin(&image);
0393 
0394     qreal width = request->width();
0395     qreal height = request->height();
0396 
0397     const QSize size = mDocument->pageSize().toSize();
0398 
0399     p.scale(width / (qreal)size.width(), height / (qreal)size.height());
0400 
0401     QRect rect;
0402     rect = QRect(0, request->pageNumber() * size.height(), size.width(), size.height());
0403     p.translate(QPoint(0, request->pageNumber() * size.height() * -1));
0404     p.setClipRect(rect);
0405 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0406     q->userMutex()->lock();
0407 #endif
0408     QAbstractTextDocumentLayout::PaintContext context;
0409     context.palette.setColor(QPalette::Text, Qt::black);
0410     //  FIXME Fix Qt, this doesn't work, we have horrible hacks
0411     //        in the generators that return html, remove that code
0412     //        if Qt ever gets fixed
0413     //     context.palette.setColor( QPalette::Link, Qt::blue );
0414     context.clip = rect;
0415     mDocument->setDefaultFont(mFont);
0416     mDocument->documentLayout()->draw(&p, context);
0417 #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
0418     q->userMutex()->unlock();
0419 #endif
0420     p.end();
0421 
0422     return image;
0423 }
0424 
0425 Okular::TextPage *TextDocumentGenerator::textPage(Okular::TextRequest *request)
0426 {
0427     Q_D(TextDocumentGenerator);
0428     return d->createTextPage(request->page()->number());
0429 }
0430 
0431 Document::PrintError TextDocumentGenerator::print(QPrinter &printer)
0432 {
0433     Q_D(TextDocumentGenerator);
0434     if (!d->mDocument) {
0435         return Document::UnknownPrintError;
0436     }
0437 
0438     d->mDocument->print(&printer);
0439 
0440     return Document::NoPrintError;
0441 }
0442 
0443 Okular::DocumentInfo TextDocumentGenerator::generateDocumentInfo(const QSet<DocumentInfo::Key> & /*keys*/) const
0444 {
0445     Q_D(const TextDocumentGenerator);
0446     return d->mDocumentInfo;
0447 }
0448 
0449 const Okular::DocumentSynopsis *TextDocumentGenerator::generateDocumentSynopsis()
0450 {
0451     Q_D(TextDocumentGenerator);
0452     if (!d->mDocumentSynopsis.hasChildNodes()) {
0453         return nullptr;
0454     } else {
0455         return &d->mDocumentSynopsis;
0456     }
0457 }
0458 
0459 QVariant TextDocumentGeneratorPrivate::metaData(const QString &key, const QVariant &option) const
0460 {
0461     Q_UNUSED(option)
0462     if (key == QLatin1String("DocumentTitle")) {
0463         return mDocumentInfo.get(DocumentInfo::Title);
0464     }
0465     return QVariant();
0466 }
0467 
0468 Okular::ExportFormat::List TextDocumentGenerator::exportFormats() const
0469 {
0470     static Okular::ExportFormat::List formats;
0471     if (formats.isEmpty()) {
0472         formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PlainText));
0473         formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PDF));
0474         if (QTextDocumentWriter::supportedDocumentFormats().contains("ODF")) {
0475             formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::OpenDocumentText));
0476         }
0477         if (QTextDocumentWriter::supportedDocumentFormats().contains("HTML")) {
0478             formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::HTML));
0479         }
0480     }
0481 
0482     return formats;
0483 }
0484 
0485 bool TextDocumentGenerator::exportTo(const QString &fileName, const Okular::ExportFormat &format)
0486 {
0487     Q_D(TextDocumentGenerator);
0488     if (!d->mDocument) {
0489         return false;
0490     }
0491 
0492     if (format.mimeType().name() == QLatin1String("application/pdf")) {
0493         QFile file(fileName);
0494         if (!file.open(QIODevice::WriteOnly)) {
0495             return false;
0496         }
0497 
0498         QPrinter printer(QPrinter::HighResolution);
0499         printer.setOutputFormat(QPrinter::PdfFormat);
0500         printer.setOutputFileName(fileName);
0501         d->mDocument->print(&printer);
0502 
0503         return true;
0504     } else if (format.mimeType().name() == QLatin1String("text/plain")) {
0505         QFile file(fileName);
0506         if (!file.open(QIODevice::WriteOnly)) {
0507             return false;
0508         }
0509 
0510         QTextStream out(&file);
0511         out << d->mDocument->toPlainText();
0512 
0513         return true;
0514     } else if (format.mimeType().name() == QLatin1String("application/vnd.oasis.opendocument.text")) {
0515         QTextDocumentWriter odfWriter(fileName, "odf");
0516 
0517         return odfWriter.write(d->mDocument);
0518     } else if (format.mimeType().name() == QLatin1String("text/html")) {
0519         QTextDocumentWriter odfWriter(fileName, "html");
0520 
0521         return odfWriter.write(d->mDocument);
0522     }
0523     return false;
0524 }
0525 
0526 bool TextDocumentGenerator::reparseConfig()
0527 {
0528     Q_D(TextDocumentGenerator);
0529     const QFont newFont = d->mGeneralSettings->font();
0530 
0531     if (newFont != d->mFont) {
0532         d->mFont = newFont;
0533         return true;
0534     }
0535 
0536     return false;
0537 }
0538 
0539 void TextDocumentGenerator::addPages(KConfigDialog * /*dlg*/)
0540 {
0541     qCWarning(OkularCoreDebug) << "You forgot to reimplement addPages in your TextDocumentGenerator";
0542     return;
0543 }
0544 
0545 TextDocumentSettings *TextDocumentGenerator::generalSettings()
0546 {
0547     Q_D(TextDocumentGenerator);
0548 
0549     return d->mGeneralSettings;
0550 }
0551 
0552 TextDocumentConverter *TextDocumentGenerator::converter()
0553 {
0554     Q_D(TextDocumentGenerator);
0555 
0556     return d->mConverter;
0557 }
0558 
0559 void TextDocumentGenerator::setTextDocument(QTextDocument *textDocument)
0560 {
0561     Q_D(TextDocumentGenerator);
0562 
0563     d->mDocument = textDocument;
0564 
0565     for (Page *p : std::as_const(d->m_document->m_pagesVector)) {
0566         p->setTextPage(nullptr);
0567     }
0568 }