118 lines
5.0 KiB
TypeScript
118 lines
5.0 KiB
TypeScript
/*
|
|
* Lightweight EXIF orientation reader + conditional JPEG -> PNG converter.
|
|
* We only care about orientation values 3,6,8 (rotations). Others return as-is.
|
|
*/
|
|
|
|
export async function readOrientation(blob: Blob): Promise<number | null> {
|
|
try {
|
|
console.log(`[readOrientation] Blob type: ${blob.type}, size: ${blob.size}`);
|
|
if (blob.type !== 'image/jpeg' && blob.type !== 'image/jpg') {
|
|
console.log(`[readOrientation] Not a JPEG, blob type: ${blob.type}`);
|
|
return null;
|
|
}
|
|
const buf = await blob.arrayBuffer();
|
|
const view = new DataView(buf);
|
|
console.log(`[readOrientation] ArrayBuffer length: ${buf.byteLength}`);
|
|
// JPEG starts with 0xFFD8
|
|
if (view.getUint16(0) !== 0xFFD8) {
|
|
console.log(`[readOrientation] Not a valid JPEG, header: ${view.getUint16(0).toString(16)}`);
|
|
return null;
|
|
}
|
|
console.log(`[readOrientation] Valid JPEG detected`);
|
|
let offset = 2;
|
|
const length = view.byteLength;
|
|
while (offset < length) {
|
|
if (view.getUint8(offset) !== 0xFF) break;
|
|
const marker = view.getUint8(offset + 1);
|
|
console.log(`[readOrientation] Processing marker: 0xFF${marker.toString(16).padStart(2, '0')} at offset ${offset}`);
|
|
if (marker === 0xE1) { // APP1 EXIF
|
|
const size = view.getUint16(offset + 2, false);
|
|
const exifHeader = offset + 4;
|
|
console.log(`[readOrientation] Found APP1 segment, size: ${size}`);
|
|
if (view.getUint32(exifHeader, false) === 0x45786966) { // 'Exif'
|
|
console.log(`[readOrientation] Found EXIF header`);
|
|
const tiff = exifHeader + 6;
|
|
const endian = view.getUint16(tiff, false);
|
|
const little = endian === 0x4949; // 'II'
|
|
console.log(`[readOrientation] Endian: ${little ? 'little' : 'big'} (${endian.toString(16)})`);
|
|
const getU16 = (p:number) => view.getUint16(p, little);
|
|
const getU32 = (p:number) => view.getUint32(p, little);
|
|
if (getU16(tiff + 2) !== 0x002A) {
|
|
console.log(`[readOrientation] Invalid TIFF magic: ${getU16(tiff + 2).toString(16)}`);
|
|
return null;
|
|
}
|
|
const ifdOffset = getU32(tiff + 4);
|
|
let dir = tiff + ifdOffset;
|
|
const entries = getU16(dir);
|
|
console.log(`[readOrientation] IFD has ${entries} entries`);
|
|
dir += 2;
|
|
for (let i=0;i<entries;i++) {
|
|
const entry = dir + i*12;
|
|
const tag = getU16(entry);
|
|
if (tag === 0x0112) { // Orientation
|
|
const orientationValue = getU16(entry + 8);
|
|
console.log(`[readOrientation] Found Orientation tag: ${orientationValue}`);
|
|
return orientationValue;
|
|
}
|
|
}
|
|
console.log(`[readOrientation] No Orientation tag found, returning 1`);
|
|
return 1; // treat as normal
|
|
}
|
|
offset += 2 + size;
|
|
} else if (marker === 0xDA) { // SOS
|
|
console.log(`[readOrientation] Reached SOS marker, stopping`);
|
|
break;
|
|
} else {
|
|
const size = view.getUint16(offset + 2, false);
|
|
offset += 2 + size;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(`[readOrientation] Error reading orientation:`, e);
|
|
}
|
|
console.log(`[readOrientation] No orientation found, returning null`);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Converts JPEG to PNG without rotation, regardless of orientation.
|
|
* Returns a PNG blob + suggested filename.
|
|
*/
|
|
export async function convertJpegIfNeeded(blob: Blob, filename: string): Promise<{blob: Blob; filename: string; changed: boolean; orientation: number | null}> {
|
|
console.log(`[exif-orientation] Processing ${filename}, blob type: ${blob.type}, size: ${blob.size}`);
|
|
const ori = await readOrientation(blob);
|
|
console.log(`[exif-orientation] Detected orientation for ${filename}: ${ori}`);
|
|
return new Promise((resolve) => {
|
|
const url = URL.createObjectURL(blob);
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const w = img.naturalWidth;
|
|
const h = img.naturalHeight;
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) {
|
|
URL.revokeObjectURL(url);
|
|
resolve({ blob, filename, changed: false, orientation: ori });
|
|
return;
|
|
}
|
|
canvas.width = w;
|
|
canvas.height = h;
|
|
ctx.drawImage(img, 0, 0);
|
|
canvas.toBlob(b => {
|
|
URL.revokeObjectURL(url);
|
|
if (!b) {
|
|
resolve({ blob, filename, changed: false, orientation: ori });
|
|
return;
|
|
}
|
|
const pngName = filename.replace(/\.(jpe?g)$/i, '') + '_converted.png';
|
|
resolve({ blob: b, filename: pngName, changed: true, orientation: ori });
|
|
}, 'image/png');
|
|
};
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(url);
|
|
resolve({ blob, filename, changed: false, orientation: ori });
|
|
};
|
|
img.src = url;
|
|
});
|
|
}
|