lightdrift-libraw 1.0.0-alpha.2 → 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
@@ -691,11 +691,10 @@ class LibRaw {
691
691
  });
692
692
  }
693
693
 
694
- // ============== JPEG CONVERSION (NEW FEATURE) ==============
694
+ // ============== MEMORY STREAM OPERATIONS (NEW FEATURE) ==============
695
695
 
696
696
  /**
697
- * Convert RAW to JPEG with advanced options
698
- * @param {string} outputPath - Output JPEG file path
697
+ * Create processed image as JPEG buffer in memory
699
698
  * @param {Object} options - JPEG conversion options
700
699
  * @param {number} [options.quality=85] - JPEG quality (1-100)
701
700
  * @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
@@ -708,9 +707,9 @@ class LibRaw {
708
707
  * @param {number} [options.overshootDeringing=false] - Overshoot deringing
709
708
  * @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
710
709
  * @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
711
- * @returns {Promise<Object>} - Conversion result with metadata
710
+ * @returns {Promise<Object>} - JPEG buffer with metadata
712
711
  */
713
- async convertToJPEG(outputPath, options = {}) {
712
+ async createJPEGBuffer(options = {}) {
714
713
  return new Promise(async (resolve, reject) => {
715
714
  try {
716
715
  // Set default options with performance-optimized values
@@ -846,29 +845,22 @@ class LibRaw {
846
845
  break;
847
846
  }
848
847
 
849
- // Convert to JPEG and save
848
+ // Convert to JPEG and get buffer
850
849
  const jpegBuffer = await sharpInstance
851
850
  .jpeg(jpegOptions)
852
851
  .toBuffer({ resolveWithObject: true });
853
852
 
854
- // Write to file
855
- await sharp(jpegBuffer.data).toFile(outputPath);
856
-
857
853
  const endTime = process.hrtime.bigint();
858
854
  const processingTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
859
855
 
860
- // Get output file stats
861
- const fs = require("fs");
862
- const stats = fs.statSync(outputPath);
863
-
864
856
  // Calculate compression ratio
865
857
  const originalSize = imageData.dataSize;
866
- const compressedSize = stats.size;
858
+ const compressedSize = jpegBuffer.data.length;
867
859
  const compressionRatio = originalSize / compressedSize;
868
860
 
869
861
  const result = {
870
862
  success: true,
871
- outputPath: outputPath,
863
+ buffer: jpegBuffer.data,
872
864
  metadata: {
873
865
  originalDimensions: {
874
866
  width: imageData.width,
@@ -897,6 +889,880 @@ class LibRaw {
897
889
  };
898
890
 
899
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
+ });
900
1766
  } catch (error) {
901
1767
  reject(new Error(`JPEG conversion failed: ${error.message}`));
902
1768
  }