/* * scribiu -- read notebooks and voice memos from Livescribe pens * Copyright (C) 2015 Javier S. Pedro * * 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 . */ #include #include #include #include "xmlutils.h" #include "smartpen.h" #include "afdnotebook.h" AfdNotebook::AfdNotebook(QObject *parent) : QObject(parent) { } AfdNotebook::~AfdNotebook() { } bool AfdNotebook::open(const QString &path) { _dir.setPath(path); if (!_dir.exists()) { qWarning() << "Directory" << _dir.absolutePath() << "does not exist"; return false; } if (!parseMainInfo()) { return false; } if (!parseMainDocument()) { return false; } if (!findPenData()) { return false; } return true; } void AfdNotebook::close() { _title.clear(); _lastPage = _firstPage = AfdPageAddress(); _pagesPerBook = 0; _gfx.clear(); _pages.clear(); _penData.clear(); _dir.setPath(QString()); } QString AfdNotebook::title() const { return _title; } quint64 AfdNotebook::guid() const { return _guid; } int AfdNotebook::numPages() const { return _pages.size(); } AfdPageAddress AfdNotebook::getPageAddress(int pageNum) const { if (pageNum < 0 || pageNum >= _pages.size()) { qWarning() << "Invalid page number:" << pageNum << "for notebook" << _title; } uint new_page = _firstPage.page() + pageNum; return AfdPageAddress(_firstPage.section(), _firstPage.segment(), _firstPage.shelf(), _firstPage.book() + new_page / _pagesPerBook, new_page % _pagesPerBook); } int AfdNotebook::getPageNumber(const AfdPageAddress &addr) const { if (addr.section() == _firstPage.section() && addr.segment() == _firstPage.segment() && addr.shelf() == _firstPage.shelf()) { long firstPage = (_firstPage.book() * _pagesPerBook) + _firstPage.page(); long page = (addr.book() * _pagesPerBook) + addr.page() - firstPage; if (page >= 0 && page < _pages.size()) { return page; } } qWarning() << "Invalid address for notebook" << _title; return -1; } QString AfdNotebook::getPageBackgroundName(int page) const { const Page& p = _pages.at(page); return p.gfx->basename; } QPixmap AfdNotebook::getPageBackground(int page) { const Page& p = _pages.at(page); QPixmap pix; if (QPixmapCache::find(p.gfx->basename, &pix)) { return pix; } const QString file = QString("userdata/lsac_data/%1.png").arg(p.gfx->basename); QImage img; if (!img.load(_dir.filePath(file), "PNG")) { qWarning() << "Could not load background file:" << _dir.absoluteFilePath(file); return pix; } QRect cropRect = img.rect(); QRect trim = getPageTrim(page); QPointF scale(cropRect.width() / double(p.size.width()), cropRect.height() / double(p.size.height())); cropRect.adjust(lround(trim.x() * scale.x()), lround(trim.y() * scale.y()), lround(-(p.size.width() - trim.bottomRight().x()) * scale.x()), lround(-(p.size.height() - trim.bottomRight().y()) * scale.y())); qDebug() << "Cropping image from" << img.rect() << "to" << cropRect; pix = QPixmap::fromImage(img.copy(cropRect)); QPixmapCache::insert(p.gfx->basename, pix); return pix; } QSize AfdNotebook::getPageSize(int page) const { const Page &p = _pages.at(page); return p.size; } QRect AfdNotebook::getPageTrim(int page) const { const Page &p = _pages.at(page); QRect trim(QPoint(0, 0), p.size); if (p.size.width() > SMARTPEN_BLEED_X * 2 && p.size.height() > SMARTPEN_BLEED_Y * 2) { trim.adjust(SMARTPEN_BLEED_X, SMARTPEN_BLEED_Y, -SMARTPEN_BLEED_X, -SMARTPEN_BLEED_Y); } return trim; } QStringList AfdNotebook::penSerials() const { return _penData.keys(); } QList AfdNotebook::pagesWithStrokes(const QString &penSerial) const { if (_penData.contains(penSerial)) { const PenData &data = _penData[penSerial]; return data.strokes.uniqueKeys(); } else { return QList(); } } QStringList AfdNotebook::strokeFiles(const QString &penSerial, int page) const { QStringList l; if (!_penData.contains(penSerial)) return l; const PenData &data = _penData[penSerial]; QMultiMap::const_iterator it = data.strokes.find(page); while (it != data.strokes.end() && it.key() == page) { const StrokeData &stroke = it.value(); l.append(_dir.filePath(stroke.file)); ++it; } return l; } bool AfdNotebook::readStrokes(const QString &penSerial, int page, StfReader::StrokeHandler *handler) { if (!_penData.contains(penSerial)) return true; StfReader stf; stf.setStrokeHandler(handler); const PenData &data = _penData[penSerial]; QMultiMap::const_iterator it = data.strokes.find(page); while (it != data.strokes.end() && it.key() == page) { const StrokeData &stroke = it.value(); qDebug() << "Reading strokes from" << stroke.file; if (!stf.parse(_dir.filePath(stroke.file))) { qWarning() << "Could not parse stroke file" << stroke.file; return false; } ++it; } return true; } QMap AfdNotebook::parsePropertyList(QIODevice *dev) { QMap result; QByteArray line; while (!(line = dev->readLine()).isEmpty()) { int sep = line.indexOf(':'); QString key = QString::fromLatin1(line.constData(), sep); result[key] = QString::fromUtf8(line.constData() + sep + 1).trimmed(); } return result; } QMap AfdNotebook::parsePropertyList(const QString &relativePath) const { QFile f(_dir.filePath(relativePath)); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { return QMap(); } return parsePropertyList(&f); } bool AfdNotebook::parseMainInfo() { QMap info = parsePropertyList("main.info"); if (info.isEmpty()) { qWarning() << "Empty main.info"; return false; } _title = info["title"]; _guid = info["guid"].mid(3).toULongLong(0, 16); _firstPage = AfdPageAddress(info["pagestart"]); _lastPage = AfdPageAddress(info["pagestop"]); _pagesPerBook = info.value("segment-pages-per-book", "108").toUInt(); return true; } bool AfdNotebook::parseMainDocument() { QFile f(_dir.filePath("main.document")); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } QXmlStreamReader r(&f); Q_ASSERT(_pages.isEmpty()); Q_ASSERT(_gfx.isEmpty()); advanceToFirstChildElement(r, "document"); while (r.readNextStartElement()) { if (r.name() == "page") { QXmlStreamAttributes attrs = r.attributes(); QString gfxfile; gfxfile += attrs.value("basepath"); gfxfile += '/'; gfxfile += attrs.value("gfxfile_ref"); Page p; p.gfx = &_gfx[gfxfile]; p.size.setWidth(attrs.value("width").toString().toInt()); p.size.setHeight(attrs.value("height").toString().toInt()); _pages.append(p); r.skipCurrentElement(); } else { qWarning() << "Ignoring unknown element" << r.name() << "in main.document"; } } if (_pages.isEmpty()) { qWarning() << "Notebook has no pages"; return false; } foreach (const QString &gfxfile, _gfx.keys()) { if (!parseGfx(gfxfile)) { return false; } } return true; } bool AfdNotebook::parseGfx(const QString &file) { QFile f(_dir.filePath(file)); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } QXmlStreamReader r(&f); Gfx &gfx = _gfx[file]; Q_ASSERT(gfx.basename.isEmpty()); advanceToFirstChildElement(r, "graphics"); advanceToFirstChildElement(r, "setbase"); advanceToFirstChildElement(r, "image"); if (!r.atEnd()) { QXmlStreamAttributes attrs = r.attributes(); QString imageSrc = attrs.value("src").toString(); qDebug() << "image src" << imageSrc; int lastSlash = imageSrc.lastIndexOf('/'); int lastDot = imageSrc.lastIndexOf('.'); if (lastSlash >= 0 && lastDot > lastSlash) { gfx.basename = imageSrc.mid(lastSlash + 1, lastDot - lastSlash - 1); qDebug() << "Got gfx" << gfx.basename; } } return true; } bool AfdNotebook::findPenData() { QDir dir(_dir.filePath("data")); if (!dir.exists()) { return false; } QStringList pageDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); foreach (QString pageName, pageDirs) { pageName.remove('/'); qDebug() << " page data" << pageName; int pageNum = getPageNumber(AfdPageAddress(pageName)); if (pageNum < 0) continue; QDir pageDir(dir.filePath(pageName)); if (!pageDir.exists()) continue; QStringList penDirs = pageDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); foreach (QString penName, penDirs) { penName.remove('/'); qDebug() << " pen data" << penName; QDir penDir(pageDir.filePath(penName)); if (!penDir.exists()) continue; QStringList strokeFiles = penDir.entryList(QStringList("*.stf"), QDir::Files); foreach (const QString &strokeFile, strokeFiles) { qDebug() << " stroke data" << strokeFile; if (strokeFile.length() != 25) { qWarning() << "Invalid stroke filename format" << strokeFile << endl; continue; } StrokeData stroke; stroke.file = penDir.filePath(strokeFile); bool ok = true; if (ok) stroke.begin = Smartpen::fromPenTime(strokeFile.mid(2, 8).toLongLong(&ok, 16) * 1000ULL); if (ok) stroke.end = Smartpen::fromPenTime(strokeFile.mid(13, 8).toLongLong(&ok, 16) * 1000ULL); if (!ok) { qWarning() << "Invalid stroke filename format" << strokeFile << endl; continue; } qDebug() << " from" << stroke.begin << "to" << stroke.end; _penData[penName].strokes.insert(pageNum, stroke); } } } return true; }