image processing lambda
This commit is contained in:
170
lambdas/imageProcessor/imageProcessor.js
Normal file
170
lambdas/imageProcessor/imageProcessor.js
Normal file
@@ -0,0 +1,170 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user