1page.title=Device Art Generator
2page.image=/images/device-art-ex-crop.jpg
3page.metaDescription=Drag and drop screenshots of your app into device artwork, for better looking promotional images and improved visual context.
4meta.tags="disttools, promoting, deviceart, marketing"
5page.tags="device, deviceart, nexus, assets"
6Xnonavpage=true
7
8@jd:body
9
10<p>The device art generator enables you to quickly wrap app screenshots in device artwork. This provides better visual context for your app screenshots on your website or in other promotional materials</p>
11
12<p class="note"><strong>Note</strong>: Do <em>not</em> use graphics created here in your 1024x500
13feature image or screenshots for your Google Play app listing.</p>
14
15
16
17<div class="supported-browser">
18
19<div class="cols">
20  <div class="col-3">
21    <h4>Step 1</h4>
22    <p>Drag a screenshot from your desktop onto a device to the right.</p>
23  </div>
24  <div class="col-10">
25    <ul class="device-list primary"></ul>
26    <a href="#" id="archive-expando">Older devices</a>
27    <ul class="device-list archive"></ul>
28  </div>
29</div>
30
31
32
33<div class="cols">
34  <div class="col-3">
35    <h4>Step 2</h4>
36    <p>Customize the generated image and drag it to your desktop to save.</p>
37    <p id="frame-customizations">
38      <input type="checkbox" id="output-shadow" checked="checked" class="form-field-checkbutton">
39      <label for="output-shadow">Shadow</label><br>
40      <input type="checkbox" id="output-glare" checked="checked" class="form-field-checkbutton">
41      <label for="output-glare">Screen Glare</label><br><br>
42      <a class="button" id="rotate-button">Rotate</a>
43    </p>
44    <p id="wear-customizations">
45      <input type="radio" id="output-square" name="output-wear" checked="checked" class="form-field-checkbutton">
46      <label for="output-square">Square</label><br>
47      <input type="radio" id="output-round" name="output-wear" class="form-field-checkbutton">
48      <label for="output-round">Round</label><br><br>
49    </p>
50  </div>
51  <div class="col-10">
52    <!-- position:relative fixes an issue where dragging an image out of a inline-block container
53         produced no drag feedback image in Chrome 28. -->
54    <div id="output" style="position:relative">No input image.</div>
55  </div>
56</div>
57
58</div>
59
60<div class="unsupported-browser" style="display: none">
61  <p class="warning"><strong>Error:</strong> This page requires
62    <span id="unsupported-browser-reason">certain features</span>, which your web browser
63    doesn't support. To continue, navigate to this page on a supported web browser, such as
64    <strong>Google Chrome</strong>.</p>
65  <a href="https://www.google.com/chrome/" class="button">Get Google Chrome</a>
66  <br><br>
67</div>
68
69<style>
70  h4 {
71    text-transform: uppercase;
72  }
73
74  .device-list {
75    padding: 1em 0 0 0;
76    margin: 0;
77  }
78
79  .device-list li {
80    display: inline-block;
81    vertical-align: bottom;
82    margin: 0;
83    margin-right: 20px;
84    text-align: center;
85  }
86
87  .device-list li .thumb-container {
88    display: inline-block;
89  }
90
91  .device-list li .thumb-container img {
92    margin-bottom: 8px;
93    opacity: 0.6;
94
95    -webkit-transition: -webkit-transform 0.2s, opacity 0.2s;
96       -moz-transition:    -moz-transform 0.2s, opacity 0.2s;
97            transition:         transform 0.2s, opacity 0.2s;
98  }
99
100  .device-list li.drag-hover .thumb-container img {
101    opacity: 1;
102
103    -webkit-transform: scale(1.1);
104       -moz-transform: scale(1.1);
105            transform: scale(1.1);
106  }
107
108  .device-list li .device-details {
109    font-size: 13px;
110    line-height: 16px;
111    color: #888;
112  }
113
114  .device-list li .device-url {
115    font-weight: bold;
116  }
117
118  #archive-expando {
119    display: block;
120    font-size: 13px;
121    font-weight: bold;
122    color: #333;
123    text-transform: uppercase;
124    margin-top: 16px;
125    padding-top: 16px;
126    padding-left: 28px;
127    border-top: 1px solid transparent;
128    background: transparent url({@docRoot}assets/images/styles/disclosure_down.png)
129                no-repeat scroll 0 8px;
130    -webkit-transition: border 0.2s;
131       -moz-transition: border 0.2s;
132            transition: border 0.2s;
133  }
134
135  #archive-expando.expanded {
136    background-image: url({@docRoot}assets/images/styles/disclosure_up.png);
137    border-top: 1px solid #ccc;
138  }
139
140  .device-list.archive {
141    max-height: 0;
142    overflow: hidden;
143    opacity: 0;
144
145    -webkit-transition: max-height 0.2s, opacity 0.2s;
146       -moz-transition: max-height 0.2s, opacity 0.2s;
147            transition: max-height 0.2s, opacity 0.2s;
148  }
149
150  .device-list.archive.expanded {
151    opacity: 1;
152    max-height: 300px;
153  }
154
155  #output {
156    color: #f44;
157    font-style: italic;
158  }
159
160  #output img {
161    max-height: 500px;
162  }
163</style>
164<script>
165  // Global variables
166  var g_currentImage;
167  var g_currentDevice;
168  var g_currentObjectURL;
169  var g_currentBlob;
170
171  // Global constants
172  var MSG_INVALID_INPUT_IMAGE = 'Invalid screenshot provided. Screenshots must be PNG files '
173      + 'matching the target device\'s screen aspect ratio in either portrait or landscape.';
174  var MSG_INVALID_WEAR_IMAGE = 'Invalid screenshot provided. Screenshots must be PNG files '
175      + 'matching the target device\'s screen aspect ratio.'
176      + ' Capture screenshots from a Wear emulator or device with '
177      + '<a href="http://developer.android.com/tools/debugging/debugging-studio.html#screenCap">Android Studio</a>.';
178  var MSG_NO_INPUT_IMAGE = 'Drag a screenshot (in PNG format) from your desktop onto a '
179      + 'target device above.'
180  var MSG_GENERATING_IMAGE = 'Generating device art&hellip;';
181
182  var MAX_DISPLAY_HEIGHT = 126; // XOOM, to fit into 200px wide
183
184  // Device manifest.
185  var DEVICES = [
186    {
187      id: 'nexus_5',
188      title: 'Nexus 5',
189      url: 'http://www.google.com/nexus/5/',
190      physicalSize: 5,
191      physicalHeight: 5.43,
192      density: 'XXHDPI',
193      landRes: ['shadow', 'back', 'fore'],
194      landOffset: [436,306],
195      portRes: ['shadow', 'back', 'fore'],
196      portOffset: [304,436],
197      portSize: [1080,1920],
198    },
199    {
200      id: 'nexus_6',
201      title: 'Nexus 6',
202      url: 'http://www.google.com/nexus/6/',
203      physicalSize: 6,
204      physicalHeight: 6.27,
205      density: '560DPI',
206      landRes: ['shadow', 'back', 'fore'],
207      landOffset: [489,327],
208      portRes: ['shadow', 'back', 'fore'],
209      portOffset: [327,489],
210      portSize: [1440, 2560],
211    },
212    {
213      id: 'nexus_7',
214      title: 'Nexus 7',
215      url: 'http://www.google.com/nexus/7/',
216      physicalSize: 7,
217      physicalHeight: 8,
218      actualResolution: [1200,1920],
219      density: 'XHDPI',
220      landRes: ['shadow', 'back', 'fore'],
221      landOffset: [326,245],
222      portRes: ['shadow', 'back', 'fore'],
223      portOffset: [244,326],
224      portSize: [800,1280]
225    },
226    {
227      id: 'nexus_9',
228      title: 'Nexus 9',
229      url: 'http://www.google.com/nexus/9/',
230      physicalSize: 9,
231      physicalHeight: 8.98,
232      actualResolution: [1536,2048],
233      density: 'XHDPI',
234      landRes: ['shadow', 'back', 'fore'],
235      landOffset: [514,350],
236      portRes: ['shadow', 'back', 'fore'],
237      portOffset: [348,514],
238      portSize: [1536,2048],
239    },
240    {
241      id: 'nexus_10',
242      title: 'Nexus 10',
243      url: 'http://www.google.com/nexus/10/',
244      physicalSize: 10,
245      physicalHeight: 7,
246      actualResolution: [1600,2560],
247      density: 'XHDPI',
248      landRes: ['shadow', 'back', 'fore'],
249      landOffset: [227,217],
250      portRes: ['shadow', 'back', 'fore'],
251      portOffset: [217,223],
252      portSize: [800,1280],
253      archived: true
254    },
255    {
256      id: 'nexus_7_2012',
257      title: 'Nexus 7 (2012)',
258      url: 'http://www.google.com/nexus/7/',
259      physicalSize: 7,
260      physicalHeight: 7.81,
261      density: '213dpi',
262      landRes: ['shadow', 'back', 'fore'],
263      landOffset: [315,270],
264      portRes: ['shadow', 'back', 'fore'],
265      portOffset: [264,311],
266      portSize: [800,1280],
267      archived: true
268    },
269    {
270      id: 'nexus_4',
271      title: 'Nexus 4',
272      url: 'http://www.google.com/nexus/4/',
273      physicalSize: 4.7,
274      physicalHeight: 5.27,
275      density: 'XHDPI',
276      landRes: ['shadow', 'back', 'fore'],
277      landOffset: [349,214],
278      portRes: ['shadow', 'back', 'fore'],
279      portOffset: [213,350],
280      portSize: [768,1280],
281      archived: true
282    },
283    {
284      id: 'wear',
285      title: 'Android Wear',
286      url: 'http://www.android.com/wear/',
287      physicalSize: 1.8,
288      physicalHeight: 1.8,
289      density: 'HDPI',
290      landRes: ['back'],
291      landOffset: [225,206],
292      portRes: ['back'],
293      portOffset: [200,214],
294      portSize: [320,320],
295    },
296    {
297      id: 'wear_square',
298      title: 'Android Wear Square',
299      url: 'http://www.android.com/wear/',
300      physicalSize: 1.8,
301      physicalHeight: 1.8,
302      density: 'HDPI',
303      landRes: ['back'],
304      landOffset: [225,206],
305      portRes: ['back'],
306      portOffset: [200,214],
307      portSize: [320,320],
308      hidden: true
309    },
310    {
311      id: 'wear_round',
312      title: 'Android Wear Round',
313      url: 'http://www.android.com/wear/',
314      physicalSize: 1.8,
315      physicalHeight: 1.8,
316      density: 'HDPI',
317      landRes: ['back'],
318      landOffset: [161,167],
319      portRes: ['back'],
320      portOffset: [128,134],
321      portSize: [320,320],
322      hidden: true
323    },
324  ];
325
326  DEVICES = DEVICES.sort(function(x, y) { return x.physicalSize - y.physicalSize; });
327
328  var MAX_HEIGHT = 0;
329  for (var i = 0; i < DEVICES.length; i++) {
330    MAX_HEIGHT = Math.max(MAX_HEIGHT, DEVICES[i].physicalHeight);
331  }
332
333  // Setup performed once the DOM is ready.
334  $(document).ready(function() {
335    if (!checkBrowser()) {
336      return;
337    }
338
339    polyfillCanvasToBlob();
340    setupUI();
341
342    // Set up Chrome drag-out
343    $.event.props.push("dataTransfer");
344    document.body.addEventListener('dragstart', function(e) {
345      var target = e.target;
346      if (target.classList.contains('dragout')) {
347        e.dataTransfer.setData('DownloadURL', target.dataset.downloadurl);
348      }
349    }, false);
350  });
351
352  /**
353   * Returns the device from DEVICES with the given id.
354   */
355  function getDeviceById(id) {
356    for (var i = 0; i < DEVICES.length; i++) {
357      if (DEVICES[i].id == id)
358        return DEVICES[i];
359    }
360    return;
361  }
362
363  /**
364   * Checks to make sure the browser supports this page. If not,
365   * updates the UI accordingly and returns false.
366   */
367  function checkBrowser() {
368    // Check for browser support
369    var browserSupportError = null;
370
371    // Must have <canvas>
372    var elem = document.createElement('canvas');
373    if (!elem.getContext || !elem.getContext('2d')) {
374      browserSupportError = 'HTML5 canvas.';
375    }
376
377    // Must have FileReader
378    if (!window.FileReader) {
379      browserSupportError = 'desktop file access';
380    }
381
382    if (browserSupportError) {
383      $('.supported-browser').hide();
384
385      $('#unsupported-browser-reason').html(browserSupportError);
386      $('.unsupported-browser').show();
387      return false;
388    }
389
390    return true;
391  }
392
393  function setupUI() {
394    $('#output').html(MSG_NO_INPUT_IMAGE);
395
396    $('#frame-customizations').hide();
397    $('#wear-customizations').hide();
398
399    $('#output-shadow, #output-glare').click(function() {
400      createFrame();
401    });
402
403    $('input[name="output-wear"]').change(function() {
404      createFrame();
405    });
406
407    // Build device list.
408    $.each(DEVICES, function() {
409      var resolution = this.actualResolution || this.portSize;
410      var scaleFactorText = '';
411      var deviceList = '.device-list.primary';
412      if (resolution[0] != this.portSize[0]) {
413        scaleFactorText = '<br>' + (100 * (this.portSize[0] / resolution[0])).toFixed(0) +
414            '% size output';
415      } else {
416        scaleFactorText = '<br>&nbsp;';
417      }
418
419      if (this.archived) {
420        deviceList = '.device-list.archive';
421      } else if (this.hidden) {
422        deviceList = '.device-list.hidden';
423      }
424
425      $('<li>')
426          .append($('<div>')
427              .addClass('thumb-container')
428              .append($('<img>')
429                  .attr('src', 'device-art-resources/' + this.id + '/thumb.png')
430                  .attr('height',
431                      Math.floor(MAX_DISPLAY_HEIGHT * this.physicalHeight / MAX_HEIGHT))))
432          .append($('<div>')
433              .addClass('device-details')
434              .html((this.url
435                  ? ('<a class="device-url" href="' + this.url + '">' + this.title + '</a>')
436                  : this.title) +
437                  '<br>' +  this.physicalSize + '" @ ' + this.density +
438                  '<br>' + (resolution[0] + 'x' + resolution[1]) + scaleFactorText))
439          .data('deviceId', this.id)
440          .appendTo(deviceList)
441    });
442
443    // Set up "older devices" expando.
444    $('#archive-expando').click(function() {
445      if ($(this).hasClass('expanded')) {
446        $(this).removeClass('expanded');
447        $('.device-list.archive').removeClass('expanded');
448      } else {
449        $(this).addClass('expanded');
450        $('.device-list.archive').addClass('expanded');
451      }
452      return false;
453    });
454
455    // Set up drag and drop.
456    $('.device-list li')
457        .live('dragover', function(evt) {
458          $(this).addClass('drag-hover');
459          evt.dataTransfer.dropEffect = 'link';
460          evt.preventDefault();
461        })
462        .live('dragleave', function(evt) {
463          $(this).removeClass('drag-hover');
464        })
465        .live('drop', function(evt) {
466          $('#output').empty().html(MSG_GENERATING_IMAGE);
467          $(this).removeClass('drag-hover');
468          g_currentDevice = getDeviceById($(this).closest('li').data('deviceId'));
469          evt.preventDefault();
470          loadImageFromFileList(evt.dataTransfer.files, function(data) {
471            if (data == null) {
472              if (g_currentDevice.id == 'wear') {
473                $('#output').html(MSG_INVALID_WEAR_IMAGE);
474              }else {
475                $('#output').html(MSG_INVALID_INPUT_IMAGE);
476              }
477              return;
478            }
479            loadImageFromUri(data.uri, function(img) {
480              g_currentFilename = data.name;
481              g_currentImage = img;
482              createFrame();
483              // Send the event to Analytics
484              ga('send', 'event', 'Distribute', 'Create Device Art', g_currentDevice.title);
485            });
486          });
487        });
488
489    // Set up rotate button.
490    $('#rotate-button').click(function() {
491      if (!g_currentImage) {
492        return;
493      }
494
495      var w = g_currentImage.naturalHeight;
496      var h = g_currentImage.naturalWidth;
497      var canvas = $('<canvas>')
498          .attr('width', w)
499          .attr('height', h)
500          .get(0);
501
502      var ctx = canvas.getContext('2d');
503      ctx.rotate(-Math.PI / 2);
504      ctx.translate(-h, 0);
505      ctx.drawImage(g_currentImage, 0, 0);
506
507      loadImageFromUri(canvas.toDataURL('image/png'), function(img) {
508        g_currentImage = img;
509        createFrame();
510      });
511    });
512  }
513
514  /**
515   * Generates the frame from the current selections (g_currentImage and g_currentDevice).
516   */
517  function createFrame() {
518    var port;
519
520    if (g_currentDevice.id == 'wear' || g_currentDevice.id == 'wear_square' || g_currentDevice.id == 'wear_round') {
521      if ($('#output-square').is(':checked')) {
522        g_currentDevice = getDeviceById('wear_square');
523      } else {
524        g_currentDevice = getDeviceById('wear_round');
525      }
526    }
527
528    var aspect1 = g_currentImage.naturalWidth / g_currentImage.naturalHeight;
529    var aspect2 = g_currentDevice.portSize[0] / g_currentDevice.portSize[1];
530
531    if (aspect1 == aspect2) {
532      port = true;
533    } else if (aspect1 == 1 / aspect2) {
534      port = false;
535    } else {
536      if (g_currentDevice.id == 'wear_square' || g_currentDevice.id == 'wear_round') {
537        alert('The screenshot must have an aspect ratio of ' +
538          aspect2.toFixed(3) +
539          ' (ideally ' + g_currentDevice.portSize[0] + 'x' + g_currentDevice.portSize[1] + ').');
540        $('#output').html(MSG_INVALID_WEAR_IMAGE);
541      }else {
542        alert('The screenshot must have an aspect ratio of ' +
543          aspect2.toFixed(3) + ' or ' + (1 / aspect2).toFixed(3) +
544          ' (ideally ' + g_currentDevice.portSize[0] + 'x' + g_currentDevice.portSize[1] +
545          ' or ' + g_currentDevice.portSize[1] + 'x' + g_currentDevice.portSize[0] + ').');
546        $('#output').html(MSG_INVALID_INPUT_IMAGE);
547      }
548      return;
549    }
550
551    // Load image resources
552    var res = port ? g_currentDevice.portRes : g_currentDevice.landRes;
553    var resList = {};
554    for (var i = 0; i < res.length; i++) {
555      resList[res[i]] = 'device-art-resources/' + g_currentDevice.id + '/' +
556          (port ? 'port_' : 'land_') + res[i] + '.png'
557    }
558
559    var resourceImages = {};
560    loadImageResources(resList, function(r) {
561      resourceImages = r;
562      continueWithResources_();
563    });
564
565    function continueWithResources_() {
566      var width = resourceImages['back'].naturalWidth;
567      var height = resourceImages['back'].naturalHeight;
568      var offset = port ? g_currentDevice.portOffset : g_currentDevice.landOffset;
569      var size = port
570          ? g_currentDevice.portSize
571          : [g_currentDevice.portSize[1], g_currentDevice.portSize[0]];
572
573      var canvas = document.createElement('canvas');
574      canvas.width = width;
575      canvas.height = height;
576
577      var ctx = canvas.getContext('2d');
578      if (resourceImages['shadow'] && $('#output-shadow').is(':checked')) {
579        ctx.drawImage(resourceImages['shadow'], 0, 0);
580      }
581      ctx.drawImage(resourceImages['back'], 0, 0);
582
583      if (g_currentDevice.id == 'wear_round') {
584        var scratchCanvas = document.createElement('canvas');
585        scratchCanvas.width = width;
586        scratchCanvas.height = height;
587        var scratchCtx = scratchCanvas.getContext('2d');
588
589
590        //drawing code
591        scratchCtx.clearRect(offset[0], offset[1], scratchCanvas.width, scratchCanvas.height);
592
593        scratchCtx.globalCompositeOperation = 'source-over'; //default
594
595        scratchCtx.drawImage(g_currentImage, offset[0], offset[1], size[0], size[1]);
596
597        scratchCtx.fillStyle = '#fff'; //color doesn't matter, but we want full opacity
598        scratchCtx.globalCompositeOperation = 'destination-in';
599        scratchCtx.beginPath();
600        scratchCtx.arc(288, 294, size[0] / 2, 0, 2 * Math.PI, false);
601        scratchCtx.closePath();
602        scratchCtx.fill();
603
604        // After tinkering with the offset, the 1 in the x-position drew the image
605        // perfectly
606        ctx.drawImage(scratchCanvas, 1, 0);
607      } else {
608        ctx.fillStyle = '#000';
609        ctx.fillRect(offset[0], offset[1], size[0], size[1]);
610        ctx.drawImage(g_currentImage, offset[0], offset[1], size[0], size[1]);
611      }
612
613      if (resourceImages['fore'] && $('#output-glare').is(':checked')) {
614        ctx.drawImage(resourceImages['fore'], 0, 0);
615      }
616
617      window.URL = window.URL || window.webkitURL;
618      if (canvas.toBlob && window.URL.createObjectURL) {
619        if (g_currentObjectURL) {
620          window.URL.revokeObjectURL(g_currentObjectURL);
621          g_currentObjectURL = null;
622        }
623        if (g_currentBlob) {
624          if (g_currentBlob.close) {
625            g_currentBlob.close();
626          }
627          g_currentBlob = null;
628        }
629
630        canvas.toBlob(function(blob) {
631          if (!blob) {
632            continueWithFinalUrl_(canvas.toDataURL('image/png'));
633            return;
634          }
635          g_currentBlob = blob;
636          g_currentObjectURL = window.URL.createObjectURL(blob);
637          continueWithFinalUrl_(g_currentObjectURL);
638        }, 'image/png');
639      } else {
640        continueWithFinalUrl_(canvas.toDataURL('image/png'));
641      }
642    }
643
644    function continueWithFinalUrl_(imageUrl) {
645      var filename = g_currentFilename
646          ? g_currentFilename.replace(/^(.+?)(\.\w+)?$/, '$1_framed.png')
647          : 'framed_screenshot.png';
648
649      var $link = $('<a>')
650          .attr('download', filename)
651          .attr('href', imageUrl)
652          .append($('<img>')
653              .addClass('dragout')
654              .attr('src', imageUrl)
655              .attr('draggable', true)
656              .attr('data-downloadurl', ['image/png', filename, imageUrl].join(':')))
657          .appendTo($('#output').empty());
658
659      if (g_currentDevice.id == 'wear' || g_currentDevice.id == 'wear_round' || g_currentDevice.id == 'wear_square') {
660        $('#wear-customizations').show();
661        $('#frame-customizations').hide();
662      } else {
663        $('#frame-customizations').show();
664        $('#wear-customizations').hide();
665      }
666    }
667  }
668
669  /**
670   * Loads an image from a data URI. The callback will be called with the <img> once
671   * it loads.
672   */
673  function loadImageFromUri(uri, callback) {
674    callback = callback || function(){};
675
676    var img = document.createElement('img');
677    img.src = uri;
678    img.onload = function() {
679      callback(img);
680    };
681    img.onerror = function() {
682      callback(null);
683    }
684  }
685
686  /**
687   * Loads a set of images (organized by ID). Once all images are loaded, the callback
688   * is triggered with a dictionary of <img>'s, organized by ID.
689   */
690  function loadImageResources(images, callback) {
691    var imageResources = {};
692
693    var checkForCompletion_ = function() {
694      for (var id in images) {
695        if (!(id in imageResources))
696          return;
697      }
698      (callback || function(){})(imageResources);
699      callback = null;
700    };
701
702    for (var id in images) {
703      var img = document.createElement('img');
704      img.src = images[id];
705      (function(img, id) {
706        img.onload = function() {
707          imageResources[id] = img;
708          checkForCompletion_();
709        };
710        img.onerror = function() {
711          imageResources[id] = null;
712          checkForCompletion_();
713        }
714      })(img, id);
715    }
716  }
717
718  /**
719   * Loads the first valid image from a FileList (e.g. drag + drop source), as a data URI. This
720   * method will throw an alert() in case of errors and call back with null.
721   *
722   * @param {FileList} fileList The FileList to load.
723   * @param {Function} callback The callback to fire once image loading is done (or fails).
724   * @return Returns an object containing 'uri' representing the loaded image. There will also be
725   *      a 'name' field indicating the file name, if one is available.
726   */
727  function loadImageFromFileList(fileList, callback) {
728    fileList = fileList || [];
729
730    var file = null;
731    for (var i = 0; i < fileList.length; i++) {
732      if (fileList[i].type.toLowerCase().match(/^image\/(png|jpeg|jpg)/)) {
733        file = fileList[i];
734        break;
735      }
736    }
737
738    if (!file) {
739      alert('Please use a valid screenshot file (PNG or JPEG format).');
740      callback(null);
741      return;
742    }
743
744    var fileReader = new FileReader();
745
746    // Closure to capture the file information.
747    fileReader.onload = function(e) {
748      callback({
749        uri: e.target.result,
750        name: file.name
751      });
752    };
753    fileReader.onerror = function(e) {
754      switch(e.target.error.code) {
755        case e.target.error.NOT_FOUND_ERR:
756          alert('File not found.');
757          break;
758        case e.target.error.NOT_READABLE_ERR:
759          alert('File is not readable.');
760          break;
761        case e.target.error.ABORT_ERR:
762          break; // noop
763        default:
764          alert('An error occurred reading this file.');
765      }
766      callback(null);
767    };
768    fileReader.onabort = function(e) {
769      alert('File read cancelled.');
770      callback(null);
771    };
772
773    fileReader.readAsDataURL(file);
774  }
775
776  /**
777   * Adds a simple version of Canvas.toBlob if toBlob isn't available.
778   */
779  function polyfillCanvasToBlob() {
780    if (!HTMLCanvasElement.prototype.toBlob && window.Blob) {
781      HTMLCanvasElement.prototype.toBlob = function(callback, mimeType, quality) {
782        if (typeof callback != 'function') {
783          throw new TypeError('Function expected');
784        }
785        var dataURL = this.toDataURL(mimeType, quality);
786        mimeType = dataURL.split(';')[0].split(':')[1];
787        var bs = window.atob(dataURL.split(',')[1]);
788        if (dataURL == 'data:,' || !bs.length) {
789          callback(null);
790          return;
791        }
792        for (var ui8arr = new Uint8Array(bs.length), i = 0; i < bs.length; ++i) {
793          ui8arr[i] = bs.charCodeAt(i);
794        }
795        callback(new Blob([ui8arr.buffer /* req'd for Safari */ || ui8arr], {type: mimeType}));
796      };
797    }
798  }
799</script>
800