mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 06:15:58 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c43f039631 | ||
|
|
8d1476d585 | ||
|
|
25b8c93ad6 | ||
|
|
5b1642b721 | ||
|
|
b62e12007f | ||
|
|
68d24cb215 |
No files matched your search
@@ -2,7 +2,6 @@
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
@@ -11,8 +10,8 @@ ESPHOME_LOG_TAG(TAG, "image_decoder");
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
bool success = this->image_->resize(width, height) > 0;
|
||||
this->size_valid_ = success;
|
||||
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||
this->draw_width_ = width;
|
||||
this->draw_height_ = height;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -20,10 +19,24 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
if (!this->size_valid_) {
|
||||
return;
|
||||
}
|
||||
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
// When shrinking, each target pixel is painted once, by the first source pixel that reaches it, rather than by every
|
||||
// pixel that lands on it. Integer maths: the S3 has no double FPU.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
int x1 = x;
|
||||
int y1 = y;
|
||||
int x2 = x + w;
|
||||
int y2 = y + h;
|
||||
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
|
||||
x1 = scale_boundary(x, this->draw_width_, buffer_width);
|
||||
y1 = scale_boundary(y, this->draw_height_, buffer_height);
|
||||
x2 = scale_boundary(x + w, this->draw_width_, buffer_width);
|
||||
y2 = scale_boundary(y + h, this->draw_height_, buffer_height);
|
||||
}
|
||||
x2 = std::min(buffer_width, x2);
|
||||
y2 = std::min(buffer_height, y2);
|
||||
for (int i = x1; i < x2; i++) {
|
||||
for (int j = y1; j < y2; j++) {
|
||||
this->image_->draw_pixel(i, j, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/// The first of `target` pixels that pixel n of `source` paints, rounded up: ceil(n * target / source). The spans of
|
||||
/// neighbouring pixels meet without overlapping, so a shrunk image paints each target pixel once.
|
||||
inline int scale_boundary(int n, int source, int target) {
|
||||
if (source <= 0)
|
||||
return 0;
|
||||
return static_cast<int>((static_cast<uint32_t>(n) * static_cast<uint32_t>(target) + source - 1) /
|
||||
static_cast<uint32_t>(source));
|
||||
}
|
||||
|
||||
enum DecodeError : int {
|
||||
DECODE_ERROR_INVALID_TYPE = -1,
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
|
||||
@@ -83,8 +92,8 @@ class ImageDecoder {
|
||||
this->expected_size_ = 0;
|
||||
this->decoded_bytes_ = 0;
|
||||
this->size_valid_ = true;
|
||||
this->x_scale_ = 1.0;
|
||||
this->y_scale_ = 1.0;
|
||||
this->draw_width_ = 0;
|
||||
this->draw_height_ = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,8 +193,9 @@ class ImageDecoder {
|
||||
uint8_t *image_buffer_() const;
|
||||
bool image_is_big_endian_() const;
|
||||
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
// Size of the image as the decoder draws it, which draw() spreads over the buffer
|
||||
int draw_width_ = 0;
|
||||
int draw_height_ = 0;
|
||||
RuntimeImage *image_;
|
||||
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||
|
||||
@@ -28,16 +28,14 @@ bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
|
||||
this->source_height_ = height;
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const uint32_t target_width = this->image_->get_buffer_width();
|
||||
const uint32_t target_height = this->image_->get_buffer_height();
|
||||
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
|
||||
// the generic draw() path rounds down and keeps the odd ones instead
|
||||
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
|
||||
const int target_width = this->image_->get_buffer_width();
|
||||
const int target_height = this->image_->get_buffer_height();
|
||||
// The same boundaries as the generic draw() path, so an 8 to 4 scale keeps columns 0, 2, 4 and 6 in both
|
||||
for (int x = 0; x <= width; x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(x, width, target_width)));
|
||||
}
|
||||
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
|
||||
for (int y = 0; y <= height; y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(y, height, target_height)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -181,8 +179,8 @@ int JpegDecoder::start_() {
|
||||
}
|
||||
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
|
||||
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
|
||||
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
|
||||
this->y_scale_ = static_cast<double>(buffer_height) / decoded_height;
|
||||
this->draw_width_ = decoded_width;
|
||||
this->draw_height_ = decoded_height;
|
||||
if (this->scale_ != 1) {
|
||||
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
|
||||
}
|
||||
|
||||
@@ -365,6 +365,23 @@ static std::vector<uint8_t> pixel_bytes(TestableRuntimeImage &img) {
|
||||
return std::vector<uint8_t>(start, start + img.get_width_stride() * img.get_height());
|
||||
}
|
||||
|
||||
// Shrinking a PNG keeps the first source pixel of each span, as the JPEG path does: the even rows and columns
|
||||
TEST(RuntimeImageDecoder, ShrunkPngKeepsTheFirstPixelOfEachSpan) {
|
||||
TestableRuntimeImage img(PNG, image::IMAGE_TYPE_RGB, false, 2, 2);
|
||||
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB), PNG));
|
||||
ASSERT_EQ(img.get_width(), 2);
|
||||
ASSERT_EQ(img.get_height(), 2);
|
||||
for (int row = 0; row < 2; row++) {
|
||||
for (int col = 0; col < 2; col++) {
|
||||
const Color pixel = img.get_pixel(col, row);
|
||||
const uint8_t *expected = PNG_RGB_EXPECTED[row * 2][col * 2];
|
||||
EXPECT_EQ(pixel.r, expected[0]) << col << "," << row;
|
||||
EXPECT_EQ(pixel.g, expected[1]) << col << "," << row;
|
||||
EXPECT_EQ(pixel.b, expected[2]) << col << "," << row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
TestableRuntimeImage img(JPEG);
|
||||
|
||||
@@ -456,6 +473,34 @@ TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
|
||||
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
|
||||
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
|
||||
|
||||
// Every target pixel of a PNG drawn at a size that is not a whole multiple: enlarged with spans 1 or 2 pixels wide, and
|
||||
// shrunk with spans of 0, 1 or 2 pixels
|
||||
TEST(RuntimeImageDecoder, ScaledPngPaintsEachSourcePixelOverItsSpan) {
|
||||
// A fixed size keeps the aspect ratio, so both are square
|
||||
for (auto [width, height] : {std::pair{6, 6}, std::pair{3, 3}}) {
|
||||
TestableRuntimeImage img(PNG, image::IMAGE_TYPE_RGB, false, width, height);
|
||||
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB), PNG));
|
||||
ASSERT_EQ(img.get_width(), width);
|
||||
ASSERT_EQ(img.get_height(), height);
|
||||
int painted = 0;
|
||||
for (int m = 0; m < 4; m++) {
|
||||
for (int n = 0; n < 4; n++) {
|
||||
const uint8_t *expected = PNG_RGB_EXPECTED[m][n];
|
||||
for (int y = scaled_boundary(m, 4, height); y < scaled_boundary(m + 1, 4, height); y++) {
|
||||
for (int x = scaled_boundary(n, 4, width); x < scaled_boundary(n + 1, 4, width); x++) {
|
||||
const Color pixel = img.get_pixel(x, y);
|
||||
EXPECT_EQ(pixel.r, expected[0]) << width << "x" << height << " at " << x << "," << y;
|
||||
EXPECT_EQ(pixel.g, expected[1]) << width << "x" << height << " at " << x << "," << y;
|
||||
EXPECT_EQ(pixel.b, expected[2]) << width << "x" << height << " at " << x << "," << y;
|
||||
painted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(painted, width * height);
|
||||
}
|
||||
}
|
||||
|
||||
// JPEGDEC decodes at the smallest of full, 1/2, 1/4 and 1/8 size that still covers the buffer
|
||||
TEST(RuntimeImageDecoder, JpegDecodesAtTheSmallestSizeThatCoversTheBuffer) {
|
||||
for (auto [size, scale] :
|
||||
|
||||
Reference in new issue
Block a user