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
3 changes: 3 additions & 0 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ macro (oiio_add_all_tests)
oiiotool-readerror
oiiotool-subimage
oiiotool-text
oiiotool-get-thumbnail
oiiotool-xform
diff
flip
Expand Down Expand Up @@ -449,6 +450,8 @@ macro (oiio_add_all_tests)
ENABLEVAR ENABLE_TARGA
IMAGEDIR oiio-images)
endif()
oiio_add_tests (oiiotool-set-thumbnail
ENABLEVAR ENABLE_TARGA)
if (WIN32)
if (OIIO_BUILD_TOOLS)
# Add test for long path handling if support is enabled at the system level.
Expand Down
56 changes: 56 additions & 0 deletions src/doc/oiiotool.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,18 @@ Reading images
to following the input with a `--ch` command, except that by integrating
into the `-i`, it potentially can avoid the I/O of the unneeded
channels.
`:get_thumbnail=` *int*
If nonzero, read the file's embedded thumbnail instead of its main
image (equivalent to a following `--get-thumbnail`). The `:fail=` and
`:index=` modifiers are forwarded (e.g. `-i:get_thumbnail=1:fail=0`),
and any auto-orientation or color conversion applies to the thumbnail.
Since a thumbnail is display-referred (typically sRGB), `:autocc=` will
linearize it like any other input.

Examples::

# Extract just the thumbnail from a large image
oiiotool -i:get_thumbnail=1 input.psd -o thumb.jpg

.. option:: --iconfig <name> <value>

Expand Down Expand Up @@ -2339,6 +2351,50 @@ current top image.
Additionally, this command can be used to remove one subimage (leaving
the others) by using the optional modifier `--subimage:delete=1`.

.. option:: --get-thumbnail

Replace the top image on the stack with its embedded thumbnail.
The thumbnail associated with the first subimage (subimage 0) is used.
Because this replaces the top image, use ``--dup`` beforehand if you also
want to keep the original.

Optional appended modifiers include:

`:fail=` *int* (default: 1)
If 1, it is an error if the image has no embedded thumbnail.
If 0, an empty (0x0) image is pushed in its place instead, so a batch
script can continue; guard any subsequent output (see the example
below), since writing the empty image is itself an error.

`:index=` *int* (default: 0)
Selects which embedded thumbnail to retrieve, for formats that can
store more than one (such as some camera raw formats). Currently only
the primary thumbnail (`index=0`, the default) is available; a nonzero
value is an error until multiple-thumbnail support is added (see issue
#4888).

Examples::

# Save the thumbnail
oiiotool input.psd --dup --get-thumbnail -o thumb.jpg

# Batch-safe: substitute an empty image for missing thumbnails, and
# guard the output so only real thumbnails are written
oiiotool input.psd --get-thumbnail:fail=0 --if "{TOP.width}" -o thumb.jpg --endif

.. option:: --set-thumbnail

Remove the top image from the stack and attach it as the thumbnail of the
image now on top (stored on the first subimage). The thumbnail may be
prepared beforehand with the usual image operations. It is written out only
if the output format supports embedded thumbnails, and may be resized or
otherwise adjusted to satisfy that format's restrictions.

Examples::

# Attach a 128x128 box-filtered copy of the image as its thumbnail
oiiotool input.exr --dup --resize:filter=box 128x128 --set-thumbnail -o out_with_thumb.tga

.. option:: --sisplit

Remove the top image from the stack, split it into its constituent
Expand Down
4 changes: 4 additions & 0 deletions src/libOpenImageIO/imagebuf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1979,6 +1979,10 @@ ImageBufImpl::set_thumbnail(const ImageBuf& thumb, DoLock do_lock)
clear_thumbnail(DoLock(false) /* we already hold the lock */);
if (thumb.initialized()) {
m_thumbnail.reset(new ImageBuf(thumb));
m_spec.attribute("thumbnail_width", thumb.spec().width);
m_spec.attribute("thumbnail_height", thumb.spec().height);
m_spec.attribute("thumbnail_nchannels", thumb.spec().nchannels);
m_has_thumbnail = true;
}
}

Expand Down
157 changes: 157 additions & 0 deletions src/libOpenImageIO/imagebuf_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,160 @@ time_get_pixels()



void
test_thumbnail()
{
std::cout << "\nTesting set_thumbnail, get_thumbnail, clear_thumbnail:\n";
ImageBuf A(ImageSpec(64, 48, 3, TypeUInt8));
ImageBufAlgo::zero(A);
OIIO_CHECK_ASSERT(!A.has_thumbnail());

// Non-square asymmetric vertical gradient. The top/bottom colors are
// deliberately not R/B mirror images, so a flip, an R/B swap, and both
// together each alter the image.
auto gradient = [](int w, int h, int nchans) {
ImageBuf buf(ImageSpec(w, h, nchans, TypeUInt8));
static const float top[4] = { 0.2f, 0.3f, 0.8f, 1.0f };
static const float bottom[4] = { 0.7f, 0.9f, 0.4f, 1.0f };
ImageBufAlgo::fill(buf, cspan<float>(top), cspan<float>(bottom));
return buf;
};
ImageBuf thumb = gradient(16, 12, 3);

A.set_thumbnail(thumb);
OIIO_CHECK_ASSERT(A.has_thumbnail());
auto t = A.get_thumbnail();
OIIO_CHECK_ASSERT(t && t->initialized());
OIIO_CHECK_EQUAL(t->spec().width, 16);
OIIO_CHECK_EQUAL(t->spec().height, 12);
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_width"), 16);
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_height"), 12);
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_nchannels"), 3);
OIIO_CHECK_EQUAL(ImageBufAlgo::compare(*t, thumb, 0.0f, 0.0f).nfail, 0);

// Test that `set_thumbnail` stores a deep copy. Mutating the source image
// afterward must not affect the stored thumbnail.
ImageBufAlgo::zero(thumb);
t = A.get_thumbnail();
OIIO_CHECK_EQUAL(
ImageBufAlgo::compare(*t, gradient(16, 12, 3), 0.0f, 0.0f).nfail, 0);

// Replace A's thumbnail with a new image.
A.set_thumbnail(gradient(8, 6, 3));
t = A.get_thumbnail();
OIIO_CHECK_EQUAL(t->spec().width, 8);
OIIO_CHECK_EQUAL(t->spec().height, 6);
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_width"), 8);
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_height"), 6);

// Test that setting an uninitialized thumbnail clears it.
A.set_thumbnail(ImageBuf());
OIIO_CHECK_ASSERT(!A.has_thumbnail());
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_width"), 0);

// Test that `clear_thumbnail` removes the thumbnail and its metadata.
A.set_thumbnail(gradient(16, 12, 3));
OIIO_CHECK_ASSERT(A.has_thumbnail());
A.clear_thumbnail();
OIIO_CHECK_ASSERT(!A.has_thumbnail());
OIIO_CHECK_EQUAL(A.spec().get_int_attribute("thumbnail_width"), 0);
}



void
test_thumbnail_tga()
{
std::cout << "\nTesting thumbnail round trip through a TGA file:\n";
ImageBuf A(ImageSpec(64, 48, 3, TypeUInt8));
ImageBufAlgo::zero(A);

// Non-square asymmetric vertical gradient. The top/bottom colors are
// deliberately not R/B mirror images, so a flip, an R/B swap, and both
// together each alter the image.
auto gradient = [](int w, int h, int nchans) {
ImageBuf buf(ImageSpec(w, h, nchans, TypeUInt8));
static const float top[4] = { 0.2f, 0.3f, 0.8f, 1.0f };
static const float bottom[4] = { 0.7f, 0.9f, 0.4f, 1.0f };
ImageBufAlgo::fill(buf, cspan<float>(top), cspan<float>(bottom));
return buf;
};

// Test that the thumbnail content survives a write/read round trip exactly.
A.set_thumbnail(gradient(16, 12, 3));
OIIO_CHECK_ASSERT(A.write("imagebuf_test_thumb1.tga"));
{
ImageBuf in("imagebuf_test_thumb1.tga");
OIIO_CHECK_ASSERT(in.has_thumbnail());
auto rt = in.get_thumbnail();
OIIO_CHECK_ASSERT(rt && rt->initialized());
OIIO_CHECK_EQUAL(rt->spec().width, 16);
OIIO_CHECK_EQUAL(rt->spec().height, 12);
OIIO_CHECK_EQUAL(rt->spec().nchannels, 3);
OIIO_CHECK_EQUAL(
ImageBufAlgo::compare(*rt, gradient(16, 12, 3), 0.0f, 0.0f).nfail,
0);
}

// Test an oversized thumbnail is resized to fit the
// format's 255 pixel dimension limit, preserving aspect ratio.
A.set_thumbnail(gradient(512, 384, 3));
OIIO_CHECK_ASSERT(A.write("imagebuf_test_thumb2.tga"));
{
ImageBuf in("imagebuf_test_thumb2.tga");
OIIO_CHECK_ASSERT(in.has_thumbnail());
auto rt = in.get_thumbnail();
OIIO_CHECK_ASSERT(rt && rt->initialized());
OIIO_CHECK_EQUAL(rt->spec().width, 255);
OIIO_CHECK_EQUAL(rt->spec().height, 191);
}

A.set_thumbnail(gradient(384, 512, 3));
OIIO_CHECK_ASSERT(A.write("imagebuf_test_thumb3.tga"));
{
ImageBuf in("imagebuf_test_thumb3.tga");
OIIO_CHECK_ASSERT(in.has_thumbnail());
auto rt = in.get_thumbnail();
OIIO_CHECK_ASSERT(rt && rt->initialized());
OIIO_CHECK_EQUAL(rt->spec().width, 191);
OIIO_CHECK_EQUAL(rt->spec().height, 255);
}

// Test a thumbnail whose channel count doesn't match the image can't be
// stored in a TGA file; the image is written without one.
A.set_thumbnail(gradient(16, 12, 4));
OIIO_CHECK_ASSERT(A.write("imagebuf_test_thumb4.tga"));
{
ImageBuf in("imagebuf_test_thumb4.tga");
OIIO_CHECK_ASSERT(!in.has_thumbnail());
}

// Test conversion between associated and unassociated alpha.
ImageBuf rgba_image(ImageSpec(64, 48, 4, TypeUInt8));
ImageBufAlgo::zero(rgba_image);
ImageBuf rgba_thumb(ImageSpec(16, 12, 4, TypeUInt8));
const float premult_rgba[4] = { 0.4f, 0.3f, 0.2f, 0.5f };
ImageBufAlgo::fill(rgba_thumb, cspan<float>(premult_rgba));
rgba_image.set_thumbnail(rgba_thumb);
OIIO_CHECK_ASSERT(rgba_image.write("imagebuf_test_thumb5.tga"));
{
ImageBuf in("imagebuf_test_thumb5.tga");
OIIO_CHECK_ASSERT(in.has_thumbnail());
auto rt = in.get_thumbnail();
OIIO_CHECK_ASSERT(rt && rt->initialized());
OIIO_CHECK_EQUAL(
ImageBufAlgo::compare(*rt, rgba_thumb, 0.005f, 0.005f).nfail, 0);
}

for (const char* f :
{ "imagebuf_test_thumb1.tga", "imagebuf_test_thumb2.tga",
"imagebuf_test_thumb3.tga", "imagebuf_test_thumb4.tga",
"imagebuf_test_thumb5.tga" })
Filesystem::remove(f);
}



void
test_read_channel_subset()
{
Expand Down Expand Up @@ -799,6 +953,9 @@ main(int argc, char* argv[])
test_set_get_pixels();
time_get_pixels();

test_thumbnail();
test_thumbnail_tga();

test_write_over();

test_uncaught_error();
Expand Down
Loading