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="layout-content-row"> 20 <div class="layout-content-col span-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="layout-content-col span-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="layout-content-row"> 34 <div class="layout-content-col span-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="layout-content-col span-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…'; 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> '; 417 } 418 419 if (this.archived) { 420 deviceList = '.device-list.archived'; 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