diff --git a/pxr/imaging/hio/CMakeLists.txt b/pxr/imaging/hio/CMakeLists.txt index 8c86a3cc2c..92d1e4fe98 100644 --- a/pxr/imaging/hio/CMakeLists.txt +++ b/pxr/imaging/hio/CMakeLists.txt @@ -40,8 +40,8 @@ pxr_library(hio CPPFILES OpenEXRImage.cpp - stbImage.cpp OpenEXR/openexr-c.c + stbImage.cpp RESOURCE_FILES plugInfo.json @@ -50,3 +50,17 @@ pxr_library(hio overview.dox ) +pxr_build_test(testHioImage + LIBRARIES + ar + hio + plug + tf + + CPPFILES + testenv/testHioImage.cpp +) + +pxr_register_test(testHioImage + COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testHioImage" +) diff --git a/pxr/imaging/hio/OpenEXR/OpenEXRCore/chunk.c b/pxr/imaging/hio/OpenEXR/OpenEXRCore/chunk.c index 304a402ccc..cfe80b4cd6 100644 --- a/pxr/imaging/hio/OpenEXR/OpenEXRCore/chunk.c +++ b/pxr/imaging/hio/OpenEXR/OpenEXRCore/chunk.c @@ -681,7 +681,7 @@ compute_chunk_unpack_size ( chansz *= (uint64_t) width; if (curc->x_sampling > 1) chansz /= ((uint64_t) curc->x_sampling); chansz *= - (uint64_t) compute_sampled_lines (height, curc->y_sampling, y); + (uint64_t) compute_sampled_height (height, curc->y_sampling, y); unpacksize += chansz; } } @@ -1723,7 +1723,7 @@ exr_write_scanline_chunk_info ( cinfo->data_offset = 0; cinfo->packed_size = 0; cinfo->unpacked_size = - compute_chunk_unpack_size (y, cinfo->width, cinfo->height, lpc, part); + compute_chunk_unpack_size (dw.min.y, cinfo->width, cinfo->height, lpc, part); return EXR_UNLOCK_AND_RETURN_PCTXT (EXR_ERR_SUCCESS); } diff --git a/pxr/imaging/hio/OpenEXR/OpenEXRCore/coding.c b/pxr/imaging/hio/OpenEXR/OpenEXRCore/coding.c index cf7cdb502b..b7e486b206 100644 --- a/pxr/imaging/hio/OpenEXR/OpenEXRCore/coding.c +++ b/pxr/imaging/hio/OpenEXR/OpenEXRCore/coding.c @@ -41,13 +41,10 @@ internal_coding_fill_channel_info ( decc->channel_name = curc->name.str; - decc->height = compute_sampled_lines ( + decc->height = compute_sampled_height ( cinfo->height, curc->y_sampling, cinfo->start_y); - - if (curc->x_sampling > 1) - decc->width = cinfo->width / curc->x_sampling; - else - decc->width = cinfo->width; + decc->width = compute_sampled_width ( + cinfo->width, curc->x_sampling, cinfo->start_x); decc->x_samples = curc->x_sampling; decc->y_samples = curc->y_sampling; @@ -99,13 +96,11 @@ internal_coding_update_channel_info ( ccic->channel_name = curc->name.str; - ccic->height = compute_sampled_lines ( + ccic->height = compute_sampled_height ( cinfo->height, curc->y_sampling, cinfo->start_y); + ccic->width = compute_sampled_width ( + cinfo->width, curc->x_sampling, cinfo->start_x); - if (curc->x_sampling > 1) - ccic->width = cinfo->width / curc->x_sampling; - else - ccic->width = cinfo->width; ccic->x_samples = curc->x_sampling; ccic->y_samples = curc->y_sampling; diff --git a/pxr/imaging/hio/OpenEXR/OpenEXRCore/internal_util.h b/pxr/imaging/hio/OpenEXR/OpenEXRCore/internal_util.h index 4dfc360fbe..3c6f02786c 100644 --- a/pxr/imaging/hio/OpenEXR/OpenEXRCore/internal_util.h +++ b/pxr/imaging/hio/OpenEXR/OpenEXRCore/internal_util.h @@ -9,7 +9,7 @@ #include static inline int -compute_sampled_lines (int height, int y_sampling, int start_y) +compute_sampled_height (int height, int y_sampling, int start_y) { int nlines; @@ -31,7 +31,7 @@ compute_sampled_lines (int height, int y_sampling, int start_y) else start = start_y; end = start_y + height - 1; - end -= (end % y_sampling); + end -= (end < 0) ? (-end % y_sampling) : (end % y_sampling); if (start > end) nlines = 0; @@ -42,4 +42,19 @@ compute_sampled_lines (int height, int y_sampling, int start_y) return nlines; } +static inline int +compute_sampled_width (int width, int x_sampling, int start_x) +{ + /* + * we require that the start_x % x_sampling == 0 and for tiled images (and for deep), + * x_sampling must be 1, so this can simplify the math compared to the y case + * where when we are reading scanline images, we always are reading the entire + * width. If this changes, can look like the above call for the lines, but + * for now can be simpler math + */ + if (x_sampling <= 1) return width; + + return (width == 1) ? 1 : (width / x_sampling); +} + #endif /* OPENEXR_PRIVATE_UTIL_H */ diff --git a/pxr/imaging/hio/OpenEXR/OpenEXRCore/parse_header.c b/pxr/imaging/hio/OpenEXR/OpenEXRCore/parse_header.c index b85273a378..d723039269 100644 --- a/pxr/imaging/hio/OpenEXR/OpenEXRCore/parse_header.c +++ b/pxr/imaging/hio/OpenEXR/OpenEXRCore/parse_header.c @@ -2552,9 +2552,9 @@ internal_exr_parse_header (struct _internal_exr_context* ctxt) { struct _internal_exr_seq_scratch scratch; struct _internal_exr_part* curpart; - uint32_t flags; + uint32_t flags = 0; uint64_t initpos; - uint8_t next_byte; + uint8_t next_byte = 0; exr_result_t rv = EXR_ERR_UNKNOWN; if (ctxt->silent_header) diff --git a/pxr/imaging/hio/OpenEXR/OpenEXRCore/part_attr.c b/pxr/imaging/hio/OpenEXR/OpenEXRCore/part_attr.c index f635d6c393..fbfa76364a 100644 --- a/pxr/imaging/hio/OpenEXR/OpenEXRCore/part_attr.c +++ b/pxr/imaging/hio/OpenEXR/OpenEXRCore/part_attr.c @@ -71,7 +71,7 @@ exr_get_attribute_by_name ( const char* name, const exr_attribute_t** outattr) { - exr_attribute_t* tmpptr; + exr_attribute_t* tmpptr = NULL; exr_result_t rv; EXR_PROMOTE_CONST_CONTEXT_AND_PART_OR_ERROR (ctxt, part_index); diff --git a/pxr/imaging/hio/OpenEXR/openexr-c.c b/pxr/imaging/hio/OpenEXR/openexr-c.c index d823e61247..5c39b5adb4 100644 --- a/pxr/imaging/hio/OpenEXR/openexr-c.c +++ b/pxr/imaging/hio/OpenEXR/openexr-c.c @@ -136,9 +136,11 @@ bool nanoexr_Gaussian_resample(const nanoexr_ImageData_t* src, // again for height radius = ceilf(sqrtf(-2.0f * sigma_h * sigma_h * logf(1.0f - support))); int filterSize_h = (int)radius; - if (!filterSize_h) + if (!filterSize_h) { + free(filter_w); return false; - + } + float* filter_h = (float*) malloc(sizeof(float) * (1 + filterSize_h) * 2); sum = 0.0f; for (int i = 0; i <= filterSize_h; i++) { @@ -498,7 +500,7 @@ exr_result_t nanoexr_write_exr( encoder.channels[c].user_pixel_stride = alphaPixelStride; encoder.channels[c].user_line_stride = alphaLineStride; encoder.channels[c].height = scansperchunk; // chunk height - encoder.channels[c].width = dataw.max.x - dataw.min.y + 1; + encoder.channels[c].width = dataw.max.x - dataw.min.x + 1; encoder.channels[c].encode_from_ptr = pAlpha; ++c; } @@ -506,7 +508,7 @@ exr_result_t nanoexr_write_exr( encoder.channels[c].user_pixel_stride = bluePixelStride; encoder.channels[c].user_line_stride = blueLineStride; encoder.channels[c].height = scansperchunk; // chunk height - encoder.channels[c].width = dataw.max.x - dataw.min.y + 1; + encoder.channels[c].width = dataw.max.x - dataw.min.x + 1; encoder.channels[c].encode_from_ptr = pBlue; ++c; } @@ -514,7 +516,7 @@ exr_result_t nanoexr_write_exr( encoder.channels[c].user_pixel_stride = greenPixelStride; encoder.channels[c].user_line_stride = greenLineStride; encoder.channels[c].height = scansperchunk; // chunk height - encoder.channels[c].width = dataw.max.x - dataw.min.y + 1; + encoder.channels[c].width = dataw.max.x - dataw.min.x + 1; encoder.channels[c].encode_from_ptr = pGreen; ++c; } @@ -522,7 +524,7 @@ exr_result_t nanoexr_write_exr( encoder.channels[c].user_pixel_stride = redPixelStride; encoder.channels[c].user_line_stride = redLineStride; encoder.channels[c].height = scansperchunk; // chunk height - encoder.channels[c].width = dataw.max.x - dataw.min.y + 1; + encoder.channels[c].width = dataw.max.x - dataw.min.x + 1; encoder.channels[c].encode_from_ptr = pRed; ++c; } @@ -574,81 +576,42 @@ int nanoexr_getPixelTypeSize(exr_pixel_type_t t) } } -static bool strIsRed(const char* layerName, const char* str) { - if (layerName && (strncmp(layerName, str, strlen(layerName)) != 0)) - return false; - - // check if the case folded string is R or RED, or ends in .R or .RED - char* folded = strdup(str); - for (int i = 0; folded[i]; ++i) { - folded[i] = tolower(folded[i]); - } - if (strcmp(folded, "y") == 0 || strcmp(folded, "r") == 0 || - strcmp(folded, "red") == 0) - return true; - size_t l = strlen(folded); - if ((l > 2) && (folded[l - 2] == '.') && (folded[l - 1] == 'r')) - return true; - if (l < 4) - return false; - return strcmp(folded + l - 4, ".red"); -} +// strcasecmp is not available in the MSVC stdlib. +#ifdef _MSC_VER +#define strcasecmp _stricmp +#endif -static bool strIsGreen(const char* layerName, const char* str) { - if (layerName && (strncmp(layerName, str, strlen(layerName)) != 0)) +static bool strIsComponent(const char* layerName, + const char* str, + const char* component) +{ + if (!str || !component) return false; - // check if the case folded string is G or GREEN, or ends in .G or .GREEN - char* folded = strdup(str); - for (int i = 0; folded[i]; ++i) { - folded[i] = tolower(folded[i]); + // if the layername is specified, it must match the beginning of the string. + // if layername was specified, move the string pointer past it + if (layerName) { + if (strncmp(layerName, str, strlen(layerName)) != 0) + return false; + str += strlen(layerName); } - if (strcmp(folded, "g") == 0 || strcmp(folded, "green") == 0) - return true; - size_t l = strlen(folded); - if ((l > 2) && (folded[l - 2] == '.') && (folded[l - 1] == 'g')) - return true; - if (l < 6) - return false; - return strcmp(folded + l - 6, ".green"); -} - -static bool strIsBlue(const char* layerName, const char* str) { - if (layerName && (strncmp(layerName, str, strlen(layerName)) != 0)) - return false; - // check if the case folded string is B or BLUE, or ends in .B or .BLUE - char* folded = strdup(str); - for (int i = 0; folded[i]; ++i) { - folded[i] = tolower(folded[i]); + // search backwards in str for a dot. If found, move the string pointer + // past it + const char* dot = strrchr(str, '.'); + if (dot) { + str = dot + 1; } - if (strcmp(folded, "b") == 0 || strcmp(folded, "blue") == 0) - return true; - size_t l = strlen(folded); - if ((l > 2) && (folded[l - 2] == '.') && (folded[l - 1] == 'b')) - return true; - if (l < 5) - return false; - return strcmp(folded + l - 5, ".blue"); -} -static bool strIsAlpha(const char* layerName, const char* str) { - if (layerName && (strncmp(layerName, str, strlen(layerName)) != 0)) - return false; - - // check if the case folded string is A or ALPHA, or ends in .A or .ALPHA - char* folded = strdup(str); - for (int i = 0; folded[i]; ++i) { - folded[i] = tolower(folded[i]); + // if one character is left, then do a case insensitive comparison with the + // first character of the component. + if (strlen(str) == 1) { + return tolower(str[0]) == tolower(component[0]); } - if (strcmp(folded, "a") == 0 || strcmp(folded, "alpha") == 0) - return true; - size_t l = strlen(folded); - if ((l > 2) && (folded[l - 2] == '.') && (folded[l - 1] == 'a')) - return true; - if (l < 6) - return false; - return strcmp(folded + l - 6, ".alpha"); + + // The final check is to return true if a case insensitive comparison of + // the string with the component is true. + return strcasecmp(str, component) == 0; } void nanoexr_release_image_data(nanoexr_ImageData_t* imageData) @@ -694,19 +657,19 @@ static exr_result_t _nanoexr_rgba_decoding_initialize( for (int c = 0; c < decoder->channel_count; ++c) { int channelIndex = -1; decoder->channels[c].decode_to_ptr = NULL; - if (strIsRed(layerName, decoder->channels[c].channel_name)) { + if (strIsComponent(layerName, decoder->channels[c].channel_name, "red")) { rgba[0] = c; channelIndex = 0; } - else if (strIsGreen(layerName, decoder->channels[c].channel_name)) { + else if (strIsComponent(layerName, decoder->channels[c].channel_name, "green")) { rgba[1] = c; channelIndex = 1; } - else if (strIsBlue(layerName, decoder->channels[c].channel_name)) { + else if (strIsComponent(layerName, decoder->channels[c].channel_name, "blue")) { rgba[2] = c; channelIndex = 2; } - else if (strIsAlpha(layerName, decoder->channels[c].channel_name)) { + else if (strIsComponent(layerName, decoder->channels[c].channel_name, "alpha")) { rgba[3] = c; channelIndex = 3; } @@ -1018,7 +981,7 @@ exr_result_t nanoexr_read_exr(const char* filename, exr_compression_t compression; rv = exr_get_compression(exr, partIndex, &compression); if (rv != EXR_ERR_SUCCESS) { - fprintf(stderr, "nanoexr compression error: %s\n", + fprintf(stderr, "nanoexr compression error: %s\n", exr_get_default_error_message(rv)); exr_finish(&exr); return rv; @@ -1068,8 +1031,8 @@ exr_result_t nanoexr_read_exr(const char* filename, img->height = height; img->dataSize = width * height * img->channelCount * bytesPerChannel; img->pixelType = pixelType; - img->dataWindowMinY = (datawin.min.y + 1) >> mipLevel; - img->dataWindowMaxY = (datawin.max.y + 1) >> mipLevel; + img->dataWindowMinY = datawin.min.y >> mipLevel; + img->dataWindowMaxY = (datawin.max.y+1) >> mipLevel; img->data = (unsigned char*) malloc(img->dataSize); if (img->data == NULL) { fprintf(stderr, "nanoexr error: could not allocate memory for image data\n"); diff --git a/pxr/imaging/hio/OpenEXRImage.cpp b/pxr/imaging/hio/OpenEXRImage.cpp index 7d643e72d5..443bc0b0c8 100644 --- a/pxr/imaging/hio/OpenEXRImage.cpp +++ b/pxr/imaging/hio/OpenEXRImage.cpp @@ -76,9 +76,10 @@ class Hio_OpenEXRImage final : public HioImage bool Write(StorageSpec const &storage, VtDictionary const &metadata) override; - // IsColorSpaceSRGB asks if the color space is SRGB, but - // what Hydra really wants to know is whether the pixels are gamma pixels. - // OpenEXR images are always linear, so just return false. + // IsColorSpaceSRGB asks if the color values are SRGB encoded against the + // SRGB curve, although what Hydra really wants to know is whether the + // pixels are gamma pixels. OpenEXR images are always linear, so always + // return false. bool IsColorSpaceSRGB() const override { return false; } HioFormat GetFormat() const override; int GetWidth() const override { return _exrReader.width; } @@ -171,9 +172,10 @@ namespace { { Hio_OpenEXRImage* self = (Hio_OpenEXRImage*) userdata; if (!self || !self->Asset() || !buffer || !sz) { - if (error_cb) + if (error_cb) { error_cb(ctxt, EXR_ERR_INVALID_ARGUMENT, "%s", "Invalid arguments to read callback"); + } return -1; } return self->Asset()->Read(buffer, sz, offset); @@ -205,8 +207,9 @@ namespace { int newHeight = height - cropTop - cropBottom; if (newWidth <= 0 || newHeight <= 0 - || (newWidth == width && newHeight == height)) + || (newWidth == width && newHeight == height)) { return; + } for (int y = 0; y < newHeight; ++y) { for (int x = 0; x < newWidth; ++x) { @@ -222,8 +225,9 @@ namespace { static void HalfToFloat(GfHalf* buffer, float* outBuffer, int width, int height, int channelCount) { - if (!buffer || !outBuffer) + if (!buffer || !outBuffer) { return; + } for (int i = 0; i < width * height * channelCount; ++i) { outBuffer[i] = buffer[i]; @@ -233,8 +237,9 @@ namespace { static void FloatToHalf(float* buffer, GfHalf* outBuffer, int width, int height, int channelCount) { - if (!buffer || !outBuffer) + if (!buffer || !outBuffer) { return; + } for (int i = 0; i < width * height * channelCount; ++i) { outBuffer[i] = buffer[i]; @@ -250,11 +255,13 @@ bool Hio_OpenEXRImage::ReadCropped( StorageSpec const& storage) { // not opened for read prior to calling ReadCropped. - if (!_asset) + if (!_asset) { return false; + } - if (cropTop < 0 || cropBottom < 0 || cropLeft < 0 || cropRight < 0) + if (cropTop < 0 || cropBottom < 0 || cropLeft < 0 || cropRight < 0) { return false; + } // cache values for the read/crop/resize pipeline @@ -274,13 +281,23 @@ bool Hio_OpenEXRImage::ReadCropped( bool outputIsHalf = HioGetHioType(storage.format) == HioTypeHalfFloat; bool outputIsUInt = HioGetHioType(storage.format) == HioTypeUnsignedInt; - // no conversion between uint and a float type is provided. - if (outputIsUInt && !inputIsUInt) + // no conversion to anything except these formats + if (!(outputIsHalf || outputIsFloat || outputIsUInt)) { return false; - if (outputIsFloat && !(inputIsFloat || inputIsHalf)) + } + + // no conversion to uint from non uint + if (outputIsUInt && !inputIsUInt) { return false; + } - int outputBytesPerPixel = (int) HioGetDataSizeOfType(storage.format) * outChannelCount; + // no coversion of non float to float + if (outputIsFloat && !(inputIsFloat || inputIsHalf)) { + return false; + } + + int outputBytesPerPixel = + (int) HioGetDataSizeOfType(storage.format) * outChannelCount; int readWidth = fileWidth - cropLeft - cropRight; int readHeight = fileHeight - cropTop - cropBottom; @@ -392,11 +409,11 @@ bool Hio_OpenEXRImage::ReadCropped( uint32_t outCount = outWidth * outHeight * outChannelCount; if (inputIsHalf && outputIsHalf) { memcpy(reinterpret_cast(storage.data), - &halfInputBuffer[0], outSize); + halfInputBuffer.data(), outSize); } else if (inputIsFloat && outputIsFloat) { memcpy(reinterpret_cast(storage.data), - &floatInputBuffer[0], outSize); + floatInputBuffer.data(), outSize); } else if (outputIsFloat) { GfHalf* src = halfInputBuffer.data(); @@ -552,16 +569,18 @@ bool Hio_OpenEXRImage::GetSamplerMetadata(HioAddressDimension dim, //static void Hio_OpenEXRImage::_AttributeReadCallback(void* self_, exr_context_t exr) { Hio_OpenEXRImage* self = reinterpret_cast(self_); - if (!self->_metadata.empty()) + if (!self->_metadata.empty()) { return; + } const int partIndex = self->_subimage; int attrCount = nanoexr_get_attribute_count(exr, partIndex); for (int i = 0; i < attrCount; ++i) { const exr_attribute_t* attr; nanoexr_get_attribute_by_index(exr, partIndex, i, &attr); - if (!attr) + if (!attr) { continue; + } // this switch is an exhaustive alphabetical treatment of all the // possible attribute types. @@ -893,7 +912,7 @@ bool Hio_OpenEXRImage::Write(StorageSpec const &storage, ++pixMul; } - if (type == HioTypeFloat) + if (type == HioTypeFloat) { rv = nanoexr_write_exr( _filename.c_str(), _AttributeWriteCallback, this, @@ -903,7 +922,8 @@ bool Hio_OpenEXRImage::Write(StorageSpec const &storage, green, pixelStride, lineStride, blue, pixelStride, lineStride, alpha, pixelStride, lineStride); - else + } + else { rv = nanoexr_write_exr( _filename.c_str(), _AttributeWriteCallback, this, @@ -913,6 +933,7 @@ bool Hio_OpenEXRImage::Write(StorageSpec const &storage, green, pixelStride, lineStride, blue, pixelStride, lineStride, alpha, pixelStride, lineStride); + } _callbackDict = nullptr; return rv == EXR_ERR_SUCCESS; diff --git a/pxr/imaging/hio/stbImage.cpp b/pxr/imaging/hio/stbImage.cpp index e0acf0a5ef..3666e8c00f 100644 --- a/pxr/imaging/hio/stbImage.cpp +++ b/pxr/imaging/hio/stbImage.cpp @@ -408,6 +408,12 @@ Hio_StbImage::ReadCropped(int const cropTop, int const cropRight, StorageSpec const & storage) { + // check if the image is already in the desired format + if (storage.format != _format) { + TF_RUNTIME_ERROR("Image format mismatch"); + return false; + } + // read from file // NOTE: stbi_load will always return image data as a contiguous block // of memory for every image format (i.e. image data is packed @@ -428,7 +434,7 @@ Hio_StbImage::ReadCropped(int const cropTop, ArResolvedPath(_filename)); if (!asset) { - TF_CODING_ERROR("Cannot open image %s for reading", _filename.c_str()); + TF_RUNTIME_ERROR("Cannot open image %s for reading", _filename.c_str()); return false; } @@ -459,7 +465,7 @@ Hio_StbImage::ReadCropped(int const cropTop, //// Read pixel data if (!imageData) { - TF_CODING_ERROR("unable to get_pixels"); + TF_RUNTIME_ERROR("unable to get_pixels"); return false; } @@ -476,7 +482,7 @@ Hio_StbImage::ReadCropped(int const cropTop, if (!_CropAndResize(imageData, cropTop, cropBottom, cropLeft, cropRight, resizeNeeded, storage)) { - TF_CODING_ERROR("Unable to crop and resize"); + TF_RUNTIME_ERROR("Unable to crop and resize"); stbi_image_free(imageData); return false; } @@ -538,7 +544,7 @@ Hio_StbImage::ReadCropped(int const cropTop, if (!storage.data) { - TF_CODING_ERROR("Failed to copy data to storage.data"); + TF_RUNTIME_ERROR("Failed to copy data to storage.data"); } stbi_image_free(imageData); @@ -623,13 +629,13 @@ Hio_StbImage::Write(StorageSpec const & storageIn, quantizedSpec = _Quantize(storageIn, quantizedData, isSRGB); } else if (type != HioTypeUnsignedByte && fileExtension != "hdr") { - TF_CODING_ERROR("stb expects unsigned byte data to write filetype %s", + TF_RUNTIME_ERROR("stb expects unsigned byte data to write filetype %s", fileExtension.c_str()); return false; } else if (type != HioTypeFloat && fileExtension == "hdr") { - TF_CODING_ERROR("stb expects linear float data to write filetype hdr"); + TF_RUNTIME_ERROR("stb expects linear float data to write filetype hdr"); return false; } diff --git a/pxr/imaging/hio/testenv/testHioImage.cpp b/pxr/imaging/hio/testenv/testHioImage.cpp new file mode 100644 index 0000000000..0ff3b40094 --- /dev/null +++ b/pxr/imaging/hio/testenv/testHioImage.cpp @@ -0,0 +1,403 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the Apache License, Version 2.0 (the "Apache License") +// with the following modification; you may not use this file except in +// compliance with the Apache License and the following modification to it: +// Section 6. Trademarks. is deleted and replaced with: +// +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor +// and its affiliates, except as required to comply with Section 4(c) of +// the License and to reproduce the content of the NOTICE file. +// +// You may obtain a copy of the Apache License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the Apache License with the above modification is +// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the Apache License for the specific +// language governing permissions and limitations under the Apache License. +// + +#include "pxr/pxr.h" +#include "pxr/base/arch/defines.h" +#include "pxr/base/plug/plugin.h" +#include "pxr/base/plug/registry.h" +#include "pxr/base/tf/diagnostic.h" +#include "pxr/imaging/hio/image.h" +#include "pxr/usd/ar/resolver.h" +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +const int w = 256; +const int h = 256; + +const std::vector& GetGrey8Values() +{ + // create a checkerboard pattern, with a stride of 32 pixels. + static std::once_flag _once; + static std::vector _grey8Values(w * h); + std::call_once(_once, []() { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int xsnap = x & 0xE0; // mask off bottom 5 bits + int ysnap = y & 0xE0; + uint8_t value = (xsnap + ysnap) & 0xff; + int index = y * w + x; + int checkIndex = (y/32 * w + x/32); + if (checkIndex & 1) { + _grey8Values[index] = value; + } + else { + _grey8Values[index] = 255 - value; + } + } + } + }); + return _grey8Values; +} + +const std::vector& GetRgb8Values() +{ + static std::once_flag _once; + static std::vector _rgb8Values(w * h * 3); + std::call_once(_once, []() { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int index = 3 * (y * w + x); + _rgb8Values[index + 0] = x & 0xff; + _rgb8Values[index + 1] = y & 0xff; + _rgb8Values[index + 2] = (x + y) & 0xff; + } + } + }); + return _rgb8Values; +} + +const std::vector& GetRgbFloatValues() +{ + static std::once_flag _once; + static std::vector _rgbFloatValues(w * h * 3); + std::call_once(_once, []() { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int index = 3 * (y * w + x); + _rgbFloatValues[index + 0] = (x & 0xff) / 255.0f; + _rgbFloatValues[index + 1] = (y & 0xff) / 255.0f; + _rgbFloatValues[index + 2] = ((x + y) & 0xff) / 255.0f; + } + } + }); + return _rgbFloatValues; +} + +int +main(int argc, char *argv[]) +{ + { + // verify that the hio plugin exists + bool found = false; + for (PlugPluginPtr plug : PlugRegistry::GetInstance().GetAllPlugins()) { + if (plug->GetName() == "hio") { + found = true; + break; + } + } + TF_AXIOM(found); + } + { + // validate that the Ar plugin loaded by asking the default resolver to + // tell the filename extension; Ar is going to be used by Hio to load + // images via the Asset API. + const TfToken fileExtension( + TfStringToLowerAscii(ArGetResolver().GetExtension("test.exr"))); + TF_AXIOM(fileExtension.GetString() == "exr"); + } + { + // get the TfType for HioImage + TfType hioImageType = TfType::Find(); + int stockPlugins = 0; + // validate that the HioImage subclass types are registered + std::vector hioImageTypes = + PlugRegistry::GetInstance().GetDirectlyDerivedTypes(hioImageType); + for (auto t : hioImageTypes) { + auto tn = t.GetTypeName(); + if (tn == "Hio_OpenEXRImage" || tn == "Hio_StbImage") + ++stockPlugins; + } + // at least exr and stb are always available. + TF_AXIOM(stockPlugins == 2); + } + + // check existence of built-in formats that should always be available, + // as part of the OpenEXR and Stb plugins tested above. + { + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.exr")); + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.bmp")); + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.jpg")); + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.jpeg")); + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.png")); + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.tga")); + TF_AXIOM(HioImage::IsSupportedImageFile("dummy.hdr")); + TF_AXIOM(!HioImage::IsSupportedImageFile("dummy.xml")); + } + + // write out the greyscale values as png, then read it back in and compare + { + const std::vector& grey8Values = GetGrey8Values(); + std::string filename = "testGrey.png"; + HioImageSharedPtr image = HioImage::OpenForWriting(filename); + TF_AXIOM(image); + + // create storage spec + HioImage::StorageSpec storageSpec; + storageSpec.width = w; + storageSpec.height = h; + storageSpec.format = HioFormatUNorm8; + storageSpec.flipped = false; + storageSpec.data = (void*) grey8Values.data(); + + TF_AXIOM(image->Write(storageSpec)); + image.reset(); + + image = HioImage::OpenForReading(filename); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatUNorm8); + TF_AXIOM(image->GetBytesPerPixel() == 1); + std::vector readback(w * h); + auto readSpec = storageSpec; + readSpec.data = readback.data(); + TF_AXIOM(image->Read(readSpec)); + TF_AXIOM(grey8Values == readback); + } + + // write out rgb8values as png, then read it back in and compare + { + const std::vector& rgb8Values = GetRgb8Values(); + std::string filename = "test.png"; + HioImageSharedPtr image = HioImage::OpenForWriting(filename); + TF_AXIOM(image); + + // create storage spec + HioImage::StorageSpec storageSpec; + storageSpec.width = w; + storageSpec.height = h; + storageSpec.format = HioFormatUNorm8Vec3srgb; + storageSpec.flipped = false; + storageSpec.data = (void*) rgb8Values.data(); + + TF_AXIOM(image->Write(storageSpec)); + image.reset(); + + image = HioImage::OpenForReading(filename); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatUNorm8Vec3srgb); + TF_AXIOM(image->GetBytesPerPixel() == 3); + std::vector readback(w * h * 3); + auto readSpec = storageSpec; + readSpec.data = readback.data(); + TF_AXIOM(image->Read(readSpec)); + TF_AXIOM(rgb8Values == readback); + } + + // repeat for jpeg + { + const std::vector& rgb8Values = GetRgb8Values(); + std::string filename = "test.jpg"; + HioImageSharedPtr image = HioImage::OpenForWriting(filename); + TF_AXIOM(image); + + // create storage spec + HioImage::StorageSpec storageSpec; + storageSpec.width = w; + storageSpec.height = h; + storageSpec.format = HioFormatUNorm8Vec3srgb; + storageSpec.flipped = false; + storageSpec.data = (void*) rgb8Values.data(); + + TF_AXIOM(image->Write(storageSpec)); + image.reset(); + + image = HioImage::OpenForReading(filename); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatUNorm8Vec3srgb); + TF_AXIOM(image->GetBytesPerPixel() == 3); + std::vector readback(w * h * 3); + auto readSpec = storageSpec; + readSpec.data = readback.data(); + TF_AXIOM(image->Read(readSpec)); + + // jpeg is lossy so compare the pixels with a tolerance of +/- 1 + for (int i = 0; i < w * h * 3; i++) { + TF_AXIOM(rgb8Values[i] - 2 <= readback[i] && readback[i] <= rgb8Values[i] + 2); + } + } + +#ifndef ARCH_OS_WINDOWS + // do a lossless comparison for exr and float32 + { + const std::vector& rgbFloatValues = GetRgbFloatValues(); + std::string filename = "test.exr"; + HioImageSharedPtr image = HioImage::OpenForWriting(filename); + TF_AXIOM(image); + + // create storage spec + HioImage::StorageSpec storageSpec; + storageSpec.width = w; + storageSpec.height = h; + storageSpec.format = HioFormatFloat32Vec3; + storageSpec.flipped = false; + storageSpec.data = (void*) rgbFloatValues.data(); + + TF_AXIOM(image->Write(storageSpec)); + image.reset(); + + image = HioImage::OpenForReading(filename); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatFloat32Vec3); + TF_AXIOM(image->GetBytesPerPixel() == sizeof(float) * 3); + std::vector readback(w * h * 3); + auto readSpec = storageSpec; + readSpec.data = readback.data(); + TF_AXIOM(image->Read(readSpec)); + TF_AXIOM(rgbFloatValues == readback); + } + + // test.exr now exists; read it requesting a half scale resize + { + const std::vector& rgbFloatValues = GetRgbFloatValues(); + HioImageSharedPtr image = HioImage::OpenForReading("test.exr"); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatFloat32Vec3); + TF_AXIOM(image->GetBytesPerPixel() == sizeof(float) * 3); + + int w2 = w/2; + int h2 = h/2; + std::vector readback(w2 * h2 * 3); + + HioImage::StorageSpec readSpec; + readSpec.width = w2; + readSpec.height = h2; + readSpec.format = HioFormatFloat32Vec3; + readSpec.data = readback.data(); + TF_AXIOM(image->Read(readSpec)); + // verify that the pixel values are approximately the same + for (int y = 0; y < h2; y++) { + for (int x = 0; x < w2; x++) { + int index = 3 * (y * w2 + x); + int index2 = 3 * (y * 2 * w + x * 2); + // a loose comparison is fine for this test + TF_AXIOM(fabsf(readback[index + 0] - rgbFloatValues[index2 + 0]) < 16.f/255.f); + TF_AXIOM(fabsf(readback[index + 1] - rgbFloatValues[index2 + 1]) < 16.f/255.f); + TF_AXIOM(fabsf(readback[index + 2] - rgbFloatValues[index2 + 2]) < 16.f/255.f); + } + } + } +#endif + + // read the test.png as float32, which is expected to fail + { + HioImageSharedPtr image = HioImage::OpenForReading("test.png"); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatUNorm8Vec3srgb); + TF_AXIOM(image->GetBytesPerPixel() == 3); + + std::cout << "Expecting an image format mismatch." << std::endl; + std::vector readback(w * h * 3); + HioImage::StorageSpec readSpec; + readSpec.width = w; + readSpec.height = h; + readSpec.format = HioFormatFloat32Vec3; + readSpec.data = readback.data(); + TF_AXIOM(!image->Read(readSpec)); + } + + // read the test.jpg as rgba, which is expected to fail + { + HioImageSharedPtr image = HioImage::OpenForReading("test.jpg"); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatUNorm8Vec3srgb); + TF_AXIOM(image->GetBytesPerPixel() == 3); + + std::cout << "Expecting an image format mismatch." << std::endl; + std::vector readback(w * h * 3); + HioImage::StorageSpec readSpec; + readSpec.width = w; + readSpec.height = h; + readSpec.format = HioFormatUNorm8Vec4srgb; + readSpec.data = readback.data(); + TF_AXIOM(!image->Read(readSpec)); + } + +#ifndef ARCH_OS_WINDOWS + // read the exr file as float32 rgba, and verify that the pixels are the + // same and that the alpha channel is full of ones. + { + HioImageSharedPtr image = HioImage::OpenForReading("test.exr"); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatFloat32Vec3); + TF_AXIOM(image->GetBytesPerPixel() == sizeof(float) * 3); + std::vector readback(w * h * 4); + HioImage::StorageSpec readSpec; + readSpec.width = w; + readSpec.height = h; + readSpec.format = HioFormatFloat32Vec4; + readSpec.data = readback.data(); + TF_AXIOM(image->Read(readSpec)); + // verify that the pixel values are the same + const std::vector& rgbFloatValues = GetRgbFloatValues(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int index = 4 * (y * w + x); + int index3 = 3 * (y * w + x); + TF_AXIOM(readback[index + 0] == rgbFloatValues[index3 + 0]); + TF_AXIOM(readback[index + 1] == rgbFloatValues[index3 + 1]); + TF_AXIOM(readback[index + 2] == rgbFloatValues[index3 + 2]); + TF_AXIOM(readback[index + 3] == 1.0f); + } + } + } + + // read the exr file as uint8_t rgba, verify this is not be supported. + { + HioImageSharedPtr image = HioImage::OpenForReading("test.exr"); + TF_AXIOM(image); + TF_AXIOM(image->GetWidth() == w); + TF_AXIOM(image->GetHeight() == h); + TF_AXIOM(image->GetFormat() == HioFormatFloat32Vec3); + TF_AXIOM(image->GetBytesPerPixel() == sizeof(float) * 3); + std::vector readback(w * h * 4); + HioImage::StorageSpec readSpec; + readSpec.width = w; + readSpec.height = h; + readSpec.format = HioFormatUNorm8Vec4srgb; + readSpec.data = readback.data(); + TF_AXIOM(!image->Read(readSpec)); + } +#endif + + printf("OK\n"); + return 0; +}