Files
rentall-app/lambdas/imageProcessor/imageProcessor.js
2026-01-14 12:11:50 -05:00

171 lines
4.2 KiB
JavaScript

const sharp = require("sharp");
const exifReader = require("exif-reader");
const { logger } = require("../shared");
/**
* Extract metadata from an image buffer
* @param {Buffer} buffer - Image buffer
* @returns {Object} Extracted metadata
*/
async function extractMetadata(buffer) {
const image = sharp(buffer);
const metadata = await image.metadata();
let exifData = {};
if (metadata.exif) {
try {
exifData = exifReader(metadata.exif);
} catch (e) {
// EXIF parsing failed, continue without it
logger.warn("Failed to parse EXIF data", { error: e.message });
}
}
return {
// GPS data
latitude: parseGpsCoordinate(
exifData?.gps?.GPSLatitude,
exifData?.gps?.GPSLatitudeRef
),
longitude: parseGpsCoordinate(
exifData?.gps?.GPSLongitude,
exifData?.gps?.GPSLongitudeRef
),
// Camera info
cameraMake: exifData?.image?.Make || null,
cameraModel: exifData?.image?.Model || null,
cameraSoftware: exifData?.image?.Software || null,
// Date/time
dateTaken: parseExifDate(
exifData?.exif?.DateTimeOriginal || exifData?.image?.DateTime
),
// Dimensions
width: metadata.width,
height: metadata.height,
orientation: metadata.orientation || 1,
// File info
fileSize: buffer.length,
};
}
/**
* Strip all metadata from an image buffer
* Uses sharp's rotate() which auto-orients AND strips EXIF
* @param {Buffer} buffer - Image buffer
* @param {string} format - Output format (default: 'jpeg')
* @returns {Buffer} Processed image buffer
*/
async function stripMetadata(buffer, format = "jpeg") {
const image = sharp(buffer);
const metadata = await image.metadata();
// Handle different formats
let processed;
if (metadata.format === "gif") {
// For GIFs, try to preserve animation but strip metadata
processed = await image
.gif()
.toBuffer();
} else if (metadata.format === "png") {
// For PNGs, rotate strips metadata and we output as PNG
processed = await image
.rotate() // Auto-orient and strip EXIF
.png()
.toBuffer();
} else if (metadata.format === "webp") {
processed = await image
.rotate()
.webp()
.toBuffer();
} else {
// Default to JPEG for best compatibility
processed = await image
.rotate() // Auto-orient and strip EXIF
.jpeg({ quality: 90 })
.toBuffer();
}
return processed;
}
/**
* Convert GPS DMS (degrees, minutes, seconds) to decimal
* @param {Array} dms - [degrees, minutes, seconds]
* @param {string} ref - N/S/E/W reference
* @returns {number|null} Decimal coordinate
*/
function parseGpsCoordinate(dms, ref) {
if (!dms || !Array.isArray(dms) || dms.length !== 3) {
return null;
}
const [degrees, minutes, seconds] = dms;
let decimal = degrees + minutes / 60 + seconds / 3600;
// South and West are negative
if (ref === "S" || ref === "W") {
decimal = -decimal;
}
// Round to 8 decimal places (about 1mm precision)
return Math.round(decimal * 100000000) / 100000000;
}
/**
* Parse EXIF date string to ISO date
* EXIF format: "YYYY:MM:DD HH:MM:SS"
* @param {string|Date} dateStr - EXIF date string or Date object
* @returns {Date|null} Parsed date
*/
function parseExifDate(dateStr) {
if (!dateStr) {
return null;
}
// If already a Date object
if (dateStr instanceof Date) {
return dateStr;
}
// EXIF format: "YYYY:MM:DD HH:MM:SS"
const match = String(dateStr).match(
/(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})/
);
if (match) {
const [, year, month, day, hour, minute, second] = match;
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`);
}
// Try parsing as ISO date
const date = new Date(dateStr);
return isNaN(date.getTime()) ? null : date;
}
/**
* Get the MIME type for a format
* @param {string} format - Image format
* @returns {string} MIME type
*/
function getMimeType(format) {
const mimeTypes = {
jpeg: "image/jpeg",
jpg: "image/jpeg",
png: "image/png",
gif: "image/gif",
webp: "image/webp",
};
return mimeTypes[format] || "image/jpeg";
}
module.exports = {
extractMetadata,
stripMetadata,
parseGpsCoordinate,
parseExifDate,
getMimeType,
};