Source: Raster.js

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 }