diff --git a/extensions/chrome/.gitignore b/extensions/chrome/.gitignore
new file mode 100644
index 000000000..3eb92306c
--- /dev/null
+++ b/extensions/chrome/.gitignore
@@ -0,0 +1 @@
+content/
diff --git a/extensions/firefox/.gitignore b/extensions/firefox/.gitignore
index 08a23850c..6eec9a7f2 100644
--- a/extensions/firefox/.gitignore
+++ b/extensions/firefox/.gitignore
@@ -1,2 +1,4 @@
content/
metadata.inc
+chrome.manifest.inc
+locale/
diff --git a/extensions/firefox/bootstrap.js b/extensions/firefox/bootstrap.js
index d03812bcb..5bf66d444 100644
--- a/extensions/firefox/bootstrap.js
+++ b/extensions/firefox/bootstrap.js
@@ -10,13 +10,26 @@ let Cc = Components.classes;
let Ci = Components.interfaces;
let Cm = Components.manager;
let Cu = Components.utils;
-let application = Cc['@mozilla.org/fuel/application;1']
- .getService(Ci.fuelIApplication);
Cu.import('resource://gre/modules/Services.jsm');
+function getBoolPref(pref, def) {
+ try {
+ return Services.prefs.getBoolPref(pref);
+ } catch (ex) {
+ return def;
+ }
+}
+
+function setStringPref(pref, value) {
+ let str = Cc['@mozilla.org/supports-string;1']
+ .createInstance(Ci.nsISupportsString);
+ str.data = value;
+ Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str);
+}
+
function log(str) {
- if (!application.prefs.getValue(EXT_PREFIX + '.pdfBugEnabled', false))
+ if (!getBoolPref(EXT_PREFIX + '.pdfBugEnabled', false))
return;
dump(str + '\n');
}
@@ -93,6 +106,6 @@ function install(aData, aReason) {
}
function uninstall(aData, aReason) {
- application.prefs.setValue(EXT_PREFIX + '.database', '{}');
+ setStringPref(EXT_PREFIX + '.database', '{}');
}
diff --git a/extensions/firefox/chrome.manifest b/extensions/firefox/chrome.manifest
new file mode 100644
index 000000000..97b76306b
--- /dev/null
+++ b/extensions/firefox/chrome.manifest
@@ -0,0 +1,3 @@
+# Additional resources for pdf.js
+
+# PDFJS_SUPPORTED_LOCALES
diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js
index af9cf41b8..1bcccbe22 100644
--- a/extensions/firefox/components/PdfStreamConverter.js
+++ b/extensions/firefox/components/PdfStreamConverter.js
@@ -13,18 +13,51 @@ const PDFJS_EVENT_ID = 'pdf.js.message';
const PDF_CONTENT_TYPE = 'application/pdf';
const EXT_PREFIX = 'extensions.uriloader@pdf.js';
const MAX_DATABASE_LENGTH = 4096;
+const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}';
+const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}';
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/NetUtil.jsm');
-let application = Cc['@mozilla.org/fuel/application;1']
- .getService(Ci.fuelIApplication);
-let privateBrowsing = Cc['@mozilla.org/privatebrowsing;1']
- .getService(Ci.nsIPrivateBrowsingService);
-let inPrivateBrowswing = privateBrowsing.privateBrowsingEnabled;
+let appInfo = Cc['@mozilla.org/xre/app-info;1']
+ .getService(Ci.nsIXULAppInfo);
+let privateBrowsing, inPrivateBrowsing;
+
+if (appInfo.ID === FIREFOX_ID) {
+ privateBrowsing = Cc['@mozilla.org/privatebrowsing;1']
+ .getService(Ci.nsIPrivateBrowsingService);
+ inPrivateBrowsing = privateBrowsing.privateBrowsingEnabled;
+} else if (appInfo.ID === SEAMONKEY_ID) {
+ privateBrowsing = null;
+ inPrivateBrowsing = false;
+}
+
+function getBoolPref(pref, def) {
+ try {
+ return Services.prefs.getBoolPref(pref);
+ } catch (ex) {
+ return def;
+ }
+}
+
+function setStringPref(pref, value) {
+ let str = Cc['@mozilla.org/supports-string;1']
+ .createInstance(Ci.nsISupportsString);
+ str.data = value;
+ Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str);
+}
+
+function getStringPref(pref, def) {
+ try {
+ return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
+ } catch (ex) {
+ return def;
+ }
+}
function log(aMsg) {
- if (!application.prefs.getValue(EXT_PREFIX + '.pdfBugEnabled', false))
+ if (!getBoolPref(EXT_PREFIX + '.pdfBugEnabled', false))
return;
let msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
Services.console.logStringMessage(msg);
@@ -37,32 +70,97 @@ function getDOMWindow(aChannel) {
return win;
}
+function getLocalizedStrings(path) {
+ var stringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
+ getService(Ci.nsIStringBundleService).
+ createBundle('chrome://pdf.js/locale/' + path);
+
+ var map = {};
+ var enumerator = stringBundle.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ var key = string.key, property = 'textContent';
+ var i = key.lastIndexOf('.');
+ if (i >= 0) {
+ property = key.substring(i + 1);
+ key = key.substring(0, i);
+ }
+ if (!(key in map))
+ map[key] = {};
+ map[key][property] = string.value;
+ }
+ return map;
+}
+
// All the priviledged actions.
function ChromeActions() {
- this.inPrivateBrowswing = privateBrowsing.privateBrowsingEnabled;
}
+
ChromeActions.prototype = {
download: function(data) {
- Services.wm.getMostRecentWindow('navigator:browser').saveURL(data);
+ let mimeService = Cc['@mozilla.org/mime;1'].getService(Ci.nsIMIMEService);
+ var handlerInfo = mimeService.
+ getFromTypeAndExtension('application/pdf', 'pdf');
+ var uri = NetUtil.newURI(data);
+
+ var extHelperAppSvc =
+ Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
+ getService(Ci.nsIExternalHelperAppService);
+ var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1'].
+ getService(Ci.nsIWindowWatcher).activeWindow;
+ var ioService = Services.io;
+ var channel = ioService.newChannel(data, null, null);
+ var listener = {
+ extListener: null,
+ onStartRequest: function(aRequest, aContext) {
+ this.extListener = extHelperAppSvc.doContent('application/pdf',
+ aRequest, frontWindow, false);
+ this.extListener.onStartRequest(aRequest, aContext);
+ },
+ onStopRequest: function(aRequest, aContext, aStatusCode) {
+ if (this.extListener)
+ this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+ onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
+ aCount) {
+ this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount);
+ }
+ };
+
+ channel.asyncOpen(listener, null);
},
setDatabase: function(data) {
- if (this.inPrivateBrowswing)
+ if (inPrivateBrowsing)
return;
// Protect against something sending tons of data to setDatabase.
if (data.length > MAX_DATABASE_LENGTH)
return;
- application.prefs.setValue(EXT_PREFIX + '.database', data);
+ setStringPref(EXT_PREFIX + '.database', data);
},
getDatabase: function() {
- if (this.inPrivateBrowswing)
+ if (inPrivateBrowsing)
return '{}';
- return application.prefs.getValue(EXT_PREFIX + '.database', '{}');
+ return getStringPref(EXT_PREFIX + '.database', '{}');
},
getLocale: function() {
- return application.prefs.getValue('general.useragent.locale', 'en-US');
+ return getStringPref('general.useragent.locale', 'en-US');
+ },
+ getStrings: function(data) {
+ try {
+ // Lazy initialization of localizedStrings
+ if (!('localizedStrings' in this))
+ this.localizedStrings = getLocalizedStrings('viewer.properties');
+
+ var result = this.localizedStrings[data];
+ return JSON.stringify(result || null);
+ } catch (e) {
+ log('Unable to retrive localized strings: ' + e);
+ return 'null';
+ }
},
pdfBugEnabled: function() {
- return application.prefs.getValue(EXT_PREFIX + '.pdfBugEnabled', false);
+ return getBoolPref(EXT_PREFIX + '.pdfBugEnabled', false);
}
};
diff --git a/extensions/firefox/install.rdf b/extensions/firefox/install.rdf
index d7eea9319..a5094c19a 100644
--- a/extensions/firefox/install.rdf
+++ b/extensions/firefox/install.rdf
@@ -8,11 +8,22 @@
PDF Viewer
PDFJSSCRIPT_VERSION
+
+
{ec8030f7-c20a-464f-9b0e-13a3a9e97384}
6.0
- 14.0a1
+ 15.0a1
+
+
+
+
+
+
+ {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+ 2.1.*
+ 2.12a1
true
diff --git a/extensions/firefox/tools/l10n.js b/extensions/firefox/tools/l10n.js
new file mode 100644
index 000000000..b16636e31
--- /dev/null
+++ b/extensions/firefox/tools/l10n.js
@@ -0,0 +1,114 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// Small subset of the webL10n API by Fabien Cazenave for pdf.js extension.
+(function(window) {
+ var gLanguage = '';
+
+ // fetch an l10n objects
+ function getL10nData(key) {
+ var response = FirefoxCom.request('getStrings', key);
+ var data = JSON.parse(response);
+ if (!data)
+ console.warn('[l10n] #' + key + ' missing for [' + gLanguage + ']');
+ return data;
+ }
+
+ // replace {{arguments}} with their values
+ function substArguments(text, args) {
+ if (!args)
+ return text;
+
+ return text.replace(/\{\{\s*(\w+)\s*\}\}/g, function(all, name) {
+ return name in args ? args[name] : '{{' + name + '}}';
+ });
+ }
+
+ // translate a string
+ function translateString(key, args, fallback) {
+ var data = getL10nData(key);
+ if (!data && fallback)
+ data = {textContent: fallback};
+ if (!data)
+ return '{{' + key + '}}';
+ return substArguments(data.textContent, args);
+ }
+
+ // translate an HTML element
+ function translateElement(element) {
+ if (!element || !element.dataset)
+ return;
+
+ // get the related l10n object
+ var data = getL10nData(element.dataset.l10nId);
+ if (!data)
+ return;
+
+ // get arguments (if any)
+ // TODO: more flexible parser?
+ var args;
+ if (element.dataset.l10nArgs) try {
+ args = JSON.parse(element.dataset.l10nArgs);
+ } catch (e) {
+ console.warn('[l10n] could not parse arguments for #' + key + '');
+ }
+
+ // translate element
+ // TODO: security check?
+ for (var k in data)
+ element[k] = substArguments(data[k], args);
+ }
+
+
+ // translate an HTML subtree
+ function translateFragment(element) {
+ element = element || document.querySelector('html');
+
+ // check all translatable children (= w/ a `data-l10n-id' attribute)
+ var children = element.querySelectorAll('*[data-l10n-id]');
+ var elementCount = children.length;
+ for (var i = 0; i < elementCount; i++)
+ translateElement(children[i]);
+
+ // translate element itself if necessary
+ if (element.dataset.l10nId)
+ translateElement(element);
+ }
+
+ window.addEventListener('DOMContentLoaded', function() {
+ gLanguage = FirefoxCom.request('getLocale', null);
+
+ translateFragment();
+
+ // fire a 'localized' DOM event
+ var evtObject = document.createEvent('Event');
+ evtObject.initEvent('localized', false, false);
+ evtObject.language = gLanguage;
+ window.dispatchEvent(evtObject);
+ });
+
+ // Public API
+ document.mozL10n = {
+ // get a localized string
+ get: translateString,
+
+ // get|set the document language and direction
+ get language() {
+ return {
+ // get|set the document language (ISO-639-1)
+ get code() { return gLanguage; },
+
+ // get the direction (ltr|rtl) of the current language
+ get direction() {
+ // http://www.w3.org/International/questions/qa-scripts
+ // Arabic, Hebrew, Farsi, Pashto, Urdu
+ var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
+ return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
+ }
+ };
+ }
+ };
+})(this);
+
diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties
index ed632b17a..c8dbe4aba 100644
--- a/l10n/en-US/viewer.properties
+++ b/l10n/en-US/viewer.properties
@@ -40,3 +40,5 @@ download_label=Download
zoom_out_label=Zoom Out
zoom_in_label=Zoom In
zoom.title=Zoom
+thumb_page_title=Page {{page}}
+thumb_page_canvas=Thumbnail of Page {{page}}
diff --git a/l10n/pl/metadata.inc b/l10n/pl/metadata.inc
new file mode 100644
index 000000000..0cc701d69
--- /dev/null
+++ b/l10n/pl/metadata.inc
@@ -0,0 +1,8 @@
+
+
+ pl
+ Przeglądarka PDF
+ Używa HTML5 do wyświetlania plików PDF bezpośrednio w Firefoksie.
+
+
+
diff --git a/l10n/pl/viewer.properties b/l10n/pl/viewer.properties
new file mode 100644
index 000000000..9fbd5e3c1
--- /dev/null
+++ b/l10n/pl/viewer.properties
@@ -0,0 +1,42 @@
+bookmark.title=Aktualny widok (kopiuj lub otwórz w nowym oknie)
+previous.title=Poprzednia strona
+next.title=Następna strona
+print.title=Drukuj
+download.title=Pobierz
+zoom_out.title=Pomniejsz
+zoom_in.title=Powiększ
+error_more_info=Więcej informacji
+error_less_info=Mniej informacji
+error_close=Zamknij
+error_build=Wersja PDF.JS: {{build}}
+error_message=Wiadomość: {{message}}
+error_stack=Stos: {{stack}}
+error_file=Plik: {{file}}
+error_line=Linia: {{line}}
+page_scale_width=Szerokość strony
+page_scale_fit=Cała strona
+page_scale_auto=Automatyczne dopasowanie
+page_scale_actual=Rzeczywisty rozmiar
+toggle_slider.title=Włącz/wyłącz suwak
+thumbs.title=Wyświetl miniatury
+outline.title=Wyświetl konspekt dokumentu
+loading=Wczytywanie... {{percent}}%
+loading_error_indicator=Błąd
+loading_error=Wystąpił błąd podczas wczytywania pliku PDF.
+rendering_error=Wystąpił błąd podczas wyświetlania strony.
+page_label=Strona:
+page_of=z {{pageCount}}
+no_outline=Konspekt nie jest dostępny
+open_file.title=Otwórz plik
+text_annotation_type=[Komentarz {{type}}]
+toggle_slider_label=Przełącz suwak
+thumbs_label=Miniatury
+outline_label=Konspekt dokumentu
+bookmark_label=Aktualny widok
+previous_label=Wstecz
+next_label=Dalej
+print_label=Drukuj
+download_label=Pobierz
+zoom_out_label=Pomniejsz
+zoom_in_label=Powiększ
+zoom.title=Powiększenie
diff --git a/l10n/ro/metadata.inc b/l10n/ro/metadata.inc
new file mode 100644
index 000000000..e2c72eca5
--- /dev/null
+++ b/l10n/ro/metadata.inc
@@ -0,0 +1,8 @@
+
+
+ ro
+ Cititor PDF
+ Afișează fișierele PDF direct în Firefox.
+
+
+
diff --git a/l10n/ro/viewer.properties b/l10n/ro/viewer.properties
new file mode 100644
index 000000000..5b79b2f51
--- /dev/null
+++ b/l10n/ro/viewer.properties
@@ -0,0 +1,42 @@
+bookmark.title=Vederea curentă (copiază sau deschide în fereastră nouă)
+previous.title=Pagina precedentă
+next.title=Pagina următoare
+print.title=Tipărește
+download.title=Descarcă
+zoom_out.title=Micșorează
+zoom_in.title=Mărește
+error_more_info=Detaliat
+error_less_info=Sumarizat
+error_close=Închide
+error_build=PDF.JS Build: {{build}}
+error_message=Message: {{message}}
+error_stack=Stack: {{stack}}
+error_file=File: {{file}}
+error_line=Line: {{line}}
+page_scale_width=După lățime
+page_scale_fit=Toată pagina
+page_scale_auto=Mărime automată
+page_scale_actual=Mărime originală
+toggle_slider.title=Vedere de ansamblu
+thumbs.title=Miniaturi
+outline.title=Cuprins
+loading=Încărcare... {{percent}}%
+loading_error_indicator=Eroare
+loading_error=S-a produs o eroare în timpul încărcării documentului.
+rendering_error=S-a produs o eroare în timpul procesării paginii.
+page_label=Pagina:
+page_of=din {{pageCount}}
+no_outline=Cuprins indisponibil
+open_file.title=Deschide fișier
+text_annotation_type=[Adnotare {{type}}]
+toggle_slider_label=Vedere de ansamblu
+thumbs_label=Miniaturi
+outline_label=Cuprins
+bookmark_label=Vederea curentă
+previous_label=Înapoi
+next_label=Înainte
+print_label=Tipărește
+download_label=Descarcă
+zoom_out_label=Micșorează
+zoom_in_label=Mărește
+zoom.title=Mărime
diff --git a/l10n/xx/viewer.properties b/l10n/xx/viewer.properties
index 5b9971d0b..e26f79422 100644
--- a/l10n/xx/viewer.properties
+++ b/l10n/xx/viewer.properties
@@ -40,3 +40,5 @@ download_label=<<<_ÐOẂпḻOãÐ_>>>
zoom_out_label=<<<_ƩOOм Oȗţ_>>>
zoom_in_label=<<<_ƩOOм iп_>>>
zoom.title=<<<_ƩOOм_>>>
+thumb_page_title=<<<_Þãģε {{page}}_>>>
+thumb_page_canvas=<<<_ţНȗмьпãiḻ O£ Þãģε {{page}}_>>>
diff --git a/make.js b/make.js
index 0a39e0b07..88f7c69a0 100755
--- a/make.js
+++ b/make.js
@@ -6,6 +6,7 @@ var ROOT_DIR = __dirname + '/', // absolute path to project's root
BUILD_TARGET = BUILD_DIR + 'pdf.js',
FIREFOX_BUILD_DIR = BUILD_DIR + '/firefox/',
EXTENSION_SRC_DIR = 'extensions/',
+ LOCALE_SRC_DIR = 'l10n/',
GH_PAGES_DIR = BUILD_DIR + 'gh-pages/',
REPO = 'git@github.com:mozilla/pdf.js.git',
PYTHON_BIN = 'python2.7';
@@ -67,8 +68,9 @@ target.web = function() {
// Creates localized resources for the viewer and extension.
//
target.locale = function() {
- var L10N_PATH = 'l10n';
var METADATA_OUTPUT = 'extensions/firefox/metadata.inc';
+ var CHROME_MANIFEST_OUTPUT = 'extensions/firefox/chrome.manifest.inc';
+ var EXTENSION_LOCALE_OUTPUT = 'extensions/firefox/locale';
var VIEWER_OUTPUT = 'web/locale.properties';
var DEFAULT_LOCALE = 'en-US';
@@ -76,13 +78,17 @@ target.locale = function() {
echo();
echo('### Building localization files');
- var subfolders = ls(L10N_PATH);
+ rm('-rf', EXTENSION_LOCALE_OUTPUT);
+ mkdir('-p', EXTENSION_LOCALE_OUTPUT);
+
+ var subfolders = ls(LOCALE_SRC_DIR);
subfolders.sort();
var metadataContent = '';
+ var chromeManifestContent = '';
var viewerOutput = '';
for (var i = 0; i < subfolders.length; i++) {
var locale = subfolders[i];
- var path = L10N_PATH + '/' + locale;
+ var path = LOCALE_SRC_DIR + locale;
if (!test('-d', path))
continue;
@@ -91,12 +97,13 @@ target.locale = function() {
continue;
}
+ mkdir('-p', EXTENSION_LOCALE_OUTPUT + '/' + locale);
+ chromeManifestContent += 'locale pdf.js ' + locale + ' locale/' + locale + '/\n';
+
if (test('-f', path + '/viewer.properties')) {
var properties = cat(path + '/viewer.properties');
- if (locale == DEFAULT_LOCALE)
- viewerOutput = '[*]\n' + properties + '\n' + viewerOutput;
- else
- viewerOutput = viewerOutput + '[' + locale + ']\n' + properties + '\n';
+ viewerOutput += '[' + locale + ']\n' + properties + '\n';
+ cp(path + '/viewer.properties', EXTENSION_LOCALE_OUTPUT + '/' + locale);
}
if (test('-f', path + '/metadata.inc')) {
@@ -106,6 +113,7 @@ target.locale = function() {
}
viewerOutput.to(VIEWER_OUTPUT);
metadataContent.to(METADATA_OUTPUT);
+ chromeManifestContent.to(CHROME_MANIFEST_OUTPUT);
};
//
@@ -227,7 +235,7 @@ var EXTENSION_WEB_FILES =
'web/viewer.css',
'web/viewer.js',
'web/viewer.html',
- 'external/webL10n/l10n.js',
+ 'extensions/firefox/tools/l10n.js',
'web/viewer-production.html'],
EXTENSION_BASE_VERSION = 'f0f0418a9c6637981fe1182b9212c2d592774c7d',
EXTENSION_VERSION_PREFIX = '0.3.',
@@ -242,6 +250,7 @@ target.extension = function() {
echo();
echo('### Building extensions');
+ target.locale();
target.production();
target.firefox();
target.chrome();
@@ -277,21 +286,24 @@ target.firefox = function() {
'*.rdf',
'*.svg',
'*.png',
+ '*.manifest',
'components',
+ 'locale',
'../../LICENSE'],
FIREFOX_EXTENSION_FILES =
['bootstrap.js',
'install.rdf',
+ 'chrome.manifest',
'icon.png',
'icon64.png',
'components',
'content',
+ 'locale',
'LICENSE'],
FIREFOX_EXTENSION_NAME = 'pdf.js.xpi',
FIREFOX_AMO_EXTENSION_NAME = 'pdf.js.amo.xpi';
- var LOCALE_CONTENT = cat('web/locale.properties');
-
+ target.locale();
target.production();
target.buildnumber();
cd(ROOT_DIR);
@@ -310,7 +322,6 @@ target.firefox = function() {
// Copy a standalone version of pdf.js inside the content directory
cp(BUILD_TARGET, FIREFOX_BUILD_CONTENT_DIR + BUILD_DIR);
cp('-R', EXTENSION_WEB_FILES, FIREFOX_BUILD_CONTENT_DIR + '/web');
- cp('web/locale.properties', FIREFOX_BUILD_CONTENT_DIR + '/web');
rm(FIREFOX_BUILD_CONTENT_DIR + '/web/viewer-production.html');
// Copy over the firefox extension snippet so we can inline pdf.js in it
@@ -319,7 +330,6 @@ target.firefox = function() {
// Modify the viewer so it does all the extension-only stuff.
cd(FIREFOX_BUILD_CONTENT_DIR + '/web');
sed('-i', /.*PDFJSSCRIPT_INCLUDE_BUNDLE.*\n/, cat(ROOT_DIR + BUILD_TARGET), 'viewer-snippet-firefox-extension.html');
- sed('-i', /.*PDFJSSCRIPT_LOCALE_DATA.*\n/, LOCALE_CONTENT, 'viewer-snippet-firefox-extension.html');
sed('-i', /.*PDFJSSCRIPT_REMOVE_CORE.*\n/g, '', 'viewer.html');
sed('-i', /.*PDFJSSCRIPT_REMOVE_FIREFOX_EXTENSION.*\n/g, '', 'viewer.html');
sed('-i', /.*PDFJSSCRIPT_INCLUDE_FIREFOX_EXTENSION.*\n/, cat('viewer-snippet-firefox-extension.html'), 'viewer.html');
@@ -328,7 +338,6 @@ target.firefox = function() {
// We don't need pdf.js anymore since its inlined
rm('-Rf', FIREFOX_BUILD_CONTENT_DIR + BUILD_DIR);
rm(FIREFOX_BUILD_CONTENT_DIR + '/web/viewer-snippet-firefox-extension.html');
- rm(FIREFOX_BUILD_CONTENT_DIR + '/web/locale.properties');
// Remove '.DS_Store' and other hidden files
find(FIREFOX_BUILD_DIR).forEach(function(file) {
if (file.match(/^\./))
@@ -342,6 +351,8 @@ target.firefox = function() {
// Update localized metadata
var localizedMetadata = cat(EXTENSION_SRC_DIR + '/firefox/metadata.inc');
sed('-i', /.*PDFJS_LOCALIZED_METADATA.*\n/, localizedMetadata, FIREFOX_BUILD_DIR + '/install.rdf');
+ var chromeManifest = cat(EXTENSION_SRC_DIR + '/firefox/chrome.manifest.inc');
+ sed('-i', /.*PDFJS_SUPPORTED_LOCALES.*\n/, chromeManifest, FIREFOX_BUILD_DIR + '/chrome.manifest');
// Create the xpi
cd(FIREFOX_BUILD_DIR);
@@ -365,25 +376,27 @@ target.mozcentral = function() {
echo();
echo('### Building mozilla-central extension');
- var MOZCENTRAL_DIR = BUILD_DIR + '/mozcentral',
- MOZCENTRAL_CONTENT_DIR = MOZCENTRAL_DIR + '/content/',
- MOZCENTRAL_L10N_DIR = MOZCENTRAL_DIR + '/l10n/',
+ var MOZCENTRAL_DIR = BUILD_DIR + 'mozcentral/',
+ MOZCENTRAL_EXTENSION_DIR = MOZCENTRAL_DIR + 'browser/app/profile/extensions/uriloader@pdf.js/',
+ MOZCENTRAL_CONTENT_DIR = MOZCENTRAL_EXTENSION_DIR + 'content/',
+ MOZCENTRAL_L10N_DIR = MOZCENTRAL_DIR + 'browser/locales/en-US/pdfviewer/',
FIREFOX_CONTENT_DIR = EXTENSION_SRC_DIR + '/firefox/content/',
FIREFOX_EXTENSION_FILES_TO_COPY =
['*.js',
'*.svg',
'*.png',
+ '*.manifest',
'install.rdf.in',
'README.mozilla',
'components',
'../../LICENSE'],
DEFAULT_LOCALE_FILES =
- ['l10n/en-US/viewer.properties',
- 'l10n/en-US/metadata.inc'],
+ [LOCALE_SRC_DIR + 'en-US/viewer.properties'],
FIREFOX_MC_EXTENSION_FILES =
['bootstrap.js',
'icon.png',
'icon64.png',
+ 'chrome.manifest',
'components',
'content',
'LICENSE'];
@@ -401,7 +414,7 @@ target.mozcentral = function() {
// Copy extension files
cd('extensions/firefox');
- cp('-R', FIREFOX_EXTENSION_FILES_TO_COPY, ROOT_DIR + MOZCENTRAL_DIR);
+ cp('-R', FIREFOX_EXTENSION_FILES_TO_COPY, ROOT_DIR + MOZCENTRAL_EXTENSION_DIR);
cd(ROOT_DIR);
// Copy a standalone version of pdf.js inside the content directory
@@ -433,11 +446,11 @@ target.mozcentral = function() {
cp(DEFAULT_LOCALE_FILES, MOZCENTRAL_L10N_DIR);
// Update the build version number
- sed('-i', /PDFJSSCRIPT_VERSION/, EXTENSION_VERSION, MOZCENTRAL_DIR + '/install.rdf.in');
- sed('-i', /PDFJSSCRIPT_VERSION/, EXTENSION_VERSION, MOZCENTRAL_DIR + '/README.mozilla');
+ sed('-i', /PDFJSSCRIPT_VERSION/, EXTENSION_VERSION, MOZCENTRAL_EXTENSION_DIR + 'install.rdf.in');
+ sed('-i', /PDFJSSCRIPT_VERSION/, EXTENSION_VERSION, MOZCENTRAL_EXTENSION_DIR + 'README.mozilla');
// List all files for mozilla-central
- cd(MOZCENTRAL_DIR);
+ cd(MOZCENTRAL_EXTENSION_DIR);
var extensionFiles = '';
find(FIREFOX_MC_EXTENSION_FILES).forEach(function(file){
if (test('-f', file))
diff --git a/src/api.js b/src/api.js
index e1991a068..976faf898 100644
--- a/src/api.js
+++ b/src/api.js
@@ -8,10 +8,12 @@
* e.g. No cross domain requests without CORS.
*
* @param {string|TypedAray} source Either a url to a PDF is located or a
- * typed array already populated with data.
+ * typed array (Uint8Array) already populated with data.
+ * @param {Object} headers An object containing the http headers like this:
+ * { Authorization: "BASIC XXX" }.
* @return {Promise} A promise that is resolved with {PDFDocumentProxy} object.
*/
-PDFJS.getDocument = function getDocument(source) {
+PDFJS.getDocument = function getDocument(source, headers) {
var promise = new PDFJS.Promise();
var transport = new WorkerTransport(promise);
if (typeof source === 'string') {
@@ -29,7 +31,8 @@ PDFJS.getDocument = function getDocument(source) {
error: function getPDFError(e) {
promise.reject('Unexpected server response of ' +
e.target.status + '.');
- }
+ },
+ headers: headers
},
function getPDFLoad(data) {
transport.sendData(data);
diff --git a/src/core.js b/src/core.js
index 190f3a1db..e1a9476a3 100644
--- a/src/core.js
+++ b/src/core.js
@@ -31,7 +31,19 @@ function getPdf(arg, callback) {
params = { url: arg };
var xhr = new XMLHttpRequest();
+
xhr.open('GET', params.url);
+
+ var headers = params.headers;
+ if (headers) {
+ for (var property in headers) {
+ if (typeof headers[property] === 'undefined')
+ continue;
+
+ xhr.setRequestHeader(property, params.headers[property]);
+ }
+ }
+
xhr.mozResponseType = xhr.responseType = 'arraybuffer';
var protocol = params.url.indexOf(':') < 0 ? window.location.protocol :
params.url.substring(0, params.url.indexOf(':') + 1);
diff --git a/src/crypto.js b/src/crypto.js
index 038c0e332..dcd820554 100644
--- a/src/crypto.js
+++ b/src/crypto.js
@@ -419,13 +419,14 @@ var CipherTransform = (function CipherTransformClosure() {
})();
var CipherTransformFactory = (function CipherTransformFactoryClosure() {
+ var defaultPasswordBytes = new Uint8Array([
+ 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
+ 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
+ 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
+ 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]);
+
function prepareKeyData(fileId, password, ownerPassword, userPassword,
flags, revision, keyLength, encryptMetadata) {
- var defaultPasswordBytes = new Uint8Array([
- 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
- 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
- 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
- 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]);
var hashData = new Uint8Array(100), i = 0, j, n;
if (password) {
n = Math.min(32, password.length);
@@ -462,9 +463,8 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() {
var cipher, checkData;
if (revision >= 3) {
- // padded password in hashData, we can use this array for user
- // password check
- i = 32;
+ for (i = 0; i < 32; ++i)
+ hashData[i] = defaultPasswordBytes[i];
for (j = 0, n = fileId.length; j < n; ++j)
hashData[i++] = fileId[j];
cipher = new ARCFourCipher(encryptionKey);
@@ -477,16 +477,53 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() {
cipher = new ARCFourCipher(derivedKey);
checkData = cipher.encryptBlock(checkData);
}
+ for (j = 0, n = checkData.length; j < n; ++j) {
+ if (userPassword[j] != checkData[j])
+ return null;
+ }
} else {
cipher = new ARCFourCipher(encryptionKey);
- checkData = cipher.encryptBlock(hashData.subarray(0, 32));
- }
- for (j = 0, n = checkData.length; j < n; ++j) {
- if (userPassword[j] != checkData[j])
- error('incorrect password');
+ checkData = cipher.encryptBlock(defaultPasswordBytes);
+ for (j = 0, n = checkData.length; j < n; ++j) {
+ if (userPassword[j] != checkData[j])
+ return null;
+ }
}
return encryptionKey;
}
+ function decodeUserPassword(password, ownerPassword, revision, keyLength) {
+ var hashData = new Uint8Array(32), i = 0, j, n;
+ n = Math.min(32, password.length);
+ for (; i < n; ++i)
+ hashData[i] = password[i];
+ j = 0;
+ while (i < 32) {
+ hashData[i++] = defaultPasswordBytes[j++];
+ }
+ var hash = calculateMD5(hashData, 0, i);
+ var keyLengthInBytes = keyLength >> 3;
+ if (revision >= 3) {
+ for (j = 0; j < 50; ++j) {
+ hash = calculateMD5(hash, 0, hash.length);
+ }
+ }
+
+ var cipher, userPassword;
+ if (revision >= 3) {
+ userPassword = ownerPassword;
+ var derivedKey = new Uint8Array(keyLengthInBytes), k;
+ for (j = 19; j >= 0; j--) {
+ for (k = 0; k < keyLengthInBytes; ++k)
+ derivedKey[k] = hash[k] ^ j;
+ cipher = new ARCFourCipher(derivedKey);
+ userPassword = cipher.encryptBlock(userPassword);
+ }
+ } else {
+ cipher = new ARCFourCipher(hash.subarray(0, keyLengthInBytes));
+ userPassword = cipher.encryptBlock(ownerPassword);
+ }
+ return userPassword;
+ }
var identityName = new Name('Identity');
@@ -516,10 +553,23 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() {
if (password)
passwordBytes = stringToBytes(password);
- this.encryptionKey = prepareKeyData(fileIdBytes, passwordBytes,
- ownerPassword, userPassword,
- flags, revision,
- keyLength, encryptMetadata);
+ var encryptionKey = prepareKeyData(fileIdBytes, passwordBytes,
+ ownerPassword, userPassword, flags,
+ revision, keyLength, encryptMetadata);
+ if (!encryptionKey && password) {
+ // Attempting use the password as an owner password
+ var decodedPassword = decodeUserPassword(passwordBytes, ownerPassword,
+ revision, keyLength);
+ encryptionKey = prepareKeyData(fileIdBytes, decodedPassword,
+ ownerPassword, userPassword, flags,
+ revision, keyLength, encryptMetadata);
+ }
+
+ if (!encryptionKey)
+ error('incorrect password or encryption data');
+
+ this.encryptionKey = encryptionKey;
+
if (algorithm == 4) {
this.cf = dict.get('CF');
this.stmf = dict.get('StmF') || identityName;
diff --git a/src/evaluator.js b/src/evaluator.js
index 64a1e97fb..f99e79a37 100644
--- a/src/evaluator.js
+++ b/src/evaluator.js
@@ -112,14 +112,33 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
};
function splitCombinedOperations(operations) {
- // Two operations can be combined together, trying to find which two
+ // Two or more operations can be combined together, trying to find which
// operations were concatenated.
- for (var i = operations.length - 1; i > 0; i--) {
- var op1 = operations.substring(0, i), op2 = operations.substring(i);
- if (op1 in OP_MAP && op2 in OP_MAP)
- return [op1, op2]; // operations found
+ var result = [];
+ var opIndex = 0;
+
+ if (!operations) {
+ return null;
}
- return null;
+
+ while (opIndex < operations.length) {
+ var currentOp = '';
+ for (var op in OP_MAP) {
+ if (op == operations.substr(opIndex, op.length) &&
+ op.length > currentOp.length) {
+ currentOp = op;
+ }
+ }
+
+ if (currentOp.length > 0) {
+ result.push(operations.substr(opIndex, currentOp.length));
+ opIndex += currentOp.length;
+ } else {
+ return null;
+ }
+ }
+
+ return result;
}
PartialEvaluator.prototype = {
@@ -268,14 +287,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
var patterns = resources.get('Pattern') || new Dict();
var parser = new Parser(new Lexer(stream), false, xref);
var res = resources;
- var hasNextObj = false, nextObj;
+ var hasNextObj = false, nextObjs;
var args = [], obj;
var TILING_PATTERN = 1, SHADING_PATTERN = 2;
while (true) {
if (hasNextObj) {
- obj = nextObj;
- hasNextObj = false;
+ obj = nextObjs.pop();
+ hasNextObj = (nextObjs.length > 0);
} else {
obj = parser.getObj();
if (isEOF(obj))
@@ -291,9 +310,12 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
if (cmds) {
cmd = cmds[0];
fn = OP_MAP[cmd];
- // feeding other command on the next interation
+ // feeding other command on the next iteration
hasNextObj = true;
- nextObj = Cmd.get(cmds[1]);
+ nextObjs = [];
+ for (var idx = 1; idx < cmds.length; idx++) {
+ nextObjs.push(Cmd.get(cmds[idx]));
+ }
}
}
assertWellFormed(fn, 'Unknown command "' + cmd + '"');
diff --git a/test/unit/crypto_spec.js b/test/unit/crypto_spec.js
index 0b82b5ccb..28dc4c872 100644
--- a/test/unit/crypto_spec.js
+++ b/test/unit/crypto_spec.js
@@ -185,3 +185,66 @@ describe('crypto', function() {
});
});
+describe('CipherTransformFactory', function() {
+ function DictMock(map) {
+ this.map = map;
+ }
+ DictMock.prototype = {
+ get: function(key) {
+ return this.map[key];
+ }
+ };
+
+ var map1 = {
+ Filter: new Name('Standard'),
+ V: 2,
+ Length: 128,
+ O: unescape('%80%C3%04%96%91o%20sl%3A%E6%1B%13T%91%F2%0DV%12%E3%FF%5E%BB%' +
+ 'E9VO%D8k%9A%CA%7C%5D'),
+ U: unescape('j%0C%8D%3EY%19%00%BCjd%7D%91%BD%AA%00%18%00%00%00%00%00%00%0' +
+ '0%00%00%00%00%00%00%00%00%00'),
+ P: -1028,
+ R: 3
+ };
+ var fileID1 = unescape('%F6%C6%AF%17%F3rR%8DRM%9A%80%D1%EF%DF%18');
+
+ var map2 = {
+ Filter: new Name('Standard'),
+ V: 4,
+ Length: 128,
+ O: unescape('sF%14v.y5%27%DB%97%0A5%22%B3%E1%D4%AD%BD%9B%3C%B4%A5%89u%15%' +
+ 'B2Y%F1h%D9%E9%F4'),
+ U: unescape('%93%04%89%A9%BF%8AE%A6%88%A2%DB%C2%A0%A8gn%00%00%00%00%00%00' +
+ '%00%00%00%00%00%00%00%00%00%00'),
+ P: -1084,
+ R: 4
+ };
+ var fileID2 = unescape('%3CL_%3AD%96%AF@%9A%9D%B3%3Cx%1Cv%AC');
+
+ describe('#ctor', function() {
+ it('should accept user password', function() {
+ var factory = new CipherTransformFactory(new DictMock(map1), fileID1,
+ '123456');
+ });
+
+ it('should accept owner password', function() {
+ var factory = new CipherTransformFactory(new DictMock(map1), fileID1,
+ '654321');
+ });
+
+ it('should not accept wrong password', function() {
+ var thrown = false;
+ try {
+ var factory = new CipherTransformFactory(new DictMock(map1), fileID1,
+ 'wrong');
+ } catch (e) {
+ thrown = true;
+ }
+ expect(thrown).toEqual(true);
+ });
+
+ it('should accept no password', function() {
+ var factory = new CipherTransformFactory(new DictMock(map2), fileID2);
+ });
+ });
+});
diff --git a/test/unit/evaluator_spec.js b/test/unit/evaluator_spec.js
new file mode 100644
index 000000000..4ee0768a7
--- /dev/null
+++ b/test/unit/evaluator_spec.js
@@ -0,0 +1,83 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+describe('evaluator', function() {
+ function XrefMock(queue) {
+ this.queue = queue;
+ }
+ XrefMock.prototype = {
+ fetchIfRef: function() {
+ return this.queue.shift();
+ }
+ };
+ function HandlerMock() {
+ this.inputs = [];
+ }
+ HandlerMock.prototype = {
+ send: function(name, data) {
+ this.inputs({name: name, data: data});
+ }
+ };
+ function ResourcesMock() { }
+ ResourcesMock.prototype = {
+ get: function(name) {
+ return this[name];
+ }
+ };
+
+ describe('splitCombinedOperations', function() {
+ it('should reject unknown operations', function() {
+ var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(),
+ 'prefix');
+ var stream = new StringStream('qTT');
+ var thrown = false;
+ try {
+ evaluator.getOperatorList(stream, new ResourcesMock(), []);
+ } catch (e) {
+ thrown = e;
+ }
+ expect(thrown).toNotEqual(false);
+ });
+
+ it('should handle one operations', function() {
+ var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(),
+ 'prefix');
+ var stream = new StringStream('Q');
+ var result = evaluator.getOperatorList(stream, new ResourcesMock(), []);
+
+ expect(!!result.fnArray && !!result.argsArray).toEqual(true);
+ expect(result.fnArray.length).toEqual(1);
+ expect(result.fnArray[0]).toEqual('restore');
+ });
+
+ it('should handle two glued operations', function() {
+ var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(),
+ 'prefix');
+ var resources = new ResourcesMock();
+ resources.Res1 = {};
+ var stream = new StringStream('/Res1 DoQ');
+ var result = evaluator.getOperatorList(stream, resources, []);
+
+ expect(!!result.fnArray && !!result.argsArray).toEqual(true);
+ expect(result.fnArray.length).toEqual(2);
+ expect(result.fnArray[0]).toEqual('paintXObject');
+ expect(result.fnArray[1]).toEqual('restore');
+ });
+
+ it('should handle tree glued operations', function() {
+ var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(),
+ 'prefix');
+ var stream = new StringStream('qqq');
+ var result = evaluator.getOperatorList(stream, new ResourcesMock(), []);
+
+ expect(!!result.fnArray && !!result.argsArray).toEqual(true);
+ expect(result.fnArray.length).toEqual(3);
+ expect(result.fnArray[0]).toEqual('save');
+ expect(result.fnArray[1]).toEqual('save');
+ expect(result.fnArray[2]).toEqual('save');
+ });
+ });
+});
+
diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html
index cdd0c297f..ca0a1aed1 100644
--- a/test/unit/unit_test.html
+++ b/test/unit/unit_test.html
@@ -39,6 +39,7 @@
+
diff --git a/web/viewer.js b/web/viewer.js
index b1d4c2cb6..1fbd91f1d 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -1179,7 +1179,8 @@ var PageView = function pageView(container, pdfPage, id, scale,
var ThumbnailView = function thumbnailView(container, pdfPage, id) {
var anchor = document.createElement('a');
anchor.href = PDFView.getAnchorUrl('#page=' + id);
- anchor.onclick = function stopNivigation() {
+ anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}');
+ anchor.onclick = function stopNavigation() {
PDFView.page = id;
return false;
};
@@ -1212,6 +1213,8 @@ var ThumbnailView = function thumbnailView(container, pdfPage, id) {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
canvas.className = 'thumbnailImage';
+ canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas',
+ {page: id}, 'Thumbnail of Page {{page}}'));
div.setAttribute('data-loaded', true);
@@ -1468,11 +1471,12 @@ window.addEventListener('load', function webViewerLoad(evt) {
if ('disableWorker' in hashParams)
PDFJS.disableWorker = (hashParams['disableWorker'] === 'true');
- var locale = !PDFJS.isFirefoxExtension ? navigator.language :
- FirefoxCom.request('getLocale', null);
- if ('locale' in hashParams)
- locale = hashParams['locale'];
- mozL10n.language.code = locale;
+ if (!PDFJS.isFirefoxExtension) {
+ var locale = navigator.language;
+ if ('locale' in hashParams)
+ locale = hashParams['locale'];
+ mozL10n.language.code = locale;
+ }
if ('disableTextLayer' in hashParams)
PDFJS.disableTextLayer = (hashParams['disableTextLayer'] === 'true');