vtf-js
Version:
A javascript IO library for the Valve Texture Format.
113 lines (112 loc) • 4.73 kB
JavaScript
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);
}
}