From 95bc99f6980051618a093dff54288253143a23cc Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Sat, 16 Jun 2012 15:15:42 -0500 Subject: [PATCH 01/27] Initial JBIG2 code --- src/jbig2.js | 520 +++++++++++++++++++++++++++++++++++++++++++ src/parser.js | 3 +- src/stream.js | 37 +++ src/worker_loader.js | 1 + test/test_slave.html | 1 + 5 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 src/jbig2.js diff --git a/src/jbig2.js b/src/jbig2.js new file mode 100644 index 000000000..d7fa36d13 --- /dev/null +++ b/src/jbig2.js @@ -0,0 +1,520 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +var Jbig2Image = (function Jbig2ImageClosure() { + + var ArithmeticDecoder = (function ArithmeticDecoderClosure() { + var QeTable = [ + {qe: 0x5601, nmps: 1, nlps: 1, switchFlag: 1}, + {qe: 0x3401, nmps: 2, nlps: 6, switchFlag: 0}, + {qe: 0x1801, nmps: 3, nlps: 9, switchFlag: 0}, + {qe: 0x0AC1, nmps: 4, nlps: 12, switchFlag: 0}, + {qe: 0x0521, nmps: 5, nlps: 29, switchFlag: 0}, + {qe: 0x0221, nmps: 38, nlps: 33, switchFlag: 0}, + {qe: 0x5601, nmps: 7, nlps: 6, switchFlag: 1}, + {qe: 0x5401, nmps: 8, nlps: 14, switchFlag: 0}, + {qe: 0x4801, nmps: 9, nlps: 14, switchFlag: 0}, + {qe: 0x3801, nmps: 10, nlps: 14, switchFlag: 0}, + {qe: 0x3001, nmps: 11, nlps: 17, switchFlag: 0}, + {qe: 0x2401, nmps: 12, nlps: 18, switchFlag: 0}, + {qe: 0x1C01, nmps: 13, nlps: 20, switchFlag: 0}, + {qe: 0x1601, nmps: 29, nlps: 21, switchFlag: 0}, + {qe: 0x5601, nmps: 15, nlps: 14, switchFlag: 1}, + {qe: 0x5401, nmps: 16, nlps: 14, switchFlag: 0}, + {qe: 0x5101, nmps: 17, nlps: 15, switchFlag: 0}, + {qe: 0x4801, nmps: 18, nlps: 16, switchFlag: 0}, + {qe: 0x3801, nmps: 19, nlps: 17, switchFlag: 0}, + {qe: 0x3401, nmps: 20, nlps: 18, switchFlag: 0}, + {qe: 0x3001, nmps: 21, nlps: 19, switchFlag: 0}, + {qe: 0x2801, nmps: 22, nlps: 19, switchFlag: 0}, + {qe: 0x2401, nmps: 23, nlps: 20, switchFlag: 0}, + {qe: 0x2201, nmps: 24, nlps: 21, switchFlag: 0}, + {qe: 0x1C01, nmps: 25, nlps: 22, switchFlag: 0}, + {qe: 0x1801, nmps: 26, nlps: 23, switchFlag: 0}, + {qe: 0x1601, nmps: 27, nlps: 24, switchFlag: 0}, + {qe: 0x1401, nmps: 28, nlps: 25, switchFlag: 0}, + {qe: 0x1201, nmps: 29, nlps: 26, switchFlag: 0}, + {qe: 0x1101, nmps: 30, nlps: 27, switchFlag: 0}, + {qe: 0x0AC1, nmps: 31, nlps: 28, switchFlag: 0}, + {qe: 0x09C1, nmps: 32, nlps: 29, switchFlag: 0}, + {qe: 0x08A1, nmps: 33, nlps: 30, switchFlag: 0}, + {qe: 0x0521, nmps: 34, nlps: 31, switchFlag: 0}, + {qe: 0x0441, nmps: 35, nlps: 32, switchFlag: 0}, + {qe: 0x02A1, nmps: 36, nlps: 33, switchFlag: 0}, + {qe: 0x0221, nmps: 37, nlps: 34, switchFlag: 0}, + {qe: 0x0141, nmps: 38, nlps: 35, switchFlag: 0}, + {qe: 0x0111, nmps: 39, nlps: 36, switchFlag: 0}, + {qe: 0x0085, nmps: 40, nlps: 37, switchFlag: 0}, + {qe: 0x0049, nmps: 41, nlps: 38, switchFlag: 0}, + {qe: 0x0025, nmps: 42, nlps: 39, switchFlag: 0}, + {qe: 0x0015, nmps: 43, nlps: 40, switchFlag: 0}, + {qe: 0x0009, nmps: 44, nlps: 41, switchFlag: 0}, + {qe: 0x0005, nmps: 45, nlps: 42, switchFlag: 0}, + {qe: 0x0001, nmps: 45, nlps: 43, switchFlag: 0}, + {qe: 0x5601, nmps: 46, nlps: 46, switchFlag: 0} + ]; + + function ArithmeticDecoder(data, start, end) { + this.data = data; + this.bp = start; + this.dataEnd = end; + + this.chigh = data[start]; + this.clow = 0; + + this.byteIn(); + + this.chigh = ((this.chigh << 7) & 0xFFFF) | ((this.clow >> 9) & 0x7F); + this.clow = (this.clow << 7) & 0xFFFF; + this.ct -= 7; + this.a = 0x8000; + } + + ArithmeticDecoder.prototype = { + byteIn: function ArithmeticDecoder_byteIn() { + var data = this.data; + var bp = this.bp; + if (data[bp] == 0xFF) { + var b1 = data[bp + 1]; + if (b1 > 0x8F) { + this.clow += 0xFF00; + this.ct = 8; + } else { + bp++; + this.clow += (data[bp] << 9); + this.ct = 7; + this.bp = bp; + } + } else { + bp++; + this.clow += bp < this.dataEnd ? (data[bp] << 8) : 0xFF00; + this.ct = 8; + this.bp = bp; + } + if (this.clow > 0xFFFF) { + this.chigh += (this.clow >> 16); + this.clow &= 0xFFFF; + } + }, + readBit: function ArithmeticDecoder_readBit(cx) { + var qeIcx = QeTable[cx.index].qe; + this.a -= qeIcx; + + if (this.chigh < qeIcx) { + var d = this.exchangeLps(cx); + this.renormD(); + return d; + } else { + this.chigh -= qeIcx; + if ((this.a & 0x8000) == 0) { + var d = this.exchangeMps(cx); + this.renormD(); + return d; + } else { + return cx.mps; + } + } + }, + renormD: function ArithmeticDecoder_renormD() { + do { + if (this.ct == 0) + this.byteIn(); + + this.a <<= 1; + this.chigh = ((this.chigh << 1) & 0xFFFF) | ((this.clow >> 15) & 1); + this.clow = (this.clow << 1) & 0xFFFF; + this.ct--; + } while ((this.a & 0x8000) == 0); + }, + exchangeMps: function ArithmeticDecoder_exchangeMps(cx) { + var d; + var qeTableIcx = QeTable[cx.index]; + if (this.a < qeTableIcx.qe) { + d = 1 - cx.mps; + + if (qeTableIcx.switchFlag == 1) { + cx.mps = 1 - cx.mps; + } + cx.index = qeTableIcx.nlps; + } else { + d = cx.mps; + cx.index = qeTableIcx.nmps; + } + return d; + }, + exchangeLps: function ArithmeticDecoder_exchangeLps(cx) { + var d; + var qeTableIcx = QeTable[cx.index]; + if (this.a < qeTableIcx.qe) { + this.a = qeTableIcx.qe; + d = cx.mps; + cx.index = qeTableIcx.nmps; + } else { + this.a = qeTableIcx.qe; + d = 1 - cx.mps; + + if (qeTableIcx.switchFlag == 1) { + cx.mps = 1 - cx.mps; + } + cx.index = qeTableIcx.nlps; + } + return d; + } + }; + + return ArithmeticDecoder; + })(); + + var SegmentTypes = [ + 'SymbolDictionary', null, null, null, 'IntermediateTextRegion', null, + 'ImmediateTextRegion', 'ImmediateLosslessTextRegion', null, null, null, + null, null, null, null, null, 'patternDictionary', null, null, null, + 'IntermediateHalftoneRegion', null, 'ImmediateHalftoneRegion', + 'ImmediateLosslessHalftoneRegion', null, null, null, null, null, null, null, + null, null, null, null, null, 'IntermediateGenericRegion', null, + 'ImmediateGenericRegion', 'ImmediateLosslessGenericRegion', + 'IntermediateGenericRefinementRegion', null, + 'ImmediateGenericRefinementRegion', + 'ImmediateLosslessGenericRefinementRegion', null, null, null, null, + 'PageInformation', 'EndOfPage', 'EndOfStripe', 'EndOfFile', 'Profiles', + 'Tables', null, null, null, null, null, null, null, null, + 'Extension' + ]; + + var CodingTemplates = [ + [{x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}, {x: -2, y: -1}, + {x: -1, y: -1}, {x: 0, y: -1}, {x: 1, y: -1}, {x: 2, y: -1}, + {x: -4, y: 0}, {x: -3, y: 0}, {x: -2, y: 0}, {x: -1, y: 0}], + [{x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}, {x: -2, y: -1}, + {x: -1, y: -1}, {x: 0, y: -1}, {x: 1, y: -1}, {x: 2, y: -1}, + {x: -3, y: 0}, {x: -2, y: 0}, {x: -1, y: 0}], + [{x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}, {x: -1, y: -1}, + {x: 0, y: -1}, {x: 1, y: -1}, {x: -2, y: 0}, {x: -1, y: 0}], + [{x: -3, y: -1}, {x: -2, y: -1}, {x: -1, y: -1}, {x: 0, y: -1}, + {x: 1, y: -1}, {x: -4, y: 0}, {x: -3, y: 0}, {x: -2, y: 0}, {x: -1, y: 0}] + ]; + + var ReusedContexts = [ + 0x1CD3, // '00111001101' (template) + '0011' (at), + 0x079A, // '001111001101' + '0', + 0x00E3, // '001110001' + '1', + 0x018B // '011000101' + '1' + ]; + + function readInt32(data, start) { + return (data[start] << 24) | (data[start + 1] << 16) | + (data[start + 2] << 8) | data[start + 3]; + } + + function readUint32(data, start) { + var value = readInt32(data, start); + return value & 0x80000000 ? (value + 4294967296) : value; + } + + function readUint16(data, start) { + return (data[start] << 8) | data[start + 1]; + } + + function readInt8(data, start) { + return (data[start] << 24) >> 24; + } + + function decodeBitmap(mmr, width, height, templateIndex, prediction, skip, at, + data, start, end) { + if (mmr) + throw 'MMR encoding is not supported'; + + var useskip = !!skip; + var template = CodingTemplates[templateIndex].concat(at); + var templateLength = template.length; + var templateX = new Int32Array(templateLength); + var templateY = new Int32Array(templateLength); + for (var k = 0; k < templateLength; k++) { + templateX[k] = template[k].x; + templateY[k] = template[k].y; + } + var pseudoPixelContext = ReusedContexts[template]; + var bitmap = []; + var decoder = new ArithmeticDecoder(data, start, end); + var contexts = [], cx; + var ltp = 0; + for (var i = 0; i < height; i++) { + if (prediction) { + cx = contexts[pseudoPixelContext]; + if (!cx) + contexts[pseudoPixelContext] = cx = {index: 0, mps: 0}; + var sltp = decoder.readBit(cx); + ltp ^= sltp; + } + if (ltp) { + bitmap.push(bitmap[bitmap.length - 1]); // duplicate previous row + continue; + } + var row = new Uint8Array(width); + bitmap.push(row); + for (var j = 0; j < width; j++) { + if (useskip && skip[i][j]) { + row[j] = 0; + continue; + } + var contextLabel = 0; + for (var k = 0; k < templateLength; k++) { + var i0 = i + templateY[k], j0 = j + templateX[k]; + if (i0 < 0 || j0 < 0 || j0 >= width) + contextLabel <<= 1; // out of bound pixel + else + contextLabel = (contextLabel << 1) | bitmap[i0][j0]; + } + cx = contexts[contextLabel]; + if (!cx) + contexts[contextLabel] = cx = {index: 0, mps: 0}; + var pixel = decoder.readBit(cx); + row[j] = pixel; + } + } + return bitmap; + } + + function readSegmentHeader(data, start) { + var segmentHeader = {}; + segmentHeader.number = readUint32(data, start); + var flags = data[start + 4]; + var segmentType = flags & 0x3F; + if (!SegmentTypes[segmentType]) + throw 'Invalid segment type: ' + segmentType; + segmentHeader.type = segmentType; + segmentHeader.typeName = SegmentTypes[segmentType]; + segmentHeader.deferredNonRetain = !!(flags & 0x80); + var pageAssociationFieldSize = !!(flags & 0x40); + var referredFlags = data[start + 5]; + var referredToCount = (referredFlags >> 5) & 7; + var retainBits = [referredFlags & 31]; + var position = start + 6; + if (referredFlags == 7) { + referredToCount = readInt32(data, position - 1) & 0x1FFFFFFF; + position += 3; + var bytes = (referredToCount + 7) >> 3; + retainBits[0] = data[position++]; + while (--bytes > 0) { + retainBits.push(data[position++]); + } + } else if (referredFlags == 5 || referredFlags == 6) + throw 'Invalid referred-to flags'; + segmentHeader.referredToCount = referredToCount; + segmentHeader.retainBits = retainBits; + var referredToSegmentNumberSize = segmentHeader.number <= 256 ? 1 : + segmentHeader.number <= 65536 ? 2 : 4; + var referredTo = []; + for (var i = 0; i < referredToCount; i++) { + var number = referredToSegmentNumberSize == 1 ? data[position] : + referredToSegmentNumberSize == 2 ? readUint16(data, position) : + readUint32(data, position); + referredTo.push(number); + position += referredToSegmentNumberSize; + } + segmentHeader.referredTo = referredTo; + if (!pageAssociationFieldSize) + segmentHeader.pageAssociation = data[position++]; + else { + segmentHeader.pageAssociation = readUint32(data, position); + position += 4; + } + segmentHeader.length = readUint32(data, position); + if (segmentHeader.length == 0xFFFFFFFF) + throw 'Unsupported unknown segment length'; + position += 4; + segmentHeader.headerEnd = position; + return segmentHeader; + } + + function readSegments(header, data, start, end) { + var segments = []; + var position = start; + while (position < end) { + var segmentHeader = readSegmentHeader(data, position); + position = segmentHeader.headerEnd; + var segment = { + header: segmentHeader, + data: data + }; + if (!header.randomAccess) { + segment.start = position; + position += segmentHeader.length; + segment.end = position; + } + segments.push(segment); + if (segmentHeader.type == 51) + break; // end of file is found + } + if (header.randomAccess) { + for (var i = 0; i < segments.length; i++) { + segments[i].start = position; + position += segments[i].header.length; + segments[i].end = position; + } + } + return segments; + } + + function readRegionSegmentInformation(data, start) { + return { + width: readUint32(data, start), + height: readUint32(data, start + 4), + x: readUint32(data, start + 8), + y: readUint32(data, start + 12), + combinationOperator: data[start + 16] & 7 + }; + } + var RegionSegmentInformationFieldLength = 17; + + function processSegment(segment, visitor) { + var header = segment.header; + if (!(header.typeName in visitor)) + return; + + var data = segment.data, position = segment.start, args; + switch (header.type) { + case 38: // ImmediateGenericRegion + var genericRegion = {}; + genericRegion.info = readRegionSegmentInformation(data, position); + position += RegionSegmentInformationFieldLength; + var genericRegionSegmentFlags = data[position++]; + genericRegion.mmr = !!(genericRegionSegmentFlags & 1); + genericRegion.template = (genericRegionSegmentFlags >> 1) & 3; + genericRegion.prediction = !!(genericRegionSegmentFlags & 4); + if (!genericRegion.mmr) { + var atLength = genericRegion.template == 0 ? 4 : 1; + var at = []; + for (var i = 0; i < atLength; i++) { + at.push({ + x: readInt8(data, position), + y: readInt8(data, position + 1) + }); + position += 2; + } + genericRegion.at = at; + } + args = [genericRegion, data, position, segment.end]; + break; + case 48: // PageInformation + var pageInfo = { + width: readUint32(data, position), + height: readUint32(data, position + 4), + resolutionX: readUint32(data, position + 8), + resolutionY: readUint32(data, position + 12) + }; + if (pageInfo.height == 0xFFFFFFFF) + delete pageInfo.height; + var pageSegmentFlags = data[position + 16]; + var pageStripingInformatiom = readUint16(data, position + 17); + pageInfo.lossless = !!(pageSegmentFlags & 1); + pageInfo.refinement = !!(pageSegmentFlags & 2); + pageInfo.defaultPixelValue = (pageSegmentFlags >> 2) & 1; + pageInfo.combinationOperator = (pageSegmentFlags >> 3) & 3; + pageInfo.requiresBuffer = !!(pageSegmentFlags & 32); + pageInfo.combinationOperatorOverride = !!(pageSegmentFlags & 64); + args = [pageInfo]; + break; + case 51: // EndOfFile + break; + default: + throw 'Segment type is not implemented: ' + + header.type + '/' + header.typeName; + } + visitor[header.typeName].apply(visitor, args); + } + + function processSegments(segments, visitor) { + for (var i = 0; i < segments.length; i++) + processSegment(segments[i], visitor); + } + + function parseJbig2(data, start, end) { + var position = start; + if (data[position] != 0x97 || data[position + 1] != 0x4A || + data[position + 2] != 0x42 || data[position + 3] != 0x32 || + data[position + 4] != 0x0D || data[position + 5] != 0x0A || + data[position + 6] != 0x1A || data[position + 7] != 0x0A) + throw 'JBIG2 invalid header'; + var header = {}; + position += 8; + var flags = data[position++]; + header.randomAccess = !(flags & 1); + if (!(flags & 2)) { + header.numberOfPages = readUint32(data, position); + position += 4; + } + var segments = readSegments(header, data, position, end); + // processSegments(segments, new SimpleSegmentVisitor()); + } + + function parseJbig2Chunk(data, start, end) { + var segments = readSegments({}, data, start, end); + var visitor = new SimpleSegmentVisitor(); + processSegments(segments, visitor); + return visitor.buffer; + } + + function SimpleSegmentVisitor() { + this.PageInformation = function(info) { + this.currentPageInfo = info; + var rowSize = (info.width + 7) >> 3; + var buffer = new Uint8Array(rowSize * info.height); + var fill = info.defaultPixelValue ? 0xFF : 0; + for (var i = 0; i < buffer.length; i++) + buffer[i] = fill; + this.buffer = buffer; + }; + this.ImmediateGenericRegion = function(region, data, start, end) { + var regionInfo = region.info, pageInfo = this.currentPageInfo; + var bitmap = decodeBitmap(region.mmr, regionInfo.width, regionInfo.height, + region.template, region.prediction, null, + region.at, data, start, end); + var width = regionInfo.width, height = regionInfo.height; + var rowSize = (pageInfo.width + 7) >> 3; + var operator = pageInfo.combinationOperatorOverride ? + regionInfo.combinationOperator : pageInfo.combinationOperator; + var buffer = this.buffer; + for (var i = 0; i < height; i++) { + var mask = 128 >> (regionInfo.x & 7); + var offset = (i + regionInfo.y) * rowSize + (regionInfo.x >> 3); + switch (operator) { + case 0: // OR + for (var j = 0; j < width; j++) { + buffer[offset] |= bitmap[i][j] ? mask : 0; + mask >>= 1; + if (!mask) { + mask = 128; + offset++; + } + } + break; + case 2: // XOR + for (var j = 0; j < width; j++) { + buffer[offset] ^= bitmap[i][j] ? mask : 0; + mask >>= 1; + if (!mask) { + mask = 128; + offset++; + } + } + break; + default: + throw 'Unimplemented operator: ' + operator; + } + } + }; + } + + function Jbig2Image() {} + + Jbig2Image.prototype = { + parseChunk: function Jbig2Image_parseChunk(data, start, end) { + return parseJbig2Chunk(data, start, end); + } + }; + + return Jbig2Image; +})(); diff --git a/src/parser.js b/src/parser.js index 6c9b4e67f..52f9eb1b0 100644 --- a/src/parser.js +++ b/src/parser.js @@ -253,7 +253,8 @@ var Parser = (function ParserClosure() { return new RunLengthStream(stream); } if (name == 'JBIG2Decode') { - error('JBIG2 image format is not currently supprted.'); + var bytes = stream.getBytes(length); + return new Jbig2Stream(bytes, stream.dict); } warn('filter "' + name + '" not supported yet'); return stream; diff --git a/src/stream.js b/src/stream.js index b0e1beb34..47704ea98 100644 --- a/src/stream.js +++ b/src/stream.js @@ -979,6 +979,43 @@ var JpxStream = (function JpxStreamClosure() { return JpxStream; })(); +/** + * For JBIG2's we use a library to decode these images and + * the stream behaves like all the other DecodeStreams. + */ +var Jbig2Stream = (function Jbig2StreamClosure() { + function Jbig2Stream(bytes, dict) { + this.dict = dict; + this.bytes = bytes; + + DecodeStream.call(this); + } + + Jbig2Stream.prototype = Object.create(DecodeStream.prototype); + + Jbig2Stream.prototype.ensureBuffer = function Jbig2Stream_ensureBuffer(req) { + if (this.bufferLength) + return; + + var jbig2Image = new Jbig2Image(); + + var data = jbig2Image.parseChunk(this.bytes, 0, this.bytes.length); + var dataLength = data.length; + + // JBIG2 had black as 1 and white as 0, inverting the colors + for (var i = 0; i < dataLength; i++) + data[i] ^= 0xFF; + + this.buffer = data; + this.bufferLength = dataLength; + }; + Jbig2Stream.prototype.getChar = function Jbig2Stream_getChar() { + error('internal error: getChar is not valid on Jbig2Stream'); + }; + + return Jbig2Stream; +})(); + var DecryptStream = (function DecryptStreamClosure() { function DecryptStream(str, decrypt) { this.str = str; diff --git a/src/worker_loader.js b/src/worker_loader.js index 6d92a95e7..66a83f325 100644 --- a/src/worker_loader.js +++ b/src/worker_loader.js @@ -24,6 +24,7 @@ var files = [ 'stream.js', 'worker.js', 'jpx.js', + 'jbig2.js', 'bidi.js', '../external/jpgjs/jpg.js' ]; diff --git a/test/test_slave.html b/test/test_slave.html index 7c2097110..a704f27ee 100644 --- a/test/test_slave.html +++ b/test/test_slave.html @@ -24,6 +24,7 @@ + From 3c874c9bb759284ea580db3a56d6dc80a080631e Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 18 Jun 2012 09:48:47 -0700 Subject: [PATCH 02/27] Change to priority rendering. --- src/api.js | 24 +++- web/viewer.js | 357 +++++++++++++++++++++++++++----------------------- 2 files changed, 214 insertions(+), 167 deletions(-) diff --git a/src/api.js b/src/api.js index 7d65f96b4..2ab23bdde 100644 --- a/src/api.js +++ b/src/api.js @@ -234,7 +234,11 @@ var PDFPageProxy = (function PDFPageProxyClosure() { * { * canvasContext(required): A 2D context of a DOM Canvas object., * textLayer(optional): An object that has beginLayout, endLayout, and - * appendText functions. + * appendText functions., + * continueCallback(optional): A function that will be called each time + * the rendering is paused. To continue + * rendering call the function that is the + * first argument to the callback. * }. * @return {Promise} A promise that is resolved when the page finishes * rendering. @@ -270,6 +274,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { else promise.resolve(); }; + var continueCallback = params.continueCallback; // Once the operatorList and fonts are loaded, do the actual rendering. this.displayReadyPromise.then( @@ -282,7 +287,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { var gfx = new CanvasGraphics(params.canvasContext, this.objs, params.textLayer); try { - this.display(gfx, params.viewport, complete); + this.display(gfx, params.viewport, complete, continueCallback); } catch (e) { complete(e); } @@ -340,7 +345,8 @@ var PDFPageProxy = (function PDFPageProxyClosure() { /** * For internal use only. */ - display: function PDFPageProxy_display(gfx, viewport, callback) { + display: function PDFPageProxy_display(gfx, viewport, callback, + continueCallback) { var stats = this.stats; stats.time('Rendering'); @@ -356,10 +362,16 @@ var PDFPageProxy = (function PDFPageProxyClosure() { stepper.nextBreakPoint = stepper.getNextBreakPoint(); } + var continueWrapper; + if (continueCallback) + continueWrapper = function() { continueCallback(next); } + else + continueWrapper = next; + var self = this; function next() { - startIdx = - gfx.executeOperatorList(operatorList, startIdx, next, stepper); + startIdx = gfx.executeOperatorList(operatorList, startIdx, + continueWrapper, stepper); if (startIdx == length) { gfx.endDrawing(); stats.timeEnd('Rendering'); @@ -367,7 +379,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { if (callback) callback(); } } - next(); + continueWrapper(); }, /** * @return {Promise} That is resolved with the a {string} that is the text diff --git a/web/viewer.js b/web/viewer.js index 29b57a948..040bf91b8 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -14,6 +14,13 @@ var kMinScale = 0.25; var kMaxScale = 4.0; var kImageDirectory = './images/'; var kSettingsMemory = 20; +var RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3 +}; + var mozL10n = document.mozL10n || document.webL10n; @@ -83,36 +90,6 @@ var ProgressBar = (function ProgressBarClosure() { return ProgressBar; })(); -var RenderingQueue = (function RenderingQueueClosure() { - function RenderingQueue() { - this.items = []; - } - - RenderingQueue.prototype = { - enqueueDraw: function RenderingQueueEnqueueDraw(item) { - if (!item.drawingRequired()) - return; // as no redraw required, no need for queueing. - - this.items.push(item); - if (this.items.length > 1) - return; // not first item - - item.draw(this.continueExecution.bind(this)); - }, - continueExecution: function RenderingQueueContinueExecution() { - var item = this.items.shift(); - - if (this.items.length == 0) - return; // queue is empty - - item = this.items[0]; - item.draw(this.continueExecution.bind(this)); - } - }; - - return RenderingQueue; -})(); - var FirefoxCom = (function FirefoxComClosure() { return { /** @@ -246,7 +223,6 @@ var Settings = (function SettingsClosure() { })(); var cache = new Cache(kCacheSize); -var renderingQueue = new RenderingQueue(); var currentPageNumber = 1; var PDFView = { @@ -258,15 +234,47 @@ var PDFView = { startedTextExtraction: false, pageText: [], container: null, + thumbnailContainer: null, initialized: false, fellback: false, pdfDocument: null, + sidebarOpen: false, + pageViewScroll: null, + thumbnailViewScroll: null, + // called once when the document is loaded initialize: function pdfViewInitialize() { - this.container = document.getElementById('viewerContainer'); + var container = this.container = document.getElementById('viewerContainer'); + this.pageViewScroll = {}; + this.watchScroll(container, this.pageViewScroll, updateViewarea); + + var thumbnailContainer = this.thumbnailContainer = + document.getElementById('thumbnailView'); + this.thumbnailViewScroll = {}; + this.watchScroll(thumbnailContainer, this.thumbnailViewScroll, + this.renderHighestPriority.bind(this)); + this.initialized = true; }, + // Helper function to keep track whether a div was scrolled up or down and + // then call a callback. + watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) { + state.down = true; + state.lastY = viewAreaElement.scrollTop; + viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) { + var currentY = viewAreaElement.scrollTop; + var lastY = state.lastY; + if (currentY > lastY) + state.down = true; + else if (currentY < lastY) + state.down = false; + // else do nothing and use previous value + state.lastY = currentY; + callback(); + }, true); + }, + setScale: function pdfViewSetScale(val, resetAutoSettings) { if (val == this.currentScale) return; @@ -610,7 +618,6 @@ var PDFView = { // when page is painted, using the image as thumbnail base pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { thumbnailView.setImage(pageView.canvas); - preDraw(); }; } @@ -735,6 +742,88 @@ var PDFView = { } }, + renderHighestPriority: function pdfViewRenderHighestPriority() { + // Pages have a higher priority than thumbnails, so check them first. + var visiblePages = this.getVisiblePages(); + var pageView = this.getHighestPriority(visiblePages, this.pages, + this.pageViewScroll.down); + if (pageView) { + this.renderView(pageView, 'page'); + return; + } + // No pages needed rendering so check thumbnails. + if (this.sidebarOpen) { + var visibleThumbs = this.getVisibleThumbs(); + var thumbView = this.getHighestPriority(visibleThumbs, + this.thumbnails, + this.thumbnailViewScroll.down); + if (thumbView) + this.renderView(thumbView, 'thumbnail'); + } + }, + + getHighestPriority: function pdfViewGetHighestPriority(visibleViews, views, + scrolledDown) { + // The state has changed figure out which page has the highest priority to + // render next (if any). + // Priority: + // 1 visible pages + // 2 if last scrolled down page after the visible pages + // 2 if last scrolled up page before the visible pages + var numVisible = visibleViews.length; + if (numVisible === 0) { + info('No visible views.'); + return false; + } + for (var i = 0; i < numVisible; ++i) { + var view = visibleViews[i].view; + if (!this.isViewFinshed(view)) + return view; + } + + // All the visible views have rendered, try to render next/previous pages. + if (scrolledDown) { + var lastVisible = visibleViews[visibleViews.length - 1]; + var nextPageIndex = lastVisible.id; + // ID's start at 1 so no need to add 1. + if (views[nextPageIndex] && !this.isViewFinshed(views[nextPageIndex])) + return views[nextPageIndex]; + } else { + var previousPageIndex = visibleViews[0].id - 2; + if (views[previousPageIndex] && + !this.isViewFinshed(views[previousPageIndex])) + return views[previousPageIndex]; + } + // Everything that needs to be rendered has been. + return false; + }, + + isViewFinshed: function pdfViewNeedsRendering(view) { + return view.renderingState === RenderingStates.FINISHED; + }, + + // Render a page or thumbnail view. This calls the appropriate function based + // on the views state. If the view is already rendered it will return false. + renderView: function pdfViewRender(view, type) { + var state = view.renderingState; + switch (state) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + PDFView.highestPriorityPage = type + view.id; + view.resume(); + break; + case RenderingStates.RUNNING: + PDFView.highestPriorityPage = type + view.id; + break; + case RenderingStates.INITIAL: + PDFView.highestPriorityPage = type + view.id; + view.draw(this.renderHighestPriority.bind(this)); + break; + } + return true; + }, + search: function pdfViewStartSearch() { // Limit this function to run every ms. var SEARCH_TIMEOUT = 250; @@ -854,7 +943,7 @@ var PDFView = { outlineView.classList.add('hidden'); searchView.classList.add('hidden'); - updateThumbViewArea(); + PDFView.renderHighestPriority(); break; case 'outline': @@ -904,63 +993,39 @@ var PDFView = { }, getVisiblePages: function pdfViewGetVisiblePages() { - var pages = this.pages; - var kBottomMargin = 10; - var kTopPadding = 30; - var visiblePages = []; - - var currentHeight = kTopPadding + kBottomMargin; - var container = this.container; - // Add 1px to the scrolltop to give a little wiggle room if the math is off, - // this won't be needed if we calc current page number based off the middle - // of the screen instead of the top. - var containerTop = container.scrollTop + 1; - for (var i = 1; i <= pages.length; ++i) { - var page = pages[i - 1]; - var pageHeight = page.height + kBottomMargin; - if (currentHeight + pageHeight > containerTop) - break; - - currentHeight += pageHeight; - } - - var containerBottom = containerTop + container.clientHeight; - for (; i <= pages.length && currentHeight < containerBottom; ++i) { - var singlePage = pages[i - 1]; - visiblePages.push({ id: singlePage.id, y: currentHeight, - view: singlePage }); - currentHeight += page.height + kBottomMargin; - } - return visiblePages; + return this.getVisibleElements(this.container, + this.pages); }, getVisibleThumbs: function pdfViewGetVisibleThumbs() { - var thumbs = this.thumbnails; - var kBottomMargin = 15; - var visibleThumbs = []; + return this.getVisibleElements(this.thumbnailContainer, + this.thumbnails); + }, - var view = document.getElementById('thumbnailView'); - var currentHeight = kBottomMargin; + // Generic helper to find out what elements are visible within a scroll pane. + getVisibleElements: function pdfViewGetVisibleElements(scrollEl, views) { + var currentHeight = 0, view; + var top = scrollEl.scrollTop; - var top = view.scrollTop; - for (var i = 1; i <= thumbs.length; ++i) { - var thumb = thumbs[i - 1]; - var thumbHeight = thumb.height * thumb.scaleY + kBottomMargin; - if (currentHeight + thumbHeight > top) + for (var i = 1; i <= views.length; ++i) { + view = views[i - 1]; + currentHeight = view.el.offsetTop; + if (currentHeight + view.el.clientHeight > top) break; - - currentHeight += thumbHeight; + currentHeight += view.el.clientHeight; } - var bottom = top + view.clientHeight; - for (; i <= thumbs.length && currentHeight < bottom; ++i) { - var singleThumb = thumbs[i - 1]; - visibleThumbs.push({ id: singleThumb.id, y: currentHeight, - view: singleThumb }); - currentHeight += singleThumb.height * singleThumb.scaleY + kBottomMargin; + var visible = []; + var bottom = top + scrollEl.clientHeight; + for (; i <= views.length && currentHeight < bottom; ++i) { + view = views[i - 1]; + currentHeight = view.el.offsetTop; + visible.push({ id: view.id, y: currentHeight, + view: view }); + currentHeight += view.el.clientHeight; } - return visibleThumbs; + return visible; }, // Helper function to parse query string (e.g. ?param1=value&parm2=...). @@ -985,10 +1050,13 @@ var PageView = function pageView(container, pdfPage, id, scale, this.scale = scale || 1.0; this.viewport = this.pdfPage.getViewport(this.scale); + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + var anchor = document.createElement('a'); anchor.name = '' + this.id; - var div = document.createElement('div'); + var div = this.el = document.createElement('div'); div.id = 'pageContainer' + this.id; div.className = 'page'; @@ -1001,6 +1069,9 @@ var PageView = function pageView(container, pdfPage, id, scale, }; this.update = function pageViewUpdate(scale) { + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + this.scale = scale || this.scale; var viewport = this.pdfPage.getViewport(this.scale); @@ -1203,16 +1274,11 @@ var PageView = function pageView(container, pdfPage, id, scale, }, 0); }; - this.drawingRequired = function() { - return !div.querySelector('canvas'); - }; - this.draw = function pageviewDraw(callback) { - if (!this.drawingRequired()) { - this.updateStats(); - callback(); - return; - } + if (this.renderingState !== RenderingStates.INITIAL) + error('Must be in new state before drawing'); + + this.renderingState = RenderingStates.RUNNING; var canvas = document.createElement('canvas'); canvas.id = 'page' + this.id; @@ -1242,6 +1308,8 @@ var PageView = function pageView(container, pdfPage, id, scale, var self = this; function pageViewDrawCallback(error) { + self.renderingState = RenderingStates.FINISHED; + if (self.loadingIconDiv) { div.removeChild(self.loadingIconDiv); delete self.loadingIconDiv; @@ -1264,7 +1332,18 @@ var PageView = function pageView(container, pdfPage, id, scale, var renderContext = { canvasContext: ctx, viewport: this.viewport, - textLayer: textLayer + textLayer: textLayer, + continueCallback: function pdfViewcContinueCallback(cont) { + if (PDFView.highestPriorityPage !== 'page' + self.id) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + } }; this.pdfPage.render(renderContext).then( function pdfPageRenderCallback() { @@ -1307,7 +1386,7 @@ var ThumbnailView = function thumbnailView(container, pdfPage, id) { var scaleX = this.scaleX = (canvasWidth / pageWidth); var scaleY = this.scaleY = (canvasHeight / pageHeight); - var div = document.createElement('div'); + var div = this.el = document.createElement('div'); div.id = 'thumbnailContainer' + id; div.className = 'thumbnail'; @@ -1315,6 +1394,7 @@ var ThumbnailView = function thumbnailView(container, pdfPage, id) { container.appendChild(anchor); this.hasImage = false; + this.renderingState = RenderingStates.INITIAL; function getPageDrawContext() { var canvas = document.createElement('canvas'); @@ -1347,22 +1427,40 @@ var ThumbnailView = function thumbnailView(container, pdfPage, id) { }; this.draw = function thumbnailViewDraw(callback) { + if (this.renderingState !== RenderingStates.INITIAL) + error('Must be in new state before drawing'); + + this.renderingState = RenderingStates.RUNNING; if (this.hasImage) { callback(); return; } + var self = this; var ctx = getPageDrawContext(); var drawViewport = pdfPage.getViewport(scaleX); var renderContext = { canvasContext: ctx, - viewport: drawViewport + viewport: drawViewport, + continueCallback: function(cont) { + if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + } }; pdfPage.render(renderContext).then( function pdfPageRenderCallback() { + self.renderingState = RenderingStates.FINISHED; callback(); }, function pdfPageRenderError(error) { + self.renderingState = RenderingStates.FINISHED; callback(); } ); @@ -1372,7 +1470,7 @@ var ThumbnailView = function thumbnailView(container, pdfPage, id) { this.setImage = function thumbnailViewSetImage(img) { if (this.hasImage || !img) return; - + this.renderingState = RenderingStates.FINISHED; var ctx = getPageDrawContext(); ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, ctx.canvas.width, ctx.canvas.height); @@ -1614,9 +1712,6 @@ window.addEventListener('load', function webViewerLoad(evt) { } }); - var thumbsView = document.getElementById('thumbnailView'); - thumbsView.addEventListener('scroll', updateThumbViewArea, true); - var mainContainer = document.getElementById('mainContainer'); var outerContainer = document.getElementById('outerContainer'); mainContainer.addEventListener('transitionend', function(e) { @@ -1633,56 +1728,19 @@ window.addEventListener('load', function webViewerLoad(evt) { this.classList.toggle('toggled'); outerContainer.classList.add('sidebarMoving'); outerContainer.classList.toggle('sidebarOpen'); - updateThumbViewArea(); + PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen'); + PDFView.renderHighestPriority(); }); PDFView.open(file, 0); }, true); -/** - * Render the next not yet visible page already such that it is - * hopefully ready once the user scrolls to it. - */ -function preDraw() { - var pages = PDFView.pages; - var visible = PDFView.getVisiblePages(); - var last = visible[visible.length - 1]; - // PageView.id is the actual page number, which is + 1 compared - // to the index in `pages`. That means, pages[last.id] is the next - // PageView instance. - if (pages[last.id] && pages[last.id].drawingRequired()) { - renderingQueue.enqueueDraw(pages[last.id]); - return; - } - // If there is nothing to draw on the next page, maybe the user - // is scrolling up, so, let's try to render the next page *before* - // the first visible page - if (pages[visible[0].id - 2]) { - renderingQueue.enqueueDraw(pages[visible[0].id - 2]); - } -} - function updateViewarea() { if (!PDFView.initialized) return; var visiblePages = PDFView.getVisiblePages(); - var pageToDraw; - for (var i = 0; i < visiblePages.length; i++) { - var page = visiblePages[i]; - var pageObj = PDFView.pages[page.id - 1]; - pageToDraw |= pageObj.drawingRequired(); - renderingQueue.enqueueDraw(pageObj); - } - - if (!visiblePages.length) - return; - - // If there is no need to draw a page that is currenlty visible, preDraw the - // next page the user might scroll to. - if (!pageToDraw) { - preDraw(); - } + PDFView.renderHighestPriority(); updateViewarea.inProgress = true; // used in "set page" var currentId = PDFView.page; @@ -1713,29 +1771,6 @@ function updateViewarea() { document.getElementById('viewBookmark').href = href; } -window.addEventListener('scroll', function webViewerScroll(evt) { - updateViewarea(); -}, true); - -var thumbnailTimer; - -function updateThumbViewArea() { - // Only render thumbs after pausing scrolling for this amount of time - // (makes UI more responsive) - var delay = 50; // in ms - - if (thumbnailTimer) - clearTimeout(thumbnailTimer); - - thumbnailTimer = setTimeout(function() { - var visibleThumbs = PDFView.getVisibleThumbs(); - for (var i = 0; i < visibleThumbs.length; i++) { - var thumb = visibleThumbs[i]; - renderingQueue.enqueueDraw(PDFView.thumbnails[thumb.id - 1]); - } - }, delay); -} - window.addEventListener('resize', function webViewerResize(evt) { if (PDFView.initialized && (document.getElementById('pageWidthOption').selected || From ffc27bca1ef0b71c260da0db82fd3e48962119dc Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 18 Jun 2012 10:49:00 -0700 Subject: [PATCH 03/27] Fix missing charset for iframe. --- src/fonts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fonts.js b/src/fonts.js index 72bcecbce..2e1702b9b 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -532,7 +532,7 @@ var FontLoader = { // XXX we should have a time-out here too, and maybe fire // pdfjsFontLoadFailed? - var src = ''; + var src = ''; src += '