lightdrift-libraw 1.0.0-alpha.1 → 1.0.0-alpha.3

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.
package/lib/index.js CHANGED
@@ -1,761 +1,2224 @@
1
- const path = require("path");
2
-
3
- let librawAddon;
4
- try {
5
- librawAddon = require("../build/Release/libraw_addon");
6
- } catch (err) {
7
- try {
8
- librawAddon = require("../build/Debug/libraw_addon");
9
- } catch (err2) {
10
- throw new Error('LibRaw addon not built. Run "npm run build" first.');
11
- }
12
- }
13
-
14
- class LibRaw {
15
- constructor() {
16
- this._wrapper = new librawAddon.LibRawWrapper();
17
- }
18
-
19
- // ============== FILE OPERATIONS ==============
20
-
21
- /**
22
- * Load a RAW file from filesystem
23
- * @param {string} filename - Path to the RAW file
24
- * @returns {Promise<boolean>} - Success status
25
- */
26
- async loadFile(filename) {
27
- return new Promise((resolve, reject) => {
28
- try {
29
- const result = this._wrapper.loadFile(filename);
30
- resolve(result);
31
- } catch (error) {
32
- reject(error);
33
- }
34
- });
35
- }
36
-
37
- /**
38
- * Load a RAW file from memory buffer
39
- * @param {Buffer} buffer - Buffer containing RAW data
40
- * @returns {Promise<boolean>} - Success status
41
- */
42
- async loadBuffer(buffer) {
43
- return new Promise((resolve, reject) => {
44
- try {
45
- const result = this._wrapper.loadBuffer(buffer);
46
- resolve(result);
47
- } catch (error) {
48
- reject(error);
49
- }
50
- });
51
- }
52
-
53
- /**
54
- * Close and cleanup resources
55
- * @returns {Promise<boolean>} - Success status
56
- */
57
- async close() {
58
- return new Promise((resolve, reject) => {
59
- try {
60
- const result = this._wrapper.close();
61
- resolve(result);
62
- } catch (error) {
63
- reject(error);
64
- }
65
- });
66
- }
67
-
68
- // ============== ERROR HANDLING ==============
69
-
70
- /**
71
- * Get the last error message
72
- * @returns {string} - Last error message
73
- */
74
- getLastError() {
75
- return this._wrapper.getLastError();
76
- }
77
-
78
- /**
79
- * Convert error code to string
80
- * @param {number} errorCode - Error code
81
- * @returns {string} - Error message
82
- */
83
- strerror(errorCode) {
84
- return this._wrapper.strerror(errorCode);
85
- }
86
-
87
- // ============== METADATA & INFORMATION ==============
88
-
89
- /**
90
- * Get basic metadata from the loaded RAW file
91
- * @returns {Promise<Object>} - Metadata object
92
- */
93
- async getMetadata() {
94
- return new Promise((resolve, reject) => {
95
- try {
96
- const metadata = this._wrapper.getMetadata();
97
- resolve(metadata);
98
- } catch (error) {
99
- reject(error);
100
- }
101
- });
102
- }
103
-
104
- /**
105
- * Get image dimensions and size information
106
- * @returns {Promise<Object>} - Size object with width/height details
107
- */
108
- async getImageSize() {
109
- return new Promise((resolve, reject) => {
110
- try {
111
- const size = this._wrapper.getImageSize();
112
- resolve(size);
113
- } catch (error) {
114
- reject(error);
115
- }
116
- });
117
- }
118
-
119
- /**
120
- * Get advanced metadata including color matrices and calibration data
121
- * @returns {Promise<Object>} - Advanced metadata object
122
- */
123
- async getAdvancedMetadata() {
124
- return new Promise((resolve, reject) => {
125
- try {
126
- const metadata = this._wrapper.getAdvancedMetadata();
127
- resolve(metadata);
128
- } catch (error) {
129
- reject(error);
130
- }
131
- });
132
- }
133
-
134
- /**
135
- * Get lens information
136
- * @returns {Promise<Object>} - Lens metadata object
137
- */
138
- async getLensInfo() {
139
- return new Promise((resolve, reject) => {
140
- try {
141
- const lensInfo = this._wrapper.getLensInfo();
142
- resolve(lensInfo);
143
- } catch (error) {
144
- reject(error);
145
- }
146
- });
147
- }
148
-
149
- /**
150
- * Get color information including white balance and color matrices
151
- * @returns {Promise<Object>} - Color information object
152
- */
153
- async getColorInfo() {
154
- return new Promise((resolve, reject) => {
155
- try {
156
- const colorInfo = this._wrapper.getColorInfo();
157
- resolve(colorInfo);
158
- } catch (error) {
159
- reject(error);
160
- }
161
- });
162
- }
163
-
164
- // ============== IMAGE PROCESSING ==============
165
-
166
- /**
167
- * Unpack thumbnail data
168
- * @returns {Promise<boolean>} - Success status
169
- */
170
- async unpackThumbnail() {
171
- return new Promise((resolve, reject) => {
172
- try {
173
- const result = this._wrapper.unpackThumbnail();
174
- resolve(result);
175
- } catch (error) {
176
- reject(error);
177
- }
178
- });
179
- }
180
-
181
- /**
182
- * Process the RAW image with current settings
183
- * @returns {Promise<boolean>} - Success status
184
- */
185
- async processImage() {
186
- return new Promise((resolve, reject) => {
187
- try {
188
- const result = this._wrapper.processImage();
189
- resolve(result);
190
- } catch (error) {
191
- reject(error);
192
- }
193
- });
194
- }
195
-
196
- /**
197
- * Subtract black level from RAW data
198
- * @returns {Promise<boolean>} - Success status
199
- */
200
- async subtractBlack() {
201
- return new Promise((resolve, reject) => {
202
- try {
203
- const result = this._wrapper.subtractBlack();
204
- resolve(result);
205
- } catch (error) {
206
- reject(error);
207
- }
208
- });
209
- }
210
-
211
- /**
212
- * Convert RAW data to image format
213
- * @returns {Promise<boolean>} - Success status
214
- */
215
- async raw2Image() {
216
- return new Promise((resolve, reject) => {
217
- try {
218
- const result = this._wrapper.raw2Image();
219
- resolve(result);
220
- } catch (error) {
221
- reject(error);
222
- }
223
- });
224
- }
225
-
226
- /**
227
- * Adjust maximum values in the image
228
- * @returns {Promise<boolean>} - Success status
229
- */
230
- async adjustMaximum() {
231
- return new Promise((resolve, reject) => {
232
- try {
233
- const result = this._wrapper.adjustMaximum();
234
- resolve(result);
235
- } catch (error) {
236
- reject(error);
237
- }
238
- });
239
- }
240
-
241
- // ============== MEMORY IMAGE CREATION ==============
242
-
243
- /**
244
- * Create processed image in memory
245
- * @returns {Promise<Object>} - Image data object with Buffer
246
- */
247
- async createMemoryImage() {
248
- return new Promise((resolve, reject) => {
249
- try {
250
- const imageData = this._wrapper.createMemoryImage();
251
- resolve(imageData);
252
- } catch (error) {
253
- reject(error);
254
- }
255
- });
256
- }
257
-
258
- /**
259
- * Create thumbnail image in memory
260
- * @returns {Promise<Object>} - Thumbnail data object with Buffer
261
- */
262
- async createMemoryThumbnail() {
263
- return new Promise((resolve, reject) => {
264
- try {
265
- const thumbData = this._wrapper.createMemoryThumbnail();
266
- resolve(thumbData);
267
- } catch (error) {
268
- reject(error);
269
- }
270
- });
271
- }
272
-
273
- // ============== FILE WRITERS ==============
274
-
275
- /**
276
- * Write processed image as PPM file
277
- * @param {string} filename - Output filename
278
- * @returns {Promise<boolean>} - Success status
279
- */
280
- async writePPM(filename) {
281
- return new Promise((resolve, reject) => {
282
- try {
283
- const result = this._wrapper.writePPM(filename);
284
- resolve(result);
285
- } catch (error) {
286
- reject(error);
287
- }
288
- });
289
- }
290
-
291
- /**
292
- * Write processed image as TIFF file
293
- * @param {string} filename - Output filename
294
- * @returns {Promise<boolean>} - Success status
295
- */
296
- async writeTIFF(filename) {
297
- return new Promise((resolve, reject) => {
298
- try {
299
- const result = this._wrapper.writeTIFF(filename);
300
- resolve(result);
301
- } catch (error) {
302
- reject(error);
303
- }
304
- });
305
- }
306
-
307
- /**
308
- * Write thumbnail to file
309
- * @param {string} filename - Output filename
310
- * @returns {Promise<boolean>} - Success status
311
- */
312
- async writeThumbnail(filename) {
313
- return new Promise((resolve, reject) => {
314
- try {
315
- const result = this._wrapper.writeThumbnail(filename);
316
- resolve(result);
317
- } catch (error) {
318
- reject(error);
319
- }
320
- });
321
- }
322
-
323
- // ============== CONFIGURATION & SETTINGS ==============
324
-
325
- /**
326
- * Set output parameters for processing
327
- * @param {Object} params - Parameter object
328
- * @returns {Promise<boolean>} - Success status
329
- */
330
- async setOutputParams(params) {
331
- return new Promise((resolve, reject) => {
332
- try {
333
- const result = this._wrapper.setOutputParams(params);
334
- resolve(result);
335
- } catch (error) {
336
- reject(error);
337
- }
338
- });
339
- }
340
-
341
- /**
342
- * Get current output parameters
343
- * @returns {Promise<Object>} - Current parameters
344
- */
345
- async getOutputParams() {
346
- return new Promise((resolve, reject) => {
347
- try {
348
- const params = this._wrapper.getOutputParams();
349
- resolve(params);
350
- } catch (error) {
351
- reject(error);
352
- }
353
- });
354
- }
355
-
356
- // ============== UTILITY FUNCTIONS ==============
357
-
358
- /**
359
- * Check if image uses floating point data
360
- * @returns {Promise<boolean>} - Floating point status
361
- */
362
- async isFloatingPoint() {
363
- return new Promise((resolve, reject) => {
364
- try {
365
- const result = this._wrapper.isFloatingPoint();
366
- resolve(result);
367
- } catch (error) {
368
- reject(error);
369
- }
370
- });
371
- }
372
-
373
- /**
374
- * Check if image is Fuji rotated
375
- * @returns {Promise<boolean>} - Fuji rotation status
376
- */
377
- async isFujiRotated() {
378
- return new Promise((resolve, reject) => {
379
- try {
380
- const result = this._wrapper.isFujiRotated();
381
- resolve(result);
382
- } catch (error) {
383
- reject(error);
384
- }
385
- });
386
- }
387
-
388
- /**
389
- * Check if image is sRAW format
390
- * @returns {Promise<boolean>} - sRAW status
391
- */
392
- async isSRAW() {
393
- return new Promise((resolve, reject) => {
394
- try {
395
- const result = this._wrapper.isSRAW();
396
- resolve(result);
397
- } catch (error) {
398
- reject(error);
399
- }
400
- });
401
- }
402
-
403
- /**
404
- * Check if thumbnail is JPEG format
405
- * @returns {Promise<boolean>} - JPEG thumbnail status
406
- */
407
- async isJPEGThumb() {
408
- return new Promise((resolve, reject) => {
409
- try {
410
- const result = this._wrapper.isJPEGThumb();
411
- resolve(result);
412
- } catch (error) {
413
- reject(error);
414
- }
415
- });
416
- }
417
-
418
- /**
419
- * Get error count during processing
420
- * @returns {Promise<number>} - Number of errors
421
- */
422
- async errorCount() {
423
- return new Promise((resolve, reject) => {
424
- try {
425
- const count = this._wrapper.errorCount();
426
- resolve(count);
427
- } catch (error) {
428
- reject(error);
429
- }
430
- });
431
- }
432
-
433
- // ============== EXTENDED UTILITY FUNCTIONS ==============
434
-
435
- /**
436
- * Check if image is Nikon sRAW format
437
- * @returns {Promise<boolean>} - True if Nikon sRAW
438
- */
439
- async isNikonSRAW() {
440
- return new Promise((resolve, reject) => {
441
- try {
442
- const result = this._wrapper.isNikonSRAW();
443
- resolve(result);
444
- } catch (error) {
445
- reject(error);
446
- }
447
- });
448
- }
449
-
450
- /**
451
- * Check if image is Coolscan NEF format
452
- * @returns {Promise<boolean>} - True if Coolscan NEF
453
- */
454
- async isCoolscanNEF() {
455
- return new Promise((resolve, reject) => {
456
- try {
457
- const result = this._wrapper.isCoolscanNEF();
458
- resolve(result);
459
- } catch (error) {
460
- reject(error);
461
- }
462
- });
463
- }
464
-
465
- /**
466
- * Check if image has floating point data
467
- * @returns {Promise<boolean>} - True if floating point data available
468
- */
469
- async haveFPData() {
470
- return new Promise((resolve, reject) => {
471
- try {
472
- const result = this._wrapper.haveFPData();
473
- resolve(result);
474
- } catch (error) {
475
- reject(error);
476
- }
477
- });
478
- }
479
-
480
- /**
481
- * Get sRAW midpoint value
482
- * @returns {Promise<number>} - sRAW midpoint
483
- */
484
- async srawMidpoint() {
485
- return new Promise((resolve, reject) => {
486
- try {
487
- const result = this._wrapper.srawMidpoint();
488
- resolve(result);
489
- } catch (error) {
490
- reject(error);
491
- }
492
- });
493
- }
494
-
495
- /**
496
- * Check if thumbnail is OK
497
- * @param {number} [maxSize=-1] - Maximum size limit
498
- * @returns {Promise<number>} - Thumbnail status
499
- */
500
- async thumbOK(maxSize = -1) {
501
- return new Promise((resolve, reject) => {
502
- try {
503
- const result = this._wrapper.thumbOK(maxSize);
504
- resolve(result);
505
- } catch (error) {
506
- reject(error);
507
- }
508
- });
509
- }
510
-
511
- /**
512
- * Get unpacker function name
513
- * @returns {Promise<string>} - Name of the unpacker function
514
- */
515
- async unpackFunctionName() {
516
- return new Promise((resolve, reject) => {
517
- try {
518
- const result = this._wrapper.unpackFunctionName();
519
- resolve(result);
520
- } catch (error) {
521
- reject(error);
522
- }
523
- });
524
- }
525
-
526
- /**
527
- * Get decoder information
528
- * @returns {Promise<Object>} - Decoder info with name and flags
529
- */
530
- async getDecoderInfo() {
531
- return new Promise((resolve, reject) => {
532
- try {
533
- const result = this._wrapper.getDecoderInfo();
534
- resolve(result);
535
- } catch (error) {
536
- reject(error);
537
- }
538
- });
539
- }
540
-
541
- // ============== ADVANCED PROCESSING ==============
542
-
543
- /**
544
- * Unpack RAW data (low-level operation)
545
- * @returns {Promise<boolean>} - Success status
546
- */
547
- async unpack() {
548
- return new Promise((resolve, reject) => {
549
- try {
550
- const result = this._wrapper.unpack();
551
- resolve(result);
552
- } catch (error) {
553
- reject(error);
554
- }
555
- });
556
- }
557
-
558
- /**
559
- * Convert RAW to image with extended options
560
- * @param {boolean} [subtractBlack=true] - Whether to subtract black level
561
- * @returns {Promise<boolean>} - Success status
562
- */
563
- async raw2ImageEx(subtractBlack = true) {
564
- return new Promise((resolve, reject) => {
565
- try {
566
- const result = this._wrapper.raw2ImageEx(subtractBlack);
567
- resolve(result);
568
- } catch (error) {
569
- reject(error);
570
- }
571
- });
572
- }
573
-
574
- /**
575
- * Adjust sizes for information only (no processing)
576
- * @returns {Promise<boolean>} - Success status
577
- */
578
- async adjustSizesInfoOnly() {
579
- return new Promise((resolve, reject) => {
580
- try {
581
- const result = this._wrapper.adjustSizesInfoOnly();
582
- resolve(result);
583
- } catch (error) {
584
- reject(error);
585
- }
586
- });
587
- }
588
-
589
- /**
590
- * Free processed image data
591
- * @returns {Promise<boolean>} - Success status
592
- */
593
- async freeImage() {
594
- return new Promise((resolve, reject) => {
595
- try {
596
- const result = this._wrapper.freeImage();
597
- resolve(result);
598
- } catch (error) {
599
- reject(error);
600
- }
601
- });
602
- }
603
-
604
- /**
605
- * Convert floating point to integer data
606
- * @param {number} [dmin=4096] - Minimum data value
607
- * @param {number} [dmax=32767] - Maximum data value
608
- * @param {number} [dtarget=16383] - Target value
609
- * @returns {Promise<boolean>} - Success status
610
- */
611
- async convertFloatToInt(dmin = 4096, dmax = 32767, dtarget = 16383) {
612
- return new Promise((resolve, reject) => {
613
- try {
614
- const result = this._wrapper.convertFloatToInt(dmin, dmax, dtarget);
615
- resolve(result);
616
- } catch (error) {
617
- reject(error);
618
- }
619
- });
620
- }
621
-
622
- // ============== MEMORY OPERATIONS EXTENDED ==============
623
-
624
- /**
625
- * Get memory image format information
626
- * @returns {Promise<Object>} - Format info with width, height, colors, bps
627
- */
628
- async getMemImageFormat() {
629
- return new Promise((resolve, reject) => {
630
- try {
631
- const result = this._wrapper.getMemImageFormat();
632
- resolve(result);
633
- } catch (error) {
634
- reject(error);
635
- }
636
- });
637
- }
638
-
639
- /**
640
- * Copy memory image to buffer
641
- * @param {Buffer} buffer - Destination buffer
642
- * @param {number} stride - Row stride in bytes
643
- * @param {boolean} bgr - Whether to use BGR order
644
- * @returns {Promise<boolean>} - Success status
645
- */
646
- async copyMemImage(buffer, stride, bgr = false) {
647
- return new Promise((resolve, reject) => {
648
- try {
649
- const result = this._wrapper.copyMemImage(buffer, stride, bgr);
650
- resolve(result);
651
- } catch (error) {
652
- reject(error);
653
- }
654
- });
655
- }
656
-
657
- // ============== COLOR OPERATIONS ==============
658
-
659
- /**
660
- * Get color filter at specific position
661
- * @param {number} row - Row position
662
- * @param {number} col - Column position
663
- * @returns {Promise<number>} - Color value
664
- */
665
- async getColorAt(row, col) {
666
- return new Promise((resolve, reject) => {
667
- try {
668
- const result = this._wrapper.getColorAt(row, col);
669
- resolve(result);
670
- } catch (error) {
671
- reject(error);
672
- }
673
- });
674
- }
675
-
676
- // ============== CANCELLATION SUPPORT ==============
677
-
678
- /**
679
- * Set cancellation flag to stop processing
680
- * @returns {Promise<boolean>} - Success status
681
- */
682
- async setCancelFlag() {
683
- return new Promise((resolve, reject) => {
684
- try {
685
- const result = this._wrapper.setCancelFlag();
686
- resolve(result);
687
- } catch (error) {
688
- reject(error);
689
- }
690
- });
691
- }
692
-
693
- /**
694
- * Clear cancellation flag
695
- * @returns {Promise<boolean>} - Success status
696
- */
697
- async clearCancelFlag() {
698
- return new Promise((resolve, reject) => {
699
- try {
700
- const result = this._wrapper.clearCancelFlag();
701
- resolve(result);
702
- } catch (error) {
703
- reject(error);
704
- }
705
- });
706
- }
707
-
708
- // ============== VERSION INFORMATION (INSTANCE METHODS) ==============
709
-
710
- /**
711
- * Get LibRaw version string
712
- * @returns {string} - Version string
713
- */
714
- version() {
715
- return this._wrapper.version();
716
- }
717
-
718
- /**
719
- * Get LibRaw version as array [major, minor, patch]
720
- * @returns {number[]} - Version number array
721
- */
722
- versionNumber() {
723
- return this._wrapper.versionNumber();
724
- }
725
-
726
- // ============== STATIC METHODS ==============
727
-
728
- /**
729
- * Get LibRaw version
730
- * @returns {string} - Version string
731
- */
732
- static getVersion() {
733
- return librawAddon.LibRawWrapper.getVersion();
734
- }
735
-
736
- /**
737
- * Get LibRaw capabilities
738
- * @returns {number} - Capabilities flags
739
- */
740
- static getCapabilities() {
741
- return librawAddon.LibRawWrapper.getCapabilities();
742
- }
743
-
744
- /**
745
- * Get list of supported cameras
746
- * @returns {string[]} - Array of camera names
747
- */
748
- static getCameraList() {
749
- return librawAddon.LibRawWrapper.getCameraList();
750
- }
751
-
752
- /**
753
- * Get count of supported cameras
754
- * @returns {number} - Number of supported cameras
755
- */
756
- static getCameraCount() {
757
- return librawAddon.LibRawWrapper.getCameraCount();
758
- }
759
- }
760
-
761
- module.exports = LibRaw;
1
+ const path = require("path");
2
+ const sharp = require("sharp");
3
+
4
+ let librawAddon;
5
+ try {
6
+ librawAddon = require("../build/Release/libraw_addon");
7
+ } catch (err) {
8
+ try {
9
+ librawAddon = require("../build/Debug/libraw_addon");
10
+ } catch (err2) {
11
+ throw new Error('LibRaw addon not built. Run "npm run build" first.');
12
+ }
13
+ }
14
+
15
+ class LibRaw {
16
+ constructor() {
17
+ this._wrapper = new librawAddon.LibRawWrapper();
18
+ this._isProcessed = false; // Track if processImage() has been called
19
+ this._processedImageData = null; // Cache processed image data
20
+ }
21
+
22
+ // ============== FILE OPERATIONS ==============
23
+
24
+ /**
25
+ * Load a RAW file from filesystem
26
+ * @param {string} filename - Path to the RAW file
27
+ * @returns {Promise<boolean>} - Success status
28
+ */
29
+ async loadFile(filename) {
30
+ return new Promise((resolve, reject) => {
31
+ try {
32
+ const result = this._wrapper.loadFile(filename);
33
+ this._isProcessed = false; // Reset processing state for new file
34
+ this._processedImageData = null; // Clear cached data
35
+ resolve(result);
36
+ } catch (error) {
37
+ reject(error);
38
+ }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Load a RAW file from memory buffer
44
+ * @param {Buffer} buffer - Buffer containing RAW data
45
+ * @returns {Promise<boolean>} - Success status
46
+ */
47
+ async loadBuffer(buffer) {
48
+ return new Promise((resolve, reject) => {
49
+ try {
50
+ const result = this._wrapper.loadBuffer(buffer);
51
+ resolve(result);
52
+ } catch (error) {
53
+ reject(error);
54
+ }
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Close and cleanup resources
60
+ * @returns {Promise<boolean>} - Success status
61
+ */
62
+ async close() {
63
+ return new Promise((resolve, reject) => {
64
+ try {
65
+ const result = this._wrapper.close();
66
+ resolve(result);
67
+ } catch (error) {
68
+ reject(error);
69
+ }
70
+ });
71
+ }
72
+
73
+ // ============== ERROR HANDLING ==============
74
+
75
+ /**
76
+ * Get the last error message
77
+ * @returns {string} - Last error message
78
+ */
79
+ getLastError() {
80
+ return this._wrapper.getLastError();
81
+ }
82
+
83
+ /**
84
+ * Convert error code to string
85
+ * @param {number} errorCode - Error code
86
+ * @returns {string} - Error message
87
+ */
88
+ strerror(errorCode) {
89
+ return this._wrapper.strerror(errorCode);
90
+ }
91
+
92
+ // ============== METADATA & INFORMATION ==============
93
+
94
+ /**
95
+ * Get basic metadata from the loaded RAW file
96
+ * @returns {Promise<Object>} - Metadata object
97
+ */
98
+ async getMetadata() {
99
+ return new Promise((resolve, reject) => {
100
+ try {
101
+ const metadata = this._wrapper.getMetadata();
102
+ resolve(metadata);
103
+ } catch (error) {
104
+ reject(error);
105
+ }
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Get image dimensions and size information
111
+ * @returns {Promise<Object>} - Size object with width/height details
112
+ */
113
+ async getImageSize() {
114
+ return new Promise((resolve, reject) => {
115
+ try {
116
+ const size = this._wrapper.getImageSize();
117
+ resolve(size);
118
+ } catch (error) {
119
+ reject(error);
120
+ }
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Get advanced metadata including color matrices and calibration data
126
+ * @returns {Promise<Object>} - Advanced metadata object
127
+ */
128
+ async getAdvancedMetadata() {
129
+ return new Promise((resolve, reject) => {
130
+ try {
131
+ const metadata = this._wrapper.getAdvancedMetadata();
132
+ resolve(metadata);
133
+ } catch (error) {
134
+ reject(error);
135
+ }
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Get lens information
141
+ * @returns {Promise<Object>} - Lens metadata object
142
+ */
143
+ async getLensInfo() {
144
+ return new Promise((resolve, reject) => {
145
+ try {
146
+ const lensInfo = this._wrapper.getLensInfo();
147
+ resolve(lensInfo);
148
+ } catch (error) {
149
+ reject(error);
150
+ }
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Get color information including white balance and color matrices
156
+ * @returns {Promise<Object>} - Color information object
157
+ */
158
+ async getColorInfo() {
159
+ return new Promise((resolve, reject) => {
160
+ try {
161
+ const colorInfo = this._wrapper.getColorInfo();
162
+ resolve(colorInfo);
163
+ } catch (error) {
164
+ reject(error);
165
+ }
166
+ });
167
+ }
168
+
169
+ // ============== IMAGE PROCESSING ==============
170
+
171
+ /**
172
+ * Unpack thumbnail data
173
+ * @returns {Promise<boolean>} - Success status
174
+ */
175
+ async unpackThumbnail() {
176
+ return new Promise((resolve, reject) => {
177
+ try {
178
+ const result = this._wrapper.unpackThumbnail();
179
+ resolve(result);
180
+ } catch (error) {
181
+ reject(error);
182
+ }
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Process the RAW image with current settings
188
+ * @returns {Promise<boolean>} - Success status
189
+ */
190
+ async processImage() {
191
+ return new Promise((resolve, reject) => {
192
+ try {
193
+ const result = this._wrapper.processImage();
194
+ this._isProcessed = true; // Mark as processed
195
+ resolve(result);
196
+ } catch (error) {
197
+ reject(error);
198
+ }
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Subtract black level from RAW data
204
+ * @returns {Promise<boolean>} - Success status
205
+ */
206
+ async subtractBlack() {
207
+ return new Promise((resolve, reject) => {
208
+ try {
209
+ const result = this._wrapper.subtractBlack();
210
+ resolve(result);
211
+ } catch (error) {
212
+ reject(error);
213
+ }
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Convert RAW data to image format
219
+ * @returns {Promise<boolean>} - Success status
220
+ */
221
+ async raw2Image() {
222
+ return new Promise((resolve, reject) => {
223
+ try {
224
+ const result = this._wrapper.raw2Image();
225
+ resolve(result);
226
+ } catch (error) {
227
+ reject(error);
228
+ }
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Adjust maximum values in the image
234
+ * @returns {Promise<boolean>} - Success status
235
+ */
236
+ async adjustMaximum() {
237
+ return new Promise((resolve, reject) => {
238
+ try {
239
+ const result = this._wrapper.adjustMaximum();
240
+ resolve(result);
241
+ } catch (error) {
242
+ reject(error);
243
+ }
244
+ });
245
+ }
246
+
247
+ // ============== MEMORY IMAGE CREATION ==============
248
+
249
+ /**
250
+ * Create processed image in memory
251
+ * @returns {Promise<Object>} - Image data object with Buffer
252
+ */
253
+ async createMemoryImage() {
254
+ return new Promise((resolve, reject) => {
255
+ try {
256
+ // Return cached data if available
257
+ if (this._processedImageData) {
258
+ resolve(this._processedImageData);
259
+ return;
260
+ }
261
+
262
+ const imageData = this._wrapper.createMemoryImage();
263
+
264
+ // Cache the result if image was processed
265
+ if (this._isProcessed) {
266
+ this._processedImageData = imageData;
267
+ }
268
+
269
+ resolve(imageData);
270
+ } catch (error) {
271
+ reject(error);
272
+ }
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Create thumbnail image in memory
278
+ * @returns {Promise<Object>} - Thumbnail data object with Buffer
279
+ */
280
+ async createMemoryThumbnail() {
281
+ return new Promise((resolve, reject) => {
282
+ try {
283
+ const thumbData = this._wrapper.createMemoryThumbnail();
284
+ resolve(thumbData);
285
+ } catch (error) {
286
+ reject(error);
287
+ }
288
+ });
289
+ }
290
+
291
+ // ============== FILE WRITERS ==============
292
+
293
+ /**
294
+ * Write processed image as PPM file
295
+ * @param {string} filename - Output filename
296
+ * @returns {Promise<boolean>} - Success status
297
+ */
298
+ async writePPM(filename) {
299
+ return new Promise((resolve, reject) => {
300
+ try {
301
+ const result = this._wrapper.writePPM(filename);
302
+ resolve(result);
303
+ } catch (error) {
304
+ reject(error);
305
+ }
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Write processed image as TIFF file
311
+ * @param {string} filename - Output filename
312
+ * @returns {Promise<boolean>} - Success status
313
+ */
314
+ async writeTIFF(filename) {
315
+ return new Promise((resolve, reject) => {
316
+ try {
317
+ const result = this._wrapper.writeTIFF(filename);
318
+ resolve(result);
319
+ } catch (error) {
320
+ reject(error);
321
+ }
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Write thumbnail to file
327
+ * @param {string} filename - Output filename
328
+ * @returns {Promise<boolean>} - Success status
329
+ */
330
+ async writeThumbnail(filename) {
331
+ return new Promise((resolve, reject) => {
332
+ try {
333
+ const result = this._wrapper.writeThumbnail(filename);
334
+ resolve(result);
335
+ } catch (error) {
336
+ reject(error);
337
+ }
338
+ });
339
+ }
340
+
341
+ // ============== CONFIGURATION & SETTINGS ==============
342
+
343
+ /**
344
+ * Set output parameters for processing
345
+ * @param {Object} params - Parameter object
346
+ * @returns {Promise<boolean>} - Success status
347
+ */
348
+ async setOutputParams(params) {
349
+ return new Promise((resolve, reject) => {
350
+ try {
351
+ const result = this._wrapper.setOutputParams(params);
352
+ resolve(result);
353
+ } catch (error) {
354
+ reject(error);
355
+ }
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Get current output parameters
361
+ * @returns {Promise<Object>} - Current parameters
362
+ */
363
+ async getOutputParams() {
364
+ return new Promise((resolve, reject) => {
365
+ try {
366
+ const params = this._wrapper.getOutputParams();
367
+ resolve(params);
368
+ } catch (error) {
369
+ reject(error);
370
+ }
371
+ });
372
+ }
373
+
374
+ // ============== UTILITY FUNCTIONS ==============
375
+
376
+ /**
377
+ * Check if image uses floating point data
378
+ * @returns {Promise<boolean>} - Floating point status
379
+ */
380
+ async isFloatingPoint() {
381
+ return new Promise((resolve, reject) => {
382
+ try {
383
+ const result = this._wrapper.isFloatingPoint();
384
+ resolve(result);
385
+ } catch (error) {
386
+ reject(error);
387
+ }
388
+ });
389
+ }
390
+
391
+ /**
392
+ * Check if image is Fuji rotated
393
+ * @returns {Promise<boolean>} - Fuji rotation status
394
+ */
395
+ async isFujiRotated() {
396
+ return new Promise((resolve, reject) => {
397
+ try {
398
+ const result = this._wrapper.isFujiRotated();
399
+ resolve(result);
400
+ } catch (error) {
401
+ reject(error);
402
+ }
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Check if image is sRAW format
408
+ * @returns {Promise<boolean>} - sRAW status
409
+ */
410
+ async isSRAW() {
411
+ return new Promise((resolve, reject) => {
412
+ try {
413
+ const result = this._wrapper.isSRAW();
414
+ resolve(result);
415
+ } catch (error) {
416
+ reject(error);
417
+ }
418
+ });
419
+ }
420
+
421
+ /**
422
+ * Check if thumbnail is JPEG format
423
+ * @returns {Promise<boolean>} - JPEG thumbnail status
424
+ */
425
+ async isJPEGThumb() {
426
+ return new Promise((resolve, reject) => {
427
+ try {
428
+ const result = this._wrapper.isJPEGThumb();
429
+ resolve(result);
430
+ } catch (error) {
431
+ reject(error);
432
+ }
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Get error count during processing
438
+ * @returns {Promise<number>} - Number of errors
439
+ */
440
+ async errorCount() {
441
+ return new Promise((resolve, reject) => {
442
+ try {
443
+ const count = this._wrapper.errorCount();
444
+ resolve(count);
445
+ } catch (error) {
446
+ reject(error);
447
+ }
448
+ });
449
+ }
450
+
451
+ // ============== EXTENDED UTILITY FUNCTIONS ==============
452
+
453
+ /**
454
+ * Check if image is Nikon sRAW format
455
+ * @returns {Promise<boolean>} - True if Nikon sRAW
456
+ */
457
+ async isNikonSRAW() {
458
+ return new Promise((resolve, reject) => {
459
+ try {
460
+ const result = this._wrapper.isNikonSRAW();
461
+ resolve(result);
462
+ } catch (error) {
463
+ reject(error);
464
+ }
465
+ });
466
+ }
467
+
468
+ /**
469
+ * Check if image is Coolscan NEF format
470
+ * @returns {Promise<boolean>} - True if Coolscan NEF
471
+ */
472
+ async isCoolscanNEF() {
473
+ return new Promise((resolve, reject) => {
474
+ try {
475
+ const result = this._wrapper.isCoolscanNEF();
476
+ resolve(result);
477
+ } catch (error) {
478
+ reject(error);
479
+ }
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Check if image has floating point data
485
+ * @returns {Promise<boolean>} - True if floating point data available
486
+ */
487
+ async haveFPData() {
488
+ return new Promise((resolve, reject) => {
489
+ try {
490
+ const result = this._wrapper.haveFPData();
491
+ resolve(result);
492
+ } catch (error) {
493
+ reject(error);
494
+ }
495
+ });
496
+ }
497
+
498
+ /**
499
+ * Get sRAW midpoint value
500
+ * @returns {Promise<number>} - sRAW midpoint
501
+ */
502
+ async srawMidpoint() {
503
+ return new Promise((resolve, reject) => {
504
+ try {
505
+ const result = this._wrapper.srawMidpoint();
506
+ resolve(result);
507
+ } catch (error) {
508
+ reject(error);
509
+ }
510
+ });
511
+ }
512
+
513
+ /**
514
+ * Check if thumbnail is OK
515
+ * @param {number} [maxSize=-1] - Maximum size limit
516
+ * @returns {Promise<number>} - Thumbnail status
517
+ */
518
+ async thumbOK(maxSize = -1) {
519
+ return new Promise((resolve, reject) => {
520
+ try {
521
+ const result = this._wrapper.thumbOK(maxSize);
522
+ resolve(result);
523
+ } catch (error) {
524
+ reject(error);
525
+ }
526
+ });
527
+ }
528
+
529
+ /**
530
+ * Get unpacker function name
531
+ * @returns {Promise<string>} - Name of the unpacker function
532
+ */
533
+ async unpackFunctionName() {
534
+ return new Promise((resolve, reject) => {
535
+ try {
536
+ const result = this._wrapper.unpackFunctionName();
537
+ resolve(result);
538
+ } catch (error) {
539
+ reject(error);
540
+ }
541
+ });
542
+ }
543
+
544
+ /**
545
+ * Get decoder information
546
+ * @returns {Promise<Object>} - Decoder info with name and flags
547
+ */
548
+ async getDecoderInfo() {
549
+ return new Promise((resolve, reject) => {
550
+ try {
551
+ const result = this._wrapper.getDecoderInfo();
552
+ resolve(result);
553
+ } catch (error) {
554
+ reject(error);
555
+ }
556
+ });
557
+ }
558
+
559
+ // ============== ADVANCED PROCESSING ==============
560
+
561
+ /**
562
+ * Unpack RAW data (low-level operation)
563
+ * @returns {Promise<boolean>} - Success status
564
+ */
565
+ async unpack() {
566
+ return new Promise((resolve, reject) => {
567
+ try {
568
+ const result = this._wrapper.unpack();
569
+ resolve(result);
570
+ } catch (error) {
571
+ reject(error);
572
+ }
573
+ });
574
+ }
575
+
576
+ /**
577
+ * Convert RAW to image with extended options
578
+ * @param {boolean} [subtractBlack=true] - Whether to subtract black level
579
+ * @returns {Promise<boolean>} - Success status
580
+ */
581
+ async raw2ImageEx(subtractBlack = true) {
582
+ return new Promise((resolve, reject) => {
583
+ try {
584
+ const result = this._wrapper.raw2ImageEx(subtractBlack);
585
+ resolve(result);
586
+ } catch (error) {
587
+ reject(error);
588
+ }
589
+ });
590
+ }
591
+
592
+ /**
593
+ * Adjust sizes for information only (no processing)
594
+ * @returns {Promise<boolean>} - Success status
595
+ */
596
+ async adjustSizesInfoOnly() {
597
+ return new Promise((resolve, reject) => {
598
+ try {
599
+ const result = this._wrapper.adjustSizesInfoOnly();
600
+ resolve(result);
601
+ } catch (error) {
602
+ reject(error);
603
+ }
604
+ });
605
+ }
606
+
607
+ /**
608
+ * Free processed image data
609
+ * @returns {Promise<boolean>} - Success status
610
+ */
611
+ async freeImage() {
612
+ return new Promise((resolve, reject) => {
613
+ try {
614
+ const result = this._wrapper.freeImage();
615
+ resolve(result);
616
+ } catch (error) {
617
+ reject(error);
618
+ }
619
+ });
620
+ }
621
+
622
+ /**
623
+ * Convert floating point to integer data
624
+ * @param {number} [dmin=4096] - Minimum data value
625
+ * @param {number} [dmax=32767] - Maximum data value
626
+ * @param {number} [dtarget=16383] - Target value
627
+ * @returns {Promise<boolean>} - Success status
628
+ */
629
+ async convertFloatToInt(dmin = 4096, dmax = 32767, dtarget = 16383) {
630
+ return new Promise((resolve, reject) => {
631
+ try {
632
+ const result = this._wrapper.convertFloatToInt(dmin, dmax, dtarget);
633
+ resolve(result);
634
+ } catch (error) {
635
+ reject(error);
636
+ }
637
+ });
638
+ }
639
+
640
+ // ============== MEMORY OPERATIONS EXTENDED ==============
641
+
642
+ /**
643
+ * Get memory image format information
644
+ * @returns {Promise<Object>} - Format info with width, height, colors, bps
645
+ */
646
+ async getMemImageFormat() {
647
+ return new Promise((resolve, reject) => {
648
+ try {
649
+ const result = this._wrapper.getMemImageFormat();
650
+ resolve(result);
651
+ } catch (error) {
652
+ reject(error);
653
+ }
654
+ });
655
+ }
656
+
657
+ /**
658
+ * Copy memory image to buffer
659
+ * @param {Buffer} buffer - Destination buffer
660
+ * @param {number} stride - Row stride in bytes
661
+ * @param {boolean} bgr - Whether to use BGR order
662
+ * @returns {Promise<boolean>} - Success status
663
+ */
664
+ async copyMemImage(buffer, stride, bgr = false) {
665
+ return new Promise((resolve, reject) => {
666
+ try {
667
+ const result = this._wrapper.copyMemImage(buffer, stride, bgr);
668
+ resolve(result);
669
+ } catch (error) {
670
+ reject(error);
671
+ }
672
+ });
673
+ }
674
+
675
+ // ============== COLOR OPERATIONS ==============
676
+
677
+ /**
678
+ * Get color filter at specific position
679
+ * @param {number} row - Row position
680
+ * @param {number} col - Column position
681
+ * @returns {Promise<number>} - Color value
682
+ */
683
+ async getColorAt(row, col) {
684
+ return new Promise((resolve, reject) => {
685
+ try {
686
+ const result = this._wrapper.getColorAt(row, col);
687
+ resolve(result);
688
+ } catch (error) {
689
+ reject(error);
690
+ }
691
+ });
692
+ }
693
+
694
+ // ============== MEMORY STREAM OPERATIONS (NEW FEATURE) ==============
695
+
696
+ /**
697
+ * Create processed image as JPEG buffer in memory
698
+ * @param {Object} options - JPEG conversion options
699
+ * @param {number} [options.quality=85] - JPEG quality (1-100)
700
+ * @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
701
+ * @param {number} [options.height] - Target height (maintains aspect ratio if width not specified)
702
+ * @param {boolean} [options.progressive=false] - Use progressive JPEG
703
+ * @param {boolean} [options.mozjpeg=true] - Use mozjpeg encoder for better compression
704
+ * @param {number} [options.chromaSubsampling='4:2:0'] - Chroma subsampling ('4:4:4', '4:2:2', '4:2:0')
705
+ * @param {boolean} [options.trellisQuantisation=false] - Enable trellis quantisation
706
+ * @param {boolean} [options.optimizeScans=false] - Optimize scan order
707
+ * @param {number} [options.overshootDeringing=false] - Overshoot deringing
708
+ * @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
709
+ * @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
710
+ * @returns {Promise<Object>} - JPEG buffer with metadata
711
+ */
712
+ async createJPEGBuffer(options = {}) {
713
+ return new Promise(async (resolve, reject) => {
714
+ try {
715
+ // Set default options with performance-optimized values
716
+ const opts = {
717
+ quality: options.quality || 85,
718
+ progressive: options.progressive || false,
719
+ mozjpeg: options.mozjpeg !== false, // Default to true for better compression
720
+ chromaSubsampling: options.chromaSubsampling || "4:2:0",
721
+ trellisQuantisation: options.trellisQuantisation || false,
722
+ optimizeScans: options.optimizeScans || false,
723
+ overshootDeringing: options.overshootDeringing || false,
724
+ optimizeCoding: options.optimizeCoding !== false, // Default to true
725
+ colorSpace: options.colorSpace || "srgb",
726
+ ...options,
727
+ };
728
+
729
+ const startTime = process.hrtime.bigint();
730
+
731
+ // Smart processing: only process if not already processed
732
+ if (!this._isProcessed) {
733
+ await this.processImage();
734
+ }
735
+
736
+ // Create processed image in memory (uses cache if available)
737
+ const imageData = await this.createMemoryImage();
738
+
739
+ if (!imageData || !imageData.data) {
740
+ throw new Error("Failed to create memory image from RAW data");
741
+ }
742
+
743
+ // Convert the LibRaw RGB data to Sharp-compatible buffer
744
+ let sharpInstance;
745
+
746
+ // Determine if this is a large image for performance optimizations
747
+ const isLargeImage = imageData.width * imageData.height > 20_000_000; // > 20MP
748
+ const fastMode = opts.fastMode !== false; // Default to fast mode
749
+
750
+ // Optimized Sharp configuration
751
+ const sharpConfig = {
752
+ raw: {
753
+ width: imageData.width,
754
+ height: imageData.height,
755
+ channels: imageData.colors,
756
+ premultiplied: false,
757
+ },
758
+ // Performance optimizations
759
+ sequentialRead: true,
760
+ limitInputPixels: false,
761
+ density: fastMode ? 72 : 300, // Lower DPI for speed
762
+ };
763
+
764
+ if (imageData.bits === 16) {
765
+ sharpConfig.raw.depth = "ushort";
766
+ }
767
+
768
+ sharpInstance = sharp(imageData.data, sharpConfig);
769
+
770
+ // Apply resizing if specified with performance optimizations
771
+ if (opts.width || opts.height) {
772
+ const resizeOptions = {
773
+ withoutEnlargement: true,
774
+ // Use faster kernel for large images or when fast mode is enabled
775
+ kernel:
776
+ isLargeImage || fastMode
777
+ ? sharp.kernel.cubic
778
+ : sharp.kernel.lanczos3,
779
+ fit: "inside",
780
+ fastShrinkOnLoad: true, // Enable fast shrink-on-load optimization
781
+ };
782
+
783
+ if (opts.width && opts.height) {
784
+ sharpInstance = sharpInstance.resize(
785
+ opts.width,
786
+ opts.height,
787
+ resizeOptions
788
+ );
789
+ } else if (opts.width) {
790
+ sharpInstance = sharpInstance.resize(
791
+ opts.width,
792
+ null,
793
+ resizeOptions
794
+ );
795
+ } else {
796
+ sharpInstance = sharpInstance.resize(
797
+ null,
798
+ opts.height,
799
+ resizeOptions
800
+ );
801
+ }
802
+ }
803
+
804
+ // Configure color space
805
+ switch (opts.colorSpace.toLowerCase()) {
806
+ case "rec2020":
807
+ sharpInstance = sharpInstance.toColorspace("rec2020");
808
+ break;
809
+ case "p3":
810
+ sharpInstance = sharpInstance.toColorspace("p3");
811
+ break;
812
+ case "cmyk":
813
+ sharpInstance = sharpInstance.toColorspace("cmyk");
814
+ break;
815
+ case "srgb":
816
+ default:
817
+ sharpInstance = sharpInstance.toColorspace("srgb");
818
+ break;
819
+ }
820
+
821
+ // Configure JPEG options with performance optimizations
822
+ const jpegOptions = {
823
+ quality: Math.max(1, Math.min(100, opts.quality)),
824
+ progressive: fastMode ? false : opts.progressive, // Disable progressive for speed
825
+ mozjpeg: fastMode ? false : opts.mozjpeg, // Disable mozjpeg for speed
826
+ trellisQuantisation: fastMode ? false : opts.trellisQuantisation,
827
+ optimizeScans: fastMode ? false : opts.optimizeScans,
828
+ overshootDeringing: false, // Always disable for speed
829
+ optimizeCoding: fastMode ? false : opts.optimizeCoding,
830
+ // Add effort control for JPEG encoding
831
+ effort: fastMode ? 1 : Math.min(opts.effort || 4, 6),
832
+ };
833
+
834
+ // Set chroma subsampling
835
+ switch (opts.chromaSubsampling) {
836
+ case "4:4:4":
837
+ jpegOptions.chromaSubsampling = "4:4:4";
838
+ break;
839
+ case "4:2:2":
840
+ jpegOptions.chromaSubsampling = "4:4:4"; // Sharp doesn't support 4:2:2, use 4:4:4 instead
841
+ break;
842
+ case "4:2:0":
843
+ default:
844
+ jpegOptions.chromaSubsampling = "4:2:0";
845
+ break;
846
+ }
847
+
848
+ // Convert to JPEG and get buffer
849
+ const jpegBuffer = await sharpInstance
850
+ .jpeg(jpegOptions)
851
+ .toBuffer({ resolveWithObject: true });
852
+
853
+ const endTime = process.hrtime.bigint();
854
+ const processingTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
855
+
856
+ // Calculate compression ratio
857
+ const originalSize = imageData.dataSize;
858
+ const compressedSize = jpegBuffer.data.length;
859
+ const compressionRatio = originalSize / compressedSize;
860
+
861
+ const result = {
862
+ success: true,
863
+ buffer: jpegBuffer.data,
864
+ metadata: {
865
+ originalDimensions: {
866
+ width: imageData.width,
867
+ height: imageData.height,
868
+ },
869
+ outputDimensions: {
870
+ width: jpegBuffer.info.width,
871
+ height: jpegBuffer.info.height,
872
+ },
873
+ fileSize: {
874
+ original: originalSize,
875
+ compressed: compressedSize,
876
+ compressionRatio: compressionRatio.toFixed(2),
877
+ },
878
+ processing: {
879
+ timeMs: processingTime.toFixed(2),
880
+ throughputMBps: (
881
+ originalSize /
882
+ 1024 /
883
+ 1024 /
884
+ (processingTime / 1000)
885
+ ).toFixed(2),
886
+ },
887
+ jpegOptions: jpegOptions,
888
+ },
889
+ };
890
+
891
+ resolve(result);
892
+ } catch (error) {
893
+ reject(new Error(`JPEG buffer creation failed: ${error.message}`));
894
+ }
895
+ });
896
+ }
897
+
898
+ /**
899
+ * Create processed image as PNG buffer in memory
900
+ * @param {Object} options - PNG conversion options
901
+ * @param {number} [options.width] - Target width
902
+ * @param {number} [options.height] - Target height
903
+ * @param {number} [options.compressionLevel=6] - PNG compression level (0-9)
904
+ * @param {boolean} [options.progressive=false] - Use progressive PNG
905
+ * @param {string} [options.colorSpace='srgb'] - Output color space
906
+ * @returns {Promise<Object>} - PNG buffer with metadata
907
+ */
908
+ async createPNGBuffer(options = {}) {
909
+ return new Promise(async (resolve, reject) => {
910
+ try {
911
+ const startTime = process.hrtime.bigint();
912
+
913
+ // Smart processing: only process if not already processed
914
+ if (!this._isProcessed) {
915
+ await this.processImage();
916
+ }
917
+
918
+ // Create processed image in memory (uses cache if available)
919
+ const imageData = await this.createMemoryImage();
920
+
921
+ if (!imageData || !imageData.data) {
922
+ throw new Error("Failed to create memory image from RAW data");
923
+ }
924
+
925
+ // Set up Sharp configuration
926
+ const sharpConfig = {
927
+ raw: {
928
+ width: imageData.width,
929
+ height: imageData.height,
930
+ channels: imageData.colors,
931
+ premultiplied: false,
932
+ },
933
+ sequentialRead: true,
934
+ limitInputPixels: false,
935
+ };
936
+
937
+ if (imageData.bits === 16) {
938
+ sharpConfig.raw.depth = "ushort";
939
+ }
940
+
941
+ let sharpInstance = sharp(imageData.data, sharpConfig);
942
+
943
+ // Apply resizing if specified
944
+ if (options.width || options.height) {
945
+ const resizeOptions = {
946
+ withoutEnlargement: true,
947
+ kernel: sharp.kernel.lanczos3,
948
+ fit: "inside",
949
+ fastShrinkOnLoad: true,
950
+ };
951
+
952
+ if (options.width && options.height) {
953
+ sharpInstance = sharpInstance.resize(
954
+ options.width,
955
+ options.height,
956
+ resizeOptions
957
+ );
958
+ } else if (options.width) {
959
+ sharpInstance = sharpInstance.resize(
960
+ options.width,
961
+ null,
962
+ resizeOptions
963
+ );
964
+ } else {
965
+ sharpInstance = sharpInstance.resize(
966
+ null,
967
+ options.height,
968
+ resizeOptions
969
+ );
970
+ }
971
+ }
972
+
973
+ // Configure color space
974
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
975
+ case "rec2020":
976
+ sharpInstance = sharpInstance.toColorspace("rec2020");
977
+ break;
978
+ case "p3":
979
+ sharpInstance = sharpInstance.toColorspace("p3");
980
+ break;
981
+ case "srgb":
982
+ default:
983
+ sharpInstance = sharpInstance.toColorspace("srgb");
984
+ break;
985
+ }
986
+
987
+ // Configure PNG options
988
+ const pngOptions = {
989
+ compressionLevel: Math.max(
990
+ 0,
991
+ Math.min(9, options.compressionLevel || 6)
992
+ ),
993
+ progressive: options.progressive || false,
994
+ quality: 100, // PNG is lossless
995
+ };
996
+
997
+ // Convert to PNG and get buffer
998
+ const pngBuffer = await sharpInstance
999
+ .png(pngOptions)
1000
+ .toBuffer({ resolveWithObject: true });
1001
+
1002
+ const endTime = process.hrtime.bigint();
1003
+ const processingTime = Number(endTime - startTime) / 1000000;
1004
+
1005
+ const result = {
1006
+ success: true,
1007
+ buffer: pngBuffer.data,
1008
+ metadata: {
1009
+ originalDimensions: {
1010
+ width: imageData.width,
1011
+ height: imageData.height,
1012
+ },
1013
+ outputDimensions: {
1014
+ width: pngBuffer.info.width,
1015
+ height: pngBuffer.info.height,
1016
+ },
1017
+ fileSize: {
1018
+ original: imageData.dataSize,
1019
+ compressed: pngBuffer.data.length,
1020
+ compressionRatio: (
1021
+ imageData.dataSize / pngBuffer.data.length
1022
+ ).toFixed(2),
1023
+ },
1024
+ processing: {
1025
+ timeMs: processingTime.toFixed(2),
1026
+ throughputMBps: (
1027
+ imageData.dataSize /
1028
+ 1024 /
1029
+ 1024 /
1030
+ (processingTime / 1000)
1031
+ ).toFixed(2),
1032
+ },
1033
+ pngOptions: pngOptions,
1034
+ },
1035
+ };
1036
+
1037
+ resolve(result);
1038
+ } catch (error) {
1039
+ reject(new Error(`PNG buffer creation failed: ${error.message}`));
1040
+ }
1041
+ });
1042
+ }
1043
+
1044
+ /**
1045
+ * Create processed image as TIFF buffer in memory
1046
+ * @param {Object} options - TIFF conversion options
1047
+ * @param {number} [options.width] - Target width
1048
+ * @param {number} [options.height] - Target height
1049
+ * @param {string} [options.compression='lzw'] - TIFF compression ('none', 'lzw', 'jpeg', 'zip')
1050
+ * @param {number} [options.quality=90] - JPEG quality when using JPEG compression
1051
+ * @param {boolean} [options.pyramid=false] - Create pyramidal TIFF
1052
+ * @param {string} [options.colorSpace='srgb'] - Output color space
1053
+ * @returns {Promise<Object>} - TIFF buffer with metadata
1054
+ */
1055
+ async createTIFFBuffer(options = {}) {
1056
+ return new Promise(async (resolve, reject) => {
1057
+ try {
1058
+ const startTime = process.hrtime.bigint();
1059
+
1060
+ // Smart processing: only process if not already processed
1061
+ if (!this._isProcessed) {
1062
+ await this.processImage();
1063
+ }
1064
+
1065
+ // Create processed image in memory (uses cache if available)
1066
+ const imageData = await this.createMemoryImage();
1067
+
1068
+ if (!imageData || !imageData.data) {
1069
+ throw new Error("Failed to create memory image from RAW data");
1070
+ }
1071
+
1072
+ // Set up Sharp configuration
1073
+ const sharpConfig = {
1074
+ raw: {
1075
+ width: imageData.width,
1076
+ height: imageData.height,
1077
+ channels: imageData.colors,
1078
+ premultiplied: false,
1079
+ },
1080
+ sequentialRead: true,
1081
+ limitInputPixels: false,
1082
+ };
1083
+
1084
+ if (imageData.bits === 16) {
1085
+ sharpConfig.raw.depth = "ushort";
1086
+ }
1087
+
1088
+ let sharpInstance = sharp(imageData.data, sharpConfig);
1089
+
1090
+ // Apply resizing if specified
1091
+ if (options.width || options.height) {
1092
+ const resizeOptions = {
1093
+ withoutEnlargement: true,
1094
+ kernel: sharp.kernel.lanczos3,
1095
+ fit: "inside",
1096
+ fastShrinkOnLoad: true,
1097
+ };
1098
+
1099
+ if (options.width && options.height) {
1100
+ sharpInstance = sharpInstance.resize(
1101
+ options.width,
1102
+ options.height,
1103
+ resizeOptions
1104
+ );
1105
+ } else if (options.width) {
1106
+ sharpInstance = sharpInstance.resize(
1107
+ options.width,
1108
+ null,
1109
+ resizeOptions
1110
+ );
1111
+ } else {
1112
+ sharpInstance = sharpInstance.resize(
1113
+ null,
1114
+ options.height,
1115
+ resizeOptions
1116
+ );
1117
+ }
1118
+ }
1119
+
1120
+ // Configure color space
1121
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
1122
+ case "rec2020":
1123
+ sharpInstance = sharpInstance.toColorspace("rec2020");
1124
+ break;
1125
+ case "p3":
1126
+ sharpInstance = sharpInstance.toColorspace("p3");
1127
+ break;
1128
+ case "srgb":
1129
+ default:
1130
+ sharpInstance = sharpInstance.toColorspace("srgb");
1131
+ break;
1132
+ }
1133
+
1134
+ // Configure TIFF options
1135
+ const tiffOptions = {
1136
+ compression: options.compression || "lzw",
1137
+ pyramid: options.pyramid || false,
1138
+ quality: options.quality || 90,
1139
+ };
1140
+
1141
+ // Convert to TIFF and get buffer
1142
+ const tiffBuffer = await sharpInstance
1143
+ .tiff(tiffOptions)
1144
+ .toBuffer({ resolveWithObject: true });
1145
+
1146
+ const endTime = process.hrtime.bigint();
1147
+ const processingTime = Number(endTime - startTime) / 1000000;
1148
+
1149
+ const result = {
1150
+ success: true,
1151
+ buffer: tiffBuffer.data,
1152
+ metadata: {
1153
+ originalDimensions: {
1154
+ width: imageData.width,
1155
+ height: imageData.height,
1156
+ },
1157
+ outputDimensions: {
1158
+ width: tiffBuffer.info.width,
1159
+ height: tiffBuffer.info.height,
1160
+ },
1161
+ fileSize: {
1162
+ original: imageData.dataSize,
1163
+ compressed: tiffBuffer.data.length,
1164
+ compressionRatio: (
1165
+ imageData.dataSize / tiffBuffer.data.length
1166
+ ).toFixed(2),
1167
+ },
1168
+ processing: {
1169
+ timeMs: processingTime.toFixed(2),
1170
+ throughputMBps: (
1171
+ imageData.dataSize /
1172
+ 1024 /
1173
+ 1024 /
1174
+ (processingTime / 1000)
1175
+ ).toFixed(2),
1176
+ },
1177
+ tiffOptions: tiffOptions,
1178
+ },
1179
+ };
1180
+
1181
+ resolve(result);
1182
+ } catch (error) {
1183
+ reject(new Error(`TIFF buffer creation failed: ${error.message}`));
1184
+ }
1185
+ });
1186
+ }
1187
+
1188
+ /**
1189
+ * Create processed image as WebP buffer in memory
1190
+ * @param {Object} options - WebP conversion options
1191
+ * @param {number} [options.width] - Target width
1192
+ * @param {number} [options.height] - Target height
1193
+ * @param {number} [options.quality=80] - WebP quality (1-100)
1194
+ * @param {boolean} [options.lossless=false] - Use lossless WebP
1195
+ * @param {number} [options.effort=4] - Encoding effort (0-6)
1196
+ * @param {string} [options.colorSpace='srgb'] - Output color space
1197
+ * @returns {Promise<Object>} - WebP buffer with metadata
1198
+ */
1199
+ async createWebPBuffer(options = {}) {
1200
+ return new Promise(async (resolve, reject) => {
1201
+ try {
1202
+ const startTime = process.hrtime.bigint();
1203
+
1204
+ // Smart processing: only process if not already processed
1205
+ if (!this._isProcessed) {
1206
+ await this.processImage();
1207
+ }
1208
+
1209
+ // Create processed image in memory (uses cache if available)
1210
+ const imageData = await this.createMemoryImage();
1211
+
1212
+ if (!imageData || !imageData.data) {
1213
+ throw new Error("Failed to create memory image from RAW data");
1214
+ }
1215
+
1216
+ // Set up Sharp configuration
1217
+ const sharpConfig = {
1218
+ raw: {
1219
+ width: imageData.width,
1220
+ height: imageData.height,
1221
+ channels: imageData.colors,
1222
+ premultiplied: false,
1223
+ },
1224
+ sequentialRead: true,
1225
+ limitInputPixels: false,
1226
+ };
1227
+
1228
+ if (imageData.bits === 16) {
1229
+ sharpConfig.raw.depth = "ushort";
1230
+ }
1231
+
1232
+ let sharpInstance = sharp(imageData.data, sharpConfig);
1233
+
1234
+ // Apply resizing if specified
1235
+ if (options.width || options.height) {
1236
+ const resizeOptions = {
1237
+ withoutEnlargement: true,
1238
+ kernel: sharp.kernel.lanczos3,
1239
+ fit: "inside",
1240
+ fastShrinkOnLoad: true,
1241
+ };
1242
+
1243
+ if (options.width && options.height) {
1244
+ sharpInstance = sharpInstance.resize(
1245
+ options.width,
1246
+ options.height,
1247
+ resizeOptions
1248
+ );
1249
+ } else if (options.width) {
1250
+ sharpInstance = sharpInstance.resize(
1251
+ options.width,
1252
+ null,
1253
+ resizeOptions
1254
+ );
1255
+ } else {
1256
+ sharpInstance = sharpInstance.resize(
1257
+ null,
1258
+ options.height,
1259
+ resizeOptions
1260
+ );
1261
+ }
1262
+ }
1263
+
1264
+ // Configure color space
1265
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
1266
+ case "rec2020":
1267
+ sharpInstance = sharpInstance.toColorspace("rec2020");
1268
+ break;
1269
+ case "p3":
1270
+ sharpInstance = sharpInstance.toColorspace("p3");
1271
+ break;
1272
+ case "srgb":
1273
+ default:
1274
+ sharpInstance = sharpInstance.toColorspace("srgb");
1275
+ break;
1276
+ }
1277
+
1278
+ // Configure WebP options
1279
+ const webpOptions = {
1280
+ quality: Math.max(1, Math.min(100, options.quality || 80)),
1281
+ lossless: options.lossless || false,
1282
+ effort: Math.max(0, Math.min(6, options.effort || 4)),
1283
+ };
1284
+
1285
+ // Convert to WebP and get buffer
1286
+ const webpBuffer = await sharpInstance
1287
+ .webp(webpOptions)
1288
+ .toBuffer({ resolveWithObject: true });
1289
+
1290
+ const endTime = process.hrtime.bigint();
1291
+ const processingTime = Number(endTime - startTime) / 1000000;
1292
+
1293
+ const result = {
1294
+ success: true,
1295
+ buffer: webpBuffer.data,
1296
+ metadata: {
1297
+ originalDimensions: {
1298
+ width: imageData.width,
1299
+ height: imageData.height,
1300
+ },
1301
+ outputDimensions: {
1302
+ width: webpBuffer.info.width,
1303
+ height: webpBuffer.info.height,
1304
+ },
1305
+ fileSize: {
1306
+ original: imageData.dataSize,
1307
+ compressed: webpBuffer.data.length,
1308
+ compressionRatio: (
1309
+ imageData.dataSize / webpBuffer.data.length
1310
+ ).toFixed(2),
1311
+ },
1312
+ processing: {
1313
+ timeMs: processingTime.toFixed(2),
1314
+ throughputMBps: (
1315
+ imageData.dataSize /
1316
+ 1024 /
1317
+ 1024 /
1318
+ (processingTime / 1000)
1319
+ ).toFixed(2),
1320
+ },
1321
+ webpOptions: webpOptions,
1322
+ },
1323
+ };
1324
+
1325
+ resolve(result);
1326
+ } catch (error) {
1327
+ reject(new Error(`WebP buffer creation failed: ${error.message}`));
1328
+ }
1329
+ });
1330
+ }
1331
+
1332
+ /**
1333
+ * Create processed image as AVIF buffer in memory
1334
+ * @param {Object} options - AVIF conversion options
1335
+ * @param {number} [options.width] - Target width
1336
+ * @param {number} [options.height] - Target height
1337
+ * @param {number} [options.quality=50] - AVIF quality (1-100)
1338
+ * @param {boolean} [options.lossless=false] - Use lossless AVIF
1339
+ * @param {number} [options.effort=4] - Encoding effort (0-9)
1340
+ * @param {string} [options.colorSpace='srgb'] - Output color space
1341
+ * @returns {Promise<Object>} - AVIF buffer with metadata
1342
+ */
1343
+ async createAVIFBuffer(options = {}) {
1344
+ return new Promise(async (resolve, reject) => {
1345
+ try {
1346
+ const startTime = process.hrtime.bigint();
1347
+
1348
+ // Smart processing: only process if not already processed
1349
+ if (!this._isProcessed) {
1350
+ await this.processImage();
1351
+ }
1352
+
1353
+ // Create processed image in memory (uses cache if available)
1354
+ const imageData = await this.createMemoryImage();
1355
+
1356
+ if (!imageData || !imageData.data) {
1357
+ throw new Error("Failed to create memory image from RAW data");
1358
+ }
1359
+
1360
+ // Set up Sharp configuration
1361
+ const sharpConfig = {
1362
+ raw: {
1363
+ width: imageData.width,
1364
+ height: imageData.height,
1365
+ channels: imageData.colors,
1366
+ premultiplied: false,
1367
+ },
1368
+ sequentialRead: true,
1369
+ limitInputPixels: false,
1370
+ };
1371
+
1372
+ if (imageData.bits === 16) {
1373
+ sharpConfig.raw.depth = "ushort";
1374
+ }
1375
+
1376
+ let sharpInstance = sharp(imageData.data, sharpConfig);
1377
+
1378
+ // Apply resizing if specified
1379
+ if (options.width || options.height) {
1380
+ const resizeOptions = {
1381
+ withoutEnlargement: true,
1382
+ kernel: sharp.kernel.lanczos3,
1383
+ fit: "inside",
1384
+ fastShrinkOnLoad: true,
1385
+ };
1386
+
1387
+ if (options.width && options.height) {
1388
+ sharpInstance = sharpInstance.resize(
1389
+ options.width,
1390
+ options.height,
1391
+ resizeOptions
1392
+ );
1393
+ } else if (options.width) {
1394
+ sharpInstance = sharpInstance.resize(
1395
+ options.width,
1396
+ null,
1397
+ resizeOptions
1398
+ );
1399
+ } else {
1400
+ sharpInstance = sharpInstance.resize(
1401
+ null,
1402
+ options.height,
1403
+ resizeOptions
1404
+ );
1405
+ }
1406
+ }
1407
+
1408
+ // Configure color space
1409
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
1410
+ case "rec2020":
1411
+ sharpInstance = sharpInstance.toColorspace("rec2020");
1412
+ break;
1413
+ case "p3":
1414
+ sharpInstance = sharpInstance.toColorspace("p3");
1415
+ break;
1416
+ case "srgb":
1417
+ default:
1418
+ sharpInstance = sharpInstance.toColorspace("srgb");
1419
+ break;
1420
+ }
1421
+
1422
+ // Configure AVIF options
1423
+ const avifOptions = {
1424
+ quality: Math.max(1, Math.min(100, options.quality || 50)),
1425
+ lossless: options.lossless || false,
1426
+ effort: Math.max(0, Math.min(9, options.effort || 4)),
1427
+ };
1428
+
1429
+ // Convert to AVIF and get buffer
1430
+ const avifBuffer = await sharpInstance
1431
+ .avif(avifOptions)
1432
+ .toBuffer({ resolveWithObject: true });
1433
+
1434
+ const endTime = process.hrtime.bigint();
1435
+ const processingTime = Number(endTime - startTime) / 1000000;
1436
+
1437
+ const result = {
1438
+ success: true,
1439
+ buffer: avifBuffer.data,
1440
+ metadata: {
1441
+ originalDimensions: {
1442
+ width: imageData.width,
1443
+ height: imageData.height,
1444
+ },
1445
+ outputDimensions: {
1446
+ width: avifBuffer.info.width,
1447
+ height: avifBuffer.info.height,
1448
+ },
1449
+ fileSize: {
1450
+ original: imageData.dataSize,
1451
+ compressed: avifBuffer.data.length,
1452
+ compressionRatio: (
1453
+ imageData.dataSize / avifBuffer.data.length
1454
+ ).toFixed(2),
1455
+ },
1456
+ processing: {
1457
+ timeMs: processingTime.toFixed(2),
1458
+ throughputMBps: (
1459
+ imageData.dataSize /
1460
+ 1024 /
1461
+ 1024 /
1462
+ (processingTime / 1000)
1463
+ ).toFixed(2),
1464
+ },
1465
+ avifOptions: avifOptions,
1466
+ },
1467
+ };
1468
+
1469
+ resolve(result);
1470
+ } catch (error) {
1471
+ reject(new Error(`AVIF buffer creation failed: ${error.message}`));
1472
+ }
1473
+ });
1474
+ }
1475
+
1476
+ /**
1477
+ * Create raw PPM buffer from processed image data
1478
+ * @returns {Promise<Object>} - PPM buffer with metadata
1479
+ */
1480
+ async createPPMBuffer() {
1481
+ return new Promise(async (resolve, reject) => {
1482
+ try {
1483
+ const startTime = process.hrtime.bigint();
1484
+
1485
+ // Smart processing: only process if not already processed
1486
+ if (!this._isProcessed) {
1487
+ await this.processImage();
1488
+ }
1489
+
1490
+ // Create processed image in memory (uses cache if available)
1491
+ const imageData = await this.createMemoryImage();
1492
+
1493
+ if (!imageData || !imageData.data) {
1494
+ throw new Error("Failed to create memory image from RAW data");
1495
+ }
1496
+
1497
+ // Create PPM header
1498
+ const header = `P6\n${imageData.width} ${imageData.height}\n255\n`;
1499
+ const headerBuffer = Buffer.from(header, "ascii");
1500
+
1501
+ // Convert image data to 8-bit RGB if needed
1502
+ let rgbData;
1503
+ if (imageData.bits === 16) {
1504
+ // Convert 16-bit to 8-bit
1505
+ const pixels = imageData.width * imageData.height;
1506
+ const channels = imageData.colors;
1507
+ rgbData = Buffer.alloc(pixels * 3); // PPM is always RGB
1508
+
1509
+ for (let i = 0; i < pixels; i++) {
1510
+ const srcOffset = i * channels * 2; // 16-bit data
1511
+ const dstOffset = i * 3;
1512
+
1513
+ // Read 16-bit values and convert to 8-bit
1514
+ rgbData[dstOffset] = Math.min(
1515
+ 255,
1516
+ Math.floor((imageData.data.readUInt16LE(srcOffset) / 65535) * 255)
1517
+ ); // R
1518
+ rgbData[dstOffset + 1] = Math.min(
1519
+ 255,
1520
+ Math.floor(
1521
+ (imageData.data.readUInt16LE(srcOffset + 2) / 65535) * 255
1522
+ )
1523
+ ); // G
1524
+ rgbData[dstOffset + 2] = Math.min(
1525
+ 255,
1526
+ Math.floor(
1527
+ (imageData.data.readUInt16LE(srcOffset + 4) / 65535) * 255
1528
+ )
1529
+ ); // B
1530
+ }
1531
+ } else {
1532
+ // Already 8-bit, just copy RGB channels
1533
+ const pixels = imageData.width * imageData.height;
1534
+ const channels = imageData.colors;
1535
+ rgbData = Buffer.alloc(pixels * 3);
1536
+
1537
+ for (let i = 0; i < pixels; i++) {
1538
+ const srcOffset = i * channels;
1539
+ const dstOffset = i * 3;
1540
+
1541
+ rgbData[dstOffset] = imageData.data[srcOffset]; // R
1542
+ rgbData[dstOffset + 1] = imageData.data[srcOffset + 1]; // G
1543
+ rgbData[dstOffset + 2] = imageData.data[srcOffset + 2]; // B
1544
+ }
1545
+ }
1546
+
1547
+ // Combine header and data
1548
+ const ppmBuffer = Buffer.concat([headerBuffer, rgbData]);
1549
+
1550
+ const endTime = process.hrtime.bigint();
1551
+ const processingTime = Number(endTime - startTime) / 1000000;
1552
+
1553
+ const result = {
1554
+ success: true,
1555
+ buffer: ppmBuffer,
1556
+ metadata: {
1557
+ format: "PPM",
1558
+ dimensions: {
1559
+ width: imageData.width,
1560
+ height: imageData.height,
1561
+ },
1562
+ fileSize: {
1563
+ original: imageData.dataSize,
1564
+ compressed: ppmBuffer.length,
1565
+ compressionRatio: (imageData.dataSize / ppmBuffer.length).toFixed(
1566
+ 2
1567
+ ),
1568
+ },
1569
+ processing: {
1570
+ timeMs: processingTime.toFixed(2),
1571
+ throughputMBps: (
1572
+ imageData.dataSize /
1573
+ 1024 /
1574
+ 1024 /
1575
+ (processingTime / 1000)
1576
+ ).toFixed(2),
1577
+ },
1578
+ },
1579
+ };
1580
+
1581
+ resolve(result);
1582
+ } catch (error) {
1583
+ reject(new Error(`PPM buffer creation failed: ${error.message}`));
1584
+ }
1585
+ });
1586
+ }
1587
+
1588
+ /**
1589
+ * Create thumbnail as JPEG buffer in memory
1590
+ * @param {Object} options - JPEG options for thumbnail
1591
+ * @param {number} [options.quality=85] - JPEG quality
1592
+ * @param {number} [options.maxSize] - Maximum dimension size
1593
+ * @returns {Promise<Object>} - Thumbnail JPEG buffer with metadata
1594
+ */
1595
+ async createThumbnailJPEGBuffer(options = {}) {
1596
+ return new Promise(async (resolve, reject) => {
1597
+ try {
1598
+ const startTime = process.hrtime.bigint();
1599
+
1600
+ // Unpack thumbnail if needed
1601
+ await this.unpackThumbnail();
1602
+
1603
+ // Create thumbnail in memory
1604
+ const thumbData = await this.createMemoryThumbnail();
1605
+
1606
+ if (!thumbData || !thumbData.data) {
1607
+ throw new Error("Failed to create memory thumbnail");
1608
+ }
1609
+
1610
+ let sharpInstance;
1611
+
1612
+ // Check if thumbnail is already JPEG
1613
+ if (await this.isJPEGThumb()) {
1614
+ // Thumbnail is already JPEG, return directly or reprocess if options specified
1615
+ if (!options.quality && !options.maxSize) {
1616
+ const result = {
1617
+ success: true,
1618
+ buffer: thumbData.data,
1619
+ metadata: {
1620
+ format: "JPEG",
1621
+ dimensions: {
1622
+ width: thumbData.width,
1623
+ height: thumbData.height,
1624
+ },
1625
+ fileSize: {
1626
+ compressed: thumbData.data.length,
1627
+ },
1628
+ processing: {
1629
+ timeMs: "0.00",
1630
+ fromCache: true,
1631
+ },
1632
+ },
1633
+ };
1634
+ resolve(result);
1635
+ return;
1636
+ } else {
1637
+ // Reprocess existing JPEG with new options
1638
+ sharpInstance = sharp(thumbData.data);
1639
+ }
1640
+ } else {
1641
+ // Convert RAW thumbnail data
1642
+ const sharpConfig = {
1643
+ raw: {
1644
+ width: thumbData.width,
1645
+ height: thumbData.height,
1646
+ channels: thumbData.colors || 3,
1647
+ premultiplied: false,
1648
+ },
1649
+ };
1650
+
1651
+ if (thumbData.bits === 16) {
1652
+ sharpConfig.raw.depth = "ushort";
1653
+ }
1654
+
1655
+ sharpInstance = sharp(thumbData.data, sharpConfig);
1656
+ }
1657
+
1658
+ // Apply max size constraint if specified
1659
+ if (options.maxSize) {
1660
+ sharpInstance = sharpInstance.resize(
1661
+ options.maxSize,
1662
+ options.maxSize,
1663
+ {
1664
+ fit: "inside",
1665
+ withoutEnlargement: true,
1666
+ }
1667
+ );
1668
+ }
1669
+
1670
+ // Configure JPEG options
1671
+ const jpegOptions = {
1672
+ quality: Math.max(1, Math.min(100, options.quality || 85)),
1673
+ progressive: false, // Thumbnails typically don't need progressive
1674
+ mozjpeg: false, // Keep simple for speed
1675
+ };
1676
+
1677
+ // Convert to JPEG buffer
1678
+ const jpegBuffer = await sharpInstance
1679
+ .jpeg(jpegOptions)
1680
+ .toBuffer({ resolveWithObject: true });
1681
+
1682
+ const endTime = process.hrtime.bigint();
1683
+ const processingTime = Number(endTime - startTime) / 1000000;
1684
+
1685
+ const result = {
1686
+ success: true,
1687
+ buffer: jpegBuffer.data,
1688
+ metadata: {
1689
+ format: "JPEG",
1690
+ originalDimensions: {
1691
+ width: thumbData.width,
1692
+ height: thumbData.height,
1693
+ },
1694
+ outputDimensions: {
1695
+ width: jpegBuffer.info.width,
1696
+ height: jpegBuffer.info.height,
1697
+ },
1698
+ fileSize: {
1699
+ original: thumbData.dataSize || thumbData.data.length,
1700
+ compressed: jpegBuffer.data.length,
1701
+ compressionRatio: (
1702
+ (thumbData.dataSize || thumbData.data.length) /
1703
+ jpegBuffer.data.length
1704
+ ).toFixed(2),
1705
+ },
1706
+ processing: {
1707
+ timeMs: processingTime.toFixed(2),
1708
+ },
1709
+ jpegOptions: jpegOptions,
1710
+ },
1711
+ };
1712
+
1713
+ resolve(result);
1714
+ } catch (error) {
1715
+ reject(
1716
+ new Error(`Thumbnail JPEG buffer creation failed: ${error.message}`)
1717
+ );
1718
+ }
1719
+ });
1720
+ }
1721
+
1722
+ // ============== JPEG CONVERSION (NEW FEATURE) ==============
1723
+
1724
+ /**
1725
+ * Convert RAW to JPEG with advanced options
1726
+ * @param {string} outputPath - Output JPEG file path
1727
+ * @param {Object} options - JPEG conversion options
1728
+ * @param {number} [options.quality=85] - JPEG quality (1-100)
1729
+ * @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
1730
+ * @param {number} [options.height] - Target height (maintains aspect ratio if width not specified)
1731
+ * @param {boolean} [options.progressive=false] - Use progressive JPEG
1732
+ * @param {boolean} [options.mozjpeg=true] - Use mozjpeg encoder for better compression
1733
+ * @param {number} [options.chromaSubsampling='4:2:0'] - Chroma subsampling ('4:4:4', '4:2:2', '4:2:0')
1734
+ * @param {boolean} [options.trellisQuantisation=false] - Enable trellis quantisation
1735
+ * @param {boolean} [options.optimizeScans=false] - Optimize scan order
1736
+ * @param {number} [options.overshootDeringing=false] - Overshoot deringing
1737
+ * @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
1738
+ * @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
1739
+ * @returns {Promise<Object>} - Conversion result with metadata
1740
+ */
1741
+ async convertToJPEG(outputPath, options = {}) {
1742
+ return new Promise(async (resolve, reject) => {
1743
+ try {
1744
+ // Create JPEG buffer first
1745
+ const result = await this.createJPEGBuffer(options);
1746
+
1747
+ // Write buffer to file
1748
+ const fs = require("fs");
1749
+ fs.writeFileSync(outputPath, result.buffer);
1750
+
1751
+ // Get output file stats
1752
+ const stats = fs.statSync(outputPath);
1753
+
1754
+ // Return result in the same format as before
1755
+ resolve({
1756
+ success: true,
1757
+ outputPath: outputPath,
1758
+ metadata: {
1759
+ ...result.metadata,
1760
+ fileSize: {
1761
+ ...result.metadata.fileSize,
1762
+ compressed: stats.size,
1763
+ },
1764
+ },
1765
+ });
1766
+ } catch (error) {
1767
+ reject(new Error(`JPEG conversion failed: ${error.message}`));
1768
+ }
1769
+ });
1770
+ }
1771
+
1772
+ /**
1773
+ * Batch convert multiple RAW files to JPEG
1774
+ * @param {string[]} inputPaths - Array of input RAW file paths
1775
+ * @param {string} outputDir - Output directory for JPEG files
1776
+ * @param {Object} options - JPEG conversion options (same as convertToJPEG)
1777
+ * @returns {Promise<Object>} - Batch conversion results
1778
+ */
1779
+ async batchConvertToJPEG(inputPaths, outputDir, options = {}) {
1780
+ return new Promise(async (resolve, reject) => {
1781
+ try {
1782
+ const fs = require("fs");
1783
+ const path = require("path");
1784
+
1785
+ // Ensure output directory exists
1786
+ if (!fs.existsSync(outputDir)) {
1787
+ fs.mkdirSync(outputDir, { recursive: true });
1788
+ }
1789
+
1790
+ const results = {
1791
+ successful: [],
1792
+ failed: [],
1793
+ summary: {
1794
+ total: inputPaths.length,
1795
+ processed: 0,
1796
+ errors: 0,
1797
+ totalProcessingTime: 0,
1798
+ averageCompressionRatio: 0,
1799
+ totalOriginalSize: 0,
1800
+ totalCompressedSize: 0,
1801
+ },
1802
+ };
1803
+
1804
+ const startTime = process.hrtime.bigint();
1805
+
1806
+ for (const inputPath of inputPaths) {
1807
+ try {
1808
+ // Generate output filename
1809
+ const baseName = path.basename(inputPath, path.extname(inputPath));
1810
+ const outputPath = path.join(outputDir, `${baseName}.jpg`);
1811
+
1812
+ // Load the RAW file
1813
+ await this.close(); // Close any previous file
1814
+ await this.loadFile(inputPath);
1815
+
1816
+ // Convert to JPEG
1817
+ const result = await this.convertToJPEG(outputPath, options);
1818
+
1819
+ results.successful.push({
1820
+ input: inputPath,
1821
+ output: outputPath,
1822
+ result: result,
1823
+ });
1824
+
1825
+ results.summary.processed++;
1826
+ results.summary.totalOriginalSize +=
1827
+ result.metadata.fileSize.original;
1828
+ results.summary.totalCompressedSize +=
1829
+ result.metadata.fileSize.compressed;
1830
+ } catch (error) {
1831
+ results.failed.push({
1832
+ input: inputPath,
1833
+ error: error.message,
1834
+ });
1835
+ results.summary.errors++;
1836
+ }
1837
+ }
1838
+
1839
+ const endTime = process.hrtime.bigint();
1840
+ results.summary.totalProcessingTime =
1841
+ Number(endTime - startTime) / 1000000; // ms
1842
+
1843
+ if (results.summary.totalOriginalSize > 0) {
1844
+ results.summary.averageCompressionRatio = (
1845
+ results.summary.totalOriginalSize /
1846
+ results.summary.totalCompressedSize
1847
+ ).toFixed(2);
1848
+ }
1849
+
1850
+ results.summary.averageProcessingTimePerFile = (
1851
+ results.summary.totalProcessingTime / inputPaths.length
1852
+ ).toFixed(2);
1853
+
1854
+ resolve(results);
1855
+ } catch (error) {
1856
+ reject(new Error(`Batch JPEG conversion failed: ${error.message}`));
1857
+ }
1858
+ });
1859
+ }
1860
+
1861
+ /**
1862
+ * Get optimal JPEG conversion settings based on image analysis
1863
+ * @param {Object} analysisOptions - Options for image analysis
1864
+ * @returns {Promise<Object>} - Recommended JPEG settings
1865
+ */
1866
+ async getOptimalJPEGSettings(analysisOptions = {}) {
1867
+ return new Promise(async (resolve, reject) => {
1868
+ try {
1869
+ // Get image metadata and process for analysis
1870
+ const metadata = await this.getMetadata();
1871
+ const imageSize = await this.getImageSize();
1872
+
1873
+ // Analyze image characteristics
1874
+ const imageArea = metadata.width * metadata.height;
1875
+ const isHighRes = imageArea > 6000 * 4000; // > 24MP
1876
+ const isMediumRes = imageArea > 3000 * 2000; // > 6MP
1877
+
1878
+ // Default settings based on image characteristics
1879
+ let recommendedSettings = {
1880
+ quality: 85,
1881
+ progressive: false,
1882
+ mozjpeg: true,
1883
+ chromaSubsampling: "4:2:0",
1884
+ optimizeCoding: true,
1885
+ trellisQuantisation: false,
1886
+ optimizeScans: false,
1887
+ reasoning: [],
1888
+ };
1889
+
1890
+ // Adjust settings based on image size
1891
+ if (isHighRes) {
1892
+ recommendedSettings.quality = 80; // Slightly lower quality for large images
1893
+ recommendedSettings.progressive = true; // Progressive loading for large images
1894
+ recommendedSettings.trellisQuantisation = true; // Better compression for large images
1895
+ recommendedSettings.reasoning.push(
1896
+ "High resolution image detected - optimizing for file size"
1897
+ );
1898
+ } else if (isMediumRes) {
1899
+ recommendedSettings.quality = 85;
1900
+ recommendedSettings.reasoning.push(
1901
+ "Medium resolution image - balanced quality/size"
1902
+ );
1903
+ } else {
1904
+ recommendedSettings.quality = 90; // Higher quality for smaller images
1905
+ recommendedSettings.chromaSubsampling = "4:4:4"; // Better chroma for small images (Sharp compatible)
1906
+ recommendedSettings.reasoning.push(
1907
+ "Lower resolution image - prioritizing quality"
1908
+ );
1909
+ }
1910
+
1911
+ // Adjust for different use cases
1912
+ if (analysisOptions.usage === "web") {
1913
+ recommendedSettings.quality = Math.min(
1914
+ recommendedSettings.quality,
1915
+ 80
1916
+ );
1917
+ recommendedSettings.progressive = true;
1918
+ recommendedSettings.optimizeScans = true;
1919
+ recommendedSettings.reasoning.push(
1920
+ "Web usage - optimized for loading speed"
1921
+ );
1922
+ } else if (analysisOptions.usage === "print") {
1923
+ recommendedSettings.quality = Math.max(
1924
+ recommendedSettings.quality,
1925
+ 90
1926
+ );
1927
+ recommendedSettings.chromaSubsampling = "4:4:4"; // Use 4:4:4 instead of 4:2:2 for Sharp compatibility
1928
+ recommendedSettings.reasoning.push(
1929
+ "Print usage - optimized for quality"
1930
+ );
1931
+ } else if (analysisOptions.usage === "archive") {
1932
+ recommendedSettings.quality = 95;
1933
+ recommendedSettings.chromaSubsampling = "4:4:4";
1934
+ recommendedSettings.trellisQuantisation = true;
1935
+ recommendedSettings.reasoning.push(
1936
+ "Archive usage - maximum quality preservation"
1937
+ );
1938
+ }
1939
+
1940
+ // Camera-specific optimizations
1941
+ if (metadata.make) {
1942
+ const make = metadata.make.toLowerCase();
1943
+ if (make.includes("canon") || make.includes("nikon")) {
1944
+ // Professional cameras often benefit from slightly different settings
1945
+ recommendedSettings.reasoning.push(
1946
+ `${metadata.make} camera detected - professional settings`
1947
+ );
1948
+ }
1949
+ }
1950
+
1951
+ resolve({
1952
+ recommended: recommendedSettings,
1953
+ imageAnalysis: {
1954
+ dimensions: {
1955
+ width: metadata.width,
1956
+ height: metadata.height,
1957
+ area: imageArea,
1958
+ },
1959
+ category: isHighRes
1960
+ ? "high-resolution"
1961
+ : isMediumRes
1962
+ ? "medium-resolution"
1963
+ : "low-resolution",
1964
+ camera: {
1965
+ make: metadata.make,
1966
+ model: metadata.model,
1967
+ },
1968
+ },
1969
+ });
1970
+ } catch (error) {
1971
+ reject(
1972
+ new Error(
1973
+ `Failed to analyze image for optimal settings: ${error.message}`
1974
+ )
1975
+ );
1976
+ }
1977
+ });
1978
+ }
1979
+
1980
+ // ============== CANCELLATION SUPPORT ==============
1981
+
1982
+ /**
1983
+ * Set cancellation flag to stop processing
1984
+ * @returns {Promise<boolean>} - Success status
1985
+ */
1986
+ async setCancelFlag() {
1987
+ return new Promise((resolve, reject) => {
1988
+ try {
1989
+ const result = this._wrapper.setCancelFlag();
1990
+ resolve(result);
1991
+ } catch (error) {
1992
+ reject(error);
1993
+ }
1994
+ });
1995
+ }
1996
+
1997
+ /**
1998
+ * Clear cancellation flag
1999
+ * @returns {Promise<boolean>} - Success status
2000
+ */
2001
+ async clearCancelFlag() {
2002
+ return new Promise((resolve, reject) => {
2003
+ try {
2004
+ const result = this._wrapper.clearCancelFlag();
2005
+ resolve(result);
2006
+ } catch (error) {
2007
+ reject(error);
2008
+ }
2009
+ });
2010
+ }
2011
+
2012
+ // ============== VERSION INFORMATION (INSTANCE METHODS) ==============
2013
+
2014
+ /**
2015
+ * Get LibRaw version string
2016
+ * @returns {string} - Version string
2017
+ */
2018
+ version() {
2019
+ return this._wrapper.version();
2020
+ }
2021
+
2022
+ /**
2023
+ * Get LibRaw version as array [major, minor, patch]
2024
+ * @returns {number[]} - Version number array
2025
+ */
2026
+ versionNumber() {
2027
+ return this._wrapper.versionNumber();
2028
+ }
2029
+
2030
+ // ============== STATIC METHODS ==============
2031
+
2032
+ /**
2033
+ * Get LibRaw version
2034
+ * @returns {string} - Version string
2035
+ */
2036
+ static getVersion() {
2037
+ return librawAddon.LibRawWrapper.getVersion();
2038
+ }
2039
+
2040
+ /**
2041
+ * Get LibRaw capabilities
2042
+ * @returns {number} - Capabilities flags
2043
+ */
2044
+ static getCapabilities() {
2045
+ return librawAddon.LibRawWrapper.getCapabilities();
2046
+ }
2047
+
2048
+ /**
2049
+ * Get list of supported cameras
2050
+ * @returns {string[]} - Array of camera names
2051
+ */
2052
+ static getCameraList() {
2053
+ return librawAddon.LibRawWrapper.getCameraList();
2054
+ }
2055
+
2056
+ /**
2057
+ * Get count of supported cameras
2058
+ * @returns {number} - Number of supported cameras
2059
+ */
2060
+ static getCameraCount() {
2061
+ return librawAddon.LibRawWrapper.getCameraCount();
2062
+ }
2063
+
2064
+ /**
2065
+ * High-performance fast JPEG conversion with minimal processing
2066
+ * @param {string} outputPath - Output JPEG file path
2067
+ * @param {Object} options - Speed-optimized JPEG options
2068
+ * @returns {Promise<Object>} - Conversion result
2069
+ */
2070
+ async convertToJPEGFast(outputPath, options = {}) {
2071
+ return this.convertToJPEG(outputPath, {
2072
+ fastMode: true,
2073
+ effort: 1, // Fastest encoding
2074
+ progressive: false,
2075
+ trellisQuantisation: false,
2076
+ optimizeScans: false,
2077
+ mozjpeg: false,
2078
+ quality: options.quality || 80,
2079
+ ...options,
2080
+ });
2081
+ }
2082
+
2083
+ /**
2084
+ * Create multiple JPEG sizes from single RAW (thumbnail, web, full)
2085
+ * @param {string} baseOutputPath - Base output path (without extension)
2086
+ * @param {Object} options - Multi-size options
2087
+ * @returns {Promise<Object>} - Multi-size conversion results
2088
+ */
2089
+ async convertToJPEGMultiSize(baseOutputPath, options = {}) {
2090
+ const sizes = options.sizes || [
2091
+ { name: "thumb", width: 400, quality: 85 },
2092
+ { name: "web", width: 1920, quality: 80 },
2093
+ { name: "full", quality: 85 },
2094
+ ];
2095
+
2096
+ // Process the RAW once (uses smart caching)
2097
+ if (!this._isProcessed) {
2098
+ await this.processImage();
2099
+ }
2100
+
2101
+ const results = {};
2102
+ const startTime = Date.now();
2103
+
2104
+ // Create all sizes sequentially to reuse cached data
2105
+ for (const sizeConfig of sizes) {
2106
+ const outputPath = `${baseOutputPath}_${sizeConfig.name}.jpg`;
2107
+ const sizeStart = Date.now();
2108
+
2109
+ const result = await this.convertToJPEG(outputPath, {
2110
+ fastMode: true,
2111
+ width: sizeConfig.width,
2112
+ height: sizeConfig.height,
2113
+ quality: sizeConfig.quality || 85,
2114
+ effort: sizeConfig.effort || 2,
2115
+ ...sizeConfig,
2116
+ });
2117
+
2118
+ const sizeEnd = Date.now();
2119
+
2120
+ results[sizeConfig.name] = {
2121
+ name: sizeConfig.name,
2122
+ outputPath,
2123
+ dimensions: result.metadata.outputDimensions,
2124
+ fileSize: result.metadata.fileSize.compressed,
2125
+ processingTime: sizeEnd - sizeStart,
2126
+ config: sizeConfig,
2127
+ };
2128
+ }
2129
+
2130
+ const endTime = Date.now();
2131
+ const totalTime = endTime - startTime;
2132
+
2133
+ return {
2134
+ success: true,
2135
+ sizes: results,
2136
+ originalDimensions: Object.values(results)[0]
2137
+ ? Object.values(results)[0].dimensions
2138
+ : { width: 0, height: 0 },
2139
+ totalProcessingTime: totalTime,
2140
+ averageTimePerSize: `${(totalTime / sizes.length).toFixed(2)}ms`,
2141
+ };
2142
+ }
2143
+
2144
+ /**
2145
+ * High-performance parallel batch conversion using worker threads
2146
+ * @param {string[]} inputPaths - Array of RAW file paths
2147
+ * @param {string} outputDir - Output directory
2148
+ * @param {Object} options - Conversion options
2149
+ * @returns {Promise<Object>} - Batch conversion results
2150
+ */
2151
+ static async batchConvertToJPEGParallel(inputPaths, outputDir, options = {}) {
2152
+ const fs = require("fs");
2153
+ const path = require("path");
2154
+ const os = require("os");
2155
+
2156
+ if (!fs.existsSync(outputDir)) {
2157
+ fs.mkdirSync(outputDir, { recursive: true });
2158
+ }
2159
+
2160
+ const maxConcurrency =
2161
+ options.maxConcurrency || Math.min(os.cpus().length, 4);
2162
+ const results = [];
2163
+ const errors = [];
2164
+ const startTime = Date.now();
2165
+
2166
+ // Process files in parallel batches
2167
+ for (let i = 0; i < inputPaths.length; i += maxConcurrency) {
2168
+ const batch = inputPaths.slice(i, i + maxConcurrency);
2169
+
2170
+ const batchPromises = batch.map(async (inputPath) => {
2171
+ try {
2172
+ const fileName = path.parse(inputPath).name;
2173
+ const outputPath = path.join(outputDir, `${fileName}.jpg`);
2174
+
2175
+ const libraw = new LibRaw();
2176
+ await libraw.loadFile(inputPath);
2177
+
2178
+ const result = await libraw.convertToJPEG(outputPath, {
2179
+ fastMode: true,
2180
+ effort: 1,
2181
+ quality: options.quality || 85,
2182
+ ...options,
2183
+ });
2184
+
2185
+ await libraw.close();
2186
+
2187
+ return {
2188
+ inputPath,
2189
+ outputPath,
2190
+ success: true,
2191
+ fileSize: result.metadata.fileSize.compressed,
2192
+ processingTime: result.metadata.processing.timeMs,
2193
+ };
2194
+ } catch (error) {
2195
+ errors.push({ inputPath, error: error.message });
2196
+ return {
2197
+ inputPath,
2198
+ success: false,
2199
+ error: error.message,
2200
+ };
2201
+ }
2202
+ });
2203
+
2204
+ const batchResults = await Promise.all(batchPromises);
2205
+ results.push(...batchResults);
2206
+ }
2207
+
2208
+ const endTime = Date.now();
2209
+ const successCount = results.filter((r) => r.success).length;
2210
+
2211
+ return {
2212
+ totalFiles: inputPaths.length,
2213
+ successCount,
2214
+ errorCount: errors.length,
2215
+ results,
2216
+ errors,
2217
+ totalProcessingTime: endTime - startTime,
2218
+ averageTimePerFile:
2219
+ successCount > 0 ? (endTime - startTime) / successCount : 0,
2220
+ };
2221
+ }
2222
+ }
2223
+
2224
+ module.exports = LibRaw;