From 22123c85f3722abad896aebb564a89d88da92e81 Mon Sep 17 00:00:00 2001 From: sje397 Date: Fri, 11 Nov 2011 01:20:17 +1100 Subject: [PATCH] Added QRCode generation functions via libqrencode. Switch on with USE_QRENCODE=1. Amended build docs for Linux and OSX, and OSX makefile. Added package 'qrencode' to gitian.yml --- bitcoin-qt.pro | 14 ++ contrib/gitian-descriptors/gitian.yml | 1 + doc/build-osx.txt | 3 + doc/build-unix.txt | 20 ++- src/makefile.osx | 4 + src/qt/addressbookpage.cpp | 30 +++++ src/qt/addressbookpage.h | 1 + src/qt/bitcoin.qrc | 1 + src/qt/forms/addressbookpage.ui | 11 ++ src/qt/forms/qrcodedialog.ui | 213 +++++++++++++++++++++++++++++++++ src/qt/qrcodedialog.cpp | 106 ++++++++++++++++ src/qt/qrcodedialog.h | 37 ++++++ src/qt/res/images/qrcode.png | Bin 0 -> 5993 bytes 13 files changed, 434 insertions(+), 7 deletions(-) create mode 100644 src/qt/forms/qrcodedialog.ui create mode 100644 src/qt/qrcodedialog.cpp create mode 100644 src/qt/qrcodedialog.h create mode 100644 src/qt/res/images/qrcode.png diff --git a/bitcoin-qt.pro b/bitcoin-qt.pro index 853f2fa..4a8dedc 100644 --- a/bitcoin-qt.pro +++ b/bitcoin-qt.pro @@ -19,6 +19,14 @@ OBJECTS_DIR = build MOC_DIR = build UI_DIR = build +# use: qmake "USE_QRCODE=1" +# libqrencode (http://fukuchi.org/works/qrencode/index.en.html) must be installed for support +contains(USE_QRCODE, 1) { + message(Building with QRCode support) + DEFINES += USE_QRCODE + LIBS += -lqrencode +} + # use: qmake "RELEASE=1" contains(RELEASE, 1) { # Mac: compile for maximum compatibility (10.5, 32-bit) @@ -199,6 +207,12 @@ FORMS += \ src/qt/forms/sendcoinsentry.ui \ src/qt/forms/askpassphrasedialog.ui +contains(USE_QRCODE, 1) { +HEADERS += src/qt/qrcodedialog.h +SOURCES += src/qt/qrcodedialog.cpp +FORMS += src/qt/forms/qrcodedialog.ui +} + CODECFORTR = UTF-8 # for lrelease/lupdate diff --git a/contrib/gitian-descriptors/gitian.yml b/contrib/gitian-descriptors/gitian.yml index 65005c6..6f503ac 100644 --- a/contrib/gitian-descriptors/gitian.yml +++ b/contrib/gitian-descriptors/gitian.yml @@ -16,6 +16,7 @@ packages: - "libssl-dev" - "git-core" - "unzip" +- "qrencode" reference_datetime: "2011-01-30 00:00:00" remotes: - "url": "https://github.com/bitcoin/bitcoin.git" diff --git a/doc/build-osx.txt b/doc/build-osx.txt index 8002441..d47febe 100644 --- a/doc/build-osx.txt +++ b/doc/build-osx.txt @@ -43,6 +43,9 @@ pushd bitcoin/contrib/minipupnpc; sudo port install; popd (this will be unnecessary soon, you will just port install miniupnpc along with the rest of the dependencies). +Optionally install qrencode (and set USE_QRCODE=1): +sudo port install qrencode + 4. Now you should be able to build bitcoind: cd bitcoin/src diff --git a/doc/build-unix.txt b/doc/build-unix.txt index f4178ca..5389339 100644 --- a/doc/build-unix.txt +++ b/doc/build-unix.txt @@ -23,12 +23,13 @@ the graphical bitcoin. Dependencies ------------ - Library Purpose Description - ------- ------- ----------- - libssl SSL Support Secure communications - libdb4.8 Berkeley DB Blockchain & wallet storage - libboost Boost C++ Library - miniupnpc UPnP Support Optional firewall-jumping support + Library Purpose Description + ------- ------- ----------- + libssl SSL Support Secure communications + libdb4.8 Berkeley DB Blockchain & wallet storage + libboost Boost C++ Library + miniupnpc UPnP Support Optional firewall-jumping support + libqrencode QRCode generation Optional QRCode generation miniupnpc may be used for UPnP port mapping. It can be downloaded from http://miniupnp.tuxfamily.org/files/. UPnP support is compiled in and @@ -37,6 +38,12 @@ turned off by default. Set USE_UPNP to a different value to control this: USE_UPNP=0 (the default) UPnP support turned off by default at runtime USE_UPNP=1 UPnP support turned on by default at runtime +libqrencode may be used for QRCode image generation. It can be downloaded +from http://fukuchi.org/works/qrencode/index.html.en, or installed via +your package manager. Set USE_QRCODE to control this: + USE_QRCODE=0 (the default) No QRCode support - libarcode not required + USE_QRCODE=1 QRCode support enabled + Licenses of statically linked libraries: Berkeley DB New BSD license with additional requirement that linked software must be free open source @@ -50,7 +57,6 @@ Versions used in this release: Boost 1.37 miniupnpc 1.6 - Dependency Build Instructions: Ubuntu & Debian ---------------------------------------------- sudo apt-get install build-essential diff --git a/src/makefile.osx b/src/makefile.osx index 4b0b521..2bb8980 100644 --- a/src/makefile.osx +++ b/src/makefile.osx @@ -95,6 +95,10 @@ else endif endif +ifdef USE_QRCODE + DEFS += -DUSE_QRCODE=$(USE_QRCODE) + LIBS += -lqrencode +endif all: bitcoind diff --git a/src/qt/addressbookpage.cpp b/src/qt/addressbookpage.cpp index 0a147c9..d207fe3 100644 --- a/src/qt/addressbookpage.cpp +++ b/src/qt/addressbookpage.cpp @@ -10,6 +10,10 @@ #include #include +#ifdef USE_QRCODE +#include "qrcodedialog.h" +#endif + AddressBookPage::AddressBookPage(Mode mode, Tabs tab, QWidget *parent) : QDialog(parent), ui(new Ui::AddressBookPage), @@ -25,6 +29,10 @@ AddressBookPage::AddressBookPage(Mode mode, Tabs tab, QWidget *parent) : ui->deleteButton->setIcon(QIcon()); #endif +#ifndef USE_QRCODE + ui->showQRCode->setVisible(false); +#endif + switch(mode) { case ForSending: @@ -169,10 +177,12 @@ void AddressBookPage::selectionChanged() break; } ui->copyToClipboard->setEnabled(true); + ui->showQRCode->setEnabled(true); } else { ui->deleteButton->setEnabled(false); + ui->showQRCode->setEnabled(false); ui->copyToClipboard->setEnabled(false); } } @@ -227,3 +237,23 @@ void AddressBookPage::exportClicked() QMessageBox::Abort, QMessageBox::Abort); } } + +void AddressBookPage::on_showQRCode_clicked() +{ +#ifdef USE_QRCODE + QTableView *table = ui->tableView; + QModelIndexList indexes = table->selectionModel()->selectedRows(AddressTableModel::Address); + + + QRCodeDialog *d; + foreach (QModelIndex index, indexes) + { + QString address = index.data().toString(), + label = index.sibling(index.row(), 0).data().toString(), + title = QString("%1 << %2 >>").arg(label).arg(address); + + QRCodeDialog *d = new QRCodeDialog(title, address, label, tab == ReceivingTab, this); + d->show(); + } +#endif +} diff --git a/src/qt/addressbookpage.h b/src/qt/addressbookpage.h index 1a97f3d..2538f31 100644 --- a/src/qt/addressbookpage.h +++ b/src/qt/addressbookpage.h @@ -54,6 +54,7 @@ private slots: void on_newAddressButton_clicked(); void on_copyToClipboard_clicked(); void selectionChanged(); + void on_showQRCode_clicked(); }; #endif // ADDRESSBOOKDIALOG_H diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index aea61d6..5693ae1 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -41,6 +41,7 @@ res/images/about.png res/images/splash2.jpg + res/images/qrcode.png res/movies/update_spinner.mng diff --git a/src/qt/forms/addressbookpage.ui b/src/qt/forms/addressbookpage.ui index fb098c8..9b301cb 100644 --- a/src/qt/forms/addressbookpage.ui +++ b/src/qt/forms/addressbookpage.ui @@ -80,6 +80,17 @@ + + + Show &QR Code + + + + :/images/qrcode:/images/qrcode + + + + Delete the currently selected address from the list. Only sending addresses can be deleted. diff --git a/src/qt/forms/qrcodedialog.ui b/src/qt/forms/qrcodedialog.ui new file mode 100644 index 0000000..fa21f60 --- /dev/null +++ b/src/qt/forms/qrcodedialog.ui @@ -0,0 +1,213 @@ + + + QRCodeDialog + + + + 0 + 0 + 320 + 404 + + + + Dialog + + + + + + + 0 + 0 + + + + + 300 + 300 + + + + QR Code + + + Qt::AlignCenter + + + + + + + + + + + + + + true + + + Request Payment + + + + + + + + + + 0 + 0 + + + + Amount: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + lnReqAmount + + + + + + + false + + + + 60 + 0 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + BTC + + + lnReqAmount + + + + + + + + + + + + + Label: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + lnLabel + + + + + + + + 100 + 0 + + + + + + + + Message: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + lnMessage + + + + + + + + 100 + 0 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Save As... + + + + + + + + + + + + + + chkReq + clicked(bool) + lnReqAmount + setEnabled(bool) + + + 92 + 285 + + + 98 + 311 + + + + + diff --git a/src/qt/qrcodedialog.cpp b/src/qt/qrcodedialog.cpp new file mode 100644 index 0000000..ed4c758 --- /dev/null +++ b/src/qt/qrcodedialog.cpp @@ -0,0 +1,106 @@ +#include "qrcodedialog.h" +#include "ui_qrcodedialog.h" +#include +#include +#include +#include +#include + +#include + +#define EXPORT_IMAGE_SIZE 256 + +QRCodeDialog::QRCodeDialog(const QString &title, const QString &addr, const QString &label, bool enableReq, QWidget *parent) : + QDialog(parent), + ui(new Ui::QRCodeDialog), + address(addr) +{ + ui->setupUi(this); + setWindowTitle(title); + setAttribute(Qt::WA_DeleteOnClose); + + ui->chkReq->setVisible(enableReq); + ui->lnReqAmount->setVisible(enableReq); + ui->lblAm1->setVisible(enableReq); + ui->lblAm2->setVisible(enableReq); + + ui->lnLabel->setText(label); + + genCode(); +} + +QRCodeDialog::~QRCodeDialog() +{ + delete ui; +} + +void QRCodeDialog::genCode() { + + QString uri = getURI(); + //qDebug() << "Encoding:" << uri.toUtf8().constData(); + QRcode *code = QRcode_encodeString(uri.toUtf8().constData(), 0, QR_ECLEVEL_L, QR_MODE_8, 1); + myImage = QImage(code->width + 8, code->width + 8, QImage::Format_RGB32); + myImage.fill(0xffffff); + unsigned char *p = code->data; + for(int y = 0; y < code->width; y++) { + for(int x = 0; x < code->width; x++) { + myImage.setPixel(x + 4, y + 4, ((*p & 1) ? 0x0 : 0xffffff)); + p++; + } + } + QRcode_free(code); + ui->lblQRCode->setPixmap(QPixmap::fromImage(myImage).scaled(300, 300)); +} + +QString QRCodeDialog::getURI() { + QString ret = QString("bitcoin:%1").arg(address); + + int paramCount = 0; + if(ui->chkReq->isChecked() && ui->lnReqAmount->text().isEmpty() == false) { + bool ok= false; + double amount = ui->lnReqAmount->text().toDouble(&ok); + if(ok) { + ret += QString("?amount=%1X8").arg(ui->lnReqAmount->text()); + paramCount++; + } + } + + if(ui->lnLabel->text().isEmpty() == false) { + QString lbl(QUrl::toPercentEncoding(ui->lnLabel->text())); + ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl); + paramCount++; + } + + if(ui->lnMessage->text().isEmpty() == false) { + QString msg(QUrl::toPercentEncoding(ui->lnMessage->text())); + ret += QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg); + paramCount++; + } + + return ret; +} + +void QRCodeDialog::on_lnReqAmount_textChanged(const QString &) { + genCode(); +} + +void QRCodeDialog::on_lnLabel_textChanged(const QString &) { + genCode(); +} + +void QRCodeDialog::on_lnMessage_textChanged(const QString &) { + genCode(); +} + +void QRCodeDialog::on_btnSaveAs_clicked() +{ + QString fn = QFileDialog::getSaveFileName(this, "Save Image...", QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation), "Images (*.png)"); + if(!fn.isEmpty()) { + myImage.scaled(EXPORT_IMAGE_SIZE, EXPORT_IMAGE_SIZE).save(fn); + } +} + +void QRCodeDialog::on_chkReq_toggled(bool) +{ + genCode(); +} diff --git a/src/qt/qrcodedialog.h b/src/qt/qrcodedialog.h new file mode 100644 index 0000000..7463a88 --- /dev/null +++ b/src/qt/qrcodedialog.h @@ -0,0 +1,37 @@ +#ifndef QRCODEDIALOG_H +#define QRCODEDIALOG_H + +#include +#include + +namespace Ui { + class QRCodeDialog; +} + +class QRCodeDialog : public QDialog +{ + Q_OBJECT + +public: + explicit QRCodeDialog(const QString &title, const QString &address, const QString &label, bool allowReq, QWidget *parent = 0); + ~QRCodeDialog(); + +private slots: + void on_lnReqAmount_textChanged(const QString &arg1); + void on_lnLabel_textChanged(const QString &arg1); + void on_lnMessage_textChanged(const QString &arg1); + void on_btnSaveAs_clicked(); + + void on_chkReq_toggled(bool checked); + +private: + Ui::QRCodeDialog *ui; + QImage myImage; + + QString getURI(); + QString address; + + void genCode(); +}; + +#endif // QRCODEDIALOG_H diff --git a/src/qt/res/images/qrcode.png b/src/qt/res/images/qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..c89a49bbceba56e1901487b970645dd9eea95608 GIT binary patch literal 5993 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