diff options
| -rw-r--r-- | .gitmodules | 6 | ||||
| -rw-r--r-- | docs/source/siteadmin/media-types.rst | 37 | ||||
| m--------- | extlib/pdf.js | 0 | ||||
| -rw-r--r-- | mediagoblin/config_spec.ini | 2 | ||||
| -rw-r--r-- | mediagoblin/media_types/pdf/__init__.py | 29 | ||||
| -rw-r--r-- | mediagoblin/media_types/pdf/migrations.py | 17 | ||||
| -rw-r--r-- | mediagoblin/media_types/pdf/models.py | 58 | ||||
| -rw-r--r-- | mediagoblin/media_types/pdf/processing.py | 276 | ||||
| -rw-r--r-- | mediagoblin/static/css/pdf_viewer.css | 1448 | ||||
| l--------- | mediagoblin/static/extlib/pdf.js | 1 | ||||
| -rw-r--r-- | mediagoblin/static/js/pdf_viewer.js | 3615 | ||||
| -rw-r--r-- | mediagoblin/templates/mediagoblin/base.html | 5 | ||||
| -rw-r--r-- | mediagoblin/templates/mediagoblin/media_displays/pdf.html | 284 | ||||
| -rw-r--r-- | mediagoblin/tests/test_mgoblin_app.ini | 2 | ||||
| -rw-r--r-- | mediagoblin/tests/test_pdf.py | 45 | ||||
| -rw-r--r-- | mediagoblin/tests/test_submission.py | 13 | ||||
| -rw-r--r-- | mediagoblin/tests/test_submission/good.pdf | bin | 0 -> 194007 bytes | 
17 files changed, 5837 insertions, 1 deletions
| diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e6a7464c --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/docs/source/siteadmin/media-types.rst b/docs/source/siteadmin/media-types.rst index 23d3f3b9..264dc4fc 100644 --- a/docs/source/siteadmin/media-types.rst +++ b/docs/source/siteadmin/media-types.rst @@ -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 +Subproject b898935eb04fa86e0911fdfa0d41828cb04802f diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index e830e863..399a4a13 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -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 index 00000000..a6d23c93 --- /dev/null +++ b/mediagoblin/media_types/pdf/__init__.py @@ -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 index 00000000..f54c23ea --- /dev/null +++ b/mediagoblin/media_types/pdf/migrations.py @@ -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 index 00000000..c39262d1 --- /dev/null +++ b/mediagoblin/media_types/pdf/models.py @@ -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 index 00000000..51862c7e --- /dev/null +++ b/mediagoblin/media_types/pdf/processing.py @@ -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 index 00000000..c04c8981 --- /dev/null +++ b/mediagoblin/static/css/pdf_viewer.css @@ -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 index 00000000..f829660a --- /dev/null +++ b/mediagoblin/static/extlib/pdf.js @@ -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 index 00000000..79c1e708 --- /dev/null +++ b/mediagoblin/static/js/pdf_viewer.js @@ -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 diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index 66b95661..9c42a756 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -16,7 +16,10 @@  # 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 index 00000000..35a61872 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/media_displays/pdf.html @@ -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 %} diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini index b78abe64..9f95a398 100644 --- a/mediagoblin/tests/test_mgoblin_app.ini +++ b/mediagoblin/tests/test_mgoblin_app.ini @@ -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 index 00000000..ee3083ef --- /dev/null +++ b/mediagoblin/tests/test_pdf.py @@ -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) diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index ac714252..462a1653 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -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.pdfBinary files differ new file mode 100644 index 00000000..ab5db006 --- /dev/null +++ b/mediagoblin/tests/test_submission/good.pdf | 
