Sequenzia/lib/assets/javascripts/moe-legacy/similar-thumbnailing.js
2013-10-26 18:06:58 -05:00

243 lines
6.7 KiB
JavaScript
Executable File

/*
* file must be a Blob object. Create and return a thumbnail of the image.
* Perform an image search using post/similar.
*
* On completion, onComplete(result) will be called, where result is an object with
* these properties:
*
* success: true or false.
*
* On failure:
* aborted: true if failure was due to a user abort.
* chromeFailure: If true, the image loaded but was empty. Chrome probably ran out
* of memory, but the selected file may be a valid image.
*
* On success:
* canvas: On success, the canvas containing the thumbnailed image.
*
*/
ThumbnailUserImage = function(file, onComplete)
{
/* Create the shared image pool, if we havn't yet. */
if(ThumbnailUserImage.image_pool == null)
ThumbnailUserImage.image_pool = new ImgPoolHandler();
this.file = file;
this.canvas = create_canvas_2d();
this.image = ThumbnailUserImage.image_pool.get();
this.onComplete = onComplete;
this.url = URL.createObjectURL(this.file);
this.image.on("load", this.image_load_event.bindAsEventListener(this));
this.image.on("abort", this.image_abort_event.bindAsEventListener(this));
this.image.on("error", this.image_error_event.bindAsEventListener(this));
document.documentElement.addClassName("progress");
this.image.src = this.url;
}
/* This is a shared pool; for clarity, don't put it in the prototype. */
ThumbnailUserImage.image_pool = null;
/* Cancel any running request. The onComplete callback will not be called.
* The object must not be reused. */
ThumbnailUserImage.prototype.destroy = function()
{
document.documentElement.removeClassName("progress");
this.onComplete = null;
this.image.stopObserving();
ThumbnailUserImage.image_pool.release(this.image);
this.image = null;
if(this.url != null)
{
URL.revokeObjectURL(this.url);
this.url = null;
}
}
ThumbnailUserImage.prototype.completed = function(result)
{
if(this.onComplete)
this.onComplete(result);
this.destroy();
}
/* When the image finishes loading after form_submit_event sets it, update the canvas
* thumbnail from it. */
ThumbnailUserImage.prototype.image_load_event = function(e)
{
/* Reduce the image size to thumbnail resolution. */
var width = this.image.width;
var height = this.image.height;
var max_width = 128;
var max_height = 128;
if(width > max_width)
{
var ratio = max_width/width;
height *= ratio; width *= ratio;
}
if(height > max_height)
{
var ratio = max_height/height;
height *= ratio; width *= ratio;
}
width = Math.round(width);
height = Math.round(height);
/* Set the canvas to the image size we want. */
var canvas = this.canvas;
canvas.width = width;
canvas.height = height;
/* Blit the image onto the canvas. */
var ctx = canvas.getContext("2d");
/* Clear the canvas, so check_image_contents can check that the data was correctly loaded. */
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height);
if(!this.check_image_contents())
{
this.completed({ success: false, chromeFailure: true });
return;
}
this.completed({ success: true, canvas: this.canvas });
}
/*
* Work around a Chrome bug. When very large images fail to load, we still get
* onload and the image acts like a loaded, completely transparent image, instead
* of firing onerror. This makes it difficult to tell if the image actually loaded
* or not. Check that the image loaded by looking at the results; reject the image
* if it's completely transparent.
*/
ThumbnailUserImage.prototype.check_image_contents = function()
{
var ctx = this.canvas.getContext("2d");
var image = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
var data = image.data;
/* Iterate through the alpha components, and search for any nonzero value. */
var idx = 3;
var max_idx = image.width * image.height * 4;
while(idx < max_idx)
{
if(data[idx] != 0)
return true;
idx += 4;
}
return false;
}
ThumbnailUserImage.prototype.image_abort_event = function(e)
{
this.completed({ success: false, aborted: true });
}
/* This happens on normal errors, usually because the file isn't a supported image. */
ThumbnailUserImage.prototype.image_error_event = function(e)
{
this.completed({ success: false });
}
/* If the necessary APIs aren't supported, don't use ThumbnailUserImage. */
if(!("URL" in window) || create_canvas_2d() == null)
ThumbnailUserImage = null;
SimilarWithThumbnailing = function(form)
{
this.similar = null;
this.form = form;
this.force_file = null;
form.on("submit", this.form_submit_event.bindAsEventListener(this));
}
SimilarWithThumbnailing.prototype.form_submit_event = function(e)
{
var post_file = this.form.down("#file");
/* If the files attribute isn't supported, or we have no file (source upload), use regular
* form submission. */
if(post_file.files == null || post_file.files.length == 0)
return;
/* If we failed to load the image last time due to a silent Chrome error, continue with
* the submission normally this time. */
var file = post_file.files[0];
if(this.force_file && this.force_file == file)
{
this.force_file = null;
return;
}
e.stop();
if(this.similar)
this.similar.destroy();
this.similar = new ThumbnailUserImage(file, this.complete.bind(this));
}
/* Submit a post/similar request using the image currently in the canvas. */
SimilarWithThumbnailing.prototype.complete = function(result)
{
if(result.chromeFailure)
{
notice("The image failed to load; submitting normally...");
this.force_file = this.file;
/* Resend the submit event. Defer it, so the notice can take effect before we
* navigate off the page. */
(function() { this.form.simulate_submit(); }).bind(this).defer();
return;
}
if(!result.success)
{
if(!result.aborted)
alert("The file couldn't be loaded.");
return;
}
/* Grab a data URL from the canvas; this is what we'll send to the server. */
var data_url = result.canvas.toDataURL();
/* Create the FormData containing the thumbnail image we're sending. */
var form_data = new FormData();
form_data.append("url", data_url);
var req = new Ajax.Request("/post/similar.json", {
method: "post",
postBody: form_data,
/* Tell Prototype not to change XHR's contentType; it breaks FormData. */
contentType: null,
onComplete: function(resp)
{
var json = resp.responseJSON;
if(!json.success)
{
notice(json.reason);
return;
}
/* Redirect to the search results. */
window.location.href = "/post/similar?search_id=" + json.search_id;
}
});
}
/* If the necessary APIs aren't supported, don't use SimilarWithThumbnailing. */
if(!("FormData" in window) || !ThumbnailUserImage)
SimilarWithThumbnailing = null;