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.
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.
12 * @param {FileEntry} entry File entry.
13 * @param {ThumbnailLoader.LoaderType=} opt_loaderType Canvas or Image loader,
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.
22 function ThumbnailLoader(entry, opt_loaderType, opt_metadata, opt_mediaType,
23 opt_useEmbedded, opt_priority) {
24 opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED;
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;
33 this.thumbnailUrl_ = entry.toURL(); // Use the URL directly.
37 this.fallbackUrl_ = null;
38 this.thumbnailUrl_ = null;
39 if (opt_metadata.external && opt_metadata.external.customIconUrl)
40 this.fallbackUrl_ = opt_metadata.external.customIconUrl;
42 // Fetch the rotation from the external properties (if available).
43 var externalTransform;
44 if (opt_metadata.external &&
45 opt_metadata.external.imageRotation !== undefined) {
49 rotate90: opt_metadata.external.imageRotation / 90
53 if (((opt_metadata.thumbnail && opt_metadata.thumbnail.url) ||
54 (opt_metadata.external && opt_metadata.external.thumbnailUrl)) &&
55 opt_useEmbedded === ThumbnailLoader.UseEmbedded.USE_EMBEDDED) {
56 // If the thumbnail generated from the local cache (metadata.thumbnail.url)
57 // is available, use it. If not, use the one passed from the external
58 // provider (metadata.external.thumbnailUrl).
60 (opt_metadata.thumbnail && opt_metadata.thumbnail.url) ||
61 (opt_metadata.external && opt_metadata.external.thumbnailUrl);
62 this.transform_ = externalTransform !== undefined ? externalTransform :
63 (opt_metadata.thumbnail && opt_metadata.thumbnail.transform);
64 } else if (FileType.isImage(entry)) {
65 this.thumbnailUrl_ = entry.toURL();
66 this.transform_ = externalTransform !== undefined ? externalTransform :
67 opt_metadata.media && opt_metadata.media.imageTransform;
68 } else if (this.fallbackUrl_) {
69 // Use fallback as the primary thumbnail.
70 this.thumbnailUrl_ = this.fallbackUrl_;
71 this.fallbackUrl_ = null;
72 } // else the generic thumbnail based on the media type will be used.
76 * In percents (0.0 - 1.0), how much area can be cropped to fill an image
77 * in a container, when loading a thumbnail in FillMode.AUTO mode.
78 * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element.
81 ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3;
84 * Type of displaying a thumbnail within a box.
87 ThumbnailLoader.FillMode = {
88 FILL: 0, // Fill whole box. Image may be cropped.
89 FIT: 1, // Keep aspect ratio, do not crop.
90 OVER_FILL: 2, // Fill whole box with possible stretching.
91 AUTO: 3 // Try to fill, but if incompatible aspect ratio, then fit.
95 * Optimization mode for downloading thumbnails.
98 ThumbnailLoader.OptimizationMode = {
99 NEVER_DISCARD: 0, // Never discards downloading. No optimization.
100 DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore.
104 * Type of element to store the image.
107 ThumbnailLoader.LoaderType = {
113 * Whether to use the embedded thumbnail, or not. The embedded thumbnail may
117 ThumbnailLoader.UseEmbedded = {
123 * Maximum thumbnail's width when generating from the full resolution image.
127 ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500;
130 * Maximum thumbnail's height when generating from the full resolution image.
134 ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500;
137 * Loads and attaches an image.
139 * @param {HTMLElement} box Container element.
140 * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
141 * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization
142 * for downloading thumbnails. By default optimizations are disabled.
143 * @param {function(Image, Object)=} opt_onSuccess Success callback,
144 * accepts the image and the transform.
145 * @param {function()=} opt_onError Error callback.
146 * @param {function()=} opt_onGeneric Callback for generic image used.
148 ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode,
149 opt_onSuccess, opt_onError, opt_onGeneric) {
150 opt_optimizationMode = opt_optimizationMode ||
151 ThumbnailLoader.OptimizationMode.NEVER_DISCARD;
153 if (!this.thumbnailUrl_) {
154 // Relevant CSS rules are in file_types.css.
155 box.setAttribute('generic-thumbnail', this.mediaType_);
156 if (opt_onGeneric) opt_onGeneric();
161 this.canvasUpToDate_ = false;
162 this.image_ = new Image();
163 this.image_.onload = function() {
164 this.attachImage(box, fillMode);
166 opt_onSuccess(this.image_, this.transform_);
168 this.image_.onerror = function() {
171 if (this.fallbackUrl_) {
172 this.thumbnailUrl_ = this.fallbackUrl_;
173 this.fallbackUrl_ = null;
174 this.load(box, fillMode, opt_optimizationMode, opt_onSuccess);
176 box.setAttribute('generic-thumbnail', this.mediaType_);
180 if (this.image_.src) {
181 console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_);
185 // TODO(mtomasz): Smarter calculation of the requested size.
186 var wasAttached = box.ownerDocument.contains(box);
187 var modificationTime = this.metadata_ &&
188 this.metadata_.filesystem &&
189 this.metadata_.filesystem.modificationTime &&
190 this.metadata_.filesystem.modificationTime.getTime();
191 this.taskId_ = util.loadImage(
194 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
195 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
197 priority: this.priority_,
198 timestamp: modificationTime },
200 if (opt_optimizationMode ==
201 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED &&
202 !box.ownerDocument.contains(box)) {
203 // If the container is not attached, then invalidate the download.
211 * Cancels loading the current image.
213 ThumbnailLoader.prototype.cancel = function() {
215 this.image_.onload = function() {};
216 this.image_.onerror = function() {};
217 util.cancelLoadImage(this.taskId_);
223 * @return {boolean} True if a valid image is loaded.
225 ThumbnailLoader.prototype.hasValidImage = function() {
226 return !!(this.image_ && this.image_.width && this.image_.height);
230 * @return {boolean} True if the image is rotated 90 degrees left or right.
233 ThumbnailLoader.prototype.isRotated_ = function() {
234 return this.transform_ && (this.transform_.rotate90 % 2 === 1);
238 * @return {number} Image width (corrected for rotation).
240 ThumbnailLoader.prototype.getWidth = function() {
241 return this.isRotated_() ? this.image_.height : this.image_.width;
245 * @return {number} Image height (corrected for rotation).
247 ThumbnailLoader.prototype.getHeight = function() {
248 return this.isRotated_() ? this.image_.width : this.image_.height;
252 * Load an image but do not attach it.
254 * @param {function(boolean)} callback Callback, parameter is true if the image
255 * has loaded successfully or a stock icon has been used.
257 ThumbnailLoader.prototype.loadDetachedImage = function(callback) {
258 if (!this.thumbnailUrl_) {
264 this.canvasUpToDate_ = false;
265 this.image_ = new Image();
266 this.image_.onload = callback.bind(null, true);
267 this.image_.onerror = callback.bind(null, false);
269 // TODO(mtomasz): Smarter calculation of the requested size.
270 var modificationTime = this.metadata_ &&
271 this.metadata_.filesystem &&
272 this.metadata_.filesystem.modificationTime &&
273 this.metadata_.filesystem.modificationTime.getTime();
274 this.taskId_ = util.loadImage(
277 { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
278 maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
280 priority: this.priority_,
281 timestamp: modificationTime });
285 * Renders the thumbnail into either canvas or an image element.
288 ThumbnailLoader.prototype.renderMedia_ = function() {
289 if (this.loaderType_ !== ThumbnailLoader.LoaderType.CANVAS)
293 this.canvas_ = document.createElement('canvas');
295 // Copy the image to a canvas if the canvas is outdated.
296 if (!this.canvasUpToDate_) {
297 this.canvas_.width = this.image_.width;
298 this.canvas_.height = this.image_.height;
299 var context = this.canvas_.getContext('2d');
300 context.drawImage(this.image_, 0, 0);
301 this.canvasUpToDate_ = true;
306 * Attach the image to a given element.
307 * @param {Element} container Parent element.
308 * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
310 ThumbnailLoader.prototype.attachImage = function(container, fillMode) {
311 if (!this.hasValidImage()) {
312 container.setAttribute('generic-thumbnail', this.mediaType_);
317 util.applyTransform(container, this.transform_);
318 var attachableMedia = this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ?
319 this.canvas_ : this.image_;
321 ThumbnailLoader.centerImage_(
322 container, attachableMedia, fillMode, this.isRotated_());
324 if (attachableMedia.parentNode !== container) {
325 container.textContent = '';
326 container.appendChild(attachableMedia);
330 attachableMedia.classList.add('cached');
334 * Gets the loaded image.
335 * TODO(mtomasz): Apply transformations.
337 * @return {Image|HTMLCanvasElement} Either image or a canvas object.
339 ThumbnailLoader.prototype.getImage = function() {
341 return this.loaderType_ === ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ :
346 * Update the image style to fit/fill the container.
348 * Using webkit center packing does not align the image properly, so we need
349 * to wait until the image loads and its dimensions are known, then manually
350 * position it at the center.
352 * @param {HTMLElement} box Containing element.
353 * @param {Image|HTMLCanvasElement} img Element containing an image.
354 * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
355 * @param {boolean} rotate True if the image should be rotated 90 degrees.
358 ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) {
359 var imageWidth = img.width;
360 var imageHeight = img.height;
365 var boxWidth = box.clientWidth;
366 var boxHeight = box.clientHeight;
370 case ThumbnailLoader.FillMode.FILL:
371 case ThumbnailLoader.FillMode.OVER_FILL:
374 case ThumbnailLoader.FillMode.FIT:
377 case ThumbnailLoader.FillMode.AUTO:
378 var imageRatio = imageWidth / imageHeight;
380 if (boxWidth && boxHeight)
381 boxRatio = boxWidth / boxHeight;
382 // Cropped area in percents.
383 var ratioFactor = boxRatio / imageRatio;
384 fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) &&
385 (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD);
389 if (boxWidth && boxHeight) {
390 // When we know the box size we can position the image correctly even
391 // in a non-square box.
392 var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth;
393 var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight;
396 Math.max(fitScaleX, fitScaleY) :
397 Math.min(fitScaleX, fitScaleY);
399 if (fillMode !== ThumbnailLoader.FillMode.OVER_FILL)
400 scale = Math.min(scale, 1); // Never overscale.
402 fractionX = imageWidth * scale / boxWidth;
403 fractionY = imageHeight * scale / boxHeight;
405 // We do not know the box size so we assume it is square.
406 // Compute the image position based only on the image dimensions.
407 // First try vertical fit or horizontal fill.
408 fractionX = imageWidth / imageHeight;
410 if ((fractionX < 1) === !!fill) { // Vertical fill or horizontal fit.
411 fractionY = 1 / fractionX;
416 function percent(fraction) {
417 return (fraction * 100).toFixed(2) + '%';
420 img.style.width = percent(fractionX);
421 img.style.height = percent(fractionY);
422 img.style.left = percent((1 - fractionX) / 2);
423 img.style.top = percent((1 - fractionY) / 2);