diff --git a/backend/public/downloads/yotrip-latest.apk b/backend/public/downloads/yotrip-latest.apk index aadfd23..5f3acf3 100644 Binary files a/backend/public/downloads/yotrip-latest.apk and b/backend/public/downloads/yotrip-latest.apk differ diff --git a/frontend/dist/index.html b/frontend/dist/index.html index fd7e497..b7e1c50 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -21,13 +21,13 @@ - - - - - - - + + + + + + + diff --git a/frontend/package.json b/frontend/package.json index 8e62372..074b47a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@capacitor/local-notifications": "^8.2.0", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4", "date-fns": "^4.4.0", + "exifr": "^7.1.3", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.8", "leaflet": "^1.9.4", diff --git a/frontend/src/utils/imageProcessor.ts b/frontend/src/utils/imageProcessor.ts index c98a2de..94f1f0b 100644 --- a/frontend/src/utils/imageProcessor.ts +++ b/frontend/src/utils/imageProcessor.ts @@ -1,126 +1,4 @@ -// Helper to extract GPS location from EXIF tags in JPEG/JPG files -function getExifGps(file: File): Promise<{ latitude: number; longitude: number } | null> { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const buffer = e.target?.result as ArrayBuffer; - const view = new DataView(buffer); - if (view.byteLength < 4) { - resolve(null); - return; - } - if (view.getUint16(0, false) !== 0xFFD8) { - resolve(null); - return; - } - let offset = 2; - const length = view.byteLength; - while (offset < length - 2) { - const marker = view.getUint16(offset, false); - if (marker === 0xFFE1) { - const app1Length = view.getUint16(offset + 2, false); - if (offset + 4 + app1Length > length) { - resolve(null); - return; - } - const exifHeader = view.getUint32(offset + 4, false); - if (exifHeader === 0x45786966) { - const tiffOffset = offset + 10; - const bigEndian = view.getUint16(tiffOffset, false) === 0x4D4D; - if (view.getUint16(tiffOffset + 2, bigEndian) !== 0x002A) { - resolve(null); - return; - } - const firstIFD = view.getUint32(tiffOffset + 4, bigEndian); - let ifdOffset = tiffOffset + firstIFD; - if (ifdOffset + 2 > length) { - resolve(null); - return; - } - const numEntries = view.getUint16(ifdOffset, bigEndian); - let gpsInfoOffset = 0; - for (let i = 0; i < numEntries; i++) { - const entryOffset = ifdOffset + 2 + i * 12; - if (entryOffset + 12 > length) break; - const tag = view.getUint16(entryOffset, bigEndian); - if (tag === 0x8825) { - gpsInfoOffset = view.getUint32(entryOffset + 8, bigEndian); - break; - } - } - if (gpsInfoOffset) { - const gpsIFDOffset = tiffOffset + gpsInfoOffset; - if (gpsIFDOffset + 2 > length) { - resolve(null); - return; - } - const numGpsEntries = view.getUint16(gpsIFDOffset, bigEndian); - let latRef = 'N'; - let lonRef = 'E'; - let latVal: number[] = []; - let lonVal: number[] = []; - for (let i = 0; i < numGpsEntries; i++) { - const entryOffset = gpsIFDOffset + 2 + i * 12; - if (entryOffset + 12 > length) break; - const tag = view.getUint16(entryOffset, bigEndian); - if (tag === 1) { - latRef = String.fromCharCode(view.getUint8(entryOffset + 8)); - } else if (tag === 2) { - const offsetVal = view.getUint32(entryOffset + 8, bigEndian); - latVal = readRationalArray(view, tiffOffset + offsetVal, bigEndian, length); - } else if (tag === 3) { - lonRef = String.fromCharCode(view.getUint8(entryOffset + 8)); - } else if (tag === 4) { - const offsetVal = view.getUint32(entryOffset + 8, bigEndian); - lonVal = readRationalArray(view, tiffOffset + offsetVal, bigEndian, length); - } - } - if (latVal.length === 3 && lonVal.length === 3) { - const latitude = convertDMSToDD(latVal[0], latVal[1], latVal[2], latRef); - const longitude = convertDMSToDD(lonVal[0], lonVal[1], lonVal[2], lonRef); - resolve({ latitude, longitude }); - return; - } - } - } - break; - } else if (marker >= 0xFFD0 && marker <= 0xFFD9) { - offset += 2; - } else { - const blockLength = view.getUint16(offset + 2, false); - offset += 2 + blockLength; - } - } - resolve(null); - } catch (err) { - console.error('Error parsing EXIF:', err); - resolve(null); - } - }; - reader.onerror = () => resolve(null); - reader.readAsArrayBuffer(file.slice(0, 128 * 1024)); - }); -} - -function readRationalArray(view: DataView, offset: number, bigEndian: boolean, maxLen: number): number[] { - const values = []; - if (offset + 24 > maxLen) return []; - for (let i = 0; i < 3; i++) { - const num = view.getUint32(offset + i * 8, bigEndian); - const den = view.getUint32(offset + i * 8 + 4, bigEndian); - values.push(den === 0 ? 0 : num / den); - } - return values; -} - -function convertDMSToDD(degrees: number, minutes: number, seconds: number, ref: string): number { - let dd = degrees + minutes / 60 + seconds / 3600; - if (ref === 'S' || ref === 'W') { - dd = -dd; - } - return dd; -} +import exifr from 'exifr'; interface ProcessedImageResult { file: File; @@ -129,17 +7,18 @@ interface ProcessedImageResult { } export const processAndResizeImage = async (file: File): Promise => { - // 1. Read EXIF coordinates first from the original file + // 1. Read EXIF coordinates first from the original file using exifr let latitude: number | null = null; let longitude: number | null = null; try { - const gps = await getExifGps(file); - if (gps) { + const gps = await exifr.gps(file); + if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') { latitude = gps.latitude; longitude = gps.longitude; + console.log('[imageProcessor] Successfully parsed EXIF coordinates via exifr:', latitude, longitude); } } catch (e) { - console.error('[imageProcessor] Failed to read EXIF GPS:', e); + console.error('[imageProcessor] Failed to read EXIF GPS using exifr:', e); } // 2. Perform resizing to maximum 2048px on its longest edge diff --git a/package-lock.json b/package-lock.json index ba330d5..9a9d794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,7 +112,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -126,14 +126,14 @@ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "backend/node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0", @@ -145,7 +145,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0" @@ -174,7 +174,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -208,6 +208,7 @@ "@capacitor/local-notifications": "^8.2.0", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4", "date-fns": "^4.4.0", + "exifr": "^7.1.3", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.8", "leaflet": "^1.9.4", @@ -1120,6 +1121,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -1137,6 +1139,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1154,6 +1157,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1171,6 +1175,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1188,6 +1193,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1205,6 +1211,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1222,6 +1229,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1239,6 +1247,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1256,6 +1265,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1273,6 +1283,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1290,6 +1301,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1307,6 +1319,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1324,6 +1337,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1341,6 +1355,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1358,6 +1373,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1375,6 +1391,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1392,6 +1409,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -1409,6 +1427,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1426,6 +1445,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1443,6 +1463,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1460,6 +1481,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1477,6 +1499,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -1494,6 +1517,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -1511,6 +1535,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -1528,6 +1553,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -1545,6 +1571,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -4055,7 +4082,7 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/quill": { @@ -4078,7 +4105,7 @@ "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -5929,7 +5956,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/dargs": {