TSM - Using Azure Function to write picture coordinates

Radu Vunvulea - Solution Architect

et's take a look at Azure Functions, from a developer perspective. In this paper, 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.

What are 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 server-less way, without thinking about where the code is hosted. You simply write the code and run it as Azure Functions. There are many things that we can say about Azure Functions, but the most important things for me are:

Our mission

The mission is to write an Azure Function that:

First step  - Create the Azure Function

The first step is to create an Azure Function. This can be done from Azure Portal. We can create a 'GenericWebhookCSharp' function initially and we will delete the parts that we don't need.

Once we have 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 to 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 requested, they are not stored in Azure Functions. They are stored as API Connections and the same connection can be used in multiple functions if needed. This trigger will be called 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 at 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 direction 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 the ""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 triggers run. If you need to write something in the logs, that needs to be specified as parameter, you can use TraceWriter with success.

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, the reference existing libraries or the 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 appear:

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 for normal usage, but specifies Azure Functions to load the assembly from its 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 you hit the Save button, the function is compiled. Any errors during the build are displayed in the Logs window.

From now one, each time you copy/upload a new file in your OneDrive folder, your function will be called automatically. In the logs you will 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 to read the GPS information easily. 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 later be used to draw the coordinates on the image.

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "ExifLib": "1.7.0.0",
        "System.Drawing.Primitives": "4.3.0"
      }
    }
   }
}

When you click the 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 have this information available.

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;
}

The next step is to call our method from the Run method (string locationText = GetCoordinate(inputFile, log). Once we click 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 will be an image whose coordinates are written in RED, see below.

Conclusion

In this paper, we discovered how we can write an Azure Function that adds, as Watermark, the GPS coordinates of the location where the picture was taken.

Azure Functions are a powerful tool which allows us to focus only on code, without having to think about infrastructure or other stuff.

The full code can be found on GitHub.