373 lines
13 KiB
C++
373 lines
13 KiB
C++
// -*- tab-width: 4; -*-
|
|
// vi: set sw=2 ts=4 expandtab:
|
|
|
|
// Copyright 2022 The Khronos Group Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
/**
|
|
* @internal
|
|
* @~English
|
|
* @file
|
|
*
|
|
* @brief ImageInput from JPEG format files.
|
|
*
|
|
* The following has a very useful summary of the metadata in JPEG files and
|
|
* its handling.
|
|
* https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
|
|
* This plug-in currently only handles 1 & 3 component images. 1 component
|
|
* is luminance. 3 component is YCbCr which the plug-in converts to RGB.
|
|
*
|
|
* @author Mark Callow.
|
|
*/
|
|
|
|
#include "imageio.h"
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
|
|
#include "encoder/jpgd.h"
|
|
|
|
using namespace jpgd;
|
|
|
|
class myjpgdstream : public jpeg_decoder_file_stream {
|
|
public:
|
|
void open(std::istream* pInputStream) {
|
|
isp = pInputStream;
|
|
}
|
|
|
|
int read(uint8_t* pBuf, int max_bytes_to_read, bool* pEOF_flag) {
|
|
if (!isp)
|
|
return -1;
|
|
|
|
if (isp->eof())
|
|
{
|
|
*pEOF_flag = true;
|
|
return 0;
|
|
}
|
|
|
|
if (isp->fail())
|
|
return -1;
|
|
|
|
isp->read(reinterpret_cast<char*>(pBuf), max_bytes_to_read);
|
|
// eof check must come first as fail bit is also set when there
|
|
// aren't enough characters to satisfy the full request.
|
|
if (isp->eof())
|
|
*pEOF_flag = true;
|
|
else if (isp->fail())
|
|
return -1;
|
|
|
|
return static_cast<int>(isp->gcount());
|
|
}
|
|
|
|
void rewind() {
|
|
isp->seekg(0);
|
|
}
|
|
|
|
protected:
|
|
std::istream* isp = nullptr;
|
|
};
|
|
|
|
#if defined(FORMAT_JPGD_STATUS_AS_INTEGER)
|
|
// Adding to a namespace outside its package is not recommended but
|
|
// since the decoder is part of Basis Universal ...
|
|
namespace jpgd {
|
|
int format_as(jpgd_status s) { return fmt::underlying(s); }
|
|
}
|
|
#else // be more user friendly.
|
|
template <> struct fmt::formatter<jpgd_status>: formatter<string_view> {
|
|
// parse is inherited from formatter<string_view>.
|
|
template <typename FormatContext>
|
|
auto format(jpgd_status s, FormatContext& ctx) const -> decltype(ctx.out()) {
|
|
string_view name;
|
|
switch (s) {
|
|
case JPGD_SUCCESS: name = "JPGD_SUCCESS"; break;
|
|
case JPGD_FAILED: name = "JPGD_FAILED"; break;
|
|
case JPGD_DONE: name = "JPGD_DONE"; break;
|
|
case JPGD_BAD_DHT_COUNTS: name = "JPGD_BAD_DHT_COUNTS"; break;
|
|
case JPGD_BAD_DHT_INDEX: name = "JPGD_BAD_DHT_INDEX"; break;
|
|
case JPGD_BAD_DHT_MARKER: name = "JPGD_BAD_DHT_MARKER"; break;
|
|
case JPGD_BAD_DQT_MARKER: name = "JPGD_BAD_DQT_MARKER"; break;
|
|
case JPGD_BAD_DQT_TABLE: name = "JPGD_BAD_DQT_TABLE"; break;
|
|
case JPGD_BAD_PRECISION: name = "JPGD_BAD_PRECISION"; break;
|
|
case JPGD_BAD_HEIGHT: name = "JPGD_BAD_HEIGHT"; break;
|
|
case JPGD_BAD_WIDTH: name = "JPGD_BAD_WIDTH"; break;
|
|
case JPGD_TOO_MANY_COMPONENTS: name = "JPGD_TOO_MANY_COMPONENTS"; break;
|
|
case JPGD_BAD_SOF_LENGTH: name = "JPGD_BAD_SOF_LENGTH"; break;
|
|
case JPGD_BAD_VARIABLE_MARKER: name = "JPGD_BAD_VARIABLE_MARKER"; break;
|
|
case JPGD_BAD_DRI_LENGTH: name = "JPGD_BAD_DRI_LENGTH"; break;
|
|
case JPGD_BAD_SOS_LENGTH: name = "JPGD_BAD_SOS_LENGTH"; break;
|
|
case JPGD_BAD_SOS_COMP_ID: name = "JPGD_BAD_SOS_COMP_ID"; break;
|
|
case JPGD_W_EXTRA_BYTES_BEFORE_MARKER:
|
|
name = "JPGD_W_EXTRA_BYTES_BEFORE_MARKER";
|
|
break;
|
|
case JPGD_NO_ARITHMITIC_SUPPORT:
|
|
name = "JPGD_NO_ARITHMITIC_SUPPORT";
|
|
break;
|
|
case JPGD_UNEXPECTED_MARKER: name = "JPGD_UNEXPECTED_MARKER"; break;
|
|
case JPGD_NOT_JPEG: name = "JPGD_NOT_JPEG"; break;
|
|
case JPGD_UNSUPPORTED_MARKER: name = "JPGD_UNSUPPORTED_MARKER"; break;
|
|
case JPGD_BAD_DQT_LENGTH: name = "JPGD_BAD_DQT_LENGTH"; break;
|
|
case JPGD_TOO_MANY_BLOCKS: name = "JPGD_TOO_MANY_BLOCKS"; break;
|
|
case JPGD_UNDEFINED_QUANT_TABLE:
|
|
name = "JPGD_UNDEFINED_QUANT_TABLE";
|
|
break;
|
|
case JPGD_UNDEFINED_HUFF_TABLE: name = "JPGD_UNDEFINED_HUFF_TABLE"; break;
|
|
case JPGD_NOT_SINGLE_SCAN: name = "JPGD_NOT_SINGLE_SCAN"; break;
|
|
case JPGD_UNSUPPORTED_COLORSPACE:
|
|
name = "JPGD_UNSUPPORTED_COLORSPACE";
|
|
break;
|
|
case JPGD_UNSUPPORTED_SAMP_FACTORS:
|
|
name = "JPGD_UNSUPPORTED_SAMP_FACTORS";
|
|
break;
|
|
case JPGD_DECODE_ERROR: name = "JPGD_DECODE_ERROR"; break;
|
|
case JPGD_BAD_RESTART_MARKER: name = "JPGD_BAD_RESTART_MARKER"; break;
|
|
case JPGD_BAD_SOS_SPECTRAL: name = "JPGD_BAD_SOS_SPECTRAL"; break;
|
|
case JPGD_BAD_SOS_SUCCESSIVE: name = "JPGD_BAD_SOS_SUCCESSIVE"; break;
|
|
case JPGD_STREAM_READ: name = "JPGD_STREAM_READ"; break;
|
|
case JPGD_NOTENOUGHMEM: name = "JPGD_NOTENOUGHMEM"; break;
|
|
case JPGD_TOO_MANY_SCANS: name = "JPGD_TOO_MANY_SCANS"; break;
|
|
}
|
|
return formatter<string_view>::format(name, ctx);
|
|
}
|
|
};
|
|
#endif
|
|
|
|
class JpegInput final : public ImageInput {
|
|
public:
|
|
JpegInput() : ImageInput("jpeg") {}
|
|
virtual ~JpegInput() { close(); }
|
|
virtual void open(ImageSpec& newspec) override;
|
|
virtual void close() override {
|
|
decodingBegun = false;
|
|
if (pJd) delete pJd;
|
|
}
|
|
|
|
virtual void readImage(void* buffer, size_t bufferByteCount,
|
|
uint subimage, uint miplevel,
|
|
const FormatDescriptor& targetFormat) override;
|
|
virtual void readScanline(void* buffer, size_t bufferByteCount,
|
|
uint y, uint z,
|
|
uint usubimage, uint miplevel,
|
|
const FormatDescriptor& targetFormat) override;
|
|
|
|
/// Read a single scanline (all channels) of native data into contiguous
|
|
/// memory.
|
|
virtual void readNativeScanline(void* /*buffer*/,
|
|
size_t /*bufferByteCount*/,
|
|
uint /*y*/, uint /*z*/,
|
|
uint /*subimage*/, uint /*miplevel*/) override { };
|
|
|
|
protected:
|
|
void readHeader();
|
|
|
|
myjpgdstream jstream;
|
|
jpeg_decoder* pJd = nullptr;
|
|
uint32_t nextScanline = 0;
|
|
bool decodingBegun = false;
|
|
};
|
|
|
|
ImageInput*
|
|
jpegInputCreate()
|
|
{
|
|
return new JpegInput;
|
|
}
|
|
|
|
const char* jpegInputExtensions[] = { "jpg", "jpeg", nullptr };
|
|
|
|
void
|
|
JpegInput::open(ImageSpec& newspec)
|
|
{
|
|
assert(isp != nullptr && "ImageInput not properly opened");
|
|
|
|
jstream.open(isp);
|
|
readHeader();
|
|
newspec = spec();
|
|
nextScanline = 0;
|
|
}
|
|
|
|
// This doesn't read the APP0 chunk. Although JFIF specs gamma = 1.0, most
|
|
// JPEG files are EXIF so this considers all JPEG files to be sRGB.
|
|
void
|
|
JpegInput::readHeader()
|
|
{
|
|
pJd = new jpeg_decoder(&jstream, jpeg_decoder::cFlagLinearChromaFiltering);
|
|
jpgd_status errorCode = pJd->get_error_code();
|
|
|
|
if (errorCode != JPGD_SUCCESS) {
|
|
if (errorCode == JPGD_NOT_JPEG) {
|
|
throw different_format();
|
|
} else if (errorCode == JPGD_NOTENOUGHMEM) {
|
|
throw std::runtime_error("JPEG decoder out of memory");
|
|
} else {
|
|
throw std::runtime_error(
|
|
fmt::format("JPEG decode failed: {}", errorCode)
|
|
);
|
|
}
|
|
}
|
|
// At this point we cannot use
|
|
// - jd.get_bytes_per_pixel
|
|
// - jd.get_bytes_per_scan_line
|
|
// because the underlying variables are not initialized until
|
|
// begin_decoding is called. In any case these are not helpful as they
|
|
// return what the decode() method will return not what is in the file.
|
|
|
|
images.emplace_back(ImageSpec(pJd->get_width(), pJd->get_height(), 1,
|
|
ImageSpec::Origin(ImageSpec::Origin::eLeft, ImageSpec::Origin::eTop),
|
|
pJd->get_num_components(), 8, // component bit length
|
|
static_cast<khr_df_sample_datatype_qualifiers_e>(0),
|
|
KHR_DF_TRANSFER_SRGB,
|
|
KHR_DF_PRIMARIES_BT709),
|
|
ImageInputFormatType::jpg);
|
|
}
|
|
|
|
|
|
/// @brief Read an image scanline into contiguous memory performing conversions
|
|
/// to @a format.
|
|
///
|
|
/// Supported conversions are
|
|
/// - changing channel count
|
|
/// - [GREY,RGB]->[GREY,RGB,RGBA]
|
|
/// When reducing to 1 channel it calculates luma for GREY from R,G & B.
|
|
/// When increasing from 1 it makes a luminance texture, R=G=B=GREY.
|
|
/// ALPHA is set to 1.0 when converting to 4 channels.
|
|
/// 2- and 4-channel inputs are not supported.
|
|
void
|
|
JpegInput::readScanline(void* bufferOut, size_t bufferByteCount, uint y, uint,
|
|
uint, uint,
|
|
const FormatDescriptor& format)
|
|
{
|
|
const auto& targetFormat = format.isUnknown() ? spec().format() : format;
|
|
const auto requestBits = targetFormat.largestChannelBitLength();
|
|
|
|
if (requestBits != 8)
|
|
throw std::runtime_error(fmt::format(
|
|
"Requested decode into {}-bit format is not supported.",
|
|
requestBits));
|
|
|
|
const bool targetL = targetFormat.samples[0].qualifierLinear;
|
|
const bool targetE = targetFormat.samples[0].qualifierExponent;
|
|
const bool targetS = targetFormat.samples[0].qualifierSigned;
|
|
const bool targetF = targetFormat.samples[0].qualifierFloat;
|
|
|
|
// Only UNORM requests are allowed for JPEG inputs
|
|
if (targetE || targetL || targetS || targetF)
|
|
throw std::runtime_error(fmt::format(
|
|
"Requested format conversion to {}-bit{}{}{}{} is not supported.",
|
|
requestBits,
|
|
targetL ? " Linear" : "",
|
|
targetE ? " Exponent" : "",
|
|
targetS ? " Signed" : "",
|
|
targetF ? " Float" : ""));
|
|
|
|
if (y >= spec().height())
|
|
y = spec().height() - 1;
|
|
|
|
if (y != nextScanline)
|
|
throw std::runtime_error("Random scanline seeking not yet implemented.");
|
|
|
|
if (!decodingBegun) {
|
|
if (pJd == nullptr)
|
|
throw std::runtime_error("No file opened.");
|
|
pJd->begin_decoding();
|
|
decodingBegun = true;
|
|
}
|
|
|
|
const uint8_t* pScanline = 0;
|
|
uint32_t scanlineByteCount;
|
|
int decodeStatus =
|
|
pJd->decode((const void**)&pScanline, &scanlineByteCount) ;
|
|
if (decodeStatus != JPGD_SUCCESS) {
|
|
assert(decodeStatus != JPGD_DONE);
|
|
// Only other decodeStatus is JPGD_FAILED;
|
|
throw std::runtime_error(
|
|
fmt::format("JPEG decode failed: {}", pJd->get_error_code())
|
|
);
|
|
}
|
|
|
|
const auto targetChannelCount = targetFormat.extended.channelCount;
|
|
|
|
if (targetChannelCount == 2)
|
|
throw std::runtime_error(fmt::format(
|
|
"Requested decode into 2 channels is not supported.")
|
|
);
|
|
|
|
uint8_t* pDst = static_cast<uint8_t*>(bufferOut);
|
|
uint32_t inputChannelCount = spec().format().extended.channelCount;
|
|
// decode() returns a bufferOut of 1 channel (for grayscale input)
|
|
// or 4 channels. Despite this decode() does not support 4 channel
|
|
// inputs. Nor does it support 2 channel inputs.
|
|
// TODO: Extend the following when decode supports 2- and 4-channel inputs.
|
|
if ((targetChannelCount == 1 && inputChannelCount == 1)
|
|
|| (targetChannelCount == 4 && inputChannelCount == 3))
|
|
{
|
|
if (bufferByteCount < scanlineByteCount)
|
|
throw buffer_too_small();
|
|
memcpy(bufferOut, pScanline, scanlineByteCount);
|
|
} else if (inputChannelCount == 1) {
|
|
if (targetChannelCount == 3) {
|
|
if (bufferByteCount < scanlineByteCount * 3)
|
|
throw buffer_too_small();
|
|
for (uint x = 0; x < spec().width(); x++) {
|
|
uint8 luma = pScanline[x];
|
|
pDst[0] = luma;
|
|
pDst[1] = luma;
|
|
pDst[2] = luma;
|
|
pDst += 3;
|
|
}
|
|
} else { // targetChannelCount = 4
|
|
if (bufferByteCount < scanlineByteCount * 4)
|
|
throw buffer_too_small();
|
|
for (uint x = 0; x < spec().width(); x++)
|
|
{
|
|
uint8 luma = pScanline[x];
|
|
pDst[0] = luma;
|
|
pDst[1] = luma;
|
|
pDst[2] = luma;
|
|
pDst[3] = 255;
|
|
pDst += 4;
|
|
}
|
|
}
|
|
} else if (inputChannelCount == 3) {
|
|
if (targetChannelCount == 1) {
|
|
if (bufferByteCount < spec().width())
|
|
throw buffer_too_small();
|
|
const int YR = 19595, YG = 38470, YB = 7471;
|
|
for (uint x = 0; x < spec().width(); x++) {
|
|
int r = pScanline[x * 4 + 0];
|
|
int g = pScanline[x * 4 + 1];
|
|
int b = pScanline[x * 4 + 2];
|
|
*pDst++ = static_cast<uint8>((r * YR + g * YG + b * YB + 32768) >> 16);
|
|
}
|
|
} else { //targetChannelCount = 3
|
|
if (bufferByteCount < spec().width() * 3)
|
|
throw buffer_too_small();
|
|
for (uint x = 0; x < spec().width(); x++) {
|
|
pDst[0] = pScanline[x * 4 + 0];
|
|
pDst[1] = pScanline[x * 4 + 1];
|
|
pDst[2] = pScanline[x * 4 + 2];
|
|
pDst += 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
nextScanline++;
|
|
}
|
|
|
|
|
|
/// @brief Read an entire image into contiguous memory performing conversions
|
|
/// to @a format.
|
|
///
|
|
/// @sa readScanline() for supported conversions
|
|
void JpegInput::readImage(void* bufferOut, size_t bufferByteCount,
|
|
uint subimage, uint miplevel,
|
|
const FormatDescriptor& format)
|
|
{
|
|
pJd->begin_decoding();
|
|
decodingBegun = true;
|
|
ImageInput::readImage(bufferOut, bufferByteCount,
|
|
subimage, miplevel,
|
|
format);
|
|
}
|
|
|