#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2021 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License // If the library is compiled as a DLL, this ensures symbols are exported #define NUCLEX_PIXELS_SOURCE 1 #include "LibTiffHelpers.h" #if defined(NUCLEX_PIXELS_HAVE_LIBTIFF) #include "Nuclex/Pixels/Errors/FileFormatError.h" #include // for assert() #include // for std::min() #include // for std::make_signed() // Enabling provides emulated TIFFMapFileProc, TIFFUnmapFileProc methods to LibTIFF. // // Originally these methods are intended to set up OS-provided methods where file contents // are actually mapped into an address range in the application's memory (and page fault // handlers then do the actual read). In our case, we just allocate enough memory and load // the whole .tif into it in one go. This might provide a tiny performance advantage because // from that point onwards, all IO *can* be internal to LibTIFF and not require calling our // adapter methods, file seeking, virtual file vtable calls etc. // //#define NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING 1 namespace { // ------------------------------------------------------------------------------------------- // /// /// Stores all information required to adapt a virtual file to the LibTIFF io methods /// struct TiffVirtualFile { /// Whether the const version of the virtual file must be used public: bool IsReadOnly; /// Current position of the file pointer public: std::uint64_t Position; /// Total length of the virtual file public: std::uint64_t Length; #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) /// Buffer holding the whole file *if* mapVirtualFiletoMemory() is called /// /// Obviously, we don't want that. /// public: std::uint8_t *Buffer; #endif // NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING public: union { /// File from which the read method is reading data public: const Nuclex::Pixels::Storage::VirtualFile *ReadableFile; /// File to which the write method is writing data public: Nuclex::Pixels::Storage::VirtualFile *WritableFile; }; /// Exception that happened in one of the LibTIFF file IO callbacks public: std::exception_ptr Exception; /// Error source location reported via the LibTIFF error handler public: std::string ErrorSource; /// Error message reported via the LibTIFF error handler public: std::string ErrorMessage; }; // ------------------------------------------------------------------------------------------- // #if 0 void discardExtendedTiffWarning( ::thandle_t fileHandle, const char *sourceModule, const char *format, ::va_list arguments ) {} #endif // ------------------------------------------------------------------------------------------- // /// Stores the error message reported from LibTIFF on internal errors /// /// File handle on which LibTIFF was working when the error happened /// /// Internal LibTIFF method reporting the error /// printf()-style format string for the error message /// /// Variable-length argument list referenced in the format string /// void handleExtendedTiffError( ::thandle_t fileHandle, const char *sourceModule, const char *format, ::va_list arguments ) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); // Only go through the trouble if the origin of the error was not an exception // of our own virtual file (that got recorded while LibTIFF got a normal error return). if(!virtualFile.Exception) { virtualFile.ErrorSource.assign(sourceModule); virtualFile.ErrorMessage.resize(1024); int characterCount = ::vsnprintf( virtualFile.ErrorMessage.data(), virtualFile.ErrorMessage.size(), format, arguments ); if(characterCount < 0) { const static std::string vsnprintfFailedMessage( u8", original (unexpanded) error message: ", 59 ); // Could do ::explain_vsnprintf() here, but we're way more interested in // why LibTIFF failed to do its work. virtualFile.ErrorMessage.assign(vsnprintfFailedMessage); virtualFile.ErrorMessage.append(format); } else { virtualFile.ErrorMessage.resize(characterCount); } } } // ------------------------------------------------------------------------------------------- // /// Reads data from a virtual file /// User-provided pointer that was passed to LibTIFF /// Buffer into which the data will be placed /// Number of bytes to attempt to read /// The number of bytes actually read from the file ::tmsize_t readFromVirtualFile( ::thandle_t fileHandle, void *buffer, ::tmsize_t byteCount ) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); try { assert(virtualFile.IsReadOnly && u8"Read is performed on file opened for reading"); std::size_t maximumBytesReadable; { std::uint64_t bytesRemaining = virtualFile.Length - virtualFile.Position; if(bytesRemaining < static_cast(byteCount)) { maximumBytesReadable = static_cast(bytesRemaining); } else { maximumBytesReadable = static_cast(byteCount); } } virtualFile.ReadableFile->ReadAt( virtualFile.Position, maximumBytesReadable, reinterpret_cast(buffer) ); virtualFile.Position += maximumBytesReadable; return maximumBytesReadable; } catch(const std::exception &) { virtualFile.Exception = std::current_exception(); return static_cast<::tmsize_t>(-1); } } // ------------------------------------------------------------------------------------------- // /// Writes data to a virtual file /// User-provided pointer that was passed to LibTIFF /// Buffer holding the data that will be written /// Number of bytes that will be written into the file /// The number of bytes actually written into the file ::tmsize_t writeToVirtualFile( ::thandle_t fileHandle, void *buffer, ::tmsize_t byteCount ) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); try { assert(!virtualFile.IsReadOnly && u8"Write is performed on file opened for writing"); virtualFile.WritableFile->WriteAt( virtualFile.Position, static_cast(byteCount), reinterpret_cast(buffer) ); virtualFile.Position += static_cast(byteCount); virtualFile.Length = std::max(virtualFile.Position, virtualFile.Length); return byteCount; } catch(const std::exception &) { virtualFile.Exception = std::current_exception(); return static_cast<::tmsize_t>(-1); } } // ------------------------------------------------------------------------------------------- // /// Changes the position of the virtual file cursor /// User-provided pointer that was passed to LibTIFF /// /// Location (relative to the anchor) where the file cursor will be placed /// /// Anchor relative to which the location will be set /// The new absolute location of the file cursor ::toff_t seekInVirtualFile(::thandle_t fileHandle, ::toff_t location, int referencePoint) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); try { switch(referencePoint) { case SEEK_SET: { virtualFile.Position = std::min( static_cast(location), virtualFile.Length ); break; } case SEEK_CUR: { virtualFile.Position = std::min( static_cast(virtualFile.Position + location), virtualFile.Length ); break; } case SEEK_END: { // On some platforms (ARM), LibTIFF's ::toff_t appears to be an unsigned type, // so we're doing some gymnastics to ensure correct behavior. typedef typename std::make_signed<::toff_t>::type SignedOffsetType; SignedOffsetType signedLocation = *reinterpret_cast(location); // Clamp the location to the size of the virtual file. It's a little bit convoluted // because we do it with unsigned numbers and want to ensure there's not chance of // a negative result or just cast unsigned to signed of half that numeric range. if(signedLocation < 0) { std::uint64_t reverseLocation = static_cast(-signedLocation); if(reverseLocation > virtualFile.Length) { virtualFile.Position = 0; } else { virtualFile.Position = virtualFile.Length - reverseLocation; } } else { virtualFile.Position = virtualFile.Length; } break; } default: { throw std::logic_error(u8"Invalid reference location passed to seekInVirtualFile()"); } } return static_cast<::toff_t>(virtualFile.Position); } catch(const std::exception &) { virtualFile.Exception = std::current_exception(); return static_cast<::toff_t>(-1); } } // ------------------------------------------------------------------------------------------- // /// Closes a virtual file /// User-provided pointer that was passed to LibTIFF /// 0 if the virtual file was closed successfully int closeVirtualFile(::thandle_t fileHandle) { (void)fileHandle; return 0; } // ------------------------------------------------------------------------------------------- // /// Determines the size of a virtual file /// User-provided pointer that was passed to LibTIFF /// The size of the virtual file ::toff_t getVirtualFileSize(::thandle_t fileHandle) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); return static_cast<::toff_t>(virtualFile.Length); } // ------------------------------------------------------------------------------------------- // #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) /// Maps the contents of a virtual file into memory /// User-provided pointer that was passed to LibTIFF /// /// Address of a pointer that will be set to the location of the virtual file's /// contents in memory /// /// Address at which the size of the virtual file will be stored /// 0 if the virtual file was successfully mapped or loaded into memory int mapVirtualFileToMemory(::thandle_t fileHandle, void **contentAddress, ::toff_t *size) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); try { assert((virtualFile.Buffer == nullptr) && u8"File is not already mapped to memory"); { std::unique_ptr content(new std::uint8_t[virtualFile.Length]); if(virtualFile.IsReadOnly) { virtualFile.ReadableFile->ReadAt(0, virtualFile.Length, content.get()); } virtualFile.Buffer = content.release(); } *contentAddress = reinterpret_cast(virtualFile.Buffer); *size = static_cast<::toff_t>(virtualFile.Length); return 0; } catch(const std::exception &error) { virtualFile.Exception = std::current_exception(); return -1; } } #endif // NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING // ------------------------------------------------------------------------------------------- // #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) /// Unmaps a virtual fiel that was mapped into memory /// User-provided pointer that was passed to LibTIFF /// Memory location the virtual file was mapped to /// Reported size of the mapped memory buffer void unmapVirtualFileFromMemory(::thandle_t fileHandle, void *contentAddress, ::toff_t size) { TiffVirtualFile &virtualFile = *reinterpret_cast(fileHandle); try { std::uint8_t *content = reinterpret_cast(contentAddress); if(!virtualFile.IsReadOnly) { virtualFile.WritableFile->WriteAt(0, static_cast(size), content); } virtualFile.Buffer = nullptr; delete[] content; } catch(const std::exception &error) { ::TIFFErrorExt(fileHandle, u8"unmapVirtualFileFromMemory()", error.what()); virtualFile.Exception = std::current_exception(); } } #endif // NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING // ------------------------------------------------------------------------------------------- // } // anonymous namespace namespace Nuclex { namespace Pixels { namespace Storage { namespace Tiff { // ------------------------------------------------------------------------------------------- // bool Helpers::IsValidTiffHeader(const std::uint8_t *fileHeader) { return ( ( ( (fileHeader[0] == 0x49) && // 1 Byte order mark (little endian) (fileHeader[1] == 0x49) // 1 ) || ( (fileHeader[0] == 0x4D) && // 1 Byte order mark (big endian) (fileHeader[1] == 0x4D) // 1 ) ) && ( ( (fileHeader[0] == 0x49) && // (fileHeader[2] == 0x2a) && // 2 Magic number (little endian) (fileHeader[3] == 0x00) // 2 ) || ( (fileHeader[0] == 0x4D) && (fileHeader[2] == 0x00) && // 2 Magic number (big endian) (fileHeader[3] == 0x2a) // 2 ) ) && ( ( (fileHeader[0] == 0x49) && // ((fileHeader[7] & 1) == 0) // 3 Offset to first IFD (little endian) ) || ( // Must be aligned to word (fileHeader[0] == 0x4D) && // ((fileHeader[4] & 1) == 0) // 3 Offset to first IFD (big endian) ) // Must be aligned to word ) ); } // ------------------------------------------------------------------------------------------- // ::TIFF *Helpers::OpenForReading( const Nuclex::Pixels::Storage::VirtualFile &file, bool headerOnly /* = false */ ) { ::TIFFSetErrorHandler(nullptr); ::TIFFSetErrorHandlerExt(&handleExtendedTiffError); //::TIFFSetWarningHandlerExt(&discardExtendedTiffWarning); // Set up a new TiffVirtualFile wrapper that will work as an adapter from // LibTIFF's virtual IO functions to this library's VirtualFile interface std::unique_ptr wrapper = std::make_unique(); { wrapper->IsReadOnly = true; wrapper->ReadableFile = &file; wrapper->Position = 0; wrapper->Length = file.GetSize(); #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) wrapper->Buffer = nullptr; #endif wrapper->ErrorSource.reserve(64); wrapper->ErrorMessage.reserve(1024); } // Now ask LibTIFF to create its own abstracted file. It will hold and own // the TiffVirtualFile instance, which is accessed by our adapter methods // whenever LibTIFF calls any of them to perform its IO tasks. TIFF *tiffFile = ::TIFFClientOpen( u8"", headerOnly ? "rmh" : "rm", reinterpret_cast<::thandle_t>(wrapper.get()), &readFromVirtualFile, &writeToVirtualFile, &seekInVirtualFile, &closeVirtualFile, &getVirtualFileSize, #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) &mapVirtualFileToMemory, &unmapVirtualFileFromMemory #else nullptr, // TIFFMapFileProc nullptr // TIFFUnmapFileProc #endif ); if(tiffFile == nullptr) { if(wrapper->Exception) { std::rethrow_exception(wrapper->Exception); } else if(!wrapper->ErrorMessage.empty()) { const static std::string messageStart( u8"Error reported by LibTIFF opening .tif file for reading: ", 57 ); std::string message; message.reserve(57 + wrapper->ErrorMessage.length()); message.assign(messageStart); message.append(wrapper->ErrorMessage); throw Errors::FileFormatError(message); } else { throw Errors::FileFormatError(u8"Could not open .tif file for reading via LibTIFF"); } } else { wrapper.release(); } return tiffFile; } // ------------------------------------------------------------------------------------------- // // Set up a new TiffVirtualFile wrapper that will work as an adapter from // LibTIFF's virtual IO functions to this library's VirtualFile interface ::TIFF *Helpers::OpenForWriting(Nuclex::Pixels::Storage::VirtualFile &file) { ::TIFFSetErrorHandler(nullptr); ::TIFFSetErrorHandlerExt(&handleExtendedTiffError); //::TIFFSetWarningHandlerExt(&discardExtendedTiffWarning); std::unique_ptr wrapper(new TiffVirtualFile()); { wrapper->IsReadOnly = false; wrapper->WritableFile = &file; wrapper->Position = 0; wrapper->Length = 0; //file.GetSize(); #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) wrapper->Buffer = nullptr; #endif wrapper->ErrorSource.reserve(64); wrapper->ErrorMessage.reserve(1024); } TIFF *tiffFile = ::TIFFClientOpen( u8"", "wm", reinterpret_cast<::thandle_t>(wrapper.get()), &writeToVirtualFile, &readFromVirtualFile, &seekInVirtualFile, &closeVirtualFile, &getVirtualFileSize, #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) &mapVirtualFileToMemory, &unmapVirtualFileFromMemory #else nullptr, // TIFFMapFileProc nullptr // TIFFUnmapFileProc #endif ); if(tiffFile == nullptr) { if(wrapper->Exception) { std::rethrow_exception(wrapper->Exception); } else if(!wrapper->ErrorMessage.empty()) { const static std::string messageStart( u8"Error reported by LibTIFF opening .tif file for writing: ", 57 ); std::string message; message.reserve(57 + wrapper->ErrorMessage.length()); message.assign(messageStart); message.append(wrapper->ErrorMessage); throw Errors::FileFormatError(message); } else { throw Errors::FileFormatError(u8"Could not open .tif file for writing via LibTIFF"); } } else { wrapper.release(); } return tiffFile; } // ------------------------------------------------------------------------------------------- // void Helpers::Close(::TIFF *tiffFile) { ::thandle_t clientData = ::TIFFClientdata(tiffFile); TiffVirtualFile *virtualFile = reinterpret_cast(clientData); #if defined(NUCLEX_PIXELS_ENABLE_LIBTIFF_MEMORY_MAPPING) if(virtualFile->Buffer != nullptr) { if(!virtualFile->IsReadOnly) { virtualFile->WritableFile->WriteAt(0, virtualFile->Length, virtualFile->Buffer); } delete[] virtualFile->Buffer; } #endif delete virtualFile; } // ------------------------------------------------------------------------------------------- // PixelFormat Helpers::GetClosestPixelFormat(TIFF *tiffFile) { // Obtain informations on the color model used, the number of color channels and // the width of the individual color channels by querying some tags from the TIFF file. std::uint16_t bitsPerColorChannel, colorChannelsPerPixel, photometricInterpretation; { int result = ::TIFFGetField(tiffFile, TIFFTAG_BITSPERSAMPLE, &bitsPerColorChannel); if(result == 0) { throw Errors::FileFormatError(u8"TIFF file has no bits per sample tag. Corrupt file?"); } result = ::TIFFGetField(tiffFile, TIFFTAG_SAMPLESPERPIXEL, &colorChannelsPerPixel); if(result == 0) { throw Errors::FileFormatError(u8"TIFF file has no samples per pixel tag. Corrupt file?"); } result = ::TIFFGetField(tiffFile, TIFFTAG_PHOTOMETRIC, &photometricInterpretation); if(result == 0) { // Behavior identical to tif_getimage.c, TIFFRGBAImageOk() if(colorChannelsPerPixel == 1) { photometricInterpretation = PHOTOMETRIC_MINISBLACK; } else if(colorChannelsPerPixel == 3) { photometricInterpretation = PHOTOMETRIC_RGB; } else { throw Errors::FileFormatError( u8"TIFF file has no photometric interpretation tag. Corrupt file?" ); } } } // Now use the informations gathered to determine the pixel format of this library // that is the closest representation to the TIFF's pixel format. Note that this isn't // required to be an exact match, it should merely avoid discarding information. // If needed, the TIFF reading code will translate the pixel format on-the-fly. switch(photometricInterpretation) { case PHOTOMETRIC_RGB: { if(bitsPerColorChannel < 9) { if(colorChannelsPerPixel == 4) { return PixelFormat::R8_G8_B8_A8_Unsigned; } else { return PixelFormat::R8_G8_B8_Unsigned; } } else { return PixelFormat::R16_G16_B16_A16_Unsigned; } } case PHOTOMETRIC_PALETTE: { return PixelFormat::R8_G8_B8_Unsigned; } case PHOTOMETRIC_MASK: { if(bitsPerColorChannel < 9) { return PixelFormat::A8_Unsigned; } else { return PixelFormat::A16_Unsigned_Native16; } } // The formats below could be supported - either by writing specific conversion // code or perhaps LibTIFF even offers to translate this to RGBA upon loading? case PHOTOMETRIC_YCBCR: { // CCIR 601 YUV throw Errors::FileFormatError( u8"Unsupported pixel format: YCbCr color model not supported" ); } case PHOTOMETRIC_CIELAB: { // 1976 CIE L*a*b* throw Errors::FileFormatError( u8"Unsupported pixel format: CIE Lab color model not supported" ); } case PHOTOMETRIC_ICCLAB: { // ICC L*a*b* (Adobe TIFF Technote 4) throw Errors::FileFormatError( u8"Unsupported pixel format: ICC Lab color model not supported" ); } case PHOTOMETRIC_ITULAB: { // ITU L*a*b* throw Errors::FileFormatError( u8"Unsupported pixel format: ITU Lab color model not supported" ); } default: { throw Errors::FileFormatError( u8"Unsupported pixel format (exotic or unknown photometric interpretation tag)" ); } } } // ------------------------------------------------------------------------------------------- // }}}} // namespace Nuclex::Pixels::Storage::Tiff #endif // defined(NUCLEX_PIXELS_HAVE_LIBTIFF)