From 8c9d36ac3bd7340f2cb673d2ef4bfdf01f01d643 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Thu, 26 Sep 2024 00:46:17 +0300 Subject: [PATCH] Splitted code into multiple files Signed-off-by: Mehmet Baker --- package.json | 2 +- src/TGAImage.ts | 636 ------------------ src/index.ts | 130 ++-- src/{ImageStats.ts => lib/ImageFileInfo.ts} | 29 +- src/lib/TGAFile.ts | 35 + src/lib/draw-methods/drawColorMapped.ts | 73 ++ src/lib/draw-methods/drawRunLengthEncoded.ts | 168 +++++ .../drawRunLengthEncodedColorMapped.ts | 142 ++++ src/lib/draw-methods/drawUncompressed.ts | 68 ++ .../draw-methods/drawUncompressedGrayscale.ts | 37 + src/{ => lib}/types.ts | 0 src/utils.ts | 70 +- src/www-index.ts | 52 ++ www/index.html | 2 +- 14 files changed, 737 insertions(+), 707 deletions(-) delete mode 100644 src/TGAImage.ts rename src/{ImageStats.ts => lib/ImageFileInfo.ts} (88%) create mode 100644 src/lib/TGAFile.ts create mode 100644 src/lib/draw-methods/drawColorMapped.ts create mode 100644 src/lib/draw-methods/drawRunLengthEncoded.ts create mode 100644 src/lib/draw-methods/drawRunLengthEncodedColorMapped.ts create mode 100644 src/lib/draw-methods/drawUncompressed.ts create mode 100644 src/lib/draw-methods/drawUncompressedGrayscale.ts rename src/{ => lib}/types.ts (100%) create mode 100644 src/www-index.ts diff --git a/package.json b/package.json index 76f8ae9..37ee807 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "homepage": "https://github.com/mehmetb/targa-from-scratch#readme", "scripts": { "build": "esbuild src/index.ts --outdir=www/js --format=esm --bundle --target=ESNext", - "start": "esbuild src/index.ts --outdir=www/js --format=esm --watch --bundle --target=ESNext --servedir=www" + "start": "esbuild src/www-index.ts --outdir=www/js --format=esm --watch --bundle --target=ESNext --servedir=www" }, "devDependencies": { "esbuild": "^0.19.5" diff --git a/src/TGAImage.ts b/src/TGAImage.ts deleted file mode 100644 index 6c0a8ec..0000000 --- a/src/TGAImage.ts +++ /dev/null @@ -1,636 +0,0 @@ -import { ImageType, AttributesType } from './types'; -import { ImageStats } from './ImageStats'; - -export default class TGAImage { - private static GRID_SIZE = 30; - - #arrayBuffer: ArrayBuffer; - private dataView: DataView; - private bytes: Uint8Array; - - private imageDataBytes: Uint8Array; - - stats: ImageStats; - - get arrayBuffer() { - return this.#arrayBuffer; - } - - set arrayBuffer(arrayBuffer: ArrayBuffer) { - this.#arrayBuffer = arrayBuffer; - this.dataView = new DataView(arrayBuffer); - this.bytes = new Uint8Array(arrayBuffer); - } - - constructor(arrayBuffer: ArrayBuffer) { - this.arrayBuffer = arrayBuffer; - this.stats = new ImageStats(arrayBuffer); - - if (this.stats.rleEncoded) { - this.imageDataBytes = this.bytes.subarray( - this.stats.imageDataFieldOffset, - this.stats.getFooterOffset(), - ); - } else { - this.imageDataBytes = this.bytes.subarray(this.stats.imageDataFieldOffset); - } - } - - private drawUncompressedGrayscale(imageData: ImageData) { - console.time('uncompressed grayscale loop'); - const { imageHeight, imageWidth, topToBottom, pixelSize } = this.stats; - const { data } = imageData; - const { imageDataBytes } = this; - data.fill(255); - - for (let y = 0; y < imageHeight; ++y) { - for (let x = 0; x < imageWidth; ++x) { - switch (pixelSize) { - case 1: { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - const byteOffset = x + y * imageWidth; - data[canvasOffset] = imageDataBytes[byteOffset]; - data[canvasOffset + 1] = imageDataBytes[byteOffset]; - data[canvasOffset + 2] = imageDataBytes[byteOffset]; - break; - } - - case 4: { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - const byteOffset = x + y * imageWidth; - data[canvasOffset] = imageDataBytes[byteOffset]; - data[canvasOffset + 1] = imageDataBytes[byteOffset]; - data[canvasOffset + 2] = imageDataBytes[byteOffset]; - break; - } - - default: { - alert('Unsupported pixel size'); - return; - } - } - } - } - - console.timeEnd('uncompressed grayscale loop'); - } - - private drawUncompressed(imageData: ImageData) { - console.time('uncompressed loop'); - const { imageHeight, imageWidth, pixelSize, topToBottom, attributesType, imageType } = this.stats; - const { data } = imageData; - const { imageDataBytes } = this; - const ab = new ArrayBuffer(2); - const ua = new Uint8Array(ab); - const dv = new DataView(ab); - let hasAlpha = true; - - if ( - attributesType && - attributesType !== AttributesType.USEFUL_ALPHA_CHANNEL && - attributesType !== AttributesType.PREMULTIPLIED_ALPHA - ) { - hasAlpha = false; - } - - for (let y = 0; y < imageHeight; ++y) { - for (let x = 0; x < imageWidth; ++x) { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - data[canvasOffset + 3] = 255; - - switch (pixelSize) { - case 2: { - const byteOffset = y * imageWidth * 2 + x * 2; - - if (imageType === ImageType.GRAY_SCALE) { - data[canvasOffset + 3] = imageDataBytes[byteOffset + 1]; - } else { - ua[0] = imageDataBytes[byteOffset]; - ua[1] = imageDataBytes[byteOffset + 1]; - - const byteValue = dv.getUint16(0, true); - // convert 5 bits to 8 bits - data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); - data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); - data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); - } - - break; - } - - case 3: { - const byteOffset = y * imageWidth * 3 + x * 3; - data[canvasOffset] = imageDataBytes[byteOffset + 2]; - data[canvasOffset + 1] = imageDataBytes[byteOffset + 1]; - data[canvasOffset + 2] = imageDataBytes[byteOffset]; - break; - } - - case 4: { - const byteOffset = y * imageWidth * 4 + x * 4; - data[canvasOffset] = imageDataBytes[byteOffset + 2]; - data[canvasOffset + 1] = imageDataBytes[byteOffset + 1]; - data[canvasOffset + 2] = imageDataBytes[byteOffset]; - - if (hasAlpha) { - data[canvasOffset + 3] = imageDataBytes[byteOffset + 3]; - } - - break; - } - } - } - } - - console.timeEnd('uncompressed loop'); - } - - private drawRunLengthEncoded(imageData: ImageData) { - console.time('run length encoded loop'); - const { imageHeight, imageWidth, pixelSize, topToBottom, attributesType, imageType } = this.stats; - const { data } = imageData; - const { imageDataBytes } = this; - const readArrayLength = imageDataBytes.length; - const ab = new ArrayBuffer(2); - const ua = new Uint8Array(ab); - const dv = new DataView(ab); - let hasAlpha = true; - let readCursor = 0; - let x = 0; - let y = 0; - let byte1; - let byte2; - let byte3; - let byte4; - - if ( - attributesType && - attributesType !== AttributesType.USEFUL_ALPHA_CHANNEL && - attributesType !== AttributesType.PREMULTIPLIED_ALPHA - ) { - hasAlpha = false; - } - - for (let i = 0; i < readArrayLength; ++i) { - const packet = imageDataBytes[readCursor++]; - - // RLE packet - if (packet >= 128) { - const repetition = packet - 128; - - switch (pixelSize) { - case 1: - byte1 = imageDataBytes[readCursor++]; - break; - - case 2: - byte1 = imageDataBytes[readCursor++]; - byte2 = imageDataBytes[readCursor++]; - break; - - case 3: - byte1 = imageDataBytes[readCursor++]; - byte2 = imageDataBytes[readCursor++]; - byte3 = imageDataBytes[readCursor++]; - break; - - case 4: - byte1 = imageDataBytes[readCursor++]; - byte2 = imageDataBytes[readCursor++]; - byte3 = imageDataBytes[readCursor++]; - byte4 = imageDataBytes[readCursor++]; - break; - } - - for (let i = 0; i <= repetition; ++i) { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - data[canvasOffset + 3] = 255; - - switch (pixelSize) { - case 1: { - data[canvasOffset] = byte1; - data[canvasOffset + 1] = byte1; - data[canvasOffset + 2] = byte1; - break; - } - - case 2: { - if (imageType === ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE) { - data[canvasOffset + 3] = byte2; - } else { - ua[0] = byte1; - ua[1] = byte2; - const byteValue = dv.getUint16(0, true); - // convert 5 bits to 8 bits - data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); - data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); - data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); - } - break; - } - - case 3: { - data[canvasOffset] = byte3; - data[canvasOffset + 1] = byte2; - data[canvasOffset + 2] = byte1; - break; - } - - case 4: { - data[canvasOffset] = byte3; - data[canvasOffset + 1] = byte2; - data[canvasOffset + 2] = byte1; - - if (hasAlpha) { - data[canvasOffset + 3] = byte4; - } - - break; - } - } - - if (x === imageWidth - 1) { - x = 0; - y += 1; - } else { - x += 1; - } - } - } else { - // raw packet - const repetition = packet; - - for (let i = 0; i <= repetition; ++i) { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - data[canvasOffset + 3] = 255; - - switch (pixelSize) { - case 1: { - data[canvasOffset] = imageDataBytes[readCursor]; - data[canvasOffset + 1] = imageDataBytes[readCursor]; - data[canvasOffset + 2] = imageDataBytes[readCursor]; - readCursor += 1; - break; - } - - case 2: { - if (imageType === ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE) { - readCursor += 1; - data[canvasOffset + 3] = imageDataBytes[readCursor++]; - } else { - ua[0] = imageDataBytes[readCursor++]; - ua[1] = imageDataBytes[readCursor++]; - const byteValue = dv.getUint16(0, true); - // convert 5 bits to 8 bits - data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); - data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); - data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); - } - break; - } - - case 3: { - data[canvasOffset] = imageDataBytes[readCursor + 2]; - data[canvasOffset + 1] = imageDataBytes[readCursor + 1]; - data[canvasOffset + 2] = imageDataBytes[readCursor]; - readCursor += 3; - break; - } - - case 4: { - data[canvasOffset] = imageDataBytes[readCursor + 2]; - data[canvasOffset + 1] = imageDataBytes[readCursor + 1]; - data[canvasOffset + 2] = imageDataBytes[readCursor]; - - if (hasAlpha) { - data[canvasOffset + 3] = imageDataBytes[readCursor + 3]; - } - - readCursor += 4; - break; - } - } - - if (x === imageWidth - 1) { - x = 0; - y += 1; - } else { - x += 1; - } - } - } - } - console.timeEnd('run length encoded loop'); - } - - private drawColorMapped(imageData: ImageData) { - console.time('color mapped loop'); - const { - imageHeight, - imageWidth, - pixelSize, - topToBottom, - colorMapPixelSize, - colorMapOrigin, - imageIdentificationFieldLength, - imageDataFieldOffset, - } = this.stats; - const { data } = imageData; - const { imageDataBytes, bytes, dataView } = this; - const padding = 18 + imageIdentificationFieldLength + colorMapOrigin; - - for (let y = 0; y < imageHeight; ++y) { - for (let x = 0; x < imageWidth; ++x) { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - data[canvasOffset + 3] = 255; - - const byteOffset = y * imageWidth * pixelSize + x * pixelSize; - const colorMapEntryOffset = - padding + - colorMapPixelSize * - (pixelSize === 1 - ? imageDataBytes[byteOffset] - : dataView.getUint16(imageDataFieldOffset + byteOffset, true)); - - switch (colorMapPixelSize) { - case 1: { - data[canvasOffset] = bytes[colorMapEntryOffset]; - data[canvasOffset + 1] = bytes[colorMapEntryOffset]; - data[canvasOffset + 2] = bytes[colorMapEntryOffset]; - break; - } - - case 2: { - const byteValue = dataView.getUint16(colorMapEntryOffset, true); - // convert 5 bits to 8 bits - data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); - data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); - data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); - break; - } - - case 3: { - data[canvasOffset] = bytes[colorMapEntryOffset + 2]; - data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; - data[canvasOffset + 2] = bytes[colorMapEntryOffset]; - break; - } - - case 4: { - data[canvasOffset] = bytes[colorMapEntryOffset + 2]; - data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; - data[canvasOffset + 2] = bytes[colorMapEntryOffset]; - data[canvasOffset + 3] = bytes[colorMapEntryOffset + 3]; - break; - } - } - } - } - - console.timeEnd('color mapped loop'); - } - - private drawRunLengthEncodedColorMapped(imageData: ImageData) { - console.time('run length encoded color mapped loop'); - const { imageHeight, imageWidth, pixelSize, topToBottom, imageIdentificationFieldLength, colorMapOrigin, imageDataFieldOffset, colorMapPixelSize } = this.stats; - const { data } = imageData; - const { imageDataBytes, bytes, dataView } = this; - const readArrayLength = imageDataBytes.length; - const padding = 18 + imageIdentificationFieldLength + colorMapOrigin; - let readCursor = 0; - let x = 0; - let y = 0; - let byte1 = 0; - let byte2 = 0; - let byte3 = 0; - let byte4 = 0; - let colorMapEntryOffset: number = 0; - - for (let i = 0; i < readArrayLength; ++i) { - const packet = imageDataBytes[readCursor++]; - - // RLE packet - if (packet >= 128) { - if (pixelSize === 1) { - colorMapEntryOffset = padding + colorMapPixelSize * imageDataBytes[readCursor++]; - } else { - colorMapEntryOffset = padding + colorMapPixelSize * dataView.getUint16(imageDataFieldOffset + readCursor, true); - readCursor += 2; - } - - const repetition = packet - 128; - byte1 = bytes[colorMapEntryOffset]; - - if (colorMapPixelSize > 2) { - byte2 = bytes[colorMapEntryOffset + 1]; - byte3 = bytes[colorMapEntryOffset + 2]; - } - - if (colorMapPixelSize > 3) { - byte4 = bytes[colorMapEntryOffset + 3]; - } - - for (let i = 0; i <= repetition; ++i) { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - data[canvasOffset + 3] = 255; - - switch (colorMapPixelSize) { - case 1: { - data[canvasOffset] = byte1; - data[canvasOffset + 1] = byte1; - data[canvasOffset + 2] = byte1; - break; - } - - case 2: { - const byteValue = dataView.getUint16(colorMapEntryOffset, true); - // convert 5 bits to 8 bits - data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); - data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); - data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); - break; - } - - case 3: { - data[canvasOffset] = byte3; - data[canvasOffset + 1] = byte2; - data[canvasOffset + 2] = byte1; - break; - } - - case 4: { - data[canvasOffset] = byte3; - data[canvasOffset + 1] = byte2; - data[canvasOffset + 2] = byte1; - data[canvasOffset + 3] = byte4; - break; - } - } - - if (x === imageWidth - 1) { - x = 0; - y += 1; - } else { - x += 1; - } - } - } else { - // raw packet - const repetition = packet; - - for (let i = 0; i <= repetition; ++i) { - const canvasOffset = topToBottom - ? y * imageWidth * 4 + x * 4 - : (imageHeight - y - 1) * imageWidth * 4 + x * 4; - - if (pixelSize === 1) { - colorMapEntryOffset = padding + colorMapPixelSize * imageDataBytes[readCursor++]; - } else { - colorMapEntryOffset = padding + colorMapPixelSize * dataView.getUint16(imageDataFieldOffset + readCursor, true); - readCursor += 2; - } - - data[canvasOffset + 3] = 255; - - switch (colorMapPixelSize) { - case 1: { - data[canvasOffset] = bytes[colorMapEntryOffset]; - data[canvasOffset + 1] = bytes[colorMapEntryOffset]; - data[canvasOffset + 2] = bytes[colorMapEntryOffset]; - break; - } - - case 2: { - const byteValue = dataView.getUint16(colorMapEntryOffset, true); - // convert 5 bits to 8 bits - data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); - data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); - data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); - break; - } - - case 3: { - data[canvasOffset] = bytes[colorMapEntryOffset + 2]; - data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; - data[canvasOffset + 2] = bytes[colorMapEntryOffset]; - break; - } - - case 4: { - data[canvasOffset] = bytes[colorMapEntryOffset + 2]; - data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; - data[canvasOffset + 2] = bytes[colorMapEntryOffset]; - data[canvasOffset + 3] = bytes[colorMapEntryOffset + 3]; - break; - } - } - - if (x === imageWidth - 1) { - x = 0; - y += 1; - } else { - x += 1; - } - } - } - } - - console.timeEnd('run length encoded color mapped loop'); - } - - async draw(canvas: HTMLCanvasElement) { - console.time('draw'); - const context = canvas.getContext('2d'); - - if (!context) { - alert('Failed to get canvas context'); - return; - } - - const begin = performance.now(); - context.clearRect(0, 0, canvas.width, canvas.height); - canvas.width = this.stats.imageWidth; - canvas.height = this.stats.imageHeight; - context.fillStyle = 'rgba(40, 40, 40, 255)'; - context.fillRect(0, 0, canvas.width, canvas.height); - - const imageData = context.createImageData(this.stats.imageWidth, this.stats.imageHeight); - - if (this.stats.rleEncoded) { - if (this.stats.imageType === ImageType.RUN_LENGTH_ENCODED_COLOR_MAPPED) { - this.drawRunLengthEncodedColorMapped(imageData); - } else { - this.drawRunLengthEncoded(imageData); - } - } else { - if (this.stats.imageType === ImageType.COLOR_MAPPED) { - this.drawColorMapped(imageData); - } else { - if (this.stats.pixelSize === 1) { - this.drawUncompressedGrayscale(imageData); - } else { - this.drawUncompressed(imageData); - } - } - } - - const hasTransparency = this.stats.pixelSize === 4 - || this.stats.colorMapPixelSize === 4 - || ( - this.stats.pixelSize === 2 - && ( - this.stats.imageType === ImageType.GRAY_SCALE - || this.stats.imageType === ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE - ) - ); - - if (hasTransparency) { - const { GRID_SIZE } = TGAImage; - const { imageWidth, imageHeight } = this.stats; - let evenRow = 0; - - for (let y = 0; y < imageHeight; y += GRID_SIZE) { - let evenColumn = 0; - - for (let x = 0; x < imageWidth; x += GRID_SIZE) { - context.fillStyle = evenRow ^ evenColumn ? 'rgba(180, 180, 180, 1)' : 'rgba(100, 100, 100, 1)'; - context.fillRect(x, y, GRID_SIZE, GRID_SIZE); - evenColumn = evenColumn === 1 ? 0 : 1; - } - - evenRow = evenRow === 1 ? 0 : 1; - } - - const bitmap = await createImageBitmap(imageData, { premultiplyAlpha: 'premultiply' }); - context.drawImage(bitmap, 0, 0); - bitmap.close(); - } else { - context.putImageData(imageData, 0, 0); - } - - this.stats.duration = performance.now() - begin; - console.info(this.stats.duration); - console.timeEnd('draw'); - } -} diff --git a/src/index.ts b/src/index.ts index 055de9b..1bc32ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,56 +1,108 @@ -import { readFile, generateImageInformationTable } from './utils'; -import TGAImage from './TGAImage'; +import { ImageType } from './lib/types'; +import TGAFile from './lib/TGAFile'; +import ImageFileInfo from './lib/ImageFileInfo'; +import drawColorMapped from './lib/draw-methods/drawColorMapped'; +import drawRunLengthEncoded from './lib/draw-methods/drawRunLengthEncoded'; +import drawRunLengthEncodedColorMapped from './lib/draw-methods/drawRunLengthEncodedColorMapped'; +import drawUncompressed from './lib/draw-methods/drawUncompressed'; +import drawUncompressedGrayscale from './lib/draw-methods/drawUncompressedGrayscale'; -// Esbuild Live Reload -new EventSource('/esbuild').addEventListener('change', () => location.reload()); +function drawTransparencyGrid(params: { context: CanvasRenderingContext2D, imageWidth: number, imageHeight: number, gridSize: number }) { + const { context, imageWidth, imageHeight, gridSize } = params; + let evenRow = 0; -const fileInput = document.querySelector('input[type=file]') as HTMLInputElement; -const canvas = document.querySelector('canvas') as HTMLCanvasElement; -const table = document.querySelector('table') as HTMLTableElement; -const template = document.querySelector('#row') as HTMLTemplateElement; + for (let y = 0; y < imageHeight; y += gridSize) { + let evenColumn = 0; -function populateStatsTable(tga: TGAImage) { - table.innerHTML = ''; + for (let x = 0; x < imageWidth; x += gridSize) { + context.fillStyle = evenRow ^ evenColumn ? 'rgba(180, 180, 180, 1)' : 'rgba(100, 100, 100, 1)'; + context.fillRect(x, y, gridSize, gridSize); + evenColumn = evenColumn === 1 ? 0 : 1; + } + + evenRow = evenRow === 1 ? 0 : 1; + } +} - const rows = generateImageInformationTable(tga); +function resetCanvas(context: CanvasRenderingContext2D, imageWidth: number, imageHeight: number) { + context.resetTransform(); + context.clearRect(0, 0, context.canvas.width, context.canvas.height); + context.canvas.width = imageWidth; + context.canvas.height = imageHeight; + context.fillStyle = 'rgba(40, 40, 40, 255)'; + context.fillRect(0, 0, context.canvas.width, context.canvas.height); +} - for (const [key, value] of Object.entries(rows)) { - const clone = template.content.cloneNode(true) as HTMLElement; - const tds = clone.querySelectorAll('td'); +function decodeTGA(tgaFile: TGAFile, context: CanvasRenderingContext2D): ImageData { + const imageData = context.createImageData(tgaFile.fileInfo.imageWidth, tgaFile.fileInfo.imageHeight); + imageData.data.fill(255); - tds[0].innerText = key; - tds[1].innerText = value; - table.appendChild(clone); + if (tgaFile.fileInfo.rleEncoded) { + if (tgaFile.fileInfo.imageType === ImageType.RUN_LENGTH_ENCODED_COLOR_MAPPED) { + drawRunLengthEncodedColorMapped(imageData, tgaFile); + } else { + drawRunLengthEncoded(imageData, tgaFile); + } + } else { + if (tgaFile.fileInfo.imageType === ImageType.COLOR_MAPPED) { + drawColorMapped(imageData, tgaFile); + } else { + if (tgaFile.fileInfo.pixelSize === 1) { + drawUncompressedGrayscale(imageData, tgaFile); + } else { + drawUncompressed(imageData, tgaFile); + } + } } - console.table(rows); + return imageData; +} + +function flipCanvasVertically(context: CanvasRenderingContext2D) { + context.translate(0, context.canvas.height); + context.scale(1, -1); } -async function drawToCanvas() { - try { - const { files } = fileInput; +export function drawToCanvas(canvas: HTMLCanvasElement, arrayBuffer: ArrayBuffer): Promise<{ duration: number, fileInfo: ImageFileInfo }> { + const context = canvas.getContext('2d'); - if (!files?.length) { - return; - } + if (!context) { + alert('Failed to get canvas context'); + return Promise.reject(new Error('Failed to get canvas context')); + } + + const start = performance.now(); - const file = files.item(0); + // read the TGA file, get image width, height and other metadata + const tgaFile = new TGAFile(arrayBuffer); - if (!file) return; + // reset the canvas and set it to correct size + resetCanvas(context, tgaFile.fileInfo.imageWidth, tgaFile.fileInfo.imageHeight); - const arrayBuffer = await readFile(file); - const tga = new TGAImage(arrayBuffer); + // decode the TGA and get an ImageData object to draw to the canvas + const imageData = decodeTGA(tgaFile, context); - tga.draw(canvas) - .then(() => { - populateStatsTable(tga); - }) - .catch(console.trace); - } catch (ex) { - alert(ex.message); + // if the image has transparency, draw a grid to show it + if (tgaFile.fileInfo.hasTransparency) { + const gridSize = Math.floor(Math.min(tgaFile.fileInfo.imageWidth / 5, 30)); + drawTransparencyGrid({ + context, + gridSize, + imageWidth: tgaFile.fileInfo.imageWidth, + imageHeight: tgaFile.fileInfo.imageHeight, + }); } -} -fileInput.addEventListener('change', () => { - drawToCanvas(); -}); + return createImageBitmap(imageData, { premultiplyAlpha: tgaFile.fileInfo.hasTransparency ? 'premultiply' : 'none' }) + .then((bitmap) => { + if (!tgaFile.fileInfo.topToBottom) { + flipCanvasVertically(context); + } + + context.drawImage(bitmap, 0, 0); + bitmap.close(); + + const end = performance.now(); + return { duration: end - start, fileInfo: tgaFile.fileInfo }; + }); +} diff --git a/src/ImageStats.ts b/src/lib/ImageFileInfo.ts similarity index 88% rename from src/ImageStats.ts rename to src/lib/ImageFileInfo.ts index febf3ec..ab00fe7 100644 --- a/src/ImageStats.ts +++ b/src/lib/ImageFileInfo.ts @@ -1,11 +1,12 @@ import { ImageType, ImageDescriptorFields, AttributesType } from './types'; -export class ImageStats { +export default class ImageFileInfo { #arrayBuffer: ArrayBuffer; private dataView: DataView; private bytes: Uint8Array; rleEncoded: boolean = false; + hasTransparency: boolean = false; colorMapType: number; imageType: ImageType; @@ -24,7 +25,6 @@ export class ImageStats { extensionOffset: number = 0; version: 1 | 2; topToBottom: boolean; - duration: number = 0; authorName: string|undefined; authorComments: string|undefined; @@ -36,11 +36,12 @@ export class ImageStats { keyColor: { red: number, green: number, blue: number, alpha: number }|undefined; aspectRatio: string|undefined; gammaValue: string|undefined; - colorCorrectionOffset: number = 0; - postageStampOffset: number = 0; + colorCorrectionOffset: number; + postageStampOffset: number; scanLineOffset: number; attributesType: AttributesType|undefined; + get arrayBuffer() { return this.#arrayBuffer; } @@ -86,6 +87,16 @@ export class ImageStats { ) { this.rleEncoded = true; } + + this.hasTransparency = this.pixelSize === 4 + || this.colorMapPixelSize === 4 + || ( + this.pixelSize === 2 + && ( + this.imageType === ImageType.GRAY_SCALE + || this.imageType === ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE + ) + ); } private getImageDataFieldOffset(): number { @@ -160,8 +171,16 @@ export class ImageStats { this.authorName = readString(EO + 1, EO + 42); this.authorComments = readString(EO + 42, EO + 366); this.jobId = readString(EO + 379, EO + 419); + this.softwareId = readString(EO + 426, EO + 466); - this.softwareVersion = readString(EO + 467, EO + 469); + + const softwareVersion = this.dataView.getUint16(EO + 467, true); + const softwareVersionLetter = String.fromCharCode(this.dataView.getUint8(EO + 469)); + const softwareVersionUnused = softwareVersion === 0 && softwareVersionLetter === ' '; + + if (!softwareVersionUnused) { + this.softwareVersion = `${(softwareVersion / 100).toFixed(2)}${softwareVersionLetter}`; + } const [month, day, year, hour, minute, second] = readShorts(EO + 367, 6); diff --git a/src/lib/TGAFile.ts b/src/lib/TGAFile.ts new file mode 100644 index 0000000..3bd4eb1 --- /dev/null +++ b/src/lib/TGAFile.ts @@ -0,0 +1,35 @@ +import ImageFileInfo from './ImageFileInfo'; + +export default class TGAFile { + #arrayBuffer: ArrayBuffer; + bytes: Uint8Array; + dataView: DataView; + + imageDataBytes: Uint8Array; + + fileInfo: ImageFileInfo; + + get arrayBuffer() { + return this.#arrayBuffer; + } + + set arrayBuffer(arrayBuffer: ArrayBuffer) { + this.#arrayBuffer = arrayBuffer; + this.dataView = new DataView(arrayBuffer); + this.bytes = new Uint8Array(arrayBuffer); + } + + constructor(arrayBuffer: ArrayBuffer) { + this.arrayBuffer = arrayBuffer; + this.fileInfo = new ImageFileInfo(arrayBuffer); + + if (this.fileInfo.rleEncoded) { + this.imageDataBytes = this.bytes.subarray( + this.fileInfo.imageDataFieldOffset, + this.fileInfo.getFooterOffset(), + ); + } else { + this.imageDataBytes = this.bytes.subarray(this.fileInfo.imageDataFieldOffset); + } + } +} diff --git a/src/lib/draw-methods/drawColorMapped.ts b/src/lib/draw-methods/drawColorMapped.ts new file mode 100644 index 0000000..3aedda7 --- /dev/null +++ b/src/lib/draw-methods/drawColorMapped.ts @@ -0,0 +1,73 @@ +import TGAFile from '../TGAFile'; +import { ImageType } from '../types'; + +export default function drawColorMapped(imageData: ImageData, tgaFile: TGAFile) { + const { + imageHeight, + imageWidth, + pixelSize, + colorMapPixelSize, + colorMapOrigin, + imageIdentificationFieldLength, + imageDataFieldOffset, + imageType + } = tgaFile.fileInfo; + const { data } = imageData; + const { imageDataBytes, bytes, dataView } = tgaFile; + const padding = 18 + imageIdentificationFieldLength + colorMapOrigin; + let canvasOffset = 0; + let byteOffset = 0; + + for (let y = 0; y < imageHeight; ++y) { + for (let x = 0; x < imageWidth; ++x) { + const colorMapEntryOffset = + padding + + colorMapPixelSize * + (pixelSize === 1 + ? imageDataBytes[byteOffset] + : dataView.getUint16(imageDataFieldOffset + byteOffset, true)); + + switch (colorMapPixelSize) { + case 1: { + data[canvasOffset] = bytes[colorMapEntryOffset]; + data[canvasOffset + 1] = bytes[colorMapEntryOffset]; + data[canvasOffset + 2] = bytes[colorMapEntryOffset]; + break; + } + + case 2: { + if (imageType === ImageType.GRAY_SCALE) { + data[canvasOffset + 3] = imageDataBytes[colorMapEntryOffset + 1]; + } else { + const byteValue = dataView.getUint16(colorMapEntryOffset, true); + + // convert 5 bits to 8 bits + data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); + data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); + data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); + } + + break; + } + + case 3: { + data[canvasOffset] = bytes[colorMapEntryOffset + 2]; + data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; + data[canvasOffset + 2] = bytes[colorMapEntryOffset]; + break; + } + + case 4: { + data[canvasOffset] = bytes[colorMapEntryOffset + 2]; + data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; + data[canvasOffset + 2] = bytes[colorMapEntryOffset]; + data[canvasOffset + 3] = bytes[colorMapEntryOffset + 3]; + break; + } + } + + canvasOffset += 4; + byteOffset += pixelSize; + } + } +} diff --git a/src/lib/draw-methods/drawRunLengthEncoded.ts b/src/lib/draw-methods/drawRunLengthEncoded.ts new file mode 100644 index 0000000..3041f31 --- /dev/null +++ b/src/lib/draw-methods/drawRunLengthEncoded.ts @@ -0,0 +1,168 @@ +import { ImageType, AttributesType } from '../types'; +import TGAFile from '../TGAFile'; + +export default function drawRunLengthEncoded(imageData: ImageData, tgaFile: TGAFile) { + const { pixelSize, attributesType, imageType } = tgaFile.fileInfo; + const { data } = imageData; + const { imageDataBytes } = tgaFile; + const readArrayLength = imageDataBytes.length; + const ab = new ArrayBuffer(2); + const ua = new Uint8Array(ab); + const dv = new DataView(ab); + let canvasOffset = 0; + let hasAlpha = true; + let readCursor = 0; + let byte1; + let byte2; + let byte3; + let byte4; + + if ( + attributesType && + attributesType !== AttributesType.USEFUL_ALPHA_CHANNEL && + attributesType !== AttributesType.PREMULTIPLIED_ALPHA + ) { + hasAlpha = false; + } + + for (let i = 0; i < readArrayLength; ++i) { + const packet = imageDataBytes[readCursor++]; + const isRLEPacket = packet >= 128; + const repetition = isRLEPacket ? packet - 128 : packet; + + if (isRLEPacket) { + switch (pixelSize) { + case 1: + byte1 = imageDataBytes[readCursor++]; + break; + + case 2: + byte1 = imageDataBytes[readCursor++]; + byte2 = imageDataBytes[readCursor++]; + + if (imageType !== ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE) { + ua[0] = byte1; + ua[1] = byte2; + const byteValue = dv.getUint16(0, true); + // convert 5 bits to 8 bits + byte3 = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); + byte2 = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); + byte1 = Math.round((byteValue & 0x001F) / 31 * 255); + } + + break; + + case 3: + byte1 = imageDataBytes[readCursor++]; + byte2 = imageDataBytes[readCursor++]; + byte3 = imageDataBytes[readCursor++]; + break; + + case 4: + byte1 = imageDataBytes[readCursor++]; + byte2 = imageDataBytes[readCursor++]; + byte3 = imageDataBytes[readCursor++]; + byte4 = imageDataBytes[readCursor++]; + break; + } + + for (let j = 0; j <= repetition; ++j) { + switch (pixelSize) { + case 1: + data[canvasOffset] = byte1; + data[canvasOffset + 1] = byte1; + data[canvasOffset + 2] = byte1; + break; + + case 2: + if (imageType === ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE) { + data[canvasOffset] = 0; + data[canvasOffset + 1] = 0; + data[canvasOffset + 2] = 0; + data[canvasOffset + 3] = byte2; + } else { + data[canvasOffset] = byte3; + data[canvasOffset + 1] = byte2; + data[canvasOffset + 2] = byte1; + } + break; + + case 3: + data[canvasOffset] = byte3; + data[canvasOffset + 1] = byte2; + data[canvasOffset + 2] = byte1; + break; + + case 4: + data[canvasOffset] = byte3; + data[canvasOffset + 1] = byte2; + data[canvasOffset + 2] = byte1; + + if (hasAlpha) { + data[canvasOffset + 3] = byte4; + } + + break; + } + + canvasOffset += 4; + } + + continue; + } + + for (let j = 0; j <= repetition; ++j) { + switch (pixelSize) { + case 1: { + data[canvasOffset] = imageDataBytes[readCursor]; + data[canvasOffset + 1] = imageDataBytes[readCursor]; + data[canvasOffset + 2] = imageDataBytes[readCursor]; + readCursor += 1; + break; + } + + case 2: { + if (imageType === ImageType.RUN_LENGTH_ENCODED_GRAY_SCALE) { + readCursor += 1; + data[canvasOffset] = 0; + data[canvasOffset + 1] = 0; + data[canvasOffset + 2] = 0; + data[canvasOffset + 3] = imageDataBytes[readCursor++]; + } else { + ua[0] = imageDataBytes[readCursor++]; + ua[1] = imageDataBytes[readCursor++]; + const byteValue = dv.getUint16(0, true); + // convert 5 bits to 8 bits + data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); + data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); + data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); + } + break; + } + + case 3: { + data[canvasOffset] = imageDataBytes[readCursor + 2]; + data[canvasOffset + 1] = imageDataBytes[readCursor + 1]; + data[canvasOffset + 2] = imageDataBytes[readCursor]; + readCursor += 3; + break; + } + + case 4: { + data[canvasOffset] = imageDataBytes[readCursor + 2]; + data[canvasOffset + 1] = imageDataBytes[readCursor + 1]; + data[canvasOffset + 2] = imageDataBytes[readCursor]; + + if (hasAlpha) { + data[canvasOffset + 3] = imageDataBytes[readCursor + 3]; + } + + readCursor += 4; + break; + } + } + + canvasOffset += 4; + } + } +} diff --git a/src/lib/draw-methods/drawRunLengthEncodedColorMapped.ts b/src/lib/draw-methods/drawRunLengthEncodedColorMapped.ts new file mode 100644 index 0000000..1484985 --- /dev/null +++ b/src/lib/draw-methods/drawRunLengthEncodedColorMapped.ts @@ -0,0 +1,142 @@ +import TGAFile from "../TGAFile"; +import { ImageType } from "../types"; + +export default function drawRunLengthEncodedColorMapped(imageData: ImageData, tgaFile: TGAFile) { + const { pixelSize, imageIdentificationFieldLength, colorMapOrigin, imageDataFieldOffset, colorMapPixelSize, imageType } = tgaFile.fileInfo; + const { data } = imageData; + const { imageDataBytes, bytes, dataView } = tgaFile; + const readArrayLength = imageDataBytes.length; + const padding = 18 + imageIdentificationFieldLength + colorMapOrigin; + let canvasOffset = 0; + let readCursor = 0; + let byte1 = 0; + let byte2 = 0; + let byte3 = 0; + let byte4 = 0; + let colorMapEntryOffset: number = 0; + + for (let i = 0; i < readArrayLength; ++i) { + const packet = imageDataBytes[readCursor++]; + + // RLE packet + if (packet >= 128) { + if (pixelSize === 1) { + colorMapEntryOffset = padding + colorMapPixelSize * imageDataBytes[readCursor++]; + } else { + colorMapEntryOffset = padding + colorMapPixelSize * dataView.getUint16(imageDataFieldOffset + readCursor, true); + readCursor += 2; + } + + const repetition = packet - 128; + byte1 = bytes[colorMapEntryOffset]; + + if (colorMapPixelSize > 2) { + byte2 = bytes[colorMapEntryOffset + 1]; + byte3 = bytes[colorMapEntryOffset + 2]; + } + + if (colorMapPixelSize > 3) { + byte4 = bytes[colorMapEntryOffset + 3]; + } + + if (colorMapPixelSize === 2) { + if (imageType === ImageType.GRAY_SCALE) { + byte4 = imageDataBytes[colorMapEntryOffset + 1]; + } else { + const byteValue = dataView.getUint16(colorMapEntryOffset, true); + // convert 5 bits to 8 bits + byte3 = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); + byte2 = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); + byte1 = Math.round((byteValue & 0x001F) / 31 * 255); + } + } + + for (let i = 0; i <= repetition; ++i) { + switch (colorMapPixelSize) { + case 1: { + data[canvasOffset] = byte1; + data[canvasOffset + 1] = byte1; + data[canvasOffset + 2] = byte1; + break; + } + + case 2: { + if (imageType === ImageType.GRAY_SCALE) { + data[canvasOffset + 3] = byte4; + } else { + data[canvasOffset] = byte3; + data[canvasOffset + 1] = byte2; + data[canvasOffset + 2] = byte1; + } + + break; + } + + case 3: { + data[canvasOffset] = byte3; + data[canvasOffset + 1] = byte2; + data[canvasOffset + 2] = byte1; + break; + } + + case 4: { + data[canvasOffset] = byte3; + data[canvasOffset + 1] = byte2; + data[canvasOffset + 2] = byte1; + data[canvasOffset + 3] = byte4; + break; + } + } + + canvasOffset += 4; + } + } else { + // raw packet + const repetition = packet; + + for (let i = 0; i <= repetition; ++i) { + if (pixelSize === 1) { + colorMapEntryOffset = padding + colorMapPixelSize * imageDataBytes[readCursor++]; + } else { + colorMapEntryOffset = padding + colorMapPixelSize * dataView.getUint16(imageDataFieldOffset + readCursor, true); + readCursor += 2; + } + + switch (colorMapPixelSize) { + case 1: { + data[canvasOffset] = bytes[colorMapEntryOffset]; + data[canvasOffset + 1] = bytes[colorMapEntryOffset]; + data[canvasOffset + 2] = bytes[colorMapEntryOffset]; + break; + } + + case 2: { + const byteValue = dataView.getUint16(colorMapEntryOffset, true); + // convert 5 bits to 8 bits + data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); + data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); + data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); + break; + } + + case 3: { + data[canvasOffset] = bytes[colorMapEntryOffset + 2]; + data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; + data[canvasOffset + 2] = bytes[colorMapEntryOffset]; + break; + } + + case 4: { + data[canvasOffset] = bytes[colorMapEntryOffset + 2]; + data[canvasOffset + 1] = bytes[colorMapEntryOffset + 1]; + data[canvasOffset + 2] = bytes[colorMapEntryOffset]; + data[canvasOffset + 3] = bytes[colorMapEntryOffset + 3]; + break; + } + } + + canvasOffset += 4; + } + } + } +} diff --git a/src/lib/draw-methods/drawUncompressed.ts b/src/lib/draw-methods/drawUncompressed.ts new file mode 100644 index 0000000..629cc59 --- /dev/null +++ b/src/lib/draw-methods/drawUncompressed.ts @@ -0,0 +1,68 @@ +import { ImageType, AttributesType } from '../types'; +import TGAFile from '../TGAFile'; + +export default function drawUncompressed(imageData: ImageData, tgaFile: TGAFile) { + const { imageHeight, imageWidth, pixelSize, attributesType, imageType, imageDataFieldOffset } = tgaFile.fileInfo; + const { data } = imageData; + const { imageDataBytes, dataView } = tgaFile; + let byteOffset = 0; + let canvasOffset = 0; + let hasAlpha = true; + + if ( + attributesType && + attributesType !== AttributesType.USEFUL_ALPHA_CHANNEL && + attributesType !== AttributesType.PREMULTIPLIED_ALPHA + ) { + hasAlpha = false; + } + + for (let y = 0; y < imageHeight; ++y) { + for (let x = 0; x < imageWidth; ++x) { + switch (pixelSize) { + // 15-bit RGB + case 2: { + if (imageType === ImageType.GRAY_SCALE) { + data[canvasOffset] = 0; + data[canvasOffset + 1] = 0; + data[canvasOffset + 2] = 0; + data[canvasOffset + 3] = imageDataBytes[byteOffset + 1]; + } else { + const byteValue = dataView.getUint16(imageDataFieldOffset + byteOffset, true); + + // convert 5 bits to 8 bits + data[canvasOffset] = Math.round(((byteValue & 0x7C00) >> 10) / 31 * 255); + data[canvasOffset + 1] = Math.round(((byteValue & 0x03E0) >> 5) / 31 * 255); + data[canvasOffset + 2] = Math.round((byteValue & 0x001F) / 31 * 255); + } + + break; + } + + // 24-bit RGB + case 3: { + data[canvasOffset] = imageDataBytes[byteOffset + 2]; + data[canvasOffset + 1] = imageDataBytes[byteOffset + 1]; + data[canvasOffset + 2] = imageDataBytes[byteOffset]; + break; + } + + // 32-bit RGBA + case 4: { + data[canvasOffset] = imageDataBytes[byteOffset + 2]; + data[canvasOffset + 1] = imageDataBytes[byteOffset + 1]; + data[canvasOffset + 2] = imageDataBytes[byteOffset]; + + if (hasAlpha) { + data[canvasOffset + 3] = imageDataBytes[byteOffset + 3]; + } + + break; + } + } + + byteOffset += pixelSize; + canvasOffset += 4; + } + } +} diff --git a/src/lib/draw-methods/drawUncompressedGrayscale.ts b/src/lib/draw-methods/drawUncompressedGrayscale.ts new file mode 100644 index 0000000..5837268 --- /dev/null +++ b/src/lib/draw-methods/drawUncompressedGrayscale.ts @@ -0,0 +1,37 @@ +import TGAFile from "../TGAFile"; + +export default function drawUncompressedGrayscale(imageData: ImageData, tgaFile: TGAFile) { + const { imageHeight, imageWidth, pixelSize } = tgaFile.fileInfo; + const { data } = imageData; + const { imageDataBytes } = tgaFile; + let canvasOffset = 0; + let byteOffset = 0; + + for (let y = 0; y < imageHeight; ++y) { + for (let x = 0; x < imageWidth; ++x) { + switch (pixelSize) { + case 1: { + data[canvasOffset] = imageDataBytes[byteOffset]; + data[canvasOffset + 1] = imageDataBytes[byteOffset]; + data[canvasOffset + 2] = imageDataBytes[byteOffset]; + break; + } + + case 4: { + data[canvasOffset] = imageDataBytes[byteOffset]; + data[canvasOffset + 1] = imageDataBytes[byteOffset]; + data[canvasOffset + 2] = imageDataBytes[byteOffset]; + break; + } + + default: { + alert('Unsupported pixel size'); + return; + } + } + + canvasOffset += 4; + byteOffset += 1; + } + } +} diff --git a/src/types.ts b/src/lib/types.ts similarity index 100% rename from src/types.ts rename to src/lib/types.ts diff --git a/src/utils.ts b/src/utils.ts index af22f58..f23b453 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ -import { ImageType, AttributesType } from './types'; -import TGAImage from './TGAImage'; +import { ImageType, AttributesType } from './lib/types'; +import ImageFileInfo from './lib/ImageFileInfo'; export function readFile(file: File): Promise { return new Promise((resolve, reject) => { @@ -22,8 +22,8 @@ function capitalize(str: string): string { }); } -function getAttributesType(tga: TGAImage) { - switch (tga.stats.attributesType) { +function getAttributesType(fileInfo: ImageFileInfo) { + switch (fileInfo.attributesType) { case AttributesType.NO_ALPHA_DATA: return 'No alpha'; case AttributesType.UNDEFINED_IGNORED: @@ -39,33 +39,47 @@ function getAttributesType(tga: TGAImage) { } } -export function generateImageInformationTable(tga: TGAImage) { +export function generateImageInformationTable(fileInfo: ImageFileInfo, duration: number) { + const attributesType = getAttributesType(fileInfo); const stats: any = { - version: tga.stats.version, - imageType: capitalize(ImageType[tga.stats.imageType].toLowerCase().replace(/_/g, ' ')), - xOrigin: tga.stats.xOrigin, - yOrigin: tga.stats.yOrigin, - imageWidth: tga.stats.imageWidth, - imageHeight: tga.stats.imageHeight, - pixelSize: tga.stats.pixelSize, - imageDescriptor: tga.stats.imageDescriptor.toString(2).padStart(8, '0'), - imageIdentificationFieldLength: tga.stats.imageIdentificationFieldLength, - topToBottom: tga.stats.isTopToBottom(), - colorMapOrigin: tga.stats.colorMapOrigin, - colorMapLength: tga.stats.colorMapLength, - colorMapPixelSize: tga.stats.colorMapPixelSize, - processingTook: `${tga.stats.duration} ms`, + version: fileInfo.version, + imageType: capitalize(ImageType[fileInfo.imageType].toLowerCase().replace(/_/g, ' ')), + xOrigin: fileInfo.xOrigin, + yOrigin: fileInfo.yOrigin, + imageWidth: fileInfo.imageWidth, + imageHeight: fileInfo.imageHeight, + pixelSize: fileInfo.pixelSize, + imageDescriptor: fileInfo.imageDescriptor.toString(2).padStart(8, '0'), + attributesType, + imageIdentificationFieldLength: fileInfo.imageIdentificationFieldLength, + topToBottom: fileInfo.isTopToBottom(), + colorMapOrigin: fileInfo.colorMapOrigin, + colorMapLength: fileInfo.colorMapLength, + colorMapPixelSize: fileInfo.colorMapPixelSize, + extensionOffset: fileInfo.extensionOffset, + authorName: fileInfo.authorName, + authorComments: fileInfo.authorComments, + dateTimeStamp: fileInfo.dateTimeStamp?.toString(), + jobId: fileInfo.jobId, + jobTime: fileInfo.jobTime, + softwareId: fileInfo.softwareId, + softwareVersion: fileInfo.softwareVersion, + keyColor: fileInfo.keyColor, + aspectRatio: fileInfo.aspectRatio, + gammaValue: fileInfo.gammaValue, + colorCorrectionOffset: fileInfo.colorCorrectionOffset, + postageStampOffset: fileInfo.postageStampOffset, + scanLineOffset: fileInfo.scanLineOffset, + processingTook: `${duration} ms`, }; - const attributesType = getAttributesType(tga); - - if (attributesType) { - stats.attributesType = attributesType; - } - const rows: { [key: string]: string } = {}; for (const [key, value] of Object.entries(stats)) { + if (value === undefined) { + continue; + } + const firsCharacter = key[0]; const field = `${firsCharacter.toUpperCase()}${key .replace(/(?!\b[A-Z])([A-Z])/g, ' $1') @@ -76,6 +90,12 @@ export function generateImageInformationTable(tga: TGAImage) { continue; } + if (key === 'keyColor' && fileInfo.keyColor) { + const { red, green, blue, alpha } = fileInfo.keyColor; + rows[field] = `rgba(${red}, ${green}, ${blue}, ${alpha})`; + continue; + } + rows[field] = value as string; } diff --git a/src/www-index.ts b/src/www-index.ts new file mode 100644 index 0000000..65e120e --- /dev/null +++ b/src/www-index.ts @@ -0,0 +1,52 @@ +import { readFile, generateImageInformationTable } from './utils'; +import { drawToCanvas } from '.'; +import ImageFileInfo from './lib/ImageFileInfo'; + +// Esbuild Live Reload +new EventSource('/esbuild').addEventListener('change', () => location.reload()); + +const fileInput = document.querySelector('input[type=file]') as HTMLInputElement; +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const table = document.querySelector('table') as HTMLTableElement; +const template = document.querySelector('#row') as HTMLTemplateElement; + +function populateStatsTable(fileInfo: ImageFileInfo, duration: number) { + table.innerHTML = ''; + + const rows = generateImageInformationTable(fileInfo, duration); + + for (const [key, value] of Object.entries(rows)) { + const clone = template.content.cloneNode(true) as HTMLElement; + const tds = clone.querySelectorAll('td'); + + tds[0].innerText = key; + tds[1].innerText = value; + table.appendChild(clone); + } + + console.table(rows); +} + +async function readFileAndDrawToCanvas() { + try { + const { files } = fileInput; + + if (!files?.length) { + return; + } + + const file = files.item(0); + + if (!file) return; + + const arrayBuffer = await readFile(file); + const { duration, fileInfo } = await drawToCanvas(canvas, arrayBuffer); + populateStatsTable(fileInfo, duration); + } catch (ex) { + alert(ex.message); + } +} + +fileInput.addEventListener('change', () => { + readFileAndDrawToCanvas(); +}); diff --git a/www/index.html b/www/index.html index 13275e3..82d6ae4 100644 --- a/www/index.html +++ b/www/index.html @@ -44,6 +44,6 @@

Stats

Sample 6 Sample 7 - +