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.
380 lines
8.7 KiB
380 lines
8.7 KiB
/* |
|
* Kchmviewer - a CHM and EPUB file viewer with broad language support |
|
* Copyright (C) 2004-2014 George Yunaev, gyunaev@ulduzsoft.com |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation, either version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
#if defined (WIN32) |
|
#include <io.h> // dup |
|
#else |
|
#include <unistd.h> |
|
#endif |
|
|
|
#include <QMessageBox> |
|
#include <QXmlSimpleReader> |
|
|
|
#include "ebook_epub.h" |
|
#include "helperxmlhandler_epubcontainer.h" |
|
#include "helperxmlhandler_epubcontent.h" |
|
#include "helperxmlhandler_epubtoc.h" |
|
|
|
static const char * URL_SCHEME_EPUB = "epub"; |
|
|
|
EBook_EPUB::EBook_EPUB() |
|
: EBook() |
|
{ |
|
m_zipFile = 0; |
|
} |
|
|
|
EBook_EPUB::~EBook_EPUB() |
|
{ |
|
close(); |
|
} |
|
|
|
bool EBook_EPUB::load(const QString &archiveName) |
|
{ |
|
close(); |
|
|
|
// We use QFile and zip_fdopen instead of zip_open because latter does not support Unicode file names |
|
m_epubFile.setFileName( archiveName ); |
|
|
|
if ( !m_epubFile.open( QIODevice::ReadOnly ) ) |
|
{ |
|
qWarning("Could not open file %s: %s", qPrintable(archiveName), qPrintable( m_epubFile.errorString())); |
|
return false; |
|
} |
|
|
|
// Open the ZIP archive: http://www.nih.at/libzip/zip_fdopen.html |
|
// Note that zip_fdopen takes control over the passed descriptor, |
|
// so we need to pass a duplicate of it for this to work correctly |
|
int fdcopy = dup( m_epubFile.handle() ); |
|
|
|
if ( fdcopy < 0 ) |
|
{ |
|
qWarning("Could not duplicate descriptor" ); |
|
return false; |
|
} |
|
|
|
int errcode; |
|
m_zipFile = zip_fdopen( fdcopy, 0, &errcode ); |
|
|
|
if ( !m_zipFile ) |
|
{ |
|
qWarning("Could not open file %s: error %d", qPrintable(archiveName), errcode); |
|
return false; |
|
} |
|
|
|
// Parse the book descriptor file |
|
if ( !parseBookinfo() ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
void EBook_EPUB::close() |
|
{ |
|
if ( m_zipFile ) |
|
{ |
|
zip_close( m_zipFile ); |
|
m_zipFile = 0; |
|
} |
|
|
|
//if ( m_epubFile.isOpen() ) |
|
// m_epubFile.close(); |
|
|
|
|
|
} |
|
|
|
bool EBook_EPUB::getFileContentAsString(QString &str, const QUrl &url) const |
|
{ |
|
return getFileAsString( str, urlToPath( url ) ); |
|
} |
|
|
|
bool EBook_EPUB::getFileContentAsBinary(QByteArray &data, const QUrl &url) const |
|
{ |
|
return getFileAsBinary( data, urlToPath( url ) ); |
|
} |
|
|
|
bool EBook_EPUB::enumerateFiles(QList<QUrl> &files) |
|
{ |
|
files = m_ebookManifest; |
|
return true; |
|
} |
|
|
|
QString EBook_EPUB::title() const |
|
{ |
|
return m_title; |
|
} |
|
|
|
QUrl EBook_EPUB::homeUrl() const |
|
{ |
|
return m_tocEntries[0].url; |
|
} |
|
|
|
bool EBook_EPUB::hasFeature(EBook::Feature code) const |
|
{ |
|
switch ( code ) |
|
{ |
|
case FEATURE_TOC: |
|
return true; |
|
|
|
case FEATURE_INDEX: |
|
return false; |
|
|
|
case FEATURE_ENCODING: |
|
return false; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
bool EBook_EPUB::getTableOfContents( QList<EBookTocEntry> &toc ) const |
|
{ |
|
toc = m_tocEntries; |
|
return true; |
|
} |
|
|
|
bool EBook_EPUB::getIndex(QList<EBookIndexEntry> &) const |
|
{ |
|
return false; |
|
} |
|
|
|
QString EBook_EPUB::getTopicByUrl(const QUrl& url) |
|
{ |
|
if ( m_urlTitleMap.contains( url ) ) |
|
return m_urlTitleMap[ url ]; |
|
|
|
return ""; |
|
} |
|
|
|
QString EBook_EPUB::currentEncoding() const |
|
{ |
|
return "UTF-8"; |
|
} |
|
|
|
bool EBook_EPUB::setCurrentEncoding(const char *) |
|
{ |
|
abort(); |
|
} |
|
|
|
bool EBook_EPUB::isSupportedUrl(const QUrl &url) |
|
{ |
|
return url.scheme() == URL_SCHEME_EPUB; |
|
} |
|
|
|
bool EBook_EPUB::parseXML(const QString &uri, QXmlDefaultHandler * parser) |
|
{ |
|
QByteArray container; |
|
|
|
if ( !getFileAsBinary( container, uri ) ) |
|
{ |
|
qDebug("Failed to retrieve XML file %s", qPrintable( uri ) ); |
|
return false; |
|
} |
|
|
|
// Use it as XML source |
|
QXmlInputSource source; |
|
source.setData( container ); |
|
|
|
// Init the reader |
|
QXmlSimpleReader reader; |
|
reader.setContentHandler( parser ); |
|
reader.setErrorHandler( parser ); |
|
|
|
return reader.parse( source ); |
|
} |
|
|
|
bool EBook_EPUB::parseBookinfo() |
|
{ |
|
// Parse the container.xml to find the content descriptor |
|
HelperXmlHandler_EpubContainer container_parser; |
|
|
|
if ( !parseXML( "META-INF/container.xml", &container_parser ) |
|
|| container_parser.contentPath.isEmpty() ) |
|
return false; |
|
|
|
// Parse the content.opf |
|
HelperXmlHandler_EpubContent content_parser; |
|
|
|
if ( !parseXML( container_parser.contentPath, &content_parser ) ) |
|
return false; |
|
|
|
// At least title and the TOC must be present |
|
if ( !content_parser.metadata.contains("title") || content_parser.tocname.isEmpty() ) |
|
return false; |
|
|
|
// All the files, including TOC, are relative to the container_parser.contentPath |
|
m_documentRoot.clear(); |
|
int sep = container_parser.contentPath.lastIndexOf( '/' ); |
|
|
|
if ( sep != -1 ) |
|
m_documentRoot = container_parser.contentPath.left( sep + 1 ); // Keep the trailing slash |
|
|
|
// Parse the TOC |
|
HelperXmlHandler_EpubTOC toc_parser( this ); |
|
|
|
if ( !parseXML( content_parser.tocname, &toc_parser ) ) |
|
return false; |
|
|
|
// Get the data |
|
m_title = content_parser.metadata[ "title" ]; |
|
|
|
// Move the manifest entries into the list |
|
Q_FOREACH( QString f, content_parser.manifest.values() ) |
|
m_ebookManifest.push_back( pathToUrl( f ) ); |
|
|
|
// Copy the manifest information and fill up the other maps if we have it |
|
if ( !toc_parser.entries.isEmpty() ) |
|
{ |
|
Q_FOREACH( EBookTocEntry e, toc_parser.entries ) |
|
{ |
|
// Add into url-title map |
|
m_urlTitleMap[ e.url ] = e.name; |
|
m_tocEntries.push_back( e ); |
|
} |
|
} |
|
else |
|
{ |
|
// Copy them from spline |
|
Q_FOREACH( QString u, content_parser.spine ) |
|
{ |
|
EBookTocEntry e; |
|
QString url = u; |
|
|
|
if ( content_parser.manifest.contains( u ) ) |
|
url = content_parser.manifest[ u ]; |
|
|
|
e.name = url; |
|
e.url= pathToUrl( url ); |
|
e.iconid = EBookTocEntry::IMAGE_NONE; |
|
e.indent = 0; |
|
|
|
// Add into url-title map |
|
m_urlTitleMap[ pathToUrl( url ) ] = url; |
|
m_tocEntries.push_back( e ); |
|
} |
|
} |
|
|
|
// EPub with an empty TOC is not valid |
|
if ( m_tocEntries.isEmpty() ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
QUrl EBook_EPUB::pathToUrl(const QString &link) const |
|
{ |
|
QUrl url; |
|
url.setScheme( URL_SCHEME_EPUB ); |
|
url.setHost( URL_SCHEME_EPUB ); |
|
|
|
// Does the link contain the fragment as well? |
|
int off = link.indexOf( '#' ); |
|
QString path; |
|
|
|
if ( off != -1 ) |
|
{ |
|
path = link.left( off ); |
|
url.setFragment( link.mid( off + 1 ) ); |
|
} |
|
else |
|
path = link; |
|
|
|
if ( !path.startsWith( '/' ) ) |
|
path.prepend( '/' ); |
|
|
|
url.setPath( QUrl::fromPercentEncoding( path.toUtf8() ) ); |
|
|
|
return url; |
|
} |
|
|
|
QString EBook_EPUB::urlToPath(const QUrl &link) const |
|
{ |
|
if ( link.scheme() == URL_SCHEME_EPUB ) |
|
return link.path(); |
|
|
|
return ""; |
|
} |
|
|
|
bool EBook_EPUB::getFileAsString(QString &str, const QString &path) const |
|
{ |
|
QByteArray data; |
|
|
|
if ( !getFileAsBinary( data, path ) ) |
|
return false; |
|
|
|
// I have never seen yet an UTF16 epub |
|
if ( data.startsWith("<?xml" ) ) |
|
{ |
|
int endxmltag = data.indexOf( "?>" ); |
|
int utf16 = data.indexOf("UTF-16"); |
|
|
|
if ( utf16 > 0 && utf16 < endxmltag ) |
|
{ |
|
QMessageBox::critical( 0, |
|
("Unsupported encoding"), |
|
("The encoding of this ebook is not supported yet. Please send it to gyunaev@ulduzsoft.com for support to be added") ); |
|
return false; |
|
} |
|
} |
|
|
|
str = QString::fromUtf8( data ); |
|
return true; |
|
} |
|
|
|
bool EBook_EPUB::getFileAsBinary(QByteArray &data, const QString &path) const |
|
{ |
|
// Retrieve the file size |
|
struct zip_stat fileinfo; |
|
QString completeUrl; |
|
|
|
if ( !path.isEmpty() && path[0] == '/' ) |
|
completeUrl = m_documentRoot + path.mid( 1 ); |
|
else |
|
completeUrl = m_documentRoot + path; |
|
|
|
//qDebug("URL requested: %s (%s)", qPrintable(path), qPrintable(completeUrl)); |
|
|
|
// http://www.nih.at/libzip/zip_stat.html |
|
if ( zip_stat( m_zipFile, completeUrl.toUtf8().constData(), 0, &fileinfo) != 0 ) |
|
{ |
|
qDebug("File %s is not found in the archive", qPrintable(completeUrl)); |
|
return false; |
|
} |
|
|
|
// Make sure the size field is valid |
|
if ( (fileinfo.valid & ZIP_STAT_SIZE) == 0 || (fileinfo.valid & ZIP_STAT_INDEX) == 0 ) |
|
return false; |
|
|
|
// Open the file |
|
struct zip_file * file = zip_fopen_index( m_zipFile, fileinfo.index, 0 ); |
|
|
|
if ( !file ) |
|
return false; |
|
|
|
// Allocate the memory and read the file |
|
data.resize( fileinfo.size ); |
|
|
|
// Could it return a positive number but not fileinfo.size??? |
|
int ret = zip_fread( file, data.data(), fileinfo.size ); |
|
if ( ret != (int) fileinfo.size ) |
|
{ |
|
zip_fclose( file ); |
|
return false; |
|
} |
|
|
|
zip_fclose( file ); |
|
return true; |
|
}
|
|
|