My Opera is closing 1st of March

Covert Tomato

Spending CPU cycles associating random words

Creating dynamic images

, ,

Now that you have played a bit with Yusef, what's still missing is the possibility to create dynamic images for your very own 2000 web counter or captcha. Or is it missing? Recent web browsers support for the so called Canvas. The power of element, which appeared with HTML5, resides in dynamic images generation.

In this post, we will first take a look at Yusef's graphics plugin, which harnesses canvas to create images thumbnails. Then we will move on to create a custom web counter.

An Overview of Graphics

Graphics' main functionality is to make thumbnails out of larger pictures. In addition to this, thumbnails are cached and served in response to HTTP requests. Please note that in the following we use the more recent Graphics version bundled with Photo Sharing, as kindly suggested by p01.

Graphics API is offers two functions: serveResizedImage serves a thumbnail on a given connection and resizeImage creates thumbnails and caches them. The actual prototypes are:

serveResizedImage( connection, image, maxWidth, maxHeight, crop, quality, originalImageFile )
  • connection: The connection over which the thumbnail is to be served.
  • image: A File (from the FileIO library) pointing to the image to be resized. If originalImageFile is also specified, this File instead refers to a previously generated thumbnail.
  • maxWidth: Thumbnail width.
  • maxHeight: Thumbnail height.
  • crop: Whether the thumbnail should occupy the whole space (true) or if it should be sized down to fit in the rectangle (false).
  • quality: The quality from 0.0 to 1.0 for jpeg generation.
  • originalImageFile (optional): The full-sized picture File (from FileIO) if the image argument already refers to a thumbnail.


resizeImage( image, maxWidth, maxHeight, crop, quality, callback, originalImageFile )
  • callback: A function called when the thumbnail is generated. The function is called with a data argument, describing the generated thumbnail. This argument is hash contains the following values: {file: The thumbnail file, canvas: The canvas used to generated the thumbnail.}
  • Other arguments have the same meaning as those in serveResizedImage.

I will not describe the serveResizedImage further. Not only this function is way out of the scope of this post, but also the code speaks for itself.

Drawing on the canvas

The resizeImage function proceeds as follows to draw the thumbnail: first, it loads the image asynchronously; second, the thumbnail is rendered on a canvas at the appropriate size; third, the image is saved to a file; fourth, the callback is called.
To load the image asynchronously, an new Image object is created. All that is required is to set its onload handler to some function before setting the image source. Unfortunately, the execution does not stop and will end up with Yusef cleanup code disposing the connection. For this kind of asynchronous scenarios, Yusef can be told that the connection is still pending:
connection.pending = true; /* Yusef won't kill the connection */

Once the image is loaded, the image's onload handler is called. This handler retrieves the actual img element from the event.srcElement argument, and compute various height and width for the drawing. The image element is passed to the canvas for rendering:
var context     = canvas.getContext( '2d' );
context.drawImage( srcImage,0,0, srcImage.width, srcImage.height, (canvas.width-tw)/2,(canvas.height-th)/2,tw,th );

A bit of caching
The resizeImage function caches its results for faster access. For each set of parameters (i.e. file name, thumbnail size and quality) unique filename identifies the thumbnail.
When resizeImage is called, the function first checks if the full sized image is in cache and whether it is still bears the same modification time stamp as when the thumbnail was generated. If the original image satisfies these conditions, the previous thumbnail is returned. Otherwise, a new thumbnail is generated.
The role of the _updateCache in this is to bookmark the original file, along with its modification date, in the cache. After registering that new entry, _updateCache also removes older cache entries1

Your very own counter
In this section, we will make a visit counter as an image. We are aiming for readability, rather than optimization, or graceful error handling. We will therefore leave out problems, such as caching results or testing existence of image files. These should be considered in real world applications.
We start of the Hello Yusef application2. The first step is to create ten 12x16 pixels images, one per digit (zero included), and put them in under a new "resources" folder at the application root. We then make an index page, serving some dumb HTML, for our testing:
var counter = 0;

/**
* Generate html content to visualize the counter
*/
Yusef.addSectionListener('_index', function(connection)
{
    counter++;

    return '<html><head><title>Counter</title></head><body>' + 
           '[html:img src="' + 
           opera.io.webserver.currentServicePath + 'counter/?c=' + counter +
           '" alt="' + counter + '"]</body></html>';
}, {ui: false});


Now, we are ready to draw the counter image. For this, we write a function which takes the counter value and paints it over a given canvas:
var digitImages = [];

/* pre-load every image of a digit */
(function() {
    var digitfile;
    var digitsDirectory = Yusef.mountpoints.application.resolve('resources');

    for (var i = 0; i < 10; ++i) {
        digitImages[i]      = new Image();
        digitfile           = digitsDirectory.resolve(i + '.png');
        digitImages[i].src  = unescape(digitfile.path);
    }

    /* Images are loaded asynchronously. 
       Since the images are stored locally, we expect that loading will be done
       by the time the first request occurs. */
})();

/**
* Paint a counter of specified value onto the canvas.
*
* @param {Number} value The counter value
* @param {HTMLCanvasElement} canvas The canvas over which the graphical
* counter is to be shown. 
*/
function paintCounter(value, canvas) {
    var DIGIT_WIDTH     = 12,
        DIGIT_HEIGHT    = 16,
        digits          = [],
        canvas, canvasctx, 
        digitfile, digitimg;

    /* Get each digit in the value */
    while (value != 0)
    {
        digits.splice(0, 0, (value % 10));
        value = Math.floor(value / 10);
    }

    /* Set the canvas to an appropriate size */
    canvas.width    = DIGIT_WIDTH * digits.length;
    canvas.height   = DIGIT_HEIGHT;

    /* All painting operations are made on a "context"; first clear the
     * drawing area */
    canvasctx       = canvas.getContext('2d');
    canvasctx.fillStyle   = '#fff';
    canvasctx.fillRect( 0,0, canvas.width, canvas.height );

    /* For each digit, paint the corresponding image onto the canvas */
    for (var i = 0; i < digits.length; ++i) {
        canvasctx.drawImage(digitImages[digits[i]], i * DIGIT_WIDTH, 0);
    }
}

At this point, if you are unfamiliar with the canvas element, you may want to take a look at other canvas articles, to get an idea of the kind of pictures that can be generated using more involved code. All that's left is to create the canvas element, and make unite serve the image it contains:
/**
* Handle requests for the counter image
*/
Yusef.addSectionListener('counter', function(connection)
{
    var value = connection.request.getItem('c'), 
        canvas;

    /* Ensure that the counter value is present */
    if (!value || value.length === 0 || (1*value[0] === NaN)) {
        connection.response.closeAndRedispatch();
    }

    value = value[0] * 1;

    /* Create a canvas and paint a counter on it */
    canvas = document.createElement('canvas');
    paintCounter(value, canvas);

    /* We use the writeImage function to give the image back to the browser.
     * This function accepts a canvas element and serializes it as PNG */
    connection.response.setResponseHeader( "Content-type", "image/png" );
    connection.response.writeImage(canvas);

    /* Try to help the garbage collection */
    canvas = null;
});


That's all for our counter! The full code is available in attachment.

counter.js

  1. This really means entries that are in RAM for the file, not the actual thumbnails files on the hard drive for some reason.
  2. Generally speaking, the libraries used in Hello Yusef are some of the latest available. Those over dev.opera.com are too often outdated.

Happy New YearDebugging Unite apps: take 2

Write a comment

New comments have been disabled for this post.