15.1.91. tablet_qt/dbobjects/blob.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS 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.

    CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

// #define DEBUG_ROTATION

#include "blob.h"
#include <QTransform>
#include "db/databasemanager.h"
#include "lib/convert.h"

const QString Blob::TABLENAME("blobs");  // as per DBCONSTANTS.js

const QString Blob::SRC_TABLE_FIELDNAME("tablename");  // as per Blob.js
const QString Blob::SRC_PK_FIELDNAME("tablepk");  // as per Blob.js
const QString SRC_FIELD_FIELDNAME("fieldname");  // as per Blob.js
const QString FILENAME_FIELDNAME("filename");  // as per Blob.js
const QString MIMETYPE_FIELDNAME("mimetype");  // new in v2.0.0
const QString BLOB_FIELDNAME("theblob"); // as per dbupload.js
// ... was a "virtual" field under Titanium and file-based BLOBs.
const QString ROTATION_FIELDNAME("image_rotation_deg_cw");
// ... rotation is anticlockwise for "x up, y up", but clockwise for "y down",
//     which is the computing norm.


Blob::Blob(CamcopsApp& app,
           DatabaseManager& db,
           const QString& src_table,
           const int src_pk,
           const QString& src_field) :
    DatabaseObject(app,
                   db,
                   TABLENAME,
                   dbconst::PK_FIELDNAME,
                   true,  // modification timestamp
                   false)  // creation timestamp
{
    // ------------------------------------------------------------------------
    // Define fields
    // ------------------------------------------------------------------------
    addField(SRC_TABLE_FIELDNAME, QMetaType::fromType<QString>(), true);
    addField(SRC_PK_FIELDNAME, QMetaType::fromType<int>(), true);
    addField(SRC_FIELD_FIELDNAME, QMetaType::fromType<QString>(), true);
    addField(FILENAME_FIELDNAME, QMetaType::fromType<QString>());
    addField(MIMETYPE_FIELDNAME, QMetaType::fromType<QString>());
    // ... maximum length 255; https://stackoverflow.com/questions/643690
    addField(BLOB_FIELDNAME, QMetaType::fromType<QByteArray>());
    addField(ROTATION_FIELDNAME, QMetaType::fromType<int>());

    // ------------------------------------------------------------------------
    // Load from database (or create/save), unless this is a specimen
    // ------------------------------------------------------------------------
    if (!src_table.isEmpty() && !src_field.isEmpty() && src_pk >= 0) {
        // Not a specimen; load, or set defaults and save
        WhereConditions where;
        where.add(SRC_TABLE_FIELDNAME, src_table);
        where.add(SRC_PK_FIELDNAME, src_pk);
        where.add(SRC_FIELD_FIELDNAME, src_field);
        bool success = load(where);  // will load the BLOB, if present
        if (!success) {
            setValue(SRC_TABLE_FIELDNAME, src_table);
            setValue(SRC_PK_FIELDNAME, src_pk);
            setValue(SRC_FIELD_FIELDNAME, src_field);
            save();
        }
    }

    // We could extend DatabaseObject::makeTable to call subclasses for extra
    // index/constraint requirements. Specifically, they need to go into the
    // CREATE TABLE syntax for SQLite:
    //      http://www.sqlite.org/syntaxdiagrams.html#table-constraint
    // However, we can equally implement the constraint by not screwing up the
    // C++ code, which is perhaps simpler (because: if the database implements
    // the code, then duff C++ code has its bacon partly saved by the database,
    // but leaving a potentially ambiguous state from the C++ perspective when
    // insertion overwrites rather than creatng.

    m_filename_stem = QString("blob_%1_%2_%3").arg(src_table)
                                              .arg(src_pk)
                                              .arg(src_field);
    // ... as per Blob.js

    m_image_loaded_from_data = false;
}


Blob::~Blob()
{
}


bool Blob::setBlob(const QVariant& value,
                   const bool save_to_db,
                   const QString& extension_without_dot,
                   const QString& mimetype)
{
    bool changed = setValue(BLOB_FIELDNAME, value);
    changed = setValue(FILENAME_FIELDNAME,
                       QString("%1.%2").arg(m_filename_stem,
                                            extension_without_dot)) || changed;
    changed = setValue(MIMETYPE_FIELDNAME, mimetype) || changed;
    changed = setValue(ROTATION_FIELDNAME, 0) || changed;

    if (save_to_db) {
        save();
    }
    m_image = QImage();  // clear cached image
    return changed;
}


QVariant Blob::blobVariant() const
{
    return value(BLOB_FIELDNAME);
}


QByteArray Blob::blobByteArray() const
{
    return valueByteArray(BLOB_FIELDNAME);
}


void Blob::makeIndexes()
{
    m_db.createIndex("_idx_blob_srctable_srcpk_srcfield",
                     TABLENAME,
                     {SRC_TABLE_FIELDNAME,
                      SRC_PK_FIELDNAME,
                      SRC_FIELD_FIELDNAME});
}


void Blob::rotateCachedImage(int angle_degrees_clockwise) const
{
    // https://doc.qt.io/qt-6.5/qtransform.html#rotate
    angle_degrees_clockwise %= 360;
    if (angle_degrees_clockwise == 0 || m_image.isNull()) {
        return;
    }
    QTransform matrix;
    matrix.rotate(angle_degrees_clockwise);
#ifdef DEBUG_ROTATION
    qDebug().nospace() << "Blob::rotateImage: rotating image of size "
                       << m_image.size() << "...";
#endif
    m_image = m_image.transformed(matrix);
#ifdef DEBUG_ROTATION
    qDebug() << "Blob::rotateImage: ... rotated to image of size"
             << m_image.size();
#endif
}


QImage Blob::image(bool* p_loaded) const
{
    if (m_image.isNull()) {
        m_image = convert::byteArrayToImage(blobByteArray(),
                                            &m_image_loaded_from_data);
        const int angle_deg_cw = valueInt(ROTATION_FIELDNAME);
        rotateCachedImage(angle_deg_cw);
    }
    if (p_loaded) {
        *p_loaded = m_image_loaded_from_data;
    }
    return m_image;
}


void Blob::rotateImage(const int angle_degrees_clockwise,
                       const bool save_to_db)
{
    int rotation = valueInt(ROTATION_FIELDNAME);
    rotation = (rotation + angle_degrees_clockwise) % 360;
    setValue(ROTATION_FIELDNAME, rotation);
    if (save_to_db) {
        save();
    }
    // We may have cached an image, so rotate that too:
    rotateCachedImage(angle_degrees_clockwise);
}


bool Blob::setImage(const QImage& image, const bool save_to_db)
{
    m_image = image;
    m_image_loaded_from_data = true;
    const QVariant value = convert::imageToVariant(image);
    bool changed = setBlob(value, save_to_db, "png", "image/png");
    return changed;
}


bool Blob::setRawImage(const QByteArray& data,
                       const bool save_to_db,
                       const QString& extension_without_dot,
                       const QString& mimetype)
{
    return setBlob(data,  // will autoconvert from QByteArray to QVariant
                   save_to_db, extension_without_dot, mimetype);
}