ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
gif.cpp
Go to the documentation of this file.
1#ifdef USE_HOST
2#include "gif.h"
3
4#include <algorithm>
5#include <cstring>
6#include <iterator>
7
8namespace esphome::snapshot {
9
10namespace {
11
12constexpr size_t MAX_PALETTE_SIZE = 256;
13// GIF needs at least two bits for each colour, even when the picture has fewer than four colours.
14constexpr unsigned MIN_PALETTE_BITS = 2;
15constexpr unsigned MAX_CODE_BITS = 12;
16constexpr uint32_t MAX_CODES = 1u << MAX_CODE_BITS;
17// Twice the number of codes there can be, so a lookup rarely has to look at more than one entry.
18constexpr size_t DICTIONARY_SIZE = 2 * MAX_CODES;
19constexpr uint32_t EMPTY_ENTRY = 0xFFFFFFFF;
20constexpr size_t MAX_SUB_BLOCK_SIZE = 255;
21// Colours are grouped by their top 5, 6 and 5 bits, which is all an RGB565 display can show.
22constexpr size_t COLOR_KEY_COUNT = 1u << 16;
23
24constexpr uint16_t color_key(uint8_t red, uint8_t green, uint8_t blue) {
25 return static_cast<uint16_t>(((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3));
26}
27
30constexpr unsigned key_channel(uint16_t key, unsigned channel) {
31 switch (channel) {
32 case 0:
33 return (key >> 11) << 3;
34 case 1:
35 return ((key >> 5) & 0x3F) << 2;
36 default:
37 return (key & 0x1F) << 3;
38 }
39}
40
42void put_le16(std::vector<uint8_t> &out, unsigned value) {
43 out.push_back(static_cast<uint8_t>(value & 0xFF));
44 out.push_back(static_cast<uint8_t>(value >> 8));
45}
46
48struct BitWriter {
49 std::vector<uint8_t> &out;
51 unsigned count{0};
52
53 void put(uint32_t value, unsigned bits) {
54 this->buffer |= value << this->count;
55 this->count += bits;
56 while (this->count >= 8) {
57 this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
58 this->buffer >>= 8;
59 this->count -= 8;
60 }
61 }
62
63 void flush() {
64 if (this->count > 0)
65 this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
66 this->buffer = 0;
67 this->count = 0;
68 }
69};
70
72void lzw_compress(const uint8_t *data, size_t length, unsigned min_code_size, std::vector<uint8_t> &out) {
73 struct Entry {
74 uint32_t key; // the code before, shifted up a byte, with the next index in the low byte
75 uint16_t code;
76 };
77 const uint32_t clear_code = 1u << min_code_size;
78 const uint32_t end_code = clear_code + 1;
79 std::vector<Entry> dictionary(DICTIONARY_SIZE);
80 BitWriter bits{out};
81 uint32_t next_code = 0;
82 unsigned code_size = 0;
83
84 auto reset = [&]() {
85 std::fill(dictionary.begin(), dictionary.end(), Entry{EMPTY_ENTRY, 0});
86 next_code = end_code + 1;
87 code_size = min_code_size + 1;
88 };
89
90 reset();
91 bits.put(clear_code, code_size);
92 uint32_t prefix = data[0];
93 for (size_t i = 1; i != length; i++) {
94 const uint32_t key = prefix << 8 | data[i];
95 size_t slot = (key * 2654435761u >> 16) & (DICTIONARY_SIZE - 1);
96 while (dictionary[slot].key != EMPTY_ENTRY && dictionary[slot].key != key)
97 slot = (slot + 1) & (DICTIONARY_SIZE - 1);
98 if (dictionary[slot].key == key) {
99 prefix = dictionary[slot].code;
100 continue;
101 }
102 bits.put(prefix, code_size);
103 if (next_code < MAX_CODES) {
104 dictionary[slot] = Entry{key, static_cast<uint16_t>(next_code++)};
105 // The reader is one entry behind, so it moves to longer codes a step after this does.
106 if (next_code > (1u << code_size))
107 code_size++;
108 } else {
109 bits.put(clear_code, code_size);
110 reset();
111 }
112 prefix = data[i];
113 }
114 bits.put(prefix, code_size);
115 // The reader adds an entry for that last code, which may take it to longer codes.
116 if (next_code == (1u << code_size) && code_size < MAX_CODE_BITS)
117 code_size++;
118 bits.put(end_code, code_size);
119 bits.flush();
120}
121
122} // namespace
123
124GifWriter::GifWriter(FILE *file, int width, int height)
125 : file_(file),
126 width_(width),
127 height_(height),
128 bins_(COLOR_KEY_COUNT),
129 palette_index_(COLOR_KEY_COUNT),
130 indices_(static_cast<size_t>(width) * height) {}
131
133 this->output_.clear();
134 static constexpr char SIGNATURE[] = "GIF89a";
135 this->output_.insert(this->output_.end(), SIGNATURE, SIGNATURE + 6);
136 put_le16(this->output_, this->width_);
137 put_le16(this->output_, this->height_);
138 this->output_.push_back(0x70); // no shared colour table; 8 bits per colour
139 this->output_.push_back(0); // background colour, unused
140 this->output_.push_back(0); // pixels are square
141 // Ask players to repeat the animation forever.
142 static constexpr char LOOP_EXTENSION[] = "\x21\xFF\x0BNETSCAPE2.0\x03\x01";
143 this->output_.insert(this->output_.end(), LOOP_EXTENSION, LOOP_EXTENSION + sizeof(LOOP_EXTENSION) - 1);
144 put_le16(this->output_, 0);
145 this->output_.push_back(0);
146 return fwrite(this->output_.data(), 1, this->output_.size(), this->file_) == this->output_.size();
147}
148
149size_t GifWriter::build_palette_(uint8_t *palette) {
150 struct Box {
151 size_t begin; // range of used_colors_, which the box owns
152 size_t end;
153 uint64_t pixels;
154 };
155
156 this->used_colors_.clear();
157 uint64_t total = 0;
158 for (size_t key = 0; key != COLOR_KEY_COUNT; key++) {
159 if (this->bins_[key].count != 0) {
160 this->used_colors_.push_back(static_cast<uint16_t>(key));
161 total += this->bins_[key].count;
162 }
163 }
164
165 // Median cut: keep cutting the box with the most pixels in two, across the colour part that
166 // varies most, until there is a box for each palette entry or every box is a single colour.
167 std::vector<Box> boxes;
168 boxes.push_back({0, this->used_colors_.size(), total});
169 while (boxes.size() < MAX_PALETTE_SIZE) {
170 size_t chosen = boxes.size();
171 for (size_t i = 0; i != boxes.size(); i++) {
172 if (boxes[i].end - boxes[i].begin >= 2 && (chosen == boxes.size() || boxes[i].pixels > boxes[chosen].pixels))
173 chosen = i;
174 }
175 if (chosen == boxes.size())
176 break;
177 const Box box = boxes[chosen];
178
179 unsigned low[3] = {255, 255, 255};
180 unsigned high[3] = {0, 0, 0};
181 for (size_t i = box.begin; i != box.end; i++) {
182 for (unsigned channel = 0; channel != 3; channel++) {
183 const unsigned value = key_channel(this->used_colors_[i], channel);
184 low[channel] = std::min(low[channel], value);
185 high[channel] = std::max(high[channel], value);
186 }
187 }
188 unsigned widest = 0;
189 for (unsigned channel = 1; channel != 3; channel++) {
190 if (high[channel] - low[channel] > high[widest] - low[widest])
191 widest = channel;
192 }
193 std::sort(this->used_colors_.begin() + box.begin, this->used_colors_.begin() + box.end,
194 [widest](uint16_t a, uint16_t b) {
195 const unsigned value_a = key_channel(a, widest);
196 const unsigned value_b = key_channel(b, widest);
197 return value_a != value_b ? value_a < value_b : a < b;
198 });
199
200 // Cut where the pixels are half on one side and half on the other, keeping a colour on each.
201 size_t cut = box.begin;
202 uint64_t below = 0;
203 while (cut < box.end - 1 && below * 2 < box.pixels)
204 below += this->bins_[this->used_colors_[cut++]].count;
205 boxes[chosen] = {box.begin, cut, below};
206 boxes.push_back({cut, box.end, box.pixels - below});
207 }
208
209 for (size_t i = 0; i != boxes.size(); i++) {
210 uint64_t sum[3] = {0, 0, 0};
211 for (size_t j = boxes[i].begin; j != boxes[i].end; j++) {
212 const ColorBin &bin = this->bins_[this->used_colors_[j]];
213 for (unsigned channel = 0; channel != 3; channel++)
214 sum[channel] += bin.sum[channel];
215 this->palette_index_[this->used_colors_[j]] = static_cast<uint8_t>(i);
216 }
217 // The average colour of the box, rounded to the nearest.
218 for (unsigned channel = 0; channel != 3; channel++)
219 palette[i * 3 + channel] = static_cast<uint8_t>((sum[channel] + boxes[i].pixels / 2) / boxes[i].pixels);
220 }
221 return boxes.size();
222}
223
224bool GifWriter::write_frame(const uint8_t *bgr, size_t row_stride, unsigned delay_centiseconds) {
225 std::fill(this->bins_.begin(), this->bins_.end(), ColorBin{});
226 for (int y = 0; y != this->height_; y++) {
227 const uint8_t *in = bgr + y * row_stride;
228 for (int x = 0; x != this->width_; x++, in += 3) {
229 ColorBin &bin = this->bins_[color_key(in[2], in[1], in[0])];
230 bin.count++;
231 bin.sum[0] += in[2];
232 bin.sum[1] += in[1];
233 bin.sum[2] += in[0];
234 }
235 }
236
237 uint8_t palette[MAX_PALETTE_SIZE * 3] = {};
238 const size_t colors = this->build_palette_(palette);
239 unsigned palette_bits = MIN_PALETTE_BITS;
240 while ((size_t{1} << palette_bits) < colors)
241 palette_bits++;
242
243 uint8_t *out_index = this->indices_.data();
244 for (int y = 0; y != this->height_; y++) {
245 const uint8_t *in = bgr + y * row_stride;
246 for (int x = 0; x != this->width_; x++, in += 3)
247 *out_index++ = this->palette_index_[color_key(in[2], in[1], in[0])];
248 }
249
250 this->output_.clear();
251 // Show the frame for the time asked, then leave it in place under the next one.
252 static constexpr uint8_t GRAPHIC_CONTROL[] = {0x21, 0xF9, 0x04, 0x04};
253 this->output_.insert(this->output_.end(), std::begin(GRAPHIC_CONTROL), std::end(GRAPHIC_CONTROL));
254 put_le16(this->output_, delay_centiseconds);
255 this->output_.push_back(0); // no transparent colour
256 this->output_.push_back(0);
257 this->output_.push_back(0x2C); // the frame fills the whole picture
258 put_le16(this->output_, 0);
259 put_le16(this->output_, 0);
260 put_le16(this->output_, this->width_);
261 put_le16(this->output_, this->height_);
262 this->output_.push_back(static_cast<uint8_t>(0x80 | (palette_bits - 1))); // has its own colour table
263 this->output_.insert(this->output_.end(), palette, palette + (size_t{3} << palette_bits));
264 this->output_.push_back(static_cast<uint8_t>(palette_bits));
265
266 std::vector<uint8_t> compressed;
267 lzw_compress(this->indices_.data(), this->indices_.size(), palette_bits, compressed);
268 for (size_t pos = 0; pos < compressed.size(); pos += MAX_SUB_BLOCK_SIZE) {
269 const size_t length = std::min(MAX_SUB_BLOCK_SIZE, compressed.size() - pos);
270 this->output_.push_back(static_cast<uint8_t>(length));
271 this->output_.insert(this->output_.end(), compressed.begin() + pos, compressed.begin() + pos + length);
272 }
273 this->output_.push_back(0);
274 return fwrite(this->output_.data(), 1, this->output_.size(), this->file_) == this->output_.size();
275}
276
278 const uint8_t trailer = 0x3B;
279 return fwrite(&trailer, 1, 1, this->file_) == 1;
280}
281
282} // namespace esphome::snapshot
283#endif
std::vector< ColorBin > bins_
Definition gif.h:42
std::vector< uint8_t > palette_index_
Definition gif.h:44
std::vector< uint16_t > used_colors_
Definition gif.h:43
std::vector< uint8_t > output_
Definition gif.h:46
GifWriter(FILE *file, int width, int height)
Definition gif.cpp:124
bool write_header()
Write the start of the file. This must come first. The animation repeats forever.
Definition gif.cpp:132
bool write_frame(const uint8_t *bgr, size_t row_stride, unsigned delay_centiseconds)
Add a frame from three bytes per pixel in blue, green, red order, topmost row first,...
Definition gif.cpp:224
std::vector< uint8_t > indices_
Definition gif.h:45
bool write_trailer()
Write the end of the file. This must come last.
Definition gif.cpp:277
size_t build_palette_(uint8_t *palette)
Choose up to 256 colours for the frame in bins_, storing them as red, green, blue triples in palette ...
Definition gif.cpp:149
std::vector< uint8_t > & out
Definition gif.cpp:49
unsigned count
Definition gif.cpp:51
uint32_t buffer
Definition gif.cpp:50
uint16_t reset
Definition ina226.h:5
std::span< const uint8_t > data
size_t size_t pos
Definition helpers.h:1179
static void uint32_t
How many pixels of one colour there are in the frame, and the sum of their red, green and blue.
Definition gif.h:30
uint8_t end[39]
Definition sun_gtil2.cpp:17
uint16_t trailer
uint16_t length
Definition tt21100.cpp:0
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6