// exrWriter.C // Copyright (c) 2009 The Foundry Visionmongers Ltd. All Rights Reserved. /* Reads exr files using libexr. This is an example of a file reader that is not a subclass of FileWriter. Instead this uses the library's reader functions and a single lock so that multiple threads do not crash the library. 04/14/03 Initial Release Charles Henrich 12/04/03 User selectable compression, Charles Henrich float precision, and autocrop 10/04 Defaulted autocrop to off spitzak 5/06 black-outside and reformatting spitzak */ #include "DDImage/DDWindows.h" #include "DDImage/Writer.h" #include "DDImage/Row.h" #include "DDImage/Knobs.h" #include "DDImage/Tile.h" #include "DDImage/DDString.h" #include "DDImage/MetaData.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace DD::Image; class exrWriter : public Writer { void autocrop_tile(Tile& img, ChannelMask channels, int* bx, int* by, int* br, int* bt); int datatype; int compression; bool autocrop; bool writeHash; int hero; int _metadataMode; bool _followStandard; public: exrWriter(Write* iop); ~exrWriter(); void metadataToExrHeader(const MetaData::Bundle& metadata, Imf::Header& exrheader); Iop* firstInput(const std::set& wantViews); void execute(); void knobs(Knob_Callback f); static const Writer::Description d; // Make it default to linear colorspace: LUT* defaultLUT() const { return LUT::getLut(LUT::FLOAT); } int split_input(int i) const { return int(executingViews().size() ? executingViews().size() : 1); } //! This writer is capable of writing out the overscan, so passthrough should not //! clip to the format. virtual bool clipToFormat() const { return false; } /** * return the view which we are expecting on input N */ int view(int n) const { std::set views = executingViews(); std::set::const_iterator i = views.begin(); while (i != views.end()) { if (!n) { return *i; } n--; i++; } return 0; } const OutputContext& inputContext(int i, OutputContext& o) const { o = iop->outputContext(); o.view(view(i)); return o; } const char* help() { return "OpenEXR high dynamic range format from ILM"; } }; static Writer* build(Write* iop) { return new exrWriter(iop); } const Writer::Description exrWriter::d("exr\0sxr\0", build); exrWriter::exrWriter(Write* iop) : Writer(iop) { datatype = 0; compression = 1; autocrop = false; writeHash = true; hero = 1; _metadataMode = 1; _followStandard = 0; } static const Imf::Compression ctypes[6] = { Imf::NO_COMPRESSION, Imf::ZIPS_COMPRESSION, Imf::ZIP_COMPRESSION, Imf::PIZ_COMPRESSION, Imf::RLE_COMPRESSION, Imf::B44_COMPRESSION }; static const char* const cnames[] = { "none", "Zip (1 scanline)", "Zip (16 scanlines)", "PIZ Wavelet (32 scanlines)", "RLE", "B44", NULL }; static const char* const dnames[] = { "16 bit half", "32 bit float", NULL }; static const char* const metadata_modes[] = { "no metadata", "default metadata", "default metadata and exr/*", "all metadata except input/*", "all metadata", NULL }; exrWriter::~exrWriter() { } class RowGroup { std::vector row; public: RowGroup(size_t n, int x, int r) { row.resize(n); for (size_t i = 0; i < n; i++) { row[i] = new Row(x, r); } } ~RowGroup() { for (size_t i = 0; i < row.size(); i++) { delete row[i]; } } Row& operator[](int i) { if (i >= int(row.size())) abort(); return *row[i]; } const Row& operator[](int i) const { if (i >= int(row.size())) abort(); return *row[i]; } }; bool timeCodeFromString(const std::string& str, Imf::TimeCode& attr, Write* iop) { if (str.length() != 11) return false; int hours = 0, mins = 0, secs = 0, frames = 0; sscanf(str.c_str(), "%02d:%02d:%02d:%02d", &hours, &mins, &secs, &frames); try { // if some thing is out of range an exception is throw // in this case just report a warning on console Imf::TimeCode a; a.setHours(hours); a.setMinutes(mins); a.setSeconds(secs); a.setFrame(frames); attr = a; } catch (const std::exception& exc) { iop->warning("EXR: Time Code Metadata warning [%s]\n", exc.what()); return false; } return true; } bool edgeCodeFromString(const std::string& str, Imf::KeyCode& attr, Write* iop) { int mfcCode, filmType, prefix, count, perfOffset; sscanf(str.c_str(), "%d %d %d %d %d", &mfcCode, &filmType, &prefix, &count, &perfOffset); try { // if some thing is out of range an exception is throw // in this case just report a warning on console Imf::KeyCode a; a.setFilmMfcCode(mfcCode); a.setFilmType(filmType); a.setPrefix(prefix); a.setCount(count); a.setPerfOffset(perfOffset); attr = a; } catch (const std::exception& exc) { iop->warning("EXR: Edge Code Metadata warning [%s]\n", exc.what()); return false; } return true; } void exrWriter::metadataToExrHeader(const MetaData::Bundle& metadata, Imf::Header& exrheader) { if (_metadataMode != 0) { // NB: if specific things are added to this list the tooltip for the "metadata" knob needs // updating std::string timeCodeStr = metadata.getString(MetaData::TIMECODE); if (!timeCodeStr.empty()) { Imf::TimeCode attr; if (timeCodeFromString(timeCodeStr, attr, iop)) { Imf::addTimeCode(exrheader, attr); } } std::string edgeCodeStr = metadata.getString(MetaData::EDGECODE); if (!edgeCodeStr.empty()) { Imf::KeyCode attr; if (edgeCodeFromString(edgeCodeStr, attr, iop)) { Imf::addKeyCode(exrheader, attr); } } double frameRate = metadata.getDouble(MetaData::FRAME_RATE); if (frameRate != 0) { Imf::Rational fps = Imf::guessExactFps(frameRate); Imf::addFramesPerSecond(exrheader, fps); } double exposure = metadata.getDouble(MetaData::EXPOSURE); if (exposure != 0) { Imf::addExpTime(exrheader, (float)exposure); } if ( writeHash ) { Hash inputHash = iop->getHashOfInputs(); std::ostringstream hashString; hashString << std::hex << inputHash.value(); Imf::StringAttribute hashAttr; hashAttr.value() = hashString.str(); exrheader.insert(MetaData::Nuke::NODE_HASH, hashAttr); } } if (_metadataMode) { for (MetaData::Bundle::const_iterator it = metadata.begin(); it != metadata.end(); it++) { std::string exrPropName = ""; if (it->first == MetaData::EXR::EXR_TILED ) { // strip exr/tiled as we always write scanline exrs exrPropName = ""; } else if (it->first.substr(0, strlen(MetaData::EXR::EXR_PREFIX)) == MetaData::EXR::EXR_PREFIX && _metadataMode >= 2) { exrPropName = it->first.substr(strlen(MetaData::EXR::EXR_PREFIX)); } else if (it->first.substr(0, strlen(MetaData::INPUT_PREFIX)) != MetaData::INPUT_PREFIX && _metadataMode >= 3) { exrPropName = MetaData::Nuke::NUKE_PREFIX + it->first; } else if (_metadataMode >= 4) { exrPropName = MetaData::Nuke::NUKE_PREFIX + it->first; } Imf::Attribute* attr = 0; const MetaData::Bundle::PropertyPtr prop = it->second; size_t psize = MetaData::getPropertySize(prop); if (!exrPropName.empty()) { if ( MetaData::isPropertyDouble(prop) ) { if (psize == 1) { attr = new Imf::FloatAttribute( (float)MetaData::getPropertyDouble(prop, 0) ); } else if (psize == 2) { attr = new Imf::V2fAttribute(Imath::V2f( (float)MetaData::getPropertyDouble(prop, 0), (float)MetaData::getPropertyDouble(prop, 1) )); } else if (psize == 3) { attr = new Imf::V3fAttribute(Imath::V3f( (float)MetaData::getPropertyDouble(prop, 0), (float)MetaData::getPropertyDouble(prop, 1), (float)MetaData::getPropertyDouble(prop, 2) )); } else if (psize == 4) { attr = new Imf::Box2fAttribute(Imath::Box2f( Imath::V2f((float)MetaData::getPropertyDouble(prop, 0), (float)MetaData::getPropertyDouble(prop, 1)), Imath::V2f((float)MetaData::getPropertyDouble(prop, 2), (float)MetaData::getPropertyDouble(prop, 3) ))); } else if (psize == 9) { float val[3][3]; for (size_t i = 0; i < psize; i++) { val[i / 3][i % 3] = (float)MetaData::getPropertyDouble(prop, i); } attr = new Imf::M33fAttribute(Imath::M33f(val)); } else if (psize == 16) { float val[4][4]; for (size_t i = 0; i < psize; i++) { val[i / 4][i % 4] = (float)MetaData::getPropertyDouble(prop, i); } attr = new Imf::M44fAttribute(Imath::M44f(val)); } } else if (MetaData::isPropertyInt( prop )) { if (psize == 1) { attr = new Imf::IntAttribute(MetaData::getPropertyInt(prop, 0)); } else if (psize == 2) { attr = new Imf::V2iAttribute( Imath::V2i(MetaData::getPropertyInt(prop, 0), MetaData::getPropertyInt(prop, 1)) ); } else if (psize == 3) { attr = new Imf::V3iAttribute( Imath::V3i(MetaData::getPropertyInt(prop, 0), MetaData::getPropertyInt(prop, 1), MetaData::getPropertyInt(prop, 2))); } else if (psize == 4) { attr = new Imf::Box2iAttribute( Imath::Box2i(Imath::V2i(MetaData::getPropertyInt(prop, 0), MetaData::getPropertyInt(prop, 1)), Imath::V2i(MetaData::getPropertyInt(prop, 2), MetaData::getPropertyInt(prop, 3)))) ; } } else if ( MetaData::isPropertyString(prop) ) { if (psize == 1) { attr = new Imf::StringAttribute( MetaData::getPropertyString(prop, 0) ); } } } if (attr && exrheader.find(exrPropName) == exrheader.end()) { exrheader.insert(exrPropName, *attr); } delete attr; } } } Iop* exrWriter::firstInput(const std::set& wantViews) { for (int i = 0; i < iop->inputs(); i++) { if (wantViews.find(view(i)) == wantViews.end()) continue; return iop->input(i); } return &input0(); } void exrWriter::execute() { const std::set execViews = executingViews(); std::set wantViews = iop->executable()->viewsToExecute(); if (wantViews.size() == 0) { wantViews = execViews; } int floatdepth = datatype ? 32 : 16; Imf::Compression compression = ctypes[this->compression]; ChannelSet channels(firstInput(wantViews)->channels()); channels &= (iop->channels()); if (!channels) { iop->critical("exrWriter: No channels selected (or available) for write\n"); return; } if (premult() && !lut()->linear() && (channels & Mask_RGB) && (input0().channels() & Mask_Alpha)) channels += (Mask_Alpha); std::vector views; std::vector viewstr; viewstr.push_back(OutputContext::viewname(hero)); if (wantViews.size() == 1) { hero = *wantViews.begin(); } for (std::set::const_iterator i = execViews.begin(); i != execViews.end(); i++) { views.push_back(*i); if (*i != hero) { viewstr.push_back(OutputContext::viewname(*i)); } } DD::Image::Box bound; bool sizewarn = false; bool firstInputBbox = true; for (int i = 0; i < iop->inputs(); i++) { if (wantViews.find(view(i)) == wantViews.end()) continue; Iop* input = iop->input(i); input->validate(true); } for (int i = 0; i < iop->inputs(); i++) { if (wantViews.find(view(i)) == wantViews.end()) continue; Iop* input = iop->input(i); int bx = input->x(); int by = input->y(); int br = input->r(); int bt = input->t(); if (input->black_outside()) { if (bx + 2 < br) { bx++; br--; } if (by + 2 < bt) { by++; bt--; } } input->request(bx, by, br, bt, channels, 1); if (br - bx > input0().format().width() * 1.5 || bt - by > input0().format().height() * 1.5) { // print this warning before it possibly crashed due to requesting a // huge buffer! if (sizewarn) { fprintf(stderr, "!WARNING! Bounding Box Area is > 1.5 times larger " "than format. You may want crop your image before writing it.\n"); sizewarn = true; } } if (autocrop) { Tile img(*input, input->x(), input->y(), input->r(), input->t(), channels, true); if (iop->aborted()) { //iop->critical("exrWriter: Write failed [Unable to get input tile]\n"); return; } autocrop_tile(img, channels, &bx, &by, &br, &bt); bt++; /* We (aka nuke) want r & t to be beyond the last pixel */ br++; } if (firstInputBbox) { bound.y(by); bound.x(bx); bound.r(br); bound.t(bt); } else { bound.y(std::min(bound.y(), by)); bound.x(std::min(bound.x(), bx)); bound.r(std::max(bound.r(), br)); bound.t(std::max(bound.t(), bt)); } firstInputBbox = false; } const Format& inputFormat = firstInput(wantViews)->format(); Imath::Box2i C_datawin; C_datawin.min.x = bound.x(); C_datawin.min.y = inputFormat.height() - bound.t(); C_datawin.max.x = bound.r() - 1; C_datawin.max.y = inputFormat.height() - bound.y() - 1; Imath::Box2i C_dispwin; C_dispwin.min.x = 0; C_dispwin.min.y = 0; C_dispwin.max.x = inputFormat.width() - 1; C_dispwin.max.y = inputFormat.height() - 1; try { int numchannels = channels.size(); Imf::OutputFile* outfile; Imf::FrameBuffer fbuf; RowGroup renderrow(executingViews().size(), bound.x(), bound.r()); RowGroup writerow(executingViews().size(), bound.x(), bound.r()); Imf::Header exrheader(C_dispwin, C_datawin, iop->format().pixel_aspect(), Imath::V2f(0, 0), 1, Imf::INCREASING_Y, compression); if (wantViews.size() > 1) { // only write multi view string if a stereo file Imf::StringVectorAttribute multiViewAttr; multiViewAttr.value() = viewstr; exrheader.insert("multiView", multiViewAttr); } Iop* metaInput = NULL; for (size_t viewIdx = 0; viewIdx < views.size(); viewIdx ++) { if (wantViews.find(views[viewIdx]) == wantViews.end()) { continue; } if (metaInput == NULL || views[viewIdx] == hero) { metaInput = iop->input(viewIdx); } } if (metaInput == NULL) { metaInput = iop->input(0); } const MetaData::Bundle& metadata = metaInput->fetchMetaData(NULL); metadataToExrHeader(metadata, exrheader); Imf::Array2D halfwriterow(numchannels * views.size(), bound.r() - bound.x()); std::map channelsperview; for (int v = 0; v < int(views.size()); v++) { if (wantViews.find(views[v]) == wantViews.end()) { continue; } int curchan = 0; foreach(z, channels) { std::string channame; switch (z) { case Chan_Red: channame = "R"; break; case Chan_Green: channame = "G"; break; case Chan_Blue: channame = "B"; break; case Chan_Alpha: channame = "A"; break; default: channame = iop->channel_name(z); break; } if (executingViews().size() > 1 && views[v] != hero) { if (_followStandard){ size_t i = channame.find('.'); if (i != channame.npos){ std::string layerName = channame.substr(0, i); std::string channelName = channame.substr(i + 1); channame = layerName + "." + OutputContext::viewname(views[v]) + "." + channelName; } else channame = OutputContext::viewname(views[v]) + "." + channame; } else channame = OutputContext::viewname(views[v]) + "." + channame; if (z == Chan_Stereo_Disp_Left_X || z == Chan_Stereo_Disp_Left_Y || z == Chan_Stereo_Disp_Right_X || z == Chan_Stereo_Disp_Right_Y) { continue; } } channelsperview[v].insert(z); if (floatdepth == 32) { exrheader.channels().insert(channame.c_str(), Imf::Channel(Imf::FLOAT)); } else { exrheader.channels().insert(channame.c_str(), Imf::Channel(Imf::HALF)); } writerow[v].writable(z); if (floatdepth == 32) { fbuf.insert(channame.c_str(), Imf::Slice(Imf::FLOAT, (char*)(float*)writerow[v][z], sizeof(float), 0)); } else { fbuf.insert(channame.c_str(), Imf::Slice(Imf::HALF, (char*)(&halfwriterow[v * numchannels + curchan][0] - C_datawin.min.x), sizeof(halfwriterow[v * numchannels * curchan][0]), 0)); curchan++; } } } std::string temp_name; temp_name = createFileHash().c_str(); temp_name += ".tmp"; outfile = new Imf::OutputFile(temp_name.c_str(), exrheader); outfile->setFrameBuffer(fbuf); for (int scanline = bound.t() - 1; scanline >= bound.y(); scanline--) { for (int v = 0; v < int(views.size()); v++) { channels = channelsperview[v]; if (wantViews.find(views[v]) == wantViews.end()) { continue; } writerow[v].pre_copy(renderrow[v], channels); iop->inputnget(v, scanline, bound.x(), bound.r(), channels, renderrow[v], 1.0/wantViews.size()); if (iop->aborted()) break; int curchan = 0; foreach(z, channels) { const float* from = renderrow[v][z]; const float* alpha = renderrow[v][Chan_Alpha]; float* to = writerow[v].writable(z); if (!lut()->linear() && z <= Chan_Blue) { to_float(z - 1, to + C_datawin.min.x, from + C_datawin.min.x, alpha + C_datawin.min.x, C_datawin.max.x - C_datawin.min.x + 1); from = to; } if (bound.r() > iop->input(v)->r()) { float* end = renderrow[v].writable(z) + bound.r(); float* start = renderrow[v].writable(z) + iop->input(v)->r(); while (start < end) { *start = 0; start++; } } if (bound.x() < iop->input(v)->x()) { float* end = renderrow[v].writable(z) + bound.x(); float* start = renderrow[v].writable(z) + iop->input(v)->x(); while (start > end) { *start = 0; start--; } } if (floatdepth == 32) { if (to != from) for (int count = C_datawin.min.x; count < C_datawin.max.x + 1; count++) to[count] = from[count]; } else { for (int count = C_datawin.min.x; count < C_datawin.max.x + 1; count++) halfwriterow[v * numchannels + curchan][count - C_datawin.min.x] = from[count]; curchan++; } } progressFraction(double(bound.t() - scanline) / (bound.t() - bound.y())); } outfile->writePixels(1); } delete outfile; if( !FileIop::renameFile( temp_name.c_str(), filename() ) ) iop->critical("Can't rename .tmp to final, %s", strerror(errno)); } catch (const std::exception& exc) { iop->critical("EXR: Write failed [%s]\n", exc.what()); return; } } void exrWriter::knobs(Knob_Callback f) { Bool_knob(f, &autocrop, "autocrop"); Tooltip(f, "Reduce the bounding box to the non-zero area. This is normally " "not needed as the zeros will compress very small, and it is slow " "as the whole image must be calculated before any can be written. " "However this may speed up some programs reading the files."); Bool_knob(f, &writeHash, "write_hash", "write hash"); SetFlags(f, Knob::INVISIBLE); Tooltip(f, "Write the hash of the node graph into the exr file. Useful to see if your image is up to date when doing a precomp."); Enumeration_knob(f, &datatype, dnames, "datatype"); Enumeration_knob(f, &compression, cnames, "compression"); Obsolete_knob(f, "stereo", 0); OneView_knob(f, &hero, "heroview"); Tooltip(f, "If stereo is on, this is the view that is written as the \"main\" image"); Enumeration_knob(f, &_metadataMode, metadata_modes, "metadata"); Tooltip(f, "Which metadata to write out to the EXR file." "

'no metadata' means that no custom attributes will be created and only metadata that fills required header fields will be written.

'default metadata' means that the optional timecode, edgecode, frame rate and exposure header fields will also be filled using metadata values."); Newline(f); Bool_knob(f, &_followStandard, "Standard layer name format"); Tooltip(f, "Older versions of Nuke write out channel names in the format: view.layer.channel." "Check this option to follow the EXR standard format: layer.view.channel"); } void exrWriter::autocrop_tile(Tile& img, ChannelMask channels, int* bx, int* by, int* br, int* bt) { int xcount, ycount; *bx = img.r(); *by = img.t(); *br = img.x(); *bt = img.y(); foreach (z, channels) { for (ycount = img.y(); ycount < img.t(); ycount++) { for (xcount = img.x(); xcount < img.r(); xcount++) { if (img[z][ycount][xcount] != 0) { if (xcount < *bx) *bx = xcount; if (ycount < *by) *by = ycount; break; } } } for (ycount = img.t() - 1; ycount >= img.y(); ycount--) { for (xcount = img.r() - 1; xcount >= img.x(); xcount--) { if (img[z][ycount][xcount] != 0) { if (xcount > *br) *br = xcount; if (ycount > *bt) *bt = ycount; break; } } } } if (*bx > *br || *by > *bt) *bx = *by = *br = *bt = 0; }