diff --git a/LICENSE b/LICENSE index f8a848205..db52dec8e 100644 --- a/LICENSE +++ b/LICENSE @@ -9,6 +9,7 @@ Yury Delendik Kalervo Kujala Adil Allawi <@ironymark> + Jakob Miland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/Makefile b/Makefile index 0ad2eb09c..e99daa832 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,10 @@ PDF_JS_FILES = \ server: @cd test; python test.py --port=8888; +# make test +# +# This target runs all the tests excluding the unit-test. This can be used for +# testing all browsers. test: shell-test browser-test # @@ -69,6 +73,13 @@ bundle: | $(BUILD_DIR) rm -f *.tmp; \ cd .. +# make unit-test +# +# This target runs in-browser unit tests with js-test-driver and jasmine unit +# test framework. +unit-test: + @cd test/unit/ ; make ; + # make browser-test # # This target runs in-browser tests using two primary arguments: a @@ -129,7 +140,7 @@ browser-test: # # SRC_DIRS := . src utils web test examples/helloworld extensions/firefox \ - extensions/firefox/components extensions/chrome + extensions/firefox/components extensions/chrome test/unit GJSLINT_FILES = $(foreach DIR,$(SRC_DIRS),$(wildcard $(DIR)/*.js)) lint: gjslint --nojsdoc $(GJSLINT_FILES) diff --git a/README.md b/README.md index da70a4f57..7e5d2eeb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pdf.js - + ## Overview diff --git a/external/jasmine/jasmine-html.js b/external/jasmine/jasmine-html.js deleted file mode 100644 index 3de4e8a5f..000000000 --- a/external/jasmine/jasmine-html.js +++ /dev/null @@ -1,676 +0,0 @@ -jasmine.HtmlReporterHelpers = {}; - -jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { - var el = document.createElement(type); - - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; - - if (typeof child === 'string') { - el.appendChild(document.createTextNode(child)); - } else { - if (child) { - el.appendChild(child); - } - } - } - - for (var attr in attrs) { - if (attr == "className") { - el[attr] = attrs[attr]; - } else { - el.setAttribute(attr, attrs[attr]); - } - } - - return el; -}; - -jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { - var results = child.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.skipped) { - status = 'skipped'; - } - - return status; -}; - -jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { - var parentDiv = this.dom.summary; - var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; - var parent = child[parentSuite]; - - if (parent) { - if (typeof this.views.suites[parent.id] == 'undefined') { - this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); - } - parentDiv = this.views.suites[parent.id].element; - } - - parentDiv.appendChild(childElement); -}; - - -jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { - for(var fn in jasmine.HtmlReporterHelpers) { - ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; - } -}; - -jasmine.HtmlReporter = function(_doc) { - var self = this; - var doc = _doc || window.document; - - var reporterView; - - var dom = {}; - - // Jasmine Reporter Public Interface - self.logRunningSpecs = false; - - self.reportRunnerStarting = function(runner) { - var specs = runner.specs() || []; - - if (specs.length == 0) { - return; - } - - createReporterDom(runner.env.versionString()); - doc.body.appendChild(dom.reporter); - - reporterView = new jasmine.HtmlReporter.ReporterView(dom); - reporterView.addSpecs(specs, self.specFilter); - }; - - self.reportRunnerResults = function(runner) { - reporterView.complete(); - }; - - self.reportSuiteResults = function(suite) { - reporterView.suiteComplete(suite); - }; - - self.reportSpecStarting = function(spec) { - if (self.logRunningSpecs) { - self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); - } - }; - - self.reportSpecResults = function(spec) { - reporterView.specComplete(spec); - }; - - self.log = function() { - var console = jasmine.getGlobal().console; - if (console && console.log) { - if (console.log.apply) { - console.log.apply(console, arguments); - } else { - console.log(arguments); // ie fix: console.log.apply doesn't exist on ie - } - } - }; - - self.specFilter = function(spec) { - if (!focusedSpecName()) { - return true; - } - - return spec.getFullName().indexOf(focusedSpecName()) === 0; - }; - - return self; - - function focusedSpecName() { - var specName; - - (function memoizeFocusedSpec() { - if (specName) { - return; - } - - var paramMap = []; - var params = doc.location.search.substring(1).split('&'); - - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); - } - - specName = paramMap.spec; - })(); - - return specName; - } - - function createReporterDom(version) { - dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, - dom.banner = self.createDom('div', { className: 'banner' }, - self.createDom('span', { className: 'title' }, "Jasmine "), - self.createDom('span', { className: 'version' }, version)), - - dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), - dom.alert = self.createDom('div', {className: 'alert'}), - dom.results = self.createDom('div', {className: 'results'}, - dom.summary = self.createDom('div', { className: 'summary' }), - dom.details = self.createDom('div', { id: 'details' })) - ); - } -}; -jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporterHelpers = {}; - -jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { - var el = document.createElement(type); - - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; - - if (typeof child === 'string') { - el.appendChild(document.createTextNode(child)); - } else { - if (child) { - el.appendChild(child); - } - } - } - - for (var attr in attrs) { - if (attr == "className") { - el[attr] = attrs[attr]; - } else { - el.setAttribute(attr, attrs[attr]); - } - } - - return el; -}; - -jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { - var results = child.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.skipped) { - status = 'skipped'; - } - - return status; -}; - -jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { - var parentDiv = this.dom.summary; - var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; - var parent = child[parentSuite]; - - if (parent) { - if (typeof this.views.suites[parent.id] == 'undefined') { - this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); - } - parentDiv = this.views.suites[parent.id].element; - } - - parentDiv.appendChild(childElement); -}; - - -jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { - for(var fn in jasmine.HtmlReporterHelpers) { - ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; - } -}; - -jasmine.HtmlReporter.ReporterView = function(dom) { - this.startedAt = new Date(); - this.runningSpecCount = 0; - this.completeSpecCount = 0; - this.passedCount = 0; - this.failedCount = 0; - this.skippedCount = 0; - - this.createResultsMenu = function() { - this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, - this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), - ' | ', - this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); - - this.summaryMenuItem.onclick = function() { - dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); - }; - - this.detailsMenuItem.onclick = function() { - showDetails(); - }; - }; - - this.addSpecs = function(specs, specFilter) { - this.totalSpecCount = specs.length; - - this.views = { - specs: {}, - suites: {} - }; - - for (var i = 0; i < specs.length; i++) { - var spec = specs[i]; - this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); - if (specFilter(spec)) { - this.runningSpecCount++; - } - } - }; - - this.specComplete = function(spec) { - this.completeSpecCount++; - - if (isUndefined(this.views.specs[spec.id])) { - this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); - } - - var specView = this.views.specs[spec.id]; - - switch (specView.status()) { - case 'passed': - this.passedCount++; - break; - - case 'failed': - this.failedCount++; - break; - - case 'skipped': - this.skippedCount++; - break; - } - - specView.refresh(); - this.refresh(); - }; - - this.suiteComplete = function(suite) { - var suiteView = this.views.suites[suite.id]; - if (isUndefined(suiteView)) { - return; - } - suiteView.refresh(); - }; - - this.refresh = function() { - - if (isUndefined(this.resultsMenu)) { - this.createResultsMenu(); - } - - // currently running UI - if (isUndefined(this.runningAlert)) { - this.runningAlert = this.createDom('a', {href: "?", className: "runningAlert bar"}); - dom.alert.appendChild(this.runningAlert); - } - this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); - - // skipped specs UI - if (isUndefined(this.skippedAlert)) { - this.skippedAlert = this.createDom('a', {href: "?", className: "skippedAlert bar"}); - } - - this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; - - if (this.skippedCount === 1 && isDefined(dom.alert)) { - dom.alert.appendChild(this.skippedAlert); - } - - // passing specs UI - if (isUndefined(this.passedAlert)) { - this.passedAlert = this.createDom('span', {href: "?", className: "passingAlert bar"}); - } - this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); - - // failing specs UI - if (isUndefined(this.failedAlert)) { - this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); - } - this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); - - if (this.failedCount === 1 && isDefined(dom.alert)) { - dom.alert.appendChild(this.failedAlert); - dom.alert.appendChild(this.resultsMenu); - } - - // summary info - this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); - this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; - }; - - this.complete = function() { - dom.alert.removeChild(this.runningAlert); - - this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; - - if (this.failedCount === 0) { - dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); - } else { - showDetails(); - } - - dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); - }; - - return this; - - function showDetails() { - if (dom.reporter.className.search(/showDetails/) === -1) { - dom.reporter.className += " showDetails"; - } - } - - function isUndefined(obj) { - return typeof obj === 'undefined'; - } - - function isDefined(obj) { - return !isUndefined(obj); - } - - function specPluralizedFor(count) { - var str = count + " spec"; - if (count > 1) { - str += "s" - } - return str; - } - -}; - -jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); - - -jasmine.HtmlReporter.SpecView = function(spec, dom, views) { - this.spec = spec; - this.dom = dom; - this.views = views; - - this.symbol = this.createDom('li', { className: 'pending' }); - this.dom.symbolSummary.appendChild(this.symbol); - - this.summary = this.createDom('div', { className: 'specSummary' }, - this.createDom('a', { - className: 'description', - href: '?spec=' + encodeURIComponent(this.spec.getFullName()), - title: this.spec.getFullName() - }, this.spec.description) - ); - - this.detail = this.createDom('div', { className: 'specDetail' }, - this.createDom('a', { - className: 'description', - href: '?spec=' + encodeURIComponent(this.spec.getFullName()), - title: this.spec.getFullName() - }, this.spec.getFullName()) - ); -}; - -jasmine.HtmlReporter.SpecView.prototype.status = function() { - return this.getSpecStatus(this.spec); -}; - -jasmine.HtmlReporter.SpecView.prototype.refresh = function() { - this.symbol.className = this.status(); - - switch (this.status()) { - case 'skipped': - break; - - case 'passed': - this.appendSummaryToSuiteDiv(); - break; - - case 'failed': - this.appendSummaryToSuiteDiv(); - this.appendFailureDetail(); - break; - } -}; - -jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { - this.summary.className += ' ' + this.status(); - this.appendToSummary(this.spec, this.summary); -}; - -jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { - this.detail.className += ' ' + this.status(); - - var resultItems = this.spec.results().getItems(); - var messagesDiv = this.createDom('div', { className: 'messages' }); - - for (var i = 0; i < resultItems.length; i++) { - var result = resultItems[i]; - - if (result.type == 'log') { - messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); - } else if (result.type == 'expect' && result.passed && !result.passed()) { - messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); - - if (result.trace.stack) { - messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); - } - } - } - - if (messagesDiv.childNodes.length > 0) { - this.detail.appendChild(messagesDiv); - this.dom.details.appendChild(this.detail); - } -}; - -jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { - this.suite = suite; - this.dom = dom; - this.views = views; - - this.element = this.createDom('div', { className: 'suite' }, - this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(this.suite.getFullName()) }, this.suite.description) - ); - - this.appendToSummary(this.suite, this.element); -}; - -jasmine.HtmlReporter.SuiteView.prototype.status = function() { - return this.getSpecStatus(this.suite); -}; - -jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { - this.element.className += " " + this.status(); -}; - -jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); - -/* @deprecated Use jasmine.HtmlReporter instead - */ -jasmine.TrivialReporter = function(doc) { - this.document = doc || document; - this.suiteDivs = {}; - this.logRunningSpecs = false; -}; - -jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { - var el = document.createElement(type); - - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; - - if (typeof child === 'string') { - el.appendChild(document.createTextNode(child)); - } else { - if (child) { el.appendChild(child); } - } - } - - for (var attr in attrs) { - if (attr == "className") { - el[attr] = attrs[attr]; - } else { - el.setAttribute(attr, attrs[attr]); - } - } - - return el; -}; - -jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { - var showPassed, showSkipped; - - this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, - this.createDom('div', { className: 'banner' }, - this.createDom('div', { className: 'logo' }, - this.createDom('span', { className: 'title' }, "Jasmine"), - this.createDom('span', { className: 'version' }, runner.env.versionString())), - this.createDom('div', { className: 'options' }, - "Show ", - showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), - this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), - showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), - this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") - ) - ), - - this.runnerDiv = this.createDom('div', { className: 'runner running' }, - this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), - this.runnerMessageSpan = this.createDom('span', {}, "Running..."), - this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) - ); - - this.document.body.appendChild(this.outerDiv); - - var suites = runner.suites(); - for (var i = 0; i < suites.length; i++) { - var suite = suites[i]; - var suiteDiv = this.createDom('div', { className: 'suite' }, - this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), - this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); - this.suiteDivs[suite.id] = suiteDiv; - var parentDiv = this.outerDiv; - if (suite.parentSuite) { - parentDiv = this.suiteDivs[suite.parentSuite.id]; - } - parentDiv.appendChild(suiteDiv); - } - - this.startedAt = new Date(); - - var self = this; - showPassed.onclick = function(evt) { - if (showPassed.checked) { - self.outerDiv.className += ' show-passed'; - } else { - self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); - } - }; - - showSkipped.onclick = function(evt) { - if (showSkipped.checked) { - self.outerDiv.className += ' show-skipped'; - } else { - self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); - } - }; -}; - -jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { - var results = runner.results(); - var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; - this.runnerDiv.setAttribute("class", className); - //do it twice for IE - this.runnerDiv.setAttribute("className", className); - var specs = runner.specs(); - var specCount = 0; - for (var i = 0; i < specs.length; i++) { - if (this.specFilter(specs[i])) { - specCount++; - } - } - var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); - message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; - this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); - - this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); -}; - -jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { - var results = suite.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.totalCount === 0) { // todo: change this to check results.skipped - status = 'skipped'; - } - this.suiteDivs[suite.id].className += " " + status; -}; - -jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { - if (this.logRunningSpecs) { - this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); - } -}; - -jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { - var results = spec.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.skipped) { - status = 'skipped'; - } - var specDiv = this.createDom('div', { className: 'spec ' + status }, - this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), - this.createDom('a', { - className: 'description', - href: '?spec=' + encodeURIComponent(spec.getFullName()), - title: spec.getFullName() - }, spec.description)); - - - var resultItems = results.getItems(); - var messagesDiv = this.createDom('div', { className: 'messages' }); - for (var i = 0; i < resultItems.length; i++) { - var result = resultItems[i]; - - if (result.type == 'log') { - messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); - } else if (result.type == 'expect' && result.passed && !result.passed()) { - messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); - - if (result.trace.stack) { - messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); - } - } - } - - if (messagesDiv.childNodes.length > 0) { - specDiv.appendChild(messagesDiv); - } - - this.suiteDivs[spec.suite.id].appendChild(specDiv); -}; - -jasmine.TrivialReporter.prototype.log = function() { - var console = jasmine.getGlobal().console; - if (console && console.log) { - if (console.log.apply) { - console.log.apply(console, arguments); - } else { - console.log(arguments); // ie fix: console.log.apply doesn't exist on ie - } - } -}; - -jasmine.TrivialReporter.prototype.getLocation = function() { - return this.document.location; -}; - -jasmine.TrivialReporter.prototype.specFilter = function(spec) { - var paramMap = {}; - var params = this.getLocation().search.substring(1).split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); - } - - if (!paramMap.spec) { - return true; - } - return spec.getFullName().indexOf(paramMap.spec) === 0; -}; diff --git a/external/jasmine/jasmine.css b/external/jasmine/jasmine.css deleted file mode 100644 index 826e57531..000000000 --- a/external/jasmine/jasmine.css +++ /dev/null @@ -1,81 +0,0 @@ -body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } - -#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } -#HTMLReporter a { text-decoration: none; } -#HTMLReporter a:hover { text-decoration: underline; } -#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } -#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } -#HTMLReporter #jasmine_content { position: fixed; right: 100%; } -#HTMLReporter .version { color: #aaaaaa; } -#HTMLReporter .banner { margin-top: 14px; } -#HTMLReporter .duration { color: #aaaaaa; float: right; } -#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } -#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } -#HTMLReporter .symbolSummary li.passed { font-size: 14px; } -#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } -#HTMLReporter .symbolSummary li.failed { line-height: 9px; } -#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } -#HTMLReporter .symbolSummary li.skipped { font-size: 14px; } -#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } -#HTMLReporter .symbolSummary li.pending { line-height: 11px; } -#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } -#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } -#HTMLReporter .runningAlert { background-color: #666666; } -#HTMLReporter .skippedAlert { background-color: #aaaaaa; } -#HTMLReporter .skippedAlert:first-child { background-color: #333333; } -#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } -#HTMLReporter .passingAlert { background-color: #a6b779; } -#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } -#HTMLReporter .failingAlert { background-color: #cf867e; } -#HTMLReporter .failingAlert:first-child { background-color: #b03911; } -#HTMLReporter .results { margin-top: 14px; } -#HTMLReporter #details { display: none; } -#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } -#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } -#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } -#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } -#HTMLReporter.showDetails .summary { display: none; } -#HTMLReporter.showDetails #details { display: block; } -#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } -#HTMLReporter .summary { margin-top: 14px; } -#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } -#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } -#HTMLReporter .summary .specSummary.failed a { color: #b03911; } -#HTMLReporter .description + .suite { margin-top: 0; } -#HTMLReporter .suite { margin-top: 14px; } -#HTMLReporter .suite a { color: #333333; } -#HTMLReporter #details .specDetail { margin-bottom: 28px; } -#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } -#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } -#HTMLReporter .resultMessage span.result { display: block; } -#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } - -#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } -#TrivialReporter a:visited, #TrivialReporter a { color: #303; } -#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } -#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } -#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } -#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } -#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } -#TrivialReporter .runner.running { background-color: yellow; } -#TrivialReporter .options { text-align: right; font-size: .8em; } -#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } -#TrivialReporter .suite .suite { margin: 5px; } -#TrivialReporter .suite.passed { background-color: #dfd; } -#TrivialReporter .suite.failed { background-color: #fdd; } -#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } -#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } -#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } -#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } -#TrivialReporter .spec.skipped { background-color: #bbb; } -#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } -#TrivialReporter .passed { background-color: #cfc; display: none; } -#TrivialReporter .failed { background-color: #fbb; } -#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } -#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } -#TrivialReporter .resultMessage .mismatch { color: black; } -#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } -#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } -#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } -#TrivialReporter #jasmine_content { position: fixed; right: 100%; } -#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } diff --git a/external/jasmine/jasmine_favicon.png b/external/jasmine/jasmine_favicon.png deleted file mode 100644 index 218f3b437..000000000 Binary files a/external/jasmine/jasmine_favicon.png and /dev/null differ diff --git a/external/jasmineAdapter/JasmineAdapter.js b/external/jasmineAdapter/JasmineAdapter.js new file mode 100644 index 000000000..3b0fb2d76 --- /dev/null +++ b/external/jasmineAdapter/JasmineAdapter.js @@ -0,0 +1,198 @@ +/** + * @fileoverview Jasmine JsTestDriver Adapter. + * @author misko@hevery.com (Misko Hevery) + */ +(function(window) { + var rootDescribes = new Describes(window); + var describePath = []; + rootDescribes.collectMode(); + + var JASMINE_TYPE = 'jasmine test case'; + TestCase('Jasmine Adapter Tests', null, JASMINE_TYPE); + + var jasminePlugin = { + name:'jasmine', + + getTestRunsConfigurationFor: function(testCaseInfos, expressions, testRunsConfiguration) { + for (var i = 0; i < testCaseInfos.length; i++) { + if (testCaseInfos[i].getType() == JASMINE_TYPE) { + testRunsConfiguration.push(new jstestdriver.TestRunConfiguration(testCaseInfos[i], [])); + } + } + return false; + }, + + runTestConfiguration: function(testRunConfiguration, onTestDone, onTestRunConfigurationComplete){ + if (testRunConfiguration.getTestCaseInfo().getType() != JASMINE_TYPE) return false; + + var jasmineEnv = jasmine.currentEnv_ = new jasmine.Env(); + rootDescribes.playback(); + var specLog = jstestdriver.console.log_ = []; + var start; + jasmineEnv.specFilter = function(spec) { + return rootDescribes.isExclusive(spec); + }; + jasmineEnv.reporter = { + log: function(str){ + specLog.push(str); + }, + + reportRunnerStarting: function(runner) { }, + + reportSpecStarting: function(spec) { + specLog = jstestdriver.console.log_ = []; + start = new Date().getTime(); + }, + + reportSpecResults: function(spec) { + var suite = spec.suite; + var results = spec.results(); + if (results.skipped) return; + var end = new Date().getTime(); + var messages = []; + var resultItems = results.getItems(); + var state = 'passed'; + for ( var i = 0; i < resultItems.length; i++) { + if (!resultItems[i].passed()) { + state = resultItems[i].message.match(/AssertionError:/) ? 'error' : 'failed'; + messages.push({ + message: resultItems[i].toString(), + name: resultItems[i].trace.name, + stack: formatStack(resultItems[i].trace.stack) + }); + } + } + onTestDone( + new jstestdriver.TestResult( + suite.getFullName(), + spec.description, + state, + jstestdriver.angular.toJson(messages), + specLog.join('\n'), + end - start)); + }, + + reportSuiteResults: function(suite) {}, + + reportRunnerResults: function(runner) { + onTestRunConfigurationComplete(); + } + }; + jasmineEnv.execute(); + return true; + }, + + onTestsFinish: function(){ + jasmine.currentEnv_ = null; + rootDescribes.collectMode(); + } + }; + jstestdriver.pluginRegistrar.register(jasminePlugin); + + function formatStack(stack) { + var lines = (stack||'').split(/\r?\n/); + var frames = []; + for (i = 0; i < lines.length; i++) { + if (!lines[i].match(/\/jasmine[\.-]/)) { + frames.push(lines[i].replace(/https?:\/\/\w+(:\d+)?\/test\//, '').replace(/^\s*/, ' ')); + } + } + return frames.join('\n'); + } + + function noop(){} + function Describes(window){ + var describes = {}; + var beforeEachs = {}; + var afterEachs = {}; + // Here we store: + // 0: everyone runs + // 1: run everything under ddescribe + // 2: run only iits (ignore ddescribe) + var exclusive = 0; + var collectMode = true; + intercept('describe', describes); + intercept('xdescribe', describes); + intercept('beforeEach', beforeEachs); + intercept('afterEach', afterEachs); + + function intercept(functionName, collection){ + window[functionName] = function(desc, fn){ + if (collectMode) { + collection[desc] = function(){ + jasmine.getEnv()[functionName](desc, fn); + }; + } else { + jasmine.getEnv()[functionName](desc, fn); + } + }; + } + window.ddescribe = function(name, fn){ + if (exclusive < 1) { + exclusive = 1; // run ddescribe only + } + window.describe(name, function(){ + var oldIt = window.it; + window.it = function(name, fn){ + fn.exclusive = 1; // run anything under ddescribe + jasmine.getEnv().it(name, fn); + }; + try { + fn.call(this); + } finally { + window.it = oldIt; + }; + }); + }; + window.iit = function(name, fn){ + exclusive = fn.exclusive = 2; // run only iits + jasmine.getEnv().it(name, fn); + }; + + + this.collectMode = function() { + collectMode = true; + exclusive = 0; // run everything + }; + this.playback = function(){ + collectMode = false; + playback(beforeEachs); + playback(afterEachs); + playback(describes); + + function playback(set) { + for ( var name in set) { + set[name](); + } + } + }; + + this.isExclusive = function(spec) { + if (exclusive) { + var blocks = spec.queue.blocks; + for ( var i = 0; i < blocks.length; i++) { + if (blocks[i].func.exclusive >= exclusive) { + return true; + } + } + return false; + } + return true; + }; + } + +})(window); + +// Patch Jasmine for proper stack traces +jasmine.Spec.prototype.fail = function (e) { + var expectationResult = new jasmine.ExpectationResult({ + passed: false, + message: e ? jasmine.util.formatException(e) : 'Exception' + }); + // PATCH + if (e) { + expectationResult.trace = e; + } + this.results_.addResult(expectationResult); +}; + diff --git a/external/jasmineAdapter/MIT.LICENSE b/external/jasmineAdapter/MIT.LICENSE new file mode 100644 index 000000000..f650924e6 --- /dev/null +++ b/external/jasmineAdapter/MIT.LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2010 + Misko Hevery + Olmo Maldonado + Christoph Pojer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/external/jsTestDriver/JsTestDriver-1.3.3d.jar b/external/jsTestDriver/JsTestDriver-1.3.3d.jar new file mode 100644 index 000000000..9de7cf64a Binary files /dev/null and b/external/jsTestDriver/JsTestDriver-1.3.3d.jar differ diff --git a/external/jsTestDriver/LICENSE-2.0.txt b/external/jsTestDriver/LICENSE-2.0.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/external/jsTestDriver/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/src/canvas.js b/src/canvas.js index cd49c88b1..3fd55b45d 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -255,8 +255,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } // Scale so that canvas units are the same as PDF user space units this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); - this.textDivs = []; - this.textLayerQueue = []; + // Move the media left-top corner to the (0,0) canvas position + this.ctx.translate(-mediaBox.x, -mediaBox.y); + + if (this.textLayer) + this.textLayer.beginLayout(); }, executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR, @@ -320,27 +323,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { endDrawing: function canvasGraphicsEndDrawing() { this.ctx.restore(); - var textLayer = this.textLayer; - if (!textLayer) - return; - - var self = this; - var textDivs = this.textDivs; - this.textLayerTimer = setInterval(function renderTextLayer() { - if (textDivs.length === 0) { - clearInterval(self.textLayerTimer); - return; - } - var textDiv = textDivs.shift(); - if (textDiv.dataset.textLength > 1) { // avoid div by zero - textLayer.appendChild(textDiv); - // Adjust div width (via letterSpacing) to match canvas text - // Due to the .offsetWidth calls, this is slow - textDiv.style.letterSpacing = - ((textDiv.dataset.canvasWidth - textDiv.offsetWidth) / - (textDiv.dataset.textLength - 1)) + 'px'; - } - }, 0); + if (this.textLayer) + this.textLayer.endLayout(); }, // Graphics state @@ -359,6 +343,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { setDash: function canvasGraphicsSetDash(dashArray, dashPhase) { this.ctx.mozDash = dashArray; this.ctx.mozDashOffset = dashPhase; + this.ctx.webkitLineDash = dashArray; + this.ctx.webkitLineDashOffset = dashPhase; }, setRenderingIntent: function canvasGraphicsSetRenderingIntent(intent) { TODO('set rendering intent: ' + intent); @@ -630,24 +616,6 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { return geometry; }, - pushTextDivs: function canvasGraphicsPushTextDivs(text) { - var div = document.createElement('div'); - var fontSize = this.current.fontSize; - - // vScale and hScale already contain the scaling to pixel units - // as mozCurrentTransform reflects ctx.scale() changes - // (see beginDrawing()) - var fontHeight = fontSize * text.geom.vScale; - div.dataset.canvasWidth = text.canvasWidth * text.geom.hScale; - - div.style.fontSize = fontHeight + 'px'; - div.style.fontFamily = this.current.font.loadedName || 'sans-serif'; - div.style.left = text.geom.x + 'px'; - div.style.top = (text.geom.y - fontHeight) + 'px'; - div.innerHTML = text.str; - div.dataset.textLength = text.length; - this.textDivs.push(div); - }, showText: function canvasGraphicsShowText(str, skipTextSelection) { var ctx = this.ctx; var current = this.current; @@ -672,6 +640,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { ctx.translate(current.x, current.y); ctx.scale(textHScale, 1); + ctx.lineWidth /= current.textMatrix[0]; if (textSelection) { this.save(); @@ -708,6 +677,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } else { ctx.save(); this.applyTextTransforms(); + ctx.lineWidth /= current.textMatrix[0] * fontMatrix[0]; + if (textSelection) text.geom = this.getTextGeometry(); @@ -744,7 +715,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { width += charWidth; - text.str += glyph.unicode === ' ' ? ' ' : glyph.unicode; + text.str += glyph.unicode === ' ' ? '\u00A0' : glyph.unicode; text.length++; text.canvasWidth += charWidth; } @@ -753,7 +724,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } if (textSelection) - this.pushTextDivs(text); + this.textLayer.appendText(text, font.loadedName, fontSize); return text; }, @@ -796,7 +767,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (e < 0 && text.geom.spaceWidth > 0) { // avoid div by zero var numFakeSpaces = Math.round(-e / text.geom.spaceWidth); if (numFakeSpaces > 0) { - text.str += ' '; + text.str += '\u00A0'; text.length++; } } @@ -806,7 +777,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (textSelection) { if (shownText.str === ' ') { - text.str += ' '; + text.str += '\u00A0'; } else { text.str += shownText.str; } @@ -819,7 +790,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } if (textSelection) - this.pushTextDivs(text); + this.textLayer.appendText(text, font.loadedName, fontSize); }, nextLineShowText: function canvasGraphicsNextLineShowText(text) { this.nextLine(); diff --git a/src/core.js b/src/core.js index 96e0c9fcc..13d9158e0 100644 --- a/src/core.js +++ b/src/core.js @@ -70,8 +70,7 @@ var Page = (function PageClosure() { this.xref = xref; this.ref = ref; - this.ctx = null; - this.callback = null; + this.displayReadyPromise = null; } Page.prototype = { @@ -110,9 +109,11 @@ var Page = (function PageClosure() { width: this.width, height: this.height }; + var mediaBox = this.mediaBox; + var offsetX = mediaBox[0], offsetY = mediaBox[1]; if (isArray(obj) && obj.length == 4) { - var tl = this.rotatePoint(obj[0], obj[1]); - var br = this.rotatePoint(obj[2], obj[3]); + var tl = this.rotatePoint(obj[0] - offsetX, obj[1] - offsetY); + var br = this.rotatePoint(obj[2] - offsetX, obj[3] - offsetY); view.x = Math.min(tl.x, br.x); view.y = Math.min(tl.y, br.y); view.width = Math.abs(tl.x - br.x); @@ -165,20 +166,12 @@ var Page = (function PageClosure() { IRQueue, fonts) { var self = this; this.IRQueue = IRQueue; - var gfx = new CanvasGraphics(this.ctx, this.objs, this.textLayer); var displayContinuation = function pageDisplayContinuation() { // Always defer call to display() to work around bug in // Firefox error reporting from XHR callbacks. setTimeout(function pageSetTimeout() { - try { - self.display(gfx, self.callback); - } catch (e) { - if (self.callback) - self.callback(e); - else - throw e; - } + self.displayReadyPromise.resolve(); }); }; @@ -323,10 +316,10 @@ var Page = (function PageClosure() { if (a) { switch (a.get('S').name) { case 'URI': - link.url = a.get('URI'); + item.url = a.get('URI'); break; case 'GoTo': - link.dest = a.get('D'); + item.dest = a.get('D'); break; default: TODO('other link types'); @@ -334,7 +327,7 @@ var Page = (function PageClosure() { } else if (annotation.has('Dest')) { // simple destination link var dest = annotation.get('Dest'); - link.dest = isName(dest) ? dest.name : dest; + item.dest = isName(dest) ? dest.name : dest; } break; case 'Widget': @@ -379,18 +372,51 @@ var Page = (function PageClosure() { item.textAlignment = getInheritableProperty(annotation, 'Q'); item.flags = getInheritableProperty(annotation, 'Ff') || 0; break; + case 'Text': + var content = annotation.get('Contents'); + var title = annotation.get('T'); + item.content = stringToPDFString(content || ''); + item.title = stringToPDFString(title || ''); + item.name = annotation.get('Name').name; + break; + default: + TODO('unimplemented annotation type: ' + subtype.name); + break; } items.push(item); } return items; }, startRendering: function pageStartRendering(ctx, callback, textLayer) { - this.ctx = ctx; - this.callback = callback; - this.textLayer = textLayer; - this.startRenderingTime = Date.now(); - this.pdf.startRendering(this); + + // If there is no displayReadyPromise yet, then the IRQueue was never + // requested before. Make the request and create the promise. + if (!this.displayReadyPromise) { + this.pdf.startRendering(this); + this.displayReadyPromise = new Promise(); + } + + // Once the IRQueue and fonts are loaded, perform the actual rendering. + this.displayReadyPromise.then( + function pageDisplayReadyPromise() { + var gfx = new CanvasGraphics(ctx, this.objs, textLayer); + try { + this.display(gfx, callback); + } catch (e) { + if (callback) + callback(e); + else + throw e; + } + }.bind(this), + function pageDisplayReadPromiseError(reason) { + if (callback) + callback(reason); + else + throw reason; + } + ); } }; @@ -513,10 +539,19 @@ var PDFDocModel = (function PDFDocModelClosure() { }, setup: function pdfDocSetup(ownerPassword, userPassword) { this.checkHeader(); - this.xref = new XRef(this.stream, - this.startXRef, - this.mainXRefEntriesOffset); - this.catalog = new Catalog(this.xref); + var xref = new XRef(this.stream, + this.startXRef, + this.mainXRefEntriesOffset); + this.xref = xref; + this.catalog = new Catalog(xref); + if (xref.trailer && xref.trailer.has('ID')) { + var fileID = ''; + var id = xref.fetchIfRef(xref.trailer.get('ID'))[0]; + id.split('').forEach(function(el) { + fileID += Number(el.charCodeAt(0)).toString(16); + }); + this.fileID = fileID; + } }, get numPages() { var linearization = this.linearization; @@ -524,6 +559,22 @@ var PDFDocModel = (function PDFDocModelClosure() { // shadow the prototype getter return shadow(this, 'numPages', num); }, + getFingerprint: function pdfDocGetFingerprint() { + if (this.fileID) { + return this.fileID; + } else { + // If we got no fileID, then we generate one, + // from the first 100 bytes of PDF + var data = this.stream.bytes.subarray(0, 100); + var hash = calculateMD5(data, 0, data.length); + var strHash = ''; + for (var i = 0, length = hash.length; i < length; i++) { + strHash += Number(hash[i]).toString(16); + } + + return strHash; + } + }, getPage: function pdfDocGetPage(n) { return this.catalog.getPage(n); } @@ -550,7 +601,7 @@ var PDFDoc = (function PDFDocClosure() { this.data = data; this.stream = stream; this.pdf = new PDFDocModel(stream); - + this.fingerprint = this.pdf.getFingerprint(); this.catalog = this.pdf.catalog; this.objs = new PDFObjects(); @@ -569,33 +620,34 @@ var PDFDoc = (function PDFDocClosure() { throw 'No PDFJS.workerSrc specified'; } - var worker; try { - worker = new Worker(workerSrc); - } catch (e) { // Some versions of FF can't create a worker on localhost, see: // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); + var worker = new Worker(workerSrc); + + var messageHandler = new MessageHandler('main', worker); + + messageHandler.on('test', function pdfDocTest(supportTypedArray) { + if (supportTypedArray) { + this.worker = worker; + this.setupMessageHandler(messageHandler); + } else { + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array(1); + // Some versions of Opera throw a DATA_CLONE_ERR on + // serializing the typed array. + messageHandler.send('test', testObj); return; - } - - var messageHandler = new MessageHandler('main', worker); - - messageHandler.on('test', function pdfDocTest(supportTypedArray) { - if (supportTypedArray) { - this.worker = worker; - this.setupMessageHandler(messageHandler); - } else { - this.setupFakeWorker(); - } - }.bind(this)); - - var testObj = new Uint8Array(1); - messageHandler.send('test', testObj); - } else { - this.setupFakeWorker(); + } catch (e) {} } + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); } PDFDoc.prototype = { @@ -679,8 +731,8 @@ var PDFDoc = (function PDFDocClosure() { messageHandler.on('page_error', function pdfDocError(data) { var page = this.pageCache[data.pageNum]; - if (page.callback) - page.callback(data.error); + if (page.displayReadyPromise) + page.displayReadyPromise.reject(data.error); else throw data.error; }, this); diff --git a/src/evaluator.js b/src/evaluator.js index 2905565da..198659201 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -118,7 +118,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var self = this; var xref = this.xref; var handler = this.handler; - var uniquePrefix = this.uniquePrefix; + var uniquePrefix = this.uniquePrefix || ''; function insertDependency(depList) { fnArray.push('dependency'); @@ -211,7 +211,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { args = [objId, w, h]; var softMask = dict.get('SMask', 'IM') || false; - if (!softMask && image instanceof JpegStream && image.isNative) { + if (!softMask && image instanceof JpegStream && + image.isNativelySupported(xref, resources)) { // These JPEGs don't need any more processing so we can just send it. fn = 'paintJpegXObject'; handler.send('obj', [objId, 'JpegStream', image.getIR()]); @@ -234,7 +235,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, handler, xref, resources, image, inline); } - uniquePrefix = uniquePrefix || ''; if (!queue.argsArray) { queue.argsArray = []; } diff --git a/src/fonts.js b/src/fonts.js index 83ce4abaa..1b959d6c2 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -2092,7 +2092,7 @@ var Font = (function FontClosure() { window.btoa(data) + ');'); var rule = "@font-face { font-family:'" + fontName + "';src:" + url + '}'; - document.documentElement.firstChild.appendChild( + document.documentElement.getElementsByTagName('head')[0].appendChild( document.createElement('style')); var styleSheet = document.styleSheets[document.styleSheets.length - 1]; diff --git a/src/function.js b/src/function.js index 6b0063218..26b8fe679 100644 --- a/src/function.js +++ b/src/function.js @@ -270,7 +270,6 @@ var PDFFunction = (function PDFFunctionClosure() { constructStiched: function pdfFunctionConstructStiched(fn, dict, xref) { var domain = dict.get('Domain'); - var range = dict.get('Range'); if (!domain) error('No domain'); @@ -279,13 +278,13 @@ var PDFFunction = (function PDFFunctionClosure() { if (inputSize != 1) error('Bad domain for stiched function'); - var fnRefs = dict.get('Functions'); + var fnRefs = xref.fetchIfRef(dict.get('Functions')); var fns = []; for (var i = 0, ii = fnRefs.length; i < ii; ++i) fns.push(PDFFunction.getIR(xref, xref.fetchIfRef(fnRefs[i]))); - var bounds = dict.get('Bounds'); - var encode = dict.get('Encode'); + var bounds = xref.fetchIfRef(dict.get('Bounds')); + var encode = xref.fetchIfRef(dict.get('Encode')); return [CONSTRUCT_STICHED, domain, bounds, encode, fns]; }, @@ -336,16 +335,550 @@ var PDFFunction = (function PDFFunctionClosure() { }; }, - constructPostScript: function pdfFunctionConstructPostScript() { - return [CONSTRUCT_POSTSCRIPT]; + constructPostScript: function pdfFunctionConstructPostScript(fn, dict, + xref) { + var domain = dict.get('Domain'); + var range = dict.get('Range'); + + if (!domain) + error('No domain.'); + + if (!range) + error('No range.'); + + var lexer = new PostScriptLexer(fn); + var parser = new PostScriptParser(lexer); + var code = parser.parse(); + + return [CONSTRUCT_POSTSCRIPT, domain, range, code]; }, - constructPostScriptFromIR: function pdfFunctionConstructPostScriptFromIR() { - TODO('unhandled type of function'); - return function constructPostScriptFromIRResult() { - return [255, 105, 180]; + constructPostScriptFromIR: + function pdfFunctionConstructPostScriptFromIR(IR) { + var domain = IR[1]; + var range = IR[2]; + var code = IR[3]; + var numOutputs = range.length / 2; + var evaluator = new PostScriptEvaluator(code); + // Cache the values for a big speed up, the cache size is limited though + // since the number of possible values can be huge from a PS function. + var cache = new FunctionCache(); + return function constructPostScriptFromIRResult(args) { + var initialStack = []; + for (var i = 0, ii = (domain.length / 2); i < ii; ++i) { + initialStack.push(args[i]); + } + + var key = initialStack.join('_'); + if (cache.has(key)) + return cache.get(key); + + var stack = evaluator.execute(initialStack); + var transformed = new Array(numOutputs); + for (i = numOutputs - 1; i >= 0; --i) { + var out = stack.pop(); + var rangeIndex = 2 * i; + if (out < range[rangeIndex]) + out = range[rangeIndex]; + else if (out > range[rangeIndex + 1]) + out = range[rangeIndex + 1]; + transformed[i] = out; + } + cache.set(key, transformed); + return transformed; }; } }; })(); +var FunctionCache = (function FunctionCacheClosure() { + // Of 10 PDF's with type4 functions the maxium number of distinct values seen + // was 256. This still may need some tweaking in the future though. + var MAX_CACHE_SIZE = 1024; + function FunctionCache() { + this.cache = {}; + this.total = 0; + } + FunctionCache.prototype = { + has: function has(key) { + return key in this.cache; + }, + get: function get(key) { + return this.cache[key]; + }, + set: function set(key, value) { + if (this.total < MAX_CACHE_SIZE) { + this.cache[key] = value; + this.total++; + } + } + }; + return FunctionCache; +})(); + +var PostScriptStack = (function PostScriptStackClosure() { + var MAX_STACK_SIZE = 100; + function PostScriptStack(initialStack) { + this.stack = initialStack || []; + } + + PostScriptStack.prototype = { + push: function push(value) { + if (this.stack.length >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + this.stack.push(value); + }, + pop: function pop() { + if (this.stack.length <= 0) + error('PostScript function stack underflow.'); + return this.stack.pop(); + }, + copy: function copy(n) { + if (this.stack.length + n >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + var stack = this.stack; + for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) + stack.push(stack[i]); + }, + index: function index(n) { + this.push(this.stack[this.stack.length - n - 1]); + }, + // rotate the last n stack elements p times + roll: function roll(n, p) { + var stack = this.stack; + var l = stack.length - n; + var r = stack.length - 1, c = l + (p - Math.floor(p / n) * n), i, j, t; + for (i = l, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = l, j = c - 1; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = c, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + } + }; + return PostScriptStack; +})(); +var PostScriptEvaluator = (function PostScriptEvaluatorClosure() { + function PostScriptEvaluator(operators, operands) { + this.operators = operators; + this.operands = operands; + } + PostScriptEvaluator.prototype = { + execute: function execute(initialStack) { + var stack = new PostScriptStack(initialStack); + var counter = 0; + var operators = this.operators; + var length = operators.length; + var operator, a, b; + while (counter < length) { + operator = operators[counter++]; + if (typeof operator == 'number') { + // Operator is really an operand and should be pushed to the stack. + stack.push(operator); + continue; + } + switch (operator) { + // non standard ps operators + case 'jz': // jump if false + b = stack.pop(); + a = stack.pop(); + if (!a) + counter = b; + break; + case 'j': // jump + a = stack.pop(); + counter = a; + break; + + // all ps operators in alphabetical order (excluding if/ifelse) + case 'abs': + a = stack.pop(); + stack.push(Math.abs(a)); + break; + case 'add': + b = stack.pop(); + a = stack.pop(); + stack.push(a + b); + break; + case 'and': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'atan': + a = stack.pop(); + stack.push(Math.atan(a)); + break; + case 'bitshift': + b = stack.pop(); + a = stack.pop(); + if (a > 0) + stack.push(a << b); + else + stack.push(a >> b); + break; + case 'ceiling': + a = stack.pop(); + stack.push(Math.ceil(a)); + break; + case 'copy': + a = stack.pop(); + stack.copy(a); + break; + case 'cos': + a = stack.pop(); + stack.push(Math.cos(a)); + break; + case 'cvi': + a = stack.pop() | 0; + stack.push(a); + break; + case 'cvr': + // noop + break; + case 'div': + b = stack.pop(); + a = stack.pop(); + stack.push(a / b); + break; + case 'dup': + stack.copy(1); + break; + case 'eq': + b = stack.pop(); + a = stack.pop(); + stack.push(a == b); + break; + case 'exch': + stack.roll(2, 1); + break; + case 'exp': + b = stack.pop(); + a = stack.pop(); + stack.push(Math.pow(a, b)); + break; + case 'false': + stack.push(false); + break; + case 'floor': + a = stack.pop(); + stack.push(Math.floor(a)); + break; + case 'ge': + b = stack.pop(); + a = stack.pop(); + stack.push(a >= b); + break; + case 'gt': + b = stack.pop(); + a = stack.pop(); + stack.push(a > b); + break; + case 'idiv': + b = stack.pop(); + a = stack.pop(); + stack.push((a / b) | 0); + break; + case 'index': + a = stack.pop(); + stack.index(a); + break; + case 'le': + b = stack.pop(); + a = stack.pop(); + stack.push(a <= b); + break; + case 'ln': + a = stack.pop(); + stack.push(Math.log(a)); + break; + case 'log': + a = stack.pop(); + stack.push(Math.log(a) / Math.LN10); + break; + case 'lt': + b = stack.pop(); + a = stack.pop(); + stack.push(a < b); + break; + case 'mod': + b = stack.pop(); + a = stack.pop(); + stack.push(a % b); + break; + case 'mul': + b = stack.pop(); + a = stack.pop(); + stack.push(a * b); + break; + case 'ne': + b = stack.pop(); + a = stack.pop(); + stack.push(a != b); + break; + case 'neg': + a = stack.pop(); + stack.push(-b); + break; + case 'not': + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'or': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a || b); + else + stack.push(a | b); + break; + case 'pop': + stack.pop(); + break; + case 'roll': + b = stack.pop(); + a = stack.pop(); + stack.roll(a, b); + break; + case 'round': + a = stack.pop(); + stack.push(Math.round(a)); + break; + case 'sin': + a = stack.pop(); + stack.push(Math.sin(a)); + break; + case 'sqrt': + a = stack.pop(); + stack.push(Math.sqrt(a)); + break; + case 'sub': + b = stack.pop(); + a = stack.pop(); + stack.push(a - b); + break; + case 'true': + stack.push(true); + break; + case 'truncate': + a = stack.pop(); + a = a < 0 ? Math.ceil(a) : Math.floor(a); + stack.push(a); + break; + case 'xor': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a != b); + else + stack.push(a ^ b); + break; + default: + error('Unknown operator ' + operator); + break; + } + } + return stack.stack; + } + }; + return PostScriptEvaluator; +})(); + +var PostScriptParser = (function PostScriptParserClosure() { + function PostScriptParser(lexer) { + this.lexer = lexer; + this.operators = []; + this.token; + this.prev; + } + PostScriptParser.prototype = { + nextToken: function nextToken() { + this.prev = this.token; + this.token = this.lexer.getToken(); + }, + accept: function accept(type) { + if (this.token.type == type) { + this.nextToken(); + return true; + } + return false; + }, + expect: function expect(type) { + if (this.accept(type)) + return true; + error('Unexpected symbol: found ' + this.token.type + ' expected ' + + type + '.'); + }, + parse: function parse() { + this.nextToken(); + this.expect(PostScriptTokenTypes.LBRACE); + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + return this.operators; + }, + parseBlock: function parseBlock() { + while (true) { + if (this.accept(PostScriptTokenTypes.NUMBER)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.OPERATOR)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + this.parseCondition(); + } else { + return; + } + } + }, + parseCondition: function parseCondition() { + // Add two place holders that will be updated later + var conditionLocation = this.operators.length; + this.operators.push(null, null); + + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + if (this.accept(PostScriptTokenTypes.IF)) { + // The true block is right after the 'if' so it just falls through on + // true else it jumps and skips the true block. + this.operators[conditionLocation] = this.operators.length; + this.operators[conditionLocation + 1] = 'jz'; + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + var jumpLocation = this.operators.length; + this.operators.push(null, null); + var endOfTrue = this.operators.length; + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + this.expect(PostScriptTokenTypes.IFELSE); + // The jump is added at the end of the true block to skip the false + // block. + this.operators[jumpLocation] = this.operators.length; + this.operators[jumpLocation + 1] = 'j'; + + this.operators[conditionLocation] = endOfTrue; + this.operators[conditionLocation + 1] = 'jz'; + } else { + error('PS Function: error parsing conditional.'); + } + } + }; + return PostScriptParser; +})(); + +var PostScriptTokenTypes = { + LBRACE: 0, + RBRACE: 1, + NUMBER: 2, + OPERATOR: 3, + IF: 4, + IFELSE: 5 +}; + +var PostScriptToken = (function PostScriptTokenClosure() { + function PostScriptToken(type, value) { + this.type = type; + this.value = value; + } + + var opCache = {}; + + PostScriptToken.getOperator = function getOperator(op) { + var opValue = opCache[op]; + if (opValue) + return opValue; + + return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op); + }; + + PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE, + '{'); + PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE, + '}'); + PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF'); + PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE, + 'IFELSE'); + return PostScriptToken; +})(); + +var PostScriptLexer = (function PostScriptLexerClosure() { + function PostScriptLexer(stream) { + this.stream = stream; + } + PostScriptLexer.prototype = { + getToken: function getToken() { + var s = ''; + var ch; + var comment = false; + var stream = this.stream; + + // skip comments + while (true) { + if (!(ch = stream.getChar())) + return EOF; + + if (comment) { + if (ch == '\x0a' || ch == '\x0d') + comment = false; + } else if (ch == '%') { + comment = true; + } else if (!Lexer.isSpace(ch)) { + break; + } + } + switch (ch) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '+': case '-': case '.': + return new PostScriptToken(PostScriptTokenTypes.NUMBER, + this.getNumber(ch)); + case '{': + return PostScriptToken.LBRACE; + case '}': + return PostScriptToken.RBRACE; + } + // operator + var str = ch.toLowerCase(); + while (true) { + ch = stream.lookChar().toLowerCase(); + if (ch >= 'a' && ch <= 'z') + str += ch; + else + break; + stream.skip(); + } + switch (str) { + case 'if': + return PostScriptToken.IF; + case 'ifelse': + return PostScriptToken.IFELSE; + default: + return PostScriptToken.getOperator(str); + } + }, + getNumber: function getNumber(ch) { + var str = ch; + var stream = this.stream; + while (true) { + ch = stream.lookChar(); + if ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') + str += ch; + else + break; + stream.skip(); + } + var value = parseFloat(str); + if (isNaN(value)) + error('Invalid floating point number: ' + value); + return value; + } + }; + return PostScriptLexer; +})(); + diff --git a/src/image.js b/src/image.js index 29bad4d8a..6e7ab2020 100644 --- a/src/image.js +++ b/src/image.js @@ -9,7 +9,7 @@ var PDFImage = (function PDFImageClosure() { * when the image data is ready. */ function handleImageData(handler, xref, res, image, promise) { - if (image instanceof JpegStream && image.isNative) { + if (image instanceof JpegStream && image.isNativelyDecodable(xref, res)) { // For natively supported jpegs send them to the main thread for decoding. var dict = image.dict; var colorSpace = dict.get('ColorSpace', 'CS'); diff --git a/src/obj.js b/src/obj.js index 453014a91..ef7932546 100644 --- a/src/obj.js +++ b/src/obj.js @@ -8,8 +8,7 @@ var Name = (function NameClosure() { this.name = name; } - Name.prototype = { - }; + Name.prototype = {}; return Name; })(); @@ -19,9 +18,7 @@ var Cmd = (function CmdClosure() { this.cmd = cmd; } - Cmd.prototype = { - }; - + Cmd.prototype = {}; var cmdCache = {}; @@ -80,8 +77,7 @@ var Ref = (function RefClosure() { this.gen = gen; } - Ref.prototype = { - }; + Ref.prototype = {}; return Ref; })(); @@ -124,11 +120,11 @@ var Catalog = (function CatalogClosure() { return shadow(this, 'toplevelPagesDict', xrefObj); }, get documentOutline() { - var obj = this.catDict.get('Outlines'); var xref = this.xref; + var obj = xref.fetchIfRef(this.catDict.get('Outlines')); var root = { items: [] }; - if (isRef(obj)) { - obj = xref.fetch(obj).get('First'); + if (isDict(obj)) { + obj = obj.get('First'); var processed = new RefSet(); if (isRef(obj)) { var queue = [{obj: obj, parent: root}]; @@ -273,7 +269,7 @@ var XRef = (function XRefClosure() { this.entries = []; this.xrefstms = {}; var trailerDict = this.readXRef(startXRef); - + this.trailer = trailerDict; // prepare the XRef cache this.cache = []; @@ -556,9 +552,7 @@ var XRef = (function XRefClosure() { }, getEntry: function xRefGetEntry(i) { var e = this.entries[i]; - if (e.free) - error('reading an XRef stream not implemented yet'); - return e; + return e.free ? null : e; // returns null is the entry is free }, fetchIfRef: function xRefFetchIfRef(obj) { if (!isRef(obj)) @@ -567,11 +561,15 @@ var XRef = (function XRefClosure() { }, fetch: function xRefFetch(ref, suppressEncryption) { var num = ref.num; - var e = this.cache[num]; - if (e) - return e; + if (num in this.cache) + return this.cache[num]; + + var e = this.getEntry(num); + + // the referenced entry can be free + if (e === null) + return (this.cache[num] = e); - e = this.getEntry(num); var gen = ref.gen; var stream, parser; if (e.uncompressed) { diff --git a/src/stream.js b/src/stream.js index d996f5c91..69748b5f2 100644 --- a/src/stream.js +++ b/src/stream.js @@ -803,29 +803,16 @@ var JpegStream = (function JpegStreamClosure() { // need to be removed this.dict = dict; - // Flag indicating wether the image can be natively loaded. - this.isNative = true; - - this.colorTransform = -1; + this.isAdobeImage = false; + this.colorTransform = dict.get('ColorTransform') || -1; if (isAdobeImage(bytes)) { - // when bug 674619 land, let's check if browser can do - // normal cmyk and then we won't have to the following - var cs = xref.fetchIfRef(dict.get('ColorSpace')); - - // DeviceRGB and DeviceGray are the only Adobe images that work natively - if (isName(cs) && (cs.name === 'DeviceRGB' || cs.name === 'DeviceGray')) { - bytes = fixAdobeImage(bytes); - this.src = bytesToString(bytes); - } else { - this.colorTransform = dict.get('ColorTransform'); - this.isNative = false; - this.bytes = bytes; - } - } else { - this.src = bytesToString(bytes); + this.isAdobeImage = true; + bytes = fixAdobeImage(bytes); } + this.bytes = bytes; + DecodeStream.call(this); } @@ -835,7 +822,8 @@ var JpegStream = (function JpegStreamClosure() { if (this.bufferLength) return; var jpegImage = new JpegImage(); - jpegImage.colorTransform = this.colorTransform; + if (this.colorTransform != -1) + jpegImage.colorTransform = this.colorTransform; jpegImage.parse(this.bytes); var width = jpegImage.width; var height = jpegImage.height; @@ -844,11 +832,39 @@ var JpegStream = (function JpegStreamClosure() { this.bufferLength = data.length; }; JpegStream.prototype.getIR = function jpegStreamGetIR() { - return this.src; + return bytesToString(this.bytes); }; JpegStream.prototype.getChar = function jpegStreamGetChar() { error('internal error: getChar is not valid on JpegStream'); }; + /** + * Checks if the image can be decoded and displayed by the browser without any + * further processing such as color space conversions. + */ + JpegStream.prototype.isNativelySupported = function isNativelySupported(xref, + res) { + var cs = ColorSpace.parse(this.dict.get('ColorSpace'), xref, res); + // when bug 674619 lands, let's check if browser can do + // normal cmyk and then we won't need to decode in JS + if (cs.name === 'DeviceGray' || cs.name === 'DeviceRGB') + return true; + if (cs.name === 'DeviceCMYK' && !this.isAdobeImage && + this.colorTransform < 1) + return true; + return false; + }; + /** + * Checks if the image can be decoded by the browser. + */ + JpegStream.prototype.isNativelyDecodable = function isNativelyDecodable(xref, + res) { + var cs = ColorSpace.parse(this.dict.get('ColorSpace'), xref, res); + var numComps = cs.numComps; + if (numComps == 1 || numComps == 3) + return true; + + return false; + }; return JpegStream; })(); @@ -1856,10 +1872,10 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { // values. The first array element indicates whether a valid code is being // returned. The second array element is the actual code. The third array // element indicates whether EOF was reached. - var findTableCode = function ccittFaxStreamFindTableCode(start, end, table, - limit) { - var limitValue = limit || 0; + CCITTFaxStream.prototype.findTableCode = + function ccittFaxStreamFindTableCode(start, end, table, limit) { + var limitValue = limit || 0; for (var i = start; i <= end; ++i) { var code = this.lookBits(i); if (code == EOF) @@ -1890,7 +1906,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { return p[1]; } } else { - var result = findTableCode(1, 7, twoDimTable); + var result = this.findTableCode(1, 7, twoDimTable); if (result[0] && result[2]) return result[1]; } @@ -1919,11 +1935,11 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { return p[1]; } } else { - var result = findTableCode(1, 9, whiteTable2); + var result = this.findTableCode(1, 9, whiteTable2); if (result[0]) return result[1]; - result = findTableCode(11, 12, whiteTable1); + result = this.findTableCode(11, 12, whiteTable1); if (result[0]) return result[1]; } @@ -1952,15 +1968,15 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { return p[1]; } } else { - var result = findTableCode(2, 6, blackTable3); + var result = this.findTableCode(2, 6, blackTable3); if (result[0]) return result[1]; - result = findTableCode(7, 12, blackTable2, 64); + result = this.findTableCode(7, 12, blackTable2, 64); if (result[0]) return result[1]; - result = findTableCode(10, 13, blackTable1); + result = this.findTableCode(10, 13, blackTable1); if (result[0]) return result[1]; } diff --git a/src/util.js b/src/util.js index 57dbca4bb..99b422296 100644 --- a/src/util.js +++ b/src/util.js @@ -206,6 +206,8 @@ var Promise = (function PromiseClosure() { */ function Promise(name, data) { this.name = name; + this.isRejected = false; + this.error = null; // If you build a promise and pass in some data it's already resolved. if (data != null) { this.isResolved = true; @@ -216,6 +218,7 @@ var Promise = (function PromiseClosure() { this._data = EMPTY_PROMISE; } this.callbacks = []; + this.errbacks = []; }; /** * Builds a promise that is resolved when all the passed in promises are @@ -282,9 +285,12 @@ var Promise = (function PromiseClosure() { if (this.isResolved) { throw 'A Promise can be resolved only once ' + this.name; } + if (this.isRejected) { + throw 'The Promise was already rejected ' + this.name; + } this.isResolved = true; - this.data = data; + this.data = data || null; var callbacks = this.callbacks; for (var i = 0, ii = callbacks.length; i < ii; i++) { @@ -292,7 +298,24 @@ var Promise = (function PromiseClosure() { } }, - then: function promiseThen(callback) { + reject: function proimseReject(reason) { + if (this.isRejected) { + throw 'A Promise can be rejected only once ' + this.name; + } + if (this.isResolved) { + throw 'The Promise was already resolved ' + this.name; + } + + this.isRejected = true; + this.error = reason || null; + var errbacks = this.errbacks; + + for (var i = 0, ii = errbacks.length; i < ii; i++) { + errbacks[i].call(null, reason); + } + }, + + then: function promiseThen(callback, errback) { if (!callback) { throw 'Requiring callback' + this.name; } @@ -301,8 +324,13 @@ var Promise = (function PromiseClosure() { if (this.isResolved) { var data = this.data; callback.call(null, data); + } else if (this.isRejected && errorback) { + var error = this.error; + errback.call(null, error); } else { this.callbacks.push(callback); + if (errback) + this.errbacks.push(errback); } } }; diff --git a/test/driver.js b/test/driver.js index 64fceee90..85d25658a 100644 --- a/test/driver.js +++ b/test/driver.js @@ -165,9 +165,14 @@ function nextPage(task, loadError) { canvas.height = pageHeight * pdfToCssUnitsCoef; clear(ctx); - // using non-attached to the document div to test + // using the text layer builder that does nothing to test // text layer creation operations - var textLayer = document.createElement('div'); + var textLayerBuilder = { + beginLayout: function nullTextLayerBuilderBeginLayout() {}, + endLayout: function nullTextLayerBuilderEndLayout() {}, + appendText: function nullTextLayerBuilderAppendText(text, fontName, + fontSize) {} + }; page.startRendering( ctx, @@ -177,7 +182,7 @@ function nextPage(task, loadError) { failureMessage = 'render : ' + error.message; snapshotCurrentPage(task, failureMessage); }, - textLayer + textLayerBuilder ); } catch (e) { failure = 'page setup : ' + e.toString(); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index fd541a06d..956980782 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -21,3 +21,4 @@ !freeculture.pdf !issue918.pdf !smaskdim.pdf +!type4psfunc.pdf diff --git a/test/pdfs/intelisa.pdf.link b/test/pdfs/intelisa.pdf.link index 3ccbd1759..25d67ef5e 100644 --- a/test/pdfs/intelisa.pdf.link +++ b/test/pdfs/intelisa.pdf.link @@ -1 +1 @@ -http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-software-developer-vol-1-manual.pdf +http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-1-manual.pdf \ No newline at end of file diff --git a/test/pdfs/issue1001.pdf.link b/test/pdfs/issue1001.pdf.link new file mode 100644 index 000000000..24e1bebc2 --- /dev/null +++ b/test/pdfs/issue1001.pdf.link @@ -0,0 +1 @@ +http://www.myhillsapartment.com/island_club/floorplans/images/links/Island_IC_brochure.pdf diff --git a/test/pdfs/issue1015.pdf.link b/test/pdfs/issue1015.pdf.link new file mode 100644 index 000000000..0878ab443 --- /dev/null +++ b/test/pdfs/issue1015.pdf.link @@ -0,0 +1 @@ +http://faculty.washington.edu/fidelr/RayaPubs/TheCaseStudyMethod.pdf diff --git a/test/pdfs/ocs.pdf.link b/test/pdfs/ocs.pdf.link new file mode 100644 index 000000000..10c2b1b9e --- /dev/null +++ b/test/pdfs/ocs.pdf.link @@ -0,0 +1 @@ +http://www.unibuc.ro/uploads_en/29535/10/Cyrillic_Alphabets-Chars.pdf diff --git a/test/pdfs/type4psfunc.pdf b/test/pdfs/type4psfunc.pdf new file mode 100755 index 000000000..e4886e918 Binary files /dev/null and b/test/pdfs/type4psfunc.pdf differ diff --git a/test/resources/firefox/prefs.js b/test/resources/firefox/prefs.js new file mode 100644 index 000000000..8d59d66bd --- /dev/null +++ b/test/resources/firefox/prefs.js @@ -0,0 +1 @@ +user_pref("browser.shell.checkDefaultBrowser", false); diff --git a/test/test_manifest.json b/test/test_manifest.json index 5b88b3136..6e3a3e52b 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -17,12 +17,13 @@ "rounds": 1, "type": "load" }, - { "id": "intelisa-load", + { "id": "intelisa-eq", "file": "pdfs/intelisa.pdf", - "md5": "f5712097d29287a97f1278839814f682", + "md5": "83032ccbfdc5a66269b1403971110abe", "link": true, + "pageLimit": 100, "rounds": 1, - "type": "load" + "type": "eq" }, { "id": "pdfspec-load", "file": "pdfs/pdf.pdf", @@ -355,6 +356,13 @@ "rounds": 1, "type": "eq" }, + { "id": "issue1001", + "file": "pdfs/issue1001.pdf", + "md5": "0f1496e80a82a923e91d9e74c55ad94e", + "rounds": 1, + "link": true, + "type": "eq" + }, { "id": "aboutstacks", "file": "pdfs/aboutstacks.pdf", "md5": "6e7c8416a293ba2d83bc8dd20c6ccf51", @@ -367,5 +375,25 @@ "md5": "de80aeca7cbf79940189fd34d59671ee", "rounds": 1, "type": "eq" + }, + { "id": "type4psfunc", + "file": "pdfs/type4psfunc.pdf", + "md5": "7e6027a02ff78577f74dccdf84e37189", + "rounds": 1, + "type": "eq" + }, + { "id": "ocs", + "file": "pdfs/ocs.pdf", + "md5": "2ade57e954ae7632749cf328daeaa7a8", + "rounds": 1, + "link": true, + "type": "load" + }, + { "id": "issue1015", + "file": "pdfs/issue1015.pdf", + "md5": "b61503d1b445742b665212866afb60e2", + "rounds": 1, + "link": true, + "type": "eq" } ] diff --git a/test/unit/Makefile b/test/unit/Makefile new file mode 100644 index 000000000..811f9155e --- /dev/null +++ b/test/unit/Makefile @@ -0,0 +1,116 @@ +# Create temporary profile directory name. +TEMP_PROFILE:=$(shell echo `pwd`)/test_reports/temp_profile + +# These are the Firefox command line arguments. +FIREFOX_ARGS:=-no-remote -profile $(TEMP_PROFILE) + +# These are the Chrome command line arguments. +CHROME_ARGS:=--user-data-dir=$(TEMP_PROFILE) --no-first-run --disable-sync + +# Unit test uses the manifest from ref test to determine which browsers will +# be used for running the unit tests. +MANIFEST:=../resources/browser_manifests/browser_manifest.json + +# This is a helper command to separate multiple browsers to their own lines +# for an easier sed operation. +SPLIT_LINES:=sed 's|,|,@|g' | tr '@' '\n' + +# This is a helper command to join multiple lines together. +JOIN_LINES:=tr -d '\n' + +# Fetch the paths to browsers that are going to be used in testing. +# For OS X the path to the binary needs to be added. +# Add the browser arguments for each browser. +# Create random profile directory for each browser. +BROWSERS_PATHS:=$(shell echo `\ + sed -n 's|.*"path":\(.*\)|\1,|p' $(MANIFEST) | \ + $(JOIN_LINES) \ +`) + +# The browser_manifest.json file has only the app directory for mac browsers. +# The absolute path to the browser binary needs to be used. +BROWSERS_PATHS_WITH_MAC_CORRECTION:=$(shell echo '$(BROWSERS_PATHS)' | \ + $(SPLIT_LINES) | \ + sed 's|\(Aurora\.app\)|\1/Contents/MacOS/firefox-bin|' | \ + sed 's|\(Firefox.*\.app\)|\1/Contents/MacOS/firefox-bin|' | \ + sed 's|\(Google Chrome\.app\)|\1/Contents/MacOS/Google Chrome|' | \ + $(JOIN_LINES) \ +) + +# Replace " with @@@@ so that echoing do not destroy the quotation marks. +QUOTATION_MARK:=\" +SUBSTITUTE_FOR_QUOTATION_MARK:=@@@@ + +# Each of the browser can have their own separate arguments. +BROWSERS_WITH_ARGUMENTS:=$(shell echo '$(BROWSERS_PATHS_WITH_MAC_CORRECTION)' | \ + $(SPLIT_LINES) | \ + sed "s|\(irefox.*\)\($(QUOTATION_MARK)\),|\1;$(FIREFOX_ARGS)\2,|" | \ + sed "s|\(hrome.*\)\($(QUOTATION_MARK)\),|\1;$(CHROME_ARGS)\2,|" | \ + $(JOIN_LINES) \ +) + +# A temporary profile directory is needed for each of the browser. In this way +# a unit test run will not disturb the main browsing session of the user. The +# $RANDOM shell variable is used to generate non-conflicting temporary +# directories. +BROWSERS_WITH_UKNOWN_RANDOM_PROFILE_PATHS:=$(shell echo '$(BROWSERS_WITH_ARGUMENTS)' | \ + $(SPLIT_LINES) | \ + sed 's|\(temp_profile\)|\1_$$RANDOM$$RANDOM|' | \ + sed "s|$(QUOTATION_MARK)|$(SUBSTITUTE_FOR_QUOTATION_MARK)|g" | \ + $(JOIN_LINES) \ +) + +# Echo the variable so that the unknown random directories become known. +# Replace @@@@ with " so that jsTestDriver will work properly. +BROWSERS:=$(shell echo "$(BROWSERS_WITH_UKNOWN_RANDOM_PROFILE_PATHS)" | \ + sed "s|$(SUBSTITUTE_FOR_QUOTATION_MARK)|$(QUOTATION_MARK)|g" \ +) + +# Get the known random directories for browsers. This information will be used +# to create the profile directories beforehand. Create also the dummy temp +# profile directory so that the mkdir command would not fail for browsers that +# do not need it. +PROFILES:=$(TEMP_PROFILE) $(shell echo '$(BROWSERS)' | \ + $(SPLIT_LINES) | \ + sed -n "s|.*\( $(TEMP_PROFILE)_[0-9]\+\).*|\1|p" | \ + $(JOIN_LINES) \ +) + +# This is the command to invoke the unit test. +PROG:=java \ +-Xms512m \ +-Xmx1024m \ +-jar ../../external/jsTestDriver/JsTestDriver-1.3.3d.jar \ +--config ./jsTestDriver.conf \ +--reset \ +--port 4224 \ +--browser $(BROWSERS) \ +--tests all \ +--testOutput ./test_reports/ + +# This default rule runs the unit tests with the constructed command. +test: + @mkdir -p $(PROFILES) + $(PROG) + @rm -rf $(PROFILES) + +# In case this Makefile needs to be debugged then this rule will provide all +# the information from intermediate steps. +debug: + @echo 'Debug browsers paths: $(BROWSERS_PATHS)' + @echo + @echo 'Debug browsers paths with mac correction: $(BROWSERS_PATHS_WITH_MAC_CORRECTION)' + @echo + @echo 'Debug browsers with arguments: $(BROWSERS_WITH_ARGUMENTS)' + @echo + @echo 'Debug browsers random profile paths: $(BROWSERS_WITH_UKNOWN_RANDOM_PROFILE_PATHS)' + @echo + @echo 'Debug browsers: $(BROWSERS)' + @echo + @echo 'Debug profiles: $(PROFILES)' + @echo + @echo 'Command to be run: $(PROG)' + @echo + +.phony:: test + diff --git a/test/unit/crypto_spec.js b/test/unit/crypto_spec.js new file mode 100644 index 000000000..0b82b5ccb --- /dev/null +++ b/test/unit/crypto_spec.js @@ -0,0 +1,187 @@ +/* -*- 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('crypto', function() { + function string2binary(s) { + var n = s.length, i; + var result = new Uint8Array(n); + for (i = 0; i < n; ++i) + result[i] = s.charCodeAt(i) % 0xFF; + return result; + } + + function hex2binary(s) { + var digits = '0123456789ABCDEF'; + s = s.toUpperCase(); + var n = s.length >> 1, i, j; + var result = new Uint8Array(n); + for (i = 0, j = 0; i < n; ++i) { + var d1 = s.charAt(j++); + var d2 = s.charAt(j++); + var value = (digits.indexOf(d1) << 4) | (digits.indexOf(d2)); + result[i] = value; + } + return result; + } + + // RFC 1321, A.5 Test suite + describe('calculateMD5', function() { + it('should pass RFC 1321 test #1', function() { + var input, result, expected; + input = string2binary(''); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('d41d8cd98f00b204e9800998ecf8427e'); + expect(result).toEqual(expected); + }); + it('should pass RFC 1321 test #2', function() { + var input, result, expected; + input = string2binary('a'); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('0cc175b9c0f1b6a831c399e269772661'); + expect(result).toEqual(expected); + }); + it('should pass RFC 1321 test #3', function() { + var input, result, expected; + input = string2binary('abc'); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('900150983cd24fb0d6963f7d28e17f72'); + expect(result).toEqual(expected); + }); + it('should pass RFC 1321 test #4', function() { + var input, result, expected; + input = string2binary('message digest'); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('f96b697d7cb7938d525a2f31aaf161d0'); + expect(result).toEqual(expected); + }); + it('should pass RFC 1321 test #5', function() { + var input, result, expected; + input = string2binary('abcdefghijklmnopqrstuvwxyz'); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('c3fcd3d76192e4007dfb496cca67e13b'); + expect(result).toEqual(expected); + }); + it('should pass RFC 1321 test #6', function() { + var input, result, expected; + input = string2binary('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv' + + 'wxyz0123456789'); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('d174ab98d277d9f5a5611c2c9f419d9f'); + expect(result).toEqual(expected); + }); + it('should pass RFC 1321 test #7', function() { + var input, result, expected; + input = string2binary('123456789012345678901234567890123456789012345678' + + '90123456789012345678901234567890'); + result = calculateMD5(input, 0, input.length); + expected = hex2binary('57edf4a22be3c955ac49da2e2107b67a'); + expect(result).toEqual(expected); + }); + }); + + // http://www.freemedialibrary.com/index.php/RC4_test_vectors are used + describe('ARCFourCipher', function() { + it('should pass test #1', function() { + var key, input, result, expected, cipher; + key = hex2binary('0123456789abcdef'); + input = hex2binary('0123456789abcdef'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('75b7878099e0c596'); + expect(result).toEqual(expected); + }); + it('should pass test #2', function() { + var key, input, result, expected, cipher; + key = hex2binary('0123456789abcdef'); + input = hex2binary('0000000000000000'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('7494c2e7104b0879'); + expect(result).toEqual(expected); + }); + it('should pass test #3', function() { + var key, input, result, expected, cipher; + key = hex2binary('0000000000000000'); + input = hex2binary('0000000000000000'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('de188941a3375d3a'); + expect(result).toEqual(expected); + }); + it('should pass test #4', function() { + var key, input, result, expected, cipher; + key = hex2binary('ef012345'); + input = hex2binary('00000000000000000000'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('d6a141a7ec3c38dfbd61'); + expect(result).toEqual(expected); + }); + it('should pass test #5', function() { + var key, input, result, expected, cipher; + key = hex2binary('0123456789abcdef'); + input = hex2binary('010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '10101010101010101010101010101010101010101010101010101010101010101010' + + '101010101010101010101'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('7595c3e6114a09780c4ad452338e1ffd9a1be9498f813d76' + + '533449b6778dcad8c78a8d2ba9ac66085d0e53d59c26c2d1c490c1ebbe0ce66d1b6b' + + '1b13b6b919b847c25a91447a95e75e4ef16779cde8bf0a95850e32af9689444fd377' + + '108f98fdcbd4e726567500990bcc7e0ca3c4aaa304a387d20f3b8fbbcd42a1bd311d' + + '7a4303dda5ab078896ae80c18b0af66dff319616eb784e495ad2ce90d7f772a81747' + + 'b65f62093b1e0db9e5ba532fafec47508323e671327df9444432cb7367cec82f5d44' + + 'c0d00b67d650a075cd4b70dedd77eb9b10231b6b5b741347396d62897421d43df9b4' + + '2e446e358e9c11a9b2184ecbef0cd8e7a877ef968f1390ec9b3d35a5585cb009290e' + + '2fcde7b5ec66d9084be44055a619d9dd7fc3166f9487f7cb272912426445998514c1' + + '5d53a18c864ce3a2b7555793988126520eacf2e3066e230c91bee4dd5304f5fd0405' + + 'b35bd99c73135d3d9bc335ee049ef69b3867bf2d7bd1eaa595d8bfc0066ff8d31509' + + 'eb0c6caa006c807a623ef84c3d33c195d23ee320c40de0558157c822d4b8c569d849' + + 'aed59d4e0fd7f379586b4b7ff684ed6a189f7486d49b9c4bad9ba24b96abf924372c' + + '8a8fffb10d55354900a77a3db5f205e1b99fcd8660863a159ad4abe40fa48934163d' + + 'dde542a6585540fd683cbfd8c00f12129a284deacc4cdefe58be7137541c047126c8' + + 'd49e2755ab181ab7e940b0c0'); + expect(result).toEqual(expected); + }); + it('should pass test #6', function() { + var key, input, result, expected, cipher; + key = hex2binary('fb029e3031323334'); + input = hex2binary('aaaa0300000008004500004e661a00008011be640a0001220af' + + 'fffff00890089003a000080a601100001000000000000204543454a4548454346434' + + '550464545494546464343414341434143414341414100002000011bd0b604'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('f69c5806bd6ce84626bcbefb9474650aad1f7909b0f64d5f' + + '58a503a258b7ed22eb0ea64930d3a056a55742fcce141d485f8aa836dea18df42c53' + + '80805ad0c61a5d6f58f41040b24b7d1a693856ed0d4398e7aee3bf0e2a2ca8f7'); + expect(result).toEqual(expected); + }); + it('should pass test #7', function() { + var key, input, result, expected, cipher; + key = hex2binary('0123456789abcdef'); + input = hex2binary('123456789abcdef0123456789abcdef0123456789abcdef0123' + + '45678'); + cipher = new ARCFourCipher(key); + result = cipher.encryptBlock(input); + expected = hex2binary('66a0949f8af7d6891f7f832ba833c00c892ebe30143ce287' + + '40011ecf'); + expect(result).toEqual(expected); + }); + }); +}); + diff --git a/test/unit/function_spec.js b/test/unit/function_spec.js new file mode 100644 index 000000000..2a1dc0b75 --- /dev/null +++ b/test/unit/function_spec.js @@ -0,0 +1,225 @@ +/* -*- 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('function', function() { + beforeEach(function() { + this.addMatchers({ + toMatchArray: function(expected) { + var actual = this.actual; + if (actual.length != expected.length) + return false; + for (var i = 0; i < expected.length; i++) { + var a = actual[i], b = expected[i]; + if (isArray(b)) { + if (a.length != b.length) + return false; + for (var j = 0; j < a.length; j++) { + var suba = a[j], subb = b[j]; + if (suba !== subb) + return false; + } + } else { + if (a !== b) + return false; + } + } + return true; + } + }); + }); + + describe('PostScriptParser', function() { + function parse(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + return parser.parse(); + } + it('parses empty programs', function() { + var output = parse('{}'); + expect(output.length).toEqual(0); + }); + it('parses positive numbers', function() { + var number = 999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative numbers', function() { + var number = -999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative floats', function() { + var number = 3.3; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses operators', function() { + var program = parse('{ sub }'); + var expectedProgram = ['sub']; + expect(program).toMatchArray(expectedProgram); + }); + it('parses if statements', function() { + var program = parse('{ { 99 } if }'); + var expectedProgram = [3, 'jz', 99]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses ifelse statements', function() { + var program = parse('{ { 99 } { 44 } ifelse }'); + var expectedProgram = [5, 'jz', 99, 6, 'j', 44]; + expect(program).toMatchArray(expectedProgram); + }); + it('handles missing brackets', function() { + expect(function() { parse('{'); }).toThrow( + new Error('Unexpected symbol: found undefined expected 1.')); + }); + }); + + describe('PostScriptEvaluator', function() { + function evaluate(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + var code = parser.parse(); + var evaluator = new PostScriptEvaluator(code); + var output = evaluator.execute(); + return output; + } + + it('pushes stack', function() { + var stack = evaluate('{ 99 }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with true', function() { + var stack = evaluate('{ 1 {99} if }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with false', function() { + var stack = evaluate('{ 0 {99} if }'); + var expectedStack = []; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with true', function() { + var stack = evaluate('{ 1 {99} {77} ifelse }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with false', function() { + var stack = evaluate('{ 0 {99} {77} ifelse }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles nested if', function() { + var stack = evaluate('{ 1 {1 {77} if} if }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + + it('abs', function() { + var stack = evaluate('{ -2 abs }'); + var expectedStack = [2]; + expect(stack).toMatchArray(expectedStack); + }); + it('adds', function() { + var stack = evaluate('{ 1 2 add }'); + var expectedStack = [3]; + expect(stack).toMatchArray(expectedStack); + }); + it('boolean ands', function() { + var stack = evaluate('{ true false and }'); + var expectedStack = [false]; + expect(stack).toMatchArray(expectedStack); + }); + it('bitwise ands', function() { + var stack = evaluate('{ 254 1 and }'); + var expectedStack = [254 & 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO atan + // TODO bitshift + // TODO ceiling + // TODO copy + // TODO cos + it('converts to int', function() { + var stack = evaluate('{ 9.9 cvi }'); + var expectedStack = [9]; + expect(stack).toMatchArray(expectedStack); + }); + it('converts negatives to int', function() { + var stack = evaluate('{ -9.9 cvi }'); + var expectedStack = [-9]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO cvr + // TODO div + it('duplicates', function() { + var stack = evaluate('{ 99 dup }'); + var expectedStack = [99, 99]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO eq + it('exchanges', function() { + var stack = evaluate('{ 44 99 exch }'); + var expectedStack = [99, 44]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO exp + // TODO false + // TODO floor + // TODO ge + // TODO gt + it('divides to integer', function() { + var stack = evaluate('{ 2 3 idiv }'); + var expectedStack = [0]; + expect(stack).toMatchArray(expectedStack); + }); + it('divides to negative integer', function() { + var stack = evaluate('{ -2 3 idiv }'); + var expectedStack = [0]; + expect(stack).toMatchArray(expectedStack); + }); + it('duplicates index', function() { + var stack = evaluate('{ 4 3 2 1 2 index }'); + var expectedStack = [4, 3, 2, 1, 3]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO le + // TODO ln + // TODO log + // TODO lt + // TODO mod + // TODO mul + // TODO ne + // TODO neg + // TODO not + // TODO or + it('pops stack', function() { + var stack = evaluate('{ 1 2 pop }'); + var expectedStack = [1]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack right', function() { + var stack = evaluate('{ 1 3 2 2 4 1 roll }'); + var expectedStack = [2, 1, 3, 2]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack left', function() { + var stack = evaluate('{ 1 3 2 2 4 -1 roll }'); + var expectedStack = [3, 2, 2, 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO round + // TODO sin + // TODO sqrt + // TODO sub + // TODO true + // TODO truncate + // TODO xor + }); +}); + diff --git a/test/unit/jsTestDriver.conf b/test/unit/jsTestDriver.conf new file mode 100644 index 000000000..4baf3c026 --- /dev/null +++ b/test/unit/jsTestDriver.conf @@ -0,0 +1,30 @@ +server: http://localhost:4224 + +load: + - ../../external/jasmine/jasmine.js + - ../../external/jasmineAdapter/JasmineAdapter.js + - ../../src/obj.js + - ../../src/core.js + - ../../src/util.js + - ../../src/canvas.js + - ../../src/obj.js + - ../../src/function.js + - ../../src/charsets.js + - ../../src/cidmaps.js + - ../../src/colorspace.js + - ../../src/crypto.js + - ../../src/evaluator.js + - ../../src/fonts.js + - ../../src/glyphlist.js + - ../../src/image.js + - ../../src/metrics.js + - ../../src/parser.js + - ../../src/pattern.js + - ../../src/stream.js + - ../../src/worker.js + - ../../external/jpgjs/jpg.js + - ../unit/obj_spec.js + - ../unit/function_spec.js + - ../unit/crypto_spec.js + - ../unit/stream_spec.js + diff --git a/test/unit/obj_spec.js b/test/unit/obj_spec.js index 4f1a0b57a..7049e769f 100644 --- a/test/unit/obj_spec.js +++ b/test/unit/obj_spec.js @@ -3,14 +3,147 @@ 'use strict'; -describe("obj", function() { +describe('obj', function() { - describe("Name", function() { - it("should retain the given name", function() { - var givenName = "Font"; + describe('Name', function() { + it('should retain the given name', function() { + var givenName = 'Font'; var name = new Name(givenName); expect(name.name).toEqual(givenName); }); }); + + describe('Cmd', function() { + it('should retain the given cmd name', function() { + var givenCmd = 'BT'; + var cmd = new Cmd(givenCmd); + expect(cmd.cmd).toEqual(givenCmd); + }); + + it('should create only one object for a command and cache it', function() { + var firstBT = Cmd.get('BT'); + var secondBT = Cmd.get('BT'); + var firstET = Cmd.get('ET'); + var secondET = Cmd.get('ET'); + expect(firstBT).toBe(secondBT); + expect(firstET).toBe(secondET); + expect(firstBT).not.toBe(firstET); + }); + }); + + describe('Dict', function() { + var checkInvalidHasValues = function(dict) { + expect(dict.has()).toBeFalsy(); + expect(dict.has('Prev')).toBeFalsy(); + }; + + var checkInvalidKeyValues = function(dict) { + expect(dict.get()).toBeUndefined(); + expect(dict.get('Prev')).toBeUndefined(); + expect(dict.get('Decode', 'D')).toBeUndefined(); + + // Note that the getter with three arguments breaks the pattern here. + expect(dict.get('FontFile', 'FontFile2', 'FontFile3')).toBeNull(); + }; + + var emptyDict, dictWithSizeKey, dictWithManyKeys; + var storedSize = 42; + var testFontFile = 'file1'; + var testFontFile2 = 'file2'; + var testFontFile3 = 'file3'; + + beforeEach(function() { + emptyDict = new Dict(); + + dictWithSizeKey = new Dict(); + dictWithSizeKey.set('Size', storedSize); + + dictWithManyKeys = new Dict(); + dictWithManyKeys.set('FontFile', testFontFile); + dictWithManyKeys.set('FontFile2', testFontFile2); + dictWithManyKeys.set('FontFile3', testFontFile3); + }); + + it('should return invalid values for unknown keys', function() { + checkInvalidHasValues(emptyDict); + checkInvalidKeyValues(emptyDict); + }); + + it('should return correct value for stored Size key', function() { + expect(dictWithSizeKey.has('Size')).toBeTruthy(); + + expect(dictWithSizeKey.get('Size')).toEqual(storedSize); + expect(dictWithSizeKey.get('Prev', 'Size')).toEqual(storedSize); + expect(dictWithSizeKey.get('Prev', 'Root', 'Size')).toEqual(storedSize); + }); + + it('should return invalid values for unknown keys when Size key is stored', + function() { + checkInvalidHasValues(dictWithSizeKey); + checkInvalidKeyValues(dictWithSizeKey); + }); + + it('should return correct value for stored Size key with undefined value', + function() { + var dict = new Dict(); + dict.set('Size'); + + expect(dict.has('Size')).toBeTruthy(); + + checkInvalidKeyValues(dict); + }); + + it('should return correct values for multiple stored keys', function() { + expect(dictWithManyKeys.has('FontFile')).toBeTruthy(); + expect(dictWithManyKeys.has('FontFile2')).toBeTruthy(); + expect(dictWithManyKeys.has('FontFile3')).toBeTruthy(); + + expect(dictWithManyKeys.get('FontFile3')).toEqual(testFontFile3); + expect(dictWithManyKeys.get('FontFile2', 'FontFile3')) + .toEqual(testFontFile2); + expect(dictWithManyKeys.get('FontFile', 'FontFile2', 'FontFile3')) + .toEqual(testFontFile); + }); + + it('should callback for each stored key', function() { + var callbackSpy = jasmine.createSpy('spy on callback in dictionary'); + + dictWithManyKeys.forEach(callbackSpy); + + expect(callbackSpy).wasCalled(); + expect(callbackSpy.argsForCall[0]).toEqual(['FontFile', testFontFile]); + expect(callbackSpy.argsForCall[1]).toEqual(['FontFile2', testFontFile2]); + expect(callbackSpy.argsForCall[2]).toEqual(['FontFile3', testFontFile3]); + expect(callbackSpy.callCount).toEqual(3); + }); + }); + + describe('Ref', function() { + it('should retain the stored values', function() { + var storedNum = 4; + var storedGen = 2; + var ref = new Ref(storedNum, storedGen); + expect(ref.num).toEqual(storedNum); + expect(ref.gen).toEqual(storedGen); + }); + }); + + describe('RefSet', function() { + it('should have a stored value', function() { + var ref = new Ref(4, 2); + var refset = new RefSet(); + refset.put(ref); + expect(refset.has(ref)).toBeTruthy(); + }); + it('should not have an unknown value', function() { + var ref = new Ref(4, 2); + var refset = new RefSet(); + expect(refset.has(ref)).toBeFalsy(); + + refset.put(ref); + var anotherRef = new Ref(2, 4); + expect(refset.has(anotherRef)).toBeFalsy(); + }); + }); }); diff --git a/test/unit/stream_spec.js b/test/unit/stream_spec.js new file mode 100644 index 000000000..1dab8e42c --- /dev/null +++ b/test/unit/stream_spec.js @@ -0,0 +1,25 @@ +/* -*- 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('stream', function() { + + describe('PredictorStream', function() { + it('should decode simple predictor data', function() { + var dict = new Dict(); + dict.set('Predictor', 12); + dict.set('Colors', 1); + dict.set('BitsPerComponent', 8); + dict.set('Columns', 2); + + var input = new Stream(new Uint8Array([2, 100, 3, 2, 1, 255, 2, 1, 255]), + 0, 9, dict); + var predictor = new PredictorStream(input, dict); + var result = predictor.getBytes(6); + + expect(result).toEqual(new Uint8Array([100, 3, 101, 2, 102, 1])); + }); + }); +}); + diff --git a/test/unit/test_reports/.gitignore b/test/unit/test_reports/.gitignore new file mode 100644 index 000000000..7193eb3d2 --- /dev/null +++ b/test/unit/test_reports/.gitignore @@ -0,0 +1,3 @@ +TEST* +temp* + diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html deleted file mode 100644 index 1fc28ef83..000000000 --- a/test/unit/unit_test.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - pdf.js unit test - - - - - - - - - - - - - - - - - - - diff --git a/web/compatibility.js b/web/compatibility.js index e4e2f2440..26405ad8f 100644 --- a/web/compatibility.js +++ b/web/compatibility.js @@ -5,11 +5,16 @@ // Checking if the typed arrays are supported (function checkTypedArrayCompatibility() { - if (typeof Uint8Array !== 'undefined') + if (typeof Uint8Array !== 'undefined') { + // some mobile version might not support Float64Array + if (typeof Float64Array === 'undefined') + window.Float64Array = Float32Array; + return; + } function subarray(start, end) { - return this.slice(start, end); + return new TypedArray(this.slice(start, end)); } function setArrayOffset(array, offset) { @@ -46,6 +51,8 @@ window.Uint32Array = TypedArray; window.Int32Array = TypedArray; window.Uint16Array = TypedArray; + window.Float32Array = TypedArray; + window.Float64Array = TypedArray; })(); // Object.create() ? diff --git a/web/images/check.svg b/web/images/check.svg new file mode 100644 index 000000000..e0e1590a9 --- /dev/null +++ b/web/images/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/images/comment.svg b/web/images/comment.svg new file mode 100644 index 000000000..84feef1c8 --- /dev/null +++ b/web/images/comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/viewer.css b/web/viewer.css index a1ef92810..e355f7fc2 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -15,6 +15,7 @@ body { /* === Toolbar === */ #controls { background-color: #eee; + background: -o-linear-gradient(bottom,#eee 0%,#fff 100%); background: -moz-linear-gradient(center bottom, #eee 0%, #fff 100%); background: -webkit-gradient(linear, left bottom, left top, color-stop(0.0, #ddd), color-stop(1.0, #fff)); border-bottom: 1px solid #666; @@ -82,6 +83,7 @@ span#info { bottom: 18px; left: -290px; transition: left 0.25s ease-in-out 1s; + -o-transition: left 0.25s ease-in-out 1s; -moz-transition: left 0.25s ease-in-out 1s; -webkit-transition: left 0.25s ease-in-out 1s; z-index: 1; @@ -90,6 +92,7 @@ span#info { #sidebar:hover { left: 0px; transition: left 0.25s ease-in-out 0s; + -o-transition: left 0.25s ease-in-out 0s; -moz-transition: left 0.25s ease-in-out 0s; -webkit-transition: left 0.25s ease-in-out 0s; } @@ -247,6 +250,35 @@ canvas { line-height:1.3; } +.annotComment > div { + position: absolute; +} + +.annotComment > img { + position: absolute; +} + +.annotComment > img:hover { + cursor: pointer; + opacity: 0.7; +} + +.annotComment > div { + padding: 0.2em; + max-width: 20em; + background-color: #F1E47B; + box-shadow: 0px 2px 10px #333; + -moz-box-shadow: 0px 2px 10px #333; + -webkit-box-shadow: 0px 2px 10px #333; +} + +.annotComment > div > h1 { + font-weight: normal; + font-size: 1.2em; + border-bottom: 1px solid #000000; + margin: 0px; +} + /* TODO: file FF bug to support ::-moz-selection:window-inactive so we can override the opaque grey background when the window is inactive; see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */ @@ -298,7 +330,7 @@ canvas { color: black; padding: 3px; margin: 3px; - white-space: pre; + width: 98%; } .clearBoth { diff --git a/web/viewer.html b/web/viewer.html index 53ca2a247..40e99004f 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -67,10 +67,11 @@ - + +
@@ -113,7 +114,7 @@
- +