1 // Copyright 2014 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.
6 * Loads a thumbnail using provided url. In CANVAS mode, loaded images
7 * are attached as <canvas> element, while in IMAGE mode as <img>.
8 * <canvas> renders faster than <img>, however has bigger memory overhead.
10 * @param {Entry} entry File entry.
11 * @param {ThumbnailLoader.LoaderType=} opt_loaderType Canvas or Image loader,
13 * @param {Object=} opt_metadata Metadata object.
14 * @param {string=} opt_mediaType Media type.
15 * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded
16 * jpeg thumbnail if available. Default: USE_EMBEDDED.
17 * @param {number=} opt_priority Priority, the highest is 0. default: 2.
20 function ThumbnailLoader(entry, opt_loaderType, opt_metadata, opt_mediaType,
21 opt_useEmbedded, opt_priority) {
22 opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED;
24 this.mediaType_ = opt_mediaType || FileType.getMediaType(entry);
25 this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE;
26 this.metadata_ = opt_metadata;
27 this.priority_ = (opt_priority !== undefined) ? opt_priority : 2;
28 this.transform_ = null;
31 this.thumbnailUrl_ = entry.toURL(); // Use the URL directly.
35 this.fallbackUrl_ = null;
36 this.thumbnailUrl_ = null;
37 if (opt_metadata.external && opt_metadata.external.customIconUrl)
38 this.fallbackUrl_ = opt_metadata.external.customIconUrl;
40 // Fetch the rotation from the external properties (if available).
41 var externalTransform;
42 if (opt_metadata.external &&
43 opt_metadata.external.imageRotation !== undefined) {
47 rotate90: opt_metadata.external.imageRotation / 90
51 if (((opt_metadata.thumbnail && opt_metadata.thumbnail.url) ||
52 (opt_metadata.external && opt_metadata.external.thumbnailUrl)) &&
53 opt_useEmbedded === ThumbnailLoader.UseEmbedded.USE_EMBEDDED) {
54 // If the thumbnail generated from the local cache (metadata.thumbnail.url)
55 // is available, use it. If not, use the one passed from the external
56 // provider (metadata.external.thumbnailUrl).
58 (opt_metadata.thumbnail && opt_metadata.thumbnail.url) ||
59 (opt_metadata.external && opt_metadata.external.thumbnailUrl);
60 this.transform_ = externalTransform !== undefined ? externalTransform :
61 (opt_metadata.thumbnail && opt_metadata.thumbnail.transform);
62 } else if (FileType.isImage(entry)) {
63 this.thumbnailUrl_ = entry.toURL();
64 this.transform_ = externalTransform !== undefined ? externalTransform :
65 opt_metadata.media && opt_metadata.media.imageTransform;
66 } else if (this.fallbackUrl_) {
67 // Use fallback as the primary thumbnail.
68 this.thumbnailUrl_ = this.fallbackUrl_;
69 this.fallbackUrl_ = null;
70 } // else the generic thumbnail based on the media type will be used.
74 * In percents (0.0 - 1.0), how much area can be cropped to fill an image
75 * in a container, when loading a thumbnail in FillMode.AUTO mode.
76 * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element.
79 ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3;
82 * Type of displaying a thumbnail within a box.
85 ThumbnailLoader.FillMode = {
86 FILL: 0, // Fill whole box. Image may be cropped.
87 FIT: 1, // Keep aspect ratio, do not crop.
88 OVER_FILL: 2, // Fill whole box with possible stretching.
89 AUTO: 3 // Try to fill, but if incompatible aspect ratio, then fit.
93 * Optimization mode for downloading thumbnails.
96 ThumbnailLoader.OptimizationMode = {
97 NEVER_DISCARD: 0, // Never discards downloading. No optimization.
98 DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore.
102 * Type of element to store the image.
105 ThumbnailLoader.LoaderType = {
111 * Whether to use the embedded thumbnail, or not. The embedded thumbnail may
115 ThumbnailLoader.UseEmbedded = {
121 * Maximum thumbnail's width when generating from the full resolution image.
125 ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500;
128 * Maximum thumbnail's height when generating from the full resolution image.
132 ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500;
135 * Loads and attaches an image.
137 * @param {Element} box Container element.
138 * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
139 * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization
140 * for downloading thumbnails. By default optimizations are disabled.
141 * @param {function(Image, Object)=} opt_onSuccess Success callback,
142 * accepts the image and the transform.
143 * @param {function()=} opt_onError Error callback.
144 * @param {function()=} opt_onGeneric Callback for generic image used.
146 ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode,
147 opt_onSuccess, opt_onError, opt_onGeneric) {
148 opt_optimizationMode = opt_optimizationMode ||
149 ThumbnailLoader.OptimizationMode.NEVER_DISCARD;
151 if (!this.thumbnailUrl_) {
152 // Relevant CSS rules are in file_types.css.
153 box.setAttribute('generic-thumbnail', this.mediaType_);
154 if (opt_onGeneric) opt_onGeneric();
159 this.canvasUpToDate_ = false;
160 this.image_ = new Image();
161 this.image_.onload = function() {
162 this.attachImage(box, fillMode);
164 opt_onSuccess(this.image_, this.transform_);
166 this.image_.onerror = function() {
169 if (this.fallbackUrl_) {
170 this.thumbnailUrl_ = this.fallbackUrl_;
171 this.fallbackUrl_ = null;
172 this.load(box, fillMode, opt_optimizationMode, opt_onSuccess);
174 box.setAttribute('generic-thumbnail', this.mediaType_);
178 if (this.image_.src) {
179 console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_);
183 // TODO(mtomasz): Smarter calculation of the requested size.
184 var wasAttached = box.ownerDocument.contains(box);
185 var modificationTime = this.metadata_ &&
186 this.metadata_.filesystem &&
187 this.metadata_.filesystem.modificationTime &&
188 this.metadata_.filesystem.modificationTime.getTime();
189 this.taskId_ = util.loadImage(
192 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
193 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
195 priority: this.priority_,
196 timestamp: modificationTime },
198 if (opt_optimizationMode ==
199 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED &&
200 !box.ownerDocument.contains(box)) {
201 // If the container is not attached, then invalidate the download.
209 * Cancels loading the current image.
211 ThumbnailLoader.prototype.cancel = function() {
213 this.image_.onload = function() {};
214 this.image_.onerror = function() {};
215 util.cancelLoadImage(this.taskId_);
221 * @return {boolean} True if a valid image is loaded.
223 ThumbnailLoader.prototype.hasValidImage = function() {
224 return !!(this.image_ && this.image_.width && this.image_.height);
228 * @return {boolean} True if the image is rotated 90 degrees left or right.
231 ThumbnailLoader.prototype.isRotated_ = function() {
232 return this.transform_ && (this.transform_.rotate90 % 2 === 1);
236 * @return {number} Image width (corrected for rotation).
238 ThumbnailLoader.prototype.getWidth = function() {
239 return this.isRotated_() ? this.image_.height : this.image_.width;
243 * @return {number} Image height (corrected for rotation).
245 ThumbnailLoader.prototype.getHeight = function() {
246 return this.isRotated_() ? this.image_.width : this.image_.height;
250 * Load an image but do not attach it.
252 * @param {function(boolean)} callback Callback, parameter is true if the image
253 * has loaded successfully or a stock icon has been used.
255 ThumbnailLoader.prototype.loadDetachedImage = function(callback) {
256 if (!this.thumbnailUrl_) {
262 this.canvasUpToDate_ = false;
263 this.image_ = new Image();
264 this.image_.onload = callback.bind(null, true);
265 this.image_.onerror = callback.bind(null, false);
267 // TODO(mtomasz): Smarter calculation of the requested size.
268 var modificationTime = this.metadata_ &&
269 this.metadata_.filesystem &&
270 this.metadata_.filesystem.modificationTime &&
271 this.metadata_.filesystem.modificationTime.getTime();
272 this.taskId_ = util.loadImage(
275 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
276 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
278 priority: this.priority_,
279 timestamp: modificationTime });
283 * Renders the thumbnail into either canvas or an image element.
286 ThumbnailLoader.prototype.renderMedia_ = function() {
287 if (this.loaderType_ !== ThumbnailLoader.LoaderType.CANVAS)
291 this.canvas_ = document.createElement('canvas');
293 // Copy the image to a canvas if the canvas is outdated.
294 if (!this.canvasUpToDate_) {
295 this.canvas_.width = this.image_.width;
296 this.canvas_.height = this.image_.height;
297 var context = this.canvas_.getContext('2d');
298 context.drawImage(this.image_, 0, 0);
299 this.canvasUpToDate_ = true;
304 * Attach the image to a given element.
305 * @param {Element} container Parent element.
306 * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
308 ThumbnailLoader.prototype.attachImage = function(container, fillMode) {
309 if (!this.hasValidImage()) {
310 container.setAttribute('generic-thumbnail', this.mediaType_);
315 util.applyTransform(container, this.transform_);
316 var attachableMedia = this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ?
317 this.canvas_ : this.image_;
319 ThumbnailLoader.centerImage_(
320 container, attachableMedia, fillMode, this.isRotated_());
322 if (attachableMedia.parentNode !== container) {
323 container.textContent = '';
324 container.appendChild(attachableMedia);
328 attachableMedia.classList.add('cached');
332 * Gets the loaded image.
333 * TODO(mtomasz): Apply transformations.
335 * @return {Image|HTMLCanvasElement} Either image or a canvas object.
337 ThumbnailLoader.prototype.getImage = function() {
339 return this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ :
344 * Update the image style to fit/fill the container.
346 * Using webkit center packing does not align the image properly, so we need
347 * to wait until the image loads and its dimensions are known, then manually
348 * position it at the center.
350 * @param {Element} box Containing element.
351 * @param {Image|HTMLCanvasElement} img Element containing an image.
352 * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
353 * @param {boolean} rotate True if the image should be rotated 90 degrees.
356 ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) {
357 var imageWidth = img.width;
358 var imageHeight = img.height;
363 var boxWidth = box.clientWidth;
364 var boxHeight = box.clientHeight;
368 case ThumbnailLoader.FillMode.FILL:
369 case ThumbnailLoader.FillMode.OVER_FILL:
372 case ThumbnailLoader.FillMode.FIT:
375 case ThumbnailLoader.FillMode.AUTO:
376 var imageRatio = imageWidth / imageHeight;
378 if (boxWidth && boxHeight)
379 boxRatio = boxWidth / boxHeight;
380 // Cropped area in percents.
381 var ratioFactor = boxRatio / imageRatio;
382 fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) &&
383 (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD);
387 if (boxWidth && boxHeight) {
388 // When we know the box size we can position the image correctly even
389 // in a non-square box.
390 var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth;
391 var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight;
394 Math.max(fitScaleX, fitScaleY) :
395 Math.min(fitScaleX, fitScaleY);
397 if (fillMode !== ThumbnailLoader.FillMode.OVER_FILL)
398 scale = Math.min(scale, 1); // Never overscale.
400 fractionX = imageWidth * scale / boxWidth;
401 fractionY = imageHeight * scale / boxHeight;
403 // We do not know the box size so we assume it is square.
404 // Compute the image position based only on the image dimensions.
405 // First try vertical fit or horizontal fill.
406 fractionX = imageWidth / imageHeight;
408 if ((fractionX < 1) === !!fill) { // Vertical fill or horizontal fit.
409 fractionY = 1 / fractionX;
414 function percent(fraction) {
415 return (fraction * 100).toFixed(2) + '%';
418 img.style.width = percent(fractionX);
419 img.style.height = percent(fractionY);
420 img.style.left = percent((1 - fractionX) / 2);
421 img.style.top = percent((1 - fractionY) / 2);