You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
14 KiB
407 lines
14 KiB
/* |
|
SPDX-FileCopyrightText: 2005 Piotr Szymański <niedakh@gmail.com> |
|
SPDX-FileCopyrightText: 2008 Albert Astals Cid <aacid@kde.org> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "generator_chm.h" |
|
|
|
#include <QDomElement> |
|
#include <QEventLoop> |
|
#include <QMutex> |
|
#include <QPainter> |
|
|
|
#include <KAboutData> |
|
#include <KHTMLView> |
|
#include <KLocalizedString> |
|
#include <QUrl> |
|
#include <dom/dom_html.h> |
|
#include <dom/dom_node.h> |
|
#include <dom/html_misc.h> |
|
#include <khtml_part.h> |
|
|
|
#include <core/action.h> |
|
#include <core/page.h> |
|
#include <core/textpage.h> |
|
#include <core/utils.h> |
|
|
|
OKULAR_EXPORT_PLUGIN(CHMGenerator, "libokularGenerator_chmlib.json") |
|
|
|
static QString absolutePath(const QString &baseUrl, const QString &path) |
|
{ |
|
QString absPath; |
|
if (path.startsWith(QLatin1Char('/'))) { |
|
// already absolute |
|
absPath = path; |
|
} else { |
|
QUrl url = QUrl::fromLocalFile(baseUrl).adjusted(QUrl::RemoveFilename); |
|
url.setPath(url.path() + path); |
|
absPath = url.toLocalFile(); |
|
} |
|
return absPath; |
|
} |
|
|
|
CHMGenerator::CHMGenerator(QObject *parent, const QVariantList &args) |
|
: Okular::Generator(parent, args) |
|
{ |
|
setFeature(TextExtraction); |
|
|
|
m_syncGen = nullptr; |
|
m_file = nullptr; |
|
m_request = nullptr; |
|
} |
|
|
|
CHMGenerator::~CHMGenerator() |
|
{ |
|
delete m_syncGen; |
|
} |
|
|
|
bool CHMGenerator::loadDocument(const QString &fileName, QVector<Okular::Page *> &pagesVector) |
|
{ |
|
m_file = EBook::loadFile(fileName); |
|
if (!m_file) { |
|
return false; |
|
} |
|
m_fileName = fileName; |
|
QList<EBookTocEntry> topics; |
|
m_file->getTableOfContents(topics); |
|
|
|
// fill m_docSyn |
|
QMap<int, QDomElement> lastIndentElement; |
|
QMap<QString, int> tmpPageList; |
|
int pageNum = 0; |
|
|
|
for (const EBookTocEntry &e : qAsConst(topics)) { |
|
QDomElement item = m_docSyn.createElement(e.name); |
|
if (!e.url.isEmpty()) { |
|
QString url = e.url.toString(); |
|
item.setAttribute(QStringLiteral("ViewportName"), url); |
|
if (!tmpPageList.contains(url)) { // add a page only once |
|
tmpPageList.insert(url, pageNum); |
|
pageNum++; |
|
} |
|
} |
|
item.setAttribute(QStringLiteral("Icon"), e.iconid); |
|
if (e.indent == 0) { |
|
m_docSyn.appendChild(item); |
|
} else { |
|
lastIndentElement[e.indent - 1].appendChild(item); |
|
} |
|
lastIndentElement[e.indent] = item; |
|
} |
|
|
|
// fill m_urlPage and m_pageUrl |
|
QList<QUrl> pageList; |
|
m_file->enumerateFiles(pageList); |
|
const QUrl home = m_file->homeUrl(); |
|
if (home.path() != QLatin1String("/")) { |
|
pageList.prepend(home); |
|
} |
|
m_pageUrl.resize(pageNum); |
|
|
|
for (const QUrl &qurl : qAsConst(pageList)) { |
|
QString url = qurl.toString(); |
|
const QString urlLower = url.toLower(); |
|
if (!urlLower.endsWith(QLatin1String(".html")) && !urlLower.endsWith(QLatin1String(".htm"))) { |
|
continue; |
|
} |
|
|
|
int pos = url.indexOf(QLatin1Char(('#'))); |
|
// insert the url into the maps, but insert always the variant without the #ref part |
|
QString tmpUrl = pos == -1 ? url : url.left(pos); |
|
|
|
// url already there, abort insertion |
|
if (m_urlPage.contains(tmpUrl)) { |
|
continue; |
|
} |
|
|
|
int foundPage = tmpPageList.value(tmpUrl, -1); |
|
if (foundPage != -1) { |
|
m_urlPage.insert(tmpUrl, foundPage); |
|
m_pageUrl[foundPage] = tmpUrl; |
|
} else { |
|
// add pages not present in toc |
|
m_urlPage.insert(tmpUrl, pageNum); |
|
m_pageUrl.append(tmpUrl); |
|
pageNum++; |
|
} |
|
} |
|
|
|
pagesVector.resize(m_pageUrl.count()); |
|
m_textpageAddedList.fill(false, pagesVector.count()); |
|
m_rectsGenerated.fill(false, pagesVector.count()); |
|
|
|
if (!m_syncGen) { |
|
m_syncGen = new KHTMLPart(); |
|
} |
|
disconnect(m_syncGen, nullptr, this, nullptr); |
|
|
|
for (int i = 0; i < m_pageUrl.count(); ++i) { |
|
preparePageForSyncOperation(m_pageUrl.at(i)); |
|
pagesVector[i] = new Okular::Page(i, m_syncGen->view()->contentsWidth(), m_syncGen->view()->contentsHeight(), Okular::Rotation0); |
|
} |
|
|
|
connect(m_syncGen, QOverload<>::of(&KHTMLPart::completed), this, &CHMGenerator::slotCompleted); |
|
connect(m_syncGen, &KParts::ReadOnlyPart::canceled, this, &CHMGenerator::slotCompleted); |
|
|
|
return true; |
|
} |
|
|
|
bool CHMGenerator::doCloseDocument() |
|
{ |
|
// delete the document information of the old document |
|
delete m_file; |
|
m_file = nullptr; |
|
m_textpageAddedList.clear(); |
|
m_rectsGenerated.clear(); |
|
m_urlPage.clear(); |
|
m_pageUrl.clear(); |
|
m_docSyn.clear(); |
|
if (m_syncGen) { |
|
m_syncGen->closeUrl(); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void CHMGenerator::preparePageForSyncOperation(const QString &url) |
|
{ |
|
QString pAddress = QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); |
|
m_chmUrl = url; |
|
|
|
m_syncGen->openUrl(QUrl(pAddress)); |
|
m_syncGen->view()->layout(); |
|
|
|
QEventLoop loop; |
|
connect(m_syncGen, QOverload<>::of(&KHTMLPart::completed), &loop, &QEventLoop::quit); |
|
connect(m_syncGen, &KParts::ReadOnlyPart::canceled, &loop, &QEventLoop::quit); |
|
// discard any user input, otherwise it breaks the "synchronicity" of this |
|
// function |
|
loop.exec(QEventLoop::ExcludeUserInputEvents); |
|
} |
|
|
|
void CHMGenerator::slotCompleted() |
|
{ |
|
if (!m_request) { |
|
return; |
|
} |
|
|
|
QImage image(m_request->width(), m_request->height(), QImage::Format_ARGB32); |
|
image.fill(Qt::white); |
|
|
|
QPainter p(&image); |
|
QRect r(0, 0, m_request->width(), m_request->height()); |
|
|
|
bool moreToPaint; |
|
m_syncGen->paint(&p, r, 0, &moreToPaint); |
|
|
|
p.end(); |
|
|
|
if (!m_textpageAddedList.at(m_request->pageNumber())) { |
|
additionalRequestData(); |
|
m_textpageAddedList[m_request->pageNumber()] = true; |
|
} |
|
|
|
m_syncGen->closeUrl(); |
|
m_chmUrl = QString(); |
|
|
|
userMutex()->unlock(); |
|
|
|
Okular::PixmapRequest *req = m_request; |
|
m_request = nullptr; |
|
|
|
if (!req->page()->isBoundingBoxKnown()) { |
|
updatePageBoundingBox(req->page()->number(), Okular::Utils::imageBoundingBox(&image)); |
|
} |
|
req->page()->setPixmap(req->observer(), new QPixmap(QPixmap::fromImage(image))); |
|
signalPixmapRequestDone(req); |
|
} |
|
|
|
Okular::DocumentInfo CHMGenerator::generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> &keys) const |
|
{ |
|
Okular::DocumentInfo docInfo; |
|
if (keys.contains(Okular::DocumentInfo::MimeType)) { |
|
docInfo.set(Okular::DocumentInfo::MimeType, QStringLiteral("application/x-chm")); |
|
} |
|
if (keys.contains(Okular::DocumentInfo::Title)) { |
|
docInfo.set(Okular::DocumentInfo::Title, m_file->title()); |
|
} |
|
return docInfo; |
|
} |
|
|
|
const Okular::DocumentSynopsis *CHMGenerator::generateDocumentSynopsis() |
|
{ |
|
return &m_docSyn; |
|
} |
|
|
|
bool CHMGenerator::canGeneratePixmap() const |
|
{ |
|
bool isLocked = true; |
|
if (userMutex()->tryLock()) { |
|
userMutex()->unlock(); |
|
isLocked = false; |
|
} |
|
|
|
return !isLocked; |
|
} |
|
|
|
void CHMGenerator::generatePixmap(Okular::PixmapRequest *request) |
|
{ |
|
int requestWidth = request->width(); |
|
int requestHeight = request->height(); |
|
|
|
userMutex()->lock(); |
|
QString url = m_pageUrl[request->pageNumber()]; |
|
|
|
QString pAddress = QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); |
|
m_chmUrl = url; |
|
m_syncGen->view()->resizeContents(requestWidth, requestHeight); |
|
m_request = request; |
|
// will emit openURL without problems |
|
m_syncGen->openUrl(QUrl(pAddress)); |
|
} |
|
|
|
void CHMGenerator::recursiveExploreNodes(DOM::Node node, Okular::TextPage *tp) |
|
{ |
|
if (node.nodeType() == DOM::Node::TEXT_NODE && !node.getRect().isNull()) { |
|
QString nodeText = node.nodeValue().string(); |
|
QRect r = node.getRect(); |
|
int vWidth = m_syncGen->view()->width(); |
|
int vHeight = m_syncGen->view()->height(); |
|
Okular::NormalizedRect *nodeNormRect; |
|
#define NOEXP |
|
#ifndef NOEXP |
|
int x, y, height; |
|
int x_next, y_next, height_next; |
|
int nodeTextLength = nodeText.length(); |
|
if (nodeTextLength == 1) { |
|
nodeNormRect = new Okular::NormalizedRect(r, vWidth, vHeight); |
|
tp->append(nodeText, nodeNormRect, nodeNormRect->bottom, 0, (nodeText == "\n")); |
|
} else { |
|
for (int i = 0; i < nodeTextLength; i++) { |
|
node.getCursor(i, x, y, height); |
|
if (i == 0) |
|
// i is 0, use left rect boundary |
|
{ |
|
// if (nodeType[i+1] |
|
node.getCursor(i + 1, x_next, y_next, height_next); |
|
nodeNormRect = new Okular::NormalizedRect(QRect(x, y, x_next - x - 1, height), vWidth, vHeight); |
|
} else if (i < nodeTextLength - 1) |
|
// i is between zero and the last element |
|
{ |
|
node.getCursor(i + 1, x_next, y_next, height_next); |
|
nodeNormRect = new Okular::NormalizedRect(QRect(x, y, x_next - x - 1, height), vWidth, vHeight); |
|
} else |
|
// the last element use right rect boundary |
|
{ |
|
node.getCursor(i - 1, x_next, y_next, height_next); |
|
} |
|
} |
|
#else |
|
nodeNormRect = new Okular::NormalizedRect(r, vWidth, vHeight); |
|
tp->append(nodeText, nodeNormRect /*,0*/); |
|
#endif |
|
} |
|
DOM::Node child = node.firstChild(); |
|
while (!child.isNull()) { |
|
recursiveExploreNodes(child, tp); |
|
child = child.nextSibling(); |
|
} |
|
} |
|
|
|
void CHMGenerator::additionalRequestData() |
|
{ |
|
Okular::Page *page = m_request->page(); |
|
const bool genObjectRects = !m_rectsGenerated.at(m_request->page()->number()); |
|
const bool genTextPage = !m_request->page()->hasTextPage() && genObjectRects; |
|
|
|
if (genObjectRects || genTextPage) { |
|
DOM::HTMLDocument domDoc = m_syncGen->htmlDocument(); |
|
// only generate object info when generating a full page not a thumbnail |
|
if (genObjectRects) { |
|
QLinkedList<Okular::ObjectRect *> objRects; |
|
int xScale = m_syncGen->view()->width(); |
|
int yScale = m_syncGen->view()->height(); |
|
// getting links |
|
DOM::HTMLCollection coll = domDoc.links(); |
|
DOM::Node n; |
|
QRect r; |
|
if (!coll.isNull()) { |
|
int size = coll.length(); |
|
for (int i = 0; i < size; i++) { |
|
n = coll.item(i); |
|
if (!n.isNull()) { |
|
QString url = n.attributes().getNamedItem("href").nodeValue().string(); |
|
r = n.getRect(); |
|
// there is no way for us to support javascript properly |
|
if (url.startsWith(QLatin1String("JavaScript:")), Qt::CaseInsensitive) { |
|
continue; |
|
} else if (url.contains(QStringLiteral(":"))) { |
|
objRects.push_back(new Okular::ObjectRect(Okular::NormalizedRect(r, xScale, yScale), false, Okular::ObjectRect::Action, new Okular::BrowseAction(QUrl(url)))); |
|
} else { |
|
Okular::DocumentViewport viewport(metaData(QStringLiteral("NamedViewport"), absolutePath(m_chmUrl, url)).toString()); |
|
objRects.push_back(new Okular::ObjectRect(Okular::NormalizedRect(r, xScale, yScale), false, Okular::ObjectRect::Action, new Okular::GotoAction(QString(), viewport))); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// getting images |
|
coll = domDoc.images(); |
|
if (!coll.isNull()) { |
|
int size = coll.length(); |
|
for (int i = 0; i < size; i++) { |
|
n = coll.item(i); |
|
if (!n.isNull()) { |
|
objRects.push_back(new Okular::ObjectRect(Okular::NormalizedRect(n.getRect(), xScale, yScale), false, Okular::ObjectRect::Image, nullptr)); |
|
} |
|
} |
|
} |
|
m_request->page()->setObjectRects(objRects); |
|
m_rectsGenerated[m_request->page()->number()] = true; |
|
} |
|
|
|
if (genTextPage) { |
|
Okular::TextPage *tp = new Okular::TextPage(); |
|
recursiveExploreNodes(domDoc, tp); |
|
page->setTextPage(tp); |
|
} |
|
} |
|
} |
|
|
|
Okular::TextPage *CHMGenerator::textPage(Okular::TextRequest * request) |
|
{ |
|
userMutex()->lock(); |
|
|
|
const Okular::Page *page = request->page(); |
|
m_syncGen->view()->resize(page->width(), page->height()); |
|
|
|
preparePageForSyncOperation(m_pageUrl[page->number()]); |
|
Okular::TextPage *tp = new Okular::TextPage(); |
|
recursiveExploreNodes(m_syncGen->htmlDocument(), tp); |
|
userMutex()->unlock(); |
|
return tp; |
|
} |
|
|
|
QVariant CHMGenerator::metaData(const QString &key, const QVariant &option) const |
|
{ |
|
if (key == QLatin1String("NamedViewport") && !option.toString().isEmpty()) { |
|
const int pos = option.toString().indexOf(QLatin1Char('#')); |
|
QString tmpUrl = pos == -1 ? option.toString() : option.toString().left(pos); |
|
Okular::DocumentViewport viewport; |
|
QMap<QString, int>::const_iterator it = m_urlPage.find(tmpUrl); |
|
if (it != m_urlPage.end()) { |
|
viewport.pageNumber = it.value(); |
|
return viewport.toString(); |
|
} |
|
|
|
} else if (key == QLatin1String("DocumentTitle")) { |
|
return m_file->title(); |
|
} |
|
return QVariant(); |
|
} |
|
|
|
/* kate: replace-tabs on; tab-width 4; */ |
|
|
|
#include "generator_chm.moc"
|
|
|