diff --git a/include/SipiImage.h b/include/SipiImage.h index 83a71827..b1c89baf 100755 --- a/include/SipiImage.h +++ b/include/SipiImage.h @@ -56,7 +56,10 @@ */ namespace Sipi { + // Used for 8 bits per sample (color channel) images typedef unsigned char byte; + + // Used for 16 bits per sample (color channel) images typedef unsigned short word; /*! Implements the values of the photometric tag of the TIFF format */ @@ -178,36 +181,27 @@ class SipiImageError final : public std::exception { * is being modified! */ class SipiImage { - friend class SipiIcc; //!< We need SipiIcc as friend class - friend class SipiIOTiff; //!< I/O class for the TIFF file format - friend class SipiIOJ2k; //!< I/O class for the JPEG2000 file format - //friend class SipiIOOpenJ2k; //!< I/O class for the JPEG2000 file format - friend class SipiIOJpeg; //!< I/O class for the JPEG file format - friend class SipiIOPng; //!< I/O class for the PNG file format - private: static std::unordered_map > io; //!< member variable holding a map of I/O class instances for the different file formats static byte bilinn(byte buf[], register int nx, register double x, register double y, register int c, register int n); - static word bilinn(word buf[], register int nx, register double x, register double y, register int c, register int n); - void ensure_exif(); protected: - size_t nx; //!< Number of horizontal pixels (width) - size_t ny; //!< Number of vertical pixels (height) - size_t nc; //!< Total number of samples per pixel - size_t bps; //!< bits per sample. Currently only 8 and 16 are supported - std::vector es; //!< meaning of extra samples - Orientation orientation; + size_t nx; //!< Number of horizontal pixels (width) + size_t ny; //!< Number of vertical pixels (height) + size_t nc; //!< Total number of samples per pixel + size_t bps; //!< bits per sample. Currently only 8 and 16 are supported + std::vector es; //!< meaning of the extra samples (channels) + Orientation orientation; //!< Orientation of the image PhotometricInterpretation photo; //!< Image type, that is the meaning of the channels - byte *pixels; //!< Pointer to block of memory holding the pixels - std::shared_ptr xmp; //!< Pointer to instance SipiXmp class (\ref SipiXmp), or NULL - std::shared_ptr icc; //!< Pointer to instance of SipiIcc class (\ref SipiIcc), or NULL - std::shared_ptr iptc; //!< Pointer to instance of SipiIptc class (\ref SipiIptc), or NULL - std::shared_ptr exif; //!< Pointer to instance of SipiExif class (\ref SipiExif), or NULL - SipiEssentials emdata; //!< Metadata to be stored in file header - shttps::Connection *conobj; //!< Pointer to HTTP connection - SkipMetadata skip_metadata; //!< If true, all metadata is stripped off + byte *pixels; //!< Pointer to block of memory holding the pixels (allways in big-endian format if interpreted as 16 bit/sample) + std::shared_ptr xmp; //!< Pointer to instance SipiXmp class (\ref SipiXmp), or NULL + std::shared_ptr icc; //!< Pointer to instance of SipiIcc class (\ref SipiIcc), or NULL + std::shared_ptr iptc; //!< Pointer to instance of SipiIptc class (\ref SipiIptc), or NULL + std::shared_ptr exif; //!< Pointer to instance of SipiExif class (\ref SipiExif), or NULL + SipiEssentials emdata; //!< Metadata to be stored in file header + shttps::Connection *conobj; //!< Pointer to HTTP connection + SkipMetadata skip_metadata; //!< If true, all metadata is stripped off public: // @@ -234,21 +228,11 @@ class SipiImageError final : public std::exception { */ SipiImage(size_t nx_p, size_t ny_p, size_t nc_p, size_t bps_p, PhotometricInterpretation photo_p); - /*! - * Checks if the actual mimetype of an image file corresponds to the indicated mimetype and the extension of the filename. - * This function is used to check if information submitted with a file are actually valid. - */ - /* ToDo: Delete - static bool checkMimeTypeConsistency(const std::string &path, const std::string &given_mimetype, - const std::string &filename); - */ - /*! * Getter for nx */ inline size_t getNx() const { return nx; }; - /*! * Getter for ny */ @@ -270,9 +254,12 @@ class SipiImageError final : public std::exception { */ inline size_t getBps() const { return bps; } + /** + * Get the exif metadata of the image. + * \return exif metadata + */ inline std::shared_ptr getExif() const { return exif; }; - /*! * Get orientation * @return Returns orientation tag @@ -281,9 +268,9 @@ class SipiImageError final : public std::exception { /*! * Set orientation parameter - * @param ori orientation to be set + * @param value orientation value to be set */ - inline void setOrientation(Orientation ori) { orientation = ori; }; + inline void setOrientation(Orientation value) { orientation = value; }; /*! @@ -364,7 +351,7 @@ class SipiImageError final : public std::exception { * * \param[in] smd Logical "or" of bitmasks for metadata to be skipped */ - inline void setSkipMetadata(SkipMetadata smd) { skip_metadata = smd; }; + void setSkipMetadata(SkipMetadata smd) { skip_metadata = smd; }; /*! @@ -372,18 +359,18 @@ class SipiImageError final : public std::exception { * * \param[in] conn_p Pointer to connection data */ - inline void connection(shttps::Connection *conobj_p) { conobj = conobj_p; }; + void connection(shttps::Connection *conobj_p) { conobj = conobj_p; }; /*! * Retrieves the connection parameters of the mongoose server from an Image instance * * \returns Pointer to connection data */ - inline shttps::Connection *connection() const { return conobj; }; + shttps::Connection *connection() const { return conobj; }; - inline void essential_metadata(const SipiEssentials &emdata_p) { emdata = emdata_p; } + void essential_metadata(const SipiEssentials &emdata_p) { emdata = emdata_p; } - inline SipiEssentials essential_metadata(void) { return emdata; } + SipiEssentials essential_metadata() const { return emdata; } /*! * Read an image from the given path @@ -504,20 +491,24 @@ class SipiImageError final : public std::exception { * so we need to remove the alpha channel. If we are dealing with a CMYK image, we need to take * this into account as well. */ - void removeExtraSamples() { + void removeExtraSamples(const bool force_gray_alpha = false) { const auto content_channels = (photo == SEPARATED ? 4 : 3); const auto extra_channels = static_cast(es.size()); - for (size_t i = content_channels; i < (extra_channels + content_channels); i++) - removeChan(i); + for (size_t i = content_channels; i < (extra_channels + content_channels); i++) { + removeChannel(i, force_gray_alpha); + } } /*! * Removes a channel from a multi component image * - * \param[in] chan Index of component to remove, starting with 0 + * \param[in] channel Index of component to remove, starting with 0 + * \param[in] force_gray_alpha If true, based on the alpha channel that is removed, a gray value is applied + * to the remaining channels. This is useful for image formats that don't support alpha channel and where the + * main content is black, so it is better separated from the background (as the default would be black). */ - void removeChan(unsigned int chan); + void removeChannel(unsigned int channel, bool force_gray_alpha = false); /*! * Crops an image to a region @@ -653,6 +644,13 @@ class SipiImageError final : public std::exception { * \returns Returns ostream object */ friend std::ostream &operator<<(std::ostream &lhs, const SipiImage &rhs); + + friend class SipiIcc; //!< We need SipiIcc as friend class + friend class SipiIOTiff; //!< I/O class for the TIFF file format + friend class SipiIOJ2k; //!< I/O class for the JPEG2000 file format + //friend class SipiIOOpenJ2k; //!< I/O class for the JPEG2000 file format + friend class SipiIOJpeg; //!< I/O class for the JPEG file format + friend class SipiIOPng; //!< I/O class for the PNG file format }; } diff --git a/src/SipiImage.cpp b/src/SipiImage.cpp index 0448a90f..4a8784e6 100755 --- a/src/SipiImage.cpp +++ b/src/SipiImage.cpp @@ -25,8 +25,10 @@ #include #include -//#include +#include + #include +#include #include "lcms2.h" #include "makeunique.h" @@ -36,11 +38,9 @@ #include "SipiImage.h" #include "formats/SipiIOTiff.h" #include "formats/SipiIOJ2k.h" -//#include "formats/SipiIOOpenJ2k.h" -#include - #include "formats/SipiIOJpeg.h" #include "formats/SipiIOPng.h" + #include "shttps/Parsing.h" static const char __file__[] = __FILE__; @@ -508,68 +508,129 @@ namespace Sipi { /*==========================================================================*/ - void SipiImage::removeChan(unsigned int chan) { - if ((nc == 1) || (chan >= nc)) { - std::string msg = "Cannot remove component: nc=" + std::to_string(nc) + " chan=" + std::to_string(chan); + void SipiImage::removeChannel(const unsigned int channel, const bool force_gray_alpha) { + if ((nc == 1) || (channel >= nc)) { + std::string msg = "Cannot remove component: nc=" + std::to_string(nc) + " chan=" + std::to_string(channel); throw SipiImageError(__file__, __LINE__, msg); } - if (!es.empty()) { - if (nc < 3) { - es.clear(); // no more alpha channel - } else if (nc > 3) { // it's probably an alpha channel - if ((nc == 4) && (photo == SEPARATED)) { // oh no – 4 channels, but CMYK - std::string msg = - "Cannot remove component: nc=" + std::to_string(nc) + " chan=" + std::to_string(chan); - throw SipiImageError(__file__, __LINE__, msg); - } else { - es.erase(es.begin() + (chan - ((photo == SEPARATED) ? 4 : 3))); - } - } else { - std::string msg = "Cannot remove component: nc=" + std::to_string(nc) + " chan=" + std::to_string(chan); + const bool has_removable_extra_samples = !es.empty(); + const bool has_two_or_less_channels = nc < 3; + const bool has_three_channels = nc == 3; + + // Assumtion: An image with two or less channels cannot have extra samples + assert(has_two_or_less_channels && has_removable_extra_samples); + + if (has_removable_extra_samples) { + + // cleanup the extra samples + // TODO: figure out why this can even happen + if (has_two_or_less_channels) { + es.clear(); + } + + // TODO: figure out when this can happen. Maybe two channels with alpha is not allowed or even possible? + if (has_three_channels) { + std::string msg = "Cannot remove component: nc=" + std::to_string(nc) + " chan=" + std::to_string(channel); + throw SipiImageError(__file__, __LINE__, msg); + } + + // TODO: figure out when this can happen and if this can/should be caught earlier + const bool cmyk_image = (nc == 4) && (photo == SEPARATED); + if (cmyk_image) { + std::string msg = "Cannot remove component: nc=" + std::to_string(nc) + " chan=" + std::to_string(channel); throw SipiImageError(__file__, __LINE__, msg); } } - if (bps == 8) { - byte *inbuf = pixels; - size_t nnc = nc - 1; - byte *outbuf = new byte[(size_t) nnc * (size_t) nx * (size_t) ny]; + constexpr int _8bps = 8; + constexpr int _16bps = 16; + /** + * Purge the channel from the image. + * The image is stored in a single array, so we need to remove the + * corresponding pixel values. We do this by copying the original and + * omitting the channel to be removed. + */ + auto purge_channel_pixels = [](const auto& original_pixels, auto& changed_pixels, const size_t nx, const size_t ny, const size_t nc, const size_t channel_to_remove, const size_t new_nc) { for (size_t j = 0; j < ny; j++) { for (size_t i = 0; i < nx; i++) { for (size_t k = 0; k < nc; k++) { - if (k == chan) continue; - outbuf[nnc * (j * nx + i) + k] = inbuf[nc * (j * nx + i) + k]; + if (k == channel_to_remove) { + continue; + } + changed_pixels[new_nc * (j * nx + i) + k] = original_pixels[nc * (j * nx + i) + k]; } } } - - pixels = outbuf; - delete[] inbuf; - } else if (bps == 16) { - word *inbuf = (word *) pixels; - size_t nnc = nc - 1; - auto *outbuf = new unsigned short[nnc * nx * ny]; - + }; + + /** + * Purge the channel from the image. + * The image is stored in a single array, so we need to remove the + * corresponding pixel values. We do this by copying the original and + * omitting the channel to be removed. Additionally, we add middle gray + * (128) to each pixel's color component where the alpha channel is 0. + */ + auto purge_channel_pixels_with_gray_alpha = [](const auto& original_pixels, auto& changed_pixels, const size_t nx, const size_t ny, const size_t nc, const size_t channel_to_remove, const size_t new_nc) { for (size_t j = 0; j < ny; j++) { for (size_t i = 0; i < nx; i++) { for (size_t k = 0; k < nc; k++) { - if (k == chan) continue; - outbuf[nnc * (j * nx + i) + k] = inbuf[nc * (j * nx + i) + k]; + if (k == channel_to_remove) { + continue; + } + changed_pixels[new_nc * (j * nx + i) + k] = (original_pixels[nc * (j * nx + i) + channel_to_remove] == 0) ? 128 : original_pixels[nc * (j * nx + i) + k]; } } } + }; + + + const auto extra_sample_to_remove = channel - ((photo == SEPARATED) ? 4 : 3); + const bool is_alpha_channel = es.at(extra_sample_to_remove) == ASSOCALPHA; + const bool is_rgb_image = photo == RGB; + + /* + * 8 bit per sample. + * Since we want to remove a channel, we need to remove the corresponding + * pixel values, since the whole image is stored in a single array. + * - if the image is RGB, we additionally check apply_gray_alpha and if true, + * we add middle gray (128) to each pixel's color component where the alpha + * channel is 0. + */ + if (bps == _8bps) { + byte *original_pixels = pixels; + const size_t new_nc = nc - 1; + auto *changed_pixels = new byte[ new_nc * nx * ny]; + + // only force gray values if the image is RGB and the alpha channel is the channel to be removed + const bool force_gray_values = force_gray_alpha && is_alpha_channel && is_rgb_image; + if (force_gray_values) { + purge_channel_pixels_with_gray_alpha(original_pixels, changed_pixels, nx, ny, nc, channel, new_nc); + } else { + purge_channel_pixels(original_pixels, changed_pixels, nx, ny, nc, channel, new_nc); + } - pixels = (byte *) outbuf; - delete[] inbuf; + pixels = changed_pixels; + delete[] original_pixels; + } else if (bps == _16bps) { + auto *original_pixels = reinterpret_cast(pixels); + size_t new_nc = nc - 1; + auto *changed_pixels = new unsigned short[new_nc * nx * ny]; + + purge_channel_pixels(original_pixels, changed_pixels, nx, ny, nc, channel, new_nc); + + pixels = reinterpret_cast(changed_pixels); + delete[] original_pixels; } else { - if (bps != 8) { - std::string msg = "Bits per sample is not supported for operation: " + std::to_string(bps); - throw SipiImageError(__file__, __LINE__, msg); - } + const std::string msg = "Bits per sample is not supported for operation: " + std::to_string(bps); + throw SipiImageError(__file__, __LINE__, msg); } + // remove the extra sample that we removed from the image + es.erase(es.begin() + extra_sample_to_remove); + + // lower channel count as we have removed a channel from the image nc--; } //============================================================================ diff --git a/src/formats/SipiIOJpeg.cpp b/src/formats/SipiIOJpeg.cpp index 351deb7b..233e074a 100644 --- a/src/formats/SipiIOJpeg.cpp +++ b/src/formats/SipiIOJpeg.cpp @@ -984,127 +984,6 @@ namespace Sipi { jpeg_destroy_decompress(&cinfo); close(infile); return info; // portions derived from IJG code */ - - /* - FILE *infile; - SipiImgInfo info; - - // - // open the input file - // - if ((infile = fopen(filepath.c_str(), "rb")) == nullptr) { - // inlock.unlock(); - info.success = SipiImgInfo::FAILURE; - return info; - } - - int marker = 0; - int dummy = 0; - if (getc(infile) != 0xFF || getc(infile) != 0xD8) { - fclose(infile); - info.success = SipiImgInfo::FAILURE; - return info; - } - for (;;) { - int discarded_bytes = 0; - if (!getbyte(marker, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - while (marker != 0xFF) { - discarded_bytes++; - if (!getbyte(marker, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - } - do { - if (!getbyte(marker, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - } while (marker == 0xFF); - - if (discarded_bytes != 0) { - fclose(infile); - info.success = SipiImgInfo::FAILURE; - return info; - } - - switch (marker) { - case 0xC0: - case 0xC1: - case 0xC2: - case 0xC3: - case 0xC5: - case 0xC6: - case 0xC7: - case 0xC9: - case 0xCA: - case 0xCB: - case 0xCD: - case 0xCE: - case 0xCF: { - if (!getword(dummy, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - if (!getbyte(dummy, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - int tmp_height; - if (!getword(tmp_height, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - info.height = tmp_height; - int tmp_width; - if (!getword(tmp_width, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - info.width = tmp_width; - info.orientation = TOPLEFT; - info.success = SipiImgInfo::DIMS; - if (!getbyte(dummy, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - fclose(infile); - return info; - } - case 0xDA: - case 0xD9: - fclose(infile); - info.success = SipiImgInfo::FAILURE; - return info; - default: { - int length; - if (!getword(length, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - if (length < 2) { - fclose(infile); - info.success = SipiImgInfo::FAILURE; - return info; - } - length -= 2; - while (length > 0) { - if (!getbyte(dummy, infile)) { - info.success = SipiImgInfo::FAILURE; - return info; - } - length--; - } - } - break; - } - } - info.success = SipiImgInfo::FAILURE; - return info; - */ } //============================================================================ @@ -1131,8 +1010,14 @@ namespace Sipi { // we have to check if the image has an alpha channel (not supported by JPEG). If // so, we remove it! // - if ((img->getNc() > 3) && (img->getNalpha() > 0)) { // we have an alpha channel and possibly a CMYK image - img->removeExtraSamples(); + const int number_of_alpha_channels = img->getNalpha(); + const int number_of_channels = img->getNc(); + bool three_or_more_channels = number_of_channels > 3; + bool more_than_zero_alpha_channel = number_of_alpha_channels > 0; + + bool range_valid = three_or_more_channels && more_than_zero_alpha_channel; + if (range_valid) { // we can have an alpha channel and possibly a CMYK image + img->removeExtraSamples(true); } Sipi::SipiIcc icc = Sipi::SipiIcc(Sipi::icc_sRGB); // force sRGB !! @@ -1144,9 +1029,9 @@ namespace Sipi { cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = jpegErrorExit; - int outfile = -1; /* target file */ + int outfile = -1; /* target file */ JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */ - int row_stride; /* physical row width in image buffer */ + int row_stride; /* physical row width in image buffer */ try { jpeg_create_compress(&cinfo); diff --git a/src/formats/SipiIOTiff.cpp b/src/formats/SipiIOTiff.cpp index f40b0bb8..56a15f98 100755 --- a/src/formats/SipiIOTiff.cpp +++ b/src/formats/SipiIOTiff.cpp @@ -440,6 +440,12 @@ namespace Sipi { } //============================================================================ + /** + * TODO: SipiImage always assumes the image data to be in big endian format. + * TIFF files can be in little endian format. Every TIFF file begins with a two-byte indicator of byte order: + * "II" for little-endian (a.k.a. "Intel byte ordering" or "MM" for big-endian (a.k.a. "Motorola byte ordering" byte ordering. + * I don't see where this is handled in the code. + */ bool SipiIOTiff::read(SipiImage *img, const std::string &filepath, std::shared_ptr region, std::shared_ptr size, bool force_bps_8, ScalingQuality scaling_quality) {