Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
713 lines
28 KiB
JavaScript
Executable file
713 lines
28 KiB
JavaScript
Executable file
'use strict';
|
|
/* global
|
|
CONFIG_MAX_IMAGE_PIXEL_SIZE,
|
|
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT,
|
|
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
|
|
cropResizeRotate,
|
|
Downsample,
|
|
getImageSize,
|
|
getVideoRotation,
|
|
isPhone,
|
|
parseJPEGMetadata,
|
|
videostorage
|
|
*/
|
|
/* exported
|
|
metadataParser
|
|
*/
|
|
|
|
//
|
|
// This file defines a single metadataParsers object. The two
|
|
// properties of this object are metadata parsing functions for image
|
|
// files and video files, intended for use with the MediaDB class.
|
|
//
|
|
// This file depends on JPEGMetadataParser.js and blobview.js
|
|
//
|
|
var metadataParser = (function() {
|
|
// If we generate our own thumbnails, aim for this size.
|
|
// Calculate needed size from longer side of the screen.
|
|
var THUMBNAIL_WIDTH = computeThumbnailWidth();
|
|
var THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH;
|
|
|
|
function computeThumbnailWidth() {
|
|
// Make sure this works regardless of current device orientation
|
|
var width = window.innerWidth || screen.width;
|
|
var height = window.innerHeight || screen.height;
|
|
var portraitWidth = Math.max(1, Math.min(width, height));
|
|
var landscapeWidth = Math.max(1, Math.max(width, height));
|
|
var thumbnailsPerRowPortrait = isPhone ? 3 : 4;
|
|
var thumbnailsPerRowLandscape = isPhone ? 4 : 6;
|
|
return Math.round(window.devicePixelRatio *
|
|
Math.max(portraitWidth / thumbnailsPerRowPortrait,
|
|
landscapeWidth / thumbnailsPerRowLandscape));
|
|
}
|
|
|
|
var thumbnailSize = {
|
|
width: THUMBNAIL_WIDTH,
|
|
height: THUMBNAIL_HEIGHT
|
|
};
|
|
|
|
// Don't try to decode image files of unknown type if bigger than this
|
|
var MAX_UNKNOWN_IMAGE_FILE_SIZE = 0.5 * 1024 * 1024; // half a megabyte
|
|
|
|
// If a jpeg does not have a suitable EXIF preview and has more than
|
|
// this number of pixels, then we create an external preview for it
|
|
var MAX_PREVIEWLESS_JPEG_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
|
|
// Note that we use screen.width instead of window.innerWidth because
|
|
// the window size is different when we are invoked for a pick activity
|
|
// (non-fullscreen) and when we are invoked normally (fullscreen).
|
|
var sw = screen.width * window.devicePixelRatio;
|
|
var sh = screen.height * window.devicePixelRatio;
|
|
|
|
// 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.
|
|
// This utility function is used by both the image and video metadata parsers
|
|
function createThumbnailFromElement(elt, video, rotation,
|
|
mirrored, callback, error) {
|
|
try {
|
|
// Create a thumbnail image
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = THUMBNAIL_WIDTH;
|
|
canvas.height = THUMBNAIL_HEIGHT;
|
|
var context = canvas.getContext('2d', { willReadFrequently: true });
|
|
var eltwidth = elt.width;
|
|
var eltheight = elt.height;
|
|
var scalex = canvas.width / eltwidth;
|
|
var scaley = canvas.height / eltheight;
|
|
|
|
// Take the larger of the two scales: we crop the image to the thumbnail
|
|
var scale = Math.max(scalex, scaley);
|
|
|
|
// Calculate the region of the image that will be copied to the
|
|
// canvas to create the thumbnail
|
|
var w = Math.round(THUMBNAIL_WIDTH / scale);
|
|
var h = Math.round(THUMBNAIL_HEIGHT / scale);
|
|
var x = Math.round((eltwidth - w) / 2);
|
|
var y = Math.round((eltheight - h) / 2);
|
|
|
|
var centerX = Math.floor(THUMBNAIL_WIDTH / 2);
|
|
var centerY = Math.floor(THUMBNAIL_HEIGHT / 2);
|
|
|
|
// If a orientation is specified, rotate/mirroring the canvas context.
|
|
if (rotation || mirrored) {
|
|
context.save();
|
|
// All transformation are applied to the center of the thumbnail.
|
|
context.translate(centerX, centerY);
|
|
}
|
|
if (mirrored) {
|
|
context.scale(-1, 1);
|
|
}
|
|
if (rotation) {
|
|
switch (rotation) {
|
|
case 90:
|
|
context.rotate(Math.PI / 2);
|
|
break;
|
|
case 180:
|
|
context.rotate(Math.PI);
|
|
break;
|
|
case 270:
|
|
context.rotate(-Math.PI / 2);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (rotation || mirrored) {
|
|
context.translate(-centerX, -centerY);
|
|
}
|
|
|
|
// Draw that region of the image into the canvas, scaling it down
|
|
context.drawImage(elt, x, y, w, h,
|
|
0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
|
|
|
|
// Restore the default rotation so the play arrow comes out correctly
|
|
if (rotation || mirrored) {
|
|
context.restore();
|
|
}
|
|
|
|
// If this is a video, superimpose a translucent play button over
|
|
// the captured video frame to distinguish it from a still photo thumbnail
|
|
if (video) {
|
|
// First draw a transparent gray circle
|
|
context.fillStyle = 'rgba(0, 0, 0, .2)';
|
|
context.beginPath();
|
|
context.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
|
|
THUMBNAIL_HEIGHT / 5, 0, 2 * Math.PI, false);
|
|
context.fill();
|
|
|
|
// Now outline the circle in white
|
|
context.strokeStyle = 'rgba(255,255,255,.6)';
|
|
context.lineWidth = 2;
|
|
context.stroke();
|
|
|
|
// And add a white play arrow.
|
|
context.beginPath();
|
|
context.fillStyle = 'rgba(255,255,255,.6)';
|
|
// The height of an equilateral triangle is sqrt(3)/2 times the side
|
|
var side = THUMBNAIL_HEIGHT / 5;
|
|
var triangle_height = side * Math.sqrt(3) / 2;
|
|
context.moveTo(THUMBNAIL_WIDTH / 2 + triangle_height * 2 / 3,
|
|
THUMBNAIL_HEIGHT / 2);
|
|
context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
|
|
THUMBNAIL_HEIGHT / 2 - side / 2);
|
|
context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
|
|
THUMBNAIL_HEIGHT / 2 + side / 2);
|
|
context.closePath();
|
|
context.fill();
|
|
}
|
|
|
|
canvas.toBlob(function(blob) {
|
|
context = null;
|
|
canvas.width = canvas.height = 0;
|
|
canvas = null;
|
|
callback(blob);
|
|
}, 'image/jpeg');
|
|
} catch (ex) {
|
|
// An error may be thrown when the drawImage decodes a broken/trancated
|
|
// image.
|
|
// The elt may be a offscreen image. So, the image metadata is parsed, and
|
|
// the image data is loaded but not decoded. The drawImage triggers the
|
|
// image decoder to decode the image data. And an error may be thrown.
|
|
error('createThumbnailFromElement:' + ex.message);
|
|
}
|
|
}
|
|
|
|
var VIDEOFILE = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.jpg/;
|
|
|
|
function metadataParser(file, metadataCallback, metadataError, bigFile) {
|
|
// If the file is a poster image for a video file, then we've want
|
|
// video metadata, not image metadata
|
|
if (VIDEOFILE.test(file.name)) {
|
|
videoMetadataParser(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) {
|
|
// The image is not a JPEG, PNG, GIF or BMP 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 the file is too small to be an image, ignore it
|
|
if (file.size < 32) {
|
|
metadataError('Ignoring small file ' + file.name);
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// If it is not too big create a preview and thumbnail.
|
|
createThumbnailAndPreview(file,
|
|
metadataCallback,
|
|
metadataError,
|
|
false,
|
|
bigFile,
|
|
{});
|
|
}
|
|
|
|
function gotImageSize(metadata) {
|
|
//
|
|
// If the image is too big, reject it now so we don't have
|
|
// memory trouble later.
|
|
//
|
|
// CONFIG_MAX_IMAGE_PIXEL_SIZE is maximum image resolution we
|
|
// can handle. It's from config.js which is generated at build
|
|
// time (see build/application-data.js).
|
|
//
|
|
// For jpeg images we can downsample while decoding which means that
|
|
// we can handle images that are quite a bit larger.
|
|
//
|
|
// getImageSize() distinguishes pjpeg files from jpeg, because
|
|
// progressive jpeg files are quite different than regular jpegs.
|
|
// Not only do pjpegs not benefit from using #-moz-samplesize, they
|
|
// actually require more memory to decode than other images of the same
|
|
// resolution, so we actually need to reduce the image size limit for
|
|
// this image type.
|
|
//
|
|
var imagesizelimit = CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
|
if (metadata.type === 'jpeg') {
|
|
imagesizelimit *= Downsample.MAX_AREA_REDUCTION;
|
|
}
|
|
else if (metadata.type === 'pjpeg') {
|
|
// As a semi-educated guess, we'll say that we can handle pjpegs
|
|
// 2/3rds the size of the largest PNG we can handle.
|
|
imagesizelimit *= 2 / 3;
|
|
}
|
|
|
|
//
|
|
// Even if we can downsample an image while decoding it, we still
|
|
// have to read the entire image file. If the file is particularly
|
|
// large we might also have memory problems. (See bug 1008834: a 20mb
|
|
// 80mp jpeg file will cause an OOM on Tarako even though we can
|
|
// decode it at < 2mp). Rather than adding another build-time config
|
|
// variable to specify the maximum file size, however, we'll just
|
|
// base the file size limit on CONFIG_MAX_IMAGE_PIXEL_SIZE.
|
|
// So if that variable is set to 2M, then we might use up to 12Mb of
|
|
// memory. 2 * 2M bytes for the image file and 4 bytes times 2M pixels
|
|
// for the decoded image. A 4mb file size limit should accomodate
|
|
// most JPEG files up to 12 or 16 megapixels.
|
|
//
|
|
// Note that this size limit is just a guess. If we continue to see
|
|
// OOMs with large files then we should change the number 2 below
|
|
// to something smaller.
|
|
//
|
|
var filesizelimit = 2 * CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
|
|
|
if (metadata.width * metadata.height > imagesizelimit ||
|
|
file.size > filesizelimit) {
|
|
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.
|
|
useFullsizeImage();
|
|
}
|
|
|
|
function previewerror(msg) {
|
|
// The preview isn't a valid jpeg file, so use the full image to
|
|
// create a preview and a thumbnail
|
|
console.error(msg);
|
|
useFullsizeImage();
|
|
}
|
|
|
|
// If we can't find a valid embedded EXIF preview image then we come here
|
|
function useFullsizeImage() {
|
|
// If the full size image is a JPEG then we'll just skip the
|
|
// preview generation because we can downsample and decode it
|
|
// at preview size when needed. So in this case, we just need
|
|
// to create a thumbnail for the image. On the other hand, if
|
|
// this is not a JPEG image then we have to create both a
|
|
// thumbnail and an external preview image. Note that progressive
|
|
// jpegs will have a type 'pjpeg' and will be handled below
|
|
// like png images.
|
|
|
|
// For jpeg images with image sizes more than 5MP, we need
|
|
// to create preview on sdcard to avoid flash while opening image.
|
|
// See bug 1181290.
|
|
// This constraint can be removed when gecko supports downscale during
|
|
// decode by canvas size for offscreen images. This will
|
|
// facilitate removing #moz-sample-size and using ddd.
|
|
// See bug 1206206.
|
|
|
|
if (metadata.type === 'jpeg') {
|
|
if (metadata.width * metadata.height <= MAX_PREVIEWLESS_JPEG_SIZE) {
|
|
// If the jpeg image isn't too big we don't need a preview
|
|
// because we can decode it quickly enough at screen size.
|
|
// So in this case we just need to create a thumbnail.
|
|
cropResizeRotate(file, null, thumbnailSize, null, metadata,
|
|
function gotThumbnail(error, thumbnailBlob) {
|
|
if (error) {
|
|
var msg = 'Failed to create thumbnail for' +
|
|
file.name;
|
|
console.error(msg);
|
|
metadataError(msg);
|
|
return;
|
|
}
|
|
|
|
metadata.preview = null;
|
|
metadata.thumbnail = thumbnailBlob;
|
|
metadataCallback(metadata);
|
|
});
|
|
}
|
|
else {
|
|
// Otherwise, this is a high-resolution jpeg that does not
|
|
// have an EXIF preview big enough for the screen. We need
|
|
// to create both a thumbnail and a preview image but we can
|
|
// do that with efficient downsampling using cropResizeRotate().
|
|
createJPEGThumbnailAndPreview(file, metadata,
|
|
metadataCallback,
|
|
metadataError);
|
|
}
|
|
}
|
|
else {
|
|
// In this case we have no preview and the image is not a jpeg
|
|
// that can be downsampled with #-moz-samplesize
|
|
createThumbnailAndPreview(file,
|
|
metadataCallback,
|
|
metadataError,
|
|
false,
|
|
bigFile,
|
|
metadata);
|
|
}
|
|
}
|
|
|
|
function previewsuccess(previewmetadata) {
|
|
// Size of the preview image
|
|
var pw = previewmetadata.width;
|
|
var ph = previewmetadata.height;
|
|
// optional configuration specifying minimum size
|
|
var mw = CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH;
|
|
var mh = CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT;
|
|
|
|
var bigenough;
|
|
|
|
// If config.js specifies a minimum required preview size,
|
|
// then this preview is big enough if both dimensions are
|
|
// larger than that configured minimum. Otherwise, the preview
|
|
// is big enough if at least one dimension is >= the screen
|
|
// size in both portait and landscape mode.
|
|
if (mw && mh) {
|
|
bigenough =
|
|
Math.max(pw, ph) >= Math.max(mw, mh) &&
|
|
Math.min(pw, ph) >= Math.min(mw, mh);
|
|
}
|
|
else {
|
|
bigenough = (pw >= sw || ph >= sh) && (pw >= sh || ph >= sw);
|
|
}
|
|
|
|
// Does the aspect ratio of the preview match the aspect ratio of
|
|
// the full-size image? If not we have to assume it is distorted
|
|
// and should not be used.
|
|
var aspectRatioMatches =
|
|
Math.abs(pw / ph - metadata.width / metadata.height) < 0.01;
|
|
|
|
// If the preview is good, use it to create a thumbnail.
|
|
if (bigenough && aspectRatioMatches) {
|
|
metadata.preview.width = pw;
|
|
metadata.preview.height = ph;
|
|
|
|
// This is the most common case. We've got a JPEG image with a
|
|
// JPEG preview. All we need is a thumbnail, and we can create that
|
|
// most efficiently with cropResizeRotate which will use
|
|
// #-moz-samplesize to save memory while decoding the image.
|
|
// Note that we apply the EXIF orientation information from
|
|
// the full size image to the preview image.
|
|
previewmetadata.rotation = metadata.rotation;
|
|
previewmetadata.mirrored = metadata.mirrored;
|
|
cropResizeRotate(previewblob, null, thumbnailSize, null,
|
|
previewmetadata,
|
|
function gotThumbnailBlob(error, thumbnailBlob) {
|
|
if (error) {
|
|
previewerror(error);
|
|
return;
|
|
}
|
|
metadata.thumbnail = thumbnailBlob;
|
|
metadataCallback(metadata);
|
|
});
|
|
} else {
|
|
// Preview isn't big enough so get one the hard way
|
|
useFullsizeImage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function is called for JPEG images that do not have a suitable
|
|
// EXIF preview image and that have more than MAX_PREVIEWLESS_JPEG_SIZE
|
|
// pixels. It creates and saves a preview image and a thumbnail.
|
|
// Unlike createThumbnailAndPreview(), it uses cropResizeRotate() which
|
|
// downsamples jpeg images while decoding them to save memory.
|
|
function createJPEGThumbnailAndPreview(file, metadata,
|
|
metadataCallback, metadataError)
|
|
{
|
|
|
|
// Figure out the preview size.
|
|
var scale = Math.max(Math.min(sw / metadata.width,
|
|
sh / metadata.height, 1),
|
|
Math.min(sh / metadata.width,
|
|
sw / metadata.height, 1));
|
|
var previewSize = {
|
|
width: Math.round(metadata.width * scale),
|
|
height: Math.round(metadata.height * scale)
|
|
};
|
|
|
|
// Create a preview image.
|
|
// Note that we can't use the full metadata object here
|
|
// because we don't want the preview image to be rotated.
|
|
// We want the same orientation as the original image
|
|
cropResizeRotate(file, null, previewSize, null,
|
|
{ width: metadata.width, height: metadata.height },
|
|
function gotPreview(error, previewBlob) {
|
|
if (error) {
|
|
var msg = 'Failed to create preview for' +
|
|
file.name;
|
|
console.error(msg);
|
|
metadataError(msg);
|
|
return;
|
|
}
|
|
|
|
// Save this preview to the sdcard
|
|
savePreview(file, previewBlob, metadataError, function(filename) {
|
|
// Record the preview image in the metadata
|
|
metadata.preview = {
|
|
filename: filename,
|
|
width: previewSize.width,
|
|
height: previewSize.height
|
|
};
|
|
|
|
// Now create the thumbnail from this smaller preview image
|
|
// In this case we need to pass the size of the preview
|
|
// and the rotation of the original image.
|
|
cropResizeRotate(previewBlob, null, thumbnailSize, null,
|
|
{
|
|
width: previewSize.width,
|
|
height: previewSize.height,
|
|
rotation: metadata.rotation
|
|
},
|
|
function gotThumbnail(error, thumbnailBlob) {
|
|
// If thumbnail creation failed report the error
|
|
if (error) {
|
|
metadataError(error);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, remember thumbnail and we're done
|
|
metadata.thumbnail = thumbnailBlob;
|
|
metadataCallback(metadata);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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
|
|
// a metadata object, and pass the object to the callback function.
|
|
// If anything goes wrong, pass an error message to the error function.
|
|
// If it is a large image, create and save a preview for it as well.
|
|
//
|
|
// We used to call this function for every image. Now that gecko supports
|
|
// the #-moz-samplesize media fragment, however, we handle jpeg images
|
|
// with cropResizeRotate() for memory efficiency, and this function is
|
|
// used only for non-jpeg files.
|
|
function createThumbnailAndPreview(file, callback, error, nopreview,
|
|
bigFile, metadata) {
|
|
var url = URL.createObjectURL(file);
|
|
offscreenImage.src = url;
|
|
|
|
offscreenImage.onerror = function() {
|
|
URL.revokeObjectURL(url);
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
error('createThumbnailAndPreview: Image failed to load');
|
|
};
|
|
|
|
offscreenImage.onload = function() {
|
|
URL.revokeObjectURL(url);
|
|
|
|
var iw = offscreenImage.width;
|
|
var ih = offscreenImage.height;
|
|
|
|
// Don't overwrite the metadata in the case we read a previewblob.
|
|
if (!nopreview) {
|
|
metadata.width = iw;
|
|
metadata.height = ih;
|
|
}
|
|
|
|
// If this is a big image, then decoding it takes a lot of memory.
|
|
// We set this flag to prevent the user from zooming in on other
|
|
// images at the same time because that also takes a lot of memory
|
|
// and we don't want to crash with an OOM. If we find one big image
|
|
// we assume that there may be others, so the flag remains set until
|
|
// the current scan is complete.
|
|
//
|
|
// XXX: When bug 854795 is fixed, we'll be able to create previews
|
|
// for large images without using so much memory, and we can remove
|
|
// this flag then.
|
|
if (iw * ih > 2 * 1024 * 1024 && bigFile) {
|
|
bigFile();
|
|
}
|
|
|
|
// 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.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
metadata.thumbnail = file;
|
|
callback(metadata);
|
|
}
|
|
else {
|
|
createThumbnailFromElement(
|
|
offscreenImage,
|
|
false,
|
|
metadata.rotation || 0,
|
|
metadata.mirrored || false,
|
|
gotThumbnail,
|
|
error);
|
|
}
|
|
|
|
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.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('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));
|
|
// preview width and height
|
|
var pw = Math.round(iw * scale);
|
|
var ph = Math.round(ih * scale);
|
|
|
|
// Create the preview in a canvas
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = pw;
|
|
canvas.height = ph;
|
|
var context = canvas.getContext('2d', { willReadFrequently: true });
|
|
context.drawImage(offscreenImage, 0, 0, iw, ih, 0, 0, pw, ph);
|
|
canvas.toBlob(function(blob) {
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
canvas.width = canvas.height = 0;
|
|
savePreview(file, blob, error, function(filename) {
|
|
metadata.preview = {
|
|
filename: filename,
|
|
width: pw,
|
|
height: ph
|
|
};
|
|
callback(metadata);
|
|
});
|
|
}, 'image/jpeg');
|
|
}
|
|
};
|
|
}
|
|
|
|
// Given an image file and a preview blob for it, this function saves
|
|
// the preview to a file in .gallery/previews/ and then passes the filename
|
|
// to the specified callback. Calls errorCallback if an error occurs.
|
|
// This is used by createThumbnailAndPreview() and by
|
|
// createJPEGThumbnailAndPreview().
|
|
function savePreview(file, previewblob, errorCallback, filenameCallback) {
|
|
var storage = navigator.getDeviceStorage('pictures');
|
|
var filename;
|
|
if (file.name[0] === '/') {
|
|
// We expect file.name to be a fully qualified name (perhaps
|
|
// something like /sdcard/DCIM/100MZLLA/IMG_0001.jpg).
|
|
var slashIndex = file.name.indexOf('/', 1);
|
|
if (slashIndex < 0) {
|
|
errorCallback('savePreview: Bad filename: \'' + file.name + '\'');
|
|
return;
|
|
}
|
|
filename =
|
|
file.name.substring(0, slashIndex) + // storageName (i.e. /sdcard)
|
|
|
|
'/.gallery/previews' +
|
|
file.name.substring(slashIndex); // rest of path (i,e, /DCIM/...)
|
|
} else {
|
|
// On non-composite storage areas (e.g. desktop), file.name will be
|
|
// a relative path.
|
|
filename = '.gallery/previews/' + file.name;
|
|
}
|
|
|
|
// Add a '.jpeg' extension to all preview files. This is necessary
|
|
// because all previews get saved as jpegs even if the original
|
|
// image is not a jpeg. But DeviceStorage uses the filename extension
|
|
// to determine the file type.
|
|
filename += '.jpeg';
|
|
|
|
// 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);
|
|
};
|
|
|
|
// Don't actually wait for the save to complete.
|
|
filenameCallback(filename);
|
|
}
|
|
}
|
|
|
|
function videoMetadataParser(file, metadataCallback, errorCallback) {
|
|
var metadata = {};
|
|
var extensions = ['webm', '3gp', 'mp4'];
|
|
function findVideo() {
|
|
var videofilename = file.name.replace(/\.jpg$/, '.' + extensions.shift());
|
|
var getreq = videostorage.get(videofilename);
|
|
getreq.onerror = function() {
|
|
if (extensions.length) findVideo();
|
|
else errorCallback('cannot get video file: ' + videofilename);
|
|
};
|
|
getreq.onsuccess = function() {
|
|
metadata.video = videofilename;
|
|
if (/\.webm$/.test(videofilename)) {
|
|
metadata.rotation = 0;
|
|
getVideoThumbnailAndSize();
|
|
return;
|
|
}
|
|
getVideoRotation(getreq.result, function(rotation) {
|
|
if (typeof rotation === 'number') {
|
|
metadata.rotation = rotation;
|
|
getVideoThumbnailAndSize();
|
|
} else errorCallback('Video rotation: ' + rotation);
|
|
});
|
|
};
|
|
}
|
|
findVideo();
|
|
|
|
function getVideoThumbnailAndSize() {
|
|
var url = URL.createObjectURL(file);
|
|
offscreenImage.src = url;
|
|
|
|
offscreenImage.onerror = function() {
|
|
URL.revokeObjectURL(url);
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
errorCallback('getVideoThumanailAndSize: Image failed to load');
|
|
};
|
|
|
|
offscreenImage.onload = function() {
|
|
URL.revokeObjectURL(url);
|
|
|
|
// We store the unrotated size of the poster image. The video
|
|
// itself might be larger or smaller than this, but it will
|
|
// have the same orientation and aspect ratio.
|
|
metadata.width = offscreenImage.width;
|
|
metadata.height = offscreenImage.height;
|
|
|
|
createThumbnailFromElement(offscreenImage,
|
|
true,
|
|
metadata.rotation,
|
|
false,
|
|
function(thumbnail) {
|
|
metadata.thumbnail = thumbnail;
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
metadataCallback(metadata);
|
|
},
|
|
errorCallback);
|
|
};
|
|
}
|
|
}
|
|
|
|
return metadataParser;
|
|
}());
|