add pdf media type
authorAlon Levy <alon@pobox.com>
Wed, 27 Mar 2013 10:21:10 +0000 (12:21 +0200)
committerAlon Levy <alon@pobox.com>
Mon, 15 Apr 2013 06:51:21 +0000 (09:51 +0300)
The new media type supports pdf and a subset of media recognized by libreoffice via
unoconv.

Every document added goes through:
* conversion to pdf with unoconv if not already a pdf
* creation of thumbnail and medium sized image, and pdfinfo generates
 some information (even for unoconv produces docs - should fix this)

Poppler (pdftocairo, pdfinfo) is used.  http://poppler.freedesktop.org/

A working but uglified pdf.js integration exists, which is enabled by
setting pdf.pdf_js=true mediagoblin_local.ini (disabled in mediagoblin.ini)

Adds one test to the test_submission test suite, and another separate test_pdf suite.
The tests are only run if media_types.pdf.processing.check_prerequisites passes, so
the test suite will not require any extra package.

TODO: make test suite say 'skipped' in that case instead of just 'ok'

Signed-off-by: Alon Levy <alon@pobox.com>
17 files changed:
.gitmodules [new file with mode: 0644]
docs/source/siteadmin/media-types.rst
extlib/pdf.js [new submodule]
mediagoblin/config_spec.ini
mediagoblin/media_types/pdf/__init__.py [new file with mode: 0644]
mediagoblin/media_types/pdf/migrations.py [new file with mode: 0644]
mediagoblin/media_types/pdf/models.py [new file with mode: 0644]
mediagoblin/media_types/pdf/processing.py [new file with mode: 0644]
mediagoblin/static/css/pdf_viewer.css [new file with mode: 0644]
mediagoblin/static/extlib/pdf.js [new symlink]
mediagoblin/static/js/pdf_viewer.js [new file with mode: 0644]
mediagoblin/templates/mediagoblin/base.html
mediagoblin/templates/mediagoblin/media_displays/pdf.html [new file with mode: 0644]
mediagoblin/tests/test_mgoblin_app.ini
mediagoblin/tests/test_pdf.py [new file with mode: 0644]
mediagoblin/tests/test_submission.py
mediagoblin/tests/test_submission/good.pdf [new file with mode: 0644]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..e6a7464
--- /dev/null
@@ -0,0 +1,6 @@
+[submodule "pdf.js"]
+       path = pdf.js
+       url = git://github.com/mozilla/pdf.js.git
+[submodule "extlib/pdf.js"]
+       path = extlib/pdf.js
+       url = git://github.com/mozilla/pdf.js.git
index 23d3f3b922a18f5056596a59e7381c81d32bf7c1..264dc4fca21256e08f7d598ba751668e27c5495c 100644 (file)
@@ -195,3 +195,40 @@ Run
 
 You should now be able to upload .obj and .stl files and MediaGoblin
 will be able to present them to your wide audience of admirers!
+
+PDF and Document
+================
+
+To enable the "PDF and Document" support plugin, you need pdftocairo, pdfinfo,
+unoconv with headless support.  All executables must be on your execution path.
+
+To install this on Fedora:
+
+.. code-block:: bash
+
+    sudo yum install -y ppoppler-utils unoconv libreoffice-headless
+
+pdf.js relies on git submodules, so be sure you have fetched them:
+
+.. code-block:: bash
+
+    git submodule init
+    git submodule update
+
+This feature has been tested on Fedora with:
+ poppler-utils-0.20.2-9.fc18.x86_64
+ unoconv-0.5-2.fc18.noarch
+ libreoffice-headless-3.6.5.2-8.fc18.x86_64
+
+It may work on some earlier versions, but that is not guaranteed.
+
+Add ``mediagoblin.media_types.pdf`` to the ``media_types`` list in your
+``mediagoblin_local.ini`` and restart MediaGoblin. 
+
+Run
+
+.. code-block:: bash
+
+    ./bin/gmg dbupdate
+
+
diff --git a/extlib/pdf.js b/extlib/pdf.js
new file mode 160000 (submodule)
index 0000000..b898935
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit b898935eb04fa86e0911fdfa0d41828cb04802f8
index e830e8631ecea16a2f96e84a2a692ba9e0893b49..399a4a138f5016714acd506ab047232aca46e11a 100644 (file)
@@ -125,6 +125,8 @@ spectrogram_fft_size = integer(default=4096)
 [media_type:mediagoblin.media_types.ascii]
 thumbnail_font = string(default=None)
 
+[media_type:mediagoblin.media_types.pdf]
+pdf_js = boolean(default=False)
 
 [celery]
 # default result stuff
diff --git a/mediagoblin/media_types/pdf/__init__.py b/mediagoblin/media_types/pdf/__init__.py
new file mode 100644 (file)
index 0000000..a6d23c9
--- /dev/null
@@ -0,0 +1,29 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from mediagoblin.media_types.pdf.processing import process_pdf, \
+    sniff_handler
+
+
+MEDIA_MANAGER = {
+    "human_readable": "PDF",
+    "processor": process_pdf, # alternately a string,
+                                # 'mediagoblin.media_types.image.processing'?
+    "sniff_handler": sniff_handler,
+    "display_template": "mediagoblin/media_displays/pdf.html",
+    "default_thumb": "images/media_thumbs/pdf.jpg",
+    "accepted_extensions": [
+        "pdf"]}
diff --git a/mediagoblin/media_types/pdf/migrations.py b/mediagoblin/media_types/pdf/migrations.py
new file mode 100644 (file)
index 0000000..f54c23e
--- /dev/null
@@ -0,0 +1,17 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+MIGRATIONS = {}
diff --git a/mediagoblin/media_types/pdf/models.py b/mediagoblin/media_types/pdf/models.py
new file mode 100644 (file)
index 0000000..c39262d
--- /dev/null
@@ -0,0 +1,58 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from mediagoblin.db.base import Base
+
+from sqlalchemy import (
+    Column, Float, Integer, String, DateTime, ForeignKey)
+from sqlalchemy.orm import relationship, backref
+
+
+BACKREF_NAME = "pdf__media_data"
+
+
+class PdfData(Base):
+    __tablename__ = "pdf__mediadata"
+
+    # The primary key *and* reference to the main media_entry
+    media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
+        primary_key=True)
+    get_media_entry = relationship("MediaEntry",
+        backref=backref(BACKREF_NAME, uselist=False,
+                        cascade="all, delete-orphan"))
+    pages = Column(Integer)
+
+    # These are taken from what pdfinfo can do, perhaps others make sense too
+    pdf_author = Column(String)
+    pdf_title = Column(String)
+    # note on keywords: this is the pdf parsed string, it should be considered a cached
+    # value like the rest of these values, since they can be deduced at query time / client
+    # side too.
+    pdf_keywords = Column(String)
+    pdf_creator = Column(String)
+    pdf_producer = Column(String)
+    pdf_creation_date = Column(DateTime)
+    pdf_modified_date = Column(DateTime)
+    pdf_version_major = Column(Integer)
+    pdf_version_minor = Column(Integer)
+    pdf_page_size_width = Column(Float) # unit: pts
+    pdf_page_size_height = Column(Float)
+    pdf_pages = Column(Integer)
+
+
+DATA_MODEL = PdfData
+MODELS = [PdfData]
diff --git a/mediagoblin/media_types/pdf/processing.py b/mediagoblin/media_types/pdf/processing.py
new file mode 100644 (file)
index 0000000..51862c7
--- /dev/null
@@ -0,0 +1,276 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import chardet
+import os
+import Image
+import logging
+import dateutil.parser
+from subprocess import STDOUT, check_output, call, CalledProcessError
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import (create_pub_filepath,
+                                    FilenameBuilder, BadMediaFail)
+from mediagoblin.tools.translate import fake_ugettext_passthrough as _
+
+_log = logging.getLogger(__name__)
+
+# TODO - cache (memoize) util
+
+# This is a list created via uniconv --show and hand removing some types that
+# we already support via other media types better.
+unoconv_supported = [
+  'bib', #      - BibTeX [.bib]
+  #bmp      - Windows Bitmap [.bmp]
+  'csv', #      - Text CSV [.csv]
+  'dbf', #      - dBASE [.dbf]
+  'dif', #      - Data Interchange Format [.dif]
+  'doc6', #     - Microsoft Word 6.0 [.doc]
+  'doc95', #    - Microsoft Word 95 [.doc]
+  'docbook', #  - DocBook [.xml]
+  'doc', #      - Microsoft Word 97/2000/XP [.doc]
+  'docx7', #    - Microsoft Office Open XML [.docx]
+  'docx', #     - Microsoft Office Open XML [.docx]
+  #emf      - Enhanced Metafile [.emf]
+  'eps', #      - Encapsulated PostScript [.eps]
+  'fodp', #     - OpenDocument Presentation (Flat XML) [.fodp]
+  'fods', #     - OpenDocument Spreadsheet (Flat XML) [.fods]
+  'fodt', #     - OpenDocument Text (Flat XML) [.fodt]
+  #gif      - Graphics Interchange Format [.gif]
+  'html', #     - HTML Document (OpenOffice.org Writer) [.html]
+  #jpg      - Joint Photographic Experts Group [.jpg]
+  'latex', #    - LaTeX 2e [.ltx]
+  'mediawiki', # - MediaWiki [.txt]
+  'met', #      - OS/2 Metafile [.met]
+  'odd', #      - OpenDocument Drawing [.odd]
+  'odg', #      - ODF Drawing (Impress) [.odg]
+  'odp', #      - ODF Presentation [.odp]
+  'ods', #      - ODF Spreadsheet [.ods]
+  'odt', #      - ODF Text Document [.odt]
+  'ooxml', #    - Microsoft Office Open XML [.xml]
+  'otg', #      - OpenDocument Drawing Template [.otg]
+  'otp', #      - ODF Presentation Template [.otp]
+  'ots', #      - ODF Spreadsheet Template [.ots]
+  'ott', #      - Open Document Text [.ott]
+  #pbm      - Portable Bitmap [.pbm]
+  #pct      - Mac Pict [.pct]
+  'pdb', #      - AportisDoc (Palm) [.pdb]
+  #pdf      - Portable Document Format [.pdf]
+  #pgm      - Portable Graymap [.pgm]
+  #png      - Portable Network Graphic [.png]
+  'pot', #      - Microsoft PowerPoint 97/2000/XP Template [.pot]
+  'potm', #     - Microsoft PowerPoint 2007/2010 XML Template [.potm]
+  #ppm      - Portable Pixelmap [.ppm]
+  'pps', #      - Microsoft PowerPoint 97/2000/XP (Autoplay) [.pps]
+  'ppt', #      - Microsoft PowerPoint 97/2000/XP [.ppt]
+  'pptx', #     - Microsoft PowerPoint 2007/2010 XML [.pptx]
+  'psw', #      - Pocket Word [.psw]
+  'pwp', #      - PlaceWare [.pwp]
+  'pxl', #      - Pocket Excel [.pxl]
+  #ras      - Sun Raster Image [.ras]
+  'rtf', #      - Rich Text Format [.rtf]
+  'sda', #      - StarDraw 5.0 (OpenOffice.org Impress) [.sda]
+  'sdc3', #     - StarCalc 3.0 [.sdc]
+  'sdc4', #     - StarCalc 4.0 [.sdc]
+  'sdc', #      - StarCalc 5.0 [.sdc]
+  'sdd3', #     - StarDraw 3.0 (OpenOffice.org Impress) [.sdd]
+  'sdd4', #     - StarImpress 4.0 [.sdd]
+  'sdd', #      - StarImpress 5.0 [.sdd]
+  'sdw3', #     - StarWriter 3.0 [.sdw]
+  'sdw4', #     - StarWriter 4.0 [.sdw]
+  'sdw', #      - StarWriter 5.0 [.sdw]
+  'slk', #      - SYLK [.slk]
+  'stc', #      - OpenOffice.org 1.0 Spreadsheet Template [.stc]
+  'std', #      - OpenOffice.org 1.0 Drawing Template [.std]
+  'sti', #      - OpenOffice.org 1.0 Presentation Template [.sti]
+  'stw', #      - Open Office.org 1.0 Text Document Template [.stw]
+  #svg      - Scalable Vector Graphics [.svg]
+  'svm', #      - StarView Metafile [.svm]
+  'swf', #      - Macromedia Flash (SWF) [.swf]
+  'sxc', #      - OpenOffice.org 1.0 Spreadsheet [.sxc]
+  'sxd3', #     - StarDraw 3.0 [.sxd]
+  'sxd5', #     - StarDraw 5.0 [.sxd]
+  'sxd', #      - OpenOffice.org 1.0 Drawing (OpenOffice.org Impress) [.sxd]
+  'sxi', #      - OpenOffice.org 1.0 Presentation [.sxi]
+  'sxw', #      - Open Office.org 1.0 Text Document [.sxw]
+  #text     - Text Encoded [.txt]
+  #tiff     - Tagged Image File Format [.tiff]
+  #txt      - Text [.txt]
+  'uop', #      - Unified Office Format presentation [.uop]
+  'uos', #      - Unified Office Format spreadsheet [.uos]
+  'uot', #      - Unified Office Format text [.uot]
+  'vor3', #     - StarDraw 3.0 Template (OpenOffice.org Impress) [.vor]
+  'vor4', #     - StarWriter 4.0 Template [.vor]
+  'vor5', #     - StarDraw 5.0 Template (OpenOffice.org Impress) [.vor]
+  'vor', #      - StarCalc 5.0 Template [.vor]
+  #wmf      - Windows Metafile [.wmf]
+  'xhtml', #    - XHTML Document [.html]
+  'xls5', #     - Microsoft Excel 5.0 [.xls]
+  'xls95', #    - Microsoft Excel 95 [.xls]
+  'xls', #      - Microsoft Excel 97/2000/XP [.xls]
+  'xlt5', #     - Microsoft Excel 5.0 Template [.xlt]
+  'xlt95', #    - Microsoft Excel 95 Template [.xlt]
+  'xlt', #      - Microsoft Excel 97/2000/XP Template [.xlt]
+  #xpm      - X PixMap [.xpm]
+]
+
+def is_unoconv_working():
+    try:
+        output = check_output([where('unoconv'), '--show'], stderr=STDOUT)
+    except CalledProcessError, e:
+        _log.warn(_('unoconv failing to run, check log file'))
+        return False
+    if 'ERROR' in output:
+        return False
+    return True
+
+def supported_extensions(cache=[None]):
+    if cache[0] == None:
+        cache[0] = 'pdf'
+        # TODO: must have libreoffice-headless installed too, need to check for it
+        if where('unoconv') and is_unoconv_working():
+            cache.extend(unoconv_supported)
+    return cache
+
+def where(name):
+    for p in os.environ['PATH'].split(os.pathsep):
+        fullpath = os.path.join(p, name)
+        if os.path.exists(fullpath):
+            return fullpath
+    return None
+
+def check_prerequisites():
+    if not where('pdfinfo'):
+        _log.warn('missing pdfinfo')
+        return False
+    if not where('pdftocairo'):
+        _log.warn('missing pdfcairo')
+        return False
+    return True
+
+def sniff_handler(media_file, **kw):
+    if not check_prerequisites():
+        return False
+    if kw.get('media') is not None:
+        name, ext = os.path.splitext(kw['media'].filename)
+        clean_ext = ext[1:].lower()
+
+        if clean_ext in supported_extensions():
+            return True
+
+    return False
+
+def create_pdf_thumb(original, thumb_filename, width, height):
+    # Note: pdftocairo adds '.png', remove it
+    thumb_filename = thumb_filename[:-4]
+    executable = where('pdftocairo')
+    args = [executable, '-scale-to', str(min(width, height)),
+            '-singlefile', '-png', original, thumb_filename]
+    _log.debug('calling {0}'.format(repr(' '.join(args))))
+    call(executable=executable, args=args)
+
+def pdf_info(original):
+    """
+    Extract dictionary of pdf information. This could use a library instead
+    of a process.
+
+    Note: I'm assuming pdfinfo output is sanitized (integers where integers are
+    expected, etc.) - if this is wrong then an exception will be raised and caught
+    leading to the dreaded error page. It seems a safe assumption.
+    """
+    ret_dict = {}
+    pdfinfo = where('pdfinfo')
+    try:
+        lines = check_output(executable=pdfinfo,
+                                args=[pdfinfo, original]).split(os.linesep)
+    except CalledProcessError:
+        _log.debug('pdfinfo could not read the pdf file.')
+        raise BadMediaFail()
+
+    info_dict = dict([[part.strip() for part in l.strip().split(':', 1)]
+                      for l in lines if ':' in l])
+
+    for date_key in [('pdf_mod_date', 'ModDate'),
+                     ('pdf_creation_date', 'CreationDate')]:
+        if date_key in info_dict:
+            ret_dict[date_key] = dateutil.parser.parse(info_dict[date_key])
+    for db_key, int_key in [('pdf_pages', 'Pages')]:
+        if int_key in info_dict:
+            ret_dict[db_key] = int(info_dict[int_key])
+
+    # parse 'PageSize' field: 595 x 842 pts (A4)
+    page_size_parts = info_dict['Page size'].split()
+    ret_dict['pdf_page_size_width'] = float(page_size_parts[0])
+    ret_dict['pdf_page_size_height'] = float(page_size_parts[2])
+
+    for db_key, str_key in [('pdf_keywords', 'Keywords'),
+        ('pdf_creator', 'Creator'), ('pdf_producer', 'Producer'),
+        ('pdf_author', 'Author'), ('pdf_title', 'Title')]:
+        ret_dict[db_key] = info_dict.get(str_key, None)
+    ret_dict['pdf_version_major'], ret_dict['pdf_version_minor'] = \
+        map(int, info_dict['PDF version'].split('.'))
+
+    return ret_dict
+
+def process_pdf(proc_state):
+    """Code to process a pdf file. Will be run by celery.
+
+    A Workbench() represents a local tempory dir. It is automatically
+    cleaned up when this function exits.
+    """
+    entry = proc_state.entry
+    workbench = proc_state.workbench
+
+    queued_filename = proc_state.get_queued_filename()
+    name_builder = FilenameBuilder(queued_filename)
+
+    media_files_dict = entry.setdefault('media_files', {})
+
+    # Copy our queued local workbench to its final destination
+    original_dest = name_builder.fill('{basename}{ext}')
+    proc_state.copy_original(original_dest)
+
+    # Create a pdf if this is a different doc, store pdf for viewer
+    ext = queued_filename.rsplit('.', 1)[-1].lower()
+    if ext == 'pdf':
+        pdf_filename = queued_filename
+    else:
+        pdf_filename = queued_filename.rsplit('.', 1)[0] + '.pdf'
+        unoconv = where('unoconv')
+        call(executable=unoconv,
+             args=[unoconv, '-v', '-f', 'pdf', queued_filename])
+        if not os.path.exists(pdf_filename):
+            _log.debug('unoconv failed to convert file to pdf')
+            raise BadMediaFail()
+        proc_state.store_public(keyname=u'pdf', local_file=pdf_filename)
+
+    pdf_info_dict = pdf_info(pdf_filename)
+
+    for name, width, height in [
+        (u'thumb', mgg.global_config['media:thumb']['max_width'],
+                   mgg.global_config['media:thumb']['max_height']),
+        (u'medium', mgg.global_config['media:medium']['max_width'],
+                   mgg.global_config['media:medium']['max_height']),
+        ]:
+        filename = name_builder.fill('{basename}.%s.png' % name)
+        path = workbench.joinpath(filename)
+        create_pdf_thumb(pdf_filename, path, width, height)
+        assert(os.path.exists(path))
+        proc_state.store_public(keyname=name, local_file=path)
+
+    proc_state.delete_queue_file()
+
+    entry.media_data_init(**pdf_info_dict)
+    entry.save()
diff --git a/mediagoblin/static/css/pdf_viewer.css b/mediagoblin/static/css/pdf_viewer.css
new file mode 100644 (file)
index 0000000..c04c898
--- /dev/null
@@ -0,0 +1,1448 @@
+/* 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.
+ */
+
+* {
+  padding: 0;
+  margin: 0;
+}
+
+html {
+  height: 100%;
+}
+
+body {
+  height: 100%;
+  background-color: #404040;
+  background-image: url(../extlib/pdf.js/web/images/texture.png);
+}
+
+body,
+input,
+button,
+select {
+  font: message-box;
+}
+
+.hidden {
+  display: none;
+}
+[hidden] {
+  display: none !important;
+}
+
+#viewerContainer:-webkit-full-screen {
+  top: 0px;
+  border-top: 2px solid transparent;
+  background-color: #404040;
+  background-image: url(../extlib/pdf.js/web/images/texture.png);
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  cursor: none;
+}
+
+#viewerContainer:-moz-full-screen {
+  top: 0px;
+  border-top: 2px solid transparent;
+  background-color: #404040;
+  background-image: url(../extlib/pdf.js/web/images/texture.png);
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  cursor: none;
+}
+
+#viewerContainer:fullscreen {
+  top: 0px;
+  border-top: 2px solid transparent;
+  background-color: #404040;
+  background-image: url(../extlib/pdf.js/web/images/texture.png);
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  cursor: none;
+}
+
+
+:-webkit-full-screen .page {
+  margin-bottom: 100%;
+}
+
+:-moz-full-screen .page {
+  margin-bottom: 100%;
+}
+
+:fullscreen .page {
+  margin-bottom: 100%;
+}
+
+#viewerContainer.presentationControls {
+  cursor: default;
+}
+
+/* outer/inner center provides horizontal center */
+html[dir='ltr'] .outerCenter {
+  float: right;
+  position: relative;
+  right: 50%;
+}
+html[dir='rtl'] .outerCenter {
+  float: left;
+  position: relative;
+  left: 50%;
+}
+html[dir='ltr'] .innerCenter {
+  float: right;
+  position: relative;
+  right: -50%;
+}
+html[dir='rtl'] .innerCenter {
+  float: left;
+  position: relative;
+  left: -50%;
+}
+
+#outerContainer {
+  width: 100%;
+  height: 100%;
+}
+
+#sidebarContainer {
+  left: 0;
+  right: 0;
+  height: 200px;
+  visibility: hidden;
+  -webkit-transition-duration: 200ms;
+  -webkit-transition-timing-function: ease;
+  -moz-transition-duration: 200ms;
+  -moz-transition-timing-function: ease;
+  -ms-transition-duration: 200ms;
+  -ms-transition-timing-function: ease;
+  -o-transition-duration: 200ms;
+  -o-transition-timing-function: ease;
+  transition-duration: 200ms;
+  transition-timing-function: ease;
+
+}
+html[dir='ltr'] #sidebarContainer {
+  -webkit-transition-property: top;
+  -moz-transition-property: top;
+  -ms-transition-property: top;
+  -o-transition-property: top;
+  transition-property: top;
+  top: -200px;
+}
+html[dir='rtl'] #sidebarContainer {
+  -webkit-transition-property: top;
+  -ms-transition-property: top;
+  -o-transition-property: top;
+  transition-property: top;
+  top: -200px;
+}
+
+#outerContainer.sidebarMoving > #sidebarContainer,
+#outerContainer.sidebarOpen > #sidebarContainer {
+  visibility: visible;
+}
+html[dir='ltr'] #outerContainer.sidebarOpen > #sidebarContainer {
+  left: 0px;
+}
+html[dir='rtl'] #outerContainer.sidebarOpen > #sidebarContainer {
+  right: 0px;
+}
+
+#mainContainer {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  min-width: 320px;
+  -webkit-transition-duration: 200ms;
+  -webkit-transition-timing-function: ease;
+  -moz-transition-duration: 200ms;
+  -moz-transition-timing-function: ease;
+  -ms-transition-duration: 200ms;
+  -ms-transition-timing-function: ease;
+  -o-transition-duration: 200ms;
+  -o-transition-timing-function: ease;
+  transition-duration: 200ms;
+  transition-timing-function: ease;
+}
+html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer {
+  -webkit-transition-property: left;
+  -moz-transition-property: left;
+  -ms-transition-property: left;
+  -o-transition-property: left;
+  transition-property: left;
+  left: 200px;
+}
+html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer {
+  -webkit-transition-property: right;
+  -moz-transition-property: right;
+  -ms-transition-property: right;
+  -o-transition-property: right;
+  transition-property: right;
+  right: 200px;
+}
+
+#sidebarContent {
+  top: 32px;
+  bottom: 0;
+  overflow: auto;
+  height: 200px;
+
+  background-color: hsla(0,0%,0%,.1);
+  box-shadow: inset -1px 0 0 hsla(0,0%,0%,.25);
+}
+html[dir='ltr'] #sidebarContent {
+  left: 0;
+}
+html[dir='rtl'] #sidebarContent {
+  right: 0;
+}
+
+#viewerContainer {
+  overflow: auto;
+  box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
+  top: 32px;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  height: 480px;
+  width: 640px;
+}
+
+.toolbar {
+  left: 0;
+  right: 0;
+  height: 32px;
+  z-index: 9999;
+  cursor: default;
+}
+
+#toolbarContainer {
+  width: 100%;
+}
+
+#toolbarSidebar {
+  width: 200px;
+  height: 32px;
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -webkit-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -moz-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -ms-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -o-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
+  box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25),
+
+              inset 0 -1px 0 hsla(0,0%,100%,.05),
+              0 1px 0 hsla(0,0%,0%,.15),
+              0 0 1px hsla(0,0%,0%,.1);
+}
+
+#toolbarViewer, .findbar {
+  position: relative;
+  height: 32px;
+  background-color: #474747; /* IE9 */
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -webkit-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -moz-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -ms-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    -o-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+  background-image: url(../extlib/pdf.js/web/images/texture.png),
+                    linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+  box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
+              inset 0 1px 1px hsla(0,0%,0%,.15),
+              inset 0 -1px 0 hsla(0,0%,100%,.05),
+              0 1px 0 hsla(0,0%,0%,.15),
+              0 1px 1px hsla(0,0%,0%,.1);
+}
+
+.findbar {
+  top: 64px;
+  z-index: 10000;
+  height: 32px;
+
+  min-width: 16px;
+  padding: 0px 6px 0px 6px;
+  margin: 4px 2px 4px 2px;
+  color: hsl(0,0%,85%);
+  font-size: 12px;
+  line-height: 14px;
+  text-align: left;
+  cursor: default;
+}
+
+html[dir='ltr'] .findbar {
+  left: 68px;
+}
+
+html[dir='rtl'] .findbar {
+  right: 68px;
+}
+
+.findbar label {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+}
+
+#findInput[data-status="pending"] {
+  background-image: url(../extlib/pdf.js/web/images/loading-small.png);
+  background-repeat: no-repeat;
+  background-position: right;
+}
+
+.doorHanger {
+  border: 1px solid hsla(0,0%,0%,.5);
+  border-radius: 2px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
+}
+.doorHanger:after, .doorHanger:before {
+  bottom: 100%;
+  border: solid transparent;
+  content: " ";
+  height: 0;
+  width: 0;
+  pointer-events: none;
+}
+.doorHanger:after {
+  border-bottom-color: hsla(0,0%,32%,.99);
+  border-width: 8px;
+}
+.doorHanger:before {
+  border-bottom-color: hsla(0,0%,0%,.5);
+  border-width: 9px;
+}
+
+html[dir='ltr'] .doorHanger:after {
+  left: 13px;
+  margin-left: -8px;
+}
+
+html[dir='ltr'] .doorHanger:before {
+  left: 13px;
+  margin-left: -9px;
+}
+
+html[dir='rtl'] .doorHanger:after {
+  right: 13px;
+  margin-right: -8px;
+}
+
+html[dir='rtl'] .doorHanger:before {
+  right: 13px;
+  margin-right: -9px;
+}
+
+#findMsg {
+  font-style: italic;
+  color: #A6B7D0;
+}
+
+.notFound {
+  background-color: rgb(255, 137, 153);
+}
+
+html[dir='ltr'] #toolbarViewerLeft {
+  margin-left: -1px;
+}
+html[dir='rtl'] #toolbarViewerRight {
+  margin-left: -1px;
+}
+
+
+html[dir='ltr'] #toolbarViewerLeft,
+html[dir='rtl'] #toolbarViewerRight {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+html[dir='ltr'] #toolbarViewerRight,
+html[dir='rtl'] #toolbarViewerLeft {
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+html[dir='ltr'] #toolbarViewerLeft > *,
+html[dir='ltr'] #toolbarViewerMiddle > *,
+html[dir='ltr'] #toolbarViewerRight > *,
+html[dir='ltr'] .findbar > * {
+  float: left;
+}
+html[dir='rtl'] #toolbarViewerLeft > *,
+html[dir='rtl'] #toolbarViewerMiddle > *,
+html[dir='rtl'] #toolbarViewerRight > *,
+html[dir='rtl'] .findbar > * {
+  float: right;
+}
+
+html[dir='ltr'] .splitToolbarButton {
+  margin: 3px 2px 4px 0;
+  display: inline-block;
+}
+html[dir='rtl'] .splitToolbarButton {
+  margin: 3px 0 4px 2px;
+  display: inline-block;
+}
+html[dir='ltr'] .splitToolbarButton > .toolbarButton {
+  border-radius: 0;
+  float: left;
+}
+html[dir='rtl'] .splitToolbarButton > .toolbarButton {
+  border-radius: 0;
+  float: right;
+}
+
+.toolbarButton {
+  border: 0 none;
+  background-color: rgba(0, 0, 0, 0);
+  width: 32px;
+  height: 25px;
+}
+
+.toolbarButton > span {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+}
+
+.toolbarButton[disabled] {
+  opacity: .5;
+}
+
+.toolbarButton.group {
+  margin-right: 0;
+}
+
+.splitToolbarButton.toggled .toolbarButton {
+  margin: 0;
+}
+
+.splitToolbarButton:hover > .toolbarButton,
+.splitToolbarButton:focus > .toolbarButton,
+.splitToolbarButton.toggled > .toolbarButton,
+.toolbarButton.textButton {
+  background-color: hsla(0,0%,0%,.12);
+  background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  border: 1px solid hsla(0,0%,0%,.35);
+  border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.15) inset,
+              0 1px 0 hsla(0,0%,100%,.05);
+  -webkit-transition-property: background-color, border-color, box-shadow;
+  -webkit-transition-duration: 150ms;
+  -webkit-transition-timing-function: ease;
+  -moz-transition-property: background-color, border-color, box-shadow;
+  -moz-transition-duration: 150ms;
+  -moz-transition-timing-function: ease;
+  -ms-transition-property: background-color, border-color, box-shadow;
+  -ms-transition-duration: 150ms;
+  -ms-transition-timing-function: ease;
+  -o-transition-property: background-color, border-color, box-shadow;
+  -o-transition-duration: 150ms;
+  -o-transition-timing-function: ease;
+  transition-property: background-color, border-color, box-shadow;
+  transition-duration: 150ms;
+  transition-timing-function: ease;
+
+}
+.splitToolbarButton > .toolbarButton:hover,
+.splitToolbarButton > .toolbarButton:focus,
+.dropdownToolbarButton:hover,
+.toolbarButton.textButton:hover,
+.toolbarButton.textButton:focus {
+  background-color: hsla(0,0%,0%,.2);
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.15) inset,
+              0 0 1px hsla(0,0%,0%,.05);
+  z-index: 199;
+}
+html[dir='ltr'] .splitToolbarButton > .toolbarButton:first-child,
+html[dir='rtl'] .splitToolbarButton > .toolbarButton:last-child {
+  position: relative;
+  margin: 0;
+  margin-right: -1px;
+  border-top-left-radius: 2px;
+  border-bottom-left-radius: 2px;
+  border-right-color: transparent;
+}
+html[dir='ltr'] .splitToolbarButton > .toolbarButton:last-child,
+html[dir='rtl'] .splitToolbarButton > .toolbarButton:first-child {
+  position: relative;
+  margin: 0;
+  margin-left: -1px;
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+  border-left-color: transparent;
+}
+.splitToolbarButtonSeparator {
+  padding: 8px 0;
+  width: 1px;
+  background-color: hsla(0,0%,00%,.5);
+  z-index: 99;
+  box-shadow: 0 0 0 1px hsla(0,0%,100%,.08);
+  display: inline-block;
+  margin: 5px 0;
+}
+html[dir='ltr'] .splitToolbarButtonSeparator {
+  float: left;
+}
+html[dir='rtl'] .splitToolbarButtonSeparator {
+  float: right;
+}
+.splitToolbarButton:hover > .splitToolbarButtonSeparator,
+.splitToolbarButton.toggled > .splitToolbarButtonSeparator {
+  padding: 12px 0;
+  margin: 1px 0;
+  box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
+  -webkit-transition-property: padding;
+  -webkit-transition-duration: 10ms;
+  -webkit-transition-timing-function: ease;
+  -moz-transition-property: padding;
+  -moz-transition-duration: 10ms;
+  -moz-transition-timing-function: ease;
+  -ms-transition-property: padding;
+  -ms-transition-duration: 10ms;
+  -ms-transition-timing-function: ease;
+  -o-transition-property: padding;
+  -o-transition-duration: 10ms;
+  -o-transition-timing-function: ease;
+  transition-property: padding;
+  transition-duration: 10ms;
+  transition-timing-function: ease;
+}
+
+.toolbarButton,
+.dropdownToolbarButton {
+  min-width: 16px;
+  padding: 2px 6px 0;
+  border: 1px solid transparent;
+  border-radius: 2px;
+  color: hsl(0,0%,95%);
+  font-size: 12px;
+  line-height: 14px;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  /* Opera does not support user-select, use <... unselectable="on"> instead */
+  cursor: default;
+  -webkit-transition-property: background-color, border-color, box-shadow;
+  -webkit-transition-duration: 150ms;
+  -webkit-transition-timing-function: ease;
+  -moz-transition-property: background-color, border-color, box-shadow;
+  -moz-transition-duration: 150ms;
+  -moz-transition-timing-function: ease;
+  -ms-transition-property: background-color, border-color, box-shadow;
+  -ms-transition-duration: 150ms;
+  -ms-transition-timing-function: ease;
+  -o-transition-property: background-color, border-color, box-shadow;
+  -o-transition-duration: 150ms;
+  -o-transition-timing-function: ease;
+  transition-property: background-color, border-color, box-shadow;
+  transition-duration: 150ms;
+  transition-timing-function: ease;
+}
+
+html[dir='ltr'] .toolbarButton,
+html[dir='ltr'] .dropdownToolbarButton {
+  margin: 3px 2px 4px 0;
+}
+html[dir='rtl'] .toolbarButton,
+html[dir='rtl'] .dropdownToolbarButton {
+  margin: 3px 0 4px 2px;
+}
+
+.toolbarButton:hover,
+.toolbarButton:focus,
+.dropdownToolbarButton {
+  background-color: hsla(0,0%,0%,.12);
+  background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  border: 1px solid hsla(0,0%,0%,.35);
+  border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.15) inset,
+              0 1px 0 hsla(0,0%,100%,.05);
+}
+
+.toolbarButton:hover:active,
+.dropdownToolbarButton:hover:active {
+  background-color: hsla(0,0%,0%,.2);
+  background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
+  box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
+              0 0 1px hsla(0,0%,0%,.2) inset,
+              0 1px 0 hsla(0,0%,100%,.05);
+  -webkit-transition-property: background-color, border-color, box-shadow;
+  -webkit-transition-duration: 10ms;
+  -webkit-transition-timing-function: linear;
+  -moz-transition-property: background-color, border-color, box-shadow;
+  -moz-transition-duration: 10ms;
+  -moz-transition-timing-function: linear;
+  -ms-transition-property: background-color, border-color, box-shadow;
+  -ms-transition-duration: 10ms;
+  -ms-transition-timing-function: linear;
+  -o-transition-property: background-color, border-color, box-shadow;
+  -o-transition-duration: 10ms;
+  -o-transition-timing-function: linear;
+  transition-property: background-color, border-color, box-shadow;
+  transition-duration: 10ms;
+  transition-timing-function: linear;
+}
+
+.toolbarButton.toggled,
+.splitToolbarButton.toggled > .toolbarButton.toggled {
+  background-color: hsla(0,0%,0%,.3);
+  background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
+  box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
+              0 0 1px hsla(0,0%,0%,.2) inset,
+              0 1px 0 hsla(0,0%,100%,.05);
+  -webkit-transition-property: background-color, border-color, box-shadow;
+  -webkit-transition-duration: 10ms;
+  -webkit-transition-timing-function: linear;
+  -moz-transition-property: background-color, border-color, box-shadow;
+  -moz-transition-duration: 10ms;
+  -moz-transition-timing-function: linear;
+  -ms-transition-property: background-color, border-color, box-shadow;
+  -ms-transition-duration: 10ms;
+  -ms-transition-timing-function: linear;
+  -o-transition-property: background-color, border-color, box-shadow;
+  -o-transition-duration: 10ms;
+  -o-transition-timing-function: linear;
+  transition-property: background-color, border-color, box-shadow;
+  transition-duration: 10ms;
+  transition-timing-function: linear;
+}
+
+.toolbarButton.toggled:hover:active,
+.splitToolbarButton.toggled > .toolbarButton.toggled:hover:active {
+  background-color: hsla(0,0%,0%,.4);
+  border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.5) hsla(0,0%,0%,.55);
+  box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset,
+              0 0 1px hsla(0,0%,0%,.3) inset,
+              0 1px 0 hsla(0,0%,100%,.05);
+}
+
+.dropdownToolbarButton {
+  width: 120px;
+  max-width: 120px;
+  padding: 3px 2px 2px;
+  overflow: hidden;
+  background: url(../extlib/pdf.js/web/images/toolbarButton-menuArrows.png) no-repeat;
+}
+html[dir='ltr'] .dropdownToolbarButton {
+  background-position: 95%;
+}
+html[dir='rtl'] .dropdownToolbarButton {
+  background-position: 5%;
+}
+
+.dropdownToolbarButton > select {
+  -webkit-appearance: none;
+  -moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */
+  min-width: 140px;
+  font-size: 12px;
+  color: hsl(0,0%,95%);
+  margin: 0;
+  padding: 0;
+  border: none;
+  background: rgba(0,0,0,0); /* Opera does not support 'transparent' <select> background */
+}
+
+.dropdownToolbarButton > select > option {
+  background: hsl(0,0%,24%);
+}
+
+#customScaleOption {
+  display: none;
+}
+
+#pageWidthOption {
+  border-bottom: 1px rgba(255, 255, 255, .5) solid;
+}
+
+html[dir='ltr'] .splitToolbarButton:first-child,
+html[dir='ltr'] .toolbarButton:first-child,
+html[dir='rtl'] .splitToolbarButton:last-child,
+html[dir='rtl'] .toolbarButton:last-child {
+  margin-left: 4px;
+}
+html[dir='ltr'] .splitToolbarButton:last-child,
+html[dir='ltr'] .toolbarButton:last-child,
+html[dir='rtl'] .splitToolbarButton:first-child,
+html[dir='rtl'] .toolbarButton:first-child {
+  margin-right: 4px;
+}
+
+.toolbarButtonSpacer {
+  width: 30px;
+  display: inline-block;
+  height: 1px;
+}
+
+.toolbarButtonFlexibleSpacer {
+  -webkit-box-flex: 1;
+  -moz-box-flex: 1;
+  min-width: 30px;
+}
+
+.toolbarButton#sidebarToggle::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-sidebarToggle.png);
+}
+
+html[dir='ltr'] #findPrevious {
+  margin-left: 3px;
+}
+html[dir='ltr'] #findNext {
+  margin-right: 3px;
+}
+
+html[dir='rtl'] #findPrevious {
+  margin-right: 3px;
+}
+html[dir='rtl'] #findNext {
+  margin-left: 3px;
+}
+
+html[dir='ltr'] .toolbarButton.findPrevious::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/findbarButton-previous.png);
+}
+
+html[dir='rtl'] .toolbarButton.findPrevious::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/findbarButton-previous-rtl.png);
+}
+
+html[dir='ltr'] .toolbarButton.findNext::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/findbarButton-next.png);
+}
+
+html[dir='rtl'] .toolbarButton.findNext::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/findbarButton-next-rtl.png);
+}
+
+html[dir='ltr'] .toolbarButton.pageUp::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-pageUp.png);
+}
+
+html[dir='rtl'] .toolbarButton.pageUp::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-pageUp-rtl.png);
+}
+
+html[dir='ltr'] .toolbarButton.pageDown::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-pageDown.png);
+}
+
+html[dir='rtl'] .toolbarButton.pageDown::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-pageDown-rtl.png);
+}
+
+.toolbarButton.zoomOut::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-zoomOut.png);
+}
+
+.toolbarButton.zoomIn::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-zoomIn.png);
+}
+
+.toolbarButton.fullscreen::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-fullscreen.png);
+}
+
+.toolbarButton.print::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-print.png);
+}
+
+.toolbarButton.openFile::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-openFile.png);
+}
+
+.toolbarButton.download::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-download.png);
+}
+
+.toolbarButton.bookmark {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  margin-top: 3px;
+  padding-top: 4px;
+}
+
+#viewBookmark[href='#'] {
+  opacity: .5;
+  pointer-events: none;
+}
+
+.toolbarButton.bookmark::before {
+  content: url(../extlib/pdf.js/web/images/toolbarButton-bookmark.png);
+}
+
+#viewThumbnail.toolbarButton::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-viewThumbnail.png);
+}
+
+#viewOutline.toolbarButton::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-viewOutline.png);
+}
+
+#viewFind.toolbarButton::before {
+  display: inline-block;
+  content: url(../extlib/pdf.js/web/images/toolbarButton-search.png);
+}
+
+
+.toolbarField {
+  padding: 3px 6px;
+  margin: 4px 0 4px 0;
+  border: 1px solid transparent;
+  border-radius: 2px;
+  background-color: hsla(0,0%,100%,.09);
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  border: 1px solid hsla(0,0%,0%,.35);
+  border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
+  box-shadow: 0 1px 0 hsla(0,0%,0%,.05) inset,
+              0 1px 0 hsla(0,0%,100%,.05);
+  color: hsl(0,0%,95%);
+  font-size: 12px;
+  line-height: 14px;
+  outline-style: none;
+  -moz-transition-property: background-color, border-color, box-shadow;
+  -moz-transition-duration: 150ms;
+  -moz-transition-timing-function: ease;
+}
+
+.toolbarField[type=checkbox] {
+  display: inline-block;
+  margin: 8px 0px;
+}
+
+.toolbarField.pageNumber {
+  min-width: 16px;
+  text-align: right;
+  width: 40px;
+}
+
+.toolbarField.pageNumber::-webkit-inner-spin-button,
+.toolbarField.pageNumber::-webkit-outer-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+}
+
+.toolbarField:hover {
+  background-color: hsla(0,0%,100%,.11);
+  border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.43) hsla(0,0%,0%,.45);
+}
+
+.toolbarField:focus {
+  background-color: hsla(0,0%,100%,.15);
+  border-color: hsla(204,100%,65%,.8) hsla(204,100%,65%,.85) hsla(204,100%,65%,.9);
+}
+
+.toolbarLabel {
+  min-width: 16px;
+  padding: 3px 6px 3px 2px;
+  margin: 4px 2px 4px 0;
+  border: 1px solid transparent;
+  border-radius: 2px;
+  color: hsl(0,0%,85%);
+  font-size: 12px;
+  line-height: 14px;
+  text-align: left;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  cursor: default;
+}
+
+#thumbnailView {
+  top: 0;
+  bottom: 0;
+  padding: 10px 10px 0;
+  overflow: auto;
+}
+
+.thumbnail {
+  float: left;
+}
+
+.thumbnail:not([data-loaded]) {
+  border: 1px dashed rgba(255, 255, 255, 0.5);
+  margin-bottom: 10px;
+}
+
+.thumbnailImage {
+  -moz-transition-duration: 150ms;
+  border: 1px solid transparent;
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
+  opacity: 0.8;
+  z-index: 99;
+}
+
+.thumbnailSelectionRing {
+  border-radius: 2px;
+  padding: 7px;
+  -moz-transition-duration: 150ms;
+}
+
+a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage,
+.thumbnail:hover > .thumbnailSelectionRing > .thumbnailImage {
+  opacity: .9;
+}
+
+a:focus > .thumbnail > .thumbnailSelectionRing,
+.thumbnail:hover > .thumbnailSelectionRing {
+  background-color: hsla(0,0%,100%,.15);
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.2) inset,
+              0 0 1px hsla(0,0%,0%,.2);
+  color: hsla(0,0%,100%,.9);
+}
+
+.thumbnail.selected > .thumbnailSelectionRing > .thumbnailImage {
+  box-shadow: 0 0 0 1px hsla(0,0%,0%,.5);
+  opacity: 1;
+}
+
+.thumbnail.selected > .thumbnailSelectionRing {
+  background-color: hsla(0,0%,100%,.3);
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.1) inset,
+              0 0 1px hsla(0,0%,0%,.2);
+  color: hsla(0,0%,100%,1);
+}
+
+#outlineView {
+  width: 192px;
+  top: 0;
+  bottom: 0;
+  padding: 4px 4px 0;
+  overflow: auto;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+}
+
+html[dir='ltr'] .outlineItem > .outlineItems {
+  margin-left: 20px;
+}
+
+html[dir='rtl'] .outlineItem > .outlineItems {
+  margin-right: 20px;
+}
+
+.outlineItem > a {
+  text-decoration: none;
+  display: inline-block;
+  min-width: 95%;
+  height: auto;
+  margin-bottom: 1px;
+  border-radius: 2px;
+  color: hsla(0,0%,100%,.8);
+  font-size: 13px;
+  line-height: 15px;
+  -moz-user-select: none;
+  white-space: normal;
+}
+
+html[dir='ltr'] .outlineItem > a {
+  padding: 2px 0 5px 10px;
+}
+
+html[dir='rtl'] .outlineItem > a {
+  padding: 2px 10px 5px 0;
+}
+
+.outlineItem > a:hover {
+  background-color: hsla(0,0%,100%,.02);
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.2) inset,
+              0 0 1px hsla(0,0%,0%,.2);
+  color: hsla(0,0%,100%,.9);
+}
+
+.outlineItem.selected {
+  background-color: hsla(0,0%,100%,.08);
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-clip: padding-box;
+  box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+              0 0 1px hsla(0,0%,100%,.1) inset,
+              0 0 1px hsla(0,0%,0%,.2);
+  color: hsla(0,0%,100%,1);
+}
+
+.noOutline,
+.noResults {
+  font-size: 12px;
+  color: hsla(0,0%,100%,.8);
+  font-style: italic;
+  cursor: default;
+}
+
+#findScrollView {
+  position: absolute;
+  top: 10px;
+  bottom: 10px;
+  left: 10px;
+  width: 280px;
+}
+
+#sidebarControls {
+  position:absolute;
+  width: 180px;
+  height: 32px;
+  left: 15px;
+  bottom: 35px;
+}
+
+canvas {
+  margin: auto;
+  display: block;
+}
+
+.page {
+  direction: ltr;
+  width: 816px;
+  height: 1056px;
+  margin: 1px auto -8px auto;
+  position: relative;
+  overflow: visible;
+  border: 9px solid transparent;
+  background-clip: content-box;
+  border-image: url(../extlib/pdf.js/web/images/shadow.png) 9 9 repeat;
+  background-color: white;
+}
+
+.page > a {
+  display: block;
+  position: absolute;
+}
+
+.page > a:hover {
+  opacity: 0.2;
+  background: #ff0;
+  box-shadow: 0px 2px 10px #ff0;
+}
+
+.loadingIcon {
+  position: absolute;
+  display: block;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background: url('../extlib/pdf.js/web/images/loading-icon.gif') center no-repeat;
+}
+
+#loadingBox {
+  position: absolute;
+  top: 50%;
+  margin-top: -25px;
+  left: 0;
+  right: 0;
+  text-align: center;
+  color: #ddd;
+  font-size: 14px;
+}
+
+#loadingBar {
+  display: inline-block;
+  clear: both;
+  margin: 0px;
+  margin-top: 5px;
+  line-height: 0;
+  border-radius: 2px;
+  width: 200px;
+  height: 25px;
+
+  background-color: hsla(0,0%,0%,.3);
+  background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+  border: 1px solid #000;
+  box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
+              0 0 1px hsla(0,0%,0%,.2) inset,
+              0 0 1px 1px rgba(255, 255, 255, 0.1);
+}
+
+#loadingBar .progress {
+  display: inline-block;
+  float: left;
+
+  background: #666;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2b2b2), color-stop(100%,#898989));
+  background: -webkit-linear-gradient(top, #b2b2b2 0%,#898989 100%);
+  background: -moz-linear-gradient(top, #b2b2b2 0%,#898989 100%);
+  background: -ms-linear-gradient(top, #b2b2b2 0%,#898989 100%);
+  background: -o-linear-gradient(top, #b2b2b2 0%,#898989 100%);
+  background: linear-gradient(top, #b2b2b2 0%,#898989 100%);
+
+  border-top-left-radius: 2px;
+  border-bottom-left-radius: 2px;
+
+  width: 0%;
+  height: 100%;
+}
+
+#loadingBar .progress.full {
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+}
+
+#loadingBar .progress.indeterminate {
+  width: 100%;
+  height: 25px;
+  background-image: -moz-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
+  background-image: -webkit-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
+  background-image: -ms-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
+  background-image: -o-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
+  background-size: 75px 25px;
+  -moz-animation: progressIndeterminate 1s linear infinite;
+  -webkit-animation: progressIndeterminate 1s linear infinite;
+}
+
+@-moz-keyframes progressIndeterminate {
+  from { background-position: 0px 0px; }
+  to { background-position: 75px 0px; }
+}
+
+@-webkit-keyframes progressIndeterminate {
+  from { background-position: 0px 0px; }
+  to { background-position: 75px 0px; }
+}
+
+.textLayer {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  color: #000;
+  font-family: sans-serif;
+  overflow: hidden;
+}
+
+.textLayer > div {
+  color: transparent;
+  position: absolute;
+  line-height: 1;
+  white-space: pre;
+  cursor: text;
+}
+
+.textLayer .highlight {
+  margin: -1px;
+  padding: 1px;
+
+  background-color: rgba(180, 0, 170, 0.2);
+  border-radius: 4px;
+}
+
+.textLayer .highlight.begin {
+  border-radius: 4px 0px 0px 4px;
+}
+
+.textLayer .highlight.end {
+  border-radius: 0px 4px 4px 0px;
+}
+
+.textLayer .highlight.middle {
+  border-radius: 0px;
+}
+
+.textLayer .highlight.selected {
+  background-color: rgba(0, 100, 0, 0.2);
+}
+
+/* TODO: file FF bug to support ::-moz-selection:window-inactive
+   so we can override the opaque grey background when the window is inactive;
+   see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */
+::selection { background:rgba(0,0,255,0.3); }
+::-moz-selection { background:rgba(0,0,255,0.3); }
+
+.annotText > div {
+  z-index: 200;
+  position: absolute;
+  padding: 0.6em;
+  max-width: 20em;
+  background-color: #FFFF99;
+  box-shadow: 0px 2px 10px #333;
+  border-radius: 7px;
+}
+
+.annotText > img {
+  position: absolute;
+  opacity: 0.6;
+}
+
+.annotText > img:hover {
+  cursor: pointer;
+  opacity: 1;
+}
+
+.annotText > div > h1 {
+  font-size: 1.2em;
+  border-bottom: 1px solid #000000;
+  margin: 0px;
+}
+
+#errorWrapper {
+  background: none repeat scroll 0 0 #FF5555;
+  color: white;
+  left: 0;
+  position: absolute;
+  right: 0;
+  top: 32px;
+  z-index: 1000;
+  padding: 3px;
+  font-size: 0.8em;
+}
+
+#errorMessageLeft {
+  float: left;
+}
+
+#errorMessageRight {
+  float: right;
+}
+
+#errorMoreInfo {
+  background-color: #FFFFFF;
+  color: black;
+  padding: 3px;
+  margin: 3px;
+  width: 98%;
+}
+
+.clearBoth {
+  clear: both;
+}
+
+.fileInput {
+  background: white;
+  color: black;
+  margin-top: 5px;
+}
+
+#PDFBug {
+  background: none repeat scroll 0 0 white;
+  border: 1px solid #666666;
+  position: fixed;
+  top: 32px;
+  right: 0;
+  bottom: 0;
+  font-size: 10px;
+  padding: 0;
+  width: 300px;
+}
+#PDFBug .controls {
+    background:#EEEEEE;
+    border-bottom: 1px solid #666666;
+    padding: 3px;
+}
+#PDFBug .panels {
+  bottom: 0;
+  left: 0;
+  overflow: auto;
+  position: absolute;
+  right: 0;
+  top: 27px;
+}
+#PDFBug button.active {
+  font-weight: bold;
+}
+.debuggerShowText {
+  background: none repeat scroll 0 0 yellow;
+  color: blue;
+  opacity: 0.3;
+}
+.debuggerHideText:hover {
+  background: none repeat scroll 0 0 yellow;
+  opacity: 0.3;
+}
+#PDFBug .stats {
+  font-family: courier;
+  font-size: 10px;
+  white-space: pre;
+}
+#PDFBug .stats .title {
+    font-weight: bold;
+}
+#PDFBug table {
+  font-size: 10px;
+}
+
+#viewer.textLayer-visible .textLayer > div,
+#viewer.textLayer-hover .textLayer > div:hover {
+  background-color: white;
+  color: black;
+}
+
+#viewer.textLayer-shadow .textLayer > div {
+  background-color: rgba(255,255,255, .6);
+  color: black;
+}
+
+@page {
+  margin: 0;
+}
+
+#printContainer {
+  display: none;
+}
+
+@media print {
+  /* Rules for browsers that don't support mozPrintCallback. */
+  #sidebarContainer, .toolbar, #loadingBox, #errorWrapper, .textLayer {
+    display: none;
+  }
+
+  #mainContainer, #viewerContainer, .page, .page canvas {
+    position: static;
+    padding: 0;
+    margin: 0;
+  }
+
+  .page {
+    float: left;
+    display: none;
+    box-shadow: none;
+  }
+
+  .page[data-loaded] {
+    display: block;
+  }
+
+  /* Rules for browsers that support mozPrintCallback */
+  body[data-mozPrintCallback] #outerContainer {
+    display: none;
+  }
+  body[data-mozPrintCallback] #printContainer {
+    display: block;
+  }
+  #printContainer canvas {
+    position: relative;
+    top: 0;
+    left: 0;
+  }
+}
+
+@media all and (max-width: 950px) {
+  html[dir='ltr'] #outerContainer.sidebarMoving .outerCenter,
+  html[dir='ltr'] #outerContainer.sidebarOpen .outerCenter {
+    float: left;
+    left: 180px;
+  }
+  html[dir='rtl'] #outerContainer.sidebarMoving .outerCenter,
+  html[dir='rtl'] #outerContainer.sidebarOpen .outerCenter {
+    float: right;
+    right: 180px;
+  }
+}
+
+@media all and (max-width: 770px) {
+  #sidebarContainer {
+    top: 33px;
+    z-index: 100;
+  }
+  #sidebarContent {
+    top: 32px;
+    background-color: hsla(0,0%,0%,.7);
+  }
+
+  html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer {
+    left: 0px;
+  }
+  html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer {
+    right: 0px;
+  }
+
+  html[dir='ltr'] .outerCenter {
+    float: left;
+    left: 180px;
+  }
+  html[dir='rtl'] .outerCenter {
+    float: right;
+    right: 180px;
+  }
+}
+
+@media all and (max-width: 600px) {
+  .hiddenSmallView {
+    display: none;
+  }
+  html[dir='ltr'] .outerCenter {
+    left: 156px;
+  }
+  html[dir='rtr'] .outerCenter {
+    right: 156px;
+  }
+  .toolbarButtonSpacer {
+    width: 0;
+  }
+}
+
+@media all and (max-width: 500px) {
+  #scaleSelectContainer, #pageNumberLabel {
+    display: none;
+  }
+}
+
diff --git a/mediagoblin/static/extlib/pdf.js b/mediagoblin/static/extlib/pdf.js
new file mode 120000 (symlink)
index 0000000..f829660
--- /dev/null
@@ -0,0 +1 @@
+../../../extlib/pdf.js
\ No newline at end of file
diff --git a/mediagoblin/static/js/pdf_viewer.js b/mediagoblin/static/js/pdf_viewer.js
new file mode 100644 (file)
index 0000000..79c1e70
--- /dev/null
@@ -0,0 +1,3615 @@
+/* -*- 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 PDFJS, PDFBug, FirefoxCom, Stats */
+
+'use strict';
+
+var DEFAULT_SCALE = 'auto';
+var DEFAULT_SCALE_DELTA = 1.1;
+var UNKNOWN_SCALE = 0;
+var CACHE_SIZE = 20;
+var CSS_UNITS = 96.0 / 72.0;
+var SCROLLBAR_PADDING = 40;
+var VERTICAL_PADDING = 5;
+var MIN_SCALE = 0.25;
+var MAX_SCALE = 4.0;
+var IMAGE_DIR = './images/';
+var SETTINGS_MEMORY = 20;
+var ANNOT_MIN_SIZE = 10;
+var RenderingStates = {
+  INITIAL: 0,
+  RUNNING: 1,
+  PAUSED: 2,
+  FINISHED: 3
+};
+var FindStates = {
+  FIND_FOUND: 0,
+  FIND_NOTFOUND: 1,
+  FIND_WRAPPED: 2,
+  FIND_PENDING: 3
+};
+
+//#if (FIREFOX || MOZCENTRAL || B2G || GENERIC || CHROME)
+//PDFJS.workerSrc = '../build/pdf.js';
+//#endif
+
+var mozL10n = document.mozL10n || document.webL10n;
+
+function getFileName(url) {
+  var anchor = url.indexOf('#');
+  var query = url.indexOf('?');
+  var end = Math.min(
+    anchor > 0 ? anchor : url.length,
+    query > 0 ? query : url.length);
+  return url.substring(url.lastIndexOf('/', end) + 1, end);
+}
+
+function scrollIntoView(element, spot) {
+  // Assuming offsetParent is available (it's not available when viewer is in
+  // hidden iframe or object). We have to scroll: if the offsetParent is not set
+  // producing the error. See also animationStartedClosure.
+  var parent = element.offsetParent;
+  var offsetY = element.offsetTop + element.clientTop;
+  if (!parent) {
+    console.error('offsetParent is not set -- cannot scroll');
+    return;
+  }
+  while (parent.clientHeight == parent.scrollHeight) {
+    offsetY += parent.offsetTop;
+    parent = parent.offsetParent;
+    if (!parent)
+      return; // no need to scroll
+  }
+  if (spot)
+    offsetY += spot.top;
+  parent.scrollTop = offsetY;
+}
+
+var Cache = function cacheCache(size) {
+  var data = [];
+  this.push = function cachePush(view) {
+    var i = data.indexOf(view);
+    if (i >= 0)
+      data.splice(i);
+    data.push(view);
+    if (data.length > size)
+      data.shift().destroy();
+  };
+};
+
+var ProgressBar = (function ProgressBarClosure() {
+
+  function clamp(v, min, max) {
+    return Math.min(Math.max(v, min), max);
+  }
+
+  function ProgressBar(id, opts) {
+
+    // Fetch the sub-elements for later
+    this.div = document.querySelector(id + ' .progress');
+
+    // Get options, with sensible defaults
+    this.height = opts.height || 100;
+    this.width = opts.width || 100;
+    this.units = opts.units || '%';
+
+    // Initialize heights
+    this.div.style.height = this.height + this.units;
+  }
+
+  ProgressBar.prototype = {
+
+    updateBar: function ProgressBar_updateBar() {
+      if (this._indeterminate) {
+        this.div.classList.add('indeterminate');
+        return;
+      }
+
+      var progressSize = this.width * this._percent / 100;
+
+      if (this._percent > 95)
+        this.div.classList.add('full');
+      else
+        this.div.classList.remove('full');
+      this.div.classList.remove('indeterminate');
+
+      this.div.style.width = progressSize + this.units;
+    },
+
+    get percent() {
+      return this._percent;
+    },
+
+    set percent(val) {
+      this._indeterminate = isNaN(val);
+      this._percent = clamp(val, 0, 100);
+      this.updateBar();
+    }
+  };
+
+  return ProgressBar;
+})();
+
+//#if FIREFOX || MOZCENTRAL
+//#include firefoxcom.js
+//#endif
+
+// Settings Manager - This is a utility for saving settings
+// First we see if localStorage is available
+// If not, we use FUEL in FF
+// Use asyncStorage for B2G
+var Settings = (function SettingsClosure() {
+//#if !(FIREFOX || MOZCENTRAL || B2G)
+  var isLocalStorageEnabled = (function localStorageEnabledTest() {
+    // Feature test as per http://diveintohtml5.info/storage.html
+    // The additional localStorage call is to get around a FF quirk, see
+    // bug #495747 in bugzilla
+    try {
+      return 'localStorage' in window && window['localStorage'] !== null &&
+          localStorage;
+    } catch (e) {
+      return false;
+    }
+  })();
+//#endif
+
+  function Settings(fingerprint) {
+    this.fingerprint = fingerprint;
+    this.initializedPromise = new PDFJS.Promise();
+
+    var resolvePromise = (function settingsResolvePromise(db) {
+      this.initialize(db || '{}');
+      this.initializedPromise.resolve();
+    }).bind(this);
+
+//#if B2G
+//  asyncStorage.getItem('database', resolvePromise);
+//#endif
+
+//#if FIREFOX || MOZCENTRAL
+//  resolvePromise(FirefoxCom.requestSync('getDatabase', null));
+//#endif
+
+//#if !(FIREFOX || MOZCENTRAL || B2G)
+    if (isLocalStorageEnabled)
+      resolvePromise(localStorage.getItem('database'));
+//#endif
+  }
+
+  Settings.prototype = {
+    initialize: function settingsInitialize(database) {
+      database = JSON.parse(database);
+      if (!('files' in database))
+        database.files = [];
+      if (database.files.length >= SETTINGS_MEMORY)
+        database.files.shift();
+      var index;
+      for (var i = 0, length = database.files.length; i < length; i++) {
+        var branch = database.files[i];
+        if (branch.fingerprint == this.fingerprint) {
+          index = i;
+          break;
+        }
+      }
+      if (typeof index != 'number')
+        index = database.files.push({fingerprint: this.fingerprint}) - 1;
+      this.file = database.files[index];
+      this.database = database;
+    },
+
+    set: function settingsSet(name, val) {
+      if (!this.initializedPromise.isResolved)
+        return;
+
+      var file = this.file;
+      file[name] = val;
+      var database = JSON.stringify(this.database);
+
+//#if B2G
+//    asyncStorage.setItem('database', database);
+//#endif
+
+//#if FIREFOX || MOZCENTRAL
+//    FirefoxCom.requestSync('setDatabase', database);
+//#endif
+
+//#if !(FIREFOX || MOZCENTRAL || B2G)
+      if (isLocalStorageEnabled)
+        localStorage.setItem('database', database);
+//#endif
+    },
+
+    get: function settingsGet(name, defaultValue) {
+      if (!this.initializedPromise.isResolved)
+        return defaultValue;
+
+      return this.file[name] || defaultValue;
+    }
+  };
+
+  return Settings;
+})();
+
+var cache = new Cache(CACHE_SIZE);
+var currentPageNumber = 1;
+
+var PDFFindController = {
+  startedTextExtraction: false,
+
+  extractTextPromises: [],
+
+  // If active, find results will be highlighted.
+  active: false,
+
+  // Stores the text for each page.
+  pageContents: [],
+
+  pageMatches: [],
+
+  // Currently selected match.
+  selected: {
+    pageIdx: -1,
+    matchIdx: -1
+  },
+
+  // Where find algorithm currently is in the document.
+  offset: {
+    pageIdx: null,
+    matchIdx: null
+  },
+
+  resumePageIdx: null,
+
+  resumeCallback: null,
+
+  state: null,
+
+  dirtyMatch: false,
+
+  findTimeout: null,
+
+  initialize: function() {
+    var events = [
+      'find',
+      'findagain',
+      'findhighlightallchange',
+      'findcasesensitivitychange'
+    ];
+
+    this.handleEvent = this.handleEvent.bind(this);
+
+    for (var i = 0; i < events.length; i++) {
+      window.addEventListener(events[i], this.handleEvent);
+    }
+  },
+
+  calcFindMatch: function(pageIndex) {
+    var pageContent = this.pageContents[pageIndex];
+    var query = this.state.query;
+    var caseSensitive = this.state.caseSensitive;
+    var queryLen = query.length;
+
+    if (queryLen === 0) {
+      // Do nothing the matches should be wiped out already.
+      return;
+    }
+
+    if (!caseSensitive) {
+      pageContent = pageContent.toLowerCase();
+      query = query.toLowerCase();
+    }
+
+    var matches = [];
+
+    var matchIdx = -queryLen;
+    while (true) {
+      matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
+      if (matchIdx === -1) {
+        break;
+      }
+
+      matches.push(matchIdx);
+    }
+    this.pageMatches[pageIndex] = matches;
+    this.updatePage(pageIndex);
+    if (this.resumePageIdx === pageIndex) {
+      var callback = this.resumeCallback;
+      this.resumePageIdx = null;
+      this.resumeCallback = null;
+      callback();
+    }
+  },
+
+  extractText: function() {
+    if (this.startedTextExtraction) {
+      return;
+    }
+    this.startedTextExtraction = true;
+
+    this.pageContents = [];
+    for (var i = 0, ii = PDFView.pdfDocument.numPages; i < ii; i++) {
+      this.extractTextPromises.push(new PDFJS.Promise());
+    }
+
+    var self = this;
+    function extractPageText(pageIndex) {
+      PDFView.pages[pageIndex].getTextContent().then(
+        function textContentResolved(data) {
+          // Build the find string.
+          var bidiTexts = data.bidiTexts;
+          var str = '';
+
+          for (var i = 0; i < bidiTexts.length; i++) {
+            str += bidiTexts[i].str;
+          }
+
+          // Store the pageContent as a string.
+          self.pageContents.push(str);
+
+          self.extractTextPromises[pageIndex].resolve(pageIndex);
+          if ((pageIndex + 1) < PDFView.pages.length)
+            extractPageText(pageIndex + 1);
+        }
+      );
+    }
+    extractPageText(0);
+    return this.extractTextPromise;
+  },
+
+  handleEvent: function(e) {
+    if (this.state === null || e.type !== 'findagain') {
+      this.dirtyMatch = true;
+    }
+    this.state = e.detail;
+    this.updateUIState(FindStates.FIND_PENDING);
+
+    this.extractText();
+
+    clearTimeout(this.findTimeout);
+    if (e.type === 'find') {
+      // Only trigger the find action after 250ms of silence.
+      this.findTimeout = setTimeout(this.nextMatch.bind(this), 250);
+    } else {
+      this.nextMatch();
+    }
+  },
+
+  updatePage: function(idx) {
+    var page = PDFView.pages[idx];
+
+    if (this.selected.pageIdx === idx) {
+      // If the page is selected, scroll the page into view, which triggers
+      // rendering the page, which adds the textLayer. Once the textLayer is
+      // build, it will scroll onto the selected match.
+      page.scrollIntoView();
+    }
+
+    if (page.textLayer) {
+      page.textLayer.updateMatches();
+    }
+  },
+
+  nextMatch: function() {
+    var pages = PDFView.pages;
+    var previous = this.state.findPrevious;
+    var numPages = PDFView.pages.length;
+
+    this.active = true;
+
+    if (this.dirtyMatch) {
+      // Need to recalculate the matches, reset everything.
+      this.dirtyMatch = false;
+      this.selected.pageIdx = this.selected.matchIdx = -1;
+      this.offset.pageIdx = previous ? numPages - 1 : 0;
+      this.offset.matchIdx = null;
+      this.hadMatch = false;
+      this.resumeCallback = null;
+      this.resumePageIdx = null;
+      this.pageMatches = [];
+      var self = this;
+
+      for (var i = 0; i < numPages; i++) {
+        // Wipe out any previous highlighted matches.
+        this.updatePage(i);
+
+        // As soon as the text is extracted start finding the matches.
+        this.extractTextPromises[i].onData(function(pageIdx) {
+          // Use a timeout since all the pages may already be extracted and we
+          // want to start highlighting before finding all the matches.
+          setTimeout(function() {
+            self.calcFindMatch(pageIdx);
+          });
+        });
+      }
+    }
+
+    // If there's no query there's no point in searching.
+    if (this.state.query === '') {
+      this.updateUIState(FindStates.FIND_FOUND);
+      return;
+    }
+
+    // If we're waiting on a page, we return since we can't do anything else.
+    if (this.resumeCallback) {
+      return;
+    }
+
+    var offset = this.offset;
+    // If there's already a matchIdx that means we are iterating through a
+    // page's matches.
+    if (offset.matchIdx !== null) {
+      var numPageMatches = this.pageMatches[offset.pageIdx].length;
+      if ((!previous && offset.matchIdx + 1 < numPageMatches) ||
+          (previous && offset.matchIdx > 0)) {
+        // The simple case, we just have advance the matchIdx to select the next
+        // match on the page.
+        this.hadMatch = true;
+        offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1;
+        this.updateMatch(true);
+        return;
+      }
+      // We went beyond the current page's matches, so we advance to the next
+      // page.
+      this.advanceOffsetPage(previous);
+    }
+    // Start searching through the page.
+    this.nextPageMatch();
+  },
+
+  nextPageMatch: function() {
+    if (this.resumePageIdx !== null)
+      console.error('There can only be one pending page.');
+
+    var matchesReady = function(matches) {
+      var offset = this.offset;
+      var numMatches = matches.length;
+      var previous = this.state.findPrevious;
+      if (numMatches) {
+        // There were matches for the page, so initialize the matchIdx.
+        this.hadMatch = true;
+        offset.matchIdx = previous ? numMatches - 1 : 0;
+        this.updateMatch(true);
+      } else {
+        // No matches attempt to search the next page.
+        this.advanceOffsetPage(previous);
+        if (offset.wrapped) {
+          offset.matchIdx = null;
+          if (!this.hadMatch) {
+            // No point in wrapping there were no matches.
+            this.updateMatch(false);
+            return;
+          }
+        }
+        // Search the next page.
+        this.nextPageMatch();
+      }
+    }.bind(this);
+
+    var pageIdx = this.offset.pageIdx;
+    var pageMatches = this.pageMatches;
+    if (!pageMatches[pageIdx]) {
+      // The matches aren't ready setup a callback so we can be notified,
+      // when they are ready.
+      this.resumeCallback = function() {
+        matchesReady(pageMatches[pageIdx]);
+      };
+      this.resumePageIdx = pageIdx;
+      return;
+    }
+    // The matches are finished already.
+    matchesReady(pageMatches[pageIdx]);
+  },
+
+  advanceOffsetPage: function(previous) {
+    var offset = this.offset;
+    var numPages = this.extractTextPromises.length;
+    offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1;
+    offset.matchIdx = null;
+    if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
+      offset.pageIdx = previous ? numPages - 1 : 0;
+      offset.wrapped = true;
+      return;
+    }
+  },
+
+  updateMatch: function(found) {
+    var state = FindStates.FIND_NOTFOUND;
+    var wrapped = this.offset.wrapped;
+    this.offset.wrapped = false;
+    if (found) {
+      var previousPage = this.selected.pageIdx;
+      this.selected.pageIdx = this.offset.pageIdx;
+      this.selected.matchIdx = this.offset.matchIdx;
+      state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND;
+      // Update the currently selected page to wipe out any selected matches.
+      if (previousPage !== -1 && previousPage !== this.selected.pageIdx) {
+        this.updatePage(previousPage);
+      }
+    }
+    this.updateUIState(state, this.state.findPrevious);
+    if (this.selected.pageIdx !== -1) {
+      this.updatePage(this.selected.pageIdx, true);
+    }
+  },
+
+  updateUIState: function(state, previous) {
+    if (PDFView.supportsIntegratedFind) {
+      FirefoxCom.request('updateFindControlState',
+                         {result: state, findPrevious: previous});
+      return;
+    }
+    PDFFindBar.updateUIState(state, previous);
+  }
+};
+
+var PDFFindBar = {
+  // TODO: Enable the FindBar *AFTER* the pagesPromise in the load function
+  // got resolved
+
+  opened: false,
+
+  initialize: function() {
+    this.bar = document.getElementById('findbar');
+    this.toggleButton = document.getElementById('viewFind');
+    this.findField = document.getElementById('findInput');
+    this.highlightAll = document.getElementById('findHighlightAll');
+    this.caseSensitive = document.getElementById('findMatchCase');
+    this.findMsg = document.getElementById('findMsg');
+    this.findStatusIcon = document.getElementById('findStatusIcon');
+
+    var self = this;
+    this.toggleButton.addEventListener('click', function() {
+      self.toggle();
+    });
+
+    this.findField.addEventListener('input', function() {
+      self.dispatchEvent('');
+    });
+
+    this.bar.addEventListener('keydown', function(evt) {
+      switch (evt.keyCode) {
+        case 13: // Enter
+          if (evt.target === self.findField) {
+            self.dispatchEvent('again', evt.shiftKey);
+          }
+          break;
+        case 27: // Escape
+          self.close();
+          break;
+      }
+    });
+
+    document.getElementById('findPrevious').addEventListener('click',
+      function() { self.dispatchEvent('again', true); }
+    );
+
+    document.getElementById('findNext').addEventListener('click', function() {
+      self.dispatchEvent('again', false);
+    });
+
+    this.highlightAll.addEventListener('click', function() {
+      self.dispatchEvent('highlightallchange');
+    });
+
+    this.caseSensitive.addEventListener('click', function() {
+      self.dispatchEvent('casesensitivitychange');
+    });
+  },
+
+  dispatchEvent: function(aType, aFindPrevious) {
+    var event = document.createEvent('CustomEvent');
+    event.initCustomEvent('find' + aType, true, true, {
+      query: this.findField.value,
+      caseSensitive: this.caseSensitive.checked,
+      highlightAll: this.highlightAll.checked,
+      findPrevious: aFindPrevious
+    });
+    return window.dispatchEvent(event);
+  },
+
+  updateUIState: function(state, previous) {
+    var notFound = false;
+    var findMsg = '';
+    var status = '';
+
+    switch (state) {
+      case FindStates.FIND_FOUND:
+        break;
+
+      case FindStates.FIND_PENDING:
+        status = 'pending';
+        break;
+
+      case FindStates.FIND_NOTFOUND:
+        findMsg = mozL10n.get('find_not_found', null, 'Phrase not found');
+        notFound = true;
+        break;
+
+      case FindStates.FIND_WRAPPED:
+        if (previous) {
+          findMsg = mozL10n.get('find_reached_top', null,
+                      'Reached top of document, continued from bottom');
+        } else {
+          findMsg = mozL10n.get('find_reached_bottom', null,
+                                'Reached end of document, continued from top');
+        }
+        break;
+    }
+
+    if (notFound) {
+      this.findField.classList.add('notFound');
+    } else {
+      this.findField.classList.remove('notFound');
+    }
+
+    this.findField.setAttribute('data-status', status);
+    this.findMsg.textContent = findMsg;
+  },
+
+  open: function() {
+    if (this.opened) return;
+
+    this.opened = true;
+    this.toggleButton.classList.add('toggled');
+    this.bar.classList.remove('hidden');
+    this.findField.select();
+    this.findField.focus();
+  },
+
+  close: function() {
+    if (!this.opened) return;
+
+    this.opened = false;
+    this.toggleButton.classList.remove('toggled');
+    this.bar.classList.add('hidden');
+
+    PDFFindController.active = false;
+  },
+
+  toggle: function() {
+    if (this.opened) {
+      this.close();
+    } else {
+      this.open();
+    }
+  }
+};
+
+var PDFView = {
+  pages: [],
+  thumbnails: [],
+  currentScale: UNKNOWN_SCALE,
+  currentScaleValue: null,
+  initialBookmark: document.location.hash.substring(1),
+  startedTextExtraction: false,
+  pageText: [],
+  container: null,
+  thumbnailContainer: null,
+  initialized: false,
+  fellback: false,
+  pdfDocument: null,
+  sidebarOpen: false,
+  pageViewScroll: null,
+  thumbnailViewScroll: null,
+  isFullscreen: false,
+  previousScale: null,
+  pageRotation: 0,
+  mouseScrollTimeStamp: 0,
+  mouseScrollDelta: 0,
+  lastScroll: 0,
+  previousPageNumber: 1,
+
+  // called once when the document is loaded
+  initialize: function pdfViewInitialize() {
+    var self = this;
+    var container = this.container = document.getElementById('viewerContainer');
+    this.pageViewScroll = {};
+    this.watchScroll(container, this.pageViewScroll, updateViewarea);
+
+    var thumbnailContainer = this.thumbnailContainer =
+                             document.getElementById('thumbnailView');
+    this.thumbnailViewScroll = {};
+    this.watchScroll(thumbnailContainer, this.thumbnailViewScroll,
+                     this.renderHighestPriority.bind(this));
+
+    PDFFindBar.initialize();
+    PDFFindController.initialize();
+
+    this.initialized = true;
+    container.addEventListener('scroll', function() {
+      self.lastScroll = Date.now();
+    }, false);
+  },
+
+  getPage: function pdfViewGetPage(n) {
+    return this.pdfDocument.getPage(n);
+  },
+
+  // Helper function to keep track whether a div was scrolled up or down and
+  // then call a callback.
+  watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) {
+    state.down = true;
+    state.lastY = viewAreaElement.scrollTop;
+    viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) {
+      var currentY = viewAreaElement.scrollTop;
+      var lastY = state.lastY;
+      if (currentY > lastY)
+        state.down = true;
+      else if (currentY < lastY)
+        state.down = false;
+      // else do nothing and use previous value
+      state.lastY = currentY;
+      callback();
+    }, true);
+  },
+
+  setScale: function pdfViewSetScale(val, resetAutoSettings, noScroll) {
+    if (val == this.currentScale)
+      return;
+
+    var pages = this.pages;
+    for (var i = 0; i < pages.length; i++)
+      pages[i].update(val * CSS_UNITS);
+
+    if (!noScroll && this.currentScale != val)
+      this.pages[this.page - 1].scrollIntoView();
+    this.currentScale = val;
+
+    var event = document.createEvent('UIEvents');
+    event.initUIEvent('scalechange', false, false, window, 0);
+    event.scale = val;
+    event.resetAutoSettings = resetAutoSettings;
+    window.dispatchEvent(event);
+  },
+
+  parseScale: function pdfViewParseScale(value, resetAutoSettings, noScroll) {
+    if ('custom' == value)
+      return;
+
+    var scale = parseFloat(value);
+    this.currentScaleValue = value;
+    if (scale) {
+      this.setScale(scale, true, noScroll);
+      return;
+    }
+
+    var container = this.container;
+    var currentPage = this.pages[this.page - 1];
+    if (!currentPage) {
+      return;
+    }
+
+    var pageWidthScale = (container.clientWidth - SCROLLBAR_PADDING) /
+                          currentPage.width * currentPage.scale / CSS_UNITS;
+    var pageHeightScale = (container.clientHeight - VERTICAL_PADDING) /
+                           currentPage.height * currentPage.scale / CSS_UNITS;
+    switch (value) {
+      case 'page-actual':
+        scale = 1;
+        break;
+      case 'page-width':
+        scale = pageWidthScale;
+        break;
+      case 'page-height':
+        scale = pageHeightScale;
+        break;
+      case 'page-fit':
+        scale = Math.min(pageWidthScale, pageHeightScale);
+        break;
+      case 'auto':
+        scale = Math.min(1.0, pageWidthScale);
+        break;
+    }
+    this.setScale(scale, resetAutoSettings, noScroll);
+
+    selectScaleOption(value);
+  },
+
+  zoomIn: function pdfViewZoomIn() {
+    var newScale = (this.currentScale * DEFAULT_SCALE_DELTA).toFixed(2);
+    newScale = Math.ceil(newScale * 10) / 10;
+    newScale = Math.min(MAX_SCALE, newScale);
+    this.parseScale(newScale, true);
+  },
+
+  zoomOut: function pdfViewZoomOut() {
+    var newScale = (this.currentScale / DEFAULT_SCALE_DELTA).toFixed(2);
+    newScale = Math.floor(newScale * 10) / 10;
+    newScale = Math.max(MIN_SCALE, newScale);
+    this.parseScale(newScale, true);
+  },
+
+  set page(val) {
+    var pages = this.pages;
+    var input = document.getElementById('pageNumber');
+    var event = document.createEvent('UIEvents');
+    event.initUIEvent('pagechange', false, false, window, 0);
+
+    if (!(0 < val && val <= pages.length)) {
+      this.previousPageNumber = val;
+      event.pageNumber = this.page;
+      window.dispatchEvent(event);
+      return;
+    }
+
+    pages[val - 1].updateStats();
+    this.previousPageNumber = currentPageNumber;
+    currentPageNumber = val;
+    event.pageNumber = val;
+    window.dispatchEvent(event);
+
+    // checking if the this.page was called from the updateViewarea function:
+    // avoiding the creation of two "set page" method (internal and public)
+    if (updateViewarea.inProgress)
+      return;
+
+    // Avoid scrolling the first page during loading
+    if (this.loading && val == 1)
+      return;
+
+    pages[val - 1].scrollIntoView();
+  },
+
+  get page() {
+    return currentPageNumber;
+  },
+
+  get supportsPrinting() {
+    var canvas = document.createElement('canvas');
+    var value = 'mozPrintCallback' in canvas;
+    // shadow
+    Object.defineProperty(this, 'supportsPrinting', { value: value,
+                                                      enumerable: true,
+                                                      configurable: true,
+                                                      writable: false });
+    return value;
+  },
+
+  get supportsFullscreen() {
+    var doc = document.documentElement;
+    var support = doc.requestFullscreen || doc.mozRequestFullScreen ||
+                  doc.webkitRequestFullScreen;
+
+    // Disable fullscreen button if we're in an iframe
+    if (!!window.frameElement)
+      support = false;
+
+    Object.defineProperty(this, 'supportsFullScreen', { value: support,
+                                                        enumerable: true,
+                                                        configurable: true,
+                                                        writable: false });
+    return support;
+  },
+
+  get supportsIntegratedFind() {
+    var support = false;
+//#if !(FIREFOX || MOZCENTRAL)
+//#else
+//  support = FirefoxCom.requestSync('supportsIntegratedFind');
+//#endif
+    Object.defineProperty(this, 'supportsIntegratedFind', { value: support,
+                                                            enumerable: true,
+                                                            configurable: true,
+                                                            writable: false });
+    return support;
+  },
+
+  get supportsDocumentFonts() {
+    var support = true;
+//#if !(FIREFOX || MOZCENTRAL)
+//#else
+//  support = FirefoxCom.requestSync('supportsDocumentFonts');
+//#endif
+    Object.defineProperty(this, 'supportsDocumentFonts', { value: support,
+                                                           enumerable: true,
+                                                           configurable: true,
+                                                           writable: false });
+    return support;
+  },
+
+  get isHorizontalScrollbarEnabled() {
+    var div = document.getElementById('viewerContainer');
+    return div.scrollWidth > div.clientWidth;
+  },
+
+  initPassiveLoading: function pdfViewInitPassiveLoading() {
+    if (!PDFView.loadingBar) {
+      PDFView.loadingBar = new ProgressBar('#loadingBar', {});
+    }
+
+    window.addEventListener('message', function window_message(e) {
+      var args = e.data;
+
+      if (typeof args !== 'object' || !('pdfjsLoadAction' in args))
+        return;
+      switch (args.pdfjsLoadAction) {
+        case 'progress':
+          PDFView.progress(args.loaded / args.total);
+          break;
+        case 'complete':
+          if (!args.data) {
+            PDFView.error(mozL10n.get('loading_error', null,
+                          'An error occurred while loading the PDF.'), e);
+            break;
+          }
+          PDFView.open(args.data, 0);
+          break;
+      }
+    });
+    FirefoxCom.requestSync('initPassiveLoading', null);
+  },
+
+  setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) {
+    this.url = url;
+    try {
+      this.setTitle(decodeURIComponent(getFileName(url)) || url);
+    } catch (e) {
+      // decodeURIComponent may throw URIError,
+      // fall back to using the unprocessed url in that case
+      this.setTitle(url);
+    }
+  },
+
+  setTitle: function pdfViewSetTitle(title) {
+    document.title = title;
+//#if B2G
+//  document.getElementById('activityTitle').textContent = title;
+//#endif
+  },
+
+  open: function pdfViewOpen(url, scale, password) {
+    var parameters = {password: password};
+    if (typeof url === 'string') { // URL
+      this.setTitleUsingUrl(url);
+      parameters.url = url;
+    } else if (url && 'byteLength' in url) { // ArrayBuffer
+      parameters.data = url;
+    }
+
+    if (!PDFView.loadingBar) {
+      PDFView.loadingBar = new ProgressBar('#loadingBar', {});
+    }
+
+    this.pdfDocument = null;
+    var self = this;
+    self.loading = true;
+    PDFJS.getDocument(parameters).then(
+      function getDocumentCallback(pdfDocument) {
+        self.load(pdfDocument, scale);
+        self.loading = false;
+      },
+      function getDocumentError(message, exception) {
+        if (exception && exception.name === 'PasswordException') {
+          if (exception.code === 'needpassword') {
+            var promptString = mozL10n.get('request_password', null,
+                                      'PDF is protected by a password:');
+            password = prompt(promptString);
+            if (password && password.length > 0) {
+              return PDFView.open(url, scale, password);
+            }
+          }
+        }
+
+        var loadingErrorMessage = mozL10n.get('loading_error', null,
+          'An error occurred while loading the PDF.');
+
+        if (exception && exception.name === 'InvalidPDFException') {
+          // change error message also for other builds
+          var loadingErrorMessage = mozL10n.get('invalid_file_error', null,
+                                        'Invalid or corrupted PDF file.');
+//#if B2G
+//        window.alert(loadingErrorMessage);
+//        return window.close();
+//#endif
+        }
+
+        if (exception && exception.name === 'MissingPDFException') {
+          // special message for missing PDF's
+          var loadingErrorMessage = mozL10n.get('missing_file_error', null,
+                                        'Missing PDF file.');
+
+//#if B2G
+//        window.alert(loadingErrorMessage);
+//        return window.close();
+//#endif
+        }
+
+        var loadingIndicator = document.getElementById('loading');
+        loadingIndicator.textContent = mozL10n.get('loading_error_indicator',
+          null, 'Error');
+        var moreInfo = {
+          message: message
+        };
+        self.error(loadingErrorMessage, moreInfo);
+        self.loading = false;
+      },
+      function getDocumentProgress(progressData) {
+        self.progress(progressData.loaded / progressData.total);
+      }
+    );
+  },
+
+  download: function pdfViewDownload() {
+    function noData() {
+      FirefoxCom.request('download', { originalUrl: url });
+    }
+    var url = this.url.split('#')[0];
+//#if !(FIREFOX || MOZCENTRAL)
+    url += '#pdfjs.action=download';
+    window.open(url, '_parent');
+//#else
+//  // Document isn't ready just try to download with the url.
+//  if (!this.pdfDocument) {
+//    noData();
+//    return;
+//  }
+//  this.pdfDocument.getData().then(
+//    function getDataSuccess(data) {
+//      var blob = PDFJS.createBlob(data.buffer, 'application/pdf');
+//      var blobUrl = window.URL.createObjectURL(blob);
+//
+//      FirefoxCom.request('download', { blobUrl: blobUrl, originalUrl: url },
+//        function response(err) {
+//          if (err) {
+//            // This error won't really be helpful because it's likely the
+//            // fallback won't work either (or is already open).
+//            PDFView.error('PDF failed to download.');
+//          }
+//          window.URL.revokeObjectURL(blobUrl);
+//        }
+//      );
+//    },
+//    noData // Error occurred try downloading with just the url.
+//  );
+//#endif
+  },
+
+  fallback: function pdfViewFallback() {
+//#if !(FIREFOX || MOZCENTRAL)
+//  return;
+//#else
+//  // Only trigger the fallback once so we don't spam the user with messages
+//  // for one PDF.
+//  if (this.fellback)
+//    return;
+//  this.fellback = true;
+//  var url = this.url.split('#')[0];
+//  FirefoxCom.request('fallback', url, function response(download) {
+//    if (!download)
+//      return;
+//    PDFView.download();
+//  });
+//#endif
+  },
+
+  navigateTo: function pdfViewNavigateTo(dest) {
+    if (typeof dest === 'string')
+      dest = this.destinations[dest];
+    if (!(dest instanceof Array))
+      return; // invalid destination
+    // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
+    var destRef = dest[0];
+    var pageNumber = destRef instanceof Object ?
+      this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1);
+    if (pageNumber > this.pages.length)
+      pageNumber = this.pages.length;
+    if (pageNumber) {
+      this.page = pageNumber;
+      var currentPage = this.pages[pageNumber - 1];
+      if (!this.isFullscreen) { // Avoid breaking fullscreen mode.
+        currentPage.scrollIntoView(dest);
+      }
+    }
+  },
+
+  getDestinationHash: function pdfViewGetDestinationHash(dest) {
+    if (typeof dest === 'string')
+      return PDFView.getAnchorUrl('#' + escape(dest));
+    if (dest instanceof Array) {
+      var destRef = dest[0]; // see navigateTo method for dest format
+      var pageNumber = destRef instanceof Object ?
+        this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
+        (destRef + 1);
+      if (pageNumber) {
+        var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber);
+        var destKind = dest[1];
+        if (typeof destKind === 'object' && 'name' in destKind &&
+            destKind.name == 'XYZ') {
+          var scale = (dest[4] || this.currentScale);
+          pdfOpenParams += '&zoom=' + (scale * 100);
+          if (dest[2] || dest[3]) {
+            pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
+          }
+        }
+        return pdfOpenParams;
+      }
+    }
+    return '';
+  },
+
+  /**
+   * For the firefox extension we prefix the full url on anchor links so they
+   * don't come up as resource:// urls and so open in new tab/window works.
+   * @param {String} anchor The anchor hash include the #.
+   */
+  getAnchorUrl: function getAnchorUrl(anchor) {
+//#if !(FIREFOX || MOZCENTRAL)
+    return anchor;
+//#else
+//  return this.url.split('#')[0] + anchor;
+//#endif
+  },
+
+  /**
+   * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
+   * @return {Object} The object with horizontal (sx) and vertical (sy)
+                      scales. The scaled property is set to false if scaling is
+                      not required, true otherwise.
+   */
+  getOutputScale: function pdfViewGetOutputDPI() {
+    var pixelRatio = 'devicePixelRatio' in window ? window.devicePixelRatio : 1;
+    return {
+      sx: pixelRatio,
+      sy: pixelRatio,
+      scaled: pixelRatio != 1
+    };
+  },
+
+  /**
+   * Show the error box.
+   * @param {String} message A message that is human readable.
+   * @param {Object} moreInfo (optional) Further information about the error
+   *                            that is more technical.  Should have a 'message'
+   *                            and optionally a 'stack' property.
+   */
+  error: function pdfViewError(message, moreInfo) {
+    var moreInfoText = mozL10n.get('error_version_info',
+      {version: PDFJS.version || '?', build: PDFJS.build || '?'},
+      'PDF.js v{{version}} (build: {{build}})') + '\n';
+    if (moreInfo) {
+      moreInfoText +=
+        mozL10n.get('error_message', {message: moreInfo.message},
+        'Message: {{message}}');
+      if (moreInfo.stack) {
+        moreInfoText += '\n' +
+          mozL10n.get('error_stack', {stack: moreInfo.stack},
+          'Stack: {{stack}}');
+      } else {
+        if (moreInfo.filename) {
+          moreInfoText += '\n' +
+            mozL10n.get('error_file', {file: moreInfo.filename},
+            'File: {{file}}');
+        }
+        if (moreInfo.lineNumber) {
+          moreInfoText += '\n' +
+            mozL10n.get('error_line', {line: moreInfo.lineNumber},
+            'Line: {{line}}');
+        }
+      }
+    }
+
+    var loadingBox = document.getElementById('loadingBox');
+    loadingBox.setAttribute('hidden', 'true');
+
+//#if !(FIREFOX || MOZCENTRAL)
+    var errorWrapper = document.getElementById('errorWrapper');
+    errorWrapper.removeAttribute('hidden');
+
+    var errorMessage = document.getElementById('errorMessage');
+    errorMessage.textContent = message;
+
+    var closeButton = document.getElementById('errorClose');
+    closeButton.onclick = function() {
+      errorWrapper.setAttribute('hidden', 'true');
+    };
+
+    var errorMoreInfo = document.getElementById('errorMoreInfo');
+    var moreInfoButton = document.getElementById('errorShowMore');
+    var lessInfoButton = document.getElementById('errorShowLess');
+    moreInfoButton.onclick = function() {
+      errorMoreInfo.removeAttribute('hidden');
+      moreInfoButton.setAttribute('hidden', 'true');
+      lessInfoButton.removeAttribute('hidden');
+    };
+    lessInfoButton.onclick = function() {
+      errorMoreInfo.setAttribute('hidden', 'true');
+      moreInfoButton.removeAttribute('hidden');
+      lessInfoButton.setAttribute('hidden', 'true');
+    };
+    moreInfoButton.removeAttribute('hidden');
+    lessInfoButton.setAttribute('hidden', 'true');
+    errorMoreInfo.value = moreInfoText;
+
+    errorMoreInfo.rows = moreInfoText.split('\n').length - 1;
+//#else
+//  console.error(message + '\n' + moreInfoText);
+//  this.fallback();
+//#endif
+  },
+
+  progress: function pdfViewProgress(level) {
+    var percent = Math.round(level * 100);
+    PDFView.loadingBar.percent = percent;
+  },
+
+  load: function pdfViewLoad(pdfDocument, scale) {
+    function bindOnAfterDraw(pageView, thumbnailView) {
+      // when page is painted, using the image as thumbnail base
+      pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() {
+        thumbnailView.setImage(pageView.canvas);
+      };
+    }
+
+    this.pdfDocument = pdfDocument;
+
+    var errorWrapper = document.getElementById('errorWrapper');
+    errorWrapper.setAttribute('hidden', 'true');
+
+    var loadingBox = document.getElementById('loadingBox');
+    loadingBox.setAttribute('hidden', 'true');
+    var loadingIndicator = document.getElementById('loading');
+    loadingIndicator.textContent = '';
+
+    var thumbsView = document.getElementById('thumbnailView');
+    thumbsView.parentNode.scrollTop = 0;
+
+    while (thumbsView.hasChildNodes())
+      thumbsView.removeChild(thumbsView.lastChild);
+
+    if ('_loadingInterval' in thumbsView)
+      clearInterval(thumbsView._loadingInterval);
+
+    var container = document.getElementById('viewer');
+    while (container.hasChildNodes())
+      container.removeChild(container.lastChild);
+
+    var pagesCount = pdfDocument.numPages;
+    var id = pdfDocument.fingerprint;
+    document.getElementById('numPages').textContent =
+      mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
+    document.getElementById('pageNumber').max = pagesCount;
+
+    PDFView.documentFingerprint = id;
+    var store = PDFView.store = new Settings(id);
+
+    this.pageRotation = 0;
+
+    var pages = this.pages = [];
+    this.pageText = [];
+    this.startedTextExtraction = false;
+    var pagesRefMap = this.pagesRefMap = {};
+    var thumbnails = this.thumbnails = [];
+
+    var pagesPromise = new PDFJS.Promise();
+    var self = this;
+
+    var firstPagePromise = pdfDocument.getPage(1);
+
+    // Fetch a single page so we can get a viewport that will be the default
+    // viewport for all pages
+    firstPagePromise.then(function(pdfPage) {
+      var viewport = pdfPage.getViewport(scale || 1.0);
+      var pagePromises = [];
+      for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
+        var viewportClone = viewport.clone();
+        var pageView = new PageView(container, pageNum, scale,
+                                    self.navigateTo.bind(self),
+                                    viewportClone);
+        var thumbnailView = new ThumbnailView(thumbsView, pageNum,
+                                              viewportClone);
+        bindOnAfterDraw(pageView, thumbnailView);
+        pages.push(pageView);
+        thumbnails.push(thumbnailView);
+      }
+
+      var event = document.createEvent('CustomEvent');
+      event.initCustomEvent('documentload', true, true, {});
+      window.dispatchEvent(event);
+
+      for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
+        var pagePromise = pdfDocument.getPage(pageNum);
+        pagePromise.then(function(pdfPage) {
+          var pageNum = pdfPage.pageNumber;
+          var pageView = pages[pageNum - 1];
+          if (!pageView.pdfPage) {
+            // The pdfPage might already be set if we've already entered
+            // pageView.draw()
+            pageView.setPdfPage(pdfPage);
+          }
+          var thumbnailView = thumbnails[pageNum - 1];
+          if (!thumbnailView.pdfPage) {
+            thumbnailView.setPdfPage(pdfPage);
+          }
+
+          var pageRef = pdfPage.ref;
+          var refStr = pageRef.num + ' ' + pageRef.gen + ' R';
+          pagesRefMap[refStr] = pdfPage.pageNumber;
+        });
+        pagePromises.push(pagePromise);
+      }
+
+      PDFJS.Promise.all(pagePromises).then(function(pages) {
+        pagesPromise.resolve(pages);
+      });
+    });
+
+    var storePromise = store.initializedPromise;
+    PDFJS.Promise.all([firstPagePromise, storePromise]).then(function() {
+      var storedHash = null;
+      if (store.get('exists', false)) {
+        var pageNum = store.get('page', '1');
+        var zoom = store.get('zoom', PDFView.currentScale);
+        var left = store.get('scrollLeft', '0');
+        var top = store.get('scrollTop', '0');
+
+        storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' +
+                    left + ',' + top;
+      }
+      self.setInitialView(storedHash, scale);
+    });
+
+    pagesPromise.then(function() {
+      if (PDFView.supportsPrinting) {
+        pdfDocument.getJavaScript().then(function(javaScript) {
+          if (javaScript.length) {
+            console.warn('Warning: JavaScript is not supported');
+            PDFView.fallback();
+          }
+          // Hack to support auto printing.
+          var regex = /\bprint\s*\(/g;
+          for (var i = 0, ii = javaScript.length; i < ii; i++) {
+            var js = javaScript[i];
+            if (js && regex.test(js)) {
+              setTimeout(function() {
+                window.print();
+              });
+              return;
+            }
+          }
+        });
+      }
+    });
+
+    var destinationsPromise = pdfDocument.getDestinations();
+    destinationsPromise.then(function(destinations) {
+      self.destinations = destinations;
+    });
+
+    // outline depends on destinations and pagesRefMap
+    var promises = [pagesPromise, destinationsPromise,
+                    PDFView.animationStartedPromise];
+    PDFJS.Promise.all(promises).then(function() {
+      pdfDocument.getOutline().then(function(outline) {
+        self.outline = new DocumentOutlineView(outline);
+      });
+
+      // Make all navigation keys work on document load,
+      // unless the viewer is embedded in another page.
+      if (window.parent.location === window.location) {
+        PDFView.container.focus();
+      }
+    });
+
+    pdfDocument.getMetadata().then(function(data) {
+      var info = data.info, metadata = data.metadata;
+      self.documentInfo = info;
+      self.metadata = metadata;
+
+      // Provides some basic debug information
+      console.log('PDF ' + pdfDocument.fingerprint + ' [' +
+                  info.PDFFormatVersion + ' ' + (info.Producer || '-') +
+                  ' / ' + (info.Creator || '-') + ']' +
+                  (PDFJS.version ? ' (PDF.js: ' + PDFJS.version + ')' : ''));
+
+      var pdfTitle;
+      if (metadata) {
+        if (metadata.has('dc:title'))
+          pdfTitle = metadata.get('dc:title');
+      }
+
+      if (!pdfTitle && info && info['Title'])
+        pdfTitle = info['Title'];
+
+      if (pdfTitle)
+        self.setTitle(pdfTitle + ' - ' + document.title);
+
+      if (info.IsAcroFormPresent) {
+        console.warn('Warning: AcroForm/XFA is not supported');
+        PDFView.fallback();
+      }
+    });
+  },
+
+  setInitialView: function pdfViewSetInitialView(storedHash, scale) {
+    // Reset the current scale, as otherwise the page's scale might not get
+    // updated if the zoom level stayed the same.
+    this.currentScale = 0;
+    this.currentScaleValue = null;
+    if (this.initialBookmark) {
+      this.setHash(this.initialBookmark);
+      this.initialBookmark = null;
+    }
+    else if (storedHash)
+      this.setHash(storedHash);
+    else if (scale) {
+      this.parseScale(scale, true);
+      this.page = 1;
+    }
+
+    if (PDFView.currentScale === UNKNOWN_SCALE) {
+      // Scale was not initialized: invalid bookmark or scale was not specified.
+      // Setting the default one.
+      this.parseScale(DEFAULT_SCALE, true);
+    }
+  },
+
+  renderHighestPriority: function pdfViewRenderHighestPriority() {
+    // Pages have a higher priority than thumbnails, so check them first.
+    var visiblePages = this.getVisiblePages();
+    var pageView = this.getHighestPriority(visiblePages, this.pages,
+                                           this.pageViewScroll.down);
+    if (pageView) {
+      this.renderView(pageView, 'page');
+      return;
+    }
+    // No pages needed rendering so check thumbnails.
+    if (this.sidebarOpen) {
+      var visibleThumbs = this.getVisibleThumbs();
+      var thumbView = this.getHighestPriority(visibleThumbs,
+                                              this.thumbnails,
+                                              this.thumbnailViewScroll.down);
+      if (thumbView)
+        this.renderView(thumbView, 'thumbnail');
+    }
+  },
+
+  getHighestPriority: function pdfViewGetHighestPriority(visible, views,
+                                                         scrolledDown) {
+    // The state has changed figure out which page has the highest priority to
+    // render next (if any).
+    // Priority:
+    // 1 visible pages
+    // 2 if last scrolled down page after the visible pages
+    // 2 if last scrolled up page before the visible pages
+    var visibleViews = visible.views;
+
+    var numVisible = visibleViews.length;
+    if (numVisible === 0) {
+      return false;
+    }
+    for (var i = 0; i < numVisible; ++i) {
+      var view = visibleViews[i].view;
+      if (!this.isViewFinished(view))
+        return view;
+    }
+
+    // All the visible views have rendered, try to render next/previous pages.
+    if (scrolledDown) {
+      var nextPageIndex = visible.last.id;
+      // ID's start at 1 so no need to add 1.
+      if (views[nextPageIndex] && !this.isViewFinished(views[nextPageIndex]))
+        return views[nextPageIndex];
+    } else {
+      var previousPageIndex = visible.first.id - 2;
+      if (views[previousPageIndex] &&
+          !this.isViewFinished(views[previousPageIndex]))
+        return views[previousPageIndex];
+    }
+    // Everything that needs to be rendered has been.
+    return false;
+  },
+
+  isViewFinished: function pdfViewNeedsRendering(view) {
+    return view.renderingState === RenderingStates.FINISHED;
+  },
+
+  // Render a page or thumbnail view. This calls the appropriate function based
+  // on the views state. If the view is already rendered it will return false.
+  renderView: function pdfViewRender(view, type) {
+    var state = view.renderingState;
+    switch (state) {
+      case RenderingStates.FINISHED:
+        return false;
+      case RenderingStates.PAUSED:
+        PDFView.highestPriorityPage = type + view.id;
+        view.resume();
+        break;
+      case RenderingStates.RUNNING:
+        PDFView.highestPriorityPage = type + view.id;
+        break;
+      case RenderingStates.INITIAL:
+        PDFView.highestPriorityPage = type + view.id;
+        view.draw(this.renderHighestPriority.bind(this));
+        break;
+    }
+    return true;
+  },
+
+  setHash: function pdfViewSetHash(hash) {
+    if (!hash)
+      return;
+
+    if (hash.indexOf('=') >= 0) {
+      var params = PDFView.parseQueryString(hash);
+      // borrowing syntax from "Parameters for Opening PDF Files"
+      if ('nameddest' in params) {
+        PDFView.navigateTo(params.nameddest);
+        return;
+      }
+      if ('page' in params) {
+        var pageNumber = (params.page | 0) || 1;
+        if ('zoom' in params) {
+          var zoomArgs = params.zoom.split(','); // scale,left,top
+          // building destination array
+
+          // If the zoom value, it has to get divided by 100. If it is a string,
+          // it should stay as it is.
+          var zoomArg = zoomArgs[0];
+          var zoomArgNumber = parseFloat(zoomArg);
+          if (zoomArgNumber)
+            zoomArg = zoomArgNumber / 100;
+
+          var dest = [null, {name: 'XYZ'},
+                      zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
+                      zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
+                      zoomArg];
+          var currentPage = this.pages[pageNumber - 1];
+          currentPage.scrollIntoView(dest);
+        } else {
+          this.page = pageNumber; // simple page
+        }
+      }
+      if ('pagemode' in params) {
+        var toggle = document.getElementById('sidebarToggle');
+        if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks') {
+          if (!this.sidebarOpen) {
+            toggle.click();
+          }
+          this.switchSidebarView(params.pagemode === 'thumbs' ?
+                                 'thumbs' : 'outline');
+        } else if (params.pagemode === 'none' && this.sidebarOpen) {
+          toggle.click();
+        }
+      }
+    } else if (/^\d+$/.test(hash)) // page number
+      this.page = hash;
+    else // named destination
+      PDFView.navigateTo(unescape(hash));
+  },
+
+  switchSidebarView: function pdfViewSwitchSidebarView(view) {
+    var thumbsView = document.getElementById('thumbnailView');
+    var outlineView = document.getElementById('outlineView');
+
+    var thumbsButton = document.getElementById('viewThumbnail');
+    var outlineButton = document.getElementById('viewOutline');
+
+    switch (view) {
+      case 'thumbs':
+        var wasOutlineViewVisible = thumbsView.classList.contains('hidden');
+
+        thumbsButton.classList.add('toggled');
+        outlineButton.classList.remove('toggled');
+        thumbsView.classList.remove('hidden');
+        outlineView.classList.add('hidden');
+
+        PDFView.renderHighestPriority();
+
+        if (wasOutlineViewVisible) {
+          // Ensure that the thumbnail of the current page is visible
+          // when switching from the outline view.
+          scrollIntoView(document.getElementById('thumbnailContainer' +
+                                                 this.page));
+        }
+        break;
+
+      case 'outline':
+        thumbsButton.classList.remove('toggled');
+        outlineButton.classList.add('toggled');
+        thumbsView.classList.add('hidden');
+        outlineView.classList.remove('hidden');
+
+        if (outlineButton.getAttribute('disabled'))
+          return;
+        break;
+    }
+  },
+
+  getVisiblePages: function pdfViewGetVisiblePages() {
+    if (!this.isFullscreen) {
+      return this.getVisibleElements(this.container, this.pages, true);
+    } else {
+      // The algorithm in getVisibleElements is broken in fullscreen mode.
+      var visible = [], page = this.page;
+      var currentPage = this.pages[page - 1];
+      visible.push({ id: currentPage.id, view: currentPage });
+
+      return { first: currentPage, last: currentPage, views: visible};
+    }
+  },
+
+  getVisibleThumbs: function pdfViewGetVisibleThumbs() {
+    return this.getVisibleElements(this.thumbnailContainer, this.thumbnails);
+  },
+
+  // Generic helper to find out what elements are visible within a scroll pane.
+  getVisibleElements: function pdfViewGetVisibleElements(
+      scrollEl, views, sortByVisibility) {
+    var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
+    var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
+
+    var visible = [], view;
+    var currentHeight, viewHeight, hiddenHeight, percentHeight;
+    var currentWidth, viewWidth;
+    for (var i = 0, ii = views.length; i < ii; ++i) {
+      view = views[i];
+      currentHeight = view.el.offsetTop + view.el.clientTop;
+      viewHeight = view.el.clientHeight;
+      if ((currentHeight + viewHeight) < top) {
+        continue;
+      }
+      if (currentHeight > bottom) {
+        break;
+      }
+      currentWidth = view.el.offsetLeft + view.el.clientLeft;
+      viewWidth = view.el.clientWidth;
+      if ((currentWidth + viewWidth) < left || currentWidth > right) {
+        continue;
+      }
+      hiddenHeight = Math.max(0, top - currentHeight) +
+                     Math.max(0, currentHeight + viewHeight - bottom);
+      percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
+
+      visible.push({ id: view.id, y: currentHeight,
+                     view: view, percent: percentHeight });
+    }
+
+    var first = visible[0];
+    var last = visible[visible.length - 1];
+
+    if (sortByVisibility) {
+      visible.sort(function(a, b) {
+        var pc = a.percent - b.percent;
+        if (Math.abs(pc) > 0.001) {
+          return -pc;
+        }
+        return a.id - b.id; // ensure stability
+      });
+    }
+    return {first: first, last: last, views: visible};
+  },
+
+  // Helper function to parse query string (e.g. ?param1=value&parm2=...).
+  parseQueryString: function pdfViewParseQueryString(query) {
+    var parts = query.split('&');
+    var params = {};
+    for (var i = 0, ii = parts.length; i < parts.length; ++i) {
+      var param = parts[i].split('=');
+      var key = param[0];
+      var value = param.length > 1 ? param[1] : null;
+      params[unescape(key)] = unescape(value);
+    }
+    return params;
+  },
+
+  beforePrint: function pdfViewSetupBeforePrint() {
+    if (!this.supportsPrinting) {
+      var printMessage = mozL10n.get('printing_not_supported', null,
+          'Warning: Printing is not fully supported by this browser.');
+      this.error(printMessage);
+      return;
+    }
+
+    var alertNotReady = false;
+    if (!this.pages.length) {
+      alertNotReady = true;
+    } else {
+      for (var i = 0, ii = this.pages.length; i < ii; ++i) {
+        if (!this.pages[i].pdfPage) {
+          alertNotReady = true;
+          break;
+        }
+      }
+    }
+    if (alertNotReady) {
+      var notReadyMessage = mozL10n.get('printing_not_ready', null,
+          'Warning: The PDF is not fully loaded for printing.');
+      window.alert(notReadyMessage);
+      return;
+    }
+
+    var body = document.querySelector('body');
+    body.setAttribute('data-mozPrintCallback', true);
+    for (var i = 0, ii = this.pages.length; i < ii; ++i) {
+      this.pages[i].beforePrint();
+    }
+  },
+
+  afterPrint: function pdfViewSetupAfterPrint() {
+    var div = document.getElementById('printContainer');
+    while (div.hasChildNodes())
+      div.removeChild(div.lastChild);
+  },
+
+  fullscreen: function pdfViewFullscreen() {
+    var isFullscreen = document.fullscreenElement || document.mozFullScreen ||
+        document.webkitIsFullScreen;
+
+    if (isFullscreen) {
+      return false;
+    }
+
+    var wrapper = document.getElementById('viewerContainer');
+    if (document.documentElement.requestFullscreen) {
+      wrapper.requestFullscreen();
+    } else if (document.documentElement.mozRequestFullScreen) {
+      wrapper.mozRequestFullScreen();
+    } else if (document.documentElement.webkitRequestFullScreen) {
+      wrapper.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
+    } else {
+      return false;
+    }
+
+    this.isFullscreen = true;
+    var currentPage = this.pages[this.page - 1];
+    this.previousScale = this.currentScaleValue;
+    this.parseScale('page-fit', true);
+
+    // Wait for fullscreen to take effect
+    setTimeout(function() {
+      currentPage.scrollIntoView();
+    }, 0);
+
+    this.showPresentationControls();
+    return true;
+  },
+
+  exitFullscreen: function pdfViewExitFullscreen() {
+    this.isFullscreen = false;
+    this.parseScale(this.previousScale);
+    this.page = this.page;
+    this.clearMouseScrollState();
+    this.hidePresentationControls();
+
+    // Ensure that the thumbnail of the current page is visible
+    // when exiting fullscreen mode.
+    scrollIntoView(document.getElementById('thumbnailContainer' + this.page));
+  },
+
+  showPresentationControls: function pdfViewShowPresentationControls() {
+    var DELAY_BEFORE_HIDING_CONTROLS = 3000;
+    var wrapper = document.getElementById('viewerContainer');
+    if (this.presentationControlsTimeout) {
+      clearTimeout(this.presentationControlsTimeout);
+    } else {
+      wrapper.classList.add('presentationControls');
+    }
+    this.presentationControlsTimeout = setTimeout(function hideControls() {
+      wrapper.classList.remove('presentationControls');
+      delete PDFView.presentationControlsTimeout;
+    }, DELAY_BEFORE_HIDING_CONTROLS);
+  },
+
+  hidePresentationControls: function pdfViewShowPresentationControls() {
+    if (!this.presentationControlsTimeout) {
+      return;
+    }
+    clearTimeout(this.presentationControlsTimeout);
+    delete this.presentationControlsTimeout;
+
+    var wrapper = document.getElementById('viewerContainer');
+    wrapper.classList.remove('presentationControls');
+  },
+
+  rotatePages: function pdfViewPageRotation(delta) {
+
+    this.pageRotation = (this.pageRotation + 360 + delta) % 360;
+
+    for (var i = 0, l = this.pages.length; i < l; i++) {
+      var page = this.pages[i];
+      page.update(page.scale, this.pageRotation);
+    }
+
+    for (var i = 0, l = this.thumbnails.length; i < l; i++) {
+      var thumb = this.thumbnails[i];
+      thumb.update(this.pageRotation);
+    }
+
+    this.parseScale(this.currentScaleValue, true);
+
+    this.renderHighestPriority();
+
+    var currentPage = this.pages[this.page - 1];
+    if (!currentPage) {
+      return;
+    }
+
+    // Wait for fullscreen to take effect
+    setTimeout(function() {
+      currentPage.scrollIntoView();
+    }, 0);
+  },
+
+  /**
+   * This function flips the page in presentation mode if the user scrolls up
+   * or down with large enough motion and prevents page flipping too often.
+   *
+   * @this {PDFView}
+   * @param {number} mouseScrollDelta The delta value from the mouse event.
+   */
+  mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) {
+    var MOUSE_SCROLL_COOLDOWN_TIME = 50;
+
+    var currentTime = (new Date()).getTime();
+    var storedTime = this.mouseScrollTimeStamp;
+
+    // In case one page has already been flipped there is a cooldown time
+    // which has to expire before next page can be scrolled on to.
+    if (currentTime > storedTime &&
+        currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME)
+      return;
+
+    // In case the user decides to scroll to the opposite direction than before
+    // clear the accumulated delta.
+    if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) ||
+        (this.mouseScrollDelta < 0 && mouseScrollDelta > 0))
+      this.clearMouseScrollState();
+
+    this.mouseScrollDelta += mouseScrollDelta;
+
+    var PAGE_FLIP_THRESHOLD = 120;
+    if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) {
+
+      var PageFlipDirection = {
+        UP: -1,
+        DOWN: 1
+      };
+
+      // In fullscreen mode scroll one page at a time.
+      var pageFlipDirection = (this.mouseScrollDelta > 0) ?
+                                PageFlipDirection.UP :
+                                PageFlipDirection.DOWN;
+      this.clearMouseScrollState();
+      var currentPage = this.page;
+
+      // In case we are already on the first or the last page there is no need
+      // to do anything.
+      if ((currentPage == 1 && pageFlipDirection == PageFlipDirection.UP) ||
+          (currentPage == this.pages.length &&
+           pageFlipDirection == PageFlipDirection.DOWN))
+        return;
+
+      this.page += pageFlipDirection;
+      this.mouseScrollTimeStamp = currentTime;
+    }
+  },
+
+  /**
+   * This function clears the member attributes used with mouse scrolling in
+   * presentation mode.
+   *
+   * @this {PDFView}
+   */
+  clearMouseScrollState: function pdfViewClearMouseScrollState() {
+    this.mouseScrollTimeStamp = 0;
+    this.mouseScrollDelta = 0;
+  }
+};
+
+var PageView = function pageView(container, id, scale,
+                                 navigateTo, defaultViewport) {
+  this.id = id;
+
+  this.rotation = 0;
+  this.scale = scale || 1.0;
+  this.viewport = defaultViewport;
+  this.pdfPageRotate = defaultViewport.rotate;
+
+  this.renderingState = RenderingStates.INITIAL;
+  this.resume = null;
+
+  this.textContent = null;
+  this.textLayer = null;
+
+  var anchor = document.createElement('a');
+  anchor.name = '' + this.id;
+
+  var div = this.el = document.createElement('div');
+  div.id = 'pageContainer' + this.id;
+  div.className = 'page';
+  div.style.width = Math.floor(this.viewport.width) + 'px';
+  div.style.height = Math.floor(this.viewport.height) + 'px';
+
+  container.appendChild(anchor);
+  container.appendChild(div);
+
+  this.setPdfPage = function pageViewSetPdfPage(pdfPage) {
+    this.pdfPage = pdfPage;
+    this.pdfPageRotate = pdfPage.rotate;
+    this.viewport = pdfPage.getViewport(this.scale);
+    this.stats = pdfPage.stats;
+    this.update();
+  };
+
+  this.destroy = function pageViewDestroy() {
+    this.update();
+    if (this.pdfPage) {
+      this.pdfPage.destroy();
+    }
+  };
+
+  this.update = function pageViewUpdate(scale, rotation) {
+    this.renderingState = RenderingStates.INITIAL;
+    this.resume = null;
+
+    if (typeof rotation !== 'undefined') {
+      this.rotation = rotation;
+    }
+
+    this.scale = scale || this.scale;
+
+    var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
+    this.viewport = this.viewport.clone({
+      scale: this.scale,
+      rotation: totalRotation
+    });
+
+    div.style.width = Math.floor(this.viewport.width) + 'px';
+    div.style.height = Math.floor(this.viewport.height) + 'px';
+
+    while (div.hasChildNodes())
+      div.removeChild(div.lastChild);
+    div.removeAttribute('data-loaded');
+
+    delete this.canvas;
+
+    this.loadingIconDiv = document.createElement('div');
+    this.loadingIconDiv.className = 'loadingIcon';
+    div.appendChild(this.loadingIconDiv);
+  };
+
+  Object.defineProperty(this, 'width', {
+    get: function PageView_getWidth() {
+      return this.viewport.width;
+    },
+    enumerable: true
+  });
+
+  Object.defineProperty(this, 'height', {
+    get: function PageView_getHeight() {
+      return this.viewport.height;
+    },
+    enumerable: true
+  });
+
+  function setupAnnotations(pdfPage, viewport) {
+    function bindLink(link, dest) {
+      link.href = PDFView.getDestinationHash(dest);
+      link.onclick = function pageViewSetupLinksOnclick() {
+        if (dest)
+          PDFView.navigateTo(dest);
+        return false;
+      };
+    }
+    function createElementWithStyle(tagName, item, rect) {
+      if (!rect) {
+        rect = viewport.convertToViewportRectangle(item.rect);
+        rect = PDFJS.Util.normalizeRect(rect);
+      }
+      var element = document.createElement(tagName);
+      element.style.left = Math.floor(rect[0]) + 'px';
+      element.style.top = Math.floor(rect[1]) + 'px';
+      element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
+      element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
+      return element;
+    }
+    function createTextAnnotation(item) {
+      var container = document.createElement('section');
+      container.className = 'annotText';
+
+      var rect = viewport.convertToViewportRectangle(item.rect);
+      rect = PDFJS.Util.normalizeRect(rect);
+      // sanity check because of OOo-generated PDFs
+      if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) {
+        rect[3] = rect[1] + ANNOT_MIN_SIZE;
+      }
+      if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) {
+        rect[2] = rect[0] + (rect[3] - rect[1]); // make it square
+      }
+      var image = createElementWithStyle('img', item, rect);
+      var iconName = item.name;
+      image.src = IMAGE_DIR + 'annotation-' +
+        iconName.toLowerCase() + '.svg';
+      image.alt = mozL10n.get('text_annotation_type', {type: iconName},
+        '[{{type}} Annotation]');
+      var content = document.createElement('div');
+      content.setAttribute('hidden', true);
+      var title = document.createElement('h1');
+      var text = document.createElement('p');
+      content.style.left = Math.floor(rect[2]) + 'px';
+      content.style.top = Math.floor(rect[1]) + 'px';
+      title.textContent = item.title;
+
+      if (!item.content && !item.title) {
+        content.setAttribute('hidden', true);
+      } else {
+        var e = document.createElement('span');
+        var lines = item.content.split(/(?:\r\n?|\n)/);
+        for (var i = 0, ii = lines.length; i < ii; ++i) {
+          var line = lines[i];
+          e.appendChild(document.createTextNode(line));
+          if (i < (ii - 1))
+            e.appendChild(document.createElement('br'));
+        }
+        text.appendChild(e);
+        image.addEventListener('mouseover', function annotationImageOver() {
+           content.removeAttribute('hidden');
+        }, false);
+
+        image.addEventListener('mouseout', function annotationImageOut() {
+           content.setAttribute('hidden', true);
+        }, false);
+      }
+
+      content.appendChild(title);
+      content.appendChild(text);
+      container.appendChild(image);
+      container.appendChild(content);
+
+      return container;
+    }
+
+    pdfPage.getAnnotations().then(function(items) {
+      for (var i = 0; i < items.length; i++) {
+        var item = items[i];
+        switch (item.type) {
+          case 'Link':
+            var link = createElementWithStyle('a', item);
+            link.href = item.url || '';
+            if (!item.url)
+              bindLink(link, ('dest' in item) ? item.dest : null);
+            div.appendChild(link);
+            break;
+          case 'Text':
+            var textAnnotation = createTextAnnotation(item);
+            if (textAnnotation)
+              div.appendChild(textAnnotation);
+            break;
+        }
+      }
+    });
+  }
+
+  this.getPagePoint = function pageViewGetPagePoint(x, y) {
+    return this.viewport.convertToPdfPoint(x, y);
+  };
+
+  this.scrollIntoView = function pageViewScrollIntoView(dest) {
+      if (!dest) {
+        scrollIntoView(div);
+        return;
+      }
+
+      var x = 0, y = 0;
+      var width = 0, height = 0, widthScale, heightScale;
+      var scale = 0;
+      switch (dest[1].name) {
+        case 'XYZ':
+          x = dest[2];
+          y = dest[3];
+          scale = dest[4];
+          // If x and/or y coordinates are not supplied, default to
+          // _top_ left of the page (not the obvious bottom left,
+          // since aligning the bottom of the intended page with the
+          // top of the window is rarely helpful).
+          x = x !== null ? x : 0;
+          y = y !== null ? y : this.height / this.scale;
+          break;
+        case 'Fit':
+        case 'FitB':
+          scale = 'page-fit';
+          break;
+        case 'FitH':
+        case 'FitBH':
+          y = dest[2];
+          scale = 'page-width';
+          break;
+        case 'FitV':
+        case 'FitBV':
+          x = dest[2];
+          scale = 'page-height';
+          break;
+        case 'FitR':
+          x = dest[2];
+          y = dest[3];
+          width = dest[4] - x;
+          height = dest[5] - y;
+          widthScale = (this.container.clientWidth - SCROLLBAR_PADDING) /
+            width / CSS_UNITS;
+          heightScale = (this.container.clientHeight - SCROLLBAR_PADDING) /
+            height / CSS_UNITS;
+          scale = Math.min(widthScale, heightScale);
+          break;
+        default:
+          return;
+      }
+
+      if (scale && scale !== PDFView.currentScale)
+        PDFView.parseScale(scale, true, true);
+      else if (PDFView.currentScale === UNKNOWN_SCALE)
+        PDFView.parseScale(DEFAULT_SCALE, true, true);
+
+      var boundingRect = [
+        this.viewport.convertToViewportPoint(x, y),
+        this.viewport.convertToViewportPoint(x + width, y + height)
+      ];
+      setTimeout(function pageViewScrollIntoViewRelayout() {
+        // letting page to re-layout before scrolling
+        var scale = PDFView.currentScale;
+        var x = Math.min(boundingRect[0][0], boundingRect[1][0]);
+        var y = Math.min(boundingRect[0][1], boundingRect[1][1]);
+        var width = Math.abs(boundingRect[0][0] - boundingRect[1][0]);
+        var height = Math.abs(boundingRect[0][1] - boundingRect[1][1]);
+
+        scrollIntoView(div, {left: x, top: y, width: width, height: height});
+      }, 0);
+  };
+
+  this.getTextContent = function pageviewGetTextContent() {
+    if (!this.textContent) {
+      this.textContent = this.pdfPage.getTextContent();
+    }
+    return this.textContent;
+  };
+
+  this.draw = function pageviewDraw(callback) {
+    var pdfPage = this.pdfPage;
+
+    if (!pdfPage) {
+      var promise = PDFView.getPage(this.id);
+      promise.then(function(pdfPage) {
+        this.setPdfPage(pdfPage);
+        this.draw(callback);
+      }.bind(this));
+      return;
+    }
+
+    if (this.renderingState !== RenderingStates.INITIAL) {
+      console.error('Must be in new state before drawing');
+    }
+
+    this.renderingState = RenderingStates.RUNNING;
+
+    var canvas = document.createElement('canvas');
+    canvas.id = 'page' + this.id;
+    div.appendChild(canvas);
+    this.canvas = canvas;
+
+    var scale = this.scale, viewport = this.viewport;
+    var outputScale = PDFView.getOutputScale();
+    canvas.width = Math.floor(viewport.width) * outputScale.sx;
+    canvas.height = Math.floor(viewport.height) * outputScale.sy;
+
+    var textLayerDiv = null;
+    if (!PDFJS.disableTextLayer) {
+      textLayerDiv = document.createElement('div');
+      textLayerDiv.className = 'textLayer';
+      textLayerDiv.style.width = canvas.width + 'px';
+      textLayerDiv.style.height = canvas.height + 'px';
+      div.appendChild(textLayerDiv);
+    }
+    var textLayer = this.textLayer =
+          textLayerDiv ? new TextLayerBuilder(textLayerDiv, this.id - 1) : null;
+
+    if (outputScale.scaled) {
+      var cssScale = 'scale(' + (1 / outputScale.sx) + ', ' +
+                                (1 / outputScale.sy) + ')';
+      CustomStyle.setProp('transform' , canvas, cssScale);
+      CustomStyle.setProp('transformOrigin' , canvas, '0% 0%');
+      if (textLayerDiv) {
+        CustomStyle.setProp('transform' , textLayerDiv, cssScale);
+        CustomStyle.setProp('transformOrigin' , textLayerDiv, '0% 0%');
+      }
+    }
+
+    var ctx = canvas.getContext('2d');
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    // TODO(mack): use data attributes to store these
+    ctx._scaleX = outputScale.sx;
+    ctx._scaleY = outputScale.sy;
+    if (outputScale.scaled) {
+      ctx.scale(outputScale.sx, outputScale.sy);
+    }
+//#if (FIREFOX || MOZCENTRAL)
+//  // Checking if document fonts are used only once
+//  var checkIfDocumentFontsUsed = !PDFView.pdfDocument.embeddedFontsUsed;
+//#endif
+
+    // Rendering area
+
+    var self = this;
+    var renderingWasReset = false;
+    function pageViewDrawCallback(error) {
+      if (renderingWasReset) {
+        return;
+      }
+
+      self.renderingState = RenderingStates.FINISHED;
+
+      if (self.loadingIconDiv) {
+        div.removeChild(self.loadingIconDiv);
+        delete self.loadingIconDiv;
+      }
+
+//#if (FIREFOX || MOZCENTRAL)
+//    if (checkIfDocumentFontsUsed && PDFView.pdfDocument.embeddedFontsUsed &&
+//        !PDFView.supportsDocumentFonts) {
+//      console.error(mozL10n.get('web_fonts_disabled', null,
+//        'Web fonts are disabled: unable to use embedded PDF fonts.'));
+//      PDFView.fallback();
+//    }
+//#endif
+      if (error) {
+        PDFView.error(mozL10n.get('rendering_error', null,
+          'An error occurred while rendering the page.'), error);
+      }
+
+      self.stats = pdfPage.stats;
+      self.updateStats();
+      if (self.onAfterDraw)
+        self.onAfterDraw();
+
+      cache.push(self);
+
+      var event = document.createEvent('CustomEvent');
+      event.initCustomEvent('pagerender', true, true, {
+        pageNumber: pdfPage.pageNumber
+      });
+      div.dispatchEvent(event);
+
+      callback();
+    }
+
+    var renderContext = {
+      canvasContext: ctx,
+      viewport: this.viewport,
+      textLayer: textLayer,
+      continueCallback: function pdfViewcContinueCallback(cont) {
+        if (self.renderingState === RenderingStates.INITIAL) {
+          // The page update() was called, we just need to abort any rendering.
+          renderingWasReset = true;
+          return;
+        }
+
+        if (PDFView.highestPriorityPage !== 'page' + self.id) {
+          self.renderingState = RenderingStates.PAUSED;
+          self.resume = function resumeCallback() {
+            self.renderingState = RenderingStates.RUNNING;
+            cont();
+          };
+          return;
+        }
+        cont();
+      }
+    };
+    this.pdfPage.render(renderContext).then(
+      function pdfPageRenderCallback() {
+        pageViewDrawCallback(null);
+      },
+      function pdfPageRenderError(error) {
+        pageViewDrawCallback(error);
+      }
+    );
+
+    if (textLayer) {
+      this.getTextContent().then(
+        function textContentResolved(textContent) {
+          textLayer.setTextContent(textContent);
+        }
+      );
+    }
+
+    setupAnnotations(this.pdfPage, this.viewport);
+    div.setAttribute('data-loaded', true);
+  };
+
+  this.beforePrint = function pageViewBeforePrint() {
+    var pdfPage = this.pdfPage;
+
+    var viewport = pdfPage.getViewport(1);
+    // Use the same hack we use for high dpi displays for printing to get better
+    // output until bug 811002 is fixed in FF.
+    var PRINT_OUTPUT_SCALE = 2;
+    var canvas = this.canvas = document.createElement('canvas');
+    canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE;
+    canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE;
+    canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt';
+    canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt';
+    var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' +
+                              (1 / PRINT_OUTPUT_SCALE) + ')';
+    CustomStyle.setProp('transform' , canvas, cssScale);
+    CustomStyle.setProp('transformOrigin' , canvas, '0% 0%');
+
+    var printContainer = document.getElementById('printContainer');
+    printContainer.appendChild(canvas);
+
+    var self = this;
+    canvas.mozPrintCallback = function(obj) {
+      var ctx = obj.context;
+
+      ctx.save();
+      ctx.fillStyle = 'rgb(255, 255, 255)';
+      ctx.fillRect(0, 0, canvas.width, canvas.height);
+      ctx.restore();
+      ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE);
+
+      var renderContext = {
+        canvasContext: ctx,
+        viewport: viewport
+      };
+
+      pdfPage.render(renderContext).then(function() {
+        // Tell the printEngine that rendering this canvas/page has finished.
+        obj.done();
+        self.pdfPage.destroy();
+      }, function(error) {
+        console.error(error);
+        // Tell the printEngine that rendering this canvas/page has failed.
+        // This will make the print proces stop.
+        if ('abort' in obj)
+          obj.abort();
+        else
+          obj.done();
+        self.pdfPage.destroy();
+      });
+    };
+  };
+
+  this.updateStats = function pageViewUpdateStats() {
+    if (!this.stats) {
+      return;
+    }
+
+    if (PDFJS.pdfBug && Stats.enabled) {
+      var stats = this.stats;
+      Stats.add(this.id, stats);
+    }
+  };
+};
+
+var ThumbnailView = function thumbnailView(container, id, defaultViewport) {
+  var anchor = document.createElement('a');
+  anchor.href = PDFView.getAnchorUrl('#page=' + id);
+  anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}');
+  anchor.onclick = function stopNavigation() {
+    PDFView.page = id;
+    return false;
+  };
+
+
+  this.pdfPage = undefined;
+  this.viewport = defaultViewport;
+  this.pdfPageRotate = defaultViewport.rotate;
+
+  this.rotation = 0;
+  this.pageWidth = this.viewport.width;
+  this.pageHeight = this.viewport.height;
+  this.pageRatio = this.pageWidth / this.pageHeight;
+  this.id = id;
+
+  this.canvasWidth = 98;
+  this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
+  this.scale = (this.canvasWidth / this.pageWidth);
+
+  var div = this.el = document.createElement('div');
+  div.id = 'thumbnailContainer' + id;
+  div.className = 'thumbnail';
+
+  if (id === 1) {
+    // Highlight the thumbnail of the first page when no page number is
+    // specified (or exists in cache) when the document is loaded.
+    div.classList.add('selected');
+  }
+
+  var ring = document.createElement('div');
+  ring.className = 'thumbnailSelectionRing';
+  ring.style.width = this.canvasWidth + 'px';
+  ring.style.height = this.canvasHeight + 'px';
+
+  div.appendChild(ring);
+  anchor.appendChild(div);
+  container.appendChild(anchor);
+
+  this.hasImage = false;
+  this.renderingState = RenderingStates.INITIAL;
+
+  this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) {
+    this.pdfPage = pdfPage;
+    this.pdfPageRotate = pdfPage.rotate;
+    this.viewport = pdfPage.getViewport(1);
+    this.update();
+  };
+
+  this.update = function thumbnailViewUpdate(rot) {
+    if (!this.pdfPage) {
+      return;
+    }
+
+    if (rot !== undefined) {
+      this.rotation = rot;
+    }
+
+    var totalRotation = (this.rotation + this.pdfPage.rotate) % 360;
+    this.viewport = this.viewport.clone({
+      scale: 1,
+      rotation: totalRotation
+    });
+    this.pageWidth = this.viewport.width;
+    this.pageHeight = this.viewport.height;
+    this.pageRatio = this.pageWidth / this.pageHeight;
+
+    this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
+    this.scale = (this.canvasWidth / this.pageWidth);
+
+    div.removeAttribute('data-loaded');
+    ring.textContent = '';
+    ring.style.width = this.canvasWidth + 'px';
+    ring.style.height = this.canvasHeight + 'px';
+
+    this.hasImage = false;
+    this.renderingState = RenderingStates.INITIAL;
+    this.resume = null;
+  };
+
+  this.getPageDrawContext = function thumbnailViewGetPageDrawContext() {
+    var canvas = document.createElement('canvas');
+    canvas.id = 'thumbnail' + id;
+
+    canvas.width = this.canvasWidth;
+    canvas.height = this.canvasHeight;
+    canvas.className = 'thumbnailImage';
+    canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas',
+      {page: id}, 'Thumbnail of Page {{page}}'));
+
+    div.setAttribute('data-loaded', true);
+
+    ring.appendChild(canvas);
+
+    var ctx = canvas.getContext('2d');
+    ctx.save();
+    ctx.fillStyle = 'rgb(255, 255, 255)';
+    ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
+    ctx.restore();
+    return ctx;
+  };
+
+  this.drawingRequired = function thumbnailViewDrawingRequired() {
+    return !this.hasImage;
+  };
+
+  this.draw = function thumbnailViewDraw(callback) {
+    if (!this.pdfPage) {
+      var promise = PDFView.getPage(this.id);
+      promise.then(function(pdfPage) {
+        this.setPdfPage(pdfPage);
+        this.draw(callback);
+      }.bind(this));
+      return;
+    }
+
+    if (this.renderingState !== RenderingStates.INITIAL) {
+      console.error('Must be in new state before drawing');
+    }
+
+    this.renderingState = RenderingStates.RUNNING;
+    if (this.hasImage) {
+      callback();
+      return;
+    }
+
+    var self = this;
+    var ctx = this.getPageDrawContext();
+    var drawViewport = this.viewport.clone({ scale: this.scale });
+    var renderContext = {
+      canvasContext: ctx,
+      viewport: drawViewport,
+      continueCallback: function(cont) {
+        if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) {
+          self.renderingState = RenderingStates.PAUSED;
+          self.resume = function() {
+            self.renderingState = RenderingStates.RUNNING;
+            cont();
+          };
+          return;
+        }
+        cont();
+      }
+    };
+    this.pdfPage.render(renderContext).then(
+      function pdfPageRenderCallback() {
+        self.renderingState = RenderingStates.FINISHED;
+        callback();
+      },
+      function pdfPageRenderError(error) {
+        self.renderingState = RenderingStates.FINISHED;
+        callback();
+      }
+    );
+    this.hasImage = true;
+  };
+
+  this.setImage = function thumbnailViewSetImage(img) {
+    if (this.hasImage || !img)
+      return;
+    this.renderingState = RenderingStates.FINISHED;
+    var ctx = this.getPageDrawContext();
+    ctx.drawImage(img, 0, 0, img.width, img.height,
+                  0, 0, ctx.canvas.width, ctx.canvas.height);
+
+    this.hasImage = true;
+  };
+};
+
+var DocumentOutlineView = function documentOutlineView(outline) {
+  var outlineView = document.getElementById('outlineView');
+  while (outlineView.firstChild)
+    outlineView.removeChild(outlineView.firstChild);
+
+  function bindItemLink(domObj, item) {
+    domObj.href = PDFView.getDestinationHash(item.dest);
+    domObj.onclick = function documentOutlineViewOnclick(e) {
+      PDFView.navigateTo(item.dest);
+      return false;
+    };
+  }
+
+  if (!outline) {
+    var noOutline = document.createElement('div');
+    noOutline.classList.add('noOutline');
+    noOutline.textContent = mozL10n.get('no_outline', null,
+      'No Outline Available');
+    outlineView.appendChild(noOutline);
+    return;
+  }
+
+  var queue = [{parent: outlineView, items: outline}];
+  while (queue.length > 0) {
+    var levelData = queue.shift();
+    var i, n = levelData.items.length;
+    for (i = 0; i < n; i++) {
+      var item = levelData.items[i];
+      var div = document.createElement('div');
+      div.className = 'outlineItem';
+      var a = document.createElement('a');
+      bindItemLink(a, item);
+      a.textContent = item.title;
+      div.appendChild(a);
+
+      if (item.items.length > 0) {
+        var itemsDiv = document.createElement('div');
+        itemsDiv.className = 'outlineItems';
+        div.appendChild(itemsDiv);
+        queue.push({parent: itemsDiv, items: item.items});
+      }
+
+      levelData.parent.appendChild(div);
+    }
+  }
+};
+
+// optimised CSS custom property getter/setter
+var CustomStyle = (function CustomStyleClosure() {
+
+  // As noted on: http://www.zachstronaut.com/posts/2009/02/17/
+  //              animate-css-transforms-firefox-webkit.html
+  // in some versions of IE9 it is critical that ms appear in this list
+  // before Moz
+  var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
+  var _cache = { };
+
+  function CustomStyle() {
+  }
+
+  CustomStyle.getProp = function get(propName, element) {
+    // check cache only when no element is given
+    if (arguments.length == 1 && typeof _cache[propName] == 'string') {
+      return _cache[propName];
+    }
+
+    element = element || document.documentElement;
+    var style = element.style, prefixed, uPropName;
+
+    // test standard property first
+    if (typeof style[propName] == 'string') {
+      return (_cache[propName] = propName);
+    }
+
+    // capitalize
+    uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
+
+    // test vendor specific properties
+    for (var i = 0, l = prefixes.length; i < l; i++) {
+      prefixed = prefixes[i] + uPropName;
+      if (typeof style[prefixed] == 'string') {
+        return (_cache[propName] = prefixed);
+      }
+    }
+
+    //if all fails then set to undefined
+    return (_cache[propName] = 'undefined');
+  };
+
+  CustomStyle.setProp = function set(propName, element, str) {
+    var prop = this.getProp(propName);
+    if (prop != 'undefined')
+      element.style[prop] = str;
+  };
+
+  return CustomStyle;
+})();
+
+var TextLayerBuilder = function textLayerBuilder(textLayerDiv, pageIdx) {
+  var textLayerFrag = document.createDocumentFragment();
+
+  this.textLayerDiv = textLayerDiv;
+  this.layoutDone = false;
+  this.divContentDone = false;
+  this.pageIdx = pageIdx;
+  this.matches = [];
+
+  this.beginLayout = function textLayerBuilderBeginLayout() {
+    this.textDivs = [];
+    this.textLayerQueue = [];
+    this.renderingDone = false;
+  };
+
+  this.endLayout = function textLayerBuilderEndLayout() {
+    this.layoutDone = true;
+    this.insertDivContent();
+  };
+
+  this.renderLayer = function textLayerBuilderRenderLayer() {
+    var self = this;
+    var textDivs = this.textDivs;
+    var bidiTexts = this.textContent.bidiTexts;
+    var textLayerDiv = this.textLayerDiv;
+    var canvas = document.createElement('canvas');
+    var ctx = canvas.getContext('2d');
+
+    // No point in rendering so many divs as it'd make the browser unusable
+    // even after the divs are rendered
+    var MAX_TEXT_DIVS_TO_RENDER = 100000;
+    if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER)
+      return;
+
+    for (var i = 0, ii = textDivs.length; i < ii; i++) {
+      var textDiv = textDivs[i];
+      if ('isWhitespace' in textDiv.dataset) {
+        continue;
+      }
+      textLayerFrag.appendChild(textDiv);
+
+      ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily;
+      var width = ctx.measureText(textDiv.textContent).width;
+
+      if (width > 0) {
+        var textScale = textDiv.dataset.canvasWidth / width;
+
+        var transform = 'scale(' + textScale + ', 1)';
+        if (bidiTexts[i].dir === 'ttb') {
+          transform = 'rotate(90deg) ' + transform;
+        }
+        CustomStyle.setProp('transform' , textDiv, transform);
+        CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%');
+
+        textLayerDiv.appendChild(textDiv);
+      }
+    }
+
+    this.renderingDone = true;
+    this.updateMatches();
+
+    textLayerDiv.appendChild(textLayerFrag);
+  };
+
+  this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() {
+    // Schedule renderLayout() if user has been scrolling, otherwise
+    // run it right away
+    var RENDER_DELAY = 200; // in ms
+    var self = this;
+    if (Date.now() - PDFView.lastScroll > RENDER_DELAY) {
+      // Render right away
+      this.renderLayer();
+    } else {
+      // Schedule
+      if (this.renderTimer)
+        clearTimeout(this.renderTimer);
+      this.renderTimer = setTimeout(function() {
+        self.setupRenderLayoutTimer();
+      }, RENDER_DELAY);
+    }
+  };
+
+  this.appendText = function textLayerBuilderAppendText(geom) {
+    var textDiv = document.createElement('div');
+
+    // vScale and hScale already contain the scaling to pixel units
+    var fontHeight = geom.fontSize * Math.abs(geom.vScale);
+    textDiv.dataset.canvasWidth = geom.canvasWidth * geom.hScale;
+    textDiv.dataset.fontName = geom.fontName;
+
+    textDiv.style.fontSize = fontHeight + 'px';
+    textDiv.style.fontFamily = geom.fontFamily;
+    textDiv.style.left = geom.x + 'px';
+    textDiv.style.top = (geom.y - fontHeight) + 'px';
+
+    // The content of the div is set in the `setTextContent` function.
+
+    this.textDivs.push(textDiv);
+  };
+
+  this.insertDivContent = function textLayerUpdateTextContent() {
+    // Only set the content of the divs once layout has finished, the content
+    // for the divs is available and content is not yet set on the divs.
+    if (!this.layoutDone || this.divContentDone || !this.textContent)
+      return;
+
+    this.divContentDone = true;
+
+    var textDivs = this.textDivs;
+    var bidiTexts = this.textContent.bidiTexts;
+
+    for (var i = 0; i < bidiTexts.length; i++) {
+      var bidiText = bidiTexts[i];
+      var textDiv = textDivs[i];
+      if (!/\S/.test(bidiText.str)) {
+        textDiv.dataset.isWhitespace = true;
+        continue;
+      }
+
+      textDiv.textContent = bidiText.str;
+      // bidiText.dir may be 'ttb' for vertical texts.
+      textDiv.dir = bidiText.dir === 'rtl' ? 'rtl' : 'ltr';
+    }
+
+    this.setupRenderLayoutTimer();
+  };
+
+  this.setTextContent = function textLayerBuilderSetTextContent(textContent) {
+    this.textContent = textContent;
+    this.insertDivContent();
+  };
+
+  this.convertMatches = function textLayerBuilderConvertMatches(matches) {
+    var i = 0;
+    var iIndex = 0;
+    var bidiTexts = this.textContent.bidiTexts;
+    var end = bidiTexts.length - 1;
+    var queryLen = PDFFindController.state.query.length;
+
+    var lastDivIdx = -1;
+    var pos;
+
+    var ret = [];
+
+    // Loop over all the matches.
+    for (var m = 0; m < matches.length; m++) {
+      var matchIdx = matches[m];
+      // # Calculate the begin position.
+
+      // Loop over the divIdxs.
+      while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
+        iIndex += bidiTexts[i].str.length;
+        i++;
+      }
+
+      // TODO: Do proper handling here if something goes wrong.
+      if (i == bidiTexts.length) {
+        console.error('Could not find matching mapping');
+      }
+
+      var match = {
+        begin: {
+          divIdx: i,
+          offset: matchIdx - iIndex
+        }
+      };
+
+      // # Calculate the end position.
+      matchIdx += queryLen;
+
+      // Somewhat same array as above, but use a > instead of >= to get the end
+      // position right.
+      while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
+        iIndex += bidiTexts[i].str.length;
+        i++;
+      }
+
+      match.end = {
+        divIdx: i,
+        offset: matchIdx - iIndex
+      };
+      ret.push(match);
+    }
+
+    return ret;
+  };
+
+  this.renderMatches = function textLayerBuilder_renderMatches(matches) {
+    // Early exit if there is nothing to render.
+    if (matches.length === 0) {
+      return;
+    }
+
+    var bidiTexts = this.textContent.bidiTexts;
+    var textDivs = this.textDivs;
+    var prevEnd = null;
+    var isSelectedPage = this.pageIdx === PDFFindController.selected.pageIdx;
+    var selectedMatchIdx = PDFFindController.selected.matchIdx;
+    var highlightAll = PDFFindController.state.highlightAll;
+
+    var infty = {
+      divIdx: -1,
+      offset: undefined
+    };
+
+    function beginText(begin, className) {
+      var divIdx = begin.divIdx;
+      var div = textDivs[divIdx];
+      div.textContent = '';
+
+      var content = bidiTexts[divIdx].str.substring(0, begin.offset);
+      var node = document.createTextNode(content);
+      if (className) {
+        var isSelected = isSelectedPage &&
+                          divIdx === selectedMatchIdx;
+        var span = document.createElement('span');
+        span.className = className + (isSelected ? ' selected' : '');
+        span.appendChild(node);
+        div.appendChild(span);
+        return;
+      }
+      div.appendChild(node);
+    }
+
+    function appendText(from, to, className) {
+      var divIdx = from.divIdx;
+      var div = textDivs[divIdx];
+
+      var content = bidiTexts[divIdx].str.substring(from.offset, to.offset);
+      var node = document.createTextNode(content);
+      if (className) {
+        var span = document.createElement('span');
+        span.className = className;
+        span.appendChild(node);
+        div.appendChild(span);
+        return;
+      }
+      div.appendChild(node);
+    }
+
+    function highlightDiv(divIdx, className) {
+      textDivs[divIdx].className = className;
+    }
+
+    var i0 = selectedMatchIdx, i1 = i0 + 1, i;
+
+    if (highlightAll) {
+      i0 = 0;
+      i1 = matches.length;
+    } else if (!isSelectedPage) {
+      // Not highlighting all and this isn't the selected page, so do nothing.
+      return;
+    }
+
+    for (i = i0; i < i1; i++) {
+      var match = matches[i];
+      var begin = match.begin;
+      var end = match.end;
+
+      var isSelected = isSelectedPage && i === selectedMatchIdx;
+      var highlightSuffix = (isSelected ? ' selected' : '');
+      if (isSelected)
+        scrollIntoView(textDivs[begin.divIdx], {top: -50});
+
+      // Match inside new div.
+      if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
+        // If there was a previous div, then add the text at the end
+        if (prevEnd !== null) {
+          appendText(prevEnd, infty);
+        }
+        // clears the divs and set the content until the begin point.
+        beginText(begin);
+      } else {
+        appendText(prevEnd, begin);
+      }
+
+      if (begin.divIdx === end.divIdx) {
+        appendText(begin, end, 'highlight' + highlightSuffix);
+      } else {
+        appendText(begin, infty, 'highlight begin' + highlightSuffix);
+        for (var n = begin.divIdx + 1; n < end.divIdx; n++) {
+          highlightDiv(n, 'highlight middle' + highlightSuffix);
+        }
+        beginText(end, 'highlight end' + highlightSuffix);
+      }
+      prevEnd = end;
+    }
+
+    if (prevEnd) {
+      appendText(prevEnd, infty);
+    }
+  };
+
+  this.updateMatches = function textLayerUpdateMatches() {
+    // Only show matches, once all rendering is done.
+    if (!this.renderingDone)
+      return;
+
+    // Clear out all matches.
+    var matches = this.matches;
+    var textDivs = this.textDivs;
+    var bidiTexts = this.textContent.bidiTexts;
+    var clearedUntilDivIdx = -1;
+
+    // Clear out all current matches.
+    for (var i = 0; i < matches.length; i++) {
+      var match = matches[i];
+      var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
+      for (var n = begin; n <= match.end.divIdx; n++) {
+        var div = textDivs[n];
+        div.textContent = bidiTexts[n].str;
+        div.className = '';
+      }
+      clearedUntilDivIdx = match.end.divIdx + 1;
+    }
+
+    if (!PDFFindController.active)
+      return;
+
+    // Convert the matches on the page controller into the match format used
+    // for the textLayer.
+    this.matches = matches =
+      this.convertMatches(PDFFindController.pageMatches[this.pageIdx] || []);
+
+    this.renderMatches(this.matches);
+  };
+};
+
+document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
+  PDFView.initialize();
+  var params = PDFView.parseQueryString(document.location.search.substring(1));
+
+//#if !(FIREFOX || MOZCENTRAL)
+  var file = params.file || DEFAULT_URL;
+//#else
+//var file = window.location.toString()
+//#endif
+
+//#if !(FIREFOX || MOZCENTRAL)
+  if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
+    document.getElementById('openFile').setAttribute('hidden', 'true');
+  } else {
+    document.getElementById('fileInput').value = null;
+  }
+//#else
+//document.getElementById('openFile').setAttribute('hidden', 'true');
+//#endif
+
+  // Special debugging flags in the hash section of the URL.
+  var hash = document.location.hash.substring(1);
+  var hashParams = PDFView.parseQueryString(hash);
+
+  if ('disableWorker' in hashParams)
+    PDFJS.disableWorker = (hashParams['disableWorker'] === 'true');
+
+//#if !(FIREFOX || MOZCENTRAL)
+  var locale = navigator.language;
+  if ('locale' in hashParams)
+    locale = hashParams['locale'];
+  mozL10n.setLanguage(locale);
+//#endif
+
+  if ('textLayer' in hashParams) {
+    switch (hashParams['textLayer']) {
+      case 'off':
+        PDFJS.disableTextLayer = true;
+        break;
+      case 'visible':
+      case 'shadow':
+      case 'hover':
+        var viewer = document.getElementById('viewer');
+        viewer.classList.add('textLayer-' + hashParams['textLayer']);
+        break;
+    }
+  }
+
+//#if !(FIREFOX || MOZCENTRAL)
+  if ('pdfBug' in hashParams) {
+//#else
+//if ('pdfBug' in hashParams && FirefoxCom.requestSync('pdfBugEnabled')) {
+//#endif
+    PDFJS.pdfBug = true;
+    var pdfBug = hashParams['pdfBug'];
+    var enabled = pdfBug.split(',');
+    PDFBug.enable(enabled);
+    PDFBug.init();
+  }
+
+  if (!PDFView.supportsPrinting) {
+    document.getElementById('print').classList.add('hidden');
+  }
+
+  if (!PDFView.supportsFullscreen) {
+    document.getElementById('fullscreen').classList.add('hidden');
+  }
+
+  if (PDFView.supportsIntegratedFind) {
+    document.querySelector('#viewFind').classList.add('hidden');
+  }
+
+  // Listen for warnings to trigger the fallback UI.  Errors should be caught
+  // and call PDFView.error() so we don't need to listen for those.
+  PDFJS.LogManager.addLogger({
+    warn: function() {
+      PDFView.fallback();
+    }
+  });
+
+  var mainContainer = document.getElementById('mainContainer');
+  var outerContainer = document.getElementById('outerContainer');
+  mainContainer.addEventListener('transitionend', function(e) {
+    if (e.target == mainContainer) {
+      var event = document.createEvent('UIEvents');
+      event.initUIEvent('resize', false, false, window, 0);
+      window.dispatchEvent(event);
+      outerContainer.classList.remove('sidebarMoving');
+    }
+  }, true);
+
+  document.getElementById('sidebarToggle').addEventListener('click',
+    function() {
+      this.classList.toggle('toggled');
+      outerContainer.classList.add('sidebarMoving');
+      outerContainer.classList.toggle('sidebarOpen');
+      PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen');
+      PDFView.renderHighestPriority();
+    });
+
+  document.getElementById('viewThumbnail').addEventListener('click',
+    function() {
+      PDFView.switchSidebarView('thumbs');
+    });
+
+  document.getElementById('viewOutline').addEventListener('click',
+    function() {
+      PDFView.switchSidebarView('outline');
+    });
+
+  document.getElementById('previous').addEventListener('click',
+    function() {
+      PDFView.page--;
+    });
+
+  document.getElementById('next').addEventListener('click',
+    function() {
+      PDFView.page++;
+    });
+
+  document.querySelector('.zoomIn').addEventListener('click',
+    function() {
+      PDFView.zoomIn();
+    });
+
+  document.querySelector('.zoomOut').addEventListener('click',
+    function() {
+      PDFView.zoomOut();
+    });
+
+  document.getElementById('fullscreen').addEventListener('click',
+    function() {
+      PDFView.fullscreen();
+    });
+
+  document.getElementById('openFile').addEventListener('click',
+    function() {
+      document.getElementById('fileInput').click();
+    });
+
+  document.getElementById('print').addEventListener('click',
+    function() {
+      window.print();
+    });
+
+  document.getElementById('download').addEventListener('click',
+    function() {
+      PDFView.download();
+    });
+
+  document.getElementById('pageNumber').addEventListener('click',
+    function() {
+      this.select();
+    });
+
+  document.getElementById('pageNumber').addEventListener('change',
+    function() {
+      // Handle the user inputting a floating point number.
+      PDFView.page = (this.value | 0);
+
+      if (this.value !== (this.value | 0).toString()) {
+        this.value = PDFView.page;
+      }
+    });
+
+  document.getElementById('scaleSelect').addEventListener('change',
+    function() {
+      PDFView.parseScale(this.value);
+    });
+
+  document.getElementById('first_page').addEventListener('click',
+    function() {
+      PDFView.page = 1;
+    });
+
+  document.getElementById('last_page').addEventListener('click',
+    function() {
+      PDFView.page = PDFView.pdfDocument.numPages;
+    });
+
+  document.getElementById('page_rotate_ccw').addEventListener('click',
+    function() {
+      PDFView.rotatePages(-90);
+    });
+
+  document.getElementById('page_rotate_cw').addEventListener('click',
+    function() {
+      PDFView.rotatePages(90);
+    });
+
+//#if (FIREFOX || MOZCENTRAL)
+//if (FirefoxCom.requestSync('getLoadingType') == 'passive') {
+//  PDFView.setTitleUsingUrl(file);
+//  PDFView.initPassiveLoading();
+//  return;
+//}
+//#endif
+
+//#if !B2G
+  PDFView.open(file, 0);
+//#endif
+}, true);
+
+function updateViewarea() {
+
+  if (!PDFView.initialized)
+    return;
+  var visible = PDFView.getVisiblePages();
+  var visiblePages = visible.views;
+  if (visiblePages.length === 0) {
+    return;
+  }
+
+  PDFView.renderHighestPriority();
+
+  var currentId = PDFView.page;
+  var firstPage = visible.first;
+
+  for (var i = 0, ii = visiblePages.length, stillFullyVisible = false;
+       i < ii; ++i) {
+    var page = visiblePages[i];
+
+    if (page.percent < 100)
+      break;
+
+    if (page.id === PDFView.page) {
+      stillFullyVisible = true;
+      break;
+    }
+  }
+
+  if (!stillFullyVisible) {
+    currentId = visiblePages[0].id;
+  }
+
+  if (!PDFView.isFullscreen) {
+    updateViewarea.inProgress = true; // used in "set page"
+    PDFView.page = currentId;
+    updateViewarea.inProgress = false;
+  }
+
+  var currentScale = PDFView.currentScale;
+  var currentScaleValue = PDFView.currentScaleValue;
+  var normalizedScaleValue = currentScaleValue == currentScale ?
+    currentScale * 100 : currentScaleValue;
+
+  var pageNumber = firstPage.id;
+  var pdfOpenParams = '#page=' + pageNumber;
+  pdfOpenParams += '&zoom=' + normalizedScaleValue;
+  var currentPage = PDFView.pages[pageNumber - 1];
+  var topLeft = currentPage.getPagePoint(PDFView.container.scrollLeft,
+    (PDFView.container.scrollTop - firstPage.y));
+  pdfOpenParams += ',' + Math.round(topLeft[0]) + ',' + Math.round(topLeft[1]);
+
+  var store = PDFView.store;
+  store.initializedPromise.then(function() {
+    store.set('exists', true);
+    store.set('page', pageNumber);
+    store.set('zoom', normalizedScaleValue);
+    store.set('scrollLeft', Math.round(topLeft[0]));
+    store.set('scrollTop', Math.round(topLeft[1]));
+  });
+  var href = PDFView.getAnchorUrl(pdfOpenParams);
+  document.getElementById('viewBookmark').href = href;
+}
+
+window.addEventListener('resize', function webViewerResize(evt) {
+  if (PDFView.initialized &&
+      (document.getElementById('pageWidthOption').selected ||
+      document.getElementById('pageFitOption').selected ||
+      document.getElementById('pageAutoOption').selected))
+      PDFView.parseScale(document.getElementById('scaleSelect').value);
+  updateViewarea();
+});
+
+window.addEventListener('hashchange', function webViewerHashchange(evt) {
+  PDFView.setHash(document.location.hash.substring(1));
+});
+
+window.addEventListener('change', function webViewerChange(evt) {
+  var files = evt.target.files;
+  if (!files || files.length === 0)
+    return;
+
+  // Read the local file into a Uint8Array.
+  var fileReader = new FileReader();
+  fileReader.onload = function webViewerChangeFileReaderOnload(evt) {
+    var buffer = evt.target.result;
+    var uint8Array = new Uint8Array(buffer);
+    PDFView.open(uint8Array, 0);
+  };
+
+  var file = files[0];
+  fileReader.readAsArrayBuffer(file);
+  PDFView.setTitleUsingUrl(file.name);
+
+  // URL does not reflect proper document location - hiding some icons.
+  document.getElementById('viewBookmark').setAttribute('hidden', 'true');
+  document.getElementById('download').setAttribute('hidden', 'true');
+}, true);
+
+function selectScaleOption(value) {
+  var options = document.getElementById('scaleSelect').options;
+  var predefinedValueFound = false;
+  for (var i = 0; i < options.length; i++) {
+    var option = options[i];
+    if (option.value != value) {
+      option.selected = false;
+      continue;
+    }
+    option.selected = true;
+    predefinedValueFound = true;
+  }
+  return predefinedValueFound;
+}
+
+window.addEventListener('localized', function localized(evt) {
+  document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
+
+  // Adjust the width of the zoom box to fit the content.
+  PDFView.animationStartedPromise.then(
+    function() {
+      var container = document.getElementById('scaleSelectContainer');
+      var select = document.getElementById('scaleSelect');
+      select.setAttribute('style', 'min-width: inherit;');
+      var width = select.clientWidth + 8;
+      select.setAttribute('style', 'min-width: ' + (width + 20) + 'px;');
+      container.setAttribute('style', 'min-width: ' + width + 'px; ' +
+                                      'max-width: ' + width + 'px;');
+  });
+}, true);
+
+window.addEventListener('scalechange', function scalechange(evt) {
+  var customScaleOption = document.getElementById('customScaleOption');
+  customScaleOption.selected = false;
+
+  if (!evt.resetAutoSettings &&
+       (document.getElementById('pageWidthOption').selected ||
+        document.getElementById('pageFitOption').selected ||
+        document.getElementById('pageAutoOption').selected)) {
+      updateViewarea();
+      return;
+  }
+
+  var predefinedValueFound = selectScaleOption('' + evt.scale);
+  if (!predefinedValueFound) {
+    customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%';
+    customScaleOption.selected = true;
+  }
+  
+  document.getElementById('zoom_out').disabled = (evt.scale === MIN_SCALE);
+  document.getElementById('zoom_in').disabled = (evt.scale === MAX_SCALE);
+
+  updateViewarea();
+}, true);
+
+window.addEventListener('pagechange', function pagechange(evt) {
+  var page = evt.pageNumber;
+  if (PDFView.previousPageNumber !== page) {
+    document.getElementById('pageNumber').value = page;
+    var selected = document.querySelector('.thumbnail.selected');
+    if (selected)
+      selected.classList.remove('selected');
+    var thumbnail = document.getElementById('thumbnailContainer' + page);
+    thumbnail.classList.add('selected');
+    var visibleThumbs = PDFView.getVisibleThumbs();
+    var numVisibleThumbs = visibleThumbs.views.length;
+    // If the thumbnail isn't currently visible scroll it into view.
+    if (numVisibleThumbs > 0) {
+      var first = visibleThumbs.first.id;
+      // Account for only one thumbnail being visible.
+      var last = numVisibleThumbs > 1 ?
+                  visibleThumbs.last.id : first;
+      if (page <= first || page >= last)
+        scrollIntoView(thumbnail);
+    }
+
+  }
+  document.getElementById('previous').disabled = (page <= 1);
+  document.getElementById('next').disabled = (page >= PDFView.pages.length);
+}, true);
+
+// Firefox specific event, so that we can prevent browser from zooming
+window.addEventListener('DOMMouseScroll', function(evt) {
+  if (evt.ctrlKey) {
+    evt.preventDefault();
+
+    var ticks = evt.detail;
+    var direction = (ticks > 0) ? 'zoomOut' : 'zoomIn';
+    for (var i = 0, length = Math.abs(ticks); i < length; i++)
+      PDFView[direction]();
+  } else if (PDFView.isFullscreen) {
+    var FIREFOX_DELTA_FACTOR = -40;
+    PDFView.mouseScroll(evt.detail * FIREFOX_DELTA_FACTOR);
+  }
+}, false);
+
+window.addEventListener('mousemove', function mousemove(evt) {
+  if (PDFView.isFullscreen) {
+    PDFView.showPresentationControls();
+  }
+}, false);
+
+window.addEventListener('mousedown', function mousedown(evt) {
+  if (PDFView.isFullscreen && evt.button === 0) {
+    // Enable clicking of links in fullscreen mode.
+    // Note: Only links that point to the currently loaded PDF document works.
+    var targetHref = evt.target.href;
+    var internalLink = targetHref && (targetHref.replace(/#.*$/, '') ===
+                                      window.location.href.replace(/#.*$/, ''));
+    if (!internalLink) {
+      // Unless an internal link was clicked, advance a page in fullscreen mode.
+      evt.preventDefault();
+      PDFView.page++;
+    }
+  }
+}, false);
+
+window.addEventListener('click', function click(evt) {
+  if (PDFView.isFullscreen && evt.button === 0) {
+    // Necessary since preventDefault() in 'mousedown' won't stop
+    // the event propagation in all circumstances.
+    evt.preventDefault();
+  }
+}, false);
+
+window.addEventListener('keydown', function keydown(evt) {
+  var handled = false;
+  var cmd = (evt.ctrlKey ? 1 : 0) |
+            (evt.altKey ? 2 : 0) |
+            (evt.shiftKey ? 4 : 0) |
+            (evt.metaKey ? 8 : 0);
+
+  // First, handle the key bindings that are independent whether an input
+  // control is selected or not.
+  if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
+    // either CTRL or META key with optional SHIFT.
+    switch (evt.keyCode) {
+      case 70:
+        if (!PDFView.supportsIntegratedFind) {
+          PDFFindBar.toggle();
+          handled = true;
+        }
+        break;
+      case 61: // FF/Mac '='
+      case 107: // FF '+' and '='
+      case 187: // Chrome '+'
+      case 171: // FF with German keyboard
+        PDFView.zoomIn();
+        handled = true;
+        break;
+      case 173: // FF/Mac '-'
+      case 109: // FF '-'
+      case 189: // Chrome '-'
+        PDFView.zoomOut();
+        handled = true;
+        break;
+      case 48: // '0'
+      case 96: // '0' on Numpad of Swedish keyboard
+        PDFView.parseScale(DEFAULT_SCALE, true);
+        handled = false; // keeping it unhandled (to restore page zoom to 100%)
+        break;
+    }
+  }
+
+  // CTRL or META with or without SHIFT.
+  if (cmd == 1 || cmd == 8 || cmd == 5 || cmd == 12) {
+    switch (evt.keyCode) {
+      case 71: // g
+        if (!PDFView.supportsIntegratedFind) {
+          PDFFindBar.dispatchEvent('again', cmd == 5 || cmd == 12);
+          handled = true;
+        }
+        break;
+    }
+  }
+
+  if (handled) {
+    evt.preventDefault();
+    return;
+  }
+
+  // Some shortcuts should not get handled if a control/input element
+  // is selected.
+  var curElement = document.activeElement;
+  if (curElement && (curElement.tagName == 'INPUT' ||
+                     curElement.tagName == 'SELECT')) {
+    return;
+  }
+  var controlsElement = document.getElementById('toolbar');
+  while (curElement) {
+    if (curElement === controlsElement && !PDFView.isFullscreen)
+      return; // ignoring if the 'toolbar' element is focused
+    curElement = curElement.parentNode;
+  }
+
+  if (cmd === 0) { // no control key pressed at all.
+    switch (evt.keyCode) {
+      case 38: // up arrow
+      case 33: // pg up
+      case 8: // backspace
+        if (!PDFView.isFullscreen && PDFView.currentScaleValue !== 'page-fit') {
+          break;
+        }
+        /* in fullscreen mode */
+        /* falls through */
+      case 37: // left arrow
+        // horizontal scrolling using arrow keys
+        if (PDFView.isHorizontalScrollbarEnabled) {
+          break;
+        }
+        /* falls through */
+      case 75: // 'k'
+      case 80: // 'p'
+        PDFView.page--;
+        handled = true;
+        break;
+      case 27: // esc key
+        if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) {
+          PDFFindBar.close();
+          handled = true;
+        }
+        break;
+      case 40: // down arrow
+      case 34: // pg down
+      case 32: // spacebar
+        if (!PDFView.isFullscreen && PDFView.currentScaleValue !== 'page-fit') {
+          break;
+        }
+        /* falls through */
+      case 39: // right arrow
+        // horizontal scrolling using arrow keys
+        if (PDFView.isHorizontalScrollbarEnabled) {
+          break;
+        }
+        /* falls through */
+      case 74: // 'j'
+      case 78: // 'n'
+        PDFView.page++;
+        handled = true;
+        break;
+
+      case 36: // home
+        if (PDFView.isFullscreen) {
+          PDFView.page = 1;
+          handled = true;
+        }
+        break;
+      case 35: // end
+        if (PDFView.isFullscreen) {
+          PDFView.page = PDFView.pdfDocument.numPages;
+          handled = true;
+        }
+        break;
+
+      case 82: // 'r'
+        PDFView.rotatePages(90);
+        break;
+    }
+  }
+
+  if (cmd == 4) { // shift-key
+    switch (evt.keyCode) {
+      case 82: // 'r'
+        PDFView.rotatePages(-90);
+        break;
+    }
+  }
+
+  if (handled) {
+    evt.preventDefault();
+    PDFView.clearMouseScrollState();
+  }
+});
+
+window.addEventListener('beforeprint', function beforePrint(evt) {
+  PDFView.beforePrint();
+});
+
+window.addEventListener('afterprint', function afterPrint(evt) {
+  PDFView.afterPrint();
+});
+
+(function fullscreenClosure() {
+  function fullscreenChange(e) {
+    var isFullscreen = document.fullscreenElement || document.mozFullScreen ||
+        document.webkitIsFullScreen;
+
+    if (!isFullscreen) {
+      PDFView.exitFullscreen();
+    }
+  }
+
+  window.addEventListener('fullscreenchange', fullscreenChange, false);
+  window.addEventListener('mozfullscreenchange', fullscreenChange, false);
+  window.addEventListener('webkitfullscreenchange', fullscreenChange, false);
+})();
+
+(function animationStartedClosure() {
+  // The offsetParent is not set until the pdf.js iframe or object is visible.
+  // Waiting for first animation.
+  var requestAnimationFrame = window.requestAnimationFrame ||
+                              window.mozRequestAnimationFrame ||
+                              window.webkitRequestAnimationFrame ||
+                              window.oRequestAnimationFrame ||
+                              window.msRequestAnimationFrame ||
+                              function startAtOnce(callback) { callback(); };
+  PDFView.animationStartedPromise = new PDFJS.Promise();
+  requestAnimationFrame(function onAnimationFrame() {
+    PDFView.animationStartedPromise.resolve();
+  });
+})();
+
+//#if B2G
+//window.navigator.mozSetMessageHandler('activity', function(activity) {
+//  var url = activity.source.data.url;
+//  PDFView.open(url);
+//  var cancelButton = document.getElementById('activityClose');
+//  cancelButton.addEventListener('click', function() {
+//    activity.postResult('close');
+//  });
+//});
+//#endif
index 66b9566149cc14b2afc0f1c0efe2718fc479f7c5..9c42a7567d0956d9450e586e087c2de50eb7b3b8 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -#}
 <!doctype html>
-<html>
+<html
+{% block mediagoblin_html_tag %}
+{% endblock mediagoblin_html_tag %}
+>
   <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
diff --git a/mediagoblin/templates/mediagoblin/media_displays/pdf.html b/mediagoblin/templates/mediagoblin/media_displays/pdf.html
new file mode 100644 (file)
index 0000000..35a6187
--- /dev/null
@@ -0,0 +1,284 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#}
+
+{% extends 'mediagoblin/user_pages/media.html' %}
+
+{% set medium_view = request.app.public_store.file_url(
+   media.media_files['medium']) %}
+
+{% if 'pdf' in media.media_files %}
+    {% set pdf_view = request.app.public_store.file_url(
+        media.media_files['pdf']) %}
+{% else %}
+    {% set pdf_view = request.app.public_store.file_url(
+        media.media_files['original']) %}
+{% endif %}
+
+{% set pdf_js = global_config.get('media_type:mediagoblin.media_types.pdf', {}).get('pdf_js', False) %}
+
+{% if pdf_js %}
+    {% block mediagoblin_html_tag %}
+    dir="ltr" mozdisallowselectionprint moznomarginboxes
+    {% endblock mediagoblin_html_tag %}
+{% endif %}
+
+{% block mediagoblin_head -%}
+  {{ super() }}
+
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+
+{% if pdf_js %}
+        <link rel="stylesheet" href="{{ request.staticdirect('/css/pdf_viewer.css') }}"/>
+        {# <link rel="resource" type="application/l10n" href="locale/locale.properties"/> #}
+
+        <script type="text/javascript">
+    var DEFAULT_URL = '{{ pdf_view }}';
+        </script>
+
+        {# TODO: include compatibility only if this is not either chrome or firefox #}
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/web/compatibility.js') }}"></script>
+
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/external/webL10n/l10n.js') }}"></script>
+
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/core.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/util.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/api.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/metadata.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/canvas.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/obj.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/function.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/charsets.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/cidmaps.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/colorspace.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/crypto.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/evaluator.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/fonts.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/glyphlist.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/image.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/metrics.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/parser.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/pattern.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/stream.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/worker.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/external/jpgjs/jpg.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/jpx.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/jbig2.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/src/bidi.js') }}"></script>
+        <script type="text/javascript">PDFJS.workerSrc = '{{ request.staticdirect('/extlib/pdf.js/src/worker_loader.js') }}';</script>
+
+        <script type="text/javascript" src="{{ request.staticdirect('/extlib/pdf.js/web/debugger.js') }}"></script>
+        <script type="text/javascript" src="{{ request.staticdirect('/js/pdf_viewer.js') }}"></script>
+{% endif %}
+
+{%- endblock %}
+
+{% block mediagoblin_media %}
+{% if pdf_js %}
+    <div id="outerContainer">
+
+      <div id="sidebarContainer">
+        <div id="toolbarSidebar">
+          <div class="splitToolbarButton toggled">
+            <button id="viewThumbnail" class="toolbarButton group toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs">
+               <span data-l10n-id="thumbs_label">Thumbnails</span>
+            </button>
+            <button id="viewOutline" class="toolbarButton group" title="Show Document Outline" tabindex="3" data-l10n-id="outline">
+               <span data-l10n-id="outline_label">Document Outline</span>
+            </button>
+          </div>
+        </div>
+        <div id="sidebarContent">
+          <div id="thumbnailView">
+          </div>
+          <div id="outlineView" class="hidden">
+          </div>
+        </div>
+      </div>  <!-- sidebarContainer -->
+
+      <div id="mainContainer">
+        <div class="findbar hidden doorHanger hiddenSmallView" id="findbar">
+          <label for="findInput" class="toolbarLabel" data-l10n-id="find_label">Find:</label>
+          <input id="findInput" class="toolbarField" tabindex="21">
+          <div class="splitToolbarButton">
+            <button class="toolbarButton findPrevious" title="" id="findPrevious" tabindex="22" data-l10n-id="find_previous">
+              <span data-l10n-id="find_previous_label">Previous</span>
+            </button>
+            <div class="splitToolbarButtonSeparator"></div>
+            <button class="toolbarButton findNext" title="" id="findNext" tabindex="23" data-l10n-id="find_next">
+              <span data-l10n-id="find_next_label">Next</span>
+            </button>
+          </div>
+          <input type="checkbox" id="findHighlightAll" class="toolbarField">
+          <label for="findHighlightAll" class="toolbarLabel" tabindex="24" data-l10n-id="find_highlight">Highlight all</label>
+          <input type="checkbox" id="findMatchCase" class="toolbarField">
+          <label for="findMatchCase" class="toolbarLabel" tabindex="25" data-l10n-id="find_match_case_label">Match case</label>
+          <span id="findMsg" class="toolbarLabel"></span>
+        </div>
+        <div class="toolbar">
+          <div id="toolbarContainer">
+            <div id="toolbarViewer">
+              <div id="toolbarViewerLeft">
+                <button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="4" data-l10n-id="toggle_sidebar">
+                  <span data-l10n-id="toggle_sidebar_label">Toggle Sidebar</span>
+                </button>
+                <div class="toolbarButtonSpacer"></div>
+                <button id="viewFind" class="toolbarButton group hiddenSmallView" title="Find in Document" tabindex="5" data-l10n-id="findbar">
+                   <span data-l10n-id="findbar_label">Find</span>
+                </button>
+                <div class="splitToolbarButton">
+                  <button class="toolbarButton pageUp" title="Previous Page" id="previous" tabindex="6" data-l10n-id="previous">
+                    <span data-l10n-id="previous_label">Previous</span>
+                  </button>
+                  <div class="splitToolbarButtonSeparator"></div>
+                  <button class="toolbarButton pageDown" title="Next Page" id="next" tabindex="7" data-l10n-id="next">
+                    <span data-l10n-id="next_label">Next</span>
+                  </button>
+                </div>
+                <label id="pageNumberLabel" class="toolbarLabel" for="pageNumber" data-l10n-id="page_label">Page: </label>
+                <input type="number" id="pageNumber" class="toolbarField pageNumber" value="1" size="4" min="1" tabindex="8">
+                </input>
+                <span id="numPages" class="toolbarLabel"></span>
+              </div>
+              <div id="toolbarViewerRight">
+                <input id="fileInput" class="fileInput" type="file" oncontextmenu="return false;" style="visibility: hidden; position: fixed; right: 0; top: 0" />
+
+                <button id="fullscreen" class="toolbarButton fullscreen hiddenSmallView" title="Switch to Presentation Mode" tabindex="12" data-l10n-id="presentation_mode">
+                  <span data-l10n-id="presentation_mode_label">Presentation Mode</span>
+                </button>
+
+                <button id="openFile" class="toolbarButton openFile hiddenSmallView" title="Open File" tabindex="13" data-l10n-id="open_file">
+                   <span data-l10n-id="open_file_label">Open</span>
+                </button>
+
+                <button id="print" class="toolbarButton print" title="Print" tabindex="14" data-l10n-id="print">
+                  <span data-l10n-id="print_label">Print</span>
+                </button>
+
+                <button id="download" class="toolbarButton download" title="Download" tabindex="15" data-l10n-id="download">
+                  <span data-l10n-id="download_label">Download</span>
+                </button>
+                <!-- <div class="toolbarButtonSpacer"></div> -->
+                <a href="#" id="viewBookmark" class="toolbarButton bookmark hiddenSmallView" title="Current view (copy or open in new window)" tabindex="16" data-l10n-id="bookmark"><span data-l10n-id="bookmark_label">Current View</span></a>
+              </div>
+              <div class="outerCenter">
+                <div class="innerCenter" id="toolbarViewerMiddle">
+                  <div class="splitToolbarButton">
+                    <button class="toolbarButton zoomOut" id="zoom_out" title="Zoom Out" tabindex="9" data-l10n-id="zoom_out">
+                      <span data-l10n-id="zoom_out_label">Zoom Out</span>
+                    </button>
+                    <div class="splitToolbarButtonSeparator"></div>
+                    <button class="toolbarButton zoomIn" id="zoom_in" title="Zoom In" tabindex="10" data-l10n-id="zoom_in">
+                      <span data-l10n-id="zoom_in_label">Zoom In</span>
+                     </button>
+                  </div>
+                  <span id="scaleSelectContainer" class="dropdownToolbarButton">
+                     <select id="scaleSelect" title="Zoom" oncontextmenu="return false;" tabindex="11" data-l10n-id="zoom">
+                      <option id="pageAutoOption" value="auto" selected="selected" data-l10n-id="page_scale_auto">Automatic Zoom</option>
+                      <option id="pageActualOption" value="page-actual" data-l10n-id="page_scale_actual">Actual Size</option>
+                      <option id="pageFitOption" value="page-fit" data-l10n-id="page_scale_fit">Fit Page</option>
+                      <option id="pageWidthOption" value="page-width" data-l10n-id="page_scale_width">Full Width</option>
+                      <option id="customScaleOption" value="custom"></option>
+                      <option value="0.5">50%</option>
+                      <option value="0.75">75%</option>
+                      <option value="1">100%</option>
+                      <option value="1.25">125%</option>
+                      <option value="1.5">150%</option>
+                      <option value="2">200%</option>
+                    </select>
+                  </span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <menu type="context" id="viewerContextMenu">
+          <menuitem label="First Page" id="first_page"
+                    data-l10n-id="first_page" ></menuitem>
+          <menuitem label="Last Page" id="last_page"
+                    data-l10n-id="last_page" ></menuitem>
+          <menuitem label="Rotate Counter-Clockwise" id="page_rotate_ccw"
+                    data-l10n-id="page_rotate_ccw" ></menuitem>
+          <menuitem label="Rotate Clockwise" id="page_rotate_cw"
+                    data-l10n-id="page_rotate_cw" ></menuitem>
+        </menu>
+
+        <div id="viewerContainer" tabindex="1">
+          <div id="viewer" contextmenu="viewerContextMenu"></div>
+        </div>
+
+        <div id="loadingBox">
+          <div id="loading"></div>
+          <div id="loadingBar"><div class="progress"></div></div>
+        </div>
+
+        <div id="errorWrapper" hidden='true'>
+          <div id="errorMessageLeft">
+            <span id="errorMessage"></span>
+            <button id="errorShowMore" onclick="" oncontextmenu="return false;" data-l10n-id="error_more_info">
+              More Information
+            </button>
+            <button id="errorShowLess" onclick="" oncontextmenu="return false;" data-l10n-id="error_less_info" hidden='true'>
+              Less Information
+            </button>
+          </div>
+          <div id="errorMessageRight">
+            <button id="errorClose" oncontextmenu="return false;" data-l10n-id="error_close">
+              Close
+            </button>
+          </div>
+          <div class="clearBoth"></div>
+          <textarea id="errorMoreInfo" hidden='true' readonly="readonly"></textarea>
+        </div>
+      </div> <!-- mainContainer -->
+
+    </div> <!-- outerContainer -->
+    <div id="printContainer"></div>
+
+{% else %}
+  <a href="{{ pdf_view }}">
+  <img id="medium"
+  class="media_image"
+  src="{{ medium_view }}"
+  alt="{% trans media_title=media.title -%} Image for {{ media_title}}{% endtrans %}"/>
+  </a>
+{% endif %}
+{% endblock %}
+
+{% block mediagoblin_sidebar %}
+  <h3>{% trans %}Download{% endtrans %}</h3>
+  <ul>
+    {% if 'original' in media.media_files %}
+      <li>
+        <a href="{{ request.app.public_store.file_url(
+                       media.media_files.original) }}">
+          {%- trans %}Original file{% endtrans -%}
+        </a>
+      </li>
+    {% endif %}
+    {% if 'pdf' in media.media_files %}
+      <li>
+        <a href="{{ request.app.public_store.file_url(
+                       media.media_files.pdf) }}">
+          {%- trans %}PDF file{% endtrans -%}
+        </a>
+      </li>
+    {% endif %}
+  </ul>
+{% endblock %}
index b78abe64fd7a811e74b64e0ca060b0927cbc57d9..9f95a398f17b58f0ce3b78e2b24ca72b25546f86 100644 (file)
@@ -16,6 +16,8 @@ allow_attachments = True
 # mediagoblin.init.celery.from_celery
 celery_setup_elsewhere = true
 
+media_types = mediagoblin.media_types.image, mediagoblin.media_types.pdf
+
 [storage:publicstore]
 base_dir = %(here)s/test_user_dev/media/public
 base_url = /mgoblin_media/
diff --git a/mediagoblin/tests/test_pdf.py b/mediagoblin/tests/test_pdf.py
new file mode 100644 (file)
index 0000000..ee3083e
--- /dev/null
@@ -0,0 +1,45 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2013 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import tempfile
+import shutil
+import os
+
+from mediagoblin.tests.tools import fixture_add_collection, fixture_add_user, \
+    get_app
+from mediagoblin.db.models import Collection, User
+from mediagoblin.db.base import Session
+from nose.tools import assert_equal
+
+from mediagoblin.media_types.pdf.processing import (
+    pdf_info, check_prerequisites, create_pdf_thumb)
+
+GOOD='mediagoblin/tests/test_submission/good.pdf'
+
+def test_pdf():
+    if not check_prerequisites():
+        return
+    good_dict = {'pdf_version_major': 1, 'pdf_title': '',
+        'pdf_page_size_width': 612, 'pdf_author': '',
+        'pdf_keywords': '', 'pdf_pages': 10,
+        'pdf_producer': 'dvips + GNU Ghostscript 7.05',
+        'pdf_version_minor': 3,
+        'pdf_creator': 'LaTeX with hyperref package',
+        'pdf_page_size_height': 792}
+    assert pdf_info(GOOD) == good_dict
+    temp_dir = tempfile.mkdtemp()
+    create_pdf_thumb(GOOD, os.path.join(temp_dir, 'good_256_256.png'), 256, 256)
+    shutil.rmtree(temp_dir)
index ac714252293041795c78901d06bfb424fb322f8e..462a16536bf38b10009e3c33afa5abb8c7990aed 100644 (file)
@@ -28,6 +28,7 @@ from mediagoblin import mg_globals
 from mediagoblin.db.models import MediaEntry
 from mediagoblin.tools import template
 from mediagoblin.media_types.image import MEDIA_MANAGER as img_MEDIA_MANAGER
+from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites
 
 def resource(filename):
     return resource_filename('mediagoblin.tests', 'test_submission/' + filename)
@@ -39,6 +40,8 @@ EVIL_FILE = resource('evil')
 EVIL_JPG = resource('evil.jpg')
 EVIL_PNG = resource('evil.png')
 BIG_BLUE = resource('bigblue.png')
+GOOD_PDF = resource('good.pdf')
+
 from .test_exif import GPS_JPG
 
 GOOD_TAG_STRING = u'yin,yang'
@@ -125,6 +128,16 @@ class TestSubmission:
         self._setup(test_app)
         self.check_normal_upload(u'Normal upload 2', GOOD_PNG)
 
+    def test_normal_pdf(self, test_app):
+        if not pdf_check_prerequisites():
+            return
+        self._setup(test_app)
+        response, context = self.do_post({'title': u'Normal upload 3 (pdf)'},
+                                         do_follow=True,
+                                         **self.upload_data(GOOD_PDF))
+        self.check_url(response, '/u/{0}/'.format(self.test_user.username))
+        assert 'mediagoblin/user_pages/user.html' in context
+
     def check_media(self, request, find_data, count=None):
         media = MediaEntry.find(find_data)
         if count is not None:
diff --git a/mediagoblin/tests/test_submission/good.pdf b/mediagoblin/tests/test_submission/good.pdf
new file mode 100644 (file)
index 0000000..ab5db00
Binary files /dev/null and b/mediagoblin/tests/test_submission/good.pdf differ