28.02.2021

Фото из Android смартфона в Qt Widgets

Если у вас нет времени читать или информация известна, окончательный код получения полноразмерного изображения из камеры Android-смартфона расположен в конце статьи.

Описание проблемы

Если вы пишете кросс-платформенное приложение, то для получения изображения из камеры для ПК можно воспользоваться классом QCamera, пример для работы с которым описан в документации Qt.

В соответствии с указанным примером мы добавляем в .pro файл

QT += multimedia multimediawidgets

Далее создаём виджет в своей программе, отображающий изображение из веб-камеры и сохраняющий его в QPixmap или QImage для дальнейшего использования.

Когда возникает задача сделать то-же самое на Android, то выясняется, что multimediawidgets не поддерживаются данной ОС и камера снимать и сохранять снимки будет, но что она отображает в текущий момент будет загадкой, т. к. QCameraViewfinder использует multimediawidgets и на Android не отображает ничего. Дальнейший поиск решения проблемы приводит к двум вариантам решения:

  1. использовать QML и написать свой Qt Quick-элемент, выполняющий эту функцию, затем состыковать его остальной частью приложения на Qt Widgets, С++;

  2. использовать приложение по-умолчанию Android-смартфона для получения фотографии, затем обработать её в своём приложении.

Рассмотрим первый вариант

Если вы С++ программист Qt Widgets, то очередное эпизодическое углубление в QML займёт у вас время, добавим к этому время на написание Qt Quick-элемента, стыковки этого элемента с С++ кодом, отладки написанного кода. Если вы не профессионал в QML получается долго и сложно.

Рассмотрим второй вариант

В Android-смартфоне уже есть приложение по-умолчанию, прекрасно выполняющее нужную функцию, нужно им просто воспользоваться, применив Java-вызовы (JNI — Java Native Interface) из С++ кода при помощи QtAndroid. Выглядит проще. Полностью работающего кода в интернете я не нашёл, и, изучив опыт других, опираясь на документацию разработчка на Android написал собственный.

Как это сделать

Если у вас нет времени читать или информация известна, окончательный код получения полноразмерного изображения из камеры Android-смартфона расположен в конце статьи.

Прочитав статью Получить фотографии на Android я сделал вывод, что применив данный метод можно получить или миниатюру изображения в виде массива пикселей или сохранить изображение в файл. Поискав готовые решения, я нашёл на GitHub подходящий код, который должен был выполнить нужную мне задачу. При его проверке, оказалось, что он устарел и теперь приводит к FileUriExposedException исключению, причины возникновения которого описаны в вышеуказанной ссылке на статью.

Чтобы разобраться самостоятельно, как оно работает, начнём с простой задачи, не требующей обращения к файлам и множества Java-вызовов — получению миниатюры.

Получение миниатюры

Начнём с .pro файла.

Он должен содержать следующие строки для поддержки Android.

android {
    QT       +=androidextras
}

Для получения результата нам понадобится класс, унаследованный от QAndroidActivityResultReceiver. Если требуется, чтобы объект нашего класса высылал изображение при помощи сигнала, то он также должен быть унаследован от любого класса Qt, имеющего базовый класс QObject.

Заголовочный файл (.h) класса имеет вид:

#ifndef CAMSHOT_H
#define CAMSHOT_H
#include <QObject>
#include <QString>
#include <cstring>
#include <QImage>
#include <QDebug>
#include <QtAndroid>
#include <QAndroidActivityResultReceiver>
#include <QAndroidParcel>
class CamShot : public QObject, public QAndroidActivityResultReceiver
{
    Q_OBJECT
public:
    CamShot(QObject *parent = nullptr):QObject(parent),QAndroidActivityResultReceiver(){}
    
    static const int RESULT_OK = -1; 
    static const int REQUEST_IMAGE_CAPTURE = 1;
    static const int REQUEST_TAKE_PHOTO = REQUEST_IMAGE_CAPTURE;
    void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data)  override;
    static QImage camThumbnailToQImage(const QAndroidJniObject &data);
public slots:
    void aMakeShot();
signals:
    void createNew(const QImage &img);
};

#endif // CAMSHOT_H

Заголовочный файл (.cpp) класса имеет вид:

QImage CamShot::camThumbnailToQImage(const QAndroidJniObject &data){
    QAndroidJniObject bundle = data.callObjectMethod("getExtras","()Landroid/os/Bundle;");
    qDebug()<<"bundle.isValid() "<<bundle.isValid()<<bundle.toString();
    QAndroidJniObject bundleKey = QAndroidJniObject::fromString("data");
    const QAndroidJniObject aBitmap (data.callObjectMethod("getParcelableExtra", "(Ljava/lang/String;)Landroid/os/Parcelable;", bundleKey.object<jstring>()));
    qDebug()<<"aBitmap.isValid() "<<aBitmap.isValid()<<aBitmap.toString();
    jint aBitmapWidth = aBitmap.callMethod<jint>("getWidth");
    jint aBitmapHeight = aBitmap.callMethod<jint>("getHeight");
    QAndroidJniEnvironment env;
    const int32_t aBitmapPixelsCount = aBitmapWidth * aBitmapHeight;
    jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
    jint aBitmapOffset = 0;
    jint aBitmapStride = aBitmapWidth;
    jint aBitmapX = 0;
    jint aBitmapY = 0;
    aBitmap.callMethod<void>("getPixels","([IIIIIII)V", pixels, aBitmapOffset, aBitmapStride, aBitmapX, aBitmapY, aBitmapWidth, aBitmapHeight);
    jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
    QImage img(aBitmapWidth, aBitmapHeight, QImage::Format_ARGB32);
    int lineSzB = aBitmapWidth * sizeof(jint);
    for (int i = 0; i < aBitmapHeight; ++i){
        uchar *pDst = img.scanLine(i);
        const uchar *pSrc = reinterpret_cast<const uchar*>(pPixels + aBitmapWidth * i + aBitmapWidth);
        memcpy(pDst, pSrc, lineSzB);
    }
    env->DeleteLocalRef(pixels); //env->ReleaseIntArrayElements(pixels, pPixels, 0); отвязывает указатель на данные массива от массива, а надо удалить сам массив, поэтому DeleteLocalRef.

    return img;
}
void CamShot::aMakeShot() {
    QAndroidJniObject action = QandroidJniObject::fromString("android.media.action.IMAGE_CAPTURE");
    //Если необходимо указать Java-класс (не аргумент функции), то указывается полное имя класса (точки-разделители заменяются на "/"), например  "android/content/Intent", "java/lang/String".
    //Если аргумент функции Java-объект, то писать имя класса начиная с "L" и ";" в конце, например "Landroid/content/Intent ;", "Ljava/lang/String;".
    //Если примитивный тип или массив, то указываются соответствующие символы без разделителей, например "V" (void) или "[IIIIIII" (массив jint, и 6 jint за ним)
    //Символы, соответствия примитивны типам:
    QAndroidJniObject intent=QAndroidJniObject("android/content/Intent","(Ljava/lang/String;)V", action.object<jstring>());
    QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);
}
void CamShot::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data){
    if ( receiverRequestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK )
    {
        const QImage thumbnail (camThumbnailToQImage(data));
        if (!thumbnail.isNull())
            emit createNew(thumbnail);
    }
}

Разберём приведённый код

Краткие правила указания аргументов в JNI-вызовах

  1. если необходимо указать имя Java-класса (не в качестве аргумента Java-функции), то указывается полное имя класса (точки-разделители заменяются на "/"), например "android/content/Intent", "java/lang/String";

  2. если аргумент функции Java-объект, то писать имя его класса начиная с "L" и ";" в конце, например "Landroid/content/Intent;", "Ljava/lang/String;";

  3. если примитивный тип или массив, то указываются соответствующие сигнатуры (символы без разделителей), например "V" (void), "I" (jint) или "[IIIIIII" (массив jint, и 6 jint за ним);

  4. сигнатуры примитивных типов:

    C/C++

    JNI

    Java

    Signature

    uint8_t/unsigned char

    jboolean

    bool

    Z

    int8_t/char/signed char

    jbyte

    byte

    B

    uint16_t/unsigned short

    jchar

    char

    C

    int16_t/short

    jshort

    short

    S

    int32_t/int/(long)

    jint

    int

    I

    int64_t/(long)/long long

    jlong

    long

    J

    float

    jfloat

    float

    F

    double

    jdouble

    double

    D

    void

    void

    V

  5. сигнатуры массивов:

    JNI

    Java

    Signature

    jbooleanArray

    bool[]

    [Z

    jbyteArray

    byte[]

    [B

    jcharArray

    char[]

    [C

    jshortArray

    short[]

    [S

    jintArray

    int[]

    [I

    jlongArray

    long[]

    [L

    jfloatArray

    float[]

    [F

    jdoubleArray

    double[]

    [D

    jarray

    type[]

    [Lfully/qualified/type/name;

    jarray

    String[]

    [Ljava/lang/String;


    Чтобы получить доступ к элементам массива, необходимо использовать JNI-методы объекта класса QAndroidJniEnvironment, например такие как: NewIntArray, GetIntArrayElements, DeleteLocalRef GetArrayLength,GetObjectArrayElement, SetObjectArrayElement, и т.д.

Подробнее можно прочитать в презентации (pdf) Practical Qt on Android JNI — qtcon.

В заголовочном файле class CamShot содержит:

  1. значения констант, взятых их документации разработчка Android (так код короче и меньше Java-вызовов);

  2. переопределение абстрактного метода void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override; в который будет передаваться Java-объект класса Intent с миниатюрой изображения;

  3. статический метод
    static QImage camThumbnailToQImage(const QAndroidJniObject &data);
    извлекающий из Java-объекта класса Intent Java-объект класса Bitmap, копирующий пиксели в массив пкселей (32-битных значений) и построчно копирующий эти пиксели в QImage;

  4. общедоступный слот
    void aMakeShot();
    вызывающий операцию по фотографированию изображения и получению его миниатюры;

  5. сигнал
    void createNew(const QImage &img);
    высылающий полученную миниатюру потребителю.

В методе void aMakeShot() создаётся Java-объект Intent в который передаётся строка со значением, указывающим, что необходимо сделать — произвести захват изображения. После этого сформированное действие (Intent) отправляется на исполнение (Activity).

В процессе выполнения действия будет запущено приложение по-умолчанию для фотографирования. Как только фотография будет сделана и подтверждена пользователем, будет произведён вызов виртуального метода handleActivityResult, в котором осуществляется проверка: является ли выполненное действие запрошенным и успешно выполненным. Если да, то вызовем статический метод camThumbnailToQImage получения изображения QImage из Java-объекта класса Bitmap и при успешном результате отправим полученное изображение потребителю сигналом Qt.

Рассмотрим статический метод
static QImage camThumbnailToQImage(const QAndroidJniObject &data) override;

Интересующее нас изображение передаётся в блоке дополнительных данных Java-объекта класса Intent и является Java-объектом класса Bundle, чтобы его получить нужно воспользоваться методом объекта Intent:
Bundle getExtras()

В Bundle хранятся ассоциативные пары <ключ-строка>:<значение>. В статье получить фотографии на Android указан ключ, по которому располагается миниатюра. Это строка "data".

Получим Java-объект класса Bitmap по ключу, воспользовавшись методом объекта Intent:
T getParcelableExtra (String name)

Cобрать из Bitmap QImage сразу не получится, т. к. у Bitmap нет указателя на данные изображения вместе с заголовком его формата. Поэтому получим его размер (ширину и высоту) в пискселях и создадим QImage аналогичного размера для копирования значений пикселей в него.

Для переноса значений пикселей из Bitmap в QImage воспользуемся методом объекта Bitmap:
void getPixels (int[] pixels, int offset, int stride, int x, int y, int width, int height)

Для этого понадобится создать линейный массив значений пикселей
jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
После того, как пиксели будут скопированы, получим указатель на начало массива, который можно использовать в C++ коде:
jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
Затем в цикле построчно скопируем значения пикселей из массива в изображение Qimage. По завершению копирования освобождаем память, выделенную под массив значений пикселей
env->DeleteLocalRef(pixels);
и возвращаем результат в виде QImage.

Отлично. Миниатюра изображения получена.

Получение полноразмерного изображения

Для получения полноразмерного изображения необходимо воспользоваться классом FileProvider, чтобы получить разделяемый Uri для файла фотоснимка. Обращаю ваше внимание, что у Android, по крайней мере, их два:

  1. androidx.core.content.FileProvider;

  2. android.support.v4.content.FileProvider.

Первый — самый современный, не поддерживается Qt, а для использования второго необходимо настроить среду QtCerator:

Установить дополнительные репозитории

Главное меню (сверху)→ «Инструменты» → «Параметры» → «Устройства»→ вкладка «Android»→ вкладка «SDK Manager»→Развернуть элемент списка «Инструменты» в список→ «Extras»→ «Android Support Repository» - поставить флажок установить и нажить на кнопку «Применить» справа.

Заменить автогенерируемые файлы настройки сборки для Android собственными

Перейти на боковой панели QtCreator на вкладку «Проекты». В левой области окна «Сборка и запуск»→ «Сборка». Тогда в правой области окна «Build Android APK» → «Create Templates». В появившемся диалоговом окне установить флажок «Копировать файлы Gradle в каталог Android», нажать на кнопку «Завершить»:

Добавить каталог со своими настройками сборки в проект

В каталоге с исходными кодами вашего приложения появится каталог «android», который необходимо добавить в проект.

Настроить в файле проекта отключаемую возможность поддержки Android

Если приложение кросс-платформенное и предполагается компиляция не только на Android, то в .pro файле необходимо добавить директиву android: перед каждым добавленным файлом:

android {
    QT       +=androidextras
}
# … 
DISTFILES += \
android:    android/AndroidManifest.xml \
android:    android/build.gradle \
android:    android/gradle/wrapper/gradle-wrapper.jar \
android:    android/gradle/wrapper/gradle-wrapper.properties \
android:    android/gradlew \
android:    android/gradlew.bat \
android:    android/res/values/libs.xml \
    todo.txt

Отредактировать AndroidManifest.xml

Отредактировать файл «AndroidManifest.xml» в android/AndroidManifest.xml, добавив в секцию после

</activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices ->

текст:

<provider android:name="android.support.v4.content.FileProvider" android:authorities="org.qtproject.example.qsketch.fileprovider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/>
</provider>

Создать файл с указанием каталога совместного использования с другими приложениями

Это нужно для того, чтобы приложение фотографирования по-умолчанию могло передать нашему приложению файл.

В каталоге сборки, там где находится автогенерируемый файл «AndroidManifest.xml» внутри каталога «res» рядом с каталогом «values», создать каталог «xml», а в нём файл «file_paths.xml» (… /abin/AndroidManifest.xml) (… /abin/res/xml/file_paths.xml). В созданный файл поместить следующие строки:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="shared" path="shared/" />
</paths>

где shared/ имя каталога в каталоге файлов нашего приложения

Добавить компонент, содержащий FileProvider в сборку

Отредактировать файл android/build.gradle, добвив в секцию dependencies текст:

compile'com.android.support:support-v4:25.3.1'

секция целиком выглядит так:

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
compile'com.android.support:support-v4:25.3.1'
}

Данная инструкция сработает, если был установлен Android Support Repository.

Настройка выполнена на основании статьи Sharing Files on Android or iOS from or with your Qt App - Part 4 и подобных статей на эту тему.

Заголовочный файл (.h) класса имеет вид:

#ifndef CAMSHOT_H
#define CAMSHOT_H
#include <QObject>
#include <QImage>
#include <QString>
#include <QDebug>

#include <QtAndroid>
#include <QAndroidActivityResultReceiver>
#include <QAndroidParcel>

#include "auxfunc.h"

class CamShot : public QObject, public QAndroidActivityResultReceiver
{
    Q_OBJECT
public:
    static const int RESULT_OK = -1;
    static const int REQUEST_IMAGE_CAPTURE = 1;
    static const int REQUEST_TAKE_PHOTO = REQUEST_IMAGE_CAPTURE;
    enum ImgOrientation {ORIENTATION_UNDEFINED = 0, ORIENTATION_NORMAL = 1, ORIENTATION_FLIP_HORIZONTAL = 2, ORIENTATION_ROTATE_180 = 3, ORIENTATION_FLIP_VERTICAL = 4, ORIENTATION_TRANSPOSE = 5,
                       ORIENTATION_ROTATE_90 = 6, ORIENTATION_TRANSVERSE = 7, ORIENTATION_ROTATE_270 = 8};
    void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;
    static QImage aBitmapToQImage(const QAndroidJniObject &aBitmap);
    static QImage camThumbnailToQImage(const QAndroidJniObject &data);
    ImgOrientation needRotateAtRightAngle();
    QImage camImageToQImage();
    static void applyOrientation(QImage &img, const ImgOrientation &orientation);


    explicit CamShot(QObject *parent = nullptr):QObject(parent),QAndroidActivityResultReceiver(){}
    ~CamShot();
private:    
    QAndroidJniObject tempImgURI;
    QAndroidJniObject tempImgFile;
    QAndroidJniObject tempImgAbsPath;
    bool _thumbnailNotFullScaleRequested;
public slots:
    void aMakeShot(const bool &thumbnailNotFullScale = false);
signals:
    void createNew(const QImage &img);
};
#endif // CAMSHOT_H

Заголовочный файл (.cpp) класса имеет вид:

QImage CamShot::aBitmapToQImage(const QAndroidJniObject &aBitmap){
    if (!aBitmap.isValid())
        return QImage();
    jint aBitmapWidth = aBitmap.callMethod<jint>("getWidth");
    jint aBitmapHeight = aBitmap.callMethod<jint>("getHeight");
    QAndroidJniEnvironment env;
    const int32_t aBitmapPixelsCount = aBitmapWidth * aBitmapHeight;
    jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
    jint aBitmapOffset = 0;
    jint aBitmapStride = aBitmapWidth;
    jint aBitmapX = 0;
    jint aBitmapY = 0;
    aBitmap.callMethod<void>("getPixels","([IIIIIII)V", pixels, aBitmapOffset, aBitmapStride, aBitmapX, aBitmapY, aBitmapWidth, aBitmapHeight);
    jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
    QImage img(aBitmapWidth, aBitmapHeight, QImage::Format_ARGB32);
    int lineSzB = aBitmapWidth * sizeof(jint);
    for (int i = 0; i < aBitmapHeight; ++i){
        uchar *pDst = img.scanLine(i);
        const uchar *pSrc = reinterpret_cast<const uchar*>(pPixels + aBitmapWidth * i + aBitmapWidth);
        memcpy(pDst, pSrc, lineSzB);
    }
    env->DeleteLocalRef(pixels); //env->ReleaseIntArrayElements(pixels, pPixels, 0); отвязывает указатель на данные массива от массива, а надо удалить сам массив, поэтому DeleteLocalRef.
    return img;
}
QImage CamShot::camThumbnailToQImage(const QAndroidJniObject &data){
    //Получить дополнительный данные
    QAndroidJniObject bundle = data.callObjectMethod("getExtras","()Landroid/os/Bundle;");
    qDebug()<<"bundle.isValid() "<<bundle.isValid()<<bundle.toString();
    //Создать объект типа jstring (строка Java) со значением "data" - ключ для извлечения из ассоциативного контейнера пар <ключ, значение> миниатюры - объекта типа Bitmap (Java)
    QAndroidJniObject bundleKey = QAndroidJniObject::fromString("data");
    //Получить по ключу "data" дополнительный данные: миниатюру в виде объекта Bitmap
    const QAndroidJniObject aBitmap (data.callObjectMethod("getParcelableExtra", "(Ljava/lang/String;)Landroid/os/Parcelable;", bundleKey.object<jstring>()));
    qDebug()<<"aBitmap.isValid() "<<aBitmap.isValid()<<aBitmap.toString();
    return aBitmapToQImage(aBitmap);
}
QImage CamShot::camImageToQImage(){
    QAndroidJniObject bitmap = QAndroidJniObject::callStaticObjectMethod("android/graphics/BitmapFactory","decodeFile","(Ljava/lang/String;)Landroid/graphics/Bitmap;",tempImgAbsPath.object<jobject>());
    qDebug()<<"bitmap.isValid() "<<bitmap.isValid()<<bitmap.toString();
    QImage img = aBitmapToQImage(bitmap);
    //Удаление файла
    if (tempImgFile.isValid())
        tempImgFile.callMethod<jboolean>("delete");
    return img;
}
CamShot::ImgOrientation CamShot::needRotateAtRightAngle(){
    //Вызов конструктора объекта
    QAndroidJniObject exifInterface = QAndroidJniObject("android/media/ExifInterface","(Ljava/lang/String;)V",
                                                     tempImgAbsPath.object<jstring>());
    qDebug() << __FUNCTION__ << "exifInterface.isValid()=" << exifInterface.isValid();
    QAndroidJniObject TAG_ORIENTATION = QAndroidJniObject::getStaticObjectField<jstring>("android/media/ExifInterface", "TAG_ORIENTATION");
    qDebug() << __FUNCTION__ << "TAG_ORIENTATION.isValid()=" << TAG_ORIENTATION.isValid()<<TAG_ORIENTATION.toString();
    const jint orientation = exifInterface.callMethod<jint>("getAttributeInt","(Ljava/lang/String;I)I",TAG_ORIENTATION.object<jstring>(),static_cast<jint>(ORIENTATION_UNDEFINED));
    return static_cast<ImgOrientation>(orientation);
}
void CamShot::applyOrientation(QImage &img, const ImgOrientation &orientation){
    switch (orientation){
    case ORIENTATION_UNDEFINED:
    case ORIENTATION_NORMAL:
        break;
    case ORIENTATION_FLIP_HORIZONTAL:{
        img = img.mirrored(true, false);
        break;
    }
    case ORIENTATION_ROTATE_180:
        Aux::rotateImgCW180(img);
        break;
    case ORIENTATION_FLIP_VERTICAL:{
        img = img.mirrored(false, true);
        break;
    }
    case ORIENTATION_TRANSPOSE:{
        img = img.mirrored(true, false);
        Aux::rotateImgCW270(img);
        break;
    }
    case ORIENTATION_ROTATE_90:
        Aux::rotateImgCW90(img);
        break;
    case ORIENTATION_TRANSVERSE:{
        img = img.mirrored(true, false);
        Aux::rotateImgCW90(img);
        break;
    }
        break;
    case ORIENTATION_ROTATE_270:
        Aux::rotateImgCW270(img);
        break;
    }
}
void CamShot::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data){
    if ( receiverRequestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK )
    {
        if (_thumbnailNotFullScaleRequested){
            const QImage thumbnail (camThumbnailToQImage(data));
            if (!thumbnail.isNull())
                emit createNew(thumbnail);
            return;
        }
        const ImgOrientation orientation = needRotateAtRightAngle();
        QImage image (camImageToQImage());
        if (!image.isNull()){
            applyOrientation(image, orientation);
            emit createNew(image);
        }
    }
}
void CamShot::aMakeShot(const bool &thumbnailNotFullScale) {
    QAndroidJniObject action = QAndroidJniObject::fromString("android.media.action.IMAGE_CAPTURE");
    //Вызов конструктора объекта
    QAndroidJniObject intent=QAndroidJniObject("android/content/Intent","(Ljava/lang/String;)V",
                                                 action.object<jstring>());
    qDebug() << __FUNCTION__ << "intent.isValid()=" << intent.isValid();
    _thumbnailNotFullScaleRequested = thumbnailNotFullScale;
    if (thumbnailNotFullScale) {
        //Для получения миниатюры
        QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);
        return;
    }
    //Для получения изображения в файл
    QAndroidJniObject context = QtAndroid::androidContext();
    QString contextStr (context.toString());
    qDebug() <<"Context: "<<contextStr;
    //Каталог для хранения файлов приложения
    QAndroidJniObject extDir = context.callObjectMethod("getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;", NULL);
    qDebug() << __FUNCTION__ << "extDir.isValid()=" << extDir.isValid()<<extDir.toString();
    //Абсолютный путь к каталогу для хранения файлов приложения в виде строки
    QAndroidJniObject extDirAbsPath = extDir.callObjectMethod("getAbsolutePath","()Ljava/lang/String;");
    //Добавим имя каталога для совместного использования файлов этого приложения другими приложениями. См. /res/xml/file_paths.xml
    extDirAbsPath = QAndroidJniObject::fromString(extDirAbsPath.toString() + "/shared");
    const QString extDirAbsPathStr = extDirAbsPath.toString();
    qDebug() << __FUNCTION__ << "extDirAbsPath.isValid()=" << extDirAbsPath.isValid()<<extDirAbsPathStr;
    //Создать объект типа Файл для разделяемого каталога
    QAndroidJniObject sharedFolder=QAndroidJniObject("java.io.File","(Ljava/lang/String;)V",
                                                      extDirAbsPath.object<jstring>());
    qDebug() << __FUNCTION__ << "sharedFolder.isValid()=" << sharedFolder.isValid()<<sharedFolder.toString();
    const jboolean sharedFolderCreated = sharedFolder.callMethod<jboolean>("mkdirs");
    Q_UNUSED(sharedFolderCreated);
    //Прежде чем пытаться создать файл с заданным именем, нужно проверить файл с этим именем на существование
    //Предположительно путь к этому файлу
    QAndroidJniObject suggestedFilePath = QAndroidJniObject::fromString(extDirAbsPathStr+"/"+"_tmp.jpg");
    qDebug() << __FUNCTION__ << "suggestedFilePath.isValid()=" << suggestedFilePath.isValid()<<suggestedFilePath.toString();
    //Создать объект типа Файл
    //Вызов конструктора объекта
    QAndroidJniObject tempImgFile=QAndroidJniObject("java.io.File","(Ljava/lang/String;)V",
                                                 suggestedFilePath.object<jstring>());
    qDebug() << __FUNCTION__ << "fileExistsCheck.isValid()=" << tempImgFile.isValid()<<tempImgFile.toString();
    //Удаление файла, если он существует
    if (tempImgFile.isValid()){
        const jboolean deleted = tempImgFile.callMethod<jboolean>("delete");
        Q_UNUSED(deleted);
    }
    //Создать физический файл для записи в него изображения по указанному пути
    const jboolean fileCreated = tempImgFile.callMethod<jboolean>("createNewFile");
    Q_UNUSED(fileCreated);
    //Абсолютный путь к созданному файлу в виде строки
    tempImgAbsPath = tempImgFile.callObjectMethod("getAbsolutePath","()Ljava/lang/String;");
    qDebug() << __FUNCTION__ << "tempImgAbsPath.isValid()=" << tempImgAbsPath.isValid()<<tempImgAbsPath.toString();
    //Получить authority для fileprovider
    const QString contextFileProviderStr ("org.qtproject.example.qsketch.fileprovider");
    const char androidFileProvider  [] = "android/support/v4/content/FileProvider";
    //const char androidxFileProvider [] = "androidx/core/content/FileProvider"; - не поддерживается Qt
    /*QAndroidJniObject*/ tempImgURI = QAndroidJniObject::callStaticObjectMethod(androidFileProvider, "getUriForFile", "(Landroid/content/Context;Ljava/lang/String;Ljava/io/File;)Landroid/net/Uri;",
                                                                             context.object<jobject>(), QAndroidJniObject::fromString(contextFileProviderStr).object<jstring>(), tempImgFile.object<jobject>());
    qDebug() << __FUNCTION__ << "tempImgURI.isValid()=" << tempImgURI.isValid()<<tempImgURI.toString();
    //Получить значение константы MediaStore.EXTRA_OUTPUT
    QAndroidJniObject MediaStore__EXTRA_OUTPUT
        = QAndroidJniObject::getStaticObjectField("android/provider/MediaStore", "EXTRA_OUTPUT", "Ljava/lang/String;");
    qDebug() << "MediaStore__EXTRA_OUTPUT.isValid()=" << MediaStore__EXTRA_OUTPUT.isValid();
    //Добавить URI путь файла для записи в него данных к задаче
    intent.callObjectMethod("putExtra","(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;",MediaStore__EXTRA_OUTPUT.object<jstring>(), tempImgURI.object<jobject>());
    qDebug() << __FUNCTION__ << "intent.isValid()=" << intent.isValid();
    QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);
}

Так-же статические методы класса Aux для поворота изображения.

Заголовочный файл (.h) класса Aux имеет вид:

#ifndef AUXFUNC_H
#define AUXFUNC_H

#include <QImage>
#include <QColor>
#include <QPainter>
#include <QMatrix>
#include <QSize>
#include <QPoint>

class Aux
{
public:
    static void resizeCenteredImg(QImage *image, const QSize &newSize, const QColor bgColor);
    static void rotateImg(QImage &img, qreal degrees);
    static void rotateImgCW90(QImage &img);
    static void rotateImgCW180(QImage &img);
    static void rotateImgCW270(QImage &img);
};


#endif // AUXFUNC_H

Файл исходного кода (.cpp) класса Aux имеет вид:

void Aux::resizeCenteredImg(QImage *image, const QSize &newSize, const QColor bgColor){
    if (image->size() == newSize)
        return;
    const QSize szDiff = newSize - image->size();
    QImage newImage(newSize, QImage::Format_ARGB32);
    newImage.fill(bgColor);
    QPainter painter(&newImage);
    painter.drawImage(QPoint(szDiff.width()/2, szDiff.height()/2), *image);
    *image = newImage;
}
void Aux::rotateImg(QImage &img, qreal degrees){
    QPoint center = img.rect().center();
    QMatrix matrix;
    matrix.translate(center.x(), center.y());
    matrix.rotate(degrees);
    img = img.transformed(matrix, Qt::SmoothTransformation);
}
void Aux::rotateImgCW90(QImage &img){
    const int w = img.width();
    const int h = img.height();
    const int maxDim = std::max(w, h);
    resizeCenteredImg(&img, QSize(maxDim, maxDim), Qt::white);
    rotateImg(img, 90);
    resizeCenteredImg(&img, QSize(h, w), Qt::white);
}
void Aux::rotateImgCW180(QImage &img){
    rotateImg(img, 180);
}
void Aux::rotateImgCW270(QImage &img){
    const int w = img.width();
    const int h = img.height();
    const int maxDim = std::max(w, h);
    resizeCenteredImg(&img, QSize(maxDim, maxDim), Qt::white);
    rotateImg(img, 270);
    resizeCenteredImg(&img, QSize(h, w), Qt::white);
}

Указанный метод позволяет получить за один раз или миниатюру или полноразмерное изображение.

Результат зависит от переданного логического параметра «thumbnailNotFullScale». Если он равен логической единице, то будет получена миниатюра, если логическому нулю, то полноразмерное изображение. Попытка получить миниатюру при запросе сохранения полноразмерного изображения в файл приведёт к исключению в JNI-вызовах.

Если миниатюра всегда ориентирована правильно, то полноразмерное изображение направлено в одну строну и его необходимо поворачивать. Информацию о необходимых преобразованиях можно получить из exif-свойств изображения при помощи ExifInterface. В обнаруженных в интернете Java-примерах преобразование к нормальной ориентации производится в Java-коде, в случае с Qt нет смысла мучить себя трудно отлаживаемыми, громоздкими JNI-вызовами и проще выполнить все необходимые преобразования в Qt.

Let's block ads! (Why?)



Комментарии