Last active
September 27, 2017 06:32
-
-
Save mrharel/7ee4aa7c3ef3ce7a44643ffe827a3b8a to your computer and use it in GitHub Desktop.
Resize and flip the file (using the exif lib) if needed
This file contains 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
import EXIF from "exif-js"; | |
const DEFAULT_MAX_SIZE = 1600; | |
const JPEG_QUALITY = 0.9; | |
const FILE_READER_STATE_DONE = 2; | |
const JPEG_FILETYPE = "image/jpeg"; | |
/** | |
* Set up operations and degrees to rotate for each EXIF orientation (index). | |
*/ | |
const ExifOrientations = [ | |
{ op: "none", degrees: 0 }, | |
{ op: "flip-x", degrees: 0 }, | |
{ op: "none", degrees: 180 }, | |
{ op: "flip-y", degrees: 0 }, | |
{ op: "flip-x", degrees: 90 }, | |
{ op: "none", degrees: 90 }, | |
{ op: "flip-x", degrees: -90 }, | |
{ op: "none", degrees: -90 }, | |
]; | |
const calculateImageResolution = (img, maxSize) => { | |
const size = { width: img.width, height: img.height }; | |
if (img.width > maxSize || img.height > maxSize) { | |
const ratio = img.width / img.height; | |
if (img.width >= img.height) { | |
size.width = maxSize; | |
size.height = maxSize / ratio; | |
} else { | |
size.height = maxSize; | |
size.width = maxSize * ratio; | |
} | |
} | |
return size; | |
}; | |
const isImageDimChanged = (orientation) => !! ~[5, 6, 7, 8].indexOf(orientation); | |
const setDimensions = (canvas, size, orientation) => { | |
if (isImageDimChanged(orientation)) { | |
canvas.setAttribute("height", `${size.width}`); | |
canvas.setAttribute("width", `${size.height}`); | |
} else { | |
canvas.setAttribute("height", `${size.height}`); | |
canvas.setAttribute("width", `${size.width}`); | |
} | |
}; | |
const flipContext = (ctx, canvas, x, y) => { | |
ctx.translate(x ? canvas.width : 0, y ? canvas.height : 0); | |
ctx.scale(x ? -1 : 1, y ? -1 : 1); | |
}; | |
const rotateContext = (ctx, attr) => { | |
const x = attr.x || 0; | |
const y = attr.y || 0; | |
let radians = 0; | |
if (attr.degrees) { | |
radians = attr.degrees * (Math.PI / 180); | |
} | |
ctx.translate(x, y); | |
ctx.rotate(radians); | |
ctx.translate(-x, -y); | |
}; | |
const getExifOrientations = (orientation) => { | |
let orientationIndex = orientation; | |
if (orientationIndex < 1 || orientationIndex > 8) { | |
orientationIndex = 1; | |
} | |
return ExifOrientations[orientationIndex - 1]; | |
}; | |
const drawFileToCanvas = (file) => { | |
const { | |
resized: { | |
meta: { | |
tags: { | |
Orientation: orientation = 1, | |
} = {}, | |
img, | |
maxSize, | |
} = {}, | |
} = {}, | |
} = file; | |
const canvas = document.createElement("canvas"); | |
const ctx = canvas.getContext("2d"); | |
if (!ctx) { | |
console.error("failed to get canvas context"); | |
return null; | |
} | |
const exifOrientation = getExifOrientations(orientation); | |
const size = calculateImageResolution(img, maxSize); | |
setDimensions(canvas, size, orientation); | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Flip vertically or horizontally | |
if (exifOrientation.op === "flip-x") flipContext(ctx, canvas, true, false); | |
if (exifOrientation.op === "flip-y") flipContext(ctx, canvas, false, true); | |
// Rotate image | |
if (exifOrientation.degrees) { | |
rotateContext(ctx, { | |
degrees: exifOrientation.degrees, | |
x: canvas.width / 2, | |
y: canvas.height / 2, | |
}); | |
if (isImageDimChanged(orientation)) { | |
const diff = canvas.width - canvas.height; | |
ctx.translate(diff / 2, -diff / 2); | |
} | |
} | |
ctx.drawImage(img, 0, 0, img.width, img.height, // Source rectangle | |
0, 0, size.width, size.height); | |
return canvas; | |
}; | |
const base64toBlob = (base64Data, contentType = "", sliceSize = 512) => { | |
const byteCharacters = atob(base64Data); | |
const byteArrays = []; | |
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { | |
const slice = byteCharacters.slice(offset, offset + sliceSize); | |
const byteNumbers = new Array(slice.length); | |
for (let i = 0; i < slice.length; i++) { | |
byteNumbers[i] = slice.charCodeAt(i); | |
} | |
const byteArray = new Uint8Array(byteNumbers); | |
byteArrays.push(byteArray); | |
} | |
return new Blob(byteArrays, { type: contentType }); | |
}; | |
const rotateAndResizeByMeta = (file, callback) => { | |
const canvas = drawFileToCanvas(file); | |
if (!canvas) { | |
callback({ err: "Failed to draw into the canvas", preview: null, imageData: null, type: null }); | |
return; | |
} | |
const dataUrl = canvas.toDataURL(JPEG_FILETYPE, JPEG_QUALITY); | |
const base64 = dataUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ""); | |
const imageData = base64toBlob(base64, JPEG_FILETYPE); | |
callback({ preview: dataUrl, imageData, type: JPEG_FILETYPE, err: null }); | |
}; | |
const readFileAndCreateImage = (file, callback) => { | |
const reader = new FileReader(); | |
reader.onload = () => { | |
if (reader.readyState === FILE_READER_STATE_DONE) { | |
const img = new Image(); | |
img.onload = () => { | |
callback({ data: reader.result, img, error: null }); | |
}; | |
img.src = file.preview; | |
} | |
}; | |
reader.onerror = () => { | |
callback({ data: null, img: null, error: "Failed to read file as array buffer" }); | |
}; | |
reader.readAsArrayBuffer(file); | |
}; | |
const rotateAndResize = (file, maxSize, callback) => readFileAndCreateImage(file, ({ data, img, error }) => { | |
file.resized = { // eslint-disable-line no-param-reassign | |
error, | |
data, | |
preview: null, | |
type: null, | |
meta: { | |
img, | |
tags: null, | |
maxSize, | |
}, | |
}; | |
if (error) { | |
callback(); | |
return; | |
} | |
file.resized.meta.tags = EXIF.readFromBinaryFile(data); // eslint-disable-line no-param-reassign | |
rotateAndResizeByMeta(file, ({ err, preview, imageData, type }) => { | |
delete file.resized.meta; // eslint-disable-line no-param-reassign | |
if (err) { | |
file.resized.error = err; // eslint-disable-line no-param-reassign | |
} else { | |
file.resized.preview = preview; // eslint-disable-line no-param-reassign | |
file.resized.type = type; // eslint-disable-line no-param-reassign | |
file.resized.data = imageData; // eslint-disable-line no-param-reassign | |
} | |
callback(); | |
}); | |
}); | |
/** | |
* This function gets a File array and add the following property to the file: | |
* resized {Object}: | |
* data {Array<UInt8>} | |
* type {String} | |
* preview {String} | |
* error {String} | |
* @param files | |
* @param maxSize | |
* @param callback | |
*/ | |
export default ({ | |
files, | |
maxSize = DEFAULT_MAX_SIZE, | |
callback }) => { | |
let completedJobs = 0; | |
files.forEach(file => rotateAndResize(file, maxSize, () => { | |
completedJobs++; | |
if (file.resized && file.resized.error) { | |
log.error(`Error in resize file : ${file.resized.error}`); | |
} | |
if (completedJobs === files.length) { | |
callback(); | |
} | |
})); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment