From 07873ef4f3a3f8f4c97a87ebfe040d9f3d36a99d Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Wed, 15 Jun 2016 16:54:48 -0500 Subject: [PATCH] feat(scaling): Allow an alternate library to be used to generate resized images #1525 --- .travis.yml | 12 +- client/js/image-support/image.js | 13 +- client/js/image-support/megapix-image.js | 88 +++- client/js/image-support/scaler.js | 6 +- client/js/templating.js | 18 +- client/js/uploader.api.js | 6 +- client/js/uploader.basic.api.js | 7 +- client/js/uploader.basic.js | 2 + client/js/uploader.js | 1 + client/js/version.js | 2 +- docs/api/methods.jmd | 37 +- docs/api/options-ui.jmd | 12 + docs/api/options.jmd | 12 + docs/features/async-tasks-and-promises.jmd | 12 +- docs/features/scaling.jmd | 41 ++ docs/features/thumbnails.jmd | 40 ++ lib/modules.js | 3 +- package.json | 6 +- test/dev/devenv.js | 34 +- test/dev/index.html | 1 + test/unit/scaling.js | 516 ++++++++++++--------- 21 files changed, 588 insertions(+), 281 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b7c9d48c..12d092782 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ --- addons: - firefox: "38.0" + firefox: "latest" sudo: false language: node_js @@ -21,16 +21,9 @@ env: awR42/q/Akos2eA8NWx5yU+hRC5rr+oQG5Eio0tzi9+y3a6VXDvgS1h2SaQz TR/MjA/29gFvV7bnp1LSs2TdZx+NGhLd4zHv01XZ+pQk/nQiW9w= -before_install: -- npm install -g grunt-cli -- git submodule update --init --recursive - before_script: - "sh -e /etc/init.d/xvfb start" -script: -- grunt travis - branches: only: - master @@ -38,6 +31,3 @@ branches: - /^feature.*$/ - /^.*fix.*$/ - /^release.*$/ -notifications: - slack: - secure: qb1LdOGlBVKCLxNi86tWrabIKs9TFa3ttpLIwu1vtEeh+R9XDeG32X89sM3a5CHRwLqkHwrs6JNcIC4qhTAKiUOiaPYPbv7PkZXX1GIuOPMBp20ghpnWA7QHv6SpmW4qDCTixZSzf0B0m97muzWm1VnotgRELbfKr9Cf/7h3jS0= diff --git a/client/js/image-support/image.js b/client/js/image-support/image.js index 62e68227e..0c5ea2215 100644 --- a/client/js/image-support/image.js +++ b/client/js/image-support/image.js @@ -167,7 +167,8 @@ qq.ImageGenerator = function(log) { maxWidth: maxSize, maxHeight: maxSize, orientation: orientation, - mime: mime + mime: mime, + resize: options.customResizeFunction }); }, @@ -177,7 +178,8 @@ qq.ImageGenerator = function(log) { mpImg.render(container, { maxWidth: maxSize, maxHeight: maxSize, - mime: mime + mime: mime, + resize: options.customResizeFunction }); } ); @@ -193,7 +195,7 @@ qq.ImageGenerator = function(log) { return drawPreview; } - function drawOnCanvasOrImgFromUrl(url, canvasOrImg, draw, maxSize) { + function drawOnCanvasOrImgFromUrl(url, canvasOrImg, draw, maxSize, customResizeFunction) { var tempImg = new Image(), tempImgRender = new qq.Promise(); @@ -213,7 +215,8 @@ qq.ImageGenerator = function(log) { mpImg.render(canvasOrImg, { maxWidth: maxSize, maxHeight: maxSize, - mime: determineMimeOfFileName(url) + mime: determineMimeOfFileName(url), + resize: customResizeFunction }); }, @@ -287,7 +290,7 @@ qq.ImageGenerator = function(log) { * * @param fileBlobOrUrl a `File`, `Blob`, or a URL pointing to the image * @param container or to contain the preview - * @param options possible properties include `maxSize` (int), `orient` (bool - default true), and `resize` (bool - default true) + * @param options possible properties include `maxSize` (int), `orient` (bool - default true), resize` (bool - default true), and `customResizeFunction`. * @returns qq.Promise fulfilled when the preview has been drawn, or the attempt has failed */ generate: function(fileBlobOrUrl, container, options) { diff --git a/client/js/image-support/megapix-image.js b/client/js/image-support/megapix-image.js index 49a132ee1..dc77a7ad6 100644 --- a/client/js/image-support/megapix-image.js +++ b/client/js/image-support/megapix-image.js @@ -72,12 +72,19 @@ /** * Rendering image element (with resizing) and get its data URL */ - function renderImageToDataURL(img, options, doSquash) { + function renderImageToDataURL(img, blob, options, doSquash) { var canvas = document.createElement("canvas"), - mime = options.mime || "image/jpeg"; + mime = options.mime || "image/jpeg", + promise = new qq.Promise(); - renderImageToCanvas(img, canvas, options, doSquash); - return canvas.toDataURL(mime, options.quality || 0.8); + renderImageToCanvas(img, blob, canvas, options, doSquash) + .then(function() { + promise.success( + canvas.toDataURL(mime, options.quality || 0.8) + ); + }) + + return promise; } function maybeCalculateDownsampledDimensions(spec) { @@ -98,16 +105,31 @@ /** * Rendering image element (with resizing) into the canvas element */ - function renderImageToCanvas(img, canvas, options, doSquash) { + function renderImageToCanvas(img, blob, canvas, options, doSquash) { var iw = img.naturalWidth, ih = img.naturalHeight, width = options.width, height = options.height, ctx = canvas.getContext("2d"), + promise = new qq.Promise(), modifiedDimensions; ctx.save(); + if (options.resize) { + return renderImageToCanvasWithCustomResizer({ + blob: blob, + canvas: canvas, + image: img, + imageHeight: ih, + imageWidth: iw, + orientation: options.orientation, + resize: options.resize, + targetHeight: height, + targetWidth: width + }) + } + if (!qq.supportedFeatures.unlimitedScaledImageSize) { modifiedDimensions = maybeCalculateDownsampledDimensions({ origWidth: width, @@ -117,7 +139,7 @@ if (modifiedDimensions) { qq.log(qq.format("Had to reduce dimensions due to device limitations from {}w / {}h to {}w / {}h", width, height, modifiedDimensions.newWidth, modifiedDimensions.newHeight), - "warn"); + "warn"); width = modifiedDimensions.newWidth; height = modifiedDimensions.newHeight; @@ -148,7 +170,7 @@ tmpCtx = tmpCanvas.getContext("2d"); while (sy < ih) { - sx = 0, + sx = 0; dx = 0; while (sx < iw) { tmpCtx.clearRect(0, 0, d, d); @@ -169,6 +191,49 @@ } canvas.qqImageRendered && canvas.qqImageRendered(); + promise.success(); + + return promise; + } + + function renderImageToCanvasWithCustomResizer(resizeInfo) { + var blob = resizeInfo.blob, + image = resizeInfo.image, + imageHeight = resizeInfo.imageHeight, + imageWidth = resizeInfo.imageWidth, + orientation = resizeInfo.orientation, + promise = new qq.Promise(), + resize = resizeInfo.resize, + sourceCanvas = document.createElement("canvas"), + sourceCanvasContext = sourceCanvas.getContext("2d"), + targetCanvas = resizeInfo.canvas, + targetHeight = resizeInfo.targetHeight, + targetWidth = resizeInfo.targetWidth; + + transformCoordinate(sourceCanvas, imageWidth, imageHeight, orientation); + + targetCanvas.height = targetHeight; + targetCanvas.width = targetWidth; + + sourceCanvasContext.drawImage(image, 0, 0); + + resize({ + blob: blob, + height: targetHeight, + image: image, + sourceCanvas: sourceCanvas, + targetCanvas: targetCanvas, + width: targetWidth + }) + .then( + function success() { + targetCanvas.qqImageRendered && targetCanvas.qqImageRendered(); + promise.success(); + }, + promise.failure + ) + + return promise; } /** @@ -315,11 +380,14 @@ if (tagName === "img") { (function() { var oldTargetSrc = target.src; - target.src = renderImageToDataURL(self.srcImage, opt, doSquash); - oldTargetSrc === target.src && target.onload(); + renderImageToDataURL(self.srcImage, self.blob, opt, doSquash) + .then(function(dataUri) { + target.src = dataUri; + oldTargetSrc === target.src && target.onload(); + }); }()) } else if (tagName === "canvas") { - renderImageToCanvas(this.srcImage, target, opt, doSquash); + renderImageToCanvas(this.srcImage, this.blob, target, opt, doSquash); } if (typeof this.onrender === "function") { this.onrender(target); diff --git a/client/js/image-support/scaler.js b/client/js/image-support/scaler.js index aad71bdbf..26b94e03f 100644 --- a/client/js/image-support/scaler.js +++ b/client/js/image-support/scaler.js @@ -12,6 +12,7 @@ qq.Scaler = function(spec, log) { "use strict"; var self = this, + customResizeFunction = spec.customResizer, includeOriginal = spec.sendOriginal, orient = spec.orient, defaultType = spec.defaultType, @@ -51,6 +52,7 @@ qq.Scaler = function(spec, log) { }), blob: new qq.BlobProxy(originalBlob, qq.bind(self._generateScaledImage, self, { + customResizeFunction: customResizeFunction, maxSize: sizeRecord.maxSize, orient: orient, type: outputType, @@ -170,6 +172,7 @@ qq.extend(qq.Scaler.prototype, { name = uploadData && uploadData.name, uuid = uploadData && uploadData.uuid, scalingOptions = { + customResizer: specs.customResizer, sendOriginal: false, orient: specs.orient, defaultType: specs.type || null, @@ -290,6 +293,7 @@ qq.extend(qq.Scaler.prototype, { "use strict"; var self = this, + customResizeFunction = spec.customResizeFunction, log = spec.log, maxSize = spec.maxSize, orient = spec.orient, @@ -303,7 +307,7 @@ qq.extend(qq.Scaler.prototype, { log("Attempting to generate scaled version for " + sourceFile.name); - imageGenerator.generate(sourceFile, canvas, {maxSize: maxSize, orient: orient}).then(function() { + imageGenerator.generate(sourceFile, canvas, {maxSize: maxSize, orient: orient, customResizeFunction: customResizeFunction}).then(function() { var scaledImageDataUri = canvas.toDataURL(type, quality), signalSuccess = function() { log("Success generating scaled version for " + sourceFile.name); diff --git a/client/js/templating.js b/client/js/templating.js index aa6134193..2ca7686f7 100644 --- a/client/js/templating.js +++ b/client/js/templating.js @@ -486,9 +486,10 @@ qq.Templating = function(spec) { relatedThumbnailId = optFileOrBlob && optFileOrBlob.qqThumbnailId, thumbnail = getThumbnail(id), spec = { + customResizeFunction: queuedThumbRequest.customResizeFunction, maxSize: thumbnailMaxSize, - scale: true, - orient: true + orient: true, + scale: true }; if (qq.supportedFeatures.imagePreviews) { @@ -534,8 +535,9 @@ qq.Templating = function(spec) { showWaitingImg = queuedThumbRequest.showWaitingImg, thumbnail = getThumbnail(id), spec = { - maxSize: thumbnailMaxSize, - scale: serverScale + customResizeFunction: queuedThumbRequest.customResizeFunction, + scale: serverScale, + maxSize: thumbnailMaxSize }; if (thumbnail) { @@ -982,16 +984,16 @@ qq.Templating = function(spec) { show(getSpinner(id)); }, - generatePreview: function(id, optFileOrBlob) { + generatePreview: function(id, optFileOrBlob, customResizeFunction) { if (!this.isHiddenForever(id)) { - thumbGenerationQueue.push({id: id, optFileOrBlob: optFileOrBlob}); + thumbGenerationQueue.push({id: id, customResizeFunction: customResizeFunction, optFileOrBlob: optFileOrBlob}); !thumbnailQueueMonitorRunning && generateNextQueuedPreview(); } }, - updateThumbnail: function(id, thumbnailUrl, showWaitingImg) { + updateThumbnail: function(id, thumbnailUrl, showWaitingImg, customResizeFunction) { if (!this.isHiddenForever(id)) { - thumbGenerationQueue.push({update: true, id: id, thumbnailUrl: thumbnailUrl, showWaitingImg: showWaitingImg}); + thumbGenerationQueue.push({customResizeFunction: customResizeFunction, update: true, id: id, thumbnailUrl: thumbnailUrl, showWaitingImg: showWaitingImg}); !thumbnailQueueMonitorRunning && generateNextQueuedPreview(); } }, diff --git a/client/js/uploader.api.js b/client/js/uploader.api.js index 795748343..2603c4354 100644 --- a/client/js/uploader.api.js +++ b/client/js/uploader.api.js @@ -574,11 +574,11 @@ if (canned) { this._templating.addFileToCache(id, this._options.formatFileName(name), prependData, dontDisplay); - this._templating.updateThumbnail(id, this._thumbnailUrls[id], true); + this._templating.updateThumbnail(id, this._thumbnailUrls[id], true, this._options.thumbnails.customResizer); } else { this._templating.addFile(id, this._options.formatFileName(name), prependData, dontDisplay); - this._templating.generatePreview(id, this.getFile(id)); + this._templating.generatePreview(id, this.getFile(id), this._options.thumbnails.customResizer); } this._filesInBatchAddedToUi += 1; @@ -696,7 +696,7 @@ // This will replace the "waiting" placeholder with a "preview not available" placeholder // if called with a null thumbnailUrl. - this._templating.updateThumbnail(fileId, thumbnailUrl); + this._templating.updateThumbnail(fileId, thumbnailUrl, this._options.thumbnails.customResizer); } }, diff --git a/client/js/uploader.basic.api.js b/client/js/uploader.basic.api.js index bf1b7a92c..126aa65ea 100644 --- a/client/js/uploader.basic.api.js +++ b/client/js/uploader.basic.api.js @@ -163,15 +163,16 @@ // returning a promise that is fulfilled when the attempt completes. // Thumbnail can either be based off of a URL for an image returned // by the server in the upload response, or the associated `Blob`. - drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer) { + drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer, customResizeFunction) { var promiseToReturn = new qq.Promise(), fileOrUrl, options; if (this._imageGenerator) { fileOrUrl = this._thumbnailUrls[fileId]; options = { - scale: maxSize > 0, - maxSize: maxSize > 0 ? maxSize : null + customResizeFunction: customResizeFunction, + maxSize: maxSize > 0 ? maxSize : null, + scale: maxSize > 0 }; // If client-side preview generation is possible diff --git a/client/js/uploader.basic.js b/client/js/uploader.basic.js index 8d95425f5..dfeae24f2 100644 --- a/client/js/uploader.basic.js +++ b/client/js/uploader.basic.js @@ -194,6 +194,8 @@ // scale images client side, upload a new file for each scaled version scaling: { + customResizer: null, + // send the original file as well sendOriginal: true, diff --git a/client/js/uploader.js b/client/js/uploader.js index a22bd4dd4..79f72f668 100644 --- a/client/js/uploader.js +++ b/client/js/uploader.js @@ -80,6 +80,7 @@ qq.FineUploader = function(o, namespace) { }, thumbnails: { + customResizer: null, maxCount: 0, placeholders: { waitUntilResponse: false, diff --git a/client/js/version.js b/client/js/version.js index 87c08bb74..1071d1542 100644 --- a/client/js/version.js +++ b/client/js/version.js @@ -1,2 +1,2 @@ /*global qq */ -qq.version = "5.10.0"; +qq.version = "5.10.0-2"; diff --git a/docs/api/methods.jmd b/docs/api/methods.jmd index fa9b60a78..755cbdd71 100644 --- a/docs/api/methods.jmd +++ b/docs/api/methods.jmd @@ -113,8 +113,7 @@ A `CanvasWrapper` object: } ]) }} -{{ api_method("drawThumbnail", "drawThumbnail (id, targetContainer[, maxSize[, fromServer]])", -"Draws a thumbnail.", +{{ api_method("drawThumbnail", "drawThumbnail (id, targetContainer[, maxSize[, fromServer[, customResizer]]])", "Draws a thumbnail.", [ { "name": "id", @@ -135,6 +134,21 @@ A `CanvasWrapper` object: "name": "fromServer", "type": "Boolean", "description": "`true` if the image data will come as a response from the server rather than be generated client-side." + }, + { + "name": "customResizer", + "type": "function", + "description": "Ignored if the current browser does not [support image previews](../browser-support.html). If you want to use an alternate library to resize the image, you must contribute a function for this option that returns a `Promise`. Once the resize is complete, your promise must be fulfilled. You may, of course, reject your returned `Promise` is the resize fails in some way. + +A `resizeInfo` object, which will be passed to the supplied function, contains the following properties: + +* `blob` - The original `File` or `Blob` object, if available. +* `height` - Desired height of the image after the resize operation. +* `image` - The original `HTMLImageElement` object, if available. +* `sourceCanvas` - `HTMLCanvasElement` element containing the original image data (not resized). +* `targetCanvas` - `HTMLCanvasElement` element containing the `HTMLCanvasElement` that should contain the resized image. +* `width` - Desired width of the image after the resize operation. +" } ], [ @@ -359,7 +373,24 @@ A `CanvasWrapper` object: { "name": "options", "type": "Object", - "description": "Information about the scaled image to generate. The `maxSize` property is required (integer). Optional properties are: `orient` (boolean, defaults to true), `type` (string, defaults to the type of the reference image), and `quality` (number between 0 and 100, defaults to 80), and `includeExif` (boolean, defaults to `false`)." + "description": "Information about the scaled image to generate. The following properties are supported: + +* `maxSize` (**required**) (integer). +* `orient` (boolean, defaults to true) +* `type` (string, defaults to the type of the reference image) +* `quality` (number between 0 and 100, defaults to 80) +* `includeExif` (boolean, defaults to `false`). +* `customResizer` (function) - Ignored if the current browser does not [support image previews](../browser-support.html). If you want to use an alternate library to resize the image, you must contribute a function for this option that returns a `Promise`. Once the resize is complete, your promise must be fulfilled. You may, of course, reject your returned `Promise` is the resize fails in some way. + +A `resizeInfo` object, which will be passed to your (optional) `customResizer` function, contains the following properties: + +* `blob` - The original `File` or `Blob` object, if available. +* `height` - Desired height of the image after the resize operation. +* `image` - The original `HTMLImageElement` object, if available. +* `sourceCanvas` - `HTMLCanvasElement` element containing the original image data (not resized). +* `targetCanvas` - `HTMLCanvasElement` element containing the `HTMLCanvasElement` that should contain the resized image. +* `width` - Desired width of the image after the resize operation. +" } ], [ diff --git a/docs/api/options-ui.jmd b/docs/api/options-ui.jmd index 3f92666ba..2c5f40676 100644 --- a/docs/api/options-ui.jmd +++ b/docs/api/options-ui.jmd @@ -102,6 +102,18 @@ options for `messages`""", "info", "Note:") }} {{ api_parent_option("thumbnails", "thumbnails", "", ( + ("thumbnails.customResizer", "customResizer", """Ignored if the current browser does not [support image previews](../browser-support.html). If you want to use an alternate library to resize the image, you must contribute a function for this option that returns a `Promise`. Once the resize is complete, your promise must be fulfilled. You may, of course, reject your returned `Promise` is the resize fails in some way. + +A `resizeInfo` object, which will be passed to the supplied function, contains the following properties: + +* `blob` - The original `File` or `Blob` object, if available. +* `height` - Desired height of the image after the resize operation. +* `image` - The original `HTMLImageElement` object, if available. +* `sourceCanvas` - `HTMLCanvasElement` element containing the original image data (not resized). +* `targetCanvas` - `HTMLCanvasElement` element containing the `HTMLCanvasElement` that should contain the resized image. +* `width` - Desired width of the image after the resize operation. +", + "Function", "undefined"), ("thumbnails.maxCount", "maxCount", "Maximum number of previews to render per Fine Uploader instance. A call to the reset method resets this value as well.", "Integer", "0",), ("thumbnails.timeBetweenThumbs", "timeBetweenThumbs", "The amount of time, in milliseconds, to pause between each preview generation process. This is in place to prevent the UI thread from locking up for a continuously long period of time, as preview generation can be a resource-intensive process.", "Integer", "750",), ) diff --git a/docs/api/options.jmd b/docs/api/options.jmd index ecee7ce5b..3b6148232 100644 --- a/docs/api/options.jmd +++ b/docs/api/options.jmd @@ -187,6 +187,18 @@ alert("The `chunking.success.endpoint` option **only** applies to traditional up {{ api_parent_option("scaling", "scaling", "See the [Upload Scaled Images feature page](../features/scaling.html) for more details.", ( + ("scaling.customResizer", "customResizer", "Ignored if the current browser does not [support image previews](../browser-support.html). If you want to use an alternate scaling library, you must contribute a function for this option that returns a `Promise`. Once the resize is complete, your promise must be fulfilled. You may, of course, reject your returned `Promise` is the resize fails in some way. + + A `resizeInfo` object, which will be passed to the supplied function, contains the following properties: + +* `blob` - The original `File` or `Blob` object, if available. +* `height` - Desired height of the image after the resize operation. +* `image` - The original `HTMLImageElement` object, if available. +* `sourceCanvas` - `HTMLCanvasElement` element containing the original image data (not resized). +* `targetCanvas` - `HTMLCanvasElement` element containing the `HTMLCanvasElement` that should contain the resized image. +* `width` - Desired width of the image after the resize operation. +", + "Function", "undefined"), ("scaling.defaultQuality", "defaultQuality", "A value between 1 and 100 that describes the requested quality of scaled images. Ignored unless the scaled image type target is image/jpeg.", "Integer", "80",), ("scaling.defaultType", "defaultType", "Scaled images will assume this image type if you don't specify a specific type in your size object, or if the type specified in the size object is not valid. You generally should not use any value other than image/jpeg or image/png here. The default value of `null` will ensure the scaled image type is PNG, unless the original file is a JPEG, in which case the scaled file will also be a JPEG. The default is probably the safest option.", "String", "null",), ("scaling.failureText", "failureText", "Text sent to your `complete` event handler as an `error` property of the `response` param if a scaled image could not be generated.", "String", "Failed to scale",), diff --git a/docs/features/async-tasks-and-promises.jmd b/docs/features/async-tasks-and-promises.jmd index 33e89ad74..d962d58e7 100644 --- a/docs/features/async-tasks-and-promises.jmd +++ b/docs/features/async-tasks-and-promises.jmd @@ -26,17 +26,17 @@ For more information on promises in JavaScript, have a look at ## Promissory Callbacks Promises are acceptable return values in the following [event handlers](../api/events.html). -All of these callbacks can also prevent an associated action from being executed +Many of these callbacks can also prevent an associated action from being executed with a false return value (non-promise) or a call to `failure()` on a returned -promise instance: +promise instance (see individual event docs for more details): -* `onSubmit` * `onCancel` * `onCredentialsExpired` -* `onValidateBatch` -* `onValidate` -* `onSubmitDelete` * `onPasteReceived` +* `onSubmit` +* `onSubmitDelete` +* `onValidate` +* `onValidateBatch` You are not required to return a promise -- you can simply return `false` (or nothing). However, there are some instances where you may want to perform diff --git a/docs/features/scaling.jmd b/docs/features/scaling.jmd index d4b6e8a79..0e8be7bc3 100644 --- a/docs/features/scaling.jmd +++ b/docs/features/scaling.jmd @@ -8,6 +8,8 @@ [s3]: s3.html [azure]: azure.html +[customResizer]: ../api/options.html#scaling.customResizer +[pica]: https://github.com/nodeca/pica [scaling]: ../api/options.html#scaling [sizes]: ../api/options.html#scaling.sizes [defaulttype]: ../api/options.html#scaling.defaultType @@ -16,10 +18,13 @@ [orient]: ../api/options.html#scaling.orient [hidescaled]: ../api/options-ui.html#scaling.hideScaled [getfile]: ../api/methods.html#getFile +[limby-resize]: https://github.com/danschumann/limby-resize [promise]: async-tasks-and-promises.html [data]: statistics-and-status-updates.html [api]: ../api/methods.html [itemlimit]: ../api/options.html#validation.itemLimit +[webworkers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers + # Generate and Upload Scaled Images {: .page-header } @@ -153,6 +158,42 @@ the parent images will include a "qquuid" request parameter instead. If you are [Azure][azure], these parameters will be associated with the file in your bucket or blob container. +### Using a third-party library to resize images + +Fine Uploader's internal image resize code delegates to the `drawImage` method on the browser's native `CanvasRenderingContext2D` object. +This object is used to manipulate a `` element, which represents a submitted image `File or `Blob`. +Most browsers use linear interpolation when resizing images. This leads to extreme aliasing and Moire patterns +which is a deal breaker for anyone resizing images for art/photo galleries, albums, etc. +These kinds of artifacts are impossible to remove after the fact. + +If speed is most important, and precise scaled image generation is _not_ paramount, you should continue to use Fine Uploader's +internal scaling implementation. However, if you want to generate the higher quality scaled images for upload, you should +instead use a third-party library to resize submitted image files, such as [pica] or [limby-resize]. As of version 5.10 of +Fine Uploader, it is extremely easy to integrate such a plug-in into this library. In fact, Fine Uploader will continue +to properly orient the submitted image file and then pass a properly sized `` to the image scaling library of +your choice to receive the resized image file. The only caveat is that, due to issues with scaling larger images in +iOS, you will need to continue to use Fine Uploader's internal scaling algorithm for that particular OS, as other +third-party scaling libraries most likely do _not_ continue logic to handle this complex case. Luckily, that is easy +to account for as well. + +If you'd like to, for example, use pica to generate higher-quality scaled images, simply pull pica into your project, +and contribute a [`scaling.customResizer` function][customResizer], like so: + +```javascript +scaling: { + customResizer: !qq.ios() && function(resizeInfo) { + return new Promise(function(resolve, reject) { + pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, resolve) + }) + }, + ... +} +``` + +That's it! The above code will result in a higher-quality scaled image, and pica even pushes resizing logic off to a +[web worker][webworkers] to reduce strain on the UI thread. + + ### Notices * Do not set the [`scaling.hideScaled` option][hidescaled] to `true` AND the [`scaling.sendOriginal` option][sendoriginal] to `false` at the same time. This will result in no files being represented in the UI for images that are scalable. diff --git a/docs/features/thumbnails.jmd b/docs/features/thumbnails.jmd index 2ad7a9039..0c92ddcd8 100644 --- a/docs/features/thumbnails.jmd +++ b/docs/features/thumbnails.jmd @@ -5,6 +5,11 @@ {% endblock %} {% block content %} {% markdown %} + +[customResizer]: ../api/options-ui.html#thumbnails.customResizer +[webworkers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers + + # Previews & Thumbnails {: .page-header } ## Summary @@ -122,6 +127,41 @@ placeholders will be treated the same way as cross-origin server-generated thumb section above for details. Re-orienting placeholder images is not supported, so, if you provide your own placeholder images, ensure they are already oriented correctly. +#### Using a third-party library to resize images + +Fine Uploader's internal image resize code delegates to the `drawImage` method on the browser's native `CanvasRenderingContext2D` object. +This object is used to manipulate a `` element, which represents a submitted image `File or `Blob`. +Most browsers use linear interpolation when resizing images. This leads to extreme aliasing and Moire patterns +which may result in lower quality displayed thumbnails. + +If speed is most important, and precise scaled thumbnail generation is _not_ paramount, you should continue to use Fine Uploader's +internal scaling implementation. However, if you want to generate the higher quality thumbnail images for display, you should +instead use a third-party library to resize submitted image files, such as [pica] or [limby-resize]. As of version 5.10 of +Fine Uploader, it is extremely easy to integrate such a plug-in into this library. In fact, Fine Uploader will continue +to properly orient the submitted image file and then pass a properly sized `` to the image scaling library of +your choice to receive the resized image file. The only caveat is that, due to issues with scaling larger images in +iOS, you will need to continue to use Fine Uploader's internal scaling algorithm for that particular OS, as other +third-party scaling libraries most likely do _not_ continue logic to handle this complex case. Luckily, that is easy +to account for as well. + +If you'd like to, for example, use pica to generate higher-quality scaled images, simply pull pica into your project, +and contribute a [`thumbnails.customResizer` function][customResizer], like so: + +```javascript +thumbnails: { + customResizer: !qq.ios() && function(resizeInfo) { + return new Promise(function(resolve, reject) { + pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, resolve) + }) + }, + ... +} +``` + +That's it! The above code will result in a higher-quality scaled thumbnail, and pica even pushes resizing logic off to a +[web worker][webworkers] to reduce strain on the UI thread. + + ### Core mode For Core mode users that need to create their own highly-customized UI, there is a [`drawThumbnail` API method](../api/methods.html) that will allow you to effortlessly render either a submitted image file's preview or a server-returned thumbnail diff --git a/lib/modules.js b/lib/modules.js index a2187323e..31a617d88 100644 --- a/lib/modules.js +++ b/lib/modules.js @@ -263,7 +263,8 @@ var //dependencies testHelperModules: [ "test/static/local/karma-runner.js", "test/static/local/blob-maker.js", - "test/static/third-party/q/q-1.0.1.js" + "test/static/third-party/q/q-1.0.1.js", + "node_modules/pica/dist/pica.js" ], fuSrcBuild: [ "_build/all!(@(*.min.js|*.gif|*.css))" diff --git a/package.json b/package.json index 34757a8b5..88fc3e3fd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fine-uploader", "title": "Fine Uploader", "main": "lib/traditional.js", - "version": "5.10.0", + "version": "5.10.0-2", "description": "Multiple file upload plugin with progress-bar, drag-and-drop, direct-to-S3 & Azure uploading, client-side image scaling, preview generation, form support, chunking, auto-resume, and tons of other features.", "keywords": [ "amazon", @@ -71,6 +71,7 @@ "karma-spec-reporter": "0.0.13", "npm": "^2.1.17", "optimist": "0.6.0", + "pica": "latest", "request": "2.21.0", "semver": "2.0.x", "string.prototype.endswith": "0.2.0", @@ -80,9 +81,12 @@ }, "scripts": { "build": "grunt package", + "devbuild": "grunt dev", + "lint": "grunt jshint:tests; grunt jshint:source; grunt jscs:tests; grunt jscs:src", "setup-dev": "(cd test/dev/handlers; curl -sS https://getcomposer.org/installer | php; php composer.phar install)", "start-local-dev": "(. test/dev/handlers/s3keys.sh; php -S 0.0.0.0:9090 -t . -c test/dev/handlers/php.ini)", "start-c9-dev": "php -S $IP:$PORT -t . -c test/dev/handlers/php.ini", + "test": "if test \"$TRAVIS\" = \"true\" ; then grunt travis ; else grunt test:firefox ; fi", "update-dev": "(cd test/dev/handlers; php composer.phar update)" }, "engines" : { diff --git a/test/dev/devenv.js b/test/dev/devenv.js index e00d423c5..b68ff952c 100644 --- a/test/dev/devenv.js +++ b/test/dev/devenv.js @@ -41,21 +41,35 @@ qq(window).attach("load", function() { enableAuto: true }, thumbnails: { + customResizer: !qq.ios() && function(resizeInfo) { + var promise = new qq.Promise(); + + pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, function() { + promise.success(); + }) + + return promise; + }, placeholders: { waitingPath: "/client/placeholders/waiting-generic.png", notAvailablePath: "/client/placeholders/not_available-generic.png" } }, - //scaling: { - // sizes: [{name: "small", maxSize: 300}] - //}, + scaling: { + customResizer: !qq.ios() && function(resizeInfo) { + var promise = new qq.Promise(); + + pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, function() { + promise.success(); + }) + + return promise; + }, + sizes: [{name: "small", maxSize: 800}] + }, session: { //endpoint: "/test/dev/handlers/vendor/fineuploader/php-traditional-server/endpoint.php?initial" }, - validation: { - //sizeLimit: 10000000, - //itemLimit: 4 - }, callbacks: { onError: errorHandler, onUpload: function (id, filename) { @@ -65,12 +79,6 @@ qq(window).attach("load", function() { }, id); } - //onStatusChange: function (id, oldS, newS) { - // qq.log("id: " + id + " " + newS); - //}, - //onComplete: function (id, name, response) { - // qq.log(response); - //} } }); diff --git a/test/dev/index.html b/test/dev/index.html index 7f64122c5..4a828e73e 100644 --- a/test/dev/index.html +++ b/test/dev/index.html @@ -207,6 +207,7 @@ + diff --git a/test/unit/scaling.js b/test/unit/scaling.js index 435ddaa56..d022c4d3b 100644 --- a/test/unit/scaling.js +++ b/test/unit/scaling.js @@ -1,4 +1,4 @@ -/* globals describe, it, qq, assert, qqtest, helpme */ +/* globals describe, it, qq, assert, qqtest, helpme, pica */ if (qq.supportedFeatures.scaling) { describe("scaling module tests", function() { "use strict"; @@ -13,6 +13,15 @@ if (qq.supportedFeatures.scaling) { } }); }, 10); + }, + typicalCustomResizer = function(resizeInfo) { + var promise = new qq.Promise(); + + pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, function() { + promise.success(); + }); + + return promise; }; it("is disabled if no sizes are specified", function() { @@ -148,14 +157,17 @@ if (qq.supportedFeatures.scaling) { }); describe("generates simple scaled image tests", function() { - function runScaleTest(orient, done) { - assert.expect(3, done); - + function runScaleTest(orient, customResizer, done) { var scalerContext = qq.extend({}, qq.Scaler.prototype), scale = qq.bind(qq.Scaler.prototype._generateScaledImage, scalerContext); qqtest.downloadFileAsBlob("up.jpg", "image/jpeg").then(function(blob) { - scale({maxSize: 50, orient: orient, log: function(){}}, blob).then(function(scaledBlob) { + scale({ + maxSize: 50, + orient: orient, + log: function(){}, + customResizeFunction: customResizer + }, blob).then(function(scaledBlob) { var URL = window.URL && window.URL.createObjectURL ? window.URL : window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL : null, @@ -167,6 +179,7 @@ if (qq.supportedFeatures.scaling) { img.onload = function() { assert.ok(this.width <= 50); assert.ok(this.height <= 50); + done(); }; img.onerror = function() { @@ -178,13 +191,27 @@ if (qq.supportedFeatures.scaling) { }); } - it("generates a properly scaled & oriented image for a reference image", function(done) { - runScaleTest(true, done); - }); + describe("using built-in resizer code", function() { + it("generates a properly scaled & oriented image for a reference image", function(done) { + runScaleTest(true, null, done); + }); - it("generates a properly scaled image for a reference image", function(done) { - runScaleTest(false, done); + it("generates a properly scaled image for a reference image", function(done) { + runScaleTest(false, null, done); + }); }); + + if (!qq.ios()) { + describe("using third-party resizer code", function() { + it("generates a properly scaled & oriented image for a reference image", function(done) { + runScaleTest(true, typicalCustomResizer, done); + }); + + it("generates a properly scaled image for a reference image", function(done) { + runScaleTest(false, typicalCustomResizer, done); + }); + }); + } }); @@ -257,119 +284,145 @@ if (qq.supportedFeatures.scaling) { }); }); - it("uploads scaled files as expected: non-chunked, default options", function(done) { - assert.expect(39, done); - - var referenceFileSize, - sizes = [ - { - name: "small", - maxSize: 50 - }, - { - name: "medium", - maxSize: 400 - } - ], - expectedUploadCallbacks = [ - {id: 0, name: "up (small).jpeg"}, - {id: 1, name: "up (medium).jpeg"}, - {id: 2, name: "up.jpeg"}, - {id: 3, name: "up2 (small).jpeg"}, - {id: 4, name: "up2 (medium).jpeg"}, - {id: 5, name: "up2.jpeg"} - ], - actualUploadCallbacks = [], - uploader = new qq.FineUploaderBasic({ - request: {endpoint: "test/uploads"}, - scaling: { - sizes: sizes - }, - callbacks: { - onUpload: function(id, name) { - assert.ok(uploader.getSize(id) > 0, "Blob size is not greater than 0"); - assert.ok(qq.isBlob(uploader.getFile(id)), "file is not a Blob"); - assert.equal(uploader.getFile(id).size, referenceFileSize); - - actualUploadCallbacks.push({id: id, name: name}); - setTimeout(function() { - var req = fileTestHelper.getRequests()[id], - parentUuid = req.requestBody.fields.qqparentuuid, - parentSize = req.requestBody.fields.qqparentsize, - parentId = uploader.getParentId(id), - file = req.requestBody.fields.qqfile; - - assert.equal(file.type, "image/jpeg"); - - if (parentId !== null) { - assert.equal(parentUuid, uploader.getUuid(parentId)); - assert.equal(parentSize, uploader.getSize(parentId)); - } - else { - assert.equal(parentUuid, undefined); - assert.equal(parentSize, undefined); - } + describe("scaled files uploads (non-chunked, default options)", function() { + function runTest(customResizer, done) { + assert.expect(39, done); - req.respond(200, null, JSON.stringify({success: true})); - }, 10); + var referenceFileSize, + sizes = [ + { + name: "small", + maxSize: 50 }, - onAllComplete: function(successful, failed) { - assert.equal(successful.length, 6); - assert.equal(failed.length, 0); - assert.deepEqual(actualUploadCallbacks, expectedUploadCallbacks); + { + name: "medium", + maxSize: 400 } - } + ], + expectedUploadCallbacks = [ + {id: 0, name: "up (small).jpeg"}, + {id: 1, name: "up (medium).jpeg"}, + {id: 2, name: "up.jpeg"}, + {id: 3, name: "up2 (small).jpeg"}, + {id: 4, name: "up2 (medium).jpeg"}, + {id: 5, name: "up2.jpeg"} + ], + actualUploadCallbacks = [], + uploader = new qq.FineUploaderBasic({ + request: {endpoint: "test/uploads"}, + scaling: { + sizes: sizes, + customResizer: customResizer + }, + callbacks: { + onUpload: function(id, name) { + assert.ok(uploader.getSize(id) > 0, "Blob size is not greater than 0"); + assert.ok(qq.isBlob(uploader.getFile(id)), "file is not a Blob"); + assert.equal(uploader.getFile(id).size, referenceFileSize); + + actualUploadCallbacks.push({id: id, name: name}); + setTimeout(function() { + var req = fileTestHelper.getRequests()[id], + parentUuid = req.requestBody.fields.qqparentuuid, + parentSize = req.requestBody.fields.qqparentsize, + parentId = uploader.getParentId(id), + file = req.requestBody.fields.qqfile; + + assert.equal(file.type, "image/jpeg"); + + if (parentId !== null) { + assert.equal(parentUuid, uploader.getUuid(parentId)); + assert.equal(parentSize, uploader.getSize(parentId)); + } + else { + assert.equal(parentUuid, undefined); + assert.equal(parentSize, undefined); + } + + req.respond(200, null, JSON.stringify({success: true})); + }, 100); + }, + onAllComplete: function(successful, failed) { + assert.equal(successful.length, 6); + assert.equal(failed.length, 0); + assert.deepEqual(actualUploadCallbacks, expectedUploadCallbacks); + } + } + }); + + qqtest.downloadFileAsBlob("up.jpg", "image/jpeg").then(function(blob) { + fileTestHelper.mockXhr(); + referenceFileSize = blob.size; + uploader.addFiles([{blob: blob, name: "up.jpeg"}, {blob: blob, name: "up2.jpeg"}]); }); + } - qqtest.downloadFileAsBlob("up.jpg", "image/jpeg").then(function(blob) { - fileTestHelper.mockXhr(); - referenceFileSize = blob.size; - uploader.addFiles([{blob: blob, name: "up.jpeg"}, {blob: blob, name: "up2.jpeg"}]); + it("uploads as expected with internal resizer code", function(done) { + runTest(null, done); }); + + if (!qq.ios()) { + it("uploads as expected with third-party resizer code", function (done) { + runTest(typicalCustomResizer, done); + }); + } }); - it("ensure scaled versions of non-JPEGs are always PNGs", function(done) { - assert.expect(4, done); + describe("jpeg to PNG conversion behavior", function() { + function runTest(customResizer, done) { + assert.expect(4, done); - var expectedOutputTypes = [ - "image/png", - "image/png", - "image/png", - "image/gif" - ], - sizes = [ - { - name: "small", - maxSize: 50 - } - ], - actualUploadCallbacks = [], - uploader = new qq.FineUploaderBasic({ - request: {endpoint: "test/uploads"}, - scaling: { - sizes: sizes - }, - callbacks: { - onUpload: function(id, name) { - actualUploadCallbacks.push({id: id, name: name}); - setTimeout(function() { - var req = fileTestHelper.getRequests()[id], - file = req.requestBody.fields.qqfile; + var expectedOutputTypes = [ + "image/png", + "image/png", + "image/png", + "image/gif" + ], + sizes = [ + { + name: "small", + maxSize: 50 + } + ], + actualUploadCallbacks = [], + uploader = new qq.FineUploaderBasic({ + request: {endpoint: "test/uploads"}, + scaling: { + customResizer: customResizer, + sizes: sizes + }, + callbacks: { + onUpload: function(id, name) { + actualUploadCallbacks.push({id: id, name: name}); + setTimeout(function() { + var req = fileTestHelper.getRequests()[id], + file = req.requestBody.fields.qqfile; - assert.equal(file.type, expectedOutputTypes[id]); + assert.equal(file.type, expectedOutputTypes[id]); - req.respond(200, null, JSON.stringify({success: true})); - }, 10); + req.respond(200, null, JSON.stringify({success: true})); + }, 100); + } } - } - }); + }); - qqtest.downloadFileAsBlob("star.png", "image/png").then(function(star) { - qqtest.downloadFileAsBlob("drop-background.gif", "image/gif").then(function(drop) { - fileTestHelper.mockXhr(); - uploader.addFiles([{blob: star, name: "star.png"}, {blob: drop, name: "drop.gif"}]); + qqtest.downloadFileAsBlob("star.png", "image/png").then(function(star) { + qqtest.downloadFileAsBlob("drop-background.gif", "image/gif").then(function(drop) { + fileTestHelper.mockXhr(); + uploader.addFiles([{blob: star, name: "star.png"}, {blob: drop, name: "drop.gif"}]); + }); }); + } + + it("behaves as expected with internal resizer", function(done) { + runTest(null, done); }); + + if (!qq.ios()) { + it("behaves as expected with custom resizer", function (done) { + runTest(typicalCustomResizer, done); + }); + } }); it("uploads scaled files as expected: chunked, default options", function(done) { @@ -512,67 +565,80 @@ if (qq.supportedFeatures.scaling) { }); }); - it("generates a scaled Blob of the original file's type if the requested type is not specified or is not valid", function(done) { - assert.expect(7, done); - - var sizes = [ - { - name: "one", - maxSize: 100, - type: "image/jpeg" - }, - { - name: "two", - maxSize: 101, - type: "image/blah" - }, - { - name: "three", - maxSize: 102 - } - ], - expectedUploadCallbacks = [ - {id: 0, name: "test (one).jpeg"}, - {id: 1, name: "test (two).png"}, - {id: 2, name: "test (three).png"}, - {id: 3, name: "test.png"} - ], - expectedScaledBlobType = [ - "image/jpeg", - "image/png", - "image/png", - "image/png" - ], - actualUploadCallbacks = [], - uploader = new qq.FineUploaderBasic({ - request: {endpoint: "test/uploads"}, - scaling: { - defaultType: "image/png", - sizes: sizes - }, - callbacks: { - onUpload: function(id, name) { - actualUploadCallbacks.push({id: id, name: name}); - setTimeout(function() { - var req = fileTestHelper.getRequests()[id], - actualType = req.requestBody.fields.qqfile.type; + describe("generating a scaled Blob of the original file's type if the requested type is not specified or is not valid", function() { + function runTest(customResizer, done) { + assert.expect(7, done); - assert.equal(actualType, expectedScaledBlobType[id], "(" + id + ") Scaled blob type (" + actualType + ") is incorrect. Expected " + expectedScaledBlobType[id]); - req.respond(200, null, JSON.stringify({success: true})); - }, 10); + var sizes = [ + { + name: "one", + maxSize: 100, + type: "image/jpeg" }, - onAllComplete: function(successful, failed) { - assert.equal(successful.length, 4); - assert.equal(failed.length, 0); - assert.deepEqual(actualUploadCallbacks, expectedUploadCallbacks); + { + name: "two", + maxSize: 101, + type: "image/blah" + }, + { + name: "three", + maxSize: 102 } - } + ], + expectedUploadCallbacks = [ + {id: 0, name: "test (one).jpeg"}, + {id: 1, name: "test (two).png"}, + {id: 2, name: "test (three).png"}, + {id: 3, name: "test.png"} + ], + expectedScaledBlobType = [ + "image/jpeg", + "image/png", + "image/png", + "image/png" + ], + actualUploadCallbacks = [], + uploader = new qq.FineUploaderBasic({ + request: {endpoint: "test/uploads"}, + scaling: { + customResizer: customResizer, + defaultType: "image/png", + sizes: sizes + }, + callbacks: { + onUpload: function(id, name) { + actualUploadCallbacks.push({id: id, name: name}); + setTimeout(function() { + var req = fileTestHelper.getRequests()[id], + actualType = req.requestBody.fields.qqfile.type; + + assert.equal(actualType, expectedScaledBlobType[id], "(" + id + ") Scaled blob type (" + actualType + ") is incorrect. Expected " + expectedScaledBlobType[id]); + req.respond(200, null, JSON.stringify({success: true})); + }, 10); + }, + onAllComplete: function(successful, failed) { + assert.equal(successful.length, 4); + assert.equal(failed.length, 0); + assert.deepEqual(actualUploadCallbacks, expectedUploadCallbacks); + } + } + }); + + qqtest.downloadFileAsBlob("star.png", "image/png").then(function(blob) { + fileTestHelper.mockXhr(); + uploader.addFiles({blob: blob, name: "test.png"}); }); + } - qqtest.downloadFileAsBlob("star.png", "image/png").then(function(blob) { - fileTestHelper.mockXhr(); - uploader.addFiles({blob: blob, name: "test.png"}); + it("behaves as expected with internal resizer", function(done) { + runTest(null, done); }); + + if (!qq.ios()) { + it("behaves as expected with custom resizer", function (done) { + runTest(typicalCustomResizer, done); + }); + } }); it("uploads scaled files as expected, excluding the original: non-chunked, default options", function(done) { @@ -721,7 +787,7 @@ if (qq.supportedFeatures.scaling) { }); describe("scaleImage API method tests", function() { - it("return a scaled version of an existing image file, fail a request for a missing file, fail a request for a non-image file", function(done) { + function runTest(customResizer, done) { assert.expect(6, done); var referenceFileSize, @@ -731,7 +797,7 @@ if (qq.supportedFeatures.scaling) { onUpload: acknowledgeRequests, onAllComplete: function(successful, failed) { - uploader.scaleImage(0, {maxSize: 10}).then(function(scaledBlob) { + uploader.scaleImage(0, {customResizer: customResizer, maxSize: 10}).then(function(scaledBlob) { assert.ok(qq.isBlob(scaledBlob)); assert.ok(scaledBlob.size < referenceFileSize); assert.equal(scaledBlob.type, "image/jpeg"); @@ -744,13 +810,13 @@ if (qq.supportedFeatures.scaling) { }); // not an image - uploader.scaleImage(1, {maxSize: 10}).then(function() {}, + uploader.scaleImage(1, {customResizer: customResizer, maxSize: 10}).then(function() {}, function() { assert.ok(true); }); //missing - uploader.scaleImage(2, {maxSize: 10}).then(function() {}, + uploader.scaleImage(2, {customResizer: customResizer, maxSize: 10}).then(function() {}, function() { assert.ok(true); }); @@ -766,64 +832,84 @@ if (qq.supportedFeatures.scaling) { uploader.addFiles([{blob: up, name: "up.jpg"}, {blob: text, name: "text.txt"}]); }); }); + } + + it("return a scaled version of an existing image file, fail a request for a missing file, fail a request for a non-image file - internal resizer", function(done) { + runTest(null, done); + }); + + it("return a scaled version of an existing image file, fail a request for a missing file, fail a request for a non-image file - custom resizer", function(done) { + runTest(typicalCustomResizer, done); }); }); - it("includes EXIF data in scaled image (only if requested & appropriate)", function(done) { - assert.expect(8, done); + describe("EXIF data inclusion in scaled images", function() { + function runTest(customResizer, done) { + assert.expect(8, done); - var getReqFor = function(uuid) { - var theReq; + var getReqFor = function (uuid) { + var theReq; - qq.each(fileTestHelper.getRequests(), function(idx, req) { - if (req.requestBody.fields.qquuid === uuid) { - theReq = req; - return false; + qq.each(fileTestHelper.getRequests(), function (idx, req) { + if (req.requestBody.fields.qquuid === uuid) { + theReq = req; + return false; + } + }); + + return theReq; + }, + uploader = new qq.FineUploaderBasic({ + request: {endpoint: "test/uploads"}, + scaling: { + customResizer: customResizer, + includeExif: true, + sizes: [{name: "scaled", maxSize: 50}] + }, + callbacks: { + onUpload: function (id) { + setTimeout(function () { + var req = getReqFor(uploader.getUuid(id)), + blob = req.requestBody.fields.qqfile, + name = req.requestBody.fields.qqfilename; + + assert.ok(qq.isBlob(blob)); + new qq.Exif(blob, function () { + }).parse().then(function (tags) { + if (name.indexOf("left") === 0) { + assert.equal(tags.Orientation, 6); + } + else { + assert.fail(null, null, name + " contains EXIF data, unexpectedly"); + } + }, function () { + if (name.indexOf("star") === 0) { + assert.ok(true); + } + else { + assert.fail(null, null, name + " does not contains EXIF data, unexpectedly"); + } + }); + req.respond(200, null, JSON.stringify({success: true})); + }, 10); + } } }); - return theReq; - }, - uploader = new qq.FineUploaderBasic({ - request: {endpoint: "test/uploads"}, - scaling: { - includeExif: true, - sizes: [{name: "scaled", maxSize: 50}] - }, - callbacks: { - onUpload: function(id) { - setTimeout(function() { - var req = getReqFor(uploader.getUuid(id)), - blob = req.requestBody.fields.qqfile, - name = req.requestBody.fields.qqfilename; - - assert.ok(qq.isBlob(blob)); - new qq.Exif(blob, function(){}).parse().then(function(tags) { - if (name.indexOf("left") === 0) { - assert.equal(tags.Orientation, 6); - } - else { - assert.fail(null, null, name + " contains EXIF data, unexpectedly"); - } - }, function() { - if (name.indexOf("star") === 0) { - assert.ok(true); - } - else { - assert.fail(null, null, name + " does not contains EXIF data, unexpectedly"); - } - }); - req.respond(200, null, JSON.stringify({success: true})); - }, 10); - } - } + qqtest.downloadFileAsBlob("left.jpg", "image/jpeg").then(function (left) { + qqtest.downloadFileAsBlob("star.png", "image/png").then(function (star) { + fileTestHelper.mockXhr(); + uploader.addFiles([{blob: left, name: "left.jpg"}, {blob: star, name: "star.png"}]); + }); + }); + } + + it("includes EXIF data only if requested & appropriate - internal resizer", function(done) { + runTest(null, done); }); - qqtest.downloadFileAsBlob("left.jpg", "image/jpeg").then(function(left) { - qqtest.downloadFileAsBlob("star.png", "image/png").then(function(star) { - fileTestHelper.mockXhr(); - uploader.addFiles([{blob: left, name: "left.jpg"}, {blob: star, name: "star.png"}]); - }); + it("includes EXIF data only if requested & appropriate - custom resizer", function(done) { + runTest(typicalCustomResizer, done); }); }); });