librawspeed
Version:
Node.js Native Addon for LibRaw - Process RAW image files with JavaScript
1,793 lines (1,618 loc) • 68 kB
JavaScript
const path = require("path");
const sharp = require("sharp");
let librawAddon;
try {
// 使用 node-gyp-build 从包根目录加载预构建文件或回退到源码编译
const packageRoot = path.resolve(__dirname, '..');
librawAddon = require('node-gyp-build')(packageRoot);
} catch (err) {
try {
// 回退到传统方式(Release)
librawAddon = require("../build/Release/libraw_addon");
} catch (err2) {
try {
// 回退到 Debug
librawAddon = require("../build/Debug/libraw_addon");
} catch (err3) {
throw new Error('LibRaw 插件未构建。请先运行 "npm run build"。');
}
}
}
class LibRaw {
constructor() {
this._wrapper = new librawAddon.LibRawWrapper();
this._isProcessed = false; // 跟踪是否已调用 processImage()
this._processedImageData = null; // 缓存处理后的图像数据
}
// ============== FILE OPERATIONS ==============
/**
* 从文件系统加载 RAW 文件
* @param {string} filename - RAW 文件路径
* @returns {Promise<boolean>} - 成功状态
*/
async loadFile(filename) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.loadFile(filename);
this._isProcessed = false; // 为新文件重置处理状态
this._processedImageData = null; // 清除缓存数据
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 从内存缓冲区加载 RAW 文件
* @param {Buffer} buffer - 包含 RAW 数据的缓冲区
* @returns {Promise<boolean>} - 成功状态
*/
async loadBuffer(buffer) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.loadBuffer(buffer);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 关闭并清理资源
* @returns {Promise<boolean>} - 成功状态
*/
async close() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.close();
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== ERROR HANDLING ==============
/**
* 获取最后的错误消息
* @returns {string} - 最后的错误消息
*/
getLastError() {
return this._wrapper.getLastError();
}
/**
* 将错误代码转换为字符串
* @param {number} errorCode - 错误代码
* @returns {string} - 错误消息
*/
strerror(errorCode) {
return this._wrapper.strerror(errorCode);
}
// ============== METADATA & INFORMATION ==============
/**
* 从加载的 RAW 文件获取基本元数据
* @returns {Promise<Object>} - 元数据对象
*/
async getMetadata() {
return new Promise((resolve, reject) => {
try {
const metadata = this._wrapper.getMetadata();
resolve(metadata);
} catch (error) {
reject(error);
}
});
}
/**
* 获取图像尺寸和大小信息
* @returns {Promise<Object>} - 包含宽度/高度详情的尺寸对象
*/
async getImageSize() {
return new Promise((resolve, reject) => {
try {
const size = this._wrapper.getImageSize();
resolve(size);
} catch (error) {
reject(error);
}
});
}
/**
* 获取高级元数据,包括色彩矩阵和校准数据
* @returns {Promise<Object>} - 高级元数据对象
*/
async getAdvancedMetadata() {
return new Promise((resolve, reject) => {
try {
const metadata = this._wrapper.getAdvancedMetadata();
resolve(metadata);
} catch (error) {
reject(error);
}
});
}
/**
* 获取镜头信息
* @returns {Promise<Object>} - 镜头元数据对象
*/
async getLensInfo() {
return new Promise((resolve, reject) => {
try {
const lensInfo = this._wrapper.getLensInfo();
resolve(lensInfo);
} catch (error) {
reject(error);
}
});
}
/**
* 获取色彩信息,包括白平衡和色彩矩阵
* @returns {Promise<Object>} - 色彩信息对象
*/
async getColorInfo() {
return new Promise((resolve, reject) => {
try {
const colorInfo = this._wrapper.getColorInfo();
resolve(colorInfo);
} catch (error) {
reject(error);
}
});
}
// ============== IMAGE PROCESSING ==============
/**
* 解包缩略图数据
* @returns {Promise<boolean>} - 成功状态
*/
async unpackThumbnail() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.unpackThumbnail();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 使用当前设置处理 RAW 图像
* @returns {Promise<boolean>} - 成功状态
*/
async processImage() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.processImage();
this._isProcessed = true; // 标记为已处理
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 从 RAW 数据中减去黑电平
* @returns {Promise<boolean>} - 成功状态
*/
async subtractBlack() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.subtractBlack();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 将 RAW 数据转换为图像格式
* @returns {Promise<boolean>} - 成功状态
*/
async raw2Image() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.raw2Image();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 调整图像中的最大值
* @returns {Promise<boolean>} - 成功状态
*/
async adjustMaximum() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.adjustMaximum();
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== MEMORY IMAGE CREATION ==============
/**
* 在内存中创建处理后的图像
* @returns {Promise<Object>} - 包含缓冲区的图像数据对象
*/
async createMemoryImage() {
return new Promise((resolve, reject) => {
try {
// 如果可用则返回缓存数据
if (this._processedImageData) {
resolve(this._processedImageData);
return;
}
const imageData = this._wrapper.createMemoryImage();
// 如果图像已处理则缓存结果
if (this._isProcessed) {
this._processedImageData = imageData;
}
resolve(imageData);
} catch (error) {
reject(error);
}
});
}
/**
* 在内存中创建缩略图
* @returns {Promise<Object>} - 包含缓冲区的缩略图数据对象
*/
async createMemoryThumbnail() {
return new Promise((resolve, reject) => {
try {
const thumbData = this._wrapper.createMemoryThumbnail();
resolve(thumbData);
} catch (error) {
reject(error);
}
});
}
// ============== FILE WRITERS ==============
/**
* 将处理后的图像写入 PPM 文件
* @param {string} filename - 输出文件名
* @returns {Promise<boolean>} - 成功状态
*/
async writePPM(filename) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.writePPM(filename);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 将处理后的图像写入 TIFF 文件
* @param {string} filename - 输出文件名
* @returns {Promise<boolean>} - 成功状态
*/
async writeTIFF(filename) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.writeTIFF(filename);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 将缩略图写入文件
* @param {string} filename - 输出文件名
* @returns {Promise<boolean>} - 成功状态
*/
async writeThumbnail(filename) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.writeThumbnail(filename);
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== CONFIGURATION & SETTINGS ==============
/**
* 设置处理输出参数
* @param {Object} params - 参数对象
* @returns {Promise<boolean>} - 成功状态
*/
async setOutputParams(params) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.setOutputParams(params);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 获取当前输出参数
* @returns {Promise<Object>} - 当前参数
*/
async getOutputParams() {
return new Promise((resolve, reject) => {
try {
const params = this._wrapper.getOutputParams();
resolve(params);
} catch (error) {
reject(error);
}
});
}
// ============== UTILITY FUNCTIONS ==============
/**
* 检查图像是否使用浮点数据
* @returns {Promise<boolean>} - 浮点状态
*/
async isFloatingPoint() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.isFloatingPoint();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 检查图像是否为富士旋转
* @returns {Promise<boolean>} - 富士旋转状态
*/
async isFujiRotated() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.isFujiRotated();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 检查图像是否为 sRAW 格式
* @returns {Promise<boolean>} - sRAW 状态
*/
async isSRAW() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.isSRAW();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 检查缩略图是否为 JPEG 格式
* @returns {Promise<boolean>} - JPEG 缩略图状态
*/
async isJPEGThumb() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.isJPEGThumb();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 获取处理过程中的错误计数
* @returns {Promise<number>} - 错误数量
*/
async errorCount() {
return new Promise((resolve, reject) => {
try {
const count = this._wrapper.errorCount();
resolve(count);
} catch (error) {
reject(error);
}
});
}
// ============== EXTENDED UTILITY FUNCTIONS ==============
/**
* 检查图像是否为尼康 sRAW 格式
* @returns {Promise<boolean>} - 如果是尼康 sRAW 则为 true
*/
async isNikonSRAW() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.isNikonSRAW();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 检查图像是否为 Coolscan NEF 格式
* @returns {Promise<boolean>} - 如果是 Coolscan NEF 则为 true
*/
async isCoolscanNEF() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.isCoolscanNEF();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 检查图像是否有浮点数据
* @returns {Promise<boolean>} - 如果有浮点数据则为 true
*/
async haveFPData() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.haveFPData();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 获取 sRAW 中点值
* @returns {Promise<number>} - sRAW 中点
*/
async srawMidpoint() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.srawMidpoint();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 检查缩略图是否正常
* @param {number} [maxSize=-1] - 最大尺寸限制
* @returns {Promise<number>} - 缩略图状态
*/
async thumbOK(maxSize = -1) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.thumbOK(maxSize);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 获取解包器函数名称
* @returns {Promise<string>} - 解包器函数名称
*/
async unpackFunctionName() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.unpackFunctionName();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 获取解码器信息
* @returns {Promise<Object>} - 包含名称和标志的解码器信息
*/
async getDecoderInfo() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.getDecoderInfo();
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== ADVANCED PROCESSING ==============
/**
* 解包 RAW 数据(低级操作)
* @returns {Promise<boolean>} - 成功状态
*/
async unpack() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.unpack();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 使用扩展选项将 RAW 转换为图像
* @param {boolean} [subtractBlack=true] - 是否减去黑电平
* @returns {Promise<boolean>} - 成功状态
*/
async raw2ImageEx(subtractBlack = true) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.raw2ImageEx(subtractBlack);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 仅调整信息尺寸(不处理)
* @returns {Promise<boolean>} - 成功状态
*/
async adjustSizesInfoOnly() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.adjustSizesInfoOnly();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 释放处理后的图像数据
* @returns {Promise<boolean>} - 成功状态
*/
async freeImage() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.freeImage();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 将浮点数据转换为整数数据
* @param {number} [dmin=4096] - 最小数据值
* @param {number} [dmax=32767] - 最大数据值
* @param {number} [dtarget=16383] - 目标值
* @returns {Promise<boolean>} - 成功状态
*/
async convertFloatToInt(dmin = 4096, dmax = 32767, dtarget = 16383) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.convertFloatToInt(dmin, dmax, dtarget);
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== MEMORY OPERATIONS EXTENDED ==============
/**
* 获取内存图像格式信息
* @returns {Promise<Object>} - 包含宽度、高度、颜色、位深度的格式信息
*/
async getMemImageFormat() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.getMemImageFormat();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 将内存图像复制到缓冲区
* @param {Buffer} buffer - 目标缓冲区
* @param {number} stride - 行步长(字节)
* @param {boolean} bgr - 是否使用 BGR 顺序
* @returns {Promise<boolean>} - 成功状态
*/
async copyMemImage(buffer, stride, bgr = false) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.copyMemImage(buffer, stride, bgr);
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== COLOR OPERATIONS ==============
/**
* 获取特定位置的颜色滤镜
* @param {number} row - 行位置
* @param {number} col - 列位置
* @returns {Promise<number>} - 颜色值
*/
async getColorAt(row, col) {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.getColorAt(row, col);
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* 获取物理白点(基于 cam_mul+cam_xyz 的 xy、Kelvin、Duv)
* @returns {Promise<{xy:{x:number,y:number}, kelvin:number, duv:number}>}
*/
async getWhitepointPhysics() {
return new Promise((resolve, reject) => {
try {
const result = this._wrapper.getWhitepointPhysics();
resolve(result);
} catch (error) {
reject(error);
}
});
}
// ============== MEMORY STREAM OPERATIONS (NEW FEATURE) ==============
/**
* 在内存中创建处理后的图像作为 JPEG 缓冲区
* @param {Object} options - JPEG 转换选项
* @param {number} [options.quality=85] - JPEG 质量 (1-100)
* @param {number} [options.width] - 目标宽度(如果未指定高度则保持宽高比)
* @param {number} [options.height] - 目标高度(如果未指定宽度则保持宽高比)
* @param {boolean} [options.progressive=false] - 使用渐进式 JPEG
* @param {boolean} [options.mozjpeg=true] - 使用 mozjpeg 编码器获得更好的压缩
* @param {number} [options.chromaSubsampling='4:2:0'] - 色度子采样 ('4:4:4', '4:2:2', '4:2:0')
* @param {boolean} [options.trellisQuantisation=false] - 启用网格量化
* @param {boolean} [options.optimizeScans=false] - 优化扫描顺序
* @param {number} [options.overshootDeringing=false] - 过冲去振铃
* @param {boolean} [options.optimizeCoding=true] - 优化霍夫曼编码
* @param {string} [options.colorSpace='srgb'] - 输出色彩空间 ('srgb', 'rec2020', 'p3', 'cmyk')
* @returns {Promise<Object>} - 包含元数据的 JPEG 缓冲区
*/
async createJPEGBuffer(options = {}) {
return new Promise(async (resolve, reject) => {
try {
// 设置具有性能优化值的默认选项
const opts = {
quality: options.quality || 85,
progressive: options.progressive || false,
mozjpeg: options.mozjpeg !== false, // 默认为 true 以获得更好的压缩
chromaSubsampling: options.chromaSubsampling || "4:2:0",
trellisQuantisation: options.trellisQuantisation || false,
optimizeScans: options.optimizeScans || false,
overshootDeringing: options.overshootDeringing || false,
optimizeCoding: options.optimizeCoding !== false, // 默认为 true
colorSpace: options.colorSpace || "srgb",
...options,
};
const startTime = process.hrtime.bigint();
// 智能处理:仅在未处理时处理
if (!this._isProcessed) {
await this.processImage();
}
// 在内存中创建处理后的图像(如果可用则使用缓存)
const imageData = await this.createMemoryImage();
if (!imageData || !imageData.data) {
throw new Error("从 RAW 数据创建内存图像失败");
}
// 将 LibRaw RGB 数据转换为 Sharp 兼容缓冲区
let sharpInstance;
// 确定这是否为大图像以进行性能优化
const isLargeImage = imageData.width * imageData.height > 20_000_000; // > 20MP
const fastMode = opts.fastMode !== false; // 默认为快速模式
// 优化的 Sharp 配置
const sharpConfig = {
raw: {
width: imageData.width,
height: imageData.height,
channels: imageData.colors,
premultiplied: false,
},
// 性能优化
sequentialRead: true,
limitInputPixels: false,
density: fastMode ? 72 : 300, // 降低 DPI 以提高速度
};
if (imageData.bits === 16) {
sharpConfig.raw.depth = "ushort";
}
sharpInstance = sharp(imageData.data, sharpConfig);
// 如果指定则应用调整大小并进行性能优化
if (opts.width || opts.height) {
const resizeOptions = {
withoutEnlargement: true,
// 对大图像或启用快速模式时使用更快的核
kernel:
isLargeImage || fastMode
? sharp.kernel.cubic
: sharp.kernel.lanczos3,
fit: "inside",
fastShrinkOnLoad: true, // 启用快速加载时缩小优化
};
if (opts.width && opts.height) {
sharpInstance = sharpInstance.resize(
opts.width,
opts.height,
resizeOptions
);
} else if (opts.width) {
sharpInstance = sharpInstance.resize(
opts.width,
null,
resizeOptions
);
} else {
sharpInstance = sharpInstance.resize(
null,
opts.height,
resizeOptions
);
}
}
// 配置色彩空间
switch (opts.colorSpace.toLowerCase()) {
case "rec2020":
sharpInstance = sharpInstance.toColorspace("rec2020");
break;
case "p3":
sharpInstance = sharpInstance.toColorspace("p3");
break;
case "cmyk":
sharpInstance = sharpInstance.toColorspace("cmyk");
break;
case "srgb":
default:
sharpInstance = sharpInstance.toColorspace("srgb");
break;
}
// 配置 JPEG 选项并进行性能优化
const jpegOptions = {
quality: Math.max(1, Math.min(100, opts.quality)),
progressive: fastMode ? false : opts.progressive, // 为速度禁用渐进式
mozjpeg: fastMode ? false : opts.mozjpeg, // 为速度禁用 mozjpeg
trellisQuantisation: fastMode ? false : opts.trellisQuantisation,
optimizeScans: fastMode ? false : opts.optimizeScans,
overshootDeringing: false, // 总是为速度禁用
optimizeCoding: fastMode ? false : opts.optimizeCoding,
// 为 JPEG 编码添加努力控制
effort: fastMode ? 1 : Math.min(opts.effort || 4, 6),
};
// 设置色度子采样
switch (opts.chromaSubsampling) {
case "4:4:4":
jpegOptions.chromaSubsampling = "4:4:4";
break;
case "4:2:2":
jpegOptions.chromaSubsampling = "4:4:4"; // Sharp 不支持 4:2:2,改用 4:4:4
break;
case "4:2:0":
default:
jpegOptions.chromaSubsampling = "4:2:0";
break;
}
// 转换为 JPEG 并获取缓冲区
const jpegBuffer = await sharpInstance
.jpeg(jpegOptions)
.toBuffer({ resolveWithObject: true });
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000; // 转换为毫秒
// 计算压缩比
const originalSize = imageData.dataSize;
const compressedSize = jpegBuffer.data.length;
const compressionRatio = originalSize / compressedSize;
const result = {
success: true,
buffer: jpegBuffer.data,
metadata: {
originalDimensions: {
width: imageData.width,
height: imageData.height,
},
outputDimensions: {
width: jpegBuffer.info.width,
height: jpegBuffer.info.height,
},
fileSize: {
original: originalSize,
compressed: compressedSize,
compressionRatio: compressionRatio.toFixed(2),
},
processing: {
timeMs: processingTime.toFixed(2),
throughputMBps: (
originalSize /
1024 /
1024 /
(processingTime / 1000)
).toFixed(2),
},
jpegOptions: jpegOptions,
},
};
resolve(result);
} catch (error) {
reject(new Error(`JPEG buffer creation failed: ${error.message}`));
}
});
}
/**
* 在内存中创建处理后的图像作为 PNG 缓冲区
* @param {Object} options - PNG 转换选项
* @param {number} [options.width] - 目标宽度
* @param {number} [options.height] - 目标高度
* @param {number} [options.compressionLevel=6] - PNG 压缩级别 (0-9)
* @param {boolean} [options.progressive=false] - 使用渐进式 PNG
* @param {string} [options.colorSpace='srgb'] - 输出色彩空间
* @returns {Promise<Object>} - 包含元数据的 PNG 缓冲区
*/
async createPNGBuffer(options = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = process.hrtime.bigint();
// 智能处理:仅在未处理时处理
if (!this._isProcessed) {
await this.processImage();
}
// 在内存中创建处理后的图像(如果可用则使用缓存)
const imageData = await this.createMemoryImage();
if (!imageData || !imageData.data) {
throw new Error("从 RAW 数据创建内存图像失败");
}
// 设置 Sharp 配置
const sharpConfig = {
raw: {
width: imageData.width,
height: imageData.height,
channels: imageData.colors,
premultiplied: false,
},
sequentialRead: true,
limitInputPixels: false,
};
if (imageData.bits === 16) {
sharpConfig.raw.depth = "ushort";
}
let sharpInstance = sharp(imageData.data, sharpConfig);
// 如果指定则应用调整大小
if (options.width || options.height) {
const resizeOptions = {
withoutEnlargement: true,
kernel: sharp.kernel.lanczos3,
fit: "inside",
fastShrinkOnLoad: true,
};
if (options.width && options.height) {
sharpInstance = sharpInstance.resize(
options.width,
options.height,
resizeOptions
);
} else if (options.width) {
sharpInstance = sharpInstance.resize(
options.width,
null,
resizeOptions
);
} else {
sharpInstance = sharpInstance.resize(
null,
options.height,
resizeOptions
);
}
}
// 配置色彩空间
switch ((options.colorSpace || "srgb").toLowerCase()) {
case "rec2020":
sharpInstance = sharpInstance.toColorspace("rec2020");
break;
case "p3":
sharpInstance = sharpInstance.toColorspace("p3");
break;
case "srgb":
default:
sharpInstance = sharpInstance.toColorspace("srgb");
break;
}
// 配置 PNG 选项
const pngOptions = {
compressionLevel: Math.max(
0,
Math.min(9, options.compressionLevel || 6)
),
progressive: options.progressive || false,
quality: 100, // PNG 是无损的
};
// 转换为 PNG 并获取缓冲区
const pngBuffer = await sharpInstance
.png(pngOptions)
.toBuffer({ resolveWithObject: true });
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000;
const result = {
success: true,
buffer: pngBuffer.data,
metadata: {
originalDimensions: {
width: imageData.width,
height: imageData.height,
},
outputDimensions: {
width: pngBuffer.info.width,
height: pngBuffer.info.height,
},
fileSize: {
original: imageData.dataSize,
compressed: pngBuffer.data.length,
compressionRatio: (
imageData.dataSize / pngBuffer.data.length
).toFixed(2),
},
processing: {
timeMs: processingTime.toFixed(2),
throughputMBps: (
imageData.dataSize /
1024 /
1024 /
(processingTime / 1000)
).toFixed(2),
},
pngOptions: pngOptions,
},
};
resolve(result);
} catch (error) {
reject(new Error(`PNG buffer creation failed: ${error.message}`));
}
});
}
/**
* Create processed image as TIFF buffer in memory
* @param {Object} options - TIFF conversion options
* @param {number} [options.width] - Target width
* @param {number} [options.height] - Target height
* @param {string} [options.compression='lzw'] - TIFF compression ('none', 'lzw', 'jpeg', 'zip')
* @param {number} [options.quality=90] - JPEG quality when using JPEG compression
* @param {boolean} [options.pyramid=false] - Create pyramidal TIFF
* @param {string} [options.colorSpace='srgb'] - Output color space
* @returns {Promise<Object>} - TIFF buffer with metadata
*/
async createTIFFBuffer(options = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = process.hrtime.bigint();
// 智能处理:仅在未处理时处理
if (!this._isProcessed) {
await this.processImage();
}
// 在内存中创建处理后的图像(如果可用则使用缓存)
const imageData = await this.createMemoryImage();
if (!imageData || !imageData.data) {
throw new Error("从 RAW 数据创建内存图像失败");
}
// 设置 Sharp 配置
const sharpConfig = {
raw: {
width: imageData.width,
height: imageData.height,
channels: imageData.colors,
premultiplied: false,
},
sequentialRead: true,
limitInputPixels: false,
};
if (imageData.bits === 16) {
sharpConfig.raw.depth = "ushort";
}
let sharpInstance = sharp(imageData.data, sharpConfig);
// 如果指定则应用调整大小
if (options.width || options.height) {
const resizeOptions = {
withoutEnlargement: true,
kernel: sharp.kernel.lanczos3,
fit: "inside",
fastShrinkOnLoad: true,
};
if (options.width && options.height) {
sharpInstance = sharpInstance.resize(
options.width,
options.height,
resizeOptions
);
} else if (options.width) {
sharpInstance = sharpInstance.resize(
options.width,
null,
resizeOptions
);
} else {
sharpInstance = sharpInstance.resize(
null,
options.height,
resizeOptions
);
}
}
// 配置色彩空间
switch ((options.colorSpace || "srgb").toLowerCase()) {
case "rec2020":
sharpInstance = sharpInstance.toColorspace("rec2020");
break;
case "p3":
sharpInstance = sharpInstance.toColorspace("p3");
break;
case "srgb":
default:
sharpInstance = sharpInstance.toColorspace("srgb");
break;
}
// 配置 TIFF 选项
const tiffOptions = {
compression: options.compression || "lzw",
pyramid: options.pyramid || false,
quality: options.quality || 90,
};
// 转换为 TIFF 并获取缓冲区
const tiffBuffer = await sharpInstance
.tiff(tiffOptions)
.toBuffer({ resolveWithObject: true });
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000;
const result = {
success: true,
buffer: tiffBuffer.data,
metadata: {
originalDimensions: {
width: imageData.width,
height: imageData.height,
},
outputDimensions: {
width: tiffBuffer.info.width,
height: tiffBuffer.info.height,
},
fileSize: {
original: imageData.dataSize,
compressed: tiffBuffer.data.length,
compressionRatio: (
imageData.dataSize / tiffBuffer.data.length
).toFixed(2),
},
processing: {
timeMs: processingTime.toFixed(2),
throughputMBps: (
imageData.dataSize /
1024 /
1024 /
(processingTime / 1000)
).toFixed(2),
},
tiffOptions: tiffOptions,
},
};
resolve(result);
} catch (error) {
reject(new Error(`TIFF buffer creation failed: ${error.message}`));
}
});
}
/**
* Create processed image as WebP buffer in memory
* @param {Object} options - WebP conversion options
* @param {number} [options.width] - Target width
* @param {number} [options.height] - Target height
* @param {number} [options.quality=80] - WebP quality (1-100)
* @param {boolean} [options.lossless=false] - Use lossless WebP
* @param {number} [options.effort=4] - Encoding effort (0-6)
* @param {string} [options.colorSpace='srgb'] - Output color space
* @returns {Promise<Object>} - WebP buffer with metadata
*/
async createWebPBuffer(options = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = process.hrtime.bigint();
// 智能处理:仅在未处理时处理
if (!this._isProcessed) {
await this.processImage();
}
// 在内存中创建处理后的图像(如果可用则使用缓存)
const imageData = await this.createMemoryImage();
if (!imageData || !imageData.data) {
throw new Error("从 RAW 数据创建内存图像失败");
}
// 设置 Sharp 配置
const sharpConfig = {
raw: {
width: imageData.width,
height: imageData.height,
channels: imageData.colors,
premultiplied: false,
},
sequentialRead: true,
limitInputPixels: false,
};
if (imageData.bits === 16) {
sharpConfig.raw.depth = "ushort";
}
let sharpInstance = sharp(imageData.data, sharpConfig);
// 如果指定则应用调整大小
if (options.width || options.height) {
const resizeOptions = {
withoutEnlargement: true,
kernel: sharp.kernel.lanczos3,
fit: "inside",
fastShrinkOnLoad: true,
};
if (options.width && options.height) {
sharpInstance = sharpInstance.resize(
options.width,
options.height,
resizeOptions
);
} else if (options.width) {
sharpInstance = sharpInstance.resize(
options.width,
null,
resizeOptions
);
} else {
sharpInstance = sharpInstance.resize(
null,
options.height,
resizeOptions
);
}
}
// 配置色彩空间
switch ((options.colorSpace || "srgb").toLowerCase()) {
case "rec2020":
sharpInstance = sharpInstance.toColorspace("rec2020");
break;
case "p3":
sharpInstance = sharpInstance.toColorspace("p3");
break;
case "srgb":
default:
sharpInstance = sharpInstance.toColorspace("srgb");
break;
}
// 配置 WebP 选项
const webpOptions = {
quality: Math.max(1, Math.min(100, options.quality || 80)),
lossless: options.lossless || false,
effort: Math.max(0, Math.min(6, options.effort || 4)),
};
// 转换为 WebP 并获取缓冲区
const webpBuffer = await sharpInstance
.webp(webpOptions)
.toBuffer({ resolveWithObject: true });
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000;
const result = {
success: true,
buffer: webpBuffer.data,
metadata: {
originalDimensions: {
width: imageData.width,
height: imageData.height,
},
outputDimensions: {
width: webpBuffer.info.width,
height: webpBuffer.info.height,
},
fileSize: {
original: imageData.dataSize,
compressed: webpBuffer.data.length,
compressionRatio: (
imageData.dataSize / webpBuffer.data.length
).toFixed(2),
},
processing: {
timeMs: processingTime.toFixed(2),
throughputMBps: (
imageData.dataSize /
1024 /
1024 /
(processingTime / 1000)
).toFixed(2),
},
webpOptions: webpOptions,
},
};
resolve(result);
} catch (error) {
reject(new Error(`WebP buffer creation failed: ${error.message}`));
}
});
}
/**
* Create processed image as AVIF buffer in memory
* @param {Object} options - AVIF conversion options
* @param {number} [options.width] - Target width
* @param {number} [options.height] - Target height
* @param {number} [options.quality=50] - AVIF quality (1-100)
* @param {boolean} [options.lossless=false] - Use lossless AVIF
* @param {number} [options.effort=4] - Encoding effort (0-9)
* @param {string} [options.colorSpace='srgb'] - Output color space
* @returns {Promise<Object>} - AVIF buffer with metadata
*/
async createAVIFBuffer(options = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = process.hrtime.bigint();
// 智能处理:仅在未处理时处理
if (!this._isProcessed) {
await this.processImage();
}
// 在内存中创建处理后的图像(如果可用则使用缓存)
const imageData = await this.createMemoryImage();
if (!imageData || !imageData.data) {
throw new Error("从 RAW 数据创建内存图像失败");
}
// 设置 Sharp 配置
const sharpConfig = {
raw: {
width: imageData.width,
height: imageData.height,
channels: imageData.colors,
premultiplied: false,
},
sequentialRead: true,
limitInputPixels: false,
};
if (imageData.bits === 16) {
sharpConfig.raw.depth = "ushort";
}
let sharpInstance = sharp(imageData.data, sharpConfig);
// 如果指定则应用调整大小
if (options.width || options.height) {
const resizeOptions = {
withoutEnlargement: true,
kernel: sharp.kernel.lanczos3,
fit: "inside",
fastShrinkOnLoad: true,
};
if (options.width && options.height) {
sharpInstance = sharpInstance.resize(
options.width,
options.height,
resizeOptions
);
} else if (options.width) {
sharpInstance = sharpInstance.resize(
options.width,
null,
resizeOptions
);
} else {
sharpInstance = sharpInstance.resize(
null,
options.height,
resizeOptions
);
}
}
// 配置色彩空间
switch ((options.colorSpace || "srgb").toLowerCase()) {
case "rec2020":
sharpInstance = sharpInstance.toColorspace("rec2020");
break;
case "p3":
sharpInstance = sharpInstance.toColorspace("p3");
break;
case "srgb":
default:
sharpInstance = sharpInstance.toColorspace("srgb");
break;
}
// 配置 AVIF 选项
const avifOptions = {
quality: Math.max(1, Math.min(100, options.quality || 50)),
lossless: options.lossless || false,
effort: Math.max(0, Math.min(9, options.effort || 4)),
};
// 转换为 AVIF 并获取缓冲区
const avifBuffer = await sharpInstance
.avif(avifOptions)
.toBuffer({ resolveWithObject: true });
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000;
const result = {
success: true,
buffer: avifBuffer.data,
metadata: {
originalDimensions: {
width: imageData.width,
height: imageData.height,
},
outputDimensions: {
width: avifBuffer.info.width,
height: avifBuffer.info.height,
},
fileSize: {
original: imageData.dataSize,
compressed: avifBuffer.data.length,
compressionRatio: (
imageData.dataSize / avifBuffer.data.length
).toFixed(2),
},
processing: {
timeMs: processingTime.toFixed(2),
throughputMBps: (
imageData.dataSize /
1024 /
1024 /
(processingTime / 1000)
).toFixed(2),
},
avifOptions: avifOptions,
},
};
resolve(result);
} catch (error) {
reject(new Error(`AVIF buffer creation failed: ${error.message}`));
}
});
}
/**
* Create raw PPM buffer from processed image data
* @returns {Promise<Object>} - PPM buffer with metadata
*/
async createPPMBuffer() {
return new Promise(async (resolve, reject) => {
try {
const startTime = process.hrtime.bigint();
// 智能处理:仅在未处理时处理
if (!this._isProcessed) {
await this.processImage();
}
// 在内存中创建处理后的图像(如果可用则使用缓存)
const imageData = await this.createMemoryImage();
if (!imageData || !imageData.data) {
throw new Error("从 RAW 数据创建内存图像失败");
}
// 创建 PPM 头部
const header = `P6\n${imageData.width} ${imageData.height}\n255\n`;
const headerBuffer = Buffer.from(header, "ascii");
// 如果需要,将图像数据转换为 8 位 RGB
let rgbData;
if (imageData.bits === 16) {
// 将 16 位转换为 8 位
const pixels = imageData.width * imageData.height;
const channels = imageData.colors;
rgbData = Buffer.alloc(pixels * 3); // PPM 总是 RGB
for (let i = 0; i < pixels; i++) {
const srcOffset = i * channels * 2; // 16 位数据
const dstOffset = i * 3;
// 读取 16 位值并转换为 8 位
rgbData[dstOffset] = Math.min(
255,
Math.floor((imageData.data.readUInt16LE(srcOffset) / 65535) * 255)
); // R
rgbData[dstOffset + 1] = Math.min(
255,
Math.floor(
(imageData.data.readUInt16LE(srcOffset + 2) / 65535) * 255
)
); // G
rgbData[dstOffset + 2] = Math.min(
255,
Math.floor(
(imageData.data.readUInt16LE(srcOffset + 4) / 65535) * 255
)
); // B
}
} else {
// Already 8-bit, just copy RGB channels
const pixels = imageData.width * imageData.height;
const channels = imageData.colors;
rgbData = Buffer.alloc(pixels * 3);
for (let i = 0; i < pixels; i++) {
const srcOffset = i * channels;
const dstOffset = i * 3;
rgbData[dstOffset] = imageData.data[srcOffset]; // R
rgbData[dstOffset + 1] = imageData.data[srcOffset + 1]; // G
rgbData[dstOffset + 2] = imageData.data[srcOffset + 2]; // B
}
}
// Combine header and data
const ppmBuffer = Buffer.concat([headerBuffer, rgbData]);
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000;
const result = {
success: true,
buffer: ppmBuffer,
metadata: {
format: "PPM",
dimensions: {
width: imageData.width,
height: imageData.height,
},
fileSize: {
original: imageData.dataSize,
compressed: ppmBuffer.length,
compressionRatio: (imageData.dataSize / ppmBuffer.length).toFixed(
2
),
},
processing: {
timeMs: processingTime.toFixed(2),
throughputMBps: (
imageData.dataSize /
1024 /
1024 /
(processingTime / 1000)
).toFixed(2),
},
},
};
resolve(result);
} catch (error) {
reject(new Error(`PPM buffer creation failed: ${error.message}`));
}
});
}
/**
* Create thumbnail as JPEG buffer in memory
* @param {Object} options - JPEG options for thumbnail
* @param {number} [options.quality=85] - JPEG quality
* @param {number} [options.maxSize] - Maximum dimension size
* @returns {Promise<Object>} - Thumbnail JPEG buffer with metadata
*/
async createThumbnailJPEGBuffer(options = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = process.hrtime.bigint();
// Unpack thumbnail if needed
await this.unpackThumbnail();
// Create thumbnail in memory
const thumbData = await this.createMemoryThumbnail();
if (!thumbData || !thumbData.data) {
throw new Error("Failed to create memory thumbnail");
}
let sharpInstance;
// Check if thumbnail is already JPEG
if (await this.isJPEGThumb()) {
// Thumbnail is already JPEG, return directly or reprocess if options specified
if (!options.quality && !options.maxSize) {
const result = {
success: true,
buffer: thumbData.data,
metadata: {
format: "JPEG",
dimensions: {
width: thumbData.width,
height: thumbData.height,
},
fileSize: {
compressed: thumbData.data.length,
},
processing: {
timeMs: "0.00",
fromCache: true,
},
},
};
resolve(result);
return;
} else {
// Reprocess existing JPEG with new options
sharpInstance = sharp(thumbData.data);
}
} else {
// Convert RAW thumbnail data
const sharpConfig = {
raw: {
width: thumbData.width,
height: thumbData.height,
channels: thumbData.colors || 3,
premultiplied: false,
},
};
if (thumbData.bits === 16) {
sharpConfig.raw.depth = "ushort";
}
sharpInstance = sharp(thumbData.data, sharpConfig);
}
// Apply max size constraint if specified
if (options.maxSize) {
sharpInstance = sharpInstance.resize(
options.maxSize,
options.maxSize,
{
fit: "inside",
withoutEnlargement: true,
}
);
}
// Configure JPEG options
const jpegOptions = {
quality: Math.max(1, Math.min(100, options.quality || 85)),
progressive: false, // Thumbnails typically don't need progressive
mozjpeg: false, // Keep simple for speed
};
// Convert to JPEG buffer
const jpegBuffer = await sharpInstance
.jpeg(jpegOptions)
.toBuffer({ resolveWithObject: true });
const endTime = process.hrtime.bigint();
const processingTime = Number(endTime - startTime) / 1000000;
const result = {
success: true,
buffer: jpegBuffer.data,
metadata: {
format: "JPEG",
originalDimensions: {
width: thumbData.width,
height: thumbData.height,
},
outputDimensions: {
width: jpegBuffer.info.width,
height: jpegBuffer.info.height,
},
fileSize: {
original: thumbData.dataSize || thumbData.data.length,
compressed: jpegBuffer.data.length,
compressionRatio: (
(thumbData.dataSize || thumbData.data.length) /
jpegBuffer.data.length
).toFixed(2),
},
processing: {
timeMs: processingTime.toFixed(2),
},
jpegOptions: jpegOptions,
},
};
resolve(result);
} catch (error) {
reject(
new Error(`Thumbnail JPEG buffer creation failed: ${error.message}`)
);
}
});
}
// ============== JPEG CONVERSION (NEW FEATURE) ==============
/**
* Convert RAW to JPEG with advanced options
* @param {string} inputPath - Input RAW file path (optional, uses currently loaded file if not provided)
* @param {string} outputPath - Output JPEG file path
* @param {Object} options - JPEG conversion options
* @param {number} [options.quality=85] - JPEG quality (1-100)
* @param {number} [options.width] - Target width (maintains aspect ratio if height not specified)
* @param {number} [options.height] - Target height (maintains aspect ratio if width not specified)
* @param {boolean} [options.progressive=false] - Use progressive JPEG
* @param {boolean} [options.mozjpeg=true] - Use mozjpeg encoder for better compression
* @param {number} [options.chromaSubsampling='4:2:0'] - Chroma subsampling ('4:4:4', '4:2:2', '4:2:0')
* @param {boolean} [options.trellisQuantisation=false] - Enable trellis quantisation
* @param {boolean} [options.optimizeScans=false] - Optimize scan order
* @param {number} [options.overshootDeringing=false] - Overshoot deringing
* @param {boolean} [options.optimizeCoding=true] - Optimize Huffman coding
* @param {string} [options.colorSpace='srgb'] - Output color space ('srgb', 'rec2020', 'p3', 'cmyk')
* @returns {Promise<Object>} - Conversion result with metadata
*/
async convertToJPEG(inputPath, outputPath, options = {}) {
return new Promise(async (resolve, reject) => {
try {
// Handle parameter variations
let actualInputPath, actualOutputPath, actualOptions;
if (typeof inputPath === 'string' && typeof outputPath === 'string') {
// Three parameters: inputPath, outputPath, options
actualInputPath = inputPath;
actualOutputPath = outputPath;
actualOptions = options || {};
} else if (typeof inputPath === 'string' && typeof outputPath === 'object') {
// Two parameters: outputPath, options (backward compatibility)
actualInputPath = null; // Use currently loaded file
actualOutputPath = inputPath;
actualOptions = outputPath || {};
} else {
throw new Error('Invalid parameters. Expected: convertToJPEG(inputPath, outputPath, options) or convertToJPEG(outputPath, options)');
}
// Load file if inputPath is provided and different from currently loaded file
if (actualInputPath) {
await this.loadFile(actualInputPath);
}
// Create JPEG buffer first
const result = await this.createJPEGBuffer(actualOptions);
// Write buffer to