243 lines
6.7 KiB
JavaScript
243 lines
6.7 KiB
JavaScript
|
/*
|
||
|
* 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;
|
||
|
|