// 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/DDMath.h" #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" #include "DDImage/TimelineRead.h" #include "DDImage/Application.h" #ifdef _WIN32 #define OPENEXR_DLL #include #endif #include #include #include #include #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" #include "ExrChannelNameToNuke.h" // Whether to print the EXR file info to the tty. //#define ENABLE_EXR_INFO_TTY enum EdgeMode { eEdgeMode_Plate, eEdgeMode_Edge, eEdgeMode_Repeat, eEdgeMode_Black, kNumEdgeModes }; static const char* kEdgeModeLabels[kNumEdgeModes+1] = { "plate detect", "edge detect", "repeat", "black", NULL }; using namespace DD::Image; // This structure stores the channel, layer and view determined from an exr // channel name. class ChannelName { public: // Default constructor for std::map ChannelName() {} // Construct from prefixed exr channel name ChannelName(const char* name, const std::vector& views) { setFromPrefixedExrName(name, views); } /*! Convert the channel name from the exr file into a nuke name. */ void setFromPrefixedExrName(const char* channelname, const std::vector& views); std::string nukeChannelName() const; std::string view() const { return _view; } bool hasLayer() const { return !_layer.empty(); } bool hasView() const { return !_view.empty(); } private: std::string _chan; std::string _layer; std::string _view; }; // CompressedScanline: used for storing and decompressing raw scan lines read from the exr file. // Used when _stripeHeight is 1 (scan line rather than planar reads). struct CompressedScanline { // The maximum size for this compressed scan line, as read from the exr header. size_t _maxSizeInBytes; // Buffer to use for storing the compressed scan line, and the size of the buffer/compressed scan line. char *_dataBuffer; int _dataSize; // Compressor to use for decompressing this scan line. Imf_Foundry::Compressor *_decompressor; // The compression format that was used to decompress the scan line, as read from the exr header. Imf_Foundry::Compressor::Format _format; // A pointer to the uncompressed data for this scanline, and the size of the scan line after uncompression. // (The memory this points to is not owned by the scanline but will be allocated and destroyed by the compressor.) const char *_uncompressedDataPtr; int _uncompressedSize; CompressedScanline() : _maxSizeInBytes(0) , _dataBuffer(0) , _dataSize(0) , _decompressor(0) , _format(Imf_Foundry::Compressor::XDR) , _uncompressedDataPtr(0) , _uncompressedSize(0) { mFnAssert("CompressedScanline constructed without an exr header or maximum data size."); } CompressedScanline(size_t maxSizeInBytes, const Imf_Foundry::Header &hdr) : _maxSizeInBytes(maxSizeInBytes) , _dataSize(0) , _format(Imf_Foundry::Compressor::XDR) , _uncompressedDataPtr(0) , _uncompressedSize(0) { _decompressor = Imf_Foundry::newCompressor(hdr.compression(), _maxSizeInBytes, hdr); _dataBuffer = new char[_maxSizeInBytes]; } ~CompressedScanline() { delete _decompressor; if (_dataBuffer) delete [] _dataBuffer; } // Uncompress this scan line. A pointer to the uncompressed data will // be stored in _uncompressedDataPtr, and its size in _uncompressedSize. int uncompress(int uncompressedSize, int exrY) { if (!_dataBuffer || _dataSize == 0) return 0; if (_dataSize < uncompressedSize) { _uncompressedSize = _decompressor->uncompress(_dataBuffer, _dataSize, exrY, _uncompressedDataPtr); _format = _decompressor->format(); } else { _uncompressedDataPtr = _dataBuffer; _uncompressedSize = _dataSize; _format = Imf_Foundry::Compressor::XDR; } return _uncompressedSize; } // Get the compression format for this scan line. const Imf_Foundry::Compressor::Format & format() const { return _format; } }; // CompressedScanlineBuffer: used for storing and decompressing multiple exr scan lines in parallel // when the exr format is ZIPS_COMPRESSION (ZIP-compressed scan lines). class CompressedScanlineBuffer { public: CompressedScanlineBuffer(const Imf_Foundry::Header &hdr) : _header(&hdr) , _compressedScanlines() , _lineSizeInBytes(0) { // Read the size of each line in the file from the header and store in _lineSizeInBytes. // Also store the maximum line size in bytes; this is the size of buffer we will allocate // for storing each scanline (so that the buffers can be reused rather than allocated for // each new scan line). _maxSizeInBytes = Imf_Foundry::bytesPerLineTable(*_header, _lineSizeInBytes); } ~CompressedScanlineBuffer() { clearAll(); } // Read a raw scanline from the input part and store in the CompressedScanlineBuffer. CompressedScanline *readRawScanlineFromFile(const Imf_Foundry::InputPart &inputPart, int exrY); // Copy the raw scan line previously stored by this thread into the frame buffer, uncompressing it first if necessary. bool copyScanlineToFrameBuffer(CompressedScanline *scanlinePtr, const Imf_Foundry::FrameBuffer &frameBuffer, int exrY); private: // The EXR header for the file these scan lines will be read from. const Imf_Foundry::Header *_header; // A std::map that will contain one CompressedScanline for each process ID that asks for one. std::map _compressedScanlines; // The size in bytes of each scan line in the exr file, indexed by y - minY. std::vector _lineSizeInBytes; // The maximum size in bytes of a compressed scan line from the exr file. size_t _maxSizeInBytes; // Delete all scan lines in the buffer. void clearAll(); // Get a pointer to the scan line for thread t. CompressedScanline *getScanline(my_thread_id_type t); }; void CompressedScanlineBuffer::clearAll() { const std::map::iterator endIt = _compressedScanlines.end(); for (std::map::iterator it = _compressedScanlines.begin(); it != endIt; it++) { delete it->second; it->second = NULL; } _compressedScanlines.clear(); } // Get a pointer to the scan line for thread t. If this thread hasn't asked for a scan line // before, this will allocate a new one. CompressedScanline *CompressedScanlineBuffer::getScanline(my_thread_id_type t) { std::map::iterator it = _compressedScanlines.find(t); if (it != _compressedScanlines.end()) return it->second; else { CompressedScanline *scanline = new CompressedScanline(_maxSizeInBytes, *_header); _compressedScanlines.insert(std::make_pair(t, scanline)); return scanline; } } // Read a raw scan line from the file, store it in the CompressedScanlineBuffer and return a // pointer to it. CompressedScanline *CompressedScanlineBuffer::readRawScanlineFromFile(const Imf_Foundry::InputPart &inputPart, int exrY) { const my_thread_id_type id = getThreadID(); CompressedScanline *scanlinePtr = getScanline(id); inputPart.rawPixelDataToBuffer(exrY, scanlinePtr->_dataBuffer, scanlinePtr->_dataSize); return scanlinePtr; } // copyScanlineToFrameBuffer: copy scan line into the frame buffer, uncompressing it if necessary. bool CompressedScanlineBuffer::copyScanlineToFrameBuffer(CompressedScanline *scanlinePtr, const Imf_Foundry::FrameBuffer &frameBuffer, int exrY) { const IMATH_NAMESPACE::Box2i &dataWindow = _header->dataWindow(); const int minX = dataWindow.min.x; const int maxX = dataWindow.max.x; const int minY = dataWindow.min.y; scanlinePtr->uncompress((int)_lineSizeInBytes[exrY - minY], exrY); // // Convert one scan line's worth of pixel data back // from the machine-independent representation, and // store the result in the frame buffer. // const char *readPtr = scanlinePtr->_uncompressedDataPtr; // // Iterate over all image channels. // const Imf_Foundry::ChannelList & channels = _header->channels(); Imf_Foundry::ChannelList::ConstIterator imgChannel = channels.begin(); const Imf_Foundry::ChannelList::ConstIterator imgChannelsEnd = channels.end(); const Imf_Foundry::FrameBuffer::ConstIterator frameBufferEnd = frameBuffer.end(); for (Imf_Foundry::FrameBuffer::ConstIterator frameBufferSlice = frameBuffer.begin(); frameBufferSlice != frameBufferEnd; ++frameBufferSlice) { const Imf_Foundry::Slice &slice = frameBufferSlice.slice(); // // Find the x coordinates of the leftmost and rightmost // sampled pixels (i.e. pixels within the data window // for which x % xSampling == 0). // int dMinX = IMATH_NAMESPACE::divp(minX, slice.xSampling); int dMaxX = IMATH_NAMESPACE::divp(maxX, slice.xSampling); while (imgChannel != imgChannelsEnd && strcmp (imgChannel.name(), frameBufferSlice.name()) < 0) { // The frame buffer contains no slice for this channel - skip it. skipChannel (readPtr, imgChannel.channel().type, dMaxX - dMinX + 1); ++imgChannel; } bool fill = false; if (imgChannel == imgChannelsEnd || strcmp (imgChannel.name(), frameBufferSlice.name()) > 0) { // // We have reached the end of the available image channels in the file, or reached a channel // that sorts after the frame buffer channel in the alphabet. Since image channels are // stored alphabetically in the file, and frame buffer channels are also sorted alphabetically // by channel name, this means the frame buffer channel is not present in the file. // // In the frame buffer, slice frameBufferSlice will be filled with a default value. // fill = true; } // // Always add the slice if "fill" is true; otherwise, test if scan line y of this channel // contains any data and skip if not (the scan line contains data only if y % ySampling == 0). // if (fill || IMATH_NAMESPACE::modp(exrY, imgChannel.channel().ySampling) == 0) { char *linePtr = slice.base + IMATH_NAMESPACE::divp(exrY, slice.ySampling) * slice.yStride; char *writePtr = linePtr + dMinX * slice.xStride; char *endPtr = linePtr + dMaxX * slice.xStride; Imf_Foundry::copyIntoFrameBuffer (readPtr, writePtr, endPtr, slice.xStride, fill, slice.fillValue, scanlinePtr->format(), slice.type, fill ? slice.type : imgChannel.channel().type); // Move to the next image channel in the file if we're not already at the end, and // if the current frame buffer channel exists in the image (i.e. "fill" is false). if (!fill) ++imgChannel; } } //next slice in frame buffer return true; } class exrReaderFormat : public ReaderFormat { friend class exrReader; bool _disable_mmap; bool _offset_negative_display_window; bool _doNotAttachPrefix; int _edgeMode; bool _alwaysIgnorePartNames; public: bool disable_mmap() const { return _disable_mmap; } bool offset_negative_display_window() const { return _offset_negative_display_window; } bool doNotAttachPrefix() const { return _doNotAttachPrefix; } int edgeMode() const { return _edgeMode; } bool alwaysIgnorePartNames() const { return _alwaysIgnorePartNames; } exrReaderFormat() { _disable_mmap = false; _offset_negative_display_window = true; _doNotAttachPrefix = false; _edgeMode = eEdgeMode_Plate; _alwaysIgnorePartNames = 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."); Enumeration_knob(c, &_edgeMode, kEdgeModeLabels, "edge_pixels", "edge pixels"); Tooltip(c, "How to treat the edges of the data window. Edge pixels are repeated or black is used." "
    " "
  • plate detect: if the data and display windows match exactly then repeat all edges otherwise use black
  • " "
  • edge detect: for each matching window edge repeat edge pixels, use black for mismatching edges
  • " "
  • repeat: always repeat edge pixels outside the data window
  • " "
  • black: always add black pixels outside the data window
  • " "
"); Bool_knob(c, &_alwaysIgnorePartNames, "ignore_part_names", "ignore part names"); Tooltip(c, "Older versions of Nuke just stored the layer name in the part " "name of multi-part files. Nuke automatically detects legacy " "files. Check this option to force the new behaviour and ignore " "the part names."); SetFlags(c, Knob::INVISIBLE); } void append(Hash& hash) { hash.append(_offset_negative_display_window); hash.append(_doNotAttachPrefix); hash.append(_edgeMode); hash.append(_alwaysIgnorePartNames); } }; class exrReader : public Reader { public: exrReader(Read*); ~exrReader(); /** * implementation of function from Reader */ PlanarPreference planarPreference() const { if (_neverPlanarInEnv) 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); #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)); } } // Get the nuke channels if present in this view. bool getChannels(const ExrChannelNameToNuke& channelName, const std::string& viewName, std::set& channels) { // Start with an empty channel set. channels.clear(); std::string viewpart = (channelName.view().length() > 0) ? channelName.view() : heroview; std::string otherpart = channelName.nukeChannelName(); bool gotChannels = false; if (viewName == viewpart) { // Perfect match, this is the priority view for this channel. fileStereo_ = true; lookupChannels(channels, otherpart.c_str()); gotChannels = true; } // The view for this channel does not match the current view. // Allow copying if this is the hero view or there is no associated view. else if (viewpart == "" || viewpart == heroview) { // Find the nuke channels with which this is associated. lookupChannels(channels, otherpart.c_str()); gotChannels = true; } return gotChannels; } private: // Store the name and part number in the channel map struct ChannelInfo { // Default constructor for std::map ChannelInfo() : name(NULL), part(0) {} // Construct using exr channel name ChannelInfo(const char* n, int p, ExrChannelNameToNuke ch) : name(n), part(p), channelName(ch) {} 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; } // Comparison operator to decide on the best match bool isBetterThan(const ChannelInfo& rhs, const std::string& view) const; const char* name; int part; ExrChannelNameToNuke channelName; }; /// Determine the exr compression type by creating a temporary MultiPartInputFile object /// and reading the header from it. Imf_Foundry::Compression getExrCompressionType(const char filename[]); // Whether the compression blocks for a particular compression type cover multiple scanlines or not. // If this is true, it's more efficient to do planar reads when possible and make use of the exr // library's internal multi-threading. bool compressionBlocksSpanMultipleScanlines(const Imf_Foundry::Compression &compressionType); // The stripe height to use for planar reads (ZIP and PIZ compressed). int stripeHeightForCompression(const Imf_Foundry::Compression& compressionType); /// Determine whether or not the exr file is tiled by creating a temporary MultiPartInputFile object /// and reading the header from it. bool hasTileDescription(const char filename[]) const; /// Determine whether or not the alpha is needed to produce our output (for pre-multiplication). bool needsAlpha(const ChannelSet& channelsToFill); 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; 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; // The value of the NUKE_EXR_NEVER_PLANAR environment variable will be stored here on construction, // as it's expensive to check this on Windows every time planarPreference is called (bug 41471). const bool _neverPlanarInEnv; // Buffers for storing raw scanlines, to enable multiple scan lines to be decompressed in // parallel by multiple engine threads. We have one buffer for each input part. std::vector _compressedScanlineBuffers; // Whether to read raw scanlines from the file, which is only possible when the file type // is "scanlineimage" and we are reading a single scan line at a time. We only use the // CompressedScanlineBuffer above when _readRawScanlines is true. bool _readRawScanlines; #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; } #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 Imf_Foundry::Compression exrReader::getExrCompressionType(const char filename[]) { // Make a temporary input file object here so we can peek at the exr header to // determine the compression type. This will determine how many threads we want // to use to read the data, which has to be set on creation of the input file // object to read from. Imf_Foundry::MultiPartInputFile inputFile(filename, 0); // "0" here means don't multi-thread reads from this file object. return inputFile.header(0).compression(); } // Whether the compression blocks for a particular compression type cover multiple scanlines or not. // If they do, we will do planar reads when possible and make use of the exr library's internal multi-threading. bool exrReader::compressionBlocksSpanMultipleScanlines(const Imf_Foundry::Compression &compressionType) { mFnAssertStatic(Imf_Foundry::DWAB_COMPRESSION+1 == Imf_Foundry::NUM_COMPRESSION_METHODS); return (compressionType == Imf_Foundry::ZIP_COMPRESSION || // zlib compression, in blocks of 16 scan lines compressionType == Imf_Foundry::PIZ_COMPRESSION || // piz-based wavelet compression compressionType == Imf_Foundry::PXR24_COMPRESSION || // lossy 24-bit float compression (LW: this is not mentioned in the documentation, but it seems to be compressed in blocks of 16 scan lines) compressionType == Imf_Foundry::B44_COMPRESSION || // lossy 4-by-4 pixel block compression, fixed compression rate compressionType == Imf_Foundry::B44A_COMPRESSION || // lossy 4-by-4 pixel block compression, flat fields are compressed more compressionType == Imf_Foundry::DWAA_COMPRESSION || // lossy DCT based compression, in blocks of 32 scanlines. More efficient for partial buffer access. compressionType == Imf_Foundry::DWAB_COMPRESSION ); // lossy DCT based compression, in blocks of 256 scanlines. More efficient space wise and faster to decode full frames than DWAA_COMPRESSION. } int exrReader::stripeHeightForCompression( const Imf_Foundry::Compression& compressionType ) { mFnAssertStatic(Imf_Foundry::DWAB_COMPRESSION+1 == Imf_Foundry::NUM_COMPRESSION_METHODS); switch(compressionType) { //Fallbacks intentional case Imf_Foundry::ZIP_COMPRESSION: case Imf_Foundry::PIZ_COMPRESSION: case Imf_Foundry::PXR24_COMPRESSION: case Imf_Foundry::B44_COMPRESSION: case Imf_Foundry::B44A_COMPRESSION: return 64; case Imf_Foundry::DWAA_COMPRESSION: case Imf_Foundry::DWAB_COMPRESSION: return 256; default: return 64; } } bool exrReader::hasTileDescription(const char filename[]) const { // Make a temporary input file object here so we can peek at the exr header to // determine the compression type. This will determine how many threads we want // to use to read the data, which has to be set on creation of the input file // object to read from. Imf_Foundry::MultiPartInputFile inputFile(filename, 0); // "0" here means don't multi-thread reads from this file object. return inputFile.header(0).hasTileDescription(); } bool exrReader::needsAlpha(const ChannelSet& channelsToFill) { // We need alpha for premultiplication if not using linear colour space, // the channels we've been asked to fill are in the RGB layer and alpha // is amongst the channels this Reader can produce. // // (Note: I'm not sure why the pre-multiplication is not done in the linear // case - however this is the logic that was used previously in // engine, so I don't want to change it -- Lucy.) return (premult() && !lut()->linear() && (channelsToFill & Mask_RGB) && (this->channels () & Mask_Alpha)); } exrReader::exrReader(Read* r) : Reader(r) , inputfile(0) #ifdef _WIN32 , inputStdStream(NULL) , inputStream(NULL) #endif , fileStereo_(false) , dataOffset(0) #ifdef EXR_USE_MMAP , exr_use_mmap(false) , fd(-1) , fileLoaded(false) , pagesize(0) #endif , _stripeHeight(0) , _neverPlanarInEnv(getenv("NUKE_EXR_NEVER_PLANAR")) , _compressedScanlineBuffers(0) , _readRawScanlines(false) { #ifdef ENABLE_EXR_INFO_TTY std::cout << "-------------------------------- EXR info --------------------------------" << std::endl; std::cout << "File: " << filename() << std::endl; #endif // ENABLE_EXR_INFO_TTY // Make it default to linear colorspace: lut_ = LUT::GetLut(LUT::FLOAT, this); #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; // Get the name of the view for which we're reading image data. const std::string viewName = OutputContext::viewName(iop->view_for_reader(), iop); try { // Peek at the header to find out which compression type was used to create the input file. // This will be used to set the number of threads the library should use for reading the file. // We read planes multi-threaded because this is faster, scanlines single-threaded because // there is no performance gain from using multiple threads. Imf_Foundry::Compression exrCompressionType = getExrCompressionType(r->filename()); // Set the default number of threads to use for the read to zero (no multi-threading). unsigned int nThreadsToUseForRead = 0; // If the compression blocks cover multiple scanlines, or the image is stored in tiles which // span multiple scanlines, we will use the PlanarIop interface to read a block of scanlines // from the file at a time. When doing this it will be more efficient to turn on multi- // threading in the exr library. // Note: This is irrelevant when reading for the timeline, since that will always be reading the // whole image via exrReader::planarReadAndDecode (apart from any edge cases that cause the client code // to fall back to the default Reader implementation planarReadAndDecode). const bool usePlanesForRead = hasTileDescription(r->filename()) || compressionBlocksSpanMultipleScanlines(exrCompressionType); if (dynamic_cast(iop)) { // We assume the timeline client code has set the desired exr lib worker thread count. nThreadsToUseForRead = Imf_Foundry::globalThreadCount(); } else if (usePlanesForRead) { nThreadsToUseForRead = DD::Image::Thread::numThreads; // Turn on multi-threading in the library, with the global thread count set to the number of // threads that Nuke is using. This enables us to use multi-threading for tiled (planar) reads. // The number of threads to use for a particular read can be set later when we open the file // for reading. Imf_Foundry::setGlobalThreadCount(nThreadsToUseForRead); } #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, nThreadsToUseForRead); #else inputfile = new Imf_Foundry::MultiPartInputFile(r->filename(), nThreadsToUseForRead); #endif exrReaderFormat* trf = dynamic_cast(r->handler()); const bool offsetNegativeDisplayWindow = trf && trf->offset_negative_display_window(); doNotAttachPrefix = trf && trf->doNotAttachPrefix(); int edgeMode = eEdgeMode_Plate; // Default mode for preview if (trf) { edgeMode = trf->edgeMode(); } bool alwaysIgnorePartNames = false; // Default mode for preview if (trf) { alwaysIgnorePartNames = trf->alwaysIgnorePartNames(); } // Metadata comes from the first part, though we'll also add the list of views later as these may be // spread over multiple parts. exrHeaderToMetadata( inputfile->header(0), _meta, 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; // 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; switch (edgeMode) { case eEdgeMode_Plate: // 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); } break; case eEdgeMode_Edge: // Add a black pixel around mismatching edges of the box, rather than // replicate the edge pixels as Nuke does by default. 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); } else { if (datawin.min.x != dispwin.min.x) { bx--; } if (datawin.max.x != dispwin.max.x) { br++; } if (datawin.min.y != dispwin.min.y) { bt++; } if (datawin.max.y != dispwin.max.y) { by--; } } break; case eEdgeMode_Repeat: // Don't add any black pixels break; case eEdgeMode_Black: // Always add black pixels around the edges of the box. bx--; by--; br++; bt++; info_.black_outside(true); break; } 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 ( usePlanesForRead ) { _stripeHeight = stripeHeightForCompression( exrCompressionType ); } // Get the number of parts in the input file const int nInputParts = inputfile->parts(); // Check the type of the input file to determine whether it is a scanline or not. // Type is an optional property for non-multipart exrs, so check whether the file has // a type or not first. // If the file doesn't have a type, we can't be sure whether it's scanline or not, so assume // it isn't, as reading raw scanlines will fail for non-scanline images. bool exrFileIsScanLine = false; if (inputfile->header(0).hasType()) exrFileIsScanLine = (strcmp(inputfile->header(0).type().c_str(), "scanlineimage") == 0); // Read raw scanlines from the file and decompress in parallel using multiple engine threads if: // - the exr file type is "scanlineimage" (exrFileIsScanLine) // - we are reading a single scanline at a time (_stripeHeight is 1) _readRawScanlines = (exrFileIsScanLine && _stripeHeight == 1); // If the exr file is scanline, and we're not reading multiple lines, make a buffer for storing compressed scan lines. // Each engine thread will be given space in this buffer for storing a raw scan line read from the input file // before decompressing it. if (_readRawScanlines) { _compressedScanlineBuffers.resize(nInputParts); for (int part = 0; part < nInputParts; part++) _compressedScanlineBuffers[part] = new CompressedScanlineBuffer(inputfile->header(part)); } // Ignore part names if selected by user bool ignorePartNames = alwaysIgnorePartNames; // If multi-part and not ignoring part names check for legacy files if (isMultipart && !ignorePartNames) { // Find the layer writing setting in the metadata MetaData::Bundle::PropertyPtr property = _meta.getData(MetaData::Nuke::FULL_LAYER_NAMES); if (property && MetaData::isPropertyInt(property)) { int fullLayerNames = MetaData::getPropertyInt(property, 0); ignorePartNames = (fullLayerNames != 0); } else { // Auto-detect legacy channel names (with no layer information) bool channelsHaveSeparators = false; // Check the channel names in every part for (int n = 0; n < inputfile->parts(); ++n) { const Imf_Foundry::ChannelList& imfchannels = inputfile->header(n).channels(); // Check every channel Imf_Foundry::ChannelList::ConstIterator chan; for (chan = imfchannels.begin(); chan != imfchannels.end(); chan++) { std::string channelname = chan.name(); // Check for '.' separator if (channelname.find('.') != std::string::npos) { channelsHaveSeparators = true; break; } } if (channelsHaveSeparators) { // Layer names are contained in the channel names ignorePartNames = true; break; } } } } // Iterate through each part for (int n = 0; n < nInputParts; ++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) { // Legacy files used the part name to store the layer name if (!ignorePartNames) { // For multi-part files we need to prepend the part name to the channel names. // The ExrChannelNameToNuke 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(); ExrChannelNameToNuke cName(channelname.c_str(), views); std::set channels; if (this->getChannels(cName, viewName, channels)) { if (!channels.empty()) { for (std::set::iterator it = channels.begin(); it != channels.end(); it++) { Channel channel = *it; ChannelInfo newChannelInfo(chan.name(), n, cName); // 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; // It may be that more than one exr channel has been found to map to a single nuke channel. // In this case we only update the channel map if the channel found is a better match. if (channel_map.find(channel) != channel_map.end()) { mFnAssertMsg(channel_map[channel].name, "All channels in exr files must have names"); writeChannelMapping = newChannelInfo.isBetterThan(channel_map[channel], viewName); } if (writeChannelMapping) { channel_map[channel] = newChannelInfo; } mask += channel; } } else { iop->warning("Cannot assign channel number to %s", cName.nukeChannelName().c_str()); } } } #ifdef ENABLE_EXR_INFO_TTY { const Imf_Foundry::Header& header = inputfile->header(n); std::cout << "--- Header: " << n << " ---" << std::endl; std::cout << "Has name: " << (header.hasName() ? "yes" : "no") << std::endl; if (header.hasName()) { std::cout << "Name: " << header.name() << std::endl; } std::cout << "Has view: " << (header.hasView() ? "yes" : "no") << std::endl; if (header.hasView()) { std::cout << "View: " << header.view() << 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; } #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; inputfile = NULL; // reset bbox so that we won't get further engine calls set_info(0, 0, 0, 0.0); #ifdef ENABLE_EXR_INFO_TTY 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; inputfile = NULL; // reset bbox so that we won't get further engine calls set_info(0, 0, 0, 0.0); #ifdef ENABLE_EXR_INFO_TTY std::cout << "Error parsing header: " << exc.what() << std::endl; std::cout << "--------------------------------------------------------------------------" << std::endl; #endif // ENABLE_EXR_INFO_TTY return; } 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); } // If we've extracted view names, add them to the metadata with a dedicated format-agnostic key. // For a single part multiview file exrHeaderToMetadata will already have automatically added the names, // with the key exr/multiView, but the views may be spread across multiple parts without the file // specifying the multiView attribute - the views member will now contain the views however they're stored // in the file. if (!views.empty()) { // First convert the vector of strings into a single string with each view separated by a newline. std::vector::const_iterator viewsEnd = views.end(); std::string viewNames; for (std::vector::const_iterator viewIter = views.begin(); viewIter != viewsEnd; ++viewIter) { viewNames += *viewIter; // Don't put a newline after the final view. if (std::distance(viewIter, viewsEnd) > 1) { viewNames += "\n"; } } // Now we can set the metadata. _meta.setData(MetaData::VIEW_NAMES, viewNames); } foreach(z, info_.channels()) _partSets[channel_map[z].part] += z; #ifdef ENABLE_EXR_INFO_TTY std::cout << "--------------------------------------------------------------------------" << std::endl; #endif // ENABLE_EXR_INFO_TTY } exrReader::~exrReader() { #ifdef EXR_USE_MMAP if (fd != -1) { close(fd); } #endif delete inputfile; #ifdef _WIN32 delete inputStream; delete inputStdStream; #endif const size_t nBuffers = _compressedScanlineBuffers.size(); for (unsigned int buffer = 0; buffer < nBuffers; buffer++) delete _compressedScanlineBuffers[buffer]; _compressedScanlineBuffers.clear(); } 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)); } try { // Scan line case: read raw scanlines into a buffer - only one thread can access the file at a time, // so this bit has a lock round it. Then decompress the scan lines and store in the frame buffer in // a separate step - multiple engine threads can do this part at once. if (_readRawScanlines) { if (iop->aborted()) return; // abort if another thread does so // Make an input part to read from. Imf_Foundry::InputPart inputPart(*inputfile, part); // Read a scan line from the input part. Only one thread should read from the file at a time. CompressedScanline *scanlinePtr = NULL; { Guard guard(C_lock); scanlinePtr = _compressedScanlineBuffers[part]->readRawScanlineFromFile(inputPart, exrY); } // Store the scan line in the frame buffer. (This will uncompress it first if necessary.) _compressedScanlineBuffers[part]->copyScanlineToFrameBuffer(scanlinePtr, fbuf, exrY); } else { // Fallback case: read and decompress the image data in one step. Only one engine thread can do this // at a time. Guard guard(C_lock); 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) { mFnAssert(inputfile != NULL); // 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 (needsAlpha(channels)) 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& outputPlane) { const int y = outputPlane.bounds().y(); const int lines = outputPlane.bounds().h(); // The channels in the image plane we've been asked to fill. const ChannelSet& outputChannels = outputPlane.channels(); // Pointer to the image plane to fill with the data read from the exr file. // This points to the output plane unless we need temporary copies of channels // that aren't in the output (e.g. alpha for premultiplication). ImagePlane* imagePlanePtr = &outputPlane; ChannelSet channelsToFill = outputChannels; // Temporary image plane to use inside this function if we need alpha and the plane // passed in doesn't have it. ImagePlane tempImagePlane; // If we need alpha (for premultiplication) and the plane we've been asked to fill // doesn't have it, set up the temporary plane and make imagePlanePtr point to that // one instead. const bool hasAlpha = outputChannels & Mask_Alpha; const bool needsTempAlpha = needsAlpha(outputChannels) && !hasAlpha; if (needsTempAlpha) { channelsToFill += Mask_Alpha; tempImagePlane = ImagePlane(outputPlane.bounds(), outputPlane.packed(), channelsToFill); imagePlanePtr = &tempImagePlane; } // Make the image plane we're going to fill in writable. (If this isn't the output // plane, the output plane will be allocated later by the call to // ImagePlane::copyIntersectionFrom().) imagePlanePtr->makeWritable(); foreach(z, channelsToFill) imagePlanePtr->fillChannel(z, 0.0); const Imath_Foundry::Box2i& datawin = inputfile->header(0).dataWindow(); mFnAssertMsg(datawin.min.x >= imagePlanePtr->bounds().x(), "Planar Interface: Pixels buffer of ImagePlane is not big enough for pixels from the EXR source"); mFnAssertMsg(datawin.max.x < imagePlanePtr->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(channelsToFill, 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 = imagePlanePtr->rowStride() * sizeof(float); // We need to get the address that corresponds to the origin, since readPixels will write into // our output buffer using // dest + x * xStride + y * yStride // where x and y are EXR pixel coordinates and the strides are what we pass in when setting up // the Slice for each channel (and actually we need to specify the appropriate dest address for // each channel, in which case channelDest, see below, will only equal dest for channel 0). // Ah, but it's a bit more complicated than that - you've noticed the use of convertY(). // We use that because we're going to automatically vertically flip the image data as we read it. // By passing y = 0 into convertY() we get the output address that corresponds to the *EXR origin* // and then passing in a negative row stride, so that as the EXR lib increments its row index, moving // down the image (remember EXR y increases downwards) then the -ve row stride cause it to write // into earlier addresses in the output buffer. // All very confusing! The simplest case is that the data and display windows match, with their // (EXR) minimum, ie top row, at (EXR) y = 0. And suppose we only using a single PlanarIop stripe. // Then call to convertY(0) will return max.y, i.e. the top row in Nuke coords. So, the dest // address corresponds to the start of the top row and then readPixels uses // (start of top row) + (x * col stride) + (y * - row stride) // to get the output buffer address (with x and y in EXR coords), so when y = 0 it writes to // the top row at the end of the buffer, and when y reaches max.y it'll write at an *earlier* // address corresponding to the bottom row at the start of the output buffer. float* dest = imagePlanePtr->writableAt(convertY(0), 0).ptr(); // Create a framebuffer for this part Imf_Foundry::FrameBuffer fbuf; foreach (z, channelsToFill) { 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 = imagePlanePtr->chanNo(z); // Place the row in the respective framebuffer float* channelDest = (dest + chanNo * imagePlanePtr->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, channelsToFill) { if (toCopy.find(z) != toCopy.end()) { imagePlanePtr->copyChannel(z, toCopy[z]); } } if (!iop->aborted() && !lut()->linear()) { const float* alpha = (channelsToFill & Mask_Alpha) ? &imagePlanePtr->at(imagePlanePtr->bounds().x(), imagePlanePtr->bounds().y(), Chan_Alpha) : 0; for (Channel chan = Chan_Red; chan <= Chan_Blue; chan = Channel(chan + 1)) { if (intersect(channelsToFill, chan)) { float* pix = &imagePlanePtr->writableAt(imagePlanePtr->bounds().x(), imagePlanePtr->bounds().y(), imagePlanePtr->chanNo(chan)); from_float(chan, pix, pix, alpha, imagePlanePtr->bounds().area(), imagePlanePtr->colStride()); } } } // If we made a temporary image plane to store the alpha, copy the // intersection between this and the output plane into the output // plane (this will allocate the output plane if necessary, and // should do a shallow copy if it's more efficient to do so). if (!iop->aborted() && needsTempAlpha) { mFnAssert(imagePlanePtr == &tempImagePlane); outputPlane.copyIntersectionFrom(*imagePlanePtr); } } // Comparison operator to decide on the best match bool exrReader::ChannelInfo::isBetterThan(const ChannelInfo& rhs, const std::string& viewname) const { // If in doubt, this one is better bool isBetter = true; const int existingLength = strlen(rhs.name); const int newLength = strlen(name); const bool existingChannelHasEmptyLayerName = (existingLength > 0) && rhs.name[0] == '.'; // Check for an exact view match (as opposed to default/hero view) if (viewname == channelName.view() && viewname != rhs.channelName.view()) { isBetter = true; } else if (viewname == rhs.channelName.view() && viewname != channelName.view()) { isBetter = false; } else if (existingChannelHasEmptyLayerName && existingLength == (newLength + 1)) { // Bug 31548 - Read - EXR rendering black for R, G and B channels // "channel" should override ".channel" isBetter = 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" isBetter = false; } return isBetter; }