-
Notifications
You must be signed in to change notification settings - Fork 505
Add GroupTransform::ParseFromBuffer to parse a LUT from memory #2326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,18 +7,97 @@ | |
| #include <OpenColorIO/OpenColorIO.h> | ||
|
|
||
| #include "ContextVariableUtils.h" | ||
| #include "HashUtils.h" | ||
| #include "OpBuilders.h" | ||
| #include "transforms/FileTransform.h" | ||
| #include "transforms/GroupTransform.h" | ||
|
|
||
|
|
||
| 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<const uint8_t *>(buffer), | ||
| reinterpret_cast<const uint8_t *>(buffer) + bufferSize) | ||
| , m_hash(hash) | ||
| { | ||
| } | ||
|
|
||
| std::vector<uint8_t> 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<uint8_t> 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<BufferConfigIOProxy>(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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be very inefficient without a file extension to hint which format parser to try first. Otherwise the buffer may need to be copied twice for each of the 20 formats. There should be a third parameter for the function (which is allowed to be empty) because callers will often know the format of the buffer. Here is proposed code: |
||
|
|
||
| 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<GroupTransformImpl *>(t); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not take a string argument. For binary formats such as ICC profiles, I think that could trigger a UTF-8 conversion. Please try the following: |
||
| .def_static("GetWriteFormats", []() | ||
| { | ||
| return WriteFormatIterator(nullptr); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<const OCIO::Lut1DTransform>(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<const OCIO::Lut1DTransform>(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"); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add the following test that the error is about a missing file rather than triggering the recursion limit: |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This must not return the same thing every time. It would trigger an infinite recursion if someone loads a CTF file containing a Reference element. Here's a proposed fix: