1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/image/test/unit/test_encoder_apng.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,470 @@ 1.4 +/* 1.5 + * Test for APNG encoding in libpr0n 1.6 + * 1.7 + */ 1.8 + 1.9 + 1.10 +const Ci = Components.interfaces; 1.11 +const Cc = Components.classes; 1.12 + 1.13 + // dispose=[none|background|previous] 1.14 + // blend=[source|over] 1.15 + 1.16 +var apng1A = { 1.17 + // A 3x3 image with 3 frames, alternating red, green, blue. RGB format. 1.18 + width : 3, height : 3, skipFirstFrame : false, 1.19 + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, 1.20 + transparency : null, 1.21 + plays : 0, 1.22 + 1.23 + frames : [ 1.24 + { // frame #1 1.25 + width : 3, height : 3, 1.26 + x_offset : 0, y_offset : 0, 1.27 + dispose : "none", blend : "source", delay : 500, 1.28 + 1.29 + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, 1.30 + transparency : null, 1.31 + 1.32 + pixels : [ 1.33 + 255,0,0, 255,0,0, 255,0,0, 1.34 + 255,0,0, 255,0,0, 255,0,0, 1.35 + 255,0,0, 255,0,0, 255,0,0 1.36 + ] 1.37 + }, 1.38 + 1.39 + { // frame #2 1.40 + width : 3, height : 3, 1.41 + x_offset : 0, y_offset : 0, 1.42 + dispose : "none", blend : "source", delay : 500, 1.43 + 1.44 + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, 1.45 + transparency : null, 1.46 + 1.47 + pixels : [ 1.48 + 0,255,0, 0,255,0, 0,255,0, 1.49 + 0,255,0, 0,255,0, 0,255,0, 1.50 + 0,255,0, 0,255,0, 0,255,0 1.51 + ] 1.52 + }, 1.53 + 1.54 + { // frame #3 1.55 + width : 3, height : 3, 1.56 + x_offset : 0, y_offset : 0, 1.57 + dispose : "none", blend : "source", delay : 500, 1.58 + 1.59 + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, 1.60 + transparency : null, 1.61 + 1.62 + pixels : [ 1.63 + 0,0,255, 0,0,255, 0,0,255, 1.64 + 0,0,255, 0,0,255, 0,0,255, 1.65 + 0,0,255, 0,0,255, 0,0,255 1.66 + ] 1.67 + } 1.68 + 1.69 + ], 1.70 + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABBJREFUCJlj+M/AAEEMWFgAj44I+H2CySsAAAAaZmNUTAAAAAEAAAADAAAAAwAAAAAAAAAAAfQD6AAA7mXKzAAAABNmZEFUAAAAAgiZY2D4zwBFWFgAhpcI+I731VcAAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABNmZEFUAAAABAiZY2Bg+A9DmCwAfaAI+AGmQVoAAAAASUVORK5CYII=" 1.71 +}; 1.72 + 1.73 + 1.74 +var apng1B = { 1.75 + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. 1.76 + width : 3, height : 3, skipFirstFrame : false, 1.77 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, 1.78 + transparency : null, 1.79 + plays : 0, 1.80 + 1.81 + frames : [ 1.82 + { // frame #1 1.83 + width : 3, height : 3, 1.84 + x_offset : 0, y_offset : 0, 1.85 + dispose : "none", blend : "source", delay : 500, 1.86 + 1.87 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.88 + 1.89 + pixels : [ 1.90 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.91 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.92 + 255,0,0,255, 255,0,0,255, 255,0,0,255 1.93 + ] 1.94 + }, 1.95 + 1.96 + { // frame #2 1.97 + width : 3, height : 3, 1.98 + x_offset : 0, y_offset : 0, 1.99 + dispose : "none", blend : "source", delay : 500, 1.100 + 1.101 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.102 + 1.103 + pixels : [ 1.104 + 0,255,0,255, 0,255,0,255, 0,255,0,255, 1.105 + 0,255,0,255, 0,255,0,255, 0,255,0,255, 1.106 + 0,255,0,255, 0,255,0,255, 0,255,0,255 1.107 + ] 1.108 + }, 1.109 + 1.110 + { // frame #3 1.111 + width : 3, height : 3, 1.112 + x_offset : 0, y_offset : 0, 1.113 + dispose : "none", blend : "source", delay : 500, 1.114 + 1.115 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.116 + 1.117 + pixels : [ 1.118 + 0,0,255,255, 0,0,255,255, 0,0,255,255, 1.119 + 0,0,255,255, 0,0,255,255, 0,0,255,255, 1.120 + 0,0,255,255, 0,0,255,255, 0,0,255,255 1.121 + ] 1.122 + } 1.123 + 1.124 + ], 1.125 + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABFJREFUCJlj+M/A8B+GGXByAF3XEe/CoiJ1AAAAGmZjVEwAAAABAAAAAwAAAAMAAAAAAAAAAAH0A+gAAO5lyswAAAASZmRBVAAAAAIImWNg+I8EcXIAVOAR77Vyl9QAAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABRmZEFUAAAABAiZY2Bg+P8fgXFxAEvpEe8rClxSAAAAAElFTkSuQmCC" 1.126 +}; 1.127 + 1.128 + 1.129 +var apng1C = { 1.130 + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. 1.131 + // The first frame is skipped, so it will only flash green/blue (or static red in an APNG-unaware viewer) 1.132 + width : 3, height : 3, skipFirstFrame : true, 1.133 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, 1.134 + transparency : null, 1.135 + plays : 0, 1.136 + 1.137 + frames : [ 1.138 + { // frame #1 1.139 + width : 3, height : 3, 1.140 + x_offset : 0, y_offset : 0, 1.141 + dispose : "none", blend : "source", delay : 500, 1.142 + 1.143 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.144 + 1.145 + pixels : [ 1.146 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.147 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.148 + 255,0,0,255, 255,0,0,255, 255,0,0,255 1.149 + ] 1.150 + }, 1.151 + 1.152 + { // frame #2 1.153 + width : 3, height : 3, 1.154 + x_offset : 0, y_offset : 0, 1.155 + dispose : "none", blend : "source", delay : 500, 1.156 + 1.157 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.158 + 1.159 + pixels : [ 1.160 + 0,255,0,255, 0,255,0,255, 0,255,0,255, 1.161 + 0,255,0,255, 0,255,0,255, 0,255,0,255, 1.162 + 0,255,0,255, 0,255,0,255, 0,255,0,255 1.163 + ] 1.164 + }, 1.165 + 1.166 + { // frame #3 1.167 + width : 3, height : 3, 1.168 + x_offset : 0, y_offset : 0, 1.169 + dispose : "none", blend : "source", delay : 500, 1.170 + 1.171 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.172 + 1.173 + pixels : [ 1.174 + 0,0,255,255, 0,0,255,255, 0,0,255,255, 1.175 + 0,0,255,255, 0,0,255,255, 0,0,255,255, 1.176 + 0,0,255,255, 0,0,255,255, 0,0,255,255 1.177 + ] 1.178 + } 1.179 + 1.180 + ], 1.181 + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAACAAAAAPONk3AAAAARSURBVAiZY/jPwPAfhhlwcgBd1xHvwqIidQAAABpmY1RMAAAAAAAAAAMAAAADAAAAAAAAAAAB9APoAAB1FiAYAAAAEmZkQVQAAAABCJljYPiPBHFyAFTgEe+kD/2tAAAAGmZjVEwAAAACAAAAAwAAAAMAAAAAAAAAAAH0A+gAAJiA8/EAAAAUZmRBVAAAAAMImWNgYPj/H4FxcQBL6RHvC5ggGQAAAABJRU5ErkJggg==" 1.182 +}; 1.183 + 1.184 + 1.185 +var apng2A = { 1.186 + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. 1.187 + // blend = over mode 1.188 + // (The green frame is a horizontal gradient, and the blue frame is a 1.189 + // vertical gradient. They stack as they animate.) 1.190 + width : 3, height : 3, skipFirstFrame : false, 1.191 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, 1.192 + transparency : null, 1.193 + plays : 0, 1.194 + 1.195 + frames : [ 1.196 + { // frame #1 1.197 + width : 3, height : 3, 1.198 + x_offset : 0, y_offset : 0, 1.199 + dispose : "none", blend : "source", delay : 500, 1.200 + 1.201 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.202 + 1.203 + pixels : [ 1.204 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.205 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.206 + 255,0,0,255, 255,0,0,255, 255,0,0,255 1.207 + ] 1.208 + }, 1.209 + 1.210 + { // frame #2 1.211 + width : 3, height : 3, 1.212 + x_offset : 0, y_offset : 0, 1.213 + dispose : "none", blend : "over", delay : 500, 1.214 + 1.215 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.216 + 1.217 + pixels : [ 1.218 + 0,255,0,255, 0,255,0,180, 0,255,0,75, 1.219 + 0,255,0,255, 0,255,0,180, 0,255,0,75, 1.220 + 0,255,0,255, 0,255,0,180, 0,255,0,75 1.221 + ] 1.222 + }, 1.223 + 1.224 + { // frame #3 1.225 + width : 3, height : 3, 1.226 + x_offset : 0, y_offset : 0, 1.227 + dispose : "none", blend : "over", delay : 500, 1.228 + 1.229 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.230 + 1.231 + pixels : [ 1.232 + 0,0,255,75, 0,0,255,75, 0,0,255,75, 1.233 + 0,0,255,180, 0,0,255,180, 0,0,255,180, 1.234 + 0,0,255,255, 0,0,255,255, 0,0,255,255 1.235 + ] 1.236 + } 1.237 + 1.238 + ], 1.239 + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABFJREFUCJlj+M/A8B+GGXByAF3XEe/CoiJ1AAAAGmZjVEwAAAABAAAAAwAAAAMAAAAAAAAAAAH0A+gAAZli+loAAAAcZmRBVAAAAAIImWNg+M/wn+E/wxaG/wzeDDg5ACeGDvKVa3pyAAAAGmZjVEwAAAADAAAAAwAAAAMAAAAAAAAAAAH0A+gAAXT0KbMAAAAcZmRBVAAAAAQImWNgYPjvjcAM/7cgMMP//zAMAPqkDvLn1m3SAAAAAElFTkSuQmCC" 1.240 +}; 1.241 + 1.242 + 1.243 +var apng2B = { 1.244 + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. 1.245 + // blend = over, dispose = background 1.246 + // (The green frame is a horizontal gradient, and the blue frame is a 1.247 + // vertical gradient. Each frame is displayed individually, blended to 1.248 + // whatever the background is.) 1.249 + width : 3, height : 3, skipFirstFrame : false, 1.250 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, 1.251 + transparency : null, 1.252 + plays : 0, 1.253 + 1.254 + frames : [ 1.255 + { // frame #1 1.256 + width : 3, height : 3, 1.257 + x_offset : 0, y_offset : 0, 1.258 + dispose : "background", blend : "source", delay : 500, 1.259 + 1.260 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.261 + 1.262 + pixels : [ 1.263 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.264 + 255,0,0,255, 255,0,0,255, 255,0,0,255, 1.265 + 255,0,0,255, 255,0,0,255, 255,0,0,255 1.266 + ] 1.267 + }, 1.268 + 1.269 + { // frame #2 1.270 + width : 3, height : 3, 1.271 + x_offset : 0, y_offset : 0, 1.272 + dispose : "background", blend : "over", delay : 500, 1.273 + 1.274 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.275 + 1.276 + pixels : [ 1.277 + 0,255,0,255, 0,255,0,180, 0,255,0,75, 1.278 + 0,255,0,255, 0,255,0,180, 0,255,0,75, 1.279 + 0,255,0,255, 0,255,0,180, 0,255,0,75 1.280 + ] 1.281 + }, 1.282 + 1.283 + { // frame #3 1.284 + width : 3, height : 3, 1.285 + x_offset : 0, y_offset : 0, 1.286 + dispose : "background", blend : "over", delay : 500, 1.287 + 1.288 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.289 + 1.290 + pixels : [ 1.291 + 0,0,255,75, 0,0,255,75, 0,0,255,75, 1.292 + 0,0,255,180, 0,0,255,180, 0,0,255,180, 1.293 + 0,0,255,255, 0,0,255,255, 0,0,255,255 1.294 + ] 1.295 + } 1.296 + 1.297 + ], 1.298 + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AEAbA0RWQAAABFJREFUCJlj+M/A8B+GGXByAF3XEe/CoiJ1AAAAGmZjVEwAAAABAAAAAwAAAAMAAAAAAAAAAAH0A+gBAYB5yxsAAAAcZmRBVAAAAAIImWNg+M/wn+E/wxaG/wzeDDg5ACeGDvKVa3pyAAAAGmZjVEwAAAADAAAAAwAAAAMAAAAAAAAAAAH0A+gBAW3vGPIAAAAcZmRBVAAAAAQImWNgYPjvjcAM/7cgMMP//zAMAPqkDvLn1m3SAAAAAElFTkSuQmCC" 1.299 +}; 1.300 + 1.301 + 1.302 +var apng3 = { 1.303 + // A 3x3 image with 4 frames. First frame is white, then 1x1 frames draw a diagonal line 1.304 + width : 3, height : 3, skipFirstFrame : false, 1.305 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, 1.306 + transparency : null, 1.307 + plays : 0, 1.308 + 1.309 + frames : [ 1.310 + { // frame #1 1.311 + width : 3, height : 3, 1.312 + x_offset : 0, y_offset : 0, 1.313 + dispose : "none", blend : "source", delay : 500, 1.314 + 1.315 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.316 + 1.317 + pixels : [ 1.318 + 1.319 + 255,255,255,255, 255,255,255,255, 255,255,255,255, 1.320 + 255,255,255,255, 255,255,255,255, 255,255,255,255, 1.321 + 255,255,255,255, 255,255,255,255, 255,255,255,255 1.322 + ] 1.323 + }, 1.324 + 1.325 + { // frame #2 1.326 + width : 1, height : 1, 1.327 + x_offset : 0, y_offset : 0, 1.328 + dispose : "none", blend : "source", delay : 500, 1.329 + 1.330 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.331 + 1.332 + pixels : [ 1.333 + 0,0,0,255 1.334 + ] 1.335 + }, 1.336 + 1.337 + { // frame #3 1.338 + width : 1, height : 1, 1.339 + x_offset : 1, y_offset : 1, 1.340 + dispose : "none", blend : "source", delay : 500, 1.341 + 1.342 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.343 + 1.344 + pixels : [ 1.345 + 0,0,0,255 1.346 + ] 1.347 + }, 1.348 + 1.349 + { // frame #4 1.350 + width : 1, height : 1, 1.351 + x_offset : 2, y_offset : 2, 1.352 + dispose : "none", blend : "source", delay : 500, 1.353 + 1.354 + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, 1.355 + 1.356 + pixels : [ 1.357 + 0,0,0,255 1.358 + ] 1.359 + } 1.360 + ], 1.361 + 1.362 + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAAEAAAAAHzNZtAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAAA5JREFUCJlj+I8EGHByALuHI91kueRqAAAAGmZjVEwAAAABAAAAAQAAAAEAAAAAAAAAAAH0A+gAADJXfawAAAARZmRBVAAAAAIImWNgYGD4DwABBAEAbr5mpgAAABpmY1RMAAAAAwAAAAEAAAABAAAAAQAAAAEB9APoAAC4OHoxAAAAEWZkQVQAAAAECJljYGBg+A8AAQQBAJZ8LRAAAAAaZmNUTAAAAAUAAAABAAAAAQAAAAIAAAACAfQD6AAA/fh01wAAABFmZEFUAAAABgiZY2BgYPgPAAEEAQB3Eum9AAAAAElFTkSuQmCC" 1.363 +}; 1.364 + 1.365 +// Main test entry point. 1.366 +function run_test() { 1.367 + dump("Checking apng1A...\n"); 1.368 + run_test_for(apng1A); 1.369 + dump("Checking apng1B...\n"); 1.370 + run_test_for(apng1B); 1.371 + dump("Checking apng1C...\n"); 1.372 + run_test_for(apng1C); 1.373 + 1.374 + dump("Checking apng2A...\n"); 1.375 + run_test_for(apng2A); 1.376 + dump("Checking apng2B...\n"); 1.377 + run_test_for(apng2B); 1.378 + 1.379 + dump("Checking apng3...\n"); 1.380 + run_test_for(apng3); 1.381 +}; 1.382 + 1.383 + 1.384 +function run_test_for(input) { 1.385 + var encoder, dataURL; 1.386 + 1.387 + encoder = encodeImage(input); 1.388 + dataURL = makeDataURL(encoder, "image/png"); 1.389 + do_check_eq(dataURL, input.expected); 1.390 +}; 1.391 + 1.392 + 1.393 +function encodeImage(input) { 1.394 + var encoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); 1.395 + encoder.QueryInterface(Ci.imgIEncoder); 1.396 + 1.397 + var options = ""; 1.398 + if (input.transparency) { options += "transparency=" + input.transparency; } 1.399 + options += ";frames=" + input.frames.length; 1.400 + options += ";skipfirstframe=" + (input.skipFirstFrame ? "yes" : "no"); 1.401 + options += ";plays=" + input.plays; 1.402 + encoder.startImageEncode(input.width, input.height, input.format, options); 1.403 + 1.404 + for (var i = 0; i < input.frames.length; i++) { 1.405 + var frame = input.frames[i]; 1.406 + 1.407 + options = ""; 1.408 + if (frame.transparency) { options += "transparency=" + input.transparency; } 1.409 + options += ";delay=" + frame.delay; 1.410 + options += ";dispose=" + frame.dispose; 1.411 + options += ";blend=" + frame.blend; 1.412 + if (frame.x_offset > 0) { options += ";xoffset=" + frame.x_offset; } 1.413 + if (frame.y_offset > 0) { options += ";yoffset=" + frame.y_offset; } 1.414 + 1.415 + encoder.addImageFrame(frame.pixels, frame.pixels.length, 1.416 + frame.width, frame.height, frame.stride, frame.format, options); 1.417 + } 1.418 + 1.419 + encoder.endImageEncode(); 1.420 + 1.421 + return encoder; 1.422 +} 1.423 + 1.424 + 1.425 +function makeDataURL(encoder, mimetype) { 1.426 + var rawStream = encoder.QueryInterface(Ci.nsIInputStream); 1.427 + 1.428 + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); 1.429 + stream.QueryInterface(Ci.nsIBinaryInputStream); 1.430 + 1.431 + stream.setInputStream(rawStream); 1.432 + 1.433 + var bytes = stream.readByteArray(stream.available()); // returns int[] 1.434 + 1.435 + var base64String = toBase64(bytes); 1.436 + 1.437 + return "data:" + mimetype + ";base64," + base64String; 1.438 +} 1.439 + 1.440 +/* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ 1.441 + 1.442 +/* Convert data (an array of integers) to a Base64 string. */ 1.443 +const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + 1.444 + '0123456789+/'; 1.445 +const base64Pad = '='; 1.446 +function toBase64(data) { 1.447 + var result = ''; 1.448 + var length = data.length; 1.449 + var i; 1.450 + // Convert every three bytes to 4 ascii characters. 1.451 + for (i = 0; i < (length - 2); i += 3) { 1.452 + result += toBase64Table[data[i] >> 2]; 1.453 + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; 1.454 + result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; 1.455 + result += toBase64Table[data[i+2] & 0x3f]; 1.456 + } 1.457 + 1.458 + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. 1.459 + if (length%3) { 1.460 + i = length - (length%3); 1.461 + result += toBase64Table[data[i] >> 2]; 1.462 + if ((length%3) == 2) { 1.463 + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; 1.464 + result += toBase64Table[(data[i+1] & 0x0f) << 2]; 1.465 + result += base64Pad; 1.466 + } else { 1.467 + result += toBase64Table[(data[i] & 0x03) << 4]; 1.468 + result += base64Pad + base64Pad; 1.469 + } 1.470 + } 1.471 + 1.472 + return result; 1.473 +}