Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ macro (oiio_add_all_tests)
# These Python tests also need access to oiio-images
oiio_add_tests (
python-imageinput python-imagebufalgo
python-thumbnail-jpeg
IMAGEDIR oiio-images
ENVIRONMENT "${_pybind_tests_pythonpath}"
)
Expand Down
11 changes: 10 additions & 1 deletion src/jpeg.imageio/jpeg_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<std::byte> data) override;
bool get_thumbnail(ImageBuf& thumb, int subimage) override;
bool close() override;

const std::string& filename() const { return m_filename; }
Expand Down Expand Up @@ -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<uint8_t> m_thumbnail_data;

void init()
{
m_raw = false;
Expand All @@ -122,6 +128,7 @@ class JpgInput final : public ImageInput {
#if defined(USE_UHDR)
m_uhdr_dec = NULL;
#endif
m_thumbnail_data = cspan<uint8_t>();
}

// Rummage through the JPEG "APP1" marker pointed to by buf, decoding
Expand All @@ -139,6 +146,8 @@ class JpgInput final : public ImageInput {

void close_file() { init(); }

void scan_for_thumbnail(cspan<uint8_t> exif);

friend class JpgOutput;
};

Expand Down
145 changes: 142 additions & 3 deletions src/jpeg.imageio/jpeginput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/fmath.h>
#include <OpenImageIO/imagebuf.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/strutil.h>
#include <OpenImageIO/tiffutils.h>
Expand Down Expand Up @@ -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>((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
Expand Down Expand Up @@ -791,4 +793,141 @@ JpgInput::jpeg_decode_iptc(string_view buf)
return decode_iptc_iim(buf, m_spec);
}

void
JpgInput::scan_for_thumbnail(cspan<uint8_t> exif)
{
try {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code performs a basic scan for the subimage tag in the exif, it doesn't check if the found bit pattern is not a part of any other exif data. We may want to move this functionality to exif.cpp, but that may affect any other readers relying on 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<uint8_t>(soi_ptr, eoi_ptr);
m_spec.attribute("thumbnail_width", width);
m_spec.attribute("thumbnail_height", height);
m_spec.attribute("thumbnail_nchannels", numchan);
}

} catch (const std::exception& e) {
errorfmt("ERROR scanning for thumbnail: {}", e.what());
}
Comment on lines +872 to +874

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the try/catch here? Is there anything called inside that will actually throw an exception?

}

bool
JpgInput::get_thumbnail(ImageBuf& thumb, int subimage)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_thumbnail() methods in the TGA input and ImageBuf contain lock guards. Does this method need to be thread safe?

{
if (!m_decomp_create) {
errorfmt("ImageInput hasn't been initialised properly");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I truly hate to pick on this, but in this code base we try to uniformly use the American spelling conventions: "initialized"

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) {
std::string errmsg = OIIO::geterror();
errorfmt("{}", errmsg.size() ? errmsg.c_str() : "unknown error");
return false;
}
Comment thread
antond-weta marked this conversation as resolved.

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(
"failed to initialise an ImageInput object with the thumbnail data");
return false;
}
Comment on lines +910 to +914

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if get_thumbnail, it's sufficient to just return false. But if you really want to return an error message, what you should be returning is image_input->geterror().


thumb.reset(image_spec);

result = image_input->read_image(0, 0, 0, image_spec.nchannels,
image_spec.format, thumb.localpixels());

if (!result) {
errorfmt(
"failed to initialise an ImageInput object of type \"jpeg\" with the thumbnail data");
image_input->close();
return false;
}
Comment on lines +921 to +926

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, too. Either just return false with no error, or do

errorfmt("{}", image_input->geterror());


image_input->close();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this I was getting address sanitiser errors in the image_input's destructor. I couldn't understand the error, and don't really think that it is related to the change.

return true;
}


OIIO_PLUGIN_NAMESPACE_END
5 changes: 4 additions & 1 deletion testsuite/gpsread/ref/out.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions testsuite/jpeg-corrupt/ref/out-alt2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions testsuite/jpeg-corrupt/ref/out-alt3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions testsuite/jpeg-corrupt/ref/out-alt4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions testsuite/jpeg-corrupt/ref/out-alt5.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions testsuite/jpeg-corrupt/ref/out.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion testsuite/python-imageinput/ref/out-python3-win.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion testsuite/python-imageinput/ref/out.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions testsuite/python-thumbnail-jpeg/ref/out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Done.
10 changes: 10 additions & 0 deletions testsuite/python-thumbnail-jpeg/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python

# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO


import os

command += pythonbin + " src/test_jpeg_thumbnail.py >> out.txt 2>&1"
28 changes: 28 additions & 0 deletions testsuite/python-thumbnail-jpeg/src/test_jpeg_thumbnail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env python

# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

from __future__ import annotations

import OpenImageIO as oiio

import os

OIIO_TESTSUITE_IMAGEDIR = os.getenv('OIIO_TESTSUITE_IMAGEDIR',
'../oiio-images')

######################################################################
# main test starts here

try:
input = oiio.ImageInput.open (OIIO_TESTSUITE_IMAGEDIR + "/tahoe-gps.jpg")
thumbnail = input.get_thumbnail()
assert thumbnail is not None
assert thumbnail.spec().width == 320
assert thumbnail.spec().height == 240
assert thumbnail.spec().nchannels == 3
print ("Done.")
except Exception as detail:
print ("Unknown exception:", detail)
Loading