// exrReader.C // Copyright (c) 2009 The Foundry Visionmongers Ltd. All Rights Reserved. /* Reads exr files using libexr. 04/14/03 Initial Release Charles Henrich (henrich@d2.com) 10/14/03 Added channel name conforming Charles Henrich (henrich@d2.com) 10/16/04 Lots of channel changes Bill Spitzak 03/27/05 Single frame buffer Bill Spitzak 01/17/08 all channel sorting done by Nuke spitzak */ #include "DDImage/DDWindows.h" #include "DDImage/Reader.h" #include "DDImage/Row.h" #include "DDImage/Knob.h" #include "DDImage/Knobs.h" #include "DDImage/Thread.h" #include "DDImage/Memory.h" #include "DDImage/LUT.h" #include "DDImage/ImagePlane.h" #ifdef _WIN32 #define OPENEXR_DLL #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #endif #include #ifdef __linux #define EXR_USE_MMAP #endif #ifdef EXR_USE_MMAP #include #include #include #include #include #endif #include #include "exrGeneral.h" // Whether to print the EXR file info to the tty. //#define ENABLE_EXR_INFO_TTY using namespace DD::Image; // This structure is just to access the validchanname() function below: class ChannelName { public: ChannelName(const char* name, int partNum, const std::vector& views) : part(partNum) { setname(name, views); } ~ChannelName() {} void setname(const char* name, const std::vector& views); std::string chan; std::string layer; std::string view; const int part; std::string name() const; }; std::string tolower(const std::string& s) { std::string r = s; for (size_t i = 0; i < r.size(); i++) { r[i] = tolower(r[i]); } return r; } class exrReaderFormat : public ReaderFormat { friend class exrReader; bool _disable_mmap; bool _offset_negative_display_window; bool _doNotAttachPrefix; public: bool disable_mmap() const { return _disable_mmap; } bool offset_negative_display_window() const { return _offset_negative_display_window; } bool doNotAttachPrefix() const { return _doNotAttachPrefix; } exrReaderFormat() { _disable_mmap = false; _offset_negative_display_window = true; _doNotAttachPrefix = false; } void knobs(Knob_Callback c) { Bool_knob(c, &_offset_negative_display_window, "offset_negative_display_window", "offset negative display window"); Tooltip(c, "EXR allows the 'display window' to have lower left corner at any position. " "Nuke's format does not support this, all formats must have lower left corner at 0,0. " "If this file does not have its left edge at x=0 Nuke can either offset the data and " "display windows so that it does, or treat the negative area as overscan and shrink " "the format by that amount all around.
" "If this option is enabled, Nuke will offset the image so that the display window " "left side is at x=0.
" "If it is disabled, the format will be shrunk on both sides by the amount that is " "negative in x as if that area was overscan."); Bool_knob(c, &_disable_mmap, "disable_mmap", "disable use of mmap()"); Tooltip(c, "Some EXR files are compressed such that is is much faster to decompress the entire image at once, rather than decompressing each line individually. Decompressing the image at once may take more memory than is available. This option is provided to disable this."); #ifndef EXR_USE_MMAP SetFlags(c, Knob::INVISIBLE); #endif Bool_knob(c, &_doNotAttachPrefix, "noprefix", "do not attach prefix"); Tooltip(c, "By default the 'exr' prefix is attached to metadata keys to make it distinct from other metadata in the tree. Enable this option to read the metadata 'as is' without attaching the exr prefix."); } void append(Hash& hash) { hash.append(_offset_negative_display_window); hash.append(_doNotAttachPrefix); } }; class exrReader : public Reader { Imf_Foundry::MultiPartInputFile* inputfile; #ifdef _WIN32 /// Special case for Windows to handle files whose names contain Unicode characters. Imf_Foundry::StdIFStream* inputStdStream; std::ifstream* inputStream; #endif Lock C_lock; static Lock sExrLibraryLock; // Store the name and part number in the channel map struct ChannelInfo { ChannelInfo() : name(NULL), part(0) {} ChannelInfo(const char* n, int p) : name(n), part(p) {} bool operator<(const ChannelInfo& rhs) const { // Sort first by part if (part < rhs.part) return true; if (rhs.part < part) return false; // Then by name return strcmp(name, rhs.name) < 0; } const char* name; int part; }; std::map channel_map; bool fileStereo_; std::vector views; std::string heroview; // dataOffset is used to deal with the case where the display window goes // not have left side at 0. Both display and data can be shifted // over or display can be shrunk as if a negative area is overscan. int dataOffset; MetaData::Bundle _meta; #ifdef EXR_USE_MMAP static bool exr_mmap_bad; bool exr_use_mmap; int fd; bool fileLoaded; int pagesize; #endif size_t _stripeHeight; std::map _partSets; public: /** * implementation of function from Reader */ PlanarPreference planarPreference() const { if (getenv("NUKE_EXR_NEVER_PLANAR")) return ePlanarNever; /* "offset negative display window" is active; which the planar case currently can't deal with */ if (dataOffset != 0) { return ePlanarNever; } /* mandate planar if _stripeHeight > 1 (ie we would have to decompress multiple lines at a time anyway */ if (_stripeHeight > 1) return ePlanarAlways; /* * otherwise deny planar access */ return ePlanarNever; } /** * always use stripes for planar access to exrs */ bool useStripes() const { return true; } /** * return the stripeHeight, kept in _stripeHeight */ size_t stripeHeight() const { return _stripeHeight; } const MetaData::Bundle& fetchMetaData(const char* key); exrReader(Read*); ~exrReader(); #ifdef EXR_USE_MMAP bool mmap_engine(const Imath_Foundry::Box2i& datawin, const Imath_Foundry::Box2i& dispwin, const ChannelSet& channels, int exrY, Row& row, int x, int X, int r, int R); #endif /** * convert a y value from Nuke coordinates (0 = bottom) to EXR coordinate (0 = top) */ int convertY(int y) const { const Imath_Foundry::Box2i& dispwin = inputfile->header(0).displayWindow(); return dispwin.max.y - y; } void normal_engine(const Imath_Foundry::Box2i& datawin, const Imath_Foundry::Box2i& dispwin, const ChannelSet& channels, int exrY, Row& row, int x, int X, int r, int R); /** * go over the parts and channels, and figure out which parts are interesting * and which channels want copying into other channels */ void processChannels(const DD::Image::ChannelSet& channels, std::set& partSet, std::map& toCopy); virtual PlanarI::PlaneID getPlaneFromChannel(Channel chan); void fetchPlane(ImagePlane& imagePlane); void engine(int y, int x, int r, ChannelMask, Row &); void _validate(bool for_real); static const Description d; bool supports_stereo() const { return true; } bool fileStereo() const { return fileStereo_; } void lookupChannels(std::set& channel, const char* name) { if (strcmp(name, "y") == 0 || strcmp(name, "Y") == 0) { channel.insert(Chan_Red); if (!iop->raw()) { channel.insert(Chan_Green); channel.insert(Chan_Blue); } } else { channel.insert(Reader::channel(name)); } } bool getChannels(const ChannelName& channelName, int view, std::set& channel) { std::string viewpart = (channelName.view.length() > 0) ? channelName.view : heroview; std::string otherpart = channelName.name(); if (OutputContext::viewname(view) == viewpart) { fileStereo_ = true; lookupChannels(channel, otherpart.c_str()); return true; } if (viewpart != "" && viewpart != heroview) { return false; } lookupChannels(channel, otherpart.c_str()); return true; } #if FN_BUILD_WITH_READER_EXTENSIONS #include "ReaderExtensions/exrReaderExtensions.cpp" #endif }; Lock exrReader::sExrLibraryLock; static Reader* build(Read* iop, int fd, const unsigned char* b, int n) { close(fd); return new exrReader(iop); } static ReaderFormat* buildformat(Read* iop) { return new exrReaderFormat(); } static bool test(int fd, const unsigned char* block, int length) { return block[0] == 0x76 && block[1] == 0x2f && block[2] == 0x31 && block[3] == 0x01; } const Reader::Description exrReader::d("exr\0sxr\0", build, test, buildformat); const MetaData::Bundle& exrReader::fetchMetaData(const char* key) { return _meta; } bool sNoCreateViews = false; static void MakeNeededViews(const std::vector& views) { if (Reader::inPreviewMode()) return; if (sNoCreateViews) return; std::vector viewsToMake; for (size_t i = 0; i < views.size(); i++) if (DD::Image::OutputContext::lookup_view(views[i]) == -1) viewsToMake.push_back(views[i]); if (!viewsToMake.empty()) { std::string buf; for (size_t i = 0; i < viewsToMake.size(); i++) { if (i) if (i == (viewsToMake.size()-1)) buf += " and "; else buf += ", "; buf += viewsToMake[i].c_str(); } if (DD::Image::Op::message_f('?', viewsToMake.size() > 1 ? "EXR has views %s, which do not exist. Create them?" : "EXR has view %s, which does not exist. Create it?", buf.c_str())) for (size_t i = 0; i < viewsToMake.size(); i++) DD::Image::OutputContext::create_view(viewsToMake[i]); else sNoCreateViews = true; } } #ifdef EXR_USE_MMAP bool exrReader::exr_mmap_bad = false; #endif #ifdef _WIN32 class WideCharWrapper { public: WideCharWrapper(const char* str) : _array(0) { int length = str ? static_cast(strlen(str)) : 0; int size = MultiByteToWideChar(CP_UTF8, 0, str, length, NULL, 0); _array = new wchar_t[size+1]; MultiByteToWideChar(CP_UTF8, 0, str, length,_array, size); _array[size] = 0; } operator const unsigned short* () const { return reinterpret_cast(_array); } ~WideCharWrapper() { delete [] _array; } private: wchar_t* _array; }; #endif exrReader::exrReader(Read* r) : Reader(r), inputfile(0), fileStereo_(false), dataOffset(0) { // Make it default to linear colorspace: lut_ = LUT::getLut(LUT::FLOAT); #ifdef EXR_USE_MMAP pagesize = sysconf(_SC_PAGE_SIZE) / sizeof(float); fileLoaded = false; exr_use_mmap = false; fd = -1; #endif std::map pixelTypes; bool doNotAttachPrefix = false; _stripeHeight = 1; try { #ifdef _WIN32 ////////////////////////////////////////////////////////////////////////// /// On Windows OpenEXR API is not working well with Utf-8 encoded strings. /// We must convert Utf-8 encoded string to wstring and pass that to ifstream. ////////////////////////////////////////////////////////////////////////// inputStream = new std::ifstream( WideCharWrapper(r->filename()), std::ios_base::binary); inputStdStream = new Imf_Foundry::StdIFStream(*inputStream, r->filename()); inputfile = new Imf_Foundry::MultiPartInputFile(*inputStdStream); #else inputfile = new Imf_Foundry::MultiPartInputFile(r->filename()); #endif int view = r->view_for_reader(); exrReaderFormat* trf = dynamic_cast(r->handler()); bool offsetNegativeDisplayWindow = trf && trf->offset_negative_display_window(); doNotAttachPrefix = trf && trf->doNotAttachPrefix(); ChannelSet mask; const bool isMultipart = inputfile->parts() > 1; #ifdef EXR_USE_MMAP Imf_Foundry::Compression compression = inputfile->header(0).compression(); exr_use_mmap = (compression == Imf_Foundry::PIZ_COMPRESSION); if (trf) { exr_use_mmap = exr_use_mmap && !trf->disable_mmap(); } else { exr_use_mmap = false; } if (exr_mmap_bad) { exr_use_mmap = false; } // might also want to try: // exr_use_mmap = inputfile->header(0).hasTileDescription(); // but we need a test file where these are different #endif const Imath_Foundry::Box2i& datawin = inputfile->header(0).dataWindow(); const Imath_Foundry::Box2i& dispwin = inputfile->header(0).displayWindow(); double aspect = inputfile->header(0).pixelAspectRatio(); Imath_Foundry::Box2i formatwin(dispwin); // Nuke requires format to start at 0,0 but EXR does not. User has a // choice to shift both the data and display windows over by the negative // amount so that they still line up but are offset into positive are // OR treat that negative display window area as overscan and subtract it // from both sides. formatwin.min.x = 0; formatwin.min.y = 0; if (dispwin.min.x != 0) { if ( !offsetNegativeDisplayWindow && (dispwin.min.x < 0) ) { // Leave data where it is and shrink the format by the negative // amount on both sides so that it starts at (0,0) as nuke requires. formatwin.max.x = dispwin.max.x - (-dispwin.min.x); } else { // Shift both to get dispwindow over to 0,0. dataOffset = -dispwin.min.x; formatwin.max.x = dispwin.max.x + dataOffset; } } // Can't do the same for y -- EXR origin is upper left, Nuke's is lower left. // Leave data where it is. Put displaywin.max.y at y=0 in Nuke and let it // extend as high as it goes. formatwin.max.y = dispwin.max.y - dispwin.min.y; /* '0' is obviously an invalid aspect ratio, but we also can't trust values of '1'. Internal images were previously always created with this value (it was hard-coded into the writer) so we can't trust that a '1' really means '1'. */ if (aspect == 1.0) aspect = 0; // Note that this sets both the format and the bbox to // 0,0 - width,height. We need to reset the correct bbox afterwards. set_info(formatwin.size().x + 1, formatwin.size().y + 1, 4, aspect); // Remember that exr boxes start at top left, Nuke's at bottom left // so we need to flip the bbox relative to the frame. int bx = datawin.min.x + dataOffset; int by = dispwin.max.y - datawin.max.y; int br = datawin.max.x + dataOffset; int bt = dispwin.max.y - datawin.min.y; // Add a black pixel around the edge of the box, to match what other // programs do with exr files, rather than replicate the edge pixels as // Nuke does by default. However this is not done if the bounding box // matches the display window, so that blurring an image that fills the // frame does not leak black in. if (datawin.min.x != dispwin.min.x || datawin.max.x != dispwin.max.x || datawin.min.y != dispwin.min.y || datawin.max.y != dispwin.max.y) { bx--; by--; br++; bt++; info_.black_outside(true); } info_.set(bx, by, br+1, bt+1); if (inputfile->header(0).lineOrder() == Imf_Foundry::INCREASING_Y) info_.ydirection(-1); else info_.ydirection(1); if (inputfile->header(0).compression() == Imf_Foundry::ZIP_COMPRESSION) _stripeHeight = 16; if (inputfile->header(0).compression() == Imf_Foundry::PIZ_COMPRESSION) _stripeHeight = 32; // Iterate through each part for (int n = 0; n < inputfile->parts(); ++n) { const Imath_Foundry::Box2i& datawin = inputfile->header(n).dataWindow(); const Imath_Foundry::Box2i& dispwin = inputfile->header(n).displayWindow(); double aspect = inputfile->header(n).pixelAspectRatio(); // Check these windows against the first header if (datawin != inputfile->header(0).dataWindow()) { throw std::runtime_error("Multipart data window size must be consistent"); } if (dispwin != inputfile->header(0).displayWindow()) { throw std::runtime_error("Multipart display window size must be consistent"); } if (aspect != inputfile->header(0).pixelAspectRatio()) { throw std::runtime_error("Multipart pixel aspect ratio must be consistent"); } if (inputfile->header(n).lineOrder() != inputfile->header(0).lineOrder()) { throw std::runtime_error("Multipart line order must be consistent"); } const Imf_Foundry::StringAttribute* stringMultiView = 0; const Imf_Foundry::StringVectorAttribute* vectorMultiView = 0; // if (inputfile->header(n).hasTileDescription()) { // const Imf_Foundry::TileDescription& t = inputfile->header(n).tileDescription(); // printf("%s Tile Description:\n", filename()); // printf(" %d %d mode %d rounding %d\n", t.xSize, t.ySize, t.mode, t.roundingMode); // } // should change this to use header->findTypedAttribute(name) // rather than the exception mechanism as it is nicer code and // less of it. But I'm too scared to do this at the moment. try { vectorMultiView = inputfile->header(n).findTypedAttribute("multiView"); if (!vectorMultiView) { Imf_Foundry::Header::ConstIterator it = inputfile->header(n).find("multiView"); if (it != inputfile->header(n).end() && !strcmp(it.attribute().typeName(), "stringvector")) vectorMultiView = static_cast(&it.attribute()); } stringMultiView = inputfile->header(n).findTypedAttribute("multiView"); } catch (...) { } if (vectorMultiView) { std::vector s = vectorMultiView->value(); bool setHero = false; for (size_t i = 0; i < s.size(); i++) { if (s[i].length()) { views.push_back(s[i]); if (!setHero) { heroview = s[i]; setHero = true; } } } } std::string channelPrefix; // For multi-part files if (isMultipart) { // For multi-part files we need to prepend the part name to the channel names. // The ChannelName constructor will parse the combined name for us. mFnAssertMsg(inputfile->header(n).hasName(), "name is mandatory in multi-part files"); if (!inputfile->header(n).name().empty()) { channelPrefix += inputfile->header(n).name(); channelPrefix += '.'; } if (inputfile->header(n).hasView()) { std::string strView = inputfile->header(n).view(); // Add to the views list if necessary if ( std::find(views.begin(), views.end(), strView) == views.end() ) { views.push_back(strView); } // Set the hero view if necessary if (heroview.empty()) heroview = strView; // We expect the separated view name to be the last part of the part name. // If not then append the view name. std::string viewSuffix; viewSuffix += '.'; viewSuffix += strView; if ( channelPrefix.rfind(viewSuffix) != (channelPrefix.size() - viewSuffix.size() - 1) ) { channelPrefix += strView; channelPrefix += '.'; } } } // For each channel in the file, create or locate the matching Nuke channel // number, and store it in the channel_map const Imf_Foundry::ChannelList& imfchannels = inputfile->header(n).channels(); Imf_Foundry::ChannelList::ConstIterator chan; for (chan = imfchannels.begin(); chan != imfchannels.end(); chan++) { pixelTypes[chan.channel().type]++; std::string channelname = channelPrefix + chan.name(); ChannelName cName(channelname.c_str(), n, views); std::set channels; if (this->getChannels(cName, view, channels)) { if (!channels.empty()) { for (std::set::iterator it = channels.begin(); it != channels.end(); it++) { Channel channel = *it; // Note: the below special cases are here because we are trying to do very clever mapping of EXR channels // into NUKE channels. Sometimes this cleverness doesn't work and we get unwanted results. bool writeChannelMapping = true; if (channel_map.find(channel) != channel_map.end()) { mFnAssertMsg(channel_map[channel].name, "All channels in exr files must have names"); const int existingLength = strlen(channel_map[channel].name); const int newLength = strlen(chan.name()); const bool existingChannelHasEmptyLayerName = (existingLength > 0) && channel_map[channel].name[0] == '.'; if (existingChannelHasEmptyLayerName && existingLength == (newLength + 1)) { // Bug 31548 - Read - EXR rendering black for R, G and B channels // "channel" should override ".channel" writeChannelMapping = true; } else if (existingLength > newLength) { // Bug 23181 - Specific named channel are not read correctly for both stereo views // the loop is in alphabetical order, so check length // to avoid replacing "right.rotopaint.blue" with // the less specific but alphabetically later // "rotopaint.blue" writeChannelMapping = false; } } if (writeChannelMapping) { channel_map[channel] = ChannelInfo(chan.name(), n); } mask += channel; } } else { iop->warning("Cannot assign channel number to %s", cName.name().c_str()); } } } #ifdef ENABLE_EXR_INFO_TTY { const Imf_Foundry::Header& header = inputfile->header(n); std::cout << "-------------------------------- EXR info --------------------------------" << std::endl; std::cout << "File: " << filename() << std::endl; std::cout << "Tiled: " << (header.hasTileDescription() ? "yes" : "no") << std::endl; std::cout << "Has preview: " << (header.hasPreviewImage() ? "yes" : "no") << std::endl; std::string lineOrder; switch (header.lineOrder()) { case Imf_Foundry::INCREASING_Y: lineOrder = "INCREASING_Y"; break; case Imf_Foundry::DECREASING_Y: lineOrder = "DECREASING_Y"; break; case Imf_Foundry::RANDOM_Y: lineOrder = "RANDOM_Y"; break; default: lineOrder = "***INVALID***"; break; } std::cout << "Line order: " << lineOrder << std::endl; std::string compression; switch (header.compression()) { case Imf_Foundry::NO_COMPRESSION: compression = "NO_COMPRESSION"; break; case Imf_Foundry::RLE_COMPRESSION: compression = "RLE_COMPRESSION"; break; case Imf_Foundry::ZIPS_COMPRESSION: compression = "ZIPS_COMPRESSION"; break; case Imf_Foundry::ZIP_COMPRESSION: compression = "ZIP_COMPRESSION"; break; case Imf_Foundry::PIZ_COMPRESSION: compression = "PIZ_COMPRESSION"; break; case Imf_Foundry::PXR24_COMPRESSION: compression = "PXR24_COMPRESSION"; break; case Imf_Foundry::B44_COMPRESSION: compression = "B44_COMPRESSION"; break; case Imf_Foundry::B44A_COMPRESSION: compression = "B44A_COMPRESSION"; break; default: compression = "***INVALID***"; break; } std::cout << "Compression: " << compression << std::endl; std::cout << "Channels:" << std::endl; const Imf_Foundry::ChannelList& channelList = header.channels(); for (Imf_Foundry::ChannelList::ConstIterator chanIter = channelList.begin(); chanIter != channelList.end(); chanIter++) { std::string channelType; switch (chanIter.channel().type) { case Imf_Foundry::UINT: channelType = "UINT"; break; case Imf_Foundry::HALF: channelType = "HALF"; break; case Imf_Foundry::FLOAT: channelType = "FLOAT"; break; default: channelType = "***INVALID***"; break; } std::cout << " " << chanIter.name() << " : " << channelType << std::endl; } const Imath_Foundry::Box2i& dataWindow = header.dataWindow(); const Imath_Foundry::Box2i& displayWindow = header.displayWindow(); std::cout << "Data window : " << dataWindow.min.x << ", " << dataWindow.min.y << " -> " << dataWindow.max.x << ", " << dataWindow.max.y << std::endl; std::cout << "Display window: " << displayWindow.min.x << ", " << displayWindow.min.y << " -> " << displayWindow.max.x << ", " << displayWindow.max.y << std::endl; std::cout << "Derived:" << std::endl; std::cout << "Bounding box : " << bx << ", " << by << " -> " << br << ", " << bt << " (actual pixels)" << std::endl; std::cout << "Format : " << formatwin.min.x << ", " << formatwin.min.y << " -> " << formatwin.max.x << ", " << formatwin.max.y << " (atcual pixels)" << std::endl; std::cout << "--------------------------------------------------------------------------" << std::endl; } #endif // ENABLE_EXR_INFO_TTY } // for each part // Finally set the channels info_.channels(mask); } catch (const Iex_Foundry::BaseExc& exc) { iop->error(exc.what()); #ifdef EXR_USE_MMAP if (fd != -1) { close(fd); } #endif delete inputfile; #ifdef ENABLE_EXR_INFO_TTY std::cout << "-------------------------------- EXR info --------------------------------" << std::endl; std::cout << "File: " << filename() << std::endl; std::cout << "Error parsing header: " << exc.what() << std::endl; std::cout << "--------------------------------------------------------------------------" << std::endl; #endif // ENABLE_EXR_INFO_TTY return; } catch (const std::exception& exc) { iop->error(exc.what()); #ifdef EXR_USE_MMAP if (fd != -1) { close(fd); } #endif delete inputfile; #ifdef ENABLE_EXR_INFO_TTY std::cout << "-------------------------------- EXR info --------------------------------" << std::endl; std::cout << "File: " << filename() << std::endl; std::cout << "Error parsing header: " << exc.what() << std::endl; std::cout << "--------------------------------------------------------------------------" << std::endl; #endif // ENABLE_EXR_INFO_TTY return; } // Metadata comes from the first part const Imf_Foundry::Header& header = inputfile->header(0); if (pixelTypes[Imf_Foundry::FLOAT] > 0) { _meta.setData(MetaData::DEPTH, MetaData::DEPTH_FLOAT); } else if (pixelTypes[Imf_Foundry::UINT] > 0) { _meta.setData(MetaData::DEPTH, MetaData::DEPTH_32); } if (pixelTypes[Imf_Foundry::HALF] > 0) { _meta.setData(MetaData::DEPTH, MetaData::DEPTH_HALF); } MakeNeededViews(views); exrHeaderToMetadata( header, _meta, doNotAttachPrefix ); foreach(z, info_.channels()) _partSets[channel_map[z].part] += z; } exrReader::~exrReader() { #ifdef EXR_USE_MMAP if (fd != -1) { close(fd); } #endif delete inputfile; #ifdef _WIN32 delete inputStream; delete inputStdStream; #endif } void exrReader::_validate(bool for_real) { } #ifdef EXR_USE_MMAP bool exrReader::mmap_engine(const Imath_Foundry::Box2i& datawin, const Imath_Foundry::Box2i& dispwin, const ChannelSet& channels, int exrY, Row& row, int x, int X, int r, int R) { int exrwd = datawin.max.x - datawin.min.x + 1; int exrht = datawin.max.y - datawin.min.y + 1; int effwd = exrwd; if ((effwd % pagesize) != 0) { effwd = ((effwd / pagesize) + 1) * pagesize; } // Record all the parts and channels std::set partSet; std::map fileChans; // Calculate all valid channels foreach(z, info_.channels()) { // Generate each channel once (avoiding channel duplication) if (fileChans.find(channel_map[z]) == fileChans.end()) { int newNo = fileChans.size(); fileChans[channel_map[z]] = newNo; // Insert a new part partSet.insert(channel_map[z].part); } } if (!fileLoaded) { Guard guard(C_lock); if (!fileLoaded) { Guard guard2(sExrLibraryLock); char* s = getenv("NUKE_EXR_TEMP_DIR"); if (!s) s = getenv("NUKE_TEMP_DIR"); char fn[1024]; mkdir(s, 0700); sprintf(fn, "%s/exr-temporary-XXXXXX", s); mktemp(fn); fd = ::open(fn, O_RDWR | O_CREAT, 0700); unlink(fn); if (fd == -1) { return false; } std::vector buffers; unsigned long long planesize = effwd * exrht * sizeof(float); unsigned long long filesize = planesize * info_.channels().size(); ::ftruncate(fd, filesize); unsigned c = 0; // Iterate through the parts (and frame buffers) std::set::const_iterator it = partSet.begin(); const std::set::const_iterator end = partSet.end(); for( ; it != end; ++it) { int part = *it; // Create a framebuffer for this part Imf_Foundry::FrameBuffer fbuf; foreach(z, info_.channels()) { // Filter to the current part if (channel_map[z].part != part) continue; // This channel is in the current part if (fileChans.find(channel_map[z]) != fileChans.end()) { c = fileChans[channel_map[z]]; } else { c = 0; } float* dest = (float*)mmap(NULL, planesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, planesize * c); if (dest == (float*)-1) { close(fd); return false; } buffers.push_back(dest); fbuf.insert(channel_map[z].name, Imf_Foundry::Slice(Imf_Foundry::FLOAT, (char*)(dest - datawin.min.x - effwd * datawin.min.y), sizeof(float), sizeof(float) * effwd)); } try { if (iop->aborted()) return true; // abort if another thread does so Imf_Foundry::InputPart inputPart(*inputfile, part); inputPart.setFrameBuffer(fbuf); inputPart.readPixels(datawin.min.y, datawin.max.y); } catch (const std::exception& exc) { iop->error(exc.what()); return true; } ; c = 0; foreach(z, info_.channels()) { munmap(buffers[c], effwd * exrht * sizeof(float)); c++; } } fileLoaded = true; } } foreach(z, info_.channels()) { if (channels & z) { int source_chan = 0; if (fileChans.find(channel_map[z]) != fileChans.end()) { source_chan = fileChans[channel_map[z]]; } else { continue; } size_t offset = sizeof(float) * (source_chan * effwd * exrht + (exrY - datawin.min.y) * effwd); size_t rowsize = exrwd * sizeof(float); struct stat s; fstat(fd, &s); const float* src = (const float*)mmap(NULL, rowsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset); if (src == (float*)-1) { iop->error("EXR reader failed."); return true; } float* dest = row.writable(z); for (int xx = x; xx < X; xx++) dest[xx] = 0; for (int xx = R; xx < r; xx++) dest[xx] = 0; memcpy(dest + X, src + (X - (datawin.min.x + dataOffset)), (R - X) * sizeof(float)); munmap((void*)src, rowsize); } } return true; } #endif void exrReader::normal_engine(const Imath_Foundry::Box2i& datawin, const Imath_Foundry::Box2i& dispwin, const ChannelSet& channels, int exrY, Row& row, int x, int X, int r, int R) { row.range(this->x(), this->r()); // Record all the parts and channels std::set partSet; std::map toCopy; processChannels(channels, partSet, toCopy); // Iterate through the parts (and frame buffers) std::set::const_iterator it = partSet.begin(); const std::set::const_iterator end = partSet.end(); for( ; it != end; ++it) { int part = *it; // Create a framebuffer for this part Imf_Foundry::FrameBuffer fbuf; foreach (z, channels) { // Filter to the current part if (channel_map[z].part != part) continue; // Skip duplicate channels if (toCopy.find(z) != toCopy.end()) { continue; } // Place the row in the respective framebuffer float* dest = row.writable(z); for (int xx = x; xx < X; xx++) dest[xx] = 0; for (int xx = R; xx < r; xx++) dest[xx] = 0; fbuf.insert(channel_map[z].name, Imf_Foundry::Slice(Imf_Foundry::FLOAT, (char*)(dest + dataOffset), sizeof(float), 0)); } { // setFrameBuffer uses shared state // TODO: We will use all available threads to perform this read Guard guard(C_lock); try { if (iop->aborted()) return; // abort if another thread does so Imf_Foundry::InputPart inputPart(*inputfile, part); inputPart.setFrameBuffer(fbuf); inputPart.readPixels(exrY); } catch (const std::exception& exc) { iop->error(exc.what()); return; } } } // Copy the duplicated channels foreach (z, channels) { if (toCopy.find(z) != toCopy.end()) { float* dest = row.writable(z); const float* src = row[toCopy[z]]; for (int col = x; col < r; col++) { dest[col] = src[col]; } } } } void exrReader::engine(int y, int x, int r, ChannelMask c1, Row& row) { // Need to find the part containing this channel const Imath_Foundry::Box2i& dispwin = inputfile->header(0).displayWindow(); const Imath_Foundry::Box2i& datawin = inputfile->header(0).dataWindow(); // Invert to EXR y coordinate: int exrY = convertY(y); // Figure out intersection of x,r with the data in exr file: const int X = MAX(x, datawin.min.x + dataOffset); const int R = MIN(r, datawin.max.x + dataOffset + 1); // Black outside the box: if (exrY < datawin.min.y || exrY > datawin.max.y || R <= X) { row.erase(c1); return; } ChannelSet channels(c1); if (premult() && !lut()->linear() && (channels & Mask_RGB) && (this->channels() & Mask_Alpha)) channels += (Mask_Alpha); #ifdef EXR_USE_MMAP if (exr_use_mmap) { if (!mmap_engine(datawin, dispwin, channels, exrY, row, x, X, r, R)) { exr_use_mmap = false; exr_mmap_bad = true; normal_engine(datawin, dispwin, channels, exrY, row, x, X, r, R); } } else { #endif normal_engine(datawin, dispwin, channels, exrY, row, x, X, r, R); #ifdef EXR_USE_MMAP } #endif // Do colorspace conversion, now that we have the alpha for premultiplied: if (!iop->aborted() && !lut()->linear()) { const float* alpha = (channels & Mask_Alpha) ? row[Chan_Alpha] + X : 0; for (Channel chan = Chan_Red; chan <= Chan_Blue; chan = Channel(chan + 1)) { if (intersect(channels, chan)) { const float* src = row[chan] + X; float* dest = row.writable(chan) + X; from_float(chan, dest, src, alpha, R - X); } } } } void exrReader::processChannels(const DD::Image::ChannelSet& channels, std::set& partSet, std::map& toCopy) { // Record all the parts and channels std::map usedChans; // Determine the channels to generate and copy foreach (z, channels) { if (channel_map.count(z) == 0) continue; // Simply copy duplicate channels if (usedChans.find(channel_map[z]) != usedChans.end()) { toCopy[z] = usedChans[channel_map[z]]; continue; } // Record the channel and part usedChans[channel_map[z]] = z; partSet.insert(channel_map[z].part); } } PlanarI::PlaneID exrReader::getPlaneFromChannel(Channel chan) { return _partSets[channel_map[chan].part]; } void exrReader::fetchPlane(ImagePlane& imagePlane) { const int y = imagePlane.bounds().y(); const int lines = imagePlane.bounds().h(); imagePlane.makeWritable(); const ChannelSet& channels = imagePlane.channels(); foreach(z, channels) imagePlane.fillChannel(z, 0.0); const Imath_Foundry::Box2i& datawin = inputfile->header(0).dataWindow(); const Imath_Foundry::Box2i& dispwin = inputfile->header(0).displayWindow(); mFnAssertMsg(datawin.min.x >= imagePlane.bounds().x(), "Planar Interface: Pixels buffer of ImagePlane is not big enough for pixels from the EXR source"); mFnAssertMsg(datawin.max.x < imagePlane.bounds().r(), "Planar Interface: Pixels buffer of ImagePlane is not big enough for pixels from the EXR source"); int exrBottom = convertY(y); int exrTop = convertY(y + lines - 1); exrBottom = std::min(exrBottom, datawin.max.y); exrTop = std::max(exrTop, datawin.min.y); if (exrTop > exrBottom) return; std::set partSet; std::map toCopy; processChannels(channels, partSet, toCopy); // Iterate through the parts (and frame buffers) std::set::const_iterator it = partSet.begin(); const std::set::const_iterator end = partSet.end(); for( ; it != end; ++it) { const int part = *it; const int rowStride = imagePlane.rowStride() * sizeof(float); float* dest = imagePlane.writableAt(convertY(dispwin.min.y), 0).ptr(); // Create a framebuffer for this part Imf_Foundry::FrameBuffer fbuf; foreach (z, channels) { if (channel_map.count(z) == 0) continue; // Filter to the current part if (channel_map[z].part != part) continue; // Skip duplicate channels if (toCopy.find(z) != toCopy.end()) { continue; } if (channel_map.find(z) != channel_map.end()) { int chanNo = imagePlane.chanNo(z); // Place the row in the respective framebuffer float* channelDest = (dest + chanNo * imagePlane.chanStride()); fbuf.insert(channel_map[z].name, Imf_Foundry::Slice(Imf_Foundry::FLOAT, (char*)channelDest, sizeof(float), -rowStride)); } } // setFrameBuffer uses shared state Guard guard(C_lock); try { if (iop->aborted()) return; // abort if another thread does so Imf_Foundry::InputPart inputPart(*inputfile, part); inputPart.setFrameBuffer(fbuf); inputPart.readPixels(exrTop, exrBottom); } catch (const std::exception& exc) { iop->error(exc.what()); return; } } // Copy the duplicated channels foreach (z, channels) { if (toCopy.find(z) != toCopy.end()) { imagePlane.copyChannel(z, toCopy[z]); } } if (!iop->aborted() && !lut()->linear()) { const float* alpha = (channels & Mask_Alpha) ? &imagePlane.at(imagePlane.bounds().x(), imagePlane.bounds().y(), Chan_Alpha) : 0; for (Channel chan = Chan_Red; chan <= Chan_Blue; chan = Channel(chan + 1)) { if (intersect(channels, chan)) { float* pix = &imagePlane.writableAt(imagePlane.bounds().x(), imagePlane.bounds().y(), imagePlane.chanNo(chan)); from_float(chan, pix, pix, alpha, imagePlane.bounds().area(), imagePlane.colStride()); } } } } std::string removedigitsfromfront(const std::string& str) { std::string ret = ""; size_t len = str.length(); size_t i = 0; while (isdigit(str[i]) && (i < len)) i++; for (; i < len; i++ ) ret += (str[i]); return ret; } std::string removeNonAlphaCharacters(const std::string& str) { std::string ret = ""; size_t len = str.length(); for ( size_t i = 0; i < len; i++ ){ if (!isalnum(str[i])) ret += '_'; else ret += str[i]; } return ret; } /*! Split a string into parts separated by a period. Only get up to three parts. The last part is just assumed to be one whole string */ std::vector split(const std::string& str, char splitChar) { std::vector ret; size_t i = str.find(splitChar); size_t offset = 0; while ( i != str.npos ){ ret.push_back(str.substr(offset, i - offset)); offset = i + 1; i = str.find(splitChar, offset); // stop once we've found two options if ( ret.size() == 2 ) break; } ret.push_back(str.substr(offset)); return ret; } bool IsView(const std::string& name, const std::vector& views) { for ( size_t i = 0; i < views.size(); i++ ){ if ( views[i] == name ){ return true; } } return false; } /*! Convert the channel name from the exr file into a nuke name. Currently this does: - splits the word at each period - Deletes all digits at the start and after each period. - Changes all non-alphanumeric characters into underscores. - ignores empty parts between periods - appends all but the last with underscores into a layer name - the last word is the channel name. - Changes all variations of rgba into "red", "green", "blue", "alpha" - Changes layer "Ci" to the main layer */ void validchanname(const char* channelname, std::string& chan, std::string& layer, std::string& view, const std::vector& views) { chan.clear(); layer.clear(); view.clear(); // split std::vector splits = split(channelname, '.'); // remove digits from the front, and remove empty strings std::vector newsplits; for ( size_t i = 0; i < splits.size(); i++ ){ std::string s = removedigitsfromfront(splits[i]); if ( s.length() > 0 ) newsplits.push_back(removeNonAlphaCharacters(s)); } // get the names out if ( newsplits.size() > 1 ){ // old nuke screwed this up, so we just test which thing is which and assign as appropriate for (size_t i = 0 ; i < (newsplits.size() - 1); i++) { if (IsView(newsplits[i], views)) { view = newsplits[i]; } else { if (!layer.empty()) layer += "_"; layer += newsplits[i]; } } chan = newsplits.back(); } else { chan = newsplits[0]; } //Ci is the primary layer in prman renders. if (layer == "Ci") layer.clear(); if (chan.empty()) chan = "unnamed"; else if (chan == "R" || chan == "r" || chan == "Red" || chan == "RED") chan = "red"; else if (chan == "G" || chan == "g" || chan == "Green" || chan == "GREEN") chan = "green"; else if (chan == "B" || chan == "b" || chan == "Blue" || chan == "BLUE") chan = "blue"; else if (chan == "A" || chan == "a" || chan == "Alpha" || chan == "ALPHA") chan = "alpha"; //std::cout << "Turned '"<& views) { validchanname(name, chan, layer, view, views); } std::string ChannelName::name() const { if (!layer.empty()) return layer + "." + chan; return chan; }