Article History

Making a photo gallery with Picasa Web Albums

In March of 2007 I set about updating the old ‘baby photos’ page I had created for photos of my new niece. It was a rather laborious process to update it and hence, hadn't been updated in quite a while. I had recently played around with Google's Picasa Web Albums and wondered if I could somehow use it to store the photos. I did some searching and it turned out I could!

If you'd like to use my method, here's how to do it.

  1. Place a JavaScript file on your web host
  2. Add some entries to your style sheets
  3. Refer to the JavaScript file in the header of your HTML page(s)
  4. Add empty elements (tags) in your HTML to be filled with the gallery thumbnails
  5. Call a JavaScript function to register the targets of the gallery
  6. Refer to the Picasa Web data sources

JavaScript file

Put this somewhere on your web host.

pwa.js:

/* Some code simplified from MochiKit */
function E (tag, attrs/*, children */) {
  var elem = document.createElement(tag);
  if (attrs) {
    for (var k in attrs) {
      var v = attrs[k];
      elem.setAttribute(k, v);
    }
  }
  if (arguments.length > 2) {
    for (var i = 2; i < arguments.length; i++) {
      elem.appendChild(arguments[i]);
    }
  }
  return elem;
}

function T (text) {
  return document.createTextNode(text);
}

function pwTOC(itemclass, tocid) {
  var toc = document.getElementById(tocid);
  if (toc) {
    toc.appendChild(E('h2', null, T('Albums')));
    var toclist = toc.appendChild(E('ul', null));

    var headers = document.getElementsByTagName('h2');
    var last = headers.length;
    var pat = new RegExp('(^|\s)' + itemclass + '(\s|$)');

    for (var i = 0; i < last; i++) {
      if (pat.test(headers[i].className)) {
        var h = headers[i];
        var n = h.nextSibling;
        while (n.id == null) {
          n = n.nextSibling;
        }

        toclist.appendChild(E('li', null,
                              E('a', { 'href':'#' + n.id,
                                    'title': h.firstChild.nodeValue },
                                T(h.firstChild.nodeValue)
                                )
                              )
                            );
      }
    }
  }
}

/* Code adapted from http://phydeaux3.blogspot.com/2006/11/picasa-web-albums-with-json.html */
function _handler(root, section) {
  var feed = root.feed;
  var contents = [];

  var num;
  var i;
  if (feed.subtitle.$t) {
    var link;
    if (feed.link) {
      num = feed.link.length;
      for (i = 0; i < num; i++) {
        if (feed.link[i].rel == 'alternate') {
          link = feed.link[i].href;
        }
      }
    }

    if (link) {
      section.appendChild(E('h3', null,
                            E('a', { 'href': link },
                              T(feed.subtitle.$t)
                              )
                            )
                          );
    } else {
      section.appendChild(E('h3', null,
                            T(feed.subtitle.$t)
                            )
                          );
    }
  }

  num = feed.entry.length;
  for (i = 0; i < num; i++) {
    var entry = feed.entry[i];
    var title = entry.title.$t;
    var summary = entry.summary.$t;
    var link = entry.link[0];

    for (var j = 1; j < entry.link.length; j++) {
      if (entry.link[j].rel == 'alternate')
        link = entry.link[j];
    }

    section.appendChild(E('p', { 'class': 'icon' },
                          E('span', null,
                            E('a', { 'href': link.href, 'title': title },
                              E('img', { 'src': summary.split('src="')[1].split('" alt')[0].replace(/288/,'160'), 'alt': title }))
                            )
                          )
                        );
  }
}

var _global = this;
function _customHandler (name, section) {
  _global[name] = function (root) { _handler(root, section); };
}

function pwTarget (albumclass) {
  var all = document.getElementsByTagName('div');
  var last = all.length;
  var pat = new RegExp('(^|\s)' + albumclass + '(\s|$)');

  for (var i = 0; i < last; i++) {
    if (pat.test(all[i].className)) {
      _customHandler(all[i].id, all[i]);
    }
  }
}

Styles

Put this either in your existing style sheet file, or in the header of your HTML file. I prefer external style sheets because they can be shared amongst multiple pages and hence save bandwidth.

.pwAlbum {
  text-align: center;
}

.pwAlbum p {
  text-align: left;
}

.pwAlbum p.icon {
  display: inline;
}

.pwAlbum span {
  display: table-cell;
  display: inline-table;
  display: inline-block;
  width: 160px;
  vertical-align: bottom;
  margin: 10px;
  padding: 10px;
}

.pwAlbum span a {
  color: #000000;
  text-decoration: none;
}

.pwAlbum img {
  border-color: #bfbfbf;
  border-style: solid outset outset solid;
  border-width: 1px 3px 3px 1px;
}

.toc {
  border: 1px solid black;
  padding-left: 1em;
  padding-bottom: 1em;
  margin: 1em;
  width: 50%;
}

.toc h2 {
  border: none;
}

.toc ul {
  padding-left: 1em;
}

.tocitem {
  clear: both;
}

HTML header

Load the JavaScript file:

  <head>
    ...
    <script type="text/javascript" src="pwa.js"></script>
    ...
  </head>
...

Elements

At the top of the page, place the table of contents (TOC) anchor:

      <div class="toc" id="pwTOC"></div>

Later, place the sections and individual albums:

      ...
      <h2 class="tocitem">April 2007</h2>
      <div class="pwAlbum" id="April2007"></div>
      <div class="pwAlbum" id="Easter2007"></div>
      ...

There can be several of these, one for each album you wish to display. The ‘class’ on the ‘div’ must be the same, and the ‘id’ must be unique. The id will be used again below for the ‘callback’ parameter.

Register the targets

After all of the above div's, add this once:

      <script type="text/javascript">
<!--<![CDATA[
pwTOC('tocitem', 'pwTOC');
pwTarget('pwAlbum');
//]]>-->
      </script>

The call to 'pwTOC' fills the table of contents (identified with id="pwTOC") with items of the named class (‘tocitem’). This is optional but useful.

The call to 'pwTarget' creates named callbacks that are used in the next section. The albums are identified by class="pwAlbum".

Sorry for all the mess, but it's necessary to ‘escape’ everything properly.

Data sources

For each album, add something like this:

      <script type="text/javascript" src="http://picasaweb.google.com/data/feed/base/user/rondatester/albumid/5041817125439808369?kind=photo&alt=json&callback=Jan2007&authkey=-r-kJySF7D0&hl=en_GB"></script>

This is the tricky part. To get the ‘src’ URL, go to the album page on Picasa Web and copy the URL from the ‘RSS’ link in the bottom right corner. Then you have to modify it:

Change the ‘alt’ parameter to ‘json’:

...&alt=rss&...

Becomes:

...&alt=json&...

And add a callback name:

...&callback=Jan2007&...

This is the ‘id’ parameter of the ‘div’ above. Make sure the parameters are separated by ampersands.

Finally, to make the HTML compliant, convert all the ampersands (‘&’) into the proper HTML entity ‘&amp;’.

Technical details

It turns out that Picasa Web can return ‘JSON’-formatted data ready for use in JavaScript. But because this data is on a different server than our gallery, XMLHTTPRequest can't be used (cross-site security restrictions). By adding a ‘callback’ parameter to the request URL, the JavaScript is turned into a function call. I had found a few sites that used this callback to call a single function. But I wanted to display multiple albums on the one page, in separate areas. So I had to come up with a way to target each album.

The method I came up with was to create a global function for each album, which is done in pwTarget(class). It scans the whole page looking for elements with a certain class. It takes the id of each one and creates a global callback function with that name for the Picasa Web code to call.

If you wish to change the markup produced by the callback, it shouldn't be too difficult. I created an E(tag, attrs, children) function, modelled on MochiKit's createDOM() function. There's also a T(text) function for creating text nodes.