Last active
August 15, 2023 17:45
-
-
Save vdavid/3f9b66b60f52204317a4cc0e77097913 to your computer and use it in GitHub Desktop.
ES6 class with zero dependencies that determines the orientation of a JPEG file, front end or back end. Returns raw orientation, but can also give a CSS transform string to use for <img style="...">. Can also convert the file to a base64 string to use as the <img src="...">. Check out the demo file below for an example on how to use it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This is a short imaginary demo file to show how to use the OrientationFixer class | |
*/ | |
/* Import (or require) the class */ | |
import OrientationFixer from 'OrientationFixer.mjs'; | |
/* Get your file, usually from an <input type="file"> */ | |
const file = new File(...); | |
/* Instantiate the orientation fixer */ | |
const orientationFixer = new OrientationFixer(); | |
/* Determine the image's orientation: a number between 1 and 8 */ | |
const orientation = await orientationFixer.determineOrientation(file); | |
/* Resolve the image orientation (number) to a CSS transform value */ | |
const orientationCss = orientationFixer.getCssTransformationByOrientationValue(orientation); | |
/* Set your image element style to fix the orientation */ | |
const imageElement = new document.getElementById('yourImageElementId'); | |
imageElement.style.transform = orientationCss; | |
/* Extra: if you also want to get the image file to a data URL */ | |
imageElement.src = orientationFixer.getBase64Image(file); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class OrientationFixer { | |
/** | |
* @param {File} file | |
* @returns {Promise<int>} | |
*/ | |
async determineOrientation(file) { | |
const data = await this._readFileToArrayBuffer(file); | |
const dataView = new DataView(data); | |
return this._isJpegFile(data, dataView) ? (this._getOrientationValueFromJpegData(dataView) || 1) : 1; | |
}; | |
/** | |
* @param {File} file | |
* @returns {Promise<string>} | |
*/ | |
async getBase64Image(file) { | |
const data = await this._readFileToArrayBuffer(file); | |
return "data:" + file.type + ";base64," + window.btoa(String.fromCharCode(new Uint8Array(data))); | |
} | |
/** | |
* @param {int} orientation | |
*/ | |
getCssTransformationByOrientationValue(orientation) { | |
const map = { | |
1: '', | |
2: 'rotateY(180deg)', | |
3: 'rotate(180deg)', | |
4: 'rotate(180deg) rotateY(180deg)', | |
5: 'rotate(270deg) rotateY(180deg)', | |
6: 'rotate(90deg)', | |
7: 'rotate(90deg) rotateY(180deg)', | |
8: 'rotate(270deg)' | |
}; | |
if (map[orientation] !== undefined) { | |
return map[orientation]; | |
} else { | |
console.error('Unknown orientation: ' + orientation + '.'); | |
} | |
} | |
/** | |
* @param {File} file | |
* @returns {Promise<ArrayBuffer>} | |
* @private | |
*/ | |
_readFileToArrayBuffer(file) { | |
return new Promise((resolve, reject) => { | |
const fileReader = new FileReader(); | |
fileReader.onloadend = progressEvent => { | |
try { | |
resolve(progressEvent.target.result); | |
} catch (error) { | |
reject(error); | |
} | |
}; | |
fileReader.readAsArrayBuffer(file); | |
}); | |
} | |
/** | |
* @param {DataView} dataView | |
* @returns {number|undefined} A number between 1 and 8, or undefined if not found. | |
* In case of undefined, 1 (no rotation) should be assumed. | |
*/ | |
_getOrientationValueFromJpegData(dataView) { | |
const exifStartUInt16 = 0xFFE1; | |
const orientationTagUInt16 = 0x0112; | |
const intelFormatLittleEndianIndicator = 0x4949; /* ...and the motorola format is 0x4D4D */ | |
const exifStartIndex = this._findUInt16InDataView(dataView, exifStartUInt16, {start: 2}); | |
if (exifStartIndex !== undefined) { | |
const isLittleEndian = dataView.getUint16(exifStartIndex + 10) === intelFormatLittleEndianIndicator; | |
const exifEndIndex = (exifStartIndex + 2) + dataView.getUint16(exifStartIndex + 2, isLittleEndian); | |
const orientationTagIndex = this._findUInt16InDataView(dataView, orientationTagUInt16, {start: exifStartIndex + 12, end: exifEndIndex, isLittleEndian}); | |
if (orientationTagIndex !== undefined) { | |
return dataView.getUint16(orientationTagIndex + 8, isLittleEndian); | |
} | |
} | |
return undefined; | |
} | |
/** | |
* @param {ArrayBuffer} data | |
* @param {DataView} dataView | |
* @returns {boolean} | |
* @private | |
*/ | |
_isJpegFile(data, dataView) { | |
return (data.byteLength >= 2) && dataView.getUint16(0) === 0xFFD8; | |
} | |
/** | |
* @param {DataView} dataView | |
* @param {number} search Two bytes. E.g. 0xFFE1 | |
* @param {int} [start] Default: 0 | |
* @param {int} [end] Default: search till the end of the data | |
* @param {boolean} [isLittleEndian] Default: false | |
* @returns {number|undefined} The byteIndex of "search" in the data, or undefined if not found. | |
*/ | |
_findUInt16InDataView(dataView, search, {start = 0, end = dataView.byteLength, isLittleEndian = false} = {}) { | |
let index = start; | |
while (index < end - 2) { | |
if (dataView.getUint16(index, isLittleEndian) === search) { | |
return index; | |
} | |
index += 2; | |
} | |
return undefined; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment