TBR reader/web(refactor): adapts old file UI to work with device-sets

Several components were refactored and refined, at a high level the UI state now
treats device-sets as a top level component (over files). The initial screens
shows a list of device-sets each of which owns a file, some meta information and
a collection of devices. Several modifications were made to the markup and style
to have a cleaner (more material based) look and feel.

Other changes in this CL include:

Behavior for "Add a file": When clicking the "+" button in the UI and a file has been selected by the user:

1. A device-set is created, with an empty file and devices collection.
2. A file with the added PDF blob is added to the new device-set.
3. The "current" device is appended to the device-set's collection of devices.
4. The new device-set shows up in the list of items on the main screen.

Application state: The entire application state in main.js is continuously
serialized and saved to a local cache based on window.localStorage. Since PDF
blobs are large and not appropriate for this type of cacheing they are saved to
IndexedDB (SEE: https://goo.gl/sKGvCB) using a small wrapper in
browser/dom/blob-store.js. The state is then rehydrated on domready from any
previously saved state. This helps ensure correct initialization of state and
child components. Additionally this makes it possible to view the pervious state
when reloading the page.

Router: The router was refactored so that a hash of routes are passed into state
construction and can be checked against on app render. The router state atom
holds the current route, params from named routes like "/posts/:id", and
attaches a popstate listener to the window. The shared anchor helper was removed
since normal click events will trigger the popstate listener and call the
router's internal routing channel.

* The files component has been deleted.
* PDF rendering has been moved to the device-set component.
* Device-set list item rendering has been moved to the device-set component.
* The file component's Blob hashes are now MD5 hex digests.

SEE: IndexedDB - https://goo.gl/sKGvCB

Next Steps:

* Device-set management UI - https://github.com/vanadium/reader/issues/38
* Hook Vanadium/Syncbase back up - https://github.com/vanadium/reader/issues/39

Closes #7

Change-Id: Ifbdd409b44d96d796865aa8c179ea85a858f3c84
diff --git a/web/browser/components/base/index.css b/web/browser/components/base/index.css
index 9669cf3..468b2c8 100644
--- a/web/browser/components/base/index.css
+++ b/web/browser/components/base/index.css
@@ -10,7 +10,7 @@
   inherits: .reset;
   inherits: .type-base;
   inherits: .type-body;
-  background-color: var(--blue-grey-900);
+  background-color: var(--blue-grey-25);
 }
 
 .reader-app {
@@ -21,10 +21,12 @@
 
 .reader-app main {
   flex: 1;
+  display: flex;
+  flex-direction: column;
 }
 
 a {
   color: var(--cyan-700);
   font-weight: 500;
   text-decoration: none;
-}
\ No newline at end of file
+}
diff --git a/web/browser/components/device-set/index.js b/web/browser/components/device-set/index.js
new file mode 100644
index 0000000..829048e
--- /dev/null
+++ b/web/browser/components/device-set/index.js
@@ -0,0 +1,139 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var cuid = require('cuid');
+var debug = require('debug')('reader:device-set');
+var device = require('../device');
+var extend = require('xtend');
+var file = require('../file');
+var hg = require('mercury');
+var read = require('../../dom/read-blob');
+
+module.exports = {
+  render: require('./render'),
+  state: state,
+  channels: {
+    load: load
+  },
+};
+
+function state(options, key) {
+  options = extend({
+    id: key || cuid(),
+    devices: {},
+    pages: {}
+  }, options);
+
+  debug('init: %o', options);
+
+  var atom = hg.state({
+    id: hg.value(options.id),
+    error: hg.value(null),
+    file: file.state(options.file),
+    pdf: hg.value(null),
+    pages: hg.varhash({
+      total: options.pages.total || 1,
+      current: options.pages.current || 1,
+    }),
+    progress: hg.value(0),
+    devices: hg.varhash(options.devices, device.state),
+    channels: {
+      load: load,
+      previous: previous,
+      next: next,
+      manage: manage
+    }
+  });
+
+  return atom;
+}
+
+// SEE: https://jsfiddle.net/6wxnd9uu/6/
+function load(state, data) {
+  var blob = state.file.blob();
+
+  if (!blob) {
+    return;
+  }
+
+  state.progress.set(0);
+
+  debug('loading Blob into PDFJS: %o', blob);
+
+  var source = { length: blob.size };
+  var transport = new PDFJS.PDFDataRangeTransport();
+
+  transport.count = 0;
+  transport.file = blob;
+  transport.length = blob.size;
+  transport.requestDataRange = requestDataRange;
+
+  function requestDataRange(begin, end) {
+    var chunk = blob.slice(begin, end);
+
+    read(chunk, function onread(err, result) {
+      transport.count += end - begin;
+      transport.onDataRange(begin, new Uint8Array(result));
+    });
+  }
+
+  PDFJS
+  .getDocument(source, transport, password, progress)
+  .then(success, error);
+
+  function password() {
+    var err = new Error('Password required');
+    state.error.set(err);
+  }
+
+  function progress(update) {
+    var float = (update.loaded/update.total) * 100;
+    var value = Math.floor(float);
+
+    // For some reason the update.loaded value above can be higher than the
+    // update.total value, in that case we can assume the progress is 100%.
+    if (value > 100) {
+      value = 100;
+    }
+
+    state.progress.set(value);
+  }
+
+  function success(pdf) {
+    pdf.toJSON = _PDFDocumentProxyToJSON;
+    state.pdf.set(pdf);
+    state.pages.put('current', state.pages.get('current'));
+    state.pages.put('total', pdf.numPages);
+  }
+
+  function error(err) {
+    state.error.set(err);
+  }
+}
+
+function previous(state, data) {
+  // Only advance if it's not the first page.
+  var current = state.pages.get('current');
+  if (current > 1) {
+    state.pages.put('current', current - 1);
+  }
+}
+
+function next(state, data) {
+  // Only advance if it's not the last page.
+  var current = state.pages.get('current');
+  var total = state.pages.get('total');
+  if (current < total) {
+    state.pages.put('current', current + 1);
+  }
+}
+
+function manage(state, data) {
+  debug('manage device set: %s', state.id());
+}
+
+// Prevent circular references when serializing state.
+function _PDFDocumentProxyToJSON() {
+  return {};
+}
diff --git a/web/browser/components/device-set/list-item.css b/web/browser/components/device-set/list-item.css
new file mode 100644
index 0000000..8b86a9a
--- /dev/null
+++ b/web/browser/components/device-set/list-item.css
@@ -0,0 +1,93 @@
+/* Copyright 2015 The Vanadium Authors. All rights reserved. */
+/* Use of this source code is governed by a BSD-style */
+/* license that can be found in the LICENSE file. */
+
+@import "../base/reset.css";
+@import "../base/typography.css";
+@import "../base/variables.css";
+
+.device-set {
+  inherits: .reset-box;
+  display: flex;
+  flex-direction: column;
+  background-color: var(--white);
+  box-shadow: var(--drop-shadow);
+  border-radius: 2px;
+  max-width: 512px;
+  overflow: hidden;
+  position: relative;
+  margin: var(--gutter) auto;
+}
+
+.device-set .header {
+  padding: calc(var(--gutter));
+}
+
+.device-set .header .title {
+  inherits: .type-title;
+}
+
+.device-set .header .subhead {
+  inherits: .type-smallhead;
+  color: var(--blue-grey-500);
+}
+
+.device-set .support {
+  background-color: var(--blue-grey-25);
+  padding: calc(var(--gutter));
+  border-top: 1px solid var(--blue-grey-100);
+}
+
+.device-set .support p {
+  margin: var(--gutter) 0;
+}
+
+.device-set .support .devices > .title {
+  font-weight: bold;
+}
+
+.device-set .support ul,
+.device-set .support li {
+  inherits: .reset-box;
+}
+
+.device-set .support ul {
+  margin-top: var(--gutter);
+}
+
+.device-set .support ul ul {
+  margin-top: 0;
+  padding-left: var(--gutter);
+}
+
+.device-set .actions {
+  display: flex;
+  justify-content: flex-end;
+  border-top: 1px solid var(--blue-grey-100);
+  padding: calc(var(--gutter) * (1/3));
+}
+
+.device-set .actions a {
+  inherits: .reset;
+  inherits: .type-smallhead;
+  border-radius: 2px;
+  text-transform: uppercase;
+  background-color: var(--white);
+  padding: calc(var(--gutter) / 2) calc(var(--gutter) * (2/3));
+  margin-left: calc(var(--gutter) * (1/3));
+  transition: box-shadow 0.2s cubic-bezier(0.4, 0, 1, 1),
+    background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+    color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.device-set .actions a:hover {
+  background-color: var(--blue-grey-50);
+}
+
+.device-set .actions a.read {
+  color: var(--cyan-800);
+}
+
+.device-set .actions a.delete {
+  color: var(--blue-grey-400);
+}
diff --git a/web/browser/components/device-set/pdf-viewer.css b/web/browser/components/device-set/pdf-viewer.css
new file mode 100644
index 0000000..662cb45
--- /dev/null
+++ b/web/browser/components/device-set/pdf-viewer.css
@@ -0,0 +1,80 @@
+/* Copyright 2015 The Vanadium Authors. All rights reserved. */
+/* Use of this source code is governed by a BSD-style */
+/* license that can be found in the LICENSE file. */
+
+@import "../base/variables.css";
+@import "../base/typography.css";
+
+.pdf-viewer {
+  flex: 1;
+}
+
+.pdf-viewer .progress {
+  display: block;
+  margin: 0;
+  height: 4px;
+  max-width: 100%;
+  position: relative;
+  background-color: var(--cyan-600);
+}
+
+.pdf-viewer .progress.hidden {
+  display: none;
+}
+
+.pdf-viewer .progress-bar {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: 0%;
+  z-index: 1;
+  transition: width 0.2s  cubic-bezier(0.4, 0, 0.2, 1);
+  background-color: var(--cyan-900);
+}
+
+.pdf-controls {
+  position: fixed;
+  top: 0;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  background-color: var(--white);
+  box-shadow: var(--drop-shadow);
+  z-index: 900;
+  display: flex;
+}
+
+.pdf-controls .pdf-controls.hidden {
+  display: none;
+}
+
+.pdf-controls .back {
+  margin-left: var(--gutter);
+}
+
+.pdf-controls .title {
+  inherits: .type-smallhead;
+  flex: 1;
+  padding: var(--gutter);
+  text-align: left;
+  color: inherit;
+}
+
+.pdf-controls .pager {
+  display: flex;
+}
+
+.pdf-controls .pager .meta {
+  margin-right: var(--gutter-half);
+}
+
+.pdf-controls .menu {
+  margin-right: var(--gutter);
+  margin-left: var(--gutter);
+}
+
+.pdf-canvas {
+  /* HACK: Header's title line-height + 2x the gutters. */
+  margin-top: calc(20px + (var(--gutter) * 2));
+}
diff --git a/web/browser/components/device-set/pdf-widget.js b/web/browser/components/device-set/pdf-widget.js
new file mode 100644
index 0000000..9045feb
--- /dev/null
+++ b/web/browser/components/device-set/pdf-widget.js
@@ -0,0 +1,88 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var document = require('global/document');
+var raf = require('raf');
+
+module.exports = PDFWidget;
+
+// TODO(jasoncampebll): add verification for pdf object
+function PDFWidget(state) {
+  if (!(this instanceof PDFWidget)) {
+    return new PDFWidget(state);
+  }
+
+  this.state = state;
+}
+
+PDFWidget.prototype.type = 'Widget';
+
+PDFWidget.prototype.init = function init() {
+  var widget = this;
+  var element = document.createElement('canvas');
+  element.setAttribute('class','pdf-canvas');
+  widget.update(null, element);
+  return element;
+};
+
+PDFWidget.prototype.update = function update(previous, element) {
+  var widget = this;
+  var state = widget.state;
+  var pdf = state.pdf;
+
+  if (!pdf || state.progress < 100) {
+    return;
+  }
+
+  var device;
+  var keys = Object.keys(state.devices);
+  var length = keys.length;
+  for (var i = 0; i < length; i++) {
+    var key = keys[i];
+    var value = state.devices[key];
+    if (value.current) {
+      device = value;
+      break;
+    }
+  }
+
+  // Set width to current device width.
+  element.width = device ? device.screen.width : element.width;
+  render(element, state);
+};
+
+var rendering = false;
+function render(element, state) {
+  if (rendering) {
+    raf(render.bind(null, element, state));
+    return;
+  }
+
+  rendering = true;
+  state.pdf.getPage(state.pages.current).then(success, error);
+
+  function success(page) {
+    var scale = element.width/page.getViewport(1.0).width;
+    var viewport = page.getViewport(scale);
+    var context = element.getContext('2d');
+
+    element.height = viewport.height;
+    element.width = viewport.width;
+
+    page.render({
+      canvasContext: context,
+      viewport: viewport
+    }).promise.then(done, done);
+  }
+
+  function error(err) {
+    process.nextTick(function() {
+      throw err;
+    });
+  }
+}
+
+function done() {
+  rendering = false;
+}
\ No newline at end of file
diff --git a/web/browser/components/device-set/render-list-item.js b/web/browser/components/device-set/render-list-item.js
new file mode 100644
index 0000000..1a39fb9
--- /dev/null
+++ b/web/browser/components/device-set/render-list-item.js
@@ -0,0 +1,49 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var click = require('../../events/click');
+var css = require('./list-item.css');
+var debug = require('debug')('reader:device-sets');
+var format = require('format');
+var h = require('mercury').h;
+var hg = require('mercury');
+var insert = require('insert-css');
+var map = require('../../util').map;
+var properties = require('../properties');
+
+module.exports = render;
+
+function render(state, channels) {
+  debug('render list-item: %o', state);
+  insert(css);
+
+  return h('.device-set', [
+    h('.header', [
+      h('.title', state.file.title),
+      h('.subhead', format('file-hash: %s', state.file.hash))
+    ]),
+    h('.support', [
+      h('.devices', [
+        h('.title', 'Devices'),
+        map(state.devices, properties.render)
+      ])
+    ]),
+    h('.actions', [
+      h('a.delete', {
+        href: '#',
+        'ev-click': click(channels.remove, { id: state.id })
+      }, 'Delete'),
+      h('a.read', {
+        href: '/#!/' + state.id,
+        // NOTE: The channels argument above are passed in by and belong to a
+        // parent component. This enables actions to be triggered from this view
+        // which can control interactions with the parent list. In the case of
+        // clicking the "Read" button here the load channel
+        // (state.channels.load) owned by the device-set/list item component
+        // needs to be called.
+        'ev-click': hg.send(state.channels.load)
+      }, 'Read')
+    ])
+  ]);
+}
diff --git a/web/browser/components/device-set/render.js b/web/browser/components/device-set/render.js
new file mode 100644
index 0000000..296ae3b
--- /dev/null
+++ b/web/browser/components/device-set/render.js
@@ -0,0 +1,78 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var click = require('../../events/click');
+var css = require('./pdf-viewer.css');
+var debug = require('debug')('reader:device-set');
+var format = require('format');
+var h = require('mercury').h;
+var hg = require('mercury');
+var insert = require('insert-css');
+var PDFWidget = require('./pdf-widget');
+
+module.exports = render;
+
+function render(state, channels) {
+  insert(css);
+
+  return h('.pdf-viewer', [
+    hg.partial(progress, state.progress),
+    hg.partial(controls, state, channels),
+    h('.pdf-widget', new PDFWidget(state))
+  ]);
+}
+
+function progress(state) {
+  debug('progress: %s', state);
+
+  if (state >= 100) {
+    return h('.progress.hidden');
+  }
+
+  return h('.progress', [
+    h('.progress-bar', {
+      style: { width: state + '%' }
+    })
+  ]);
+}
+
+function controls(state, channels) {
+  if (state.progress < 100) {
+    return h('.pdf-controls.hidden');
+  }
+
+  return h('.pdf-controls', [
+    h('a.back', {
+      href: '/#!/'
+    }, [
+      h('i.material-icons', 'arrow_back')
+    ]),
+    h('.title', state.file.title),
+    h('.pager', [
+      h('.meta', format('Page: %s of %s',
+        state.pages.current,
+        state.pages.total)),
+      h('a.previous', {
+        href: '#',
+        'ev-click': click(channels.previous)
+      }, [
+        h('i.material-icons', 'chevron_left'),
+      ]),
+      h('a.next', {
+        href: '#',
+        'ev-click': click(channels.next)
+      }, [
+        h('i.material-icons', 'chevron_right'),
+      ])
+    ]),
+    h('a.menu', {
+      href: '#',
+      'ev-click': click(channels.manage)
+    },
+    [
+      h('i.material-icons', 'more_vert'),
+    ])
+  ]);
+}
+
diff --git a/web/browser/components/device-sets/device-sets.css b/web/browser/components/device-sets/device-sets.css
new file mode 100644
index 0000000..5e87f6c
--- /dev/null
+++ b/web/browser/components/device-sets/device-sets.css
@@ -0,0 +1,68 @@
+/* Copyright 2015 The Vanadium Authors. All rights reserved. */
+/* Use of this source code is governed by a BSD-style */
+/* license that can be found in the LICENSE file. */
+
+@import "../base/variables.css";
+
+.device-sets {
+  /* HACK: Header's title line-height + 2x the gutters. */
+  margin-top: calc(20px + (var(--gutter) * 2));
+  padding-left: var(--gutter);
+  padding-right: var(--gutter);
+  border-top: 1px solid transparent;
+  flex: 1;
+}
+
+.device-sets .blank-slate {
+  padding: var(--gutter) 0;
+}
+
+.device-sets footer {
+  display: flex;
+  position: fixed;
+  right: 0;
+  bottom: 0;
+}
+
+.device-sets footer .hidden {
+  display: none;
+}
+
+.device-sets footer .spacer {
+  flex: 1;
+}
+
+.device-sets footer .fab {
+  position: relative;
+  margin-right: var(--gutter);
+  margin-bottom: var(--gutter);
+  border-radius: 50%;
+  width: calc(var(--gutter) * 2);
+  height: calc(var(--gutter) * 2);
+  box-shadow: var(--drop-shadow);
+  background-color: var(--deeporange-A200);
+  padding: 0;
+  overflow: hidden;
+  cursor: pointer;
+  transition: box-shadow 0.2s cubic-bezier(0.4, 0, 1, 1),
+    background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+    color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.device-sets footer .fab:hover {
+
+}
+
+.device-sets footer .fab:active {
+  box-shadow: var(--drop-shadow-intense);
+  background-color: color(var(--deeporange-A200) shade(10%));
+}
+
+.device-sets footer .fab .material-icons {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  line-height: 1;
+  transform: translate(-12px, -12px);
+  color: var(--white);
+}
diff --git a/web/browser/components/device-sets/index.js b/web/browser/components/device-sets/index.js
new file mode 100644
index 0000000..5867b40
--- /dev/null
+++ b/web/browser/components/device-sets/index.js
@@ -0,0 +1,70 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var debug = require('debug')('reader:device-sets');
+var device = require('../device');
+var deviceSet = require('../device-set');
+var format = require('format');
+var hg = require('mercury');
+var window = require('global/window');
+
+module.exports = {
+  render: require('./render'),
+  state: state
+};
+
+function state(options) {
+  options = options || {};
+
+  debug('init: %o', options);
+
+  var atom = hg.state({
+    error: hg.value(null),
+    current: hg.value(null),
+    collection: hg.varhash(options.collection || {}, deviceSet.state),
+    channels: {
+      add: add,
+      remove: remove
+    }
+  });
+
+  return atom;
+}
+
+function add(state, data) {
+  if (! data.blob) {
+    return;
+  }
+
+  var blob = data.blob;
+
+  debug('adding new device set for file: %o', blob);
+
+  if (blob.type !== 'application/pdf') {
+    var message = format('The file "%s" is not a PDF.', blob.name);
+    var err = new Error(message);
+    return state.error.set(err);
+  }
+
+  var ds = deviceSet.state({
+    file: { blob: data.blob }
+  });
+
+  var d = device.state({
+    type: 'Browser',
+    current: true,
+    arch: window.navigator.platform,
+    screen: {
+      width: window.innerWidth,
+      height: window.innerHeight,
+    }
+  });
+
+  ds.devices.put(d.id(), d);
+  state.collection.put(ds.id(), ds);
+}
+
+function remove(state, data) {
+  state.collection.delete(data.id);
+}
diff --git a/web/browser/components/device-sets/render.js b/web/browser/components/device-sets/render.js
new file mode 100644
index 0000000..4e4280f
--- /dev/null
+++ b/web/browser/components/device-sets/render.js
@@ -0,0 +1,57 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var css = require('./device-sets.css');
+var debug = require('debug')('reader:device-sets');
+var file = require('../../events/file');
+var h = require('mercury').h;
+var hg = require('mercury');
+var hg = require('mercury');
+var insert = require('insert-css');
+var map = require('../../util').map;
+var renderListItem = require('../device-set/render-list-item');
+var show = require('../device-set/render');
+
+module.exports = render;
+
+function render(state, channels) {
+  insert(css);
+
+  if (state.current) {
+    debug('=== SHOW %s ===', state.current);
+    var current = state.collection[state.current];
+    return show(current, current.channels);
+  } else {
+    debug('=== LIST ===');
+    return list(state, channels);
+  }
+}
+
+function list(state, channels) {
+  var children = map(state.collection, renderListItem, channels);
+  if (children.length === 0) {
+    children = [ hg.partial(blank) ];
+  }
+
+  var footer = h('footer', [
+    h('.spacer'),
+    h('label.fab', [
+      h('i.material-icons', 'add'),
+      h('input.hidden', {
+        type: 'file',
+        'ev-event': file(channels.add)
+      })
+    ])
+  ]);
+
+  children.push(footer);
+
+  return h('.device-sets', children);
+}
+
+function blank() {
+  return h('.blank-slate', 'Add a new PDF file below to get started.');
+}
+
+
diff --git a/web/browser/components/device/index.js b/web/browser/components/device/index.js
new file mode 100644
index 0000000..efd7de8
--- /dev/null
+++ b/web/browser/components/device/index.js
@@ -0,0 +1,77 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var cuid = require('cuid');
+var debug = require('debug')('reader:device');
+var event = require('synthetic-dom-events');
+var extend = require('xtend');
+var hg = require('mercury');
+var raf = require('raf');
+var window = require('global/window');
+
+module.exports = {
+  state: state
+};
+
+function state(options, key) {
+  options = extend({
+    id: key || cuid(),
+    screen: {}
+  }, options);
+  debug('init: %o', options);
+
+  var atom = hg.state({
+    id: hg.value(options.id),
+    current: hg.value(options.current || false),
+    type: hg.value(options.type),
+    alias: hg.value(options.alias),
+    arch: hg.value(options.arch),
+    screen: hg.struct({
+      width: hg.value(options.screen.width),
+      height: hg.value(options.screen.height)
+    })
+  });
+
+  if (atom.current()) {
+    // Fire the resize event just in case the size has changed since a previous
+    // value was stashed.
+    window.addEventListener('resize', resize(atom));
+    window.dispatchEvent(event('resize'));
+  }
+
+  return atom;
+}
+
+// HACK(jasoncampbell): I couldn't get this event plumbed into to
+// state.channels.resize handler. This is a quick way to get an
+// optimized resize listener around window resize events.
+// SEE: https://developer.mozilla.org/en-US/docs/Web/Events/resize
+//
+// TODO(jasoncampbell): Make it so only the last resize event trigers the state
+// update.
+function resize(state) {
+  var running = false;
+
+  return function listener(event) {
+    if (! running) {
+      running = true;
+      raf(update);
+    }
+  };
+
+  function update() {
+    var width = window.innerWidth;
+    var height = window.innerHeight;
+
+    if (state.screen.width() !== width) {
+      state.screen.width.set(width);
+    }
+
+    if (state.screen.height() !== height) {
+      state.screen.height.set(height);
+    }
+
+    running = false;
+  }
+}
diff --git a/web/browser/components/error/index.js b/web/browser/components/error/index.js
new file mode 100644
index 0000000..bb7df17
--- /dev/null
+++ b/web/browser/components/error/index.js
@@ -0,0 +1,20 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var hg = require('mercury');
+
+module.exports = {
+  state: state,
+  render: require('./render')
+};
+
+function state(options) {
+  options = options || {};
+
+  var atom = hg.state({
+
+  });
+
+  return atom;
+}
diff --git a/web/browser/components/file/hash-blob.js b/web/browser/components/file/hash-blob.js
new file mode 100644
index 0000000..e6b965b
--- /dev/null
+++ b/web/browser/components/file/hash-blob.js
@@ -0,0 +1,37 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var assert = require('assert');
+var BlobReader = require('readable-blob-stream');
+var crypto = require('crypto');
+var once = require('once');
+var pump = require('pump');
+var through = require('through2');
+var window = require('global/window');
+
+module.exports = hash;
+
+function hash(blob, callback) {
+  callback = once(callback);
+  assert.ok(blob instanceof window.Blob, 'Must use a Blob object.');
+
+  var md5 = crypto.createHash('md5');
+  var writer = through(update);
+  var reader = new BlobReader(blob);
+
+  pump(reader, writer, finish);
+
+  function update(buffer, enc, callback) {
+    md5.update(buffer, enc);
+    callback();
+  }
+
+  function finish(err) {
+    if (err) {
+      return callback(err);
+    }
+
+    callback(null, md5.digest('hex'));
+  }
+}
diff --git a/web/browser/components/file/index.js b/web/browser/components/file/index.js
new file mode 100644
index 0000000..28cb027
--- /dev/null
+++ b/web/browser/components/file/index.js
@@ -0,0 +1,91 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var cuid = require('cuid');
+var db = require('../../dom/blob-store');
+var debug = require('debug')('reader:file');
+var extend = require('xtend');
+var hash = require('./hash-blob');
+var hg = require('mercury');
+var window = require('global/window');
+
+module.exports = {
+  state: state
+};
+
+function state(options, key) {
+  options = extend({
+    id: key || cuid()
+  }, options);
+
+  debug('init: %o', options);
+
+  // If the blob was created in this application instance it will be a File
+  // object and have a name attribute. If it was created by a peer it will
+  // manifest locally as a Blob object (Files can't be directly constructed).
+  //
+  // SEE: https://developer.mozilla.org/en-US/docs/Web/API/File
+  var blob = options.blob || {};
+
+  var atom = hg.state({
+    id: hg.value(options.id),
+    ref: hg.value(options.ref || ''),
+    title: hg.value(options.title || blob.name || ''),
+    size: hg.value(options.size || blob.size),
+    type: hg.value(options.type || blob.type),
+    blob: hg.value(blob || null),
+    hash: hg.value(options.hash || ''),
+    error: hg.value(null),
+  });
+
+  // If this file's blob is set, hash it's contents and save it in the local db
+  // for later. This makes reloading the page easier as the blob data will be
+  // available for quick retreival later.
+  if (blob instanceof window.Blob) {
+    save(atom, { blob: atom.blob() });
+  } else if (atom.hash()) {
+    load(atom, { hash: atom.hash() });
+  }
+
+  return atom;
+}
+
+function save(state, data) {
+  if (! data.blob) {
+    return;
+  }
+
+  hash(data.blob, onhash);
+
+  function onhash(err, digest) {
+    if (err) {
+      return done(err);
+    }
+
+    state.hash.set(digest);
+    db.put(digest, data.blob, done);
+  }
+
+  function done(err) {
+    if (err) {
+      return state.error.set(err);
+    }
+  }
+}
+
+function load(state, data) {
+  if (! data.hash) {
+    return;
+  }
+
+  db.get(data.hash, onget);
+
+  function onget(err, blob) {
+    if (err) {
+      return state.error.set(err);
+    }
+
+    state.blob.set(blob);
+  }
+}
diff --git a/web/browser/components/files/index.css b/web/browser/components/files/index.css
deleted file mode 100644
index aac36d8..0000000
--- a/web/browser/components/files/index.css
+++ /dev/null
@@ -1,18 +0,0 @@
-/* Copyright 2015 The Vanadium Authors. All rights reserved. */
-/* Use of this source code is governed by a BSD-style */
-/* license that can be found in the LICENSE file. */
-
-@import "../base/variables.css";
-
-.files .blank-slate {
-  margin: var(--gutter);
-  padding: var(--gutter);
-}
-
-.file {
-  margin: 0px var(--gutter);
-  padding: var(--gutter);
-  background-color: var(--white);
-  border-bottom: 1px dashed var(--grey-100);
-  box-shadow: var(--drop-shadow);
-}
diff --git a/web/browser/components/files/index.js b/web/browser/components/files/index.js
deleted file mode 100644
index bbd273d..0000000
--- a/web/browser/components/files/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-
-module.exports = {
-  state: require('./state'),
-  render: require('./render')
-};
diff --git a/web/browser/components/files/render.js b/web/browser/components/files/render.js
deleted file mode 100644
index 9b0f350..0000000
--- a/web/browser/components/files/render.js
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var anchor = require('../router/anchor');
-var click = require('../../events/click');
-var css = require('./index.css');
-var format = require('format');
-var h = require('mercury').h;
-var hg = require('mercury');
-var insert = require('insert-css');
-var toArray = require('../../util').toArray;
-
-module.exports = render;
-
-function render(state, channels) {
-  insert(css);
-
-  var children = toArray(state.collection).map(file, channels);
-
-  if (children.length === 0) {
-    children = [ hg.partial(blank) ];
-  }
-
-  return h('.files', children);
-}
-
-// Called as an array iterator with the this argument set to the component's
-// state.channels attribute.
-// SEE: https://goo.gl/tu7srT
-function file(state, index, collection) {
-  var channels = this;
-  var ref = (state.ref || 'pending save');
-
-  return h('.file', [
-    h('h2.type-title', [
-      anchor({
-        href: format('/%s', state.id)
-      }, state.title)
-    ]),
-    h('p.type-caption', [
-      h('span', format('%s - %s - %s -', state.type, state.id, ref)),
-      h('a.delete', {
-        href: '#',
-        'ev-click': click(channels.remove, { id: state.id })
-      }, 'DELETE')
-    ])
-  ]);
-}
-
-function blank() {
-  return h('.blank-slate', 'Add a new PDF file below to get started.');
-}
diff --git a/web/browser/components/files/state.js b/web/browser/components/files/state.js
deleted file mode 100644
index b33a110..0000000
--- a/web/browser/components/files/state.js
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var debug = require('debug')('reader:files');
-var assert = require('assert');
-var cuid = require('cuid');
-
-module.exports = function create(options) {
-  options = options || {};
-
-  var state = hg.state({
-    error: hg.value(null),
-    store: hg.value(null),
-    collection: hg.varhash({}, createFile),
-    channels: {
-      add: add,
-      remove: remove
-    }
-  });
-
-  return state;
-};
-
-function add(state, data) {
-  if (!data.file) {
-    return;
-  }
-
-  debug('adding file: %o', data.file);
-  // TODO(jasoncampbell): Add validation for blob.type === "application/pdf"
-  var key = cuid();
-
-  state.collection.put(key, {
-    blob: data.file
-  });
-}
-
-function remove(state, data) {
-  assert.ok(data.id, 'data.id required');
-  state.collection.delete(data.id);
-}
-
-function createFile(options, key) {
-  key = key || cuid();
-
-  // If the blob was created in this application instance it will be a File
-  // object and have a name attribute. If it was created by a peer it will
-  // manifest locally as a Blob object (Files can't be directly constructed).
-  //
-  // SEE: https://developer.mozilla.org/en-US/docs/Web/API/File
-  var blob = options.blob;
-
-  return hg.struct({
-    id: hg.value(key),
-    ref: hg.value(options.ref || ''),
-    title: hg.value(options.title || blob.name || ''),
-    size: hg.value(options.size || blob.size),
-    type: hg.value(options.type || blob.type),
-    blob: hg.value(blob || null)
-  });
-}
diff --git a/web/browser/components/footer/index.css b/web/browser/components/footer/index.css
deleted file mode 100644
index e75b838..0000000
--- a/web/browser/components/footer/index.css
+++ /dev/null
@@ -1,35 +0,0 @@
-/* Copyright 2015 The Vanadium Authors. All rights reserved. */
-/* Use of this source code is governed by a BSD-style */
-/* license that can be found in the LICENSE file. */
-
-@import "../base/variables.css";
-
-footer {
-  position: relative;
-}
-
-footer .add-file {
-  display: block;
-  position: absolute;
-  right: var(--gutter);
-  bottom: var(--gutter);
-  background-color: grey;
-  width: calc(var(--gutter) * 2);
-  height: calc(var(--gutter) * 2);
-  border-radius: 50%;
-  box-shadow: var(--drop-shadow);
-  text-indent: -10000px;
-  cursor: pointer;
-}
-
-footer .add-file.active {
-  background-color: var(--deeporange-A200);
-}
-
-footer .add-file:hover {
-  background-color: var(--cyan-900);
-}
-
-footer .add-file:active {
-  background-color: var(--cyan-800);
-}
diff --git a/web/browser/components/footer/index.js b/web/browser/components/footer/index.js
deleted file mode 100644
index 0e4980c..0000000
--- a/web/browser/components/footer/index.js
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var h = require('mercury').h;
-var file = require('../../events/file');
-var insert = require('insert-css');
-var css = require('./index.css');
-
-module.exports = {
-  render: render
-};
-
-function render(state, add) {
-  insert(css);
-
-  if (state.hash) {
-    return h('div.hidden');
-  }
-
-  return h('footer', [
-    h('label.add-file',
-    {
-      className: state.store ? 'active' : ''
-    },
-    [
-      h('input.hidden', {
-        type: 'file',
-        'ev-event': file(add)
-      })
-    ])
-  ]);
-}
diff --git a/web/browser/components/header/index.css b/web/browser/components/header/index.css
index a9ca5af..9a1ca7f 100644
--- a/web/browser/components/header/index.css
+++ b/web/browser/components/header/index.css
@@ -6,11 +6,19 @@
 @import "../base/typography.css";
 
 header {
+  position: fixed;
+  top: 0;
   width: 100%;
   display: flex;
   align-items: center;
   background-color: var(--cyan-800);
-  box-shadow: var(--drop-shadow-intense);
+  box-shadow: var(--drop-shadow);
+  color: var(--white);
+  z-index: 900;
+}
+
+header.hidden {
+  display: none;
 }
 
 header a.menu,
@@ -24,7 +32,7 @@
 header .title {
   inherits: .type-smallhead;
   flex: 1;
-  text-align: center;
-  /* TODO(jasoncampbell): use a real material design based high-contrast theme */
-  color: var(--white);
+  padding: var(--gutter);
+  text-align: left;
+  color: inherit;
 }
diff --git a/web/browser/components/header/index.js b/web/browser/components/header/index.js
index 19369e2..967ad27 100644
--- a/web/browser/components/header/index.js
+++ b/web/browser/components/header/index.js
@@ -2,10 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+var css = require('./index.css');
 var h = require('mercury').h;
 var insert = require('insert-css');
-var css = require('./index.css');
-var anchor = require('../router/anchor');
 
 module.exports = {
   render: render
@@ -14,18 +13,9 @@
 function render(state, channels) {
   insert(css);
 
-  if (state.hash) {
-    return h('.hidden');
-  }
-
   return h('header', [
-    anchor({
-      href: '/',
-      className: 'menu'
-    }, 'Menu'),
-    h('.title', 'PDF Reader'),
-    h('a.more', {
-      href: '#'
-    }, '...')
+    h('a.title', {
+      href: '/#!/'
+    }, 'PDF Reader')
   ]);
 }
diff --git a/web/browser/components/pdf/index.css b/web/browser/components/pdf/index.css
deleted file mode 100644
index 4d24594..0000000
--- a/web/browser/components/pdf/index.css
+++ /dev/null
@@ -1,67 +0,0 @@
-/* Copyright 2015 The Vanadium Authors. All rights reserved. */
-/* Use of this source code is governed by a BSD-style */
-/* license that can be found in the LICENSE file. */
-
-@import "../base/variables.css";
-@import "../base/typography.css";
-
-.controls {
-  right: 0;
-  text-align: center;
-  padding: var(--gutter-half);
-  display: flex;
-  align-items: center;
-  background-color: var(--cyan-800);
-  box-shadow: var(--drop-shadow);
-  position: relative;
-  z-index: 9999;
-}
-
-.center-controls {
-  display: inline-block;
-  margin: 0px auto 0px auto;
-}
-
-.left-controls {
-  display: inline-block;
-  float: left;
-}
-
-.right-controls {
-  display: inline-block;
-  float: right;
-}
-
-.clear-controls {
-  clear: both;
-}
-
-.pdf {
-  position: absolute;
-  z-index: 1;
-  top: var(--gutter);
-  left: 0;
-  right: 0;
-  bottom: 0;
-  overflow: auto;
-}
-
-.pdf-canvas {
-  display: block;
-  margin: 0px auto 0px auto;
-  box-shadow: var(--drop-shadow);
-}
-
-span.pagecount {
-  padding-left: 1em;
-  padding-right: 1em;
-  color: var(--white);
-}
-
-button.link {
-  margin-left: 1em;
-}
-
-span.controls-open {
-  color: var(--white);
-}
\ No newline at end of file
diff --git a/web/browser/components/pdf/index.js b/web/browser/components/pdf/index.js
deleted file mode 100644
index 80408b6..0000000
--- a/web/browser/components/pdf/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-module.exports = {
-  state: require('./state'),
-  render: require('./render')
-};
diff --git a/web/browser/components/pdf/render.js b/web/browser/components/pdf/render.js
deleted file mode 100644
index 19e1eda..0000000
--- a/web/browser/components/pdf/render.js
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var PDFWidget = require('../../widgets/pdf-widget');
-var format = require('format');
-var insert = require('insert-css');
-var anchor = require('../../router/anchor');
-var css = require('./index.css');
-
-module.exports = render;
-
-function render(state, channels) {
-  insert(css);
-
-  return h('.pdfviewer', [
-    hg.partial(controls, state, channels),
-    h('.pdf', new PDFWidget(state))
-  ]);
-}
-
-function controls(state, channels) {
-  var current = state.pages.current;
-  var total = state.pages.total;
-  var linked = state.pages.linked;
-  var message = format('Page %s of %s', current, total);
-
-  return h('.controls', [
-    h('.left-controls', [
-      anchor({
-        href: '/'
-      }, [
-        h('span.controls-open', 'Open')
-      ])
-    ]),
-    h('.center-controls', [
-      h('button.prev', {
-        disabled: (current === 1),
-        'ev-click': hg.send(channels.previous)
-      }, 'Prev'),
-      h('span.pagecount', message),
-      h('button.next', {
-        disabled: (current === total),
-        'ev-click': hg.send(channels.next)
-      }, 'Next'),
-      h('button.link', {
-        'ev-click': hg.send(channels.link)
-      }, linked ? 'Unlink' : 'Link')
-    ]),
-    h('.right-controls', []),
-    h('.clear-controls', [])
-  ]);
-}
diff --git a/web/browser/components/pdf/state.js b/web/browser/components/pdf/state.js
deleted file mode 100644
index 117971c..0000000
--- a/web/browser/components/pdf/state.js
+++ /dev/null
@@ -1,124 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var debug = require('debug')('reader:pdf');
-var window = require('global/window');
-
-module.exports = create;
-
-function create(options) {
-  var state = hg.state({
-    error: hg.value(null),
-    pdf: hg.value(null),
-    pages: hg.varhash({
-      total: 1,
-      current: 1,
-    }),
-    progress: hg.value(0),
-    scale: hg.value(1.5),
-    linked: hg.value(false),
-    file: hg.struct(options.file || {}),
-    channels: {
-      previous: previous,
-      next: next,
-      link: link
-    }
-  });
-
-  state.file(function update(file) {
-    debug('file update');
-    load(state, file.blob);
-  });
-
-  state.error(function(err) {
-    if (!err) {
-      return;
-    }
-
-    console.error('TODO: add an error component');
-    console.error(err.stack);
-  });
-
-  // Initialize the async PDFJS file loading.
-  load(state, options.file && options.file.blob);
-
-  return state;
-}
-
-function next(state, data) {
-  // Only advance if it's not the last page.
-  var current = state.pages.get('current');
-  var total = state.pages.get('total');
-  if (current < total) {
-    state.pages.put('current', current + 1);
-  }
-}
-
-function previous(state, data) {
-  // Only advance if it's not the first page.
-  var current = state.pages.get('current');
-  if (current > 1) {
-    state.pages.put('current', current - 1);
-  }
-}
-
-function link(state, data) {
-  state.pages.put('linked', !state.pages.get('linked'));
-}
-
-function load(state, file) {
-  if (!file) {
-    return;
-  }
-
-  debug('loading file into PDFJS: %o', file);
-
-  var transport = new PDFJS.PDFDataRangeTransport();
-  var source = {};
-
-  // SEE: https://jsfiddle.net/6wxnd9uu/6/
-  transport.count = 0;
-  transport.file = file;
-  transport.length = file.size;
-  source.length = file.size;
-  transport.requestDataRange = requestDataRange;
-
-  function requestDataRange(begin, end) {
-    var blob = this.file.slice(begin, end);
-    var fileReader = new window.FileReader();
-
-    fileReader.onload = function onload() {
-      transport.count += end - begin;
-      transport.onDataRange(begin, new Uint8Array(this.result));
-    };
-
-    fileReader.readAsArrayBuffer(blob);
-  }
-
-  PDFJS
-  .getDocument(source, transport, password, progress)
-  .then(success, error);
-
-  function password() {
-    var err = new Error('Password required');
-    state.error.set(err);
-  }
-
-  // TODO: Add a progress loader to the UI.
-  function progress(update) {
-  }
-
-  function success(pdf) {
-    debug('PDF loaded: %o', pdf);
-    state.pdf.set(pdf);
-    state.pages.put('current', 1);
-    state.pages.put('total', pdf.numPages);
-  }
-
-  function error(err) {
-    debug('error file loading into PDFJS: %o', err);
-    state.error.set(err);
-  }
-}
diff --git a/web/browser/components/properties.js b/web/browser/components/properties.js
new file mode 100644
index 0000000..c60702e
--- /dev/null
+++ b/web/browser/components/properties.js
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var format = require('format');
+var h = require('mercury').h;
+
+module.exports = {
+  render: render
+};
+
+// Helper method for rendering a list of properties on a state object. The
+// render happens recursively if a property's value is an object.
+function render(state) {
+  var keys = Object.keys(state);
+  var childern = [];
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var value = state[key];
+    var node;
+
+    if (typeof value === 'object' && value !== null) {
+      node = h('li.nested', [
+        h('.title', key + ':'),
+        render(value)
+      ]);
+    } else {
+      node = h('li.property', format('%s: %s', key, value));
+    }
+
+    childern.push(node);
+  }
+
+  return h('ul.properties', childern);
+}
diff --git a/web/browser/components/router/anchor.js b/web/browser/components/router/anchor.js
deleted file mode 100644
index 44a9e62..0000000
--- a/web/browser/components/router/anchor.js
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var href = require('./index').href;
-var url = require('url');
-var document = require('global/document');
-// var hashbang = require('./hashbang');
-
-// Create a handle for dom-delegation. This step treats the generated `handle`
-// the same as a mercury channel.
-var handle = hg.Delegator.allocateHandle(click);
-var options = { preventDefault: true };
-
-module.exports = anchor;
-
-// # anchor(attributes, text)
-//
-// Helper for creating virtual-dom anchors that trigger route changes. All
-// event/DOM delegation and router coupling are handled in this function
-// so that any anchor tags can be simply created with:
-//
-//     h('p', [
-//       anchor({ href: '/some-url' }, 'Click me!');
-//     ]);
-//
-// Clicking the generated link will fire the callbacks in the router and have
-// the application state update appropriately.
-function anchor(attributes, text) {
-  attributes['ev-click'] = hg.sendClick(handle, {
-    href: attributes.href
-  }, options);
-
-  return h('a', attributes, text);
-}
-
-// # click(data)
-//
-// Used as a mercury channel to update the current route using the exported
-// `router.href` observable.
-function click(data) {
-  var current = String(document.location.href);
-  var destination = url.resolve(current, data.href);
-  href.set(destination);
-}
diff --git a/web/browser/components/router/index.js b/web/browser/components/router/index.js
index 4755b97..d0152e6 100644
--- a/web/browser/components/router/index.js
+++ b/web/browser/components/router/index.js
@@ -2,8 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-var debug = require('debug')('reader:component:router');
+var debug = require('debug')('reader:router');
 var document = require('global/document');
+var extend = require('xtend');
 var format = require('format');
 var hashbang = require('./hashbang');
 var hg = require('mercury');
@@ -13,34 +14,35 @@
 var source = require('geval/source');
 var window = require('global/window');
 
-var href = hg.value('');
-
 module.exports = {
-  state: state,
-  render: render,
-  href: href
+  state: state
 };
 
-function state(map) {
-  debug('initializing: %o', map);
+function state(options) {
+  options = extend({ routes: {} }, options);
+  debug('init: %o', options);
 
   var atom = hg.state({
-    href: hg.value(''),
-    route: hg.struct({}),
-    routes: [],
-    channels: {
-      match: match
-    }
+    routes: hg.varhash({}),
+    href: hg.value(options.href || ''),
+    query: hg.value(null),
+    params: hg.value({}),
+    route: hg.value(null)
   });
 
-  for (var key in map) { // jshint ignore: line
+  // Map keys in options.routes and map to regular expresions which can be
+  // matched against later.
+  for (var key in options.routes) { // jshint ignore: line
+    var pattern = options.routes[key];
     var keys = [];
 
-    atom.routes.push({
-      pattern: (key === '*') ? '(.*)' : key, // "*" should be greedy.
+    // The pattern "*" should be greedy.
+    var path = (pattern === '*') ? '(.*)' : pattern;
+
+    atom.routes.put(key, {
+      pattern: pattern,
       keys: keys,
-      re: pathToRegexp(key, keys),
-      fn: map[key]
+      re: pathToRegexp(path, keys)
     });
   }
 
@@ -50,18 +52,6 @@
     match(atom, { href: href });
   });
 
-  // Changes to atom.href should be reflected in the url bar.
-  atom.href(function onhref(href) {
-    window.history.pushState(null, document.title, href);
-  });
-
-  // Bind the shared href atom used in ./anchor to this component's match
-  // channel.
-  module.exports.href(function onhref(href) {
-    match(atom, { href: href });
-  });
-
-  // Fire the initial route on initialization.
   debug('firing initial route');
   match(atom, {
     href: String(document.location.href)
@@ -70,34 +60,12 @@
   return atom;
 }
 
-function render(state) {
-  // Safely map arguments without deoptimizing the render function.
-  // SEE: http://git.io/XTo7TQ
-  var length = arguments.length - 1;
-  var args = new Array(length);
-  for (var i = 0; i < length; i++) {
-    args[i] = arguments[i + 1];
-  }
-
-  var route = state.route;
-
-  // Append params and route to the end of the arguments and call the current
-  // route's render function.
-  args.push(route.params);
-  args.push(route);
-
-  return route.fn.apply(null, args);
-}
-
 function match(state, data) {
-  debug('channel: match %s', data.href);
-
   if (state.href() === data.href) {
     debug('no update to href, skipping');
     return;
   }
 
-  var routes = state.routes;
   var url = parse(data.href);
   var hash = (url.hash) ? url.hash : hashbang(url.pathname);
 
@@ -110,32 +78,32 @@
 
   state.href.set(href);
 
-  var length = routes.length;
+  var routes = state.routes();
+  var keys = Object.keys(routes);
+  var length = keys.length;
   var _match;
 
   for (var i = 0; i < length; i++) {
-    var route = routes[i];
+    var key = keys[i];
+    var route = state.routes.get(key);
     _match = hash.match(route.re);
 
     if (!_match) {
       continue;
     }
 
-    var result = {
-      route: route.pattern,
-      fn: route.fn,
-      query: qs.parse(url.query),
-      params: {}
-    };
+    state.query.set(qs.parse(url.query));
+    state.route.set(route.pattern);
 
+    var params = {};
     var ki = route.keys.length;
     while (ki--) {
-      var key = route.keys[ki].name;
+      var param = route.keys[ki].name;
       var value = _match[ki+1];
-      result.params[key] = value;
+      params[param] = value;
     }
 
-    state.route.set(result);
+    state.params.set(params);
 
     break;
   }
diff --git a/web/browser/dom/blob-store.js b/web/browser/dom/blob-store.js
new file mode 100644
index 0000000..4f11169
--- /dev/null
+++ b/web/browser/dom/blob-store.js
@@ -0,0 +1,110 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var once = require('once');
+var thunky = require('thunky');
+var window = require('global/window');
+
+var database = thunky(open);
+
+module.exports = {
+  get: get,
+  put: put
+};
+
+// Open, initialize and return an IndexedDB instance.
+function open(callback) {
+  // Use the `indexedDB.open()` factory to get a IDBOpenDBRequest which inherits
+  // from IDBRequest. Do the right thing for every event handler.
+  //
+  // SEE: https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open
+  // SEE: https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest
+  // SEE: https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest
+  var req = window.indexedDB.open('reader-files');
+  req.onupgradeneeded = onupgradeneeded;
+  req.onblocked = onfailure;
+  req.onfailure = onfailure;
+  req.onsuccess = onsuccess;
+
+  // This event fires when a new version fires, this is a simple prototype
+  // without versions so this only happens on new databases. In this case the
+  // initial object store needs to be created.
+  function onupgradeneeded(event) {
+    var db = event.target.result;
+    db.createObjectStore('reader-files');
+  }
+
+  function onsuccess(event) {
+    var db = event.target.result;
+    callback(null, db);
+  }
+
+  function onfailure(event) {
+    callback(req.error);
+  }
+}
+
+// Put a Blob/File to the database.
+function put(hash, blob, callback) {
+  callback = once(callback);
+
+  database(ondb);
+
+  function ondb(err, db) {
+    if (err) {
+      return callback(err);
+    }
+
+    // Use a transaction to get the underlying "store" and save the passed in
+    // Blob.
+    //
+    // SEE: https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction
+    var transaction = db.transaction([ 'reader-files' ], 'readwrite');
+    var store = transaction.objectStore('reader-files');
+    var req = store.put(blob, hash);
+    req.onsuccess = onsuccess;
+    req.onfailure = onfailure;
+
+    function onsuccess(event) {
+      var res = event.target.result;
+      callback(null, res);
+    }
+
+    function onfailure(event) {
+      callback(req.error);
+    }
+  }
+}
+
+function get(hash, callback) {
+  callback = once(callback);
+
+  database(ondb);
+
+  function ondb(err, db) {
+    if (err) {
+      return callback(err);
+    }
+
+    // Use a transaction to get the underlying "store" and retrieve the Blob
+    // identified by it's hash.
+    //
+    // SEE: https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction
+    var transaction = db.transaction([ 'reader-files' ], 'readwrite');
+    var store = transaction.objectStore('reader-files');
+    var req = store.get(hash);
+    req.onsuccess = onsuccess;
+    req.onfailure = onfailure;
+    req.onabort = onfailure;
+
+    function onsuccess(event) {
+      var blob = event.target.result;
+      callback(null, blob);
+    }
+
+    function onfailure(event) {
+      callback(req.error);
+    }
+  }
+}
diff --git a/web/browser/dom/local-stash.js b/web/browser/dom/local-stash.js
new file mode 100644
index 0000000..f5e2013
--- /dev/null
+++ b/web/browser/dom/local-stash.js
@@ -0,0 +1,34 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var window = require('global/window');
+
+module.exports = stash;
+module.exports.get = get;
+module.exports.set = set;
+module.exports.del = del;
+
+function stash(key, value){
+  if (typeof value === 'undefined') {
+    return get(key);
+  } else {
+    return set(key, value);
+  }
+}
+
+function get(key){
+  var local = window.localStorage.getItem(key);
+
+  return JSON.parse(local);
+}
+
+function set(key, value){
+  value = JSON.stringify(value);
+
+  window.localStorage.setItem(key, value);
+}
+
+function del(key){
+  window.localStorage.removeItem(key);
+}
diff --git a/web/browser/dom/read-blob.js b/web/browser/dom/read-blob.js
new file mode 100644
index 0000000..83cb2c8
--- /dev/null
+++ b/web/browser/dom/read-blob.js
@@ -0,0 +1,62 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var once = require('once');
+var window = require('global/window');
+
+module.exports = read;
+
+// A simplified API for window.FileReader.
+//
+//     read(blob, function onread(err, result) {
+//       if (err) {
+//         console.error(err);
+//         return;
+//       }
+//
+//       console.log('result', result)
+//     })
+//
+// The callback will be triggered with err being an error object and result
+// being an array buffer.
+//
+// SEE: https://developer.mozilla.org/en-US/docs/Web/API/FileReader
+function read(blob, callback) {
+  var reader = new window.FileReader();
+
+  // Add listeners to all events, using the once module ensures the callback
+  // will only be called for the first handler to encounter a state error, or
+  // success update.
+  callback = once(callback);
+  reader.onabort = onabort.bind(reader, callback);
+  reader.onerror = onerror.bind(reader, callback);
+  reader.onload = onload.bind(reader, callback);
+  reader.onloadstart = noop;
+  reader.onloadend = onloadend.bind(reader, callback);
+  reader.onprogress = noop;
+
+  reader.readAsArrayBuffer(blob);
+}
+
+function onabort(callback) {
+  var err = new Error('Read aborted.');
+  callback(err);
+}
+
+// Error only handler.
+function onerror(callback) {
+  callback(this.error);
+}
+
+// The success only hanlder.
+function onload(callback) {
+  callback(null, this.result);
+}
+
+// The success and error handler.
+function onloadend(callback) {
+  callback(this.error, this.result);
+}
+
+function noop() {}
diff --git a/web/browser/events/click.js b/web/browser/events/click.js
index b618a32..6f35174 100644
--- a/web/browser/events/click.js
+++ b/web/browser/events/click.js
@@ -1,11 +1,14 @@
 // Copyright 2015 The Vanadium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
+
+var defaults = { preventDefault: true };
+var extend = require('xtend');
 var hg = require('mercury');
-var options = { preventDefault: true };
 
 module.exports = click;
 
-function click(sink, data) {
+function click(sink, data, options) {
+  options = extend(defaults, options);
   return hg.sendClick(sink, data, options);
 }
diff --git a/web/browser/events/file.js b/web/browser/events/file.js
index 2e7f532..1b979ae 100644
--- a/web/browser/events/file.js
+++ b/web/browser/events/file.js
@@ -22,7 +22,7 @@
   }
 
   // Merges passed in data.
-  var file = target.files[0];
-  var data = extend({ file: file }, this.data);
+  var blob = target.files[0];
+  var data = extend({ blob: blob }, this.data);
   broadcast(data);
 }
diff --git a/web/browser/hash-blob.js b/web/browser/hash-blob.js
deleted file mode 100644
index a130b93..0000000
--- a/web/browser/hash-blob.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var assert = require('assert');
-var BlobReader = require('readable-blob-stream');
-var once = require('once');
-var sha256d = require('sha256d');
-var through = require('through2');
-var window = require('global/window');
-
-module.exports = hash;
-
-function hash(blob, callback) {
-  callback = once(callback);
-  assert.ok(blob instanceof window.Blob, 'Must use a Blob object.');
-
-  var h = sha256d();
-  var stream = through(write, flush);
-  var rs = new BlobReader(blob);
-
-  rs
-  .on('error', callback)
-  .pipe(stream)
-  .on('error', callback);
-
-  function write(buffer, enc, cb) {
-    h.update(buffer);
-    cb();
-  }
-
-  function flush(cb) {
-    callback(null, h.digest('hex'));
-    cb();
-  }
-}
diff --git a/web/browser/main.js b/web/browser/main.js
index 0c1c662..8bc89d9 100644
--- a/web/browser/main.js
+++ b/web/browser/main.js
@@ -4,73 +4,115 @@
 
 var css = require('./components/base/index.css');
 var debug = require('debug')('reader:main');
+var deviceSet = require('./components/device-set');
+var deviceSets = require('./components/device-sets');
 var document = require('global/document');
 var domready = require('domready');
-var files = require('./components/files');
-var footer = require('./components/footer');
 var format = require('format');
 var h = require('mercury').h;
 var header = require('./components/header');
 var hg = require('mercury');
 var insert = require('insert-css');
-var mover = require('./components/mover');
 var router = require('./components/router');
+var stash = require('./dom/local-stash');
 var window = require('global/window');
 
+
 // Expose globals for debugging.
 window.debug = require('debug');
 window.require = require;
 global.require = require;
 
+var routes = {
+  INDEX: '#!/',
+  SHOW: '#!/:id',
+  NOT_FOUND: '*',
+};
+
+// Returns the application's global state atom based on previously stored state.
+function state(dehydrated) {
+  dehydrated = dehydrated || {};
+  debug('reyhdrating from %o', dehydrated);
+  return hg.state({
+    // Router options are never rehydrated from stored state, the router will
+    // only pay attention to default values and what is in the window.location
+    // APIs. This prevents user confusion when the stored route doesn't match
+    // location.href.
+    router: router.state({ routes: routes }),
+    deviceSets: deviceSets.state(dehydrated.deviceSets),
+  });
+}
+
 domready(function ondomready() {
   debug('domready');
 
-  // Top level state.
-  var state = hg.state({
-    store: hg.value(null),
-    files: files.state(),
-    mover: mover.state({}),
-    router: router.state({
-      '#!/': index,
-      '#!/mover': showMover,
-      '#!/:id': show,
-      '*': notfound
-    })
-  });
+  var stored = stash('state');
+  var atom = state(stored);
 
-  hg.app(document.body, state, render);
+  // HACK(jasoncampbell): When the initial route is for a device-set it's PDF
+  // file should be shown. Loading a PDF file into the PDF.js renderer is a
+  // mutlistep process and to make matters more complicated, due to thier size
+  // PDF blobs are stored via a different mechanism than the simple state stash
+  // (SEE: ./dom/blob-store.js).
+  //
+  // Check if the current route is routes.SHOW.
+  if (atom.router.route() === routes.SHOW) {
+    // Retrieve the current device-set.
+    var params = atom.router.params();
+    var ds = atom.deviceSets.collection.get(params.id);
+    // Listen for changes to the underlying Blob. At some point after
+    // initialization it might be retreived from either a local store or
+    // Syncbase and set, the watch function below will change anytime the value
+    // is updated.
+    var remove = ds.file.blob(function blobchange(blob) {
+      // If the Blob object is set then load it so that it can be rendered.
+      if (blob instanceof window.Blob) {
+        deviceSet.channels.load(ds);
+        // The initial work is done, this listener can be removed.
+        remove();
+      }
+    });
+  }
+
+  // TODO(jasoncampbell): Can there be a dynamic error listener which maps
+  // errors to the top error component?
+
+  hg.app(document.body, atom, render);
 });
 
 function render(state) {
+  // Save the state for later, this is a quick way to limit localStorage writes
+  // to the same RAF as the main render function.
+  stash('state', state);
   debug('render: %o', state);
   insert(css);
 
-  return h('.reader-app', [
-    hg.partial(header.render, state),
-    hg.partial(router.render, state.router, state),
-    hg.partial(footer.render, state, state.files.channels.add)
-  ]);
-}
+  var children = [];
 
-function index(state, params, route) {
-  return h('main', [
-    hg.partial(files.render, state.files, state.files.channels)
-  ]);
-}
+  switch (state.router.route) {
+    case routes.INDEX:
+      children = [
+        hg.partial(header.render, state),
+        hg.partial(deviceSets.render,
+          state.deviceSets,
+          state.deviceSets.channels)
+      ];
+      break;
+    case routes.SHOW:
+      var key = state.router.params.id;
+      var value = state.deviceSets.collection[key];
+      children = [
+        hg.partial(deviceSet.render, value, value.channels)
+      ];
+      break;
+    case routes.NOT_FOUND:
+      children = [
+        hg.partial(notfound, state)
+      ];
+      break;
+  }
 
-function show(state, params, route) {
-  debug('show: %s', params.id);
-
-  return h('main', [
-
-  ]);
-}
-
-function showMover(state, params, route) {
-  debug('show mover');
-  return h('main', [
-    hg.partial(mover.render, state.mover, state.mover.channels),
-  ]);
+  return h('.reader-app', children);
 }
 
 function notfound(state) {
@@ -78,7 +120,7 @@
   console.error('TODO: not found error - %s', href);
 
   return h('.notfound', [
-    h('Not Found.'),
+    h('h1', 'Not Found.'),
     h('p', format('The page "%s" does not exisit.', state.router.href))
   ]);
 }
diff --git a/web/browser/util.js b/web/browser/util.js
index ce66587..799e0e8 100644
--- a/web/browser/util.js
+++ b/web/browser/util.js
@@ -5,9 +5,27 @@
 module.exports = {
   toArray: toArray,
   removed: removed,
-  each: each
+  each: each,
+  map: map
 };
 
+// Map an object's keys to vdom.
+function map(object, render, channels) {
+  var array = [];
+  var keys = Object.keys(object);
+  var length = keys.length;
+
+  for (var i = 0; i < length; i++) {
+    var key = keys[i];
+    var value = object[key];
+    var item = render(value, channels);
+
+    array.push(item);
+  }
+
+  return array;
+}
+
 // # toArray(object)
 //
 // Convert an object to an array which contains just the enumerable keys of the
diff --git a/web/browser/widgets/pdf-widget.js b/web/browser/widgets/pdf-widget.js
deleted file mode 100644
index 334546f..0000000
--- a/web/browser/widgets/pdf-widget.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-var document = require('global/document');
-
-module.exports = PDFWidget;
-
-// TODO(jasoncampebll): add verification for pdf object
-function PDFWidget(state) {
-  if (!(this instanceof PDFWidget)) {
-    return new PDFWidget(state);
-  }
-
-  this.state = state;
-}
-
-PDFWidget.prototype.type = 'Widget';
-
-PDFWidget.prototype.init = function init() {
-  var widget = this;
-  var element = document.createElement('canvas');
-  element.setAttribute('class','pdf-canvas');
-
-  widget.update(null, element);
-  return element;
-};
-
-PDFWidget.prototype.update = function update(previous, element) {
-  var widget = this;
-  var state = widget.state;
-  var pdf = state.pdf;
-
-  if (!pdf) {
-    return;
-  }
-
-  // TODO(jasoncampbell): It would be better to have this operation in a
-  // different place and only have this widget handle the render aspect of the
-  // page.
-  pdf.getPage(state.pages.current).then(success, error);
-
-  function success(page) {
-    var viewport = page.getViewport(state.scale);
-    var context = element.getContext('2d');
-
-    element.height = viewport.height;
-    element.width = viewport.width;
-
-    page.render({
-      canvasContext: context,
-      viewport: viewport
-    });
-  }
-
-  function error(err) {
-    process.nextTick(function() {
-      throw err;
-    });
-  }
-};
diff --git a/web/package.json b/web/package.json
index 9c0c428..5f59a84 100644
--- a/web/package.json
+++ b/web/package.json
@@ -20,7 +20,6 @@
     "jshint": "^2.8.0",
     "myth": "^1.5.0",
     "npm-css": "^0.2.3",
-    "raf": "^3.0.0",
     "rework": "^1.0.1",
     "rework-import": "^2.1.0",
     "rework-inherit": "^0.2.3",
@@ -39,8 +38,13 @@
     "global": "^4.3.0",
     "insert-css": "^0.2.0",
     "mercury": "^14.0.0",
+    "once": "^1.3.2",
     "path-to-regexp": "^1.2.1",
+    "pump": "^1.0.1",
     "qs": "^5.2.0",
+    "raf": "^3.1.0",
+    "readable-blob-stream": "^1.1.0",
+    "thunky": "^0.1.0",
     "xtend": "^4.0.0"
   }
 }
diff --git a/web/public/index.html b/web/public/index.html
index f379e43..f6fddff 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -1,2 +1,3 @@
 <script type="text/javascript" src="/pdf.js"></script>
 <script type="text/javascript" src="/bundle.js"></script>
+<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
diff --git a/web/public/pdf.js b/web/public/pdf.js
index 9382fd5..e353b5e 100644
--- a/web/public/pdf.js
+++ b/web/public/pdf.js
@@ -1,7 +1,3 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -26,33 +22,14 @@
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.1.114';
-PDFJS.build = '3fd44fd';
+PDFJS.version = '1.1.366';
+PDFJS.build = '9e9df56';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-/* Copyright 2012 Mozilla Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref, URL,
-           Promise */
 
-'use strict';
 
 var globalScope = (typeof window === 'undefined') ? this : window;
 
@@ -85,6 +62,14 @@
   LINK: 3
 };
 
+var AnnotationBorderStyleType = {
+  SOLID: 1,
+  DASHED: 2,
+  BEVELED: 3,
+  INSET: 4,
+  UNDERLINE: 5
+};
+
 var StreamType = {
   UNKNOWN: 0,
   FLATE: 1,
@@ -538,7 +523,7 @@
   }
 });
 
-  // Lazy test if the userAgant support CanvasTypedArrays
+  // Lazy test if the userAgent support CanvasTypedArrays
 function hasCanvasTypedArrays() {
   var canvas = document.createElement('canvas');
   canvas.width = canvas.height = 1;
@@ -977,6 +962,10 @@
   return decodeURIComponent(escape(str));
 }
 
+function utf8StringToString(str) {
+  return unescape(encodeURIComponent(str));
+}
+
 function isEmptyObj(obj) {
   for (var key in obj) {
     return false;
@@ -2273,13 +2262,10 @@
      * annotation objects.
      */
     getAnnotations: function PDFPageProxy_getAnnotations() {
-      if (this.annotationsPromise) {
-        return this.annotationsPromise;
+      if (!this.annotationsPromise) {
+        this.annotationsPromise = this.transport.getAnnotations(this.pageIndex);
       }
-
-      var promise = this.transport.getAnnotations(this.pageIndex);
-      this.annotationsPromise = promise;
-      return promise;
+      return this.annotationsPromise;
     },
     /**
      * Begins the process of rendering a page to the desired context.
@@ -2325,6 +2311,7 @@
                                                       this.commonObjs,
                                                       intentState.operatorList,
                                                       this.pageNumber);
+      internalRenderTask.useRequestAnimationFrame = renderingIntent !== 'print';
       if (!intentState.renderTasks) {
         intentState.renderTasks = [];
       }
@@ -3130,6 +3117,7 @@
     this.running = false;
     this.graphicsReadyCallback = null;
     this.graphicsReady = false;
+    this.useRequestAnimationFrame = false;
     this.cancelled = false;
     this.capability = createPromiseCapability();
     this.task = new RenderTask(this);
@@ -3203,7 +3191,11 @@
     },
 
     _scheduleNext: function InternalRenderTask__scheduleNext() {
-      window.requestAnimationFrame(this._nextBound);
+      if (this.useRequestAnimationFrame) {
+        window.requestAnimationFrame(this._nextBound);
+      } else {
+        Promise.resolve(undefined).then(this._nextBound);
+      }
     },
 
     _next: function InternalRenderTask__next() {
@@ -4514,7 +4506,7 @@
       }
 
       var name = fontObj.loadedName || 'sans-serif';
-      var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') :
+      var bold = fontObj.black ? (fontObj.bold ? '900' : 'bold') :
                                  (fontObj.bold ? 'bold' : 'normal');
 
       var italic = fontObj.italic ? 'italic' : 'normal';
@@ -4774,6 +4766,7 @@
       if (isTextInvisible || fontSize === 0) {
         return;
       }
+      this.cachedGetSinglePixelWidth = null;
 
       ctx.save();
       ctx.transform.apply(ctx, current.textMatrix);
@@ -6360,6 +6353,13 @@
     var rules = [];
     var fontsToLoad = [];
     var fontLoadPromises = [];
+    var getNativeFontPromise = function(nativeFontFace) {
+      // Return a promise that is always fulfilled, even when the font fails to
+      // load.
+      return nativeFontFace.loaded.catch(function(e) {
+        warn('Failed to load font "' + nativeFontFace.family + '": ' + e);
+      });
+    };
     for (var i = 0, ii = fonts.length; i < ii; i++) {
       var font = fonts[i];
 
@@ -6373,7 +6373,7 @@
       if (this.isFontLoadingAPISupported) {
         var nativeFontFace = font.createNativeFontFace();
         if (nativeFontFace) {
-          fontLoadPromises.push(nativeFontFace.loaded);
+          fontLoadPromises.push(getNativeFontPromise(nativeFontFace));
         }
       } else {
         var rule = font.bindDOM();
@@ -6386,7 +6386,7 @@
 
     var request = FontLoader.queueLoadingCallback(callback);
     if (this.isFontLoadingAPISupported) {
-      Promise.all(fontsToLoad).then(function() {
+      Promise.all(fontLoadPromises).then(function() {
         request.complete();
       });
     } else if (rules.length > 0 && !this.isSyncFontLoadingSupported) {
@@ -6624,25 +6624,70 @@
     style.fontFamily = fontFamily + fallbackName;
   }
 
-  function initContainer(item, drawBorder) {
+  function initContainer(item) {
     var container = document.createElement('section');
     var cstyle = container.style;
     var width = item.rect[2] - item.rect[0];
     var height = item.rect[3] - item.rect[1];
 
-    var bWidth = item.borderWidth || 0;
-    if (bWidth) {
-      width = width - 2 * bWidth;
-      height = height - 2 * bWidth;
-      cstyle.borderWidth = bWidth + 'px';
-      var color = item.color;
-      if (drawBorder && color) {
-        cstyle.borderStyle = 'solid';
-        cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255),
-                                             Math.round(color[1] * 255),
-                                             Math.round(color[2] * 255));
+    // Border
+    if (item.borderStyle.width > 0) {
+      // Border width
+      container.style.borderWidth = item.borderStyle.width + 'px';
+      if (item.borderStyle.style !== AnnotationBorderStyleType.UNDERLINE) {
+        // Underline styles only have a bottom border, so we do not need
+        // to adjust for all borders. This yields a similar result as
+        // Adobe Acrobat/Reader.
+        width = width - 2 * item.borderStyle.width;
+        height = height - 2 * item.borderStyle.width;
+      }
+
+      // Horizontal and vertical border radius
+      var horizontalRadius = item.borderStyle.horizontalCornerRadius;
+      var verticalRadius = item.borderStyle.verticalCornerRadius;
+      if (horizontalRadius > 0 || verticalRadius > 0) {
+        var radius = horizontalRadius + 'px / ' + verticalRadius + 'px';
+        CustomStyle.setProp('borderRadius', container, radius);
+      }
+
+      // Border style
+      switch (item.borderStyle.style) {
+        case AnnotationBorderStyleType.SOLID:
+          container.style.borderStyle = 'solid';
+          break;
+
+        case AnnotationBorderStyleType.DASHED:
+          container.style.borderStyle = 'dashed';
+          break;
+
+        case AnnotationBorderStyleType.BEVELED:
+          warn('Unimplemented border style: beveled');
+          break;
+
+        case AnnotationBorderStyleType.INSET:
+          warn('Unimplemented border style: inset');
+          break;
+
+        case AnnotationBorderStyleType.UNDERLINE:
+          container.style.borderBottomStyle = 'solid';
+          break;
+
+        default:
+          break;
+      }
+
+      // Border color
+      if (item.color) {
+        container.style.borderColor =
+          Util.makeCssRgb(item.color[0] | 0,
+                          item.color[1] | 0,
+                          item.color[2] | 0);
+      } else {
+        // Transparent (invisible) border, so do not draw it at all.
+        container.style.borderWidth = 0;
       }
     }
+
     cstyle.width = width + 'px';
     cstyle.height = height + 'px';
     return container;
@@ -6683,7 +6728,7 @@
       rect[2] = rect[0] + (rect[3] - rect[1]); // make it square
     }
 
-    var container = initContainer(item, false);
+    var container = initContainer(item);
     container.className = 'annotText';
 
     var image  = document.createElement('img');
@@ -6706,17 +6751,15 @@
     content.setAttribute('hidden', true);
 
     var i, ii;
-    if (item.hasBgColor) {
+    if (item.hasBgColor && item.color) {
       var color = item.color;
 
       // Enlighten the color (70%)
       var BACKGROUND_ENLIGHT = 0.7;
-      var r = BACKGROUND_ENLIGHT * (1.0 - color[0]) + color[0];
-      var g = BACKGROUND_ENLIGHT * (1.0 - color[1]) + color[1];
-      var b = BACKGROUND_ENLIGHT * (1.0 - color[2]) + color[2];
-      content.style.backgroundColor = Util.makeCssRgb((r * 255) | 0,
-                                                      (g * 255) | 0,
-                                                      (b * 255) | 0);
+      var r = BACKGROUND_ENLIGHT * (255 - color[0]) + color[0];
+      var g = BACKGROUND_ENLIGHT * (255 - color[1]) + color[1];
+      var b = BACKGROUND_ENLIGHT * (255 - color[2]) + color[2];
+      content.style.backgroundColor = Util.makeCssRgb(r | 0, g | 0, b | 0);
     }
 
     var title = document.createElement('h1');
@@ -6792,7 +6835,7 @@
   }
 
   function getHtmlElementForLinkAnnotation(item) {
-    var container = initContainer(item, true);
+    var container = initContainer(item);
     container.className = 'annotLink';
 
     var link = document.createElement('a');
diff --git a/web/public/pdf.worker.js b/web/public/pdf.worker.js
index df4883d..2c32874 100644
--- a/web/public/pdf.worker.js
+++ b/web/public/pdf.worker.js
@@ -1,7 +1,3 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -26,33 +22,14 @@
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.1.114';
-PDFJS.build = '3fd44fd';
+PDFJS.version = '1.1.366';
+PDFJS.build = '9e9df56';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-/* Copyright 2012 Mozilla Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref, URL,
-           Promise */
 
-'use strict';
 
 var globalScope = (typeof window === 'undefined') ? this : window;
 
@@ -85,6 +62,14 @@
   LINK: 3
 };
 
+var AnnotationBorderStyleType = {
+  SOLID: 1,
+  DASHED: 2,
+  BEVELED: 3,
+  INSET: 4,
+  UNDERLINE: 5
+};
+
 var StreamType = {
   UNKNOWN: 0,
   FLATE: 1,
@@ -538,7 +523,7 @@
   }
 });
 
-  // Lazy test if the userAgant support CanvasTypedArrays
+  // Lazy test if the userAgent support CanvasTypedArrays
 function hasCanvasTypedArrays() {
   var canvas = document.createElement('canvas');
   canvas.width = canvas.height = 1;
@@ -977,6 +962,10 @@
   return decodeURIComponent(escape(str));
 }
 
+function utf8StringToString(str) {
+  return unescape(encodeURIComponent(str));
+}
+
 function isEmptyObj(obj) {
   for (var key in obj) {
     return false;
@@ -1644,6 +1633,26 @@
     return array.buffer;
   }
 
+  var supportsMozChunked = (function supportsMozChunkedClosure() {
+    var x = new XMLHttpRequest();
+    try {
+      // Firefox 37- required .open() to be called before setting responseType.
+      // https://bugzilla.mozilla.org/show_bug.cgi?id=707484
+      x.open('GET', 'https://example.com');
+    } catch (e) {
+      // Even though the URL is not visited, .open() could fail if the URL is
+      // blocked, e.g. via the connect-src CSP directive or the NoScript addon.
+      // When this error occurs, this feature detection method will mistakenly
+      // report that moz-chunked-arraybuffer is not supported in Firefox 37-.
+    }
+    try {
+      x.responseType = 'moz-chunked-arraybuffer';
+      return x.responseType === 'moz-chunked-arraybuffer';
+    } catch (e) {
+      return false;
+    }
+  })();
+
   NetworkManager.prototype = {
     requestRange: function NetworkManager_requestRange(begin, end, listeners) {
       var args = {
@@ -1684,17 +1693,11 @@
         pendingRequest.expectedStatus = 200;
       }
 
-      if (args.onProgressiveData) {
-        // Some legacy browsers might throw an exception.
-        try {
-          xhr.responseType = 'moz-chunked-arraybuffer';
-        } catch(e) {}
-        if (xhr.responseType === 'moz-chunked-arraybuffer') {
-          pendingRequest.onProgressiveData = args.onProgressiveData;
-          pendingRequest.mozChunked = true;
-        } else {
-          xhr.responseType = 'arraybuffer';
-        }
+      var useMozChunkedLoading = supportsMozChunked && !!args.onProgressiveData;
+      if (useMozChunkedLoading) {
+        xhr.responseType = 'moz-chunked-arraybuffer';
+        pendingRequest.onProgressiveData = args.onProgressiveData;
+        pendingRequest.mozChunked = true;
       } else {
         xhr.responseType = 'arraybuffer';
       }
@@ -1957,14 +1960,9 @@
     },
 
     nextEmptyChunk: function ChunkedStream_nextEmptyChunk(beginChunk) {
-      var chunk, n;
-      for (chunk = beginChunk, n = this.numChunks; chunk < n; ++chunk) {
-        if (!this.loadedChunks[chunk]) {
-          return chunk;
-        }
-      }
-      // Wrap around to beginning
-      for (chunk = 0; chunk < beginChunk; ++chunk) {
+      var chunk, numChunks = this.numChunks;
+      for (var i = 0; i < numChunks; ++i) {
+        chunk = (beginChunk + i) % numChunks; // Wrap around to beginning
         if (!this.loadedChunks[chunk]) {
           return chunk;
         }
@@ -2241,7 +2239,7 @@
       this.requestChunks(chunksToRequest, callback);
     },
 
-    // Groups a sorted array of chunks into as few continguous larger
+    // Groups a sorted array of chunks into as few contiguous larger
     // chunks as possible
     groupChunks: function ChunkedStreamManager_groupChunks(chunks) {
       var groupedChunks = [];
@@ -2366,15 +2364,6 @@
     },
 
     getEndChunk: function ChunkedStreamManager_getEndChunk(end) {
-      if (end % this.chunkSize === 0) {
-        return end / this.chunkSize;
-      }
-
-      // 0 -> 0
-      // 1 -> 1
-      // 99 -> 1
-      // 100 -> 1
-      // 101 -> 2
       var chunk = Math.floor((end - 1) / this.chunkSize) + 1;
       return chunk;
     }
@@ -2611,17 +2600,33 @@
       return this.pageDict.get(key);
     },
 
-    getInheritedPageProp: function Page_inheritPageProp(key) {
-      var dict = this.pageDict;
-      var value = dict.get(key);
-      while (value === undefined) {
-        dict = dict.get('Parent');
-        if (!dict) {
+    getInheritedPageProp: function Page_getInheritedPageProp(key) {
+      var dict = this.pageDict, valueArray = null, loopCount = 0;
+      var MAX_LOOP_COUNT = 100;
+      // Always walk up the entire parent chain, to be able to find
+      // e.g. \Resources placed on multiple levels of the tree.
+      while (dict) {
+        var value = dict.get(key);
+        if (value) {
+          if (!valueArray) {
+            valueArray = [];
+          }
+          valueArray.push(value);
+        }
+        if (++loopCount > MAX_LOOP_COUNT) {
+          warn('Page_getInheritedPageProp: maximum loop count exceeded.');
           break;
         }
-        value = dict.get(key);
+        dict = dict.get('Parent');
       }
-      return value;
+      if (!valueArray) {
+        return Dict.empty;
+      }
+      if (valueArray.length === 1 || !isDict(valueArray[0]) ||
+          loopCount > MAX_LOOP_COUNT) {
+        return valueArray[0];
+      }
+      return Dict.merge(this.xref, valueArray);
     },
 
     get content() {
@@ -2629,14 +2634,10 @@
     },
 
     get resources() {
-      var value = this.getInheritedPageProp('Resources');
       // For robustness: The spec states that a \Resources entry has to be
-      // present, but can be empty. Some document omit it still. In this case
-      // return an empty dictionary:
-      if (value === undefined) {
-        value = Dict.empty;
-      }
-      return shadow(this, 'resources', value);
+      // present, but can be empty. Some document omit it still, in this case
+      // we return an empty dictionary.
+      return shadow(this, 'resources', this.getInheritedPageProp('Resources'));
     },
 
     get mediaBox() {
@@ -2666,11 +2667,6 @@
       return shadow(this, 'view', cropBox);
     },
 
-    get annotationRefs() {
-      return shadow(this, 'annotationRefs',
-                    this.getInheritedPageProp('Annots'));
-    },
-
     get rotate() {
       var rotate = this.getInheritedPageProp('Rotate') || 0;
       // Normalize rotation so it's a multiple of 90 and between 0 and 270
@@ -2816,18 +2812,20 @@
       var annotations = this.annotations;
       var annotationsData = [];
       for (var i = 0, n = annotations.length; i < n; ++i) {
-        annotationsData.push(annotations[i].getData());
+        annotationsData.push(annotations[i].data);
       }
       return annotationsData;
     },
 
     get annotations() {
       var annotations = [];
-      var annotationRefs = (this.annotationRefs || []);
+      var annotationRefs = this.getInheritedPageProp('Annots') || [];
+      var annotationFactory = new AnnotationFactory();
       for (var i = 0, n = annotationRefs.length; i < n; ++i) {
         var annotationRef = annotationRefs[i];
-        var annotation = Annotation.fromRef(this.xref, annotationRef);
-        if (annotation) {
+        var annotation = annotationFactory.create(this.xref, annotationRef);
+        if (annotation &&
+            (annotation.isViewable() || annotation.isPrintable())) {
           annotations.push(annotation);
         }
       }
@@ -2909,6 +2907,10 @@
   PDFDocument.prototype = {
     parse: function PDFDocument_parse(recoveryMode) {
       this.setup(recoveryMode);
+      var version = this.catalog.catDict.get('Version');
+      if (isName(version)) {
+        this.pdfFormatVersion = version.name;
+      }
       try {
         // checking if AcroForm is present
         this.acroForm = this.catalog.catDict.get('AcroForm');
@@ -3010,8 +3012,10 @@
           }
           version += String.fromCharCode(ch);
         }
-        // removing "%PDF-"-prefix
-        this.pdfFormatVersion = version.substring(5);
+        if (!this.pdfFormatVersion) {
+          // removing "%PDF-"-prefix
+          this.pdfFormatVersion = version.substring(5);
+        }
         return;
       }
       // May not be a PDF file, continue anyway.
@@ -3288,6 +3292,24 @@
 
   Dict.empty = new Dict(null);
 
+  Dict.merge = function Dict_merge(xref, dictArray) {
+    var mergedDict = new Dict(xref);
+
+    for (var i = 0, ii = dictArray.length; i < ii; i++) {
+      var dict = dictArray[i];
+      if (!isDict(dict)) {
+        continue;
+      }
+      for (var keyName in dict.map) {
+        if (mergedDict.map[keyName]) {
+          continue;
+        }
+        mergedDict.map[keyName] = dict.map[keyName];
+      }
+    }
+    return mergedDict;
+  };
+
   return Dict;
 })();
 
@@ -3542,7 +3564,7 @@
       }
 
       var xref = this.xref;
-      var dest, nameTreeRef, nameDictionaryRef;
+      var dest = null, nameTreeRef, nameDictionaryRef;
       var obj = this.catDict.get('Names');
       if (obj && obj.has('Dests')) {
         nameTreeRef = obj.getRaw('Dests');
@@ -3550,17 +3572,11 @@
         nameDictionaryRef = this.catDict.get('Dests');
       }
 
-      if (nameDictionaryRef) {
-        // reading simple destination dictionary
-        obj = nameDictionaryRef;
-        obj.forEach(function catalogForEach(key, value) {
-          if (!value) {
-            return;
-          }
-          if (key === destinationId) {
-            dest = fetchDestination(value);
-          }
-        });
+      if (nameDictionaryRef) { // Simple destination dictionary.
+        var value = nameDictionaryRef.get(destinationId);
+        if (value) {
+          dest = fetchDestination(value);
+        }
       }
       if (nameTreeRef) {
         var nameTree = new NameTree(nameTreeRef, xref);
@@ -3597,6 +3613,19 @@
       var obj = this.catDict.get('Names');
 
       var javaScript = [];
+      function appendIfJavaScriptDict(jsDict) {
+        var type = jsDict.get('S');
+        if (!isName(type) || type.name !== 'JavaScript') {
+          return;
+        }
+        var js = jsDict.get('JS');
+        if (isStream(js)) {
+          js = bytesToString(js.getBytes());
+        } else if (!isString(js)) {
+          return;
+        }
+        javaScript.push(stringToPDFString(js));
+      }
       if (obj && obj.has('JavaScript')) {
         var nameTree = new NameTree(obj.getRaw('JavaScript'), xref);
         var names = nameTree.getAll();
@@ -3607,36 +3636,25 @@
           // We don't really use the JavaScript right now. This code is
           // defensive so we don't cause errors on document load.
           var jsDict = names[name];
-          if (!isDict(jsDict)) {
-            continue;
+          if (isDict(jsDict)) {
+            appendIfJavaScriptDict(jsDict);
           }
-          var type = jsDict.get('S');
-          if (!isName(type) || type.name !== 'JavaScript') {
-            continue;
-          }
-          var js = jsDict.get('JS');
-          if (!isString(js) && !isStream(js)) {
-            continue;
-          }
-          if (isStream(js)) {
-            js = bytesToString(js.getBytes());
-          }
-          javaScript.push(stringToPDFString(js));
         }
       }
 
       // Append OpenAction actions to javaScript array
       var openactionDict = this.catDict.get('OpenAction');
-      if (isDict(openactionDict)) {
-        var objType = openactionDict.get('Type');
+      if (isDict(openactionDict, 'Action')) {
         var actionType = openactionDict.get('S');
-        var action = openactionDict.get('N');
-        var isPrintAction = (isName(objType) && objType.name === 'Action' &&
-                            isName(actionType) && actionType.name === 'Named' &&
-                            isName(action) && action.name === 'Print');
-
-        if (isPrintAction) {
-          javaScript.push('print(true);');
+        if (isName(actionType) && actionType.name === 'Named') {
+          // The named Print action is not a part of the PDF 1.7 specification,
+          // but is supported by many PDF readers/writers (including Adobe's).
+          var action = openactionDict.get('N');
+          if (isName(action) && action.name === 'Print') {
+            javaScript.push('print({});');
+          }
+        } else {
+          appendIfJavaScriptDict(openactionDict);
         }
       }
 
@@ -4136,12 +4154,13 @@
           trailers.push(position);
           position += skipUntil(buffer, position, startxrefBytes);
         } else if ((m = /^(\d+)\s+(\d+)\s+obj\b/.exec(token))) {
-          this.entries[m[1]] = {
-            offset: position,
-            gen: m[2] | 0,
-            uncompressed: true
-          };
-
+          if (typeof this.entries[m[1]] === 'undefined') {
+            this.entries[m[1]] = {
+              offset: position - stream.start,
+              gen: m[2] | 0,
+              uncompressed: true
+            };
+          }
           var contentLength = skipUntil(buffer, position, endobjBytes) + 7;
           var content = buffer.subarray(position, position + contentLength);
 
@@ -4150,8 +4169,8 @@
           var xrefTagOffset = skipUntil(content, 0, xrefBytes);
           if (xrefTagOffset < contentLength &&
               content[xrefTagOffset + 5] < 64) {
-            xrefStms.push(position);
-            this.xrefstms[position] = 1; // don't read it recursively
+            xrefStms.push(position - stream.start);
+            this.xrefstms[position - stream.start] = 1; // Avoid recursion
           }
 
           position += contentLength;
@@ -4470,7 +4489,7 @@
         var names = obj.get('Names');
         if (names) {
           for (i = 0, n = names.length; i < n; i += 2) {
-            dict[names[i]] = xref.fetchIfRef(names[i + 1]);
+            dict[xref.fetchIfRef(names[i])] = xref.fetchIfRef(names[i + 1]);
           }
         }
       }
@@ -4509,9 +4528,9 @@
           var kid = xref.fetchIfRef(kids[m]);
           var limits = kid.get('Limits');
 
-          if (destinationId < limits[0]) {
+          if (destinationId < xref.fetchIfRef(limits[0])) {
             r = m - 1;
-          } else if (destinationId > limits[1]) {
+          } else if (destinationId > xref.fetchIfRef(limits[1])) {
             l = m + 1;
           } else {
             kidsOrNames = xref.fetchIfRef(kids[m]);
@@ -4535,9 +4554,9 @@
           // Check only even indices (0, 2, 4, ...) because the
           // odd indices contain the actual D array.
           m = (l + r) & ~1;
-          if (destinationId < names[m]) {
+          if (destinationId < xref.fetchIfRef(names[m])) {
             r = m - 2;
-          } else if (destinationId > names[m]) {
+          } else if (destinationId > xref.fetchIfRef(names[m])) {
             l = m + 2;
           } else {
             return xref.fetchIfRef(names[m + 1]);
@@ -4885,7 +4904,54 @@
 
 
 var DEFAULT_ICON_SIZE = 22; // px
-var SUPPORTED_TYPES = ['Link', 'Text', 'Widget'];
+
+/**
+ * @constructor
+ */
+function AnnotationFactory() {}
+AnnotationFactory.prototype = {
+  /**
+   * @param {XRef} xref
+   * @param {Object} ref
+   * @returns {Annotation}
+   */
+  create: function AnnotationFactory_create(xref, ref) {
+    var dict = xref.fetchIfRef(ref);
+    if (!isDict(dict)) {
+      return;
+    }
+
+    // Determine the annotation's subtype.
+    var subtype = dict.get('Subtype');
+    subtype = isName(subtype) ? subtype.name : '';
+
+    // Return the right annotation object based on the subtype and field type.
+    var parameters = {
+      dict: dict,
+      ref: ref
+    };
+
+    switch (subtype) {
+      case 'Link':
+        return new LinkAnnotation(parameters);
+
+      case 'Text':
+        return new TextAnnotation(parameters);
+
+      case 'Widget':
+        var fieldType = Util.getInheritableProperty(dict, 'FT');
+        if (isName(fieldType) && fieldType.name === 'Tx') {
+          return new TextWidgetAnnotation(parameters);
+        }
+        return new WidgetAnnotation(parameters);
+
+      default:
+        warn('Unimplemented annotation type "' + subtype + '", ' +
+             'falling back to base annotation');
+        return new Annotation(parameters);
+    }
+  }
+};
 
 var Annotation = (function AnnotationClosure() {
   // 12.5.5: Algorithm: Appearance streams
@@ -4938,72 +5004,16 @@
     var data = this.data = {};
 
     data.subtype = dict.get('Subtype').name;
-    var rect = dict.get('Rect') || [0, 0, 0, 0];
-    data.rect = Util.normalizeRect(rect);
     data.annotationFlags = dict.get('F');
 
-    var color = dict.get('C');
-    if (!color) {
-      // The PDF spec does not mention how a missing color array is interpreted.
-      // Adobe Reader seems to default to black in this case.
-      data.color = [0, 0, 0];
-    } else if (isArray(color)) {
-      switch (color.length) {
-        case 0:
-          // Empty array denotes transparent border.
-          data.color = null;
-          break;
-        case 1:
-          // TODO: implement DeviceGray
-          break;
-        case 3:
-          data.color = color;
-          break;
-        case 4:
-          // TODO: implement DeviceCMYK
-          break;
-      }
-    }
+    this.setRectangle(dict.get('Rect'));
+    data.rect = this.rectangle;
 
-    // Some types of annotations have border style dict which has more
-    // info than the border array
-    if (dict.has('BS')) {
-      var borderStyle = dict.get('BS');
-      data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1;
-    } else {
-      var borderArray = dict.get('Border') || [0, 0, 1];
-      data.borderWidth = borderArray[2] || 0;
+    this.setColor(dict.get('C'));
+    data.color = this.color;
 
-      // TODO: implement proper support for annotations with line dash patterns.
-      var dashArray = borderArray[3];
-      if (data.borderWidth > 0 && dashArray) {
-        if (!isArray(dashArray)) {
-          // Ignore the border if dashArray is not actually an array,
-          // this is consistent with the behaviour in Adobe Reader.
-          data.borderWidth = 0;
-        } else {
-          var dashArrayLength = dashArray.length;
-          if (dashArrayLength > 0) {
-            // According to the PDF specification: the elements in a dashArray
-            // shall be numbers that are nonnegative and not all equal to zero.
-            var isInvalid = false;
-            var numPositive = 0;
-            for (var i = 0; i < dashArrayLength; i++) {
-              var validNumber = (+dashArray[i] >= 0);
-              if (!validNumber) {
-                isInvalid = true;
-                break;
-              } else if (dashArray[i] > 0) {
-                numPositive++;
-              }
-            }
-            if (isInvalid || numPositive === 0) {
-              data.borderWidth = 0;
-            }
-          }
-        }
-      }
-    }
+    this.borderStyle = data.borderStyle = new AnnotationBorderStyle();
+    this.setBorderStyle(dict);
 
     this.appearance = getDefaultAppearance(dict);
     data.hasAppearance = !!this.appearance;
@@ -5011,20 +5021,110 @@
   }
 
   Annotation.prototype = {
+    /**
+     * Set the rectangle.
+     *
+     * @public
+     * @memberof Annotation
+     * @param {Array} rectangle - The rectangle array with exactly four entries
+     */
+    setRectangle: function Annotation_setRectangle(rectangle) {
+      if (isArray(rectangle) && rectangle.length === 4) {
+        this.rectangle = Util.normalizeRect(rectangle);
+      } else {
+        this.rectangle = [0, 0, 0, 0];
+      }
+    },
 
-    getData: function Annotation_getData() {
-      return this.data;
+    /**
+     * Set the color and take care of color space conversion.
+     *
+     * @public
+     * @memberof Annotation
+     * @param {Array} color - The color array containing either 0
+     *                        (transparent), 1 (grayscale), 3 (RGB) or
+     *                        4 (CMYK) elements
+     */
+    setColor: function Annotation_setColor(color) {
+      var rgbColor = new Uint8Array(3); // Black in RGB color space (default)
+      if (!isArray(color)) {
+        this.color = rgbColor;
+        return;
+      }
+
+      switch (color.length) {
+        case 0: // Transparent, which we indicate with a null value
+          this.color = null;
+          break;
+
+        case 1: // Convert grayscale to RGB
+          ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0);
+          this.color = rgbColor;
+          break;
+
+        case 3: // Convert RGB percentages to RGB
+          ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0);
+          this.color = rgbColor;
+          break;
+
+        case 4: // Convert CMYK to RGB
+          ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0);
+          this.color = rgbColor;
+          break;
+
+        default:
+          this.color = rgbColor;
+          break;
+      }
+    },
+
+    /**
+     * Set the border style (as AnnotationBorderStyle object).
+     *
+     * @public
+     * @memberof Annotation
+     * @param {Dict} borderStyle - The border style dictionary
+     */
+    setBorderStyle: function Annotation_setBorderStyle(borderStyle) {
+      if (!isDict(borderStyle)) {
+        return;
+      }
+      if (borderStyle.has('BS')) {
+        var dict = borderStyle.get('BS');
+        var dictType;
+
+        if (!dict.has('Type') || (isName(dictType = dict.get('Type')) &&
+                                  dictType.name === 'Border')) {
+          this.borderStyle.setWidth(dict.get('W'));
+          this.borderStyle.setStyle(dict.get('S'));
+          this.borderStyle.setDashArray(dict.get('D'));
+        }
+      } else if (borderStyle.has('Border')) {
+        var array = borderStyle.get('Border');
+        if (isArray(array) && array.length >= 3) {
+          this.borderStyle.setHorizontalCornerRadius(array[0]);
+          this.borderStyle.setVerticalCornerRadius(array[1]);
+          this.borderStyle.setWidth(array[2]);
+
+          if (array.length === 4) { // Dash array available
+            this.borderStyle.setDashArray(array[3]);
+          }
+        }
+      } else {
+        // There are no border entries in the dictionary. According to the
+        // specification, we should draw a solid border of width 1 in that
+        // case, but Adobe Reader did not implement that part of the
+        // specification and instead draws no border at all, so we do the same.
+        // See also https://github.com/mozilla/pdf.js/issues/6179.
+        this.borderStyle.setWidth(0);
+      }
     },
 
     isInvisible: function Annotation_isInvisible() {
       var data = this.data;
-      if (data && SUPPORTED_TYPES.indexOf(data.subtype) !== -1) {
-        return false;
-      } else {
-        return !!(data &&
-                  data.annotationFlags &&            // Default: not invisible
-                  data.annotationFlags & 0x1);       // Invisible
-      }
+      return !!(data &&
+                data.annotationFlags &&            // Default: not invisible
+                data.annotationFlags & 0x1);       // Invisible
     },
 
     isViewable: function Annotation_isViewable() {
@@ -5100,70 +5200,6 @@
     }
   };
 
-  Annotation.getConstructor =
-      function Annotation_getConstructor(subtype, fieldType) {
-
-    if (!subtype) {
-      return;
-    }
-
-    // TODO(mack): Implement FreeText annotations
-    if (subtype === 'Link') {
-      return LinkAnnotation;
-    } else if (subtype === 'Text') {
-      return TextAnnotation;
-    } else if (subtype === 'Widget') {
-      if (!fieldType) {
-        return;
-      }
-
-      if (fieldType === 'Tx') {
-        return TextWidgetAnnotation;
-      } else {
-        return WidgetAnnotation;
-      }
-    } else {
-      return Annotation;
-    }
-  };
-
-  Annotation.fromRef = function Annotation_fromRef(xref, ref) {
-
-    var dict = xref.fetchIfRef(ref);
-    if (!isDict(dict)) {
-      return;
-    }
-
-    var subtype = dict.get('Subtype');
-    subtype = isName(subtype) ? subtype.name : '';
-    if (!subtype) {
-      return;
-    }
-
-    var fieldType = Util.getInheritableProperty(dict, 'FT');
-    fieldType = isName(fieldType) ? fieldType.name : '';
-
-    var Constructor = Annotation.getConstructor(subtype, fieldType);
-    if (!Constructor) {
-      return;
-    }
-
-    var params = {
-      dict: dict,
-      ref: ref,
-    };
-
-    var annotation = new Constructor(params);
-
-    if (annotation.isViewable() || annotation.isPrintable()) {
-      return annotation;
-    } else {
-      if (SUPPORTED_TYPES.indexOf(subtype) === -1) {
-        warn('unimplemented annotation type: ' + subtype);
-      }
-    }
-  };
-
   Annotation.appendToOperatorList = function Annotation_appendToOperatorList(
       annotations, opList, pdfManager, partialEvaluator, intent) {
 
@@ -5197,6 +5233,144 @@
   return Annotation;
 })();
 
+/**
+ * Contains all data regarding an annotation's border style.
+ *
+ * @class
+ */
+var AnnotationBorderStyle = (function AnnotationBorderStyleClosure() {
+  /**
+   * @constructor
+   * @private
+   */
+  function AnnotationBorderStyle() {
+    this.width = 1;
+    this.style = AnnotationBorderStyleType.SOLID;
+    this.dashArray = [3];
+    this.horizontalCornerRadius = 0;
+    this.verticalCornerRadius = 0;
+  }
+
+  AnnotationBorderStyle.prototype = {
+    /**
+     * Set the width.
+     *
+     * @public
+     * @memberof AnnotationBorderStyle
+     * @param {integer} width - The width
+     */
+    setWidth: function AnnotationBorderStyle_setWidth(width) {
+      if (width === (width | 0)) {
+        this.width = width;
+      }
+    },
+
+    /**
+     * Set the style.
+     *
+     * @public
+     * @memberof AnnotationBorderStyle
+     * @param {Object} style - The style object
+     * @see {@link shared/util.js}
+     */
+    setStyle: function AnnotationBorderStyle_setStyle(style) {
+      if (!style) {
+        return;
+      }
+      switch (style.name) {
+        case 'S':
+          this.style = AnnotationBorderStyleType.SOLID;
+          break;
+
+        case 'D':
+          this.style = AnnotationBorderStyleType.DASHED;
+          break;
+
+        case 'B':
+          this.style = AnnotationBorderStyleType.BEVELED;
+          break;
+
+        case 'I':
+          this.style = AnnotationBorderStyleType.INSET;
+          break;
+
+        case 'U':
+          this.style = AnnotationBorderStyleType.UNDERLINE;
+          break;
+
+        default:
+          break;
+      }
+    },
+
+    /**
+     * Set the dash array.
+     *
+     * @public
+     * @memberof AnnotationBorderStyle
+     * @param {Array} dashArray - The dash array with at least one element
+     */
+    setDashArray: function AnnotationBorderStyle_setDashArray(dashArray) {
+      // We validate the dash array, but we do not use it because CSS does not
+      // allow us to change spacing of dashes. For more information, visit
+      // http://www.w3.org/TR/css3-background/#the-border-style.
+      if (isArray(dashArray) && dashArray.length > 0) {
+        // According to the PDF specification: the elements in a dashArray
+        // shall be numbers that are nonnegative and not all equal to zero.
+        var isValid = true;
+        var allZeros = true;
+        for (var i = 0, len = dashArray.length; i < len; i++) {
+          var element = dashArray[i];
+          var validNumber = (+element >= 0);
+          if (!validNumber) {
+            isValid = false;
+            break;
+          } else if (element > 0) {
+            allZeros = false;
+          }
+        }
+        if (isValid && !allZeros) {
+          this.dashArray = dashArray;
+        } else {
+          this.width = 0; // Adobe behavior when the array is invalid.
+        }
+      } else if (dashArray) {
+        this.width = 0; // Adobe behavior when the array is invalid.
+      }
+    },
+
+    /**
+     * Set the horizontal corner radius (from a Border dictionary).
+     *
+     * @public
+     * @memberof AnnotationBorderStyle
+     * @param {integer} radius - The horizontal corner radius
+     */
+    setHorizontalCornerRadius:
+        function AnnotationBorderStyle_setHorizontalCornerRadius(radius) {
+      if (radius === (radius | 0)) {
+        this.horizontalCornerRadius = radius;
+      }
+    },
+
+    /**
+     * Set the vertical corner radius (from a Border dictionary).
+     *
+     * @public
+     * @memberof AnnotationBorderStyle
+     * @param {integer} radius - The vertical corner radius
+     */
+    setVerticalCornerRadius:
+        function AnnotationBorderStyle_setVerticalCornerRadius(radius) {
+      if (radius === (radius | 0)) {
+        this.verticalCornerRadius = radius;
+      }
+    }
+  };
+
+  return AnnotationBorderStyle;
+})();
+
 var WidgetAnnotation = (function WidgetAnnotationClosure() {
 
   function WidgetAnnotation(params) {
@@ -5297,21 +5471,9 @@
   return TextWidgetAnnotation;
 })();
 
-var InteractiveAnnotation = (function InteractiveAnnotationClosure() {
-  function InteractiveAnnotation(params) {
-    Annotation.call(this, params);
-
-    this.data.hasHtml = true;
-  }
-
-  Util.inherit(InteractiveAnnotation, Annotation, { });
-
-  return InteractiveAnnotation;
-})();
-
 var TextAnnotation = (function TextAnnotationClosure() {
   function TextAnnotation(params) {
-    InteractiveAnnotation.call(this, params);
+    Annotation.call(this, params);
 
     var dict = params.dict;
     var data = this.data;
@@ -5321,6 +5483,7 @@
     data.annotationType = AnnotationType.TEXT;
     data.content = stringToPDFString(content || '');
     data.title = stringToPDFString(title || '');
+    data.hasHtml = true;
 
     if (data.hasAppearance) {
       data.name = 'NoIcon';
@@ -5335,18 +5498,19 @@
     }
   }
 
-  Util.inherit(TextAnnotation, InteractiveAnnotation, { });
+  Util.inherit(TextAnnotation, Annotation, { });
 
   return TextAnnotation;
 })();
 
 var LinkAnnotation = (function LinkAnnotationClosure() {
   function LinkAnnotation(params) {
-    InteractiveAnnotation.call(this, params);
+    Annotation.call(this, params);
 
     var dict = params.dict;
     var data = this.data;
     data.annotationType = AnnotationType.LINK;
+    data.hasHtml = true;
 
     var action = dict.get('A');
     if (action && isDict(action)) {
@@ -5364,7 +5528,15 @@
         if (!isValidUrl(url, false)) {
           url = '';
         }
-        data.url = url;
+        // According to ISO 32000-1:2008, section 12.6.4.7, 
+        // URI should to be encoded in 7-bit ASCII.
+        // Some bad PDFs may have URIs in UTF-8 encoding, see Bugzilla 1122280.
+        try {
+          data.url = stringToUTF8String(url);
+        } catch (e) {
+          // Fall back to a simple copy.
+          data.url = url;
+        }
       } else if (linkType === 'GoTo') {
         data.dest = action.get('D');
       } else if (linkType === 'GoToR') {
@@ -5402,7 +5574,7 @@
     return url;
   }
 
-  Util.inherit(LinkAnnotation, InteractiveAnnotation, { });
+  Util.inherit(LinkAnnotation, Annotation, { });
 
   return LinkAnnotation;
 })();
@@ -5753,7 +5925,10 @@
         var rmin = encode[2 * i];
         var rmax = encode[2 * i + 1];
 
-        tmpBuf[0] = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin);
+        // Prevent the value from becoming NaN as a result
+        // of division by zero (fixes issue6113.pdf).
+        tmpBuf[0] = dmin === dmax ? rmin :
+                    rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin);
 
         // call the appropriate function
         fns[i](tmpBuf, 0, dest, destOffset);
@@ -6762,9 +6937,9 @@
           error('unrecognized colorspace ' + mode);
       }
     } else if (isArray(cs)) {
-      mode = cs[0].name;
+      mode = xref.fetchIfRef(cs[0]).name;
       this.mode = mode;
-      var numComps, params;
+      var numComps, params, alt;
 
       switch (mode) {
         case 'DeviceGray':
@@ -6786,6 +6961,17 @@
           var stream = xref.fetchIfRef(cs[1]);
           var dict = stream.dict;
           numComps = dict.get('N');
+          alt = dict.get('Alternate');
+          if (alt) {
+            var altIR = ColorSpace.parseToIR(alt, xref, res);
+            // Parse the /Alternate CS to ensure that the number of components
+            // are correct, and also (indirectly) that it is not a PatternCS.
+            var altCS = ColorSpace.fromIR(altIR);
+            if (altCS.numComps === numComps) {
+              return altIR;
+            }
+            warn('ICCBased color space: Ignoring incorrect /Alternate entry.');
+          }
           if (numComps === 1) {
             return 'DeviceGrayCS';
           } else if (numComps === 3) {
@@ -6795,7 +6981,7 @@
           }
           break;
         case 'Pattern':
-          var basePatternCS = cs[1];
+          var basePatternCS = cs[1] || null;
           if (basePatternCS) {
             basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res);
           }
@@ -6803,7 +6989,7 @@
         case 'Indexed':
         case 'I':
           var baseIndexedCS = ColorSpace.parseToIR(cs[1], xref, res);
-          var hiVal = cs[2] + 1;
+          var hiVal = xref.fetchIfRef(cs[2]) + 1;
           var lookup = xref.fetchIfRef(cs[3]);
           if (isStream(lookup)) {
             lookup = lookup.getBytes();
@@ -6811,18 +6997,18 @@
           return ['IndexedCS', baseIndexedCS, hiVal, lookup];
         case 'Separation':
         case 'DeviceN':
-          var name = cs[1];
+          var name = xref.fetchIfRef(cs[1]);
           numComps = 1;
           if (isName(name)) {
             numComps = 1;
           } else if (isArray(name)) {
             numComps = name.length;
           }
-          var alt = ColorSpace.parseToIR(cs[2], xref, res);
+          alt = ColorSpace.parseToIR(cs[2], xref, res);
           var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3]));
           return ['AlternateCS', numComps, alt, tintFnIR];
         case 'Lab':
-          params = cs[1].getAll();
+          params = xref.fetchIfRef(cs[1]).getAll();
           return ['LabCS', params];
         default:
           error('unimplemented color space object "' + mode + '"');
@@ -6842,7 +7028,7 @@
    * @param {Number} n Number of components the color space has.
    */
   ColorSpace.isDefaultDecode = function ColorSpace_isDefaultDecode(decode, n) {
-    if (!decode) {
+    if (!isArray(decode)) {
       return true;
     }
 
@@ -9647,6 +9833,14 @@
     var fileIdBytes = stringToBytes(fileId);
     var passwordBytes;
     if (password) {
+      if (revision === 6) {
+        try {
+          password = utf8StringToString(password);
+        } catch (ex) {
+          warn('CipherTransformFactory: ' +
+               'Unable to convert UTF8 encoded password.');
+        }
+      }
       passwordBytes = stringToBytes(password);
     }
 
@@ -9751,7 +9945,7 @@
 
   CipherTransformFactory.prototype = {
     createCipherTransform:
-      function CipherTransformFactory_createCipherTransform(num, gen) {
+        function CipherTransformFactory_createCipherTransform(num, gen) {
       if (this.algorithm === 4 || this.algorithm === 5) {
         return new CipherTransform(
           buildCipherConstructor(this.cf, this.stmf,
@@ -9772,7 +9966,7 @@
 })();
 
 
-var PatternType = {
+var ShadingType = {
   FUNCTION_BASED: 1,
   AXIAL: 2,
   RADIAL: 3,
@@ -9804,17 +9998,17 @@
 
     try {
       switch (type) {
-        case PatternType.AXIAL:
-        case PatternType.RADIAL:
+        case ShadingType.AXIAL:
+        case ShadingType.RADIAL:
           // Both radial and axial shadings are handled by RadialAxial shading.
           return new Shadings.RadialAxial(dict, matrix, xref, res);
-        case PatternType.FREE_FORM_MESH:
-        case PatternType.LATTICE_FORM_MESH:
-        case PatternType.COONS_PATCH_MESH:
-        case PatternType.TENSOR_PATCH_MESH:
+        case ShadingType.FREE_FORM_MESH:
+        case ShadingType.LATTICE_FORM_MESH:
+        case ShadingType.COONS_PATCH_MESH:
+        case ShadingType.TENSOR_PATCH_MESH:
           return new Shadings.Mesh(shading, matrix, xref, res);
         default:
-          throw new Error('Unknown PatternType: ' + type);
+          throw new Error('Unsupported ShadingType: ' + type);
       }
     } catch (ex) {
       if (ex instanceof MissingDataException) {
@@ -9862,7 +10056,7 @@
       extendEnd = extendArr[1];
     }
 
-    if (this.shadingType === PatternType.RADIAL &&
+    if (this.shadingType === ShadingType.RADIAL &&
        (!extendStart || !extendEnd)) {
       // Radial gradient only currently works if either circle is fully within
       // the other circle.
@@ -9937,13 +10131,13 @@
       var coordsArr = this.coordsArr;
       var shadingType = this.shadingType;
       var type, p0, p1, r0, r1;
-      if (shadingType === PatternType.AXIAL) {
+      if (shadingType === ShadingType.AXIAL) {
         p0 = [coordsArr[0], coordsArr[1]];
         p1 = [coordsArr[2], coordsArr[3]];
         r0 = null;
         r1 = null;
         type = 'axial';
-      } else if (shadingType === PatternType.RADIAL) {
+      } else if (shadingType === ShadingType.RADIAL) {
         p0 = [coordsArr[0], coordsArr[1]];
         p1 = [coordsArr[3], coordsArr[4]];
         r0 = coordsArr[2];
@@ -9977,7 +10171,7 @@
 
     var numComps = context.numComps;
     this.tmpCompsBuf = new Float32Array(numComps);
-    var csNumComps = context.colorSpace;
+    var csNumComps = context.colorSpace.numComps;
     this.tmpCsCompsBuf = context.colorFn ? new Float32Array(csNumComps) :
                                            this.tmpCompsBuf;
   }
@@ -10097,13 +10291,10 @@
 
       reader.align();
     }
-
-    var psPacked = new Int32Array(ps);
-
     mesh.figures.push({
       type: 'triangles',
-      coords: psPacked,
-      colors: psPacked
+      coords: new Int32Array(ps),
+      colors: new Int32Array(ps),
     });
   }
 
@@ -10118,13 +10309,10 @@
       coords.push(coord);
       colors.push(color);
     }
-
-    var psPacked = new Int32Array(ps);
-
     mesh.figures.push({
       type: 'lattice',
-      coords: psPacked,
-      colors: psPacked,
+      coords: new Int32Array(ps),
+      colors: new Int32Array(ps),
       verticesPerRow: verticesPerRow
     });
   }
@@ -10266,29 +10454,32 @@
           break;
         case 1:
           tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15];
-          ps[12] = pi + 5; ps[13] = pi + 4;  ps[14] = pi + 3;  ps[15] = pi + 2;
-          ps[ 8] = pi + 6; /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 1;
-          ps[ 4] = pi + 7; /* calculated below              */ ps[ 7] = pi;
-          ps[ 0] = tmp1;   ps[ 1] = tmp2;    ps[ 2] = tmp3;    ps[ 3] = tmp4;
+          ps[12] = tmp4; ps[13] = pi + 0;  ps[14] = pi + 1;  ps[15] = pi + 2;
+          ps[ 8] = tmp3; /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 3;
+          ps[ 4] = tmp2; /* calculated below              */ ps[ 7] = pi + 4;
+          ps[ 0] = tmp1; ps[ 1] = pi + 7;   ps[ 2] = pi + 6; ps[ 3] = pi + 5;
           tmp1 = cs[2]; tmp2 = cs[3];
-          cs[2] = ci + 1; cs[3] = ci;
-          cs[0] = tmp1;   cs[1] = tmp2;
+          cs[2] = tmp2;   cs[3] = ci;
+          cs[0] = tmp1;   cs[1] = ci + 1;
           break;
         case 2:
-          ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6;   ps[15] = pi + 5;
-          ps[ 8] = ps[11]; /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 4;
-          ps[ 4] = ps[7];  /* calculated below              */ ps[ 7] = pi + 3;
-          ps[ 0] = ps[3];  ps[ 1] = pi;     ps[ 2] = pi + 1;   ps[ 3] = pi + 2;
-          cs[2] = cs[3]; cs[3] = ci + 1;
-          cs[0] = cs[1]; cs[1] = ci;
+          tmp1 = ps[15];
+          tmp2 = ps[11];
+          ps[12] = ps[3];  ps[13] = pi + 0; ps[14] = pi + 1;   ps[15] = pi + 2;
+          ps[ 8] = ps[7];  /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 3;
+          ps[ 4] = tmp2;   /* calculated below              */ ps[ 7] = pi + 4;
+          ps[ 0] = tmp1;  ps[ 1] = pi + 7;   ps[ 2] = pi + 6;  ps[ 3] = pi + 5;
+          tmp1 = cs[3];
+          cs[2] = cs[1]; cs[3] = ci;
+          cs[0] = tmp1;  cs[1] = ci + 1;
           break;
         case 3:
-          ps[12] = ps[0];  ps[13] = ps[1];   ps[14] = ps[2];   ps[15] = ps[3];
-          ps[ 8] = pi;     /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 7;
-          ps[ 4] = pi + 1; /* calculated below              */ ps[ 7] = pi + 6;
-          ps[ 0] = pi + 2; ps[ 1] = pi + 3;  ps[ 2] = pi + 4;  ps[ 3] = pi + 5;
-          cs[2] = cs[0]; cs[3] = cs[1];
-          cs[0] = ci;    cs[1] = ci + 1;
+          ps[12] = ps[0];  ps[13] = pi + 0;   ps[14] = pi + 1; ps[15] = pi + 2;
+          ps[ 8] = ps[1];  /* values for 5, 6, 9, 10 are    */ ps[11] = pi + 3;
+          ps[ 4] = ps[2];  /* calculated below              */ ps[ 7] = pi + 4;
+          ps[ 0] = ps[3];  ps[ 1] = pi + 7;   ps[ 2] = pi + 6; ps[ 3] = pi + 5;
+          cs[2] = cs[0]; cs[3] = ci;
+          cs[0] = cs[1]; cs[1] = ci + 1;
           break;
       }
       // set p11, p12, p21, p22
@@ -10373,29 +10564,32 @@
           break;
         case 1:
           tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15];
-          ps[12] = pi + 5; ps[13] = pi + 4;  ps[14] = pi + 3;  ps[15] = pi + 2;
-          ps[ 8] = pi + 6; ps[ 9] = pi + 11; ps[10] = pi + 10; ps[11] = pi + 1;
-          ps[ 4] = pi + 7; ps[ 5] = pi + 8;  ps[ 6] = pi + 9;  ps[ 7] = pi;
-          ps[ 0] = tmp1;   ps[ 1] = tmp2;    ps[ 2] = tmp3;    ps[ 3] = tmp4;
+          ps[12] = tmp4;   ps[13] = pi + 0;  ps[14] = pi + 1;  ps[15] = pi + 2;
+          ps[ 8] = tmp3;   ps[ 9] = pi + 9;  ps[10] = pi + 10; ps[11] = pi + 3;
+          ps[ 4] = tmp2;   ps[ 5] = pi + 8;  ps[ 6] = pi + 11; ps[ 7] = pi + 4;
+          ps[ 0] = tmp1;   ps[ 1] = pi + 7;  ps[ 2] = pi + 6;  ps[ 3] = pi + 5;
           tmp1 = cs[2]; tmp2 = cs[3];
-          cs[2] = ci + 1; cs[3] = ci;
-          cs[0] = tmp1;   cs[1] = tmp2;
+          cs[2] = tmp2;   cs[3] = ci;
+          cs[0] = tmp1;   cs[1] = ci + 1;
           break;
         case 2:
-          ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6;  ps[15] = pi + 5;
-          ps[ 8] = ps[11]; ps[ 9] = pi + 8; ps[10] = pi + 11; ps[11] = pi + 4;
-          ps[ 4] = ps[7];  ps[ 5] = pi + 9; ps[ 6] = pi + 10; ps[ 7] = pi + 3;
-          ps[ 0] = ps[3];  ps[ 1] = pi;     ps[ 2] = pi + 1;  ps[ 3] = pi + 2;
-          cs[2] = cs[3]; cs[3] = ci + 1;
-          cs[0] = cs[1]; cs[1] = ci;
+          tmp1 = ps[15];
+          tmp2 = ps[11];
+          ps[12] = ps[3]; ps[13] = pi + 0; ps[14] = pi + 1;  ps[15] = pi + 2;
+          ps[ 8] = ps[7]; ps[ 9] = pi + 9; ps[10] = pi + 10; ps[11] = pi + 3;
+          ps[ 4] = tmp2;  ps[ 5] = pi + 8; ps[ 6] = pi + 11; ps[ 7] = pi + 4;
+          ps[ 0] = tmp1;  ps[ 1] = pi + 7; ps[ 2] = pi + 6;  ps[ 3] = pi + 5;
+          tmp1 = cs[3];
+          cs[2] = cs[1]; cs[3] = ci;
+          cs[0] = tmp1;  cs[1] = ci + 1;
           break;
         case 3:
-          ps[12] = ps[0];  ps[13] = ps[1];   ps[14] = ps[2];   ps[15] = ps[3];
-          ps[ 8] = pi;     ps[ 9] = pi + 9;  ps[10] = pi + 8;  ps[11] = pi + 7;
-          ps[ 4] = pi + 1; ps[ 5] = pi + 10; ps[ 6] = pi + 11; ps[ 7] = pi + 6;
-          ps[ 0] = pi + 2; ps[ 1] = pi + 3;  ps[ 2] = pi + 4;  ps[ 3] = pi + 5;
-          cs[2] = cs[0]; cs[3] = cs[1];
-          cs[0] = ci;    cs[1] = ci + 1;
+          ps[12] = ps[0];  ps[13] = pi + 0;  ps[14] = pi + 1;  ps[15] = pi + 2;
+          ps[ 8] = ps[1];  ps[ 9] = pi + 9;  ps[10] = pi + 10; ps[11] = pi + 3;
+          ps[ 4] = ps[2];  ps[ 5] = pi + 8;  ps[ 6] = pi + 11; ps[ 7] = pi + 4;
+          ps[ 0] = ps[3];  ps[ 1] = pi + 7;  ps[ 2] = pi + 6;  ps[ 3] = pi + 5;
+          cs[2] = cs[0]; cs[3] = ci;
+          cs[0] = cs[1]; cs[1] = ci + 1;
           break;
       }
       mesh.figures.push({
@@ -10484,19 +10678,19 @@
 
     var patchMesh = false;
     switch (this.shadingType) {
-      case PatternType.FREE_FORM_MESH:
+      case ShadingType.FREE_FORM_MESH:
         decodeType4Shading(this, reader);
         break;
-      case PatternType.LATTICE_FORM_MESH:
+      case ShadingType.LATTICE_FORM_MESH:
         var verticesPerRow = dict.get('VerticesPerRow') | 0;
         assert(verticesPerRow >= 2, 'Invalid VerticesPerRow');
         decodeType5Shading(this, reader, verticesPerRow);
         break;
-      case PatternType.COONS_PATCH_MESH:
+      case ShadingType.COONS_PATCH_MESH:
         decodeType6Shading(this, reader);
         patchMesh = true;
         break;
-      case PatternType.TENSOR_PATCH_MESH:
+      case ShadingType.TENSOR_PATCH_MESH:
         decodeType7Shading(this, reader);
         patchMesh = true;
         break;
@@ -11187,6 +11381,10 @@
               }
               // eagerly compile XForm objects
               var name = args[0].name;
+              if (!name) {
+                warn('XObject must be referred to by name.');
+                continue;
+              }
               if (imageCache[name] !== undefined) {
                 operatorList.addOp(imageCache[name].fn, imageCache[name].args);
                 args = null;
@@ -11485,6 +11683,17 @@
           var tsm = [textState.fontSize * textState.textHScale, 0,
                      0, textState.fontSize,
                      0, textState.textRise];
+
+          if (font.isType3Font &&
+              textState.fontMatrix !== FONT_IDENTITY_MATRIX &&
+              textState.fontSize === 1) {
+            var glyphHeight = font.bbox[3] - font.bbox[1];
+            if (glyphHeight > 0) {
+              glyphHeight = glyphHeight * textState.fontMatrix[3];
+              tsm[3] *= glyphHeight;
+            }
+          }
+
           var trm = textChunk.transform = Util.transform(textState.ctm,
                                     Util.transform(textState.textMatrix, tsm));
           if (!font.vertical) {
@@ -11538,16 +11747,23 @@
           // var x = pt[0];
           // var y = pt[1];
 
+          var charSpacing = 0;
+          if (textChunk.str.length > 0) {
+            // Apply char spacing only when there are chars.
+            // As a result there is only spacing between glyphs.
+            charSpacing = textState.charSpacing;
+          }
+
           var tx = 0;
           var ty = 0;
           if (!font.vertical) {
             var w0 = glyphWidth * textState.fontMatrix[0];
-            tx = (w0 * textState.fontSize + textState.charSpacing) *
+            tx = (w0 * textState.fontSize + charSpacing) *
                  textState.textHScale;
             width += tx;
           } else {
             var w1 = glyphWidth * textState.fontMatrix[0];
-            ty = w1 * textState.fontSize + textState.charSpacing;
+            ty = w1 * textState.fontSize + charSpacing;
             height += ty;
           }
           textState.translateTextMatrix(tx, ty);
@@ -11813,8 +12029,13 @@
               var data = diffEncoding[j];
               if (isNum(data)) {
                 index = data;
-              } else {
+              } else if (isName(data)) {
                 differences[index++] = data.name;
+              } else if (isRef(data)) {
+                diffEncoding[j--] = xref.fetch(data);
+                continue;
+              } else {
+                error('Invalid entry in \'Differences\' array: ' + data);
               }
             }
           }
@@ -12163,6 +12384,7 @@
           // is a tagged pdf. Create a barbebones one to get by.
           descriptor = new Dict(null);
           descriptor.set('FontName', Name.get(type));
+          descriptor.set('FontBBox', dict.get('FontBBox'));
         } else {
           // Before PDF 1.5 if the font was one of the base 14 fonts, having a
           // FontDescriptor was not required.
@@ -14652,6 +14874,13 @@
   '3316': 578, '3379': 42785, '3393': 1159, '3416': 8377
 };
 
+// The glyph map for ArialBlack differs slightly from the glyph map used for
+// other well-known standard fonts. Hence we use this (incomplete) CID to GID
+// mapping to adjust the glyph map for non-embedded ArialBlack fonts.
+var SupplementalGlyphMapForArialBlack = {
+  '227': 322, '264': 261, '291': 346,
+};
+
 // Some characters, e.g. copyrightserif, are mapped to the private use area and
 // might not be displayed using standard fonts. Mapping/hacking well-known chars
 // to the similar equivalents in the normal characters range.
@@ -16540,6 +16769,32 @@
   return OpenTypeFileBuilder;
 })();
 
+// Problematic Unicode characters in the fonts that needs to be moved to avoid
+// issues when they are painted on the canvas, e.g. complex-script shaping or
+// control/whitespace characters. The ranges are listed in pairs: the first item
+// is a code of the first problematic code, the second one is the next
+// non-problematic code. The ranges must be in sorted order.
+var ProblematicCharRanges = new Int32Array([
+  // Control characters.
+  0x0000, 0x0020,
+  0x007F, 0x00A1,
+  0x00AD, 0x00AE,
+  // Chars that is used in complex-script shaping.
+  0x0600, 0x0780,
+  0x08A0, 0x10A0,
+  0x1780, 0x1800,
+  // General punctuation chars.
+  0x2000, 0x2010,
+  0x2011, 0x2012,
+  0x2028, 0x2030,
+  0x205F, 0x2070,
+  0x25CC, 0x25CD,
+  // Chars that is used in complex-script shaping.
+  0xAA60, 0xAA80,
+  // Specials Unicode block.
+  0xFFF0, 0x10000
+]);
+
 /**
  * 'Font' is the class the outside world should use, it encapsulate all the font
  * decoding logics whatever type it is (assuming the font type is supported).
@@ -16582,6 +16837,7 @@
     this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
     this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
     this.fontMatrix = properties.fontMatrix;
+    this.bbox = properties.bbox;
 
     this.toUnicode = properties.toUnicode = this.buildToUnicode(properties);
 
@@ -16633,8 +16889,13 @@
         // Standard fonts might be embedded as CID font without glyph mapping.
         // Building one based on GlyphMapForStandardFonts.
         var map = [];
-        for (var code in GlyphMapForStandardFonts) {
-          map[+code] = GlyphMapForStandardFonts[code];
+        for (charCode in GlyphMapForStandardFonts) {
+          map[+charCode] = GlyphMapForStandardFonts[charCode];
+        }
+        if (/ArialBlack/i.test(name)) {
+          for (charCode in SupplementalGlyphMapForArialBlack) {
+            map[+charCode] = SupplementalGlyphMapForArialBlack[charCode];
+          }
         }
         var isIdentityUnicode = this.toUnicode instanceof IdentityToUnicodeMap;
         if (!isIdentityUnicode) {
@@ -16822,31 +17083,18 @@
    * @return {boolean}
    */
   function isProblematicUnicodeLocation(code) {
-    if (code <= 0x1F) { // Control chars
-      return true;
+    // Using binary search to find a range start.
+    var i = 0, j = ProblematicCharRanges.length - 1;
+    while (i < j) {
+      var c = (i + j + 1) >> 1;
+      if (code < ProblematicCharRanges[c]) {
+        j = c - 1;
+      } else {
+        i = c;
+      }
     }
-    if (code >= 0x80 && code <= 0x9F) { // Control chars
-      return true;
-    }
-    if ((code >= 0x2000 && code <= 0x200F) || // General punctuation chars
-        (code >= 0x2028 && code <= 0x202F) ||
-        (code >= 0x2060 && code <= 0x206F)) {
-      return true;
-    }
-    if (code >= 0xFFF0 && code <= 0xFFFF) { // Specials Unicode block
-      return true;
-    }
-    switch (code) {
-      case 0x7F: // Control char
-      case 0xA0: // Non breaking space
-      case 0xAD: // Soft hyphen
-      case 0x0E33: // Thai character SARA AM
-      case 0x2011: // Non breaking hyphen
-      case 0x205F: // Medium mathematical space
-      case 0x25CC: // Dotted circle (combining mark)
-        return true;
-    }
-    return false;
+    // Even index means code in problematic range.
+    return !(i & 1);
   }
 
   /**
@@ -17393,7 +17641,10 @@
           }
         }
 
-        if (!potentialTable) {
+        if (potentialTable) {
+          font.pos = start + potentialTable.offset;
+        }
+        if (!potentialTable || font.peekByte() === -1) {
           warn('Could not find a preferred cmap table.');
           return {
             platformId: -1,
@@ -17403,7 +17654,6 @@
           };
         }
 
-        font.pos = start + potentialTable.offset;
         var format = font.getUint16();
         var length = font.getUint16();
         var language = font.getUint16();
@@ -17838,6 +18088,9 @@
           default:
             warn('Unknown/unsupported post table version ' + version);
             valid = false;
+            if (properties.defaultEncoding) {
+              glyphNames = properties.defaultEncoding;
+            }
             break;
         }
         properties.glyphNames = glyphNames;
@@ -18166,7 +18419,7 @@
       var isTrueType = !tables['CFF '];
       if (!isTrueType) {
         // OpenType font
-        if (header.version === 'OTTO' ||
+        if ((header.version === 'OTTO' && properties.type !== 'CIDFontType2') ||
             !tables.head || !tables.hhea || !tables.maxp || !tables.post) {
           // no major tables: throwing everything at CFFFont
           cffFile = new Stream(tables['CFF '].data);
@@ -18264,13 +18517,22 @@
         }
       }
 
-      var charCodeToGlyphId = [], charCode, toUnicode = properties.toUnicode;
+      var charCodeToGlyphId = [], charCode;
+      var toUnicode = properties.toUnicode, widths = properties.widths;
+      var skipToUnicode = (toUnicode instanceof IdentityToUnicodeMap ||
+                           toUnicode.length === 0x10000);
 
-      function hasGlyph(glyphId, charCode) {
+      // Helper function to try to skip mapping of empty glyphs.
+      // Note: In some cases, just relying on the glyph data doesn't work,
+      //       hence we also use a few heuristics to fix various PDF files.
+      function hasGlyph(glyphId, charCode, widthCode) {
         if (!missingGlyphs[glyphId]) {
           return true;
         }
-        if (charCode >= 0 && toUnicode.has(charCode)) {
+        if (!skipToUnicode && charCode >= 0 && toUnicode.has(charCode)) {
+          return true;
+        }
+        if (widths && widthCode >= 0 && isNum(widths[widthCode])) {
           return true;
         }
         return false;
@@ -18290,7 +18552,7 @@
           }
 
           if (glyphId >= 0 && glyphId < numGlyphs &&
-              hasGlyph(glyphId, charCode)) {
+              hasGlyph(glyphId, charCode, cid)) {
             charCodeToGlyphId[charCode] = glyphId;
           }
         });
@@ -18351,18 +18613,19 @@
             var found = false;
             for (i = 0; i < cmapMappingsLength; ++i) {
               if (cmapMappings[i].charCode === unicodeOrCharCode &&
-                  hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode)) {
+                  hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode, -1)) {
                 charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
                 found = true;
                 break;
               }
             }
             if (!found && properties.glyphNames) {
-              // Try to map using the post table. There are currently no known
-              // pdfs that this fixes.
+              // Try to map using the post table.
               var glyphId = properties.glyphNames.indexOf(glyphName);
-              if (glyphId > 0 && hasGlyph(glyphId, -1)) {
+              if (glyphId > 0 && hasGlyph(glyphId, -1, -1)) {
                 charCodeToGlyphId[charCode] = glyphId;
+              } else {
+                charCodeToGlyphId[charCode] = 0; // notdef
               }
             }
           }
@@ -30173,6 +30436,16 @@
         this.buf2 = this.lexer.getObj();
       }
     },
+    tryShift: function Parser_tryShift() {
+      try {
+        this.shift();
+        return true;
+      } catch (e) {
+        // Upon failure, the caller should reset this.lexer.pos to a known good
+        // state and call this.shift() twice to reset the buffers.
+        return false;
+      }
+    },
     getObj: function Parser_getObj(cipherTransform) {
       var buf1 = this.buf1;
       this.shift();
@@ -30546,9 +30819,10 @@
       stream.pos = pos + length;
       lexer.nextChar();
 
-      this.shift(); // '>>'
-      this.shift(); // 'stream'
-      if (!isCmd(this.buf1, 'endstream')) {
+      // Shift '>>' and check whether the new object marks the end of the stream
+      if (this.tryShift() && isCmd(this.buf2, 'endstream')) {
+        this.shift(); // 'stream'
+      } else {
         // bad stream length, scanning for endstream
         stream.pos = pos;
         var SCAN_BLOCK_SIZE = 2048;
@@ -30780,6 +31054,11 @@
       if (ch === 0x2D) { // '-'
         sign = -1;
         ch = this.nextChar();
+
+        if (ch === 0x2D) { // '-'
+          // Ignore double negative (this is consistent with Adobe Reader).
+          ch = this.nextChar();
+        }
       } else if (ch === 0x2B) { // '+'
         ch = this.nextChar();
       }
@@ -30958,9 +31237,8 @@
           strBuf.push(String.fromCharCode(ch));
         }
       }
-      if (strBuf.length > 128) {
-        error('Warning: name token is longer than allowed by the spec: ' +
-              strBuf.length);
+      if (strBuf.length > 127) {
+        warn('name token is longer than allowed by the spec: ' + strBuf.length);
       }
       return Name.get(strBuf.join(''));
     },
@@ -32295,7 +32573,8 @@
   JpegStream.prototype.isNativelySupported =
       function JpegStream_isNativelySupported(xref, res) {
     var cs = ColorSpace.parse(this.dict.get('ColorSpace', 'CS'), xref, res);
-    return cs.name === 'DeviceGray' || cs.name === 'DeviceRGB';
+    return (cs.name === 'DeviceGray' || cs.name === 'DeviceRGB') &&
+           cs.isDefaultDecode(this.dict.get('Decode', 'D'));
   };
   /**
    * Checks if the image can be decoded by the browser.
@@ -32303,8 +32582,8 @@
   JpegStream.prototype.isNativelyDecodable =
       function JpegStream_isNativelyDecodable(xref, res) {
     var cs = ColorSpace.parse(this.dict.get('ColorSpace', 'CS'), xref, res);
-    var numComps = cs.numComps;
-    return numComps === 1 || numComps === 3;
+    return (cs.numComps === 1 || cs.numComps === 3) &&
+           cs.isDefaultDecode(this.dict.get('Decode', 'D'));
   };
 
   return JpegStream;
@@ -39143,30 +39422,6 @@
   return bidi;
 })();
 
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-
-/* Copyright 2014 Opera Software ASA
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- *
- * Based on https://code.google.com/p/smhasher/wiki/MurmurHash3.
- * Hashes roughly 100 KB per millisecond on i7 3.4 GHz.
- */
-/* globals Uint32ArrayView */
-
-'use strict';
 
 var MurmurHash3_64 = (function MurmurHash3_64Closure (seed) {
   // Workaround for missing math precison in JS.