Refactor PDFThumbnailView to look more similar to PDFPageView

This includes an optimization to zero the height and width of existing thumbnail canvases, when they are removed and recreated during rotation of the document. (Credit goes to nnethercote, who initially found this in PR 4920.)
This commit is contained in:
Jonas Jenwald 2015-01-25 13:27:11 +01:00
parent 7f8f404536
commit 878aa76c15
2 changed files with 110 additions and 84 deletions

View File

@ -18,7 +18,8 @@
'use strict'; 'use strict';
var THUMBNAIL_CANVAS_BORDER_WIDTH = 1; var THUMBNAIL_WIDTH = 98; // px
var THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px
/** /**
* @typedef {Object} PDFThumbnailViewOptions * @typedef {Object} PDFThumbnailViewOptions
@ -43,9 +44,8 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
tempCanvas.width = width; tempCanvas.width = width;
tempCanvas.height = height; tempCanvas.height = height;
// Since this is a temporary canvas, we need to fill // Since this is a temporary canvas, we need to fill the canvas with a white
// the canvas with a white background ourselves. // background ourselves. |_getPageDrawContext| uses CSS rules for this.
// |getPageDrawContext| uses CSS rules for this.
var ctx = tempCanvas.getContext('2d'); var ctx = tempCanvas.getContext('2d');
ctx.save(); ctx.save();
ctx.fillStyle = 'rgb(255, 255, 255)'; ctx.fillStyle = 'rgb(255, 255, 255)';
@ -65,6 +65,29 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
var linkService = options.linkService; var linkService = options.linkService;
var renderingQueue = options.renderingQueue; var renderingQueue = options.renderingQueue;
this.id = id;
this.renderingId = 'thumbnail' + id;
this.pdfPage = null;
this.rotation = 0;
this.viewport = defaultViewport;
this.pdfPageRotate = defaultViewport.rotation;
this.linkService = linkService;
this.renderingQueue = renderingQueue;
this.hasImage = false;
this.resume = null;
this.renderingState = RenderingStates.INITIAL;
this.pageWidth = this.viewport.width;
this.pageHeight = this.viewport.height;
this.pageRatio = this.pageWidth / this.pageHeight;
this.canvasWidth = THUMBNAIL_WIDTH;
this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0;
this.scale = this.canvasWidth / this.pageWidth;
var anchor = document.createElement('a'); var anchor = document.createElement('a');
anchor.href = linkService.getAnchorUrl('#page=' + id); anchor.href = linkService.getAnchorUrl('#page=' + id);
anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}');
@ -73,24 +96,10 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
return false; return false;
}; };
this.pdfPage = undefined; var div = document.createElement('div');
this.viewport = defaultViewport;
this.pdfPageRotate = defaultViewport.rotation;
this.rotation = 0;
this.pageWidth = this.viewport.width;
this.pageHeight = this.viewport.height;
this.pageRatio = this.pageWidth / this.pageHeight;
this.id = id;
this.renderingId = 'thumbnail' + id;
this.canvasWidth = 98;
this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0;
this.scale = this.canvasWidth / this.pageWidth;
var div = this.el = document.createElement('div');
div.id = 'thumbnailContainer' + id; div.id = 'thumbnailContainer' + id;
div.className = 'thumbnail'; div.className = 'thumbnail';
this.el = div; // TODO: replace 'el' property usage.
this.div = div; this.div = div;
if (id === 1) { if (id === 1) {
@ -101,19 +110,14 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
var ring = document.createElement('div'); var ring = document.createElement('div');
ring.className = 'thumbnailSelectionRing'; ring.className = 'thumbnailSelectionRing';
ring.style.width = var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
this.canvasWidth + 2 * THUMBNAIL_CANVAS_BORDER_WIDTH + 'px'; ring.style.width = this.canvasWidth + borderAdjustment + 'px';
ring.style.height = ring.style.height = this.canvasHeight + borderAdjustment + 'px';
this.canvasHeight + 2 * THUMBNAIL_CANVAS_BORDER_WIDTH + 'px';
this.ring = ring; this.ring = ring;
div.appendChild(ring); div.appendChild(ring);
anchor.appendChild(div); anchor.appendChild(div);
container.appendChild(anchor); container.appendChild(anchor);
this.hasImage = false;
this.renderingState = RenderingStates.INITIAL;
this.renderingQueue = renderingQueue;
} }
PDFThumbnailView.prototype = { PDFThumbnailView.prototype = {
@ -122,18 +126,14 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
this.pdfPageRotate = pdfPage.rotate; this.pdfPageRotate = pdfPage.rotate;
var totalRotation = (this.rotation + this.pdfPageRotate) % 360; var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
this.viewport = pdfPage.getViewport(1, totalRotation); this.viewport = pdfPage.getViewport(1, totalRotation);
this.update(); this.reset();
}, },
update: function PDFThumbnailView_update(rotation) { reset: function PDFThumbnailView_reset() {
if (rotation !== undefined) { this.hasImage = false;
this.rotation = rotation; this.resume = null;
} this.renderingState = RenderingStates.INITIAL;
var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
this.viewport = this.viewport.clone({
scale: 1,
rotation: totalRotation
});
this.pageWidth = this.viewport.width; this.pageWidth = this.viewport.width;
this.pageHeight = this.viewport.height; this.pageHeight = this.viewport.height;
this.pageRatio = this.pageWidth / this.pageHeight; this.pageRatio = this.pageWidth / this.pageHeight;
@ -142,20 +142,42 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
this.scale = (this.canvasWidth / this.pageWidth); this.scale = (this.canvasWidth / this.pageWidth);
this.div.removeAttribute('data-loaded'); this.div.removeAttribute('data-loaded');
this.ring.textContent = ''; var ring = this.ring;
this.ring.style.width = var childNodes = ring.childNodes;
this.canvasWidth + 2 * THUMBNAIL_CANVAS_BORDER_WIDTH + 'px'; for (var i = childNodes.length - 1; i >= 0; i--) {
this.ring.style.height = ring.removeChild(childNodes[i]);
this.canvasHeight + 2 * THUMBNAIL_CANVAS_BORDER_WIDTH + 'px'; }
var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
ring.style.width = this.canvasWidth + borderAdjustment + 'px';
ring.style.height = this.canvasHeight + borderAdjustment + 'px';
this.hasImage = false; if (this.canvas) {
this.renderingState = RenderingStates.INITIAL; // Zeroing the width and height causes Firefox to release graphics
this.resume = null; // resources immediately, which can greatly reduce memory consumption.
this.canvas.width = 0;
this.canvas.height = 0;
delete this.canvas;
}
}, },
getPageDrawContext: function PDFThumbnailView_getPageDrawContext() { update: function PDFThumbnailView_update(rotation) {
if (typeof rotation !== 'undefined') {
this.rotation = rotation;
}
var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
this.viewport = this.viewport.clone({
scale: 1,
rotation: totalRotation
});
this.reset();
},
/**
* @private
*/
_getPageDrawContext: function PDFThumbnailView_getPageDrawContext() {
var canvas = document.createElement('canvas'); var canvas = document.createElement('canvas');
canvas.id = 'thumbnail' + this.id; canvas.id = this.renderingId;
canvas.width = this.canvasWidth; canvas.width = this.canvasWidth;
canvas.height = this.canvasHeight; canvas.height = this.canvasHeight;
@ -163,8 +185,8 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas',
{page: this.id}, 'Thumbnail of Page {{page}}')); {page: this.id}, 'Thumbnail of Page {{page}}'));
this.canvas = canvas;
this.div.setAttribute('data-loaded', true); this.div.setAttribute('data-loaded', true);
this.ring.appendChild(canvas); this.ring.appendChild(canvas);
return canvas.getContext('2d'); return canvas.getContext('2d');
@ -178,25 +200,25 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
if (this.renderingState !== RenderingStates.INITIAL) { if (this.renderingState !== RenderingStates.INITIAL) {
console.error('Must be in new state before drawing'); console.error('Must be in new state before drawing');
} }
this.renderingState = RenderingStates.RUNNING;
if (this.hasImage) { if (this.hasImage) {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
this.hasImage = true;
this.renderingState = RenderingStates.RUNNING;
var resolveRenderPromise, rejectRenderPromise; var resolveRenderPromise, rejectRenderPromise;
var promise = new Promise(function (resolve, reject) { var promise = new Promise(function (resolve, reject) {
resolveRenderPromise = resolve; resolveRenderPromise = resolve;
rejectRenderPromise = reject; rejectRenderPromise = reject;
}); });
var self = this; var self = this;
var ctx = this.getPageDrawContext(); var ctx = this._getPageDrawContext();
var drawViewport = this.viewport.clone({ scale: this.scale }); var drawViewport = this.viewport.clone({ scale: this.scale });
var renderContext = { var renderContext = {
canvasContext: ctx, canvasContext: ctx,
viewport: drawViewport, viewport: drawViewport,
continueCallback: function(cont) { continueCallback: function renderContinueCallback(cont) {
if (!self.renderingQueue.isHighestPriority(self)) { if (!self.renderingQueue.isHighestPriority(self)) {
self.renderingState = RenderingStates.PAUSED; self.renderingState = RenderingStates.PAUSED;
self.resume = function() { self.resume = function() {
@ -218,7 +240,6 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
rejectRenderPromise(error); rejectRenderPromise(error);
} }
); );
this.hasImage = true;
return promise; return promise;
}, },
@ -230,41 +251,39 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
if (!this.pdfPage) { if (!this.pdfPage) {
this.setPdfPage(pageView.pdfPage); this.setPdfPage(pageView.pdfPage);
} }
this.hasImage = true;
this.renderingState = RenderingStates.FINISHED; this.renderingState = RenderingStates.FINISHED;
var ctx = this.getPageDrawContext();
var ctx = this._getPageDrawContext();
var canvas = ctx.canvas; var canvas = ctx.canvas;
if (img.width <= 2 * canvas.width) { if (img.width <= 2 * canvas.width) {
ctx.drawImage(img, 0, 0, img.width, img.height, ctx.drawImage(img, 0, 0, img.width, img.height,
0, 0, canvas.width, canvas.height); 0, 0, canvas.width, canvas.height);
} else { return;
// drawImage does an awful job of rescaling the image,
// doing it gradually.
var MAX_NUM_SCALING_STEPS = 3;
var reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS;
var reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
var reducedImage = getTempCanvas(reducedWidth, reducedHeight);
var reducedImageCtx = reducedImage.getContext('2d');
while (reducedWidth > img.width || reducedHeight > img.height) {
reducedWidth >>= 1;
reducedHeight >>= 1;
}
reducedImageCtx.drawImage(img, 0, 0, img.width, img.height,
0, 0, reducedWidth, reducedHeight);
while (reducedWidth > 2 * canvas.width) {
reducedImageCtx.drawImage(reducedImage,
0, 0, reducedWidth, reducedHeight,
0, 0,
reducedWidth >> 1, reducedHeight >> 1);
reducedWidth >>= 1;
reducedHeight >>= 1;
}
ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight,
0, 0, canvas.width, canvas.height);
} }
// drawImage does an awful job of rescaling the image, doing it gradually.
var MAX_NUM_SCALING_STEPS = 3;
var reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS;
var reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
var reducedImage = getTempCanvas(reducedWidth, reducedHeight);
var reducedImageCtx = reducedImage.getContext('2d');
this.hasImage = true; while (reducedWidth > img.width || reducedHeight > img.height) {
reducedWidth >>= 1;
reducedHeight >>= 1;
}
reducedImageCtx.drawImage(img, 0, 0, img.width, img.height,
0, 0, reducedWidth, reducedHeight);
while (reducedWidth > 2 * canvas.width) {
reducedImageCtx.drawImage(reducedImage,
0, 0, reducedWidth, reducedHeight,
0, 0, reducedWidth >> 1, reducedHeight >> 1);
reducedWidth >>= 1;
reducedHeight >>= 1;
}
ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight,
0, 0, canvas.width, canvas.height);
} }
}; };

View File

@ -104,6 +104,13 @@ var PDFThumbnailViewer = (function PDFThumbnailViewerClosure() {
}, },
cleanup: function PDFThumbnailViewer_cleanup() { cleanup: function PDFThumbnailViewer_cleanup() {
var tempCanvas = PDFThumbnailView.tempImageCache;
if (tempCanvas) {
// Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption.
tempCanvas.width = 0;
tempCanvas.height = 0;
}
PDFThumbnailView.tempImageCache = null; PDFThumbnailView.tempImageCache = null;
}, },