1 page.title=Device Art Generator 2 page.image=/images/device-art-ex-crop.jpg 3 page.metaDescription=Drag and drop screenshots of your app into device artwork, for better looking promotional images and improved visual context. 4 meta.tags="disttools, promoting, deviceart, marketing" 5 page.tags="device, deviceart, nexus, assets" 6 Xnonavpage=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 13 feature 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…'; 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> '; 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