diff --git a/images/buttons.png b/images/buttons.png index 682212660..3357b47d6 100644 Binary files a/images/buttons.png and b/images/buttons.png differ diff --git a/images/combobox.png b/images/combobox.png deleted file mode 100644 index f1527d6a2..000000000 Binary files a/images/combobox.png and /dev/null differ diff --git a/images/source/ComboBox.psd.zip b/images/source/ComboBox.psd.zip deleted file mode 100644 index 232604c75..000000000 Binary files a/images/source/ComboBox.psd.zip and /dev/null differ diff --git a/images/source/FileButton.psd.zip b/images/source/FileButton.psd.zip new file mode 100644 index 000000000..1f2b51cee Binary files /dev/null and b/images/source/FileButton.psd.zip differ diff --git a/multi-page-viewer.css b/multi-page-viewer.css index c96567465..7f4701022 100644 --- a/multi-page-viewer.css +++ b/multi-page-viewer.css @@ -27,7 +27,30 @@ span { .control > input { float: left; + border: 1px solid #4d4d4d; + height: 20px; + padding: 0px; margin: 0px 2px 0px 0px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); +} + +.control > select { + float: left; + border: 1px solid #4d4d4d; + height: 22px; + padding: 2px 0px 0px; + margin: 0px 0px 1px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); } .control > span { @@ -61,7 +84,7 @@ span { background-color: #eee; border-bottom: 1px solid #666; padding: 4px 0px 0px 8px; - position:fixed; + position: fixed; left: 0px; top: 0px; height: 40px; @@ -79,7 +102,7 @@ span { #previousPageButton { background: url('images/buttons.png') no-repeat 0px -23px; - cursor: pointer; + cursor: default; display: inline-block; float: left; margin: 0px; @@ -97,7 +120,7 @@ span { #nextPageButton { background: url('images/buttons.png') no-repeat -28px -23px; - cursor: pointer; + cursor: default; display: inline-block; float: left; margin: 0px; @@ -113,71 +136,61 @@ span { background: url('images/buttons.png') no-repeat -28px 0px; } -#scaleComboBoxInput { - background: url('images/combobox.png') no-repeat 0px -23px; +#openFileButton { + background: url('images/buttons.png') no-repeat -56px -23px; + cursor: default; display: inline-block; float: left; - margin: 0px; - width: 35px; + margin: 0px 0px 0px 3px; + width: 29px; height: 23px; } -#scaleComboBoxInput input { - background: none; - border: 0px; - margin: 3px 2px 0px; - width: 31px; +#openFileButton.down { + background: url('images/buttons.png') no-repeat -56px -46px; } -#scaleComboBoxButton { - background: url('images/combobox.png') no-repeat -41px -23px; - cursor: pointer; - display: inline-block; - float: left; - margin: 0px; - width: 21px; - height: 23px; +#openFileButton.disabled { + background: url('images/buttons.png') no-repeat -56px 0px; } -#scaleComboBoxButton.down { - background: url('images/combobox.png') no-repeat -41px -46px; -} - -#scaleComboBoxButton.disabled { - background: url('images/combobox.png') no-repeat -41px 0px; -} - -#scaleComboBoxList { - background-color: #fff; - border: 1px solid #666; - clear: both; - position: relative; +#fileInput { display: none; - top: -20px; - width: 48px; } -#scaleComboBoxList > ul { - list-style: none; - padding: 0px; - margin: 0px; -} - -#scaleComboBoxList > ul > li { - display: inline-block; - cursor: pointer; - width: 100%; -} - -#scaleComboBoxList > ul > li:hover { - background-color: #09f; - color: #fff; -} - -#pageNumber, #scale { +#pageNumber { text-align: right; } +#sidebar { + background-color: rgba(0, 0, 0, 0.8); + position: fixed; + width: 150px; + top: 62px; + bottom: 18px; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + -moz-border-radius-topright: 8px; + -moz-border-radius-bottomright: 8px; + -webkit-border-top-right-radius: 8px; + -webkit-border-bottom-right-radius: 8px; +} + +#sidebarScrollView { + position: absolute; + overflow: hidden; + overflow-y: auto; + top: 40px; + right: 10px; + bottom: 10px; + left: 10px; +} + +#sidebarContentView { + height: auto; + width: 100px; +} + #viewer { margin: 44px 0px 0px; padding: 8px 0px; diff --git a/multi-page-viewer.html b/multi-page-viewer.html index 692cfb1c4..ffbdfe707 100644 --- a/multi-page-viewer.html +++ b/multi-page-viewer.html @@ -2,7 +2,8 @@ pdf.js Multi-Page Viewer - + + @@ -11,7 +12,8 @@
- + + Previous/Next @@ -21,20 +23,29 @@ Page Number - + Zoom -
-
    -
  • 50%
  • -
  • 75%
  • -
  • 100%
  • -
  • 125%
  • -
  • 150%
  • -
  • 200%
  • -
-
+
+ + + + Open File
+
diff --git a/multi-page-viewer.js b/multi-page-viewer.js index 6cb46a08a..baad7809e 100644 --- a/multi-page-viewer.js +++ b/multi-page-viewer.js @@ -11,7 +11,8 @@ var PDFViewer = { previousPageButton: null, nextPageButton: null, pageNumberInput: null, - scaleInput: null, + scaleSelect: null, + fileInput: null, willJumpToPage: false, @@ -66,92 +67,103 @@ var PDFViewer = { removePage: function(num) { var div = document.getElementById('pageContainer' + num); - if (div && div.hasChildNodes()) { - while (div.childNodes.length > 0) { + if (div) { + while (div.hasChildNodes()) { div.removeChild(div.firstChild); } } }, drawPage: function(num) { - if (PDFViewer.pdf) { - var page = PDFViewer.pdf.getPage(num); - var div = document.getElementById('pageContainer' + num); - - if (div && !div.hasChildNodes()) { - var canvas = document.createElement('canvas'); - canvas.id = 'page' + num; - canvas.mozOpaque = true; - - // Canvas dimensions must be specified in CSS pixels. CSS pixels - // are always 96 dpi. These dimensions are 8.5in x 11in at 96dpi. - canvas.width = PDFViewer.pageWidth(); - canvas.height = PDFViewer.pageHeight(); - - var ctx = canvas.getContext('2d'); - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - - var gfx = new CanvasGraphics(ctx); - var fonts = []; + if (!PDFViewer.pdf) { + return; + } - // page.compile will collect all fonts for us, once we have loaded them - // we can trigger the actual page rendering with page.display - page.compile(gfx, fonts); - - var fontsReady = true; - - // Inspect fonts and translate the missing one - var fontCount = fonts.length; - - for (var i = 0; i < fontCount; i++) { - var font = fonts[i]; - - if (Fonts[font.name]) { - fontsReady = fontsReady && !Fonts[font.name].loading; - continue; - } + var div = document.getElementById('pageContainer' + num); + var canvas = document.createElement('canvas'); + + if (div && !div.hasChildNodes()) { + div.appendChild(canvas); + + var page = PDFViewer.pdf.getPage(num); - new Font(font.name, font.file, font.properties); - - fontsReady = false; + canvas.id = 'page' + num; + canvas.mozOpaque = true; + + // Canvas dimensions must be specified in CSS pixels. CSS pixels + // are always 96 dpi. These dimensions are 8.5in x 11in at 96dpi. + canvas.width = PDFViewer.pageWidth(); + canvas.height = PDFViewer.pageHeight(); + + var ctx = canvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + + var gfx = new CanvasGraphics(ctx); + var fonts = []; + + // page.compile will collect all fonts for us, once we have loaded them + // we can trigger the actual page rendering with page.display + page.compile(gfx, fonts); + + var areFontsReady = true; + + // Inspect fonts and translate the missing one + var fontCount = fonts.length; + + for (var i = 0; i < fontCount; i++) { + var font = fonts[i]; + + if (Fonts[font.name]) { + areFontsReady = areFontsReady && !Fonts[font.name].loading; + continue; } - var pageInterval; - var delayLoadFont = function() { - for (var i = 0; i < fontCount; i++) { - if (Fonts[font.name].loading) { - return; - } - } - - clearInterval(pageInterval); - - PDFViewer.drawPage(num); - } - - if (!fontsReady) { - pageInterval = setInterval(delayLoadFont, 10); - return; - } - - page.display(gfx); - div.appendChild(canvas); + new Font(font.name, font.file, font.properties); + + areFontsReady = false; } + + var pageInterval; + + var delayLoadFont = function() { + for (var i = 0; i < fontCount; i++) { + if (Fonts[font.name].loading) { + return; + } + } + + clearInterval(pageInterval); + + while (div.hasChildNodes()) { + div.removeChild(div.firstChild); + } + + PDFViewer.drawPage(num); + } + + if (!areFontsReady) { + pageInterval = setInterval(delayLoadFont, 10); + return; + } + + page.display(gfx); } }, changeScale: function(num) { - while (PDFViewer.element.childNodes.length > 0) { + while (PDFViewer.element.hasChildNodes()) { PDFViewer.element.removeChild(PDFViewer.element.firstChild); } PDFViewer.scale = num / 100; + var i; + if (PDFViewer.pdf) { - for (var i = 1; i <= PDFViewer.numberOfPages; i++) { + for (i = 1; i <= PDFViewer.numberOfPages; i++) { PDFViewer.createPage(i); } @@ -160,7 +172,21 @@ var PDFViewer = { } } - PDFViewer.scaleInput.value = Math.floor(PDFViewer.scale * 100) + '%'; + for (i = 0; i < PDFViewer.scaleSelect.childNodes; i++) { + var option = PDFViewer.scaleSelect.childNodes[i]; + + if (option.value == num) { + if (!option.selected) { + option.selected = 'selected'; + } + } else { + if (option.selected) { + option.removeAttribute('selected'); + } + } + } + + PDFViewer.scaleSelect.value = Math.floor(PDFViewer.scale * 100) + '%'; }, goToPage: function(num) { @@ -190,7 +216,7 @@ var PDFViewer = { } }, - open: function(url) { + openURL: function(url) { PDFViewer.url = url; document.title = url; @@ -206,21 +232,35 @@ var PDFViewer = { req.responseArrayBuffer || req.response; - PDFViewer.pdf = new PDFDoc(new Stream(data)); - PDFViewer.numberOfPages = PDFViewer.pdf.numPages; - document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); - - for (var i = 1; i <= PDFViewer.numberOfPages; i++) { - PDFViewer.createPage(i); - } - - if (PDFViewer.numberOfPages > 0) { - PDFViewer.drawPage(1); - } + PDFViewer.readPDF(data); } }; req.send(null); + }, + + readPDF: function(data) { + while (PDFViewer.element.hasChildNodes()) { + PDFViewer.element.removeChild(PDFViewer.element.firstChild); + } + + PDFViewer.pdf = new PDFDoc(new Stream(data)); + PDFViewer.numberOfPages = PDFViewer.pdf.numPages; + document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); + + for (var i = 1; i <= PDFViewer.numberOfPages; i++) { + PDFViewer.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + document.location.hash = 1; + } + + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? + 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? + 'disabled' : ''; } }; @@ -320,41 +360,67 @@ window.onload = function() { this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; }; - PDFViewer.scaleInput = document.getElementById('scale'); - PDFViewer.scaleInput.buttonElement = document.getElementById('scaleComboBoxButton'); - PDFViewer.scaleInput.buttonElement.listElement = document.getElementById('scaleComboBoxList'); - PDFViewer.scaleInput.onchange = function(evt) { + PDFViewer.scaleSelect = document.getElementById('scaleSelect'); + PDFViewer.scaleSelect.onchange = function(evt) { PDFViewer.changeScale(parseInt(this.value)); }; - - PDFViewer.scaleInput.buttonElement.onclick = function(evt) { - this.listElement.style.display = (this.listElement.style.display === 'block') ? 'none' : 'block'; - }; - PDFViewer.scaleInput.buttonElement.onmousedown = function(evt) { - if (this.className.indexOf('disabled') === -1) { - this.className = 'down'; - } - }; - PDFViewer.scaleInput.buttonElement.onmouseup = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - PDFViewer.scaleInput.buttonElement.onmouseout = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - var listItems = PDFViewer.scaleInput.buttonElement.listElement.getElementsByTagName('LI'); - - for (var i = 0; i < listItems.length; i++) { - var listItem = listItems[i]; - listItem.onclick = function(evt) { - PDFViewer.changeScale(parseInt(this.innerHTML)); - PDFViewer.scaleInput.buttonElement.listElement.style.display = 'none'; + if (window.File && window.FileReader && window.FileList && window.Blob) { + var openFileButton = document.getElementById('openFileButton'); + openFileButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.fileInput.click(); + } }; + openFileButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + openFileButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + openFileButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.fileInput = document.getElementById('fileInput'); + PDFViewer.fileInput.onchange = function(evt) { + var files = evt.target.files; + + if (files.length > 0) { + var file = files[0]; + var fileReader = new FileReader(); + + document.title = file.name; + + // Read the local file into a Uint8Array. + fileReader.onload = function(evt) { + var data = evt.target.result; + var buffer = new ArrayBuffer(data.length); + var uint8Array = new Uint8Array(buffer); + + for (var i = 0; i < data.length; i++) { + uint8Array[i] = data.charCodeAt(i); + } + + PDFViewer.readPDF(uint8Array); + }; + + // Read as a binary string since "readAsArrayBuffer" is not yet + // implemented in Firefox. + fileReader.readAsBinaryString(file); + } + }; + PDFViewer.fileInput.value = null; + } else { + document.getElementById('fileWrapper').style.display = 'none'; } PDFViewer.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber; - PDFViewer.scale = parseInt(PDFViewer.scaleInput.value) / 100 || 1.0; - PDFViewer.open(PDFViewer.queryParams.file || PDFViewer.url); + PDFViewer.scale = parseInt(PDFViewer.scaleSelect.value) / 100 || 1.0; + + PDFViewer.openURL(PDFViewer.queryParams.file || PDFViewer.url); window.onscroll = function(evt) { var lastPagesDrawn = PDFViewer.lastPagesDrawn; diff --git a/pdf.js b/pdf.js index 23dd5bee9..96042d6c9 100644 --- a/pdf.js +++ b/pdf.js @@ -61,49 +61,50 @@ var Stream = (function() { this.bytes = Uint8Array(arrayBuffer); this.start = start || 0; this.pos = this.start; - this.end = (start + length) || this.bytes.byteLength; + this.end = (start + length) || this.bytes.length; this.dict = dict; } + // required methods for a stream. if a particular stream does not + // implement these, an error should be thrown constructor.prototype = { get length() { return this.end - this.start; }, getByte: function() { - var bytes = this.bytes; if (this.pos >= this.end) - return -1; - return bytes[this.pos++]; + return; + return this.bytes[this.pos++]; }, // returns subarray of original buffer // should only be read getBytes: function(length) { var bytes = this.bytes; var pos = this.pos; + var strEnd = this.end; + + if (!length) + return bytes.subarray(pos, strEnd); var end = pos + length; - var strEnd = this.end; - if (!end || end > strEnd) + if (end > strEnd) end = strEnd; this.pos = end; return bytes.subarray(pos, end); }, lookChar: function() { - var bytes = this.bytes; if (this.pos >= this.end) return; - return String.fromCharCode(bytes[this.pos]); + return String.fromCharCode(this.bytes[this.pos]); }, getChar: function() { - var ch = this.lookChar(); - if (!ch) - return ch; - this.pos++; - return ch; + if (this.pos >= this.end) + return; + return String.fromCharCode(this.bytes[this.pos++]); }, skip: function(n) { - if (!n && !IsNum(n)) + if (!n) n = 1; this.pos += n; }, @@ -135,6 +136,91 @@ var StringStream = (function() { return constructor; })(); +// super class for the decoding streams +var DecodeStream = (function() { + function constructor() { + this.pos = 0; + this.bufferLength = 0; + this.eof = false; + this.buffer = null; + } + + constructor.prototype = { + ensureBuffer: function(requested) { + var buffer = this.buffer; + var current = buffer ? buffer.byteLength : 0; + if (requested < current) + return buffer; + var size = 512; + while (size < requested) + size <<= 1; + var buffer2 = Uint8Array(size); + for (var i = 0; i < current; ++i) + buffer2[i] = buffer[i]; + return this.buffer = buffer2; + }, + getByte: function() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return; + this.readBlock(); + } + return this.buffer[this.pos++]; + }, + getBytes: function(length) { + var pos = this.pos; + + if (length) { + this.ensureBuffer(pos + length); + var end = pos + length; + + while (!this.eof && this.bufferLength < end) + this.readBlock(); + + var bufEnd = this.bufferLength; + if (end > bufEnd) + end = bufEnd; + } else { + while (!this.eof) + this.readBlock(); + + var end = this.bufferLength; + } + + this.pos = end; + return this.buffer.subarray(pos, end) + }, + lookChar: function() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return; + this.readBlock(); + } + return String.fromCharCode(this.buffer[this.pos]); + }, + getChar: function() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return; + this.readBlock(); + } + return String.fromCharCode(this.buffer[this.pos++]); + }, + skip: function(n) { + if (!n) + n = 1; + this.pos += n; + } + }; + + return constructor; +})(); + + + var FlateStream = (function() { const codeLenCodeMap = Uint32Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 @@ -259,269 +345,393 @@ var FlateStream = (function() { this.bytes = bytes; this.bytesPos = bytesPos; - this.eof = false; + this.codeSize = 0; this.codeBuf = 0; - - this.pos = 0; - this.bufferLength = 0; + + DecodeStream.call(this); } - constructor.prototype = { - getBits: function(bits) { - var codeSize = this.codeSize; - var codeBuf = this.codeBuf; - var bytes = this.bytes; - var bytesPos = this.bytesPos; + constructor.prototype = Object.create(DecodeStream.prototype); - var b; - while (codeSize < bits) { - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad encoding in flate stream"); - codeBuf |= b << codeSize; - codeSize += 8; - } - b = codeBuf & ((1 << bits) - 1); - this.codeBuf = codeBuf >> bits; - this.codeSize = codeSize -= bits; - this.bytesPos = bytesPos; - return b; - }, - getCode: function(table) { - var codes = table[0]; - var maxLen = table[1]; - var codeSize = this.codeSize; - var codeBuf = this.codeBuf; - var bytes = this.bytes; - var bytesPos = this.bytesPos; + constructor.prototype.getBits = function(bits) { + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + var bytes = this.bytes; + var bytesPos = this.bytesPos; - while (codeSize < maxLen) { - var b; - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad encoding in flate stream"); - codeBuf |= (b << codeSize); - codeSize += 8; - } - var code = codes[codeBuf & ((1 << maxLen) - 1)]; - var codeLen = code >> 16; - var codeVal = code & 0xffff; - if (codeSize == 0|| codeSize < codeLen || codeLen == 0) + var b; + while (codeSize < bits) { + if (typeof (b = bytes[bytesPos++]) == "undefined") error("Bad encoding in flate stream"); - this.codeBuf = (codeBuf >> codeLen); - this.codeSize = (codeSize - codeLen); - this.bytesPos = bytesPos; - return codeVal; - }, - ensureBuffer: function(requested) { - var buffer = this.buffer; - var current = buffer ? buffer.byteLength : 0; - if (requested < current) - return buffer; - var size = 512; - while (size < requested) - size <<= 1; - var buffer2 = Uint8Array(size); - for (var i = 0; i < current; ++i) - buffer2[i] = buffer[i]; - return this.buffer = buffer2; - }, - getByte: function() { - var pos = this.pos; - while (this.bufferLength <= pos) { - if (this.eof) - return; - this.readBlock(); - } - return this.buffer[this.pos++]; - }, - getBytes: function(length) { - var pos = this.pos; - - while (!this.eof && this.bufferLength < pos + length) - this.readBlock(); - - var end = pos + length; - var bufEnd = this.bufferLength; - - if (end > bufEnd) - end = bufEnd; - - this.pos = end; - return this.buffer.subarray(pos, end) - }, - lookChar: function() { - var pos = this.pos; - while (this.bufferLength <= pos) { - if (this.eof) - return; - this.readBlock(); - } - return String.fromCharCode(this.buffer[pos]); - }, - getChar: function() { - var ch = this.lookChar(); - // shouldnt matter what the position is if we get past the eof - // so no need to check if ch is undefined - this.pos++; - return ch; - }, - skip: function(n) { - if (!n) - n = 1; - this.pos += n; - }, - generateHuffmanTable: function(lengths) { - var n = lengths.length; - - // find max code length - var maxLen = 0; - for (var i = 0; i < n; ++i) { - if (lengths[i] > maxLen) - maxLen = lengths[i]; - } - - // build the table - var size = 1 << maxLen; - var codes = Uint32Array(size); - for (var len = 1, code = 0, skip = 2; - len <= maxLen; - ++len, code <<= 1, skip <<= 1) { - for (var val = 0; val < n; ++val) { - if (lengths[val] == len) { - // bit-reverse the code - var code2 = 0; - var t = code; - for (var i = 0; i < len; ++i) { - code2 = (code2 << 1) | (t & 1); - t >>= 1; - } - - // fill the table entries - for (var i = code2; i < size; i += skip) - codes[i] = (len << 16) | val; - - ++code; - } - } - } - - return [codes, maxLen]; - }, - readBlock: function() { - function repeat(stream, array, len, offset, what) { - var repeat = stream.getBits(len) + offset; - while (repeat-- > 0) - array[i++] = what; - } - - var bytes = this.bytes; - var bytesPos = this.bytesPos; - - // read block header - var hdr = this.getBits(3); - if (hdr & 1) - this.eof = true; - hdr >>= 1; + codeBuf |= b << codeSize; + codeSize += 8; + } + b = codeBuf & ((1 << bits) - 1); + this.codeBuf = codeBuf >> bits; + this.codeSize = codeSize -= bits; + this.bytesPos = bytesPos; + return b; + }; + constructor.prototype.getCode = function(table) { + var codes = table[0]; + var maxLen = table[1]; + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + var bytes = this.bytes; + var bytesPos = this.bytesPos; + while (codeSize < maxLen) { var b; - if (hdr == 0) { // uncompressed block - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - var blockLen = b; - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - blockLen |= (b << 8); - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - var check = b; - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - check |= (b << 8); - if (check != (~this.blockLen & 0xffff)) - error("Bad uncompressed block length in flate stream"); - var bufferLength = this.bufferLength; - var buffer = this.ensureBuffer(bufferLength + blockLen); - this.bufferLength = bufferLength + blockLen; - for (var n = bufferLength; n < blockLen; ++n) { - if (typeof (b = bytes[bytesPos++]) == "undefined") { - this.eof = true; - break; + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad encoding in flate stream"); + codeBuf |= (b << codeSize); + codeSize += 8; + } + var code = codes[codeBuf & ((1 << maxLen) - 1)]; + var codeLen = code >> 16; + var codeVal = code & 0xffff; + if (codeSize == 0|| codeSize < codeLen || codeLen == 0) + error("Bad encoding in flate stream"); + this.codeBuf = (codeBuf >> codeLen); + this.codeSize = (codeSize - codeLen); + this.bytesPos = bytesPos; + return codeVal; + }; + constructor.prototype.generateHuffmanTable = function(lengths) { + var n = lengths.length; + + // find max code length + var maxLen = 0; + for (var i = 0; i < n; ++i) { + if (lengths[i] > maxLen) + maxLen = lengths[i]; + } + + // build the table + var size = 1 << maxLen; + var codes = Uint32Array(size); + for (var len = 1, code = 0, skip = 2; + len <= maxLen; + ++len, code <<= 1, skip <<= 1) { + for (var val = 0; val < n; ++val) { + if (lengths[val] == len) { + // bit-reverse the code + var code2 = 0; + var t = code; + for (var i = 0; i < len; ++i) { + code2 = (code2 << 1) | (t & 1); + t >>= 1; } - buffer[n] = b; - } - return; - } - var litCodeTable; - var distCodeTable; - if (hdr == 1) { // compressed block, fixed codes - litCodeTable = fixedLitCodeTab; - distCodeTable = fixedDistCodeTab; - } else if (hdr == 2) { // compressed block, dynamic codes - var numLitCodes = this.getBits(5) + 257; - var numDistCodes = this.getBits(5) + 1; - var numCodeLenCodes = this.getBits(4) + 4; + // fill the table entries + for (var i = code2; i < size; i += skip) + codes[i] = (len << 16) | val; - // build the code lengths code table - var codeLenCodeLengths = Array(codeLenCodeMap.length); - var i = 0; - while (i < numCodeLenCodes) - codeLenCodeLengths[codeLenCodeMap[i++]] = this.getBits(3); - var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths); - - // build the literal and distance code tables - var len = 0; - var i = 0; - var codes = numLitCodes + numDistCodes; - var codeLengths = new Array(codes); - while (i < codes) { - var code = this.getCode(codeLenCodeTab); - if (code == 16) { - repeat(this, codeLengths, 2, 3, len); - } else if (code == 17) { - repeat(this, codeLengths, 3, 3, len = 0); - } else if (code == 18) { - repeat(this, codeLengths, 7, 11, len = 0); - } else { - codeLengths[i++] = len = code; - } - } - - litCodeTable = this.generateHuffmanTable(codeLengths.slice(0, numLitCodes)); - distCodeTable = this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes)); - } else { - error("Unknown block type in flate stream"); - } - - var pos = this.bufferLength; - while (true) { - var code1 = this.getCode(litCodeTable); - if (code1 == 256) { - this.bufferLength = pos; - return; - } - if (code1 < 256) { - var buffer = this.ensureBuffer(pos + 1); - buffer[pos++] = code1; - } else { - code1 -= 257; - code1 = lengthDecode[code1]; - var code2 = code1 >> 16; - if (code2 > 0) - code2 = this.getBits(code2); - var len = (code1 & 0xffff) + code2; - code1 = this.getCode(distCodeTable); - code1 = distDecode[code1]; - code2 = code1 >> 16; - if (code2 > 0) - code2 = this.getBits(code2); - var dist = (code1 & 0xffff) + code2; - var buffer = this.ensureBuffer(pos + len); - for (var k = 0; k < len; ++k, ++pos) - buffer[pos] = buffer[pos - dist]; + ++code; } } } + + return [codes, maxLen]; + }; + constructor.prototype.readBlock = function() { + function repeat(stream, array, len, offset, what) { + var repeat = stream.getBits(len) + offset; + while (repeat-- > 0) + array[i++] = what; + } + + var bytes = this.bytes; + var bytesPos = this.bytesPos; + + // read block header + var hdr = this.getBits(3); + if (hdr & 1) + this.eof = true; + hdr >>= 1; + + var b; + if (hdr == 0) { // uncompressed block + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + var blockLen = b; + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + blockLen |= (b << 8); + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + var check = b; + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + check |= (b << 8); + if (check != (~this.blockLen & 0xffff)) + error("Bad uncompressed block length in flate stream"); + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + blockLen); + this.bufferLength = bufferLength + blockLen; + for (var n = bufferLength; n < blockLen; ++n) { + if (typeof (b = bytes[bytesPos++]) == "undefined") { + this.eof = true; + break; + } + buffer[n] = b; + } + return; + } + + var litCodeTable; + var distCodeTable; + if (hdr == 1) { // compressed block, fixed codes + litCodeTable = fixedLitCodeTab; + distCodeTable = fixedDistCodeTab; + } else if (hdr == 2) { // compressed block, dynamic codes + var numLitCodes = this.getBits(5) + 257; + var numDistCodes = this.getBits(5) + 1; + var numCodeLenCodes = this.getBits(4) + 4; + + // build the code lengths code table + var codeLenCodeLengths = Array(codeLenCodeMap.length); + var i = 0; + while (i < numCodeLenCodes) + codeLenCodeLengths[codeLenCodeMap[i++]] = this.getBits(3); + var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths); + + // build the literal and distance code tables + var len = 0; + var i = 0; + var codes = numLitCodes + numDistCodes; + var codeLengths = new Array(codes); + while (i < codes) { + var code = this.getCode(codeLenCodeTab); + if (code == 16) { + repeat(this, codeLengths, 2, 3, len); + } else if (code == 17) { + repeat(this, codeLengths, 3, 3, len = 0); + } else if (code == 18) { + repeat(this, codeLengths, 7, 11, len = 0); + } else { + codeLengths[i++] = len = code; + } + } + + litCodeTable = + this.generateHuffmanTable(codeLengths.slice(0, numLitCodes)); + distCodeTable = + this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes)); + } else { + error("Unknown block type in flate stream"); + } + + var buffer = this.buffer; + var limit = buffer ? buffer.length : 0; + var pos = this.bufferLength; + while (true) { + var code1 = this.getCode(litCodeTable); + if (code1 < 256) { + if (pos + 1 >= limit) { + buffer = this.ensureBuffer(pos + 1); + limit = buffer.length; + } + buffer[pos++] = code1; + continue; + } + if (code1 == 256) { + this.bufferLength = pos; + return; + } + code1 -= 257; + code1 = lengthDecode[code1]; + var code2 = code1 >> 16; + if (code2 > 0) + code2 = this.getBits(code2); + var len = (code1 & 0xffff) + code2; + code1 = this.getCode(distCodeTable); + code1 = distDecode[code1]; + code2 = code1 >> 16; + if (code2 > 0) + code2 = this.getBits(code2); + var dist = (code1 & 0xffff) + code2; + if (pos + len >= limit) { + buffer = this.ensureBuffer(pos + len); + limit = buffer.length; + } + for (var k = 0; k < len; ++k, ++pos) + buffer[pos] = buffer[pos - dist]; + } + }; + + return constructor; +})(); + +var PredictorStream = (function() { + function constructor(stream, params) { + var predictor = this.predictor = params.get("Predictor") || 1; + + if (predictor <= 1) + return stream; // no prediction + if (predictor !== 2 && (predictor < 10 || predictor > 15)) + error("Unsupported predictor"); + + if (predictor === 2) + this.readBlock = this.readBlockTiff; + else + this.readBlock = this.readBlockPng; + + this.stream = stream; + this.dict = stream.dict; + if (params.has("EarlyChange")) { + error("EarlyChange predictor parameter is not supported"); + } + var colors = this.colors = params.get("Colors") || 1; + var bits = this.bits = params.get("BitsPerComponent") || 8; + var columns = this.columns = params.get("Columns") || 1; + + var pixBytes = this.pixBytes = (colors * bits + 7) >> 3; + // add an extra pixByte to represent the pixel left of column 0 + var rowBytes = this.rowBytes = (columns * colors * bits + 7) >> 3; + + DecodeStream.call(this); + } + + constructor.prototype = Object.create(DecodeStream.prototype); + + constructor.prototype.readBlockTiff = function() { + var buffer = this.buffer; + var pos = this.pos; + + var rowBytes = this.rowBytes; + var pixBytes = this.pixBytes; + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + rowBytes); + var currentRow = buffer.subarray(bufferLength, bufferLength + rowBytes); + + var bits = this.bits; + var colors = this.colors; + + var rawBytes = this.stream.getBytes(rowBytes); + + if (bits === 1) { + var inbuf = 0; + for (var i = 0; i < rowBytes; ++i) { + var c = rawBytes[i]; + inBuf = (inBuf << 8) | c; + // bitwise addition is exclusive or + // first shift inBuf and then add + currentRow[i] = (c ^ (inBuf >> colors)) & 0xFF; + // truncate inBuf (assumes colors < 16) + inBuf &= 0xFFFF; + } + } else if (bits === 8) { + for (var i = 0; i < colors; ++i) + currentRow[i] = rawBytes[i]; + for (; i < rowBytes; ++i) + currentRow[i] = currentRow[i - colors] + rawBytes[i]; + } else { + var compArray = new Uint8Array(colors + 1); + var bitMask = (1 << bits) - 1; + var inbuf = 0, outbut = 0; + var inbits = 0, outbits = 0; + var j = 0, k = 0; + var columns = this.columns; + for (var i = 0; i < columns; ++i) { + for (var kk = 0; kk < colors; ++kk) { + if (inbits < bits) { + inbuf = (inbuf << 8) | (rawBytes[j++] & 0xFF); + inbits += 8; + } + compArray[kk] = (compArray[kk] + + (inbuf >> (inbits - bits))) & bitMask; + inbits -= bits; + outbuf = (outbuf << bits) | compArray[kk]; + outbits += bits; + if (outbits >= 8) { + currentRow[k++] = (outbuf >> (outbits - 8)) & 0xFF; + outbits -= 8; + } + } + } + if (outbits > 0) { + currentRow[k++] = (outbuf << (8 - outbits)) + + (inbuf & ((1 << (8 - outbits)) - 1)) + } + } + this.bufferLength += rowBytes; + }; + constructor.prototype.readBlockPng = function() { + var buffer = this.buffer; + var pos = this.pos; + + var rowBytes = this.rowBytes; + var pixBytes = this.pixBytes; + + var predictor = this.stream.getByte(); + var rawBytes = this.stream.getBytes(rowBytes); + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + pixBytes); + + var currentRow = buffer.subarray(bufferLength, bufferLength + rowBytes); + var prevRow = buffer.subarray(bufferLength - rowBytes, bufferLength); + if (prevRow.length == 0) + prevRow = currentRow; + + switch (predictor) { + case 0: + break; + case 1: + for (var i = 0; i < pixBytes; ++i) + currentRow[i] = rawBytes[i]; + for (; i < rowBytes; ++i) + currentRow[i] = (currentRow[i - pixBytes] + rawBytes[i]) & 0xFF; + break; + case 2: + for (var i = 0; i < rowBytes; ++i) + currentRow[i] = (prevRow[i] + rawBytes[i]) & 0xFF; + break; + case 3: + for (var i = 0; i < pixBytes; ++i) + currentRow[i] = (prevRow[i] >> 1) + rawBytes[i]; + for (; i < rowBytes; ++i) + currentRow[i] = (((prevRow[i] + currentRow[i - pixBytes]) + >> 1) + rawBytes[i]) & 0xFF; + break; + case 4: + // we need to save the up left pixels values. the simplest way + // is to create a new buffer + for (var i = 0; i < pixBytes; ++i) + currentRow[i] = rawBytes[i]; + for (; i < rowBytes; ++i) { + var up = prevRow[i]; + var upLeft = lastRow[i - pixBytes]; + var left = currentRow[i - pixBytes]; + var p = left + up - upLeft; + + var pa = p - left; + if (pa < 0) + pa = -pa; + var pb = p - up; + if (pb < 0) + pb = -pb; + var pc = p - upLeft; + if (pc < 0) + pc = -pc; + + var c = rawBytes[i]; + if (pa <= pb && pa <= pc) + currentRow[i] = left + c; + else if (pb <= pc) + currentRow[i] = up + c; + else + currentRow[i] = upLeft + c; + break; + } + default: + error("Unsupported predictor"); + break; + } + this.bufferLength += rowBytes; }; return constructor; @@ -543,101 +753,14 @@ var JpegStream = (function() { constructor.prototype = { getImage: function() { return this.domImage; + }, + getChar: function() { + error("internal error: getChar is not valid on JpegStream"); } }; return constructor; })(); - -var PredictorStream = (function() { - function constructor(stream, params) { - this.stream = stream; - this.dict = stream.dict; - this.predictor = params.get("Predictor") || 1; - if (this.predictor <= 1) { - return stream; // no prediction - } - if (params.has("EarlyChange")) { - error("EarlyChange predictor parameter is not supported"); - } - this.colors = params.get("Colors") || 1; - this.bitsPerComponent = params.get("BitsPerComponent") || 8; - this.columns = params.get("Columns") || 1; - if (this.colors !== 1 || this.bitsPerComponent !== 8) { - error("Multi-color and multi-byte predictors are not supported"); - } - if (this.predictor < 10 || this.predictor > 15) { - error("Unsupported predictor"); - } - this.currentRow = new Uint8Array(this.columns); - this.pos = 0; - this.bufferLength = 0; - } - - constructor.prototype = { - readRow : function() { - var lastRow = this.currentRow; - var predictor = this.stream.getByte(); - var currentRow = this.stream.getBytes(this.columns), i; - switch (predictor) { - default: - error("Unsupported predictor"); - break; - case 0: - break; - case 2: - for (i = 0; i < currentRow.length; ++i) { - currentRow[i] = (lastRow[i] + currentRow[i]) & 0xFF; - } - break; - } - this.pos = 0; - this.bufferLength = currentRow.length; - this.currentRow = currentRow; - }, - getByte : function() { - if (this.pos >= this.bufferLength) { - this.readRow(); - } - return this.currentRow[this.pos++]; - }, - getBytes : function(n) { - var i, bytes; - bytes = new Uint8Array(n); - for (i = 0; i < n; ++i) { - if (this.pos >= this.bufferLength) { - this.readRow(); - } - bytes[i] = this.currentRow[this.pos++]; - } - return bytes; - }, - getChar : function() { - return String.formCharCode(this.getByte()); - }, - lookChar : function() { - if (this.pos >= this.bufferLength) { - this.readRow(); - } - return String.formCharCode(this.currentRow[this.pos]); - }, - skip : function(n) { - var i; - if (!n) { - n = 1; - } - while (n > this.bufferLength - this.pos) { - n -= this.bufferLength - this.pos; - this.readRow(); - if (this.bufferLength === 0) break; - } - this.pos += n; - } - }; - - return constructor; -})(); - var DecryptStream = (function() { function constructor(str, fileKey, encAlgorithm, keyLength) { TODO("decrypt stream is not implemented"); @@ -648,6 +771,74 @@ var DecryptStream = (function() { return constructor; })(); +var Ascii85Stream = (function() { + function constructor(str) { + this.str = str; + this.dict = str.dict; + this.input = new Uint8Array(5); + + DecodeStream.call(this); + } + + constructor.prototype = Object.create(DecodeStream.prototype); + constructor.prototype.readBlock = function() { + const tildaCode = "~".charCodeAt(0); + const zCode = "z".charCodeAt(0); + var str = this.str; + + var c = str.getByte(); + while (Lexer.isSpace(String.fromCharCode(c))) + c = str.getByte(); + + if (!c || c === tildaCode) { + this.eof = true; + return; + } + + var bufferLength = this.bufferLength; + + // special code for z + if (c == zCode) { + var buffer = this.ensureBuffer(bufferLength + 4); + for (var i = 0; i < 4; ++i) + buffer[bufferLength + i] = 0; + this.bufferLength += 4; + } else { + var input = this.input; + input[0] = c; + for (var i = 1; i < 5; ++i){ + c = str.getByte(); + while (Lexer.isSpace(String.fromCharCode(c))) + c = str.getByte(); + + input[i] = c; + + if (!c || c == tildaCode) + break; + } + var buffer = this.ensureBuffer(bufferLength + i - 1); + this.bufferLength += i - 1; + + // partial ending; + if (i < 5) { + for (; i < 5; ++i) + input[i] = 0x21 + 84; + this.eof = true; + } + var t = 0; + for (var i = 0; i < 5; ++i) + t = t * 85 + (input[i] - 0x21); + + for (var i = 3; i >= 0; --i){ + buffer[bufferLength + i] = t & 0xFF; + t >>= 8; + } + } + }; + + return constructor; +})(); + var Name = (function() { function constructor(name) { this.name = name; @@ -682,6 +873,9 @@ var Dict = (function() { get2: function(key1, key2) { return this.get(key1) || this.get(key2); }, + get3: function(key1, key2, key3) { + return this.get(key1) || this.get(key2) || this.get(key3); + }, has: function(key) { return key in this.map; }, @@ -689,11 +883,10 @@ var Dict = (function() { this.map[key] = value; }, forEach: function(aCallback) { - for (var key in this.map) - aCallback(key, this.map[key]); + for (var key in this.map) + aCallback(key, this.map[key]); } }; - return constructor; })(); @@ -1005,7 +1198,7 @@ var Lexer = (function() { break; } } - + // start reading token switch (ch) { case '0': case '1': case '2': case '3': case '4': @@ -1179,7 +1372,7 @@ var Parser = (function() { } return str; } - + // simple object var obj = this.buf1; this.shift(); @@ -1192,7 +1385,7 @@ var Parser = (function() { // get stream start position lexer.skipToNextLine(); var pos = stream.pos; - + // get length var length = dict.get("Length"); var xref = this.xref; @@ -1230,7 +1423,8 @@ var Parser = (function() { if (IsArray(filter)) { var filterArray = filter; var paramsArray = params; - for (filter in filterArray) { + for (var i = 0, ii = filterArray.length; i < ii; ++i) { + filter = filterArray[i]; if (!IsName(filter)) error("Bad filter name"); else { @@ -1252,6 +1446,8 @@ var Parser = (function() { } else if (name == "DCTDecode") { var bytes = stream.getBytes(length); return new JpegStream(bytes, stream.dict); + } else if (name == "ASCII85Decode") { + return new Ascii85Stream(stream); } else { error("filter '" + name + "' not supported yet"); } @@ -1261,7 +1457,7 @@ var Parser = (function() { return constructor; })(); - + var Linearization = (function() { function constructor(stream) { this.parser = new Parser(new Lexer(stream), false); @@ -1443,26 +1639,29 @@ var XRef = (function() { for (i = 0; i < n; ++i) { var type = 0, offset = 0, generation = 0; for (j = 0; j < typeFieldWidth; ++j) - type = (type << 8) | stream.getByte(); + type = (type << 8) | stream.getByte(); + // if type field is absent, its default value = 1 + if (typeFieldWidth == 0) + type = 1; for (j = 0; j < offsetFieldWidth; ++j) - offset = (offset << 8) | stream.getByte(); + offset = (offset << 8) | stream.getByte(); for (j = 0; j < generationFieldWidth; ++j) - generation = (generation << 8) | stream.getByte(); - var entry = new Ref(offset, generation); - if (typeFieldWidth > 0) { - switch (type) { - case 0: - entry.free = true; - break; - case 1: - entry.uncompressed = true; - break; - case 2: - break; - default: - error("Invalid XRef entry type"); - break; - } + generation = (generation << 8) | stream.getByte(); + var entry = {} + entry.offset = offset; + entry.gen = generation; + switch (type) { + case 0: + entry.free = true; + break; + case 1: + entry.uncompressed = true; + break; + case 2: + break; + default: + error("Invalid XRef entry type"); + break; } if (!this.entries[first + i]) this.entries[first + i] = entry; @@ -1592,14 +1791,28 @@ var Page = (function() { } constructor.prototype = { + getPageProp: function(key) { + return this.pageDict.get(key); + }, + inheritPageProp: function(key) { + var dict = this.pageDict; + var obj = dict.get(key); + while (!obj) { + dict = this.xref.fetchIfRef(dict.get("Parent")); + if (!dict) + break; + obj = dict.get(key); + } + return obj; + }, get content() { - return shadow(this, "content", this.pageDict.get("Contents")); + return shadow(this, "content", this.getPageProp("Contents")); }, get resources() { - return shadow(this, "resources", this.pageDict.get("Resources")); + return shadow(this, "resources", this.inheritPageProp("Resources")); }, get mediaBox() { - var obj = this.pageDict.get("MediaBox"); + var obj = this.inheritPageProp("MediaBox"); return shadow(this, "mediaBox", ((IsArray(obj) && obj.length == 4) ? obj : null)); @@ -1688,7 +1901,7 @@ var Catalog = (function() { pageCache.push(new Page(this.xref, pageCache.length, obj)); } else { // must be a child page dictionary assertWellFormed(IsDict(obj), - "page dictionary kid reference points to wrong type of object"); + "page dictionary kid reference points to wrong type of object"); this.traverseKids(obj); } } @@ -2049,6 +2262,7 @@ var CanvasGraphics = (function() { S: "stroke", s: "closeStroke", f: "fill", + F: "fill", "f*": "eoFill", B: "fillStroke", "B*": "eoFillStroke", @@ -2129,13 +2343,18 @@ var CanvasGraphics = (function() { constructor.prototype = { translateFont: function(fontDict, xref, resources) { - var descriptor = xref.fetch(fontDict.get("FontDescriptor")); + var fd = fontDict.get("FontDescriptor"); + if (!fd) + // XXX deprecated "special treatment" for standard + // fonts? What do we need to do here? + return; + var descriptor = xref.fetch(fd); var fontName = descriptor.get("FontName"); assertWellFormed(IsName(fontName), "invalid font name"); fontName = fontName.name.replace("+", "_"); - var fontFile = descriptor.get2("FontFile", "FontFile2"); + var fontFile = descriptor.get3("FontFile", "FontFile2", "FontFile3"); if (!fontFile) error("FontFile not found for font: " + fontName); fontFile = xref.fetchIfRef(fontFile); @@ -2162,10 +2381,10 @@ var CanvasGraphics = (function() { // Get the font charset if any var charset = descriptor.get("CharSet"); - if (charset) + if (charset) { assertWellFormed(IsString(charset), "invalid charset"); - - charset = charset.split("/"); + charset = charset.split("/"); + } } else if (IsName(encoding)) { var encoding = Encodings[encoding.name]; if (!encoding) @@ -2507,7 +2726,7 @@ var CanvasGraphics = (function() { setWordSpacing: function(spacing) { TODO("word spacing"); }, - setHSpacing: function(scale) { + setHScale: function(scale) { TODO("horizontal text scale"); }, setLeading: function(leading) { @@ -2779,7 +2998,6 @@ var CanvasGraphics = (function() { shadingFill: function(entryRef) { var xref = this.xref; var res = this.res; - var shadingRes = xref.fetchIfRef(res.get("Shading")); if (!shadingRes) error("No shading resource found"); @@ -2804,13 +3022,15 @@ var CanvasGraphics = (function() { if (background) TODO("handle background colors"); - const types = [null, this.fillFunctionShading, - this.fillAxialShading, this.fillRadialShading]; - + const types = [null, + this.fillFunctionShading, + this.fillAxialShading, + this.fillRadialShading]; + var typeNum = shading.get("ShadingType"); var fillFn = types[typeNum]; - if (!fillFn) - error("Unknown type of shading"); + if (!fillFn) + error("Unknown or NYI type of shading '"+ typeNum +"'"); fillFn.apply(this, [shading]); this.restore(); @@ -2820,7 +3040,7 @@ var CanvasGraphics = (function() { var coordsArr = sh.get("Coords"); var x0 = coordsArr[0], y0 = coordsArr[1], x1 = coordsArr[2], y1 = coordsArr[3]; - + var t0 = 0.0, t1 = 1.0; if (sh.has("Domain")) { var domainArr = sh.get("Domain"); @@ -2863,6 +3083,10 @@ var CanvasGraphics = (function() { this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); }, + fillRadialShading: function(sh) { + TODO("radial shading"); + }, + // Images beginInlineImage: function() { TODO("inline images"); @@ -2879,12 +3103,12 @@ var CanvasGraphics = (function() { return; xobj = this.xref.fetchIfRef(xobj); assertWellFormed(IsStream(xobj), "XObject should be a stream"); - + var oc = xobj.dict.get("OC"); if (oc) { TODO("oc for xobject"); } - + var opi = xobj.dict.get("OPI"); if (opi) { TODO("opi for xobject"); @@ -2986,15 +3210,14 @@ var CanvasGraphics = (function() { // actual image var csStream = dict.get2("ColorSpace", "CS"); csStream = xref.fetchIfRef(csStream); - if (IsName(csStream) && inline) + if (IsName(csStream) && inline) csStream = colorSpaces.get(csStream); - - var colorSpace = new ColorSpace(xref, csStream); + var colorSpace = new ColorSpace(xref, csStream); var decode = dict.get2("Decode", "D"); TODO("create color map"); - + var mask = image.dict.get("Mask"); mask = xref.fetchIfRef(mask); var smask = image.dict.get("SMask"); @@ -3019,7 +3242,7 @@ var CanvasGraphics = (function() { var maskBPC = maskDict.get2("BitsPerComponent", "BPC"); if (!maskBPC) error("Invalid image mask bpc"); - + var maskCsStream = maskDict.get2("ColorSpace", "CS"); maskCsStream = xref.fetchIfRef(maskCsStream); var maskColorSpace = new ColorSpace(xref, maskCsStream); @@ -3029,9 +3252,7 @@ var CanvasGraphics = (function() { var maskDecode = maskDict.get2("Decode", "D"); if (maskDecode) TODO("Handle mask decode"); - // handle matte object - } else { - smask = null; + // handle matte object } var tmpCanvas = document.createElement("canvas"); @@ -3040,21 +3261,21 @@ var CanvasGraphics = (function() { var tmpCtx = tmpCanvas.getContext("2d"); var imgData = tmpCtx.getImageData(0, 0, w, h); var pixels = imgData.data; - + if (bitsPerComponent != 8) - error("unhandled number of bits per component"); - + error("unhandled number of bits per component"); + if (smask) { if (maskColorSpace.numComps != 1) error("Incorrect number of components in smask"); - + var numComps = colorSpace.numComps; var imgArray = image.getBytes(numComps * w * h); var imgIdx = 0; var smArray = smask.getBytes(w * h); var smIdx = 0; - + var length = 4 * w * h; switch (numComps) { case 1: @@ -3075,13 +3296,13 @@ var CanvasGraphics = (function() { } break; default: - error("unhandled amount of components per pixel: " + numComps); + TODO("Images with "+ numComps + " components per pixel"); } } else { var numComps = colorSpace.numComps; var imgArray = image.getBytes(numComps * w * h); var imgIdx = 0; - + var length = 4 * w * h; switch (numComps) { case 1: @@ -3102,7 +3323,7 @@ var CanvasGraphics = (function() { } break; default: - error("unhandled amount of components per pixel: " + numComps); + TODO("Images with "+ numComps + " components per pixel"); } } tmpCtx.putImageData(imgData, 0, 0); @@ -3183,12 +3404,15 @@ var ColorSpace = (function() { case "G": this.numComps = 1; break; + case "DeviceRGB": + this.numComps = 3; + break; } TODO("fill in color space constructor"); } else if (IsArray(cs)) { var mode = cs[0].name; this.mode = mode; - + var stream = cs[1]; stream = xref.fetchIfRef(stream); @@ -3201,19 +3425,28 @@ var ColorSpace = (function() { break; case "ICCBased": var dict = stream.dict; - this.stream = stream; this.dict = dict; this.numComps = dict.get("N"); break; + case "Indexed": + this.stream = stream; + this.dict = stream.dict; + var base = cs[1]; + var hival = cs[2]; + assertWellFormed(0 <= hival && hival <= 255, "hival in range"); + var lookupTable = cs[3]; + TODO("implement 'Indexed' color space"); + this.numComps = 3; // HACK + break; default: - error("unrecognized color space object"); + error("unrecognized color space object '"+ mode +"'"); } } else { error("unrecognized color space object"); } }; - + constructor.prototype = { }; @@ -3226,13 +3459,15 @@ var PDFFunction = (function() { if (!dict) dict = fn; - const types = [this.constructSampled, null, - this.constructInterpolated, this.constructStiched, - this.constructPostScript]; - + const types = [this.constructSampled, + null, + this.constructInterpolated, + this.constructStiched, + this.constructPostScript]; + var typeNum = dict.get("FunctionType"); var typeFn = types[typeNum]; - if (!typeFn) + if (!typeFn) error("Unknown type of function"); typeFn.apply(this, [fn, dict]); @@ -3245,7 +3480,7 @@ var PDFFunction = (function() { if (!domain || !range) error("No domain or range"); - + var inputSize = domain.length / 2; var outputSize = range.length / 2; @@ -3259,7 +3494,7 @@ var PDFFunction = (function() { order = 1; if (order !== 1) error ("No support for cubic spline interpolation"); - + var encode = dict.get("Encode"); if (!encode) { encode = []; @@ -3288,15 +3523,14 @@ var PDFFunction = (function() { for (var i = 0; i < inputSize; i++) { var i2 = i * 2; - + // clip to the domain var v = clip(args[i], domain[i2], domain[i2 + 1]); // encode - v = encode[i2] + ((v - domain[i2]) * - (encode[i2 + 1] - encode[i2]) / - (domain[i2 + 1] - domain[i2])); - + v = encode[i2] + ((v - domain[i2]) * + (encode[i2 + 1] - encode[i2]) / + (domain[i2 + 1] - domain[i2])); // clip to the size args[i] = clip(v, 0, size[i] - 1); } @@ -3319,12 +3553,11 @@ var PDFFunction = (function() { var high = samples[ceil + i]; var v = low * scale + high * (1 - scale); } - + var i2 = i * 2; // decode - v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) / - ((1 << bps) - 1)); - + v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) / + ((1 << bps) - 1)); // clip to the domain output.push(clip(v, range[i2], range[i2 + 1])); } @@ -3359,10 +3592,10 @@ var PDFFunction = (function() { }, constructInterpolated: function() { error("unhandled type of function"); - }, + }, constructStiched: function() { error("unhandled type of function"); - }, + }, constructPostScript: function() { error("unhandled type of function"); } diff --git a/test.py b/test.py index 46d30fef5..0c326ec09 100644 --- a/test.py +++ b/test.py @@ -1,7 +1,16 @@ -import json, os, sys, subprocess +import json, os, sys, subprocess, urllib2 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from urlparse import urlparse + +def prompt(question): + '''Return True iff the user answered "yes" to |question|.''' + inp = raw_input(question +' [yes/no] > ') + return inp == 'yes' ANAL = True +DEFAULT_MANIFEST_FILE = 'test_manifest.json' +REFDIR = 'ref' +TMPDIR = 'tmp' VERBOSE = False MIMEs = { @@ -20,6 +29,12 @@ class State: remaining = 0 results = { } done = False + masterMode = False + numErrors = 0 + numEqFailures = 0 + numEqNoSnapshot = 0 + numFBFFailures = 0 + numLoadFailures = 0 class Result: def __init__(self, snapshot, failure): @@ -34,8 +49,11 @@ class PDFTestHandler(BaseHTTPRequestHandler): BaseHTTPRequestHandler.log_request(code, size) def do_GET(self): + url = urlparse(self.path) + # Ignore query string + path, _ = url.path, url.query cwd = os.getcwd() - path = os.path.abspath(os.path.realpath(cwd + os.sep + self.path)) + path = os.path.abspath(os.path.realpath(cwd + os.sep + path)) cwd = os.path.abspath(cwd) prefix = os.path.commonprefix(( path, cwd )) _, ext = os.path.splitext(path) @@ -69,31 +87,59 @@ class PDFTestHandler(BaseHTTPRequestHandler): self.end_headers() result = json.loads(self.rfile.read(numBytes)) - browser = 'firefox4' - id, failure, round, page, snapshot = result['id'], result['failure'], result['round'], result['page'], result['snapshot'] + browser, id, failure, round, page, snapshot = result['browser'], result['id'], result['failure'], result['round'], result['page'], result['snapshot'] taskResults = State.taskResults[browser][id] - taskResults[round][page - 1] = Result(snapshot, failure) + taskResults[round].append(Result(snapshot, failure)) + assert len(taskResults[round]) == page if result['taskDone']: check(State.manifest[id], taskResults, browser) + # Please oh please GC this ... + del State.taskResults[browser][id] State.remaining -= 1 State.done = (0 == State.remaining) - -def set_up(): + +def setUp(manifestFile, masterMode): # Only serve files from a pdf.js clone assert not ANAL or os.path.isfile('pdf.js') and os.path.isdir('.git') + State.masterMode = masterMode + if masterMode and os.path.isdir(TMPDIR): + print 'Temporary snapshot dir tmp/ is still around.' + print 'tmp/ can be removed if it has nothing you need.' + if prompt('SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY'): + subprocess.call(( 'rm', '-rf', 'tmp' )) + + assert not os.path.isdir(TMPDIR) + testBrowsers = [ b for b in - ( 'firefox4', ) -#'chrome12', 'chrome13', 'firefox5', 'firefox6','opera11' ): + ( 'firefox5', ) +#'chrome12', 'chrome13', 'firefox4', 'firefox6','opera11' ): if os.access(b, os.R_OK | os.X_OK) ] - mf = open('test_manifest.json') + mf = open(manifestFile) manifestList = json.load(mf) mf.close() + for item in manifestList: + f, isLink = item['file'], item.get('link', False) + if isLink and not os.access(f, os.R_OK): + linkFile = open(f +'.link') + link = linkFile.read() + linkFile.close() + + sys.stdout.write('Downloading '+ link +' to '+ f +' ...') + sys.stdout.flush() + response = urllib2.urlopen(link) + + out = open(f, 'w') + out.write(response.read()) + out.close() + + print 'done' + for b in testBrowsers: State.taskResults[b] = { } for item in manifestList: @@ -101,15 +147,16 @@ def set_up(): State.manifest[id] = item taskResults = [ ] for r in xrange(rounds): - taskResults.append([ None ] * 100) + taskResults.append([ ]) State.taskResults[b][id] = taskResults State.remaining = len(manifestList) for b in testBrowsers: print 'Launching', b + qs = 'browser='+ b +'&manifestFile='+ manifestFile subprocess.Popen(( os.path.abspath(os.path.realpath(b)), - 'http://localhost:8080/test_slave.html' )) + 'http://localhost:8080/test_slave.html?'+ qs)) def check(task, results, browser): @@ -123,13 +170,14 @@ def check(task, results, browser): failure = pageResult.failure if failure: failed = True + State.numErrors += 1 print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure if failed: return kind = task['type'] - if '==' == kind: + if 'eq' == kind: checkEq(task, results, browser) elif 'fbf' == kind: checkFBF(task, results, browser) @@ -140,23 +188,60 @@ def check(task, results, browser): def checkEq(task, results, browser): - print ' !!! [TODO: == tests] !!!' - print 'TEST-PASS | == test', task['id'], '| in', browser + pfx = os.path.join(REFDIR, sys.platform, browser, task['id']) + results = results[0] + passed = True + for page in xrange(len(results)): + snapshot = results[page].snapshot + ref = None + eq = True + + path = os.path.join(pfx, str(page + 1)) + if not os.access(path, os.R_OK): + print 'WARNING: no reference snapshot', path + State.numEqNoSnapshot += 1 + else: + f = open(path) + ref = f.read() + f.close() + + eq = (ref == snapshot) + if not eq: + print 'TEST-UNEXPECTED-FAIL | eq', task['id'], '| in', browser, '| rendering of page', page + 1, '!= reference rendering' + passed = False + State.numEqFailures += 1 + + if State.masterMode and (ref is None or not eq): + tmpTaskDir = os.path.join(TMPDIR, sys.platform, browser, task['id']) + try: + os.makedirs(tmpTaskDir) + except OSError, e: + pass + + of = open(os.path.join(tmpTaskDir, str(page + 1)), 'w') + of.write(snapshot) + of.close() + + if passed: + print 'TEST-PASS | eq test', task['id'], '| in', browser -printed = [False] def checkFBF(task, results, browser): round0, round1 = results[0], results[1] assert len(round0) == len(round1) + passed = True for page in xrange(len(round1)): r0Page, r1Page = round0[page], round1[page] if r0Page is None: break if r0Page.snapshot != r1Page.snapshot: print 'TEST-UNEXPECTED-FAIL | forward-back-forward test', task['id'], '| in', browser, '| first rendering of page', page + 1, '!= second' - print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser + passed = False + State.numFBFFailures += 1 + if passed: + print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser def checkLoad(task, results, browser): @@ -165,11 +250,55 @@ def checkLoad(task, results, browser): print 'TEST-PASS | load test', task['id'], '| in', browser -def main(): - set_up() +def processResults(): + print '' + numErrors, numEqFailures, numEqNoSnapshot, numFBFFailures = State.numErrors, State.numEqFailures, State.numEqNoSnapshot, State.numFBFFailures + numFatalFailures = (numErrors + numFBFFailures) + if 0 == numEqFailures and 0 == numFatalFailures: + print 'All tests passed.' + else: + print 'OHNOES! Some tests failed!' + if 0 < numErrors: + print ' errors:', numErrors + if 0 < numEqFailures: + print ' different ref/snapshot:', numEqFailures + if 0 < numFBFFailures: + print ' different first/second rendering:', numFBFFailures + + if State.masterMode and (0 < numEqFailures or 0 < numEqNoSnapshot): + print "Some eq tests failed or didn't have snapshots." + print 'Checking to see if master references can be updated...' + if 0 < numFatalFailures: + print ' No. Some non-eq tests failed.' + else: + ' Yes! The references in tmp/ can be synced with ref/.' + if not prompt('Would you like to update the master copy in ref/?'): + print ' OK, not updating.' + else: + sys.stdout.write(' Updating ... ') + + # XXX unclear what to do on errors here ... + # NB: do *NOT* pass --delete to rsync. That breaks this + # entire scheme. + subprocess.check_call(( 'rsync', '-arv', 'tmp/', 'ref/' )) + + print 'done' + + +def main(args): + masterMode = False + manifestFile = DEFAULT_MANIFEST_FILE + if len(args) == 1: + masterMode = (args[0] == '-m') + manifestFile = args[0] if not masterMode else manifestFile + + setUp(manifestFile, masterMode) + server = HTTPServer(('127.0.0.1', 8080), PDFTestHandler) while not State.done: server.handle_request() + processResults() + if __name__ == '__main__': - main() + main(sys.argv[1:]) diff --git a/test_manifest.json b/test_manifest.json index 2f45a026c..036b7aafc 100644 --- a/test_manifest.json +++ b/test_manifest.json @@ -1,8 +1,8 @@ [ - { "id": "tracemonkey-==", + { "id": "tracemonkey-eq", "file": "tests/tracemonkey.pdf", "rounds": 1, - "type": "==" + "type": "eq" }, { "id": "tracemonkey-fbf", "file": "tests/tracemonkey.pdf", @@ -13,5 +13,11 @@ "file": "tests/canvas.pdf", "rounds": 1, "type": "load" + }, + { "id": "pdfspec-load", + "file": "tests/pdf.pdf", + "link": true, + "rounds": 1, + "type": "load" } ] diff --git a/test_slave.html b/test_slave.html index c560d90d0..06b911810 100644 --- a/test_slave.html +++ b/test_slave.html @@ -5,9 +5,24 @@