@loaders.gl/textures
Version:
Framework-independent loaders for compressed and super compressed (basis) textures
305 lines • 12.1 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { GL_RGBA32F } from "../gl-extensions.js";
const HDR_MAGIC_HEADERS = ['#?RADIANCE', '#?RGBE'];
const HDR_FORMAT = '32-bit_rle_rgbe';
export function isHDR(arrayBuffer) {
const state = {
data: new Uint8Array(arrayBuffer),
offset: 0
};
const firstLine = readLine(state);
return firstLine ? HDR_MAGIC_HEADERS.includes(firstLine) : false;
}
export function parseHDR(arrayBuffer) {
const state = {
data: new Uint8Array(arrayBuffer),
offset: 0
};
const header = readHeader(state);
const { width, height } = header;
const rgbeData = readPixels(state, header);
const data = convertRGBEToFloat(rgbeData);
const level = {
shape: 'texture-level',
compressed: false,
width,
height,
data,
levelSize: data.byteLength,
format: GL_RGBA32F,
textureFormat: 'rgba32float'
};
return {
shape: 'texture',
type: '2d',
format: 'rgba32float',
...(header.metadata ? { metadata: header.metadata } : {}),
data: [level]
};
}
function readHeader(state) {
const magicHeader = readLine(state);
if (!magicHeader || !HDR_MAGIC_HEADERS.includes(magicHeader)) {
throw new Error('RadianceHDRLoader: bad initial token');
}
let hasFormat = false;
const metadata = {};
while (state.offset < state.data.length) {
const line = readLine(state);
if (line === null) {
break;
}
if (!line || line.startsWith('#')) {
continue;
}
if (line.startsWith('FORMAT=')) {
hasFormat = line.slice('FORMAT='.length) === HDR_FORMAT;
if (!hasFormat) {
throw new Error('RadianceHDRLoader: unsupported format specifier');
}
continue;
}
parseMetadataLine(metadata, line);
const dimensions = parseDimensions(line);
if (dimensions) {
if (!hasFormat) {
throw new Error('RadianceHDRLoader: missing format specifier');
}
return {
...dimensions,
...(hasMetadata(metadata) ? { metadata } : {})
};
}
}
if (!hasFormat) {
throw new Error('RadianceHDRLoader: missing format specifier');
}
throw new Error('RadianceHDRLoader: missing image size specifier');
}
function parseDimensions(line) {
const match = line.match(/^([+-])([YX])\s+(\d+)\s+([+-])([YX])\s+(\d+)$/);
if (!match) {
return null;
}
const majorSign = match[1] === '+' ? 1 : -1;
const majorAxis = match[2];
const majorLength = Number(match[3]);
const minorSign = match[4] === '+' ? 1 : -1;
const minorAxis = match[5];
const minorLength = Number(match[6]);
if (majorAxis === minorAxis) {
throw new Error('RadianceHDRLoader: invalid image dimensions');
}
const width = majorAxis === 'X' ? majorLength : minorLength;
const height = majorAxis === 'Y' ? majorLength : minorLength;
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
throw new Error('RadianceHDRLoader: invalid image dimensions');
}
return { width, height, majorAxis, majorSign, minorAxis, minorSign };
}
function readPixels(state, header) {
const { width, height } = header;
const pixelCount = width * height;
const flatByteLength = pixelCount * 4;
const scanlineLength = header.minorAxis === 'X' ? width : height;
const scanlineCount = header.majorAxis === 'Y' ? height : width;
if (scanlineLength < 8 || scanlineLength > 0x7fff) {
return reorderPixels(readFlatPixels(state, flatByteLength), header);
}
if (state.offset + 4 > state.data.length) {
throw new Error('RadianceHDRLoader: unexpected end of file');
}
const data = state.data;
const isRunLengthEncoded = data[state.offset] === 2 && data[state.offset + 1] === 2 && !(data[state.offset + 2] & 0x80);
if (!isRunLengthEncoded) {
return reorderPixels(readFlatPixels(state, flatByteLength), header);
}
const scanlineWidth = (data[state.offset + 2] << 8) | data[state.offset + 3];
if (scanlineWidth !== scanlineLength) {
return reorderPixels(readFlatPixels(state, flatByteLength), header);
}
const pixels = new Uint8Array(flatByteLength);
const scanlineBuffer = new Uint8Array(scanlineLength * 4);
for (let scanlineIndex = 0; scanlineIndex < scanlineCount; scanlineIndex++) {
if (state.offset + 4 > data.length) {
throw new Error('RadianceHDRLoader: unexpected end of file');
}
const red = data[state.offset++];
const green = data[state.offset++];
const blue = data[state.offset++];
const exponent = data[state.offset++];
if (red !== 2 || green !== 2 || ((blue << 8) | exponent) !== scanlineLength) {
throw new Error('RadianceHDRLoader: bad rgbe scanline format');
}
for (let channelIndex = 0; channelIndex < 4; channelIndex++) {
const channelOffset = channelIndex * scanlineLength;
const channelEnd = channelOffset + scanlineLength;
let pixelOffset = channelOffset;
while (pixelOffset < channelEnd) {
if (state.offset >= data.length) {
throw new Error('RadianceHDRLoader: unexpected end of file');
}
let count = data[state.offset++];
if (count > 128) {
count -= 128;
if (count === 0 || pixelOffset + count > channelEnd || state.offset >= data.length) {
throw new Error('RadianceHDRLoader: bad scanline data');
}
const value = data[state.offset++];
scanlineBuffer.fill(value, pixelOffset, pixelOffset + count);
pixelOffset += count;
continue;
}
if (count === 0 || pixelOffset + count > channelEnd || state.offset + count > data.length) {
throw new Error('RadianceHDRLoader: bad scanline data');
}
scanlineBuffer.set(data.subarray(state.offset, state.offset + count), pixelOffset);
pixelOffset += count;
state.offset += count;
}
}
for (let pixelIndex = 0; pixelIndex < scanlineLength; pixelIndex++) {
const outputOffset = getOutputOffset(header, scanlineIndex, pixelIndex);
pixels[outputOffset] = scanlineBuffer[pixelIndex];
pixels[outputOffset + 1] = scanlineBuffer[pixelIndex + scanlineLength];
pixels[outputOffset + 2] = scanlineBuffer[pixelIndex + scanlineLength * 2];
pixels[outputOffset + 3] = scanlineBuffer[pixelIndex + scanlineLength * 3];
}
}
return pixels;
}
function reorderPixels(data, header) {
const pixels = new Uint8Array(data.length);
const scanlineLength = header.minorAxis === 'X' ? header.width : header.height;
const scanlineCount = header.majorAxis === 'Y' ? header.height : header.width;
for (let scanlineIndex = 0; scanlineIndex < scanlineCount; scanlineIndex++) {
for (let pixelIndex = 0; pixelIndex < scanlineLength; pixelIndex++) {
const sourceOffset = (scanlineIndex * scanlineLength + pixelIndex) * 4;
const outputOffset = getOutputOffset(header, scanlineIndex, pixelIndex);
pixels[outputOffset] = data[sourceOffset];
pixels[outputOffset + 1] = data[sourceOffset + 1];
pixels[outputOffset + 2] = data[sourceOffset + 2];
pixels[outputOffset + 3] = data[sourceOffset + 3];
}
}
return pixels;
}
function getOutputOffset(header, scanlineIndex, pixelIndex) {
const majorCoordinate = getCoordinate(header.majorAxis === 'X' ? header.width : header.height, header.majorSign, scanlineIndex);
const minorCoordinate = getCoordinate(header.minorAxis === 'X' ? header.width : header.height, header.minorSign, pixelIndex);
const x = header.majorAxis === 'X' ? majorCoordinate : minorCoordinate;
const y = header.majorAxis === 'Y' ? majorCoordinate : minorCoordinate;
return ((header.height - 1 - y) * header.width + x) * 4;
}
function getCoordinate(length, sign, index) {
return sign === 1 ? index : length - 1 - index;
}
function readFlatPixels(state, byteLength) {
if (state.offset + byteLength > state.data.length) {
throw new Error('RadianceHDRLoader: unexpected end of file');
}
const pixels = state.data.slice(state.offset, state.offset + byteLength);
state.offset += byteLength;
return pixels;
}
function convertRGBEToFloat(data) {
const floatData = new Float32Array(data.length);
for (let sourceOffset = 0; sourceOffset < data.length; sourceOffset += 4) {
const exponent = data[sourceOffset + 3];
const destinationOffset = sourceOffset;
if (exponent > 0) {
const scale = Math.pow(2, exponent - 128) / 255;
floatData[destinationOffset] = data[sourceOffset] * scale;
floatData[destinationOffset + 1] = data[sourceOffset + 1] * scale;
floatData[destinationOffset + 2] = data[sourceOffset + 2] * scale;
}
floatData[destinationOffset + 3] = 1;
}
return floatData;
}
function readLine(state) {
if (state.offset >= state.data.length) {
return null;
}
const lineStart = state.offset;
while (state.offset < state.data.length) {
const byte = state.data[state.offset++];
if (byte === 0x0a) {
const line = decodeASCII(state.data.subarray(lineStart, state.offset - 1));
return line.endsWith('\r') ? line.slice(0, -1) : line;
}
}
const line = decodeASCII(state.data.subarray(lineStart, state.offset));
return line.endsWith('\r') ? line.slice(0, -1) : line;
}
function decodeASCII(data) {
let line = '';
for (const byte of data) {
line += String.fromCharCode(byte);
}
return line;
}
function parseMetadataLine(metadata, line) {
if (line.startsWith('COLORCORR=')) {
const values = parseNumberList(line.slice('COLORCORR='.length), 3);
if (values) {
metadata.colorCorrection = values;
}
return;
}
if (line.startsWith('EXPOSURE=')) {
const value = parseNumber(line.slice('EXPOSURE='.length));
if (value !== null) {
metadata.exposure = value;
}
return;
}
if (line.startsWith('GAMMA=')) {
const value = parseNumber(line.slice('GAMMA='.length));
if (value !== null) {
metadata.gamma = value;
}
return;
}
if (line.startsWith('PIXASPECT=')) {
const value = parseNumber(line.slice('PIXASPECT='.length));
if (value !== null) {
metadata.pixelAspectRatio = value;
}
return;
}
if (line.startsWith('PRIMARIES=')) {
const values = parseNumberList(line.slice('PRIMARIES='.length), 8);
if (values) {
metadata.primaries = values;
}
return;
}
if (line.startsWith('SOFTWARE=')) {
metadata.software = line.slice('SOFTWARE='.length).trim();
return;
}
if (line.startsWith('VIEW=')) {
metadata.view = line.slice('VIEW='.length).trim();
}
}
function parseNumber(text) {
const value = Number(text.trim());
return Number.isFinite(value) ? value : null;
}
function parseNumberList(text, count) {
const values = text
.trim()
.split(/\s+/)
.map((value) => Number(value));
if (values.length !== count || values.some((value) => !Number.isFinite(value))) {
return null;
}
return values;
}
function hasMetadata(metadata) {
return Object.keys(metadata).length > 0;
}
//# sourceMappingURL=parse-hdr.js.map