Files
note2any/src/exif-orientation.ts
2025-09-25 22:35:01 +08:00

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;
});
}