A “citable” deep-zooming image viewer

A couple weeks ago, when I was pulling together all the images for Textplot, a little text network-analysis project I’ve been working on recently, I ran into a familiar problem – I wanted a way to “cite” really specific parts of the images. This seemed especially important in this case because the images were enormous (about 20,000 pixels in height and width) and really dense – big clouds of tiny little words. And, in the blog post, I wanted to spend a bunch of time talking about really low-level, nitty-gritty details in the networks, often the locations of individual words – a kind of visual close reading. You can give “directions” for where to find things (“bottom left,” “right in the middle,” etc.), but that’s annoying for the reader, and most people won’t bother with it. What you really want is a way to quote the images in the way that you can quote a piece of writing – some way to pull out little snippets and build them into the narrative in a reasonably fluid, low-friction way. Sort of like the hard-linking feature we added to Neatline in version 2.3, which makes it possible to reference a specific record in the URL for an exhibit.

So, I decided to try to whip up something similar for static images, this time using OpenSeadragon, the venerable Javascript library for deep-zooming images. Along the way, I needed to solve a related problem – I had lots of images, so I wanted to find a way to write one little chunk of code that could handle all of them at once, ideally without having to monkey around with anything that I couldn’t just throw onto a s3 bucket along with the tile pyramids. After trying a couple of approaches, I ended up with a little project called osd-dzi-viewer, which is a barebones but workable solution – it’s a tiny static site generator that spins up a Backbone router that listens for routes like:

your.s3.bucket/#image-group/image

Where image-group is a directory on s3 (or GH pages, or whatever) that sits next to the generated index.html file, and image is a directory inside image-group that contains a filed called image.dzi and the corresponding tile pyramid under image_files. So, you might have a directory structure like this:

├── index.html
├── script.js
├── style.css
├── image-group
│   ├── image1
│   │   ├── image1.dzi
│   │   └── image1_files/
│   ├── image2
│   │   ├── image2.dzi
│   │   └── image2_files/
│   └── image3
│       ├── image3.dzi
│       └── image3_files/

Then, as the user pans and zooms around the image, the route gets updated with the current focus and zoom position, which makes it possible to link back to any given location. Eg, stuff like:

your.s3.bucket/#image-group/image/0.5615/0.4206/8.4324

Or, in the wild – here’s “napoleon” in War and Peace. The Javascript will also gracefully swap out the image on the fly if the image-group or image parts of the URL are changed. This makes it possible to link back to the same tab a bunch of times using target="_new" attributes on the anchors, even if you’re referencing more than one image – like this. This can be nice because it makes it easy for the reader to toggle back and forth without getting swamped with dozens of new tabs.

hard-link

Anyway, this is really simple code, and would be easy enough to adapt for projects that need more structure on the actual image pages (branding, navigation, metadata, etc). To build the viewer, just clone the repo, install npm and bower, and then run grunt compile:min, which writes the generated files to a _site directory. Or, just grab a pre-built archive from the releases on GitHub.

  • Shawn Graham

    so…. deep zoom image format eh? imagemagick?

    • dclure

      Hey Shawn,

      You’ll need to install three things to get this working, if you don’t already have them:

      1. Node.js:

      > brew install node

      2. Vips (image processing tools):

      > brew tap homebrew/science

      > brew install vips

      3. Grunt (task runner):

      > npm install -g grunt-cli

      Then, slice the image with vips. Say the image is called “war-and-peace.png”:

      > vips dzsave war-and-peace.png war-and-peace

      This will create a directory called war-and-peace with the war-and-peace.dzi file, and the tiles in a subdirectory called war-and-peace_files. Next, clone the osd-dzi-viewer repo, change down into it, and run:

      > npm install
      > grunt

      Which will pull down the dependencies and start the development environment. Once Grunt is running, you should see a new directory called _site, which contains the compiled html/js/css for the viewer. This is basically a mini static site, just like the output of something like Jekyll, that can be served via s3 / apache / nginx / etc.

      Last, move the tile pyramid into place. First, make a “group” directory inside the _site directory, which is basically just a namespace for a collection of tile pyramids (one particular blog post, a project, etc). Eg:

      > mkdir _site/test-project

      And then symlink (or move) the tile pyramid into that directory:

      > ln -s /path/to/war-and-peace _site/test-project

      Now, open a tab and hit http://localhost:8000/#test-project/war-and-peace, and you should see the zoomable image. Let me know if this works!

      • Shawn Graham

        Thanks!

        …bah. All sorts of crazy errors happening; am going to shut this down and try again in the morning when my brain is fresh. I appreciate all the help!

      • Shawn Graham

        ok, got as far as grunt, but getting:

        MACPA406-SG:osd-dzi-viewer-0.1.0 shawngraham$ grunt
        grunt-cli: The grunt command line interface. (v0.1.13)

        Fatal error: Unable to find local grunt.

        If you’re seeing this message, either a Gruntfile wasn’t found or grunt
        hasn’t been installed locally to your project. For more information about
        installing and configuring grunt, please see the Getting Started guide:
        http://gruntjs.com/getting-started

        …but I see a gruntfile.js in that directory.

        As always, I appreciate your help!

        • dclure

          Eh, looks like I didn’t bake in the top-level Grunt dependency. I just pushed a fix – pull the changes, reinstall the dependencies, and see if it turns over:

          > git pull origin master
          > npm install
          > grunt

          • Shawn Graham

            getting closer. Got grunt running (though had to grunt –force), dropped the pyramid into the _site directory, but no joy at http://localhost:8000/#testproject/oc-diaries just blank.

          • dclure

            What was the error that made it necessary to use –force?

          • Shawn Graham

            MACPA406-SG:osd-dzi-viewer shawngraham$ grunt

            Running “jade:dist” (jade) task
            File _site/index.html created.
            Running “browserify:dist” (browserify) task
            >> Error: ENOENT, open ‘/Users/shawngraham/githubstuff/osd-dzi-viewer/bower_components/openseadragon/openseadragon.js’

            Warning: Error running grunt-browserify. Use –force to continue.

            Aborted due to warnings.

            Total 2.2s

          • dclure

            Ah, do:

            > npm install -g bower
            > bower install

            And then try `grunt` again. Sorry, it’s weirdly easy to forgot how to set up your own projects from scratch.

          • Shawn Graham

            oh I understand! and now we’re closer still. In the browser, I get this error: unable to open [object Object]:http 404 attempting to load tile source. So I must’ve screwed something up back at the VIPS stage.

          • dclure

            Is the directory structure right? Should look like:

            https://gist.github.com/davidmcclure/c6b657127addcbe91601

          • Shawn Graham

            this is what I’ve got, looks same to me?

          • Shawn Graham

            ok, this looks like your gist

          • dclure

            No, the first one was right – oc-diaries.dzi should be next to oc-diaries_files, not inside it. In the browser, when you get the error – do you see any Javascript errors on the console?

          • Shawn Graham

            well geeze. I moved it back, fired up grunt again, and lo! where first there was desolation, a blank waste, now there is a network! So I guess I can just move the static _site folder to eg gh-pages and we’re off to the races? Thank you for all your patience!

          • dclure

            Yep, that should work. s3 is also great for this kind of stuff.

          • Shawn Graham

            ok, that’s what I tried, back to blankness http://shawngraham.github.io/Open-Context-Site-Diaries/

          • dclure

            Oh, you need to nest the `oc-diaries` directory under a group directory. Eg, create a top-level dir called `test-project` (or whatever), and put `oc-diaries` under that. Then, the URL will be:

            http://shawngraham.github.io/Open-Context-Site-Diaries/#test-project/oc-diaries

          • Shawn Graham

            ok, so http://shawngraham.github.io/Open-Context-Site-Diaries/#textplot/oc-diaries giving me an error message (chrome) in safari crashed the thing. Ah fun, eh? Maybe s3 is the way to go after all.

          • dclure

            Ah – move index.html, script.js, and style.css up into the root directory. So, the `textplot` directory is parallel with those files.

            (Sorry, the “group” directory concept is a little weird – the idea is to make it possible to namespace sets of images. Eg, for different blog posts.)

          • Shawn Graham
          • dclure

            w00t

          • Shawn Graham

            Ah, that makes sense. So I could do eg a series of texplots, and only have to upload the tile stack, as it were.