171 lines
4.2 KiB
JavaScript
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,
|
|
};
|