├── xsendfile_example ├── __init__.py ├── urls.py ├── settings.py ├── views.py └── wsgi.py ├── .gitignore ├── requirements.txt ├── diagram.png ├── docs ├── viewerjs │ ├── pdfjsversion.js │ ├── images │ │ ├── nlnet.png │ │ ├── kogmbh.png │ │ ├── texture.png │ │ ├── toolbarButton-pageUp.png │ │ ├── toolbarButton-zoomIn.png │ │ ├── toolbarButton-download.png │ │ ├── toolbarButton-pageDown.png │ │ ├── toolbarButton-zoomOut.png │ │ ├── toolbarButton-fullscreen.png │ │ ├── toolbarButton-menuArrows.png │ │ └── toolbarButton-presentation.png │ ├── example.local.css │ ├── ui_utils.js │ ├── text_layer_builder.js │ ├── compatibility.js │ └── index.html ├── presentation.pdf └── index.html ├── provisioning ├── docker │ ├── settings.py │ └── httpd.conf ├── ansible │ ├── xsendfile_example.vhost │ └── playbook.yml └── default │ └── xsendfile_example.vhost ├── manage.py ├── diagram.plantuml ├── Dockerfile ├── Vagrantfile ├── fabfile.py └── README.md /xsendfile_example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vagrant 3 | xsendfile-example.tar 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==1.4.22 2 | django-sendfile==0.3.11 3 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/diagram.png -------------------------------------------------------------------------------- /docs/viewerjs/pdfjsversion.js: -------------------------------------------------------------------------------- 1 | var /**@const{!string}*/pdfjs_version = "v1.1.114"; 2 | -------------------------------------------------------------------------------- /docs/presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/presentation.pdf -------------------------------------------------------------------------------- /docs/viewerjs/images/nlnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/nlnet.png -------------------------------------------------------------------------------- /docs/viewerjs/images/kogmbh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/kogmbh.png -------------------------------------------------------------------------------- /docs/viewerjs/images/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/texture.png -------------------------------------------------------------------------------- /provisioning/docker/settings.py: -------------------------------------------------------------------------------- 1 | from docker_settings import * 2 | 3 | # must be the same value as in httpd.conf 4 | SENDFILE_ROOT_DIR = '/data' 5 | -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-pageUp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-pageUp.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-zoomIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-zoomIn.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-download.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-pageDown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-pageDown.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-zoomOut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-zoomOut.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-fullscreen.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-menuArrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-menuArrows.png -------------------------------------------------------------------------------- /docs/viewerjs/images/toolbarButton-presentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/xsendfile-example/master/docs/viewerjs/images/toolbarButton-presentation.png -------------------------------------------------------------------------------- /xsendfile_example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | urlpatterns = patterns('', 4 | url(r'^(?P.*)$', 'xsendfile_example.views.serve_file', name='serve_file'), 5 | ) 6 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xsendfile_example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /xsendfile_example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for xsendfile_example project. 2 | 3 | DEBUG = False 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ROOT_URLCONF = 'xsendfile_example.urls' 7 | 8 | WSGI_APPLICATION = 'xsendfile_example.wsgi.application' 9 | 10 | SENDFILE_BACKEND = 'sendfile.backends.xsendfile' 11 | SENDFILE_ROOT_DIR = '/home/vagrant/media' -------------------------------------------------------------------------------- /diagram.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | Client -> Webserver : GET /lorem.webm 3 | Webserver -> Application 4 | rnote over Application 5 | do something 6 | like checking 7 | the requesting 8 | user's permissions 9 | endrnote 10 | rnote over Application 11 | set X-SENDFILE header 12 | endrnote 13 | Application -> Webserver 14 | rnote over Webserver 15 | check X-SENDFILE header 16 | endrnote 17 | Webserver -> Client : serve lorem.webm 18 | @enduml 19 | -------------------------------------------------------------------------------- /provisioning/ansible/xsendfile_example.vhost: -------------------------------------------------------------------------------- 1 | 2 | 3 | XSendFile On 4 | XSendFilePath /home/vagrant/media 5 | 6 | WSGIScriptAlias / /home/vagrant/xsendfile_example/xsendfile_example/wsgi.py 7 | WSGIDaemonProcess xsendfile_example python-path=/home/vagrant/xsendfile_example:/home/vagrant/venv/lib/python2.7/site-packages 8 | WSGIProcessGroup xsendfile_example 9 | 10 | 11 | 12 | Require all granted 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /provisioning/default/xsendfile_example.vhost: -------------------------------------------------------------------------------- 1 | 2 | 3 | XSendFile On 4 | XSendFilePath /home/vagrant/media 5 | 6 | WSGIScriptAlias / /home/vagrant/xsendfile_example/xsendfile_example/wsgi.py 7 | WSGIDaemonProcess xsendfile_example python-path=/home/vagrant/xsendfile_example:/home/vagrant/venv/lib/python2.7/site-packages 8 | WSGIProcessGroup xsendfile_example 9 | 10 | 11 | 12 | Require all granted 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /xsendfile_example/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.http import HttpResponse, Http404 4 | from django.template import Context, Template 5 | from django.conf import settings 6 | 7 | from sendfile import sendfile 8 | 9 | 10 | def serve_file(request, path): 11 | path = os.path.join(settings.SENDFILE_ROOT_DIR, path) 12 | 13 | if os.path.isfile(path): 14 | return sendfile(request, path) 15 | 16 | elif os.path.isdir(path): 17 | files = [] 18 | for f in os.listdir(path): 19 | if os.path.isfile(os.path.join(path, f)): 20 | files.append(f) 21 | t = Template(""" 22 | 23 | """) 28 | return HttpResponse(t.render(Context({"files": files}))) 29 | 30 | raise Http404() 31 | -------------------------------------------------------------------------------- /provisioning/ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | remote_user: vagrant 4 | tasks: 5 | - apt: pkg={{ item }} state=latest 6 | with_items: 7 | - apache2 8 | - libapache2-mod-wsgi 9 | - libapache2-mod-xsendfile 10 | - python-pip 11 | - python-virtualenv 12 | become: true 13 | - apache2_module: name={{ item }} 14 | with_items: 15 | - wsgi 16 | - xsendfile 17 | become: true 18 | - command: a2dissite 000-default 19 | become: true 20 | - template: src=xsendfile_example.vhost dest=/etc/apache2/sites-available/xsendfile_example.conf 21 | become: true 22 | - command: a2ensite xsendfile_example 23 | args: 24 | creates: /etc/apache2/sites-enabled/xsendfile_example.conf 25 | notify: 26 | - restart apache 27 | become: true 28 | - pip: name=pip virtualenv=/home/vagrant/venv 29 | handlers: 30 | - name: restart apache 31 | service: name=apache2 state=restarted 32 | become: true 33 | -------------------------------------------------------------------------------- /docs/viewerjs/example.local.css: -------------------------------------------------------------------------------- 1 | /* This is just a sample file with CSS rules. You should write your own @font-face declarations 2 | * to add support for your desired fonts. 3 | */ 4 | 5 | @font-face { 6 | font-family: 'Novecentowide Book'; 7 | src: url("/ViewerJS/fonts/Novecentowide-Bold-webfont.eot"); 8 | src: url("/ViewerJS/fonts/Novecentowide-Bold-webfont.eot?#iefix") format("embedded-opentype"), 9 | url("/ViewerJS/fonts/Novecentowide-Bold-webfont.woff") format("woff"), 10 | url("/fonts/Novecentowide-Bold-webfont.ttf") format("truetype"), 11 | url("/fonts/Novecentowide-Bold-webfont.svg#NovecentowideBookBold") format("svg"); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | @font-face { 17 | font-family: 'exotica'; 18 | src: url('/ViewerJS/fonts/Exotica-webfont.eot'); 19 | src: url('/ViewerJS/fonts/Exotica-webfont.eot?#iefix') format('embedded-opentype'), 20 | url('/ViewerJS/fonts/Exotica-webfont.woff') format('woff'), 21 | url('/ViewerJS/fonts/Exotica-webfont.ttf') format('truetype'), 22 | url('/ViewerJS/fonts/Exotica-webfont.svg#exoticamedium') format('svg'); 23 | font-weight: normal; 24 | font-style: normal; 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /xsendfile_example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for xsendfile_example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xsendfile_example.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /provisioning/docker/httpd.conf: -------------------------------------------------------------------------------- 1 | ServerRoot "/usr/local/apache2" 2 | 3 | Listen 80 4 | 5 | LoadModule authz_core_module modules/mod_authz_core.so 6 | LoadModule unixd_module modules/mod_unixd.so 7 | LoadModule alias_module modules/mod_alias.so 8 | LoadModule log_config_module modules/mod_log_config.so 9 | LoadModule wsgi_module modules/mod_wsgi.so 10 | LoadModule xsendfile_module modules/mod_xsendfile.so 11 | 12 | 13 | User daemon 14 | Group daemon 15 | 16 | 17 | ErrorLog /proc/self/fd/2 18 | LogLevel warn 19 | 20 | 21 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined 22 | LogFormat "%h %l %u %t \"%r\" %>s %b" common 23 | 24 | 25 | # You need to enable mod_logio.c to use %I and %O 26 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio 27 | 28 | 29 | CustomLog /proc/self/fd/1 common 30 | 31 | 32 | # / is handled by our application but there is no favicon, so 33 | # we're catching it here preventing a 500 in the application 34 | Redirect 404 /favicon.ico 35 | 36 | XSendFile On 37 | # must be the same value as in settings.py 38 | XSendFilePath /data 39 | 40 | WSGIScriptAlias / /xsendfile-example/xsendfile_example/wsgi.py 41 | WSGIDaemonProcess xsendfile_example python-path=/xsendfile-example 42 | WSGIProcessGroup xsendfile_example 43 | 44 | 45 | 46 | Require all granted 47 | 48 | 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM httpd:2.4 2 | 3 | # install setup and build dependencies 4 | RUN apt-get update && apt-get install -y \ 5 | build-essential \ 6 | wget \ 7 | python-dev \ 8 | python2.7 \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # install mod_wsgi 12 | RUN wget -O /tmp/mod_wsgi.tar.gz https://github.com/GrahamDumpleton/mod_wsgi/archive/4.5.6.tar.gz \ 13 | && mkdir /tmp/mod_wsgi \ 14 | && tar -xf /tmp/mod_wsgi.tar.gz -C /tmp/mod_wsgi --strip-components=1 \ 15 | && cd /tmp/mod_wsgi \ 16 | && ./configure \ 17 | && make \ 18 | && make install \ 19 | && rm -r /tmp/* 20 | 21 | # install mod_xsendfile 22 | RUN wget -O /tmp/mod_xsendfile.tar.gz https://tn123.org/mod_xsendfile/mod_xsendfile-0.12.tar.gz \ 23 | && mkdir /tmp/mod_xsendfile \ 24 | && tar -xf /tmp/mod_xsendfile.tar.gz -C /tmp/mod_xsendfile --strip-components=1 \ 25 | && cd /tmp/mod_xsendfile \ 26 | && apxs -cia mod_xsendfile.c \ 27 | && rm -r /tmp/* 28 | 29 | # install pip 30 | RUN wget -O /tmp/get-pip.py https://bootstrap.pypa.io/get-pip.py \ 31 | && python2.7 /tmp/get-pip.py \ 32 | && rm -r /tmp/* 33 | 34 | COPY ./provisioning/docker/httpd.conf /usr/local/apache2/conf/httpd.conf 35 | 36 | COPY ./requirements.txt /tmp 37 | RUN pip install --no-cache-dir -r /tmp/requirements.txt 38 | 39 | RUN mkdir -p /xsendfile-example/xsendfile_example 40 | WORKDIR /xsendfile-example 41 | COPY ./xsendfile_example xsendfile_example 42 | RUN mv xsendfile_example/settings.py xsendfile_example/docker_settings.py 43 | COPY ./provisioning/docker/settings.py xsendfile_example/settings.py 44 | 45 | RUN mkdir /data 46 | VOLUME /data 47 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 7 | config.vm.box = "ubuntu/trusty64" 8 | config.vm.provider "virtualbox" do |vb| 9 | vb.customize ["modifyvm", :id, "--memory", "1024"] 10 | end 11 | 12 | config.vm.define "ansible", autostart: false do |ansible_machine| 13 | ansible_machine.vm.network "forwarded_port", guest: 80, host: 8081 14 | ansible_machine.vm.provision "ansible" do |ansible| 15 | ansible.playbook = "./provisioning/ansible/playbook.yml" 16 | end 17 | end 18 | 19 | config.vm.define "default", autostart: false do |default_machine| 20 | default_machine.vm.network "forwarded_port", guest: 80, host: 8080 21 | default_machine.vm.provision "shell", 22 | inline: "apt-get update && \ 23 | apt-get install -y \ 24 | apache2 libapache2-mod-wsgi libapache2-mod-xsendfile python-pip \ 25 | python-virtualenv && 26 | a2enmod wsgi && \ 27 | a2enmod xsendfile && \ 28 | a2dissite 000-default" 29 | default_machine.vm.provision "file", 30 | source: "./provisioning/default/xsendfile_example.vhost", 31 | destination: "/home/vagrant/xsendfile_example.vhost" 32 | default_machine.vm.provision "shell", 33 | inline: "cp /home/vagrant/xsendfile_example.vhost \ 34 | /etc/apache2/sites-available/xsendfile_example.conf && \ 35 | a2ensite xsendfile_example && \ 36 | service apache2 restart" 37 | default_machine.vm.provision "shell", 38 | inline: "virtualenv /home/vagrant/venv", 39 | privileged: false 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | from tempfile import NamedTemporaryFile 2 | from os.path import join, abspath 3 | from fabric.api import * 4 | from fabric.colors import red 5 | from fabtools import python 6 | from fabtools.vagrant import vagrant 7 | 8 | USAGE=red("""Usage: 9 | fab -H deploy - Deploy on 10 | fab vagrant:default deploy - Deploy on a Vagrant VM 11 | fab vagrant:ansible deploy - Deploy on an Ansible provisioned Vagrant VM 12 | """) 13 | PACKAGE_NAME = 'xsendfile-example.tar' 14 | RED_DOT = """iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAA 15 | ABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9Y 16 | GARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1 17 | Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1 18 | exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq 19 | 2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg==""" 20 | 21 | 22 | @task 23 | def deploy(): 24 | """ 25 | deploy xsendfile_example application 26 | """ 27 | if not env.hosts: 28 | abort(USAGE) 29 | # create package from code 30 | commit = local('git stash create', capture=True) 31 | if not commit: 32 | commit = 'HEAD' 33 | local('git archive %s > %s' % (commit, PACKAGE_NAME)) 34 | # copy package to VM 35 | put(PACKAGE_NAME, '~') 36 | with cd('~'): 37 | # install package 38 | run('mkdir xsendfile_example', quiet=True) 39 | with cd('xsendfile_example'): 40 | run('tar -xf ~/%s' % PACKAGE_NAME) 41 | # install application dependencies 42 | with python.virtualenv('/home/vagrant/venv'): 43 | python.install_requirements('requirements.txt') 44 | # create sample media file 45 | run('mkdir media', quiet=True) 46 | with cd('media'): 47 | run('echo "%s" | base64 -d > red_dot.png' % RED_DOT) 48 | sudo('service apache2 restart') 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xsendfile-example 2 | ================= 3 | 4 | Django application that shows how to [serve files with Apache and X-SENDFILE](https://speakerdeck.com/danjou/protecting-static-files-in-your-web-app) 5 | by using [django-sendfile](https://github.com/johnsensible/django-sendfile). 6 | 7 | ![sequence diagram showing a request/response cycle with X-SENDFILE](diagram.png) 8 | 9 | --- 10 | 11 | This project also serves as a playground for other technologies. 12 | 13 | ## Usage 14 | 15 | This is a pretty normal Django application, nothing too fancy. So you could just 16 | grab the `xsendfile_example` folder and run with it. But to make it a bit easier 17 | I provide 3 options to run this application. 18 | 19 | ### Docker 20 | 21 | Install [Docker](https://www.docker.com/). 22 | 23 | *Docker lets you provision container images including your application which you 24 | can then deploy and run.* 25 | 26 | Build the image: 27 | 28 | docker build --tag xsendfile-example . 29 | 30 | Run the container: 31 | 32 | docker run --rm -p 8082:80 -v /path/to/your/data:/data xsendfile-example 33 | 34 | Browse to http://localhost:8082/ 35 | 36 | ### Vagrant + Fabric 37 | 38 | Install [Vagrant](http://www.vagrantup.com/), [VirtualBox](https://www.virtualbox.org/) 39 | and [Fabric](http://www.fabfile.org/)+[fabtools](http://fabtools.readthedocs.io). 40 | 41 | *Vagrant helps with provisioning VMs especially for development, in this case 42 | VirtualBox VMs. Fabric is a command-line tool for application deployment via 43 | SSH.* 44 | 45 | Boot and provision the VM: 46 | 47 | vagrant up default 48 | 49 | Deploy the application 50 | 51 | fab vagrant:default deploy 52 | 53 | Browse to http://localhost:8080/ 54 | 55 | ### Vagrant + Fabric + Ansible 56 | 57 | This option works pretty much the same as the one before except that it uses 58 | [Ansible](https://www.ansible.com/), so you have to install that too. 59 | 60 | *Ansible is a configuration management tool that lets you automate server 61 | installations not only for development but also for production.* 62 | 63 | Boot and provision the VM: 64 | 65 | vagrant up ansible 66 | 67 | Deploy the application 68 | 69 | fab vagrant:ansible deploy 70 | 71 | Browse to http://localhost:8081/ 72 | 73 | ## Notes 74 | 75 | If you want to run the project using Django's development server you have to set 76 | another backend in `xsendfile_example/settings.py`: 77 | 78 | SENDFILE_BACKEND = 'sendfile.backends.development' 79 | -------------------------------------------------------------------------------- /docs/viewerjs/ui_utils.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* Copyright 2012 Mozilla Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | var CSS_UNITS = 96.0 / 72.0; 20 | var DEFAULT_SCALE = 'auto'; 21 | var UNKNOWN_SCALE = 0; 22 | var MAX_AUTO_SCALE = 1.25; 23 | var SCROLLBAR_PADDING = 40; 24 | var VERTICAL_PADDING = 5; 25 | 26 | // optimised CSS custom property getter/setter 27 | var CustomStyle = (function CustomStyleClosure() { 28 | 29 | // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ 30 | // animate-css-transforms-firefox-webkit.html 31 | // in some versions of IE9 it is critical that ms appear in this list 32 | // before Moz 33 | var prefixes = ['ms', 'Moz', 'Webkit', 'O']; 34 | var _cache = {}; 35 | 36 | function CustomStyle() {} 37 | 38 | CustomStyle.getProp = function get(propName, element) { 39 | // check cache only when no element is given 40 | if (arguments.length === 1 && typeof _cache[propName] === 'string') { 41 | return _cache[propName]; 42 | } 43 | 44 | element = element || document.documentElement; 45 | var style = element.style, prefixed, uPropName; 46 | 47 | // test standard property first 48 | if (typeof style[propName] === 'string') { 49 | return (_cache[propName] = propName); 50 | } 51 | 52 | // capitalize 53 | uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); 54 | 55 | // test vendor specific properties 56 | for (var i = 0, l = prefixes.length; i < l; i++) { 57 | prefixed = prefixes[i] + uPropName; 58 | if (typeof style[prefixed] === 'string') { 59 | return (_cache[propName] = prefixed); 60 | } 61 | } 62 | 63 | //if all fails then set to undefined 64 | return (_cache[propName] = 'undefined'); 65 | }; 66 | 67 | CustomStyle.setProp = function set(propName, element, str) { 68 | var prop = this.getProp(propName); 69 | if (prop !== 'undefined') { 70 | element.style[prop] = str; 71 | } 72 | }; 73 | 74 | return CustomStyle; 75 | })(); 76 | 77 | function getFileName(url) { 78 | var anchor = url.indexOf('#'); 79 | var query = url.indexOf('?'); 80 | var end = Math.min( 81 | anchor > 0 ? anchor : url.length, 82 | query > 0 ? query : url.length); 83 | return url.substring(url.lastIndexOf('/', end) + 1, end); 84 | } 85 | 86 | /** 87 | * Returns scale factor for the canvas. It makes sense for the HiDPI displays. 88 | * @return {Object} The object with horizontal (sx) and vertical (sy) 89 | scales. The scaled property is set to false if scaling is 90 | not required, true otherwise. 91 | */ 92 | function getOutputScale(ctx) { 93 | var devicePixelRatio = window.devicePixelRatio || 1; 94 | var backingStoreRatio = ctx.webkitBackingStorePixelRatio || 95 | ctx.mozBackingStorePixelRatio || 96 | ctx.msBackingStorePixelRatio || 97 | ctx.oBackingStorePixelRatio || 98 | ctx.backingStorePixelRatio || 1; 99 | var pixelRatio = devicePixelRatio / backingStoreRatio; 100 | return { 101 | sx: pixelRatio, 102 | sy: pixelRatio, 103 | scaled: pixelRatio !== 1 104 | }; 105 | } 106 | 107 | /** 108 | * Scrolls specified element into view of its parent. 109 | * element {Object} The element to be visible. 110 | * spot {Object} An object with optional top and left properties, 111 | * specifying the offset from the top left edge. 112 | */ 113 | function scrollIntoView(element, spot) { 114 | // Assuming offsetParent is available (it's not available when viewer is in 115 | // hidden iframe or object). We have to scroll: if the offsetParent is not set 116 | // producing the error. See also animationStartedClosure. 117 | var parent = element.offsetParent; 118 | var offsetY = element.offsetTop + element.clientTop; 119 | var offsetX = element.offsetLeft + element.clientLeft; 120 | if (!parent) { 121 | console.error('offsetParent is not set -- cannot scroll'); 122 | return; 123 | } 124 | while (parent.clientHeight === parent.scrollHeight) { 125 | if (parent.dataset._scaleY) { 126 | offsetY /= parent.dataset._scaleY; 127 | offsetX /= parent.dataset._scaleX; 128 | } 129 | offsetY += parent.offsetTop; 130 | offsetX += parent.offsetLeft; 131 | parent = parent.offsetParent; 132 | if (!parent) { 133 | return; // no need to scroll 134 | } 135 | } 136 | if (spot) { 137 | if (spot.top !== undefined) { 138 | offsetY += spot.top; 139 | } 140 | if (spot.left !== undefined) { 141 | offsetX += spot.left; 142 | parent.scrollLeft = offsetX; 143 | } 144 | } 145 | parent.scrollTop = offsetY; 146 | } 147 | 148 | /** 149 | * Helper function to start monitoring the scroll event and converting them into 150 | * PDF.js friendly one: with scroll debounce and scroll direction. 151 | */ 152 | function watchScroll(viewAreaElement, callback) { 153 | var debounceScroll = function debounceScroll(evt) { 154 | if (rAF) { 155 | return; 156 | } 157 | // schedule an invocation of scroll for next animation frame. 158 | rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { 159 | rAF = null; 160 | 161 | var currentY = viewAreaElement.scrollTop; 162 | var lastY = state.lastY; 163 | if (currentY !== lastY) { 164 | state.down = currentY > lastY; 165 | } 166 | state.lastY = currentY; 167 | callback(state); 168 | }); 169 | }; 170 | 171 | var state = { 172 | down: true, 173 | lastY: viewAreaElement.scrollTop, 174 | _eventHandler: debounceScroll 175 | }; 176 | 177 | var rAF = null; 178 | viewAreaElement.addEventListener('scroll', debounceScroll, true); 179 | return state; 180 | } 181 | 182 | /** 183 | * Use binary search to find the index of the first item in a given array which 184 | * passes a given condition. The items are expected to be sorted in the sense 185 | * that if the condition is true for one item in the array, then it is also true 186 | * for all following items. 187 | * 188 | * @returns {Number} Index of the first array element to pass the test, 189 | * or |items.length| if no such element exists. 190 | */ 191 | function binarySearchFirstItem(items, condition) { 192 | var minIndex = 0; 193 | var maxIndex = items.length - 1; 194 | 195 | if (items.length === 0 || !condition(items[maxIndex])) { 196 | return items.length; 197 | } 198 | if (condition(items[minIndex])) { 199 | return minIndex; 200 | } 201 | 202 | while (minIndex < maxIndex) { 203 | var currentIndex = (minIndex + maxIndex) >> 1; 204 | var currentItem = items[currentIndex]; 205 | if (condition(currentItem)) { 206 | maxIndex = currentIndex; 207 | } else { 208 | minIndex = currentIndex + 1; 209 | } 210 | } 211 | return minIndex; /* === maxIndex */ 212 | } 213 | 214 | /** 215 | * Generic helper to find out what elements are visible within a scroll pane. 216 | */ 217 | function getVisibleElements(scrollEl, views, sortByVisibility) { 218 | var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; 219 | var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; 220 | 221 | function isElementBottomBelowViewTop(view) { 222 | var element = view.div; 223 | var elementBottom = 224 | element.offsetTop + element.clientTop + element.clientHeight; 225 | return elementBottom > top; 226 | } 227 | 228 | var visible = [], view, element; 229 | var currentHeight, viewHeight, hiddenHeight, percentHeight; 230 | var currentWidth, viewWidth; 231 | var firstVisibleElementInd = (views.length === 0) ? 0 : 232 | binarySearchFirstItem(views, isElementBottomBelowViewTop); 233 | 234 | for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) { 235 | view = views[i]; 236 | element = view.div; 237 | currentHeight = element.offsetTop + element.clientTop; 238 | viewHeight = element.clientHeight; 239 | 240 | if (currentHeight > bottom) { 241 | break; 242 | } 243 | 244 | currentWidth = element.offsetLeft + element.clientLeft; 245 | viewWidth = element.clientWidth; 246 | if (currentWidth + viewWidth < left || currentWidth > right) { 247 | continue; 248 | } 249 | hiddenHeight = Math.max(0, top - currentHeight) + 250 | Math.max(0, currentHeight + viewHeight - bottom); 251 | percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; 252 | 253 | visible.push({ 254 | id: view.id, 255 | x: currentWidth, 256 | y: currentHeight, 257 | view: view, 258 | percent: percentHeight 259 | }); 260 | } 261 | 262 | var first = visible[0]; 263 | var last = visible[visible.length - 1]; 264 | 265 | if (sortByVisibility) { 266 | visible.sort(function(a, b) { 267 | var pc = a.percent - b.percent; 268 | if (Math.abs(pc) > 0.001) { 269 | return -pc; 270 | } 271 | return a.id - b.id; // ensure stability 272 | }); 273 | } 274 | return {first: first, last: last, views: visible}; 275 | } 276 | 277 | /** 278 | * Event handler to suppress context menu. 279 | */ 280 | function noContextMenuHandler(e) { 281 | e.preventDefault(); 282 | } 283 | 284 | /** 285 | * Returns the filename or guessed filename from the url (see issue 3455). 286 | * url {String} The original PDF location. 287 | * @return {String} Guessed PDF file name. 288 | */ 289 | function getPDFFileNameFromURL(url) { 290 | var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/; 291 | // SCHEME HOST 1.PATH 2.QUERY 3.REF 292 | // Pattern to get last matching NAME.pdf 293 | var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i; 294 | var splitURI = reURI.exec(url); 295 | var suggestedFilename = reFilename.exec(splitURI[1]) || 296 | reFilename.exec(splitURI[2]) || 297 | reFilename.exec(splitURI[3]); 298 | if (suggestedFilename) { 299 | suggestedFilename = suggestedFilename[0]; 300 | if (suggestedFilename.indexOf('%') !== -1) { 301 | // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf 302 | try { 303 | suggestedFilename = 304 | reFilename.exec(decodeURIComponent(suggestedFilename))[0]; 305 | } catch(e) { // Possible (extremely rare) errors: 306 | // URIError "Malformed URI", e.g. for "%AA.pdf" 307 | // TypeError "null has no properties", e.g. for "%2F.pdf" 308 | } 309 | } 310 | } 311 | return suggestedFilename || 'document.pdf'; 312 | } 313 | 314 | var ProgressBar = (function ProgressBarClosure() { 315 | 316 | function clamp(v, min, max) { 317 | return Math.min(Math.max(v, min), max); 318 | } 319 | 320 | function ProgressBar(id, opts) { 321 | this.visible = true; 322 | 323 | // Fetch the sub-elements for later. 324 | this.div = document.querySelector(id + ' .progress'); 325 | 326 | // Get the loading bar element, so it can be resized to fit the viewer. 327 | this.bar = this.div.parentNode; 328 | 329 | // Get options, with sensible defaults. 330 | this.height = opts.height || 100; 331 | this.width = opts.width || 100; 332 | this.units = opts.units || '%'; 333 | 334 | // Initialize heights. 335 | this.div.style.height = this.height + this.units; 336 | this.percent = 0; 337 | } 338 | 339 | ProgressBar.prototype = { 340 | 341 | updateBar: function ProgressBar_updateBar() { 342 | if (this._indeterminate) { 343 | this.div.classList.add('indeterminate'); 344 | this.div.style.width = this.width + this.units; 345 | return; 346 | } 347 | 348 | this.div.classList.remove('indeterminate'); 349 | var progressSize = this.width * this._percent / 100; 350 | this.div.style.width = progressSize + this.units; 351 | }, 352 | 353 | get percent() { 354 | return this._percent; 355 | }, 356 | 357 | set percent(val) { 358 | this._indeterminate = isNaN(val); 359 | this._percent = clamp(val, 0, 100); 360 | this.updateBar(); 361 | }, 362 | 363 | setWidth: function ProgressBar_setWidth(viewer) { 364 | if (viewer) { 365 | var container = viewer.parentNode; 366 | var scrollbarWidth = container.offsetWidth - viewer.offsetWidth; 367 | if (scrollbarWidth > 0) { 368 | this.bar.setAttribute('style', 'width: calc(100% - ' + 369 | scrollbarWidth + 'px);'); 370 | } 371 | } 372 | }, 373 | 374 | hide: function ProgressBar_hide() { 375 | if (!this.visible) { 376 | return; 377 | } 378 | this.visible = false; 379 | this.bar.classList.add('hidden'); 380 | document.body.classList.remove('loadingInProgress'); 381 | }, 382 | 383 | show: function ProgressBar_show() { 384 | if (this.visible) { 385 | return; 386 | } 387 | this.visible = true; 388 | document.body.classList.add('loadingInProgress'); 389 | this.bar.classList.remove('hidden'); 390 | } 391 | }; 392 | 393 | return ProgressBar; 394 | })(); 395 | -------------------------------------------------------------------------------- /docs/viewerjs/text_layer_builder.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* Copyright 2012 Mozilla Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /* globals CustomStyle, PDFJS */ 17 | 18 | 'use strict'; 19 | 20 | var MAX_TEXT_DIVS_TO_RENDER = 100000; 21 | 22 | var NonWhitespaceRegexp = /\S/; 23 | 24 | function isAllWhitespace(str) { 25 | return !NonWhitespaceRegexp.test(str); 26 | } 27 | 28 | /** 29 | * @typedef {Object} TextLayerBuilderOptions 30 | * @property {HTMLDivElement} textLayerDiv - The text layer container. 31 | * @property {number} pageIndex - The page index. 32 | * @property {PageViewport} viewport - The viewport of the text layer. 33 | * @property {PDFFindController} findController 34 | */ 35 | 36 | /** 37 | * TextLayerBuilder provides text-selection functionality for the PDF. 38 | * It does this by creating overlay divs over the PDF text. These divs 39 | * contain text that matches the PDF text they are overlaying. This object 40 | * also provides a way to highlight text that is being searched for. 41 | * @class 42 | */ 43 | var TextLayerBuilder = (function TextLayerBuilderClosure() { 44 | function TextLayerBuilder(options) { 45 | this.textLayerDiv = options.textLayerDiv; 46 | this.renderingDone = false; 47 | this.divContentDone = false; 48 | this.pageIdx = options.pageIndex; 49 | this.pageNumber = this.pageIdx + 1; 50 | this.matches = []; 51 | this.viewport = options.viewport; 52 | this.textDivs = []; 53 | this.findController = options.findController || null; 54 | } 55 | 56 | TextLayerBuilder.prototype = { 57 | _finishRendering: function TextLayerBuilder_finishRendering() { 58 | this.renderingDone = true; 59 | 60 | var event = document.createEvent('CustomEvent'); 61 | event.initCustomEvent('textlayerrendered', true, true, { 62 | pageNumber: this.pageNumber 63 | }); 64 | this.textLayerDiv.dispatchEvent(event); 65 | }, 66 | 67 | renderLayer: function TextLayerBuilder_renderLayer() { 68 | var textLayerFrag = document.createDocumentFragment(); 69 | var textDivs = this.textDivs; 70 | var textDivsLength = textDivs.length; 71 | var canvas = document.createElement('canvas'); 72 | var ctx = canvas.getContext('2d'); 73 | 74 | // No point in rendering many divs as it would make the browser 75 | // unusable even after the divs are rendered. 76 | if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { 77 | this._finishRendering(); 78 | return; 79 | } 80 | 81 | var lastFontSize; 82 | var lastFontFamily; 83 | for (var i = 0; i < textDivsLength; i++) { 84 | var textDiv = textDivs[i]; 85 | if (textDiv.dataset.isWhitespace !== undefined) { 86 | continue; 87 | } 88 | 89 | var fontSize = textDiv.style.fontSize; 90 | var fontFamily = textDiv.style.fontFamily; 91 | 92 | // Only build font string and set to context if different from last. 93 | if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) { 94 | ctx.font = fontSize + ' ' + fontFamily; 95 | lastFontSize = fontSize; 96 | lastFontFamily = fontFamily; 97 | } 98 | 99 | var width = ctx.measureText(textDiv.textContent).width; 100 | if (width > 0) { 101 | textLayerFrag.appendChild(textDiv); 102 | var transform; 103 | if (textDiv.dataset.canvasWidth !== undefined) { 104 | // Dataset values come of type string. 105 | var textScale = textDiv.dataset.canvasWidth / width; 106 | transform = 'scaleX(' + textScale + ')'; 107 | } else { 108 | transform = ''; 109 | } 110 | var rotation = textDiv.dataset.angle; 111 | if (rotation) { 112 | transform = 'rotate(' + rotation + 'deg) ' + transform; 113 | } 114 | if (transform) { 115 | CustomStyle.setProp('transform' , textDiv, transform); 116 | } 117 | } 118 | } 119 | 120 | this.textLayerDiv.appendChild(textLayerFrag); 121 | this._finishRendering(); 122 | this.updateMatches(); 123 | }, 124 | 125 | /** 126 | * Renders the text layer. 127 | * @param {number} timeout (optional) if specified, the rendering waits 128 | * for specified amount of ms. 129 | */ 130 | render: function TextLayerBuilder_render(timeout) { 131 | if (!this.divContentDone || this.renderingDone) { 132 | return; 133 | } 134 | 135 | if (this.renderTimer) { 136 | clearTimeout(this.renderTimer); 137 | this.renderTimer = null; 138 | } 139 | 140 | if (!timeout) { // Render right away 141 | this.renderLayer(); 142 | } else { // Schedule 143 | var self = this; 144 | this.renderTimer = setTimeout(function() { 145 | self.renderLayer(); 146 | self.renderTimer = null; 147 | }, timeout); 148 | } 149 | }, 150 | 151 | appendText: function TextLayerBuilder_appendText(geom, styles) { 152 | var style = styles[geom.fontName]; 153 | var textDiv = document.createElement('div'); 154 | this.textDivs.push(textDiv); 155 | if (isAllWhitespace(geom.str)) { 156 | textDiv.dataset.isWhitespace = true; 157 | return; 158 | } 159 | var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); 160 | var angle = Math.atan2(tx[1], tx[0]); 161 | if (style.vertical) { 162 | angle += Math.PI / 2; 163 | } 164 | var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); 165 | var fontAscent = fontHeight; 166 | if (style.ascent) { 167 | fontAscent = style.ascent * fontAscent; 168 | } else if (style.descent) { 169 | fontAscent = (1 + style.descent) * fontAscent; 170 | } 171 | 172 | var left; 173 | var top; 174 | if (angle === 0) { 175 | left = tx[4]; 176 | top = tx[5] - fontAscent; 177 | } else { 178 | left = tx[4] + (fontAscent * Math.sin(angle)); 179 | top = tx[5] - (fontAscent * Math.cos(angle)); 180 | } 181 | textDiv.style.left = left + 'px'; 182 | textDiv.style.top = top + 'px'; 183 | textDiv.style.fontSize = fontHeight + 'px'; 184 | textDiv.style.fontFamily = style.fontFamily; 185 | 186 | textDiv.textContent = geom.str; 187 | // |fontName| is only used by the Font Inspector. This test will succeed 188 | // when e.g. the Font Inspector is off but the Stepper is on, but it's 189 | // not worth the effort to do a more accurate test. 190 | if (PDFJS.pdfBug) { 191 | textDiv.dataset.fontName = geom.fontName; 192 | } 193 | // Storing into dataset will convert number into string. 194 | if (angle !== 0) { 195 | textDiv.dataset.angle = angle * (180 / Math.PI); 196 | } 197 | // We don't bother scaling single-char text divs, because it has very 198 | // little effect on text highlighting. This makes scrolling on docs with 199 | // lots of such divs a lot faster. 200 | if (textDiv.textContent.length > 1) { 201 | if (style.vertical) { 202 | textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; 203 | } else { 204 | textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; 205 | } 206 | } 207 | }, 208 | 209 | setTextContent: function TextLayerBuilder_setTextContent(textContent) { 210 | this.textContent = textContent; 211 | 212 | var textItems = textContent.items; 213 | for (var i = 0, len = textItems.length; i < len; i++) { 214 | this.appendText(textItems[i], textContent.styles); 215 | } 216 | this.divContentDone = true; 217 | }, 218 | 219 | convertMatches: function TextLayerBuilder_convertMatches(matches) { 220 | var i = 0; 221 | var iIndex = 0; 222 | var bidiTexts = this.textContent.items; 223 | var end = bidiTexts.length - 1; 224 | var queryLen = (this.findController === null ? 225 | 0 : this.findController.state.query.length); 226 | var ret = []; 227 | 228 | for (var m = 0, len = matches.length; m < len; m++) { 229 | // Calculate the start position. 230 | var matchIdx = matches[m]; 231 | 232 | // Loop over the divIdxs. 233 | while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { 234 | iIndex += bidiTexts[i].str.length; 235 | i++; 236 | } 237 | 238 | if (i === bidiTexts.length) { 239 | console.error('Could not find a matching mapping'); 240 | } 241 | 242 | var match = { 243 | begin: { 244 | divIdx: i, 245 | offset: matchIdx - iIndex 246 | } 247 | }; 248 | 249 | // Calculate the end position. 250 | matchIdx += queryLen; 251 | 252 | // Somewhat the same array as above, but use > instead of >= to get 253 | // the end position right. 254 | while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { 255 | iIndex += bidiTexts[i].str.length; 256 | i++; 257 | } 258 | 259 | match.end = { 260 | divIdx: i, 261 | offset: matchIdx - iIndex 262 | }; 263 | ret.push(match); 264 | } 265 | 266 | return ret; 267 | }, 268 | 269 | renderMatches: function TextLayerBuilder_renderMatches(matches) { 270 | // Early exit if there is nothing to render. 271 | if (matches.length === 0) { 272 | return; 273 | } 274 | 275 | var bidiTexts = this.textContent.items; 276 | var textDivs = this.textDivs; 277 | var prevEnd = null; 278 | var pageIdx = this.pageIdx; 279 | var isSelectedPage = (this.findController === null ? 280 | false : (pageIdx === this.findController.selected.pageIdx)); 281 | var selectedMatchIdx = (this.findController === null ? 282 | -1 : this.findController.selected.matchIdx); 283 | var highlightAll = (this.findController === null ? 284 | false : this.findController.state.highlightAll); 285 | var infinity = { 286 | divIdx: -1, 287 | offset: undefined 288 | }; 289 | 290 | function beginText(begin, className) { 291 | var divIdx = begin.divIdx; 292 | textDivs[divIdx].textContent = ''; 293 | appendTextToDiv(divIdx, 0, begin.offset, className); 294 | } 295 | 296 | function appendTextToDiv(divIdx, fromOffset, toOffset, className) { 297 | var div = textDivs[divIdx]; 298 | var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); 299 | var node = document.createTextNode(content); 300 | if (className) { 301 | var span = document.createElement('span'); 302 | span.className = className; 303 | span.appendChild(node); 304 | div.appendChild(span); 305 | return; 306 | } 307 | div.appendChild(node); 308 | } 309 | 310 | var i0 = selectedMatchIdx, i1 = i0 + 1; 311 | if (highlightAll) { 312 | i0 = 0; 313 | i1 = matches.length; 314 | } else if (!isSelectedPage) { 315 | // Not highlighting all and this isn't the selected page, so do nothing. 316 | return; 317 | } 318 | 319 | for (var i = i0; i < i1; i++) { 320 | var match = matches[i]; 321 | var begin = match.begin; 322 | var end = match.end; 323 | var isSelected = (isSelectedPage && i === selectedMatchIdx); 324 | var highlightSuffix = (isSelected ? ' selected' : ''); 325 | 326 | if (this.findController) { 327 | this.findController.updateMatchPosition(pageIdx, i, textDivs, 328 | begin.divIdx, end.divIdx); 329 | } 330 | 331 | // Match inside new div. 332 | if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { 333 | // If there was a previous div, then add the text at the end. 334 | if (prevEnd !== null) { 335 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); 336 | } 337 | // Clear the divs and set the content until the starting point. 338 | beginText(begin); 339 | } else { 340 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); 341 | } 342 | 343 | if (begin.divIdx === end.divIdx) { 344 | appendTextToDiv(begin.divIdx, begin.offset, end.offset, 345 | 'highlight' + highlightSuffix); 346 | } else { 347 | appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, 348 | 'highlight begin' + highlightSuffix); 349 | for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { 350 | textDivs[n0].className = 'highlight middle' + highlightSuffix; 351 | } 352 | beginText(end, 'highlight end' + highlightSuffix); 353 | } 354 | prevEnd = end; 355 | } 356 | 357 | if (prevEnd) { 358 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); 359 | } 360 | }, 361 | 362 | updateMatches: function TextLayerBuilder_updateMatches() { 363 | // Only show matches when all rendering is done. 364 | if (!this.renderingDone) { 365 | return; 366 | } 367 | 368 | // Clear all matches. 369 | var matches = this.matches; 370 | var textDivs = this.textDivs; 371 | var bidiTexts = this.textContent.items; 372 | var clearedUntilDivIdx = -1; 373 | 374 | // Clear all current matches. 375 | for (var i = 0, len = matches.length; i < len; i++) { 376 | var match = matches[i]; 377 | var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); 378 | for (var n = begin, end = match.end.divIdx; n <= end; n++) { 379 | var div = textDivs[n]; 380 | div.textContent = bidiTexts[n].str; 381 | div.className = ''; 382 | } 383 | clearedUntilDivIdx = match.end.divIdx + 1; 384 | } 385 | 386 | if (this.findController === null || !this.findController.active) { 387 | return; 388 | } 389 | 390 | // Convert the matches on the page controller into the match format 391 | // used for the textLayer. 392 | this.matches = this.convertMatches(this.findController === null ? 393 | [] : (this.findController.pageMatches[this.pageIdx] || [])); 394 | this.renderMatches(this.matches); 395 | } 396 | }; 397 | return TextLayerBuilder; 398 | })(); 399 | 400 | /** 401 | * @constructor 402 | * @implements IPDFTextLayerFactory 403 | */ 404 | function DefaultTextLayerFactory() {} 405 | DefaultTextLayerFactory.prototype = { 406 | /** 407 | * @param {HTMLDivElement} textLayerDiv 408 | * @param {number} pageIndex 409 | * @param {PageViewport} viewport 410 | * @returns {TextLayerBuilder} 411 | */ 412 | createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { 413 | return new TextLayerBuilder({ 414 | textLayerDiv: textLayerDiv, 415 | pageIndex: pageIndex, 416 | viewport: viewport 417 | }); 418 | } 419 | }; 420 | -------------------------------------------------------------------------------- /docs/viewerjs/compatibility.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 3 | /* Copyright 2012 Mozilla Foundation 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | /* globals VBArray, PDFJS */ 18 | 19 | 'use strict'; 20 | 21 | // Initializing PDFJS global object here, it case if we need to change/disable 22 | // some PDF.js features, e.g. range requests 23 | if (typeof PDFJS === 'undefined') { 24 | (typeof window !== 'undefined' ? window : this).PDFJS = {}; 25 | } 26 | 27 | // Checking if the typed arrays are supported 28 | // Support: iOS<6.0 (subarray), IE<10, Android<4.0 29 | (function checkTypedArrayCompatibility() { 30 | if (typeof Uint8Array !== 'undefined') { 31 | // Support: iOS<6.0 32 | if (typeof Uint8Array.prototype.subarray === 'undefined') { 33 | Uint8Array.prototype.subarray = function subarray(start, end) { 34 | return new Uint8Array(this.slice(start, end)); 35 | }; 36 | Float32Array.prototype.subarray = function subarray(start, end) { 37 | return new Float32Array(this.slice(start, end)); 38 | }; 39 | } 40 | 41 | // Support: Android<4.1 42 | if (typeof Float64Array === 'undefined') { 43 | window.Float64Array = Float32Array; 44 | } 45 | return; 46 | } 47 | 48 | function subarray(start, end) { 49 | return new TypedArray(this.slice(start, end)); 50 | } 51 | 52 | function setArrayOffset(array, offset) { 53 | if (arguments.length < 2) { 54 | offset = 0; 55 | } 56 | for (var i = 0, n = array.length; i < n; ++i, ++offset) { 57 | this[offset] = array[i] & 0xFF; 58 | } 59 | } 60 | 61 | function TypedArray(arg1) { 62 | var result, i, n; 63 | if (typeof arg1 === 'number') { 64 | result = []; 65 | for (i = 0; i < arg1; ++i) { 66 | result[i] = 0; 67 | } 68 | } else if ('slice' in arg1) { 69 | result = arg1.slice(0); 70 | } else { 71 | result = []; 72 | for (i = 0, n = arg1.length; i < n; ++i) { 73 | result[i] = arg1[i]; 74 | } 75 | } 76 | 77 | result.subarray = subarray; 78 | result.buffer = result; 79 | result.byteLength = result.length; 80 | result.set = setArrayOffset; 81 | 82 | if (typeof arg1 === 'object' && arg1.buffer) { 83 | result.buffer = arg1.buffer; 84 | } 85 | return result; 86 | } 87 | 88 | window.Uint8Array = TypedArray; 89 | window.Int8Array = TypedArray; 90 | 91 | // we don't need support for set, byteLength for 32-bit array 92 | // so we can use the TypedArray as well 93 | window.Uint32Array = TypedArray; 94 | window.Int32Array = TypedArray; 95 | window.Uint16Array = TypedArray; 96 | window.Float32Array = TypedArray; 97 | window.Float64Array = TypedArray; 98 | })(); 99 | 100 | // URL = URL || webkitURL 101 | // Support: Safari<7, Android 4.2+ 102 | (function normalizeURLObject() { 103 | if (!window.URL) { 104 | window.URL = window.webkitURL; 105 | } 106 | })(); 107 | 108 | // Object.defineProperty()? 109 | // Support: Android<4.0, Safari<5.1 110 | (function checkObjectDefinePropertyCompatibility() { 111 | if (typeof Object.defineProperty !== 'undefined') { 112 | var definePropertyPossible = true; 113 | try { 114 | // some browsers (e.g. safari) cannot use defineProperty() on DOM objects 115 | // and thus the native version is not sufficient 116 | Object.defineProperty(new Image(), 'id', { value: 'test' }); 117 | // ... another test for android gb browser for non-DOM objects 118 | var Test = function Test() {}; 119 | Test.prototype = { get id() { } }; 120 | Object.defineProperty(new Test(), 'id', 121 | { value: '', configurable: true, enumerable: true, writable: false }); 122 | } catch (e) { 123 | definePropertyPossible = false; 124 | } 125 | if (definePropertyPossible) { 126 | return; 127 | } 128 | } 129 | 130 | Object.defineProperty = function objectDefineProperty(obj, name, def) { 131 | delete obj[name]; 132 | if ('get' in def) { 133 | obj.__defineGetter__(name, def['get']); 134 | } 135 | if ('set' in def) { 136 | obj.__defineSetter__(name, def['set']); 137 | } 138 | if ('value' in def) { 139 | obj.__defineSetter__(name, function objectDefinePropertySetter(value) { 140 | this.__defineGetter__(name, function objectDefinePropertyGetter() { 141 | return value; 142 | }); 143 | return value; 144 | }); 145 | obj[name] = def.value; 146 | } 147 | }; 148 | })(); 149 | 150 | 151 | // No XMLHttpRequest#response? 152 | // Support: IE<11, Android <4.0 153 | (function checkXMLHttpRequestResponseCompatibility() { 154 | var xhrPrototype = XMLHttpRequest.prototype; 155 | var xhr = new XMLHttpRequest(); 156 | if (!('overrideMimeType' in xhr)) { 157 | // IE10 might have response, but not overrideMimeType 158 | // Support: IE10 159 | Object.defineProperty(xhrPrototype, 'overrideMimeType', { 160 | value: function xmlHttpRequestOverrideMimeType(mimeType) {} 161 | }); 162 | } 163 | if ('responseType' in xhr) { 164 | return; 165 | } 166 | 167 | // The worker will be using XHR, so we can save time and disable worker. 168 | PDFJS.disableWorker = true; 169 | 170 | Object.defineProperty(xhrPrototype, 'responseType', { 171 | get: function xmlHttpRequestGetResponseType() { 172 | return this._responseType || 'text'; 173 | }, 174 | set: function xmlHttpRequestSetResponseType(value) { 175 | if (value === 'text' || value === 'arraybuffer') { 176 | this._responseType = value; 177 | if (value === 'arraybuffer' && 178 | typeof this.overrideMimeType === 'function') { 179 | this.overrideMimeType('text/plain; charset=x-user-defined'); 180 | } 181 | } 182 | } 183 | }); 184 | 185 | // Support: IE9 186 | if (typeof VBArray !== 'undefined') { 187 | Object.defineProperty(xhrPrototype, 'response', { 188 | get: function xmlHttpRequestResponseGet() { 189 | if (this.responseType === 'arraybuffer') { 190 | return new Uint8Array(new VBArray(this.responseBody).toArray()); 191 | } else { 192 | return this.responseText; 193 | } 194 | } 195 | }); 196 | return; 197 | } 198 | 199 | Object.defineProperty(xhrPrototype, 'response', { 200 | get: function xmlHttpRequestResponseGet() { 201 | if (this.responseType !== 'arraybuffer') { 202 | return this.responseText; 203 | } 204 | var text = this.responseText; 205 | var i, n = text.length; 206 | var result = new Uint8Array(n); 207 | for (i = 0; i < n; ++i) { 208 | result[i] = text.charCodeAt(i) & 0xFF; 209 | } 210 | return result.buffer; 211 | } 212 | }); 213 | })(); 214 | 215 | // window.btoa (base64 encode function) ? 216 | // Support: IE<10 217 | (function checkWindowBtoaCompatibility() { 218 | if ('btoa' in window) { 219 | return; 220 | } 221 | 222 | var digits = 223 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 224 | 225 | window.btoa = function windowBtoa(chars) { 226 | var buffer = ''; 227 | var i, n; 228 | for (i = 0, n = chars.length; i < n; i += 3) { 229 | var b1 = chars.charCodeAt(i) & 0xFF; 230 | var b2 = chars.charCodeAt(i + 1) & 0xFF; 231 | var b3 = chars.charCodeAt(i + 2) & 0xFF; 232 | var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); 233 | var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; 234 | var d4 = i + 2 < n ? (b3 & 0x3F) : 64; 235 | buffer += (digits.charAt(d1) + digits.charAt(d2) + 236 | digits.charAt(d3) + digits.charAt(d4)); 237 | } 238 | return buffer; 239 | }; 240 | })(); 241 | 242 | // window.atob (base64 encode function)? 243 | // Support: IE<10 244 | (function checkWindowAtobCompatibility() { 245 | if ('atob' in window) { 246 | return; 247 | } 248 | 249 | // https://github.com/davidchambers/Base64.js 250 | var digits = 251 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 252 | window.atob = function (input) { 253 | input = input.replace(/=+$/, ''); 254 | if (input.length % 4 === 1) { 255 | throw new Error('bad atob input'); 256 | } 257 | for ( 258 | // initialize result and counters 259 | var bc = 0, bs, buffer, idx = 0, output = ''; 260 | // get next character 261 | buffer = input.charAt(idx++); 262 | // character found in table? 263 | // initialize bit storage and add its ascii value 264 | ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, 265 | // and if not first of each 4 characters, 266 | // convert the first 8 bits to one ascii character 267 | bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 268 | ) { 269 | // try to find character in table (0-63, not found => -1) 270 | buffer = digits.indexOf(buffer); 271 | } 272 | return output; 273 | }; 274 | })(); 275 | 276 | // Function.prototype.bind? 277 | // Support: Android<4.0, iOS<6.0 278 | (function checkFunctionPrototypeBindCompatibility() { 279 | if (typeof Function.prototype.bind !== 'undefined') { 280 | return; 281 | } 282 | 283 | Function.prototype.bind = function functionPrototypeBind(obj) { 284 | var fn = this, headArgs = Array.prototype.slice.call(arguments, 1); 285 | var bound = function functionPrototypeBindBound() { 286 | var args = headArgs.concat(Array.prototype.slice.call(arguments)); 287 | return fn.apply(obj, args); 288 | }; 289 | return bound; 290 | }; 291 | })(); 292 | 293 | // HTMLElement dataset property 294 | // Support: IE<11, Safari<5.1, Android<4.0 295 | (function checkDatasetProperty() { 296 | var div = document.createElement('div'); 297 | if ('dataset' in div) { 298 | return; // dataset property exists 299 | } 300 | 301 | Object.defineProperty(HTMLElement.prototype, 'dataset', { 302 | get: function() { 303 | if (this._dataset) { 304 | return this._dataset; 305 | } 306 | 307 | var dataset = {}; 308 | for (var j = 0, jj = this.attributes.length; j < jj; j++) { 309 | var attribute = this.attributes[j]; 310 | if (attribute.name.substring(0, 5) !== 'data-') { 311 | continue; 312 | } 313 | var key = attribute.name.substring(5).replace(/\-([a-z])/g, 314 | function(all, ch) { 315 | return ch.toUpperCase(); 316 | }); 317 | dataset[key] = attribute.value; 318 | } 319 | 320 | Object.defineProperty(this, '_dataset', { 321 | value: dataset, 322 | writable: false, 323 | enumerable: false 324 | }); 325 | return dataset; 326 | }, 327 | enumerable: true 328 | }); 329 | })(); 330 | 331 | // HTMLElement classList property 332 | // Support: IE<10, Android<4.0, iOS<5.0 333 | (function checkClassListProperty() { 334 | var div = document.createElement('div'); 335 | if ('classList' in div) { 336 | return; // classList property exists 337 | } 338 | 339 | function changeList(element, itemName, add, remove) { 340 | var s = element.className || ''; 341 | var list = s.split(/\s+/g); 342 | if (list[0] === '') { 343 | list.shift(); 344 | } 345 | var index = list.indexOf(itemName); 346 | if (index < 0 && add) { 347 | list.push(itemName); 348 | } 349 | if (index >= 0 && remove) { 350 | list.splice(index, 1); 351 | } 352 | element.className = list.join(' '); 353 | return (index >= 0); 354 | } 355 | 356 | var classListPrototype = { 357 | add: function(name) { 358 | changeList(this.element, name, true, false); 359 | }, 360 | contains: function(name) { 361 | return changeList(this.element, name, false, false); 362 | }, 363 | remove: function(name) { 364 | changeList(this.element, name, false, true); 365 | }, 366 | toggle: function(name) { 367 | changeList(this.element, name, true, true); 368 | } 369 | }; 370 | 371 | Object.defineProperty(HTMLElement.prototype, 'classList', { 372 | get: function() { 373 | if (this._classList) { 374 | return this._classList; 375 | } 376 | 377 | var classList = Object.create(classListPrototype, { 378 | element: { 379 | value: this, 380 | writable: false, 381 | enumerable: true 382 | } 383 | }); 384 | Object.defineProperty(this, '_classList', { 385 | value: classList, 386 | writable: false, 387 | enumerable: false 388 | }); 389 | return classList; 390 | }, 391 | enumerable: true 392 | }); 393 | })(); 394 | 395 | // Check console compatibility 396 | // In older IE versions the console object is not available 397 | // unless console is open. 398 | // Support: IE<10 399 | (function checkConsoleCompatibility() { 400 | if (!('console' in window)) { 401 | window.console = { 402 | log: function() {}, 403 | error: function() {}, 404 | warn: function() {} 405 | }; 406 | } else if (!('bind' in console.log)) { 407 | // native functions in IE9 might not have bind 408 | console.log = (function(fn) { 409 | return function(msg) { return fn(msg); }; 410 | })(console.log); 411 | console.error = (function(fn) { 412 | return function(msg) { return fn(msg); }; 413 | })(console.error); 414 | console.warn = (function(fn) { 415 | return function(msg) { return fn(msg); }; 416 | })(console.warn); 417 | } 418 | })(); 419 | 420 | // Check onclick compatibility in Opera 421 | // Support: Opera<15 422 | (function checkOnClickCompatibility() { 423 | // workaround for reported Opera bug DSK-354448: 424 | // onclick fires on disabled buttons with opaque content 425 | function ignoreIfTargetDisabled(event) { 426 | if (isDisabled(event.target)) { 427 | event.stopPropagation(); 428 | } 429 | } 430 | function isDisabled(node) { 431 | return node.disabled || (node.parentNode && isDisabled(node.parentNode)); 432 | } 433 | if (navigator.userAgent.indexOf('Opera') !== -1) { 434 | // use browser detection since we cannot feature-check this bug 435 | document.addEventListener('click', ignoreIfTargetDisabled, true); 436 | } 437 | })(); 438 | 439 | // Checks if possible to use URL.createObjectURL() 440 | // Support: IE 441 | (function checkOnBlobSupport() { 442 | // sometimes IE loosing the data created with createObjectURL(), see #3977 443 | if (navigator.userAgent.indexOf('Trident') >= 0) { 444 | PDFJS.disableCreateObjectURL = true; 445 | } 446 | })(); 447 | 448 | // Checks if navigator.language is supported 449 | (function checkNavigatorLanguage() { 450 | if ('language' in navigator) { 451 | return; 452 | } 453 | PDFJS.locale = navigator.userLanguage || 'en-US'; 454 | })(); 455 | 456 | (function checkRangeRequests() { 457 | // Safari has issues with cached range requests see: 458 | // https://github.com/mozilla/pdf.js/issues/3260 459 | // Last tested with version 6.0.4. 460 | // Support: Safari 6.0+ 461 | var isSafari = Object.prototype.toString.call( 462 | window.HTMLElement).indexOf('Constructor') > 0; 463 | 464 | // Older versions of Android (pre 3.0) has issues with range requests, see: 465 | // https://github.com/mozilla/pdf.js/issues/3381. 466 | // Make sure that we only match webkit-based Android browsers, 467 | // since Firefox/Fennec works as expected. 468 | // Support: Android<3.0 469 | var regex = /Android\s[0-2][^\d]/; 470 | var isOldAndroid = regex.test(navigator.userAgent); 471 | 472 | // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318 473 | var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent); 474 | 475 | if (isSafari || isOldAndroid || isChromeWithRangeBug) { 476 | PDFJS.disableRange = true; 477 | PDFJS.disableStream = true; 478 | } 479 | })(); 480 | 481 | // Check if the browser supports manipulation of the history. 482 | // Support: IE<10, Android<4.2 483 | (function checkHistoryManipulation() { 484 | // Android 2.x has so buggy pushState support that it was removed in 485 | // Android 3.0 and restored as late as in Android 4.2. 486 | // Support: Android 2.x 487 | if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) { 488 | PDFJS.disableHistory = true; 489 | } 490 | })(); 491 | 492 | // Support: IE<11, Chrome<21, Android<4.4, Safari<6 493 | (function checkSetPresenceInImageData() { 494 | // IE < 11 will use window.CanvasPixelArray which lacks set function. 495 | if (window.CanvasPixelArray) { 496 | if (typeof window.CanvasPixelArray.prototype.set !== 'function') { 497 | window.CanvasPixelArray.prototype.set = function(arr) { 498 | for (var i = 0, ii = this.length; i < ii; i++) { 499 | this[i] = arr[i]; 500 | } 501 | }; 502 | } 503 | } else { 504 | // Old Chrome and Android use an inaccessible CanvasPixelArray prototype. 505 | // Because we cannot feature detect it, we rely on user agent parsing. 506 | var polyfill = false, versionMatch; 507 | if (navigator.userAgent.indexOf('Chrom') >= 0) { 508 | versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); 509 | // Chrome < 21 lacks the set function. 510 | polyfill = versionMatch && parseInt(versionMatch[2]) < 21; 511 | } else if (navigator.userAgent.indexOf('Android') >= 0) { 512 | // Android < 4.4 lacks the set function. 513 | // Android >= 4.4 will contain Chrome in the user agent, 514 | // thus pass the Chrome check above and not reach this block. 515 | polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent); 516 | } else if (navigator.userAgent.indexOf('Safari') >= 0) { 517 | versionMatch = navigator.userAgent. 518 | match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//); 519 | // Safari < 6 lacks the set function. 520 | polyfill = versionMatch && parseInt(versionMatch[1]) < 6; 521 | } 522 | 523 | if (polyfill) { 524 | var contextPrototype = window.CanvasRenderingContext2D.prototype; 525 | contextPrototype._createImageData = contextPrototype.createImageData; 526 | contextPrototype.createImageData = function(w, h) { 527 | var imageData = this._createImageData(w, h); 528 | imageData.data.set = function(arr) { 529 | for (var i = 0, ii = this.length; i < ii; i++) { 530 | this[i] = arr[i]; 531 | } 532 | }; 533 | return imageData; 534 | }; 535 | } 536 | } 537 | })(); 538 | 539 | // Support: IE<10, Android<4.0, iOS 540 | (function checkRequestAnimationFrame() { 541 | function fakeRequestAnimationFrame(callback) { 542 | window.setTimeout(callback, 20); 543 | } 544 | 545 | var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); 546 | if (isIOS) { 547 | // requestAnimationFrame on iOS is broken, replacing with fake one. 548 | window.requestAnimationFrame = fakeRequestAnimationFrame; 549 | return; 550 | } 551 | if ('requestAnimationFrame' in window) { 552 | return; 553 | } 554 | window.requestAnimationFrame = 555 | window.mozRequestAnimationFrame || 556 | window.webkitRequestAnimationFrame || 557 | fakeRequestAnimationFrame; 558 | })(); 559 | 560 | (function checkCanvasSizeLimitation() { 561 | var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); 562 | var isAndroid = /Android/g.test(navigator.userAgent); 563 | if (isIOS || isAndroid) { 564 | // 5MP 565 | PDFJS.maxCanvasPixels = 5242880; 566 | } 567 | })(); 568 | 569 | // Disable fullscreen support for certain problematic configurations. 570 | // Support: IE11+ (when embedded). 571 | (function checkFullscreenSupport() { 572 | var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 && 573 | window.parent !== window); 574 | if (isEmbeddedIE) { 575 | PDFJS.disableFullscreen = true; 576 | } 577 | })(); 578 | -------------------------------------------------------------------------------- /docs/viewerjs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 31 | 32 | ViewerJS 33 | 38 | 39 | 77 | 78 | 79 | 80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 |
88 |
89 |
90 |
91 |
92 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 | 105 |
106 | 107 |
108 | 109 | 121 | 122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | ✖ 140 |
141 |
142 |
143 |
144 | 145 | 146 | --------------------------------------------------------------------------------