From a69e97943539a8abc4d2762638c169dc19c88516 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 7 Jun 2015 21:22:45 +0200 Subject: initial import --- .gitignore | 1 + afdnotebook.cc | 399 +++++++++++++++++++++++++++++++++++++++++++++++++ afdnotebook.h | 119 +++++++++++++++ bitreader.cc | 45 ++++++ bitreader.h | 25 ++++ main.cc | 11 ++ mainwindow.cc | 66 +++++++++ mainwindow.h | 37 +++++ mainwindow.ui | 354 ++++++++++++++++++++++++++++++++++++++++++++ notebookmodel.cc | 323 ++++++++++++++++++++++++++++++++++++++++ notebookmodel.h | 49 +++++++ notebookview.cc | 194 ++++++++++++++++++++++++ notebookview.h | 59 ++++++++ pageitem.cc | 64 ++++++++ pageitem.h | 32 ++++ scribiu.pro | 35 +++++ smartpen.cc | 424 +++++++++++++++++++++++++++++++++++++++++++++++++++++ smartpen.h | 78 ++++++++++ smartpenmanager.cc | 69 +++++++++ smartpenmanager.h | 42 ++++++ smartpensyncer.cc | 214 +++++++++++++++++++++++++++ smartpensyncer.h | 39 +++++ stfgraphicsitem.cc | 67 +++++++++ stfgraphicsitem.h | 20 +++ stfreader.cc | 336 ++++++++++++++++++++++++++++++++++++++++++ stfreader.h | 51 +++++++ xmlutils.cc | 13 ++ xmlutils.h | 8 + 28 files changed, 3174 insertions(+) create mode 100644 .gitignore create mode 100644 afdnotebook.cc create mode 100644 afdnotebook.h create mode 100644 bitreader.cc create mode 100644 bitreader.h create mode 100644 main.cc create mode 100644 mainwindow.cc create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100644 notebookmodel.cc create mode 100644 notebookmodel.h create mode 100644 notebookview.cc create mode 100644 notebookview.h create mode 100644 pageitem.cc create mode 100644 pageitem.h create mode 100644 scribiu.pro create mode 100644 smartpen.cc create mode 100644 smartpen.h create mode 100644 smartpenmanager.cc create mode 100644 smartpenmanager.h create mode 100644 smartpensyncer.cc create mode 100644 smartpensyncer.h create mode 100644 stfgraphicsitem.cc create mode 100644 stfgraphicsitem.h create mode 100644 stfreader.cc create mode 100644 stfreader.h create mode 100644 xmlutils.cc create mode 100644 xmlutils.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75c107b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pro.user diff --git a/afdnotebook.cc b/afdnotebook.cc new file mode 100644 index 0000000..38f1fdc --- /dev/null +++ b/afdnotebook.cc @@ -0,0 +1,399 @@ +#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 = PageAddress(0, 0, 0, 0, 0); + _pagesPerBook = 0; + _gfx.clear(); + _pages.clear(); + _penData.clear(); + _dir.setPath(QString()); +} + +QString AfdNotebook::title() const +{ + return _title; +} + +int AfdNotebook::numPages() const +{ + return _pages.size(); +} + +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; +} + +AfdNotebook::PageAddress::PageAddress(const QString &str) +{ + QStringList parts = str.split('.'); + if (parts.count() == 5) { + series = parts[0].toUInt(); + shelf = parts[1].toUInt(); + segment = parts[2].toUInt(); + book = parts[3].toUInt(); + page = parts[4].toUInt(); + } else if (parts.count() == 4) { + series = 0; + shelf = parts[0].toUInt(); + segment = parts[1].toUInt(); + book = parts[2].toUInt(); + page = parts[3].toUInt(); + } else { + qWarning() << "Unknown page address syntax:" << str; + } +} + +QString AfdNotebook::PageAddress::toString() const +{ + QStringList l; + l.reserve(5); + + if (series) { + l.append(QString::number(series)); + } + + l.append(QString::number(shelf)); + l.append(QString::number(segment)); + l.append(QString::number(book)); + l.append(QString::number(page)); + + return l.join("."); +} + +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"]; + _firstPage = PageAddress(info["pagestart"]); + _lastPage = PageAddress(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(PageAddress(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; + + qDebug() << " " << strokeFile.mid(2, 8) << strokeFile.mid(13, 8); + qDebug() << " " << strokeFile.mid(2, 8).toUInt(0, 16) << strokeFile.mid(13, 8).toUInt(0, 16); + + 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; +} + +AfdNotebook::PageAddress AfdNotebook::getPageAddress(int page) const +{ + PageAddress addr = _firstPage; + Q_ASSERT(page >= 0); + uint new_page = addr.page + page; + addr.book += new_page / _pagesPerBook; + addr.page = new_page % _pagesPerBook; + return addr; +} + +int AfdNotebook::getPageNumber(const PageAddress &addr) +{ + // series(0), shelf(0), segment(0), book(0), page(0) + if (addr.series == _firstPage.series && addr.shelf == _firstPage.shelf && addr.segment == _firstPage.segment) { + int firstPage = (_firstPage.book * _pagesPerBook) + _firstPage.page; + int page = (addr.book * _pagesPerBook) + addr.page - firstPage; + if (page >= 0 && page < _pages.size()) { + return page; + } + } + + qWarning() << "Invalid address for notebook" << _title; + return -1; +} diff --git a/afdnotebook.h b/afdnotebook.h new file mode 100644 index 0000000..ea90661 --- /dev/null +++ b/afdnotebook.h @@ -0,0 +1,119 @@ +#ifndef AFDNOTEBOOK_H +#define AFDNOTEBOOK_H + +#include +#include +#include +#include +#include "stfreader.h" + +class AfdNotebook : public QObject +{ + Q_OBJECT + +public: + AfdNotebook(QObject *parent = 0); + ~AfdNotebook(); + + bool open(const QString &path); + void close(); + + QString title() const; + + int numPages() const; + + QString getPageBackgroundName(int page) const; + QPixmap getPageBackground(int page); + + QSize getPageSize(int page) const; + QRect getPageTrim(int page) const; + + QStringList penSerials() const; + QList pagesWithStrokes(const QString &penSerial) const; + + QStringList strokeFiles(const QString &penSerial, int page) const; + + bool readStrokes(const QString &penSerial, int page, StfReader::StrokeHandler *handler); + +private: + struct PageAddress { + PageAddress(); + explicit PageAddress(uint shelf, uint segment, uint book, uint page); + explicit PageAddress(uint series, uint shelf, uint segment, uint book, uint page); + explicit PageAddress(const QString &str); + + QString toString() const; + + bool operator<(const PageAddress& o) const; + bool operator==(const PageAddress& o) const; + + uint series : 12; + uint shelf : 12; + uint segment : 16; + uint book : 12; + uint page : 12; + }; + + struct Gfx { + QString basename; + }; + + struct Page { + Gfx *gfx; + QSize size; + }; + + struct StrokeData { + QString file; + QDateTime begin; + QDateTime end; + }; + + struct PenData { + QMultiMap strokes; + }; + +private: + static QMap parsePropertyList(QIODevice *dev); + QMap parsePropertyList(const QString &relativePath) const; + + bool parseMainInfo(); + bool parseMainDocument(); + bool parseGfx(const QString &file); + bool findPenData(); + + PageAddress getPageAddress(int page) const; + int getPageNumber(const PageAddress &addr); + +private: + QDir _dir; + QString _title; + PageAddress _firstPage, _lastPage; + uint _pagesPerBook; + QMap _gfx; + QList _pages; + QMap _penData; +}; + +inline AfdNotebook::PageAddress::PageAddress() + : series(0), shelf(0), segment(0), book(0), page(0) +{ +} + +inline AfdNotebook::PageAddress::PageAddress(uint shelf, uint segment, uint book, uint page) + : series(0), shelf(shelf), segment(segment), book(book), page(page) +{ +} + +inline AfdNotebook::PageAddress::PageAddress(uint series, uint shelf, uint segment, uint book, uint page) + : series(series), shelf(shelf), segment(segment), book(book), page(page) +{ +} + +inline bool AfdNotebook::PageAddress::operator ==(const PageAddress &o) const +{ + return series == o.series && shelf == o.shelf && segment == o.segment && + book == o.book && page == o.page; +} + +#endif diff --git a/bitreader.cc b/bitreader.cc new file mode 100644 index 0000000..dd85f74 --- /dev/null +++ b/bitreader.cc @@ -0,0 +1,45 @@ +#include "bitreader.h" + +BitReader::BitReader(QIODevice *device) + : child(device), buf(0), avail(0) +{ +} + +BitReader::~BitReader() +{ +} + +quint64 BitReader::readBits(int n) +{ + quint64 x = peekBits(n); + + Q_ASSERT(avail >= n); + buf -= x << (avail - n); + avail -= n; + + return x; +} + +quint64 BitReader::peekBits(int n) +{ + while (n > avail) { + char c; + child->getChar(&c); + buf = (buf << 8) | (quint8)(c); + avail += 8; + } + quint64 x = buf >> (avail - n); + + return x; +} + +void BitReader::skipUntilNextByte() +{ + int skip = avail % 8; + readBits(skip); +} + +bool BitReader::atEnd() +{ + return avail == 0 && child->atEnd(); +} diff --git a/bitreader.h b/bitreader.h new file mode 100644 index 0000000..c4b9207 --- /dev/null +++ b/bitreader.h @@ -0,0 +1,25 @@ +#ifndef BITREADER_H +#define BITREADER_H + +#include + +class BitReader +{ +public: + BitReader(QIODevice * device); + ~BitReader(); + + quint64 readBits(int n); + quint64 peekBits(int n); + + void skipUntilNextByte(); + + bool atEnd(); + +private: + QIODevice *child; + quint64 buf; + int avail; +}; + +#endif // BITREADER_H diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..a080bdd --- /dev/null +++ b/main.cc @@ -0,0 +1,11 @@ +#include "mainwindow.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/mainwindow.cc b/mainwindow.cc new file mode 100644 index 0000000..793a722 --- /dev/null +++ b/mainwindow.cc @@ -0,0 +1,66 @@ +#include +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + _notebooks(new NotebookModel(this)), + _manager(new SmartpenManager(this)) +{ + ui->setupUi(this); + ui->notebookTree->setModel(_notebooks); + ui->notebookTree->header()->setResizeMode(0, QHeaderView::Stretch); + ui->notebookTree->header()->setResizeMode(1, QHeaderView::Fixed); + ui->notebookTree->header()->setResizeMode(2, QHeaderView::Fixed); + ui->notebookTree->expandAll(); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::closeNotebook() +{ + _curPenName.clear(); + _curNotebookName.clear(); + ui->notebookView->setNotebook(QString()); +} + +void MainWindow::openNotebook(const QString &pen, const QString ¬ebook) +{ + if (_curPenName == pen && _curNotebookName == notebook) return; + + _curPenName = pen; + _curNotebookName = notebook; + + QString nbDir = _notebooks->notebookDirectory(_curPenName, _curNotebookName); + + qDebug() << "Opening notebook" << _curPenName << _curNotebookName << nbDir; + + ui->notebookView->setNotebook(nbDir); +} + +void MainWindow::handleNotebookSelected(const QModelIndex &index) +{ + if (!index.isValid()) { + closeNotebook(); + return; + } + QModelIndex parent = index.parent(); + if (!parent.isValid()) { + closeNotebook(); + return; + } + + QModelIndex child = parent.child(index.row(), 0); + + openNotebook(_notebooks->data(parent, Qt::DisplayRole).toString(), + _notebooks->data(child, Qt::DisplayRole).toString()); +} + +void MainWindow::handleCurPageChanged() +{ + ui->pageEdit->setText(QString::number(ui->notebookView->curPage() + 1)); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..4072ffa --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,37 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include "notebookmodel.h" +#include "smartpenmanager.h" + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +public slots: + void closeNotebook(); + void openNotebook(const QString &pen, const QString ¬ebook); + +private slots: + void handleNotebookSelected(const QModelIndex &index); + void handleCurPageChanged(); + +private: + Ui::MainWindow *ui; + NotebookModel *_notebooks; + SmartpenManager *_manager; + + QString _curPenName; + QString _curNotebookName; +}; + +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..dceeadc --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,354 @@ + + + MainWindow + + + + 0 + 0 + 718 + 386 + + + + Scribiu + + + + + 3 + + + 0 + + + 3 + + + 0 + + + 3 + + + + + Qt::Horizontal + + + + QAbstractItemView::NoEditTriggers + + + 10 + + + true + + + false + + + false + + + 27 + + + false + + + + + + 2 + + + + + + + + 30 + 30 + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + 4 + + + + + + + + 30 + 30 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Zoom: + + + Qt::PlainText + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + 200 + + + 10 + + + 50 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + + + + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOn + + + + + + + + + + + + + 0 + 0 + 718 + 23 + + + + + &File + + + + + + + + + + + + + + &Quit + + + Ctrl+Q + + + QAction::QuitRole + + + + + + + NotebookView + QGraphicsView +
notebookview.h
+ + curPageChanged() + setZoom(int) + prevPage() + nextPage() + +
+
+ + + + notebookTree + activated(QModelIndex) + MainWindow + handleNotebookSelected(QModelIndex) + + + 159 + 193 + + + 358 + 192 + + + + + notebookTree + clicked(QModelIndex) + MainWindow + handleNotebookSelected(QModelIndex) + + + 159 + 193 + + + 358 + 192 + + + + + zoomSlider + valueChanged(int) + notebookView + setZoom(int) + + + 671 + 42 + + + 521 + 210 + + + + + notebookView + curPageChanged() + MainWindow + handleCurPageChanged() + + + 521 + 210 + + + 358 + 192 + + + + + prevButton + clicked() + notebookView + prevPage() + + + 341 + 42 + + + 521 + 210 + + + + + nextButton + clicked() + notebookView + nextPage() + + + 433 + 42 + + + 521 + 210 + + + + + + handleNotebookSelected(QModelIndex) + handleZoomChanged(int) + handleCurPageChanged() + +
diff --git a/notebookmodel.cc b/notebookmodel.cc new file mode 100644 index 0000000..1d8228c --- /dev/null +++ b/notebookmodel.cc @@ -0,0 +1,323 @@ +#include +#include +#include +#include +#include +#include "notebookmodel.h" + +#define NUM_COLUMNS 3 +#define PEN_INDEX_ID 0xFFFFFFFFU + +NotebookModel::NotebookModel(QObject *parent) : + QAbstractItemModel(parent), + _dataDir(QDesktopServices::storageLocation(QDesktopServices::DataLocation)), + _watcher() +{ + if (!_dataDir.exists()) { + if (!_dataDir.mkpath(".")) { + qWarning() << "Cannot create my data directory:" << _dataDir.absolutePath(); + } + } + _watcher.addPath(_dataDir.absolutePath()); + connect(&_watcher, SIGNAL(directoryChanged(QString)), SLOT(handleChangedDirectory(QString))); + refresh(); +} + +QString NotebookModel::penDirectory(const QString &name) const +{ + return _dataDir.filePath(name + ".pen"); +} + +QString NotebookModel::notebookDirectory(const QString &penName, const QString &nbName) const +{ + return _dataDir.filePath(penName + ".pen" + "/" + nbName + ".afd"); +} + +QString NotebookModel::notebookDirectory(const QModelIndex &index) const +{ + if (!index.isValid()) return QString(); + quint32 id = index.internalId(); + if (id != PEN_INDEX_ID) { + const QString &penName = _pens[id]; + const QStringList ¬ebooks = _notebooks[penName]; + const QString ¬ebookName = notebooks.at(index.row()); + return notebookDirectory(penName, notebookName); + } + return QString(); +} + +QVariant NotebookModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) return QVariant(); + quint32 id = index.internalId(); + if (id == PEN_INDEX_ID) { + int penIndex = index.row(); + if (penIndex < 0 || penIndex >= _pens.size()) return QVariant(); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case 0: + return _pens[penIndex]; + } + break; + } + } else { + const QString &penName = _pens[id]; + const QStringList ¬ebooks = _notebooks[penName]; + const QString ¬ebookName = notebooks.at(index.row()); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case 0: + return notebooks.at(index.row()); + case 1: + return 0; + } + break; + case Qt::DecorationRole: + switch (index.column()) { + case 0: + return getNotebookIcon(penName, notebookName); + case 1: + return QVariant(); + case 2: + if (isNotebookLocked(penName, notebookName)) { + return QApplication::style()->standardIcon(QStyle::SP_BrowserReload); + } + } + case Qt::TextAlignmentRole: + switch (index.column()) { + case 0: + return Qt::AlignLeft; + case 1: + case 2: + return Qt::AlignCenter; + } + } + } + return QVariant(); +} + +Qt::ItemFlags NotebookModel::flags(const QModelIndex &index) const +{ + switch (index.column()) { + case 0: + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + default: + return Qt::ItemIsSelectable; + } +} + +QModelIndex NotebookModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) { + quint32 id = parent.internalId(); + if (id == PEN_INDEX_ID) { + // Pen index + const quint32 penIndex = parent.row(); + const QString &penName = _pens.at(penIndex); + const QStringList ¬ebooks = _notebooks[penName]; + if (row >= 0 && row < notebooks.size() && column >= 0 && column < NUM_COLUMNS) { + return createIndex(row, column, penIndex); + } else { + return QModelIndex(); + } + } else { + // Notebook index + return QModelIndex(); // Nothing inside notebooks + } + } else if (row >= 0 && row < _pens.size() && column >= 0 && column < NUM_COLUMNS) { + return createIndex(row, column, PEN_INDEX_ID); + } else { + return QModelIndex(); + } +} + +QModelIndex NotebookModel::parent(const QModelIndex &child) const +{ + if (child.isValid()) { + quint32 id = child.internalId(); + if (id == PEN_INDEX_ID) { + return QModelIndex(); // Pens have no parent + } else { + return createIndex(id, 0, PEN_INDEX_ID); + } + } else { + return QModelIndex(); + } +} + +int NotebookModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + quint32 id = parent.internalId(); + if (id == PEN_INDEX_ID) { + const quint32 penIndex = parent.row(); + const QString &penName = _pens.at(penIndex); + const QStringList ¬ebooks = _notebooks[penName]; + return notebooks.size(); + } else { + return 0; // Nothing inside notebooks + } + } else { + return _pens.size(); + } +} + +int NotebookModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return NUM_COLUMNS; +} + +void NotebookModel::refresh() +{ + QStringList pens = _dataDir.entryList(QStringList("*.pen"), QDir::Dirs, QDir::Name); + for (int i = 0; i < pens.size(); i++) { + pens[i].chop(4); + } + + int i = 0, j = 0; + while (i < _pens.size() && j < pens.size()) { + int comp = QString::compare(_pens[i], pens[j], Qt::CaseInsensitive); + if (comp == 0) { + ++i; + ++j; + } else if (comp > 0) { /* _pens[i] > pens[j] */ + beginInsertRows(QModelIndex(), i, i); + _pens.insert(i, pens[j]); + endInsertRows(); + ++i; + ++j; + } else { /* _pens[i] < pens[j] */ + beginRemoveRows(QModelIndex(), i, i); + _notebooks.remove(_pens[i]); + _pens.removeAt(i); + endRemoveRows(); + } + } + if (i < _pens.size()) { + Q_ASSERT(j == pens.size()); + beginRemoveRows(QModelIndex(), i, _pens.size() - 1); + while (i < _pens.size()) { + _notebooks.remove(_pens[i]); + _pens.removeAt(i); + } + endRemoveRows(); + } else if (j < pens.size()) { + Q_ASSERT(i == _pens.size()); + beginInsertRows(QModelIndex(), i, i + (pens.size() - j)); + _pens.append(pens.mid(j)); + endInsertRows(); + } + + foreach (const QString &pen, _pens) { + refreshPen(pen); + } +} + +void NotebookModel::refreshPen(const QString &name) +{ + QDir penDir(penDirectory(name)); + if (!penDir.exists()) { + return; + } + + _watcher.addPath(penDir.canonicalPath()); + + QStringList &curNotebooks = _notebooks[name]; + QStringList diskNotebooks = penDir.entryList(QStringList("*.afd"), QDir::Dirs, QDir::Name); + for (int i = 0; i < diskNotebooks.size(); i++) { + diskNotebooks[i].chop(4); + } + + QModelIndex penIndex = index(indexOfPen(name), 0, QModelIndex()); + + int i = 0, j = 0; + while (i < curNotebooks.size() && j < diskNotebooks.size()) { + int comp = QString::compare(curNotebooks[i], diskNotebooks[j], Qt::CaseInsensitive); + if (comp == 0) { + ++i; + ++j; + } else if (comp > 0) { /* _pens[i] > pens[j] */ + beginInsertRows(penIndex, i, i); + curNotebooks.insert(i, diskNotebooks[j]); + endInsertRows(); + ++i; + ++j; + } else { /* _pens[i] < pens[j] */ + beginRemoveRows(penIndex, i, i); + curNotebooks.removeAt(i); + endRemoveRows(); + } + } + if (i < curNotebooks.size()) { + Q_ASSERT(j == diskNotebooks.size()); + beginRemoveRows(penIndex, i, curNotebooks.size() - 1); + curNotebooks.erase(curNotebooks.begin() + i, curNotebooks.end()); + endRemoveRows(); + } else if (j < diskNotebooks.size()) { + Q_ASSERT(i == curNotebooks.size()); + beginInsertRows(penIndex, i, i + (diskNotebooks.size() - j)); + curNotebooks.append(diskNotebooks.mid(j)); + endInsertRows(); + } + + qDebug() << "Found" << curNotebooks.size() << "notebook for pen" << name; +} + +int NotebookModel::indexOfPen(const QString &name) +{ + QStringList::const_iterator it = qBinaryFind(_pens, name); + if (it == _pens.end()) { + return -1; + } else { + return it - _pens.begin(); + } +} + +QDir NotebookModel::notebookDir(const QString &pen, const QString ¬ebook) const +{ + return QDir(_dataDir.filePath("%1.pen/%2.afd").arg(pen, notebook)); +} + +QIcon NotebookModel::getNotebookIcon(const QString &pen, const QString ¬ebook) const +{ + static QStringList candidates; + if (candidates.isEmpty()) { + candidates << "userdata/icon/Notebook.png" + << "userdata/icon/active_64x64.png" + << "userdata/icon/active_32x32.png" + << "userdata/icon/active_16x16.png"; + } + QIcon icon; + + QDir dir = notebookDir(pen, notebook); + if (dir.exists()) { + foreach (const QString &candidate, candidates) { + if (dir.exists(candidate)) { + icon.addFile(dir.filePath(candidate)); + } + } + } + + return icon; +} + +bool NotebookModel::isNotebookLocked(const QString &pen, const QString ¬ebook) const +{ + QDir dir = notebookDir(pen, notebook); + if (dir.exists(".sync.lck")) { + return true; // TODO check if stale + } else { + return false; + } +} + +void NotebookModel::handleChangedDirectory(const QString &path) +{ + qDebug() << "changed" << path; + if (path == _dataDir.absolutePath()) { + refresh(); + } +} diff --git a/notebookmodel.h b/notebookmodel.h new file mode 100644 index 0000000..83bb004 --- /dev/null +++ b/notebookmodel.h @@ -0,0 +1,49 @@ +#ifndef NOTEBOOKMODEL_H +#define NOTEBOOKMODEL_H + +#include +#include +#include + +class NotebookModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit NotebookModel(QObject *parent = 0); + + QString penDirectory(const QString &name) const; + QString notebookDirectory(const QString &penName, const QString &nbName) const; + QString notebookDirectory(const QModelIndex &index) const; + + QVariant data(const QModelIndex &index, int role) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + QModelIndex index(int row, int column, const QModelIndex &parent) const; + QModelIndex parent(const QModelIndex &child) const; + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + +signals: + +public slots: + void refresh(); + void refreshPen(const QString &name); + +private: + +private: + int indexOfPen(const QString &name); + QDir notebookDir(const QString &pen, const QString ¬ebook) const; + QIcon getNotebookIcon(const QString &pen, const QString ¬ebook) const; + bool isNotebookLocked(const QString &pen, const QString ¬ebook) const; + +private slots: + void handleChangedDirectory(const QString &path); + +private: + QDir _dataDir; + QFileSystemWatcher _watcher; + QStringList _pens; + QHash _notebooks; +}; + +#endif // NOTEBOOKMODEL_H diff --git a/notebookview.cc b/notebookview.cc new file mode 100644 index 0000000..2605792 --- /dev/null +++ b/notebookview.cc @@ -0,0 +1,194 @@ +#include +#include +#include "notebookview.h" + +#define VIEW_MARGIN 2 +#define PAGE_SEPARATION 100 + +NotebookView::NotebookView(QWidget *parent) : + QGraphicsView(parent), _nb(new AfdNotebook(this)), + _zoom(100), _curPage(0) +{ + setScene(new QGraphicsScene(this)); + setTransformationAnchor(AnchorUnderMouse); + setDragMode(ScrollHandDrag); + setRenderHints(QPainter::Antialiasing); +} + +void NotebookView::setNotebook(const QString &path) +{ + removePages(); + if (!path.isEmpty()) { + if (_nb->open(path)) { + _nbPath = path; + if (_zoom > 100) { + _zoom = 100; + emit zoomChanged(); + } + createPages(); + emit pageNumbersChanged(); + if (!_pages.isEmpty()) { + _curPage = _pages.begin().key(); + centerOn(_pages[_curPage]); + emit curPageChanged(); + } + } else { + qWarning() << "Could not open notebook:" << _nbPath; + } + } +} + +QString NotebookView::notebook() const +{ + return _nbPath; +} + +QList NotebookView::pageNumbers() const +{ + return _pages.keys(); +} + +int NotebookView::curPage() const +{ + return _curPage; +} + +void NotebookView::setCurPage(int page) +{ + if (page != _curPage) { + _curPage = page; + if (_zoom > 100) { + setZoom(100); + } + if (_pages.contains(_curPage)) { + centerOn(_pages[_curPage]); + } + emit curPageChanged(); + } +} + +int NotebookView::zoom() const +{ + return _zoom; +} + +void NotebookView::setZoom(int zoom) +{ + if (zoom != _zoom) { + _zoom = zoom; + calculateScale(); + } +} + +void NotebookView::clear() +{ + removePages(); + emit pageNumbersChanged(); +} + +void NotebookView::prevPage() +{ + QMap::iterator it = _pages.lowerBound(_curPage); + if (it != _pages.end() && it != _pages.begin()) { + --it; + setCurPage(it.key()); + } +} + +void NotebookView::nextPage() +{ + QMap::iterator it = _pages.upperBound(_curPage); + if (it != _pages.end()) { + setCurPage(it.key()); + } +} + +void NotebookView::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + calculateScale(); +} + +void NotebookView::scrollContentsBy(int dx, int dy) +{ + QGraphicsView::scrollContentsBy(dx, dy); + QGraphicsItem *item = itemAt(size().width() / 2, size().height() / 2); + while (item && item->type() != PageItem::Type) { + item = item->parentItem(); + } + if (item && item->type() == PageItem::Type) { + PageItem * page = static_cast(item); + int centerPage = page->pageNum(); + if (centerPage != _curPage) { + _curPage = centerPage; + emit curPageChanged(); + } + } +} + +void NotebookView::removePages() +{ + _pages.clear(); + scene()->clear(); + scene()->setSceneRect(QRectF()); + _nb->close(); + _nbPath.clear(); + _maxPageSize.setWidth(0); + _maxPageSize.setHeight(0); + resetTransform(); +} + +void NotebookView::createPages() +{ + QStringList pens = _nb->penSerials(); + if (pens.isEmpty()) return; + + QList pagesWithStrokes = _nb->pagesWithStrokes(pens.first()); + Q_ASSERT(_pages.isEmpty()); + + _maxPageSize.setWidth(0); + _maxPageSize.setHeight(0); + foreach (int pageNum, pagesWithStrokes) { + PageItem *page = new PageItem(_nb, pageNum); + QRectF box = page->boundingRect(); + if (box.width() > _maxPageSize.width()) { + _maxPageSize.setWidth(box.width()); + } + if (box.height() > _maxPageSize.height()) { + _maxPageSize.setHeight(box.height()); + } + _pages.insert(pageNum, page); + } + + calculateScale(); + + qreal curY = 0; + foreach (PageItem *page, _pages) { + QRectF box = page->boundingRect(); + page->setPos((_maxPageSize.width() - box.width()) / 2.0, curY); + curY += box.height(); + curY += PAGE_SEPARATION; + + scene()->addItem(page); + } + + scene()->setSceneRect(0, 0, _maxPageSize.width(), curY); +} + +void NotebookView::calculateScale() +{ + if (_pages.isEmpty() || _maxPageSize.isEmpty()) return; + const int margin = VIEW_MARGIN; + QRectF viewRect = viewport()->rect().adjusted(margin, margin, -margin, margin); + qreal baseScale = qMin(viewRect.width() / _maxPageSize.width(), + viewRect.height() / _maxPageSize.height()); + resetTransform(); + scale(baseScale, baseScale); + if (_zoom < 100) { + qreal s = 0.25 + ((_zoom / 100.0) * 0.75); + scale(s, s); + } else if (_zoom > 100) { + qreal s = 1.0 + (_zoom - 100) * 0.015; + scale(s, s); + } +} diff --git a/notebookview.h b/notebookview.h new file mode 100644 index 0000000..e51afa3 --- /dev/null +++ b/notebookview.h @@ -0,0 +1,59 @@ +#ifndef NOTEBOOKVIEW_H +#define NOTEBOOKVIEW_H + +#include +#include +#include "afdnotebook.h" +#include "pageitem.h" + +class NotebookView : public QGraphicsView +{ + Q_OBJECT + Q_PROPERTY(QString notebook WRITE setNotebook READ notebook) + Q_PROPERTY(QList pageNumbers READ pageNumbers NOTIFY pageNumbersChanged) + Q_PROPERTY(int curPage READ curPage WRITE setCurPage NOTIFY curPageChanged) + Q_PROPERTY(int zoom READ zoom WRITE setZoom NOTIFY zoomChanged) + +public: + explicit NotebookView(QWidget *parent = 0); + + void setNotebook(const QString &path); + QString notebook() const; + + QList pageNumbers() const; + + int curPage() const; + void setCurPage(int page); + + int zoom() const; + +signals: + void pageNumbersChanged(); + void curPageChanged(); + void zoomChanged(); + +public slots: + void clear(); + void setZoom(int zoom); + void prevPage(); + void nextPage(); + +protected: + void resizeEvent(QResizeEvent *event); + void scrollContentsBy(int dx, int dy); + +private: + void removePages(); + void createPages(); + void calculateScale(); + +private: + AfdNotebook *_nb; + QString _nbPath; + QMap _pages; + QSizeF _maxPageSize; + int _zoom; + int _curPage; +}; + +#endif // NOTEBOOKVIEW_H diff --git a/pageitem.cc b/pageitem.cc new file mode 100644 index 0000000..f938eeb --- /dev/null +++ b/pageitem.cc @@ -0,0 +1,64 @@ +#include +#include +#include "pageitem.h" +#include "stfgraphicsitem.h" + +PageItem::PageItem(AfdNotebook *nb, int pageNum, QGraphicsItem *parent) : + QGraphicsItem(parent), _nb(nb), _pageNum(pageNum), _strokesLoaded(false) +{ + _pageSize = _nb->getPageSize(pageNum); + _pageTrim = _nb->getPageTrim(pageNum); + + QGraphicsRectItem *border = new QGraphicsRectItem(_pageTrim, this); + border->setPen(QPen(Qt::black)); + + QPixmap bgPix = _nb->getPageBackground(_pageNum); + QGraphicsPixmapItem *bg = new QGraphicsPixmapItem(bgPix, this); + bg->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); + bg->setTransformationMode(Qt::SmoothTransformation); + QRectF bgRect = bg->boundingRect(); + bg->scale(_pageTrim.width() / bgRect.width(), _pageTrim.height() / bgRect.height()); + bg->setPos(_pageTrim.topLeft()); +} + +QRectF PageItem::boundingRect() const +{ + return QRectF(QPointF(0, 0), _pageSize); +} + +void PageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(painter); + Q_UNUSED(option); + Q_UNUSED(widget); + if (!_strokesLoaded) { + createStrokes(); + _strokesLoaded = true; + } +} + +int PageItem::type() const +{ + return Type; +} + +int PageItem::pageNum() const +{ + return _pageNum; +} + +void PageItem::createStrokes() +{ + QStringList pens = _nb->penSerials(); + if (pens.isEmpty()) return; + QStringList strokeFiles = _nb->strokeFiles(pens.first(), _pageNum); + foreach (const QString &strokeFile, strokeFiles) { + QFile f(strokeFile); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open stroke file:" << strokeFile; + continue; + } + new StfGraphicsItem(&f, this); + } + qDebug() << "strokes loaded for page" << _pageNum; +} diff --git a/pageitem.h b/pageitem.h new file mode 100644 index 0000000..2e6059a --- /dev/null +++ b/pageitem.h @@ -0,0 +1,32 @@ +#ifndef PAGEITEM_H +#define PAGEITEM_H + +#include + +#include "afdnotebook.h" + +class PageItem : public QGraphicsItem +{ +public: + explicit PageItem(AfdNotebook *nb, int pageNum, QGraphicsItem *parent = 0); + + enum { Type = UserType + 'p' }; + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + int type() const; + + int pageNum() const; + +private: + void createStrokes(); + +private: + AfdNotebook *_nb; + int _pageNum; + QSize _pageSize; + QRect _pageTrim; + bool _strokesLoaded; +}; + +#endif // PAGEITEM_H diff --git a/scribiu.pro b/scribiu.pro new file mode 100644 index 0000000..641f286 --- /dev/null +++ b/scribiu.pro @@ -0,0 +1,35 @@ +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = scribiu +TEMPLATE = app + +CONFIG += link_pkgconfig +PKGCONFIG += libudev libusb openobex +LIBS += -lquazip + +SOURCES += main.cc\ + mainwindow.cc \ + smartpenmanager.cc \ + notebookmodel.cc \ + smartpensyncer.cc \ + smartpen.cc bitreader.cc stfreader.cc \ + xmlutils.cc \ + notebookview.cc \ + afdnotebook.cc \ + pageitem.cc \ + stfgraphicsitem.cc + +HEADERS += mainwindow.h \ + smartpenmanager.h \ + notebookmodel.h \ + smartpensyncer.h \ + smartpen.h bitreader.h stfreader.h \ + xmlutils.h \ + notebookview.h \ + afdnotebook.h \ + pageitem.h \ + stfgraphicsitem.h + +FORMS += mainwindow.ui diff --git a/smartpen.cc b/smartpen.cc new file mode 100644 index 0000000..6df3fd1 --- /dev/null +++ b/smartpen.cc @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include "xmlutils.h" +#include "smartpen.h" + +#define PEN_EPOCH (1289335960000LL) +#define PEN_MTU 900 +#define PEN_TIMEOUT_SECONDS 10 + +#define INVALID_CID 0xFFFFFFFFU + +/* Terrible hack comes now: */ +struct obex_usb_intf_transport_t { + struct obex_usb_intf_transport_t *prev, *next; /* Next and previous interfaces in the list */ + struct usb_device *device; /* USB device that has the interface */ +}; + +Smartpen::Smartpen(QObject *parent) : + QObject(parent), _obex(0), _connId(INVALID_CID) +{ +} + +Smartpen::~Smartpen() +{ + if (_connId != INVALID_CID || _obex) { + disconnectFromPen(); + } +} + +bool Smartpen::isConnected() const +{ + return _obex && _connId != INVALID_CID; +} + +QByteArray Smartpen::getObject(const QString &name) +{ + obex_object_t *obj = OBEX_ObjectNew(_obex, OBEX_CMD_GET); + Q_ASSERT(obj); + + addConnHeader(obj); + + obex_headerdata_t hd; + QByteArray encodedName = encodeUtf16(name); + hd.bs = reinterpret_cast(encodedName.constData()); + if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_NAME, hd, encodedName.size(), 0) < 0) { + qCritical("Could not add name header"); + OBEX_ObjectDelete(_obex, obj); + return QByteArray(); + } + + qDebug() << "Getting object" << name; + + if (OBEX_Request(_obex, obj) < 0) { + qWarning() << "Get object request failed"; + return QByteArray(); + } + + QDateTime start = QDateTime::currentDateTimeUtc(); + QDateTime now; + do { + OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + now = QDateTime::currentDateTimeUtc(); + } while (_inBuf.isEmpty() && start.secsTo(now) < PEN_TIMEOUT_SECONDS); + + if (_inBuf.isEmpty()) { + qWarning() << "Did not receive any data in" << start.secsTo(now) << "seconds"; + } + + QByteArray result; + qSwap(_inBuf, result); + + return result; +} + +QString Smartpen::getParameter(Parameters parameter) +{ + QString objectName = QString("ppdata?key=pp%1").arg(int(parameter), 4); + QByteArray data = getObject(objectName); + QXmlStreamReader r(data); + + advanceToFirstChildElement(r, "xml"); + advanceToFirstChildElement(r, "parameter"); + + if (!r.atEnd()) { + QXmlStreamAttributes attrs = r.attributes(); + return attrs.value("value").toString(); + } + + return QString(); +} + +QString Smartpen::getPenName() +{ + QString name = getParameter(PenName); + if (name.isEmpty()) { + return name; + } + + QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1()); + return QString::fromUtf8(hex); +} + +QVariantMap Smartpen::getPenInfo() +{ + QVariantMap result; + QByteArray data = getObject("peninfo"); + QXmlStreamReader r(data); + + advanceToFirstChildElement(r, "xml"); + advanceToFirstChildElement(r, "peninfo"); + + if (!r.atEnd()) { + Q_ASSERT(r.isStartElement() && r.name() == "peninfo"); + QString penId = r.attributes().value("penid").toString(); + result["penid"] = penId; + result["penserial"] = toPenSerial(penId.mid(2).toULongLong(0, 16)); + + while (r.readNextStartElement()) { + if (r.name() == "battery") { + result["battery"] = r.attributes().value("level").toString(); + r.skipCurrentElement(); + } else if (r.name() == "time") { + result["time"] = r.attributes().value("absolute").toString(); + r.skipCurrentElement(); + } else { + r.skipCurrentElement(); + } + } + } else { + qWarning() << "Could not parse peninfo XML"; + } + + return result; +} + +QList Smartpen::getChangeList(const QDateTime &from) +{ + QList result; + QByteArray data = getObject(QString("changelist?start_time=%1").arg(toPenTime(from))); + QXmlStreamReader r(data); + + advanceToFirstChildElement(r, "xml"); + advanceToFirstChildElement(r, "changelist"); + + if (!r.atEnd()) { + Q_ASSERT(r.isStartElement() && r.name() == "changelist"); + + while (r.readNextStartElement()) { + if (r.name() == "lsp") { + QXmlStreamAttributes attrs = r.attributes(); + ChangeReport report; + if (attrs.hasAttribute("guid")) { + report.guid = attrs.value("guid").toString(); + report.title = attrs.value("title").toString(); + result.append(report); + } + r.skipCurrentElement(); + } else { + r.skipCurrentElement(); + } + } + + } else { + qWarning() << "Could not parse changelist XML"; + } + + return result; +} + +QByteArray Smartpen::getLspData(const QString &name, const QDateTime &from) +{ + return getObject(QString("lspdata?name=%1&start_time=%2").arg(name).arg(toPenTime(from))); +} + +qint64 Smartpen::toPenTime(const QDateTime &dt) +{ + if (dt.isValid()) { + return dt.toMSecsSinceEpoch() - PEN_EPOCH; + } else { + return 0; + } +} + +QDateTime Smartpen::fromPenTime(qint64 t) +{ + if (t) { + return QDateTime::fromMSecsSinceEpoch(t + PEN_EPOCH).toLocalTime(); + } else { + return QDateTime(); + } +} + +QString Smartpen::toPenSerial(quint64 id) +{ + QString serial; + serial.reserve(3 + 1 + 3 + 1 + 3 + 1 + 2 + 1); + + serial.append(toPenSerialSegment(id >> 32, 3)); + serial.append('-'); + serial.append(toPenSerialSegment(id, 6).mid(0, 3)); + serial.append('-'); + serial.append(toPenSerialSegment(id, 6).mid(3, 3)); + serial.append('-'); + serial.append(toPenSerialSegment(id % 0x36D, 2)); + + return serial; +} + +bool Smartpen::connectToPen(const Address &addr) +{ + if (_obex) { + qWarning() << "Already connected"; + return false; + } + + _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, 0); + Q_ASSERT(_obex); + + OBEX_SetUserData(_obex, this); + OBEX_SetTransportMTU(_obex, PEN_MTU, PEN_MTU); + + obex_interface_t *interfaces, *ls_interface = 0; + int count = OBEX_FindInterfaces(_obex, &interfaces); + for (int i = 0; i < count; i++) { + if (interfaces[i].usb.intf->device->bus->location == addr.first && + interfaces[i].usb.intf->device->devnum == addr.second) { + ls_interface = &interfaces[i]; + } + } + + if (!ls_interface) { + qWarning() << "Could not find Lightscribe interface on device:" << addr; + return false; + } + + usb_dev_handle *handle = usb_open(ls_interface->usb.intf->device); + if (handle) { + qDebug() << "resetting usb device"; + usb_reset(handle); + usb_close(handle); + } else { + qWarning() << "could not open usb device for resetting"; + } + + qDebug() << "connecting to" << ls_interface->usb.product; + + if (OBEX_InterfaceConnect(_obex, ls_interface) < 0) { + qWarning() << "Could not connect to Livescribe interface"; + return false; + } + + static const char * livescribe_service = "LivescribeService"; + obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_CONNECT); + obex_headerdata_t hd; + int hd_len; + + Q_ASSERT(object); + + hd.bs = reinterpret_cast(livescribe_service); + hd_len = strlen(livescribe_service) + 1; + if (OBEX_ObjectAddHeader(_obex, object, OBEX_HDR_TARGET, hd, hd_len, 0) < 0) { + qWarning() << "Failed to add Target header"; + OBEX_ObjectDelete(_obex, object); + return false; + } + + if (OBEX_Request(_obex, object) < 0) { + qWarning() << "Failed to make connection request"; + OBEX_ObjectDelete(_obex, object); + return false; + } + + qDebug() << "Connection in progress"; + + OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + + return _connId != INVALID_CID; +} + +void Smartpen::disconnectFromPen() +{ + if (_connId != INVALID_CID) { + if (_obex) { + obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_DISCONNECT); + Q_ASSERT(object); + addConnHeader(object); + OBEX_Request(_obex, object); + OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + } + _connId = INVALID_CID; + } + if (_obex) { + OBEX_Cleanup(_obex); + _obex = 0; + } + _inBuf.clear(); +} + +void Smartpen::obexEventCb(obex_t *handle, obex_object_t *obj, + int mode, int event, int obex_cmd, int obex_rsp) +{ + Smartpen *smartpen = static_cast(OBEX_GetUserData(handle)); + Q_UNUSED(mode); + smartpen->handleObexEvent(obj, event, obex_cmd, obex_rsp); +} + +void Smartpen::handleObexEvent(obex_object_t *object, + int event, int obex_cmd, int obex_rsp) +{ + + switch (event) { + case OBEX_EV_PROGRESS: + if (obex_cmd == OBEX_CMD_GET) { + // It seems that the pen wants us to add this header on every continue response + addConnHeader(object); + } + break; + case OBEX_EV_REQDONE: + qDebug() << "event reqdone cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp); + handleObexRequestDone(object, obex_cmd, obex_rsp); + break; + case OBEX_EV_LINKERR: + qWarning() << "link error cmd=" << obex_cmd; + emit error(); + break; + default: + qDebug() << "event" << event << obex_cmd << obex_rsp; + break; + } +} + +void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp) +{ + quint8 header_id; + obex_headerdata_t hdata; + quint32 hlen; + + switch (obex_cmd & ~OBEX_FINAL) { + case OBEX_CMD_CONNECT: + switch (obex_rsp) { + case OBEX_RSP_SUCCESS: + while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) { + if (header_id == OBEX_HDR_CONNECTION) { + Q_ASSERT(_connId == INVALID_CID); + _connId = hdata.bq4; + qDebug() << "Connection established, id:" << _connId; + } + } + break; + default: + qWarning() << "Failed connection request:" << OBEX_ResponseToString(obex_rsp); + emit error(); + break; + } + break; + case OBEX_CMD_DISCONNECT: + switch (obex_rsp) { + case OBEX_RSP_SUCCESS: + qDebug() << "Disconnected succesfully"; + _connId = INVALID_CID; + break; + default: + qWarning() << "Failed disconnection request:" << OBEX_ResponseToString(obex_rsp); + _connId = INVALID_CID; + break; + } + + break; + case OBEX_CMD_GET: + switch (obex_rsp) { + case OBEX_RSP_SUCCESS: + qDebug() << "GET request succesful"; + while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) { + if (header_id == OBEX_HDR_BODY || header_id == OBEX_HDR_BODY_END) { + _inBuf = QByteArray(reinterpret_cast(hdata.bs), hlen); + } + } + break; + default: + qWarning() << "Failed GET request:" << OBEX_ResponseToString(obex_rsp); + break; + } + + break; + } +} + +QString Smartpen::toPenSerialSegment(quint32 id, int len) +{ + static const char chars[] = "ABCDEFGHJKMNPQRSTUWXYZ23456789"; + static const unsigned int num_chars = sizeof(chars) - 1; + + QString segment(len, Qt::Uninitialized); + + for (int i = 0; i < len; i++) { + segment[len - (i + 1)] = chars[id % num_chars]; + id /= num_chars; + } + + return segment; +} + +QByteArray Smartpen::encodeUtf16(const QString &s) +{ + const int size = s.size(); + QByteArray data((size + 1) * sizeof(quint16), Qt::Uninitialized); + quint16 *p = reinterpret_cast(data.data()); + for (int i = 0; i < size; i++) { + p[i] = qToBigEndian(s.at(i).unicode()); + } + p[size] = 0; + return data; +} + +void Smartpen::addConnHeader(obex_object_t *obj) const +{ + obex_headerdata_t hd; + hd.bq4 = _connId; + if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_CONNECTION, hd, sizeof(hd.bq4), 0) < 0) { + qCritical() << "Could not add connection header"; + } +} diff --git a/smartpen.h b/smartpen.h new file mode 100644 index 0000000..c93e016 --- /dev/null +++ b/smartpen.h @@ -0,0 +1,78 @@ +#ifndef SMARTPEN_H +#define SMARTPEN_H + +#include +#include +#include +#include + +// TODO: These values are mostly random. +#define SMARTPEN_DPI_X (800.0) +#define SMARTPEN_DPI_Y (800.0) + +#define SMARTPEN_BLEED_X 333.3 +#define SMARTPEN_BLEED_Y 333.3 + +class Smartpen : public QObject +{ + Q_OBJECT + +public: + explicit Smartpen(QObject *parent = 0); + ~Smartpen(); + + typedef QPair Address; + + bool isConnected() const; + + enum Parameters { + PenName = 8011 + }; + + QByteArray getObject(const QString& name); + QString getParameter(Parameters parameter); + + QString getPenName(); + QVariantMap getPenInfo(); + + struct ChangeReport { + QString guid; + QString title; + }; + + QList getChangeList(const QDateTime &from = QDateTime()); + + QByteArray getLspData(const QString &name, const QDateTime &from = QDateTime()); + QByteArray getPaperReplay(); + + static qint64 toPenTime(const QDateTime &dt); + static QDateTime fromPenTime(qint64 t); + + static QString toPenSerial(quint64 id); + +public slots: + bool connectToPen(const Address &addr); + void disconnectFromPen(); + +signals: + void error(); + +private: + static void obexEventCb(obex_t *handle, obex_object_t *obj, + int mode, int event, int obex_cmd, int obex_rsp); + void handleObexEvent(obex_object_t *object, + int event, int obex_cmd, int obex_rsp); + void handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp); + + static QString toPenSerialSegment(quint32 id, int len); + + static QByteArray encodeUtf16(const QString &s); + void addConnHeader(obex_object_t *object) const; + +private: + obex_t * _obex; + quint32 _connId; + QByteArray _inBuf; +}; + +#endif // SMARTPEN_H diff --git a/smartpenmanager.cc b/smartpenmanager.cc new file mode 100644 index 0000000..d560b5b --- /dev/null +++ b/smartpenmanager.cc @@ -0,0 +1,69 @@ +#include +#include + +#include "smartpenmanager.h" + +SmartpenManager::SmartpenManager(QObject *parent) + : QObject(parent), _udev(udev_new()), _monitor(udev_monitor_new_from_netlink(_udev, "udev")), + _notifier(new QSocketNotifier(udev_monitor_get_fd(_monitor), QSocketNotifier::Read)) +{ + udev_monitor_filter_add_match_tag(_monitor, "livescribe-pen"); + + connect(_notifier, SIGNAL(activated(int)), SLOT(handleMonitorActivity())); + + udev_monitor_enable_receiving(_monitor); + + udev_enumerate *scan = udev_enumerate_new(_udev); + udev_enumerate_add_match_tag(scan, "livescribe-pen"); + + if (udev_enumerate_scan_devices(scan) == 0) { + udev_list_entry *l = udev_enumerate_get_list_entry(scan), *i; + udev_list_entry_foreach(i, l) { + const char *path = udev_list_entry_get_name(i); + udev_device *dev = udev_device_new_from_syspath(_udev, path); + processDevice(dev); + udev_device_unref(dev); + } + } else { + qWarning() << "Failed to scan for devices"; + } + + udev_enumerate_unref(scan); +} + +SmartpenManager::~SmartpenManager() +{ + delete _notifier; + udev_monitor_unref(_monitor); + udev_unref(_udev); +} + +void SmartpenManager::handleMonitorActivity() +{ + qDebug() << "udev activity"; + udev_device *dev = udev_monitor_receive_device(_monitor); + udev_device_unref(dev); +} + +void SmartpenManager::handleSyncerFinished() +{ + SmartpenSyncer *syncer = static_cast(sender()); + Smartpen::Address addr = syncer->penAddress(); + qDebug() << "Finished synchronization with pen with address:" << addr; + _syncers.remove(addr); + syncer->deleteLater(); +} + +void SmartpenManager::processDevice(udev_device *dev) +{ + uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum")); + uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum")); + + Smartpen::Address addr(busnum, devnum); + if (!_syncers.contains(addr)) { + SmartpenSyncer *syncer = new SmartpenSyncer(addr, this); + _syncers.insert(addr, syncer); + connect(syncer, SIGNAL(finished()), SLOT(handleSyncerFinished())); + syncer->start(); + } +} diff --git a/smartpenmanager.h b/smartpenmanager.h new file mode 100644 index 0000000..d1b4800 --- /dev/null +++ b/smartpenmanager.h @@ -0,0 +1,42 @@ +#ifndef SMARTPENMANAGER_H +#define SMARTPENMANAGER_H + +#include +#include +#include +#include +#include "smartpensyncer.h" + +struct udev; +struct udev_monitor; +struct udev_device; + +class SmartpenManager : public QObject +{ + Q_OBJECT + +public: + explicit SmartpenManager(QObject *parent = 0); + ~SmartpenManager(); + +signals: + void syncComplete(const QString &penName); + +public slots: + +private slots: + void handleMonitorActivity(); + void handleSyncerFinished(); + +private: + void processDevice(udev_device *dev); + +private: + udev *_udev; + udev_monitor *_monitor; + QSocketNotifier *_notifier; + QMap, SmartpenSyncer*> _syncers; + +}; + +#endif // SMARTPENMANAGER_H diff --git a/smartpensyncer.cc b/smartpensyncer.cc new file mode 100644 index 0000000..50304a3 --- /dev/null +++ b/smartpensyncer.cc @@ -0,0 +1,214 @@ +#include +#include +#include +#include +#include +#include +#include "smartpensyncer.h" + +#define BUFFER_SIZE 16 * 1024 + +namespace { +static QString cleanFilename(QString s) +{ + static const QRegExp re("[^0-9A-Za-z-_]+"); + return s.replace(re, "_"); +} + +static QDateTime getTimestampFileDate(const QString &path) +{ + QFileInfo info(path); + qDebug() << "Checking timestamp" << info.filePath(); + if (info.exists()) { + return info.lastModified(); + } else { + return QDateTime(); + } +} + +static void setTimestampFileDate(const QString &path) +{ + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning() << "Could not set timestamp file:" << path; + return; + } + f.close(); +} + +void removeTimestampFile(const QString &path) +{ + QFile f(path); + if (!f.remove()) { + qWarning() << "Cannot remove timestamp file:" << path; + } +} +} + +SmartpenSyncer::SmartpenSyncer(const Smartpen::Address &addr, QObject *parent) : + QThread(parent), _addr(addr), _pen(new Smartpen(this)), _errored(false), _aborted(false) +{ +} + +SmartpenSyncer::~SmartpenSyncer() +{ + if (isRunning()) { + _aborted = true; + wait(); + } +} + +Smartpen::Address SmartpenSyncer::penAddress() const +{ + return _addr; +} + +void SmartpenSyncer::abort() +{ + _aborted = true; +} + +void SmartpenSyncer::run() +{ + if (!_pen->connectToPen(_addr)) { + qWarning() << "Could not connect to pen with USB address: " << _addr; + _errored = true; + return; + } + + _penName = _pen->getPenName(); + qDebug() << "got pen name:" << _penName; + + QVariantMap penInfo = _pen->getPenInfo(); + if (penInfo.isEmpty()) { + qWarning() << "Could not get pen info"; + _errored = true; + return; + } + + _penSerial = penInfo["penserial"].toString(); + + _penDataDir.setPath(QDesktopServices::storageLocation(QDesktopServices::DataLocation) + "/" + _penName + ".pen"); + if (!_penDataDir.exists()) { + if (!_penDataDir.mkpath(".")) { + qWarning() << "Cannot create pen data directory:" << _penDataDir.absolutePath(); + } + } + + if (!syncPen()) { + _errored = true; + } + + _pen->disconnectFromPen(); +} + +bool SmartpenSyncer::syncPen() +{ + QDateTime lastSyncTime = getTimestampFileDate(_penDataDir.filePath(".lastsync")); + QList changes = _pen->getChangeList(lastSyncTime); + + foreach(const Smartpen::ChangeReport &change, changes) { + qDebug() << "Synchronizing guid: " << change.guid << change.title; + if (!syncNotebook(change)) { + return false; + } + } + + setTimestampFileDate(_penDataDir.filePath(".lastsync")); + + return true; +} + +bool SmartpenSyncer::syncNotebook(const Smartpen::ChangeReport &change) +{ + QDir notebookDir(_penDataDir.filePath(change.title + ".afd")); + if (!notebookDir.exists()) { + if (!notebookDir.mkpath(".")) { + qWarning() << "Cannot create notebook data directory:" << notebookDir.absolutePath(); + } + } + + setTimestampFileDate(notebookDir.filePath(".sync.lck")); + + QDateTime lastSyncTime = getTimestampFileDate(notebookDir.filePath(".lastsync")); + QByteArray lspData = _pen->getLspData(change.guid, lastSyncTime); + + if (!extractZip(lspData, notebookDir)) { + return false; + } + + setTimestampFileDate(notebookDir.filePath(".lastsync")); + removeTimestampFile(notebookDir.filePath(".sync.lck")); + + return true; +} + +bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir) +{ + QBuffer zipBuffer(&zipData); + QuaZip zip(&zipBuffer); + QuaZipFile zipFile(&zip); + + if (!zip.open(QuaZip::mdUnzip)) { + qWarning() << "Could not open zip file"; + return false; + } + + QScopedArrayPointer buffer(new char[BUFFER_SIZE]); + + for (bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) { + QString zipName = zip.getCurrentFileName(); + if (!dir.absoluteFilePath(zipName).startsWith(dir.absolutePath())) { + qWarning() << "broken zip filename:" << zipName; + continue; + } + QFileInfo finfo(dir.filePath(zipName)); + if (!dir.mkpath(finfo.path())) { + qWarning() << "cannot mkpath for:" << finfo.absoluteFilePath(); + } + + if (zipName.endsWith('/')) { + // Nothing to do + continue; + } + + if (!zipFile.open(QIODevice::ReadOnly)) { + qWarning() << "cannot open zip file for reading:" << zipName; + continue; + } + + QFile file(finfo.filePath()); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning() << "cannot open for writing:" << finfo.absoluteFilePath(); + zipFile.close(); + continue; + } + + while (!zipFile.atEnd()) { + qint64 read = zipFile.read(buffer.data(), BUFFER_SIZE); + if (read <= 0) { + qWarning() << "short read on:" << zipName; + zipFile.close(); + continue; + } + qint64 written = file.write(buffer.data(), read); + if (written != read) { + qWarning() << "short write on:" << file.fileName(); + zipFile.close(); + continue; + } + } + + file.close(); + zipFile.close(); + } + + buffer.reset(); + + if (zip.getZipError() == UNZ_OK) { + return true; + } else { + qWarning() << "Error while decompressing"; + return false; + } +} diff --git a/smartpensyncer.h b/smartpensyncer.h new file mode 100644 index 0000000..6279485 --- /dev/null +++ b/smartpensyncer.h @@ -0,0 +1,39 @@ +#ifndef SMARTPENSYNCER_H +#define SMARTPENSYNCER_H + +#include +#include +#include "smartpen.h" + +class SmartpenSyncer : public QThread +{ + Q_OBJECT +public: + explicit SmartpenSyncer(const Smartpen::Address &addr, QObject *parent = 0); + ~SmartpenSyncer(); + + Smartpen::Address penAddress() const; + +signals: + +public slots: + void abort(); + +private: + void run(); + bool syncPen(); + bool syncNotebook(const Smartpen::ChangeReport &change); + bool extractZip(QByteArray &zipData, QDir &dir); + +private: + Smartpen::Address _addr; + Smartpen *_pen; + bool _errored; + bool _aborted; + + QString _penSerial; + QString _penName; + QDir _penDataDir; +}; + +#endif // SMARTPENSYNCER_H diff --git a/stfgraphicsitem.cc b/stfgraphicsitem.cc new file mode 100644 index 0000000..865a029 --- /dev/null +++ b/stfgraphicsitem.cc @@ -0,0 +1,67 @@ +#include + +#include "stfreader.h" +#include "stfgraphicsitem.h" + +class StfToGraphicsPathItems : public StfReader::StrokeHandler { + QGraphicsItem *parent; + QPainterPath path; + QRectF bound; + +public: + StfToGraphicsPathItems(QGraphicsItem* parent) + : parent(parent), path(), bound(0.0, 0.0, 1.0, 1.0) { + } + + ~StfToGraphicsPathItems() { + } + + bool startStroke(const QPoint& p, int, quint64) { + path = QPainterPath(QPointF(p)); + return true; + } + + bool strokePoint(const QPoint& p, int, quint64) { + path.lineTo(QPointF(p)); + return true; + } + + bool endStroke() { + bound |= path.boundingRect(); + new QGraphicsPathItem(path, parent); + /* Parent will take the child down with him when deleted. */ + return true; + } + + const QRectF& boundingRect() { + return bound; + } +}; + +StfGraphicsItem::StfGraphicsItem(QIODevice *dev, QGraphicsItem *parent) : + QGraphicsItem(parent) +{ + setFlags(ItemHasNoContents); + + StfToGraphicsPathItems h(this); + StfReader r; + r.setStrokeHandler(&h); + if (r.parse(dev)) { + bbox = h.boundingRect(); + } +} + +QRectF StfGraphicsItem::boundingRect() const +{ + return bbox; +} + +void StfGraphicsItem::paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) +{ + /* Intentionally empty; ItemHasNoContents; children will do the painting. */ +} + +int StfGraphicsItem::type() const +{ + return Type; +} diff --git a/stfgraphicsitem.h b/stfgraphicsitem.h new file mode 100644 index 0000000..ed62ede --- /dev/null +++ b/stfgraphicsitem.h @@ -0,0 +1,20 @@ +#ifndef STFGRAPHICSITEM_H +#define STFGRAPHICSITEM_H + +#include + +class StfGraphicsItem : public QGraphicsItem +{ + QRectF bbox; + +public: + explicit StfGraphicsItem(QIODevice *stf, QGraphicsItem *parent = 0); + + enum { Type = UserType + 's' }; + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + int type() const; +}; + +#endif // STFGRAPHICSITEM_H diff --git a/stfreader.cc b/stfreader.cc new file mode 100644 index 0000000..c5edff0 --- /dev/null +++ b/stfreader.cc @@ -0,0 +1,336 @@ +#include +#include + +#include "stfreader.h" + +#define TABLE_v(i, ...) static qint8 tab_ ## i [] = { __VA_ARGS__ } +#define TABLE static CodeTable table[] = { +#define TABLE_i(i) {i, sizeof(tab_ ## i), tab_ ## i}, +#define END_TABLE }; static int table_size = sizeof(table) / sizeof(CodeTable); + +StfReader::StfReader() +: handler(0) +{ +} + +StfReader::~StfReader() +{ +} + +StfReader::StrokeHandler::~StrokeHandler() +{ +} + +bool StfReader::parseV1(BitReader& br) +{ + quint64 cur_time = 0; + + while (!br.atEnd()) { + syncV1(br); + quint8 header = br.readBits(8); + quint64 time; + QPoint p0, pa; + int f0; + + switch (header) { + case 0x80: /* End of file. */ + return true; + break; + case 0: + time = br.readBits(8); + break; + case 0x08: + time = br.readBits(16); + break; + case 0x10: + time = br.readBits(32); + break; + case 0x18: + time = br.readBits(64); + break; + default: + qWarning("Unknown header 0x%x", header); + break; + } + + /* Start of a stroke. */ + cur_time += time; + p0.setX(br.readBits(16)); + p0.setY(br.readBits(16)); + f0 = readForce(br); + + if (handler) { + bool res = handler->startStroke(p0, f0, cur_time); + if (!res) return false; + } + + while (!br.atEnd()) { + header = readHeader(br); + if (header == 0 || header == 1) { + time = readTime(br); + } else { + int header2 = readHeader2(br); + switch (header2) { + case 0: + time = br.readBits(8); + break; + case 1: + time = br.readBits(16); + break; + case 2: + time = br.readBits(32); + break; + default: + qWarning("Unknown stroke time header %d", header2); + } + } + + if (time == 0) { + if (handler) { + bool res = handler->endStroke(); + if (!res) return false; + } + break; + } + + bool do_delta; + QPoint delta; + qint8 deltaf; + + if (header > 0) { + bool have_len = br.readBits(1); + if (have_len) { + do_delta = false; + QPoint p1; + p1.setX(br.readBits(16)); + p1.setY(br.readBits(16)); + pa = p1 - p0; + } else { + do_delta = true; + delta.setX(br.readBits(8)); + delta.setY(br.readBits(8)); + } + } else { + do_delta = true; + delta.setX(readDeltaX(br)); + delta.setY(readDeltaY(br)); + } + + deltaf = readDeltaF(br); + + if (do_delta) { + pa = delta + (pa * static_cast(time)) / 255; + } + + p0 += pa; + pa *= 256 / static_cast(time); + f0 += deltaf; + + if (handler) { + bool res = handler->strokePoint(p0, f0, cur_time); + if (!res) return false; + } + } + + } + + return false; +} + +void StfReader::syncV1(BitReader &br) +{ + br.skipUntilNextByte(); + while (!br.atEnd() && (br.peekBits(8) & ~(0x80|0x10|0x08))) { + br.readBits(8); + } +} + +qint8 StfReader::decodeV1(BitReader& br, CodeTable* tab, int tab_size) +{ + int got_bits = 0; + int codeacc = 0; + int stream = 0; + + for (int i = 0; i < tab_size; i++) { + int get_bits = tab[i].bits - got_bits; + codeacc <<= get_bits; + codeacc += tab[i].size; + + stream <<= get_bits; + stream |= br.readBits(get_bits); + + got_bits += get_bits; + + int idx = stream - codeacc; + if (idx < 0) { + idx = tab[i].size + idx; + Q_ASSERT(idx >= 0 && idx < tab[i].size); + return tab[i].data[idx]; + } + } + + qWarning("Unknown code"); + return 0; +} + +qint8 StfReader::readForce(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(6, 1,4,7,9,10,11,13,15,17,20,21,22,23,24,25,26,27,28,30); + TABLE_v(7, 2,3,5,6,8,12,14,16,18,19,29,31,32,33,34,35,36,49,52); + TABLE_v(8, 37,45,46,47,48,50,51,53,54); + TABLE_v(9, 38,39,40,41,44,55,56); + TABLE_v(10, 43,57,58); + TABLE_v(11, 42,59,60,61,62,63); + TABLE + TABLE_i(1) TABLE_i(6) TABLE_i(7) TABLE_i(8) TABLE_i(9) TABLE_i(10) TABLE_i(11) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readHeader(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(2, 1, 2); + TABLE + TABLE_i(1) TABLE_i(2) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readHeader2(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(2, 1, 3); + TABLE + TABLE_i(1) TABLE_i(2) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readHeader3(BitReader &br) +{ + TABLE_v(1, 0,1); + TABLE + TABLE_i(1) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readTime(BitReader &br) +{ + TABLE_v(1, 1); + TABLE_v(2, 2); + TABLE_v(4, 0,3,4); + TABLE_v(6, 5,6); + TABLE_v(7, 7,8); + TABLE_v(8, 9); + TABLE_v(9, 10,11); + TABLE_v(10, 12,13,14); + TABLE_v(11, 15); + TABLE_v(12, 16,17,18,19,21,22); + TABLE_v(13, 20,23,24,25,26,27); + TABLE_v(14, 28,29,30,31,32,35,36,37,40); + TABLE_v(15, 38,39,41,44,45,48,50,53,59,60,67,73,82,91,98,99,102); + TABLE_v(16, 33,34,42,43,46,47,49,51,52,54,55,56,57,58,61,62,63,64,65,66,68,69,70,71,72,74,75,76,77,78,79,80,81,83,84,85,86,87,88,89,90,92,93,94,95,96,97,100,101,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127); + TABLE + TABLE_i(1) TABLE_i(2) TABLE_i(4) TABLE_i(6) TABLE_i(7) TABLE_i(8) + TABLE_i(9) TABLE_i(10) TABLE_i(11) TABLE_i(12) TABLE_i(13) TABLE_i(14) + TABLE_i(15) TABLE_i(16) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readDeltaX(BitReader &br) +{ + TABLE_v(4, 3,4,5); + TABLE_v(5, 0,1,2,6,7,8,9,-8,-7,-6,-5,-4,-3,-2,-1); + TABLE_v(6, 10,11,12,13,-13,-12,-11,-10,-9); + TABLE_v(7, 14,15,16,17,18,19,-18,-17,-16,-15,-14); + TABLE_v(8, 20,21,22,23,24,26,-25,-24,-23,-22,-21,-20,-19); + TABLE_v(9, 25,27,28,29,30,31,32,33,34,36,38,-36,-34,-33,-32,-31,-30,-29,-28,-27,-26); + TABLE_v(10, 35,37,39,40,41,42,43,44,45,46,47,49,55,-57,-50,-47,-46,-44,-43,-42,-41,-40,-39,-38,-37,-35); + TABLE + TABLE_i(4) TABLE_i(5) TABLE_i(6) TABLE_i(7) TABLE_i(8) TABLE_i(9) TABLE_i(10) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readDeltaY(BitReader &br) +{ + TABLE_v(5, 0,1,2,3,4,5,6,7,8,-9,-8,-7,-6,-5,-4,-3,-2,-1); + TABLE_v(6, 9,10,11,12,13,-14,-13,-12,-11,-10); + TABLE_v(7, 14,15,16,17,18,19,20,-20,-19,-18,-17,-16,-15); + TABLE_v(8, 21,22,23,24,25,26,27,28,29,30,31,-27,-26,-25,-24,-23,-22,-21); + TABLE_v(9, 32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,49,-45,-44,-43,-40,-39,-38,-37,-36,-35,-34,-33,-32,-31,-30,-29,-28); + TABLE_v(10, 47,48,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,66,67,68,69,71,72,73,78,85,-75,-69,-64,-62,-61,-60,-59,-58,-57,-56,-55,-54,-53,-52,-51,-50,-49,-48,-47,-46,-42,-41); + + TABLE + TABLE_i(5) TABLE_i(6) TABLE_i(7) TABLE_i(8) TABLE_i(9) TABLE_i(10) + END_TABLE + + return decodeV1(br, table, table_size); +} + +qint8 StfReader::readDeltaF(BitReader &br) +{ + TABLE_v(1, 0); + TABLE_v(3, 1); + TABLE_v(4, -1); + TABLE_v(5, 2,-2); + TABLE_v(6, 3,-3); + TABLE_v(7, 4,52,53,-4); + TABLE_v(8, 5,6,49,50,51,54,55,-17,-16,-15,-14,-13,-12,-7,-6,-5); + TABLE_v(9, 7,8,9,10,11,12,13,30,31,36,37,38,39,40,41,46,48,56,57,-56,-55,-54,-53,-52,-51,-50,-49,-40,-38,-21,-20,-19,-18,-11,-10,-9,-8); + TABLE_v(10, 14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,32,33,34,35,42,43,44,45,47,58,-57,-48,-47,-46,-45,-44,-43,-42,-41,-39,-37,-36,-35,-34,-33,-32,-31,-29,-28,-27,-26,-25,-24,-23,-22); + TABLE_v(11, -59,-58,-30); + TABLE_v(12, 59,-64); + TABLE_v(13, 60,61,62,63,-63,-62,-61,-60); + TABLE + TABLE_i(1) TABLE_i(3) TABLE_i(4) TABLE_i(5) TABLE_i(6) TABLE_i(7) + TABLE_i(8) TABLE_i(9) TABLE_i(10) TABLE_i(11) TABLE_i(12) TABLE_i(13) + END_TABLE + + return decodeV1(br, table, table_size); +} + +bool StfReader::parse(QIODevice *device) +{ + char magic; + if (!device->getChar(&magic)) return false; + if (magic != 0x1) return false; + if (!device->getChar(&magic)) return false; + if (magic != 0x0) return false; + + QString version(device->read(14)); + if (version != "Anoto STF v1.0") { + return false; + } + + BitReader br(device); + + speed = br.readBits(16); + + return parseV1(br); +} + +bool StfReader::parse(const QString &filename) +{ + QFile f(filename); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open" << f.fileName(); + } + return parse(&f); +} + +void StfReader::setStrokeHandler(StrokeHandler *newHandler) +{ + Q_ASSERT(newHandler); + handler = newHandler; +} diff --git a/stfreader.h b/stfreader.h new file mode 100644 index 0000000..06013e1 --- /dev/null +++ b/stfreader.h @@ -0,0 +1,51 @@ +#ifndef STFREADER_H +#define STFREADER_H + +#include +#include "bitreader.h" + +class StfReader +{ +public: + class StrokeHandler + { + public: + virtual ~StrokeHandler(); + virtual bool startStroke(const QPoint& p, int force, quint64 time) = 0; + virtual bool strokePoint(const QPoint& p, int force, quint64 time) = 0; + virtual bool endStroke() = 0; + }; + +protected: + struct CodeTable { + int bits; + int size; + qint8 * data; + }; + + StrokeHandler *handler; + int speed; + + bool parseV1(BitReader& br); + void syncV1(BitReader& br); + qint8 decodeV1(BitReader& br, CodeTable* tab, int tab_size); + + qint8 readForce(BitReader& br); + qint8 readHeader(BitReader& br); + qint8 readHeader2(BitReader& br); + qint8 readHeader3(BitReader& br); + qint8 readTime(BitReader& br); + qint8 readDeltaX(BitReader& br); + qint8 readDeltaY(BitReader& br); + qint8 readDeltaF(BitReader& br); + +public: + StfReader(); + ~StfReader(); + + bool parse(QIODevice *device); + bool parse(const QString &filename); + void setStrokeHandler(StrokeHandler *newHandler); +}; + +#endif // STFREADER_H diff --git a/xmlutils.cc b/xmlutils.cc new file mode 100644 index 0000000..350949c --- /dev/null +++ b/xmlutils.cc @@ -0,0 +1,13 @@ +#include "xmlutils.h" + +bool advanceToFirstChildElement(QXmlStreamReader &r, const char *tag) +{ + while (r.readNextStartElement()) { + if (r.name() == tag) { + return true; + } else { + r.skipCurrentElement(); + } + } + return false; +} diff --git a/xmlutils.h b/xmlutils.h new file mode 100644 index 0000000..561b136 --- /dev/null +++ b/xmlutils.h @@ -0,0 +1,8 @@ +#ifndef XMLUTILS_H +#define XMLUTILS_H + +#include + +bool advanceToFirstChildElement(QXmlStreamReader &r, const char *tag); + +#endif // XMLUTILS_H -- cgit v1.2.3