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;
}

Copy link
Copy Markdown
Collaborator

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:

// ConfigIOProxy implementation that provides the contents of a single in-memory LUT file
// buffer, under one synthetic file name.  Any other file name is reported as missing.
class BufferConfigIOProxy : public ConfigIOProxy
{
public:
    BufferConfigIOProxy(const char * buffer,
                        size_t bufferSize,
                        const std::string & hash,
                        const std::string & syntheticFileName)
        : m_buffer(reinterpret_cast<const uint8_t *>(buffer),
                   reinterpret_cast<const uint8_t *>(buffer) + bufferSize)
        , m_hash(hash)
        , m_syntheticFileName(syntheticFileName)
    {
    }

    std::vector<uint8_t> getLutData(const char * filepath) const override
    {
        // Return an empty hash to avoid infinite recursion if m_buffer
        // contains a CTF with a Reference element.
        return isBufferPath(filepath) ? m_buffer : std::vector<uint8_t>();
    }

    std::string getConfigData() const override
    {
        // Unused, the config itself is not provided through the proxy.
        return "";
    }

    std::string getFastLutFileHash(const char * filepath) const override
    {
        // An empty hash makes FileExists false, to avoid infinite recursion if m_buffer
        // contains a CTF with a Reference element.
        return isBufferPath(filepath) ? m_hash : "";
    }

private:
    // The proxy is given the *resolved* path, i.e. the synthetic name joined with the
    // config's working directory.  ParseFromBuffer builds its config from a stream, which
    // leaves the working directory empty, so the resolved path is the synthetic name
    // verbatim.
    bool isBufferPath(const char * filepath) const
    {
        return filepath && m_syntheticFileName == filepath;
    }

    std::vector<uint8_t> m_buffer;
    std::string m_hash;
    std::string m_syntheticFileName;
};


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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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:

GroupTransformRcPtr GroupTransform::ParseFromBuffer(const char * buffer,
                                                    size_t bufferSize,
                                                    const char * formatHint)
{
    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);

    // 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.
    std::string syntheticFileName = "ParseFromBuffer:" + contentHash;

    if (formatHint && *formatHint)
    {
        // Append the hint as a file name extension, so that the readers registered for that
        // extension are tried first rather than trying every reader in turn.
        const char * ext = (*formatHint == '.') ? formatHint + 1 : formatHint;
        if (*ext)
        {
            syntheticFileName += ".";
            syntheticFileName += StringUtils::Lower(ext);
        }
    }

    ConfigIOProxyRcPtr ciop = std::make_shared<BufferConfigIOProxy>(buffer,
                                                                    bufferSize,
                                                                    contentHash,
                                                                    syntheticFileName);

    ConfigRcPtr config = Config::CreateRaw()->createEditableCopy();
    config->setConfigIOProxy(ciop);

    FileTransformRcPtr fileTransform = FileTransform::Create();
    fileTransform->setSrc(syntheticFileName.c_str());

    try
    {
        ConstProcessorRcPtr processor = config->getProcessor(fileTransform);
        return processor->createGroupTransform();
    }
    // Note that a CTF containing a Reference element will trigger this. The message should
    // contain the path that the Reference wants to load.
    catch (Exception & e)
    {
        std::ostringstream os;
        os << "GroupTransform::ParseFromBuffer: Error parsing LUT from buffer: " << e.what();
        throw Exception(os.str().c_str());
    }
}


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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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("ParseFromBuffer", [](const py::buffer & buffer,
                                          const std::string & formatHint)
            {
                // Note that py::buffer accepts any bytes-like object (bytes, bytearray,
                // memoryview, ...) but not str, which would need to be encoded first and so
                // must not be silently transcoded here.
                py::buffer_info info = buffer.request();
                checkCContiguousArray(info);
                return GroupTransform::ParseFromBuffer(
                    static_cast<const char *>(info.ptr),
                    static_cast<size_t>(info.size * info.itemsize),
                    formatHint.c_str());
            },
             "buffer"_a,
             "formatHint"_a = "",
             DOC(GroupTransform, ParseFromBuffer, 2))

.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");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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:

    // A buffer that refers to an external file must report that file as missing, since only
    // the contents of the buffer itself are available. In particular the reference must not
    // resolve back to the buffer, which would report a recursion instead.
    {
        const std::string content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                                    "<ProcessList id=\"ref\" version=\"1.7\">\n"
                                    "   <Reference inBitDepth=\"32f\" outBitDepth=\"32f\" "
                                    "path=\"lut1d_1.spi1d\"/>\n"
                                    "</ProcessList>\n";

        OCIO_CHECK_THROW_WHAT(OCIO::GroupTransform::ParseFromBuffer(content.c_str(),
                                                                    content.size(),
                                                                    "ctf"),
                              OCIO::Exception,
                              "could not be located");
    }

}
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