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