| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * ImageLoader loads an image from a given Entry into a canvas in two steps: |
| 7 * 1. Loads the image into an HTMLImageElement. |
| 8 * 2. Copies pixels from HTMLImageElement to HTMLCanvasElement. This is done |
| 9 * stripe-by-stripe to avoid freezing up the UI. The transform is taken into |
| 10 * account. |
| 11 * |
| 12 * @param {!HTMLDocument} document Owner document. |
| 13 * @param {!MetadataModel} metadataModel |
| 14 * @constructor |
| 15 * @struct |
| 16 */ |
| 17 ImageUtil.ImageLoader = function(document, metadataModel) { |
| 18 this.document_ = document; |
| 19 |
| 20 /** |
| 21 * @private {!MetadataModel} |
| 22 * @const |
| 23 */ |
| 24 this.metadataModel_ = metadataModel; |
| 25 |
| 26 this.generation_ = 0; |
| 27 |
| 28 /** |
| 29 * @type {number} |
| 30 * @private |
| 31 */ |
| 32 this.timeout_ = 0; |
| 33 |
| 34 /** |
| 35 * @type {?function(!HTMLCanvasElement, string=)} |
| 36 * @private |
| 37 */ |
| 38 this.callback_ = null; |
| 39 |
| 40 /** |
| 41 * @type {FileEntry} |
| 42 * @private |
| 43 */ |
| 44 this.entry_ = null; |
| 45 }; |
| 46 |
| 47 /** |
| 48 * Loads an image. |
| 49 * TODO(mtomasz): Simplify, or even get rid of this class and merge with the |
| 50 * ThumbnaiLoader class. |
| 51 * |
| 52 * @param {!GalleryItem} item Item representing the image to be loaded. |
| 53 * @param {function(!HTMLCanvasElement, string=)} callback Callback to be |
| 54 * called when loaded. The second optional argument is an error identifier. |
| 55 * @param {number=} opt_delay Load delay in milliseconds, useful to let the |
| 56 * animations play out before the computation heavy image loading starts. |
| 57 */ |
| 58 ImageUtil.ImageLoader.prototype.load = function(item, callback, opt_delay) { |
| 59 var entry = item.getEntry(); |
| 60 |
| 61 this.cancel(); |
| 62 this.entry_ = entry; |
| 63 this.callback_ = callback; |
| 64 |
| 65 var targetImage = assertInstanceof(this.document_.createElement('img'), |
| 66 HTMLImageElement); |
| 67 // The transform fetcher is not cancellable so we need a generation counter. |
| 68 var generation = ++this.generation_; |
| 69 |
| 70 /** |
| 71 * @param {!HTMLImageElement} image Image to be transformed. |
| 72 * @param {Object=} opt_transform Transformation. |
| 73 */ |
| 74 var onTransform = function(image, opt_transform) { |
| 75 if (generation === this.generation_) { |
| 76 this.convertImage_(image, opt_transform); |
| 77 } |
| 78 }; |
| 79 onTransform = onTransform.bind(this); |
| 80 |
| 81 /** |
| 82 * @param {string=} opt_error Error. |
| 83 */ |
| 84 var onError = function(opt_error) { |
| 85 targetImage.onerror = null; |
| 86 targetImage.onload = null; |
| 87 var tmpCallback = this.callback_; |
| 88 this.callback_ = null; |
| 89 var emptyCanvas = assertInstanceof(this.document_.createElement('canvas'), |
| 90 HTMLCanvasElement); |
| 91 emptyCanvas.width = 0; |
| 92 emptyCanvas.height = 0; |
| 93 tmpCallback(emptyCanvas, opt_error); |
| 94 }; |
| 95 onError = onError.bind(this); |
| 96 |
| 97 var loadImage = function(url) { |
| 98 if (generation !== this.generation_) |
| 99 return; |
| 100 |
| 101 metrics.startInterval(ImageUtil.getMetricName('LoadTime')); |
| 102 this.timeout_ = 0; |
| 103 |
| 104 targetImage.onload = function() { |
| 105 targetImage.onerror = null; |
| 106 targetImage.onload = null; |
| 107 if (generation !== this.generation_) |
| 108 return; |
| 109 this.metadataModel_.get([entry], ['contentImageTransform']).then( |
| 110 function(metadataItems) { |
| 111 onTransform(targetImage, metadataItems[0].contentImageTransform); |
| 112 }.bind(this)); |
| 113 }.bind(this); |
| 114 |
| 115 // The error callback has an optional error argument, which in case of a |
| 116 // general error should not be specified |
| 117 targetImage.onerror = onError.bind(null, 'GALLERY_IMAGE_ERROR'); |
| 118 |
| 119 targetImage.src = url; |
| 120 }.bind(this); |
| 121 |
| 122 // Loads the image. If already loaded, then forces a reload. |
| 123 var startLoad = function() { |
| 124 if (generation !== this.generation_) |
| 125 return; |
| 126 |
| 127 // Obtain target URL. |
| 128 if (FileType.isRaw(entry)) { |
| 129 var timestamp = |
| 130 item.getMetadataItem() && |
| 131 item.getMetadataItem().modificationTime && |
| 132 item.getMetadataItem().modificationTime.getTime(); |
| 133 ImageLoaderClient.getInstance().load(entry.toURL(), function(result) { |
| 134 if (generation !== this.generation_) |
| 135 return; |
| 136 if (result.status === 'success') |
| 137 loadImage(result.data); |
| 138 else |
| 139 onError('GALLERY_IMAGE_ERROR'); |
| 140 }.bind(this), { |
| 141 cache: true, |
| 142 timestamp: timestamp, |
| 143 priority: 0 // Use highest priority to show main image. |
| 144 }); |
| 145 return; |
| 146 } |
| 147 |
| 148 // Load the image directly. The query parameter is workaround for |
| 149 // crbug.com/379678, which force to update the contents of the image. |
| 150 loadImage(entry.toURL() + '?nocache=' + Date.now()); |
| 151 }.bind(this); |
| 152 |
| 153 if (opt_delay) { |
| 154 this.timeout_ = setTimeout(startLoad, opt_delay); |
| 155 } else { |
| 156 startLoad(); |
| 157 } |
| 158 }; |
| 159 |
| 160 /** |
| 161 * @return {boolean} True if an image is loading. |
| 162 */ |
| 163 ImageUtil.ImageLoader.prototype.isBusy = function() { |
| 164 return !!this.callback_; |
| 165 }; |
| 166 |
| 167 /** |
| 168 * @param {Entry} entry Image entry. |
| 169 * @return {boolean} True if loader loads this image. |
| 170 */ |
| 171 ImageUtil.ImageLoader.prototype.isLoading = function(entry) { |
| 172 return this.isBusy() && util.isSameEntry(this.entry_, entry); |
| 173 }; |
| 174 |
| 175 /** |
| 176 * @param {function(!HTMLCanvasElement, string=)} callback To be called when the |
| 177 * image loaded. |
| 178 */ |
| 179 ImageUtil.ImageLoader.prototype.setCallback = function(callback) { |
| 180 this.callback_ = callback; |
| 181 }; |
| 182 |
| 183 /** |
| 184 * Stops loading image. |
| 185 */ |
| 186 ImageUtil.ImageLoader.prototype.cancel = function() { |
| 187 if (!this.callback_) |
| 188 return; |
| 189 this.callback_ = null; |
| 190 if (this.timeout_) { |
| 191 clearTimeout(this.timeout_); |
| 192 this.timeout_ = 0; |
| 193 } |
| 194 this.generation_++; // Silence the transform fetcher if it is in progress. |
| 195 }; |
| 196 |
| 197 /** |
| 198 * @param {!HTMLImageElement} image Image to be transformed. |
| 199 * @param {!Object} transform transformation description to apply to the image. |
| 200 * @private |
| 201 */ |
| 202 ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) { |
| 203 if (!transform || |
| 204 (transform.rotate90 === 0 && |
| 205 transform.scaleX === 1 && |
| 206 transform.scaleY === 1)) { |
| 207 setTimeout(this.callback_, 0, image); |
| 208 this.callback_ = null; |
| 209 return; |
| 210 } |
| 211 var canvas = this.document_.createElement('canvas'); |
| 212 |
| 213 if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions. |
| 214 canvas.width = image.height; |
| 215 canvas.height = image.width; |
| 216 } else { |
| 217 canvas.width = image.width; |
| 218 canvas.height = image.height; |
| 219 } |
| 220 |
| 221 var context = canvas.getContext('2d'); |
| 222 context.save(); |
| 223 context.translate(canvas.width / 2, canvas.height / 2); |
| 224 context.rotate(transform.rotate90 * Math.PI / 2); |
| 225 context.scale(transform.scaleX, transform.scaleY); |
| 226 |
| 227 var stripCount = Math.ceil(image.width * image.height / (1 << 21)); |
| 228 var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0; |
| 229 |
| 230 this.copyStrip_(context, image, 0, step); |
| 231 }; |
| 232 |
| 233 /** |
| 234 * @param {!CanvasRenderingContext2D} context Context to draw. |
| 235 * @param {!HTMLImageElement} image Image to draw. |
| 236 * @param {number} firstRow Number of the first pixel row to draw. |
| 237 * @param {number} rowCount Count of pixel rows to draw. |
| 238 * @private |
| 239 */ |
| 240 ImageUtil.ImageLoader.prototype.copyStrip_ = function( |
| 241 context, image, firstRow, rowCount) { |
| 242 var lastRow = Math.min(firstRow + rowCount, image.height); |
| 243 |
| 244 context.drawImage( |
| 245 image, 0, firstRow, image.width, lastRow - firstRow, |
| 246 -image.width / 2, firstRow - image.height / 2, |
| 247 image.width, lastRow - firstRow); |
| 248 |
| 249 if (lastRow === image.height) { |
| 250 context.restore(); |
| 251 if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls. |
| 252 metrics.recordInterval(ImageUtil.getMetricName('LoadTime')); |
| 253 } |
| 254 setTimeout(this.callback_, 0, context.canvas); |
| 255 this.callback_ = null; |
| 256 } else { |
| 257 var self = this; |
| 258 this.timeout_ = setTimeout( |
| 259 function() { |
| 260 self.timeout_ = 0; |
| 261 self.copyStrip_(context, image, lastRow, rowCount); |
| 262 }, 0); |
| 263 } |
| 264 }; |
| 265 |
| OLD | NEW |