Last active
May 3, 2024 15:41
-
-
Save CPatchane/bcd523298e64b1fa813cfae82b0f2b42 to your computer and use it in GitHub Desktop.
getaround-tech-blog_exif-data-manipulation-javascript
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
// Gist used for this getaround tech article: https://getaround.tech/exif-data-manipulation-javascript/ | |
const getUpdatedImage = (image, onReady) => { | |
const reader = new FileReader() | |
reader.addEventListener("load", ({ target }) => { | |
if (!target) throw new Error("no blob found") | |
const { result: buffer } = target | |
if (!buffer || typeof buffer === "string") { | |
throw new Error("not a valid JPEG") | |
} | |
const view = new DataView(buffer) | |
let offset = 0 | |
const SOI = 0xFFD8 | |
if (view.getUint16(offset) !== SOI) throw new Error("not a valid JPEG") | |
const SOS = 0xFFDA | |
const APP1 = 0xFFE1 | |
// We can skip the last two bytes 0000 and just read the four first bytes | |
const EXIF = 0x45786966 | |
const LITTLE_ENDIAN = 0x4949 | |
const BIG_ENDIAN = 0x4D4D | |
const TAG_ID_EXIF_SUB_IFD_POINTER = 0x8769 | |
const TAG_ID_ORIENTATION = 0x0112 | |
const newOrientationValue = 1 | |
const TAG_ID_EXIF_IMAGE_WIDTH = 0xA002 | |
const TAG_ID_EXIF_IMAGE_HEIGHT = 0xA003 | |
const newWidthValue = 1920 | |
const newHeightValue = 1080 | |
let marker | |
// The first two bytes (offset 0-1) was the SOI marker | |
offset += 2 | |
while (marker !== SOS) { | |
marker = view.getUint16(offset) | |
const size = view.getUint16(offset + 2) | |
if (marker === APP1 && view.getUint32(offset + 4) === EXIF) { | |
// The APP1 here is at the very beginning of the file | |
// So at this point offset = 2, | |
// + 10 to skip to the bytes after the Exif word | |
offset += 10 | |
let isLittleEndian = null | |
if (view.getUint16(offset) === LITTLE_ENDIAN) isLittleEndian = true | |
if (view.getUint16(offset) === BIG_ENDIAN) isLittleEndian = false | |
if (!isLittleEndian) throw new Error("invalid endian") | |
// From now, the endianness must be specify each time we read bytes | |
// 42 | |
if (view.getUint16(offset + 2, isLittleEndian) !== 0x2a) { | |
throw new Error("invalid endian") | |
} | |
// At this point offset = 12 | |
// IFD0 offset is given by the next 4 bytes after 42 | |
const ifd0Offset = view.getUint32(offset + 4, isLittleEndian) | |
const ifd0TagsCount = view.getUint16(offset + ifd0Offset, isLittleEndian) | |
// IFD0 ends after the two-byte tags count word + all the tags | |
const endOfIFD0TagsOffset = offset + ifd0Offset + 2 + ifd0TagsCount * 12 | |
// To store the Exif IFD offset | |
let exifSubIfdOffset = 0 | |
for ( | |
let i = offset + ifd0Offset + 2; | |
i < endOfIFD0TagsOffset; | |
i += 12 | |
) { | |
// First 2 bytes = tag type | |
const tagId = view.getUint16(i, isLittleEndian) | |
// If Orientation tag | |
if (tagId === TAG_ID_ORIENTATION) { | |
// Then 2 bytes for the tag type | |
// 3 = SHORT type | |
if (view.getUint16(i + 2, isLittleEndian) !== 3) { | |
throw new Error("Wrong orientation data type") | |
} | |
// Then 4 bytes for the count | |
if (view.getUint32(i + 4, isLittleEndian) !== 1) { | |
throw new Error("Wrong orientation data count") | |
} | |
// Since it's a SHORT, 2 bytes must be written | |
view.setUint16(i + 8, newOrientationValue, isLittleEndian) | |
} | |
// If ExifIFD offset tag | |
if (tagId === TAG_ID_EXIF_SUB_IFD_POINTER) { | |
// It's a LONG, so 4 bytes must be read | |
exifSubIfdOffset = view.getUint32(i + 8, isLittleEndian) | |
} | |
} | |
if (exifSubIfdOffset) { | |
const exifSubIfdTagsCount = view.getUint16(offset + exifSubIfdOffset, isLittleEndian) | |
// This IFD also ends after the two-byte tags count word + all the tags | |
const endOfExifSubIfdTagsOffset = | |
offset + | |
exifSubIfdOffset + | |
2 + | |
exifSubIfdTagsCount * 12 | |
for ( | |
let i = offset + exifSubIfdOffset + 2; | |
i < endOfExifSubIfdTagsOffset; | |
i += 12 | |
) { | |
// First 2 bytes = tag type | |
const tagId = view.getUint16(i, isLittleEndian) | |
// If wanted tags found | |
if (tagId === TAG_ID_EXIF_IMAGE_WIDTH || tagId === TAG_ID_EXIF_IMAGE_HEIGHT) { | |
// Then 2 bytes for the tag type | |
// 3 = SHORT type | |
if (view.getUint16(i + 2, isLittleEndian) !== 4) { | |
throw new Error("Wrong data type") | |
} | |
// Then 4 bytes for the count | |
if (view.getUint32(i + 4, isLittleEndian) !== 1) { | |
throw new Error("Wrong data count") | |
} | |
if (tagId === TAG_ID_EXIF_IMAGE_WIDTH) { | |
// Since it's a LONG, 4 bytes must be written | |
view.setUint32(i + 8, newWidthValue, isLittleEndian) | |
} else if (tagId === TAG_ID_EXIF_IMAGE_HEIGHT) { | |
// Since it's a LONG, 4 bytes must be written | |
view.setUint32(i + 8, newHeightValue, isLittleEndian) | |
} | |
} | |
} | |
} | |
return onReady(new Blob([view])) | |
} | |
// We skip the entire segment (header of 2 bytes + size of the segment) | |
offset += 2 + size | |
} | |
return | |
}) | |
// The image is given here as a a Blob, but readAsArrayBuffer can also take a File | |
reader.readAsArrayBuffer(image) | |
} | |
// 200x200 image | |
const base64ImageUrl = "https://picsum.photos/id/314/200.jpg" | |
fetch(base64ImageUrl) | |
.then(res => res.blob()) | |
.then((imageBlob) => getUpdatedImage(imageBlob, (newImageBlob) => { | |
// Exif data (ExifImageWidth and ExifImageHeight) now display 1920x1080 | |
// You might want to change other metadata like size or image width/height | |
const dataURL = URL.createObjectURL(newImageBlob) | |
const link = document.createElement("a") | |
link.download = "update-exif-test.jpeg" | |
link.href = dataURL | |
link.click() | |
})) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@TimurAUG. Thanks for the feedback, are you sure you're running it on a true JPEG image? Things are completely different for other image types like PNG or WEBP, metadata are encoded differently.
This piece of code was for this article that is focusing only on JPEG images.