• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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: 'https://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      archived: true
199    },
200    {
201      id: 'nexus_5x',
202      title: 'Nexus 5X',
203      url: 'https://www.google.com/nexus/5x/',
204      physicalSize: 5.2,
205      physicalHeight: 5.625,
206      density: '420DPI',
207      landRes: ['shadow', 'back', 'fore'],
208      landOffset: [485,313],
209      portRes: ['shadow', 'back', 'fore'],
210      portOffset: [305,485],
211      portSize: [1080,1920],
212    },
213    {
214      id: 'nexus_6',
215      title: 'Nexus 6',
216      url: 'https://www.google.com/nexus/6/',
217      physicalSize: 6,
218      physicalHeight: 6.27,
219      density: '560DPI',
220      landRes: ['shadow', 'back', 'fore'],
221      landOffset: [489,327],
222      portRes: ['shadow', 'back', 'fore'],
223      portOffset: [327,489],
224      portSize: [1440, 2560],
225      archived: true
226    },
227    {
228      id: 'nexus_6p',
229      title: 'Nexus 6P',
230      url: 'https://www.google.com/nexus/6p/',
231      physicalSize: 5.7,
232      physicalHeight: 6.125,
233      density: '560DPI',
234      landRes: ['shadow', 'back', 'fore'],
235      landOffset: [579,321],
236      portRes: ['shadow', 'back', 'fore'],
237      portOffset: [312,579],
238      portSize: [1440, 2560]
239    },
240    {
241      id: 'nexus_7',
242      title: 'Nexus 7',
243      url: 'http://www.google.com/nexus/7/',
244      physicalSize: 7,
245      physicalHeight: 8,
246      actualResolution: [1200,1920],
247      density: 'XHDPI',
248      landRes: ['shadow', 'back', 'fore'],
249      landOffset: [326,245],
250      portRes: ['shadow', 'back', 'fore'],
251      portOffset: [244,326],
252      portSize: [800,1280],
253      archived: true
254    },
255    {
256      id: 'nexus_9',
257      title: 'Nexus 9',
258      url: 'https://www.google.com/nexus/9/',
259      physicalSize: 9,
260      physicalHeight: 8.98,
261      actualResolution: [1536,2048],
262      density: 'XHDPI',
263      landRes: ['shadow', 'back', 'fore'],
264      landOffset: [514,350],
265      portRes: ['shadow', 'back', 'fore'],
266      portOffset: [348,514],
267      portSize: [1536,2048],
268    },
269    {
270      id: 'nexus_10',
271      title: 'Nexus 10',
272      url: 'https://www.google.com/nexus/10/',
273      physicalSize: 10,
274      physicalHeight: 7,
275      actualResolution: [1600,2560],
276      density: 'XHDPI',
277      landRes: ['shadow', 'back', 'fore'],
278      landOffset: [227,217],
279      portRes: ['shadow', 'back', 'fore'],
280      portOffset: [217,223],
281      portSize: [800,1280],
282      archived: true
283    },
284    {
285      id: 'nexus_4',
286      title: 'Nexus 4',
287      url: 'https://www.google.com/nexus/4/',
288      physicalSize: 4.7,
289      physicalHeight: 5.27,
290      density: 'XHDPI',
291      landRes: ['shadow', 'back', 'fore'],
292      landOffset: [349,214],
293      portRes: ['shadow', 'back', 'fore'],
294      portOffset: [213,350],
295      portSize: [768,1280],
296      archived: true
297    },
298    {
299      id: 'wear',
300      title: 'Android Wear',
301      url: 'https://www.android.com/wear/',
302      physicalSize: 1.8,
303      physicalHeight: 1.8,
304      density: 'HDPI',
305      landRes: ['back'],
306      landOffset: [225,206],
307      portRes: ['back'],
308      portOffset: [200,214],
309      portSize: [320,320],
310    },
311    {
312      id: 'wear_square',
313      title: 'Android Wear Square',
314      url: 'https://www.android.com/wear/',
315      physicalSize: 1.8,
316      physicalHeight: 1.8,
317      density: 'HDPI',
318      landRes: ['back'],
319      landOffset: [225,206],
320      portRes: ['back'],
321      portOffset: [200,214],
322      portSize: [320,320],
323      hidden: true
324    },
325    {
326      id: 'wear_round',
327      title: 'Android Wear Round',
328      url: 'https://www.android.com/wear/',
329      physicalSize: 1.8,
330      physicalHeight: 1.8,
331      density: 'HDPI',
332      landRes: ['back'],
333      landOffset: [161,167],
334      portRes: ['back'],
335      portOffset: [128,134],
336      portSize: [320,320],
337      hidden: true
338    },
339  ];
340
341  DEVICES = DEVICES.sort(function(x, y) { return x.physicalSize - y.physicalSize; });
342
343  var MAX_HEIGHT = 0;
344  for (var i = 0; i < DEVICES.length; i++) {
345    MAX_HEIGHT = Math.max(MAX_HEIGHT, DEVICES[i].physicalHeight);
346  }
347
348  // Setup performed once the DOM is ready.
349  $(document).ready(function() {
350    if (!checkBrowser()) {
351      return;
352    }
353
354    polyfillCanvasToBlob();
355    setupUI();
356
357    // Set up Chrome drag-out
358    $.event.props.push("dataTransfer");
359    document.body.addEventListener('dragstart', function(e) {
360      var target = e.target;
361      if (target.classList.contains('dragout')) {
362        e.dataTransfer.setData('DownloadURL', target.dataset.downloadurl);
363      }
364    }, false);
365  });
366
367  /**
368   * Returns the device from DEVICES with the given id.
369   */
370  function getDeviceById(id) {
371    for (var i = 0; i < DEVICES.length; i++) {
372      if (DEVICES[i].id == id)
373        return DEVICES[i];
374    }
375    return;
376  }
377
378  /**
379   * Checks to make sure the browser supports this page. If not,
380   * updates the UI accordingly and returns false.
381   */
382  function checkBrowser() {
383    // Check for browser support
384    var browserSupportError = null;
385
386    // Must have <canvas>
387    var elem = document.createElement('canvas');
388    if (!elem.getContext || !elem.getContext('2d')) {
389      browserSupportError = 'HTML5 canvas.';
390    }
391
392    // Must have FileReader
393    if (!window.FileReader) {
394      browserSupportError = 'desktop file access';
395    }
396
397    if (browserSupportError) {
398      $('.supported-browser').hide();
399
400      $('#unsupported-browser-reason').html(browserSupportError);
401      $('.unsupported-browser').show();
402      return false;
403    }
404
405    return true;
406  }
407
408  function setupUI() {
409    $('#output').html(MSG_NO_INPUT_IMAGE);
410
411    $('#frame-customizations').hide();
412    $('#wear-customizations').hide();
413
414    $('#output-shadow, #output-glare').click(function() {
415      createFrame();
416    });
417
418    $('input[name="output-wear"]').change(function() {
419      createFrame();
420    });
421
422    // Build device list.
423    $.each(DEVICES, function() {
424      var resolution = this.actualResolution || this.portSize;
425      var scaleFactorText = '';
426      var deviceList = '.device-list.primary';
427      if (resolution[0] != this.portSize[0]) {
428        scaleFactorText = '<br>' + (100 * (this.portSize[0] / resolution[0])).toFixed(0) +
429            '% size output';
430      } else {
431        scaleFactorText = '<br>&nbsp;';
432      }
433
434      if (this.archived) {
435        deviceList = '.device-list.archive';
436      } else if (this.hidden) {
437        deviceList = '.device-list.hidden';
438      }
439
440      $('<li>')
441          .append($('<div>')
442              .addClass('thumb-container')
443              .append($('<img>')
444                  .attr('src', 'device-art-resources/' + this.id + '/thumb.png')
445                  .attr('height',
446                      Math.floor(MAX_DISPLAY_HEIGHT * this.physicalHeight / MAX_HEIGHT))))
447          .append($('<div>')
448              .addClass('device-details')
449              .html((this.url
450                  ? ('<a class="device-url" href="' + this.url + '">' + this.title + '</a>')
451                  : this.title) +
452                  '<br>' +  this.physicalSize + '" @ ' + this.density +
453                  '<br>' + (resolution[0] + 'x' + resolution[1]) + scaleFactorText))
454          .data('deviceId', this.id)
455          .appendTo(deviceList)
456    });
457
458    // Set up "older devices" expando.
459    $('#archive-expando').click(function() {
460      if ($(this).hasClass('expanded')) {
461        $(this).removeClass('expanded');
462        $('.device-list.archive').removeClass('expanded');
463      } else {
464        $(this).addClass('expanded');
465        $('.device-list.archive').addClass('expanded');
466      }
467      return false;
468    });
469
470    // Set up drag and drop.
471    $('.device-list li')
472        .live('dragover', function(evt) {
473          $(this).addClass('drag-hover');
474          evt.dataTransfer.dropEffect = 'link';
475          evt.preventDefault();
476        })
477        .live('dragleave', function(evt) {
478          $(this).removeClass('drag-hover');
479        })
480        .live('drop', function(evt) {
481          $('#output').empty().html(MSG_GENERATING_IMAGE);
482          $(this).removeClass('drag-hover');
483          g_currentDevice = getDeviceById($(this).closest('li').data('deviceId'));
484          evt.preventDefault();
485          loadImageFromFileList(evt.dataTransfer.files, function(data) {
486            if (data == null) {
487              if (g_currentDevice.id == 'wear') {
488                $('#output').html(MSG_INVALID_WEAR_IMAGE);
489              }else {
490                $('#output').html(MSG_INVALID_INPUT_IMAGE);
491              }
492              return;
493            }
494            loadImageFromUri(data.uri, function(img) {
495              g_currentFilename = data.name;
496              g_currentImage = img;
497              createFrame();
498              // Send the event to Analytics
499              ga('send', 'event', 'Distribute', 'Create Device Art', g_currentDevice.title);
500            });
501          });
502        });
503
504    // Set up rotate button.
505    $('#rotate-button').click(function() {
506      if (!g_currentImage) {
507        return;
508      }
509
510      var w = g_currentImage.naturalHeight;
511      var h = g_currentImage.naturalWidth;
512      var canvas = $('<canvas>')
513          .attr('width', w)
514          .attr('height', h)
515          .get(0);
516
517      var ctx = canvas.getContext('2d');
518      ctx.rotate(-Math.PI / 2);
519      ctx.translate(-h, 0);
520      ctx.drawImage(g_currentImage, 0, 0);
521
522      loadImageFromUri(canvas.toDataURL('image/png'), function(img) {
523        g_currentImage = img;
524        createFrame();
525      });
526    });
527  }
528
529  /**
530   * Generates the frame from the current selections (g_currentImage and g_currentDevice).
531   */
532  function createFrame() {
533    var port;
534
535    if (g_currentDevice.id == 'wear' || g_currentDevice.id == 'wear_square' || g_currentDevice.id == 'wear_round') {
536      if ($('#output-square').is(':checked')) {
537        g_currentDevice = getDeviceById('wear_square');
538      } else {
539        g_currentDevice = getDeviceById('wear_round');
540      }
541    }
542
543    var aspect1 = g_currentImage.naturalWidth / g_currentImage.naturalHeight;
544    var aspect2 = g_currentDevice.portSize[0] / g_currentDevice.portSize[1];
545
546    if (aspect1 == aspect2) {
547      port = true;
548    } else if (aspect1 == 1 / aspect2) {
549      port = false;
550    } else {
551      if (g_currentDevice.id == 'wear_square' || g_currentDevice.id == 'wear_round') {
552        alert('The screenshot must have an aspect ratio of ' +
553          aspect2.toFixed(3) +
554          ' (ideally ' + g_currentDevice.portSize[0] + 'x' + g_currentDevice.portSize[1] + ').');
555        $('#output').html(MSG_INVALID_WEAR_IMAGE);
556      }else {
557        alert('The screenshot must have an aspect ratio of ' +
558          aspect2.toFixed(3) + ' or ' + (1 / aspect2).toFixed(3) +
559          ' (ideally ' + g_currentDevice.portSize[0] + 'x' + g_currentDevice.portSize[1] +
560          ' or ' + g_currentDevice.portSize[1] + 'x' + g_currentDevice.portSize[0] + ').');
561        $('#output').html(MSG_INVALID_INPUT_IMAGE);
562      }
563      return;
564    }
565
566    // Load image resources
567    var res = port ? g_currentDevice.portRes : g_currentDevice.landRes;
568    var resList = {};
569    for (var i = 0; i < res.length; i++) {
570      resList[res[i]] = 'device-art-resources/' + g_currentDevice.id + '/' +
571          (port ? 'port_' : 'land_') + res[i] + '.png'
572    }
573
574    var resourceImages = {};
575    loadImageResources(resList, function(r) {
576      resourceImages = r;
577      continueWithResources_();
578    });
579
580    function continueWithResources_() {
581      var width = resourceImages['back'].naturalWidth;
582      var height = resourceImages['back'].naturalHeight;
583      var offset = port ? g_currentDevice.portOffset : g_currentDevice.landOffset;
584      var size = port
585          ? g_currentDevice.portSize
586          : [g_currentDevice.portSize[1], g_currentDevice.portSize[0]];
587
588      var canvas = document.createElement('canvas');
589      canvas.width = width;
590      canvas.height = height;
591
592      var ctx = canvas.getContext('2d');
593      if (resourceImages['shadow'] && $('#output-shadow').is(':checked')) {
594        ctx.drawImage(resourceImages['shadow'], 0, 0);
595      }
596      ctx.drawImage(resourceImages['back'], 0, 0);
597
598      if (g_currentDevice.id == 'wear_round') {
599        var scratchCanvas = document.createElement('canvas');
600        scratchCanvas.width = width;
601        scratchCanvas.height = height;
602        var scratchCtx = scratchCanvas.getContext('2d');
603
604
605        //drawing code
606        scratchCtx.clearRect(offset[0], offset[1], scratchCanvas.width, scratchCanvas.height);
607
608        scratchCtx.globalCompositeOperation = 'source-over'; //default
609
610        scratchCtx.drawImage(g_currentImage, offset[0], offset[1], size[0], size[1]);
611
612        scratchCtx.fillStyle = '#fff'; //color doesn't matter, but we want full opacity
613        scratchCtx.globalCompositeOperation = 'destination-in';
614        scratchCtx.beginPath();
615        scratchCtx.arc(288, 294, size[0] / 2, 0, 2 * Math.PI, false);
616        scratchCtx.closePath();
617        scratchCtx.fill();
618
619        // After tinkering with the offset, the 1 in the x-position drew the image
620        // perfectly
621        ctx.drawImage(scratchCanvas, 1, 0);
622      } else {
623        ctx.fillStyle = '#000';
624        ctx.fillRect(offset[0], offset[1], size[0], size[1]);
625        ctx.drawImage(g_currentImage, offset[0], offset[1], size[0], size[1]);
626      }
627
628      if (resourceImages['fore'] && $('#output-glare').is(':checked')) {
629        ctx.drawImage(resourceImages['fore'], 0, 0);
630      }
631
632      window.URL = window.URL || window.webkitURL;
633      if (canvas.toBlob && window.URL.createObjectURL) {
634        if (g_currentObjectURL) {
635          window.URL.revokeObjectURL(g_currentObjectURL);
636          g_currentObjectURL = null;
637        }
638        if (g_currentBlob) {
639          if (g_currentBlob.close) {
640            g_currentBlob.close();
641          }
642          g_currentBlob = null;
643        }
644
645        canvas.toBlob(function(blob) {
646          if (!blob) {
647            continueWithFinalUrl_(canvas.toDataURL('image/png'));
648            return;
649          }
650          g_currentBlob = blob;
651          g_currentObjectURL = window.URL.createObjectURL(blob);
652          continueWithFinalUrl_(g_currentObjectURL);
653        }, 'image/png');
654      } else {
655        continueWithFinalUrl_(canvas.toDataURL('image/png'));
656      }
657    }
658
659    function continueWithFinalUrl_(imageUrl) {
660      var filename = g_currentFilename
661          ? g_currentFilename.replace(/^(.+?)(\.\w+)?$/, '$1_framed.png')
662          : 'framed_screenshot.png';
663
664      var $link = $('<a>')
665          .attr('download', filename)
666          .attr('href', imageUrl)
667          .append($('<img>')
668              .addClass('dragout')
669              .attr('src', imageUrl)
670              .attr('draggable', true)
671              .attr('data-downloadurl', ['image/png', filename, imageUrl].join(':')))
672          .appendTo($('#output').empty());
673
674      if (g_currentDevice.id == 'wear' || g_currentDevice.id == 'wear_round' || g_currentDevice.id == 'wear_square') {
675        $('#wear-customizations').show();
676        $('#frame-customizations').hide();
677      } else {
678        $('#frame-customizations').show();
679        $('#wear-customizations').hide();
680      }
681    }
682  }
683
684  /**
685   * Loads an image from a data URI. The callback will be called with the <img> once
686   * it loads.
687   */
688  function loadImageFromUri(uri, callback) {
689    callback = callback || function(){};
690
691    var img = document.createElement('img');
692    img.src = uri;
693    img.onload = function() {
694      callback(img);
695    };
696    img.onerror = function() {
697      callback(null);
698    }
699  }
700
701  /**
702   * Loads a set of images (organized by ID). Once all images are loaded, the callback
703   * is triggered with a dictionary of <img>'s, organized by ID.
704   */
705  function loadImageResources(images, callback) {
706    var imageResources = {};
707
708    var checkForCompletion_ = function() {
709      for (var id in images) {
710        if (!(id in imageResources))
711          return;
712      }
713      (callback || function(){})(imageResources);
714      callback = null;
715    };
716
717    for (var id in images) {
718      var img = document.createElement('img');
719      img.src = images[id];
720      (function(img, id) {
721        img.onload = function() {
722          imageResources[id] = img;
723          checkForCompletion_();
724        };
725        img.onerror = function() {
726          imageResources[id] = null;
727          checkForCompletion_();
728        }
729      })(img, id);
730    }
731  }
732
733  /**
734   * Loads the first valid image from a FileList (e.g. drag + drop source), as a data URI. This
735   * method will throw an alert() in case of errors and call back with null.
736   *
737   * @param {FileList} fileList The FileList to load.
738   * @param {Function} callback The callback to fire once image loading is done (or fails).
739   * @return Returns an object containing 'uri' representing the loaded image. There will also be
740   *      a 'name' field indicating the file name, if one is available.
741   */
742  function loadImageFromFileList(fileList, callback) {
743    fileList = fileList || [];
744
745    var file = null;
746    for (var i = 0; i < fileList.length; i++) {
747      if (fileList[i].type.toLowerCase().match(/^image\/(png|jpeg|jpg)/)) {
748        file = fileList[i];
749        break;
750      }
751    }
752
753    if (!file) {
754      alert('Please use a valid screenshot file (PNG or JPEG format).');
755      callback(null);
756      return;
757    }
758
759    var fileReader = new FileReader();
760
761    // Closure to capture the file information.
762    fileReader.onload = function(e) {
763      callback({
764        uri: e.target.result,
765        name: file.name
766      });
767    };
768    fileReader.onerror = function(e) {
769      switch(e.target.error.code) {
770        case e.target.error.NOT_FOUND_ERR:
771          alert('File not found.');
772          break;
773        case e.target.error.NOT_READABLE_ERR:
774          alert('File is not readable.');
775          break;
776        case e.target.error.ABORT_ERR:
777          break; // noop
778        default:
779          alert('An error occurred reading this file.');
780      }
781      callback(null);
782    };
783    fileReader.onabort = function(e) {
784      alert('File read cancelled.');
785      callback(null);
786    };
787
788    fileReader.readAsDataURL(file);
789  }
790
791  /**
792   * Adds a simple version of Canvas.toBlob if toBlob isn't available.
793   */
794  function polyfillCanvasToBlob() {
795    if (!HTMLCanvasElement.prototype.toBlob && window.Blob) {
796      HTMLCanvasElement.prototype.toBlob = function(callback, mimeType, quality) {
797        if (typeof callback != 'function') {
798          throw new TypeError('Function expected');
799        }
800        var dataURL = this.toDataURL(mimeType, quality);
801        mimeType = dataURL.split(';')[0].split(':')[1];
802        var bs = window.atob(dataURL.split(',')[1]);
803        if (dataURL == 'data:,' || !bs.length) {
804          callback(null);
805          return;
806        }
807        for (var ui8arr = new Uint8Array(bs.length), i = 0; i < bs.length; ++i) {
808          ui8arr[i] = bs.charCodeAt(i);
809        }
810        callback(new Blob([ui8arr.buffer /* req'd for Safari */ || ui8arr], {type: mimeType}));
811      };
812    }
813  }
814</script>
815