feat: implement imageProcessor.ts and fix fullscreen photo lightbox alignment
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
interface ProcessedImageResult {
|
||||
file: File;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
|
||||
export const processAndResizeImage = async (file: File): Promise<ProcessedImageResult> => {
|
||||
// 1. Read EXIF coordinates first from the original file
|
||||
let latitude: number | null = null;
|
||||
let longitude: number | null = null;
|
||||
try {
|
||||
const gps = await getExifGps(file);
|
||||
if (gps) {
|
||||
latitude = gps.latitude;
|
||||
longitude = gps.longitude;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[imageProcessor] Failed to read EXIF GPS:', e);
|
||||
}
|
||||
|
||||
// 2. Perform resizing to maximum 2048px on its longest edge
|
||||
return new Promise((resolve) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return resolve({ file, latitude, longitude });
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (event) => {
|
||||
const img = new Image();
|
||||
img.src = event.target?.result as string;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
const MAX_SIZE = 2048;
|
||||
|
||||
if (width > MAX_SIZE || height > MAX_SIZE) {
|
||||
if (width > height) {
|
||||
height = Math.round((height * MAX_SIZE) / width);
|
||||
width = MAX_SIZE;
|
||||
} else {
|
||||
width = Math.round((width * MAX_SIZE) / height);
|
||||
height = MAX_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return resolve({ file, latitude, longitude });
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const newName = file.name.replace(/\.[^/.]+$/, "") + ".jpg";
|
||||
const resizedFile = new File([blob], newName, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
resolve({
|
||||
file: resizedFile,
|
||||
latitude,
|
||||
longitude
|
||||
});
|
||||
} else {
|
||||
resolve({ file, latitude, longitude });
|
||||
}
|
||||
}, 'image/jpeg', 0.88); // 88% quality sweet-spot
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve({ file, latitude, longitude });
|
||||
};
|
||||
};
|
||||
reader.onerror = () => {
|
||||
resolve({ file, latitude, longitude });
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user