Skip to content

Commit

Permalink
File I/O documentation and code cleanup. Fixes file name extension ha…
Browse files Browse the repository at this point in the history
…ndling.
  • Loading branch information
crisluengo committed Mar 13, 2024
1 parent 120c3c0 commit 68f4d8f
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 57 deletions.
7 changes: 7 additions & 0 deletions changelogs/diplib_next.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ title: "Changes DIPlib 3.x.x"
The functions `dip::ImageRead()` and `dip::ImageWrite()` now recognize PNG files. Previously, PNG files could only
be read through *DIPjavaio* with Bio-Formats.

- Added function `dip::FileAppendExtension()`.

### Changed functionality

- The 'diverging' color map in `dip::ApplyColorMap()` switched from CET-D08 to CET-D07 (i.e. using yellow
Expand All @@ -24,6 +26,8 @@ title: "Changes DIPlib 3.x.x"
`dip::Distribution::ConstSample` as argument. `dip::Distribution::MutableIterator` and `dip::Distribution::ConstIterator`
are two template specializations, the first one replaces the use of `dip::Distribution::Iterator`.

- `dip::FileAddExtension()` has been deprecated, the functionality is not needed in *DIPlib*.

### Bug fixes

- XYZ to Yxy conversion and its inverse were reversed, and Yxy to gray conversion was incorrect. This means
Expand All @@ -45,6 +49,9 @@ title: "Changes DIPlib 3.x.x"

- Equality comparison of `dip::PixelSize` objects was documented to use a tolerance, but it wasn't using one.

- `dip::ImageReadTIFF()`, `dip::ImageReadJPEG()` and `dip::ImageReadNPY()` would replace the extension of the given
file name instead of simply appending an appropriate extension, as described in the documentation.

### Updated dependencies

- Updated included *zlib* to version 1.3.1. This version no longer uses K&R function declarations, which generated
Expand Down
31 changes: 21 additions & 10 deletions include/diplib/file_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct FileInformation {
/// and data type also supported by *DIPlib*, and therefore is used as the default image file format.
///
/// The function tries to open `filename` as given first, and if that fails, it appends ".ics" to the
/// name and tries again.
/// name and tries again. If `filename` has an ".ids" extension, it is replaced with ".ics".
///
/// `roi` can be set to read in a subset of the pixels in the file. If only one array element is given,
/// it is used for all dimensions. An empty array indicates that all pixels should be read. Otherwise,
Expand Down Expand Up @@ -123,7 +123,7 @@ DIP_NODISCARD inline Image ImageReadICS(
}

/// \brief Reads image information and metadata from the ICS file `filename`, without reading the actual
/// pixel data. See \ref dip::ImageReadICS for more details.
/// pixel data. See \ref dip::ImageReadICS for more details on the file format and the handling of `filename`.
DIP_EXPORT FileInformation ImageReadICSInfo( String const& filename );

/// \brief Returns true if the file `filename` is an ICS file.
Expand All @@ -143,7 +143,7 @@ DIP_EXPORT bool ImageIsICS( String const& filename );
/// The ".ics" extension will be added to `filename` if it's not there. Overwrites any other file with the same name.
///
/// `history` is a set of strings that are written as history lines, and will be recovered by the
/// \ref dip::ImageReadICSInfo function.
/// \ref dip::FileInformation struct when reading with \ref dip::ImageReadICS or \ref dip::ImageReadICSInfo.
///
/// Set `significantBits` only if the number of significant bits is different from the full range of the data
/// type of `image` (use 0 otherwise). For example, it can be used to specify that a camera has produced
Expand Down Expand Up @@ -287,7 +287,7 @@ DIP_NODISCARD inline Image ImageReadTIFFSeries(
}

/// \brief Reads image information and metadata from the TIFF file `filename`, without reading the actual
/// pixel data.
/// pixel data. See \ref dip::ImageReadTIFF for more details on the handling of `filename` and `imageNumber`.
DIP_EXPORT FileInformation ImageReadTIFFInfo( String const& filename, dip::uint imageNumber = 0 );

/// \brief Returns true if the file `filename` is a TIFF file.
Expand All @@ -296,7 +296,10 @@ DIP_EXPORT bool ImageIsTIFF( String const& filename );
/// \brief Writes `image` as a TIFF file.
///
/// The TIFF image file format is very flexible in how data can be written, but is limited to multiple pages
/// of 2D images. A 3D image will be written as a multi-page TIFF file (not yet implemented).
/// of 2D images. A 3D image will be written as a multi-page TIFF file.
///
/// !!! warning
/// Writing of 3D images has not yet been implemented.
///
/// A tensor image will be written as an image with multiple samples per pixel, but the tensor shape will be lost.
/// If the tensor image has color space information, and it is one of the few color spaces known to the TIFF
Expand Down Expand Up @@ -355,7 +358,7 @@ DIP_NODISCARD inline Image ImageReadJPEG(
}

/// \brief Reads image information and metadata from the JPEG file `filename`, without reading the actual
/// pixel data.
/// pixel data. See \ref dip::ImageReadJPEG for more details on the handling of `filename`.
DIP_EXPORT FileInformation ImageReadJPEGInfo( String const& filename );

/// \brief Returns true if the file `filename` is a JPEG file.
Expand Down Expand Up @@ -403,7 +406,7 @@ DIP_NODISCARD inline Image ImageReadPNG(
}

/// \brief Reads image information and metadata from the PNG file `filename`, without reading the actual
/// pixel data.
/// pixel data. See \ref dip::ImageReadPNG for more details on the handling of `filename`.
DIP_EXPORT FileInformation ImageReadPNGInfo( String const& filename );

/// \brief Returns true if the file `filename` is a PNG file.
Expand Down Expand Up @@ -476,7 +479,7 @@ DIP_NODISCARD inline Image ImageReadNPY(
}

/// \brief Reads array information (size and data type) from the NumPy NPY file `filename`, without reading the actual
/// pixel data.
/// pixel data. See \ref dip::ImageReadNPY for more details on the handling of `filename`.
DIP_EXPORT FileInformation ImageReadNPYInfo( String const& filename );

/// \brief Returns true if the file `filename` is a NPY file.
Expand Down Expand Up @@ -511,7 +514,7 @@ inline String::size_type FileGetExtensionPosition(
return sep + 1 + pos;
}

/// \brief Returns true if the file has an extension.
/// \brief Returns true if the file name has an extension.
inline bool FileHasExtension(
String const& filename
) {
Expand All @@ -529,7 +532,7 @@ inline String FileGetExtension(
return filename.substr( pos + 1 );
}

/// \brief Returns true if the file has the given extension.
/// \brief Returns true if the file name has the given extension.
inline bool FileCompareExtension(
String const& filename,
String const& extension
Expand All @@ -538,6 +541,7 @@ inline bool FileCompareExtension(
}

/// \brief Adds the given extension to the file name, replacing any existing extension.
[[ deprecated( "DIPlib doesn't need functionality to replace an extension." ) ]]
inline String FileAddExtension(
String const& filename,
String const& extension
Expand All @@ -546,6 +550,13 @@ inline String FileAddExtension(
return filename.substr( 0, pos ) + String{ '.' } + extension;
}

/// \brief Appends the given extension to the file name.
inline String FileAppendExtension(
String const& filename,
String const& extension
) {
return filename + String{ '.' } + extension;
}

/// \endgroup

Expand Down
3 changes: 2 additions & 1 deletion include/diplib/linear.h
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,8 @@ DIP_NODISCARD inline Image GaborFIR(
/// Set `process` to false for those dimensions that should not be filtered. This is equivalent to setting
/// `sigmas` to 0 for those dimensions.
///
/// The `order` parameter is not yet implemented. It is ignored and assumed 0 for each dimension.
/// !!! warning
/// The `filterOrder` parameter is not yet implemented. It is ignored and assumed 0 for each dimension.
///
/// \see dip::Gabor2D, dip::GaborFIR
///
Expand Down
12 changes: 9 additions & 3 deletions include/diplib/simple_file_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,25 @@ DIP_NODISCARD inline Image ImageRead(
/// - `"png"`: Create a PNG file, use \ref dip::ImageWritePNG.
/// - `"npy"`: Create a NumPy NPY file, use \ref dip::ImageWriteNPY.
/// - `""`: Select the format by looking at the file name extension. If no extension is
/// present, it defaults to ICS version 2. This is the default.
/// present, it uses ICS version 2. This is the default.
///
/// The ICS format can store any image, with all its information, such that reading the file using \ref dip::ImageRead
/// or \ref dip::ImageReadICS yields an image that is identical (except the strides might be different).
///
/// The TIFF format can store 2D images, as well as 3D images as a series of 2D slides (not yet implemented).
/// The TIFF format can store 2D images, as well as 3D images as a series of 2D slides (but this is not yet implemented).
/// A limited set of color spaces are recognized, other color images are stored without color space information.
/// Complex data is not supported, other data types are. But note that images other than 8-bit or 16-bit unsigned
/// integer lead to files that are not recognized by most readers.
///
/// The JPEG format can store 2D images. Tensor images are always tagged as sRGB. Most metadata will be lost.
/// The JPEG format can store 2D images with 1 or 3 tensor elements. Tensor images are always tagged as sRGB. Most
/// metadata will be lost.
/// Image data is converted to 8-bit unsigned integer, without scaling.
///
/// The PNG format can store 2D images with 1 to 4 tensor elements. Images with 3 or 4 tensor elements are always
/// tagged as sRGB, those with 1 or 2 as grayscale. The 2nd or 4th tensor element is the alpha channel.
/// Image data is converted to 8-bit unsigned integer, without scaling, unless the image is binary or 16-bit unsigned
/// integer.
///
/// The NPY format stores raw pixel data for a scalar image. Tensor images cannot be written. All metadata will be lost.
///
/// `compression` determines the compression method used when writing the pixel data. It can be one of the
Expand Down
4 changes: 4 additions & 0 deletions src/file_io/file_io_support.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
* limitations under the License.
*/

#include <algorithm>

#include "diplib.h"
#include "diplib/file_io.h"
#include "file_io_support.h"

namespace dip {
Expand Down
8 changes: 7 additions & 1 deletion src/file_io/ics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@

#ifdef DIP_CONFIG_HAS_ICS

#include <cstdlib> // std::strtoul
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <string>
#include <utility>
#include <vector>

#include "diplib.h"
#include "diplib/file_io.h"
Expand Down
20 changes: 12 additions & 8 deletions src/file_io/jpeg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@

#ifdef DIP_CONFIG_HAS_JPEG

#include <cstdio>
#include <cstring>
#include <utility>
#include <vector>

#include "diplib.h"
#include "diplib/file_io.h"

Expand Down Expand Up @@ -62,13 +67,12 @@ class JpegInput {
: filename_( std::move( filename )), jerr_( error_msg ) {
infile_ = std::fopen( filename_.c_str(), "rb" );
if( infile_ == nullptr ) {
if( !FileHasExtension( filename_ )) {
filename_ = FileAddExtension( filename_, "jpg" ); // Try with "jpg" extension
filename_ = FileAppendExtension( filename_, "jpg" ); // Try with "jpg" extension
infile_ = std::fopen( filename_.c_str(), "rb" );
if( infile_ == nullptr ) {
filename_.back() = 'e';
filename_.push_back( 'g' ); // Try with "jpeg" extension
infile_ = std::fopen( filename_.c_str(), "rb" );
if( infile_ == nullptr ) {
filename_ = FileAddExtension( filename_, "jpeg" ); // Try with "jpeg" extension
infile_ = std::fopen( filename_.c_str(), "rb" );
}
}
}
if( infile_ == nullptr ) {
Expand Down Expand Up @@ -109,12 +113,12 @@ class JpegInput {

class JpegOutput {
public:
explicit JpegOutput( String const& filename, std::jmp_buf const& setjmp_buffer, String& error_msg ) : jerr_( error_msg ) {
JpegOutput( String const& filename, std::jmp_buf const& setjmp_buffer, String& error_msg ) : jerr_( error_msg ) {
// Open the file for writing
if( FileHasExtension( filename )) {
outfile_ = std::fopen(filename.c_str(), "wb");
} else {
outfile_ = std::fopen( FileAddExtension( filename, "jpg" ).c_str(), "wb" );
outfile_ = std::fopen( FileAppendExtension( filename, "jpg" ).c_str(), "wb" );
}
if( outfile_ == nullptr ) {
DIP_THROW_RUNTIME( "Could not open file for writing" );
Expand Down
32 changes: 19 additions & 13 deletions src/file_io/npy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@
// See https://github.com/rogersce/cnpy for cnpy
// See https://github.com/llohse/libnpy for libnpy

#include <regex>
#include <cstring>
#include <fstream>
#include <istream>
#include <numeric>
#include <ostream>
#include <regex>
#include <sstream>
#include <string>
#include <utility>

#include "diplib.h"
#include "diplib/file_io.h"
Expand Down Expand Up @@ -86,8 +93,9 @@ char TypeChar( DataType dt ) {
case DT_SCOMPLEX:
case DT_DCOMPLEX:
return 'c';
default:
DIP_THROW( E::NOT_REACHABLE );
}
DIP_THROW( "Unknown data type" ); // This should never happen, but GCC complains.
}

void ReverseArray( UnsignedArray& array ) {
Expand All @@ -97,7 +105,7 @@ void ReverseArray( UnsignedArray& array ) {
}
}

std::string CreateHeaderDict( DataType dataType, UnsignedArray const& sizes, bool fortranOrder ) {
String CreateHeaderDict( DataType dataType, UnsignedArray const& sizes, bool fortranOrder ) {
std::ostringstream out;
out << "{'descr': '" << SystemEndianChar();
out << TypeChar( dataType );
Expand All @@ -112,10 +120,10 @@ std::string CreateHeaderDict( DataType dataType, UnsignedArray const& sizes, boo

void WriteHeader( std::ostream& ostream, DataType dataType, UnsignedArray const& sizes, bool fortranOrder ) {
WriteMagic( ostream );
std::string headerDict = CreateHeaderDict( dataType, sizes, fortranOrder );
String headerDict = CreateHeaderDict( dataType, sizes, fortranOrder );
dip::uint length = magicStringLength + 3 + headerDict.length();
dip::uint padding = 64 - length % 64;
headerDict += std::string( padding, ' ' );
headerDict += String( padding, ' ' );
headerDict += '\n';
length = headerDict.length();
ostream.put( static_cast< char >( length & 0xffu ));
Expand All @@ -129,7 +137,7 @@ void ReadHeader( std::istream& istream, DataType& dataType, UnsignedArray& sizes
istream.read( buf, 2 );
DIP_THROW_IF( !istream, BAD_NPY_HEADER );
dip::uint length = static_cast< dip::uint >( buf[ 0 ] ) + ( static_cast< dip::uint >( buf[ 1 ] ) << 8u );
std::string headerDict( length, '\0' );
String headerDict( length, '\0' );
istream.read( &headerDict[ 0 ], static_cast< dip::sint >( length ));
//std::cout << "Header length: " << length << '\n';
//std::cout << "Header contents: >>" << headerDict << "<<\n";
Expand All @@ -148,7 +156,7 @@ void ReadHeader( std::istream& istream, DataType& dataType, UnsignedArray& sizes
std::regex_search( headerDict, res, regex_shape );
DIP_THROW_IF ( res.size() != 2, "Failed to parse NPY header keyword 'shape'" );
//std::cout << "shape: " << res.str() << '\n' << " : ";
std::string shapeStr = res.str( 1 );
String shapeStr = res.str( 1 );
std::regex regex_num( "[0-9]+" );
for( auto it = std::sregex_iterator( shapeStr.begin(), shapeStr.end(), regex_num ); it != std::sregex_iterator{}; ++it ) {
//std::cout << it->str() << ", ";
Expand All @@ -166,7 +174,7 @@ void ReadHeader( std::istream& istream, DataType& dataType, UnsignedArray& sizes
// A is the endianness char, one of littleEndianChar, bigEndianChar or noEndianChar
// B is the data type char
// xxx is the number of bytes for the data type
std::string descr = res.str( 1 );
String descr = res.str( 1 );
swapEndianness = !(( descr[ 0 ] == SystemEndianChar() ) || ( descr[ 0 ] == noEndianChar )); // if unknown endianness, just read in as-is
dip::uint bytes = std::stoul( descr.substr( 2 ));
if( bytes == 1 ) {
Expand Down Expand Up @@ -257,10 +265,8 @@ std::ifstream OpenNPYForReading( String filename, FileInformation& fileInformati
fileInformation.name = std::move( filename );
std::ifstream istream( fileInformation.name, std::ifstream::binary );
if( !istream ) {
if( !FileHasExtension( fileInformation.name )) {
fileInformation.name = FileAddExtension( fileInformation.name, "npy" );
istream.open( fileInformation.name, std::ifstream::binary );
}
fileInformation.name = FileAppendExtension( fileInformation.name, "npy" );
istream.open( fileInformation.name, std::ifstream::binary );
if( !istream ) {
DIP_THROW_RUNTIME( "Could not open the specified NPY file" );
}
Expand Down Expand Up @@ -358,7 +364,7 @@ void ImageWriteNPY(
if( FileHasExtension( filename )) {
ostream.open( filename, std::ofstream::binary );
} else {
ostream.open( FileAddExtension( filename, "npy" ), std::ofstream::binary );
ostream.open( FileAppendExtension( filename, "npy" ), std::ofstream::binary );
}
if( !ostream ) {
DIP_THROW_RUNTIME( "Could not open specified NPY file for writing" );
Expand Down
8 changes: 3 additions & 5 deletions src/file_io/png.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ class PngInput {
PngInput( String filename ) : filename_( std::move( filename )) {
infile_ = std::fopen( filename_.c_str(), "rb" );
if( infile_ == nullptr ) {
if( !FileHasExtension( filename_ )) {
filename_ = FileAddExtension( filename_, "png" ); // Try with "png" extension
infile_ = std::fopen( filename_.c_str(), "rb" );
}
filename_ = FileAppendExtension( filename_, "png" ); // Try with "png" extension
infile_ = std::fopen( filename_.c_str(), "rb" );
}
if( infile_ == nullptr ) {
DIP_THROW_RUNTIME( "Could not open the specified PNG file" );
Expand Down Expand Up @@ -95,7 +93,7 @@ class PngOutput {
if( FileHasExtension( filename )) {
outfile_ = std::fopen(filename.c_str(), "wb");
} else {
outfile_ = std::fopen( FileAddExtension( filename, "png" ).c_str(), "wb" );
outfile_ = std::fopen( FileAppendExtension( filename, "png" ).c_str(), "wb" );
}
if( outfile_ == nullptr ) {
DIP_THROW_RUNTIME( "Could not open file for writing" );
Expand Down
Loading

0 comments on commit 68f4d8f

Please sign in to comment.