1 jls.loader.provide('jls.util.Image'); 2 3 jls.loader.requireLibrary('jls_jpeg'); 4 5 jls.loader.require('jls.io.File'); 6 jls.loader.require('jls.io.FileChannel'); 7 8 jls.util.Image = jls.lang.Class.create( /** @lends jls.util.Image.prototype */ 9 { 10 /** 11 * Creates an image. 12 * 13 * @param {Number} width The image width. 14 * @param {Number} height The image height. 15 * @param {Number} [colorSpace] The color space. 16 * @param {Number} [components] The component count per pixel. 17 * @param {Number} [orientation] The image orientation. 18 * @constructs 19 * @class This class represents an image 20 */ 21 initialize : function(width, height, colorSpace, components, orientation) { 22 this.width = width; 23 this.height = height; 24 this.colorSpace = (typeof colorSpace == 'number') ? colorSpace : jls.util.Image.COLOR_SPACE_RGB; 25 this.components = 0; 26 switch (this.colorSpace) { 27 case jls.util.Image.COLOR_SPACE_GRAYSCALE: 28 this.components = 1; 29 break; 30 case jls.util.Image.COLOR_SPACE_RGB: 31 case jls.util.Image.COLOR_SPACE_YCbCr: 32 case jls.util.Image.COLOR_SPACE_BGR: 33 this.components = 3; 34 break; 35 case jls.util.Image.COLOR_SPACE_CMYK: 36 case jls.util.Image.COLOR_SPACE_YCCK: 37 case jls.util.Image.COLOR_SPACE_BGRA: 38 this.components = 4; 39 break; 40 case jls.util.Image.COLOR_SPACE_UNKNOWN: 41 break; 42 default: 43 throw new jls.lang.Exception('Unknown color space (' + this.colorSpace + ')'); 44 } 45 if (typeof components == 'number') { 46 this.components = components; 47 } 48 this.orientation = (typeof orientation == 'number') ? orientation : jls.util.Image.TOP_LEFT; 49 this.bpp = this.components; 50 this._bitPerPixel = this.bpp * 8; 51 this.bpr = (this.width * this._bitPerPixel) / 8; 52 if (this.bpr % 4 != 0) { 53 this.bpr = this.bpr + 4 - this.bpr % 4; 54 } 55 this._size = this.bpr * this.height; 56 jls.logger.info('jls.util.Image(' + width + ', ' + height + ', ' + colorSpace + ', ' + components + ', ' + orientation + ') ' + this._size + ' bytes'); 57 this._buffer = jls.lang.ByteBuffer.allocate(this._size); 58 }, 59 /** 60 * Returns the image width. 61 * 62 * @returns {Number} The image width. 63 */ 64 getWidth : function() { 65 return this.width; 66 }, 67 /** 68 * Returns the image height. 69 * 70 * @returns {Number} The image height. 71 */ 72 getHeight : function() { 73 return this.height; 74 }, 75 /** 76 * Returns the image buffer. 77 * 78 * @returns {jls.lang.Buffer} The image buffer. 79 */ 80 getBuffer : function() { 81 return this._buffer; 82 }, 83 _moveTo : function(x, y) { 84 var offset = this.bpr * y + this.bpp * x; 85 this._buffer.setPosition(offset); 86 return this; 87 }, 88 getPixel : function(x, y) { 89 this._moveTo(x, y); 90 var p = []; 91 p.push(this._buffer.getByte()); 92 p.push(this._buffer.getByte()); 93 p.push(this._buffer.getByte()); 94 return p; 95 }, 96 setPixel : function(x, y, r, g, b) { 97 this._moveTo(x, y); 98 this._buffer.putByte(r); 99 this._buffer.putByte(g); 100 this._buffer.putByte(b); 101 return this; 102 }, 103 /** 104 * Returns the image orientation. 105 * 106 * @returns {Number} The image orientation. 107 */ 108 getOrientation : function() { 109 return this.orientation; 110 }, 111 setOrientation : function(orientation) { 112 this.orientation = orientation; 113 }, 114 getColorSpace : function() { 115 return this.colorSpace; 116 }, 117 changeColorSpace : function(colorSpace) { 118 if (this.colorSpace == colorSpace) { 119 return; 120 } 121 switch (this.colorSpace) { 122 case jls.util.Image.COLOR_SPACE_RGB: 123 switch (colorSpace) { 124 case jls.util.Image.COLOR_SPACE_YCbCr: 125 this.rgbToYuv(); 126 break; 127 case jls.util.Image.COLOR_SPACE_BGR: 128 this.componentSwap(); 129 break; 130 default: 131 throw new jls.lang.Exception('Unsupported color space change (' + this.colorSpace + '->' + colorSpace + ')'); 132 } 133 break; 134 case jls.util.Image.COLOR_SPACE_YCbCr: 135 switch (colorSpace) { 136 case jls.util.Image.COLOR_SPACE_RGB: 137 this.yuvToRgb(); 138 break; 139 default: 140 throw new jls.lang.Exception('Unsupported color space change (' + this.colorSpace + '->' + colorSpace + ')'); 141 } 142 break; 143 case jls.util.Image.COLOR_SPACE_BGR: 144 this.componentSwap(); 145 break; 146 default: 147 throw new jls.lang.Exception('Unsupported color space change (' + this.colorSpace + '->' + colorSpace + ')'); 148 } 149 this.colorSpace = colorSpace; 150 }, 151 getComponents : function() { 152 return this.components; 153 }, 154 /** 155 * Returns a duplicate of this image. 156 * 157 * @returns {jls.util.Image} The duplicate image. 158 */ 159 duplicate : function() { 160 jls.logger.info('jls.util.Image.duplicate()'); 161 var img = new jls.util.Image(this.getWidth(), this.getHeight(), this.getColorSpace(), this.getComponents(), this.getOrientation()); 162 //img2.putBuffer(img.getBuffer()); 163 img.getBuffer().byteArray().memcpy(0, this.getBuffer().byteArray(), 0, this.getBuffer().byteArray().size()); 164 return img; 165 }, 166 /** 167 * Returns a subsample of this image. 168 * 169 * @param {Number} width The width of the subsample image. 170 * @param {Number} height The height of the subsample image. 171 * @returns {jls.util.Image} The subsample image. 172 */ 173 subsample : function(width, height) { 174 jls.logger.info('jls.util.Image.subsample(' + width + ', ' + height + ')'); 175 var img = new jls.util.Image(width, height, this.getColorSpace(), this.getComponents(), this.getOrientation()); 176 _native.jpeg.subsampleBilinear(this.getBuffer().byteArray(), this, img.getBuffer().byteArray(), img); 177 return img; 178 }, 179 /** 180 * Swap the per pixel components depending on the indexes provided. 181 * 182 * @param {Array} [indexes] The new indexes. 183 */ 184 componentSwap : function(indexes) { 185 jls.logger.info('jls.util.Image.componentSwap()'); 186 if (! indexes) { 187 indexes = []; 188 for (var i = 0; i < this.bpp; i++) { 189 indexes.push(this.bpp - 1 - i); 190 } 191 } 192 _native.jpeg.componentSwap(this.getBuffer().byteArray(), this, indexes); 193 }, 194 /** 195 * Applies kernel based convolution on this image. 196 * 197 * @param {Array} kernel The kernel to use. 198 * @param {Number} [componentStart] The first component to apply convolution. 199 * @param {Number} [componentStop] The last component to apply convolution. 200 */ 201 convolve : function(kernel, componentStart, componentStop) { 202 jls.logger.info('jls.util.Image.convolve()'); 203 _native.jpeg.convolve(this.getBuffer().byteArray(), this, kernel, componentStart, componentStop); 204 }, 205 /* 206 * Merges an image with this image. 207 * 208 * @param {jls.util.Image} img The image to merge with. 209 */ 210 merge : function(img) { 211 jls.logger.info('jls.util.Image.merge()'); 212 _native.jpeg.merge(this.getBuffer().byteArray(), this, img.getBuffer().byteArray(), img); 213 }, 214 /** 215 * Applies a matrix on the pixel components of this image. 216 * 217 * @param {Array} matrix The matrix to use. 218 * @param {Array} [delta] The delta to use. 219 */ 220 componentMatrix : function(matrix, delta) { 221 jls.logger.info('jls.util.Image.componentMatrix()'); 222 if (! delta) { 223 delta = []; 224 for (var i = 0; i < this.bpp; i++) { 225 delta.push(0); 226 } 227 } 228 _native.jpeg.componentMatrix(this.getBuffer().byteArray(), this, matrix, delta); 229 }, 230 rgbToYuv : function() { 231 this.componentMatrix([ 232 0.29900, 0.58700, 0.11400, 233 -0.16874, -0.33126, 0.50000, 234 0.50000, -0.41869, -0.08131 235 ], [ 236 0.5, 127.5, 127.5 237 //0, 128, 128 238 ]); 239 }, 240 yuvToRgb : function() { 241 this.componentMatrix([ 242 1, 0, 1.402, 243 1, -0.34414, -0.71414, 244 1, 1.772, 0 245 ], [ 246 -179.456 + 0.5, 44.04992 + 91.40992 - 0.5, -226.816 + 0.5 247 //-179.456, 44.04992 + 91.40992, -226.816 248 ]); 249 } 250 }); 251 252 /* 253 typedef struct tagBITMAPFILEHEADER { 254 WORD bfType; 2 255 DWORD bfSize; 4 256 WORD bfReserved1; 2 257 WORD bfReserved2; 2 258 DWORD bfOffBits; 4 259 } BITMAPFILEHEADER, *PBITMAPFILEHEADER; = 14 260 261 typedef struct tagBITMAPCOREHEADER { 262 DWORD bcSize; 263 WORD bcWidth; 264 WORD bcHeight; 265 WORD bcPlanes; 266 WORD bcBitCount; 267 } BITMAPCOREHEADER, *PBITMAPCOREHEADER; = 12 268 269 typedef struct tagBITMAPINFOHEADER { 270 DWORD biSize; 271 LONG biWidth; 272 LONG biHeight; 273 WORD biPlanes; 274 WORD biBitCount; 275 DWORD biCompression; 276 DWORD biSizeImage; 277 LONG biXPelsPerMeter; 278 LONG biYPelsPerMeter; 279 DWORD biClrUsed; 280 DWORD biClrImportant; 281 } BITMAPINFOHEADER, *PBITMAPINFOHEADER; = 9*4 + 2*2 = 40 282 */ 283 284 Object.extend(jls.util.Image, /** @lends jls.util.Image */ 285 { 286 /* 287 * Orientations from EXIF as firt row - first column. 288 * 289 * Here is a description given by Adam M. Costello: 290 * For convenience, here is what the letter F would look like if it were tagged correctly and 291 * displayed by a program that ignores the orientation tag (thus showing the stored image): 292 * 293 * 1 2 3 4 5 6 7 8 294 * 295 * 888888 888888 88 88 8888888888 88 88 8888888888 296 * 88 88 88 88 88 88 88 88 88 88 88 88 297 * 8888 8888 8888 8888 88 8888888888 8888888888 88 298 * 88 88 88 88 299 * 88 88 888888 888888 300 * 301 */ 302 TOP_LEFT: 1, 303 TOP_RIGHT: 2, 304 BOTTOM_RIGHT: 3, 305 BOTTOM_LEFT: 4, 306 LEFT_TOP: 5, 307 RIGHT_TOP: 6, 308 RIGHT_BOTTOM: 7, 309 LEFT_BOTTOM: 8, 310 /* 311 * Color spaces 312 */ 313 COLOR_SPACE_UNKNOWN: _native.jpeg.COLOR_SPACE_UNKNOWN, 314 COLOR_SPACE_GRAYSCALE: _native.jpeg.COLOR_SPACE_GRAYSCALE, 315 COLOR_SPACE_RGB: _native.jpeg.COLOR_SPACE_RGB, 316 COLOR_SPACE_YCbCr: _native.jpeg.COLOR_SPACE_YCbCr, 317 COLOR_SPACE_CMYK: _native.jpeg.COLOR_SPACE_CMYK, 318 COLOR_SPACE_YCCK: _native.jpeg.COLOR_SPACE_YCCK, 319 320 COLOR_SPACE_BGR: 10, 321 //COLOR_SPACE_BGRA: 11, 322 323 /** 324 * Reads a BMP. 325 * 326 * @param {jls.io.FileInputStream} input The input to read the image. 327 * @returns {jls.util.Image} The image. 328 */ 329 readBMP : function(input) { 330 /*var dumpObj = function(obj) { 331 for (var key in obj) { 332 if (typeof obj[key] == 'function') continue; 333 jls.logger.debug('obj.' + key + ': ' + obj[key]); 334 } 335 };*/ 336 /*var dumpNum = function(num) { 337 jls.logger.debug('num: ' + num + ' (0x' + num.toString(16) + ')'); 338 };*/ 339 var buffer = jls.lang.ByteBuffer.allocate(1024); 340 buffer.setByteOrder(jls.lang.Buffer.LITTLE_ENDIAN); 341 342 buffer.clear(); 343 buffer.setLimit(14); 344 input.read(buffer); 345 buffer.flip(); 346 //var typeStr = buffer.getString(2); 347 //jls.logger.debug('type: "' + typeStr + '"'); 348 var typeStr = buffer.getShort(); 349 var size = buffer.getInt(true); 350 //dumpNum(size); 351 buffer.incrementPosition(4); 352 var offBits = buffer.getInt(true); 353 //dumpNum(offBits); 354 355 buffer.clear(); 356 buffer.setLimit(offBits - 14); 357 input.read(buffer); 358 buffer.flip(); 359 360 var sSize = buffer.getInt(true); 361 //dumpNum(sSize); 362 var width, height, planes, bitCount; 363 if (sSize == 12) { 364 width = buffer.getShort(true); 365 height = buffer.getShort(true); 366 planes = buffer.getShort(true); 367 bitCount = buffer.getShort(true); 368 } else if (sSize == 40) { 369 width = buffer.getInt(true); 370 height = buffer.getInt(true); 371 planes = buffer.getShort(true); 372 bitCount = buffer.getShort(true); 373 } else { 374 throw 'Invalid BMP (' + sSize + ')'; 375 } 376 jls.logger.debug('BMP: ' + width + ', ' + height + ', ' + planes + ', ' + bitCount); 377 var img = new jls.util.Image(width, height); 378 input.read(img.getBuffer()); 379 return img; 380 }, 381 /** 382 * Reads a JPEG. 383 * 384 * @param {jls.io.FileInputStream} input The input to read the image. 385 * @param {Number} [colorSpace] The output color space. 386 * @param {Number} [gamma] The gamma. 387 * @param {Function} [scaleCalculator] A callback to calculate the scale depending on the image dimension. 388 * @returns {jls.util.Image} The image. 389 */ 390 readJPEG : function(input, colorSpace, gamma, scaleCalculator) { 391 var jdecompressor = new _native.jpeg.JpegDecompressor(); 392 var buffer = jls.lang.ByteBuffer.allocate(8192); 393 /** @ignore */ 394 var srcMgr = { 395 sourceBuffer : buffer, 396 sourceByteArray : buffer.byteArray(), 397 sourceCapacity : buffer.capacity(), 398 sourceOffset : 0, 399 initSource : function() { 400 jls.logger.debug('initSource()'); 401 }, 402 fillSource : function() { 403 jls.logger.trace('fillSource(), offset: ' + this.sourceOffset + 404 ', capacity: ' + this.sourceCapacity); 405 this.sourceBuffer.clear(); 406 this.sourceBuffer.setPosition(this.sourceOffset); 407 this.sourceCapacity = input.read(this.sourceBuffer); 408 }, 409 skipSource : function(count) { 410 jls.logger.warn('skipSource(' + count + '), offset: ' + this.sourceOffset + 411 ', capacity: ' + this.sourceCapacity); 412 }, 413 termSource : function() { 414 jls.logger.trace('termSource(), offset: ' + this.sourceOffset + 415 ', capacity: ' + this.sourceCapacity); 416 } 417 }; 418 var outColorSpace = _native.jpeg.COLOR_SPACE_UNKNOWN; 419 if ((typeof colorSpace == 'number') && (colorSpace >= _native.jpeg.COLOR_SPACE_UNKNOWN) && (colorSpace <= _native.jpeg.COLOR_SPACE_YCCK)) { 420 outColorSpace = colorSpace; 421 } 422 gamma = gamma || 1.0; 423 jdecompressor.initialize(srcMgr); 424 jdecompressor.setColorSpace(outColorSpace); 425 jdecompressor.setGamma(gamma); 426 if (scaleCalculator) { 427 var s = scaleCalculator(jdecompressor.getWidth(true), jdecompressor.getHeight(true)); 428 if (s) { 429 jdecompressor.setScale(s[0] /*Num*/, s[1] /*Denom*/); 430 } 431 } 432 jdecompressor.start(); 433 var width = jdecompressor.getWidth(); 434 var height = jdecompressor.getHeight(); 435 var components = jdecompressor.getComponents(); 436 jls.logger.debug('JPEG: ' + width + 'x' + height + ', ' + components + ', ' + outColorSpace); 437 var img = new jls.util.Image(width, height); 438 jdecompressor.read(img.getBuffer().byteArray(), -1, img.bpr); 439 jdecompressor.finish(); 440 //img.componentSwap(); 441 img.colorSpace = jdecompressor.getColorSpace(); 442 img.setOrientation(jls.util.Image.BOTTOM_LEFT); 443 if ((typeof colorSpace == 'number') && (colorSpace != _native.jpeg.COLOR_SPACE_UNKNOWN)) { 444 img.changeColorSpace(colorSpace); 445 } 446 buffer.free(); 447 return img; 448 }, 449 /** 450 * Writes a JPEG. 451 * 452 * @param {jls.util.Image} img The image to write. 453 * @param {jls.io.FileOutputStream} output The output to write the image. 454 */ 455 writeJPEG : function(img, output, quality) { 456 quality = quality || 75; 457 var buffer = jls.lang.ByteBuffer.allocate(8192); 458 var jcompress = new _native.jpeg.JpegCompression(); 459 var destMgr = { 460 destinationBuffer : buffer, 461 destinationByteArray : buffer.byteArray(), 462 destinationCapacity : buffer.capacity(), 463 destinationOffset : 0, 464 initDestination : function() { 465 jls.logger.trace('initDestination()'); 466 }, 467 flushDestination : function() { 468 jls.logger.trace('flushDestination()'); 469 this.destinationBuffer.clear(); 470 output.write(this.destinationBuffer); 471 }, 472 termDestination : function(count) { 473 jls.logger.trace('termDestination(' + count + ')'); 474 this.destinationBuffer.clear(); 475 this.destinationBuffer.setLimit(count); 476 output.write(this.destinationBuffer); 477 } 478 }; 479 jls.logger.debug('ColorSpace: ' + img.getColorSpace()); 480 jcompress.start(destMgr, img.getWidth(), img.getHeight(), img.getComponents(), img.getColorSpace(), quality); 481 jcompress.write(img.getBuffer().byteArray(), -1); 482 jcompress.finish(); 483 buffer.free(); 484 } 485 }); 486 487