Skip to content

Commit

Permalink
BUG: JPEG IO: handle CMYK correctly
Browse files Browse the repository at this point in the history
4-components JPEG files  were erroneously assumed to be RBGA,
corrected. Convert CMYK to RBG. The option to load original
channels as VECTOR image is available.
  • Loading branch information
issakomi authored and dzenanz committed Dec 27, 2021
1 parent 5d860e6 commit ca4babd
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 30 deletions.
11 changes: 9 additions & 2 deletions Modules/IO/JPEG/include/itkJPEGImageIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class ITKIOJPEG_EXPORT JPEGImageIO : public ImageIOBase
itkSetMacro(Progressive, bool);
itkGetConstMacro(Progressive, bool);

/** Convert to RGB if out_color_space is CMYK, default is true */
itkSetMacro(CMYKtoRGB, bool);
itkGetConstMacro(CMYKtoRGB, bool);

/*-------- This part of the interface deals with reading data. ------ */

/** Determine the file type. Returns true if this ImageIO can read the
Expand Down Expand Up @@ -113,8 +117,11 @@ class ITKIOJPEG_EXPORT JPEGImageIO : public ImageIOBase
void
WriteSlice(std::string & fileName, const void * const buffer);

/** Default = true*/
bool m_Progressive;
bool m_Progressive{ true };

bool m_CMYKtoRGB{ true };

bool m_IsCMYK{ false };
};
} // end namespace itk

Expand Down
121 changes: 94 additions & 27 deletions Modules/IO/JPEG/src/itkJPEGImageIO.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ JPEGImageIO::Read(void * buffer)
// use this class so return will call close
JPEGFileWrapper JPEGfp(this->GetFileName(), "rb");
FILE * fp = JPEGfp.m_FilePointer;

if (!fp)
{
itkExceptionMacro("Error JPEGImageIO could not open file: " << this->GetFileName() << std::endl
Expand All @@ -186,10 +185,11 @@ JPEGImageIO::Read(void * buffer)
jerr.pub.error_exit = itk_jpeg_error_exit;
// for any output message call itk_jpeg_output_message
jerr.pub.output_message = itk_jpeg_output_message;

if (setjmp(jerr.setjmp_buffer))
{
jpeg_destroy_decompress(&cinfo);
itkExceptionMacro("libjpeg could not read file: " << this->GetFileName());
itkExceptionMacro("JPEG fatal error in the file: " << this->GetFileName());
}

jpeg_create_decompress(&cinfo);
Expand All @@ -207,50 +207,100 @@ JPEGImageIO::Read(void * buffer)
// prepare to read the bulk data
jpeg_start_decompress(&cinfo);

const auto rowbytes = cinfo.output_width * cinfo.output_components;
auto * tempImage = static_cast<JSAMPLE *>(buffer);

auto * volatile row_pointers = new JSAMPROW[cinfo.output_height];
for (size_t ui = 0; ui < cinfo.output_height; ++ui)

{
row_pointers[ui] = tempImage + rowbytes * ui;
const auto rowbytes = cinfo.output_width * this->GetNumberOfComponents();
auto * tempImage = static_cast<JSAMPLE *>(buffer);
for (size_t ui = 0; ui < cinfo.output_height; ++ui)
{
row_pointers[ui] = tempImage + rowbytes * ui;
}
}

// read the bulk data
while (cinfo.output_scanline < cinfo.output_height)
if (m_IsCMYK && m_CMYKtoRGB)
{
if (setjmp(jerr.setjmp_buffer))
JSAMPROW buf1[1];
auto * volatile buf0 = new JSAMPLE[cinfo.output_width * 4];
buf1[0] = buf0;

while (cinfo.output_scanline < cinfo.output_height)
{
itkWarningMacro("JPEG error in the file " << this->GetFileName());
jpeg_destroy_decompress(&cinfo);
delete[] row_pointers;
return;
if (setjmp(jerr.setjmp_buffer))
{
jpeg_destroy_decompress(&cinfo);
delete[] row_pointers;
delete[] buf0;
itkWarningMacro(<< "JPEG error in the file " << this->GetFileName());
return;
}

jpeg_read_scanlines(&cinfo, buf1, 1);

if (cinfo.output_scanline > 0)
{
const size_t scanline = cinfo.output_scanline - 1;
for (size_t i = 0; i < cinfo.output_width; ++i)
{
// Gimp approach: the following code assumes inverted CMYK values,
// even when an APP14 marker doesn't exist. This is the behavior
// of recent versions of PhotoShop as well.
const float K = buf1[0][4 * i + 3];
row_pointers[scanline][3 * i + 0] = static_cast<JSAMPLE>(buf1[0][4 * i + 0] * K / 255.0f);
row_pointers[scanline][3 * i + 1] = static_cast<JSAMPLE>(buf1[0][4 * i + 1] * K / 255.0f);
row_pointers[scanline][3 * i + 2] = static_cast<JSAMPLE>(buf1[0][4 * i + 2] * K / 255.0f);
}
}
}
jpeg_read_scanlines(&cinfo, &row_pointers[cinfo.output_scanline], cinfo.output_height - cinfo.output_scanline);

// finish the decompression step
jpeg_finish_decompress(&cinfo);

// destroy the decompression object
jpeg_destroy_decompress(&cinfo);

delete[] buf0;
}
else
{
while (cinfo.output_scanline < cinfo.output_height)
{
if (setjmp(jerr.setjmp_buffer))
{
jpeg_destroy_decompress(&cinfo);
delete[] row_pointers;
itkWarningMacro(<< "JPEG error in the file " << this->GetFileName());
return;
}

jpeg_read_scanlines(&cinfo, &row_pointers[cinfo.output_scanline], cinfo.output_height - cinfo.output_scanline);
}

// finish the decompression step
jpeg_finish_decompress(&cinfo);
// finish the decompression step
jpeg_finish_decompress(&cinfo);

// destroy the decompression object
jpeg_destroy_decompress(&cinfo);
// destroy the decompression object
jpeg_destroy_decompress(&cinfo);
}

delete[] row_pointers;
}

JPEGImageIO::JPEGImageIO()
{
this->SetNumberOfDimensions(2);
m_PixelType = IOPixelEnum::SCALAR;

// 12bits is not working right now, but this should be doable
#if BITS_IN_JSAMPLE == 8
m_ComponentType = IOComponentEnum::UCHAR;
#else
# error "JPEG files with more than 8 bits per sample are not supported"
#endif

m_UseCompression = false;
this->Self::SetQuality(95);
m_Progressive = true;

m_Spacing[0] = 1.0;
m_Spacing[1] = 1.0;

Expand All @@ -274,6 +324,8 @@ JPEGImageIO::PrintSelf(std::ostream & os, Indent indent) const
Superclass::PrintSelf(os, indent);
os << indent << "Quality : " << this->GetQuality() << "\n";
os << indent << "Progressive : " << m_Progressive << "\n";
os << indent << "CMYK to RGB : " << m_CMYKtoRGB << "\n";
os << indent << "IsCMYK : " << m_IsCMYK << "\n";
}

void
Expand All @@ -285,6 +337,8 @@ JPEGImageIO::ReadImageInformation()
m_Origin[0] = 0.0;
m_Origin[1] = 0.0;

m_IsCMYK = false;

// use this class so return will call close
JPEGFileWrapper JPEGfp(m_FileName.c_str(), "rb");
FILE * fp = JPEGfp.m_FilePointer;
Expand Down Expand Up @@ -328,23 +382,36 @@ JPEGImageIO::ReadImageInformation()
m_Dimensions[0] = cinfo.output_width;
m_Dimensions[1] = cinfo.output_height;

this->SetNumberOfComponents(cinfo.output_components);

switch (this->GetNumberOfComponents())
switch (cinfo.output_components)
{
case 1:
m_PixelType = IOPixelEnum::SCALAR;
this->SetNumberOfComponents(1);
break;
case 3:
m_PixelType = IOPixelEnum::RGB;
this->SetNumberOfComponents(3);
break;
case 4:
// FIXME
m_PixelType = IOPixelEnum::RGBA;
itkWarningMacro("JPEG image may be opened incorrectly");
break;
if (cinfo.out_color_space == JCS_CMYK)
{
m_IsCMYK = true;
if (m_CMYKtoRGB)
{
m_PixelType = IOPixelEnum::RGB;
this->SetNumberOfComponents(3);
}
else
{
m_PixelType = IOPixelEnum::VECTOR;
this->SetNumberOfComponents(4);
}
break;
}
// else fallthrough
default:
m_PixelType = IOPixelEnum::VECTOR;
this->SetNumberOfComponents(cinfo.output_components);
itkWarningMacro("JPEG image may be opened incorrectly");
break;
}
Expand Down
5 changes: 4 additions & 1 deletion Modules/IO/JPEG/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ itkJPEGImageIOTest.cxx
itkJPEGImageIOTest2.cxx
itkJPEGImageIODegenerateCasesTest.cxx
itkJPEGImageIOBrokenCasesTest.cxx

itkJPEGImageIOCMYKTest.cxx
)

CreateTestDriver(ITKIOJPEG "${ITKIOJPEG-Test_LIBRARIES}" "${ITKIOJPEGTests}")
Expand All @@ -28,3 +28,6 @@ itk_add_test(NAME itkJPEGImageIOTestCorruptedImage
itk_add_test(NAME itkJPEGImageIOTestCorruptedImage2
COMMAND ITKIOJPEGTestDriver
itkJPEGImageIOBrokenCasesTest DATA{Input/corrupted2.jpg})
itk_add_test(NAME itkJPEGImageIOTestCMYKImage
COMMAND ITKIOJPEGTestDriver
itkJPEGImageIOCMYKTest DATA{Input/cmyk.jpg})
1 change: 1 addition & 0 deletions Modules/IO/JPEG/test/Input/cmyk.jpg.sha512
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0cd89dc9d94de4cbefa7026979c136e42d4441ecc42db4207342e43dc115b3522739d8db81632746e482b44d05edd6b5cafa5c3d5b62152f5ed74a522ae76a4a
78 changes: 78 additions & 0 deletions Modules/IO/JPEG/test/itkJPEGImageIOCMYKTest.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*=========================================================================
*
* Copyright NumFOCUS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*=========================================================================*/

#include "itkJPEGImageIO.h"
#include "itkImageFileReader.h"
#include "itkTestingMacros.h"

int
itkJPEGImageIOCMYKTest(int argc, char * argv[])
{
if (argc != 2)
{
std::cerr << "Missing parameters." << std::endl;
std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv);
std::cerr << " inputFilename" << std::endl;
return EXIT_FAILURE;
}

constexpr unsigned int Dimension = 2;
using PixelType = unsigned char;
using ImageType = itk::Image<PixelType, Dimension>;

{
itk::JPEGImageIO::Pointer io = itk::JPEGImageIO::New();

itk::ImageFileReader<ImageType>::Pointer reader = itk::ImageFileReader<ImageType>::New();

reader->SetFileName(argv[1]);

reader->SetImageIO(io);

ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update());

if (!(io->GetPixelType() == itk::CommonEnums::IOPixel::RGB))
{
std::cout << "Expected RGB. Test failed" << std::endl;
return EXIT_FAILURE;
}
}

{
itk::JPEGImageIO::Pointer io = itk::JPEGImageIO::New();

itk::ImageFileReader<ImageType>::Pointer reader = itk::ImageFileReader<ImageType>::New();

io->SetCMYKtoRGB(false);

reader->SetFileName(argv[1]);

reader->SetImageIO(io);

ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update());

if (!(io->GetPixelType() == itk::CommonEnums::IOPixel::VECTOR))
{
std::cout << "Expected VECTOR. Test failed" << std::endl;
return EXIT_FAILURE;
}
}

std::cout << "Test finished." << std::endl;
return EXIT_SUCCESS;
}

0 comments on commit ca4babd

Please sign in to comment.