Skip to content

Commit

Permalink
fraction/double
Browse files Browse the repository at this point in the history
Signed-off-by: Cary Phillips <[email protected]>
  • Loading branch information
cary-ilm committed Jun 1, 2024
1 parent 0d63c4e commit 828c39f
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 51 deletions.
89 changes: 72 additions & 17 deletions src/wrappers/python/PyOpenEXR.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,26 @@ py_cast(const py::object& object)
return nullptr;
}

template <>
const double*
py_cast(const py::object& object)
{
//
// Recognize a 1-element array of double as a DoubleAttribute
//

if (py::isinstance<py::array_t<double>>(object))
{
auto a = object.cast<py::array_t<double>>();
if (a.size() == 1)
{
py::buffer_info buf = a.request();
return static_cast<const double*>(buf.ptr);
}
}
return nullptr;
}

template <class T>
py::array
make_v2(const Vec2<T>& v)
Expand Down Expand Up @@ -1031,8 +1051,13 @@ PyFile::get_attribute_object(const std::string& name, const Attribute* a)
if (auto v = dynamic_cast<const CompressionAttribute*> (a))
return py::cast(v->value());

//
// Convert Double attribute to a single-element numpy array, so
// its type is preserved.
//

if (auto v = dynamic_cast<const DoubleAttribute*> (a))
return py::cast(PyDouble(v->value()));
return py::array_t<double>(1, &v->value());

if (auto v = dynamic_cast<const EnvmapAttribute*> (a))
return py::cast(v->value());
Expand Down Expand Up @@ -1183,7 +1208,11 @@ PyFile::get_attribute_object(const std::string& name, const Attribute* a)
}

if (auto v = dynamic_cast<const RationalAttribute*> (a))
return py::cast(v->value());
{
py::module fractions = py::module::import("fractions");
py::object Fraction = fractions.attr("Fraction");
return Fraction(v->value().n, v->value().d);
}

if (auto v = dynamic_cast<const TileDescriptionAttribute*> (a))
return py::cast(v->value());
Expand All @@ -1209,7 +1238,9 @@ PyFile::get_attribute_object(const std::string& name, const Attribute* a)
if (auto v = dynamic_cast<const V3dAttribute*> (a))
return make_v3(v->value());

throw std::runtime_error("unrecognized attribute type");
std::stringstream err;
err << "unsupported attribute type: " << a->typeName();
throw std::runtime_error(err.str());

return py::none();
}
Expand Down Expand Up @@ -1441,6 +1472,7 @@ is_chromaticities(const py::object& object, Chromaticities& v)
return false;
}

#if XXX

template <class T>
Vec2<T>
Expand Down Expand Up @@ -1482,6 +1514,7 @@ get_m44(const py::array& a)
v[8], v[9], v[10], v[11],
v[12], v[13], v[14], v[15]);
}
#endif

void
PyFile::insert_attribute(Header& header, const std::string& name, const py::object& object)
Expand All @@ -1492,7 +1525,11 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje

std::stringstream err;

// Required to be Box2i?
//
// If the attribute is standard/required, its type is fixed, so
// cast the rhs to the appropriate type if possible, or reject it
// as an error if not.
//

if (name == "dataWindow" ||
name == "displayWindow" ||
Expand Down Expand Up @@ -1541,6 +1578,10 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje
throw std::invalid_argument(err.str());
}

//
// Recognize tuples and arrays as V2/V3 i/f/d or M33/M44 f/d
//

V2i v2i;
if (is_v2i(object, v2i))
{
Expand Down Expand Up @@ -1611,6 +1652,10 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje
return;
}

//
// Recognize 2-tuples of 2-vectors as boxes
//

Box2i box2i;
if (is_box2i(object, box2i))
{
Expand All @@ -1625,13 +1670,24 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje
return;
}

//
// Recognize an 8-tuple as chromaticities
//

Chromaticities c;
if (is_chromaticities(object, c))
{
header.insert(name, ChromaticitiesAttribute(c));
return;
}

//
// Inspect the rhs type
//

py::module fractions = py::module::import("fractions");
py::object Fraction = fractions.attr("Fraction");

if (py::isinstance<py::list>(object))
{
auto list = py::cast<py::list>(object);
Expand Down Expand Up @@ -1662,12 +1718,12 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje
header.insert(name, CompressionAttribute(static_cast<Compression>(*v)));
else if (auto v = py_cast<Envmap>(object))
header.insert(name, EnvmapAttribute(static_cast<Envmap>(*v)));
else if (py::isinstance<py::float_>(object))
header.insert(name, FloatAttribute(py::cast<py::float_>(object)));
else if (py::isinstance<PyDouble>(object))
header.insert(name, DoubleAttribute(py::cast<PyDouble>(object).d));
else if (py::isinstance<py::int_>(object))
header.insert(name, IntAttribute(py::cast<py::int_>(object)));
else if (py::isinstance<py::float_>(object))
header.insert(name, FloatAttribute(py::cast<py::float_>(object)));
else if (auto v = py_cast<double>(object))
header.insert(name, DoubleAttribute(*v));
else if (auto v = py_cast<KeyCode>(object))
header.insert(name, KeyCodeAttribute(*v));
else if (auto v = py_cast<LineOrder>(object))
Expand All @@ -1681,8 +1737,6 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje
PreviewImage p(width, height, pixels);
header.insert(name, PreviewImageAttribute(p));
}
else if (auto v = py_cast<Rational>(object))
header.insert(name, RationalAttribute(*v));
else if (auto v = py_cast<TileDescription>(object))
header.insert(name, TileDescriptionAttribute(*v));
else if (auto v = py_cast<TimeCode>(object))
Expand Down Expand Up @@ -1713,10 +1767,17 @@ PyFile::insert_attribute(Header& header, const std::string& name, const py::obje
}
else if (py::isinstance<py::str>(object))
header.insert(name, StringAttribute(py::str(object)));
else if (py::isinstance(object, Fraction))
{
int n = py::int_(object.attr("numerator"));
int d = py::int_(object.attr("denominator"));
Rational r(n, d);
header.insert(name, RationalAttribute(r));
}
else
{
auto t = py::str(object.attr("__class__").attr("__name__"));
err << "unrecognized type of attribute '" << name << "': " << t << " " << py::str(object);
err << "unrecognized type of attribute '" << name << "': type=" << t << " value=" << py::str(object);
if (py::isinstance<py::array>(object))
{
auto a = object.cast<py::array>();
Expand Down Expand Up @@ -2155,12 +2216,6 @@ PYBIND11_MODULE(OpenEXR, m)
.def_readwrite("pixels", &PyPreviewImage::pixels)
;

py::class_<PyDouble>(m, "Double")
.def(py::init<double>())
.def("__repr__", [](const PyDouble& d) { return repr(d.d); })
.def(py::self == py::self)
;

//
// The File API: Channel, Part, and File
//
Expand Down
20 changes: 0 additions & 20 deletions src/wrappers/python/PyOpenEXR.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,26 +200,6 @@ PyPreviewImage::operator==(const PyPreviewImage& other) const
}


//
// PyDouble supports the "double" attribute.
//
// When reading an attribute of type "double", a python object of type
// PyDouble is created, so that when the header is written, it will be
// of type double, since python makes no distinction between float and
// double numerical types.
//

class PyDouble
{
public:
PyDouble(double x) : d(x) {}

bool operator==(const PyDouble& other) const { return d == other.d; }

double d;
};


inline std::ostream&
operator<< (std::ostream& s, const Chromaticities& c)
{
Expand Down
13 changes: 11 additions & 2 deletions src/wrappers/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,25 @@ To modify the header metadata in a file:

with OpenEXR.File("readme.exr") as f:
f.header()["displayWindow"] = OpenEXR.Box2i((3,4),(5,6))
f.header()["displayWindow"] = ((3,4),(5,6))
f.header()["screenWindowCenter"] = np.array([1.0,2.0],'float32')
f.header()["comments"] = "test image"
f.header()["longitude"] = -122.5
f.write("readme_modified.exr")

with OpenEXR.File("readme_modified.exr") as o:
assert o.header()["displayWindow"] == OpenEXR.Box2i((3,4),(5,6))
dw = o.header()["displayWindow"]
assert (tuple(dw[0]), tuple(dw[1])) == ((3,4),(5,6))
swc = o.header()["screenWindowCenter"]
assert tuple(swc) == (1.0, 2.0)
assert o.header()["comments"] == "test image"
assert o.header()["longitude"] == -122.5

Note that OpenEXR's Imath-based vector and matrix attribute values
appear in the header dictionary as 2-element, 3-element, 3x3, 4x4
numpy arrays, and bounding boxes appear as tuples of 2-element arrays,
or tuples for convenience.

To read and write a multi-part file, use a list of ``Part`` objects:

height, width = (20, 10)
Expand Down
2 changes: 1 addition & 1 deletion src/wrappers/python/tests/test_deep.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_read_deep(self):

# OpenEXR.writeDeepExample()
# OpenEXR.readDeepExample()
f = OpenEXR.File("test.deep.exr")
# f = OpenEXR.File("test.deep.exr")
print("ok")

if __name__ == '__main__':
Expand Down
16 changes: 5 additions & 11 deletions src/wrappers/python/tests/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import atexit
import unittest
import numpy as np
import fractions

import OpenEXR

Expand Down Expand Up @@ -223,13 +224,6 @@ def test_keycode(self):
k.perfsPerFrame == perfsPerFrame and
k.perfsPerCount == perfsPerCount)

def test_rational(self):

r = OpenEXR.Rational(1,2)

self.assertEqual(r.n, 1)
self.assertEqual(r.d, 2)

def test_empty_header(self):

# Construct a file from scratch and write it.
Expand Down Expand Up @@ -451,7 +445,7 @@ def test_write_float(self):
header["box2i"] = ((0,1), (2,3))
header["box2f"] = ((0,1), (2,3))
header["compression"] = OpenEXR.ZIPS_COMPRESSION
header["double"] = OpenEXR.Double(42000)
header["double"] = np.array([42000.0], 'float64')
header["float"] = 4.2
header["int"] = 42
header["keycode"] = OpenEXR.KeyCode(0,0,0,0,0,4,64)
Expand All @@ -460,7 +454,7 @@ def test_write_float(self):
header["m33d"] = np.identity(3, 'float64')
header["m44f"] = np.identity(4, 'float32')
header["m44d"] = np.identity(4, 'float64')
header["rational"] = OpenEXR.Rational(1,3)
header["rational"] = fractions.Fraction(1,3)
header["string"] = "stringy"
header["timecode"] = OpenEXR.TimeCode(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18)
header["v2i"] = (1,2)
Expand Down Expand Up @@ -520,7 +514,7 @@ def make_header():
header["box2i"] = ((0,1),(2,3))
header["box2f"] = ((0,1),(2,3))
header["compression"] = OpenEXR.ZIPS_COMPRESSION
header["double"] = OpenEXR.Double(42000)
header["double"] = np.array([42000.0], 'float64')
header["float"] = 4.2
header["int"] = 42
header["keycode"] = OpenEXR.KeyCode(0,0,0,0,0,4,64)
Expand All @@ -530,7 +524,7 @@ def make_header():
header["m44f"] = np.identity(4, 'float32')
header["m44d"] = np.identity(4, 'float64')
header["preview"] = OpenEXR.PreviewImage(P)
header["rational"] = OpenEXR.Rational(1,3)
header["rational"] = fractions.Fraction(1,3)
header["string"] = "stringy"
header["timecode"] = OpenEXR.TimeCode(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18)
header["v2i"] = (1,2)
Expand Down

0 comments on commit 828c39f

Please sign in to comment.