feat: replace manual binary EXIF parser with robust exifr library on frontend

This commit is contained in:
2026-06-27 22:04:21 +07:00
parent 414fac3e72
commit 7f426c8e46
5 changed files with 49 additions and 142 deletions
Binary file not shown.
+7 -7
View File
@@ -21,13 +21,13 @@
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" /> <meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." /> <meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" /> <meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
<script type="module" crossorigin src="/assets/index-CqagDF1K.js"></script> <script type="module" crossorigin src="/assets/index-B_EfWg-q.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Cyuzqnbw.js"> <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CMxvf4Kt.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-others-VIU5qAPG.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-others-9VIIAnER.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-DOXkNaRS.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-editor-BDwQQzB8.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-maps-DY-S_hoR.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-maps-1-B38H26.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BqXL8Z-i.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-react-OU16SOQH.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-IjT8x9OX.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-pdf-CKa2gyHu.js">
<link rel="stylesheet" crossorigin href="/assets/vendor-maps-CcXJxNtP.css"> <link rel="stylesheet" crossorigin href="/assets/vendor-maps-CcXJxNtP.css">
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css"> <link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
<link rel="stylesheet" crossorigin href="/assets/index-w726aohe.css"> <link rel="stylesheet" crossorigin href="/assets/index-w726aohe.css">
+1
View File
@@ -18,6 +18,7 @@
"@capacitor/local-notifications": "^8.2.0", "@capacitor/local-notifications": "^8.2.0",
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"exifr": "^7.1.3",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8", "jspdf-autotable": "^5.0.8",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
+6 -127
View File
@@ -1,126 +1,4 @@
// Helper to extract GPS location from EXIF tags in JPEG/JPG files import exifr from 'exifr';
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;
}
interface ProcessedImageResult { interface ProcessedImageResult {
file: File; file: File;
@@ -129,17 +7,18 @@ interface ProcessedImageResult {
} }
export const processAndResizeImage = async (file: File): Promise<ProcessedImageResult> => { export const processAndResizeImage = async (file: File): Promise<ProcessedImageResult> => {
// 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 latitude: number | null = null;
let longitude: number | null = null; let longitude: number | null = null;
try { try {
const gps = await getExifGps(file); const gps = await exifr.gps(file);
if (gps) { if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') {
latitude = gps.latitude; latitude = gps.latitude;
longitude = gps.longitude; longitude = gps.longitude;
console.log('[imageProcessor] Successfully parsed EXIF coordinates via exifr:', latitude, longitude);
} }
} catch (e) { } 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 // 2. Perform resizing to maximum 2048px on its longest edge
+35 -8
View File
@@ -112,7 +112,7 @@
"version": "5.22.0", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
"dev": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -126,14 +126,14 @@
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
"dev": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"backend/node_modules/@prisma/fetch-engine": { "backend/node_modules/@prisma/fetch-engine": {
"version": "5.22.0", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.22.0", "@prisma/debug": "5.22.0",
@@ -145,7 +145,7 @@
"version": "5.22.0", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.22.0" "@prisma/debug": "5.22.0"
@@ -174,7 +174,7 @@
"version": "5.22.0", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"dev": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -208,6 +208,7 @@
"@capacitor/local-notifications": "^8.2.0", "@capacitor/local-notifications": "^8.2.0",
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"exifr": "^7.1.3",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8", "jspdf-autotable": "^5.0.8",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -1120,6 +1121,7 @@
"os": [ "os": [
"aix" "aix"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1137,6 +1139,7 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1154,6 +1157,7 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1171,6 +1175,7 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1188,6 +1193,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1205,6 +1211,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1222,6 +1229,7 @@
"os": [ "os": [
"freebsd" "freebsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1239,6 +1247,7 @@
"os": [ "os": [
"freebsd" "freebsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1256,6 +1265,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1273,6 +1283,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1290,6 +1301,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1307,6 +1319,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1324,6 +1337,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1341,6 +1355,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1358,6 +1373,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1375,6 +1391,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1392,6 +1409,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1409,6 +1427,7 @@
"os": [ "os": [
"netbsd" "netbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1426,6 +1445,7 @@
"os": [ "os": [
"netbsd" "netbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1443,6 +1463,7 @@
"os": [ "os": [
"openbsd" "openbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1460,6 +1481,7 @@
"os": [ "os": [
"openbsd" "openbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1477,6 +1499,7 @@
"os": [ "os": [
"openharmony" "openharmony"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1494,6 +1517,7 @@
"os": [ "os": [
"sunos" "sunos"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1511,6 +1535,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1528,6 +1553,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1545,6 +1571,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -4055,7 +4082,7 @@
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/quill": { "node_modules/@types/quill": {
@@ -4078,7 +4105,7 @@
"version": "18.3.31", "version": "18.3.31",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@@ -5929,7 +5956,7 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dargs": { "node_modules/dargs": {