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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -8,7 +8,21 @@ try {
8
8
  try {
9
9
  librawAddon = require("../build/Debug/libraw_addon");
10
10
  } catch (err2) {
11
- throw new Error('LibRaw addon not built. Run "npm run build" first.');
11
+ throw new Error(
12
+ "LibRaw addon not built. Please ensure the native module is compiled.\n" +
13
+ "\n" +
14
+ "If using Docker/pnpm, try:\n" +
15
+ " pnpm rebuild lightdrift-libraw\n" +
16
+ "Or set enable-pre-post-scripts=true in .npmrc\n" +
17
+ "\n" +
18
+ "If on Linux/Mac, ensure LibRaw is installed:\n" +
19
+ " Alpine: apk add libraw-dev\n" +
20
+ " Debian: apt-get install libraw-dev\n" +
21
+ " Mac: brew install libraw\n" +
22
+ "\n" +
23
+ "Build error: " +
24
+ err2.message
25
+ );
12
26
  }
13
27
  }
14
28
 
@@ -691,11 +705,10 @@ class LibRaw {
691
705
  });
692
706
  }
693
707
 
694
- // ============== JPEG CONVERSION (NEW FEATURE) ==============
708
+ // ============== MEMORY STREAM OPERATIONS (NEW FEATURE) ==============
695
709
 
696
710
  /**
697
- * Convert RAW to JPEG with advanced options
698
- * @param {string} outputPath - Output JPEG file path
711
+ * Create processed image as JPEG buffer in memory
699
712
  * @param {Object} options - JPEG conversion options
700
713
  * @param {number} [options.quality=85] - JPEG quality (1-100)
701
714
  * @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
@@ -708,9 +721,9 @@ class LibRaw {
708
721
  * @param {number} [options.overshootDeringing=false] - Overshoot deringing
709
722
  * @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
710
723
  * @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
711
- * @returns {Promise<Object>} - Conversion result with metadata
724
+ * @returns {Promise<Object>} - JPEG buffer with metadata
712
725
  */
713
- async convertToJPEG(outputPath, options = {}) {
726
+ async createJPEGBuffer(options = {}) {
714
727
  return new Promise(async (resolve, reject) => {
715
728
  try {
716
729
  // Set default options with performance-optimized values
@@ -846,29 +859,22 @@ class LibRaw {
846
859
  break;
847
860
  }
848
861
 
849
- // Convert to JPEG and save
862
+ // Convert to JPEG and get buffer
850
863
  const jpegBuffer = await sharpInstance
851
864
  .jpeg(jpegOptions)
852
865
  .toBuffer({ resolveWithObject: true });
853
866
 
854
- // Write to file
855
- await sharp(jpegBuffer.data).toFile(outputPath);
856
-
857
867
  const endTime = process.hrtime.bigint();
858
868
  const processingTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
859
869
 
860
- // Get output file stats
861
- const fs = require("fs");
862
- const stats = fs.statSync(outputPath);
863
-
864
870
  // Calculate compression ratio
865
871
  const originalSize = imageData.dataSize;
866
- const compressedSize = stats.size;
872
+ const compressedSize = jpegBuffer.data.length;
867
873
  const compressionRatio = originalSize / compressedSize;
868
874
 
869
875
  const result = {
870
876
  success: true,
871
- outputPath: outputPath,
877
+ buffer: jpegBuffer.data,
872
878
  metadata: {
873
879
  originalDimensions: {
874
880
  width: imageData.width,
@@ -897,6 +903,880 @@ class LibRaw {
897
903
  };
898
904
 
899
905
  resolve(result);
906
+ } catch (error) {
907
+ reject(new Error(`JPEG buffer creation failed: ${error.message}`));
908
+ }
909
+ });
910
+ }
911
+
912
+ /**
913
+ * Create processed image as PNG buffer in memory
914
+ * @param {Object} options - PNG conversion options
915
+ * @param {number} [options.width] - Target width
916
+ * @param {number} [options.height] - Target height
917
+ * @param {number} [options.compressionLevel=6] - PNG compression level (0-9)
918
+ * @param {boolean} [options.progressive=false] - Use progressive PNG
919
+ * @param {string} [options.colorSpace='srgb'] - Output color space
920
+ * @returns {Promise<Object>} - PNG buffer with metadata
921
+ */
922
+ async createPNGBuffer(options = {}) {
923
+ return new Promise(async (resolve, reject) => {
924
+ try {
925
+ const startTime = process.hrtime.bigint();
926
+
927
+ // Smart processing: only process if not already processed
928
+ if (!this._isProcessed) {
929
+ await this.processImage();
930
+ }
931
+
932
+ // Create processed image in memory (uses cache if available)
933
+ const imageData = await this.createMemoryImage();
934
+
935
+ if (!imageData || !imageData.data) {
936
+ throw new Error("Failed to create memory image from RAW data");
937
+ }
938
+
939
+ // Set up Sharp configuration
940
+ const sharpConfig = {
941
+ raw: {
942
+ width: imageData.width,
943
+ height: imageData.height,
944
+ channels: imageData.colors,
945
+ premultiplied: false,
946
+ },
947
+ sequentialRead: true,
948
+ limitInputPixels: false,
949
+ };
950
+
951
+ if (imageData.bits === 16) {
952
+ sharpConfig.raw.depth = "ushort";
953
+ }
954
+
955
+ let sharpInstance = sharp(imageData.data, sharpConfig);
956
+
957
+ // Apply resizing if specified
958
+ if (options.width || options.height) {
959
+ const resizeOptions = {
960
+ withoutEnlargement: true,
961
+ kernel: sharp.kernel.lanczos3,
962
+ fit: "inside",
963
+ fastShrinkOnLoad: true,
964
+ };
965
+
966
+ if (options.width && options.height) {
967
+ sharpInstance = sharpInstance.resize(
968
+ options.width,
969
+ options.height,
970
+ resizeOptions
971
+ );
972
+ } else if (options.width) {
973
+ sharpInstance = sharpInstance.resize(
974
+ options.width,
975
+ null,
976
+ resizeOptions
977
+ );
978
+ } else {
979
+ sharpInstance = sharpInstance.resize(
980
+ null,
981
+ options.height,
982
+ resizeOptions
983
+ );
984
+ }
985
+ }
986
+
987
+ // Configure color space
988
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
989
+ case "rec2020":
990
+ sharpInstance = sharpInstance.toColorspace("rec2020");
991
+ break;
992
+ case "p3":
993
+ sharpInstance = sharpInstance.toColorspace("p3");
994
+ break;
995
+ case "srgb":
996
+ default:
997
+ sharpInstance = sharpInstance.toColorspace("srgb");
998
+ break;
999
+ }
1000
+
1001
+ // Configure PNG options
1002
+ const pngOptions = {
1003
+ compressionLevel: Math.max(
1004
+ 0,
1005
+ Math.min(9, options.compressionLevel || 6)
1006
+ ),
1007
+ progressive: options.progressive || false,
1008
+ quality: 100, // PNG is lossless
1009
+ };
1010
+
1011
+ // Convert to PNG and get buffer
1012
+ const pngBuffer = await sharpInstance
1013
+ .png(pngOptions)
1014
+ .toBuffer({ resolveWithObject: true });
1015
+
1016
+ const endTime = process.hrtime.bigint();
1017
+ const processingTime = Number(endTime - startTime) / 1000000;
1018
+
1019
+ const result = {
1020
+ success: true,
1021
+ buffer: pngBuffer.data,
1022
+ metadata: {
1023
+ originalDimensions: {
1024
+ width: imageData.width,
1025
+ height: imageData.height,
1026
+ },
1027
+ outputDimensions: {
1028
+ width: pngBuffer.info.width,
1029
+ height: pngBuffer.info.height,
1030
+ },
1031
+ fileSize: {
1032
+ original: imageData.dataSize,
1033
+ compressed: pngBuffer.data.length,
1034
+ compressionRatio: (
1035
+ imageData.dataSize / pngBuffer.data.length
1036
+ ).toFixed(2),
1037
+ },
1038
+ processing: {
1039
+ timeMs: processingTime.toFixed(2),
1040
+ throughputMBps: (
1041
+ imageData.dataSize /
1042
+ 1024 /
1043
+ 1024 /
1044
+ (processingTime / 1000)
1045
+ ).toFixed(2),
1046
+ },
1047
+ pngOptions: pngOptions,
1048
+ },
1049
+ };
1050
+
1051
+ resolve(result);
1052
+ } catch (error) {
1053
+ reject(new Error(`PNG buffer creation failed: ${error.message}`));
1054
+ }
1055
+ });
1056
+ }
1057
+
1058
+ /**
1059
+ * Create processed image as TIFF buffer in memory
1060
+ * @param {Object} options - TIFF conversion options
1061
+ * @param {number} [options.width] - Target width
1062
+ * @param {number} [options.height] - Target height
1063
+ * @param {string} [options.compression='lzw'] - TIFF compression ('none', 'lzw', 'jpeg', 'zip')
1064
+ * @param {number} [options.quality=90] - JPEG quality when using JPEG compression
1065
+ * @param {boolean} [options.pyramid=false] - Create pyramidal TIFF
1066
+ * @param {string} [options.colorSpace='srgb'] - Output color space
1067
+ * @returns {Promise<Object>} - TIFF buffer with metadata
1068
+ */
1069
+ async createTIFFBuffer(options = {}) {
1070
+ return new Promise(async (resolve, reject) => {
1071
+ try {
1072
+ const startTime = process.hrtime.bigint();
1073
+
1074
+ // Smart processing: only process if not already processed
1075
+ if (!this._isProcessed) {
1076
+ await this.processImage();
1077
+ }
1078
+
1079
+ // Create processed image in memory (uses cache if available)
1080
+ const imageData = await this.createMemoryImage();
1081
+
1082
+ if (!imageData || !imageData.data) {
1083
+ throw new Error("Failed to create memory image from RAW data");
1084
+ }
1085
+
1086
+ // Set up Sharp configuration
1087
+ const sharpConfig = {
1088
+ raw: {
1089
+ width: imageData.width,
1090
+ height: imageData.height,
1091
+ channels: imageData.colors,
1092
+ premultiplied: false,
1093
+ },
1094
+ sequentialRead: true,
1095
+ limitInputPixels: false,
1096
+ };
1097
+
1098
+ if (imageData.bits === 16) {
1099
+ sharpConfig.raw.depth = "ushort";
1100
+ }
1101
+
1102
+ let sharpInstance = sharp(imageData.data, sharpConfig);
1103
+
1104
+ // Apply resizing if specified
1105
+ if (options.width || options.height) {
1106
+ const resizeOptions = {
1107
+ withoutEnlargement: true,
1108
+ kernel: sharp.kernel.lanczos3,
1109
+ fit: "inside",
1110
+ fastShrinkOnLoad: true,
1111
+ };
1112
+
1113
+ if (options.width && options.height) {
1114
+ sharpInstance = sharpInstance.resize(
1115
+ options.width,
1116
+ options.height,
1117
+ resizeOptions
1118
+ );
1119
+ } else if (options.width) {
1120
+ sharpInstance = sharpInstance.resize(
1121
+ options.width,
1122
+ null,
1123
+ resizeOptions
1124
+ );
1125
+ } else {
1126
+ sharpInstance = sharpInstance.resize(
1127
+ null,
1128
+ options.height,
1129
+ resizeOptions
1130
+ );
1131
+ }
1132
+ }
1133
+
1134
+ // Configure color space
1135
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
1136
+ case "rec2020":
1137
+ sharpInstance = sharpInstance.toColorspace("rec2020");
1138
+ break;
1139
+ case "p3":
1140
+ sharpInstance = sharpInstance.toColorspace("p3");
1141
+ break;
1142
+ case "srgb":
1143
+ default:
1144
+ sharpInstance = sharpInstance.toColorspace("srgb");
1145
+ break;
1146
+ }
1147
+
1148
+ // Configure TIFF options
1149
+ const tiffOptions = {
1150
+ compression: options.compression || "lzw",
1151
+ pyramid: options.pyramid || false,
1152
+ quality: options.quality || 90,
1153
+ };
1154
+
1155
+ // Convert to TIFF and get buffer
1156
+ const tiffBuffer = await sharpInstance
1157
+ .tiff(tiffOptions)
1158
+ .toBuffer({ resolveWithObject: true });
1159
+
1160
+ const endTime = process.hrtime.bigint();
1161
+ const processingTime = Number(endTime - startTime) / 1000000;
1162
+
1163
+ const result = {
1164
+ success: true,
1165
+ buffer: tiffBuffer.data,
1166
+ metadata: {
1167
+ originalDimensions: {
1168
+ width: imageData.width,
1169
+ height: imageData.height,
1170
+ },
1171
+ outputDimensions: {
1172
+ width: tiffBuffer.info.width,
1173
+ height: tiffBuffer.info.height,
1174
+ },
1175
+ fileSize: {
1176
+ original: imageData.dataSize,
1177
+ compressed: tiffBuffer.data.length,
1178
+ compressionRatio: (
1179
+ imageData.dataSize / tiffBuffer.data.length
1180
+ ).toFixed(2),
1181
+ },
1182
+ processing: {
1183
+ timeMs: processingTime.toFixed(2),
1184
+ throughputMBps: (
1185
+ imageData.dataSize /
1186
+ 1024 /
1187
+ 1024 /
1188
+ (processingTime / 1000)
1189
+ ).toFixed(2),
1190
+ },
1191
+ tiffOptions: tiffOptions,
1192
+ },
1193
+ };
1194
+
1195
+ resolve(result);
1196
+ } catch (error) {
1197
+ reject(new Error(`TIFF buffer creation failed: ${error.message}`));
1198
+ }
1199
+ });
1200
+ }
1201
+
1202
+ /**
1203
+ * Create processed image as WebP buffer in memory
1204
+ * @param {Object} options - WebP conversion options
1205
+ * @param {number} [options.width] - Target width
1206
+ * @param {number} [options.height] - Target height
1207
+ * @param {number} [options.quality=80] - WebP quality (1-100)
1208
+ * @param {boolean} [options.lossless=false] - Use lossless WebP
1209
+ * @param {number} [options.effort=4] - Encoding effort (0-6)
1210
+ * @param {string} [options.colorSpace='srgb'] - Output color space
1211
+ * @returns {Promise<Object>} - WebP buffer with metadata
1212
+ */
1213
+ async createWebPBuffer(options = {}) {
1214
+ return new Promise(async (resolve, reject) => {
1215
+ try {
1216
+ const startTime = process.hrtime.bigint();
1217
+
1218
+ // Smart processing: only process if not already processed
1219
+ if (!this._isProcessed) {
1220
+ await this.processImage();
1221
+ }
1222
+
1223
+ // Create processed image in memory (uses cache if available)
1224
+ const imageData = await this.createMemoryImage();
1225
+
1226
+ if (!imageData || !imageData.data) {
1227
+ throw new Error("Failed to create memory image from RAW data");
1228
+ }
1229
+
1230
+ // Set up Sharp configuration
1231
+ const sharpConfig = {
1232
+ raw: {
1233
+ width: imageData.width,
1234
+ height: imageData.height,
1235
+ channels: imageData.colors,
1236
+ premultiplied: false,
1237
+ },
1238
+ sequentialRead: true,
1239
+ limitInputPixels: false,
1240
+ };
1241
+
1242
+ if (imageData.bits === 16) {
1243
+ sharpConfig.raw.depth = "ushort";
1244
+ }
1245
+
1246
+ let sharpInstance = sharp(imageData.data, sharpConfig);
1247
+
1248
+ // Apply resizing if specified
1249
+ if (options.width || options.height) {
1250
+ const resizeOptions = {
1251
+ withoutEnlargement: true,
1252
+ kernel: sharp.kernel.lanczos3,
1253
+ fit: "inside",
1254
+ fastShrinkOnLoad: true,
1255
+ };
1256
+
1257
+ if (options.width && options.height) {
1258
+ sharpInstance = sharpInstance.resize(
1259
+ options.width,
1260
+ options.height,
1261
+ resizeOptions
1262
+ );
1263
+ } else if (options.width) {
1264
+ sharpInstance = sharpInstance.resize(
1265
+ options.width,
1266
+ null,
1267
+ resizeOptions
1268
+ );
1269
+ } else {
1270
+ sharpInstance = sharpInstance.resize(
1271
+ null,
1272
+ options.height,
1273
+ resizeOptions
1274
+ );
1275
+ }
1276
+ }
1277
+
1278
+ // Configure color space
1279
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
1280
+ case "rec2020":
1281
+ sharpInstance = sharpInstance.toColorspace("rec2020");
1282
+ break;
1283
+ case "p3":
1284
+ sharpInstance = sharpInstance.toColorspace("p3");
1285
+ break;
1286
+ case "srgb":
1287
+ default:
1288
+ sharpInstance = sharpInstance.toColorspace("srgb");
1289
+ break;
1290
+ }
1291
+
1292
+ // Configure WebP options
1293
+ const webpOptions = {
1294
+ quality: Math.max(1, Math.min(100, options.quality || 80)),
1295
+ lossless: options.lossless || false,
1296
+ effort: Math.max(0, Math.min(6, options.effort || 4)),
1297
+ };
1298
+
1299
+ // Convert to WebP and get buffer
1300
+ const webpBuffer = await sharpInstance
1301
+ .webp(webpOptions)
1302
+ .toBuffer({ resolveWithObject: true });
1303
+
1304
+ const endTime = process.hrtime.bigint();
1305
+ const processingTime = Number(endTime - startTime) / 1000000;
1306
+
1307
+ const result = {
1308
+ success: true,
1309
+ buffer: webpBuffer.data,
1310
+ metadata: {
1311
+ originalDimensions: {
1312
+ width: imageData.width,
1313
+ height: imageData.height,
1314
+ },
1315
+ outputDimensions: {
1316
+ width: webpBuffer.info.width,
1317
+ height: webpBuffer.info.height,
1318
+ },
1319
+ fileSize: {
1320
+ original: imageData.dataSize,
1321
+ compressed: webpBuffer.data.length,
1322
+ compressionRatio: (
1323
+ imageData.dataSize / webpBuffer.data.length
1324
+ ).toFixed(2),
1325
+ },
1326
+ processing: {
1327
+ timeMs: processingTime.toFixed(2),
1328
+ throughputMBps: (
1329
+ imageData.dataSize /
1330
+ 1024 /
1331
+ 1024 /
1332
+ (processingTime / 1000)
1333
+ ).toFixed(2),
1334
+ },
1335
+ webpOptions: webpOptions,
1336
+ },
1337
+ };
1338
+
1339
+ resolve(result);
1340
+ } catch (error) {
1341
+ reject(new Error(`WebP buffer creation failed: ${error.message}`));
1342
+ }
1343
+ });
1344
+ }
1345
+
1346
+ /**
1347
+ * Create processed image as AVIF buffer in memory
1348
+ * @param {Object} options - AVIF conversion options
1349
+ * @param {number} [options.width] - Target width
1350
+ * @param {number} [options.height] - Target height
1351
+ * @param {number} [options.quality=50] - AVIF quality (1-100)
1352
+ * @param {boolean} [options.lossless=false] - Use lossless AVIF
1353
+ * @param {number} [options.effort=4] - Encoding effort (0-9)
1354
+ * @param {string} [options.colorSpace='srgb'] - Output color space
1355
+ * @returns {Promise<Object>} - AVIF buffer with metadata
1356
+ */
1357
+ async createAVIFBuffer(options = {}) {
1358
+ return new Promise(async (resolve, reject) => {
1359
+ try {
1360
+ const startTime = process.hrtime.bigint();
1361
+
1362
+ // Smart processing: only process if not already processed
1363
+ if (!this._isProcessed) {
1364
+ await this.processImage();
1365
+ }
1366
+
1367
+ // Create processed image in memory (uses cache if available)
1368
+ const imageData = await this.createMemoryImage();
1369
+
1370
+ if (!imageData || !imageData.data) {
1371
+ throw new Error("Failed to create memory image from RAW data");
1372
+ }
1373
+
1374
+ // Set up Sharp configuration
1375
+ const sharpConfig = {
1376
+ raw: {
1377
+ width: imageData.width,
1378
+ height: imageData.height,
1379
+ channels: imageData.colors,
1380
+ premultiplied: false,
1381
+ },
1382
+ sequentialRead: true,
1383
+ limitInputPixels: false,
1384
+ };
1385
+
1386
+ if (imageData.bits === 16) {
1387
+ sharpConfig.raw.depth = "ushort";
1388
+ }
1389
+
1390
+ let sharpInstance = sharp(imageData.data, sharpConfig);
1391
+
1392
+ // Apply resizing if specified
1393
+ if (options.width || options.height) {
1394
+ const resizeOptions = {
1395
+ withoutEnlargement: true,
1396
+ kernel: sharp.kernel.lanczos3,
1397
+ fit: "inside",
1398
+ fastShrinkOnLoad: true,
1399
+ };
1400
+
1401
+ if (options.width && options.height) {
1402
+ sharpInstance = sharpInstance.resize(
1403
+ options.width,
1404
+ options.height,
1405
+ resizeOptions
1406
+ );
1407
+ } else if (options.width) {
1408
+ sharpInstance = sharpInstance.resize(
1409
+ options.width,
1410
+ null,
1411
+ resizeOptions
1412
+ );
1413
+ } else {
1414
+ sharpInstance = sharpInstance.resize(
1415
+ null,
1416
+ options.height,
1417
+ resizeOptions
1418
+ );
1419
+ }
1420
+ }
1421
+
1422
+ // Configure color space
1423
+ switch ((options.colorSpace || "srgb").toLowerCase()) {
1424
+ case "rec2020":
1425
+ sharpInstance = sharpInstance.toColorspace("rec2020");
1426
+ break;
1427
+ case "p3":
1428
+ sharpInstance = sharpInstance.toColorspace("p3");
1429
+ break;
1430
+ case "srgb":
1431
+ default:
1432
+ sharpInstance = sharpInstance.toColorspace("srgb");
1433
+ break;
1434
+ }
1435
+
1436
+ // Configure AVIF options
1437
+ const avifOptions = {
1438
+ quality: Math.max(1, Math.min(100, options.quality || 50)),
1439
+ lossless: options.lossless || false,
1440
+ effort: Math.max(0, Math.min(9, options.effort || 4)),
1441
+ };
1442
+
1443
+ // Convert to AVIF and get buffer
1444
+ const avifBuffer = await sharpInstance
1445
+ .avif(avifOptions)
1446
+ .toBuffer({ resolveWithObject: true });
1447
+
1448
+ const endTime = process.hrtime.bigint();
1449
+ const processingTime = Number(endTime - startTime) / 1000000;
1450
+
1451
+ const result = {
1452
+ success: true,
1453
+ buffer: avifBuffer.data,
1454
+ metadata: {
1455
+ originalDimensions: {
1456
+ width: imageData.width,
1457
+ height: imageData.height,
1458
+ },
1459
+ outputDimensions: {
1460
+ width: avifBuffer.info.width,
1461
+ height: avifBuffer.info.height,
1462
+ },
1463
+ fileSize: {
1464
+ original: imageData.dataSize,
1465
+ compressed: avifBuffer.data.length,
1466
+ compressionRatio: (
1467
+ imageData.dataSize / avifBuffer.data.length
1468
+ ).toFixed(2),
1469
+ },
1470
+ processing: {
1471
+ timeMs: processingTime.toFixed(2),
1472
+ throughputMBps: (
1473
+ imageData.dataSize /
1474
+ 1024 /
1475
+ 1024 /
1476
+ (processingTime / 1000)
1477
+ ).toFixed(2),
1478
+ },
1479
+ avifOptions: avifOptions,
1480
+ },
1481
+ };
1482
+
1483
+ resolve(result);
1484
+ } catch (error) {
1485
+ reject(new Error(`AVIF buffer creation failed: ${error.message}`));
1486
+ }
1487
+ });
1488
+ }
1489
+
1490
+ /**
1491
+ * Create raw PPM buffer from processed image data
1492
+ * @returns {Promise<Object>} - PPM buffer with metadata
1493
+ */
1494
+ async createPPMBuffer() {
1495
+ return new Promise(async (resolve, reject) => {
1496
+ try {
1497
+ const startTime = process.hrtime.bigint();
1498
+
1499
+ // Smart processing: only process if not already processed
1500
+ if (!this._isProcessed) {
1501
+ await this.processImage();
1502
+ }
1503
+
1504
+ // Create processed image in memory (uses cache if available)
1505
+ const imageData = await this.createMemoryImage();
1506
+
1507
+ if (!imageData || !imageData.data) {
1508
+ throw new Error("Failed to create memory image from RAW data");
1509
+ }
1510
+
1511
+ // Create PPM header
1512
+ const header = `P6\n${imageData.width} ${imageData.height}\n255\n`;
1513
+ const headerBuffer = Buffer.from(header, "ascii");
1514
+
1515
+ // Convert image data to 8-bit RGB if needed
1516
+ let rgbData;
1517
+ if (imageData.bits === 16) {
1518
+ // Convert 16-bit to 8-bit
1519
+ const pixels = imageData.width * imageData.height;
1520
+ const channels = imageData.colors;
1521
+ rgbData = Buffer.alloc(pixels * 3); // PPM is always RGB
1522
+
1523
+ for (let i = 0; i < pixels; i++) {
1524
+ const srcOffset = i * channels * 2; // 16-bit data
1525
+ const dstOffset = i * 3;
1526
+
1527
+ // Read 16-bit values and convert to 8-bit
1528
+ rgbData[dstOffset] = Math.min(
1529
+ 255,
1530
+ Math.floor((imageData.data.readUInt16LE(srcOffset) / 65535) * 255)
1531
+ ); // R
1532
+ rgbData[dstOffset + 1] = Math.min(
1533
+ 255,
1534
+ Math.floor(
1535
+ (imageData.data.readUInt16LE(srcOffset + 2) / 65535) * 255
1536
+ )
1537
+ ); // G
1538
+ rgbData[dstOffset + 2] = Math.min(
1539
+ 255,
1540
+ Math.floor(
1541
+ (imageData.data.readUInt16LE(srcOffset + 4) / 65535) * 255
1542
+ )
1543
+ ); // B
1544
+ }
1545
+ } else {
1546
+ // Already 8-bit, just copy RGB channels
1547
+ const pixels = imageData.width * imageData.height;
1548
+ const channels = imageData.colors;
1549
+ rgbData = Buffer.alloc(pixels * 3);
1550
+
1551
+ for (let i = 0; i < pixels; i++) {
1552
+ const srcOffset = i * channels;
1553
+ const dstOffset = i * 3;
1554
+
1555
+ rgbData[dstOffset] = imageData.data[srcOffset]; // R
1556
+ rgbData[dstOffset + 1] = imageData.data[srcOffset + 1]; // G
1557
+ rgbData[dstOffset + 2] = imageData.data[srcOffset + 2]; // B
1558
+ }
1559
+ }
1560
+
1561
+ // Combine header and data
1562
+ const ppmBuffer = Buffer.concat([headerBuffer, rgbData]);
1563
+
1564
+ const endTime = process.hrtime.bigint();
1565
+ const processingTime = Number(endTime - startTime) / 1000000;
1566
+
1567
+ const result = {
1568
+ success: true,
1569
+ buffer: ppmBuffer,
1570
+ metadata: {
1571
+ format: "PPM",
1572
+ dimensions: {
1573
+ width: imageData.width,
1574
+ height: imageData.height,
1575
+ },
1576
+ fileSize: {
1577
+ original: imageData.dataSize,
1578
+ compressed: ppmBuffer.length,
1579
+ compressionRatio: (imageData.dataSize / ppmBuffer.length).toFixed(
1580
+ 2
1581
+ ),
1582
+ },
1583
+ processing: {
1584
+ timeMs: processingTime.toFixed(2),
1585
+ throughputMBps: (
1586
+ imageData.dataSize /
1587
+ 1024 /
1588
+ 1024 /
1589
+ (processingTime / 1000)
1590
+ ).toFixed(2),
1591
+ },
1592
+ },
1593
+ };
1594
+
1595
+ resolve(result);
1596
+ } catch (error) {
1597
+ reject(new Error(`PPM buffer creation failed: ${error.message}`));
1598
+ }
1599
+ });
1600
+ }
1601
+
1602
+ /**
1603
+ * Create thumbnail as JPEG buffer in memory
1604
+ * @param {Object} options - JPEG options for thumbnail
1605
+ * @param {number} [options.quality=85] - JPEG quality
1606
+ * @param {number} [options.maxSize] - Maximum dimension size
1607
+ * @returns {Promise<Object>} - Thumbnail JPEG buffer with metadata
1608
+ */
1609
+ async createThumbnailJPEGBuffer(options = {}) {
1610
+ return new Promise(async (resolve, reject) => {
1611
+ try {
1612
+ const startTime = process.hrtime.bigint();
1613
+
1614
+ // Unpack thumbnail if needed
1615
+ await this.unpackThumbnail();
1616
+
1617
+ // Create thumbnail in memory
1618
+ const thumbData = await this.createMemoryThumbnail();
1619
+
1620
+ if (!thumbData || !thumbData.data) {
1621
+ throw new Error("Failed to create memory thumbnail");
1622
+ }
1623
+
1624
+ let sharpInstance;
1625
+
1626
+ // Check if thumbnail is already JPEG
1627
+ if (await this.isJPEGThumb()) {
1628
+ // Thumbnail is already JPEG, return directly or reprocess if options specified
1629
+ if (!options.quality && !options.maxSize) {
1630
+ const result = {
1631
+ success: true,
1632
+ buffer: thumbData.data,
1633
+ metadata: {
1634
+ format: "JPEG",
1635
+ dimensions: {
1636
+ width: thumbData.width,
1637
+ height: thumbData.height,
1638
+ },
1639
+ fileSize: {
1640
+ compressed: thumbData.data.length,
1641
+ },
1642
+ processing: {
1643
+ timeMs: "0.00",
1644
+ fromCache: true,
1645
+ },
1646
+ },
1647
+ };
1648
+ resolve(result);
1649
+ return;
1650
+ } else {
1651
+ // Reprocess existing JPEG with new options
1652
+ sharpInstance = sharp(thumbData.data);
1653
+ }
1654
+ } else {
1655
+ // Convert RAW thumbnail data
1656
+ const sharpConfig = {
1657
+ raw: {
1658
+ width: thumbData.width,
1659
+ height: thumbData.height,
1660
+ channels: thumbData.colors || 3,
1661
+ premultiplied: false,
1662
+ },
1663
+ };
1664
+
1665
+ if (thumbData.bits === 16) {
1666
+ sharpConfig.raw.depth = "ushort";
1667
+ }
1668
+
1669
+ sharpInstance = sharp(thumbData.data, sharpConfig);
1670
+ }
1671
+
1672
+ // Apply max size constraint if specified
1673
+ if (options.maxSize) {
1674
+ sharpInstance = sharpInstance.resize(
1675
+ options.maxSize,
1676
+ options.maxSize,
1677
+ {
1678
+ fit: "inside",
1679
+ withoutEnlargement: true,
1680
+ }
1681
+ );
1682
+ }
1683
+
1684
+ // Configure JPEG options
1685
+ const jpegOptions = {
1686
+ quality: Math.max(1, Math.min(100, options.quality || 85)),
1687
+ progressive: false, // Thumbnails typically don't need progressive
1688
+ mozjpeg: false, // Keep simple for speed
1689
+ };
1690
+
1691
+ // Convert to JPEG buffer
1692
+ const jpegBuffer = await sharpInstance
1693
+ .jpeg(jpegOptions)
1694
+ .toBuffer({ resolveWithObject: true });
1695
+
1696
+ const endTime = process.hrtime.bigint();
1697
+ const processingTime = Number(endTime - startTime) / 1000000;
1698
+
1699
+ const result = {
1700
+ success: true,
1701
+ buffer: jpegBuffer.data,
1702
+ metadata: {
1703
+ format: "JPEG",
1704
+ originalDimensions: {
1705
+ width: thumbData.width,
1706
+ height: thumbData.height,
1707
+ },
1708
+ outputDimensions: {
1709
+ width: jpegBuffer.info.width,
1710
+ height: jpegBuffer.info.height,
1711
+ },
1712
+ fileSize: {
1713
+ original: thumbData.dataSize || thumbData.data.length,
1714
+ compressed: jpegBuffer.data.length,
1715
+ compressionRatio: (
1716
+ (thumbData.dataSize || thumbData.data.length) /
1717
+ jpegBuffer.data.length
1718
+ ).toFixed(2),
1719
+ },
1720
+ processing: {
1721
+ timeMs: processingTime.toFixed(2),
1722
+ },
1723
+ jpegOptions: jpegOptions,
1724
+ },
1725
+ };
1726
+
1727
+ resolve(result);
1728
+ } catch (error) {
1729
+ reject(
1730
+ new Error(`Thumbnail JPEG buffer creation failed: ${error.message}`)
1731
+ );
1732
+ }
1733
+ });
1734
+ }
1735
+
1736
+ // ============== JPEG CONVERSION (NEW FEATURE) ==============
1737
+
1738
+ /**
1739
+ * Convert RAW to JPEG with advanced options
1740
+ * @param {string} outputPath - Output JPEG file path
1741
+ * @param {Object} options - JPEG conversion options
1742
+ * @param {number} [options.quality=85] - JPEG quality (1-100)
1743
+ * @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
1744
+ * @param {number} [options.height] - Target height (maintains aspect ratio if width not specified)
1745
+ * @param {boolean} [options.progressive=false] - Use progressive JPEG
1746
+ * @param {boolean} [options.mozjpeg=true] - Use mozjpeg encoder for better compression
1747
+ * @param {number} [options.chromaSubsampling='4:2:0'] - Chroma subsampling ('4:4:4', '4:2:2', '4:2:0')
1748
+ * @param {boolean} [options.trellisQuantisation=false] - Enable trellis quantisation
1749
+ * @param {boolean} [options.optimizeScans=false] - Optimize scan order
1750
+ * @param {number} [options.overshootDeringing=false] - Overshoot deringing
1751
+ * @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
1752
+ * @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
1753
+ * @returns {Promise<Object>} - Conversion result with metadata
1754
+ */
1755
+ async convertToJPEG(outputPath, options = {}) {
1756
+ return new Promise(async (resolve, reject) => {
1757
+ try {
1758
+ // Create JPEG buffer first
1759
+ const result = await this.createJPEGBuffer(options);
1760
+
1761
+ // Write buffer to file
1762
+ const fs = require("fs");
1763
+ fs.writeFileSync(outputPath, result.buffer);
1764
+
1765
+ // Get output file stats
1766
+ const stats = fs.statSync(outputPath);
1767
+
1768
+ // Return result in the same format as before
1769
+ resolve({
1770
+ success: true,
1771
+ outputPath: outputPath,
1772
+ metadata: {
1773
+ ...result.metadata,
1774
+ fileSize: {
1775
+ ...result.metadata.fileSize,
1776
+ compressed: stats.size,
1777
+ },
1778
+ },
1779
+ });
900
1780
  } catch (error) {
901
1781
  reject(new Error(`JPEG conversion failed: ${error.message}`));
902
1782
  }