pdfviewer
Version:
PDF Viewer using Mozila PDF JS.
1,724 lines (1,563 loc) • 142 kB
JavaScript
/* 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.
*/
'use strict';
(function(window, document) {
/* default-preferences.js */
var DEFAULT_PREFERENCES = {
showPreviousViewOnLoad: true,
defaultZoomValue: '',
sidebarViewOnLoad: 0,
enableHandToolOnLoad: false,
enableWebGL: false,
pdfBugEnabled: false,
disableRange: false,
disableStream: false,
disableAutoFetch: false,
disableFontFace: false,
//#if B2G
//disableTextLayer: true,
//useOnlyCssZoom: true
//#else
disableTextLayer: false,
useOnlyCssZoom: false
//#endif
};
/* end default_preferences.js */
/* i10n.js */
document.webL10n = (function(window, document, undefined) {
var gL10nData = {};
var gTextData = '';
var gTextProp = 'textContent';
var gLanguage = '';
var gMacros = {};
var gReadyState = 'loading';
/**
* Synchronously loading l10n resources significantly minimizes flickering
* from displaying the app with non-localized strings and then updating the
* strings. Although this will block all script execution on this page, we
* expect that the l10n resources are available locally on flash-storage.
*
* As synchronous XHR is generally considered as a bad idea, we're still
* loading l10n resources asynchronously -- but we keep this in a setting,
* just in case... and applications using this library should hide their
* content until the `localized' event happens.
*/
var gAsyncResourceLoading = true; // read-only
/**
* DOM helpers for the so-called "HTML API".
*
* These functions are written for modern browsers. For old versions of IE,
* they're overridden in the 'startup' section at the end of this file.
*/
function getL10nResourceLinks() {
return document.querySelectorAll('link[type="application/l10n"]');
}
function getL10nDictionary() {
var script = document.querySelector('script[type="application/l10n"]');
// TODO: support multiple and external JSON dictionaries
return script ? JSON.parse(script.innerHTML) : null;
}
function getTranslatableChildren(element) {
return element ? element.querySelectorAll('*[data-l10n-id]') : [];
}
function getL10nAttributes(element) {
if (!element)
return {};
var l10nId = element.getAttribute('data-l10n-id');
var l10nArgs = element.getAttribute('data-l10n-args');
var args = {};
if (l10nArgs) {
try {
args = JSON.parse(l10nArgs);
} catch (e) {
console.warn('could not parse arguments for #' + l10nId);
}
}
return { id: l10nId, args: args };
}
function fireL10nReadyEvent(lang) {
var evtObject = document.createEvent('Event');
evtObject.initEvent('localized', true, false);
evtObject.language = lang;
document.dispatchEvent(evtObject);
}
function xhrLoadText(url, onSuccess, onFailure) {
onSuccess = onSuccess || function _onSuccess(data) {};
onFailure = onFailure || function _onFailure() {
console.warn(url + ' not found.');
};
var xhr = new XMLHttpRequest();
xhr.open('GET', url, gAsyncResourceLoading);
if (xhr.overrideMimeType) {
xhr.overrideMimeType('text/plain; charset=utf-8');
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) {
onSuccess(xhr.responseText);
} else {
onFailure();
}
}
};
xhr.onerror = onFailure;
xhr.ontimeout = onFailure;
// in Firefox OS with the app:// protocol, trying to XHR a non-existing
// URL will raise an exception here -- hence this ugly try...catch.
try {
xhr.send(null);
} catch (e) {
onFailure();
}
}
/**
* l10n resource parser:
* - reads (async XHR) the l10n resource matching `lang';
* - imports linked resources (synchronously) when specified;
* - parses the text data (fills `gL10nData' and `gTextData');
* - triggers success/failure callbacks when done.
*
* @param {string} href
* URL of the l10n resource to parse.
*
* @param {string} lang
* locale (language) to parse. Must be a lowercase string.
*
* @param {Function} successCallback
* triggered when the l10n resource has been successully parsed.
*
* @param {Function} failureCallback
* triggered when the an error has occured.
*
* @return {void}
* uses the following global variables: gL10nData, gTextData, gTextProp.
*/
function parseResource(href, lang, successCallback, failureCallback) {
var baseURL = href.replace(/[^\/]*$/, '') || './';
// handle escaped characters (backslashes) in a string
function evalString(text) {
if (text.lastIndexOf('\\') < 0)
return text;
return text.replace(/\\\\/g, '\\')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\b/g, '\b')
.replace(/\\f/g, '\f')
.replace(/\\{/g, '{')
.replace(/\\}/g, '}')
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
}
// parse *.properties text data into an l10n dictionary
// If gAsyncResourceLoading is false, then the callback will be called
// synchronously. Otherwise it is called asynchronously.
function parseProperties(text, parsedPropertiesCallback) {
var dictionary = {};
// token expressions
var reBlank = /^\s*|\s*$/;
var reComment = /^\s*#|^\s*$/;
var reSection = /^\s*\[(.*)\]\s*$/;
var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
// parse the *.properties file into an associative array
function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) {
var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
var currentLang = '*';
var genericLang = lang.split('-', 1)[0];
var skipLang = false;
var match = '';
function nextEntry() {
// Use infinite loop instead of recursion to avoid reaching the
// maximum recursion limit for content with many lines.
while (true) {
if (!entries.length) {
parsedRawLinesCallback();
return;
}
var line = entries.shift();
// comment or blank line?
if (reComment.test(line))
continue;
// the extended syntax supports [lang] sections and @import rules
if (extendedSyntax) {
match = reSection.exec(line);
if (match) { // section start?
// RFC 4646, section 4.4, "All comparisons MUST be performed
// in a case-insensitive manner."
currentLang = match[1].toLowerCase();
skipLang = (currentLang !== '*') &&
(currentLang !== lang) && (currentLang !== genericLang);
continue;
} else if (skipLang) {
continue;
}
match = reImport.exec(line);
if (match) { // @import rule?
loadImport(baseURL + match[1], nextEntry);
return;
}
}
// key-value pair
var tmp = line.match(reSplit);
if (tmp && tmp.length == 3) {
dictionary[tmp[1]] = evalString(tmp[2]);
}
}
}
nextEntry();
}
// import another *.properties file
function loadImport(url, callback) {
xhrLoadText(url, function(content) {
parseRawLines(content, false, callback); // don't allow recursive imports
}, null);
}
// fill the dictionary
parseRawLines(text, true, function() {
parsedPropertiesCallback(dictionary);
});
}
// load and parse l10n data (warning: global variables are used here)
xhrLoadText(href, function(response) {
gTextData += response; // mostly for debug
// parse *.properties text data into an l10n dictionary
parseProperties(response, function(data) {
// find attribute descriptions, if any
for (var key in data) {
var id, prop, index = key.lastIndexOf('.');
if (index > 0) { // an attribute has been specified
id = key.substring(0, index);
prop = key.substr(index + 1);
} else { // no attribute: assuming text content by default
id = key;
prop = gTextProp;
}
if (!gL10nData[id]) {
gL10nData[id] = {};
}
gL10nData[id][prop] = data[key];
}
// trigger callback
if (successCallback) {
successCallback();
}
});
}, failureCallback);
}
// load and parse all resources for the specified locale
function loadLocale(lang, callback) {
// RFC 4646, section 2.1 states that language tags have to be treated as
// case-insensitive. Convert to lowercase for case-insensitive comparisons.
if (lang) {
lang = lang.toLowerCase();
}
callback = callback || function _callback() {};
clear();
gLanguage = lang;
// check all <link type="application/l10n" href="..." /> nodes
// and load the resource files
var langLinks = getL10nResourceLinks();
var langCount = langLinks.length;
if (langCount === 0) {
// we might have a pre-compiled dictionary instead
var dict = getL10nDictionary();
if (dict && dict.locales && dict.default_locale) {
console.log('using the embedded JSON directory, early way out');
gL10nData = dict.locales[lang];
if (!gL10nData) {
var defaultLocale = dict.default_locale.toLowerCase();
for (var anyCaseLang in dict.locales) {
anyCaseLang = anyCaseLang.toLowerCase();
if (anyCaseLang === lang) {
gL10nData = dict.locales[lang];
break;
} else if (anyCaseLang === defaultLocale) {
gL10nData = dict.locales[defaultLocale];
}
}
}
callback();
} else {
console.log('no resource to load, early way out');
}
// early way out
fireL10nReadyEvent(lang);
gReadyState = 'complete';
return;
}
// start the callback when all resources are loaded
var onResourceLoaded = null;
var gResourceCount = 0;
onResourceLoaded = function() {
gResourceCount++;
if (gResourceCount >= langCount) {
callback();
fireL10nReadyEvent(lang);
gReadyState = 'complete';
}
};
// load all resource files
function L10nResourceLink(link) {
var href = link.href;
// Note: If |gAsyncResourceLoading| is false, then the following callbacks
// are synchronously called.
this.load = function(lang, callback) {
parseResource(href, lang, callback, function() {
console.warn(href + ' not found.');
// lang not found, used default resource instead
console.warn('"' + lang + '" resource not found');
gLanguage = '';
// Resource not loaded, but we still need to call the callback.
callback();
});
};
}
for (var i = 0; i < langCount; i++) {
var resource = new L10nResourceLink(langLinks[i]);
resource.load(lang, onResourceLoaded);
}
}
// clear all l10n data
function clear() {
gL10nData = {};
gTextData = '';
gLanguage = '';
// TODO: clear all non predefined macros.
// There's no such macro /yet/ but we're planning to have some...
}
/**
* Get rules for plural forms (shared with JetPack), see:
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
*
* @param {string} lang
* locale (language) used.
*
* @return {Function}
* returns a function that gives the plural form name for a given integer:
* var fun = getPluralRules('en');
* fun(1) -> 'one'
* fun(0) -> 'other'
* fun(1000) -> 'other'.
*/
function getPluralRules(lang) {
var locales2rules = {
'af': 3,
'ak': 4,
'am': 4,
'ar': 1,
'asa': 3,
'az': 0,
'be': 11,
'bem': 3,
'bez': 3,
'bg': 3,
'bh': 4,
'bm': 0,
'bn': 3,
'bo': 0,
'br': 20,
'brx': 3,
'bs': 11,
'ca': 3,
'cgg': 3,
'chr': 3,
'cs': 12,
'cy': 17,
'da': 3,
'de': 3,
'dv': 3,
'dz': 0,
'ee': 3,
'el': 3,
'en': 3,
'eo': 3,
'es': 3,
'et': 3,
'eu': 3,
'fa': 0,
'ff': 5,
'fi': 3,
'fil': 4,
'fo': 3,
'fr': 5,
'fur': 3,
'fy': 3,
'ga': 8,
'gd': 24,
'gl': 3,
'gsw': 3,
'gu': 3,
'guw': 4,
'gv': 23,
'ha': 3,
'haw': 3,
'he': 2,
'hi': 4,
'hr': 11,
'hu': 0,
'id': 0,
'ig': 0,
'ii': 0,
'is': 3,
'it': 3,
'iu': 7,
'ja': 0,
'jmc': 3,
'jv': 0,
'ka': 0,
'kab': 5,
'kaj': 3,
'kcg': 3,
'kde': 0,
'kea': 0,
'kk': 3,
'kl': 3,
'km': 0,
'kn': 0,
'ko': 0,
'ksb': 3,
'ksh': 21,
'ku': 3,
'kw': 7,
'lag': 18,
'lb': 3,
'lg': 3,
'ln': 4,
'lo': 0,
'lt': 10,
'lv': 6,
'mas': 3,
'mg': 4,
'mk': 16,
'ml': 3,
'mn': 3,
'mo': 9,
'mr': 3,
'ms': 0,
'mt': 15,
'my': 0,
'nah': 3,
'naq': 7,
'nb': 3,
'nd': 3,
'ne': 3,
'nl': 3,
'nn': 3,
'no': 3,
'nr': 3,
'nso': 4,
'ny': 3,
'nyn': 3,
'om': 3,
'or': 3,
'pa': 3,
'pap': 3,
'pl': 13,
'ps': 3,
'pt': 3,
'rm': 3,
'ro': 9,
'rof': 3,
'ru': 11,
'rwk': 3,
'sah': 0,
'saq': 3,
'se': 7,
'seh': 3,
'ses': 0,
'sg': 0,
'sh': 11,
'shi': 19,
'sk': 12,
'sl': 14,
'sma': 7,
'smi': 7,
'smj': 7,
'smn': 7,
'sms': 7,
'sn': 3,
'so': 3,
'sq': 3,
'sr': 11,
'ss': 3,
'ssy': 3,
'st': 3,
'sv': 3,
'sw': 3,
'syr': 3,
'ta': 3,
'te': 3,
'teo': 3,
'th': 0,
'ti': 4,
'tig': 3,
'tk': 3,
'tl': 4,
'tn': 3,
'to': 0,
'tr': 0,
'ts': 3,
'tzm': 22,
'uk': 11,
'ur': 3,
've': 3,
'vi': 0,
'vun': 3,
'wa': 4,
'wae': 3,
'wo': 0,
'xh': 3,
'xog': 3,
'yo': 0,
'zh': 0,
'zu': 3
};
// utility functions for plural rules methods
function isIn(n, list) {
return list.indexOf(n) !== -1;
}
function isBetween(n, start, end) {
return start <= n && n <= end;
}
// list of all plural rules methods:
// map an integer to the plural form name to use
var pluralRules = {
'0': function(n) {
return 'other';
},
'1': function(n) {
if ((isBetween((n % 100), 3, 10)))
return 'few';
if (n === 0)
return 'zero';
if ((isBetween((n % 100), 11, 99)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'2': function(n) {
if (n !== 0 && (n % 10) === 0)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'3': function(n) {
if (n == 1)
return 'one';
return 'other';
},
'4': function(n) {
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'5': function(n) {
if ((isBetween(n, 0, 2)) && n != 2)
return 'one';
return 'other';
},
'6': function(n) {
if (n === 0)
return 'zero';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'7': function(n) {
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'8': function(n) {
if ((isBetween(n, 3, 6)))
return 'few';
if ((isBetween(n, 7, 10)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'9': function(n) {
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'10': function(n) {
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
return 'few';
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
return 'one';
return 'other';
},
'11': function(n) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if ((n % 10) === 0 ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 11, 14)))
return 'many';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'12': function(n) {
if ((isBetween(n, 2, 4)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'13': function(n) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 12, 14)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'14': function(n) {
if ((isBetween((n % 100), 3, 4)))
return 'few';
if ((n % 100) == 2)
return 'two';
if ((n % 100) == 1)
return 'one';
return 'other';
},
'15': function(n) {
if (n === 0 || (isBetween((n % 100), 2, 10)))
return 'few';
if ((isBetween((n % 100), 11, 19)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'16': function(n) {
if ((n % 10) == 1 && n != 11)
return 'one';
return 'other';
},
'17': function(n) {
if (n == 3)
return 'few';
if (n === 0)
return 'zero';
if (n == 6)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'18': function(n) {
if (n === 0)
return 'zero';
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
return 'one';
return 'other';
},
'19': function(n) {
if ((isBetween(n, 2, 10)))
return 'few';
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'20': function(n) {
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
isBetween((n % 100), 10, 19) ||
isBetween((n % 100), 70, 79) ||
isBetween((n % 100), 90, 99)
))
return 'few';
if ((n % 1000000) === 0 && n !== 0)
return 'many';
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
return 'two';
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
return 'one';
return 'other';
},
'21': function(n) {
if (n === 0)
return 'zero';
if (n == 1)
return 'one';
return 'other';
},
'22': function(n) {
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
return 'one';
return 'other';
},
'23': function(n) {
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
return 'one';
return 'other';
},
'24': function(n) {
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
return 'few';
if (isIn(n, [2, 12]))
return 'two';
if (isIn(n, [1, 11]))
return 'one';
return 'other';
}
};
// return a function that gives the plural form name for a given integer
var index = locales2rules[lang.replace(/-.*$/, '')];
if (!(index in pluralRules)) {
console.warn('plural form unknown for [' + lang + ']');
return function() { return 'other'; };
}
return pluralRules[index];
}
// pre-defined 'plural' macro
gMacros.plural = function(str, param, key, prop) {
var n = parseFloat(param);
if (isNaN(n))
return str;
// TODO: support other properties (l20n still doesn't...)
if (prop != gTextProp)
return str;
// initialize _pluralRules
if (!gMacros._pluralRules) {
gMacros._pluralRules = getPluralRules(gLanguage);
}
var index = '[' + gMacros._pluralRules(n) + ']';
// try to find a [zero|one|two] key if it's defined
if (n === 0 && (key + '[zero]') in gL10nData) {
str = gL10nData[key + '[zero]'][prop];
} else if (n == 1 && (key + '[one]') in gL10nData) {
str = gL10nData[key + '[one]'][prop];
} else if (n == 2 && (key + '[two]') in gL10nData) {
str = gL10nData[key + '[two]'][prop];
} else if ((key + index) in gL10nData) {
str = gL10nData[key + index][prop];
} else if ((key + '[other]') in gL10nData) {
str = gL10nData[key + '[other]'][prop];
}
return str;
};
/**
* l10n dictionary functions
*/
// fetch an l10n object, warn if not found, apply `args' if possible
function getL10nData(key, args, fallback) {
var data = gL10nData[key];
if (!data) {
console.warn('#' + key + ' is undefined.');
if (!fallback) {
return null;
}
data = fallback;
}
/** This is where l10n expressions should be processed.
* The plan is to support C-style expressions from the l20n project;
* until then, only two kinds of simple expressions are supported:
* {[ index ]} and {{ arguments }}.
*/
var rv = {};
for (var prop in data) {
var str = data[prop];
str = substIndexes(str, args, key, prop);
str = substArguments(str, args, key);
rv[prop] = str;
}
return rv;
}
// replace {[macros]} with their values
function substIndexes(str, args, key, prop) {
var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
var reMatch = reIndex.exec(str);
if (!reMatch || !reMatch.length)
return str;
// an index/macro has been found
// Note: at the moment, only one parameter is supported
var macroName = reMatch[1];
var paramName = reMatch[2];
var param;
if (args && paramName in args) {
param = args[paramName];
} else if (paramName in gL10nData) {
param = gL10nData[paramName];
}
// there's no macro parser yet: it has to be defined in gMacros
if (macroName in gMacros) {
var macro = gMacros[macroName];
str = macro(str, param, key, prop);
}
return str;
}
// replace {{arguments}} with their values
function substArguments(str, args, key) {
var reArgs = /\{\{\s*(.+?)\s*\}\}/g;
return str.replace(reArgs, function(matched_text, arg) {
if (args && arg in args) {
return args[arg];
}
if (arg in gL10nData) {
return gL10nData[arg];
}
console.log('argument {{' + arg + '}} for #' + key + ' is undefined.');
return matched_text;
});
}
// translate an HTML element
function translateElement(element) {
var l10n = getL10nAttributes(element);
if (!l10n.id)
return;
// get the related l10n object
var data = getL10nData(l10n.id, l10n.args);
if (!data) {
console.warn('#' + l10n.id + ' is undefined.');
return;
}
// translate element (TODO: security checks?)
if (data[gTextProp]) { // XXX
if (getChildElementCount(element) === 0) {
element[gTextProp] = data[gTextProp];
} else {
// this element has element children: replace the content of the first
// (non-empty) child textNode and clear other child textNodes
var children = element.childNodes;
var found = false;
for (var i = 0, l = children.length; i < l; i++) {
if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
if (found) {
children[i].nodeValue = '';
} else {
children[i].nodeValue = data[gTextProp];
found = true;
}
}
}
// if no (non-empty) textNode is found, insert a textNode before the
// first element child.
if (!found) {
var textNode = document.createTextNode(data[gTextProp]);
element.insertBefore(textNode, element.firstChild);
}
}
delete data[gTextProp];
}
for (var k in data) {
element[k] = data[k];
}
}
// webkit browsers don't currently support 'children' on SVG elements...
function getChildElementCount(element) {
if (element.children) {
return element.children.length;
}
if (typeof element.childElementCount !== 'undefined') {
return element.childElementCount;
}
var count = 0;
for (var i = 0; i < element.childNodes.length; i++) {
count += element.nodeType === 1 ? 1 : 0;
}
return count;
}
// translate an HTML subtree
function translateFragment(element) {
element = element || document.documentElement;
// check all translatable children (= w/ a `data-l10n-id' attribute)
var children = getTranslatableChildren(element);
var elementCount = children.length;
for (var i = 0; i < elementCount; i++) {
translateElement(children[i]);
}
// translate element itself if necessary
translateElement(element);
}
return {
// get a localized string
get: function(key, args, fallbackString) {
var index = key.lastIndexOf('.');
var prop = gTextProp;
if (index > 0) { // An attribute has been specified
prop = key.substr(index + 1);
key = key.substring(0, index);
}
var fallback;
if (fallbackString) {
fallback = {};
fallback[prop] = fallbackString;
}
var data = getL10nData(key, args, fallback);
if (data && prop in data) {
return data[prop];
}
return '{{' + key + '}}';
},
// debug
getData: function() { return gL10nData; },
getText: function() { return gTextData; },
// get|set the document language
getLanguage: function() { return gLanguage; },
setLanguage: function(lang, callback) {
loadLocale(lang, function() {
if (callback)
callback();
translateFragment();
});
},
// get the direction (ltr|rtl) of the current language
getDirection: function() {
// http://www.w3.org/International/questions/qa-scripts
// Arabic, Hebrew, Farsi, Pashto, Urdu
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
var shortCode = gLanguage.split('-', 1)[0];
return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';
},
// translate an element or document fragment
translate: translateFragment,
// this can be used to prevent race conditions
getReadyState: function() { return gReadyState; },
ready: function(callback) {
if (!callback) {
return;
} else if (gReadyState == 'complete' || gReadyState == 'interactive') {
window.setTimeout(function() {
callback();
});
} else if (document.addEventListener) {
document.addEventListener('localized', function once() {
document.removeEventListener('localized', once);
callback();
});
}
}
};
}) (window, document);
/* end l10n.js */
/* ui_utils.js */
var CSS_UNITS = 96.0 / 72.0;
var DEFAULT_SCALE = 'auto';
var UNKNOWN_SCALE = 0;
var MAX_AUTO_SCALE = 1.25;
var SCROLLBAR_PADDING = 40;
var VERTICAL_PADDING = 5;
// 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;
})();
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);
}
/**
* 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.
*/
function getOutputScale(ctx) {
var devicePixelRatio = window.devicePixelRatio || 1;
var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
var pixelRatio = devicePixelRatio / backingStoreRatio;
return {
sx: pixelRatio,
sy: pixelRatio,
scaled: pixelRatio !== 1
};
}
/**
* Scrolls specified element into view of its parent.
* element {Object} The element to be visible.
* spot {Object} An object with optional top and left properties,
* specifying the offset from the top left edge.
*/
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;
var offsetX = element.offsetLeft + element.clientLeft;
if (!parent) {
console.error('offsetParent is not set -- cannot scroll');
return;
}
while (parent.clientHeight === parent.scrollHeight) {
if (parent.dataset._scaleY) {
offsetY /= parent.dataset._scaleY;
offsetX /= parent.dataset._scaleX;
}
offsetY += parent.offsetTop;
offsetX += parent.offsetLeft;
parent = parent.offsetParent;
if (!parent) {
return; // no need to scroll
}
}
if (spot) {
if (spot.top !== undefined) {
offsetY += spot.top;
}
if (spot.left !== undefined) {
offsetX += spot.left;
parent.scrollLeft = offsetX;
}
}
parent.scrollTop = offsetY;
}
/**
* Helper function to start monitoring the scroll event and converting them into
* PDF.js friendly one: with scroll debounce and scroll direction.
*/
function watchScroll(viewAreaElement, callback) {
var debounceScroll = function debounceScroll(evt) {
if (rAF) {
return;
}
// schedule an invocation of scroll for next animation frame.
rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
rAF = null;
var currentY = viewAreaElement.scrollTop;
var lastY = state.lastY;
if (currentY !== lastY) {
state.down = currentY > lastY;
}
state.lastY = currentY;
callback(state);
});
};
var state = {
down: true,
lastY: viewAreaElement.scrollTop,
_eventHandler: debounceScroll
};
var rAF = null;
viewAreaElement.addEventListener('scroll', debounceScroll, true);
return state;
}
/**
* Use binary search to find the index of the first item in a given array which
* passes a given condition. The items are expected to be sorted in the sense
* that if the condition is true for one item in the array, then it is also true
* for all following items.
*
* @returns {Number} Index of the first array element to pass the test,
* or |items.length| if no such element exists.
*/
function binarySearchFirstItem(items, condition) {
var minIndex = 0;
var maxIndex = items.length - 1;
if (items.length === 0 || !condition(items[maxIndex])) {
return items.length;
}
if (condition(items[minIndex])) {
return minIndex;
}
while (minIndex < maxIndex) {
var currentIndex = (minIndex + maxIndex) >> 1;
var currentItem = items[currentIndex];
if (condition(currentItem)) {
maxIndex = currentIndex;
} else {
minIndex = currentIndex + 1;
}
}
return minIndex; /* === maxIndex */
}
/**
* Generic helper to find out what elements are visible within a scroll pane.
*/
function getVisibleElements(scrollEl, views, sortByVisibility) {
var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
function isElementBottomBelowViewTop(view) {
var element = view.div;
var elementBottom =
element.offsetTop + element.clientTop + element.clientHeight;
return elementBottom > top;
}
var visible = [], view, element;
var currentHeight, viewHeight, hiddenHeight, percentHeight;
var currentWidth, viewWidth;
var firstVisibleElementInd = (views.length === 0) ? 0 :
binarySearchFirstItem(views, isElementBottomBelowViewTop);
for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) {
view = views[i];
element = view.div;
currentHeight = element.offsetTop + element.clientTop;
viewHeight = element.clientHeight;
if (currentHeight > bottom) {
break;
}
currentWidth = element.offsetLeft + element.clientLeft;
viewWidth = element.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,
x: currentWidth,
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};
}
/**
* Event handler to suppress context menu.
*/
function noContextMenuHandler(e) {
e.preventDefault();
}
/**
* Returns the filename or guessed filename from the url (see issue 3455).
* url {String} The original PDF location.
* @return {String} Guessed PDF file name.
*/
function getPDFFileNameFromURL(url) {
var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
// SCHEME HOST 1.PATH 2.QUERY 3.REF
// Pattern to get last matching NAME.pdf
var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
var splitURI = reURI.exec(url);
var suggestedFilename = reFilename.exec(splitURI[1]) ||
reFilename.exec(splitURI[2]) ||
reFilename.exec(splitURI[3]);
if (suggestedFilename) {
suggestedFilename = suggestedFilename[0];
if (suggestedFilename.indexOf('%') !== -1) {
// URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
try {
suggestedFilename =
reFilename.exec(decodeURIComponent(suggestedFilename))[0];
} catch(e) { // Possible (extremely rare) errors:
// URIError "Malformed URI", e.g. for "%AA.pdf"
// TypeError "null has no properties", e.g. for "%2F.pdf"
}
}
}
return suggestedFilename || 'document.pdf';
}
var ProgressBar = (function ProgressBarClosure() {
function clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
function ProgressBar(id, opts) {
this.visible = true;
// Fetch the sub-elements for later.
this.div = document.querySelector(id + ' .progress');
// Get the loading bar element, so it can be resized to fit the viewer.
this.bar = this.div.parentNode;
// 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;
this.percent = 0;
}
ProgressBar.prototype = {
updateBar: function ProgressBar_updateBar() {
if (this._indeterminate) {
this.div.classList.add('indeterminate');
this.div.style.width = this.width + this.units;
return;
}
this.div.classList.remove('indeterminate');
var progressSize = this.width * this._percent / 100;
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();
},
setWidth: function ProgressBar_setWidth(viewer) {
if (viewer) {
var container = viewer.parentNode;
var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
if (scrollbarWidth > 0) {
this.bar.setAttribute('style', 'width: calc(100% - ' +
scrollbarWidth + 'px);');
}
}
},
hide: function ProgressBar_hide() {
if (!this.visible) {
return;
}
this.visible = false;
this.bar.classList.add('hidden');
document.body.classList.remove('loadingInProgress');
},
show: function ProgressBar_show() {
if (this.visible) {
return;
}
this.visible = true;
document.body.classList.add('loadingInProgress');
this.bar.classList.remove('hidden');
}
};
return ProgressBar;
})();
/* end ui_utils.js */
/* preferences.js */
/**
* Preferences - Utility for storing persistent settings.
* Used for settings that should be applied to all opened documents,
* or every time the viewer is loaded.
*/
var Preferences = {
prefs: Object.create(DEFAULT_PREFERENCES),
isInitializedPromiseResolved: false,
initializedPromise: null,
/**
* Initialize and fetch the current preference values from storage.
* @return {Promise} A promise that is resolved when the preferences
* have been initialized.
*/
initialize: function preferencesInitialize() {
return this.initializedPromise =
this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) {
this.isInitializedPromiseResolved = true;
if (prefObj) {
this.prefs = prefObj;
}
}.bind(this));
},
/**
* Stub function for writing preferences to storage.
* NOTE: This should be overridden by a build-specific function defined below.
* @param {Object} prefObj The preferences that should be written to storage.
* @return {Promise} A promise that is resolved when the preference values
* have been written.
*/
_writeToStorage: function preferences_writeToStorage(prefObj) {
return Promise.resolve();
},
/**
* Stub function for reading preferences from storage.
* NOTE: This should be overridden by a build-specific function defined below.
* @param {Object} prefObj The preferences that should be read from storage.
* @return {Promise} A promise that is resolved with an {Object} containing
* the preferences that have been read.
*/
_readFromStorage: function preferences_readFromStorage(prefObj) {
return Promise.resolve();
},
/**
* Reset the preferences to their default values and update storage.
* @return {Promise} A promise that is resolved when the preference values
* have been reset.
*/
reset: function preferencesReset() {
return this.initializedPromise.then(function() {
this.prefs = Object.create(DEFAULT_PREFERENCES);
return this._writeToStorage(DEFAULT_PREFERENCES);
}.bind(this));
},
/**
* Replace the current preference values with the ones from storage.
* @return {Promise} A promise that is resolved when the preference values
* have been updated.
*/
reload: function preferencesReload() {
return this.initializedPromise.then(function () {
this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) {
if (prefObj) {
this.prefs = prefObj;
}
}.bind(this));
}.bind(this));
},
/**
* Set the value of a preference.
* @param {string} name The name of the preference that should be changed.
* @param {boolean|number|string} value The new value of the preference.
* @return {Promise} A promise that is resolved when the value has been set,
* provided that the preference exists and the types match.
*/
set: function preferencesSet(name, value) {
return this.initializedPromise.then(function () {
if (DEFAULT_PREFERENCES[name] === undefined) {
throw new Error('preferencesSet: \'' + name + '\' is undefined.');
} else if (value === undefined) {
throw new Error('preferencesSet: no value is specified.');
}
var valueType = typeof value;
var defaultType = typeof DEFAULT_PREFERENCES[name];
if (valueType !== defaultType) {
if (valueType === 'number' && defaultType === 'string') {
value = value.toString();
} else {
throw new Error('Preferences_set: \'' + value + '\' is a \"' +
valueType + '\", expected \"' + defaultType + '\".');
}
} else {
if (valueType === 'number' && (value | 0) !== value) {
throw new Error('Preferences_set: \'' + value +
'\' must be an \"integer\".');
}
}
this.prefs[name] = value;
return this._writeToStorage(this.prefs);
}.bind(this));
},
/**
* Get the value of a preference.
* @param {string} name The name of the preference whose value is requested.
* @return {Promise} A promise that is resolved with a {boolean|number|string}
* containing the value of the preference.
*/
get: function preferencesGet(name) {
return this.initializedPromise.then(function () {
var defaultValue = DEFAULT_PREFERENCES[name];
if (defaultValue === undefined) {
throw new Error('preferencesGet: \'' + name + '\' is undefined.');
} else {
var prefValue = this.prefs[name];
if (prefValue !== undefined) {
return prefValue;
}
}
return defaultValue;
}.bind(this));
}
};
//#if !(FIREFOX || MOZCENTRAL || B2G || CHROME)
Preferences._writeToStorage = function (prefObj) {
return new Promise(function (resolve) {
localStorage.setItem('pdfjs.preferences', JSON.stringify(prefObj));
resolve();
});
};
Preferences._readFromStorage = function (prefObj) {
return new Promise(function (resolve) {
var readPrefs = JSON.parse(localStorage.getItem('pdfjs.preferences'));
resolve(readPrefs);
});
};
//#endif
/* end preferences.js */
/* download_manager.js */
var DownloadManager = (function DownloadManagerClosure() {
function download(blobUrl, filename) {
var a = document.createElement('a');
if (a.click) {
// Use a.click() if available. Otherwise, Chrome might show
// "Unsafe JavaScript attempt to initiate a navigation change
// for frame with URL" and not open the PDF at all.
// Supported by (not mentioned = untested):
// - Firefox 6 - 19 (4- does not support a.click, 5 ignores a.click)
// - Chrome 19 - 26 (18- does not support a.click)
// - Opera 9 - 12.15
// - Internet Explorer 6 - 10
// - Safari 6 (5.1- does not support a.click)
a.href = blobUrl;
a.target = '_parent';
// Use a.download if available. This increases the likelihood that
// the file is downloaded instead of opened by another PDF plugin.
if ('download' in a) {
a.download = filename;
}
// <a> must be in the document for IE and recent Firefox versions.
// (otherwise .click() is ignored)
(document.body || document.documentElement).appendChild(a);
a.click();
a.parentNode.removeChild(a);
} else {
if (window.top === window &&
blobUrl.split('#')[0] === window.location.href.split('#')[0]) {
// If _parent == self, then opening an identical URL with different
// location hash will only cause a navigation, not a download.
var padCharacter = blobUrl.indexOf('?') === -1 ? '?' : '&';
blobUrl = blobUrl.replace(/#|$/, padCharacter + '$&');
}
window.open(blobUrl, '_parent');
}
}
function DownloadManager() {}
DownloadManager.prototype = {
downloadUrl: function DownloadManager_downloadUrl(url, filename) {
if (!PDFJS.isValidUrl(url, true)) {
return; // restricted/invalid URL
}
download(url + '#pdfjs.action=download', filename);
},
downloadData: function DownloadManager_downloadData(data, filename,
contentType) {
if (navigator.msSaveBlob) { // IE10 and above
return navigator.msSaveBlob(new Blob([data], { type: contentType }),
filename);
}
var blobUrl = PDFJS.createObjectURL(data, contentType);
download(blobUrl, filename);
},
download: function DownloadManager_download(blob, url, filename) {
if (!URL) {
// URL.createObjectURL is not supported
this.downloadUrl(url, filename);
return;
}
if (navigator.msSaveBlob) {
// IE10 / IE11
if (!navigator.msSaveBlob(blob, filename)) {
this.downloadUrl(url, filename);
}
return;
}
var blobUrl = URL.createObjectURL(blob);
download(blobUrl, filename);
}
};
return DownloadManager;
})();
/* end download_manager.js */
/* pdf_rendering_queue.js */
var CLEANUP_TIMEOUT = 30000;
var RenderingStates = {
INITIAL: 0,
RUNNING: 1,
PAUSED: 2,
FINISHED: 3
};
/**
* Controls rendering of the views for pages and thumbnails.
* @class
*/
var PDFRenderingQueue = (function PDFRenderingQueueClosure() {
/**
* @constructs
*/
function PDFRenderingQueue() {
this.pdfViewer = null;
this.pdfThumbnailViewer = null;
this.onIdle = null;
this.highestPriorityPage = null;
this.idleTimeout = null;
this.printing = false;
this.isThumbnailViewEnabled = false;
}
PDFRenderingQueue.prototype = /** @lends PDFRenderingQueue.prototype */ {
/**
* @param {PDFViewer} pdfViewer
*/
setViewer: function PDFRenderingQueue_setViewer(pdfViewer) {
this.pdfViewer = pdfViewer;
},
/**
* @param {PDFThumbnailViewer} pdfThumbnailViewer
*/
setThumbnailViewer:
function PDFRenderingQueue_setThumbnailViewer(pdfThumbnailViewer) {
this.pdfThumbnailViewer = pdfThumbnailViewer;
},
/**
* @param {IRenderableView} view
* @returns {boolean}
*/
isHighestPriority: function PDFRenderingQueue_isHighestPriority(view) {
return this.highestPriorityPage === view.renderingId;
},
renderHighestPriority: function
PDFRenderingQueue_renderHighestPriority(currentlyVisiblePages) {
if (this.idleTimeout) {
clearTimeout(this.idleTimeout);
this.idleTimeout = null;
}
// Pages have a higher priority than thumbnails, so check them first