This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
/
pick.js
361 lines (321 loc) · 13.3 KB
/
pick.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/* global $, setView, LAYOUT_MODE, photodb, LazyLoader, Spinner, ImageEditor */
/* global cropResizeRotate, MediaFrame */
/* global CONFIG_MAX_PICK_PIXEL_SIZE, CONFIG_MAX_IMAGE_PIXEL_SIZE */
/* exported Pick */
'use strict';
// XXX: the pick activity could, and probably should be handled with a
// completely different entry point from regular invocations of
// Gallery. If we can modularize the bootstrap/startup code and the
// thumbnail display code enough that we can use it in both entry
// points, it would probably be better to do it that way.
/*
* A pick activity can be in two distinct states:
*
* 1) the picking state where the user is browsing thumbnails.
* Tapping a thumbnail moves to state 2. Tapping the cancel
* button cancels the activity and the app exits.
*
* 2) the preview/cropping phase where the user sees a full-screen
* image and may have the option to crop it and also sees
* cancel and done buttons. Tapping cancel moves back to state 1.
* Tapping Done ends the pick activity and the app exits.
*
* This Pick module defines start(), select(), end(), cancel()
* and restart() methods to handle these state transitions.
* Note that this is not a Pick class, just a module of interacting
* functions for managing picks. The gallery code needs to call
* Pick.start() and Pick.crop().
*/
var Pick = (function() {
var request;
var pickType;
var pickWidth, pickHeight;
var pickedFileInfo;
var pickedFile;
var cropEditor;
// Called when we are first start up with the activity request object
function start(activity) {
request = activity;
pickType = request.source.data.type;
if (request.source.data.width && request.source.data.height) {
pickWidth = request.source.data.width;
pickHeight = request.source.data.height;
}
else {
pickWidth = pickHeight = 0;
}
setView(LAYOUT_MODE.pick);
// re-run the font-fit logic when header is visible
var pickHeading = $('pick-header-title');
pickHeading.textContent = pickHeading.textContent;
// Clicking on the pick back button cancels the pick activity.
$('pick-header').addEventListener('action', cancel);
// In crop view, the back button goes back to pick view
$('crop-top').addEventListener('action', restart);
// In crop view, the Done button crops crops the image and returns
// it to the invoking app;
$('crop-done-button').addEventListener('click', end);
}
// Called when the user selects a thumbnail in pick mode.
function select(fileinfo) {
pickedFileInfo = fileinfo;
// Do we actually want to allow the user to crop the image?
var nocrop = request.source.data.nocrop;
if (nocrop) {
// If we're not cropping show file name in the title bar
var fileName = pickedFileInfo.name.split('/').pop();
$('crop-header').textContent =
fileName.substr(0, fileName.lastIndexOf('.')) || fileName;
}
setView(LAYOUT_MODE.crop);
// Before the picked image is loaded, the done button is disabled
// to avoid users picking a black/empty image.
var doneButton = $('crop-done-button');
doneButton.disabled = true;
// We need all of these for cropping the photo:
// - ImageEditor to display the crop overlay.
// - frame_scripts because it has gesture_detector in it.
// - crop_resize_rotate.js scripts for cropResizeRotate().
LazyLoader.load(['js/frame_scripts.js',
'shared/js/media/crop_resize_rotate.js',
'js/ImageEditor.js'], gotScripts);
// When the scripts we need are loaded, load the picked file we need
function gotScripts() {
photodb.getFile(pickedFileInfo.name, gotFile);
}
// This is called with the file that needs to be cropped
function gotFile(file) {
pickedFile = file;
var previewData = pickedFileInfo.metadata.preview;
if (!previewData) {
// If there is no preview at all, this is a small image and
// it is its own preview. Just crop with the full-size image
startCrop();
}
else if (previewData.filename) {
// If there is an external preview file, use that. This means that
// the EXIF preview was not big enough
var storage = navigator.getDeviceStorage('pictures');
var getreq = storage.get(previewData.filename);
getreq.onsuccess = function() {
startCrop(getreq.result);
};
// If we fail to get the preview file, just use the full-size image
getreq.onerror = function() {
startCrop();
};
}
else {
// Otherwise, use the internal EXIF preview.
// This should be the normal case.
startCrop(pickedFile.slice(previewData.start,
previewData.end,
'image/jpeg'));
}
function startCrop(previewBlob) {
// Before the user can crop the image we have to create a
// preview of the image at the correct size and orientation
// if we do not already have one.
var blob, metadata, outputSize, useSpinner;
if (previewBlob) {
// If there is a preview, use it at full size. If we're using
// a preview we need to pass the size of the preview, but the
// EXIF orientation data from the fullsize image.
blob = previewBlob;
metadata = {
width: previewData.width,
height: previewData.height,
rotation: pickedFileInfo.metadata.rotation,
mirrored: pickedFileInfo.metadata.mirrored
};
outputSize = null;
useSpinner = false;
}
else {
// If there is no preview, use the picked file, but specify a maximum
// size so we don't decode at a size larger than needed.
blob = pickedFile;
metadata = pickedFileInfo.metadata;
var windowSize = window.innerWidth * window.innerHeight *
window.devicePixelRatio * window.devicePixelRatio;
outputSize = Math.min(windowSize,
CONFIG_MAX_PICK_PIXEL_SIZE ||
CONFIG_MAX_IMAGE_PIXEL_SIZE);
useSpinner = metadata.width * metadata.height > outputSize;
}
// Make sure the image is rotated correctly so that it appears
// right side up in the crop UI. Note that we only display a spinner
// here if we have to downsample a large image.
if (useSpinner) {
Spinner.show();
}
cropResizeRotate(blob, null, outputSize, null, metadata,
gotRotatedBlob);
}
function gotRotatedBlob(error, rotatedBlob) {
Spinner.hide();
if (error) {
console.error('Error while rotating image:', error);
rotatedBlob = pickedFile;
}
cropEditor = new ImageEditor(rotatedBlob, $('crop-frame'), {},
cropEditorReady, true);
}
function cropEditorReady() {
// Enable the done button so that users can finish picking image.
doneButton.disabled = false;
// If the initiating app doesn't want to allow the user to crop
// the image, we don't display the crop overlay. But we still use
// this image editor to preview the image.
if (nocrop) {
// Set a fake crop region even though we won't display it
// so that hasBeenCropped() works.
cropEditor.cropOverlayRegion.left = 0;
cropEditor.cropOverlayRegion.top = 0;
cropEditor.cropOverlayRegion.right = cropEditor.dest.w;
cropEditor.cropOverlayRegion.bottom = cropEditor.dest.h;
return;
}
cropEditor.showCropOverlay();
if (pickWidth) {
cropEditor.setCropAspectRatio(pickWidth, pickHeight);
}
else {
cropEditor.setCropAspectRatio(); // free form cropping
}
}
}
}
function end() {
// First, figure out what kind of image to return to the requesting app.
// If the activity request specifically included 'image/jpeg' or
// 'image/png', then we'll use that type. Otherwise, if a generic
// 'image/*' was requested (or if an unsupported type was requested)
// then we use null as the type. This value is passed to
// cropResizeRotate() and will leave the image unchanged if possible
// or will use jpeg if changes are needed.
// EXIF should be preserved.
if (Array.isArray(pickType)) {
if (pickType.indexOf(pickedFileInfo.type) !== -1) {
pickType = pickedFileInfo.type;
}
else if (pickType.indexOf('image/jpeg') !== -1) {
pickType = 'image/jpeg+exif';
}
else if (pickType.indexOf('image/png') !== -1) {
pickType = 'image/png';
}
else {
pickType = null; // Return unchanged or convert to JPEG
}
}
else if (pickType === 'image/*') {
pickType = null; // Return unchanged or convert to JPEG
}
if (pickType && pickType !== 'image/jpeg' &&
pickType !== 'image/jpeg+exif' && pickType !== 'image/png') {
pickType = null; // Return unchanged or convert to JPEG
}
else if (!pickType && pickedFileInfo.type == 'image/jpeg') {
// if we picked a JPEG image, then explicitly force EXIF copy.
pickType = 'image/jpeg+exif';
}
// In order to determine the cropRegion and outputSize arguments to
// cropResizeRotate() below we need to know the actual image size.
// If the image has EXIF rotation, we need to take that into account.
var fullImageWidth, fullImageHeight;
var rotation = pickedFileInfo.metadata.rotation || 0;
if (rotation === 90 || rotation === 270) {
fullImageWidth = pickedFileInfo.metadata.height;
fullImageHeight = pickedFileInfo.metadata.width;
}
else {
fullImageWidth = pickedFileInfo.metadata.width;
fullImageHeight = pickedFileInfo.metadata.height;
}
var cropRegion, cropFraction;
if (request.source.data.nocrop || !cropEditor.hasBeenCropped()) {
cropRegion = null;
cropFraction = 1;
}
else {
// Get the user's crop region from the crop editor
cropRegion = cropEditor.getCropRegion();
cropFraction = cropRegion.width * cropRegion.height;
// Scale to match the actual image size
cropRegion.left = Math.round(cropRegion.left * fullImageWidth);
cropRegion.top = Math.round(cropRegion.top * fullImageHeight);
cropRegion.width = Math.round(cropRegion.width * fullImageWidth);
cropRegion.height = Math.round(cropRegion.height * fullImageHeight);
}
var outputSize;
if (pickWidth && pickHeight) {
outputSize = { width: pickWidth, height: pickHeight };
}
else {
// If no desired size is specified, we have to impose some kind of limit
// so that really big images aren't decoded at full size. If there is no
// build time configuration that specifies the desired maximum pick size,
// check for maximum decode size device can handle based on device memory
// and set outputsize to MediaFrame.maxImageDecodeSize
// else we use half of the configured maximum decode size. Pick
// activities are memory-sensitive because the system app needs to keep
// both the requesting app and the gallery app alive at once.
outputSize =
CONFIG_MAX_PICK_PIXEL_SIZE ||
MediaFrame.maxImageDecodeSize ||
CONFIG_MAX_IMAGE_PIXEL_SIZE >> 1;
// If the pick request specifed a maxFileSizeBytes parameter then
// we'll use this as a hint for the output size. (We make no guarantee
// about the file size of the returned blob, but we try to be close)
// JPEG files typically have about 3 times as many pixels as bytes.
if (request.source.data.maxFileSizeBytes) {
var requestOutputSize =
Math.round(request.source.data.maxFileSizeBytes / cropFraction * 3);
outputSize = Math.min(outputSize, requestOutputSize);
}
}
// show spinner if cropResizeRotate will decode and modify the image
if (cropRegion !== null ||
typeof outputsize === 'object' ||
outputSize < fullImageWidth * fullImageHeight ||
pickedFileInfo.metadata.rotation ||
pickedFileInfo.metadata.mirrored) {
Spinner.show();
}
cropResizeRotate(pickedFile, cropRegion, outputSize, pickType,
pickedFileInfo.metadata,
function(error, blob) {
Spinner.hide();
if (error) {
console.error('while resizing image: ' + error);
blob = pickedFile;
}
// Finally, return the blob to the invoking app
request.postResult({
name: blob.name || pickedFile.name,
type: blob.type,
blob: blob
});
});
}
function cancel() {
request.postError('pick cancelled');
}
// Stop cropping the image and go back to picking mode
function restart() {
pickedFileInfo = pickedFile = null;
if (cropEditor) {
cropEditor.destroy();
cropEditor = null;
}
setView(LAYOUT_MODE.pick);
}
return {
start: start,
select: select,
cancel: cancel,
restart: restart,
end: end
};
}());