Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use gray instead of black values for transparent parts of image when returning as JPEG #412

Merged
merged 2 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 44 additions & 46 deletions include/SipiImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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<std::string, std::shared_ptr<SipiIO> > 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<ExtraSamples> 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<ExtraSamples> 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<SipiXmp> xmp; //!< Pointer to instance SipiXmp class (\ref SipiXmp), or NULL
std::shared_ptr<SipiIcc> icc; //!< Pointer to instance of SipiIcc class (\ref SipiIcc), or NULL
std::shared_ptr<SipiIptc> iptc; //!< Pointer to instance of SipiIptc class (\ref SipiIptc), or NULL
std::shared_ptr<SipiExif> 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)
subotic marked this conversation as resolved.
Show resolved Hide resolved
std::shared_ptr<SipiXmp> xmp; //!< Pointer to instance SipiXmp class (\ref SipiXmp), or NULL
std::shared_ptr<SipiIcc> icc; //!< Pointer to instance of SipiIcc class (\ref SipiIcc), or NULL
std::shared_ptr<SipiIptc> iptc; //!< Pointer to instance of SipiIptc class (\ref SipiIptc), or NULL
std::shared_ptr<SipiExif> 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:
//
Expand All @@ -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
*/
Expand All @@ -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<SipiExif> getExif() const { return exif; };


/*!
* Get orientation
* @return Returns orientation tag
Expand All @@ -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; };


/*!
Expand Down Expand Up @@ -364,26 +351,26 @@ 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; };


/*!
* Stores the connection parameters of the shttps server in an Image instance
*
* \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
Expand Down Expand Up @@ -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<int>(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
Expand Down Expand Up @@ -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
subotic marked this conversation as resolved.
Show resolved Hide resolved
friend class SipiIOJpeg; //!< I/O class for the JPEG file format
friend class SipiIOPng; //!< I/O class for the PNG file format
};
}

Expand Down
147 changes: 105 additions & 42 deletions src/SipiImage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
#include <vector>
#include <cmath>

//#include <memory>
#include <cassert>

#include <climits>
#include <sys/stat.h>

#include "lcms2.h"
#include "makeunique.h"
Expand All @@ -36,11 +38,9 @@
#include "SipiImage.h"
#include "formats/SipiIOTiff.h"
#include "formats/SipiIOJ2k.h"
//#include "formats/SipiIOOpenJ2k.h"
#include <sys/stat.h>

#include "formats/SipiIOJpeg.h"
#include "formats/SipiIOPng.h"

#include "shttps/Parsing.h"

static const char __file__[] = __FILE__;
Expand Down Expand Up @@ -508,68 +508,131 @@ 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;
const bool has_three_or_more_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) {
subotic marked this conversation as resolved.
Show resolved Hide resolved

// cleanup the extra samples
// TODO: figure out why this can even happen
subotic marked this conversation as resolved.
Show resolved Hide resolved
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 cought earlier
subotic marked this conversation as resolved.
Show resolved Hide resolved
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<unsigned short *>(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<unsigned char *>(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--;
}
//============================================================================
Expand Down
Loading
Loading