tgaimage
Version:
TGA Image Loader for Web browsers
511 lines (454 loc) • 12.9 kB
JavaScript
'use strict'
/*global Buffer*/
const _ImageType = {
noImage: 0,
colorMapped: 1,
RGB: 2,
blackAndWhite: 3,
runlengthColorMapped: 9,
runlengthRGB: 10,
compressedBlackAndWhite: 11,
compressedColorMapped: 32,
compressed4PassQTColorMapped: 33
}
const _headerLength = 18
class TGAImage {
/**
* constructor
* @param {Buffer|ArrayBuffer} data -
* @constructor
*/
constructor(data) {
if(data instanceof Buffer){
this._buffer = data
}else if(typeof data === 'string'){
this._buffer = Buffer.from(data, 'binary')
}else if(data){
this._buffer = Buffer.from(data)
}else{
this._buffer = null
}
// Header
this._idLength = 0
this._colorMapType = 0
this._imageType = 0
this._colorMapOrigin = 0
this._colorMapLength = 0
this._colorMapDepth = 0
this._imageXOrigin = 0
this._imageYOrigin = 0
this._imageWidth = 0
this._imageHeight = 0
this._imageDepth = 0
this._alphaDepth = 0
this._leftToRight = true
this._topToBottom = false
this._interleave = false
this._hasAlpha = false
// Image Identification Field
this._imageID = null
// Image Data
this._canvas = null
this._context = null
this._imageData = null
this._image = null
// for HTML Image tag compatibility
this._src = null
this.onload = null
this.onerror = null
this._resolveFunc = null
this._rejectFunc = null
this._didLoad = new Promise((resolve, reject) => {
this._resolveFunc = resolve
this._rejectFunc = reject
})
if(data){
this._parseData()
}
}
static imageWithData(data) {
return new TGAImage(data)
}
static imageWithURL(url) {
const image = new TGAImage()
image._loadURL(url)
return image
}
_loadURL(url) {
this._src = url
this._requestBinaryFile(url).then((data) => {
this._buffer = Buffer.from(data)
this._parseData()
}).catch((error) => {
this._reject(error)
})
}
_requestBinaryFile(url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest()
request.open('GET', url)
request.responseType = 'arraybuffer'
request.onload = (ev) => {
if(request.response){
resolve(request.response)
}else{
reject(request)
}
}
request.onerror = (ev) => {
reject(ev)
}
request.send(null)
})
}
_parseData() {
this._readHeader()
this._readImageID()
this._initImage()
const data = this._getImageData()
switch(this._imageType){
case _ImageType.noImage: {
// nothing to do
break
}
case _ImageType.colorMapped: {
this._parseColorMapData(data)
break
}
case _ImageType.RGB: {
this._parseRGBData(data)
break
}
case _ImageType.blackAndWhite: {
this._parseBlackAndWhiteData(data)
break
}
case _ImageType.runlengthColorMapped: {
this._parseColorMapData(data)
break
}
case _ImageType.runlengthRGB: {
this._parseRGBData(data)
break
}
case _ImageType.compressedBlackAndWhite: {
this._parseBlackAndWhiteData(data)
break
}
case _ImageType.compressedColorMapped: {
console.error('parser for compressed TGA is not implemeneted')
break
}
case _ImageType.compressed4PassQTColorMapped: {
console.error('parser for compressed TGA is not implemeneted')
break
}
default: {
throw new Error('unknown imageType: ' + this._imageType)
}
}
this._setImage()
this._deleteBuffer()
}
_readHeader() {
this._idLength = this._buffer.readUIntLE(0, 1)
this._colorMapType = this._buffer.readUIntLE(1, 1)
this._imageType = this._buffer.readUIntLE(2, 1)
this._colorMapOrigin = this._buffer.readUIntLE(3, 2)
this._colorMapLength = this._buffer.readUIntLE(5, 2)
this._colorMapDepth = this._buffer.readUIntLE(7, 1)
this._imageXOrigin = this._buffer.readUIntLE(8, 2)
this._imageYOrigin = this._buffer.readUIntLE(10, 2)
this._imageWidth = this._buffer.readUIntLE(12, 2)
this._imageHeight = this._buffer.readUIntLE(14, 2)
this._imageDepth = this._buffer.readUIntLE(16, 1)
const descriptor = this._buffer.readUIntLE(17, 1)
this._alphaDepth = descriptor & 0x0F
this._leftToRight = ((descriptor & 0x10) === 0)
this._topToBottom = ((descriptor & 0x20) > 0)
this._interleave = descriptor & 0xC0
}
_readImageID() {
if(this._idLength > 0){
this._imageID = this._buffer.subarray(_headerLength, this._idLength)
}
}
_initImage() {
if(this._imageType === _ImageType.noImage){
return
}
if(this._imageWidth <= 0 || this._imageHeight <= 0){
return
}
this._canvas = document.createElement('canvas')
this._canvas.width = this._imageWidth
this._canvas.height = this._imageHeight
this._context = this._canvas.getContext('2d')
this._imageData = this._context.createImageData(this._imageWidth, this._imageHeight)
}
_setImage() {
this._context.putImageData(this._imageData, 0, 0)
this._image = new Image()
this._image.width = this._imageWidth
this._image.height = this._imageHeight
this._image.onload = () => {
this._resolve()
}
this._image.src = this._canvas.toDataURL()
}
_deleteBuffer() {
if(this._buffer){
delete this._buffer
this._buffer = null
}
if(this._imageData){
delete this._imageData
this._imageData = null
}
}
_parseColorMapData(buf) {
if(this._colorMapDepth === 24 || this._colorMapDepth === 16 || this._colorMapDepth === 15){
this._hasAlpha = false
}else if(this._colorMapDepth === 32){
this._hasAlpha = true
}else{
throw new Error('unknown colorMapDepth: ' + this._colorMapDepth)
}
const colorMapDataPos = _headerLength + this._idLength
const colorMapDataSize = Math.ceil(this._colorMapDepth / 8)
const colorMapDataLen = colorMapDataSize * this._colorMapLength
const imageDataSize = 1
const colorMap = []
let pos = colorMapDataPos
for(let i=0; i<this._colorMapLength; i++){
const rgba = this._getRGBA(this._buffer, pos, this._colorMapDepth)
colorMap.push(rgba)
pos += colorMapDataSize
}
const data = this._imageData.data
let initX = 0
let initY = 0
let xStep = 1
let yStep = 1
if(!this._leftToRight){
initX = this._imageWidth - 1
xStep = -1
}
if(!this._topToBottom){
initY = this._imageHeight - 1
yStep = -1
}
pos = 0
let y = initY
const defaultColor = [0xFF, 0xFF, 0xFF, 0xFF]
for(let iy=0; iy<this._imageHeight; iy++){
let x = initX
for(let ix=0; ix<this._imageWidth; ix++){
const index = (y * this._imageWidth + x) * 4
let color = defaultColor
const mapNo = buf[pos] - this._colorMapOrigin
if(mapNo >= 0){
color = colorMap[mapNo]
}
data[index] = color[0]
data[index+1] = color[1]
data[index+2] = color[2]
data[index+3] = color[3]
x += xStep
pos += imageDataSize
}
y += yStep
}
}
_parseRGBData(buf) {
if(this._imageDepth === 24 || this._imageDepth === 16 || this._imageDepth === 15){
this._hasAlpha = false
}else if(this._imageDepth === 32){
this._hasAlpha = true
}else{
throw new Error('unknown imageDepth: ' + this._imageDepth)
}
const imageDataSize = Math.ceil(this._imageDepth / 8)
const data = this._imageData.data
let initX = 0
let initY = 0
let xStep = 1
let yStep = 1
if(!this._leftToRight){
initX = this._imageWidth - 1
xStep = -1
}
if(!this._topToBottom){
initY = this._imageHeight - 1
yStep = -1
}
let pos = 0
let y = initY
for(let iy=0; iy<this._imageHeight; iy++){
let x = initX
for(let ix=0; ix<this._imageWidth; ix++){
const index = (y * this._imageWidth + x) * 4
const rgba = this._getRGBA(buf, pos, this._imageDepth)
data[index] = rgba[0]
data[index+1] = rgba[1]
data[index+2] = rgba[2]
data[index+3] = rgba[3]
x += xStep
pos += imageDataSize
}
y += yStep
}
}
_getRGBA(buf, offset, depth) {
if(depth === 15){
const r = (buf[offset+1] & 0x7c) << 1
const g = ((buf[offset+1] & 0x03) << 6) | ((buf[offset] & 0xe0) >> 2)
const b = (buf[offset] & 0x1f) << 3
//const a = (buf[offset+1] & 0x80) > 0 ? 255 : 0
const a = 255
return [r, g, b, a]
}else if(depth === 16){
const r = (buf[offset+1] & 0x7c) << 1
const g = ((buf[offset+1] & 0x03) << 6) | ((buf[offset] & 0xe0) >> 2)
const b = (buf[offset] & 0x1f) << 3
const a = 255
return [r, g, b, a]
}else if(depth === 24){
return [buf[offset+2], buf[offset+1], buf[offset], 255]
}else if(depth === 32){
return [buf[offset+2], buf[offset+1], buf[offset], buf[offset+3]]
}
throw new Error('unsupported imageDepth: ' + depth)
}
_parseBlackAndWhiteData(buf) {
if(this._imageDepth == 8){
this._hasAlpha = false
}else if(this._imageDepth == 16){
this._hasAlpha = true
}else{
throw new Error('unknown imageDepth: ' + this._imageDepth)
}
const imageDataSize = this._imageDepth / 8
const data = this._imageData.data
let initX = 0
let initY = 0
let xStep = 1
let yStep = 1
if(!this._leftToRight){
initX = this._imageWidth - 1
xStep = -1
}
if(!this._topToBottom){
initY = this._imageHeight - 1
yStep = -1
}
let pos = 0
if(this._hasAlpha){
let y = initY
for(let iy=0; iy<this._imageHeight; iy++){
let x = initX
for(let ix=0; ix<this._imageWidth; ix++){
const index = (y * this._imageWidth + x) * 4
const c = buf[pos]
const a = buf[pos+1]
data[index] = c
data[index+1] = c
data[index+2] = c
data[index+3] = a
x += xStep
pos += imageDataSize
}
y += yStep
}
}else{
let y = initY
for(let iy=0; iy<this._imageHeight; iy++){
let x = initX
for(let ix=0; ix<this._imageWidth; ix++){
const index = (y * this._imageWidth + x) * 4
const c = buf[pos]
const a = 255
data[index] = c
data[index+1] = c
data[index+2] = c
data[index+3] = a
x += xStep
pos += imageDataSize
}
y += yStep
}
}
}
_getImageData() {
let data = null
if(this._imageType !== _ImageType.none){
const colorMapDataLen = Math.ceil(this._colorMapDepth / 8) * this._colorMapLength
const start = _headerLength + this._idLength + colorMapDataLen
data = this._buffer.subarray(start)
}
if(this._imageType === _ImageType.runlengthColorMapped
|| this._imageType === _ImageType.runlengthRGB){
data = this._decompressRunlengthData(data)
}else if(this._imageType === _ImageType.compressedBlackAndWhite){
data = this._decompressRunlengthData(data)
}else if(this._imageType === _ImageType.compressedColorMapped){
// TODO: implement
console.error('Compressed Color Mapped TGA Image data is not supported')
}else if(this._imageType === _ImageType.compressed4PassQTColorMapped){
// TODO: implement
console.error('Compressed Color Mapped TGA Image data is not supported')
}
return data
}
_decompressRunlengthData(data) {
const d = []
const elementCount = Math.ceil(this._imageDepth / 8)
const dataLength = elementCount * this._imageWidth * this._imageHeight
let pos = 0
while(d.length < dataLength){
const packet = data[pos]
pos += 1
if((packet & 0x80) !== 0){ // RLE
const elements = data.slice(pos, pos + elementCount)
pos += elementCount
const count = (packet & 0x7F) + 1
for(let i=0; i<count; i++){
d.push(...elements)
}
}else{ // RAW
const len = (packet + 1) * elementCount
d.push(...data.slice(pos, pos + len))
pos += len
}
}
return d
}
get image() {
return this._image
}
get canvas() {
return this._canvas
}
get didLoad() {
return this._didLoad
}
_resolve(e) {
if(this.onload){
this.onload(e)
}
this._resolveFunc(e)
}
_reject(e) {
if(this.onerror){
this.onerror(e)
}
this._rejectFunc(e)
}
get src() {
return this._src
}
set src(newValue) {
this._loadURL(newValue)
}
}
module.exports = TGAImage