Skip to content

Instantly share code, notes, and snippets.

@tonytonyjan
Last active November 28, 2024 09:12
Show Gist options
  • Save tonytonyjan/ffb7cd0e82cb293b843ece7e79364233 to your computer and use it in GitHub Desktop.
Save tonytonyjan/ffb7cd0e82cb293b843ece7e79364233 to your computer and use it in GitHub Desktop.
copy EXIF from one JPEG blob to another and return a new JPEG blob.
// Copyright (c) 2019 Weihang Jian <tonytonyjan@gmail.com>
export default async (src, dest) => {
const exif = await retrieveExif(src);
return new Blob([dest.slice(0, 2), exif, dest.slice(2)], {
type: "image/jpeg"
});
};
export const SOS = 0xffda,
APP1 = 0xffe1,
EXIF = 0x45786966,
retrieveExif = blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", ({ target: { result: buffer } }) => {
const view = new DataView(buffer);
let offset = 0;
if (view.getUint16(offset) !== 0xffd8)
return reject("not a valid jpeg");
offset += 2;
while (true) {
const marker = view.getUint16(offset);
if (marker === SOS) break;
const size = view.getUint16(offset + 2);
if (marker === APP1 && view.getUint32(offset + 4) === EXIF)
return resolve(blob.slice(offset, offset + 2 + size));
offset += 2 + size;
}
return resolve(new Blob());
});
reader.readAsArrayBuffer(blob);
});
// Copyright (c) 2022 Weihang Jian <tonytonyjan@gmail.com>
export default async (srcBlob, destBlob) => {
const exif = await getApp1Segment(srcBlob);
return new Blob([destBlob.slice(0, 2), exif, destBlob.slice(2)], {
type: "image/jpeg",
});
};
const SOI = 0xffd8,
SOS = 0xffda,
APP1 = 0xffe1,
EXIF = 0x45786966,
LITTLE_ENDIAN = 0x4949,
BIG_ENDIAN = 0x4d4d,
TAG_ID_ORIENTATION = 0x0112,
TAG_TYPE_SHORT = 3,
getApp1Segment = (blob) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", ({ target: { result: buffer } }) => {
const view = new DataView(buffer);
let offset = 0;
if (view.getUint16(offset) !== SOI) return reject("not a valid JPEG");
offset += 2;
while (true) {
const marker = view.getUint16(offset);
if (marker === SOS) break;
const size = view.getUint16(offset + 2);
if (marker === APP1 && view.getUint32(offset + 4) === EXIF) {
const tiffOffset = offset + 10;
let littleEndian;
switch (view.getUint16(tiffOffset)) {
case LITTLE_ENDIAN:
littleEndian = true;
break;
case BIG_ENDIAN:
littleEndian = false;
break;
default:
return reject("TIFF header contains invalid endian");
}
if (view.getUint16(tiffOffset + 2, littleEndian) !== 0x2a)
return reject("TIFF header contains invalid version");
const ifd0Offset = view.getUint32(tiffOffset + 4, littleEndian);
let endOfTagsOffset =
tiffOffset +
ifd0Offset +
2 +
view.getUint16(tiffOffset + ifd0Offset, littleEndian) * 12;
for (
let i = tiffOffset + ifd0Offset + 2;
i < endOfTagsOffset;
i += 12
) {
const tagId = view.getUint16(i, littleEndian);
if (tagId == TAG_ID_ORIENTATION) {
if (view.getUint16(i + 2, littleEndian) !== TAG_TYPE_SHORT)
return reject("Orientation data type is invalid");
if (view.getUint32(i + 4, littleEndian) !== 1)
return reject("Orientation data count is invalid");
view.setUint16(i + 8, 1, littleEndian);
break;
}
}
return resolve(buffer.slice(offset, offset + 2 + size));
}
offset += 2 + size;
}
return resolve(new Blob());
});
reader.readAsArrayBuffer(blob);
});
@basuneko
Copy link

Many thanks for this gist.
I think there may be a bug on line 59 of copyExifWithoutOrientation: tagId doesn't respect endianness. It should be const tagId = view.getUint16(i, littleEndian);

@UliPlabst
Copy link

@basuneko is right, there is a bug

@tonytonyjan
Copy link
Author

@basuneko thanks for finding this bug! I have updated the gist and it should fix it.

Many thanks! 🍻

@winsonloh
Copy link

Many huge thanks for this!
This more useful than those library

@zhiweio
Copy link

zhiweio commented Nov 28, 2024

Cool, it works!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment