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
29 changes: 29 additions & 0 deletions include/OpenColorIO/OpenColorTransforms.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
79 changes: 79 additions & 0 deletions src/OpenColorIO/transforms/GroupTransform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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);
Expand Down
7 changes: 7 additions & 0 deletions src/bindings/python/transforms/PyGroupTransform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
92 changes: 92 additions & 0 deletions tests/cpu/transforms/GroupTransform_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
24 changes: 24 additions & 0 deletions tests/python/GroupTransformTest.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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().
Expand Down