OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 'use strict'; | |
6 | |
7 /** | |
8 * Loads a thumbnail using provided url. In CANVAS mode, loaded images | |
9 * are attached as <canvas> element, while in IMAGE mode as <img>. | |
10 * <canvas> renders faster than <img>, however has bigger memory overhead. | |
11 * | |
12 * @param {FileEntry} entry File entry. | |
13 * @param {ThumbnailLoader.LoaderType=} opt_loaderType Canvas or Image loader, | |
14 * default: IMAGE. | |
15 * @param {Object=} opt_metadata Metadata object. | |
16 * @param {string=} opt_mediaType Media type. | |
17 * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded | |
18 * jpeg thumbnail if available. Default: USE_EMBEDDED. | |
19 * @param {number=} opt_priority Priority, the highest is 0. default: 2. | |
20 * @constructor | |
21 */ | |
22 function ThumbnailLoader(entry, opt_loaderType, opt_metadata, opt_mediaType, | |
23 opt_useEmbedded, opt_priority) { | |
24 opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED; | |
25 | |
26 this.mediaType_ = opt_mediaType || FileType.getMediaType(entry); | |
27 this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE; | |
28 this.metadata_ = opt_metadata; | |
29 this.priority_ = (opt_priority !== undefined) ? opt_priority : 2; | |
30 this.transform_ = null; | |
31 | |
32 if (!opt_metadata) { | |
33 this.thumbnailUrl_ = entry.toURL(); // Use the URL directly. | |
34 return; | |
35 } | |
36 | |
37 this.fallbackUrl_ = null; | |
38 this.thumbnailUrl_ = null; | |
39 if (opt_metadata.drive && opt_metadata.drive.customIconUrl) | |
40 this.fallbackUrl_ = opt_metadata.drive.customIconUrl; | |
41 | |
42 // Fetch the rotation from the Drive metadata (if available). | |
43 var driveTransform; | |
44 if (opt_metadata.drive && opt_metadata.drive.imageRotation !== undefined) { | |
45 driveTransform = { | |
46 scaleX: 1, | |
47 scaleY: 1, | |
48 rotate90: opt_metadata.drive.imageRotation / 90 | |
49 }; | |
50 } | |
51 | |
52 if (opt_metadata.thumbnail && opt_metadata.thumbnail.url && | |
53 opt_useEmbedded === ThumbnailLoader.UseEmbedded.USE_EMBEDDED) { | |
54 this.thumbnailUrl_ = opt_metadata.thumbnail.url; | |
55 this.transform_ = driveTransform !== undefined ? driveTransform : | |
56 opt_metadata.thumbnail.transform; | |
57 } else if (FileType.isImage(entry)) { | |
58 this.thumbnailUrl_ = entry.toURL(); | |
59 this.transform_ = driveTransform !== undefined ? driveTransform : | |
60 opt_metadata.media && opt_metadata.media.imageTransform; | |
61 } else if (this.fallbackUrl_) { | |
62 // Use fallback as the primary thumbnail. | |
63 this.thumbnailUrl_ = this.fallbackUrl_; | |
64 this.fallbackUrl_ = null; | |
65 } // else the generic thumbnail based on the media type will be used. | |
66 } | |
67 | |
68 /** | |
69 * In percents (0.0 - 1.0), how much area can be cropped to fill an image | |
70 * in a container, when loading a thumbnail in FillMode.AUTO mode. | |
71 * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element. | |
72 * @type {number} | |
73 */ | |
74 ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3; | |
75 | |
76 /** | |
77 * Type of displaying a thumbnail within a box. | |
78 * @enum {number} | |
79 */ | |
80 ThumbnailLoader.FillMode = { | |
81 FILL: 0, // Fill whole box. Image may be cropped. | |
82 FIT: 1, // Keep aspect ratio, do not crop. | |
83 OVER_FILL: 2, // Fill whole box with possible stretching. | |
84 AUTO: 3 // Try to fill, but if incompatible aspect ratio, then fit. | |
85 }; | |
86 | |
87 /** | |
88 * Optimization mode for downloading thumbnails. | |
89 * @enum {number} | |
90 */ | |
91 ThumbnailLoader.OptimizationMode = { | |
92 NEVER_DISCARD: 0, // Never discards downloading. No optimization. | |
93 DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore. | |
94 }; | |
95 | |
96 /** | |
97 * Type of element to store the image. | |
98 * @enum {number} | |
99 */ | |
100 ThumbnailLoader.LoaderType = { | |
101 IMAGE: 0, | |
102 CANVAS: 1 | |
103 }; | |
104 | |
105 /** | |
106 * Whether to use the embedded thumbnail, or not. The embedded thumbnail may | |
107 * be small. | |
108 * @enum {number} | |
109 */ | |
110 ThumbnailLoader.UseEmbedded = { | |
111 USE_EMBEDDED: 0, | |
112 NO_EMBEDDED: 1 | |
113 }; | |
114 | |
115 /** | |
116 * Maximum thumbnail's width when generating from the full resolution image. | |
117 * @const | |
118 * @type {number} | |
119 */ | |
120 ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500; | |
121 | |
122 /** | |
123 * Maximum thumbnail's height when generating from the full resolution image. | |
124 * @const | |
125 * @type {number} | |
126 */ | |
127 ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500; | |
128 | |
129 /** | |
130 * Loads and attaches an image. | |
131 * | |
132 * @param {HTMLElement} box Container element. | |
133 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. | |
134 * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization | |
135 * for downloading thumbnails. By default optimizations are disabled. | |
136 * @param {function(Image, Object)} opt_onSuccess Success callback, | |
137 * accepts the image and the transform. | |
138 * @param {function} opt_onError Error callback. | |
139 * @param {function} opt_onGeneric Callback for generic image used. | |
140 */ | |
141 ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode, | |
142 opt_onSuccess, opt_onError, opt_onGeneric) { | |
143 opt_optimizationMode = opt_optimizationMode || | |
144 ThumbnailLoader.OptimizationMode.NEVER_DISCARD; | |
145 | |
146 if (!this.thumbnailUrl_) { | |
147 // Relevant CSS rules are in file_types.css. | |
148 box.setAttribute('generic-thumbnail', this.mediaType_); | |
149 if (opt_onGeneric) opt_onGeneric(); | |
150 return; | |
151 } | |
152 | |
153 this.cancel(); | |
154 this.canvasUpToDate_ = false; | |
155 this.image_ = new Image(); | |
156 this.image_.onload = function() { | |
157 this.attachImage(box, fillMode); | |
158 if (opt_onSuccess) | |
159 opt_onSuccess(this.image_, this.transform_); | |
160 }.bind(this); | |
161 this.image_.onerror = function() { | |
162 if (opt_onError) | |
163 opt_onError(); | |
164 if (this.fallbackUrl_) { | |
165 this.thumbnailUrl_ = this.fallbackUrl_; | |
166 this.fallbackUrl_ = null; | |
167 this.load(box, fillMode, opt_optimizationMode, opt_onSuccess); | |
168 } else { | |
169 box.setAttribute('generic-thumbnail', this.mediaType_); | |
170 } | |
171 }.bind(this); | |
172 | |
173 if (this.image_.src) { | |
174 console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_); | |
175 return; | |
176 } | |
177 | |
178 // TODO(mtomasz): Smarter calculation of the requested size. | |
179 var wasAttached = box.ownerDocument.contains(box); | |
180 var modificationTime = this.metadata_ && | |
181 this.metadata_.filesystem && | |
182 this.metadata_.filesystem.modificationTime && | |
183 this.metadata_.filesystem.modificationTime.getTime(); | |
184 this.taskId_ = util.loadImage( | |
185 this.image_, | |
186 this.thumbnailUrl_, | |
187 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, | |
188 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, | |
189 cache: true, | |
190 priority: this.priority_, | |
191 timestamp: modificationTime }, | |
192 function() { | |
193 if (opt_optimizationMode == | |
194 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED && | |
195 !box.ownerDocument.contains(box)) { | |
196 // If the container is not attached, then invalidate the download. | |
197 return false; | |
198 } | |
199 return true; | |
200 }); | |
201 }; | |
202 | |
203 /** | |
204 * Cancels loading the current image. | |
205 */ | |
206 ThumbnailLoader.prototype.cancel = function() { | |
207 if (this.taskId_) { | |
208 this.image_.onload = function() {}; | |
209 this.image_.onerror = function() {}; | |
210 util.cancelLoadImage(this.taskId_); | |
211 this.taskId_ = null; | |
212 } | |
213 }; | |
214 | |
215 /** | |
216 * @return {boolean} True if a valid image is loaded. | |
217 */ | |
218 ThumbnailLoader.prototype.hasValidImage = function() { | |
219 return !!(this.image_ && this.image_.width && this.image_.height); | |
220 }; | |
221 | |
222 /** | |
223 * @return {boolean} True if the image is rotated 90 degrees left or right. | |
224 * @private | |
225 */ | |
226 ThumbnailLoader.prototype.isRotated_ = function() { | |
227 return this.transform_ && (this.transform_.rotate90 % 2 === 1); | |
228 }; | |
229 | |
230 /** | |
231 * @return {number} Image width (corrected for rotation). | |
232 */ | |
233 ThumbnailLoader.prototype.getWidth = function() { | |
234 return this.isRotated_() ? this.image_.height : this.image_.width; | |
235 }; | |
236 | |
237 /** | |
238 * @return {number} Image height (corrected for rotation). | |
239 */ | |
240 ThumbnailLoader.prototype.getHeight = function() { | |
241 return this.isRotated_() ? this.image_.width : this.image_.height; | |
242 }; | |
243 | |
244 /** | |
245 * Load an image but do not attach it. | |
246 * | |
247 * @param {function(boolean)} callback Callback, parameter is true if the image | |
248 * has loaded successfully or a stock icon has been used. | |
249 */ | |
250 ThumbnailLoader.prototype.loadDetachedImage = function(callback) { | |
251 if (!this.thumbnailUrl_) { | |
252 callback(true); | |
253 return; | |
254 } | |
255 | |
256 this.cancel(); | |
257 this.canvasUpToDate_ = false; | |
258 this.image_ = new Image(); | |
259 this.image_.onload = callback.bind(null, true); | |
260 this.image_.onerror = callback.bind(null, false); | |
261 | |
262 // TODO(mtomasz): Smarter calculation of the requested size. | |
263 var modificationTime = this.metadata_ && | |
264 this.metadata_.filesystem && | |
265 this.metadata_.filesystem.modificationTime && | |
266 this.metadata_.filesystem.modificationTime.getTime(); | |
267 this.taskId_ = util.loadImage( | |
268 this.image_, | |
269 this.thumbnailUrl_, | |
270 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH, | |
271 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT, | |
272 cache: true, | |
273 priority: this.priority_, | |
274 timestamp: modificationTime }); | |
275 }; | |
276 | |
277 /** | |
278 * Renders the thumbnail into either canvas or an image element. | |
279 * @private | |
280 */ | |
281 ThumbnailLoader.prototype.renderMedia_ = function() { | |
282 if (this.loaderType_ !== ThumbnailLoader.LoaderType.CANVAS) | |
283 return; | |
284 | |
285 if (!this.canvas_) | |
286 this.canvas_ = document.createElement('canvas'); | |
287 | |
288 // Copy the image to a canvas if the canvas is outdated. | |
289 if (!this.canvasUpToDate_) { | |
290 this.canvas_.width = this.image_.width; | |
291 this.canvas_.height = this.image_.height; | |
292 var context = this.canvas_.getContext('2d'); | |
293 context.drawImage(this.image_, 0, 0); | |
294 this.canvasUpToDate_ = true; | |
295 } | |
296 }; | |
297 | |
298 /** | |
299 * Attach the image to a given element. | |
300 * @param {Element} container Parent element. | |
301 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. | |
302 */ | |
303 ThumbnailLoader.prototype.attachImage = function(container, fillMode) { | |
304 if (!this.hasValidImage()) { | |
305 container.setAttribute('generic-thumbnail', this.mediaType_); | |
306 return; | |
307 } | |
308 | |
309 this.renderMedia_(); | |
310 util.applyTransform(container, this.transform_); | |
311 var attachableMedia = this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? | |
312 this.canvas_ : this.image_; | |
313 | |
314 ThumbnailLoader.centerImage_( | |
315 container, attachableMedia, fillMode, this.isRotated_()); | |
316 | |
317 if (attachableMedia.parentNode !== container) { | |
318 container.textContent = ''; | |
319 container.appendChild(attachableMedia); | |
320 } | |
321 | |
322 if (!this.taskId_) | |
323 attachableMedia.classList.add('cached'); | |
324 }; | |
325 | |
326 /** | |
327 * Gets the loaded image. | |
328 * TODO(mtomasz): Apply transformations. | |
329 * | |
330 * @return {Image|HTMLCanvasElement} Either image or a canvas object. | |
331 */ | |
332 ThumbnailLoader.prototype.getImage = function() { | |
333 this.renderMedia_(); | |
334 return this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ : | |
335 this.image_; | |
336 }; | |
337 | |
338 /** | |
339 * Update the image style to fit/fill the container. | |
340 * | |
341 * Using webkit center packing does not align the image properly, so we need | |
342 * to wait until the image loads and its dimensions are known, then manually | |
343 * position it at the center. | |
344 * | |
345 * @param {HTMLElement} box Containing element. | |
346 * @param {Image|HTMLCanvasElement} img Element containing an image. | |
347 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. | |
348 * @param {boolean} rotate True if the image should be rotated 90 degrees. | |
349 * @private | |
350 */ | |
351 ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) { | |
352 var imageWidth = img.width; | |
353 var imageHeight = img.height; | |
354 | |
355 var fractionX; | |
356 var fractionY; | |
357 | |
358 var boxWidth = box.clientWidth; | |
359 var boxHeight = box.clientHeight; | |
360 | |
361 var fill; | |
362 switch (fillMode) { | |
363 case ThumbnailLoader.FillMode.FILL: | |
364 case ThumbnailLoader.FillMode.OVER_FILL: | |
365 fill = true; | |
366 break; | |
367 case ThumbnailLoader.FillMode.FIT: | |
368 fill = false; | |
369 break; | |
370 case ThumbnailLoader.FillMode.AUTO: | |
371 var imageRatio = imageWidth / imageHeight; | |
372 var boxRatio = 1.0; | |
373 if (boxWidth && boxHeight) | |
374 boxRatio = boxWidth / boxHeight; | |
375 // Cropped area in percents. | |
376 var ratioFactor = boxRatio / imageRatio; | |
377 fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) && | |
378 (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD); | |
379 break; | |
380 } | |
381 | |
382 if (boxWidth && boxHeight) { | |
383 // When we know the box size we can position the image correctly even | |
384 // in a non-square box. | |
385 var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth; | |
386 var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight; | |
387 | |
388 var scale = fill ? | |
389 Math.max(fitScaleX, fitScaleY) : | |
390 Math.min(fitScaleX, fitScaleY); | |
391 | |
392 if (fillMode !== ThumbnailLoader.FillMode.OVER_FILL) | |
393 scale = Math.min(scale, 1); // Never overscale. | |
394 | |
395 fractionX = imageWidth * scale / boxWidth; | |
396 fractionY = imageHeight * scale / boxHeight; | |
397 } else { | |
398 // We do not know the box size so we assume it is square. | |
399 // Compute the image position based only on the image dimensions. | |
400 // First try vertical fit or horizontal fill. | |
401 fractionX = imageWidth / imageHeight; | |
402 fractionY = 1; | |
403 if ((fractionX < 1) === !!fill) { // Vertical fill or horizontal fit. | |
404 fractionY = 1 / fractionX; | |
405 fractionX = 1; | |
406 } | |
407 } | |
408 | |
409 function percent(fraction) { | |
410 return (fraction * 100).toFixed(2) + '%'; | |
411 } | |
412 | |
413 img.style.width = percent(fractionX); | |
414 img.style.height = percent(fractionY); | |
415 img.style.left = percent((1 - fractionX) / 2); | |
416 img.style.top = percent((1 - fractionY) / 2); | |
417 }; | |
OLD | NEW |