UNPKG

vtf-js

Version:

A javascript IO library for the Valve Texture Format.

113 lines (112 loc) 4.73 kB
import { VFormats } from './enums.js'; import { VFilters, VImageScaler } from './resize.js'; import { clamp } from './utils.js'; /** Does the current environment support sec-float16array? */ export const HAS_FLOAT16 = typeof Float16Array !== 'undefined'; /** All currently-registered image codecs. */ export const VCodecs = {}; /** Register an image encoder/decoder for the specified format. */ export function registerCodec(format, codec) { VCodecs[format] = codec; } export function getCodec(format, strict = true) { const codec = VCodecs[format]; if (!codec && strict) throw Error(`getCodec: Could not get codec for format ${VFormats[format]}!`); return codec; } /** Decoded RGBA image data. */ export class VImageData { isEncoded = false; width; height; data; constructor(data, width, height) { this.data = data; this.width = width; this.height = height; } /** * Returns a remapped copy of this image with the specified data format, normalizing floating-point formats to 0-1. * If this image is already of the specified format, this method returns self. * If `do_clamp` is set to true, the data will be clamped between 0 and the array's maximum value. * @example const converted: VImageData<Float32Array> = image.convert(Float32Array); */ convert(type, do_clamp = true) { if (this.data instanceof type) return this; const out = new type(this.data.length); const is_input_int = !(this.data instanceof Float32Array || this.data instanceof Float64Array || (HAS_FLOAT16 && this.data instanceof Float16Array)); const is_output_int = !(out instanceof Float32Array || out instanceof Float64Array || (HAS_FLOAT16 && out instanceof Float16Array)); const input_max = is_input_int ? 2 ** (this.data.BYTES_PER_ELEMENT * 8) - 1 : 1; const output_max = is_output_int ? 2 ** (out.BYTES_PER_ELEMENT * 8) - 1 : 1; const mult_factor = output_max / input_max; const add_factor = 0; // (+is_input_int - +is_output_int) * 0.5; if (do_clamp) { for (let i = 0; i < this.data.length; i++) out[i] = clamp(this.data[i] * mult_factor + add_factor, 0, output_max); } else { for (let i = 0; i < this.data.length; i++) out[i] = this.data[i] * mult_factor + add_factor; } return new VImageData(out, this.width, this.height); } /** Encodes this image into the specified format and validates the length of the resulting data. */ encode(format) { const codec = getCodec(format); const length = codec.length(this.width, this.height); const out = codec.encode(this); if (out.data.length !== length) throw Error(`VImageData.encode: Encoded ${VFormats[format]} image failed length validation! (expected ${length} but got ${out.data.length})`); return out; } /** Dummy function - returns self. */ decode() { return this; } /** * Returns a resampled copy of this image with the given dimensions. * ### If you are batch-resizing images, create and reuse a VImageScaler for better performance! */ resize(width, height, options) { options ??= {}; options.filter ??= VFilters.Triangle; const scaler = new VImageScaler(this.width, this.height, width, height, options.filter); const out_data = new this.data.constructor(width * height * 4); const out = new VImageData(out_data, width, height); return scaler.resize(this, out); } /** Retrieves the constructor of this image's data with a type-safe wrapper. */ getDataConstructor() { return this.data.constructor; } } /** Format-encoded image data. */ export class VEncodedImageData { isEncoded = true; width; height; format; data; constructor(data, width, height, format) { this.data = data; this.width = width; this.height = height; this.format = format; } /** Decodes this image into RGBA pixel data. */ decode() { const length = this.width * this.height * 4; const out = getCodec(this.format).decode(this); if (out.data.length !== length) throw Error(`VImageData.encode: Decoded ${VFormats[this.format]} image failed length validation! (expected ${length} but got ${out.data.length})`); return out; } /** If necessary, decodes and encodes this image into the desired format. Otherwise, returns self. */ encode(format) { if (format === this.format) return this; return this.decode().encode(format); } }