update at 2025-09-25 22:35:01
This commit is contained in:
117
src/exif-orientation.ts
Normal file
117
src/exif-orientation.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user