Welcome!

Welcome to the official BlackBerry Support Community Forums.

This is your resource to discuss support topics with your peers, and learn from each other.

inside custom component

Native Development

Reply
Developer
dbigham
Posts: 555
Registered: ‎04-01-2009
My Device: Z10, PlayBook
Accepted Solution

Writing EXIF Data (And how to set the photo rating)

Has anyone had luck writing EXIF data back to photos? Reading EXIF data is easy enough, as demonstrated by 'photobomber'. Writing EXIF data appears to be more painful, however.

 

In particular, I'm trying to set the photo's rating.  I've found mentions on the web of its existance in EXIF, with the code:

 

0x4746

 

That said, when I check exif-tag.h, it doesn't appear to be listed.

 

It's unclear to me whether that's a deal breaker or not.  Are the tags listed in exif-tag.h the only ones that can be used? Or are they simply convenience definitions of the most commonly used tag codes?

 

Thanks,

Daniel

Developer
BBSJdev
Posts: 6,118
Registered: ‎07-05-2012
My Device: Playbook, Dev Alpha C, Z10 LE, Z30

Re: Writing EXIF Data (And how to set the photo rating)

[ Edited ]
I'll answer that from an EXIF point of view (previous coding done on other systems) and not a BB10 coding point of view, the EXIF format is a bit of a mess with anyone able to add their own tags.  For this reason, writing back EXIF data or manipulating it has always been a bit of a hit and miss affair.

 

So to answer this question,

 


dbigham wrote:

 

It's unclear to me whether that's a deal breaker or not.  Are the tags listed in exif-tag.h the only ones that can be used? Or are they simply convenience definitions of the most commonly used tag codes?

 


from a EXIF point of view the answer is no and yes respectively from a BB10 coding/API point I haven't looked at it so someone who has will have to answer but my bet is that only the most common tags will have been included.

 

Take a look here to understand the history and some of the potential minefields you will hit if you intend to write an app that can cope with properly writing EXIF data back to image (and possibly other) formats,

 

http://en.wikipedia.org/wiki/Exchangeable_image_file_format

 

So 'Painful' is definately the right word and not just from a BlackBerry perspective. :smileyhappy:


If you've been helped click on Like Button, if you've been saved buy the app. :smileyhappy:

Developer of stokLocker, Sympatico and Super Sentences.
Developer
BBSJdev
Posts: 6,118
Registered: ‎07-05-2012
My Device: Playbook, Dev Alpha C, Z10 LE, Z30

Re: Writing EXIF Data (And how to set the photo rating)

Sorry I know it wasn't exactly the answer you were looking for but I just thought I ought to warn you what you might be getting yourself in to. :smileyhappy:


If you've been helped click on Like Button, if you've been saved buy the app. :smileyhappy:

Developer of stokLocker, Sympatico and Super Sentences.
Developer
dbigham
Posts: 555
Registered: ‎04-01-2009
My Device: Z10, PlayBook

Re: Writing EXIF Data (And how to set the photo rating)

Incase others are reading this in the future and wondering how to do this: My conclusion is that the best way to set meta data such as:

 

- Photo rating

- Tags

- Description

 

... is NOT to use EXIF.  Rather, it seems that XMP is a much better choice:

 

http://en.wikipedia.org/wiki/Extensible_Metadata_Platform

 

Conveniently, BB10 comes with the XMP library, and it can be added to your project quite easily. (right click on project node in "Project Explorer", go to "Configure" popup menu item, then "Add Library")

 

Here's the code I added bo my main.cpp:

 

 

//-----------------------------------------------------------------------------
// XMP
//-----------------------------------------------------------------------------

// Must be defined to instantiate template classes
#define TXMP_STRING_TYPE std::string

// Must be defined to give access to XMPFiles
#define XMP_INCLUDE_XMPFILES 1

#define UNIX_ENV 1

#include "xmp/XMP.incl_cpp"
#include "xmp/XMP.hpp"

#include <iostream>
#include <fstream>

using namespace std;

//-----------------------------------------------------------------------------

 ... and here's what I added to the .h file that I wanted to use XMP in: (notice it doesn't include the XMP.incl_cpp from above)

 

//-----------------------------------------------------------------------------
// XMP
//-----------------------------------------------------------------------------

#define TXMP_STRING_TYPE std::string
#define XMP_INCLUDE_XMPFILES 1
#define UNIX_ENV 1

#include "xmp/XMP.hpp"

#include <iostream>
#include <fstream>

using namespace std;

//-----------------------------------------------------------------------------

 And finally, here's a big copy and paste of some of the key code that I adapted from sample code in the XMP SDK that I downloaded separately for reading/writing some common tags.  Feel free to use this code in your own apps, or to adapt it, if it's useful.

 

int ImageViewContainer::xmpWrite()
{
    qDebug() << "xmpWrite()";

    string filename = file.toStdString();

    if (!SXMPMeta::Initialize())
    {
        qDebug() << "XMP: An error occurred initializing XMP.";
        return -1;
    }

    XMP_OptionBits options = 0;

    #if UNIX_ENV
        options |= kXMPFiles_ServerMode;
    #endif

    if (!SXMPFiles::Initialize(options))
    {
        qDebug() << "XMP: An error occurred initializing SXMPFiles.";
        return -1;
    }

    try
    {
        // Try using the smart handler.
        XMP_OptionBits opts = kXMPFiles_OpenForUpdate | kXMPFiles_OpenUseSmartHandler;

        bool ok;
        SXMPFiles myFile;

        // Open the file.
        ok = myFile.OpenFile(filename, kXMP_UnknownFile, opts);
        if (!ok)
        {
            qDebug() << "XMP: No smart handler available for " + file + ". Trying packet scanning.";

            // Now try using packet scanning.
            opts = kXMPFiles_OpenForUpdate | kXMPFiles_OpenUsePacketScanning;
            ok = myFile.OpenFile(filename, kXMP_UnknownFile, opts);
        }

        // If the file is open then read the metadata.
        if (ok)
        {
            qDebug() << "XMP: Opened: " << file;

            // Create the xmp object and get the xmp data.
            SXMPMeta meta;
            myFile.GetXMP(&meta);

            // Should we be doing this?
            meta.SetProperty(kXMP_NS_XMP, "CreatorTool", "Updated by PhotoStar", 0);

            int ratingToSet = rating;
            if (ratingToSet == -1) { ratingToSet = 0; }
            meta.SetProperty(kXMP_NS_XMP, "Rating", QString::number(ratingToSet).toStdString(), 0);

            // Required before we can call other functions that refer to this namespace
            // without getting an exception.
            std::string tmp;
            meta.RegisterNamespace("http://ns.microsoft.com/photo/1.0/", "MicrosoftPhoto", &tmp);

            if (ratingToSet == -1)
            {
                meta.DeleteProperty("http://ns.microsoft.com/photo/1.0/", "Rating");
            }
            else
            {
                int microsoftRating;

                // Mapping:
                // xmp:Rating=1 -> MicrosoftPhoto:Rating=1
                // xmp:Rating=2 -> MicrosoftPhoto:Rating=25
                // xmp:Rating=3 -> MicrosoftPhoto:Rating=50
                // xmp:Rating=4 -> MicrosoftPhoto:Rating=75
                // xmp:Rating=5 -> MicrosoftPhoto:Rating=100

                if (ratingToSet == 1)
                {
                    microsoftRating = 1;
                }
                else
                {
                    microsoftRating = (ratingToSet - 1) * 25;
                }

                qDebug() << "MicrosoftPhoto:Rating: " << QString::number(microsoftRating);
                meta.SetProperty("http://ns.microsoft.com/photo/1.0/", "Rating", QString::number(microsoftRating).toStdString(), 0);
            }

            // Delete old tags. (?)
            meta.DeleteProperty(kXMP_NS_DC, "subject");

            // Tags
            for (int i = 0; i < tags.size(); ++i)
            {
                string value = tags[i].toStdString();
                meta.AppendArrayItem(kXMP_NS_DC, "subject", kXMP_PropArrayIsOrdered, value, 0);
                //meta.SetArrayItem(kXMP_NS_DC, "subject", i + 1, value, 0);
            }

            meta.SetLocalizedText(kXMP_NS_XMP, "Description", "en", "en-US", description.toStdString(), NULL);

            // Update the Metadata Date
            XMP_DateTime updatedTime;

            // Get the current time. This is a UTC time automatically adjusted for the local time.
            SXMPUtils::CurrentDateTime(&updatedTime);
            //if (meta.DoesPropertyExist(kXMP_NS_XMP, "MetadataDate"))
            //{
                meta.SetProperty_Date(kXMP_NS_XMP, "MetadataDate", updatedTime, 0);
            //}

            // Now update alt-text properties
            //meta.SetLocalizedText(kXMP_NS_DC, "title", "en", "en-US", "Kind of pretty");

            // For debugging
            if (false)
            {
                // Serialize the packet and write the buffer to a file.
                // Let the padding be computed and use the default linefeed and indents without limits.
                string metaBuffer;
                meta.SerializeToBuffer(&metaBuffer, 0, 0, "", "", 0);

                //qDebug() << QString::fromStdString(metaBuffer);

                // Write the packet to a file as RDF
                writeRDFToFile(&metaBuffer, filename + "_XMP_RDF.txt");
            }

            // Check we can put the XMP packet back into the file.
            if (myFile.CanPutXMP(meta))
            {
                // If so then update the file with the modified XMP.
                myFile.PutXMP(meta);
            }
            else
            {
                // Silent for now. TODO: Should this be indicated in
                // the UI somehow?
                qDebug() << "XMP: Can't write to file.";
            }

            // Close the SXMPFile. This *must* be called. The XMP is not actually written and the
            // disk file is not closed until this call is made.
            myFile.CloseFile();
        }
        else
        {
            qDebug() << "XMP: Unable to open " << file;
        }
    }
    catch(XMP_Error & e)
    {
        qDebug() << "XMP ERROR: " << QString::fromStdString(e.GetErrMsg());
    }

    // Terminate the toolkit
    SXMPFiles::Terminate();
    SXMPMeta::Terminate();

    return 0;
}

int ImageViewContainer::xmpRead()
{
    qDebug() << "xmpRead()";

    string filename = file.toStdString();

    if (!SXMPMeta::Initialize())
    {
        qDebug() << "XMP: An error occurred initializing XMP.";
        return -1;
    }

    XMP_OptionBits options = 0;

    #if UNIX_ENV
        options |= kXMPFiles_ServerMode;
    #endif

    if (!SXMPFiles::Initialize(options))
    {
        qDebug() << "XMP: An error occurred initializing SXMPFiles.";
        return -1;
    }

    try
    {
        // Try using the smart handler.
        XMP_OptionBits opts = kXMPFiles_OpenForRead | kXMPFiles_OpenUseSmartHandler;

        bool ok;
        SXMPFiles myFile;

        // Open the file.
        ok = myFile.OpenFile(filename, kXMP_UnknownFile, opts);
        if (!ok)
        {
            qDebug() << "XMP: No smart handler available for " + file + ". Trying packet scanning.";

            // Now try using packet scanning.
            opts = kXMPFiles_OpenForUpdate | kXMPFiles_OpenUsePacketScanning;
            ok = myFile.OpenFile(filename, kXMP_UnknownFile, opts);
        }

        // If the file is open then read the metadata.
        if (ok)
        {
            qDebug() << "XMP: Opened: " << file;

            // Create the xmp object and get the xmp data.
            SXMPMeta meta;
            myFile.GetXMP(&meta);

            bool exists;

            // Stores the value for the property.
            string value;

            // XMP Rating
            exists = meta.GetProperty(kXMP_NS_XMP, "Rating", &value, NULL);
            if (exists)
            {
                rating = QString::fromStdString(value).toInt(&ok);
                if (!ok)
                {
                    rating = -1;
                }
            }
            else
            {
                rating = -1;
            }

            // Microsoft Rating (only look for this if xmp:Rating is missing)
            if (rating == -1)
            {
                // Required before we can call other functions that refer to this namespace
                // without getting an exception.
                std::string tmp;
                meta.RegisterNamespace("http://ns.microsoft.com/photo/1.0/", "MicrosoftPhoto", &tmp);

                exists = meta.GetProperty("http://ns.microsoft.com/photo/1.0/", "MicrosoftPhoto:Rating", &value, NULL);
                //exists = meta.GetProperty(kXMP_NS_XMP, "Rating", &value, NULL);
                if (exists)
                {
                    rating = QString::fromStdString(value).toInt(&ok);
                    if (!ok)
                    {
                        rating = -1;
                    }
                    else
                    {
                        // The Microsoft rating is 0, 25, 50, 75, 100.
                        rating /= 25;
                    }
                }
                else
                {
                    rating = -1;
                }
            }

            value.clear();

            qDebug() << "XMP Rating: " << rating;

            // Tags
            tags.clear();
            int arraySize = meta.CountArrayItems(kXMP_NS_DC, "subject");
            for (int i = 1; i <= arraySize; i++)
            {
                meta.GetArrayItem(kXMP_NS_DC, "subject", i, &value, 0);
                qDebug() << "XMP Tag[" << i << "]: " << QString::fromStdString(value);
                tags.append(QString::fromStdString(value));
            }

            value.clear();

            // Description
            meta.GetLocalizedText(kXMP_NS_XMP, "Description", "en", "en-US", NULL, &value, NULL);
            description = QString::fromStdString(value);
            qDebug() << "XMP Description: " << description;

            timestamp = QDateTime();
            XMP_DateTime updatedTime;
            // Get the current time. This is a UTC time automatically adjusted for the local time.
            SXMPUtils::CurrentDateTime(&updatedTime);
            if (meta.DoesPropertyExist(kXMP_NS_XMP, "MetadataDate"))
            {
                meta.GetProperty_Date(kXMP_NS_XMP, "MetadataDate", &updatedTime, 0);
                if (updatedTime.hasDate)
                {
                    if (updatedTime.hasTime)
                    {
                        timestamp = QDateTime(QDate(updatedTime.year, updatedTime.month, updatedTime.day), QTime(updatedTime.hour, updatedTime.minute, updatedTime.second));
                    }
                    else
                    {
                        timestamp = QDateTime(QDate(updatedTime.year, updatedTime.month, updatedTime.day), QTime(0, 0, 0));
                    }
                }
            }

            // Close the SXMPFile. The resource file is already closed if it was
            // opened as read only but this call must still be made.
            myFile.CloseFile();
        }
        else
        {
            qDebug() << "XMP: Unable to open " << file;
        }
    }
    catch(XMP_Error & e)
    {
        qDebug() << "XMP ERROR: " << QString::fromStdString(e.GetErrMsg());
    }

    // Terminate the toolkit
    SXMPFiles::Terminate();
    SXMPMeta::Terminate();

    return 0;
}

 

Note that the above code makes use of some class member variables:

 

    int rating;
    QList<QString> tags;
    QString description;
    QDateTime timestamp;

 

One final note: There's some code that concerns itself with the MicrosoftPhoto:Rating XMP tag. This is because I'm on Windows and I've noticed that if you click on a photo in Windows Explorer, it allows you to view/edit some of the meta data (such as star rating) directly within Explorer. When you do this, it sets the MicrosoftPhoto:Rating tag (as well as the xmp:Rating tag, I think). Once it has set that, if your app updates the xmp:Rating tag but doesn't update the MicrosoftPhoto:Rating tag, and the file gets back onto Windows, then it continues to use the old rating prior to your app updating it. (it seems to ignore xmp:Rating if MicrosoftPhoto:Rating is present)