Create an Image Montage and a Corresponding HTML Area Map

As noted elsewhere Linux Journal turned 15 this month. Hopefully, you enjoyed viewing all our old covers. In the pursuit of the best way to display those covers my first prototype was rejected. Take a look at what didn't make the cut and read on to find out how it was created.

If you haven't looked, the idea was to create a single image where all the covers were reduced to thumbnail size and displayed in a grid where each row in the grid would be a year's worth of covers. Further the image itself would serve as an HTML <map>, each cover being an <area>. As you moved the mouse over each thumbnail the full size cover would be displayed elsewhere on the page.

The main thing that I wasn't quite sure how to accomplish at the get go was creating the thumbnails and pasting them together. I certainly wasn't going to do it manually, that's what programs are for.

My first thought was to use ImageMagick and PythonMagick but the rather poor ImageMagick API documentation convinced me otherwise. Thought two was PIL, the Python Imaging Library.

The entire program is attached and is obviously going to require some modifications for use in other contexts but below is the meat of the program:

 85 montage_width  = (THUMBNAIL_WIDTH  * THUMBNAIL_COLUMNS) + YEAR_WIDTH
 86 montage_height = THUMBNAIL_HEIGHT * THUMBNAIL_ROWS
 87 montage_img    = Image.new('RGB', (montage_width, montage_height))
 88 montage_draw   = ImageDraw.Draw(montage_img)
 89 
 90 print 'Thumbnails: %dx%d' % (THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
 91 print 'Cols Rows : %dx%d' % (THUMBNAIL_COLUMNS, THUMBNAIL_ROWS)
 92 print 'Size      : %dx%d' % (montage_width, montage_height)
 93 
 94 img_tag  = '<img src="%s" width="%d" height="%d" alt="%s" border="0" usemap="#%s" />\n'
 95 map_tag  = '<map id="%s" name="%s">\n'
 96 area_tag = '  <area shape="rect" '         \
 97                    'coords="%d,%d,%d,%d" ' \
 98                    'href="%s" '            \
 99                    'onmouseover="javascript:cmap_show_image(\'%s\', \'%s\', %d, \'%s\');"/>\n'
100 
101 # Create html file.
102 html_output = open(MONTAGE_HTML, 'w')
103 
104 # Insert javascript.
105 html_output.write('<script language="JavaScript">\n');
106 html_output.write(open(MONTAGE_JS).read() % (len(covers), image_basename))
107 html_output.write('</script>\n\n');
108 
109 # Add image tag for cover montage.
110 html_output.write('<table border="0"><tr><td>\n')
111 html_output.write(img_tag % (montage_url, montage_width, montage_height,
112                              MONTAGE_IMG_ALT, MONTAGE_MAP_ID))
113 html_output.write(map_tag % (MONTAGE_MAP_ID, MONTAGE_MAP_ID))
114 
115 year   = 1994
116 column = FIRST_ROW_BLANKS
117 xpos   = YEAR_WIDTH + (THUMBNAIL_WIDTH * column)
118 ypos   = 0
119 for ix, cover in enumerate(covers):
120     inum = ix + 1
121 
122     # Load cover image, resize it and paste it into the montage.
123     img   = Image.open(cover)
124     img   = img.resize((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT))
125     xpos2 = xpos + THUMBNAIL_WIDTH
126     ypos2 = ypos + THUMBNAIL_HEIGHT
127     box = (xpos, ypos, xpos2, ypos2)
128     montage_img.paste(img, box)
129 
130     # Add area tag for this image to the map.
131     imonth, iyear = convert_issue_to_month_year(inum)
132     ititle        = '#%d - %s, %s' % (inum, imonth, iyear)
133     ilink         = AREA_HREF % inum
134     html_output.write(area_tag % (xpos, ypos, xpos2, ypos2,
135                                   ilink,
136                                   COVER_IMG_ID, MONTAGE_MAP_ID, inum, ititle))
137 
138     column += 1
139     if ix == 1  or  ix == 2:
140         xpos   += THUMBNAIL_WIDTH
141         column += 1
142 
143     if column == THUMBNAIL_COLUMNS  or  ix == len(covers)-1:
144         # Add the row label to the montage.
145         if YEAR_WIDTH > 0:
146             ystr = '%d' % year
147             ysz  = montage_draw.textsize(ystr)
148             h    = ysz[1]
149             y    = ypos + ((THUMBNAIL_HEIGHT - h + h // 2) // 2)
150             montage_draw.text((4, y), ystr)
151             year += 1
152 
153         xpos    = YEAR_WIDTH
154         ypos   += THUMBNAIL_HEIGHT
155         column  = 0
156     else:
157         xpos += THUMBNAIL_WIDTH
158 
159 montage_img.save(MONTAGE_JPG, 'JPEG')
160 
161 
162 # Finish html.
163 html_output.write('</map>\n')
164 
165 html_output.write('</td>\n')
166 html_output.write('<td valign="center">\n')
167 img_tag = '<img src="%s%03d.jpg" id="%s"  border="0" alt="%s" />\n'
168 html_output.write(img_tag % (image_basename, len(covers) , COVER_IMG_ID, MONTAGE_IMG_ALT))
169 html_output.write('</tr></table>\n')
170 
171 html_output.close()

A few lines in Image.new() is called to create a montage image of the size needed to hold all the thumbnails and an extra column for the year. Then a drawing surface on the image is created.

After that a file is opened for containing the generated HTML. The first thing added to the file is the JavaScript (see below) that provides the onmouseover handling for the <area> tags. The JavaScript file contains a couple of parameters (the number of images and the URL to the images) which are filled in during the copy process. Having the Python program fill in the URL makes for easier testing where the local URL may be different than the live URL.

Next the <table> tag is written and the montage image and the <map> tag are added to the first column. The montage image is shown in one column and the full size cover is shown in the second column. Then the program begins iterating over all the cover images.

Each cover image is read, resized, and pasted into the montage. The coordinates for pasting the image into the montage are the same ones that are used for the corresponding <area> tag so that is then added to the HTML output file. Then comes some funny business related to skipping some spots in the montage (during our first year we had a couple of bi-monthly issues). After that a check is done to see if we are in a new row and if so we write the year into the first column of the montage image using PIL.ImageDraw.text()

When all the covers have been processed the montage image is saved to a file. The <map> is closed and the second column of the table is written with an <img> tag for the full sized cover. Then the <table> is closed and the HTML is done.

The JavaScript for handling the onmouseover event is presented below:

// The percent format fields are filled in by cmap.py
var cmap_n_images        = %d;
var cmap_image_base      = '%s';
var cmap_image_urls      = Array();
var cmap_image_objs      = Array();

function cmap_show_image(imgid, mapid, inum, ititle)
{
    var img = document.getElementById(imgid);
    var map = document.getElementById(mapid);
    var i;
    var j;

    if ( img  &&  map ) {
        if ( cmap_image_urls.length == 0 ) {
            // Create image urls and image objects.
            for ( i = 0; i < cmap_n_images; i++ ) {
                j = i + 1;
                if      ( j < 10  ) zero = '00';
                else if ( j < 100 ) zero = '0';
                else                zero = '';
                cmap_image_urls[i] = cmap_image_base + zero + j + '.jpg';
                cmap_image_objs[i] = new Image();
            }
        }

        i = inum - 1;
        if ( !cmap_image_objs[i].src ) {
            cmap_image_objs[i].src = cmap_image_urls[i];
        }
        img.src   = cmap_image_objs[i].src;
        map.title = ititle
    }
}

Its operation is fairly simple, the cmap_show_image() function is called each time the mouse moves over an <area> tag. The function is called with the id of the full size image tag, the id of the map, the issue number, and the issue title.

On the first pass through the function creates Image objects for all the cover images and generates the URLs for all the images. The Image objects are created without specifying the src attribute, so they are empty images at this point.

The function then checks to see if the cover image for the issue specified as a parameter has its src attribute set, if not it sets it which will cause the image to be loaded. By loading the images only when the mouse moves over an area in the map all the images don't need to be loaded upfront.

At the end of the function, the image object's src attribute is copied to the full size cover image so that the full size cover gets displayed in the second column. The last action is to set the map title to the title of the issue.

Mitch Frazier is an embedded systems programmer at Emerson Electric Co. Mitch has been a contributor to and a friend of Linux Journal since the early 2000s.

Load Disqus comments