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

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,1358 @@
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
+ // ============== JPEG CONVERSION (NEW FEATURE) ==============
695
+
696
+ /**
697
+ * Convert RAW to JPEG with advanced options
698
+ * @param {string} outputPath - Output JPEG file path
699
+ * @param {Object} options - JPEG conversion options
700
+ * @param {number} [options.quality=85] - JPEG quality (1-100)
701
+ * @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
702
+ * @param {number} [options.height] - Target height (maintains aspect ratio if width not specified)
703
+ * @param {boolean} [options.progressive=false] - Use progressive JPEG
704
+ * @param {boolean} [options.mozjpeg=true] - Use mozjpeg encoder for better compression
705
+ * @param {number} [options.chromaSubsampling='4:2:0'] - Chroma subsampling ('4:4:4', '4:2:2', '4:2:0')
706
+ * @param {boolean} [options.trellisQuantisation=false] - Enable trellis quantisation
707
+ * @param {boolean} [options.optimizeScans=false] - Optimize scan order
708
+ * @param {number} [options.overshootDeringing=false] - Overshoot deringing
709
+ * @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
710
+ * @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
711
+ * @returns {Promise<Object>} - Conversion result with metadata
712
+ */
713
+ async convertToJPEG(outputPath, options = {}) {
714
+ return new Promise(async (resolve, reject) => {
715
+ try {
716
+ // Set default options with performance-optimized values
717
+ const opts = {
718
+ quality: options.quality || 85,
719
+ progressive: options.progressive || false,
720
+ mozjpeg: options.mozjpeg !== false, // Default to true for better compression
721
+ chromaSubsampling: options.chromaSubsampling || "4:2:0",
722
+ trellisQuantisation: options.trellisQuantisation || false,
723
+ optimizeScans: options.optimizeScans || false,
724
+ overshootDeringing: options.overshootDeringing || false,
725
+ optimizeCoding: options.optimizeCoding !== false, // Default to true
726
+ colorSpace: options.colorSpace || "srgb",
727
+ ...options,
728
+ };
729
+
730
+ const startTime = process.hrtime.bigint();
731
+
732
+ // Smart processing: only process if not already processed
733
+ if (!this._isProcessed) {
734
+ await this.processImage();
735
+ }
736
+
737
+ // Create processed image in memory (uses cache if available)
738
+ const imageData = await this.createMemoryImage();
739
+
740
+ if (!imageData || !imageData.data) {
741
+ throw new Error("Failed to create memory image from RAW data");
742
+ }
743
+
744
+ // Convert the LibRaw RGB data to Sharp-compatible buffer
745
+ let sharpInstance;
746
+
747
+ // Determine if this is a large image for performance optimizations
748
+ const isLargeImage = imageData.width * imageData.height > 20_000_000; // > 20MP
749
+ const fastMode = opts.fastMode !== false; // Default to fast mode
750
+
751
+ // Optimized Sharp configuration
752
+ const sharpConfig = {
753
+ raw: {
754
+ width: imageData.width,
755
+ height: imageData.height,
756
+ channels: imageData.colors,
757
+ premultiplied: false,
758
+ },
759
+ // Performance optimizations
760
+ sequentialRead: true,
761
+ limitInputPixels: false,
762
+ density: fastMode ? 72 : 300, // Lower DPI for speed
763
+ };
764
+
765
+ if (imageData.bits === 16) {
766
+ sharpConfig.raw.depth = "ushort";
767
+ }
768
+
769
+ sharpInstance = sharp(imageData.data, sharpConfig);
770
+
771
+ // Apply resizing if specified with performance optimizations
772
+ if (opts.width || opts.height) {
773
+ const resizeOptions = {
774
+ withoutEnlargement: true,
775
+ // Use faster kernel for large images or when fast mode is enabled
776
+ kernel:
777
+ isLargeImage || fastMode
778
+ ? sharp.kernel.cubic
779
+ : sharp.kernel.lanczos3,
780
+ fit: "inside",
781
+ fastShrinkOnLoad: true, // Enable fast shrink-on-load optimization
782
+ };
783
+
784
+ if (opts.width && opts.height) {
785
+ sharpInstance = sharpInstance.resize(
786
+ opts.width,
787
+ opts.height,
788
+ resizeOptions
789
+ );
790
+ } else if (opts.width) {
791
+ sharpInstance = sharpInstance.resize(
792
+ opts.width,
793
+ null,
794
+ resizeOptions
795
+ );
796
+ } else {
797
+ sharpInstance = sharpInstance.resize(
798
+ null,
799
+ opts.height,
800
+ resizeOptions
801
+ );
802
+ }
803
+ }
804
+
805
+ // Configure color space
806
+ switch (opts.colorSpace.toLowerCase()) {
807
+ case "rec2020":
808
+ sharpInstance = sharpInstance.toColorspace("rec2020");
809
+ break;
810
+ case "p3":
811
+ sharpInstance = sharpInstance.toColorspace("p3");
812
+ break;
813
+ case "cmyk":
814
+ sharpInstance = sharpInstance.toColorspace("cmyk");
815
+ break;
816
+ case "srgb":
817
+ default:
818
+ sharpInstance = sharpInstance.toColorspace("srgb");
819
+ break;
820
+ }
821
+
822
+ // Configure JPEG options with performance optimizations
823
+ const jpegOptions = {
824
+ quality: Math.max(1, Math.min(100, opts.quality)),
825
+ progressive: fastMode ? false : opts.progressive, // Disable progressive for speed
826
+ mozjpeg: fastMode ? false : opts.mozjpeg, // Disable mozjpeg for speed
827
+ trellisQuantisation: fastMode ? false : opts.trellisQuantisation,
828
+ optimizeScans: fastMode ? false : opts.optimizeScans,
829
+ overshootDeringing: false, // Always disable for speed
830
+ optimizeCoding: fastMode ? false : opts.optimizeCoding,
831
+ // Add effort control for JPEG encoding
832
+ effort: fastMode ? 1 : Math.min(opts.effort || 4, 6),
833
+ };
834
+
835
+ // Set chroma subsampling
836
+ switch (opts.chromaSubsampling) {
837
+ case "4:4:4":
838
+ jpegOptions.chromaSubsampling = "4:4:4";
839
+ break;
840
+ case "4:2:2":
841
+ jpegOptions.chromaSubsampling = "4:4:4"; // Sharp doesn't support 4:2:2, use 4:4:4 instead
842
+ break;
843
+ case "4:2:0":
844
+ default:
845
+ jpegOptions.chromaSubsampling = "4:2:0";
846
+ break;
847
+ }
848
+
849
+ // Convert to JPEG and save
850
+ const jpegBuffer = await sharpInstance
851
+ .jpeg(jpegOptions)
852
+ .toBuffer({ resolveWithObject: true });
853
+
854
+ // Write to file
855
+ await sharp(jpegBuffer.data).toFile(outputPath);
856
+
857
+ const endTime = process.hrtime.bigint();
858
+ const processingTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
859
+
860
+ // Get output file stats
861
+ const fs = require("fs");
862
+ const stats = fs.statSync(outputPath);
863
+
864
+ // Calculate compression ratio
865
+ const originalSize = imageData.dataSize;
866
+ const compressedSize = stats.size;
867
+ const compressionRatio = originalSize / compressedSize;
868
+
869
+ const result = {
870
+ success: true,
871
+ outputPath: outputPath,
872
+ metadata: {
873
+ originalDimensions: {
874
+ width: imageData.width,
875
+ height: imageData.height,
876
+ },
877
+ outputDimensions: {
878
+ width: jpegBuffer.info.width,
879
+ height: jpegBuffer.info.height,
880
+ },
881
+ fileSize: {
882
+ original: originalSize,
883
+ compressed: compressedSize,
884
+ compressionRatio: compressionRatio.toFixed(2),
885
+ },
886
+ processing: {
887
+ timeMs: processingTime.toFixed(2),
888
+ throughputMBps: (
889
+ originalSize /
890
+ 1024 /
891
+ 1024 /
892
+ (processingTime / 1000)
893
+ ).toFixed(2),
894
+ },
895
+ jpegOptions: jpegOptions,
896
+ },
897
+ };
898
+
899
+ resolve(result);
900
+ } catch (error) {
901
+ reject(new Error(`JPEG conversion failed: ${error.message}`));
902
+ }
903
+ });
904
+ }
905
+
906
+ /**
907
+ * Batch convert multiple RAW files to JPEG
908
+ * @param {string[]} inputPaths - Array of input RAW file paths
909
+ * @param {string} outputDir - Output directory for JPEG files
910
+ * @param {Object} options - JPEG conversion options (same as convertToJPEG)
911
+ * @returns {Promise<Object>} - Batch conversion results
912
+ */
913
+ async batchConvertToJPEG(inputPaths, outputDir, options = {}) {
914
+ return new Promise(async (resolve, reject) => {
915
+ try {
916
+ const fs = require("fs");
917
+ const path = require("path");
918
+
919
+ // Ensure output directory exists
920
+ if (!fs.existsSync(outputDir)) {
921
+ fs.mkdirSync(outputDir, { recursive: true });
922
+ }
923
+
924
+ const results = {
925
+ successful: [],
926
+ failed: [],
927
+ summary: {
928
+ total: inputPaths.length,
929
+ processed: 0,
930
+ errors: 0,
931
+ totalProcessingTime: 0,
932
+ averageCompressionRatio: 0,
933
+ totalOriginalSize: 0,
934
+ totalCompressedSize: 0,
935
+ },
936
+ };
937
+
938
+ const startTime = process.hrtime.bigint();
939
+
940
+ for (const inputPath of inputPaths) {
941
+ try {
942
+ // Generate output filename
943
+ const baseName = path.basename(inputPath, path.extname(inputPath));
944
+ const outputPath = path.join(outputDir, `${baseName}.jpg`);
945
+
946
+ // Load the RAW file
947
+ await this.close(); // Close any previous file
948
+ await this.loadFile(inputPath);
949
+
950
+ // Convert to JPEG
951
+ const result = await this.convertToJPEG(outputPath, options);
952
+
953
+ results.successful.push({
954
+ input: inputPath,
955
+ output: outputPath,
956
+ result: result,
957
+ });
958
+
959
+ results.summary.processed++;
960
+ results.summary.totalOriginalSize +=
961
+ result.metadata.fileSize.original;
962
+ results.summary.totalCompressedSize +=
963
+ result.metadata.fileSize.compressed;
964
+ } catch (error) {
965
+ results.failed.push({
966
+ input: inputPath,
967
+ error: error.message,
968
+ });
969
+ results.summary.errors++;
970
+ }
971
+ }
972
+
973
+ const endTime = process.hrtime.bigint();
974
+ results.summary.totalProcessingTime =
975
+ Number(endTime - startTime) / 1000000; // ms
976
+
977
+ if (results.summary.totalOriginalSize > 0) {
978
+ results.summary.averageCompressionRatio = (
979
+ results.summary.totalOriginalSize /
980
+ results.summary.totalCompressedSize
981
+ ).toFixed(2);
982
+ }
983
+
984
+ results.summary.averageProcessingTimePerFile = (
985
+ results.summary.totalProcessingTime / inputPaths.length
986
+ ).toFixed(2);
987
+
988
+ resolve(results);
989
+ } catch (error) {
990
+ reject(new Error(`Batch JPEG conversion failed: ${error.message}`));
991
+ }
992
+ });
993
+ }
994
+
995
+ /**
996
+ * Get optimal JPEG conversion settings based on image analysis
997
+ * @param {Object} analysisOptions - Options for image analysis
998
+ * @returns {Promise<Object>} - Recommended JPEG settings
999
+ */
1000
+ async getOptimalJPEGSettings(analysisOptions = {}) {
1001
+ return new Promise(async (resolve, reject) => {
1002
+ try {
1003
+ // Get image metadata and process for analysis
1004
+ const metadata = await this.getMetadata();
1005
+ const imageSize = await this.getImageSize();
1006
+
1007
+ // Analyze image characteristics
1008
+ const imageArea = metadata.width * metadata.height;
1009
+ const isHighRes = imageArea > 6000 * 4000; // > 24MP
1010
+ const isMediumRes = imageArea > 3000 * 2000; // > 6MP
1011
+
1012
+ // Default settings based on image characteristics
1013
+ let recommendedSettings = {
1014
+ quality: 85,
1015
+ progressive: false,
1016
+ mozjpeg: true,
1017
+ chromaSubsampling: "4:2:0",
1018
+ optimizeCoding: true,
1019
+ trellisQuantisation: false,
1020
+ optimizeScans: false,
1021
+ reasoning: [],
1022
+ };
1023
+
1024
+ // Adjust settings based on image size
1025
+ if (isHighRes) {
1026
+ recommendedSettings.quality = 80; // Slightly lower quality for large images
1027
+ recommendedSettings.progressive = true; // Progressive loading for large images
1028
+ recommendedSettings.trellisQuantisation = true; // Better compression for large images
1029
+ recommendedSettings.reasoning.push(
1030
+ "High resolution image detected - optimizing for file size"
1031
+ );
1032
+ } else if (isMediumRes) {
1033
+ recommendedSettings.quality = 85;
1034
+ recommendedSettings.reasoning.push(
1035
+ "Medium resolution image - balanced quality/size"
1036
+ );
1037
+ } else {
1038
+ recommendedSettings.quality = 90; // Higher quality for smaller images
1039
+ recommendedSettings.chromaSubsampling = "4:4:4"; // Better chroma for small images (Sharp compatible)
1040
+ recommendedSettings.reasoning.push(
1041
+ "Lower resolution image - prioritizing quality"
1042
+ );
1043
+ }
1044
+
1045
+ // Adjust for different use cases
1046
+ if (analysisOptions.usage === "web") {
1047
+ recommendedSettings.quality = Math.min(
1048
+ recommendedSettings.quality,
1049
+ 80
1050
+ );
1051
+ recommendedSettings.progressive = true;
1052
+ recommendedSettings.optimizeScans = true;
1053
+ recommendedSettings.reasoning.push(
1054
+ "Web usage - optimized for loading speed"
1055
+ );
1056
+ } else if (analysisOptions.usage === "print") {
1057
+ recommendedSettings.quality = Math.max(
1058
+ recommendedSettings.quality,
1059
+ 90
1060
+ );
1061
+ recommendedSettings.chromaSubsampling = "4:4:4"; // Use 4:4:4 instead of 4:2:2 for Sharp compatibility
1062
+ recommendedSettings.reasoning.push(
1063
+ "Print usage - optimized for quality"
1064
+ );
1065
+ } else if (analysisOptions.usage === "archive") {
1066
+ recommendedSettings.quality = 95;
1067
+ recommendedSettings.chromaSubsampling = "4:4:4";
1068
+ recommendedSettings.trellisQuantisation = true;
1069
+ recommendedSettings.reasoning.push(
1070
+ "Archive usage - maximum quality preservation"
1071
+ );
1072
+ }
1073
+
1074
+ // Camera-specific optimizations
1075
+ if (metadata.make) {
1076
+ const make = metadata.make.toLowerCase();
1077
+ if (make.includes("canon") || make.includes("nikon")) {
1078
+ // Professional cameras often benefit from slightly different settings
1079
+ recommendedSettings.reasoning.push(
1080
+ `${metadata.make} camera detected - professional settings`
1081
+ );
1082
+ }
1083
+ }
1084
+
1085
+ resolve({
1086
+ recommended: recommendedSettings,
1087
+ imageAnalysis: {
1088
+ dimensions: {
1089
+ width: metadata.width,
1090
+ height: metadata.height,
1091
+ area: imageArea,
1092
+ },
1093
+ category: isHighRes
1094
+ ? "high-resolution"
1095
+ : isMediumRes
1096
+ ? "medium-resolution"
1097
+ : "low-resolution",
1098
+ camera: {
1099
+ make: metadata.make,
1100
+ model: metadata.model,
1101
+ },
1102
+ },
1103
+ });
1104
+ } catch (error) {
1105
+ reject(
1106
+ new Error(
1107
+ `Failed to analyze image for optimal settings: ${error.message}`
1108
+ )
1109
+ );
1110
+ }
1111
+ });
1112
+ }
1113
+
1114
+ // ============== CANCELLATION SUPPORT ==============
1115
+
1116
+ /**
1117
+ * Set cancellation flag to stop processing
1118
+ * @returns {Promise<boolean>} - Success status
1119
+ */
1120
+ async setCancelFlag() {
1121
+ return new Promise((resolve, reject) => {
1122
+ try {
1123
+ const result = this._wrapper.setCancelFlag();
1124
+ resolve(result);
1125
+ } catch (error) {
1126
+ reject(error);
1127
+ }
1128
+ });
1129
+ }
1130
+
1131
+ /**
1132
+ * Clear cancellation flag
1133
+ * @returns {Promise<boolean>} - Success status
1134
+ */
1135
+ async clearCancelFlag() {
1136
+ return new Promise((resolve, reject) => {
1137
+ try {
1138
+ const result = this._wrapper.clearCancelFlag();
1139
+ resolve(result);
1140
+ } catch (error) {
1141
+ reject(error);
1142
+ }
1143
+ });
1144
+ }
1145
+
1146
+ // ============== VERSION INFORMATION (INSTANCE METHODS) ==============
1147
+
1148
+ /**
1149
+ * Get LibRaw version string
1150
+ * @returns {string} - Version string
1151
+ */
1152
+ version() {
1153
+ return this._wrapper.version();
1154
+ }
1155
+
1156
+ /**
1157
+ * Get LibRaw version as array [major, minor, patch]
1158
+ * @returns {number[]} - Version number array
1159
+ */
1160
+ versionNumber() {
1161
+ return this._wrapper.versionNumber();
1162
+ }
1163
+
1164
+ // ============== STATIC METHODS ==============
1165
+
1166
+ /**
1167
+ * Get LibRaw version
1168
+ * @returns {string} - Version string
1169
+ */
1170
+ static getVersion() {
1171
+ return librawAddon.LibRawWrapper.getVersion();
1172
+ }
1173
+
1174
+ /**
1175
+ * Get LibRaw capabilities
1176
+ * @returns {number} - Capabilities flags
1177
+ */
1178
+ static getCapabilities() {
1179
+ return librawAddon.LibRawWrapper.getCapabilities();
1180
+ }
1181
+
1182
+ /**
1183
+ * Get list of supported cameras
1184
+ * @returns {string[]} - Array of camera names
1185
+ */
1186
+ static getCameraList() {
1187
+ return librawAddon.LibRawWrapper.getCameraList();
1188
+ }
1189
+
1190
+ /**
1191
+ * Get count of supported cameras
1192
+ * @returns {number} - Number of supported cameras
1193
+ */
1194
+ static getCameraCount() {
1195
+ return librawAddon.LibRawWrapper.getCameraCount();
1196
+ }
1197
+
1198
+ /**
1199
+ * High-performance fast JPEG conversion with minimal processing
1200
+ * @param {string} outputPath - Output JPEG file path
1201
+ * @param {Object} options - Speed-optimized JPEG options
1202
+ * @returns {Promise<Object>} - Conversion result
1203
+ */
1204
+ async convertToJPEGFast(outputPath, options = {}) {
1205
+ return this.convertToJPEG(outputPath, {
1206
+ fastMode: true,
1207
+ effort: 1, // Fastest encoding
1208
+ progressive: false,
1209
+ trellisQuantisation: false,
1210
+ optimizeScans: false,
1211
+ mozjpeg: false,
1212
+ quality: options.quality || 80,
1213
+ ...options,
1214
+ });
1215
+ }
1216
+
1217
+ /**
1218
+ * Create multiple JPEG sizes from single RAW (thumbnail, web, full)
1219
+ * @param {string} baseOutputPath - Base output path (without extension)
1220
+ * @param {Object} options - Multi-size options
1221
+ * @returns {Promise<Object>} - Multi-size conversion results
1222
+ */
1223
+ async convertToJPEGMultiSize(baseOutputPath, options = {}) {
1224
+ const sizes = options.sizes || [
1225
+ { name: "thumb", width: 400, quality: 85 },
1226
+ { name: "web", width: 1920, quality: 80 },
1227
+ { name: "full", quality: 85 },
1228
+ ];
1229
+
1230
+ // Process the RAW once (uses smart caching)
1231
+ if (!this._isProcessed) {
1232
+ await this.processImage();
1233
+ }
1234
+
1235
+ const results = {};
1236
+ const startTime = Date.now();
1237
+
1238
+ // Create all sizes sequentially to reuse cached data
1239
+ for (const sizeConfig of sizes) {
1240
+ const outputPath = `${baseOutputPath}_${sizeConfig.name}.jpg`;
1241
+ const sizeStart = Date.now();
1242
+
1243
+ const result = await this.convertToJPEG(outputPath, {
1244
+ fastMode: true,
1245
+ width: sizeConfig.width,
1246
+ height: sizeConfig.height,
1247
+ quality: sizeConfig.quality || 85,
1248
+ effort: sizeConfig.effort || 2,
1249
+ ...sizeConfig,
1250
+ });
1251
+
1252
+ const sizeEnd = Date.now();
1253
+
1254
+ results[sizeConfig.name] = {
1255
+ name: sizeConfig.name,
1256
+ outputPath,
1257
+ dimensions: result.metadata.outputDimensions,
1258
+ fileSize: result.metadata.fileSize.compressed,
1259
+ processingTime: sizeEnd - sizeStart,
1260
+ config: sizeConfig,
1261
+ };
1262
+ }
1263
+
1264
+ const endTime = Date.now();
1265
+ const totalTime = endTime - startTime;
1266
+
1267
+ return {
1268
+ success: true,
1269
+ sizes: results,
1270
+ originalDimensions: Object.values(results)[0]
1271
+ ? Object.values(results)[0].dimensions
1272
+ : { width: 0, height: 0 },
1273
+ totalProcessingTime: totalTime,
1274
+ averageTimePerSize: `${(totalTime / sizes.length).toFixed(2)}ms`,
1275
+ };
1276
+ }
1277
+
1278
+ /**
1279
+ * High-performance parallel batch conversion using worker threads
1280
+ * @param {string[]} inputPaths - Array of RAW file paths
1281
+ * @param {string} outputDir - Output directory
1282
+ * @param {Object} options - Conversion options
1283
+ * @returns {Promise<Object>} - Batch conversion results
1284
+ */
1285
+ static async batchConvertToJPEGParallel(inputPaths, outputDir, options = {}) {
1286
+ const fs = require("fs");
1287
+ const path = require("path");
1288
+ const os = require("os");
1289
+
1290
+ if (!fs.existsSync(outputDir)) {
1291
+ fs.mkdirSync(outputDir, { recursive: true });
1292
+ }
1293
+
1294
+ const maxConcurrency =
1295
+ options.maxConcurrency || Math.min(os.cpus().length, 4);
1296
+ const results = [];
1297
+ const errors = [];
1298
+ const startTime = Date.now();
1299
+
1300
+ // Process files in parallel batches
1301
+ for (let i = 0; i < inputPaths.length; i += maxConcurrency) {
1302
+ const batch = inputPaths.slice(i, i + maxConcurrency);
1303
+
1304
+ const batchPromises = batch.map(async (inputPath) => {
1305
+ try {
1306
+ const fileName = path.parse(inputPath).name;
1307
+ const outputPath = path.join(outputDir, `${fileName}.jpg`);
1308
+
1309
+ const libraw = new LibRaw();
1310
+ await libraw.loadFile(inputPath);
1311
+
1312
+ const result = await libraw.convertToJPEG(outputPath, {
1313
+ fastMode: true,
1314
+ effort: 1,
1315
+ quality: options.quality || 85,
1316
+ ...options,
1317
+ });
1318
+
1319
+ await libraw.close();
1320
+
1321
+ return {
1322
+ inputPath,
1323
+ outputPath,
1324
+ success: true,
1325
+ fileSize: result.metadata.fileSize.compressed,
1326
+ processingTime: result.metadata.processing.timeMs,
1327
+ };
1328
+ } catch (error) {
1329
+ errors.push({ inputPath, error: error.message });
1330
+ return {
1331
+ inputPath,
1332
+ success: false,
1333
+ error: error.message,
1334
+ };
1335
+ }
1336
+ });
1337
+
1338
+ const batchResults = await Promise.all(batchPromises);
1339
+ results.push(...batchResults);
1340
+ }
1341
+
1342
+ const endTime = Date.now();
1343
+ const successCount = results.filter((r) => r.success).length;
1344
+
1345
+ return {
1346
+ totalFiles: inputPaths.length,
1347
+ successCount,
1348
+ errorCount: errors.length,
1349
+ results,
1350
+ errors,
1351
+ totalProcessingTime: endTime - startTime,
1352
+ averageTimePerFile:
1353
+ successCount > 0 ? (endTime - startTime) / successCount : 0,
1354
+ };
1355
+ }
1356
+ }
1357
+
1358
+ module.exports = LibRaw;