• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 14683349798

26 Apr 2025 05:08PM UTC coverage: 74.055% (-0.9%) from 74.913%
14683349798

push

github

web-flow
feat(core): TextureFormat generics (#2377)

2019 of 2652 branches covered (76.13%)

Branch coverage included in aggregate %.

62 of 262 new or added lines in 15 files covered. (23.66%)

196 existing lines in 9 files now uncovered.

26575 of 35960 relevant lines covered (73.9%)

47.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.83
/modules/core/src/shadertypes/textures/pixel-utils.ts
1
// luma.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
export type ImageData = {
1✔
6
  /** Offset into the data (in addition to any offset built-in to the ArrayBufferView) */
1✔
7
  byteOffset?: number;
1✔
8
  /** The stride, in bytes, between the beginning of each texel block row and the subsequent texel block row. Required if there are multiple texel block rows (i.e. the copy height or depth is more than one block). */
1✔
9
  bytesPerRow?: number;
1✔
10
  /** Number or rows per image (needed if multiple images are being set) */
1✔
11
  rowsPerImage?: number;
1✔
12
  /** Bits per channel */
1✔
13
  bitsPerChannel: [number, number, number, number];
1✔
14
};
1✔
15

1✔
16
export type PixelData = {
1✔
17
  arrayBuffer: ArrayBuffer;
1✔
18
  width: number;
1✔
19
  height: number;
1✔
20
  /** Bytes per pixel */
1✔
21
  bytesPerPixel: number;
1✔
22
  bytesPerRow: number;
1✔
23
  bitsPerChannel: [number, number, number, number];
1✔
24
};
1✔
25

1✔
26
/**
1✔
27
 * Extracts a single RGBA pixel value from PixelData at the given (x, y) coordinate.
1✔
28
 *
1✔
29
 * The pixel's data is assumed to be packed according to pixelData.bitsPerChannel.
1✔
30
 * The pixel data for a given row is padded to pixelData.bytesPerRow.
1✔
31
 *
1✔
32
 * @param pixelData - The metadata and data for the pixel buffer.
1✔
33
 * @param x - The x coordinate (0-based).
1✔
34
 * @param y - The y coordinate (0-based).
1✔
35
 * @returns A tuple [r, g, b, a] where each channel is the extracted numeric value.
1✔
36
 *
1✔
37
* @example
1✔
38

1✔
39
Assume you obtained an ArrayBuffer from copyTextureToBuffer and have the following metadata:
1✔
40

1✔
41
  const pixelData: PixelData = {
1✔
42
    bitsPerChannel: [5, 6, 5, 0], // For example, a 16-bit RGB565 format (no alpha)
1✔
43
    width: 800,
1✔
44
    height: 600,
1✔
45
    bytesPerPixel: 2,           // 16 bits per pixel
1✔
46
    bytesPerRow: 1600,          // Assuming no extra padding
1✔
47
    arrayBuffer: myTextureBuffer, // Obtained from copyTextureToBuffer
1✔
48
  };
1✔
49

1✔
50
You can then extract the pixel at (x, y) like so:
1✔
51

1✔
52
  const rgba = extractPixel(pixelData, x, y);
1✔
53
  console.log("Extracted RGBA:", rgba);
1✔
54

1✔
55
For RGBA formats where all channels are present (e.g. [8, 8, 8, 8]), the function will extract a 4-channel pixel value.
1✔
56
*/
1✔
57

1✔
58
export function readPixel(
1✔
59
  pixelData: PixelData,
×
60
  x: number,
×
61
  y: number,
×
62
  bitsPerChannel: [number, number, number, number]
×
63
): [number, number, number, number] {
×
64
  if (x < 0 || x >= pixelData.width || y < 0 || y >= pixelData.height) {
×
65
    throw new Error('Coordinates out of bounds.');
×
66
  }
×
67

×
68
  // Compute the byte offset of the pixel in the buffer.
×
69
  const byteOffset = y * pixelData.bytesPerRow + x * pixelData.bytesPerPixel;
×
70

×
71
  // Create a Uint8Array view for this pixel's bytes.
×
72
  // We only need to view pixelData.bytesPerPixel bytes.
×
73
  const pixelDataView = new DataView(pixelData.arrayBuffer, byteOffset, pixelData.bytesPerPixel);
×
74

×
75
  let bitOffsetWithinPixel = 0;
×
76
  const channels: number[] = [];
×
77

×
78
  // Extract each of the four channels.
×
79
  for (let i = 0; i < 4; i++) {
×
80
    const bits = bitsPerChannel[i];
×
81
    // If a channel's bit width is zero or negative, consider it not present.
×
82
    if (bits <= 0) {
×
83
      channels.push(0);
×
84
    } else {
×
85
      const channelValue = readBitsFromDataView(pixelDataView, bitOffsetWithinPixel, bits);
×
86
      channels.push(channelValue);
×
87
      bitOffsetWithinPixel += bits;
×
88
    }
×
89
  }
×
90

×
91
  return [channels[0], channels[1], channels[2], channels[3]];
×
92
}
×
93

1✔
94
/**
1✔
95
 * Encodes an RGBA pixel into a DataView at a given bit offset according to a specified bit layout.
1✔
96
 *
1✔
97
 * The channels are written sequentially in the order R, G, B, A. For each channel, the number
1✔
98
 * of bits is taken from the bitsPerChannel array. Channel values are masked to fit within the specified width.
1✔
99
 *
1✔
100
 * @param dataView - The DataView into which the pixel will be encoded.
1✔
101
 * @param bitOffset - The bit offset in the DataView where the pixel should be written.
1✔
102
 * @param bitsPerChannel - A tuple specifying the number of bits for each channel: [R, G, B, A].
1✔
103
 * @param pixel - A tuple [r, g, b, a] containing the channel values (as numbers).
1✔
104
 *
1✔
105
 * @example
1✔
106

1✔
107
Assume you want to encode a pixel into a packed format where:
1✔
108
  - Red uses 5 bits
1✔
109
  - Green uses 6 bits
1✔
110
  - Blue uses 5 bits
1✔
111
  - Alpha is not used (0 bits)
1✔
112
And the pixel format is packed into 16 bits total.
1✔
113

1✔
114
You might have:
1✔
115
  const bitsPerChannel: [number, number, number, number] = [5, 6, 5, 0];
1✔
116
  const pixel: [number, number, number, number] = [15, 31, 15, 0]; // Example values
1✔
117
  const buffer = new ArrayBuffer(2); // 16 bits = 2 bytes
1✔
118
  const dataView = new DataView(buffer);
1✔
119

1✔
120
Now encode the pixel at bit offset 0:
1✔
121
  encodePixel(dataView, 0, bitsPerChannel, pixel);
1✔
122

1✔
123
The dataView now contains the 16-bit packed pixel value in big-endian order.
1✔
124
*/
1✔
125
export function writePixel(
1✔
UNCOV
126
  dataView: DataView,
×
UNCOV
127
  bitOffset: number,
×
UNCOV
128
  bitsPerChannel: [number, number, number, number],
×
UNCOV
129
  pixel: [number, number, number, number]
×
UNCOV
130
): void {
×
UNCOV
131
  let currentBitOffset = bitOffset;
×
UNCOV
132
  for (let channel = 0; channel < 4; channel++) {
×
UNCOV
133
    const bits = bitsPerChannel[channel];
×
UNCOV
134
    // Clamp the channel value to the maximum allowed by the bit width.
×
UNCOV
135
    const maxValue = (1 << bits) - 1;
×
UNCOV
136
    const channelValue = pixel[channel] & maxValue;
×
UNCOV
137
    writeBitsToDataView(dataView, currentBitOffset, bits, channelValue);
×
UNCOV
138
    currentBitOffset += bits;
×
UNCOV
139
  }
×
UNCOV
140
}
×
141

1✔
142
/**
1✔
143
 * Reads a specified number of bits from a DataView starting at a given bit offset.
1✔
144
 *
1✔
145
 * For channels with a bit width of 8, 16, or 32 bits and when the bitOffset is byte-aligned,
1✔
146
 * this function uses DataView methods for fast extraction.
1✔
147
 *
1✔
148
 * Bits are assumed to be stored in big-endian order (i.e. the most-significant bit is at position 7 in each byte).
1✔
149
 *
1✔
150
 * @param dataView - The DataView containing the data.
1✔
151
 * @param bitOffset - The offset (in bits) within the data from which to start reading.
1✔
152
 * @param bitCount - The number of bits to read (supported range: 1 to 32).
1✔
153
 * @returns The extracted value as a number.
1✔
154
 */
1✔
155
export function readBitsFromDataView(
1✔
UNCOV
156
  dataView: DataView,
×
UNCOV
157
  bitOffset: number,
×
UNCOV
158
  bitCount: number
×
UNCOV
159
): number {
×
UNCOV
160
  // Check if we can optimize when bitOffset is byte-aligned.
×
UNCOV
161
  if (bitOffset % 8 === 0) {
×
UNCOV
162
    const byteOffset = bitOffset / 8;
×
UNCOV
163
    if (bitCount === 8 && byteOffset + 1 <= dataView.byteLength) {
×
UNCOV
164
      return dataView.getUint8(byteOffset);
×
UNCOV
165
    } else if (bitCount === 16 && byteOffset + 2 <= dataView.byteLength) {
×
UNCOV
166
      // false for big-endian reading.
×
UNCOV
167
      return dataView.getUint16(byteOffset, false);
×
UNCOV
168
    } else if (bitCount === 32 && byteOffset + 4 <= dataView.byteLength) {
×
UNCOV
169
      return dataView.getUint32(byteOffset, false);
×
UNCOV
170
    }
×
UNCOV
171
  }
×
UNCOV
172

×
UNCOV
173
  // Fallback: bit-level extraction for non-aligned or non-standard bit widths.
×
UNCOV
174
  let value = 0;
×
UNCOV
175
  for (let i = 0; i < bitCount; i++) {
×
UNCOV
176
    const overallBitIndex = bitOffset + i;
×
UNCOV
177
    const byteIndex = Math.floor(overallBitIndex / 8);
×
UNCOV
178
    const bitIndex = overallBitIndex % 8;
×
UNCOV
179
    // Read the byte and extract the bit at position (7 - bitIndex).
×
UNCOV
180
    const byteValue = dataView.getUint8(byteIndex);
×
UNCOV
181
    const bit = (byteValue >> (7 - bitIndex)) & 1;
×
UNCOV
182
    value = (value << 1) | bit;
×
UNCOV
183
  }
×
UNCOV
184
  return value;
×
UNCOV
185
}
×
186

1✔
187
/**
1✔
188
 * Writes a specified number of bits from a value into a DataView at a given bit offset.
1✔
189
 *
1✔
190
 * For channels with a bit width of 8, 16, or 32 bits and when the bit offset is byte-aligned,
1✔
191
 * this function uses DataView methods for fast writing.
1✔
192
 *
1✔
193
 * Bits are assumed to be stored in big-endian order (i.e. the most-significant bit is at position 7 in each byte).
1✔
194
 *
1✔
195
 * @param dataView - The DataView to write into.
1✔
196
 * @param bitOffset - The bit offset at which to begin writing.
1✔
197
 * @param bitCount - The number of bits to write (supported range: 1 to 32).
1✔
198
 * @param value - The numeric value whose lower bitCount bits will be written.
1✔
199
 */
1✔
200
export function writeBitsToDataView(
1✔
UNCOV
201
  dataView: DataView,
×
UNCOV
202
  bitOffset: number,
×
UNCOV
203
  bitCount: number,
×
UNCOV
204
  value: number
×
UNCOV
205
): void {
×
UNCOV
206
  // If the bitOffset is byte-aligned, we may optimize for common bit widths.
×
UNCOV
207
  if (bitOffset % 8 === 0) {
×
UNCOV
208
    const byteOffset = bitOffset / 8;
×
UNCOV
209
    if (bitCount === 8 && byteOffset + 1 <= dataView.byteLength) {
×
UNCOV
210
      dataView.setUint8(byteOffset, value & 0xff);
×
UNCOV
211
      return;
×
UNCOV
212
    } else if (bitCount === 16 && byteOffset + 2 <= dataView.byteLength) {
×
UNCOV
213
      dataView.setUint16(byteOffset, value & 0xffff, false); // big-endian
×
UNCOV
214
      return;
×
UNCOV
215
    } else if (bitCount === 32 && byteOffset + 4 <= dataView.byteLength) {
×
216
      dataView.setUint32(byteOffset, value, false); // big-endian
×
217
      return;
×
218
    }
×
UNCOV
219
  }
×
UNCOV
220

×
UNCOV
221
  // Fallback: write bit-by-bit.
×
UNCOV
222
  for (let i = 0; i < bitCount; i++) {
×
UNCOV
223
    const overallBitIndex = bitOffset + i;
×
UNCOV
224
    const byteIndex = Math.floor(overallBitIndex / 8);
×
UNCOV
225
    const bitIndex = overallBitIndex % 8;
×
UNCOV
226
    const mask = 1 << (7 - bitIndex);
×
UNCOV
227
    // Extract the i-th bit from value (starting from the most-significant bit)
×
UNCOV
228
    const bitValue = (value >> (bitCount - 1 - i)) & 1;
×
UNCOV
229
    // Read the current byte.
×
UNCOV
230
    let currentByte = dataView.getUint8(byteIndex);
×
UNCOV
231
    // Clear the target bit.
×
UNCOV
232
    currentByte &= ~mask;
×
UNCOV
233
    // Set the target bit if bitValue is 1.
×
UNCOV
234
    if (bitValue) {
×
UNCOV
235
      currentByte |= mask;
×
UNCOV
236
    }
×
UNCOV
237
    dataView.setUint8(byteIndex, currentByte);
×
UNCOV
238
  }
×
UNCOV
239
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc