mirror of
https://github.com/esphome/esphome.git
synced 2026-08-28 00:33:31 +00:00
[runtime_image] Add support for QOI images (#16945)
This commit is contained in:
@@ -28,6 +28,7 @@ ImageDecoder = runtime_image_ns.class_("ImageDecoder")
|
||||
BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder)
|
||||
JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder)
|
||||
PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder)
|
||||
QoiDecoder = runtime_image_ns.class_("QoiDecoder", ImageDecoder)
|
||||
|
||||
# Runtime image class
|
||||
RuntimeImage = runtime_image_ns.class_(
|
||||
@@ -37,9 +38,10 @@ RuntimeImage = runtime_image_ns.class_(
|
||||
# Image format enum
|
||||
ImageFormat = runtime_image_ns.enum("ImageFormat")
|
||||
IMAGE_FORMAT_AUTO = ImageFormat.AUTO
|
||||
IMAGE_FORMAT_BMP = ImageFormat.BMP
|
||||
IMAGE_FORMAT_JPEG = ImageFormat.JPEG
|
||||
IMAGE_FORMAT_PNG = ImageFormat.PNG
|
||||
IMAGE_FORMAT_BMP = ImageFormat.BMP
|
||||
IMAGE_FORMAT_QOI = ImageFormat.QOI
|
||||
|
||||
# Export enum for decode errors
|
||||
DecodeError = runtime_image_ns.enum("DecodeError")
|
||||
@@ -115,14 +117,27 @@ class PNGFormat(Format):
|
||||
cg.add_library("pngle", "1.1.0")
|
||||
|
||||
|
||||
class QOIFormat(Format):
|
||||
"""QOI format decoder configuration."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("QOI", QoiDecoder)
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_QOI")
|
||||
|
||||
|
||||
# Decodable formats only; platforms that support runtime detection accept
|
||||
# "AUTO" in their own schema and get_format() resolves it
|
||||
_JPEG_FORMAT = JPEGFormat()
|
||||
|
||||
# Registry of available formats
|
||||
IMAGE_FORMATS = {
|
||||
"BMP": BMPFormat(),
|
||||
"JPEG": _JPEG_FORMAT,
|
||||
"JPG": _JPEG_FORMAT, # Alias for JPEG
|
||||
"PNG": PNGFormat(),
|
||||
"QOI": QOIFormat(),
|
||||
}
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
|
||||
@@ -21,6 +21,9 @@ static constexpr MimeLookup MIME_LOOKUP_TABLE[] = {
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
{"image/png", ImageFormat::PNG}, {"image/x-png", ImageFormat::PNG},
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
{"image/qoi", ImageFormat::QOI}, {"image/x-qoi", ImageFormat::QOI},
|
||||
#endif
|
||||
};
|
||||
|
||||
const char *get_mime_type_for_format(ImageFormat format) {
|
||||
|
||||
@@ -11,12 +11,14 @@ enum ImageFormat {
|
||||
/** Format is supplied per decode, e.g. detected from the Content-Type header
|
||||
* by online_image; sniffing the image data is not implemented. */
|
||||
AUTO,
|
||||
/** BMP format. */
|
||||
BMP,
|
||||
/** JPEG format. */
|
||||
JPEG,
|
||||
/** PNG format. */
|
||||
PNG,
|
||||
/** BMP format. */
|
||||
BMP,
|
||||
/** QOI format. */
|
||||
QOI,
|
||||
};
|
||||
|
||||
/// Canonical MIME type for a format; "image/*" for AUTO/unknown
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "qoi_decoder.h"
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
static const char *const TAG = "image_decoder.qoi";
|
||||
|
||||
constexpr uint8_t QOI_OP_RGB = 0b11111110;
|
||||
constexpr uint8_t QOI_OP_RGBA = 0b11111111;
|
||||
constexpr uint8_t QOI_OP_INDEX = 0b00000000; // 00xxxxxx
|
||||
constexpr uint8_t QOI_OP_DIFF = 0b01000000; // 01xxxxxx
|
||||
constexpr uint8_t QOI_OP_LUMA = 0b10000000; // 10xxxxxx
|
||||
constexpr uint8_t QOI_OP_RUN = 0b11000000; // 11xxxxxx
|
||||
|
||||
constexpr uint8_t QOI_RGB_CHUNK_SIZE = 4;
|
||||
constexpr uint8_t QOI_RGBA_CHUNK_SIZE = 5;
|
||||
constexpr uint8_t QOI_LUMA_CHUNK_SIZE = 2;
|
||||
|
||||
constexpr uint8_t QOI_MASK_OP = 0b11000000;
|
||||
constexpr uint8_t QOI_MASK_VALUE = 0b00111111;
|
||||
|
||||
constexpr size_t QOI_HEADER_SIZE = 14;
|
||||
constexpr size_t QOI_COLOR_TABLE_SIZE = 64;
|
||||
|
||||
inline size_t qoi_color_table_index(const Color &color) {
|
||||
// QOI color hash function: (r * 3 + g * 5 + b * 7 + a * 11) % 64
|
||||
return (color.r * 3 + color.g * 5 + color.b * 7 + color.w * 11) &
|
||||
63; // modulo 64 is equivalent to bitwise AND with 63 (0b00111111)
|
||||
}
|
||||
|
||||
void QoiDecoder::reset() {
|
||||
ImageDecoder::reset();
|
||||
this->current_index_ = 0;
|
||||
this->paint_index_ = 0;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->bits_per_pixel_ = 0;
|
||||
this->last_pixel_ = Color(0, 0, 0, 255);
|
||||
if (this->color_table_) {
|
||||
std::fill_n(this->color_table_.get(), QOI_COLOR_TABLE_SIZE, Color());
|
||||
}
|
||||
}
|
||||
|
||||
int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
size_t index = 0;
|
||||
if (this->current_index_ == 0) {
|
||||
if (size < QOI_HEADER_SIZE) {
|
||||
return 0; // Need more data for file header
|
||||
}
|
||||
|
||||
/** QOI Header definition, for reference:
|
||||
char magic[4]; // magic bytes "qoif"
|
||||
uint32_t width; // image width in pixels (BE)
|
||||
uint32_t height; // image height in pixels (BE)
|
||||
uint8_t channels; // 3 = RGB, 4 = RGBA
|
||||
uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear
|
||||
*/
|
||||
// Check if the file is a QOI image
|
||||
if (buffer[0] != 'q' || buffer[1] != 'o' || buffer[2] != 'i' || buffer[3] != 'f') {
|
||||
ESP_LOGE(TAG, "Not a QOI file");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
|
||||
this->width_ = encode_uint32(buffer[4], buffer[5], buffer[6], buffer[7]);
|
||||
this->height_ = encode_uint32(buffer[8], buffer[9], buffer[10], buffer[11]);
|
||||
if (this->width_ == 0 || this->height_ == 0) {
|
||||
ESP_LOGE(TAG, "Invalid image dimensions: (%zux%zu)", this->width_, this->height_);
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
uint8_t channels = buffer[12];
|
||||
if (channels < 3 || channels > 4) {
|
||||
ESP_LOGE(TAG, "Unsupported number of channels: %d", channels);
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
this->bits_per_pixel_ = channels * 8;
|
||||
uint8_t colorspace = buffer[13];
|
||||
if (colorspace > 1) {
|
||||
ESP_LOGE(TAG, "Unsupported colorspace value: %d", colorspace);
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
ESP_LOGD(TAG, "QOI image header: width=%zu, height=%zu, channels=%d, colorspace=%d", this->width_, this->height_,
|
||||
channels, colorspace);
|
||||
|
||||
if (!this->color_table_) {
|
||||
this->color_table_ = std::make_unique<Color[]>(QOI_COLOR_TABLE_SIZE);
|
||||
}
|
||||
|
||||
if (!this->set_size(this->width_, this->height_)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
this->current_index_ = QOI_HEADER_SIZE;
|
||||
index = QOI_HEADER_SIZE;
|
||||
} // Current_index == 0
|
||||
|
||||
Color color;
|
||||
const size_t total_pixels = this->width_ * this->height_;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
color = this->last_pixel_;
|
||||
uint8_t byte = buffer[index];
|
||||
if (byte == QOI_OP_RGB) {
|
||||
if (size < index + QOI_RGB_CHUNK_SIZE) {
|
||||
return index; // Need more data for RGB chunk
|
||||
}
|
||||
index++;
|
||||
color.r = buffer[index++];
|
||||
color.g = buffer[index++];
|
||||
color.b = buffer[index++];
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
} else if (byte == QOI_OP_RGBA) {
|
||||
if (size < index + QOI_RGBA_CHUNK_SIZE) {
|
||||
return index; // Need more data for RGBA chunk
|
||||
}
|
||||
index++;
|
||||
color.r = buffer[index++];
|
||||
color.g = buffer[index++];
|
||||
color.b = buffer[index++];
|
||||
color.w = buffer[index++];
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_RUN) {
|
||||
// QOI run chunk
|
||||
size_t run_length = (byte & QOI_MASK_VALUE) + 1; // run length is encoded in the lower 6 bits, plus one
|
||||
for (size_t i = 0; i < run_length; i++) {
|
||||
// TODO: optimize by drawing runs of pixels at once instead of one by one
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
}
|
||||
index++;
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_LUMA) {
|
||||
if (size < index + QOI_LUMA_CHUNK_SIZE) {
|
||||
return index; // Need more data for LUMA chunk
|
||||
}
|
||||
index++;
|
||||
uint8_t byte2 = buffer[index++];
|
||||
uint8_t delta_g = (byte & QOI_MASK_VALUE) - 32;
|
||||
color.r += delta_g - 8 + ((byte2 >> 4) & 0x0f);
|
||||
color.g += delta_g;
|
||||
color.b += delta_g - 8 + (byte2 & 0x0f);
|
||||
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_DIFF) {
|
||||
color.r += ((byte >> 4) & 0x03) - 2;
|
||||
color.g += ((byte >> 2) & 0x03) - 2;
|
||||
color.b += (byte & 0x03) - 2;
|
||||
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
index++;
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_INDEX) {
|
||||
color = this->color_table_[byte];
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
index++;
|
||||
}
|
||||
this->last_pixel_ = color;
|
||||
this->color_table_[qoi_color_table_index(color)] = color;
|
||||
}
|
||||
this->decoded_bytes_ += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_RUNTIME_IMAGE_QOI
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/**
|
||||
* @brief Image decoder specialization for QOI images.
|
||||
*/
|
||||
class QoiDecoder : public ImageDecoder {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new QOI decoder object.
|
||||
*
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
QoiDecoder(RuntimeImage *image) : ImageDecoder(image, QOI) {}
|
||||
|
||||
void reset() override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
bool is_finished() const override {
|
||||
if (this->bits_per_pixel_ == 0) {
|
||||
// header not yet received, so dimensions not yet determined
|
||||
return false;
|
||||
}
|
||||
// QOI is finished when we've decoded all pixel data
|
||||
return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Color[]> color_table_;
|
||||
size_t current_index_{0};
|
||||
size_t paint_index_{0};
|
||||
size_t width_{0};
|
||||
size_t height_{0};
|
||||
Color last_pixel_{0, 0, 0, 255}; // QOI spec defines initial previous pixel as opaque black
|
||||
uint16_t bits_per_pixel_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
#endif // USE_RUNTIME_IMAGE_QOI
|
||||
@@ -15,6 +15,9 @@
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
#include "png_decoder.h"
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
#include "qoi_decoder.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
@@ -367,6 +370,10 @@ std::unique_ptr<ImageDecoder> RuntimeImage::create_decoder_(ImageFormat format)
|
||||
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||
case PNG:
|
||||
return make_unique<PngDecoder>(this);
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
case QOI:
|
||||
return make_unique<QoiDecoder>(this);
|
||||
#endif
|
||||
case AUTO:
|
||||
ESP_LOGE(TAG, "Image format could not be determined; set `format:` explicitly in the configuration");
|
||||
|
||||
@@ -233,8 +233,9 @@
|
||||
#endif
|
||||
#define USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
|
||||
#define USE_RUNTIME_IMAGE_BMP
|
||||
#define USE_RUNTIME_IMAGE_PNG
|
||||
#define USE_RUNTIME_IMAGE_JPEG
|
||||
#define USE_RUNTIME_IMAGE_PNG
|
||||
#define USE_RUNTIME_IMAGE_QOI
|
||||
#define USE_RUNTIME_STATS
|
||||
#define USE_OTA
|
||||
#define USE_OTA_PASSWORD
|
||||
|
||||
@@ -62,6 +62,12 @@ image:
|
||||
url: http://www.faqs.org/images/library.jpg
|
||||
format: AUTO
|
||||
type: RGB565
|
||||
- platform: online_image
|
||||
id: online_qoi_image
|
||||
url: https://www.example.org/image.qoi
|
||||
format: QOI
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
|
||||
# Check the set_url action
|
||||
esphome:
|
||||
|
||||
@@ -9,7 +9,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# tests have two decoder types and every retained decoder is under test.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
enable_format("BMP")
|
||||
enable_format("PNG")
|
||||
enable_format("JPEG")
|
||||
enable_format("PNG")
|
||||
enable_format("QOI")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
|
||||
@@ -70,11 +70,36 @@ static const uint8_t PNG_RGB_EXPECTED[4][4][3] = {
|
||||
{{0x12, 0x34, 0x56}, {0x65, 0x43, 0x21}, {0xFE, 0xDC, 0xBA}, {0xAB, 0xCD, 0xEF}},
|
||||
};
|
||||
|
||||
// 3x3 QOI, exercising all possible chunk types
|
||||
static const uint8_t QOI_RGBA[] = {
|
||||
0x71, 0x6F, 0x69, 0x66, // Header: 'qoif'
|
||||
0x00, 0x00, 0x00, 0x03, // Width: 3
|
||||
0x00, 0x00, 0x00, 0x03, // Height: 3
|
||||
0x04, // Channels: 4 (RGBA)
|
||||
0x00, // Colorspace: 0 (SRGB)
|
||||
0xC1, // 1. QOI_OP_RUN
|
||||
0x79, // 2. QOI_OP_DIFF
|
||||
0xAA, 0x79, // 3. QOI_OP_LUMA
|
||||
0xFE, 0xC8, 0x64, 0x32, // 4. QOI_OP_RGB
|
||||
0xFF, 0x78, 0x50, 0x28,
|
||||
0x64, // 5. QOI_OP_RGBA
|
||||
0x31, // 6. QOI_OP_INDEX
|
||||
0xC1, // 7. QOI_OP_RUN
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // End Marker
|
||||
};
|
||||
|
||||
static const uint8_t QOI_EXPECTED_RGBA[3][3][4] = {
|
||||
{{0x00, 0x00, 0x00, 0xFF}, {0x00, 0x00, 0x00, 0xFF}, {0x01, 0x00, 0xFF, 0xFF}},
|
||||
{{0x0A, 0x0A, 0x0A, 0xFF}, {0xC8, 0x64, 0x32, 0xFF}, {0x78, 0x50, 0x28, 0x64}},
|
||||
{{0x01, 0x00, 0xFF, 0xFF}, {0x01, 0x00, 0xFF, 0xFF}, {0x01, 0x00, 0xFF, 0xFF}}
|
||||
|
||||
};
|
||||
|
||||
/// Exposes the protected decoder machinery so reuse and eviction can be observed directly.
|
||||
class TestableRuntimeImage : public RuntimeImage {
|
||||
public:
|
||||
explicit TestableRuntimeImage(ImageFormat format)
|
||||
: RuntimeImage(format, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, nullptr, false, 0, 0) {}
|
||||
explicit TestableRuntimeImage(ImageFormat format, image::Transparency transparency = image::TRANSPARENCY_OPAQUE)
|
||||
: RuntimeImage(format, image::IMAGE_TYPE_RGB, transparency, nullptr, false, 0, 0) {}
|
||||
|
||||
ImageDecoder *decoder() { return this->decoder_.get(); }
|
||||
};
|
||||
@@ -132,6 +157,19 @@ template<size_t H, size_t W> static void expect_pixels(TestableRuntimeImage &img
|
||||
}
|
||||
}
|
||||
|
||||
template<size_t H, size_t W>
|
||||
static void expect_pixels_rgba(TestableRuntimeImage &img, const uint8_t (&expected)[H][W][4]) {
|
||||
ASSERT_EQ(img.get_width(), static_cast<int>(W));
|
||||
ASSERT_EQ(img.get_height(), static_cast<int>(H));
|
||||
for (size_t y = 0; y < H; y++) {
|
||||
for (size_t x = 0; x < W; x++) {
|
||||
SCOPED_TRACE(::testing::Message() << "pixel (" << x << "," << y << ")");
|
||||
Color color = img.get_pixel(x, y);
|
||||
EXPECT_THAT((std::array<uint8_t, 4>{color.r, color.g, color.b, color.w}),
|
||||
::testing::ElementsAreArray(expected[y][x]));
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST(RuntimeImageDecoder, DecoderStaysWarmAcrossDecodes) {
|
||||
TestableRuntimeImage img(BMP);
|
||||
|
||||
@@ -337,6 +375,33 @@ TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
}
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
TEST(RuntimeImageDecoder, QoiDecoderStaysWarmAcrossDecodes) {
|
||||
TestableRuntimeImage img(QOI, image::TRANSPARENCY_ALPHA_CHANNEL);
|
||||
|
||||
ASSERT_TRUE(decode_all(img, QOI_RGBA, sizeof(QOI_RGBA)));
|
||||
expect_pixels_rgba(img, QOI_EXPECTED_RGBA);
|
||||
ImageDecoder *first = img.decoder();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
ASSERT_TRUE(decode_all(img, QOI_RGBA, sizeof(QOI_RGBA)));
|
||||
expect_pixels_rgba(img, QOI_EXPECTED_RGBA);
|
||||
EXPECT_EQ(img.decoder(), first) << "decoder must be reused, not reallocated";
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, QoiChunkedFeedDecodesLikeDownloadLoop) {
|
||||
TestableRuntimeImage img(QOI, image::TRANSPARENCY_ALPHA_CHANNEL);
|
||||
|
||||
ASSERT_TRUE(decode_chunked(img, QOI_RGBA, sizeof(QOI_RGBA), 10));
|
||||
expect_pixels_rgba(img, QOI_EXPECTED_RGBA);
|
||||
ImageDecoder *first = img.decoder();
|
||||
|
||||
// Chunked again on the warm decoder: the cross-call resume state
|
||||
// (current_index_ / paint_index_) must have been fully reset.
|
||||
ASSERT_TRUE(decode_chunked(img, QOI_RGBA, sizeof(QOI_RGBA), 10));
|
||||
expect_pixels_rgba(img, QOI_EXPECTED_RGBA);
|
||||
EXPECT_EQ(img.decoder(), first);
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, SessionFlagsTrackLifecycle) {
|
||||
TestableRuntimeImage img(BMP);
|
||||
std::vector<uint8_t> buffer(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP));
|
||||
|
||||
@@ -10,12 +10,14 @@ TEST(RuntimeImageMime, FormatForKnownMimeTypes) {
|
||||
EXPECT_EQ(get_format_for_mime_type("image/bmp"), BMP);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-ms-bmp"), BMP);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-bmp"), BMP);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/png"), PNG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-png"), PNG);
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
EXPECT_EQ(get_format_for_mime_type("image/jpeg"), JPEG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/jpg"), JPEG);
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
EXPECT_EQ(get_format_for_mime_type("image/png"), PNG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-png"), PNG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/qoi"), QOI);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-qoi"), QOI);
|
||||
}
|
||||
|
||||
TEST(RuntimeImageMime, FormatMatchingIsCaseInsensitive) {
|
||||
@@ -39,20 +41,22 @@ TEST(RuntimeImageMime, UnknownMimeTypeHasNoFormat) {
|
||||
|
||||
TEST(RuntimeImageMime, MimeTypeForFormatRoundTrip) {
|
||||
EXPECT_STREQ(get_mime_type_for_format(BMP), "image/bmp");
|
||||
EXPECT_STREQ(get_mime_type_for_format(PNG), "image/png");
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
EXPECT_STREQ(get_mime_type_for_format(JPEG), "image/jpeg");
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
EXPECT_STREQ(get_mime_type_for_format(PNG), "image/png");
|
||||
EXPECT_STREQ(get_mime_type_for_format(QOI), "image/qoi");
|
||||
// AUTO has no single MIME type and falls back to the wildcard
|
||||
EXPECT_STREQ(get_mime_type_for_format(AUTO), "image/*");
|
||||
|
||||
// Every decodable format must resolve back to itself through its MIME type
|
||||
for (ImageFormat format : {
|
||||
BMP,
|
||||
PNG,
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
JPEG,
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
PNG,
|
||||
QOI,
|
||||
}) {
|
||||
EXPECT_EQ(get_format_for_mime_type(get_mime_type_for_format(format)), format) << format;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user