Merge remote branch 'upstream/master'

This commit is contained in:
Vivien Nicolas 2011-06-22 11:31:24 +02:00
commit fdf4db3365
12 changed files with 1110 additions and 635 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Binary file not shown.

View File

@ -27,7 +27,30 @@ span {
.control > input { .control > input {
float: left; float: left;
border: 1px solid #4d4d4d;
height: 20px;
padding: 0px;
margin: 0px 2px 0px 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 { .control > span {
@ -61,7 +84,7 @@ span {
background-color: #eee; background-color: #eee;
border-bottom: 1px solid #666; border-bottom: 1px solid #666;
padding: 4px 0px 0px 8px; padding: 4px 0px 0px 8px;
position:fixed; position: fixed;
left: 0px; left: 0px;
top: 0px; top: 0px;
height: 40px; height: 40px;
@ -79,7 +102,7 @@ span {
#previousPageButton { #previousPageButton {
background: url('images/buttons.png') no-repeat 0px -23px; background: url('images/buttons.png') no-repeat 0px -23px;
cursor: pointer; cursor: default;
display: inline-block; display: inline-block;
float: left; float: left;
margin: 0px; margin: 0px;
@ -97,7 +120,7 @@ span {
#nextPageButton { #nextPageButton {
background: url('images/buttons.png') no-repeat -28px -23px; background: url('images/buttons.png') no-repeat -28px -23px;
cursor: pointer; cursor: default;
display: inline-block; display: inline-block;
float: left; float: left;
margin: 0px; margin: 0px;
@ -113,71 +136,61 @@ span {
background: url('images/buttons.png') no-repeat -28px 0px; background: url('images/buttons.png') no-repeat -28px 0px;
} }
#scaleComboBoxInput { #openFileButton {
background: url('images/combobox.png') no-repeat 0px -23px; background: url('images/buttons.png') no-repeat -56px -23px;
cursor: default;
display: inline-block; display: inline-block;
float: left; float: left;
margin: 0px; margin: 0px 0px 0px 3px;
width: 35px; width: 29px;
height: 23px; height: 23px;
} }
#scaleComboBoxInput input { #openFileButton.down {
background: none; background: url('images/buttons.png') no-repeat -56px -46px;
border: 0px;
margin: 3px 2px 0px;
width: 31px;
} }
#scaleComboBoxButton { #openFileButton.disabled {
background: url('images/combobox.png') no-repeat -41px -23px; background: url('images/buttons.png') no-repeat -56px 0px;
cursor: pointer;
display: inline-block;
float: left;
margin: 0px;
width: 21px;
height: 23px;
} }
#scaleComboBoxButton.down { #fileInput {
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;
display: none; display: none;
top: -20px;
width: 48px;
} }
#scaleComboBoxList > ul { #pageNumber {
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 {
text-align: right; 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 { #viewer {
margin: 44px 0px 0px; margin: 44px 0px 0px;
padding: 8px 0px; padding: 8px 0px;

View File

@ -2,7 +2,8 @@
<html> <html>
<head> <head>
<title>pdf.js Multi-Page Viewer</title> <title>pdf.js Multi-Page Viewer</title>
<link rel="stylesheet" href="multi-page-viewer.css" type="text/css" media="screen" charset="utf-8"/> <meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
<link rel="stylesheet" href="multi-page-viewer.css" type="text/css" media="screen"/>
<script type="text/javascript" src="pdf.js"></script> <script type="text/javascript" src="pdf.js"></script>
<script type="text/javascript" src="fonts.js"></script> <script type="text/javascript" src="fonts.js"></script>
<script type="text/javascript" src="glyphlist.js"></script> <script type="text/javascript" src="glyphlist.js"></script>
@ -11,7 +12,8 @@
<body> <body>
<div id="controls"> <div id="controls">
<span class="control"> <span class="control">
<span id="previousPageButton"></span><span id="nextPageButton"></span> <span id="previousPageButton" class="disabled"></span>
<span id="nextPageButton" class="disabled"></span>
<span class="label">Previous/Next</span> <span class="label">Previous/Next</span>
</span> </span>
<span class="control"> <span class="control">
@ -21,20 +23,29 @@
<span class="label">Page Number</span> <span class="label">Page Number</span>
</span> </span>
<span class="control"> <span class="control">
<span id="scaleComboBoxInput"><input type="text" id="scale" value="100%" size="2"/></span><span id="scaleComboBoxButton"></span> <select id="scaleSelect">
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100" selected="selected">100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
<option value="200">200%</option>
</select>
<span class="label">Zoom</span> <span class="label">Zoom</span>
<div id="scaleComboBoxList"> </span>
<ul> <span class="control">
<li>50%</li> <span id="openFileButton"></span>
<li>75%</li> <input type="file" id="fileInput"/>
<li>100%</li> <span class="label">Open File</span>
<li>125%</li>
<li>150%</li>
<li>200%</li>
</ul>
</div>
</span> </span>
</div> </div>
<!--<div id="sidebar">
<div id="sidebarScrollView">
<div id="sidebarContentView">
</div>
</div>
</div>-->
<div id="viewer"></div> <div id="viewer"></div>
</body> </body>
</html> </html>

View File

@ -11,7 +11,8 @@ var PDFViewer = {
previousPageButton: null, previousPageButton: null,
nextPageButton: null, nextPageButton: null,
pageNumberInput: null, pageNumberInput: null,
scaleInput: null, scaleSelect: null,
fileInput: null,
willJumpToPage: false, willJumpToPage: false,
@ -66,20 +67,26 @@ var PDFViewer = {
removePage: function(num) { removePage: function(num) {
var div = document.getElementById('pageContainer' + num); var div = document.getElementById('pageContainer' + num);
if (div && div.hasChildNodes()) { if (div) {
while (div.childNodes.length > 0) { while (div.hasChildNodes()) {
div.removeChild(div.firstChild); div.removeChild(div.firstChild);
} }
} }
}, },
drawPage: function(num) { drawPage: function(num) {
if (PDFViewer.pdf) { if (!PDFViewer.pdf) {
var page = PDFViewer.pdf.getPage(num); return;
}
var div = document.getElementById('pageContainer' + num); var div = document.getElementById('pageContainer' + num);
var canvas = document.createElement('canvas');
if (div && !div.hasChildNodes()) { if (div && !div.hasChildNodes()) {
var canvas = document.createElement('canvas'); div.appendChild(canvas);
var page = PDFViewer.pdf.getPage(num);
canvas.id = 'page' + num; canvas.id = 'page' + num;
canvas.mozOpaque = true; canvas.mozOpaque = true;
@ -101,7 +108,7 @@ var PDFViewer = {
// we can trigger the actual page rendering with page.display // we can trigger the actual page rendering with page.display
page.compile(gfx, fonts); page.compile(gfx, fonts);
var fontsReady = true; var areFontsReady = true;
// Inspect fonts and translate the missing one // Inspect fonts and translate the missing one
var fontCount = fonts.length; var fontCount = fonts.length;
@ -110,16 +117,17 @@ var PDFViewer = {
var font = fonts[i]; var font = fonts[i];
if (Fonts[font.name]) { if (Fonts[font.name]) {
fontsReady = fontsReady && !Fonts[font.name].loading; areFontsReady = areFontsReady && !Fonts[font.name].loading;
continue; continue;
} }
new Font(font.name, font.file, font.properties); new Font(font.name, font.file, font.properties);
fontsReady = false; areFontsReady = false;
} }
var pageInterval; var pageInterval;
var delayLoadFont = function() { var delayLoadFont = function() {
for (var i = 0; i < fontCount; i++) { for (var i = 0; i < fontCount; i++) {
if (Fonts[font.name].loading) { if (Fonts[font.name].loading) {
@ -129,29 +137,33 @@ var PDFViewer = {
clearInterval(pageInterval); clearInterval(pageInterval);
while (div.hasChildNodes()) {
div.removeChild(div.firstChild);
}
PDFViewer.drawPage(num); PDFViewer.drawPage(num);
} }
if (!fontsReady) { if (!areFontsReady) {
pageInterval = setInterval(delayLoadFont, 10); pageInterval = setInterval(delayLoadFont, 10);
return; return;
} }
page.display(gfx); page.display(gfx);
div.appendChild(canvas);
}
} }
}, },
changeScale: function(num) { changeScale: function(num) {
while (PDFViewer.element.childNodes.length > 0) { while (PDFViewer.element.hasChildNodes()) {
PDFViewer.element.removeChild(PDFViewer.element.firstChild); PDFViewer.element.removeChild(PDFViewer.element.firstChild);
} }
PDFViewer.scale = num / 100; PDFViewer.scale = num / 100;
var i;
if (PDFViewer.pdf) { if (PDFViewer.pdf) {
for (var i = 1; i <= PDFViewer.numberOfPages; i++) { for (i = 1; i <= PDFViewer.numberOfPages; i++) {
PDFViewer.createPage(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) { goToPage: function(num) {
@ -190,7 +216,7 @@ var PDFViewer = {
} }
}, },
open: function(url) { openURL: function(url) {
PDFViewer.url = url; PDFViewer.url = url;
document.title = url; document.title = url;
@ -206,6 +232,18 @@ var PDFViewer = {
req.responseArrayBuffer || req.responseArrayBuffer ||
req.response; req.response;
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.pdf = new PDFDoc(new Stream(data));
PDFViewer.numberOfPages = PDFViewer.pdf.numPages; PDFViewer.numberOfPages = PDFViewer.pdf.numPages;
document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString();
@ -216,11 +254,13 @@ var PDFViewer = {
if (PDFViewer.numberOfPages > 0) { if (PDFViewer.numberOfPages > 0) {
PDFViewer.drawPage(1); PDFViewer.drawPage(1);
document.location.hash = 1;
} }
}
};
req.send(null); 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' : ''; this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : '';
}; };
PDFViewer.scaleInput = document.getElementById('scale'); PDFViewer.scaleSelect = document.getElementById('scaleSelect');
PDFViewer.scaleInput.buttonElement = document.getElementById('scaleComboBoxButton'); PDFViewer.scaleSelect.onchange = function(evt) {
PDFViewer.scaleInput.buttonElement.listElement = document.getElementById('scaleComboBoxList');
PDFViewer.scaleInput.onchange = function(evt) {
PDFViewer.changeScale(parseInt(this.value)); PDFViewer.changeScale(parseInt(this.value));
}; };
PDFViewer.scaleInput.buttonElement.onclick = function(evt) { if (window.File && window.FileReader && window.FileList && window.Blob) {
this.listElement.style.display = (this.listElement.style.display === 'block') ? 'none' : 'block'; var openFileButton = document.getElementById('openFileButton');
openFileButton.onclick = function(evt) {
if (this.className.indexOf('disabled') === -1) {
PDFViewer.fileInput.click();
}
}; };
PDFViewer.scaleInput.buttonElement.onmousedown = function(evt) { openFileButton.onmousedown = function(evt) {
if (this.className.indexOf('disabled') === -1) { if (this.className.indexOf('disabled') === -1) {
this.className = 'down'; this.className = 'down';
} }
}; };
PDFViewer.scaleInput.buttonElement.onmouseup = function(evt) { openFileButton.onmouseup = function(evt) {
this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : '';
}; };
PDFViewer.scaleInput.buttonElement.onmouseout = function(evt) { openFileButton.onmouseout = function(evt) {
this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : '';
}; };
var listItems = PDFViewer.scaleInput.buttonElement.listElement.getElementsByTagName('LI'); PDFViewer.fileInput = document.getElementById('fileInput');
PDFViewer.fileInput.onchange = function(evt) {
var files = evt.target.files;
for (var i = 0; i < listItems.length; i++) { if (files.length > 0) {
var listItem = listItems[i]; var file = files[0];
listItem.onclick = function(evt) { var fileReader = new FileReader();
PDFViewer.changeScale(parseInt(this.innerHTML));
PDFViewer.scaleInput.buttonElement.listElement.style.display = 'none'; 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.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber;
PDFViewer.scale = parseInt(PDFViewer.scaleInput.value) / 100 || 1.0; PDFViewer.scale = parseInt(PDFViewer.scaleSelect.value) / 100 || 1.0;
PDFViewer.open(PDFViewer.queryParams.file || PDFViewer.url);
PDFViewer.openURL(PDFViewer.queryParams.file || PDFViewer.url);
window.onscroll = function(evt) { window.onscroll = function(evt) {
var lastPagesDrawn = PDFViewer.lastPagesDrawn; var lastPagesDrawn = PDFViewer.lastPagesDrawn;

649
pdf.js
View File

@ -61,49 +61,50 @@ var Stream = (function() {
this.bytes = Uint8Array(arrayBuffer); this.bytes = Uint8Array(arrayBuffer);
this.start = start || 0; this.start = start || 0;
this.pos = this.start; this.pos = this.start;
this.end = (start + length) || this.bytes.byteLength; this.end = (start + length) || this.bytes.length;
this.dict = dict; this.dict = dict;
} }
// required methods for a stream. if a particular stream does not
// implement these, an error should be thrown
constructor.prototype = { constructor.prototype = {
get length() { get length() {
return this.end - this.start; return this.end - this.start;
}, },
getByte: function() { getByte: function() {
var bytes = this.bytes;
if (this.pos >= this.end) if (this.pos >= this.end)
return -1; return;
return bytes[this.pos++]; return this.bytes[this.pos++];
}, },
// returns subarray of original buffer // returns subarray of original buffer
// should only be read // should only be read
getBytes: function(length) { getBytes: function(length) {
var bytes = this.bytes; var bytes = this.bytes;
var pos = this.pos; var pos = this.pos;
var strEnd = this.end;
if (!length)
return bytes.subarray(pos, strEnd);
var end = pos + length; var end = pos + length;
var strEnd = this.end; if (end > strEnd)
if (!end || end > strEnd)
end = strEnd; end = strEnd;
this.pos = end; this.pos = end;
return bytes.subarray(pos, end); return bytes.subarray(pos, end);
}, },
lookChar: function() { lookChar: function() {
var bytes = this.bytes;
if (this.pos >= this.end) if (this.pos >= this.end)
return; return;
return String.fromCharCode(bytes[this.pos]); return String.fromCharCode(this.bytes[this.pos]);
}, },
getChar: function() { getChar: function() {
var ch = this.lookChar(); if (this.pos >= this.end)
if (!ch) return;
return ch; return String.fromCharCode(this.bytes[this.pos++]);
this.pos++;
return ch;
}, },
skip: function(n) { skip: function(n) {
if (!n && !IsNum(n)) if (!n)
n = 1; n = 1;
this.pos += n; this.pos += n;
}, },
@ -135,6 +136,91 @@ var StringStream = (function() {
return constructor; 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() { var FlateStream = (function() {
const codeLenCodeMap = Uint32Array([ const codeLenCodeMap = Uint32Array([
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
@ -259,16 +345,16 @@ var FlateStream = (function() {
this.bytes = bytes; this.bytes = bytes;
this.bytesPos = bytesPos; this.bytesPos = bytesPos;
this.eof = false;
this.codeSize = 0; this.codeSize = 0;
this.codeBuf = 0; this.codeBuf = 0;
this.pos = 0; DecodeStream.call(this);
this.bufferLength = 0;
} }
constructor.prototype = { constructor.prototype = Object.create(DecodeStream.prototype);
getBits: function(bits) {
constructor.prototype.getBits = function(bits) {
var codeSize = this.codeSize; var codeSize = this.codeSize;
var codeBuf = this.codeBuf; var codeBuf = this.codeBuf;
var bytes = this.bytes; var bytes = this.bytes;
@ -286,8 +372,8 @@ var FlateStream = (function() {
this.codeSize = codeSize -= bits; this.codeSize = codeSize -= bits;
this.bytesPos = bytesPos; this.bytesPos = bytesPos;
return b; return b;
}, };
getCode: function(table) { constructor.prototype.getCode = function(table) {
var codes = table[0]; var codes = table[0];
var maxLen = table[1]; var maxLen = table[1];
var codeSize = this.codeSize; var codeSize = this.codeSize;
@ -311,66 +397,8 @@ var FlateStream = (function() {
this.codeSize = (codeSize - codeLen); this.codeSize = (codeSize - codeLen);
this.bytesPos = bytesPos; this.bytesPos = bytesPos;
return codeVal; return codeVal;
}, };
ensureBuffer: function(requested) { constructor.prototype.generateHuffmanTable = function(lengths) {
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; var n = lengths.length;
// find max code length // find max code length
@ -406,8 +434,8 @@ var FlateStream = (function() {
} }
return [codes, maxLen]; return [codes, maxLen];
}, };
readBlock: function() { constructor.prototype.readBlock = function() {
function repeat(stream, array, len, offset, what) { function repeat(stream, array, len, offset, what) {
var repeat = stream.getBits(len) + offset; var repeat = stream.getBits(len) + offset;
while (repeat-- > 0) while (repeat-- > 0)
@ -487,23 +515,31 @@ var FlateStream = (function() {
} }
} }
litCodeTable = this.generateHuffmanTable(codeLengths.slice(0, numLitCodes)); litCodeTable =
distCodeTable = this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes)); this.generateHuffmanTable(codeLengths.slice(0, numLitCodes));
distCodeTable =
this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes));
} else { } else {
error("Unknown block type in flate stream"); error("Unknown block type in flate stream");
} }
var buffer = this.buffer;
var limit = buffer ? buffer.length : 0;
var pos = this.bufferLength; var pos = this.bufferLength;
while (true) { while (true) {
var code1 = this.getCode(litCodeTable); 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) { if (code1 == 256) {
this.bufferLength = pos; this.bufferLength = pos;
return; return;
} }
if (code1 < 256) {
var buffer = this.ensureBuffer(pos + 1);
buffer[pos++] = code1;
} else {
code1 -= 257; code1 -= 257;
code1 = lengthDecode[code1]; code1 = lengthDecode[code1];
var code2 = code1 >> 16; var code2 = code1 >> 16;
@ -516,12 +552,186 @@ var FlateStream = (function() {
if (code2 > 0) if (code2 > 0)
code2 = this.getBits(code2); code2 = this.getBits(code2);
var dist = (code1 & 0xffff) + code2; var dist = (code1 & 0xffff) + code2;
var buffer = this.ensureBuffer(pos + len); if (pos + len >= limit) {
buffer = this.ensureBuffer(pos + len);
limit = buffer.length;
}
for (var k = 0; k < len; ++k, ++pos) for (var k = 0; k < len; ++k, ++pos)
buffer[pos] = buffer[pos - dist]; 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; return constructor;
@ -543,101 +753,14 @@ var JpegStream = (function() {
constructor.prototype = { constructor.prototype = {
getImage: function() { getImage: function() {
return this.domImage; return this.domImage;
},
getChar: function() {
error("internal error: getChar is not valid on JpegStream");
} }
}; };
return constructor; 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() { var DecryptStream = (function() {
function constructor(str, fileKey, encAlgorithm, keyLength) { function constructor(str, fileKey, encAlgorithm, keyLength) {
TODO("decrypt stream is not implemented"); TODO("decrypt stream is not implemented");
@ -648,6 +771,74 @@ var DecryptStream = (function() {
return constructor; 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() { var Name = (function() {
function constructor(name) { function constructor(name) {
this.name = name; this.name = name;
@ -682,6 +873,9 @@ var Dict = (function() {
get2: function(key1, key2) { get2: function(key1, key2) {
return this.get(key1) || this.get(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) { has: function(key) {
return key in this.map; return key in this.map;
}, },
@ -693,7 +887,6 @@ var Dict = (function() {
aCallback(key, this.map[key]); aCallback(key, this.map[key]);
} }
}; };
return constructor; return constructor;
})(); })();
@ -1230,7 +1423,8 @@ var Parser = (function() {
if (IsArray(filter)) { if (IsArray(filter)) {
var filterArray = filter; var filterArray = filter;
var paramsArray = params; var paramsArray = params;
for (filter in filterArray) { for (var i = 0, ii = filterArray.length; i < ii; ++i) {
filter = filterArray[i];
if (!IsName(filter)) if (!IsName(filter))
error("Bad filter name"); error("Bad filter name");
else { else {
@ -1252,6 +1446,8 @@ var Parser = (function() {
} else if (name == "DCTDecode") { } else if (name == "DCTDecode") {
var bytes = stream.getBytes(length); var bytes = stream.getBytes(length);
return new JpegStream(bytes, stream.dict); return new JpegStream(bytes, stream.dict);
} else if (name == "ASCII85Decode") {
return new Ascii85Stream(stream);
} else { } else {
error("filter '" + name + "' not supported yet"); error("filter '" + name + "' not supported yet");
} }
@ -1444,12 +1640,16 @@ var XRef = (function() {
var type = 0, offset = 0, generation = 0; var type = 0, offset = 0, generation = 0;
for (j = 0; j < typeFieldWidth; ++j) 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) for (j = 0; j < offsetFieldWidth; ++j)
offset = (offset << 8) | stream.getByte(); offset = (offset << 8) | stream.getByte();
for (j = 0; j < generationFieldWidth; ++j) for (j = 0; j < generationFieldWidth; ++j)
generation = (generation << 8) | stream.getByte(); generation = (generation << 8) | stream.getByte();
var entry = new Ref(offset, generation); var entry = {}
if (typeFieldWidth > 0) { entry.offset = offset;
entry.gen = generation;
switch (type) { switch (type) {
case 0: case 0:
entry.free = true; entry.free = true;
@ -1463,7 +1663,6 @@ var XRef = (function() {
error("Invalid XRef entry type"); error("Invalid XRef entry type");
break; break;
} }
}
if (!this.entries[first + i]) if (!this.entries[first + i])
this.entries[first + i] = entry; this.entries[first + i] = entry;
} }
@ -1592,14 +1791,28 @@ var Page = (function() {
} }
constructor.prototype = { 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() { get content() {
return shadow(this, "content", this.pageDict.get("Contents")); return shadow(this, "content", this.getPageProp("Contents"));
}, },
get resources() { get resources() {
return shadow(this, "resources", this.pageDict.get("Resources")); return shadow(this, "resources", this.inheritPageProp("Resources"));
}, },
get mediaBox() { get mediaBox() {
var obj = this.pageDict.get("MediaBox"); var obj = this.inheritPageProp("MediaBox");
return shadow(this, "mediaBox", ((IsArray(obj) && obj.length == 4) return shadow(this, "mediaBox", ((IsArray(obj) && obj.length == 4)
? obj ? obj
: null)); : null));
@ -2049,6 +2262,7 @@ var CanvasGraphics = (function() {
S: "stroke", S: "stroke",
s: "closeStroke", s: "closeStroke",
f: "fill", f: "fill",
F: "fill",
"f*": "eoFill", "f*": "eoFill",
B: "fillStroke", B: "fillStroke",
"B*": "eoFillStroke", "B*": "eoFillStroke",
@ -2129,13 +2343,18 @@ var CanvasGraphics = (function() {
constructor.prototype = { constructor.prototype = {
translateFont: function(fontDict, xref, resources) { 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"); var fontName = descriptor.get("FontName");
assertWellFormed(IsName(fontName), "invalid font name"); assertWellFormed(IsName(fontName), "invalid font name");
fontName = fontName.name.replace("+", "_"); fontName = fontName.name.replace("+", "_");
var fontFile = descriptor.get2("FontFile", "FontFile2"); var fontFile = descriptor.get3("FontFile", "FontFile2", "FontFile3");
if (!fontFile) if (!fontFile)
error("FontFile not found for font: " + fontName); error("FontFile not found for font: " + fontName);
fontFile = xref.fetchIfRef(fontFile); fontFile = xref.fetchIfRef(fontFile);
@ -2162,10 +2381,10 @@ var CanvasGraphics = (function() {
// Get the font charset if any // Get the font charset if any
var charset = descriptor.get("CharSet"); var charset = descriptor.get("CharSet");
if (charset) if (charset) {
assertWellFormed(IsString(charset), "invalid charset"); assertWellFormed(IsString(charset), "invalid charset");
charset = charset.split("/"); charset = charset.split("/");
}
} else if (IsName(encoding)) { } else if (IsName(encoding)) {
var encoding = Encodings[encoding.name]; var encoding = Encodings[encoding.name];
if (!encoding) if (!encoding)
@ -2507,7 +2726,7 @@ var CanvasGraphics = (function() {
setWordSpacing: function(spacing) { setWordSpacing: function(spacing) {
TODO("word spacing"); TODO("word spacing");
}, },
setHSpacing: function(scale) { setHScale: function(scale) {
TODO("horizontal text scale"); TODO("horizontal text scale");
}, },
setLeading: function(leading) { setLeading: function(leading) {
@ -2779,7 +2998,6 @@ var CanvasGraphics = (function() {
shadingFill: function(entryRef) { shadingFill: function(entryRef) {
var xref = this.xref; var xref = this.xref;
var res = this.res; var res = this.res;
var shadingRes = xref.fetchIfRef(res.get("Shading")); var shadingRes = xref.fetchIfRef(res.get("Shading"));
if (!shadingRes) if (!shadingRes)
error("No shading resource found"); error("No shading resource found");
@ -2804,13 +3022,15 @@ var CanvasGraphics = (function() {
if (background) if (background)
TODO("handle background colors"); TODO("handle background colors");
const types = [null, this.fillFunctionShading, const types = [null,
this.fillAxialShading, this.fillRadialShading]; this.fillFunctionShading,
this.fillAxialShading,
this.fillRadialShading];
var typeNum = shading.get("ShadingType"); var typeNum = shading.get("ShadingType");
var fillFn = types[typeNum]; var fillFn = types[typeNum];
if (!fillFn) if (!fillFn)
error("Unknown type of shading"); error("Unknown or NYI type of shading '"+ typeNum +"'");
fillFn.apply(this, [shading]); fillFn.apply(this, [shading]);
this.restore(); this.restore();
@ -2863,6 +3083,10 @@ var CanvasGraphics = (function() {
this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10);
}, },
fillRadialShading: function(sh) {
TODO("radial shading");
},
// Images // Images
beginInlineImage: function() { beginInlineImage: function() {
TODO("inline images"); TODO("inline images");
@ -2990,7 +3214,6 @@ var CanvasGraphics = (function() {
csStream = colorSpaces.get(csStream); csStream = colorSpaces.get(csStream);
var colorSpace = new ColorSpace(xref, csStream); var colorSpace = new ColorSpace(xref, csStream);
var decode = dict.get2("Decode", "D"); var decode = dict.get2("Decode", "D");
TODO("create color map"); TODO("create color map");
@ -3030,8 +3253,6 @@ var CanvasGraphics = (function() {
if (maskDecode) if (maskDecode)
TODO("Handle mask decode"); TODO("Handle mask decode");
// handle matte object // handle matte object
} else {
smask = null;
} }
var tmpCanvas = document.createElement("canvas"); var tmpCanvas = document.createElement("canvas");
@ -3075,7 +3296,7 @@ var CanvasGraphics = (function() {
} }
break; break;
default: default:
error("unhandled amount of components per pixel: " + numComps); TODO("Images with "+ numComps + " components per pixel");
} }
} else { } else {
var numComps = colorSpace.numComps; var numComps = colorSpace.numComps;
@ -3102,7 +3323,7 @@ var CanvasGraphics = (function() {
} }
break; break;
default: default:
error("unhandled amount of components per pixel: " + numComps); TODO("Images with "+ numComps + " components per pixel");
} }
} }
tmpCtx.putImageData(imgData, 0, 0); tmpCtx.putImageData(imgData, 0, 0);
@ -3183,6 +3404,9 @@ var ColorSpace = (function() {
case "G": case "G":
this.numComps = 1; this.numComps = 1;
break; break;
case "DeviceRGB":
this.numComps = 3;
break;
} }
TODO("fill in color space constructor"); TODO("fill in color space constructor");
} else if (IsArray(cs)) { } else if (IsArray(cs)) {
@ -3201,13 +3425,22 @@ var ColorSpace = (function() {
break; break;
case "ICCBased": case "ICCBased":
var dict = stream.dict; var dict = stream.dict;
this.stream = stream; this.stream = stream;
this.dict = dict; this.dict = dict;
this.numComps = dict.get("N"); this.numComps = dict.get("N");
break; 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: default:
error("unrecognized color space object"); error("unrecognized color space object '"+ mode +"'");
} }
} else { } else {
error("unrecognized color space object"); error("unrecognized color space object");
@ -3226,8 +3459,10 @@ var PDFFunction = (function() {
if (!dict) if (!dict)
dict = fn; dict = fn;
const types = [this.constructSampled, null, const types = [this.constructSampled,
this.constructInterpolated, this.constructStiched, null,
this.constructInterpolated,
this.constructStiched,
this.constructPostScript]; this.constructPostScript];
var typeNum = dict.get("FunctionType"); var typeNum = dict.get("FunctionType");
@ -3296,7 +3531,6 @@ var PDFFunction = (function() {
v = encode[i2] + ((v - domain[i2]) * v = encode[i2] + ((v - domain[i2]) *
(encode[i2 + 1] - encode[i2]) / (encode[i2 + 1] - encode[i2]) /
(domain[i2 + 1] - domain[i2])); (domain[i2 + 1] - domain[i2]));
// clip to the size // clip to the size
args[i] = clip(v, 0, size[i] - 1); args[i] = clip(v, 0, size[i] - 1);
} }
@ -3324,7 +3558,6 @@ var PDFFunction = (function() {
// decode // decode
v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) / v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) /
((1 << bps) - 1)); ((1 << bps) - 1));
// clip to the domain // clip to the domain
output.push(clip(v, range[i2], range[i2 + 1])); output.push(clip(v, range[i2], range[i2 + 1]));
} }

165
test.py
View File

@ -1,7 +1,16 @@
import json, os, sys, subprocess import json, os, sys, subprocess, urllib2
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 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 ANAL = True
DEFAULT_MANIFEST_FILE = 'test_manifest.json'
REFDIR = 'ref'
TMPDIR = 'tmp'
VERBOSE = False VERBOSE = False
MIMEs = { MIMEs = {
@ -20,6 +29,12 @@ class State:
remaining = 0 remaining = 0
results = { } results = { }
done = False done = False
masterMode = False
numErrors = 0
numEqFailures = 0
numEqNoSnapshot = 0
numFBFFailures = 0
numLoadFailures = 0
class Result: class Result:
def __init__(self, snapshot, failure): def __init__(self, snapshot, failure):
@ -34,8 +49,11 @@ class PDFTestHandler(BaseHTTPRequestHandler):
BaseHTTPRequestHandler.log_request(code, size) BaseHTTPRequestHandler.log_request(code, size)
def do_GET(self): def do_GET(self):
url = urlparse(self.path)
# Ignore query string
path, _ = url.path, url.query
cwd = os.getcwd() 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) cwd = os.path.abspath(cwd)
prefix = os.path.commonprefix(( path, cwd )) prefix = os.path.commonprefix(( path, cwd ))
_, ext = os.path.splitext(path) _, ext = os.path.splitext(path)
@ -69,31 +87,59 @@ class PDFTestHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
result = json.loads(self.rfile.read(numBytes)) result = json.loads(self.rfile.read(numBytes))
browser = 'firefox4' browser, id, failure, round, page, snapshot = result['browser'], result['id'], result['failure'], result['round'], result['page'], result['snapshot']
id, failure, round, page, snapshot = result['id'], result['failure'], result['round'], result['page'], result['snapshot']
taskResults = State.taskResults[browser][id] 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']: if result['taskDone']:
check(State.manifest[id], taskResults, browser) check(State.manifest[id], taskResults, browser)
# Please oh please GC this ...
del State.taskResults[browser][id]
State.remaining -= 1 State.remaining -= 1
State.done = (0 == State.remaining) State.done = (0 == State.remaining)
def set_up(): def setUp(manifestFile, masterMode):
# Only serve files from a pdf.js clone # Only serve files from a pdf.js clone
assert not ANAL or os.path.isfile('pdf.js') and os.path.isdir('.git') 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 testBrowsers = [ b for b in
( 'firefox4', ) ( 'firefox5', )
#'chrome12', 'chrome13', 'firefox5', 'firefox6','opera11' ): #'chrome12', 'chrome13', 'firefox4', 'firefox6','opera11' ):
if os.access(b, os.R_OK | os.X_OK) ] if os.access(b, os.R_OK | os.X_OK) ]
mf = open('test_manifest.json') mf = open(manifestFile)
manifestList = json.load(mf) manifestList = json.load(mf)
mf.close() 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: for b in testBrowsers:
State.taskResults[b] = { } State.taskResults[b] = { }
for item in manifestList: for item in manifestList:
@ -101,15 +147,16 @@ def set_up():
State.manifest[id] = item State.manifest[id] = item
taskResults = [ ] taskResults = [ ]
for r in xrange(rounds): for r in xrange(rounds):
taskResults.append([ None ] * 100) taskResults.append([ ])
State.taskResults[b][id] = taskResults State.taskResults[b][id] = taskResults
State.remaining = len(manifestList) State.remaining = len(manifestList)
for b in testBrowsers: for b in testBrowsers:
print 'Launching', b print 'Launching', b
qs = 'browser='+ b +'&manifestFile='+ manifestFile
subprocess.Popen(( os.path.abspath(os.path.realpath(b)), 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): def check(task, results, browser):
@ -123,13 +170,14 @@ def check(task, results, browser):
failure = pageResult.failure failure = pageResult.failure
if failure: if failure:
failed = True failed = True
State.numErrors += 1
print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure
if failed: if failed:
return return
kind = task['type'] kind = task['type']
if '==' == kind: if 'eq' == kind:
checkEq(task, results, browser) checkEq(task, results, browser)
elif 'fbf' == kind: elif 'fbf' == kind:
checkFBF(task, results, browser) checkFBF(task, results, browser)
@ -140,22 +188,59 @@ def check(task, results, browser):
def checkEq(task, results, browser): def checkEq(task, results, browser):
print ' !!! [TODO: == tests] !!!' pfx = os.path.join(REFDIR, sys.platform, browser, task['id'])
print 'TEST-PASS | == test', task['id'], '| in', browser 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): def checkFBF(task, results, browser):
round0, round1 = results[0], results[1] round0, round1 = results[0], results[1]
assert len(round0) == len(round1) assert len(round0) == len(round1)
passed = True
for page in xrange(len(round1)): for page in xrange(len(round1)):
r0Page, r1Page = round0[page], round1[page] r0Page, r1Page = round0[page], round1[page]
if r0Page is None: if r0Page is None:
break break
if r0Page.snapshot != r1Page.snapshot: 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-UNEXPECTED-FAIL | forward-back-forward test', task['id'], '| in', browser, '| first rendering of page', page + 1, '!= second'
passed = False
State.numFBFFailures += 1
if passed:
print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser
@ -165,11 +250,55 @@ def checkLoad(task, results, browser):
print 'TEST-PASS | load test', task['id'], '| in', browser print 'TEST-PASS | load test', task['id'], '| in', browser
def main(): def processResults():
set_up() 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) server = HTTPServer(('127.0.0.1', 8080), PDFTestHandler)
while not State.done: while not State.done:
server.handle_request() server.handle_request()
processResults()
if __name__ == '__main__': if __name__ == '__main__':
main() main(sys.argv[1:])

View File

@ -1,8 +1,8 @@
[ [
{ "id": "tracemonkey-==", { "id": "tracemonkey-eq",
"file": "tests/tracemonkey.pdf", "file": "tests/tracemonkey.pdf",
"rounds": 1, "rounds": 1,
"type": "==" "type": "eq"
}, },
{ "id": "tracemonkey-fbf", { "id": "tracemonkey-fbf",
"file": "tests/tracemonkey.pdf", "file": "tests/tracemonkey.pdf",
@ -13,5 +13,11 @@
"file": "tests/canvas.pdf", "file": "tests/canvas.pdf",
"rounds": 1, "rounds": 1,
"type": "load" "type": "load"
},
{ "id": "pdfspec-load",
"file": "tests/pdf.pdf",
"link": true,
"rounds": 1,
"type": "load"
} }
] ]

View File

@ -5,9 +5,24 @@
<script type="text/javascript" src="fonts.js"></script> <script type="text/javascript" src="fonts.js"></script>
<script type="text/javascript" src="glyphlist.js"></script> <script type="text/javascript" src="glyphlist.js"></script>
<script type="application/javascript"> <script type="application/javascript">
var canvas, currentTask, currentTaskIdx, failure, manifest, pdfDoc, stdout; var browser, canvas, currentTask, currentTaskIdx, failure, manifest, pdfDoc, stdout;
function queryParams() {
var qs = window.location.search.substring(1);
var kvs = qs.split("&");
var params = { };
for (var i = 0; i < kvs.length; ++i) {
var kv = kvs[i].split("=");
params[unescape(kv[0])] = unescape(kv[1]);
}
return params;
}
function load() { function load() {
var params = queryParams();
browser = params.browser;
manifestFile = params.manifestFile;
canvas = document.createElement("canvas"); canvas = document.createElement("canvas");
// 8.5x11in @ 100% ... XXX need something better here // 8.5x11in @ 100% ... XXX need something better here
canvas.width = 816; canvas.width = 816;
@ -15,10 +30,11 @@ function load() {
canvas.mozOpaque = true; canvas.mozOpaque = true;
stdout = document.getElementById("stdout"); stdout = document.getElementById("stdout");
log("Harness thinks this browser is '"+ browser +"'\n");
log("Fetching manifest ..."); log("Fetching manifest ...");
var r = new XMLHttpRequest(); var r = new XMLHttpRequest();
r.open("GET", "test_manifest.json", false); r.open("GET", manifestFile, false);
r.onreadystatechange = function(e) { r.onreadystatechange = function(e) {
if (r.readyState == 4) { if (r.readyState == 4) {
log("done\n"); log("done\n");
@ -67,14 +83,13 @@ function nextPage() {
failure = ''; failure = '';
log(" drawing page "+ currentTask.pageNum +"..."); log(" drawing page "+ currentTask.pageNum +"...");
currentPage = pdfDoc.getPage(currentTask.pageNum);
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext("2d");
clear(ctx); clear(ctx);
var fonts = []; var fonts = [];
var gfx = new CanvasGraphics(ctx); var gfx = new CanvasGraphics(ctx);
try { try {
currentPage = pdfDoc.getPage(currentTask.pageNum);
currentPage.compile(gfx, fonts); currentPage.compile(gfx, fonts);
} catch(e) { } catch(e) {
failure = 'compile: '+ e.toString(); failure = 'compile: '+ e.toString();
@ -111,7 +126,8 @@ function done() {
} }
function sendTaskResult(snapshot) { function sendTaskResult(snapshot) {
var result = { id: currentTask.id, var result = { browser: browser,
id: currentTask.id,
taskDone: currentTask.taskDone, taskDone: currentTask.taskDone,
failure: failure, failure: failure,
file: currentTask.file, file: currentTask.file,

1
tests/pdf.pdf.link Normal file
View File

@ -0,0 +1 @@
http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/pdf_reference_1-7.pdf