1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
import { addSignals } from './Signals.js'
/*
* @fileoverview
* Raster module provides functionality for loading and managing image data in various formats.
* Supports multiple color formats and handles both local and remote image loading with CORS support.
*/
/**
* @typedef {('vec3'|'vec4'|'float')} Raster#Format
* Defines the color format for image data storage in textures and renderbuffers.
* @property {'vec3'} vec3 - RGB format (3 components without alpha)
* @property {'vec4'} vec4 - RGBA format (4 components with alpha)
* @property {'float'} float - Single-channel format for coefficient data
*/
/**
* Raster class handles image loading and texture creation for OpenLIME.
* Provides functionality for:
* - Loading images from URLs or blobs
* - Converting images to WebGL textures
* - Handling different color formats
* - Supporting partial content requests
* - Managing CORS requests
* - Creating mipmaps for large textures
*/
class Raster {
/**
* Creates a new Raster instance.
* @param {Object} [options] - Configuration options
* @param {Raster#Format} [options.format='vec3'] - Color format for image data:
* - 'vec3' for RGB images
* - 'vec4' for RGBA images
* - 'float' for coefficient data
*/
constructor(options) {
Object.assign(this, {
format: 'vec3'
});
this._texture = null;
Object.assign(this, options);
}
/**
* Loads an image tile and converts it to a WebGL texture.
* Supports both full and partial content requests.
* @async
* @param {Object} tile - The tile to load
* @param {string} tile.url - URL of the image
* @param {number} [tile.start] - Start byte for partial requests
* @param {number} [tile.end] - End byte for partial requests
* @param {WebGLRenderingContext} gl - The WebGL rendering context
* @returns {Promise<Array>} Promise resolving to [texture, size]:
* - texture: WebGLTexture object
* - size: Size of the image in bytes (width * height * components)
* @throws {Error} If server doesn't support partial content requests when required
*/
async loadImage(tile, gl) {
let img;
let cors = (new URL(tile.url, window.location.href)).origin !== window.location.origin;
if (tile.end || typeof createImageBitmap == 'undefined') {
let options = {};
options.headers = { range: `bytes=${tile.start}-${tile.end}`, 'Accept-Encoding': 'indentity', mode: cors ? 'cors' : 'same-origin' };
let response = await fetch(tile.url, options);
if (!response.ok) {
console.error(`Failed to load ${tile.url}: ${response.status} ${response.statusText}`);
return;
}
if (response.status != 206)
throw new Error("The server doesn't support partial content requests (206).");
let blob = await response.blob();
img = await this.blobToImage(blob, gl);
} else {
img = document.createElement('img');
if (cors) img.crossOrigin = "";
img.onerror = function (e) { console.log("Texture loading error!"); };
img.src = tile.url;
await new Promise((resolve, reject) => {
img.onload = () => { resolve(); }
});
}
const tex = this.loadTexture(gl, img);
//TODO 3 is not accurate for type of image, when changing from rgb to grayscale, fix this value.
let nchannels = 3; // Channel is important only for tarzoom data. Tarzoom data inside format is JPG = RGB = 3 channels
const size = img.width * img.height * nchannels;
this.emit('loaded');
return [tex, size];
}
/**
* Converts a Blob to an Image or ImageBitmap.
* Handles browser-specific differences in image orientation.
* @private
* @async
* @param {Blob} blob - Image data as Blob
* @param {WebGLRenderingContext} gl - The WebGL rendering context
* @returns {Promise<HTMLImageElement|ImageBitmap>} Promise resolving to the image
*/
async blobToImage(blob, gl) {
let img;
if (typeof createImageBitmap != 'undefined') {
var isFirefox = typeof InstallTrigger !== 'undefined';
//firefox does not support options for this call, BUT the image is automatically flipped.
if (isFirefox)
img = await createImageBitmap(blob);
else
img = await createImageBitmap(blob, { imageOrientation1: 'flipY' });
} else { //fallback for IOS
let urlCreator = window.URL || window.webkitURL;
img = document.createElement('img');
img.onerror = function (e) { console.log("Texture loading error!"); };
img.src = urlCreator.createObjectURL(blob);
await new Promise((resolve, reject) => { img.onload = () => resolve() });
urlCreator.revokeObjectURL(img.src);
}
return img;
}
/**
* Creates a WebGL texture from an image.
* Handles different color formats and automatically creates mipmaps for large textures.
* @private
* @param {WebGLRenderingContext} gl - The WebGL rendering context
* @param {HTMLImageElement|ImageBitmap} img - The source image
* @returns {WebGLTexture} The created texture
*
* @property {number} width - Width of the loaded image (set after loading)
* @property {number} height - Height of the loaded image (set after loading)
*/
loadTexture(gl, img) {
this.width = img.width;
this.height = img.height;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
let glFormat = gl.RGBA;
let internalFormat = gl.RGBA;
switch (this.format) {
case 'vec3':
glFormat = gl.RGB;
break;
case 'vec4':
glFormat = gl.RGBA;
break;
case 'float':
// Use RED instead of LUMINANCE for WebGL2
glFormat = gl instanceof WebGL2RenderingContext ? gl.RED : gl.LUMINANCE;
break;
default:
break;
}
// For WebGL2, use proper internal format for linear textures
if (this.format === 'float') {
// For float textures in WebGL2, use R8 as internal format
internalFormat = gl.R8;
} else {
internalFormat = glFormat === gl.RGB ? gl.RGB : gl.RGBA;
}
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, glFormat, gl.UNSIGNED_BYTE, img);
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
//build mipmap for large images.
if (this.width > 1024 || this.height > 1024) {
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
} else {
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
this._texture = tex;
return tex;
}
}
/**
* Example usage of Raster:
* ```javascript
* // Create a Raster for RGBA images
* const raster = new Raster({ format: 'vec4' });
*
* // Load an image tile
* const tile = {
* url: 'https://example.com/image.jpg',
* start: 0,
* end: 1024 // Optional: for partial loading
* };
*
* // Get WebGL context and load the image
* const gl = canvas.getContext('webgl');
* const [texture, size] = await raster.loadImage(tile, gl);
*
* // Texture is now ready for use in WebGL
* gl.bindTexture(gl.TEXTURE_2D, texture);
* ```
*/
addSignals(Raster, 'loaded');
export { Raster }