const fs = require('fs'); const { exiftool } = require('exiftool-vendored'); const piexif = require('piexifjs'); /** * Parses GPS coordinates from an image file (JPEG or DNG) using ExifTool * @param {string} filePath - Path to the image file * @returns {Promise<{lat: number, lng: number}|null>} GPS coordinates or null if not found */ const getGPSCoordinates = async (filePath) => { try { const tags = await exiftool.read(filePath); if (tags && typeof tags.GPSLatitude === 'number' && typeof tags.GPSLongitude === 'number') { return { lat: tags.GPSLatitude, lng: tags.GPSLongitude }; } return null; } catch (error) { console.warn(`ExifTool could not read GPS from ${filePath}: ${error.message}`); return null; } }; /** * Converts decimal degrees to Degrees, Minutes, Seconds rational format for piexifjs * @param {number} deg - Decimal degrees coordinate * @returns {Array} Array of rational numbers [[D, 1], [M, 1], [S * 100, 100]] */ const degToDmsRational = (deg) => { const absolute = Math.abs(deg); const d = Math.floor(absolute); const m = Math.floor((absolute - d) * 60); const s = Math.round((absolute - d - m / 60) * 3600 * 100); return [ [d, 1], [m, 1], [s, 100] ]; }; /** * Injects GPS coordinates into a JPEG image file using piexifjs * @param {string} filePath - Path to the JPEG file * @param {number} lat - Latitude * @param {number} lng - Longitude */ const injectGPSCoordinates = async (filePath, lat, lng) => { try { const data = fs.readFileSync(filePath); // Kiểm tra marker SOI (Start of Image) trực tiếp trên Buffer if (data[0] !== 0xFF || data[1] !== 0xD8) { throw new Error("Tệp tin không phải là định dạng JPEG hợp lệ (thiếu SOI marker)."); } const jpegBinary = data.toString('binary'); let exifObj; try { exifObj = piexif.load(jpegBinary); } catch (e) { // Nếu không có EXIF hoặc lỗi khi nạp, khởi tạo đối tượng sạch exifObj = { "0th": {}, "Exif": {}, "GPS": {} }; } // Đảm bảo các cấu trúc IFD tồn tại trước khi ghi đè exifObj["GPS"] = exifObj["GPS"] || {}; const latRef = lat >= 0 ? 'N' : 'S'; const lngRef = lng >= 0 ? 'E' : 'W'; // Thêm Version ID (Bắt buộc để một số trình đọc nhận diện khối GPS) exifObj["GPS"][piexif.GPSIFD.GPSVersionID] = [2, 2, 0, 0]; exifObj["GPS"][piexif.GPSIFD.GPSLatitudeRef] = latRef; exifObj["GPS"][piexif.GPSIFD.GPSLatitude] = degToDmsRational(lat); exifObj["GPS"][piexif.GPSIFD.GPSLongitudeRef] = lngRef; exifObj["GPS"][piexif.GPSIFD.GPSLongitude] = degToDmsRational(lng); // Chỉ đóng gói các IFD cần thiết để tránh lỗi 'pack' từ dữ liệu rác const exifBytes = piexif.dump({ "0th": exifObj["0th"] || {}, "Exif": exifObj["Exif"] || {}, "GPS": exifObj["GPS"] || {} }); const newJpegBinary = piexif.insert(exifBytes, jpegBinary); fs.writeFileSync(filePath, Buffer.from(newJpegBinary, 'binary')); } catch (error) { throw new Error(`Failed to inject EXIF GPS: ${error.message}`); } }; module.exports = { getGPSCoordinates, injectGPSCoordinates };