// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen // SPDX-License-Identifier: BSD-3-Clause #include "vtkHDRReader.h" #include "vtkFileResourceStream.h" #include "vtkImageData.h" #include "vtkImageFlip.h" #include "vtkImagePermute.h" #include "vtkLookupTable.h" #include "vtkMathUtilities.h" #include "vtkNew.h" #include "vtkObjectFactory.h" #include "vtkPointData.h" #include "vtkResourceParser.h" #include "vtkStringScanner.h" #include "vtksys/FStream.hxx" #include "vtksys/SystemTools.hxx" #include VTK_ABI_NAMESPACE_BEGIN vtkStandardNewMacro(vtkHDRReader); #define HDR_DATA_SIZE 3 // The standard XYZ to linear RGB transformation matrix (for D65 illuminant) constexpr float matrixXYZ2RGB[3][3] = { { 3.2404542f, -1.5371385f, -0.4985314f }, { -0.9692660f, 1.8760108f, 0.0415560f }, { 0.0556434f, -0.2040259f, 1.0572252f } }; vtkHDRReader::vtkHDRReader() { this->Gamma = 1.0; this->Exposure = 1.0; this->PixelAspect = 1.0; this->SetDataByteOrderToLittleEndian(); } //------------------------------------------------------------------------------ vtkHDRReader::~vtkHDRReader() = default; //------------------------------------------------------------------------------ void vtkHDRReader::PrintSelf(ostream& os, vtkIndent indent) { this->Superclass::PrintSelf(os, indent); os << indent << "ProgramType: " << this->ProgramType << "\n"; os << indent << "Format: " << this->Format << "\n"; os << indent << "Gamma: " << this->Gamma << "\n"; os << indent << "Exposure: " << this->Exposure << "\n"; os << indent << "PixelAspect: " << this->PixelAspect << "\n"; os << indent << "FlippedX: " << this->FlippedX << "\n"; os << indent << "SwappedAxis: " << this->SwappedAxis << "\n"; } //------------------------------------------------------------------------------ void vtkHDRReader::ExecuteInformation() { // if the user has not set the extent, but has set the VOI // set the zaxis extent to the VOI z axis if (this->DataExtent[4] == 0 && this->DataExtent[5] == 0 && (this->DataVOI[4] || this->DataVOI[5])) { this->DataExtent[4] = this->DataVOI[4]; this->DataExtent[5] = this->DataVOI[5]; } // Setup filename to read the header this->ComputeInternalFileName(this->DataExtent[4]); if (this->InternalFileName == nullptr || this->InternalFileName[0] == '\0') { return; } // Fill headers data, HeaderSize and DataExtent // VTK errors are handled in ReadHeaderData. if (!this->ReadHeaderData()) { return; } this->CloseFile(); // if the user has set the VOI, just make sure its valid if (this->DataVOI[0] || this->DataVOI[1] || this->DataVOI[2] || this->DataVOI[3] || this->DataVOI[4] || this->DataVOI[5]) { if ((this->DataVOI[0] < 0) || (this->DataVOI[1] >= this->GetWidth()) || (this->DataVOI[2] < 0) || (this->DataVOI[3] >= this->GetHeight())) { vtkWarningMacro( "The requested VOI is larger than the file's (" << this->InternalFileName << ") extent "); this->DataVOI[0] = this->DataExtent[0]; this->DataVOI[1] = this->DataExtent[1]; this->DataVOI[2] = this->DataExtent[2]; this->DataVOI[3] = this->DataExtent[3]; } } this->SetDataScalarTypeToFloat(); this->SetNumberOfScalarComponents(3); this->vtkImageReader::ExecuteInformation(); } //------------------------------------------------------------------------------ int vtkHDRReader::CanReadFile(const char* fname) { // get the magic number by reading in a file vtksys::ifstream ifs(fname, vtksys::ifstream::in); if (ifs.fail()) { vtkErrorMacro(<< "Could not open file " << fname); return 0; } // The file must begin with magic number #? if ((ifs.get() != '#') && (ifs.get() != '?')) { ifs.close(); return 0; } ifs.close(); return 1; } //------------------------------------------------------------------------------ // This function reads a data from a file. The datas extent/axes // are assumed to be the same as the file extent/order. void vtkHDRReader::ExecuteDataWithInformation(vtkDataObject* output, vtkInformation* outInfo) { vtkImageData* data = this->AllocateOutputData(output, outInfo); if (this->UpdateExtentIsEmpty(outInfo, output)) { return; } if (this->InternalFileName == nullptr) { vtkErrorMacro(<< "Either a FileName or FilePrefix must be specified."); return; } data->GetPointData()->GetScalars()->SetName("HDRImage"); this->ComputeDataIncrements(); // Only float value for RGBE data float* outPtr = reinterpret_cast(data->GetScalarPointer()); this->HDRReaderUpdate(data, outPtr); } //------------------------------------------------------------------------------ void vtkHDRReader::HDRReaderUpdate(vtkImageData* data, float* outPtr) { vtkIdType outIncr[3]; int outExtent[6]; data->GetExtent(outExtent); data->GetIncrements(outIncr); // Total size of the output in bytes int outPtrSize = (outExtent[1] - outExtent[0] + 1) * (outExtent[3] - outExtent[2] + 1) * this->GetNumberOfScalarComponents(); // Read multiples files for (int idx2 = outExtent[4]; idx2 <= outExtent[5]; ++idx2) { this->ComputeInternalFileName(idx2); // read in a HDR file if (!this->HDRReaderUpdateSlice(outPtr, outExtent)) { return; } if (this->Format == FORMAT_32BIT_RLE_XYZE) { // Convert from xyz to rgb this->ConvertAllDataFromRGBToXYZ(outPtr, outPtrSize); } this->UpdateProgress((idx2 - outExtent[4]) / (outExtent[5] - outExtent[4] + 1.0)); outPtr += outIncr[2]; } if (this->FlippedX) { vtkNew flipXFilter; flipXFilter->SetFilteredAxis(0); flipXFilter->SetInputData(data); flipXFilter->Update(); data->DeepCopy(flipXFilter->GetOutput()); } if (this->SwappedAxis) { // Permute X and Y vtkNew permuteAxisFilter; permuteAxisFilter->SetFilteredAxes(1, 0, 2); permuteAxisFilter->SetInputData(data); permuteAxisFilter->Update(); data->DeepCopy(permuteAxisFilter->GetOutput()); } } void vtkHDRReader::ConvertAllDataFromRGBToXYZ(float* outPtr, int size) { for (int i = 0; i < size; i += HDR_DATA_SIZE) { vtkHDRReader::XYZ2RGB(matrixXYZ2RGB, this->Gamma, outPtr[i], outPtr[i + 1], outPtr[i + 2]); } } //------------------------------------------------------------------------------ // This function reads in one data of data. // templated to handle different data types. bool vtkHDRReader::HDRReaderUpdateSlice(float* outPtr, int* outExt) { vtkResourceStream* stream = this->GetStream(); vtkNew fileStream; if (!stream) { if (!fileStream->Open(this->InternalFileName)) { vtkErrorMacro("Could not open file " << this->InternalFileName); return false; } stream = fileStream; } // Ignore header stream->Seek(this->HeaderSize, vtkResourceStream::SeekDirection::Begin); // Even if we have a smaller extent, the RLE encoding forces us to read all the line width const int width = this->GetWidth(); const int extentWidth = outExt[1] - outExt[0] + 1; // We read only lines that we need (depending on outExt[2] and outExt[3]); int nbLinesLeft = outExt[3] - outExt[2] + 1; const int totalNbLines = nbLinesLeft; float* outPtr2 = outPtr; // decrPtr is the number of byte needed to go to the previous line int decrPtr = 0; if (this->FileLowerLeft) { // Start at the end of outPtr and go up outPtr2 = outPtr + (nbLinesLeft - 1) * extentWidth * 3; decrPtr = 2 * extentWidth * 3; } if ((width < 8) || (width > 0x7fff)) { // File not run length encoded this->ReadAllFileNoRLE(stream, outPtr2, decrPtr, outExt); this->CloseFile(); return true; } // We need to skip some lines int nbSkipLines = outExt[2]; // Store the position in case of a non RLE encoded file vtkTypeInt64 afterHeader = stream->Tell(); // rgbe will hold the value of each channels unsigned char rgbe[4]; // lineBuffer will contain data read from the file // 4 uchar for each pixel of a line std::vector lineBuffer(width * 4); while (nbLinesLeft > 0) { stream->Read(rgbe, sizeof(unsigned char) * 4); // New RLE encoding. // First 4 bytes -> rgbe // The record begins with an unnormalized pixel having two // bytes equal to 2, followed by the upper byte and the lower // byte of the scanline length (which mustbe less than // 32768). A run is indicated by a byte with its high-order // bit set, corresponding to a count with excess 128. A non-run // is indicated with a byte less than 128. if ((rgbe[0] != 2) || (rgbe[1] != 2) || (rgbe[2] & 0x80)) { // if it is not the first line that we read, then there is an error if (nbLinesLeft != totalNbLines) { vtkErrorMacro(<< "HDRReader: First 4 bytes of the line " << totalNbLines - nbLinesLeft << " are wrong"); this->CloseFile(); return false; } // else, it means that the file not run length encoded // Restore position and skip beginning lines stream->Seek( afterHeader + (outExt[2] * this->GetWidth() * 4), vtkResourceStream::SeekDirection::Begin); // Read all the file with no RLE encoding if (!this->ReadAllFileNoRLE(stream, outPtr2, decrPtr, outExt)) { vtkErrorMacro(<< "HDRReader : Bad line data"); this->CloseFile(); return false; } return true; } if (((static_cast(rgbe[2])) << 8 | rgbe[3]) != width) { vtkErrorMacro(<< "HDRReader: Wrong scanline width"); this->CloseFile(); return false; } // File is run length encoded, read line by line // Read each of the four channel into the lineBuffer if (!this->ReadLineRLE(stream, lineBuffer.data())) { vtkErrorMacro(<< "HDRReader : Bad line data"); this->CloseFile(); return false; } // Skip lines if (nbSkipLines > 0) { nbSkipLines--; } else { // Convert lineBuffer to RGB floats this->FillOutPtrRLE(outExt, outPtr2, lineBuffer); // If we need to read bottom up, go to the beginning of the previous line outPtr2 -= decrPtr; nbLinesLeft--; } } this->CloseFile(); return true; } //------------------------------------------------------------------------------ // VTK_DEPRECATED_IN_9_6_0 bool vtkHDRReader::HasError(std::istream* is) { if (!*is) { vtkErrorMacro(<< "HDRReader : Read error"); this->CloseFile(); return true; } return false; } //------------------------------------------------------------------------------ int vtkHDRReader::GetWidth() const { return this->DataExtent[1] - this->DataExtent[0] + 1; } //------------------------------------------------------------------------------ int vtkHDRReader::GetHeight() const { return this->DataExtent[3] - this->DataExtent[2] + 1; } //------------------------------------------------------------------------------ bool vtkHDRReader::ReadHeaderData() { vtkResourceStream* stream = this->GetStream(); vtkNew fileStream; if (!stream) { if (!fileStream->Open(this->InternalFileName)) { vtkErrorMacro("Could not open file " << this->InternalFileName); return false; } stream = fileStream; } stream->Seek(0, vtkResourceStream::SeekDirection::Begin); int headersize = 0; vtkNew parser; parser->SetStream(stream); std::string str; vtkTypeInt64 cur = parser->Tell(); vtkParseResult res = parser->ReadLine(str); if (res == vtkParseResult::Error) { vtkErrorMacro("Error reading program type"); return false; } headersize += parser->Tell() - cur; this->ProgramType = str.substr(2); // Read header while (true) { cur = parser->Tell(); res = parser->ReadLine(str); if (res == vtkParseResult::Error) { vtkErrorMacro("Error reading program type"); return false; } headersize += parser->Tell() - cur; // EOL if (str.empty()) { break; } // skip comments if (str.front() == '#') { continue; } // Split by '=' std::string::size_type equalChar = str.find_first_of('='); std::string key = str.substr(0, equalChar); std::string value = str.substr(equalChar + 1); if (key == "FORMAT") { if (value == "32-bit_rle_rgbe") { this->Format = vtkHDRReader::FORMAT_32BIT_RLE_RGBE; } else if (value == "32-bit_rle_xyze") { this->Format = vtkHDRReader::FORMAT_32BIT_RLE_XYZE; } } else if (key == "GAMMA") { this->Gamma = vtk::scan_value(value)->value(); } else if (key == "EXPOSURE") { this->Exposure = vtk::scan_value(value)->value(); } else if (key == "PIXASPECT") { this->PixelAspect = vtk::scan_value(value)->value(); } } // End of header // Line for width, height, and sign // The X and Y are immediately preceded by a sign which can be used to indicate flipping, // the order of the X and Y indicate rotation cur = parser->Tell(); res = parser->ReadLine(str); if (res == vtkParseResult::Error) { vtkErrorMacro("Error reading program type"); return false; } headersize += parser->Tell() - cur; std::istringstream iss(str); char x, y, signX, signY; int h, w; iss >> std::skipws >> signY >> y >> h >> signX >> x >> w; if (y == 'X') { // Column order -> a 90 degrees turn this->SwappedAxis = true; std::swap(h, w); } // Row order means that X is horizontal axis, and Y is vertical axis if (signX == '-') { // X axis goes from right to left this->FlippedX = true; } // TODO should be a + here, but after tests, it seems that it is inversed if (signY == '-') { this->FileLowerLeft = 1; // Default 0 } // Set header size this->ManualHeaderSize = 1; this->HeaderSize = headersize; // Set size of the image this->DataExtent[0] = 0; this->DataExtent[1] = w - 1; this->DataExtent[2] = 0; this->DataExtent[3] = h - 1; // Set data spacing this->DataSpacing[0] = 1; this->DataSpacing[1] = this->PixelAspect; this->DataSpacing[2] = 1; return true; } //------------------------------------------------------------------------------ void vtkHDRReader::FillOutPtrRLE( int* outExt, float*& outPtr, std::vector& lineBuffer) { unsigned char rgbe[4]; int width = this->GetWidth(); // Convert lineBuffer to RGB floats for (int i = outExt[0]; i <= outExt[1]; ++i) { rgbe[0] = lineBuffer[i]; rgbe[1] = lineBuffer[i + width]; rgbe[2] = lineBuffer[i + 2 * width]; rgbe[3] = lineBuffer[i + 3 * width]; this->RGBE2Float(rgbe, outPtr[0], outPtr[1], outPtr[2]); outPtr += HDR_DATA_SIZE; } } //------------------------------------------------------------------------------ void vtkHDRReader::FillOutPtrNoRLE( int* outExt, float*& outPtr, std::vector& lineBuffer) { // Convert lineBuffer to RGB floats for (int i = outExt[0]; i <= outExt[1]; ++i) { this->RGBE2Float(lineBuffer.data() + 4 * i, outPtr[0], outPtr[1], outPtr[2]); outPtr += HDR_DATA_SIZE; } } //------------------------------------------------------------------------------ bool vtkHDRReader::ReadAllFileNoRLE( vtkResourceStream* stream, float* outPtr, int decrPtr, int* outExt) { std::vector lineBuffer(this->GetWidth() * 4); int nbLinesLeft = outExt[3] - outExt[2] + 1; while (nbLinesLeft > 0) { std::size_t size = sizeof(unsigned char) * lineBuffer.size(); if (stream->Read(lineBuffer.data(), size) != size) { vtkErrorMacro("Unable to read as expected"); return false; } // Convert lineBuffer to RGB floats this->FillOutPtrNoRLE(outExt, outPtr, lineBuffer); // Do nothing if we read top to bottom (decrPtr = 0) // Else go to the beginning of the previous line outPtr -= decrPtr; nbLinesLeft--; } return true; } //------------------------------------------------------------------------------ bool vtkHDRReader::ReadLineRLE(vtkResourceStream* stream, unsigned char* lineBufferPtr) { // A line in RLE is sorted by channels, ie. it begins by all the red, then green, blue, and // exponent It means that we need to read all the line even if we have an smaller extent int width = this->GetWidth(); int count; // The data for each encoded channel unsigned char buffer[2]; for (int i = 0; i < 4; ++i) { unsigned char* ptrEnd = lineBufferPtr + width; while (lineBufferPtr < ptrEnd) { std::size_t size = sizeof(unsigned char) * 2; if (stream->Read(buffer, size) != size) { vtkErrorMacro("Unable to read as expected"); return false; } if (buffer[0] > 128) { // A run of the same value count = buffer[0] - 128; if ((count == 0) || (count > ptrEnd - lineBufferPtr)) { return false; } // Copy the same value in the lineBuffer std::fill(lineBufferPtr, lineBufferPtr + count, buffer[1]); lineBufferPtr += count; } else { // A non run count = buffer[0]; if ((count == 0) || (count > ptrEnd - lineBufferPtr)) { return false; } *lineBufferPtr++ = buffer[1]; // Read count bytes into output if (--count > 0) { size = sizeof(unsigned char) * count; if (stream->Read(lineBufferPtr, size) != size) { vtkErrorMacro("Unable to read as expected"); return false; } lineBufferPtr += count; } } } } return true; } //------------------------------------------------------------------------------ void vtkHDRReader::RGBE2Float(unsigned char* rgbe, float& r, float& g, float& b) { if (rgbe[3]) /*nonzero pixel*/ { float f = std::ldexp(1.0, rgbe[3] - (128 + 8)) / this->Exposure; r = rgbe[0] * f; g = rgbe[1] * f; b = rgbe[2] * f; } else { r = g = b = 0.0; } } //------------------------------------------------------------------------------ void vtkHDRReader::XYZ2RGB( const float convertMatrix[3][3], double gamma, float& r, float& g, float& b) { // Copy initial xyz values float x = r, y = g, z = b; // Convert XYZ to linear RGB float linearR = convertMatrix[0][0] * x + convertMatrix[0][1] * y + convertMatrix[0][2] * z; float linearG = convertMatrix[1][0] * x + convertMatrix[1][1] * y + convertMatrix[1][2] * z; float linearB = convertMatrix[2][0] * x + convertMatrix[2][1] * y + convertMatrix[2][2] * z; // Use sRGB transfer function if gamma is approximately 1.0 (default) // Otherwise use the custom gamma from the file auto gammaTransfer = [gamma](float linear) -> float { if (vtkMathUtilities::NearlyEqual(gamma, 1.0)) { // Standard sRGB transfer function (IEC 61966-2-1:1999) return linear <= 0.0031308f ? 12.92f * linear : 1.055f * std::pow(linear, 1.0f / 2.4f) - 0.055f; } else { // Custom gamma correction from file return std::pow(linear, 1.0f / static_cast(gamma)); } }; // Apply gamma correction and store results r = gammaTransfer(linearR); g = gammaTransfer(linearG); b = gammaTransfer(linearB); // Clamp values to [0,1] range r = std::max(0.0f, std::min(1.0f, r)); g = std::max(0.0f, std::min(1.0f, g)); b = std::max(0.0f, std::min(1.0f, b)); } VTK_ABI_NAMESPACE_END