UNPKG

@loaders.gl/textures

Version:

Framework-independent loaders for compressed and super compressed (basis) textures

305 lines 12.1 kB
// 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