Solved: Create standalone executable for MacOS with OpenCV and libmagic

53 views Asked by At

I'm currently trying to compile my program to be standalone. I'm on a MacOS M2, and the target os is also a MacOS M2. I want to just have to drag and drop the executable from one to another mac and basically run it without installing anything...

First of all, I'm not sure that totally possible, and secondly, I'm not really good at these things (compiling correctly, static linking etc.).

I tried to statically compiling OpenCV and libmagic (from "file"), and I don't really know what I did wrong, but I think OpenCV is not correctly built... Here are the sources I used :

(Edit 1:) The current error is when I compile using the big g++ command (that you can see lower in this post). The error in question : ld: library not found for -lopencv_viz clang: error: linker command failed with exit code 1 (use -v to see invocation) I checked for sources of viz module (and I can found them). I correctly include the "include" folder of viz module, so I don't know why it can't found it with -lopencv_viz...

(Edit 2:) I finally find out how to compile to standalone. First, you will need to build VTK for OpenCV (https://vtk.org/download/). After that, you will need to build OpenCV (with VTK and Contrib Modules). To do that I used this command :

cmake -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules -DVTK_DIR=../../VTK-9.3.0/dylibs-build/final-install/usr/local/lib/cmake/vtk-9.3/ -DBUILD_ZLIB=ON -DBUILD_JPEG=ON -DBUILD_PNG=ON -DBUILD_TIFF=ON -DBUILD_WEBP=ON -DBUILD_OPENJPEG=ON -DBUILD_JASPER=ON -DBUILD_OPENEXR=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_JPEG_TURBO_DISABLE=ON -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF ..

Next, you should build libmagic. To do that, I needed to use autoreconf --install (I got some issues with autoconf, but not with autoreconf...). After that, just follow "INSTALL" file from the git. Finally, I bundled all generated libs to a folder at the .cpp root, and I did the same for includes files. So my final compilation command is :

g++ -std=c++17 -I./include/libmagic -L./lib/libmagic -lmagic -llzma -lbz2 -lz -I./include/opencv -L./lib/opencv -lopencv_gapi -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_sfm -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_xfeatures2d -lopencv_shape -lopencv_ml -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core -o image_processing image_processing.cpp

And finally, I can deliver the program to other MacOS M2 into a .zip file. This is not the best way to do that. But it works, so that ok. I tried to set "-rpath" on the g++ command, so that could be more efficient, but when I use "otool -l image_processing", every libs are searched by the rpath (path starting with "@rpath") except for the libmagic (and I don't know why...).

Here is the current command I use to compile : g++ -std=c++17 pkg-config --static --cflags --libs libmagic pkg-config --static --cflags --libs opencv4 -o image_indexer image_indexer.cpp

Which is translate to : g++ -std=c++17 -I/opt/homebrew/Cellar/libmagic/5.45/include -L/opt/homebrew/Cellar/libmagic/5.45/lib -lmagic -llzma -lbz2 -lz -I/opt/homebrew/opt/opencv/include/opencv4 -L/opt/homebrew/opt/opencv/lib -lopencv_gapi -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_sfm -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_xfeatures2d -lopencv_shape -lopencv_ml -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core -o image_indexer image_indexer.cpp

So there is the program in question :

#include <iostream>
#include <magic.h>
#include <opencv2/opencv.hpp>
#include <filesystem>
#include <vector>
#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>
#include <thread>
#include <mutex>
#include <functional>
#include <queue>
#include <condition_variable>

using namespace std;
namespace fs = filesystem;
mutex mtx;

class ThreadPool {
    public:
        ThreadPool( size_t length ) : stop( false ), finishedThreads( 0 ), totalThreads( 0 ) {
            for( size_t i = 0; i < length; ++i ) {
                threads.emplace_back( [this] {
                    while( true ) {
                        function<void()> task;
                        {
                            unique_lock<mutex> lock( queueMutex );
                            condition.wait( lock, [this] { return stop || ! tasks.empty(); } );
                            if( stop && tasks.empty() ) return;
                            task = std::move( tasks.front() );
                            tasks.pop();
                        }
                        task();
                        {
                            lock_guard<mutex> lock( counterMutex );
                            ++finishedThreads;
                            if( finishedThreads == totalThreads ) {
                                threadsFinished.notify_one();
                            }
                        }
                    }
                } );
            }
        }
        template<typename F>
        void push( F&& task ) {
            {
                unique_lock<mutex> lock( queueMutex );
                tasks.push( std::forward<F>( task ) );
            }
            condition.notify_one();
        }
        void waitAll( int total ) {
            unique_lock<mutex> lock( counterMutex );
            totalThreads = static_cast<size_t>( total );
            threadsFinished.wait( lock, [this] { return finishedThreads == totalThreads; } );
        }
        ~ThreadPool() {
            {
                unique_lock<mutex> lock( queueMutex );
                stop = true;
            }
            condition.notify_all();
            for( auto& thread : threads )
                thread.join();
        }
    private:
        vector<thread> threads;
        queue<function<void()>> tasks;
        mutex queueMutex;
        mutex counterMutex;
        condition_variable condition;
        condition_variable threadsFinished;
        size_t finishedThreads;
        size_t totalThreads;
        bool stop;
};

bool isFileReadable( const string& filePath ) {
    try {
        fs::file_status status = fs::status( filePath );
        return fs::is_regular_file( status ) && ( ( status.permissions() & fs::perms::owner_read ) != fs::perms::none );
    } catch( const fs::filesystem_error& e ) {
        cerr << "ERREUR: " << e.what() << endl;
        return false;
    }
}

bool isHiddenFile( const string& filePath ) {
    fs::path path( filePath );
    #ifdef _WIN32
    DWORD attributes = GetFileAttributes( path.c_str() );
    if( attributes != INVALID_FILE_ATTRIBUTES ) {
        return attributes & FILE_ATTRIBUTE_HIDDEN;
    }
    return false;
    #else
    return ! path.empty() && path.filename().string().front() == '.';
    #endif
}

bool startsWith( const string& input, const string& needle ) {
    return input.substr( 0, needle.size() ) == needle;
}

void writeHTML( const string& content, const string outputFilePath ) {
    lock_guard<mutex> lock( mtx );
    ofstream outputFile( outputFilePath, ios::app );
    if( outputFile.is_open() ) {
        outputFile << content;
        outputFile.close();
    }
}

string base64_encode( const unsigned char* to_encode, unsigned int in_len ) {
    string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    string result;
    int i = 0;
    int j = 0;
    unsigned char char_array_3[ 3 ];
    unsigned char char_array_4[ 4 ];
    while( in_len-- ) {
        char_array_3[ i++ ] = *( to_encode++ );
        if( i == 3 ) {
            char_array_4[ 0 ] = ( char_array_3[ 0 ] & 0xFC ) >> 2;
            char_array_4[ 1 ] = (( char_array_3[ 0 ] & 0x03 ) << 4 ) + ( ( char_array_3[ 1 ] & 0xF0 ) >> 4 );
            char_array_4[ 2 ] = (( char_array_3[ 1 ] & 0x0F ) << 2 ) + ( ( char_array_3[ 2 ] & 0xC0 ) >> 6 );
            char_array_4[ 3 ] = char_array_3[ 2 ] & 0x3F;
            for( i = 0; i < 4; i++ ) {
                result += base64_chars[ char_array_4[ i ] ];
            }
            i = 0;
        }
    }
    if( i ) {
        for( j = i; j < 3; j++ ) {
            char_array_3[ j ] = '\0';
        }
        char_array_4[ 0 ] = ( char_array_3[ 0 ] & 0xFC ) >> 2;
        char_array_4[ 1 ] = ( ( char_array_3[ 0 ] & 0x03 ) << 4 ) + ( ( char_array_3[ 1 ] & 0xF0 ) >> 4 );
        char_array_4[ 0 ] = ( ( char_array_3[ 1 ] & 0x0F ) << 2 ) + ( ( char_array_3[ 2 ] & 0xC0 ) >> 6 );
        char_array_4[ 0 ] = char_array_3[ 2 ] & 0x3F;
        for( j = 0; j < i + 1; j++ ) {
            result += base64_chars[ char_array_4[ j ] ];
        }
        while( i++ < 3 ) {
            result += '=';
        }
    }
    return result;
}

cv::Mat resizeImage( const cv::Mat& image, const int& width, const int& height ) {
    cv::Mat resized;
    cv::resize( image, resized, cv::Size( width, height ), 0, 0, cv::INTER_AREA );
    return resized;
}

string encodeToBase64( const cv::Mat& image ) {
    vector<uchar> buffer;
    cv::imencode( ".jpg", image, buffer );
    return base64_encode( buffer.data(), buffer.size() );
}

string getFileMime( const string& filePath ) {
    magic_t magicCookie = magic_open( MAGIC_MIME_TYPE );
    if( magicCookie == NULL ) {
        return "unknown";
    }
    magic_load( magicCookie, NULL );
    const char* mimeType = magic_file( magicCookie, filePath.c_str() );
    string mimeTypeString = mimeType ? mimeType : "unknown";
    magic_close( magicCookie );
    return mimeTypeString;
}

void processImage( const string outputFile, const string& filePath, const int& width, const int& height, bool verbose=false ) {
    if( ! isFileReadable( filePath ) ) return;
    fs::directory_entry fileEntry( filePath );
    string mime = getFileMime( filePath );
    string fileName = fileEntry.path().filename().string();
    if( startsWith( mime, "image/" ) ) {
        cv::Mat image = cv::imread( filePath );
        if( ! image.empty() ) {
            cv::Mat resizedImage = resizeImage( image, width, height );
            string base64Data = encodeToBase64( resizedImage );
            string tempContent = R"(
    <div style="position: relative;">
            )";
            tempContent += "<img src=\"data:" + mime + ";base64," + base64Data + "\" alt=\"" + fileName + "\" title=\"" + fileName + "\" />";
            tempContent += R"(
        <div style="position: absolute;bottom: 0;left: 0;display: flex;align-items: center;justify-content: center;width: 100%;height: fit-content;padding-block: 1rem;padding-inline: .75rem;background: rgba(255 255 255 / .5);">
            <span>Nom : )";
            tempContent += fileName;
            tempContent += R"(</span>
        </div>
    </div>
            )";
            writeHTML( tempContent, outputFile );
            if( verbose ) cout << "Image actuelle : " << filePath << endl;
        }
    }
}

int processImagesInFolder( const string outputFile, ThreadPool& threadPool, const string& folderPath, const int& width, const int& height, bool verbose=false, int count=0 ) {
    vector<string> filePaths;
    vector<thread> threads;
    try {
        for( const auto& entry : fs::directory_iterator( folderPath ) ) {
            if( fs::is_regular_file( entry ) && ! isHiddenFile( entry.path().string() ) ) {
                string filePath = entry.path().string();
                ++count;
                threadPool.push( [=]() {
                    processImage( outputFile, filePath, width, height, verbose );
                } );
            } else if( fs::is_directory( entry ) && ! isHiddenFile( entry.path().string() ) ) {
                count += processImagesInFolder( outputFile, threadPool, fs::canonical( entry.path() ).string(), width, height, verbose, count );
            }
        }
    } catch( fs::filesystem_error e ) {
        cerr << "HANDLING: " << e.what() << endl;
    }
    return count;
}

int main( int argc, char* argv[] ) {
    unordered_map<string, string> args;
    for( int i = 0; i < argc; ++i ) {
        string argument = argv[ i ];
        if( argument.substr( 0, 2 ) == "--" ) {
            size_t position = argument.find( '=' );
            if( position != string::npos ) {
                string key = argument.substr( 2, position - 2 );
                string value = argument.substr( position + 1 );
                args[ key ] = value;
            }
        }
    }
    int width = 300;
    int height = 250;
    int threadsCount = 10;
    bool verbose = false;
    string filePath = "./";
    string filename = "index.html";
    if( args.count( "width" ) ) {
        width = stoi( args[ "width" ] );
    }
    if( args.count( "height" ) ) {
        height = stoi( args[ "height" ] );
    }
    if(
        args.count( "verbose" ) &&
        (
            args[ "verbose" ] == "true" ||
            args[ "verbose" ] == "yes" ||
            args[ "verbose" ] == "1" ||
            args[ "verbose" ] == "y"
        )
    ) {
        verbose = true;
    }
    if( args.count( "threads") ) {
        threadsCount = stoi( args[ "threads" ] );
    }
    if( args.count( "outFolder" ) ) {
        filePath = args[ "outFolder" ];
    }
    if( args.count( "outFilename" ) ) {
        filename = args[ "outFilename" ];
    }
    if( args.count( "folder" ) ) {
        cout << "L'indexation peut prendre un certain temps...." << endl;
        ThreadPool threadPool( static_cast<size_t>( threadsCount ) );
        string finalHTML = R"(
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Indexation photo</title>
</head>
<body style="background-color: rgb(31 41 55);">
    <div style="display: flex;flex-wrap: wrap;justify-content: space-around;gap: 1.5rem;padding-block: 1rem;padding-inline: 2.5rem;">)";
        if( filePath.back() != '/' ) filePath += '/';
        ofstream outputFile( filePath + filename );
        if( outputFile.is_open() ) {
            outputFile << finalHTML;
            outputFile.close();
        }
        string folderPath = args[ "folder" ];
        int total = processImagesInFolder( filePath + filename, threadPool, folderPath, width, height, verbose );
        if( verbose ) cout << total <<" fichiers à traiter." << endl;
        threadPool.waitAll( total );
        finalHTML = R"(
    </div>
</body>
</html>)";
        writeHTML( finalHTML, filePath + filename );
        cout << endl << "Fichier index.html généré avec succès.\nIl répertori les images du dossier : " << folderPath << endl;
        return 0;
    } else {
        cout << "Pour utiliser cet indexeur, veuillez préciser au moins l'argument 'folder'" << endl;
        cout << "--> Utilisation: ./image_indexer --folder=/Users/name/Pictures" << endl << endl;
        cout << "Arguments :" << endl;
        cout << "--folder: Permet de préciser le dossier a indexer" << endl;
        cout << "--width: Précise la longueur en pixel de l'image retaillée" << endl;
        cout << "--height: Précise la hauteur en pixel de l'image retaillée" << endl;
        cout << "--verbose: Affiche l'image actuellement traitée (préciser 'y'/'yes'/'1')" << endl;
        cout << "--threads: Nombre de threads en simulatané (défaut: 10)" << endl;
        cout << "--outFolder: Chemin du dossier de génération" << endl;
        cout << "--outFilename: Nom du fichier HTML généré" << endl;
        return 1;
    }
}

I tried to compile OpenCV and libmagic statically. I think no problems with libmagic (no errors etc), but for OpenCV it's another history...

I used this command to build OpenCV with modules : mkdir build && cd build && cmake -D CMAKE_BUILD_TYPE=Release -D BUILD_SHARED_LIBS=OFF -D OPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules ../opencv/ && make -j8 This take a while to build.

After that, I just basically tried to link all required libraries to the G++ command. That give me something like that (I replaced all absolute path) :

g++ -std=c++17 -lbz2 -lz -o image_processing image_processing.cpp \
-I/content/opencv/modules/core/include \
-I/content/file/src \
-I/content/opencv4/opencv/include \
-I/content/opencv/modules/features2d/include \
-I/content/opencv/modules/calib3d/include \
-I/content/modules/opencv_contrib/modules/core/include \
-I/content/opencv4/build \
-I/content/opencv/modules/flann/include \
-I/content/opencv/modules/dnn/include \
-I/content/opencv/modules/highgui/include \
-I/content/opencv/modules/imgcodecs/include \
-I/content/opencv/modules/videoio/include \
-I/content/opencv/modules/imgproc/include \
-I/content/opencv/modules/gapi/include \
-I/content/opencv/modules/ml/include \
-I/content/opencv/modules/objdetect/include \
-I/content/opencv/modules/photo/include \
-I/content/opencv/modules/stitching/include \
-I/content/opencv/modules/video/include \
-I/content/opencv/modules/shape/include \
-I/content/opencv/modules/superres/include \
-I/content/opencv/modules/videostab/include \
-I/content/opencv/modules/aruco/include \
-I/content/opencv/modules/bgsegm/include \
-I/content/opencv/modules/bioinspired/include \
-I/content/opencv/modules/ccalib/include \
-I/content/opencv/modules/datasets/include \
-I/content/opencv/modules/dpm/include \
-I/content/opencv/modules/face/include \
-I/content/opencv/modules/freetype/include \
-I/content/opencv/modules/fuzzy/include \
-I/content/opencv/modules/hdf/include \
-I/content/opencv/modules/line_descriptor/include \
-I/content/opencv/modules/optflow/include \
-I/content/modules/opencv_contrib/modules/video/include \
-I/content/modules/opencv_contrib/modules/plot/include \
-I/content/modules/opencv_contrib/modules/reg/include \
-I/content/modules/opencv_contrib/modules/saliency/include \
-I/content/modules/opencv_contrib/modules/stereo/include \
-I/content/modules/opencv_contrib/modules/structured_light/include \
-I/content/modules/opencv_contrib/modules/phase_unwrapping/include \
-I/content/modules/opencv_contrib/modules/rgbd/include \
-I/content/modules/opencv_contrib/modules/viz/include \
-I/content/modules/opencv_contrib/modules/surface_matching/include \
-I/content/modules/opencv_contrib/modules/text/include \
-I/content/modules/opencv_contrib/modules/ximgproc/include \
-I/content/modules/opencv_contrib/modules/xobjdetect/include \
-I/content/modules/opencv_contrib/modules/xphoto/include \
-I/content/modules/opencv_contrib/modules/xobjdetect/include \
-L/content/modules/build/lib \
-L/content/file/src/.libs \
-lopencv_shape \
-lopencv_stitching \
-lopencv_superres \
-lopencv_videostab \
-lopencv_aruco \
-lopencv_bgsegm \
-lopencv_bioinspired \
-lopencv_ccalib \
-lopencv_datasets \
-lopencv_dpm \
-lopencv_face \
-lopencv_freetype \
-lopencv_fuzzy \
-lopencv_hdf \
-lopencv_line_descriptor \
-lopencv_optflow \
-lopencv_video \
-lopencv_plot \
-lopencv_reg \
-lopencv_saliency \
-lopencv_stereo \
-lopencv_structured_light \
-lopencv_phase_unwrapping \
-lopencv_rgbd \
-lopencv_viz \
-lopencv_surface_matching \
-lopencv_text \
-lopencv_ximgproc \
-lopencv_calib3d \
-lopencv_features2d \
-lopencv_flann \
-lopencv_xobjdetect \
-lopencv_objdetect \
-lopencv_ml \
-lopencv_xphoto \
-lopencv_highgui \
-lopencv_videoio \
-lopencv_imgcodecs \
-lopencv_photo \
-lopencv_imgproc \
-lopencv_core
1

There are 1 answers

0
Kum0 On

I finally find out how to compile to standalone.

This is not a really good solution because it's only aimed to be shared with other MacOS M2, and there is other ways to do this better.

So first, you will need to build VTK for OpenCV (https://vtk.org/download/).

After that, you will need to build OpenCV, with VTK and Contrib Modules. To do it, I used this command :

cmake -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules -DVTK_DIR=../../VTK-9.3.0/dylibs-build/final-install/usr/local/lib/cmake/vtk-9.3/ -DBUILD_ZLIB=ON -DBUILD_JPEG=ON -DBUILD_PNG=ON -DBUILD_TIFF=ON -DBUILD_WEBP=ON -DBUILD_OPENJPEG=ON -DBUILD_JASPER=ON -DBUILD_OPENEXR=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_JPEG_TURBO_DISABLE=ON -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF ..

Next, you need to build libmagic. To do that, you should follow instructions from the github.

In my case, I needed to use autoreconf --install instead of autoconf. (I got some issues with autoconf, but not with autoreconf...).

Finally, I bundled all generated libs, and all included files to a folder at the root of the program.

Then I built the program using this command :

g++ -std=c++17 -I./include/libmagic -L./lib/libmagic -lmagic -llzma -lbz2 -lz -I./include/opencv -L./lib/opencv -lopencv_gapi -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_sfm -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_xfeatures2d -lopencv_shape -lopencv_ml -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core -o image_processing image_processing.cpp

As I said, this is not the best way to do it (I suppose). And I tried to use the "rpath" option from g++, but for some reason, I can't make it work for libmagic. (To check, I used otool -l image_processing, and every lib paths starts with "@rpath", except for those from libmagic)

(Thanks @Botje and @user12002570)