Overview

Skill Level: Beginner

This recipe shows how to create an OpenWhisk action that converts images to the JPEG format and how that can be easily access by making it a Web Action; we will code the action in Node.js and use R on the client-side to interact with OpenWhisk.

Ingredients

  • OpenWhisk account (via IBM Bluemix: https://www.ibm.com/cloud-computing/bluemix/openwhisk )
  • OpenWhisk CLI tools (https://console.ng.bluemix.net/openwhisk/cli)
  • GNU R (https://www.r-project.org/)
  • (Optional) Editor for editing Javascript and R code; GNU Emacs (https://www.gnu.org/software/emacs/) is used in the examples, popular choices include Eclipse (https://eclipse.org/), RStudio (https://www.rstudio.com/) and Atom (https://atom.io/) - or vi(m). For the JavaScript part the built-in editor available in OpenWhisk will be used
    The examples assume a GNU/Linux system but can work in other operating systems. The examples given will work by themselves though so this is optional.
  • (Optional) The "base64" command; if not available a pre-converted file is used for CLI testing.
  • (Optional) git to clone the code (https://git-scm.com/)
  • (Optional) an image file - a sample one is included.

Step-by-step

  1. Objectives

    This recipe shows how to create an OpenWhisk action that converts image files to JPEG, and then how that action can be made available as a Web Action and used in an R application.

    The choice of technology is tied with specific requirements I had, specifically the use of R code in the client side, but it is generalisable: the action will be used by using HTTP POST requests and as such only the details around passing images around to and from OpenWhisk are dependent on the specific language and framework being used.

    The action is very simple and that is by design; the goal of converting to JPEG was actually born out of a real use case since while working with Watson Visual Recognition I needed to provide images in JPEG or PNG but was working with a BMP dataset.

  2. Getting ready

    If you don’t have a Bluemix account and access to the OpenWhisk dashboard (https://console.ng.bluemix.net/openwhisk/dashboard) you will need to take care of that first (outside the scope of this recipe but trivial)

    The OpenWhisk CLI tools can be donwloaded following the instructions on the OpenWhisk documentation.

  3. Creating the action

    We will be developing the action using the OpenWhisk editor; in the OpenWhisk dashboard select the “Develop in your browser” option to get to the editor.

    getting_started

     

    In the editor press “Create an Action”; I called it “jpeginator” (!) but feel free to use something else, the REST endpoint will be obtained latter. Confirm the creation and look at the editor.

    create_action

  4. Adding the code

    The action comes with a default boilerplate; the code is trivial and is built around the gm package for node. Since the conversion is asynchronous we will use Promises.

    // The JPEGINATOR
    // OpenWhisk action to convert images to JPEG
    //
    // Author: Frederico Munoz <frederico.munoz@pt.ibm.com>
    // Date: APR 2017
    // License: Eclipse Public License 1.0

    var fs = require('fs');
    var gm = require('gm').subClass({imageMagick: true});

    exports.main = function (params) {

    // We must receive the image as base64 and identified by the image
    // keyword
    var b64string = params.image;
    var buf = Buffer.from(b64string, 'base64')

    // Using promises since gm is async
    return new Promise(function(resolve, reject) {
    gm(buf).toBuffer('JPEG', function (err, buffer) {
    if (err)
    {
    console.log("Error");
    return {result: "error"};
    } else {
    console.log("Success");
    resolve({image: buffer.toString('base64')});
    }
    });
    });
    }

    Copy the source code (also available in this gist) and replace all the text in the editor with it; select “Make it live” in the right bottom corner to activate the action.

    Note that there is no error checking in the code; this is by design given the goal of this recipe.

  5. Testing the action

    After making the code live an option “View REST Endpoint” becomes visible; click it to see how we can interact with the action. For use with the wsk tool the action name suffices (it also works with the Fully Qualified Name, of course).

    To test it we can use this image from the Rua Augusta Arch in Lisbon; I have converted it into BMP and resized it to avoid problems with parameter length limitations that can occur when testing using the command line. To test in the command line we will use the base64 command but I added a base64 version of the image in this gist .

    lisboa_arco

    Save the image and/or the base64 version in a directory, and from the terminal and in that directory test the action:

    $ wsk action invoke --blocking --result jpeginator --param image "$(base64 -w 0 lisboa_arco.bmp)"

    or to use the already encoded version

    $ wsk action invoke --blocking --result jpeginator --param image "$(cat lisboa_arco.b64)"

    The result will be similar to the following (I’m using head -c 80 to limit the output since the returned value is an extremely long string):

     

    $ wsk action invoke --blocking --result jpeginator --param image "$(base64 -w 0 lisboa_arco.bmp)"|head -c 80
     {
    "image": "/9j/4AAQSkZJRgABAQIAdgB2AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgG
     

    The fundamental part of our action is done and with some additional commands we can confirm that it works as expected: using awk to extract the base64 string and decoding it we get a JPEG image:

    $ wsk action invoke --blocking --result jpeginator --param image "$(base64 -w 0 lisboa_arco.bmp)" | awk '/image/ {gsub("\"","");print $2}' | base64 -d > result.jpg
    $ file result.jpg
    result.jpg: JPEG image data, JFIF standard 1.01

    OpenWhisk has a real-time monitor that can be used to view the result: select “Monitor” in the top menu and in the left the last activity is displayed.

    activity_log

  6. Making it a Web Action

    As it is right now we have already a good solution: the action works when invoked and we can reap the benefits of OpenWhisk (auto-scaling, for one) without any additional change. We can interact with it by passing the necessary authentication information – the REST Endpoint information area mentioned before includes an example using curl

    auth

    By using a Web Action we can expose the service in a slightly different way, and one of the differences is the ability to bypass authentication (in order to eliminate it or to build a different authentication mechanism), this on top of some additional features that streamline the use of the action by HTTP requests.

    To enable it go to “Manage” in the top menu, in the action details choose “Manage Action”, go to the  “Additional Details” pane and  select the Enable as Web Action option.

    make_wa

     

    The web Action URL is accessable by our applications and provides a way to expose a service without necessarily tying the usage to the owner of the action.

  7. Modify the code

    To make the action usable as a Web Action some slight changes are made; the complete details can be read on the official documentation on the topic but they will essentially alter the way the action builds the reply.

    // The JPEGINATOR
    // OpenWhisk action to convert images to JPEG
    //
    // Author: Frederico Munoz <frederico.munoz@pt.ibm.com>
    // Data: APR 2017
    // License: Eclipse Public License 1.0

    var fs = require('fs');
    var gm = require('gm').subClass({imageMagick: true});

    function main (params) {

    var b64string = params.image;
    var buf = Buffer.from(b64string, 'base64')

    return new Promise(function(resolve, reject) {
    gm(buf).toBuffer('JPEG', function (err, buffer) {
    if (err)
    {
    console.log("Error");
    return {result: "error"};
    } else {
    console.log("Success");
    resolve({headers: { 'Content-Type': 'image/jpeg' },
    statusCode: 200,
    body: buffer.toString('base64')});
    }
    });
    });
    }

    This is again available at this revision in the same gist.

    The main difference in this code is the return value where we added some Web Action specific values. This would make it simpler to include in, say, a web application since the return value would be of the appropriate content.

    One thing to note is that since we chose to keep the input as JSON the same code we had before works: params.image contains the base64 string since we are keeping the same overall approach and the parser does The Right Thing. Consider though that when making an OpenWhisk action a Web Action there could be changes to how interaction is planned.

    As an example, consider this: we will see that to send data to our action we will use the following POST request in R:

    POST(url, body = list(image = b64), encode = "json")


    The “encode” option makes httr (the R package we will use) send the data as JSON, which is what we are already expecting on our action, but if we wanted to “upload” a file using a multipart POST (which is what is generally used when submiting an image through a regular HTML form) we wouls use something like this

    POST(url, body = list(image=upload_file("lisboa_arco.png")), encode = "multipart")

     

    • Now, with this approach (which is similar to using “curl -F “image=@lisbon_arco.png” …”) we would not be able to keep our code intact in terms of parsing the data since we wouldn’t have the image string in params.image: reading the documentation and experimenting a bit would show us that Web Actions can receive the following as parameters automatically:
    • __ow_method (type: string). the HTTP method of the request.
      __ow_headers (type: map string to string): A the request headers.
      __ow_path (type: string): the unmatched path of the request (matching stops after consuming the action extension).
      __ow_user (type: string): the namespace identifying the OpenWhisk authenticated subject
      __ow_body (type: string): the request body entity, as a base64 encoded string when content is binary, or plain string otherwise
      __ow_query (type: string): the query parameters from the request as an unparsed string
      If we were to use a multipart POST request the base64 image would be part of the __ow_body variable; more than that, every image upload would be part of the __ow_body variable, properly identified via MIME content types and separators, so we would need to parse it.

    This is outside the scope of this recipe but the general idea is rather simple: different ways to send data into a Web Action will have different ways to reference it inside our action code in OpenWhisk

  8. Consume the action in R

    We can now use the OpenWhisk Web Action from our code.The following R code encondes the image and uses the Web Action to convert it, saving the result (also available in this gist):

    ### THE JPEGINATOR
    ### Openwhisk bmp->jpeg conversion using OpenWhisk
    ### Author: Frederico Munoz <frederico.munoz@pt.ibm.com>
    ### Date: APR 2017
    ### License: Eclipse Public License v1.0

    ## Load the image-related libraries
    library(bmp)
    library(pixmap)
    library(base64enc)
    library(httr)
    library(jsonlite)

    ## Read and encode
    read_filename <- file("lisboa_arco.png", "rb")
    b64 <- base64encode(read_filename)

    ## Set the REST endpoint - change for your own
    url <- "https://openwhisk.ng.bluemix.net/api/v1/web/frederico.munoz%40pt.ibm.com_Default/default/jpeginator.json"

    ## The POST request itself, passing the image as a JSON argument
    req <- POST(url, body = list(image = b64), encode = "json")

    ## Parsing the request
    j <- fromJSON(content(req, "text"))

    ## Let's see what we have here
    str(j)

    ## Write file (which is now a JPEG, courtesy of the JPEGINATOR!!!1!
    ## ...
    writeBin(base64decode(j$body), "lisboa2.jpg")

     

    Running the code will produce a JPEG file; the following is the output of one inspection command:

     

     > str(j)
    List of 3
    $ headers :List of 1
    ..$ Content-Type: chr "image/jpeg"
    $ statusCode: int 200
    $ body : chr "/9j/4AAQSkZJRgABAQIAdgB2AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQM"| __truncated__

     

    I have made the R code available in an IBM Data Science Experience notebook , create an account and copy the project to experiment with the code online (the resulting Jupyter notebook is also available on github)

  9. Making use of Content Extensions

    The code above is very similar to the wsk invokation would be like, especially because we are receiving a JSON object. Since we have a Web Action though we can make us of content extensions; from the docs:


    Content extensions: the request must specify its desired content type as one of .json, .html, .http, .svg or .text. This is done by adding an extension to the action name in the URI, so that an action /guest/demo/hello is referenced as /guest/demo/hello.http for example to receive an HTTP response back. For convenience, the .http extension is assumed when no extension is detected.

    Notice that the modified code has a Content-Type header which sets it as “image/jpeg”; by using Content Extensions we can ask for the HTTP version of the reply which will return the raw JPEG image instead of a dictionary.

    ## New REST endpoint with an http extension
    url <- "https://openwhisk.ng.bluemix.net/api/v1/web/frederico.munoz%40pt.ibm.com_Default/default/jpeginator.http"

    ## The POST request itself, passing the image as a JSON argument
    req <- POST(url, body = list(image = b64), encode = "json"

    The JPEG image is already in the raw format and we can use it directly

    >req
    Response [https://openwhisk.ng.bluemix.net/api/v1/web/frederico.munoz%40pt.ibm.com_Default/default/jpeginator.http] Date: 2017-04-21 15:29
    Status: 200
    Content-Type: image/jpeg
    Size: 11.9 kB
    <BINARY BODY>

    This can be seen (and replayed) in the Data Science Experience notebook already mentioned.

  10. Conclusion

    This is a small example of the possibilities that OpenWhisk offers, and Web Actions in particular. The R code can be reused since OpenWhisk has very clear integration points, and with Web Actions and Content Extensions allows for several different approaches to be used.

Join The Discussion