Skip to content

Instantly share code, notes, and snippets.

@CPatchane
Last active September 16, 2023 18:12
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
getaround-tech-blog_exif-data-manipulation-javascript
// Gist used for this getaround tech article: https://getaround.tech/exif-data-manipulation-typescript/
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