/** * @fileoverview jsTGALoader - Javascript loader for TGA file * @author Vincent Thibault * @version 1.2.0 * @blog http://blog.robrowser.com/javascript-tga-loader.html */ /* Copyright (c) 2013, Vincent Thibault. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ (function(_global) { 'use strict'; /** * TGA Namespace * @constructor */ window.Targa = function() { } /** * @var {object} TGA type constants */ Targa.Type = { NO_DATA: 0, INDEXED: 1, RGB: 2, GREY: 3, RLE_INDEXED: 9, RLE_RGB: 10, RLE_GREY: 11 }; /** * @var {object} TGA origin constants */ Targa.Origin = { BOTTOM_LEFT: 0x00, BOTTOM_RIGHT: 0x01, TOP_LEFT: 0x02, TOP_RIGHT: 0x03, SHIFT: 0x04, MASK: 0x30 }; /** * Check the header of TGA file to detect errors * * @param {object} tga header structure * @throws Error */ function checkHeader( header ) { // What the need of a file without data ? if (header.imageType === Targa.Type.NO_DATA) { throw new Error('Targa::checkHeader() - No data'); } // Indexed type if (header.hasColorMap) { if (header.colorMapLength > 256 || header.colorMapDepth !== 24 || header.colorMapType !== 1) { throw new Error('Targa::checkHeader() - Invalid colormap for indexed type'); } } else { if (header.colorMapType) { throw new Error('Targa::checkHeader() - Why does the image contain a palette ?'); } } // Check image size if (header.width <= 0 || header.height <= 0) { throw new Error('Targa::checkHeader() - Invalid image size'); } // Check pixel size if (header.pixelDepth !== 8 && header.pixelDepth !== 16 && header.pixelDepth !== 24 && header.pixelDepth !== 32) { throw new Error('Targa::checkHeader() - Invalid pixel size "' + header.pixelDepth + '"'); } } /** * Decode RLE compression * * @param {Uint8Array} data * @param {number} offset in data to start loading RLE * @param {number} pixel count * @param {number} output buffer size */ function decodeRLE( data, offset, pixelSize, outputSize) { var pos, c, count, i; var pixels, output; output = new Uint8Array(outputSize); pixels = new Uint8Array(pixelSize); pos = 0; while (pos < outputSize) { c = data[offset++]; count = (c & 0x7f) + 1; // RLE pixels. if (c & 0x80) { // Bind pixel tmp array for (i = 0; i < pixelSize; ++i) { pixels[i] = data[offset++]; } // Copy pixel array for (i = 0; i < count; ++i) { output.set(pixels, pos); pos += pixelSize; } } // Raw pixels. else { count *= pixelSize; for (i = 0; i < count; ++i) { output[pos++] = data[offset++]; } } } return output; } /** * Return a ImageData object from a TGA file (8bits) * * @param {Array} imageData - ImageData to bind * @param {Array} indexes - index to colormap * @param {Array} colormap * @param {number} width * @param {number} y_start - start at y pixel. * @param {number} x_start - start at x pixel. * @param {number} y_step - increment y pixel each time. * @param {number} y_end - stop at pixel y. * @param {number} x_step - increment x pixel each time. * @param {number} x_end - stop at pixel x. * @returns {Array} imageData */ function getImageData8bits(imageData, indexes, colormap, width, y_start, y_step, y_end, x_start, x_step, x_end) { var color, i, x, y; for (i = 0, y = y_start; y !== y_end; y += y_step) { for (x = x_start; x !== x_end; x += x_step, i++) { color = indexes[i]; imageData[(x + width * y) * 4 + 3] = 255; imageData[(x + width * y) * 4 + 2] = colormap[(color * 3) + 0]; imageData[(x + width * y) * 4 + 1] = colormap[(color * 3) + 1]; imageData[(x + width * y) * 4 + 0] = colormap[(color * 3) + 2]; } } return imageData; } /** * Return a ImageData object from a TGA file (16bits) * * @param {Array} imageData - ImageData to bind * @param {Array} pixels data * @param {Array} colormap - not used * @param {number} width * @param {number} y_start - start at y pixel. * @param {number} x_start - start at x pixel. * @param {number} y_step - increment y pixel each time. * @param {number} y_end - stop at pixel y. * @param {number} x_step - increment x pixel each time. * @param {number} x_end - stop at pixel x. * @returns {Array} imageData */ function getImageData16bits(imageData, pixels, colormap, width, y_start, y_step, y_end, x_start, x_step, x_end) { var color, i, x, y; for (i = 0, y = y_start; y !== y_end; y += y_step) { for (x = x_start; x !== x_end; x += x_step, i += 2) { color = pixels[i + 0] | (pixels[i + 1] << 8); imageData[(x + width * y) * 4 + 0] = (color & 0x7C00) >> 7; imageData[(x + width * y) * 4 + 1] = (color & 0x03E0) >> 2; imageData[(x + width * y) * 4 + 2] = (color & 0x001F) >> 3; imageData[(x + width * y) * 4 + 3] = (color & 0x8000) ? 0 : 255; } } return imageData; } /** * Return a ImageData object from a TGA file (24bits) * * @param {Array} imageData - ImageData to bind * @param {Array} pixels data * @param {Array} colormap - not used * @param {number} width * @param {number} y_start - start at y pixel. * @param {number} x_start - start at x pixel. * @param {number} y_step - increment y pixel each time. * @param {number} y_end - stop at pixel y. * @param {number} x_step - increment x pixel each time. * @param {number} x_end - stop at pixel x. * @returns {Array} imageData */ function getImageData24bits(imageData, pixels, colormap, width, y_start, y_step, y_end, x_start, x_step, x_end) { var i, x, y; for (i = 0, y = y_start; y !== y_end; y += y_step) { for (x = x_start; x !== x_end; x += x_step, i += 3) { imageData[(x + width * y) * 4 + 3] = 255; imageData[(x + width * y) * 4 + 2] = pixels[i + 0]; imageData[(x + width * y) * 4 + 1] = pixels[i + 1]; imageData[(x + width * y) * 4 + 0] = pixels[i + 2]; } } return imageData; } /** * Return a ImageData object from a TGA file (32bits) * * @param {Array} imageData - ImageData to bind * @param {Array} pixels data * @param {Array} colormap - not used * @param {number} width * @param {number} y_start - start at y pixel. * @param {number} x_start - start at x pixel. * @param {number} y_step - increment y pixel each time. * @param {number} y_end - stop at pixel y. * @param {number} x_step - increment x pixel each time. * @param {number} x_end - stop at pixel x. * @returns {Array} imageData */ function getImageData32bits(imageData, pixels, colormap, width, y_start, y_step, y_end, x_start, x_step, x_end) { var i, x, y; for (i = 0, y = y_start; y !== y_end; y += y_step) { for (x = x_start; x !== x_end; x += x_step, i += 4) { imageData[(x + width * y) * 4 + 2] = pixels[i + 0]; imageData[(x + width * y) * 4 + 1] = pixels[i + 1]; imageData[(x + width * y) * 4 + 0] = pixels[i + 2]; imageData[(x + width * y) * 4 + 3] = pixels[i + 3]; } } return imageData; } /** * Return a ImageData object from a TGA file (8bits grey) * * @param {Array} imageData - ImageData to bind * @param {Array} pixels data * @param {Array} colormap - not used * @param {number} width * @param {number} y_start - start at y pixel. * @param {number} x_start - start at x pixel. * @param {number} y_step - increment y pixel each time. * @param {number} y_end - stop at pixel y. * @param {number} x_step - increment x pixel each time. * @param {number} x_end - stop at pixel x. * @returns {Array} imageData */ function getImageDataGrey8bits(imageData, pixels, colormap, width, y_start, y_step, y_end, x_start, x_step, x_end) { var color, i, x, y; for (i = 0, y = y_start; y !== y_end; y += y_step) { for (x = x_start; x !== x_end; x += x_step, i++) { color = pixels[i]; imageData[(x + width * y) * 4 + 0] = color; imageData[(x + width * y) * 4 + 1] = color; imageData[(x + width * y) * 4 + 2] = color; imageData[(x + width * y) * 4 + 3] = 255; } } return imageData; } /** * Return a ImageData object from a TGA file (16bits grey) * * @param {Array} imageData - ImageData to bind * @param {Array} pixels data * @param {Array} colormap - not used * @param {number} width * @param {number} y_start - start at y pixel. * @param {number} x_start - start at x pixel. * @param {number} y_step - increment y pixel each time. * @param {number} y_end - stop at pixel y. * @param {number} x_step - increment x pixel each time. * @param {number} x_end - stop at pixel x. * @returns {Array} imageData */ function getImageDataGrey16bits(imageData, pixels, colormap, width, y_start, y_step, y_end, x_start, x_step, x_end) { var i, x, y; for (i = 0, y = y_start; y !== y_end; y += y_step) { for (x = x_start; x !== x_end; x += x_step, i += 2) { imageData[(x + width * y) * 4 + 0] = pixels[i + 0]; imageData[(x + width * y) * 4 + 1] = pixels[i + 0]; imageData[(x + width * y) * 4 + 2] = pixels[i + 0]; imageData[(x + width * y) * 4 + 3] = pixels[i + 1]; } } return imageData; } /** * Open a targa file using XHR, be aware with Cross Domain files... * * @param {string} path - Path of the filename to load * @param {function} callback - callback to trigger when the file is loaded */ Targa.prototype.open = function targaOpen(path, callback) { var req, tga = this; req = new XMLHttpRequest(); req.open('GET', path, true); req.responseType = 'arraybuffer'; req.onload = function() { if (this.status === 200) { tga.load(new Uint8Array(req.response)); if (callback) { callback.call(tga); } } }; req.send(null); }; /** * Load and parse a TGA file * * @param {Uint8Array} data - TGA file buffer array */ Targa.prototype.load = function targaLoad( data ) { var offset = 0; // Not enough data to contain header ? if (data.length < 0x12) { throw new Error('Targa::load() - Not enough data to contain header'); } // Read TgaHeader this.header = { /* 0x00 BYTE */ idLength: data[offset++], /* 0x01 BYTE */ colorMapType: data[offset++], /* 0x02 BYTE */ imageType: data[offset++], /* 0x03 WORD */ colorMapIndex: data[offset++] | data[offset++] << 8, /* 0x05 WORD */ colorMapLength: data[offset++] | data[offset++] << 8, /* 0x07 BYTE */ colorMapDepth: data[offset++], /* 0x08 WORD */ offsetX: data[offset++] | data[offset++] << 8, /* 0x0a WORD */ offsetY: data[offset++] | data[offset++] << 8, /* 0x0c WORD */ width: data[offset++] | data[offset++] << 8, /* 0x0e WORD */ height: data[offset++] | data[offset++] << 8, /* 0x10 BYTE */ pixelDepth: data[offset++], /* 0x11 BYTE */ flags: data[offset++] }; // Set shortcut this.header.hasEncoding = (this.header.imageType === Targa.Type.RLE_INDEXED || this.header.imageType === Targa.Type.RLE_RGB || this.header.imageType === Targa.Type.RLE_GREY); this.header.hasColorMap = (this.header.imageType === Targa.Type.RLE_INDEXED || this.header.imageType === Targa.Type.INDEXED); this.header.isGreyColor = (this.header.imageType === Targa.Type.RLE_GREY || this.header.imageType === Targa.Type.GREY); // Check if a valid TGA file (or if we can load it) checkHeader(this.header); // Move to data offset += this.header.idLength; if (offset >= data.length) { throw new Error('Targa::load() - No data'); } // Read palette if (this.header.hasColorMap) { var colorMapSize = this.header.colorMapLength * (this.header.colorMapDepth >> 3); this.palette = data.subarray( offset, offset + colorMapSize); offset += colorMapSize; } var pixelSize = this.header.pixelDepth >> 3; var imageSize = this.header.width * this.header.height; var pixelTotal = imageSize * pixelSize; // RLE encoded if (this.header.hasEncoding) { this.imageData = decodeRLE(data, offset, pixelSize, pixelTotal); } // RAW pixels else { this.imageData = data.subarray( offset, offset + (this.header.hasColorMap ? imageSize : pixelTotal) ); } }; /** * Return a ImageData object from a TGA file * * @param {object} imageData - Optional ImageData to work with * @returns {object} imageData */ Targa.prototype.getImageData = function targaGetImageData( imageData ) { var width = this.header.width; var height = this.header.height; var origin = (this.header.flags & Targa.Origin.MASK) >> Targa.Origin.SHIFT; var x_start, x_step, x_end, y_start, y_step, y_end; var getImageData; // Create an imageData if (!imageData) { if (document) { imageData = document.createElement('canvas').getContext('2d').createImageData(width, height); } // In Thread context ? else { imageData = { width: width, height: height, data: new Uint8ClampedArray(width * height * 4) }; } } if (origin === Targa.Origin.TOP_LEFT || origin === Targa.Origin.TOP_RIGHT) { y_start = 0; y_step = 1; y_end = height; } else { y_start = height - 1; y_step = -1; y_end = -1; } if (origin === Targa.Origin.TOP_LEFT || origin === Targa.Origin.BOTTOM_LEFT) { x_start = 0; x_step = 1; x_end = width; } else { x_start = width - 1; x_step = -1; x_end = -1; } // TODO: use this.header.offsetX and this.header.offsetY ? switch (this.header.pixelDepth) { case 8: getImageData = this.header.isGreyColor ? getImageDataGrey8bits : getImageData8bits; break; case 16: getImageData = this.header.isGreyColor ? getImageDataGrey16bits : getImageData16bits; break; case 24: getImageData = getImageData24bits; break; case 32: getImageData = getImageData32bits; break; } getImageData(imageData.data, this.imageData, this.palette, width, y_start, y_step, y_end, x_start, x_step, x_end); return imageData; }; /** * Return a canvas with the TGA render on it * * @returns {object} CanvasElement */ Targa.prototype.getCanvas = function targaGetCanvas() { var canvas, ctx, imageData; canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); imageData = ctx.createImageData(this.header.width, this.header.height); canvas.width = this.header.width; canvas.height = this.header.height; ctx.putImageData(this.getImageData(imageData), 0, 0); return canvas; }; /** * Return a dataURI of the TGA file * * @param {string} type - Optional image content-type to output (default: image/png) * @returns {string} url */ Targa.prototype.getDataURL = function targaGetDatURL( type ) { return this.getCanvas().toDataURL(type || 'image/png'); }; // Find Context var shim = {}; if (typeof(exports) === 'undefined') { if (typeof(define) === 'function' && typeof(define.amd) === 'object' && define.amd) { define(function(){ return Targa; }); } else { // Browser shim.exports = typeof(window) !== 'undefined' ? window : _global; } } else { // Commonjs shim.exports = exports; } // Export if (shim.exports) { shim.exports.TGA = Targa; } })(this);