Public Access
Storage: view pictures, PNG, JPEG, BMP and GIF (#45)
Enter on a picture shows it: shrunk to fit the screen, or at its own size with Enter again and the arrows to move. Dithered to the screen's 256 colours; a colour the screen has exactly is left alone, so screenshots are shown as they are. The picture is decoded once, straight into the screen's buffer, and kept there (App::retainsContent): no copy in memory. Decoding runs on the storage task, so the keys keep working and a 12 megapixel photograph appears as it comes instead of tripping the watchdog. PNG, BMP and GIF are read by decoders of our own, host-tested against files made by Pillow; the PNG one needs 32 KB where the display library's needed 44 KB in one block, which the device often doesn't have. JPEG uses the library's TJpgDec. Also corrects two sentences that still gave 16 KB as the editing limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
#include "png_reader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
namespace roro::files {
|
||||
|
||||
namespace {
|
||||
uint32_t be32(const uint8_t* p) { return (static_cast<uint32_t>(p[0]) << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; }
|
||||
|
||||
const char* const kDamaged = "This PNG is damaged";
|
||||
const char* const kCut = "This PNG is cut short";
|
||||
const char* const kNoMemory = "Not enough memory for this PNG";
|
||||
|
||||
// A Huffman code as its lengths say: how many codes of each length, and the symbols in order.
|
||||
struct Huffman {
|
||||
uint16_t count[16];
|
||||
uint16_t symbol[288];
|
||||
|
||||
// False if the lengths don't make a code.
|
||||
bool build(const uint8_t* lengths, int n) {
|
||||
std::memset(count, 0, sizeof count);
|
||||
for (int i = 0; i < n; i++) count[lengths[i]]++;
|
||||
int left = 1;
|
||||
for (int len = 1; len < 16; len++) {
|
||||
left = (left << 1) - count[len];
|
||||
if (left < 0) return false;
|
||||
}
|
||||
uint16_t offs[16];
|
||||
offs[1] = 0;
|
||||
for (int len = 1; len < 15; len++) offs[len + 1] = static_cast<uint16_t>(offs[len] + count[len]);
|
||||
for (int i = 0; i < n; i++)
|
||||
if (lengths[i]) symbol[offs[lengths[i]]++] = static_cast<uint16_t>(i);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Everything one decoding holds, but the window and the two rows: on the heap, in one piece.
|
||||
struct Work {
|
||||
// The file, and the IDAT chunks as one stream of bytes.
|
||||
const ImageRead* read = nullptr;
|
||||
uint32_t size = 0, at = 0, chunkLeft = 0;
|
||||
uint8_t in[256];
|
||||
size_t inHave = 0, inAt = 0;
|
||||
bool inEnd = false;
|
||||
// Bits.
|
||||
uint32_t hold = 0;
|
||||
int held = 0;
|
||||
// The picture.
|
||||
int width = 0, height = 0, depth = 0, type = 0, channels = 0, bpp = 0;
|
||||
uint32_t rowBytes = 0;
|
||||
uint8_t palette[768], alpha[256];
|
||||
bool hasAlpha = false;
|
||||
// The window, the rows, and where the decoding is.
|
||||
std::unique_ptr<uint8_t[]> window, rows;
|
||||
uint32_t windowSize = 0, written = 0;
|
||||
uint8_t *cur = nullptr, *prev = nullptr;
|
||||
int64_t pos = -1; // in the row; -1: its filter byte comes next
|
||||
int filter = 0, y = 0;
|
||||
bool stop = false;
|
||||
const char* problem = nullptr;
|
||||
const ImagePixels* pixels = nullptr;
|
||||
const std::function<bool(int)>* rowNeeded = nullptr;
|
||||
const std::function<bool(int)>* enough = nullptr;
|
||||
Huffman lengths, distances;
|
||||
uint8_t codeLengths[320];
|
||||
|
||||
int byte() {
|
||||
if (inAt >= inHave) {
|
||||
while (!chunkLeft && !inEnd) { // the next IDAT, past this one's checksum
|
||||
uint8_t head[12];
|
||||
if (at + 12 > size || (*read)(at, head, 12) != 12) return inEnd = true, -1;
|
||||
at += 4; // the checksum
|
||||
if (std::memcmp(head + 8, "IDAT", 4) != 0) return inEnd = true, -1;
|
||||
chunkLeft = be32(head + 4);
|
||||
at += 8;
|
||||
}
|
||||
if (inEnd) return -1;
|
||||
size_t want = std::min<size_t>(sizeof in, chunkLeft);
|
||||
inHave = (*read)(at, in, want);
|
||||
inAt = 0;
|
||||
if (inHave != want) return inEnd = true, -1;
|
||||
at += static_cast<uint32_t>(want);
|
||||
chunkLeft -= static_cast<uint32_t>(want);
|
||||
}
|
||||
return in[inAt++];
|
||||
}
|
||||
int bits(int n) { // -1: no more
|
||||
while (held < n) {
|
||||
int b = byte();
|
||||
if (b < 0) return -1;
|
||||
hold |= static_cast<uint32_t>(b) << held;
|
||||
held += 8;
|
||||
}
|
||||
int v = static_cast<int>(hold & ((1u << n) - 1));
|
||||
hold >>= n;
|
||||
held -= n;
|
||||
return v;
|
||||
}
|
||||
int decode(const Huffman& h) { // -1: no more, or not a code
|
||||
int code = 0, first = 0, index = 0;
|
||||
for (int len = 1; len < 16; len++) {
|
||||
int b = bits(1);
|
||||
if (b < 0) return -1;
|
||||
code |= b;
|
||||
int n = h.count[len];
|
||||
if (code - n < first) return h.symbol[index + (code - first)];
|
||||
index += n;
|
||||
first += n;
|
||||
first <<= 1;
|
||||
code <<= 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void row();
|
||||
// One byte out of the decompression: into the window, and into the row being rebuilt.
|
||||
void out(uint8_t b) {
|
||||
window[written++ & (windowSize - 1)] = b;
|
||||
if (pos < 0) {
|
||||
if (b > 4) {
|
||||
problem = kDamaged;
|
||||
stop = true;
|
||||
}
|
||||
filter = b;
|
||||
pos = 0;
|
||||
return;
|
||||
}
|
||||
uint32_t i = static_cast<uint32_t>(pos);
|
||||
int a = i >= static_cast<uint32_t>(bpp) ? cur[i - bpp] : 0, up = prev[i], c = i >= static_cast<uint32_t>(bpp) ? prev[i - bpp] : 0, add = 0;
|
||||
switch (filter) {
|
||||
case 1: add = a; break;
|
||||
case 2: add = up; break;
|
||||
case 3: add = (a + up) >> 1; break;
|
||||
case 4: {
|
||||
int p = a + up - c, pa = std::abs(p - a), pb = std::abs(p - up), pc = std::abs(p - c);
|
||||
add = pa <= pb && pa <= pc ? a : pb <= pc ? up : c;
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
cur[i] = static_cast<uint8_t>(b + add);
|
||||
if (static_cast<uint32_t>(++pos) < rowBytes) return;
|
||||
if (!rowNeeded || !*rowNeeded || (*rowNeeded)(y)) row();
|
||||
std::swap(cur, prev);
|
||||
pos = -1;
|
||||
y++;
|
||||
if (y >= height || (enough && *enough && (*enough)(y))) stop = true;
|
||||
}
|
||||
bool inflate();
|
||||
};
|
||||
|
||||
// The row as colours: runs of pixels, broken where one is transparent.
|
||||
void Work::row() {
|
||||
uint8_t run[64 * 3];
|
||||
int n = 0, from = 0;
|
||||
auto flush = [&]() {
|
||||
if (n) (*pixels)(from, y, n, run);
|
||||
n = 0;
|
||||
};
|
||||
int top = (1 << depth) - 1;
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t r, g, b, a = 255;
|
||||
auto sample = [&](int k) -> int { // the k-th value of this pixel, as 8 bits; an index stays an index
|
||||
if (depth == 8) return cur[x * channels + k];
|
||||
if (depth == 16) return cur[(x * channels + k) * 2];
|
||||
int bit = x * depth, v = (cur[bit >> 3] >> (8 - depth - (bit & 7))) & top;
|
||||
return type == 3 ? v : v * 255 / top;
|
||||
};
|
||||
switch (type) {
|
||||
case 0: r = g = b = static_cast<uint8_t>(sample(0)); break;
|
||||
case 2: r = static_cast<uint8_t>(sample(0)), g = static_cast<uint8_t>(sample(1)), b = static_cast<uint8_t>(sample(2)); break;
|
||||
case 3: {
|
||||
int i = sample(0);
|
||||
r = palette[i * 3], g = palette[i * 3 + 1], b = palette[i * 3 + 2];
|
||||
if (hasAlpha) a = alpha[i];
|
||||
break;
|
||||
}
|
||||
case 4: r = g = b = static_cast<uint8_t>(sample(0)), a = static_cast<uint8_t>(sample(1)); break;
|
||||
default: r = static_cast<uint8_t>(sample(0)), g = static_cast<uint8_t>(sample(1)), b = static_cast<uint8_t>(sample(2)), a = static_cast<uint8_t>(sample(3)); break;
|
||||
}
|
||||
if (a < 128) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
if (!n) from = x;
|
||||
run[n * 3] = r, run[n * 3 + 1] = g, run[n * 3 + 2] = b;
|
||||
if (++n == 64) flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
// Deflate (RFC 1951) inside a zlib stream (RFC 1950). False with `problem` set, or true when the
|
||||
// stream ended or enough rows were made.
|
||||
bool Work::inflate() {
|
||||
static const uint16_t kLenBase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
|
||||
static const uint8_t kLenExtra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
|
||||
static const uint16_t kDistBase[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
|
||||
static const uint8_t kDistExtra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
|
||||
static const uint8_t kOrder[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
|
||||
auto fail = [this](const char* what) {
|
||||
if (!problem) problem = what;
|
||||
return false;
|
||||
};
|
||||
for (bool last = false; !last && !stop;) {
|
||||
int head = bits(3);
|
||||
if (head < 0) return fail(kCut);
|
||||
last = head & 1;
|
||||
int kind = head >> 1;
|
||||
if (kind == 0) { // stored
|
||||
hold = 0;
|
||||
held = 0;
|
||||
int a = byte(), b = byte(), c = byte(), d = byte();
|
||||
if (d < 0) return fail(kCut);
|
||||
int len = a | (b << 8);
|
||||
if (len != ((c | (d << 8)) ^ 0xFFFF)) return fail(kDamaged);
|
||||
for (int i = 0; i < len && !stop; i++) {
|
||||
int v = byte();
|
||||
if (v < 0) return fail(kCut);
|
||||
out(static_cast<uint8_t>(v));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (kind == 3) return fail(kDamaged);
|
||||
if (kind == 1) { // the code every decoder knows
|
||||
for (int i = 0; i < 288; i++) codeLengths[i] = i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : 8;
|
||||
lengths.build(codeLengths, 288);
|
||||
for (int i = 0; i < 30; i++) codeLengths[i] = 5;
|
||||
distances.build(codeLengths, 30);
|
||||
} else { // a code of the block's own, itself sent coded
|
||||
int nlen = bits(5), ndist = bits(5), ncode = bits(4);
|
||||
if (ncode < 0) return fail(kCut);
|
||||
nlen += 257, ndist += 1, ncode += 4;
|
||||
if (nlen > 286 || ndist > 30) return fail(kDamaged);
|
||||
uint8_t first[19] = {0};
|
||||
for (int i = 0; i < ncode; i++) {
|
||||
int v = bits(3);
|
||||
if (v < 0) return fail(kCut);
|
||||
first[kOrder[i]] = static_cast<uint8_t>(v);
|
||||
}
|
||||
if (!lengths.build(first, 19)) return fail(kDamaged);
|
||||
for (int i = 0; i < nlen + ndist;) {
|
||||
int sym = decode(lengths);
|
||||
if (sym < 0) return fail(kDamaged);
|
||||
if (sym < 16) {
|
||||
codeLengths[i++] = static_cast<uint8_t>(sym);
|
||||
continue;
|
||||
}
|
||||
int repeat, value = 0;
|
||||
if (sym == 16) {
|
||||
if (!i) return fail(kDamaged);
|
||||
value = codeLengths[i - 1];
|
||||
repeat = 3 + bits(2);
|
||||
} else if (sym == 17) {
|
||||
repeat = 3 + bits(3);
|
||||
} else {
|
||||
repeat = 11 + bits(7);
|
||||
}
|
||||
if (i + repeat > nlen + ndist) return fail(kDamaged);
|
||||
while (repeat--) codeLengths[i++] = static_cast<uint8_t>(value);
|
||||
}
|
||||
uint8_t dist[30];
|
||||
std::memcpy(dist, codeLengths + nlen, static_cast<size_t>(ndist));
|
||||
if (!lengths.build(codeLengths, nlen) || !distances.build(dist, ndist)) return fail(kDamaged);
|
||||
}
|
||||
while (!stop) {
|
||||
int sym = decode(lengths);
|
||||
if (sym < 0) return fail(inEnd ? kCut : kDamaged);
|
||||
if (sym < 256) {
|
||||
out(static_cast<uint8_t>(sym));
|
||||
continue;
|
||||
}
|
||||
if (sym == 256) break;
|
||||
sym -= 257;
|
||||
if (sym >= 29) return fail(kDamaged);
|
||||
int extra = bits(kLenExtra[sym]);
|
||||
int dsym = decode(distances);
|
||||
if (extra < 0 || dsym < 0 || dsym >= 30) return fail(inEnd ? kCut : kDamaged);
|
||||
int dextra = bits(kDistExtra[dsym]);
|
||||
if (dextra < 0) return fail(kCut);
|
||||
uint32_t len = static_cast<uint32_t>(kLenBase[sym] + extra), dist = static_cast<uint32_t>(kDistBase[dsym] + dextra);
|
||||
if (dist > written || dist > windowSize) return fail(kDamaged);
|
||||
for (uint32_t i = 0; i < len && !stop; i++) out(window[(written - dist) & (windowSize - 1)]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string readPng(const ImageRead& read, uint32_t size, const ImagePixels& pixels, const std::function<bool(int y)>& rowNeeded,
|
||||
const std::function<bool(int y)>& enough) {
|
||||
static const uint8_t kSignature[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
|
||||
uint8_t head[33];
|
||||
if (read(0, head, sizeof head) != sizeof head || std::memcmp(head, kSignature, 8) != 0 || std::memcmp(head + 12, "IHDR", 4) != 0) return kDamaged;
|
||||
std::unique_ptr<Work> w(new (std::nothrow) Work);
|
||||
if (!w) return kNoMemory;
|
||||
w->read = &read;
|
||||
w->size = size;
|
||||
w->pixels = &pixels;
|
||||
w->rowNeeded = &rowNeeded;
|
||||
w->enough = &enough;
|
||||
uint32_t width = be32(head + 16), height = be32(head + 20);
|
||||
w->depth = head[24];
|
||||
w->type = head[25];
|
||||
if (!width || !height || width > 16384 || height > 16384 || head[26] || head[27]) return kDamaged;
|
||||
if (head[28]) return "An interlaced PNG can't be shown";
|
||||
w->width = static_cast<int>(width);
|
||||
w->height = static_cast<int>(height);
|
||||
static const int8_t kChannels[7] = {1, 0, 3, 1, 2, 0, 4};
|
||||
int depth = w->depth, type = w->type;
|
||||
bool depthOk = depth == 8 || (depth == 16 && type != 3) || ((depth == 1 || depth == 2 || depth == 4) && (type == 0 || type == 3));
|
||||
if (type > 6 || !kChannels[type] || !depthOk) return "This kind of PNG can't be shown";
|
||||
w->channels = kChannels[type];
|
||||
w->bpp = std::max(1, w->channels * depth / 8);
|
||||
w->rowBytes = (width * static_cast<uint32_t>(w->channels * depth) + 7) / 8;
|
||||
std::memset(w->palette, 0, sizeof w->palette);
|
||||
std::memset(w->alpha, 255, sizeof w->alpha);
|
||||
|
||||
// The chunks before the picture: the palette and its transparency.
|
||||
uint32_t at = 33;
|
||||
for (int guard = 0; guard < 1000; guard++) {
|
||||
uint8_t c[8];
|
||||
if (at + 8 > size || read(at, c, 8) != 8) return kCut;
|
||||
uint32_t len = be32(c);
|
||||
if (std::memcmp(c + 4, "IDAT", 4) == 0) break;
|
||||
if (std::memcmp(c + 4, "IEND", 4) == 0 || len > size) return kDamaged;
|
||||
if (std::memcmp(c + 4, "PLTE", 4) == 0 && read(at + 8, w->palette, std::min<size_t>(len, 768)) != std::min<size_t>(len, 768)) return kCut;
|
||||
if (std::memcmp(c + 4, "tRNS", 4) == 0 && type == 3) {
|
||||
if (read(at + 8, w->alpha, std::min<size_t>(len, 256)) != std::min<size_t>(len, 256)) return kCut;
|
||||
w->hasAlpha = true;
|
||||
}
|
||||
at += 12 + len;
|
||||
}
|
||||
w->at = at - 4; // as if a chunk's checksum had just been reached: byte() steps over it to the IDAT
|
||||
|
||||
// The zlib header says how far back the data refers: the window is that big and no bigger.
|
||||
int cmf = w->byte(), flg = w->byte();
|
||||
if (flg < 0) return kCut;
|
||||
if ((cmf & 0x0F) != 8 || (cmf >> 4) > 7 || ((cmf << 8) | flg) % 31 || (flg & 0x20)) return kDamaged;
|
||||
w->windowSize = 1u << ((cmf >> 4) + 8);
|
||||
w->window.reset(new (std::nothrow) uint8_t[w->windowSize]);
|
||||
w->rows.reset(new (std::nothrow) uint8_t[static_cast<size_t>(w->rowBytes) * 2]());
|
||||
if (!w->window || !w->rows) return kNoMemory;
|
||||
w->cur = w->rows.get();
|
||||
w->prev = w->rows.get() + w->rowBytes;
|
||||
if (enough && enough(0)) return "";
|
||||
if (!w->inflate()) return w->problem ? w->problem : kDamaged;
|
||||
if (w->problem) return w->problem;
|
||||
return w->stop ? "" : kCut; // the data ended before the last row
|
||||
}
|
||||
|
||||
} // namespace roro::files
|
||||
Reference in New Issue
Block a user