// 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 else if (view.getUint16(offset) === BIG_ENDIAN) isLittleEndian = false else 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() }))