// 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 "DDImage/LUT.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "exrGeneral.h" 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; float _dwCompressionLevel; bool autocrop; bool writeHash; int hero; int _metadataMode; bool _doNotWriteNukePrefix; bool _followStandard; int _multipartInterleaveMode; bool _truncateChannelNames; bool _writeFullLayerNames; public: // Multipart interleave modes enum MultipartInterleaveMode { eInterleave_Channels_Layers_Views, // This makes a single part file for backwards compatibility eInterleave_Channels_Layers, // This separates views for forwards compatibility with EXR 2.0 eInterleave_Channels // This separates views and layers for faster reading of individual layers }; // Multpart mode labels static const char* const multipartModeLabels[]; // String comparison for possibly null strings struct LessThanStr { bool operator()(const char* s1, const char* s2) const; }; exrWriter(Write* iop); ~exrWriter(); Iop* firstInput(const std::set& wantViews); void execute(); void knobs(Knob_Callback f); int knob_changed(Knob *k); static const Writer::Description d; // Make it default to linear colorspace: LUT* defaultLUT() const { return LUT::GetLut(LUT::FLOAT, this); } int split_input(int i) const { // BW: It may be that executingViews is empty. This is user controlled // using the "views" knob on the Write node. 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(const int inputIndex) const { // Assign inputs directly from the executingViews set std::set views = executingViews(); // Iterate through the views int currentInputIndex = 0; std::set::const_iterator itView = views.begin(); while (itView != views.end()) { // Return if we've reached the input index if (currentInputIndex == inputIndex) { return *itView; } // Move to the next input currentInputIndex++; itView++; } // BW: It may be that executingViews is empty. This is user controlled // using the "views" knob on the Write node. In other cases we do not // expect view() to be called for non-existent inputs. mFnAssertMsg(views.empty(), "exrWriter: View for input not found"); return 0; } /** * return the input which we are expecting for view V */ int inputIndex(int view) const { // Inputs are assigned directly from the executingViews set std::set views = executingViews(); // Iterate through the views int currentInputIndex = 0; std::set::const_iterator itView = views.begin(); while (itView != views.end()) { // Return if we've found the view if (*itView == view) { return currentInputIndex; } // Move to the next input currentInputIndex++; itView++; } // BW: It may be that executingViews is empty. This is user controlled // using the "views" knob on the Write node. In other cases we do not // expect inputIndex() to be called for non-existent inputs. mFnAssertMsg(views.empty(), "exrWriter: Input for view not found"); return 0; } bool compressionHasLevel() const { const Imf_Foundry::Compression compressionVal = ctypes[this->compression]; const bool hasLevel = ( compressionVal == Imf_Foundry::DWAA_COMPRESSION || compressionVal == Imf_Foundry::DWAB_COMPRESSION ); return hasLevel; } 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"; } }; const char* const exrWriter::multipartModeLabels[] = { "channels, layers and views", "channels and layers", "channels", NULL }; bool exrWriter::LessThanStr::operator()(const char* s1, const char* s2) const { // Collate null pointers if (s1 == NULL && s2 == NULL) return false; if (s1 == NULL) return true; if (s2 == NULL) return false; return strcmp(s1, s2) < 0; } static Writer* build(Write* iop) { return new exrWriter(iop); } const Writer::Description exrWriter::d("exr\0sxr\0", build); exrWriter::exrWriter(Write* iop) : Writer(iop) { setFlags(DONT_CHECK_INPUT0_CHANNELS); datatype = 0; compression = 1; //RP:defaulting compression level to the same as in in OpenEXR //FROM:ImfDwaCompressor.cpp // Compression level is controlled by setting an int/float/double attribute // on the header named "dwaCompressionLevel". This is a thinly veiled name for // the "base-error" value mentioned above. The "base-error" is just // dwaCompressionLevel / 100000. The default value of 45.0 is generally // pretty good at generating "visually lossless" values at reasonable // data rates. Setting dwaCompressionLevel to 0 should result in no additional // quantization at the quantization stage (though there may be // quantization in practice at the CSC/DCT steps). But if you really // want lossless compression, there are pleanty of other choices // of compressors ;) _dwCompressionLevel = 45.0f; autocrop = false; writeHash = true; hero = 1; _metadataMode = eDefaultMetaData; _doNotWriteNukePrefix = false; _followStandard = 0; // Default to backwards compatibility _multipartInterleaveMode = eInterleave_Channels_Layers_Views; _truncateChannelNames = false; _writeFullLayerNames = false; } 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]; } }; 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_Foundry::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; if (wantViews.size() == 1) { hero = *wantViews.begin(); } // Hero view comes first if it has been requested if (execViews.find(hero) != execViews.end()) { views.push_back(hero); viewstr.push_back(OutputContext::viewName(hero, iop)); } for (std::set::const_iterator i = execViews.begin(); i != execViews.end(); i++) { if (*i != hero) { views.push_back(*i); viewstr.push_back(OutputContext::viewName(*i, iop)); } } 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); 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_Foundry::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_Foundry::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; // Bug 33310 - nuke.cancel() in write node fails to cancel if (iop->aborted()) { // abort before writing anything so that we don't end up with partial files written return; } try { // The number of distinct channels including disparity channels // Note: Disparity channels do not belong to any view int numchannels = channels.size(); RowGroup renderrow(executingViews().size(), bound.x(), bound.r()); RowGroup writerow(executingViews().size(), bound.x(), bound.r()); // Determine the number of parts to write // Resolve the layers // Note that not all channels belong to a layer. // We need to find all of the layers (including NULL for channels outside a layer) typedef std::set StringSet; StringSet layerSet; // We need to map all of the channels (to avoid losing channels outside a layer) typedef std::multimap StringChannelMap; StringChannelMap layerChannelMap; foreach(z, channels) { // Note that the layer name may be null const char* layerName = getLayerName(z); // Fix up rgb to rgba (regardless of presence of alpha) if (layerName && !strcmp(layerName, "rgb")) layerName = "rgba"; layerSet.insert(layerName); layerChannelMap.insert( std::pair(layerName, z) ); } int numLayers = layerSet.size(); // The number of views int numViews = wantViews.size(); // The number of parts int numParts = 0; switch (_multipartInterleaveMode) { case eInterleave_Channels_Layers_Views: // All in one numParts = 1; break; case eInterleave_Channels_Layers: // Part per view numParts = numViews; break; case eInterleave_Channels: // Part per view per layer numParts = numViews * numLayers; break; }; mFnAssert(numParts); // Create an array of headers (one for each part) Imf_Foundry::Header exrHeaderTemplate(C_dispwin, C_datawin, iop->format().pixel_aspect(), Imath_Foundry::V2f(0, 0), 1, Imf_Foundry::INCREASING_Y, compression); exrHeaderTemplate.setType(Imf_Foundry::SCANLINEIMAGE); exrHeaderTemplate.setVersion(1); //If the compression method is either DWAA or DWAB set the value, it defaults to 45 if ( compressionHasLevel() ) { Imf_Foundry::addDwaCompressionLevel( exrHeaderTemplate, _dwCompressionLevel ); } std::vector exrheaders(numParts, exrHeaderTemplate); // Create an array of frame buffers (to match) std::vector fbufs(numParts); // Set the multiview attribute if necessary if (numParts == 1 && wantViews.size() > 1) { // only write multi view string if a stereo file Imf_Foundry::StringVectorAttribute multiViewAttr; multiViewAttr.value() = viewstr; exrheaders[0].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) { const int inputIdx = inputIndex( views[viewIdx] ); metaInput = iop->input(inputIdx); } } if (metaInput == NULL) { metaInput = iop->input(0); } const MetaData::Bundle& metadata = metaInput->fetchMetaData(NULL); Hash nodeHash = iop->getHashOfInputs(); metadataToExrHeader( (enum ExrMetaDataMode) _metadataMode, metadata, exrheaders[0], iop, writeHash ? &nodeHash : 0, _doNotWriteNukePrefix, _writeFullLayerNames ); Imf_Foundry::Array2D halfwriterow(numchannels * views.size(), bound.r() - bound.x()); std::map channelsperview; // Index the channels per view to allow for missing disparity channels std::vector< std::map > rowChannelIndices(views.size()); int currentPart = 0; // Loop through views for (int v = 0; v < int(views.size()); v++) { mFnAssert(currentPart < numParts); if (wantViews.find(views[v]) == wantViews.end()) { continue; } int curchan = 0; // Loop through layers StringSet::const_iterator itLayerSet = layerSet.begin(); StringSet::const_iterator endLayerSet = layerSet.end(); for ( ; itLayerSet != endLayerSet; ++itLayerSet) { mFnAssert(currentPart < numParts); // Get the layer name const char* layerName = *itLayerSet; // For multipart files if (numParts > 1) { // Set the view attribute exrheaders[currentPart].setView(viewstr[v]); // Set the name attribute std::string name; if (layerName && _multipartInterleaveMode == eInterleave_Channels) { name += layerName; name += '.'; } name += viewstr[v]; exrheaders[currentPart].setName(name); } // Find the channels in this layer StringChannelMap::const_iterator itLayerChannelMap = layerChannelMap.lower_bound(layerName); StringChannelMap::const_iterator upperLayerChannelMap = layerChannelMap.upper_bound(layerName); ChannelSet layerChannels; for( ; itLayerChannelMap != upperLayerChannelMap; ++itLayerChannelMap) { layerChannels += itLayerChannelMap->second; } // Loop through channels foreach(z, layerChannels) { 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; } // Add the view name if necessary if (executingViews().size() > 1 && views[v] != hero && _multipartInterleaveMode == eInterleave_Channels_Layers_Views) { 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], iop) + "." + channelName; } else channame = OutputContext::viewName(views[v], iop) + "." + channame; } else channame = OutputContext::viewName(views[v], iop) + "." + channame; // Skip disparity channels for all but the default (hero) view 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; } } // Remove the layer name if necessary // Note that the view will not be part of the Nuke channel name. if (!_writeFullLayerNames && _multipartInterleaveMode == eInterleave_Channels) { size_t i = channame.find('.'); if (i != channame.npos){ channame = channame.substr(i + 1); } } // truncate channel name to a maximum of 31 chars static const size_t MAX_CHANNEL_SIZE = 31; if (_truncateChannelNames && (channame.size() > MAX_CHANNEL_SIZE) ) { channame = channame.substr( 0, MAX_CHANNEL_SIZE ); } channelsperview[v].insert(z); if (floatdepth == 32) { exrheaders[currentPart].channels().insert(channame.c_str(), Imf_Foundry::Channel(Imf_Foundry::FLOAT)); } else { exrheaders[currentPart].channels().insert(channame.c_str(), Imf_Foundry::Channel(Imf_Foundry::HALF)); } writerow[v].writable(z); if (floatdepth == 32) { fbufs[currentPart].insert(channame.c_str(), Imf_Foundry::Slice(Imf_Foundry::FLOAT, (char*)(float*)writerow[v][z], sizeof(float), 0)); } else { fbufs[currentPart].insert(channame.c_str(), Imf_Foundry::Slice(Imf_Foundry::HALF, (char*)(&halfwriterow[v * numchannels + curchan][0] - C_datawin.min.x), sizeof(halfwriterow[v * numchannels * curchan][0]), 0)); // Store the row channel index for this view and channel rowChannelIndices[v][z] = curchan; curchan++; } } // Move to the next layer if (_multipartInterleaveMode == eInterleave_Channels) currentPart++; } // Move to the next view if (_multipartInterleaveMode == eInterleave_Channels_Layers) currentPart++; } std::string temp_name; temp_name = createFileHash().c_str(); temp_name += ".tmp"; // Scope use of the output file { // Create an output file Imf_Foundry::MultiPartOutputFile outfile(temp_name.c_str(), &exrheaders[0], numParts); // Write to each part for (int i = 0; i < numParts; ++i) { Imf_Foundry::OutputPart outpart(outfile, i); outpart.setFrameBuffer(fbufs[i]); 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; } const int inputIdx = inputIndex( views[v] ); writerow[v].pre_copy(renderrow[v], channels); iop->inputnget(inputIdx, scanline, bound.x(), bound.r(), channels, renderrow[v], 1.0/wantViews.size()); if (iop->aborted()) break; if (bound.is_constant()) { foreach(z, channels) renderrow[v].erase(z); continue; } 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(inputIdx)->r()) { float* end = renderrow[v].writable(z) + bound.r(); float* start = renderrow[v].writable(z) + iop->input(inputIdx)->r(); while (start < end) { *start = 0; start++; } } if (bound.x() < iop->input(inputIdx)->x()) { float* end = renderrow[v].writable(z) + bound.x(); float* start = renderrow[v].writable(z) + iop->input(inputIdx)->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 { // Get the row channel index for this view and channel int curchan = rowChannelIndices[v][z]; 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]; } } progressFraction( (double(bound.t() - scanline) / (bound.t() - bound.y()) + i) / numParts); } outpart.writePixels(1); } } } // Scope use of the output file 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"); Knob* compressionLevel = Float_knob(f, &_dwCompressionLevel, IRange(0.0f, 500.0f),"dw_compression_level","compression level"); if (compressionLevel) { if ( compressionHasLevel() ) { compressionLevel->enable(true); compressionLevel->show(); } else{ compressionLevel->enable(false); compressionLevel->hide(); } } 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."); Bool_knob(f, &_doNotWriteNukePrefix, "noprefix", "do not attach prefix" ); Tooltip(f, "By default unknown metadata keys have the prefix 'nuke' attached to them before writing them into the file. Enable this option to write the metadata 'as is' without the nuke prefix."); Newline(f); Enumeration_knob(f, &_multipartInterleaveMode, multipartModeLabels, "interleave"); Tooltip(f, "Which groups to interleave in the EXR data." "

'interleave channels, layers and views' for backwards compatibility. " "This writes all the channels to a single part ensuring compatibility with earlier versions of Nuke.

" "

'interleave channels and layers' for forwards compatibility. " "This creates a multi-part file optimised for size.

" "

'interleave channels' to resolve layers into separate parts. " "This creates a multi-part file optimized for read performance.

" "For full compatibility with versions of Nuke using OpenEXR 1.x " "'truncate channel names' should be used to ensure that channels " "do not overflow the name buffer."); 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"); Newline(f); Bool_knob(f, &_writeFullLayerNames, "write_full_layer_names", "write full layer names"); Tooltip(f, "Older versions of Nuke just stored the layer name in the part " "name of multi-part files. Check this option to always write the " "layer name in the channel names following the EXR standard."); SetFlags(f, Knob::DISABLED); Newline(f); Bool_knob(f, &_truncateChannelNames, "truncateChannelNames", "truncate channel names"); Tooltip(f, "Truncate channel names to a maximum of 31 characters for backwards compatibility"); } int exrWriter::knob_changed(Knob *k) { int changed = 0; if ( k == &Knob::showPanel || k->is("metadata") ) { Knob* noPrefixKnob = iop->knob( "noprefix"); // Its possible that replaceable knobs may not exist if file_type knob is blank. This is allowed. if (noPrefixKnob) { noPrefixKnob->enable( ExrMetaDataMode(_metadataMode) >= eAllMetadataExceptInput ); } changed = 1; } if ( k == &Knob::showPanel || k->is("interleave") ) { Knob* writeFullLayerNamesKnob = iop->knob( "write_full_layer_names"); if (writeFullLayerNamesKnob) { writeFullLayerNamesKnob->enable( _multipartInterleaveMode == eInterleave_Channels ); } Knob* truncateChannelNamesKnob = iop->knob( "truncateChannelNames"); if (truncateChannelNamesKnob) { truncateChannelNamesKnob->enable( _multipartInterleaveMode == eInterleave_Channels_Layers_Views ); } changed = 1; } const Knob* compressionKnob = iop->knob("compression"); if (k == &Knob::showPanel || k == compressionKnob) { Knob* compressionLevel = iop->knob("dw_compression_level"); if( compressionLevel ) { if ( compressionHasLevel() ) { compressionLevel->enable(true); compressionLevel->show(); } else{ compressionLevel->enable(false); compressionLevel->hide(); } } changed = 1; } return changed; } 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; }