File size: 3,850 Bytes
ef6e870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import {
	EXIF_SCAN_BYTE_LIMIT,
	JPEG_SOI_MARKER,
	APP1_MARKER,
	SOS_MARKER,
	EXIF_SIGNATURE,
	TIFF_LITTLE_ENDIAN,
	TIFF_MAGIC,
	EXIF_ORIENTATION_TAG,
	IFD_ENTRY_SIZE
} from '$lib/constants/jpeg-exif';
import { MimeTypeImage } from '$lib/enums';

/**
 * Read the EXIF orientation tag from a JPEG base64 data URL
 *
 * Only a bounded prefix of the base64 payload is decoded, the APP1 segment
 * always sits near the start of the file.
 * @param base64UrlJpeg - The JPEG base64 data URL to inspect
 * @returns The orientation value (1 to 8), or 1 when absent or unreadable
 */
export function getJpegOrientationFromDataURL(base64UrlJpeg: string): number {
	try {
		const payloadStart = base64UrlJpeg.indexOf(',') + 1;

		if (payloadStart <= 0) {
			return 1;
		}

		// Keep the slice a multiple of 4 characters so atob accepts it
		const charLimit = Math.ceil(EXIF_SCAN_BYTE_LIMIT / 3) * 4;
		const slice = base64UrlJpeg.slice(payloadStart, payloadStart + charLimit);
		const binary = atob(slice.slice(0, slice.length - (slice.length % 4)));
		const bytes = new Uint8Array(binary.length);

		for (let i = 0; i < binary.length; i++) {
			bytes[i] = binary.charCodeAt(i);
		}

		return findExifOrientation(new DataView(bytes.buffer));
	} catch {
		return 1;
	}
}

/**
 * Walk the JPEG segments of a header buffer looking for the APP1 EXIF block
 * @param view - DataView over the JPEG header bytes
 * @returns The orientation value (1 to 8), or 1 when absent or malformed
 */
function findExifOrientation(view: DataView): number {
	if (view.byteLength < 4 || view.getUint16(0) !== JPEG_SOI_MARKER) {
		return 1;
	}

	let offset = 2;

	while (offset + 4 <= view.byteLength) {
		if (view.getUint8(offset) !== 0xff) {
			return 1;
		}

		const marker = view.getUint8(offset + 1);

		// Compressed image data starts here: no EXIF past this point
		if (marker === SOS_MARKER) {
			return 1;
		}

		const segmentLength = view.getUint16(offset + 2);

		if (marker === APP1_MARKER) {
			return parseExifOrientation(view, offset + 4, segmentLength);
		}

		offset += 2 + segmentLength;
	}

	return 1;
}

/**
 * Parse the orientation tag from an APP1 EXIF payload
 * @param view - DataView over the JPEG header bytes
 * @param start - Offset of the APP1 payload, right after the segment length
 * @param segmentLength - Declared APP1 segment length
 * @returns The orientation value (1 to 8), or 1 when absent or malformed
 */
function parseExifOrientation(view: DataView, start: number, segmentLength: number): number {
	const end = Math.min(start + segmentLength, view.byteLength);

	// The payload opens with the "Exif\0\0" signature
	if (
		start + 6 > end ||
		view.getUint32(start) !== EXIF_SIGNATURE ||
		view.getUint16(start + 4) !== 0
	) {
		return 1;
	}

	const tiff = start + 6;

	if (tiff + 8 > end) {
		return 1;
	}

	const littleEndian = view.getUint16(tiff) === TIFF_LITTLE_ENDIAN;

	if (view.getUint16(tiff + 2, littleEndian) !== TIFF_MAGIC) {
		return 1;
	}

	const ifdOffset = view.getUint32(tiff + 4, littleEndian);

	if (tiff + ifdOffset + 2 > end) {
		return 1;
	}

	const entryCount = view.getUint16(tiff + ifdOffset, littleEndian);

	// Scan IFD0 entries for the orientation tag
	for (let i = 0; i < entryCount; i++) {
		const entry = tiff + ifdOffset + 2 + i * IFD_ENTRY_SIZE;

		if (entry + IFD_ENTRY_SIZE > end) {
			return 1;
		}

		if (view.getUint16(entry, littleEndian) === EXIF_ORIENTATION_TAG) {
			const orientation = view.getUint16(entry + 8, littleEndian);

			return orientation >= 1 && orientation <= 8 ? orientation : 1;
		}
	}

	return 1;
}

/**
 * Check if a MIME type represents a JPEG
 * @param mimeType - The MIME type to check
 * @returns True if the MIME type is a JPEG variant
 */
export function isJpegMimeType(mimeType: string): boolean {
	return mimeType === MimeTypeImage.JPEG || mimeType === MimeTypeImage.JPG;
}