Skip to content

Commit

Permalink
Merge pull request mozilla-b2g#8773 from davidflanagan/bug847060
Browse files Browse the repository at this point in the history
Bug 847060 - create and save a preview image during metadata parsing for large images that do not have an embedded preview r=dkuo
  • Loading branch information
David Flanagan committed Mar 30, 2013
2 parents 3d7523e + 77b0c4a commit 055ff9c
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 118 deletions.
1 change: 1 addition & 0 deletions apps/gallery/Makefile
Expand Up @@ -8,6 +8,7 @@ METADATA_SCRIPTS = \
../../shared/js/blobview.js \
../../shared/js/media/jpeg_metadata_parser.js \
../../shared/js/media/get_video_rotation.js \
js/imagesize.js \
js/MetadataParser.js

concatenated_scripts: js/frame_scripts.js js/metadata_scripts.js
Expand Down
277 changes: 181 additions & 96 deletions apps/gallery/js/MetadataParser.js
Expand Up @@ -12,15 +12,19 @@ var metadataParser = (function() {
var THUMBNAIL_WIDTH = 120;
var THUMBNAIL_HEIGHT = 120;

// Don't try to decode image files bigger than this
var MAX_IMAGE_FILE_SIZE = 3 * 1024 * 1024; // 3 megabytes
// Don't try to decode image files of unknown type if bigger than this
var MAX_UNKNOWN_IMAGE_FILE_SIZE = .5 * 1024 * 1024; // half a megabyte

// Don't try to open images with more pixels than this
var MAX_IMAGE_PIXEL_SIZE = 5 * 1024 * 1024; // 5 megapixels

// An <img> element for loading images
var offscreenImage = new Image();

// The screen size. Preview images must be at least this big
var sw = window.innerWidth;
var sh = window.innerHeight;

// Create a thumbnail size canvas, copy the <img> or <video> into it
// cropping the edges as needed to make it fit, and then extract the
// thumbnail image as a blob and pass it to the callback.
Expand Down Expand Up @@ -118,95 +122,123 @@ var metadataParser = (function() {
return;
}

if (file.type !== 'image/jpeg') {
// For any kind of image other than JPEG, we just have to get
// our metadata with an <img> tag
getImageSizeAndThumbnail(file, metadataCallback, metadataError);
return;
// Figure out how big the image is if we can. For JPEG files this
// calls the JPEG parser and returns the EXIF preview if there is one.
getImageSize(file, gotImageSize, gotImageSizeError);

function gotImageSizeError(errmsg) {
// If the error message is anything other than unknown image type
// it means we've got a corrupt image file, or the image metdata parser
// can't handle the file for some reason. Log a warning but keep going
// in case the image is good and the metadata parser is buggy.
if (errmsg !== 'unknown image type') {
console.warn('getImageSize', errmsg, file.name);
}

// The image is not a JPEG, PNG or GIF file. We may still be
// able to decode and display it but we don't know the image
// size, so we won't even try if the file is too big.
if (file.size > MAX_UNKNOWN_IMAGE_FILE_SIZE) {
metadataError('Ignoring large file ' + file.name);
return;
}

// If it is not too big create a preview and thumbnail.
createThumbnailAndPreview(file, metadataCallback, metadataError);
}
else { // This is the jpeg case
parseJPEGMetadata(file, function(metadata) {

// If the image is too big, reject it now so we don't have
// memory trouble later.
if (metadata.width * metadata.height > MAX_IMAGE_PIXEL_SIZE) {
metadataError('Ignoring high-resolution image ' + file.name);
return;
}

// If the file included a preview image, use that to
// create a thumbnail. Otherwise, get the size and thumbnail
// from an offscreen image
if (metadata.preview) {
// Create a blob that is just the preview image
var previewblob = file.slice(metadata.preview.start,
metadata.preview.end,
'image/jpeg');
getImageSizeAndThumbnail(previewblob,
function(m) {
metadata.preview.width = m.width;
metadata.preview.height = m.height;
metadata.thumbnail = m.thumbnail;
metadataCallback(metadata);
},
function(errmsg) {
// If something went wrong with the
// preview blob, then fall back on
// the full-size image
console.warn('Error creating thumbnail' +
' from preview:', errmsg);
getImageSizeAndThumbnail(file,
metadataCallback,
metadataError);
});
function gotImageSize(metadata) {
// If the image is too big, reject it now so we don't have
// memory trouble later.
if (metadata.width * metadata.height > MAX_IMAGE_PIXEL_SIZE) {
metadataError('Ignoring high-resolution image ' + file.name);
return;
}

// If the file included a preview image, see if it is big enough
if (metadata.preview) {
// Create a blob that is just the preview image
var previewblob = file.slice(metadata.preview.start,
metadata.preview.end,
'image/jpeg');

// Check to see if the preview is big enough to use in MediaFrame
parseJPEGMetadata(previewblob, previewsuccess, previewerror);
}
else {
// If there wasn't a preview image, then generate a preview and
// thumbnail from the full size image.
createThumbnailAndPreview(file, metadataCallback, metadataError);
}

function previewerror(msg) {
// The preview isn't a valid jpeg file, so use the full image to
// create a preview and a thumbnail
createThumbnailAndPreview(file, metadataCallback, metadataError);
}

function previewsuccess(previewmetadata) {
var pw = previewmetadata.width; // size of the preview image
var ph = previewmetadata.height;

// If the preview is big enough, use it to create a thumbnail.
// A preview is big enough if at least one dimension is >= the
// screen size in both portait and landscape mode.
if ((pw >= sw || pw >= sh) && (pw >= sh || ph >= sw)) {
// The final argument true means don't actually create a preview
createThumbnailAndPreview(previewblob,
function(m) {
metadata.preview.width = m.width;
metadata.preview.height = m.height;
metadata.thumbnail = m.thumbnail;
metadataCallback(metadata);
},
function(errmsg) {
// If something went wrong with the
// preview blob, then fall back on
// the full-size image
console.warn('Error creating thumbnail' +
' from preview:', errmsg);
createThumbnailAndPreview(file,
metadataCallback,
metadataError);
},
true);
}
else {
// If there wasn't a preview image, then generate one from
// the full size image.
getImageSizeAndThumbnail(file, metadataCallback, metadataError);
// Preview isn't big enough so get one the hard way
createThumbnailAndPreview(file, metadataCallback, metadataError);
}
}, function(errmsg) {
// If we couldn't parse the JPEG file, then try again with
// an <img> element. This will probably fail, too.
console.warn('In parseJPEGMetadata: for file', file.name, errmsg);
getImageSizeAndThumbnail(file, metadataCallback, metadataError);
});
}
}
}

// Load an image from a file into an <img> tag, and then use that
// to get its dimensions and create a thumbnail. Store these values in
// an metadata object, and pass the object to the callback function.
// a metadata object, and pass the object to the callback function.
// If anything goes wrong, pass an error message to the error function.
function getImageSizeAndThumbnail(file, callback, error) {
// If the file size is too big it might not actually be an image file
// or it might be too big for us to process without memory problems.
// So we're not even going to try.
if (file.size > MAX_IMAGE_FILE_SIZE) {
error('Ignoring large file ' + file.name);
return;
}

// If it is a large image, create and save a preview for it as well.
function createThumbnailAndPreview(file, callback, error, nopreview) {
var metadata = {};
var url = URL.createObjectURL(file);
offscreenImage.src = url;

offscreenImage.onerror = function() {
URL.revokeObjectURL(url);
offscreenImage.removeAttribute('src');
error('getImageSizeAndThumbnail: Image failed to load');
offscreenImage.src = '';
error('createThumbnailAndPreview: Image failed to load');
};

offscreenImage.onload = function() {
URL.revokeObjectURL(url);
metadata.width = offscreenImage.width;
metadata.height = offscreenImage.height;
var iw = metadata.width = offscreenImage.width;
var ih = metadata.height = offscreenImage.height;

// If the image was already thumbnail size, it is its own thumbnail
// and it does not need a preview
if (metadata.width <= THUMBNAIL_WIDTH &&
metadata.height <= THUMBNAIL_HEIGHT) {
offscreenImage.removeAttribute('src');
offscreenImage.src = '';
//
// XXX
// Because of a gecko bug, we can't just store the image file itself
Expand All @@ -218,35 +250,88 @@ var metadataParser = (function() {
callback(metadata);
}
else {
createThumbnailFromElement(offscreenImage, false, 0,
function(thumbnail) {
metadata.thumbnail = thumbnail;
offscreenImage.removeAttribute('src');
// if the image was small, call the
// callback synchronously
if (metadata.width * metadata.height <
250000) {
callback(metadata);
}
else {
// We just decoded a big image and
// gecko needs time to release the
// memory. See Bug 792139, e.g.
// So wait before calling the callback.
// Sadly this is just idle time and
// makes scaning images without
// previews extra slow. But the
// alternative is crashing with an OOM.
// I've tested this with 250 1200x1600
// images. A delay of 200ms crashes.
// A delay of 300ms works. Rounding up
// to 400 to allow variablity between
// devices.
setTimeout(function() {
callback(metadata);
}, 400);
}
});
createThumbnailFromElement(offscreenImage, false, 0, gotThumbnail);
}

function gotThumbnail(thumbnail) {
metadata.thumbnail = thumbnail;
// If no preview was requested, or if if the image was less
// than half a megapixel then it does not need a preview
// image, and we can call the callback right away
if (nopreview || metadata.width * metadata.height < 512 * 1024) {
offscreenImage.src = '';
callback(metadata);
}
else {
// Otherwise, this was a big image and we need to create a
// preview for it so we can avoid decoding the full size
// image again when possible
createAndSavePreview();
}
}

function createAndSavePreview() {
// Figure out the preview size.
// Make sure the size is big enough for both landscape and portrait
var scale = Math.max(Math.min(sw / iw, sh / ih, 1),
Math.min(sh / iw, sw / ih, 1));
var pw = iw * scale, ph = ih * scale; // preview width and height;

// Create the preview in a canvas
var canvas = document.createElement('canvas');
canvas.width = pw;
canvas.height = ph;
var context = canvas.getContext('2d');
context.drawImage(offscreenImage, 0, 0, iw, ih, 0, 0, pw, ph);
offscreenImage.src = '';
canvas.toBlob(function(blob) {
canvas.width = canvas.height = 0;
savePreview(blob);
}, 'image/jpeg');

function savePreview(previewblob) {
var storage = navigator.getDeviceStorage('pictures');
var filename = '.gallery/previews/' + file.name;

// Delete any existing preview by this name
var delreq = storage.delete(filename);
delreq.onsuccess = delreq.onerror = save;

function save() {
var savereq = storage.addNamed(previewblob, filename);
savereq.onerror = function() {
console.error('Could not save preview image', filename);
done();
};
savereq.onsuccess = function() {
metadata.preview = {
filename: filename,
width: pw,
height: ph
};
done();
};
}

function done() {
//
// We just decoded a big image and gecko apparently needs time to
// release the memory. See Bug 792139, e.g. So wait before
// calling the callback. Sadly this is just idle time and
// makes scaning images without previews extra slow. But the
// alternative is crashing with an OOM. And if we don't
// decode the image and generate the preview now then we
// might crash with the OOM later when we display the
// image. With ~5mp images, I need a > 3000ms timeout to
// avoid a crash. With ~2mp images I found that 500ms worked.
//
var mp = iw * ih / (1024 * 1024);
var idletime = mp * mp * 150;
setTimeout(function() {
callback(metadata);
}, idletime);
}
}
}
};
}
Expand Down Expand Up @@ -279,7 +364,7 @@ var metadataParser = (function() {

offscreenImage.onerror = function() {
URL.revokeObjectURL(url);
offscreenImage.removeAttribute('src');
offscreenImage.src = '';
errorCallback('getVideoThumanailAndSize: Image failed to load');
};

Expand All @@ -294,7 +379,7 @@ var metadataParser = (function() {
createThumbnailFromElement(offscreenImage, true, metadata.rotation,
function(thumbnail) {
metadata.thumbnail = thumbnail;
offscreenImage.removeAttribute('src');
offscreenImage.src = '';
metadataCallback(metadata);
});
};
Expand Down
9 changes: 9 additions & 0 deletions apps/gallery/js/gallery.js
Expand Up @@ -457,6 +457,15 @@ function deleteFile(n) {
if (fileinfo.metadata.video) {
videostorage.delete(fileinfo.metadata.video);
}

// If the metdata parser saved a preview image for this photo,
// delete that, too.
if (fileinfo.metadata.preview && fileinfo.metadata.preview.filename) {
// We use raw device storage here instead of MediaDB because that is
// what MetadataParser.js uses for saving the preview.
var pictures = navigator.getDeviceStorage('pictures');
pictures.delete(fileinfo.metadata.preview.filename);
}
}

function fileCreated(fileinfo) {
Expand Down
2 changes: 1 addition & 1 deletion apps/gallery/js/imagesize.js
@@ -1,6 +1,6 @@
/*
* Determine the pixel dimensions of an image without actually
* decoding the image. Pass an object of metadata to the callback
* decoding the image. Passes an object of metadata to the callback
* function on success or an error message to the error function on
* failure. The metadata object will include type, width and height
* properties. Supported image types are GIF, PNG and JPEG. JPEG
Expand Down

0 comments on commit 055ff9c

Please sign in to comment.