diff --git a/include/OpenColorIO/OpenColorTransforms.h b/include/OpenColorIO/OpenColorTransforms.h index 94dc58d0d6..a30af71f9b 100644 --- a/include/OpenColorIO/OpenColorTransforms.h +++ b/include/OpenColorIO/OpenColorTransforms.h @@ -1532,6 +1532,35 @@ class OCIOEXPORT GroupTransform : public Transform public: static GroupTransformRcPtr Create(); + /** + * \brief Parse the contents of a LUT file contained in a memory buffer and return the + * transforms it contains as a GroupTransform. + * + * This allows any of the LUT file formats supported by FileTransform to be parsed from + * memory rather than from a file on disk. The buffer contents is tried against each of + * the LUT file readers until one of them is able to parse it (the same fallback behavior + * that FileTransform uses for files with an unrecognized extension). The available + * readers may be listed using FileTransform::GetNumFormats and + * FileTransform::GetFormatNameByIndex. + * + * The buffer contents are copied internally, so the buffer only needs to remain valid + * for the duration of the call. + * + * The parsed contents are stored in the global (process-wide) file cache, keyed on a hash + * of the buffer contents, so parsing a buffer with identical contents again is a cache + * hit. As with LUT files loaded from disk, the cache may be flushed using + * \ref ClearAllCaches. + * + * \param buffer Pointer to the in-memory contents of the LUT file. + * \param bufferSize Size of the buffer, in bytes. + * + * \return The GroupTransform containing the parsed transforms. + * + * \throw Exception if the buffer is null or empty, or if none of the LUT file readers + * are able to parse the buffer contents. + */ + static GroupTransformRcPtr ParseFromBuffer(const char * buffer, size_t bufferSize); + virtual const FormatMetadata & getFormatMetadata() const noexcept = 0; virtual FormatMetadata & getFormatMetadata() noexcept = 0; diff --git a/src/OpenColorIO/transforms/GroupTransform.cpp b/src/OpenColorIO/transforms/GroupTransform.cpp index 89ba45c267..965d2f5f7f 100755 --- a/src/OpenColorIO/transforms/GroupTransform.cpp +++ b/src/OpenColorIO/transforms/GroupTransform.cpp @@ -7,6 +7,7 @@ #include #include "ContextVariableUtils.h" +#include "HashUtils.h" #include "OpBuilders.h" #include "transforms/FileTransform.h" #include "transforms/GroupTransform.h" @@ -14,11 +15,89 @@ namespace OCIO_NAMESPACE { + +namespace +{ +// ConfigIOProxy implementation that provides the contents of a single in-memory LUT file +// buffer, regardless of the file path being requested. +class BufferConfigIOProxy : public ConfigIOProxy +{ +public: + BufferConfigIOProxy(const char * buffer, size_t bufferSize, const std::string & hash) + : m_buffer(reinterpret_cast(buffer), + reinterpret_cast(buffer) + bufferSize) + , m_hash(hash) + { + } + + std::vector getLutData(const char * /* filepath */) const override + { + return m_buffer; + } + + std::string getConfigData() const override + { + // Unused, the config itself is not provided through the proxy. + return ""; + } + + std::string getFastLutFileHash(const char * /* filepath */) const override + { + return m_hash; + } + +private: + std::vector m_buffer; + std::string m_hash; +}; +} // anonymous namespace + GroupTransformRcPtr GroupTransform::Create() { return GroupTransformRcPtr(new GroupTransformImpl(), &GroupTransformImpl::Deleter); } +GroupTransformRcPtr GroupTransform::ParseFromBuffer(const char * buffer, size_t bufferSize) +{ + if (!buffer || bufferSize == 0) + { + throw Exception("GroupTransform::ParseFromBuffer: buffer is null or empty."); + } + + // Hash the buffer contents. The hash is used both to form the synthetic file name given + // to the FileTransform and as the fast LUT file hash returned by the ConfigIOProxy, so + // that the global file caches (which are keyed on these strings) never confuse the + // contents of two different buffers. + const std::string contentHash = CacheIDHash(buffer, bufferSize); + + ConfigIOProxyRcPtr ciop = std::make_shared(buffer, + bufferSize, + contentHash); + + ConfigRcPtr config = Config::CreateRaw()->createEditableCopy(); + config->setConfigIOProxy(ciop); + + // Prefix the hash to form a synthetic file name so that the global file cache entries + // created here can never collide with those of a real file whose resolved path happens + // to match the bare hash string. + const std::string syntheticFileName = "ParseFromBuffer:" + contentHash; + + FileTransformRcPtr fileTransform = FileTransform::Create(); + fileTransform->setSrc(syntheticFileName.c_str()); + + try + { + ConstProcessorRcPtr processor = config->getProcessor(fileTransform); + return processor->createGroupTransform(); + } + catch (Exception & e) + { + std::ostringstream os; + os << "GroupTransform::ParseFromBuffer: Error parsing LUT from buffer: " << e.what(); + throw Exception(os.str().c_str()); + } +} + void GroupTransformImpl::Deleter(GroupTransform * t) { delete static_cast(t); diff --git a/src/bindings/python/transforms/PyGroupTransform.cpp b/src/bindings/python/transforms/PyGroupTransform.cpp index 2b5757427f..0e30c1e460 100644 --- a/src/bindings/python/transforms/PyGroupTransform.cpp +++ b/src/bindings/python/transforms/PyGroupTransform.cpp @@ -57,6 +57,13 @@ void bindPyGroupTransform(py::module & m) "direction"_a = DEFAULT->getDirection(), DOC(GroupTransform, Create)) + .def_static("ParseFromBuffer", [](const std::string & buffer) + { + return GroupTransform::ParseFromBuffer(buffer.data(), buffer.size()); + }, + "buffer"_a.none(false), + DOC(GroupTransform, ParseFromBuffer)) + .def_static("GetWriteFormats", []() { return WriteFormatIterator(nullptr); diff --git a/tests/cpu/transforms/GroupTransform_tests.cpp b/tests/cpu/transforms/GroupTransform_tests.cpp index 67a0f656df..2bb86b14a0 100644 --- a/tests/cpu/transforms/GroupTransform_tests.cpp +++ b/tests/cpu/transforms/GroupTransform_tests.cpp @@ -114,3 +114,95 @@ OCIO_ADD_TEST(GroupTransform, write_with_noops) OCIO_CHECK_NO_THROW(group->write(config, OCIO::FILEFORMAT_CLF, oss)); } } + +namespace +{ +std::string ReadTestFile(const std::string & fileName) +{ + const std::string filePath(OCIO::GetTestFilesDir() + "/" + fileName); + + std::ifstream fstream; + OCIO::Platform::OpenInputFileStream(fstream, + filePath.c_str(), + std::ios_base::in | std::ios_base::binary); + if (fstream.fail()) + { + std::ostringstream os; + os << "Error opening test file: " << filePath; + throw OCIO::Exception(os.str().c_str()); + } + + std::stringstream buffer; + buffer << fstream.rdbuf(); + return buffer.str(); +} +} + +OCIO_ADD_TEST(GroupTransform, parse_from_buffer) +{ + // Parse a SPI1D LUT from a memory buffer. + { + const std::string content = ReadTestFile("lut1d_1.spi1d"); + + OCIO::GroupTransformRcPtr group; + OCIO_CHECK_NO_THROW(group = OCIO::GroupTransform::ParseFromBuffer(content.c_str(), + content.size())); + OCIO_REQUIRE_ASSERT(group); + OCIO_REQUIRE_EQUAL(group->getNumTransforms(), 1); + + auto lut = OCIO_DYNAMIC_POINTER_CAST(group->getTransform(0)); + OCIO_REQUIRE_ASSERT(lut); + OCIO_CHECK_EQUAL(lut->getLength(), 512U); + + float r = 0.f, g = 0.f, b = 0.f; + lut->getValue(1, r, g, b); + OCIO_CHECK_CLOSE(r, 0.00195695f, 1e-8f); + OCIO_CHECK_CLOSE(g, 0.00195695f, 1e-8f); + OCIO_CHECK_CLOSE(b, 0.00195695f, 1e-8f); + + // Parsing the same buffer again works and gives the same result. + OCIO::GroupTransformRcPtr group2; + OCIO_CHECK_NO_THROW(group2 = OCIO::GroupTransform::ParseFromBuffer(content.c_str(), + content.size())); + OCIO_REQUIRE_ASSERT(group2); + OCIO_REQUIRE_EQUAL(group2->getNumTransforms(), 1); + } + + // Parse a CLF LUT from a memory buffer. This also validates that a different buffer + // parsed within the same process is not confused with the previous one by the global + // file caches. + { + const std::string content = ReadTestFile("clf/lut1d_example.clf"); + + OCIO::GroupTransformRcPtr group; + OCIO_CHECK_NO_THROW(group = OCIO::GroupTransform::ParseFromBuffer(content.c_str(), + content.size())); + OCIO_REQUIRE_ASSERT(group); + OCIO_REQUIRE_EQUAL(group->getNumTransforms(), 1); + + auto lut = OCIO_DYNAMIC_POINTER_CAST(group->getTransform(0)); + OCIO_REQUIRE_ASSERT(lut); + OCIO_CHECK_EQUAL(lut->getLength(), 65U); + } + + // Null or empty buffer must throw. + { + OCIO_CHECK_THROW_WHAT(OCIO::GroupTransform::ParseFromBuffer(nullptr, 0), + OCIO::Exception, + "buffer is null or empty"); + + const std::string content = ReadTestFile("lut1d_1.spi1d"); + OCIO_CHECK_THROW_WHAT(OCIO::GroupTransform::ParseFromBuffer(content.c_str(), 0), + OCIO::Exception, + "buffer is null or empty"); + } + + // Malformed buffer contents must throw rather than crash or silently succeed. + { + const std::string content = "This is not the content of any supported LUT format."; + OCIO_CHECK_THROW_WHAT(OCIO::GroupTransform::ParseFromBuffer(content.c_str(), + content.size()), + OCIO::Exception, + "Error parsing LUT from buffer"); + } +} diff --git a/tests/python/GroupTransformTest.py b/tests/python/GroupTransformTest.py index cf3820cb32..19e9a18e76 100644 --- a/tests/python/GroupTransformTest.py +++ b/tests/python/GroupTransformTest.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright Contributors to the OpenColorIO Project. +import os import unittest import PyOpenColorIO as OCIO +from UnitTestUtils import TEST_DATAFILES_DIR from TransformsBaseTest import TransformsBaseTest @@ -131,6 +133,28 @@ def test_constructor_wrong_parameter_type(self): with self.assertRaises(TypeError): group_tr = OCIO.FixedFunctionTransform(invalid) + def test_parse_from_buffer(self): + """ + Test the ParseFromBuffer() static method. + """ + + test_file = os.path.join(TEST_DATAFILES_DIR, 'lut1d_1.spi1d') + with open(test_file, 'rb') as f: + content = f.read() + + group_tr = OCIO.GroupTransform.ParseFromBuffer(content) + self.assertEqual(len(group_tr), 1) + self.assertIsInstance(group_tr[0], OCIO.Lut1DTransform) + + # An empty buffer raises. + with self.assertRaises(OCIO.Exception): + OCIO.GroupTransform.ParseFromBuffer(b'') + + # Contents that no LUT file reader can parse raises. + with self.assertRaises(OCIO.Exception): + OCIO.GroupTransform.ParseFromBuffer( + b'This is not the content of any supported LUT format.') + def test_write_clf(self): """ Test write().