@mentra/sdk 2.1.3 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
55
55
  exports.BitmapUtils = void 0;
56
56
  const fs = __importStar(require("fs/promises"));
57
57
  const path = __importStar(require("path"));
58
+ const jimp_1 = require("jimp");
58
59
  /**
59
60
  * Utility class for working with bitmap images in MentraOS applications
60
61
  */
@@ -72,16 +73,10 @@ class BitmapUtils {
72
73
  * session.layouts.showBitmapView(bmpHex);
73
74
  * ```
74
75
  */
75
- static async loadBmpAsHex(filePath) {
76
+ static async loadBmpFromFileAsHex(filePath) {
76
77
  try {
77
78
  const bmpData = await fs.readFile(filePath);
78
- // Basic BMP validation - check for BMP signature
79
- if (bmpData.length < 14 || bmpData[0] !== 0x42 || bmpData[1] !== 0x4D) {
80
- throw new Error(`File ${filePath} is not a valid BMP file (missing BM signature)`);
81
- }
82
- const hexString = bmpData.toString('hex');
83
- console.log(`📁 Loaded BMP: ${path.basename(filePath)} (${bmpData.length} bytes)`);
84
- return hexString;
79
+ return this.loadBmpFromDataAsHex(bmpData);
85
80
  }
86
81
  catch (error) {
87
82
  if (error instanceof Error) {
@@ -90,6 +85,136 @@ class BitmapUtils {
90
85
  throw new Error(`Failed to load BMP file ${filePath}: Unknown error`);
91
86
  }
92
87
  }
88
+ static async convert24BitTo1BitBMP(input24BitBmp) {
89
+ // Read header information from 24-bit BMP
90
+ const width = input24BitBmp.readUInt32LE(18);
91
+ const height = Math.abs(input24BitBmp.readInt32LE(22)); // Height can be negative (top-down BMP)
92
+ const isTopDown = input24BitBmp.readInt32LE(22) < 0;
93
+ const bitsPerPixel = input24BitBmp.readUInt16LE(28);
94
+ if (bitsPerPixel !== 24) {
95
+ throw new Error("Input must be a 24-bit BMP");
96
+ }
97
+ // Calculate row sizes (both must be 4-byte aligned)
98
+ const rowSize24 = Math.ceil((width * 3) / 4) * 4;
99
+ const rowSize1 = Math.ceil(width / 32) * 4; // 32 pixels per 4 bytes
100
+ // Calculate sizes for 1-bit BMP
101
+ const colorTableSize = 8; // 2 colors * 4 bytes each
102
+ const headerSize = 54 + colorTableSize;
103
+ const pixelDataSize = rowSize1 * height;
104
+ const fileSize = headerSize + pixelDataSize;
105
+ // Create new buffer for 1-bit BMP
106
+ const output1BitBmp = Buffer.alloc(fileSize);
107
+ let offset = 0;
108
+ // Write BMP file header (14 bytes)
109
+ output1BitBmp.write("BM", offset);
110
+ offset += 2; // Signature
111
+ output1BitBmp.writeUInt32LE(fileSize, offset);
112
+ offset += 4; // File size
113
+ output1BitBmp.writeUInt16LE(0, offset);
114
+ offset += 2; // Reserved 1
115
+ output1BitBmp.writeUInt16LE(0, offset);
116
+ offset += 2; // Reserved 2
117
+ output1BitBmp.writeUInt32LE(headerSize, offset);
118
+ offset += 4; // Pixel data offset
119
+ // Write DIB header (40 bytes)
120
+ output1BitBmp.writeUInt32LE(40, offset);
121
+ offset += 4; // DIB header size
122
+ output1BitBmp.writeInt32LE(width, offset);
123
+ offset += 4; // Width
124
+ output1BitBmp.writeInt32LE(height, offset);
125
+ offset += 4; // Height (positive for bottom-up)
126
+ output1BitBmp.writeUInt16LE(1, offset);
127
+ offset += 2; // Planes
128
+ output1BitBmp.writeUInt16LE(1, offset);
129
+ offset += 2; // Bits per pixel (1-bit)
130
+ output1BitBmp.writeUInt32LE(0, offset);
131
+ offset += 4; // Compression (none)
132
+ output1BitBmp.writeUInt32LE(pixelDataSize, offset);
133
+ offset += 4; // Image size
134
+ output1BitBmp.writeInt32LE(2835, offset);
135
+ offset += 4; // X pixels per meter (72 DPI)
136
+ output1BitBmp.writeInt32LE(2835, offset);
137
+ offset += 4; // Y pixels per meter (72 DPI)
138
+ output1BitBmp.writeUInt32LE(2, offset);
139
+ offset += 4; // Colors used
140
+ output1BitBmp.writeUInt32LE(2, offset);
141
+ offset += 4; // Important colors
142
+ // Write color table (8 bytes)
143
+ // Black (index 0): B=0, G=0, R=0, Reserved=0
144
+ output1BitBmp.writeUInt32LE(0x00000000, offset);
145
+ offset += 4;
146
+ // White (index 1): B=255, G=255, R=255, Reserved=0
147
+ output1BitBmp.writeUInt8(255, offset++); // Blue
148
+ output1BitBmp.writeUInt8(255, offset++); // Green
149
+ output1BitBmp.writeUInt8(255, offset++); // Red
150
+ output1BitBmp.writeUInt8(0, offset++); // Reserved
151
+ // Convert pixel data from 24-bit to 1-bit
152
+ const pixelDataStart24 = 54; // 24-bit BMP has no color table
153
+ for (let y = 0; y < height; y++) {
154
+ // BMP files are usually stored bottom-up
155
+ const sourceY = isTopDown ? y : height - 1 - y;
156
+ const destY = height - 1 - y; // Always write bottom-up for compatibility
157
+ // Initialize the row with zeros
158
+ const rowData = Buffer.alloc(rowSize1);
159
+ for (let x = 0; x < width; x++) {
160
+ // Get pixel from 24-bit BMP
161
+ const offset24 = pixelDataStart24 + sourceY * rowSize24 + x * 3;
162
+ const blue = input24BitBmp[offset24];
163
+ const green = input24BitBmp[offset24 + 1];
164
+ const red = input24BitBmp[offset24 + 2];
165
+ // Determine if pixel is white (assuming pure black or white)
166
+ // White = 1, Black = 0
167
+ const isWhite = red > 128 || green > 128 || blue > 128 ? 1 : 0;
168
+ // Calculate bit position
169
+ const byteIndex = Math.floor(x / 8);
170
+ const bitPosition = 7 - (x % 8); // MSB first
171
+ // Set bit if white
172
+ if (isWhite) {
173
+ rowData[byteIndex] |= 1 << bitPosition;
174
+ }
175
+ }
176
+ // Write row to output buffer
177
+ const destOffset = offset + destY * rowSize1;
178
+ rowData.copy(output1BitBmp, destOffset);
179
+ }
180
+ return output1BitBmp;
181
+ }
182
+ static async loadBmpFromDataAsHex(bmpData) {
183
+ try {
184
+ // Basic BMP validation - check for BMP signature
185
+ if (bmpData.length < 14 || bmpData[0] !== 0x42 || bmpData[1] !== 0x4d) {
186
+ throw new Error(`Bmp data is not a valid BMP file (missing BM signature)`);
187
+ }
188
+ let finalBmpData = bmpData;
189
+ // Load the image with Jimp
190
+ const image = await jimp_1.Jimp.read(bmpData);
191
+ // Check if we need to add padding
192
+ if (image.width !== 576 || image.height !== 135) {
193
+ console.log(`Adding padding to BMP since it isn't 576x135 (current: ${image.width}x${image.height})`);
194
+ // Create a new 576x135 white canvas
195
+ const paddedImage = new jimp_1.Jimp({
196
+ width: 576,
197
+ height: 135,
198
+ color: 0xffffffff,
199
+ });
200
+ // // Calculate position to place the original image (with padding)
201
+ const leftPadding = 40; // 40px padding on left
202
+ const topPadding = 35; // 35px padding on top
203
+ // Composite the original image onto the white canvas
204
+ paddedImage.composite(image, leftPadding, topPadding);
205
+ finalBmpData = await this.convert24BitTo1BitBMP(await paddedImage.getBuffer("image/bmp"));
206
+ }
207
+ // No padding needed, just return as hex
208
+ console.log(`finalBmpData: ${finalBmpData.length} bytes`);
209
+ return finalBmpData.toString("hex");
210
+ }
211
+ catch (error) {
212
+ if (error instanceof Error) {
213
+ throw new Error(`Failed to load BMP data: ${error.message}`);
214
+ }
215
+ throw new Error(`Failed to load BMP data: Unknown error`);
216
+ }
217
+ }
93
218
  /**
94
219
  * Load multiple BMP frames as hex array for animations
95
220
  *
@@ -111,19 +236,19 @@ class BitmapUtils {
111
236
  * ```
112
237
  */
113
238
  static async loadBmpFrames(basePath, frameCount, options = {}) {
114
- const { filePattern = 'animation_10_frame_{i}.bmp', startFrame = 1, validateFrames = true, skipMissingFrames = false } = options;
239
+ const { filePattern = "animation_10_frame_{i}.bmp", startFrame = 1, validateFrames = true, skipMissingFrames = false, } = options;
115
240
  const frames = [];
116
241
  const errors = [];
117
242
  for (let i = 0; i < frameCount; i++) {
118
243
  const frameNumber = startFrame + i;
119
- const fileName = filePattern.replace('{i}', frameNumber.toString());
244
+ const fileName = filePattern.replace("{i}", frameNumber.toString());
120
245
  const filePath = path.join(basePath, fileName);
121
246
  try {
122
- const frameHex = await this.loadBmpAsHex(filePath);
247
+ const frameHex = await this.loadBmpFromFileAsHex(filePath);
123
248
  if (validateFrames) {
124
249
  const validation = this.validateBmpHex(frameHex);
125
250
  if (!validation.isValid) {
126
- const errorMsg = `Frame ${frameNumber} validation failed: ${validation.errors.join(', ')}`;
251
+ const errorMsg = `Frame ${frameNumber} validation failed: ${validation.errors.join(", ")}`;
127
252
  if (skipMissingFrames) {
128
253
  console.warn(`⚠️ ${errorMsg} - skipping`);
129
254
  continue;
@@ -137,7 +262,7 @@ class BitmapUtils {
137
262
  frames.push(frameHex);
138
263
  }
139
264
  catch (error) {
140
- const errorMsg = `Failed to load frame ${frameNumber} (${fileName}): ${error instanceof Error ? error.message : 'Unknown error'}`;
265
+ const errorMsg = `Failed to load frame ${frameNumber} (${fileName}): ${error instanceof Error ? error.message : "Unknown error"}`;
141
266
  if (skipMissingFrames) {
142
267
  console.warn(`⚠️ ${errorMsg} - skipping`);
143
268
  continue;
@@ -148,7 +273,7 @@ class BitmapUtils {
148
273
  }
149
274
  }
150
275
  if (errors.length > 0) {
151
- throw new Error(`Failed to load frames:\n${errors.join('\n')}`);
276
+ throw new Error(`Failed to load frames:\n${errors.join("\n")}`);
152
277
  }
153
278
  if (frames.length === 0) {
154
279
  throw new Error(`No valid frames loaded from ${basePath}`);
@@ -176,35 +301,37 @@ class BitmapUtils {
176
301
  const errors = [];
177
302
  let byteCount = 0;
178
303
  let blackPixels = 0;
179
- let metadata = {};
304
+ const metadata = {};
180
305
  try {
181
306
  // Basic hex validation
182
- if (typeof hexString !== 'string' || hexString.length === 0) {
183
- errors.push('Hex string is empty or invalid');
307
+ if (typeof hexString !== "string" || hexString.length === 0) {
308
+ errors.push("Hex string is empty or invalid");
184
309
  return { isValid: false, byteCount: 0, blackPixels: 0, errors };
185
310
  }
186
311
  if (hexString.length % 2 !== 0) {
187
- errors.push('Hex string length must be even');
312
+ errors.push("Hex string length must be even");
188
313
  return { isValid: false, byteCount: 0, blackPixels: 0, errors };
189
314
  }
190
315
  // Convert to buffer
191
- const buffer = Buffer.from(hexString, 'hex');
316
+ const buffer = Buffer.from(hexString, "hex");
192
317
  byteCount = buffer.length;
193
318
  // BMP signature validation
194
319
  if (buffer.length < 14) {
195
- errors.push('File too small to be a valid BMP (minimum 14 bytes for header)');
320
+ errors.push("File too small to be a valid BMP (minimum 14 bytes for header)");
196
321
  }
197
322
  else {
198
- if (buffer[0] !== 0x42 || buffer[1] !== 0x4D) {
323
+ if (buffer[0] !== 0x42 || buffer[1] !== 0x4d) {
199
324
  errors.push('Invalid BMP signature (should start with "BM")');
200
325
  }
201
326
  }
202
327
  // Size validation for MentraOS (576x135 = ~9782 bytes expected)
203
328
  const expectedSize = 9782;
204
- if (buffer.length < expectedSize - 100) { // Allow some tolerance
329
+ if (buffer.length < expectedSize - 100) {
330
+ // Allow some tolerance
205
331
  errors.push(`BMP too small (${buffer.length} bytes, expected ~${expectedSize})`);
206
332
  }
207
- else if (buffer.length > expectedSize + 1000) { // Allow some tolerance
333
+ else if (buffer.length > expectedSize + 1000) {
334
+ // Allow some tolerance
208
335
  errors.push(`BMP too large (${buffer.length} bytes, expected ~${expectedSize})`);
209
336
  }
210
337
  // Extract BMP metadata if header is valid
@@ -214,37 +341,37 @@ class BitmapUtils {
214
341
  const width = buffer.readUInt32LE(18);
215
342
  const height = buffer.readUInt32LE(22);
216
343
  metadata.dimensions = { width, height };
217
- metadata.format = 'BMP';
344
+ metadata.format = "BMP";
218
345
  // Validate dimensions for MentraOS glasses
219
346
  if (width !== 576 || height !== 135) {
220
347
  errors.push(`Invalid dimensions (${width}x${height}, expected 576x135 for MentraOS)`);
221
348
  }
222
349
  }
223
350
  catch (e) {
224
- errors.push('Failed to parse BMP header metadata');
351
+ errors.push("Failed to parse BMP header metadata");
225
352
  }
226
353
  }
227
354
  // Pixel data validation (assumes 54-byte header + pixel data)
228
355
  if (buffer.length > 62) {
229
356
  const pixelData = buffer.slice(62); // Skip BMP header
230
- blackPixels = Array.from(pixelData).filter(b => b !== 0xFF).length;
357
+ blackPixels = Array.from(pixelData).filter((b) => b !== 0xff).length;
231
358
  if (blackPixels === 0) {
232
- errors.push('No black pixels found (image appears to be all white)');
359
+ errors.push("No black pixels found (image appears to be all white)");
233
360
  }
234
361
  }
235
362
  else {
236
- errors.push('File too small to contain pixel data');
363
+ errors.push("File too small to contain pixel data");
237
364
  }
238
365
  }
239
366
  catch (error) {
240
- errors.push(`Failed to parse hex data: ${error instanceof Error ? error.message : 'Unknown error'}`);
367
+ errors.push(`Failed to parse hex data: ${error instanceof Error ? error.message : "Unknown error"}`);
241
368
  }
242
369
  return {
243
370
  isValid: errors.length === 0,
244
371
  byteCount,
245
372
  blackPixels,
246
373
  errors,
247
- metadata: Object.keys(metadata).length > 0 ? metadata : undefined
374
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
248
375
  };
249
376
  }
250
377
  /**
@@ -265,13 +392,13 @@ class BitmapUtils {
265
392
  let buffer;
266
393
  // Convert input to buffer
267
394
  switch (fromFormat) {
268
- case 'hex':
269
- buffer = Buffer.from(data, 'hex');
395
+ case "hex":
396
+ buffer = Buffer.from(data, "hex");
270
397
  break;
271
- case 'base64':
272
- buffer = Buffer.from(data, 'base64');
398
+ case "base64":
399
+ buffer = Buffer.from(data, "base64");
273
400
  break;
274
- case 'buffer':
401
+ case "buffer":
275
402
  buffer = data;
276
403
  break;
277
404
  default:
@@ -279,11 +406,11 @@ class BitmapUtils {
279
406
  }
280
407
  // Convert buffer to target format
281
408
  switch (toFormat) {
282
- case 'hex':
283
- return buffer.toString('hex');
284
- case 'base64':
285
- return buffer.toString('base64');
286
- case 'buffer':
409
+ case "hex":
410
+ return buffer.toString("hex");
411
+ case "base64":
412
+ return buffer.toString("base64");
413
+ case "buffer":
287
414
  return buffer;
288
415
  default:
289
416
  throw new Error(`Unsupported target format: ${toFormat}`);
@@ -303,8 +430,8 @@ class BitmapUtils {
303
430
  */
304
431
  static getBitmapInfo(hexString) {
305
432
  try {
306
- const buffer = Buffer.from(hexString, 'hex');
307
- const isValidBmp = buffer.length >= 14 && buffer[0] === 0x42 && buffer[1] === 0x4D;
433
+ const buffer = Buffer.from(hexString, "hex");
434
+ const isValidBmp = buffer.length >= 14 && buffer[0] === 0x42 && buffer[1] === 0x4d;
308
435
  let width;
309
436
  let height;
310
437
  if (isValidBmp && buffer.length >= 54) {
@@ -317,20 +444,20 @@ class BitmapUtils {
317
444
  }
318
445
  }
319
446
  const pixelData = buffer.slice(62);
320
- const blackPixels = Array.from(pixelData).filter(b => b !== 0xFF).length;
447
+ const blackPixels = Array.from(pixelData).filter((b) => b !== 0xff).length;
321
448
  return {
322
449
  byteCount: buffer.length,
323
450
  blackPixels,
324
451
  width,
325
452
  height,
326
- isValidBmp
453
+ isValidBmp,
327
454
  };
328
455
  }
329
456
  catch (error) {
330
457
  return {
331
458
  byteCount: 0,
332
459
  blackPixels: 0,
333
- isValidBmp: false
460
+ isValidBmp: false,
334
461
  };
335
462
  }
336
463
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/sdk",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "Build apps for MentraOS smartglasses. This SDK provides everything you need to create real-time smartglasses applications.",
5
5
  "source": "src/index.ts",
6
6
  "main": "dist/index.js",
@@ -29,6 +29,7 @@
29
29
  "cookie-parser": "^1.4.7",
30
30
  "dotenv": "^16.4.0",
31
31
  "express": "^4.18.2",
32
+ "jimp": "^1.6.0",
32
33
  "jsonwebtoken": "^8.5.1",
33
34
  "jsrsasign": "^11.1.0",
34
35
  "multer": "^2.0.1",