#include "epub.h" #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Lettore ZIP minimale (solo STORE e DEFLATE, niente crittografia) // --------------------------------------------------------------------------- namespace { struct ZipEntry { QString name; quint16 method; quint32 compSize; quint32 uncompSize; quint32 localOffset; }; quint16 le16(const QByteArray &d, int off) { return (quint8)d.at(off) | ((quint8)d.at(off + 1) << 8); } quint32 le32(const QByteArray &d, int off) { return (quint32)(quint8)d.at(off) | ((quint32)(quint8)d.at(off + 1) << 8) | ((quint32)(quint8)d.at(off + 2) << 16) | ((quint32)(quint8)d.at(off + 3) << 24); } bool readZipEntries(const QByteArray &data, QList *entries) { // EOCD: cerca la firma PK\x05\x06 negli ultimi 64 KB + commento const int eocdMax = qMin(data.size(), 65557); int eocd = -1; for (int i = data.size() - eocdMax; i <= data.size() - 22; ++i) { if (data.at(i) == 'P' && data.at(i + 1) == 'K' && data.at(i + 2) == '\x05' && data.at(i + 3) == '\x06') { eocd = i; break; } } if (eocd < 0) return false; const quint32 cdOffset = le32(data, eocd + 16); const quint16 cdCount = le16(data, eocd + 10); int off = cdOffset; for (int i = 0; i < cdCount; ++i) { if (off + 46 > data.size() || data.at(off) != 'P' || data.at(off + 1) != 'K' || data.at(off + 2) != '\x01' || data.at(off + 3) != '\x02') return false; ZipEntry e; e.method = le16(data, off + 10); e.compSize = le32(data, off + 20); e.uncompSize = le32(data, off + 24); const quint16 nameLen = le16(data, off + 28); const quint16 extraLen = le16(data, off + 30); const quint16 commentLen = le16(data, off + 32); e.localOffset = le32(data, off + 42); e.name = QString::fromUtf8(data.mid(off + 46, nameLen)); entries->append(e); off += 46 + nameLen + extraLen + commentLen; } return true; } QByteArray extractEntryData(const QByteArray &data, const ZipEntry &e) { if ((int)e.localOffset + 30 > data.size()) return QByteArray(); const int lo = e.localOffset; if (data.at(lo) != 'P' || data.at(lo + 1) != 'K' || data.at(lo + 2) != '\x03' || data.at(lo + 3) != '\x04') return QByteArray(); const quint16 nameLen = le16(data, lo + 26); const quint16 extraLen = le16(data, lo + 28); const int dataOff = lo + 30 + nameLen + extraLen; if (dataOff + (int)e.compSize > data.size()) return QByteArray(); const QByteArray raw = data.mid(dataOff, e.compSize); if (e.method == 0) // STORE return raw; if (e.method == 8) { // DEFLATE (raw, senza header zlib) QByteArray out; out.resize(e.uncompSize); z_stream zs; std::memset(&zs, 0, sizeof(zs)); if (inflateInit2(&zs, -15) != Z_OK) return QByteArray(); zs.next_in = reinterpret_cast(const_cast(raw.constData())); zs.avail_in = raw.size(); zs.next_out = reinterpret_cast(out.data()); zs.avail_out = out.size(); const int r = inflate(&zs, Z_FINISH); inflateEnd(&zs); if (r != Z_STREAM_END) return QByteArray(); out.resize(zs.total_out); return out; } return QByteArray(); // metodo non supportato } // --------------------------------------------------------------------------- // Lunghezza del testo VISIBILE di un capitolo (criterio DOM/JS): // tag rimossi, entità decodificate (1 carattere), head/script/style/noscript/ // title esclusi, spazi e newline contati come nel text node del browser. // --------------------------------------------------------------------------- int plainTextLength(const QString &html) { // conta solo il testo dentro (come il walker JS che parte da // document.body): l'header XML/DOCTYPE e il non contano const int bodyStart = html.indexOf(QStringLiteral("= 0) ? html.mid(bodyStart, (bodyEnd >= bodyStart ? bodyEnd : html.size()) - bodyStart) : html; int count = 0; int i = 0; const int n = scope.size(); while (i < n) { const QChar c = scope.at(i); if (c == QLatin1Char('<')) { const int close = scope.indexOf(QLatin1Char('>'), i + 1); if (close < 0) { ++i; continue; } QString tag = scope.mid(i + 1, close - i - 1).trimmed().toLower(); if (tag.startsWith(QLatin1String("!--"))) { const int end = scope.indexOf(QLatin1String("-->"), close); i = (end < 0) ? n : end + 3; continue; } if (tag.startsWith(QLatin1Char('/'))) tag = tag.mid(1); int sp = 0; while (sp < tag.size() && tag.at(sp).isLetterOrNumber()) ++sp; const QString name = tag.left(sp); if (name == QLatin1String("script") || name == QLatin1String("style") || name == QLatin1String("noscript") || name == QLatin1String("head") || name == QLatin1String("title")) { // salta tutto il contenuto (e la chiusura) del blocco raw const QString closeTag = QStringLiteral("'), end); i = (gt < 0) ? n : gt + 1; continue; } i = close + 1; continue; } if (c == QLatin1Char('&')) { const int semi = scope.indexOf(QLatin1Char(';'), i + 1); if (semi > i && semi - i <= 10) { const QString ent = scope.mid(i + 1, semi - i - 1); if (ent == QLatin1String("amp") || ent == QLatin1String("lt") || ent == QLatin1String("gt") || ent == QLatin1String("quot") || ent == QLatin1String("apos") || ent == QLatin1String("nbsp") || ent.startsWith(QLatin1String("#"))) { ++count; i = semi + 1; continue; } } } ++count; ++i; } return count; } // --------------------------------------------------------------------------- // HTML -> rich text Qt (per Text QML, niente WebView): tag di base convertiti, // blocchi raw (script/style/head/...) saltati, immagini ignorate, i tag non // supportati diventano trasparenti (resta il contenuto). // --------------------------------------------------------------------------- // Entità HTML nominate NON standard XML (QXmlStreamReader le rifiuta senza DTD): // decodificate prima del parse. Quelle XML standard (amp lt gt quot apos) e le // numeriche (&#NNN; &#xHH;) le gestisce il parser. QString decodeHtmlEntities(const QString &s) { static const QHash entities = [] { QHash h; h.insert(QStringLiteral("nbsp"), QChar(0x00A0)); h.insert(QStringLiteral("mdash"), QChar(0x2014)); h.insert(QStringLiteral("ndash"), QChar(0x2013)); h.insert(QStringLiteral("rsquo"), QChar(0x2019)); h.insert(QStringLiteral("lsquo"), QChar(0x2018)); h.insert(QStringLiteral("rdquo"), QChar(0x201D)); h.insert(QStringLiteral("ldquo"), QChar(0x201C)); h.insert(QStringLiteral("sbquo"), QChar(0x201A)); h.insert(QStringLiteral("bdquo"), QChar(0x201E)); h.insert(QStringLiteral("hellip"), QChar(0x2026)); h.insert(QStringLiteral("dagger"), QChar(0x2020)); h.insert(QStringLiteral("Dagger"), QChar(0x2021)); h.insert(QStringLiteral("bull"), QChar(0x2022)); h.insert(QStringLiteral("middot"), QChar(0x00B7)); h.insert(QStringLiteral("eacute"), QChar(0x00E9)); h.insert(QStringLiteral("egrave"), QChar(0x00E8)); h.insert(QStringLiteral("agrave"), QChar(0x00E0)); h.insert(QStringLiteral("ugrave"), QChar(0x00F9)); h.insert(QStringLiteral("igrave"), QChar(0x00EC)); h.insert(QStringLiteral("ograve"), QChar(0x00F2)); h.insert(QStringLiteral("aacute"), QChar(0x00E1)); h.insert(QStringLiteral("uacute"), QChar(0x00FA)); h.insert(QStringLiteral("iacute"), QChar(0x00ED)); h.insert(QStringLiteral("oacute"), QChar(0x00F3)); h.insert(QStringLiteral("auml"), QChar(0x00E4)); h.insert(QStringLiteral("euml"), QChar(0x00EB)); h.insert(QStringLiteral("iuml"), QChar(0x00EF)); h.insert(QStringLiteral("ouml"), QChar(0x00F6)); h.insert(QStringLiteral("uuml"), QChar(0x00FC)); h.insert(QStringLiteral("szlig"), QChar(0x00DF)); h.insert(QStringLiteral("deg"), QChar(0x00B0)); h.insert(QStringLiteral("plusmn"), QChar(0x00B1)); h.insert(QStringLiteral("times"), QChar(0x00D7)); h.insert(QStringLiteral("divide"), QChar(0x00F7)); h.insert(QStringLiteral("copy"), QChar(0x00A9)); h.insert(QStringLiteral("reg"), QChar(0x00AE)); h.insert(QStringLiteral("trade"), QChar(0x2122)); h.insert(QStringLiteral("euro"), QChar(0x20AC)); h.insert(QStringLiteral("pound"), QChar(0x00A3)); h.insert(QStringLiteral("cent"), QChar(0x00A2)); h.insert(QStringLiteral("yen"), QChar(0x00A5)); h.insert(QStringLiteral("para"), QChar(0x00B6)); h.insert(QStringLiteral("sect"), QChar(0x00A7)); h.insert(QStringLiteral("laquo"), QChar(0x00AB)); h.insert(QStringLiteral("raquo"), QChar(0x00BB)); h.insert(QStringLiteral("lsaquo"), QChar(0x2039)); h.insert(QStringLiteral("rsaquo"), QChar(0x203A)); h.insert(QStringLiteral("prime"), QChar(0x2032)); h.insert(QStringLiteral("Prime"), QChar(0x2033)); h.insert(QStringLiteral("permil"), QChar(0x2030)); return h; }(); QString out; out.reserve(s.size()); int i = 0; const int n = s.size(); while (i < n) { const QChar c = s.at(i); if (c == QLatin1Char('&')) { const int semi = s.indexOf(QLatin1Char(';'), i + 1); if (semi > i && semi - i <= 12) { const QString ent = s.mid(i + 1, semi - i - 1); const QChar ch = entities.value(ent); if (!ch.isNull()) { out += ch; i = semi + 1; continue; } } } out += c; ++i; } return out; } QString htmlToRichText(const QString &html) { const int bodyStart = html.indexOf(QStringLiteral("= 0) { // includi anche la chiusura : il frammento deve essere XML valido const int end = (bodyEnd >= bodyStart) ? bodyEnd + 7 : html.size(); scope = html.mid(bodyStart, end - bodyStart); } else { scope = html; } scope = decodeHtmlEntities(scope); QString out; QStringList stack; QXmlStreamReader r(scope); while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t == QXmlStreamReader::StartElement) { const QString name = r.name().toString().toLower(); if (name == QLatin1String("script") || name == QLatin1String("style") || name == QLatin1String("noscript") || name == QLatin1String("head") || name == QLatin1String("title")) { r.skipCurrentElement(); continue; } if (name == QLatin1String("br")) { out += QStringLiteral("
"); stack.append(QString()); continue; } if (name == QLatin1String("img") || name == QLatin1String("hr") || name == QLatin1String("input")) { stack.append(QString()); continue; } QString tag; if (name == QLatin1String("b") || name == QLatin1String("strong")) tag = QStringLiteral("b"); else if (name == QLatin1String("i") || name == QLatin1String("em")) tag = QStringLiteral("i"); else if (name == QLatin1String("u")) tag = QStringLiteral("u"); else if (name == QLatin1String("s") || name == QLatin1String("strike") || name == QLatin1String("del")) tag = QStringLiteral("s"); else if (name == QLatin1String("ul")) tag = QStringLiteral("ul"); else if (name == QLatin1String("ol")) tag = QStringLiteral("ol"); else if (name == QLatin1String("li")) tag = QStringLiteral("li"); else if (name == QLatin1String("table")) tag = QStringLiteral("table border=\"1\" cellspacing=\"0\" cellpadding=\"4\""); else if (name == QLatin1String("tr")) tag = QStringLiteral("tr"); else if (name == QLatin1String("td") || name == QLatin1String("th")) tag = QStringLiteral("td"); else if (name == QLatin1String("p") || name == QLatin1String("blockquote")) tag = QStringLiteral("p"); else if (name == QLatin1String("h1") || name == QLatin1String("h2") || name == QLatin1String("h3") || name == QLatin1String("h4") || name == QLatin1String("h5") || name == QLatin1String("h6")) tag = name; // div/span/a/section/... -> trasparenti (tag vuoto) if (!tag.isEmpty()) { out += QLatin1Char('<') + tag + QLatin1Char('>'); stack.append(tag); } else { stack.append(QString()); } } else if (t == QXmlStreamReader::EndElement) { if (!stack.isEmpty()) { const QString tag = stack.takeLast(); if (!tag.isEmpty()) { const int sp = tag.indexOf(QLatin1Char(' ')); out += QStringLiteral(" 0 ? tag.left(sp) : tag) + QLatin1Char('>'); } } } else if (t == QXmlStreamReader::Characters) { out += r.text(); } } // HTML non ben formato (parse fallito): fallback grezzo — testo con
// sui newline e paragrafi sui doppi newline if (r.hasError()) { out.clear(); int i = 0; const int n = scope.size(); while (i < n) { const QChar c = scope.at(i); if (c == QLatin1Char('<')) { const int close = scope.indexOf(QLatin1Char('>'), i + 1); i = (close < 0) ? n : close + 1; continue; } if (c == QLatin1Char('&')) { const int semi = scope.indexOf(QLatin1Char(';'), i + 1); if (semi > i && semi - i <= 10) { const QString ent = scope.mid(i + 1, semi - i - 1); if (ent == QLatin1String("amp")) out += QLatin1Char('&'); else if (ent == QLatin1String("lt")) out += QLatin1Char('<'); else if (ent == QLatin1String("gt")) out += QLatin1Char('>'); else if (ent == QLatin1String("nbsp")) out += QLatin1Char(' '); else if (ent.startsWith(QLatin1String("#"))) { bool ok = false; const int cp = ent.mid(1).toInt(&ok); if (ok && cp > 0 && cp < 0x10FFFF) out += QChar(cp); } else { out += c; out += ent; out += QLatin1Char(';'); } i = semi + 1; continue; } } if (c == QLatin1Char('\n')) { // doppio newline = paragrafo, singolo = a capo if (i + 1 < n && scope.at(i + 1) == QLatin1Char('\n')) { out += QStringLiteral("

"); ++i; } else { out += QStringLiteral("
"); } ++i; continue; } out += c; ++i; } } return out; } } // namespace // --------------------------------------------------------------------------- // EpubBook // --------------------------------------------------------------------------- EpubBook::EpubBook(QObject *parent) : QObject(parent) { } EpubBook::~EpubBook() { close(); } void EpubBook::close() { if (!m_baseDir.isEmpty()) { QDir(m_baseDir).removeRecursively(); m_baseDir.clear(); } m_chapters.clear(); m_textLengths.clear(); m_title.clear(); m_author.clear(); } bool EpubBook::open(const QString &epubPath) { close(); m_opfDir.clear(); QFile f(epubPath); if (!f.open(QIODevice::ReadOnly)) return false; const QByteArray data = f.readAll(); f.close(); QList entries; if (!readZipEntries(data, &entries)) return false; m_baseDir = QDir::tempPath() + QStringLiteral("/harbour-calibreweb/books/") + QString::number(qHash(epubPath), 16); if (!QDir().mkpath(m_baseDir)) return false; // estrai tutto (skip directory e path traversal) for (const ZipEntry &e : entries) { if (e.name.endsWith(QLatin1Char('/')) || e.name.contains(QStringLiteral(".."))) continue; const QByteArray content = extractEntryData(data, e); if (content.isNull()) continue; const QString dest = m_baseDir + QLatin1Char('/') + e.name; QDir().mkpath(QFileInfo(dest).absolutePath()); QFile out(dest); if (out.open(QIODevice::WriteOnly)) { out.write(content); out.close(); } } if (!parseContainer()) return false; if (!parseOpf()) return false; const bool ok = !m_chapters.isEmpty(); if (ok) { // lunghezze testo LAZY: -1 = da calcolare al primo accesso. L'apertura // resta veloce anche per libri grossi (niente parse di tutti i capitoli). m_textLengths.clear(); m_textLengths.reserve(m_chapters.size()); for (int i = 0; i < m_chapters.size(); ++i) m_textLengths.append(-1); } emit changed(); return ok; } bool EpubBook::parseContainer() { QFile f(m_baseDir + QStringLiteral("/META-INF/container.xml")); if (!f.open(QIODevice::ReadOnly)) return false; QXmlStreamReader r(&f); while (!r.atEnd() && !r.hasError()) { if (r.readNextStartElement() && r.name() == QLatin1String("rootfile")) { const QString fullPath = r.attributes().value(QLatin1String("full-path")).toString(); if (!fullPath.isEmpty()) { // path RELATIVO alla root epub, non assoluto rispetto alla CWD m_opfDir = QFileInfo(fullPath).path(); if (m_opfDir == QStringLiteral(".")) m_opfDir.clear(); return true; } } } return false; } QString EpubBook::absolutePath(const QString &relativeToOpfDir) const { QString rel = relativeToOpfDir; rel.replace(QLatin1Char('\\'), QLatin1Char('/')); if (rel.contains(QLatin1String(".."))) return QString(); if (!m_opfDir.isEmpty()) rel = m_opfDir + QLatin1Char('/') + rel; return QDir(m_baseDir).filePath(rel); } bool EpubBook::parseOpf() { QFile f(absolutePath(QStringLiteral("content.opf"))); if (!f.exists()) f.setFileName(absolutePath(QStringLiteral("package.opf"))); if (!f.open(QIODevice::ReadOnly)) return false; QXmlStreamReader r(&f); // manifest: id -> {href, media-type, properties} QHash manifestHref; QHash manifestType; QHash manifestProps; // spine: idrefs in ordine QStringList spine; QString navItemId; QString ncxItemId; while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t != QXmlStreamReader::StartElement) continue; const QStringRef name = r.name(); const QXmlStreamAttributes a = r.attributes(); if (name == QLatin1String("title") && m_title.isEmpty()) { m_title = r.readElementText().trimmed(); } else if (name == QLatin1String("creator") && m_author.isEmpty()) { m_author = r.readElementText().trimmed(); } else if (name == QLatin1String("item")) { const QString id = a.value(QLatin1String("id")).toString(); manifestHref.insert(id, a.value(QLatin1String("href")).toString()); manifestType.insert(id, a.value(QLatin1String("media-type")).toString()); const QString props = a.value(QLatin1String("properties")).toString(); manifestProps.insert(id, props); if (props.contains(QLatin1String("nav"))) navItemId = id; if (a.value(QLatin1String("media-type")).toString() == QLatin1String("application/x-dtbncx+xml")) ncxItemId = id; } else if (name == QLatin1String("itemref")) { spine.append(a.value(QLatin1String("idref")).toString()); } } if (spine.isEmpty() || manifestHref.isEmpty()) return false; // TOC: EPUB3 nav oppure EPUB2 NCX if (!navItemId.isEmpty()) parseTocEpub3(absolutePath(manifestHref.value(navItemId))); else if (!ncxItemId.isEmpty()) parseTocEpub2(absolutePath(manifestHref.value(ncxItemId))); // mappa path assoluto -> titolo dal TOC QHash titleByPath; for (const EpubChapter &c : m_chapters) titleByPath.insert(c.path, c.title); m_chapters.clear(); // costruisci i capitoli nell'ordine dello spine int n = 0; for (const QString &idref : spine) { const QString href = manifestHref.value(idref); if (href.isEmpty()) continue; const QString abs = absolutePath(href); if (abs.isEmpty() || !QFile::exists(abs)) continue; EpubChapter c; c.id = idref; c.path = abs; c.title = titleByPath.value(abs); if (c.title.isEmpty()) c.title = QStringLiteral("Capitolo ") + QString::number(n + 1); m_chapters.append(c); ++n; } return true; } void EpubBook::parseTocEpub3(const QString &navPath) { QFile f(navPath); if (!f.open(QIODevice::ReadOnly)) return; QXmlStreamReader r(&f); bool inTocNav = false; int depth = 0; while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t == QXmlStreamReader::StartElement) { const QStringRef name = r.name(); if (name == QLatin1String("nav")) { if (!inTocNav) { const QString type = r.attributes() .value(QLatin1String("http://www.idpf.org/2007/ops"), QLatin1String("type")).toString(); if (type != QLatin1String("toc")) continue; inTocNav = true; } ++depth; continue; } if (!inTocNav || depth == 0) continue; if (name == QLatin1String("a")) { const QString href = r.attributes().value(QLatin1String("href")).toString(); const QString text = r.readElementText().trimmed(); if (!href.isEmpty() && !text.isEmpty()) { EpubChapter c; c.path = absolutePath(href); c.title = text; m_chapters.append(c); } } } else if (t == QXmlStreamReader::EndElement && r.name() == QLatin1String("nav")) { if (inTocNav && --depth <= 0) break; } } } void EpubBook::parseTocEpub2(const QString &ncxPath) { QFile f(ncxPath); if (!f.open(QIODevice::ReadOnly)) return; QXmlStreamReader r(&f); EpubChapter pending; bool havePending = false; while (!r.atEnd() && !r.hasError()) { const QXmlStreamReader::TokenType t = r.readNext(); if (t == QXmlStreamReader::StartElement) { const QStringRef name = r.name(); if (name == QLatin1String("navPoint")) { // nel NCX il (titolo) viene PRIMA del (src): // accumula in pending e chiudi alla fine del navPoint pending = EpubChapter(); havePending = true; } else if (name == QLatin1String("text")) { if (havePending) pending.title = r.readElementText().trimmed(); } else if (name == QLatin1String("content")) { const QString src = r.attributes().value(QLatin1String("src")).toString(); if (havePending) pending.path = absolutePath(src); } } else if (t == QXmlStreamReader::EndElement && r.name() == QLatin1String("navPoint")) { if (havePending && !pending.path.isEmpty()) m_chapters.append(pending); havePending = false; } } } QString EpubBook::chapterFilePath(int index) const { if (index < 0 || index >= m_chapters.size()) return QString(); return m_chapters.at(index).path; } QString EpubBook::chapterTitle(int index) const { if (index < 0 || index >= m_chapters.size()) return QString(); return m_chapters.at(index).title; } QVariantList EpubBook::chapterList() const { QVariantList list; for (int i = 0; i < m_chapters.size(); ++i) { QVariantMap m; m.insert(QStringLiteral("index"), i); m.insert(QStringLiteral("title"), m_chapters.at(i).title); list.append(m); } return list; } QString EpubBook::chapterHtml(int index, int fontSizePx, int marginPx, int marginVerticalPx) const { if (index < 0 || index >= m_chapters.size()) return QString(); QFile f(m_chapters.at(index).path); if (!f.open(QIODevice::ReadOnly)) return QString(); QString html = QString::fromUtf8(f.readAll()); // stile di reflow iniettato nel documento: !important vince sui CSS del libro. // I margini si possono aggiornare a caldo via runJavaScript (setProperty con // 'important' inline), senza ricaricare il capitolo. // height:auto + overflow:visible forzano lo scroll anche quando il CSS del // libro imposta height:100%/overflow:hidden (senza, il documento non scorre // e ogni tap salta al capitolo successivo). const QString style = QStringLiteral( "") .arg(fontSizePx).arg(marginPx).arg(marginVerticalPx); const int headEnd = html.indexOf(QStringLiteral(""), 0, Qt::CaseInsensitive); if (headEnd >= 0) html.insert(headEnd, style); else html.prepend(style); return html; } QString EpubBook::chapterBaseUrl(int index) const { if (index < 0 || index >= m_chapters.size()) return QString(); return QUrl::fromLocalFile( QFileInfo(m_chapters.at(index).path).absolutePath() + QStringLiteral("/")) .toString(); } QString EpubBook::chapterRichText(int index) const { if (index < 0 || index >= m_chapters.size()) return QString(); QFile f(m_chapters.at(index).path); if (!f.open(QIODevice::ReadOnly)) return QString(); const QString html = QString::fromUtf8(f.readAll()); return htmlToRichText(html); } // calcola (e cache) la lunghezza del capitolo index se non ancora nota void EpubBook::ensureTextLength(int index) const { if (index < 0 || index >= m_textLengths.size() || m_textLengths.at(index) >= 0) return; const EpubChapter &c = m_chapters.at(index); QFile f(c.path); if (!f.open(QIODevice::ReadOnly)) { m_textLengths[index] = 0; return; } const QString html = QString::fromUtf8(f.readAll()); m_textLengths[index] = plainTextLength(html); } int EpubBook::chapterTextLength(int index) const { if (index < 0 || index >= m_textLengths.size()) return 0; ensureTextLength(index); return m_textLengths.at(index); } qint64 EpubBook::chapterStartChar(int index) const { qint64 start = 0; const int end = qMin(index, m_textLengths.size()); for (int i = 0; i < end; ++i) { ensureTextLength(i); start += m_textLengths.at(i); } return start; } QString EpubBook::makeBookmarkKey(int chapterIndex, qint64 charOffset) const { const qint64 abs = chapterStartChar(chapterIndex) + qMax(0, charOffset); return QStringLiteral("cw:v2:%1").arg(abs); } QVariantMap EpubBook::parseBookmarkKey(const QString &key) const { QVariantMap result; const QStringList parts = key.split(QLatin1Char(':')); if (parts.size() < 3 || parts.at(0) != QLatin1String("cw")) return result; if (parts.at(1) == QLatin1String("v2")) { bool ok = false; const qint64 abs = parts.at(2).toLongLong(&ok); if (!ok || abs < 0) return result; // trova il capitolo che contiene il carattere assoluto int ch = 0; qint64 start = 0; for (int i = 0; i < m_textLengths.size(); ++i) { if (start + m_textLengths.at(i) > abs) { ch = i; break; } start += m_textLengths.at(i); ch = i + 1; } if (ch >= m_chapters.size()) ch = qMax(0, m_chapters.size() - 1); result.insert(QStringLiteral("chapter"), ch); result.insert(QStringLiteral("charOffset"), qMax(0, abs - chapterStartChar(ch))); return result; } // v1 (vecchie chiavi, migrazione): "cw:v1::" if (parts.at(1) == QLatin1String("v1") && parts.size() == 4) { bool okC = false, okF = false; const int ch = parts.at(2).toInt(&okC); const double fr = parts.at(3).toDouble(&okF); if (!okC || !okF) return result; result.insert(QStringLiteral("chapter"), qMax(0, ch)); result.insert(QStringLiteral("charOffset"), qRound64(fr * chapterTextLength(ch))); return result; } return result; }