diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index e1be9b89f8..1186ffcdfc 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -235,6 +235,7 @@ macro (oiio_add_all_tests) # Tests that require oiio-images: oiio_add_tests (gpsread + jpeg-thumbnail oiiotool-attribs texture-filtersize texture-filtersize-stochastic diff --git a/src/jpeg.imageio/jpeg_pvt.h b/src/jpeg.imageio/jpeg_pvt.h index faedb3c257..6f452346e8 100644 --- a/src/jpeg.imageio/jpeg_pvt.h +++ b/src/jpeg.imageio/jpeg_pvt.h @@ -64,7 +64,8 @@ class JpgInput final : public ImageInput { const char* format_name(void) const override { return "jpeg"; } int supports(string_view feature) const override { - return (feature == "exif" || feature == "iptc" || feature == "ioproxy"); + return (feature == "exif" || feature == "iptc" || feature == "ioproxy" + || feature == "thumbnail"); } bool valid_file(Filesystem::IOProxy* ioproxy) const override; @@ -77,6 +78,7 @@ class JpgInput final : public ImageInput { int z, void* data) override; bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend, span data) override; + bool get_thumbnail(ImageBuf& thumb, int subimage) override; bool close() override; const std::string& filename() const { return m_filename; } @@ -108,6 +110,10 @@ class JpgInput final : public ImageInput { uhdr_codec_private_t* m_uhdr_dec; #endif + // This will point to an embedded JPEG thumbnail if exists. + // This span is only valid until jpeg_destroy_decompress(&m_cinfo) is called. + cspan m_thumbnail_data; + void init() { m_raw = false; @@ -122,6 +128,7 @@ class JpgInput final : public ImageInput { #if defined(USE_UHDR) m_uhdr_dec = NULL; #endif + m_thumbnail_data = cspan(); } // Rummage through the JPEG "APP1" marker pointed to by buf, decoding @@ -139,6 +146,8 @@ class JpgInput final : public ImageInput { void close_file() { init(); } + void scan_for_thumbnail(cspan exif); + friend class JpgOutput; }; diff --git a/src/jpeg.imageio/jpeginput.cpp b/src/jpeg.imageio/jpeginput.cpp index cacd06d337..53400f4bad 100644 --- a/src/jpeg.imageio/jpeginput.cpp +++ b/src/jpeg.imageio/jpeginput.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -296,13 +297,14 @@ JpgInput::open(const std::string& name, ImageSpec& newspec) if (is_exif_marker(m)) { // The block starts with "Exif\0\0", so skip 6 bytes to get // to the start of the actual Exif data TIFF directory - bool ok = decode_exif(string_view((char*)m->data + 6, - m->data_length - 6), - m_spec); + auto exif = cspan((uint8_t*)m->data + 6, + m->data_length - 6); + bool ok = decode_exif(exif, m_spec); if (!ok && OIIO::get_int_attribute("imageinput:strict")) { errorfmt("Could not decode Exif"); return false; } + scan_for_thumbnail(exif); } else if (m->marker == (JPEG_APP0 + 1) && m->data_length >= 28 && !strncmp((const char*)m->data, "http://ns.adobe.com/xap/1.0/", 28)) { //NOSONAR @@ -791,4 +793,133 @@ JpgInput::jpeg_decode_iptc(string_view buf) return decode_iptc_iim(buf, m_spec); } +void +JpgInput::scan_for_thumbnail(cspan exif) +{ + bool host_little = littleendian(); + bool file_little = (exif[0] == 0x49 && exif[1] == 0x49); + bool swab = (host_little != file_little); + + const uint8_t* soi_ptr = nullptr; + const uint8_t* eoi_ptr = nullptr; + + size_t i = 0; + while (i < exif.size() - 1) { + if (exif[i] == 0xFF && exif[i + 1] == 0xD8) { + soi_ptr = &exif[i]; + break; + } + i++; + } + + if (!soi_ptr) + return; + + // extract image properties along the way + uint16_t width = 0; + uint16_t height = 0; + int numchan = 0; + + while (i < exif.size() - 1) { + if (exif[i++] == 0xFF) { + const auto marker = exif[i]; + if (marker == 0xD9) { + eoi_ptr = &exif[i] + 1; // one past (exclusive) + break; + } else if (marker == 0xC0 || marker == 0xC2) { + uint16_t length = (exif.at(i + 2) << 8) + exif.at(i + 1); + height = (exif.at(i + 5) << 8) + exif.at(i + 4); + width = (exif.at(i + 7) << 8) + exif.at(i + 6); + numchan = exif[i + 8]; + + if (swab) { + swap_endian(&length); + swap_endian(&height); + swap_endian(&width); + } + + length -= 2; + + if (i + length < exif.size() - 1) { + i += length; + } + } else if ((marker >= 0xC0 && marker < 0xD0) + || (marker > 0xD9 && marker <= 0xFE)) { + // Skip ahead for markers that have an associated length + uint16_t length = (exif.at(i + 2) << 8) + exif.at(i + 1); + + if (swab) { + swap_endian(&length); + } + + length -= 2; + + if (i + length < exif.size() - 1) { + i += length; + } + } + } + } + + if (eoi_ptr) { + m_thumbnail_data = cspan(soi_ptr, eoi_ptr); + m_spec.attribute("thumbnail_width", width); + m_spec.attribute("thumbnail_height", height); + m_spec.attribute("thumbnail_nchannels", numchan); + } +} + +bool +JpgInput::get_thumbnail(ImageBuf& thumb, int subimage) +{ + if (!m_decomp_create) { + errorfmt("ImageInput hasn't been initialized properly"); + return false; + } + + if (m_thumbnail_data.empty()) { + errorfmt("No thumbnail found"); + return false; + } + + auto image_input = OIIO::ImageInput::create("jpeg", false); + if (image_input == nullptr) { + errorfmt("OIIO::ImageInput::create(\"jpeg\") error"); + return false; + } + + Filesystem::IOMemReader proxy(m_thumbnail_data.data(), + m_thumbnail_data.size()); + bool result = image_input->valid_file(&proxy); + if (!result) { + errorfmt("the thumbnail is not a valid image of type \"jpeg\""); + return false; + } + + ImageSpec temp_spec, image_spec; + Filesystem::IOProxy* pp = &proxy; + temp_spec.attribute("oiio:ioproxy", TypeDesc::PTR, &pp); + + result = image_input->open("", image_spec, temp_spec); + if (!result) { + errorfmt("{}", image_input->geterror()); + return false; + } + + thumb.reset(image_spec); + + result = image_input->read_image(0, 0, 0, image_spec.nchannels, + image_spec.format, thumb.localpixels()); + + if (!result) { + errorfmt("{}", image_input->geterror()); + image_input->close(); + return false; + } + + image_input->close(); + return true; +} + + OIIO_PLUGIN_NAMESPACE_END diff --git a/testsuite/gpsread/ref/out.txt b/testsuite/gpsread/ref/out.txt index 3e36ee7c64..d928106e43 100644 --- a/testsuite/gpsread/ref/out.txt +++ b/testsuite/gpsread/ref/out.txt @@ -1,12 +1,15 @@ Reading ../oiio-images/tahoe-gps.jpg ../oiio-images/tahoe-gps.jpg : 2048 x 1536, 3 channel, uint8 jpeg - SHA-1: C3B420EF8C20C4771F6F7BF89EA9D9D4002BA016 + SHA-1: 71EBEC73B8E8B3533780B15147E61D895C80E8B1 channel list: R, G, B Make: "HTC" Model: "T-Mobile G1" Orientation: 1 (normal) ResolutionUnit: "none" Software: "title;va" + thumbnail_height: 240 + thumbnail_nchannels: 3 + thumbnail_width: 320 XResolution: 72 YResolution: 72 Exif:ColorSpace: 1 diff --git a/testsuite/jpeg-corrupt/ref/out-alt2.txt b/testsuite/jpeg-corrupt/ref/out-alt2.txt index 4bb15103d5..52553d6424 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt2.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt2.txt @@ -4,6 +4,9 @@ src/corrupt-exif.jpg : 256 x 256, 3 channel, uint8 jpeg channel list: R, G, B Orientation: 1 (normal) ResolutionUnit: "in" + thumbnail_height: 160 + thumbnail_nchannels: 3 + thumbnail_width: 160 XResolution: 300 YResolution: 300 jpeg:subsampling: "4:2:0" diff --git a/testsuite/jpeg-corrupt/ref/out-alt3.txt b/testsuite/jpeg-corrupt/ref/out-alt3.txt index 459988f3cf..5a2a1b4819 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt3.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt3.txt @@ -4,6 +4,9 @@ src/corrupt-exif.jpg : 256 x 256, 3 channel, uint8 jpeg channel list: R, G, B Orientation: 1 (normal) ResolutionUnit: "in" + thumbnail_height: 160 + thumbnail_nchannels: 3 + thumbnail_width: 160 XResolution: 300 YResolution: 300 jpeg:subsampling: "4:2:0" diff --git a/testsuite/jpeg-corrupt/ref/out-alt4.txt b/testsuite/jpeg-corrupt/ref/out-alt4.txt index 22a7d09d32..932b64769a 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt4.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt4.txt @@ -4,6 +4,9 @@ src/corrupt-exif.jpg : 256 x 256, 3 channel, uint8 jpeg channel list: R, G, B Orientation: 1 (normal) ResolutionUnit: "in" + thumbnail_height: 160 + thumbnail_nchannels: 3 + thumbnail_width: 160 XResolution: 300 YResolution: 300 jpeg:subsampling: "4:2:0" diff --git a/testsuite/jpeg-corrupt/ref/out-alt5.txt b/testsuite/jpeg-corrupt/ref/out-alt5.txt index d8aa9d2c5f..8f2410da26 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt5.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt5.txt @@ -4,6 +4,9 @@ src/corrupt-exif.jpg : 256 x 256, 3 channel, uint8 jpeg channel list: R, G, B Orientation: 1 (normal) ResolutionUnit: "in" + thumbnail_height: 160 + thumbnail_nchannels: 3 + thumbnail_width: 160 XResolution: 300 YResolution: 300 jpeg:subsampling: "4:2:0" diff --git a/testsuite/jpeg-corrupt/ref/out.txt b/testsuite/jpeg-corrupt/ref/out.txt index 86e3266344..1c5d34f2e6 100644 --- a/testsuite/jpeg-corrupt/ref/out.txt +++ b/testsuite/jpeg-corrupt/ref/out.txt @@ -4,6 +4,9 @@ src/corrupt-exif.jpg : 256 x 256, 3 channel, uint8 jpeg channel list: R, G, B Orientation: 1 (normal) ResolutionUnit: "in" + thumbnail_height: 160 + thumbnail_nchannels: 3 + thumbnail_width: 160 XResolution: 300 YResolution: 300 jpeg:subsampling: "4:2:0" diff --git a/testsuite/jpeg-thumbnail/ref/out.txt b/testsuite/jpeg-thumbnail/ref/out.txt new file mode 100644 index 0000000000..dbba2d2969 --- /dev/null +++ b/testsuite/jpeg-thumbnail/ref/out.txt @@ -0,0 +1,2 @@ + 320 x 240, 3 channel, uint8 + channel list: R, G, B diff --git a/testsuite/jpeg-thumbnail/run.py b/testsuite/jpeg-thumbnail/run.py new file mode 100755 index 0000000000..3b335b895f --- /dev/null +++ b/testsuite/jpeg-thumbnail/run.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + + +import os +OIIO_TESTSUITE_IMAGEDIR = os.getenv('OIIO_TESTSUITE_IMAGEDIR') + +redirect = " >> out.txt 2>&1 " + +command += oiiotool ( OIIO_TESTSUITE_IMAGEDIR + '/tahoe-gps.jpg --thumbnail-get --eraseattrib ".*" --printinfo' ) +outputs = [ "out.txt" ] diff --git a/testsuite/python-imageinput/ref/out-python3-win.txt b/testsuite/python-imageinput/ref/out-python3-win.txt index c0b3429a5e..691369dd18 100644 --- a/testsuite/python-imageinput/ref/out-python3-win.txt +++ b/testsuite/python-imageinput/ref/out-python3-win.txt @@ -36,6 +36,9 @@ Opened "D:/a/OpenImageIO/OpenImageIO/build/testsuite/oiio-images/tahoe-gps.jpg" GPS:TimeStamp = (17.0, 56.0, 33.0) GPS:MapDatum = "WGS-84" GPS:DateStamp = "1915:08:08" + thumbnail_width = 320 + thumbnail_height = 240 + thumbnail_nchannels = 3 Opened "grid.tx" as a tiff resolution 1024x1024+0+0 @@ -230,7 +233,7 @@ Testing read_native_deep_image: Testing get_thumbnail: get_thumbnail returns ImageBuf: True - thumbnail initialized: False + thumbnail initialized: True Testing has_error/geterror: scanline on tiled None: True diff --git a/testsuite/python-imageinput/ref/out.txt b/testsuite/python-imageinput/ref/out.txt index 886a5e62f0..86f75969a6 100644 --- a/testsuite/python-imageinput/ref/out.txt +++ b/testsuite/python-imageinput/ref/out.txt @@ -36,6 +36,9 @@ Opened "../oiio-images/tahoe-gps.jpg" as a jpeg GPS:TimeStamp = (17.0, 56.0, 33.0) GPS:MapDatum = "WGS-84" GPS:DateStamp = "1915:08:08" + thumbnail_width = 320 + thumbnail_height = 240 + thumbnail_nchannels = 3 Opened "grid.tx" as a tiff resolution 1024x1024+0+0 @@ -230,7 +233,7 @@ Testing read_native_deep_image: Testing get_thumbnail: get_thumbnail returns ImageBuf: True - thumbnail initialized: False + thumbnail initialized: True Testing has_error/geterror: scanline on tiled None: True