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.