* refactor: Constants * refactor: Constants/Enums cleanup * refactor: Constant objects instead of multiple single value constants * refactor: Cleanup constants
137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
import { EXIF } from '$lib/constants';
|
|
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) !== EXIF.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 === EXIF.SOS_MARKER) {
|
|
return 1;
|
|
}
|
|
|
|
const segmentLength = view.getUint16(offset + 2);
|
|
|
|
if (marker === EXIF.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.EXIF_SIGNATURE ||
|
|
view.getUint16(start + 4) !== 0
|
|
) {
|
|
return 1;
|
|
}
|
|
|
|
const tiff = start + 6;
|
|
|
|
if (tiff + 8 > end) {
|
|
return 1;
|
|
}
|
|
|
|
const littleEndian = view.getUint16(tiff) === EXIF.TIFF_LITTLE_ENDIAN;
|
|
|
|
if (view.getUint16(tiff + 2, littleEndian) !== EXIF.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 * EXIF.IFD_ENTRY_SIZE;
|
|
|
|
if (entry + EXIF.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;
|
|
}
|