96f412e269
- export: encodeToBase64 thay SkData.toString, dùng expo-media-library/legacy - GPS ảnh gốc: resolver EXIF -> MediaLibrary -> device (ACCESS_MEDIA_LOCATION), tag nguồn watermark - contrast vào color matrix (có hiệu lực mono), ẩn highlight/shadow, denoise/clarity ghi chú export-only - init session tuần tự, tách setState lồng nhau, share file bằng expo-sharing, DONE lưu gallery - Phase 4A: chụp camera thật qua vision-camera takePhoto rồi export filter
99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
// Pure EXIF GPS helpers — tolerant of the varied shapes EXIF parsers return:
|
|
// numbers (37.7749), arrays ([d, m, s]), rational strings ("37/1", "46/1", "26964/1000")
|
|
// and arrays of rational strings.
|
|
|
|
export function parseRational(value: unknown): number | null {
|
|
if (typeof value === 'number') {
|
|
return Number.isFinite(value) ? value : null;
|
|
}
|
|
if (typeof value === 'string') {
|
|
const parts = value.trim().split('/');
|
|
if (parts.length === 2) {
|
|
const num = Number(parts[0]);
|
|
const den = Number(parts[1]);
|
|
if (Number.isFinite(num) && Number.isFinite(den) && den !== 0) return num / den;
|
|
}
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function toDecimal(value: unknown, ref: string): number | null {
|
|
// value: single decimal (number|string) OR array [deg, min, sec]
|
|
if (Array.isArray(value)) {
|
|
const d = parseRational(value[0]);
|
|
const m = parseRational(value[1]);
|
|
const s = value.length > 2 ? parseRational(value[2]) : 0;
|
|
if (d === null || m === null || s === null || d < 0 || m < 0 || s < 0) return null;
|
|
return d + m / 60 + s / 3600;
|
|
}
|
|
const n = parseRational(value);
|
|
return n === null || n < 0 ? null : n;
|
|
}
|
|
|
|
export function parseExifCoordinate(
|
|
raw: unknown,
|
|
ref: unknown
|
|
): number | null {
|
|
const absolute = toDecimal(raw, String(ref));
|
|
if (absolute === null || absolute === 0) return null; // ignore 0,0 placeholders
|
|
const negative = ref === 'S' || ref === 'W';
|
|
return negative ? -absolute : absolute;
|
|
}
|
|
|
|
export interface ExifLike {
|
|
GPSLatitude?: unknown;
|
|
GPSLatitudeRef?: unknown;
|
|
GPSLongitude?: unknown;
|
|
GPSLongitudeRef?: unknown;
|
|
GPSDateStamp?: unknown;
|
|
GPSTimeStamp?: unknown;
|
|
DateTimeOriginal?: unknown;
|
|
}
|
|
|
|
export function parseExifGPS(exif: ExifLike | null | undefined): { latitude: number; longitude: number } | null {
|
|
if (!exif) return null;
|
|
const lat = parseExifCoordinate(exif.GPSLatitude, exif.GPSLatitudeRef);
|
|
const lon = parseExifCoordinate(exif.GPSLongitude, exif.GPSLongitudeRef);
|
|
if (lat === null || lon === null) return null;
|
|
if (Math.abs(lat) > 90 || Math.abs(lon) > 180) return null; // invalid ranges
|
|
if (lat === 0 && lon === 0) return null;
|
|
return { latitude: lat, longitude: lon };
|
|
}
|
|
|
|
const parseTimeStamp = (value: unknown): [number, number, number] | null => {
|
|
if (!Array.isArray(value)) return null;
|
|
const h = parseRational(value[0]);
|
|
const m = parseRational(value[1]);
|
|
const s = value.length > 2 ? parseRational(value[2]) : 0;
|
|
if (h === null || m === null || s === null) return null;
|
|
if (h > 23 || m > 59 || s > 61) return null;
|
|
return [h, m, s];
|
|
};
|
|
|
|
// EXIF stores dates as "2023:01:02 15:04:05" — colons, not dashes.
|
|
export function parseExifTimestamp(exif: ExifLike | null | undefined): number | null {
|
|
if (!exif) return null;
|
|
const raw = typeof exif.DateTimeOriginal === 'string' ? exif.DateTimeOriginal : '';
|
|
let dateText = raw.trim();
|
|
if (!dateText && typeof exif.GPSDateStamp === 'string') {
|
|
const gpsDate = (exif.GPSDateStamp as string).trim();
|
|
const gpsTime = parseTimeStamp(exif.GPSTimeStamp);
|
|
if (gpsTime) {
|
|
dateText = `${gpsDate} ${gpsTime[0]}:${gpsTime[1]}:${gpsTime[2]}`;
|
|
} else if (gpsDate) {
|
|
dateText = `${gpsDate} 00:00:00`;
|
|
}
|
|
}
|
|
if (!dateText) return null;
|
|
// Normalize "2023:1:2 15:4:5" (any of : / - separators, unpadded parts)
|
|
// into an ISO string "2023-01-02T15:04:05" before parsing.
|
|
const m = dateText.match(/^(\d{4})[:/-](\d{1,2})[:/-](\d{1,2})(?:[ T]+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
|
|
if (!m) return null;
|
|
const two = (v: string) => v.padStart(2, '0');
|
|
const [, y, mo, d, h = '0', mi = '0', s = '0'] = m;
|
|
const time = Date.parse(`${y}-${two(mo)}-${two(d)}T${two(h)}:${two(mi)}:${two(s)}`);
|
|
return Number.isFinite(time) ? time : null;
|
|
}
|