From 1a831d7bccdc9ace61fcd6178859cd29f15b9b30 Mon Sep 17 00:00:00 2001 From: Geoffrey Prewett Date: Fri, 13 Oct 2023 17:47:28 -0400 Subject: [PATCH 1/3] Handle Exif orientation for JPEG images --- src/Image.cc | 344 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/Image.h | 22 +++- 2 files changed, 361 insertions(+), 5 deletions(-) diff --git a/src/Image.cc b/src/Image.cc index 35ee7947a..f00894a87 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -825,6 +825,48 @@ static void jpeg_mem_src (j_decompress_ptr cinfo, void* buffer, long nbytes) { #endif +class BufferReader : public Image::Reader { +public: + BufferReader(uint8_t* buf, unsigned len) : _buf(buf), _len(len), _idx(0) {} + + bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } + + uint8_t getNext() override { return _buf[_idx++]; } + + void skipBytes(unsigned n) override { _idx += n; } + +private: + uint8_t* _buf; // we do not own this + unsigned _len; + unsigned _idx; +}; + +class StreamReader : public Image::Reader { +public: + StreamReader(FILE *stream) : _stream(stream), _len(0), _idx(0) { + fseeko(_stream, 0, SEEK_END); + _len = ftello(_stream); + fseeko(_stream, 0, SEEK_SET); + } + + bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } + + uint8_t getNext() override { + ++_idx; + return getc(_stream); + } + + void skipBytes(unsigned n) override { + _idx += n; + fseeko(_stream, _idx, SEEK_SET); + } + +private: + FILE* _stream; + off_t _len; + off_t _idx; +}; + void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode) { int stride = naturalWidth * 4; for (int y = 0; y < naturalHeight; ++y) { @@ -843,10 +885,11 @@ void Image::jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src */ cairo_status_t -Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { +Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args, Orientation orientation) { + const int channels = 4; cairo_status_t status = CAIRO_STATUS_SUCCESS; - uint8_t *data = new uint8_t[naturalWidth * naturalHeight * 4]; + uint8_t *data = new uint8_t[naturalWidth * naturalHeight * channels]; if (!data) { jpeg_abort_decompress(args); jpeg_destroy_decompress(args); @@ -893,6 +936,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { break; } + updateDimensionsForOrientation(orientation); + if (!status) { _surface = cairo_image_surface_create_for_data( data @@ -906,6 +951,8 @@ Image::decodeJPEGIntoSurface(jpeg_decompress_struct *args) { jpeg_destroy_decompress(args); status = cairo_surface_status(_surface); + rotatePixels(data, naturalWidth, naturalHeight, channels, orientation); + delete[] src; if (status) { @@ -981,6 +1028,10 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { return CAIRO_STATUS_NO_MEMORY; } + BufferReader reader(buf, len); + Orientation orientation = getExifOrientation(reader); + updateDimensionsForOrientation(orientation); + // New image surface _surface = cairo_image_surface_create_for_data( data @@ -999,6 +1050,8 @@ Image::decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len) { return status; } + rotatePixels(data, naturalWidth, naturalHeight, 1, orientation); + _data = data; return assignDataAsMime(buf, len, CAIRO_MIME_TYPE_JPEG); @@ -1058,6 +1111,9 @@ Image::assignDataAsMime(uint8_t *data, int len, const char *mime_type) { cairo_status_t Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { + BufferReader reader(buf, len); + Orientation orientation = getExifOrientation(reader); + // TODO: remove this duplicate logic // JPEG setup struct jpeg_decompress_struct args; @@ -1085,7 +1141,7 @@ Image::loadJPEGFromBuffer(uint8_t *buf, unsigned len) { width = naturalWidth = args.output_width; height = naturalHeight = args.output_height; - return decodeJPEGIntoSurface(&args); + return decodeJPEGIntoSurface(&args, orientation); } /* @@ -1101,6 +1157,13 @@ Image::loadJPEG(FILE *stream) { #else if (data_mode == DATA_IMAGE) { // Can lazily read in the JPEG. #endif + Orientation orientation = NORMAL; + { + StreamReader reader(stream); + orientation = getExifOrientation(reader); + rewind(stream); + } + // JPEG setup struct jpeg_decompress_struct args; struct canvas_jpeg_error_mgr err; @@ -1133,7 +1196,7 @@ Image::loadJPEG(FILE *stream) { width = naturalWidth = args.output_width; height = naturalHeight = args.output_height; - status = decodeJPEGIntoSurface(&args); + status = decodeJPEGIntoSurface(&args, orientation); fclose(stream); } else { // We'll need the actual source jpeg data, so read fully. uint8_t *buf; @@ -1173,6 +1236,279 @@ Image::loadJPEG(FILE *stream) { return status; } +/* + * Returns the Exif orientation if one exists, otherwise returns NORMAL + */ + +Image::Orientation +Image::getExifOrientation(Reader& jpeg) { + static const char kJpegStartOfImage = (char)0xd8; + static const char kJpegStartOfFrameBaseline = (char)0xc0; + static const char kJpegStartOfFrameProgressive = (char)0xc2; + static const char kJpegHuffmanTable = (char)0xc4; + static const char kJpegQuantizationTable = (char)0xdb; + static const char kJpegRestartInterval = (char)0xdd; + static const char kJpegComment = (char)0xfe; + static const char kJpegStartOfScan = (char)0xda; + static const char kJpegApp0 = (char)0xe0; + static const char kJpegApp1 = (char)0xe1; + + // Find the Exif tag (if it exists) + int exif_len = 0; + bool done = false; + while (!done && jpeg.hasBytes(1)) { + while (jpeg.hasBytes(1) && jpeg.getNext() != 0xff) { + // noop + } + if (jpeg.hasBytes(1)) { + char tag = jpeg.getNext(); + switch (tag) { + case kJpegStartOfImage: + break; // beginning of file, no extra bytes + case kJpegRestartInterval: + jpeg.skipBytes(4); + break; + case kJpegStartOfFrameBaseline: + case kJpegStartOfFrameProgressive: + case kJpegHuffmanTable: + case kJpegQuantizationTable: + case kJpegComment: + case kJpegApp0: + case kJpegApp1: { + if (jpeg.hasBytes(2)) { + uint16_t tag_len = 0; + tag_len |= jpeg.getNext() << 8; + tag_len |= jpeg.getNext(); + // The tag length includes the two bytes for the length + uint16_t tag_content_len = std::max(0, tag_len - 2); + if (tag != kJpegApp1 || !jpeg.hasBytes(tag_content_len)) { + jpeg.skipBytes(tag_content_len); // skip JPEG tags we ignore. + } else if (!jpeg.hasBytes(6)) { + jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0" + } else { + if (jpeg.getNext() == 'E' && jpeg.getNext() == 'x' && + jpeg.getNext() == 'i' && jpeg.getNext() == 'f' && + jpeg.getNext() == '\0' && jpeg.getNext() == '\0') { + exif_len = tag_content_len - 6; + done = true; + } else { + jpeg.skipBytes(tag_content_len); // too short to have "Exif\0\0" + } + } + } else { + done = true; // shouldn't happen: corrupt file or we have a bug + } + break; + } + case kJpegStartOfScan: + default: + done = true; // got to the image, apparently no exif tags here + break; + } + } + } + + // Parse exif if it exists. If it does, we have already checked that jpeglen + // is longer than exifStart + exifLen, so we can safely index the data + if (exif_len > 0) { + // The first two bytes of TIFF header are "II" if little-endian ("Intel") + // and "MM" if big-endian ("Motorola") + const bool isLE = (jpeg.getNext() == 'I'); + jpeg.skipBytes(3); // +1 for the other I/M, +2 for 0x002a + + auto readUint16Little = [](Reader &jpeg) -> uint32_t { + uint16_t val = uint16_t(jpeg.getNext()); + val |= uint16_t(jpeg.getNext()) << 8; + return val; + }; + auto readUint32Little = [](Reader &jpeg) -> uint32_t { + uint32_t val = uint32_t(jpeg.getNext()); + val |= uint32_t(jpeg.getNext()) << 8; + val |= uint32_t(jpeg.getNext()) << 16; + val |= uint32_t(jpeg.getNext()) << 24; + return val; + }; + auto readUint16Big = [](Reader &jpeg) -> uint32_t { + uint16_t val = uint16_t(jpeg.getNext()) << 8; + val |= uint16_t(jpeg.getNext()); + return val; + }; + auto readUint32Big = [](Reader &jpeg) -> uint32_t { + uint32_t val = uint32_t(jpeg.getNext()) << 24; + val |= uint32_t(jpeg.getNext()) << 16; + val |= uint32_t(jpeg.getNext()) << 8; + val |= uint32_t(jpeg.getNext()); + return val; + }; + // The first two bytes of TIFF header are "II" if little-endian ("Intel") + // and "MM" if big-endian ("Motorola") + auto readUint32 = (isLE ? readUint32Little : readUint32Big); + auto readUint16 = (isLE ? readUint16Little : readUint16Big); + // offset to the IFD0 (offset from beginning of TIFF header, II/MM, + // which is 8 bytes before where we are after reading the uint32) + jpeg.skipBytes(readUint32(jpeg) - 8); + + // Read the IFD0 ("Image File Directory 0") + // | NN | n entries in directory (2 bytes) + // | TT | tt | nnnn | vvvv | entry: tag (2b), data type (2b), + // n components (4b), value/offset (4b) + if (jpeg.hasBytes(2)) { + uint16_t nEntries = readUint16(jpeg); + for (uint16_t i = 0; i < nEntries && jpeg.hasBytes(2); ++i) { + uint16_t tag = readUint16(jpeg); + // The entry is 12 bytes. We already read the 2 bytes for the tag. + jpeg.skipBytes(6); // skip 2 for the data type, skip 4 n components. + if (tag == 0x112) { + switch (readUint16(jpeg)) { // orientation tag is always one uint16 + case 1: return NORMAL; + case 2: return MIRROR_HORIZ; + case 3: return ROTATE_180; + case 4: return MIRROR_VERT; + case 5: return MIRROR_HORIZ_AND_ROTATE_270_CW; + case 6: return ROTATE_90_CW; + case 7: return MIRROR_HORIZ_AND_ROTATE_90_CW; + case 8: return ROTATE_270_CW; + default: return NORMAL; + } + } else { + jpeg.skipBytes(4); // skip the four bytes for the value + } + } + } + } + + return NORMAL; +} + +/* + * Updates the dimensions of the bitmap according to the orientation + */ + +void Image::updateDimensionsForOrientation(Orientation orientation) { + switch (orientation) { + case ROTATE_90_CW: + case ROTATE_270_CW: + case MIRROR_HORIZ_AND_ROTATE_90_CW: + case MIRROR_HORIZ_AND_ROTATE_270_CW: { + int tmp = naturalWidth; + naturalWidth = naturalHeight; + naturalHeight = tmp; + tmp = width; + width = height; + height = tmp; + break; + } + case NORMAL: + case MIRROR_HORIZ: + case MIRROR_VERT: + case ROTATE_180: + default: { + break; + } + } +} + +/* + * Rotates the pixels to the correct orientation. + */ + +void +Image::rotatePixels(uint8_t* pixels, int width, int height, int channels, + Orientation orientation) { + auto swapPixel = [channels](uint8_t* pixels, int src_idx, int dst_idx) { + uint8_t tmp; + for (int i = 0; i < channels; ++i) { + tmp = pixels[src_idx + i]; + pixels[src_idx + i] = pixels[dst_idx + i]; + pixels[dst_idx + i] = tmp; + } + }; + + auto mirrorHoriz = [swapPixel](uint8_t* pixels, int width, int height, int channels) { + int midX = width / 2; // ok to truncate if odd, since we don't swap a center pixel + for (int y = 0; y < height; ++y) { + for (int x = 0; x < midX; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = (y * width + width - 1 - x) * channels; + swapPixel(pixels, orig_idx, new_idx); + } + } + }; + + auto mirrorVert = [swapPixel](uint8_t* pixels, int width, int height, int channels) { + int midY = height / 2; // ok to truncate if odd, since we don't swap a center pixel + for (int y = 0; y < midY; ++y) { + for (int x = 0; x < width; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = ((height - y - 1) * width + x) * channels; + swapPixel(pixels, orig_idx, new_idx); + } + } + }; + + auto rotate90 = [](uint8_t* pixels, int width, int height, int channels) { + const int n_bytes = width * height * channels; + uint8_t *unrotated = new uint8_t[n_bytes]; + if (!unrotated) { + return; + } + std::memcpy(unrotated, pixels, n_bytes); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width ; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = (x * height + height - 1 - y) * channels; + std::memcpy(pixels + new_idx, unrotated + orig_idx, channels); + } + } + }; + + auto rotate270 = [](uint8_t* pixels, int width, int height, int channels) { + const int n_bytes = width * height * channels; + uint8_t *unrotated = new uint8_t[n_bytes]; + if (!unrotated) { + return; + } + std::memcpy(unrotated, pixels, n_bytes); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width ; ++x) { + int orig_idx = (y * width + x) * channels; + int new_idx = ((width - 1 - x) * height + y) * channels; + std::memcpy(pixels + new_idx, unrotated + orig_idx, channels); + } + } + }; + + switch (orientation) { + case MIRROR_HORIZ: + mirrorHoriz(pixels, width, height, channels); + break; + case MIRROR_VERT: + mirrorVert(pixels, width, height, channels); + break; + case ROTATE_180: + mirrorHoriz(pixels, width, height, channels); + mirrorVert(pixels, width, height, channels); + break; + case ROTATE_90_CW: + rotate90(pixels, height, width, channels); // swap w/h because we need orig w/h + break; + case ROTATE_270_CW: + rotate270(pixels, height, width, channels); // swap w/h because we need orig w/h + break; + case MIRROR_HORIZ_AND_ROTATE_90_CW: + mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h + rotate90(pixels, height, width, channels); + break; + case MIRROR_HORIZ_AND_ROTATE_270_CW: + mirrorHoriz(pixels, height, width, channels); // swap w/h because we need orig w/h + rotate270(pixels, height, width, channels); + break; + case NORMAL: + default: + break; + } +} + #endif /* HAVE_JPEG */ #ifdef HAVE_RSVG diff --git a/src/Image.h b/src/Image.h index 62bc3f13b..495fb702a 100644 --- a/src/Image.h +++ b/src/Image.h @@ -78,12 +78,32 @@ class Image: public Nan::ObjectWrap { cairo_status_t loadGIF(FILE *stream); #endif #ifdef HAVE_JPEG + enum Orientation { + NORMAL, + MIRROR_HORIZ, + MIRROR_VERT, + ROTATE_180, + ROTATE_90_CW, + ROTATE_270_CW, + MIRROR_HORIZ_AND_ROTATE_90_CW, + MIRROR_HORIZ_AND_ROTATE_270_CW + }; cairo_status_t loadJPEGFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadJPEG(FILE *stream); void jpegToARGB(jpeg_decompress_struct* args, uint8_t* data, uint8_t* src, JPEGDecodeL decode); - cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info); + cairo_status_t decodeJPEGIntoSurface(jpeg_decompress_struct *info, Orientation orientation); cairo_status_t decodeJPEGBufferIntoMimeSurface(uint8_t *buf, unsigned len); cairo_status_t assignDataAsMime(uint8_t *data, int len, const char *mime_type); + + class Reader { + public: + virtual bool hasBytes(unsigned n) const = 0; + virtual uint8_t getNext() = 0; + virtual void skipBytes(unsigned n) = 0; + }; + Orientation getExifOrientation(Reader& jpeg); + void updateDimensionsForOrientation(Orientation orientation); + void rotatePixels(uint8_t* pixels, int width, int height, int channels, Orientation orientation); #endif cairo_status_t loadBMPFromBuffer(uint8_t *buf, unsigned len); cairo_status_t loadBMP(FILE *stream); From 54e2d53b678eadd638fc7ae1d39f1d9d3cc3bb20 Mon Sep 17 00:00:00 2001 From: Geoffrey Prewett Date: Thu, 19 Oct 2023 12:48:29 -0400 Subject: [PATCH 2/3] Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbdb8254..366669aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Fixed Exif orientation in JPEG files being ignored (#1670) 2.11.2 ================== From 1729874f506ff50d371e1a09133c2b6302824a99 Mon Sep 17 00:00:00 2001 From: Geoffrey Prewett Date: Fri, 11 Oct 2024 13:56:09 -0400 Subject: [PATCH 3/3] Changes for PR --- CHANGELOG.md | 2 +- src/Image.cc | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 781cb4f7f..240fa2605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed -* Fixed Exif orientation in JPEG files being ignored (#1670) 3.0.0 @@ -45,6 +44,7 @@ This release notably changes to using N-API. 🎉 * Allow alternate or properly escaped quotes *within* font-family names * Fix TextMetrics type to include alphabeticBaseline, emHeightAscent, and emHeightDescent properties * Fix class properties should have defaults as standard js classes (#2390) +* Fixed Exif orientation in JPEG files being ignored (#1670) 2.11.2 ================== diff --git a/src/Image.cc b/src/Image.cc index c02cec885..970cd2e28 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -772,7 +772,11 @@ class BufferReader : public Image::Reader { bool hasBytes(unsigned n) const override { return (_idx + n - 1 < _len); } - uint8_t getNext() override { return _buf[_idx++]; } + uint8_t getNext() override { + if (_idx < _len) { + return _buf[_idx++]; + } + } void skipBytes(unsigned n) override { _idx += n; }