Skip to main content

Writing GPS coordinates on an image from OneDrive using Azure Functions

Let's take a look over Azure Functions, from a developer perspective. In this post we will write an Azure Function that adds the GPS coordinates of a picture as watermark.

If you want to jump to the code, without any other explanations, than feel free to check the GitHub repository - https://github.com/vunvulear/Stuff/tree/master/Azure/azure-function-extractgpslocation-onedrive-drawwatermark 

What is Azure Functions?
The best explanation that I can give is Azure Functions are the AWS Lambda in Azure world. They allow us to execute code in a serverless way, without thinking about where you host the code, you just write the code and run it as Azure Functions.
There are many things that we can say about Azure Functions, the most important things for me are:
  • You can write code in different languages (C#, Node.JS, PHP, Python, Bash, Batch, PowerShell)
  • Multiple connectors and triggers, from queues to Blob Storage and Google Drive, all of them are supported
  • Flexible and transparent charging model, you pay only what you use
  • Real time processing
  • Refs to external libraries (e.g. NuGet) are supported

Our mission
The mission is to write an Azure Functions that:
  • Is triggered each time when a new image is added to a OneDrive folder
  • Extract the GPS coordinates from image attributes (if are available)
  • Draw GPS coordinates on the image
  • Save image in another folder in OneDrive 
First step  - Create the Azure Function
The first step is to create an Azure Function. This can be done from Azure Portal, a nice documentation related to it can be found here. We can create a 'GenericWebhookCSharp' function initially and we will delete the parts that we don't need.
Once we done this, we will need to go to the Integrate tab and specify our trigger. Delete all the Triggers and Outputs that are already defined. We will define them again.

Triggers and binding
In our case we will need use External File and in the connection field we will create a new one that will point to OneDrive. It is important to know that even if your access credentials are requests, they are not stored in Azure Functions. They are stored are API Connections and the same connection can be used in multiple functions if needed. This trigger will be called in the moment when a new file will be copied in the given path. As output, we will need to use the same External File. Now, we can reuse the same connection, that we created for the trigger - onedrive_ONEDRIVE. 
Now, let's take a look on the binding. 
{
  "bindings": [
    {
      "type": "apiHubFileTrigger",
      "name": "inputFile",
      "path": "Imagini/Peliculă/{filename}",
      "connection": "onedrive_ONEDRIVE",
      "direction": "in"
    },
    {
      "type": "apiHubFile",
      "name": "outputFile",
      "path": "Imagini/Peliculă/pictureswithgpswatermark/{rand-guid}.jpg",
      "connection": "onedrive_ONEDRIVE",
      "direction": "out"
    }
  ],
  "disabled": false
}

The first binding specifies the trigger. As we can see the direct is in and the connection is done to OneDrive. The 'path' is relative to our OneDrive. In my case, the folder that is monitored is 'Imagini/Peliculă'. {filename} is the file parameter. In Azure Function, we will refer to the input file using the 'name' attribute - inputFile.
Similar to it, we have the output, that is written in ""Imagini/Peliculă/pictureswithgpswatermark" folder. '{rand-guid}' is used to generate a random name for each image. 

Writing an empty function
As we can see below, we have inputFile and outputFile as parameters to the Run method. This method is the entry point each time when trigger runs. If you need to write something to logs, you can use TraceWriter with success, that needs to be specified as parameter.
public static void Run(Stream inputFile, Stream outputFile, TraceWriter log)
{
     log.Info("Image Process Starts"); 

     log.Info("Image Process Ends");
}

We can define our own class, reference existing libraries or nuget packages. To be able to work with files and this type of binding we'll need to add a reference to ApiHub, otherwise an encrypted error will be throw:
Exception while executing function: Functions.SaasFileTriggerCSharp1. Microsoft.Azure.WebJobs.Host: One or more errors occurred. Exception binding parameter 'input'. Microsoft.Azure.ApiHub.Sdk

The reference is added as a normal using, but specifies to Azure Functions to load the assembly from his shared repository.
#r "Microsoft.Azure.WebJobs.Extensions.ApiHub" //

Save and Run
Before hitting the Save button, make sure that Logs window is visible. This is useful, because each time when you hit Save button, the function is compiled. Any error during the build are displayed in the Logs window.

From now one, each time when you copy/upload a new file in your OneDrive folder, your function will be called automatically. In the logs you shall be able to see the output logs.

Reading the GPS location
To be able to read the GPS location from images, we will use ExifLib. This nuget package allow us easily to read GPS information. To be able to push a Nuget package, we will need to open project.json and add a dependence to our nuget package. Below you can find how the JSON should look like. I also added the Nuget package that will be used later on to draw the coordinates on the image
{
  "frameworks": {
    "net46":{
      "dependencies": {
        "ExifLib": "1.7.0.0",
        "System.Drawing.Primitives": "4.3.0"
      }
    }
   }
}

In the moment when you click Save button, you will see that the function is compiled and the Nuget packages and all package dependencies are downloaded.
When we run the code that extracts the GPS location we shall take into account the cases when an image doesn't has this information.
private static string GetCoordinate(Stream image, TraceWriter log)
{
    log.Info("Extract location information");
    ExifReader exifReader = new ExifReader(image);
    double[] latitudeComponents;
    exifReader.GetTagValue(ExifTags.GPSLatitude, out latitudeComponents);
    double[] longitudeComponents;
    exifReader.GetTagValue(ExifTags.GPSLongitude, out longitudeComponents);

    log.Info("Prepare string content");
    string location = string.Empty;
    if (latitudeComponents == null ||
        longitudeComponents == null)
    {
        location = "No GPS location";
    }
    else
    {
        double latitude = 0;
        double longitude = 0;
        latitude = latitudeComponents[0] + latitudeComponents[1] / 60 + latitudeComponents[2] / 3600;
        longitude = longitudeComponents[0] + longitudeComponents[1] / 60 + longitudeComponents[2] / 3600;

        location = $"Latitude: '{latitude}' | Longitude: '{longitude}'";
    }

    return location;
}

Next step is to call our method from the Run method (string locationText = GetCoordinate(inputFile, log). Once we click on save, the GPS location for each image can be found in the log window.

string locationText = GetCoordinate(inputFile, log);
log.Info($"Text to be written: '{locationText}'");
---- log window ----
2016-12-06T00:06:35.680 Image Process Starts
2016-12-06T00:06:35.680 Extract location information
2016-12-06T00:06:35.680 Prepare string content
2016-12-06T00:06:35.680 Text to be written: 'Latitude: '46.7636219722222' | Longitude: '23.5550620833333''


Write the watermark (coordinates)
The last step is to write the text on the image and copy the image stream to the output. The code is the same code that is required in a console application for the same task.

private static void WriteWatermark(string watermarkContent, Stream originalImage, Stream newImage, TraceWriter log)
{
    log.Info("Write text to picture");
    using (Image inputImage = Image.FromStream(originalImage, true))
    {
        using (Graphics graphic = Graphics.FromImage(inputImage))
        {
            graphic.SmoothingMode = SmoothingMode.HighQuality;
            graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphic.PixelOffsetMode = PixelOffsetMode.HighQuality;
            graphic.DrawString(watermarkContent, new Font("Tahoma", 100, FontStyle.Bold), Brushes.Red, 200, 200);
            graphic.Flush();

            log.Info("Write to the output stream");
            inputImage.Save(newImage, ImageFormat.Jpeg);
        }
    }
}

Don't forget to reset the cursor position of the inputFile stream before calling the WriteWatermark. This is necessary because reading coordinates will move the cursor from position 0.
In the end, the run method should look like this:
public static void Run(Stream inputFile, Stream outputFile, TraceWriter log)
{
    
    log.Info("Image Process Starts"); 

    string locationText = GetCoordinate(inputFile, log);
    log.Info($"Text to be written: '{locationText}'");

    // Reset position. After Exif operations the cursor location is not on position 0 anymore;
    inputFile.Position = 0;

    WriteWatermark(locationText, inputFile, outputFile, log);

    log.Info("Image Process Ends");
}

Final output
The final output, of our function shall be an image that was the coordinates written with RED, see below.

Conclusion
In this post we discovered how we can write an Azure Function that adds as watermark the GPS coordinates of the location where the picture was taken. In the next post we will discover how we can do the same think from Visual Studio directly and how we can integrate CI to Azure Function.

Full code can be found on GitHub - https://github.com/vunvulear/Stuff/tree/master/Azure/azure-function-extractgpslocation-onedrive-drawwatermark 

Next post about Azure Functions - Azure Functions integration with Visual Studio and CI

Comments

Popular posts from this blog

How to check in AngularJS if a service was register or not

There are cases when you need to check in a service or a controller was register in AngularJS.
For example a valid use case is when you have the same implementation running on multiple application. In this case, you may want to intercept the HTTP provider and add a custom step there. This step don’t needs to run on all the application, only in the one where the service exist and register.
A solution for this case would be to have a flag in the configuration that specify this. In the core you would have an IF that would check the value of this flag.
Another solution is to check if a specific service was register in AngularJS or not. If the service was register that you would execute your own logic.
To check if a service was register or not in AngularJS container you need to call the ‘has’ method of ‘inhector’. It will return TRUE if the service was register.
if ($injector.has('httpInterceptorService')) { $httpProvider.interceptors.push('httpInterceptorService&#…

ADO.NET provider with invariant name 'System.Data.SqlClient' could not be loaded

Today blog post will be started with the following error when running DB tests on the CI machine:
threw exception: System.InvalidOperationException: The Entity Framework provider type 'System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer' registered in the application config file for the ADO.NET provider with invariant name 'System.Data.SqlClient' could not be loaded. Make sure that the assembly-qualified name is used and that the assembly is available to the running application. See http://go.microsoft.com/fwlink/?LinkId=260882 for more information. at System.Data.Entity.Infrastructure.DependencyResolution.ProviderServicesFactory.GetInstance(String providerTypeName, String providerInvariantName) This error happened only on the Continuous Integration machine. On the devs machines, everything has fine. The classic problem – on my machine it’s working. The CI has the following configuration:

TeamCity.NET 4.51EF 6.0.2VS2013
It seems that there …

Run native .NET application in Docker (.NET Framework 4.6.2)

Scope
The main scope of this post is to see how we can run a legacy application written in .NET Framework in Docker.

Context
First of all, let’s define what is a legacy application in our context. By a legacy application we understand an application that runs .NET Framework 3.5 or higher in a production environment where we don’t have any more the people or documentation that would help us to understand what is happening behind the scene.
In this scenarios, you might want to migrate the current solution from a standard environment to Docker. There are many advantages for such a migration, like:

Continuous DeploymentTestingIsolationSecurity at container levelVersioning ControlEnvironment Standardization
Until now, we didn’t had the possibility to run a .NET application in Docker. With .NET Core, there was support for .NET Core in Docker, but migration from a full .NET framework to .NET Core can be costly and even impossible. Not only because of lack of features, but also because once you…