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/CHANGELOG.md +408 -0
- package/README.md +344 -21
- package/binding.gyp +37 -18
- package/lib/index.d.ts +637 -492
- package/lib/index.js +896 -16
- package/package.json +41 -37
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(
|
|
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
|
-
// ==============
|
|
708
|
+
// ============== MEMORY STREAM OPERATIONS (NEW FEATURE) ==============
|
|
695
709
|
|
|
696
710
|
/**
|
|
697
|
-
*
|
|
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>} -
|
|
724
|
+
* @returns {Promise<Object>} - JPEG buffer with metadata
|
|
712
725
|
*/
|
|
713
|
-
async
|
|
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
|
|
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 =
|
|
872
|
+
const compressedSize = jpegBuffer.data.length;
|
|
867
873
|
const compressionRatio = originalSize / compressedSize;
|
|
868
874
|
|
|
869
875
|
const result = {
|
|
870
876
|
success: true,
|
|
871
|
-
|
|
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
|
}
|