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/CHANGELOG.md +408 -0
- package/README.md +266 -8
- package/lib/index.d.ts +139 -0
- package/lib/index.js +881 -15
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -691,11 +691,10 @@ class LibRaw {
|
|
|
691
691
|
});
|
|
692
692
|
}
|
|
693
693
|
|
|
694
|
-
// ==============
|
|
694
|
+
// ============== MEMORY STREAM OPERATIONS (NEW FEATURE) ==============
|
|
695
695
|
|
|
696
696
|
/**
|
|
697
|
-
*
|
|
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>} -
|
|
710
|
+
* @returns {Promise<Object>} - JPEG buffer with metadata
|
|
712
711
|
*/
|
|
713
|
-
async
|
|
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
|
|
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 =
|
|
858
|
+
const compressedSize = jpegBuffer.data.length;
|
|
867
859
|
const compressionRatio = originalSize / compressedSize;
|
|
868
860
|
|
|
869
861
|
const result = {
|
|
870
862
|
success: true,
|
|
871
|
-
|
|
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
|
}
|