Skip to main content

How to upload images from the camera control to Azure blob storage

We have seen several customers asking us this question- “Can I upload the photos taken using the Camera control in PowerApps to an Azure blob storage?”. The answer is : “Yes certainly, you can do that using a Custom API in PowerApps”.  The immediate next question is : “Do you have an example for such an API?”. This blog post is attempting to provide an answer to that question.

This article explains how to create a Azure API App which takes in a image file, stores it on an Azure blob storage account and returns the URL of the image. This API can then be called from any application including PowerApps.

The complete source code for this solution can be found at https://github.com/pratapladhani/ImageUploadAPI.

Lets start.


Create an Azure API App

Create a new ASP.NET Web Application > Azure API App > by the name ImageUploadAPI.

Select the Host in the cloud checkbox.

 

image

 

Provide the API App Name, select the appropriate Subscription, Resource Group and App Service Plan and click on Create.

 image

 

Create a New Class in the Controllers folder by the name “AddFileParamTypes.cs”. Replace the code in the file with the code given below:

using System.Collections.Generic;
using System.Web.Http.Description;
using Swashbuckle.Swagger;

namespace ImageUploadAPI.Controllers
{
    public class AddFileParamTypes : IOperationFilter
    {
        public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
        {
            if (operation.operationId == "UploadImage")  // SwaggerOperation
            {
                operation.consumes.Add("multipart/form-data");
                operation.parameters = new List<Parameter>
                {
                    new Parameter
                    {
                        name = "file",
                        required = true,
                        type = "file",
                        @in = "formData",
                        vendorExtensions = new Dictionary<string, object> { {"x-ms-media-kind", "image" } }
                    }
                };

                operation.parameters.Add(new Parameter()
                {
                    name = "fileName",
                    @in = "query",
                    required = false,
                    type = "string"
                });
            }
        }
    }
}

Create a New Class in the Controllers folder by the name “InMemoryMultipartFormDataStreamProvider.cs”. Replace the code in the file with the code given below:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace ImageUploadAPI.Controllers
{
    public class InMemoryMultipartFormDataStreamProvider : MultipartStreamProvider
    {
        private NameValueCollection _formData = new NameValueCollection();
        private List<HttpContent> _fileContents = new List<HttpContent>();

        // Set of indexes of which HttpContents we designate as form data
        private Collection<bool> _isFormData = new Collection<bool>();

        /// <summary>
        /// Gets a <see cref="NameValueCollection"/> of form data passed as part of the multipart form data.
        /// </summary>
        public NameValueCollection FormData
        {
            get { return _formData; }
        }

        /// <summary>
        /// Gets list of <see cref="HttpContent"/>s which contain uploaded files as in-memory representation.
        /// </summary>
        public List<HttpContent> Files
        {
            get { return _fileContents; }
        }

        public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
        {
            // For form data, Content-Disposition header is a requirement
            ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
            if (contentDisposition != null)
            {
                // We will post process this as form data
                _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));

                return new MemoryStream();
            }

            // If no Content-Disposition header was present.
            throw new InvalidOperationException(string.Format("Did not find required '{0}' header field in MIME multipart body part..", "Content-Disposition"));
        }

        /// <summary>
        /// Read the non-file contents as form data.
        /// </summary>
        /// <returns></returns>
        public override async Task ExecutePostProcessingAsync()
        {
            // Find instances of non-file HttpContents and read them asynchronously
            // to get the string content and then add that as form data
            for (int index = 0; index < Contents.Count; index++)
            {
                if (_isFormData[index])
                {
                    HttpContent formContent = Contents[index];
                    // Extract name from Content-Disposition header. We know from earlier that the header is present.
                    ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition;
                    string formFieldName = UnquoteToken(contentDisposition.Name) ?? String.Empty;

                    // Read the contents as string data and add to form data
                    string formFieldValue = await formContent.ReadAsStringAsync();
                    FormData.Add(formFieldName, formFieldValue);
                }
                else
                {
                    _fileContents.Add(Contents[index]);
                }
            }
        }

        /// <summary>
        /// Remove bounding quotes on a token if present
        /// </summary>
        /// <param name="token">Token to unquote.</param>
        /// <returns>Unquoted token.</returns>
        private static string UnquoteToken(string token)
        {
            if (String.IsNullOrWhiteSpace(token))
            {
                return token;
            }

            if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
            {
                return token.Substring(1, token.Length - 2);
            }

            return token;
        }
    }
}

Create a New Class in the Controllers folder by the name “UploadedFileInfo.cs”. Replace the code in the file with the code given below:

namespace ImageUploadAPI.Controllers
{
    public class UploadedFileInfo
    {
        public string FileName { get; set; }
        public string FileExtension { get; set; }
        public string FileURL { get; set; }
        public string ContentType { get; set; }
    }
}

Add the Windows Azure Storage Nuget Package to the project by running the following command in the Package Manager Console.

Install-Package WindowsAzure.Storage 

 

Delete the ValuesController class from the Controllers folder

Create a new Class in the Controllers folder by the name “UploadImageController.cs”.  Replace the code in the file with the following code.

using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.WindowsAzure.Storage;
using System.Configuration;
using ImageUploadAPI.Controllers;
using Swashbuckle.Swagger.Annotations;

namespace ImageUpload.Controllers
{
    public class UploadImageController : ApiController
    {
        [HttpPost]
        [SwaggerResponse(
            HttpStatusCode.OK,
            Description = "Saved successfully",
            Type = typeof(UploadedFileInfo))]
        [SwaggerResponse(
            HttpStatusCode.BadRequest,
            Description = "Could not find file to upload")]
        [SwaggerOperation("UploadImage")]
        
        public async Task<IHttpActionResult> UploadImage(string fileName = "")
        {
            //Use a GUID in case the fileName is not specified
            if (fileName == "")
            {
                fileName = Guid.NewGuid().ToString();
            }
            //Check if submitted content is of MIME Multi Part Content with Form-data in it?
            if (!Request.Content.IsMimeMultipartContent("form-data"))
            {
                return BadRequest("Could not find file to upload");
            }

            //Read the content in a InMemory Muli-Part Form Data format
            var provider = await Request.Content.ReadAsMultipartAsync(new InMemoryMultipartFormDataStreamProvider());

            //Get the first file
            var files = provider.Files;
            var uploadedFile = files[0];

            //Extract the file extention
            var extension = ExtractExtension(uploadedFile);
            //Get the file's content type
            var contentType = uploadedFile.Headers.ContentType.ToString();

            //create the full name of the image with the fileName and extension
            var imageName = string.Concat(fileName, extension);

            //Get the reference to the Blob Storage and upload the file there
            var storageConnectionString = ConfigurationManager.AppSettings["StorageConnectionString"];
            var storageAccount = CloudStorageAccount.Parse(storageConnectionString);
            var blobClient = storageAccount.CreateCloudBlobClient();
            var container = blobClient.GetContainerReference("images");
            container.CreateIfNotExists();

            var blockBlob = container.GetBlockBlobReference(imageName);
            blockBlob.Properties.ContentType = contentType;
            using (var fileStream = await uploadedFile.ReadAsStreamAsync()) //as Stream is IDisposable
            {
                blockBlob.UploadFromStream(fileStream);
            }
            var fileInfo = new UploadedFileInfo
            {
                FileName = fileName,
                FileExtension = extension,
                ContentType = contentType,
                FileURL = blockBlob.Uri.ToString()
            };
            return Ok(fileInfo);

        }

        public static string ExtractExtension(HttpContent file)
        {
            var invalidChars = Path.GetInvalidFileNameChars();
            var fileStreamName = file.Headers.ContentDisposition.FileName;
            var fileName = new string(fileStreamName.Where(x => !invalidChars.Contains(x)).ToArray());
            var extension = Path.GetExtension(fileName);
            return extension;
        }
    }
}

Open the SwaggerConfig.cs file from the App_Start folder.

Search for the following comment in the code

 

// ***** Uncomment the following to enable the swagger UI *****

 

Uncomment the next 4 lines of code after the comment, once done the code should look like as follows.

  
                    })
                    .EnableSwaggerUi(c =>
                    {

Search for the following comment in the code

//c.OperationFilter<AddDefaultResponse>();

Add the following two lines of code immediately after the line

// Adding the AddFileParamTypes OperationFilter for Swagger to understand Multipart Form Data
c.OperationFilter<AddFileParamTypes>();

Test your API locally using the Microsoft Azure Storage Emulator

Open the Web.Config file and find the <appSettings> section.

Add the following line in between the <appSettings> and </appSettings> tag like this .

  <appSettings>
    <add key="StorageConnectionString" value="UseDevelopmentStorage=true" />
  </appSettings>

Start the Microsoft Azure Storage Emulator as we have used the UseDevelopmentStorage=true flag above to test.

Run the application by clicking F5. It should open the browser window with the URL http://localhost:<PortNumber>/. Append the string swagger to the end of the URL to make it http://localhost:<PortNumber>/swagger in the browser Address Bar and press Enter. 

You should see the swagger UI page. Expand by clicking the UploadImage link and click on the “Post” button. Browse for an image file from your machine. [Optionally] provide a fileName and Click on the “Try it out!” button.

 

image

You should see a 200 Response Code with a JSON in the Response Body containing the four properties similar to the screenshot below.

image 

 


Publish the API to Azure

Create a new Storage account of type Blob Storage or use an existing one and add a Container of type Blob by the name images. Construct your StorageConnectionString by replace the MyStorageAccountName and 1234 with your Storage account name and one of the Access keys.

DefaultEndpointsProtocol=https;AccountName=MyStorageAccountName;AccountKey=1234

 

Change the value of the StorageConnectionString in the Web.Config file to the actual StorageConnectionString constructed above.

 

Publish your API to Azure, by right clicking on the project and selecting Publish.

Test by appending swagger to the end of the URL for example https://yourapiname.azurewebsites.net/swagger.

You should the swagger UI page as earlier.  Expand by clicking the UploadImage link and click on the “Post” button. Browse for an image  file from your machine. [Optionally] provide a fileName and Click on the “Try it out!” button.

image

You should see a 200 Response Code with a JSON in the Response Body containing the four properties similar to the screenshot below.
image

Source Code Location

The complete source code for this solution can be found at https://github.com/pratapladhani/ImageUploadAPI.


Download the Swagger file for registration with PowerApps

PowerApps requires the APIs to be registered using the swagger file. Swagger UI allows us to download the swagger file directly from the browser. 

Copy the URL from the Swagger UI (as shown in the image below) and paste it on the address bar of a new browser window. image

Save the file locally in your machine by the name “ImageUploadAPI-swagger.json”.

image


Register the Custom API in PowerApps using the swagger file

Browse to https://web.powerapps.com. Login using your work or school email id. Click on Manage on the left nav and then on Connections.

image

 

Then click on New Connection on the top right side.

image

 

Click on Custom and then on New custom API.

image

Give a Name “ImageUpload” for your custom API. This is how you will call your API from PowerApps. Select the Swagger file from your local machine for Swagger API definition. [Optional] Add any API icon file (32X32).  [Optional] Provide a description for your API.

 

image

 

Click on Next. Select “No Authentication” as the authentication type and click on Create. It is not recommended to leave your API endpoint as “No Authentication”. We shall test the API first and later I shall point to the steps for securing the API and the custom API endpoint with AAD authentication.

image


Create a PowerApp with Camera control

Open PowerApps on your Windows 8/10 machine (If you haven’t installed PowerApps yet, you can download PowerApps from http://aka.ms/powerappswin). Sign-in using your work or school email id.  Then click on New from the left nav and look for Create an app > Blank app > Tablet layout.

image

 

This will open a blank Tablet app with a default screen. Click on Insert > Media > Camera to insert a camera control to the page.

image

 

Move the Camera1 control to the center of the screen.

image

[Optional] If you are using a device with multipe cameras, you may want to have a way to switch between front and back cameras. We shall insert a Toggle Switch an bind it to the Camera control to toggle between front and back cameras.

Click on Insert > Controls > Toggle to insert a Toggle control. Click Insert > Text box to insert a Text box (label) control. Place it next to the camera and change the Text property for the Text box to "Toggle front/back camera".

image

With the Camera1 control selected, change the dropdown on the top left to Camera and type the following formula in the Formula bar.

If(Toggle1.Value, 1,0)

image

 

Add a Text Input Box for entering the File name.  Click Insert > TextText input to add a Text input control to the screen.

image

Move the Text Input below the Camera control. Change the Text property of the Text Input to an empty string "". Change the HintText property of the Text Input to "File name". image

Click on File > Save and the give a name to your app “CameraUploadApp” to save your app to The cloud.

image


Use the custom API to upload the image from the Camera control

Add the Data source for the custom API in the PowerApps app. Click on Content > Data Sources > Add data source button from the right pane.

image

Click on Add connection.

image

Select the ImageUpload API that you created earlier and click on Connect to add it to the app.

image

 

Now we shall use a formula to upload the camera’s image using the api and collect the output file path to a static collection.

Click on the Camera control then click on Action > OnSelect and enter the following formula:

image

Collect(MyPictures, {URL:ImageUploadAPI.UploadImage(Camera1.Photo, {fileName: TextInput1.Text}).FileURL, Name:TextInput1.Text, Time:Now()})

This formula adds passes the current Camera picture and the text from the Text Input box to the API and adds the output FileURL to a new collection with these three columns : URL, Name & Time. It also assigns the same Text Input text to the Name and assigns the current time to the Time column.

Lets now insert a Gallery below to display this new collection.

Click on Insert > Gallery > Select the third item on the first row - Horizontal Image Gallery With Text to add a gallery to the screen.

image

Position it below the Camera Control and resize it to use the full width of the screen.

image

With the Gallery1 selected, change the Items property from ImageGallerySample to MyPictures.

image

Click on the Preview the app  icon on the top right OR press F5 to Preview the app.

image

Enter the name of the image and then click on the Camera to execute the upload. Change the name of the image and click on the Camera again to upload a new image.

You should see the newly uploaded images in the gallery below.  Select the Gallery and use the right nav to modify which columns bind to which controls. Select the URL, Name & Time respectively for the Image & and the two text boxes.

image

image

 

You have successfully uploaded the images to the blob storage. You can verify that with Azure Storage Explorer as well.

image


[Optional but recommended] Secure the API app and the custom API with Azure Active Directory

Please follow this article to configure AAD for the API App.

Once completed follow this tutorial for configuring AAD authentication for the custom API.

In a subsequent blog post, I’ll try to show all the steps for securing both the API App as well as protecting the custom API also with AAD.


Feedback welcome

Hope this is helpful. Looking forward to the great scenarios that you all with build using PowerApps.

Please share your feedback using the comments below.  Also do suggest other topics that you would like us to blog about.