#include "Decoder/Jpeg.hpp" #include #include #include #include #define STB_IMAGE_STATIC #define STB_IMAGE_IMPLEMENTATION #define STBI_ONLY_JPEG #define STBI_NO_STDIO #include "stb_image.h" #include "turbojpeg.h" namespace Decoder::Jpeg { namespace { constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; class Handle { public: explicit Handle(int type) : handle(tj3Init(type)) { if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed"); } ~Handle() { tj3Destroy(handle); } Handle(const Handle&) = delete; Handle& operator=(const Handle&) = delete; operator tjhandle() const { return handle; } private: tjhandle handle; }; class Buffer { public: ~Buffer() { tj3Free(data); } unsigned char* data = nullptr; }; int toSubsampling(Sampling sampling) { switch (sampling) { case Sampling::Yuv444: return TJSAMP_444; case Sampling::Yuv422: return TJSAMP_422; case Sampling::Yuv420: return TJSAMP_420; } throw std::invalid_argument("Jpeg::Encode: invalid sampling"); } } // namespace std::vector Encode(std::span pixels, std::uint32_t width, std::uint32_t height, std::uint32_t channels, int quality, Sampling sampling, std::uint32_t restartBlocks, std::uint32_t restartRows) { if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { throw std::invalid_argument("Jpeg::Encode: unsupported image size"); } if (channels != 1 && channels != 3) throw std::invalid_argument("Jpeg::Encode: channels must be 1 or 3"); if (quality < 1 || quality > 100) throw std::invalid_argument("Jpeg::Encode: quality must be 1-100"); if (pixels.size() < static_cast(width) * height * channels) { throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); } if (restartBlocks > MAX_DIMENSION || restartRows > MAX_DIMENSION || (restartBlocks != 0 && restartRows != 0)) { throw std::invalid_argument("Jpeg::Encode: invalid restart interval"); } const int subsampling = toSubsampling(sampling); Handle handle(TJINIT_COMPRESS); if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); } if ((restartBlocks != 0 && tj3Set(handle, TJPARAM_RESTARTBLOCKS, static_cast(restartBlocks)) < 0) || (restartRows != 0 && tj3Set(handle, TJPARAM_RESTARTROWS, static_cast(restartRows)) < 0)) { throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); } Buffer buffer; std::size_t size = 0; const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB; if (tj3Compress8(handle, pixels.data(), static_cast(width), 0, static_cast(height), pixelFormat, &buffer.data, &size) < 0) { throw std::runtime_error("Jpeg::Encode: encoding failed"); } return {buffer.data, buffer.data + size}; } std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) { if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8"); } const tjscalingfactor factor{1, static_cast(denominator)}; return static_cast(TJSCALED(static_cast(dimension), factor)); } std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator) { if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8"); } if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; Handle handle(TJINIT_DECOMPRESS); if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed"); if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt; const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE); if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) { throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented"); } const int width = tj3Get(handle, TJPARAM_JPEGWIDTH); const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT); if (width <= 0 || height <= 0) return std::nullopt; if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast(denominator)}) < 0) { throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor"); } const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3; Image image{ScaledDimension(static_cast(width), denominator), ScaledDimension(static_cast(height), denominator), channels, {}}; image.pixels.resize(static_cast(image.width) * image.height * channels); if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) { return std::nullopt; } return image; } std::optional
ParseHeader(std::span jpeg) { if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; int width = 0; int height = 0; int channels = 0; if (!stbi_info_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels)) return std::nullopt; if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt; return Header{static_cast(width), static_cast(height), static_cast(channels)}; } std::optional Decode(std::span jpeg) { if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; int width = 0; int height = 0; int channels = 0; const std::unique_ptr decoded( stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), static_cast(channels), {}}; image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * image.channels); return image; } } // namespace Decoder::Jpeg