1 /*
2 This file is part of kde-thumbnailer-epub
3 Copyright (C) 2012-2017 Giacomo Barazzetti <giacomosrv@gmail.com>
4 
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "epub.h"
20 
21 #include <algorithm>
22 #include <functional>
23 
24 #include <QtCore/QDebug>
25 #include <QtCore/QDir>
26 #include <QtGui/QImage>
27 
28 const QStringList imageExt({"jpg", "jpeg", "png", "gif", "bmp"});
29 const QStringList xmlExt({"xhtml", "xhtm", "html", "htm", "xml"});
30 bool endsWith (const QString &coverUrl, const QStringList &extensions);
31 
Epub(const QString & path)32 Epub::Epub(const QString &path)
33     : mZip(path)
34 {
35     qDebug() << "[epub thumb] > Opening" << path;
36 }
37 
~Epub()38 Epub::~Epub()
39 {
40     mZip.close();
41 }
42 
open()43 bool Epub::open()
44 {
45     if (mZip.open(QIODevice::ReadOnly)) {
46         qDebug() << "[epub thumb] Retrieving files list...";
47         loadFilesList(mZip.directory());
48         mFilesList.sort(Qt::CaseSensitive);
49 
50         if (mFilesList.isEmpty()) {
51             qDebug() << "[epub thumb] > No files found.";
52             return false;
53         }
54 
55         if (!loadOpf()) {
56             qDebug() << "[epub thumb] > No OPF file found.";
57             return false;
58         }
59 
60         return true;
61     }
62 
63     qDebug() << "[epub thumb] > Couldn't open";
64     return false;
65 }
66 
getCoverImage(QImage & coverImage)67 bool Epub::getCoverImage(QImage &coverImage)
68 {
69     std::function<QString()> strategies[4] = {
70         std::bind(&Epub::getRefFromMetadata, this),
71         std::bind(&Epub::getRefFromGuide, this),
72         std::bind(&Epub::getRefFromSpine, this),
73         std::bind(&Epub::getRefByName, this)
74     };
75 
76     for (auto strategy : strategies) {
77         QString ref = strategy();
78 
79         if (!ref.isEmpty()) {
80             bool coverFound = false;
81 
82             if (isImage(ref)) {
83                 coverFound = true;
84             } else {
85                 QString tRef = getRefFromXhtm(ref);
86                 if (!tRef.isEmpty()) {
87                     ref = tRef;
88                     coverFound = true;
89                 }
90             }
91 
92             if (coverFound) {
93                 qDebug() << "[epub thumb] Cover url:" << ref;
94 
95                 if (coverImage.loadFromData(getFile(ref))) {
96                     qDebug() << "[epub thumb] > Generated.";
97                     return true;
98                 }
99             }
100         }
101     }
102 
103     qDebug() << "[epub thumb] > No cover found.";
104     return false;
105 }
106 
107 //-------------------------------------
108 
loadFilesList(const KArchiveDirectory * dir,const QString & subPath)109 void Epub::loadFilesList(const KArchiveDirectory *dir, const QString &subPath)
110 {
111     for (const QString &fileName : dir->entries()) {
112          const KArchiveEntry *entry = dir->entry(fileName);
113          const QString tUrl = QDir(subPath).filePath(fileName);
114 
115          if (entry->isFile()) {
116              if (fileName.endsWith("opf")) {
117                  mOpfUrl = tUrl;
118              } else if (endsWith(fileName, imageExt) || endsWith(fileName, xmlExt)) {
119                  mFilesList.append(tUrl);
120              }
121          } else {
122              loadFilesList(static_cast<const KArchiveDirectory*>(entry), tUrl);
123          }
124     }
125 }
126 
loadOpf()127 bool Epub::loadOpf()
128 {
129     if (!mOpfUrl.isEmpty()) {
130         mOpf = QString(getFile(mOpfUrl));
131         mOpfQuery.setFocus(mOpf);
132     }
133 
134     return !mOpf.isEmpty();
135 }
136 
getRefFromMetadata()137 QString Epub::getRefFromMetadata()
138 {
139     qDebug() << "[epub thumb] Searching for id in metadata...";
140 
141     const QString id = getAttrFromOpf("/package/metadata/meta[@name='cover']/@content/string()", "http://www.idpf.org/2007/opf");
142 
143     if (!id.isEmpty()) {
144         qDebug() << "[epub thumb] Id:" << id;
145         return getAbsoluteUrl(getRefById(id));
146     }
147 
148     qDebug() << "[epub thumb] Id not found/invalid.";
149     return QString();
150 }
151 
getRefFromGuide()152 QString Epub::getRefFromGuide()
153 {
154     qDebug() << "[epub thumb] Searching for ref in guide...";
155 
156     const QString ref = getAttrFromOpf("/package/guide/reference[@type='cover' or @title='cover']/@href/string()", "http://www.idpf.org/2007/opf");
157 
158     if (!ref.isEmpty()) {
159         qDebug() << "[epub thumb] Ref:" << ref;
160         return getAbsoluteUrl(ref);
161     }
162 
163     qDebug() << "[epub thumb] Ref not found/invalid.";
164     return QString();
165 }
166 
getRefFromSpine()167 QString Epub::getRefFromSpine()
168 {
169     qDebug() << "[epub thumb] Searching for first xhtm in spine...";
170 
171     const QString id = getAttrFromOpf("/package/spine/itemref[1]/@idref/string()", "http://www.idpf.org/2007/opf");
172 
173     if (!id.isEmpty()) {
174         qDebug() << "[epub thumb] Id:" << id;
175         return getAbsoluteUrl(getRefById(id));
176     }
177 
178     qDebug() << "[epub thumb] Id not found/invalid.";
179     return QString();
180 }
181 
getRefByName() const182 QString Epub::getRefByName() const
183 {
184     qDebug() << "[epub thumb] Searching for cover by generic filename...";
185     return getAbsoluteUrl("cover");
186 }
187 
getRefById(const QString & coverId)188 QString Epub::getRefById(const QString &coverId)
189 {
190     qDebug() << "[epub thumb] Searching for ref in manifest...";
191 
192     //CHECK: query.bindVariable("myId", QXmlItem(QVariant(coverId)));
193     const QString query = "/package/manifest/item[@id='" + coverId + "']/@href/string()";
194     const QString ref = getAttrFromOpf(query, "http://www.idpf.org/2007/opf");
195 
196     if (ref.isEmpty()) {
197         qDebug() << "[epub thumb] No ref found.";
198     } else {
199         qDebug() << "[epub thumb] Ref:" << ref;
200     }
201 
202     return ref;
203 }
204 
getAbsoluteUrl(const QString & url) const205 QString Epub::getAbsoluteUrl(const QString &url) const
206 {
207     QString fileUrl;
208 
209     for (const QString &fileName : mFilesList) {
210         if (fileName.contains(url, Qt::CaseInsensitive)) {
211             fileUrl = fileName;
212             break;
213         }
214     }
215 
216     return fileUrl;
217 }
218 
isImage(const QString & ref) const219 bool Epub::isImage(const QString &ref) const
220 {
221     if (endsWith(ref, imageExt)) {
222         return true;
223     }
224 
225     return false;
226 }
227 
getRefFromXhtm(const QString & xhtmUrl) const228 QString Epub::getRefFromXhtm(const QString &xhtmUrl) const
229 {
230     QXmlQuery xHtmlQuery;
231     xHtmlQuery.setFocus(getFile(xhtmUrl));
232     xHtmlQuery.setQuery("(//*:image[1]/@*:href | //*:img[1]/@src)/string()");
233 
234     QString ref;
235     xHtmlQuery.evaluateTo(&ref);
236     ref = ref.remove('\n');
237 
238     // fixes relative path
239     if (ref.startsWith("../")) {
240         ref = ref.remove(0, 3);
241     }
242 
243     return getAbsoluteUrl(ref);
244 }
245 
getAttrFromOpf(const QString & query,const QString & namesp)246 QString Epub::getAttrFromOpf(const QString &query, const QString &namesp)
247 {
248     const QString tNamesp("declare default element namespace \"" + namesp + "\";");
249     mOpfQuery.setQuery(tNamesp + query);
250 
251     QString attribute;
252     mOpfQuery.evaluateTo(&attribute);
253     attribute = attribute.remove('\n');
254 
255     return attribute;
256 }
257 
getFile(const QString & fileName) const258 QByteArray Epub::getFile(const QString &fileName) const
259 {
260     const KArchiveDirectory *dir = mZip.directory();
261     const KZipFileEntry *file = static_cast<const KZipFileEntry*>(dir->entry(fileName));
262 
263     return file->data();
264 }
265 
266 //--------------------
267 
endsWith(const QString & coverUrl,const QStringList & extensions)268 bool endsWith (const QString &coverUrl, const QStringList &extensions)
269 {
270     return std::any_of(extensions.begin(), extensions.end(), [&coverUrl](const QString& ext) -> bool {
271         return coverUrl.endsWith("." + ext, Qt::CaseInsensitive);
272     });
273 }
274