The Aviary SDK is now part of the Adobe Creative SDK

The Aviary SDK is now the Image Editing component in the Adobe Creative SDK. Visit CreativeSDK.com to access the latest Image Editing SDK and brand new SDK components by Adobe, offering features like store to Creative Cloud and publish to Behance.

High Resolution Image Editing

See the Setup Guide for details on getting high resolution output with the Gradle build of the Aviary SDK.

High Resolution Image Editing

Revision 2.0.0

Depending on the Aviary-SDK version you're using, you can post process images from 3MP up to 30MP (megapixels). This document will guide you in the process of editing high resolution images.

Table Of Contents


1. Preface

By default, the Aviary editor works on a medium resolution image in order to speed up the performance of the editor. In fact, the image returned in your onActivityResult will have, more or less, the same size of the device screen pixels.

If you want to save a high resolution version of the edited image, you should go through the following steps.

2. Edit the AndroidManifest.xml

First, you need to edit your AndroidManifest.xml file in order to enable the ContentProvider used by Aviary to store the information on the current edited Image.

When you enable the High Resolution image processing, you first need to include this entry inside the <application> tag of your AndroidManifest.xml file:

    <provider 
        android:name="com.aviary.android.feather.library.providers.FeatherContentProvider"
        android:exported="false"
        android:process=":your_process_name"
        android:authorities="your_unique_authority_name">
    </provider>

IMPORTANT: Do not use the sample android:authorities value included in this code. You MUST use the package name of your own app as the string value.

The authorities value must be unique inside the user's device (otherwise users cannot install the application from the Google Play Store), so it is always good practice to use your application package name. (Read more about ContentProvider authority here).

3. Create the Session key

In the calling Intent you must pass an extra string, the high resolution session id, like this:

import com.aviary.android.feather.headless.utils.StringUtils;
...
final String session_name = StringUtils.getSha256( System.currentTimeMillis() + API_KEY );
newIntent.putExtra( Constants.EXTRA_OUTPUT_HIRES_SESSION_ID, session_name );

The session string must be unique and must be 64 char in length. Once Aviary starts, it will start collecting the information of every action performed on the image and will store those actions in the internal ContentProvider (remember to add the provider tag to the AndroidManifest first!).

4. Retrieve the Session key

  1. Once your activity calls the "onActivityResult" method, you first need to retrieve the session id used for editing the Image (this will be the same string you passed in the calling Intent).

    public void onActivityResult( int requestCode, int resultCode, Intent data ) {
        if ( resultCode == RESULT_OK ) {
            if( requestCode == ACTION_REQUEST_AVIARY ) {
                Bundle extra = data.getExtras();
                if( null != extra ) {
                    if( extra.containsKey(Constants.EXTRA_OUTPUT_HIRES_SESSION_ID) ) {
                        final String session_id = 
                            extra.getString(Constants.EXTRA_OUTPUT_HIRES_SESSION_ID);
                    }
                }
            }
        }
    }
    

5. Process the High Resolution Image

5.1. At this point you need to create the output file, where the high resolution image will be saved.

File output = new File(..);
output.createNewFile();

Then initialize the session Uri:

Uri sessionUri = FeatherContentProvider.SessionsDbColumns.getContentUri(this, session_id);

session_id is the String you read first in the onActivityResult method.


5.2. Create a query which will return a Cursor with the information about the given session

Cursor cursor = getContentResolver().query( sessionUri, null, null, null, null );

5.3. And finally initialize the Session object using the cursor just created.

FeatherContentProvider.SessionsDbColumns.Session session = null;
if ( null != cursor ) {
    session = FeatherContentProvider.SessionsDbColumns.Session.Create( cursor );
    cursor.close();
}   

The Session class contains the following information:

  • id: [long] the internal id of the session
  • session: [String] the session name, 64 char wide value (the same one that you passed in the Intent)
  • ctime: [long] the session creation time
  • file_name: [String] the original image path used as input (the same one that you passed in the calling Intent)

5.4. Now you must query the ContentProvider to get the list of actions to be applied on the original image.

Uri actionsUri = FeatherContentProvider.ActionsDbColumns
    .getContentUri( this, session.session );
Cursor actionCursor = getContentResolver().query( actionsUri, null, null, null, null );

5.5. And finally the steps to load, apply the actions and save the HD image.

Note: these steps should be performed in a separate thread, like an AsyncTask.


5.6. Create an instance of MoaHD class.

import import com.aviary.android.feather.headless.filters.NativeFilterProxy;
import com.aviary.android.feather.headless.moa.MoaHD;
import com.aviary.android.feather.library.providers.FeatherContentProvider.ActionsDbColumns.Action;
...

// This must be invoked if the FeatherActivity runs in a separate process (this depends
// on your AndroidManifest.xml configuration)
try {
    NativeFilterProxy.init(getBaseContext());
} catch( AviaryInitializationException e ) {
    e.printStackTrace();
    return;
}

MoaHD moa = new MoaHD();

5.6.1 High Resolution limitations

If your application has the required permission (hires) then you can set the maximum size of the output image by setting setMaxMegaPixels in this way. Remember to call this method just before loading the source image:

moa.setMaxMegaPixels( MegaPixels.Mp30 );

5.7. Now load the image in memory, note that the srcPath can be either a string absolute path or an int ( int if you're passing a filedescriptor - see ParcelFileDescriptor.getFd()))

The srcPath is the same you passed in the calling Intent (but it can also be retrieved from the session.file_name field)

try {
    moa.load( srcPath );
} catch( AviaryExecutionException e ) {
    e.printStackTrace();
    Log.d( LOG_TAG, "error code: " + e.getCode() );
    return;
}

Both load and save methods of the MoaHD class will throw an exception if there is an error. The exception is an instance of the AviaryExecutionException class.
You can inspect the error code of the exception to get more informations about the error ( using the getCode method of the AviaryExecutionException class).

Possible exceptions are listed below:

  • FILE_NOT_FOUND_ERROR (value: 1)
  • FILE_EXCEED_MAX_SIZE_ERROR (value: 2)
  • FILE_NOT_LOADED_ERROR (value: 3)
  • INVALID_CONTEXT_ERROR (value: 4)
  • FILE_ALREADY_LOADED_ERROR (value: 5)
  • DECODER_NOT_FOUND_ERROR (value: 6)
  • ENCODER_NOT_FOUND_ERROR (value: 7)
  • DECODE_ERROR (value: 8)
  • ENCODE_ERROR (value: 9)
  • INSTANCE_NULL_ERROR (value: 10)
  • UNKNOWN_ERROR (value: 11)

5.8. Then, for every row in the actions Cursor ( the Cursor you created at point 5.4 ), apply the action to the moa instance:

if ( actionCursor.moveToFirst() ) {
    do {
        // load the action from the current cursor
        Action action = Action.Create( actionCursor );
        if ( null != action ) {
            Log.d( LOG_TAG, "executing: " + action.id + "(" + action.session_id + " on " + action.ctime + ") = " + action.getActions() );

            // apply a list of actions to the current image
            moa.applyActions( action.getActions() );
        } else {
            Log.e( LOG_TAG, "Woa, something went wrong! Invalid action returned" );
        }

        // move the cursor to next position
    } while ( actionCursor.moveToNext() );
}

5.9. Finally you can save the output image. dstPath must be an absolute string path:

try {
    moa.save( dstPath );
} catch( AviaryExecutionException e ) {
    e.printStackTrace();
    Log.d( LOG_TAG, "code: " + e.getCode() );
} finally {
    // remember to call this method once you've done with the moa instance
    moa.dispose();
}

5.10. And remember to close the cursor:

actionCursor.close();

6. Retain EXIF Tags

By default the Aviary-SDK will retain the original image's EXIF data and save it into the edited image. But when you process the hi-res (see the high resolution section) image after Aviary is closed, you'll need to do some additional configuration to retain the EXIF data:

  • Use the included ExifInterfaceExtended class (inside the it.sephiroth.android.library.media package), which can handle a larger number of tags, rather than the Android default ExifInterface class.

The full exif library is also distributed with sources here: https://github.com/sephiroth74/Android-Exif-Extended

  • Create a new instance of ExifInterfaceExtended, passing the original source image:

    import it.sephiroth.android.library.media.ExifInterfaceExtended;
    ...
    ExifInterfaceExtended originalExif = new ExifInterfaceExtended( srcPath );
    
  • Copy the original exif tags into a temporary Bundle object:

    Bundle out = new Bundle();
    originalExif.copyTo(out);
    
  • When the hi-res process is complete, create a new ExifInterfaceExtended instance passing the path of the just created image:

    newExif = new ExifInterfaceExtended( dstPath );
    
  • Copy the temporary bundle object back to the new exif instance:

    // remember the current image width an height
    int imageWidth = newExif.getAttributeInt( ExifInterfaceExtended.TAG_EXIF_IMAGE_WIDTH, 0 );
    int imageLength = newExif.getAttributeInt( ExifInterfaceExtended.TAG_EXIF_IMAGE_LENGTH, 0 );
    
    
    // This method will copy all the attributes of the 
    // original EXIF into the new EXIF instance
    newExif.copyFrom( out, true );
    
    
    // restore the correct tags
    newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_IMAGE_WIDTH, String.valueOf( imageWidth ) );
    newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_IMAGE_LENGTH, String.valueOf( imageLength ) );
    
    
    // the editor already auto rotate the image pixels
    newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_ORIENTATION, "0" );
    
    
    // let's update the software tag too
    newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_SOFTWARE, "Aviary" );
    
    
    // update the datetime
    newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_DATETIME, ExifInterfaceExtended.getExifFormattedDate( new Date() ) );
    
  • Save the new exif tags:

    try {
        newExif.saveAttributes();
    } catch ( IOException e ) {
        e.printStackTrace();
    }
    

Remember to save the exif tags after you call the moa.save() method.

8. Full Code

This is the full code for hi-res processing ( you can find the same code in our Demo app ). In the onActivityResult of our app we're calling the following method, passing the same session-id we've used to start the Aviary-SDK:

/**
 * Start the hi-res image processing.
 */
private void processHD( final String session_name, File destination ) {

    Log.i( LOG_TAG, "processHD: " + session_name );

    try {
        if ( destination == null || !destination.createNewFile() ) {
            Log.e( LOG_TAG, "Failed to create a new file" );
            return;
        }
    } catch ( IOException e ) {
        Log.e( LOG_TAG, e.getMessage() );
        Toast.makeText( this, e.getLocalizedMessage(), Toast.LENGTH_SHORT ).show();
        return;
    }

    String error = null;

    // Now we need to fetch the session information from the content provider
    FeatherContentProvider.SessionsDbColumns.Session session = null;

    Uri sessionUri = FeatherContentProvider.SessionsDbColumns.getContentUri( this, session_name );

    // this query will return a cursor with the informations about the given session
    Cursor cursor = getContentResolver().query( sessionUri, null, null, null, null );

    if ( null != cursor ) {
        session = FeatherContentProvider.SessionsDbColumns.Session.Create( cursor );
        cursor.close();
    }

    if ( null != session ) {
        // Print out the session informations
        Log.d( LOG_TAG, "session.id: " + session.id ); // session _id
        Log.d( LOG_TAG, "session.name: " + session.session ); // session name
        Log.d( LOG_TAG, "session.ctime: " + session.ctime ); // creation time
        Log.d( LOG_TAG, "session.file_name: " + session.file_name ); // original file, it is the same you passed in the
                                                                    // startActivityForResult Intent

        // Now, based on the session information we need to retrieve
        // the list of actions to apply to the hi-res image
        Uri actionsUri = FeatherContentProvider.ActionsDbColumns.getContentUri( this, session.session );

        // this query will return the list of actions performed on the original file, during the FeatherActivity session.
        // Now you can apply each action to the hi-res image to replicate the same result on the bigger image
        cursor = getContentResolver().query( actionsUri, null, null, null, null );

        if ( null != cursor ) {
            // If the cursor is valid we will start a new asynctask process to query the cursor
            // and apply all the actions in a queue
            HDAsyncTask task = new HDAsyncTask( Uri.parse( session.file_name ), destination.getAbsolutePath(), session_name );
            task.execute( cursor );
        } else {
            error = "Failed to retrieve the list of actions!";
        }
    } else {
        error = "Failed to retrieve the session informations";
    }

    if ( null != error ) {
        Toast.makeText( this, error, Toast.LENGTH_LONG ).show();
    }
}

Then this is the HDTask code:

private class HDAsyncTask extends AsyncTask<Cursor, Integer, String> {
    Uri uri_;
    String dstPath_;
    ProgressDialog progress_;
    String session_;
    ExifInterfaceExtended exif_;

    /**
     * Initialize the HiRes async task
     * 
     * @param source
     *           - source image file
     * @param destination
     *           - destination image file
     * @param session_id
     *           - the session id used to retrieve the list of actions
     */
    public HDAsyncTask( Uri source, String destination, String session_id ) {
        uri_ = source;
        dstPath_ = destination;
        session_ = session_id;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        progress_ = new ProgressDialog( MainActivity.this );
        progress_.setIndeterminate( true );
        progress_.setTitle( "Processing Hi-res image" );
        progress_.setMessage( "Loading image..." );
        progress_.setProgressStyle( ProgressDialog.STYLE_SPINNER );
        progress_.setCancelable( false );
        progress_.show();
    }

    @Override
    protected void onProgressUpdate( Integer... values ) {
        super.onProgressUpdate( values );

        final int index = values[0];
        final int total = values[1];
        String message = "";

        if ( index == -1 )
            message = "Saving image...";
        else
            message = "Applying action " + ( index + 1 ) + " of " + ( total );

        progress_.setMessage( message );

        Log.d( LOG_TAG, index + "/" + total + ", message: " + message );
    }

    @Override
    protected String doInBackground( Cursor... params ) {
        Cursor cursor = params[0];

        if ( null != cursor ) {

            // IMPORTANT NOTE:
            // If in your manifest you're using a different process for the FeatherActivity Activity
            // then you *MUST* call this method before using any of the MoaHD methods, otherwise
            // you will receive a java exception
            try {
                NativeFilterProxy.init( getBaseContext() );
            } catch ( AviaryInitializationException e ) {
                return e.getMessage();
            }


            // Initialize the class to perform HD operations
            MoaHD moa = new MoaHD();
 
            //  by default the maximum image size for hi-res is set to 13Mp
            moa.setMaxMegaPixels( MegaPixels.Mp15 );

            boolean loaded;
            try {
                loaded = loadImage( moa );
            } catch ( AviaryExecutionException e ) {
                return e.getMessage();
            }

            // if image is loaded
            if ( loaded ) {

                final int total_actions = cursor.getCount();

                Log.d( LOG_TAG, "total actions: " + total_actions );

                if ( cursor.moveToFirst() ) {

                    // get the total number of actions in the queue
                    // we're adding also the 'load' and the 'save' action to the total count

                    // now for each action in the given cursor, apply the action to
                    // the MoaHD instance
                    do {
                        // send a progress notification to the progressbar dialog
                        publishProgress( cursor.getPosition(), total_actions );

                        // load the action from the current cursor
                        Action action = Action.Create( cursor );
                        if ( null != action ) {
                            Log.d( LOG_TAG, "executing: " + action.id + "(" + action.session_id + " on " + action.ctime + ") = "
                                    + action.getActions() );

                            // apply a list of actions to the current image
                            moa.applyActions( action.getActions() );
                        } else {
                            Log.e( LOG_TAG, "Woa, something went wrong! Invalid action returned" );
                        }

                        // move the cursor to next position
                    } while ( cursor.moveToNext() );
                }

                // at the end of all the operations we need to save
                // the modified image to a new file
                publishProgress( -1, -1 );

                try {
                    moa.save( dstPath_ );
                } catch ( AviaryExecutionException e ) {
                    return e.getMessage();
                } finally {
                    moa.dispose();
                }

                // ok, now we can save the source image EXIF tags
                // to the new image
                if ( null != exif_ ) {
                    saveExif( exif_, dstPath_ );
                }

            } else {
                return "Failed to load the image";
            }

            cursor.close();

            // and unload the current bitmap. Note that you *MUST* call this method to free the memory allocated with the load
            // method

            if( moa.isLoaded() ) {
                try {
                    moa.unload();
                } catch ( AviaryExecutionException e ) {}
            }

            if( !moa.isDisposed() ) {
                // finally dispose the moahd instance
                moa.dispose();
            }
        }

        return null;
    }

    /**
     * Save the Exif tags to the new image
     * 
     * @param originalExif
     * @param filename
     */
    private void saveExif( ExifInterfaceExtended originalExif, String filename ) {
        // ok, now we can save back the EXIF tags
        // to the new file
        ExifInterfaceExtended newExif = null;
        try {
            newExif = new ExifInterfaceExtended( dstPath_ );
        } catch ( IOException e ) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        if ( null != newExif && null != originalExif ) {

            // save the original exif tags to a Bundle object
            Bundle out = new Bundle();
            originalExif.copyTo( out );

            // import the exif tags from the original file 
            newExif.copyFrom( out, true );

            // this should be changed because the editor already rotate the image
            newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_ORIENTATION, "0" );
            // let's update the software tag too
            newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_SOFTWARE, "Aviary " + SDKUtils.SDK_VERSION );
            // ...and the modification date
            newExif.setAttribute( ExifInterfaceExtended.TAG_EXIF_DATETIME, ExifInterfaceExtended.formatDate( new Date() ) );
            try {
                newExif.saveAttributes();
            } catch ( IOException e ) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onPostExecute( String errorString ) {
        super.onPostExecute( errorString );

        if ( progress_.getWindow() != null ) {
            progress_.dismiss();
        }

        // in case we had an error...
        if ( null != errorString ) {
            Toast.makeText( MainActivity.this, "There was an error: " + errorString, Toast.LENGTH_SHORT ).show();
            return;
        }

        // finally notify the MediaScanner of the new generated file
        updateMedia( dstPath_ );

        // now ask the user if he want to see the saved image
        new AlertDialog.Builder( MainActivity.this ).setTitle( "File saved" )
                .setMessage( "File saved in " + dstPath_ + ". Do you want to see the HD file?" )
                .setPositiveButton( android.R.string.yes, new OnClickListener() {

                    @Override
                    public void onClick( DialogInterface dialog, int which ) {

                        Intent intent = new Intent( Intent.ACTION_VIEW );

                        String filepath = dstPath_;
                        if ( !filepath.startsWith( "file:" ) ) {
                            filepath = "file://" + filepath;
                        }
                        intent.setDataAndType( Uri.parse( filepath ), "image/jpeg" );
                        startActivity( intent );

                    }
                } ).setNegativeButton( android.R.string.no, null ).show();

        // we don't need the session anymore, now we can delete it.
        Log.d( LOG_TAG, "delete session: " + session_ );
        deleteSession( session_ );
    }

    private boolean loadImage( MoaHD moa ) throws AviaryExecutionException {
        final String srcPath = IOUtils.getRealFilePath( MainActivity.this, uri_ );
        if ( srcPath != null ) {

            // Let's try to load the EXIF tags from
            // the source image
            try {
                exif_ = new ExifInterfaceExtended( srcPath );
            } catch ( IOException e ) {
                e.printStackTrace();
            }
            moa.load( srcPath );
            return true;

        } else {

            if ( SystemUtils.isHoneyComb() ) {
                InputStream stream = null;
                try {
                    stream = getContentResolver().openInputStream( uri_ );
                } catch ( IOException e ) {
                    // stream is not valid
                    e.printStackTrace();
                    return false;
                }

                moa.load( stream );
                return true;

            } else {
                ParcelFileDescriptor fd = null;
                try {
                    fd = getContentResolver().openFileDescriptor( uri_, "r" );
                } catch ( FileNotFoundException e ) {
                    // file not found
                    e.printStackTrace();
                    return false;
                }

                moa.load( fd.getFileDescriptor() );
                return true;
            }
        }
    }
}

8. Example

If you want to see the complete code listing, you can download the sample application from here.