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:
@@ -39,6 +39,13 @@ class App {
|
||||
|
||||
virtual void draw(Canvas& canvas) = 0;
|
||||
|
||||
// An App whose screen is costly to draw again (a picture decoded from the card, issue #45) can
|
||||
// keep what it drew: while this is true its part of the screen isn't cleared before draw(),
|
||||
// which then draws only what changed. contentLost() says that it was cleared after all, or
|
||||
// that something drawn over it has gone: everything has to be drawn again.
|
||||
virtual bool retainsContent() const { return false; }
|
||||
virtual void contentLost() {}
|
||||
|
||||
void requestRedraw() { redraw_ = true; }
|
||||
|
||||
bool consumeRedraw() {
|
||||
|
||||
@@ -292,6 +292,14 @@ inline constexpr KeyHelp kViewerOta[] = {
|
||||
{"Tab", "the file as hex"},
|
||||
};
|
||||
|
||||
// viewer-image: A picture
|
||||
inline constexpr KeyHelp kViewerImage[] = {
|
||||
{"Enter", "its own size, or all of it"},
|
||||
{"; . , /", "move around it"},
|
||||
{"i", "its size"},
|
||||
{"Tab", "the file as hex"},
|
||||
};
|
||||
|
||||
// notes: Notes, the list
|
||||
inline constexpr KeyHelp kNotes[] = {
|
||||
{"; .", "up, down"},
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
#include "image_file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include "file_names.h"
|
||||
#include "png_rgb332.h"
|
||||
|
||||
namespace roro::files {
|
||||
|
||||
namespace {
|
||||
uint32_t le16(const uint8_t* p) { return p[0] | (p[1] << 8); }
|
||||
uint32_t le32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (static_cast<uint32_t>(p[3]) << 24); }
|
||||
uint32_t be16(const uint8_t* p) { return (p[0] << 8) | p[1]; }
|
||||
uint32_t be32(const uint8_t* p) { return (static_cast<uint32_t>(p[0]) << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; }
|
||||
|
||||
constexpr int kMaxSide = 16384; // more than that isn't a picture for this screen
|
||||
const uint8_t kPngSignature[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
|
||||
|
||||
// A file read from its start on, a small block at a time.
|
||||
class Stream {
|
||||
public:
|
||||
Stream(const ImageRead& read, uint32_t size) : read_(read), size_(size) {}
|
||||
int get() {
|
||||
if (at_ >= have_) {
|
||||
if (next_ >= size_) return -1;
|
||||
have_ = read_(next_, buf_, std::min<size_t>(sizeof buf_, size_ - next_));
|
||||
at_ = 0;
|
||||
next_ += static_cast<uint32_t>(have_);
|
||||
if (!have_) return -1;
|
||||
}
|
||||
return buf_[at_++];
|
||||
}
|
||||
bool take(uint8_t* into, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
int c = get();
|
||||
if (c < 0) return false;
|
||||
into[i] = static_cast<uint8_t>(c);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool skip(size_t len) {
|
||||
size_t buffered = std::min(len, have_ - at_);
|
||||
at_ += buffered;
|
||||
len -= buffered;
|
||||
if (len > size_ - next_) return false;
|
||||
next_ += static_cast<uint32_t>(len);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const ImageRead& read_;
|
||||
uint32_t size_, next_ = 0;
|
||||
uint8_t buf_[256];
|
||||
size_t have_ = 0, at_ = 0;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
ImageKind imageKindOfName(const std::string& name) {
|
||||
std::string ext = extensionOf(name);
|
||||
if (ext == "png") return ImageKind::Png;
|
||||
if (ext == "jpg" || ext == "jpeg") return ImageKind::Jpeg;
|
||||
if (ext == "bmp") return ImageKind::Bmp;
|
||||
if (ext == "gif") return ImageKind::Gif;
|
||||
return ImageKind::None;
|
||||
}
|
||||
|
||||
ImageKind imageKindOfBytes(const uint8_t* head, size_t len) {
|
||||
if (len >= 8 && std::memcmp(head, kPngSignature, 8) == 0) return ImageKind::Png;
|
||||
if (len >= 3 && head[0] == 0xFF && head[1] == 0xD8 && head[2] == 0xFF) return ImageKind::Jpeg;
|
||||
if (len >= 6 && (std::memcmp(head, "GIF87a", 6) == 0 || std::memcmp(head, "GIF89a", 6) == 0)) return ImageKind::Gif;
|
||||
if (len >= 2 && head[0] == 'B' && head[1] == 'M') return ImageKind::Bmp;
|
||||
return ImageKind::None;
|
||||
}
|
||||
|
||||
const char* imageKindName(ImageKind kind) {
|
||||
switch (kind) {
|
||||
case ImageKind::Png: return "PNG";
|
||||
case ImageKind::Jpeg: return "JPEG";
|
||||
case ImageKind::Bmp: return "BMP";
|
||||
case ImageKind::Gif: return "GIF";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
std::string imageInfo(const ImageRead& read, uint32_t size, ImageInfo& out) {
|
||||
uint8_t head[32];
|
||||
size_t n = read(0, head, std::min<size_t>(sizeof head, size));
|
||||
out.kind = imageKindOfBytes(head, n);
|
||||
long w = 0, h = 0;
|
||||
switch (out.kind) {
|
||||
case ImageKind::Png:
|
||||
if (n < 24 || std::memcmp(head + 12, "IHDR", 4) != 0) return "This PNG is damaged";
|
||||
w = static_cast<long>(be32(head + 16));
|
||||
h = static_cast<long>(be32(head + 20));
|
||||
break;
|
||||
case ImageKind::Gif:
|
||||
if (n < 10) return "This GIF is damaged";
|
||||
w = static_cast<long>(le16(head + 6));
|
||||
h = static_cast<long>(le16(head + 8));
|
||||
break;
|
||||
case ImageKind::Bmp: {
|
||||
if (n < 26) return "This BMP is damaged";
|
||||
uint32_t dib = le32(head + 14);
|
||||
if (dib < 40) return "This kind of BMP can't be shown";
|
||||
w = static_cast<int32_t>(le32(head + 18));
|
||||
h = static_cast<int32_t>(le32(head + 22));
|
||||
if (h < 0) h = -h; // top row first
|
||||
break;
|
||||
}
|
||||
case ImageKind::Jpeg: {
|
||||
// Marker after marker until the one that carries the size.
|
||||
uint32_t at = 2;
|
||||
for (int guard = 0; guard < 4000; guard++) {
|
||||
uint8_t m[9];
|
||||
if (read(at, m, 4) != 4 || m[0] != 0xFF) return "This JPEG is damaged";
|
||||
uint8_t marker = m[1];
|
||||
if (marker == 0xFF) { // padding
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD8)) { // no length
|
||||
at += 2;
|
||||
continue;
|
||||
}
|
||||
if (marker == 0xD9 || marker == 0xDA) return "This JPEG is damaged"; // the picture, and no size yet
|
||||
bool frame = marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC;
|
||||
if (frame) {
|
||||
if (read(at, m, 9) != 9) return "This JPEG is damaged";
|
||||
h = static_cast<long>(be16(m + 5));
|
||||
w = static_cast<long>(be16(m + 7));
|
||||
if (marker == 0xC2) return "A progressive JPEG can't be shown";
|
||||
if (marker != 0xC0 && marker != 0xC1) return "This kind of JPEG can't be shown";
|
||||
break;
|
||||
}
|
||||
at += 2 + be16(m + 2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: return "Not a picture this can show";
|
||||
}
|
||||
if (w <= 0 || h <= 0) return "This picture is damaged";
|
||||
if (w > kMaxSide || h > kMaxSide) return "Too big: 16,384 pixels a side at most";
|
||||
out.width = static_cast<int>(w);
|
||||
out.height = static_cast<int>(h);
|
||||
return "";
|
||||
}
|
||||
|
||||
uint32_t screenshotPixelsAt(const ImageRead& read, uint32_t size, int width, int height) {
|
||||
if (width <= 0 || height <= 0 || size != png::Rgb332Writer::fileSize(width, height)) return 0;
|
||||
// Signature, IHDR, then a palette of 256 colours, then the one IDAT: a zlib header and a
|
||||
// single stored block.
|
||||
uint8_t ihdr[5], plte[8], idat[15];
|
||||
constexpr uint32_t kPlteAt = 8 + 25, kIdatAt = kPlteAt + 12 + 768;
|
||||
if (read(24, ihdr, 5) != 5 || ihdr[0] != 8 || ihdr[1] != 3 || ihdr[4] != 0) return 0;
|
||||
if (read(kPlteAt, plte, 8) != 8 || be32(plte) != 768 || std::memcmp(plte + 4, "PLTE", 4) != 0) return 0;
|
||||
if (read(kIdatAt, idat, 15) != 15 || std::memcmp(idat + 4, "IDAT", 4) != 0) return 0;
|
||||
uint32_t raw = static_cast<uint32_t>(width + 1) * static_cast<uint32_t>(height);
|
||||
if (idat[8] != 0x78 || idat[10] != 0x01 || le16(idat + 11) != raw || le16(idat + 13) != (raw ^ 0xFFFF)) return 0;
|
||||
return kIdatAt + 15 + 1; // past the first row's filter byte
|
||||
}
|
||||
|
||||
uint8_t rgb332Dithered(uint8_t r, uint8_t g, uint8_t b, int x, int y) {
|
||||
static const uint8_t kBayer[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
|
||||
int threshold = kBayer[((y & 3) << 2) | (x & 3)] * 16 + 8; // 8 to 248
|
||||
auto level = [threshold](int v, int top) {
|
||||
int nearest = (v * top + 127) / 255;
|
||||
if (nearest * 255 / top == v) return nearest; // a colour the screen has
|
||||
int low = v * top / 255, rest = v * top - low * 255;
|
||||
return rest > threshold ? low + 1 : low;
|
||||
};
|
||||
return static_cast<uint8_t>((level(r, 7) << 5) | (level(g, 7) << 2) | level(b, 3));
|
||||
}
|
||||
|
||||
bool ImageMap::at(int sx, int sy, int& tx, int& ty) const {
|
||||
if (sx < 0 || sy < 0) return false;
|
||||
if (scale >= 65536) {
|
||||
tx = sx + offX;
|
||||
ty = sy + offY;
|
||||
} else {
|
||||
uint64_t fx = static_cast<uint64_t>(sx) * scale, fy = static_cast<uint64_t>(sy) * scale;
|
||||
if ((fx & 0xFFFF) >= scale || (fy & 0xFFFF) >= scale) return false;
|
||||
tx = static_cast<int>(fx >> 16) + offX;
|
||||
ty = static_cast<int>(fy >> 16) + offY;
|
||||
}
|
||||
if (tx < 0 || ty < 0 || tx >= viewW || ty >= viewH) return false;
|
||||
tx += viewX;
|
||||
ty += viewY;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImageMap::rowUsed(int sy) const {
|
||||
if (sy < 0) return false;
|
||||
int ty;
|
||||
if (scale >= 65536) {
|
||||
ty = sy + offY;
|
||||
} else {
|
||||
uint64_t fy = static_cast<uint64_t>(sy) * scale;
|
||||
if ((fy & 0xFFFF) >= scale) return false;
|
||||
ty = static_cast<int>(fy >> 16) + offY;
|
||||
}
|
||||
return ty >= 0 && ty < viewH;
|
||||
}
|
||||
|
||||
bool ImageMap::below(int sy) const {
|
||||
if (sy < 0) return false;
|
||||
int ty = scale >= 65536 ? sy + offY : static_cast<int>((static_cast<uint64_t>(sy) * scale) >> 16) + offY;
|
||||
return ty >= viewH;
|
||||
}
|
||||
|
||||
ImageFrame::ImageFrame(int width, int height, int viewX, int viewY, int viewW, int viewH)
|
||||
: w_(std::max(1, width)), h_(std::max(1, height)), vx_(viewX), vy_(viewY), vw_(std::max(1, viewW)), vh_(std::max(1, viewH)) {}
|
||||
|
||||
uint32_t ImageFrame::fitScale() const {
|
||||
uint64_t sx = (static_cast<uint64_t>(vw_) << 16) / static_cast<uint64_t>(w_), sy = (static_cast<uint64_t>(vh_) << 16) / static_cast<uint64_t>(h_);
|
||||
return static_cast<uint32_t>(std::min<uint64_t>(65536, std::max<uint64_t>(1, std::min(sx, sy))));
|
||||
}
|
||||
|
||||
void ImageFrame::toggle() {
|
||||
if (!bigger()) return;
|
||||
actual_ = !actual_;
|
||||
if (actual_) { // the middle of it first
|
||||
panX_ = std::max(0, (w_ - vw_) / 2);
|
||||
panY_ = std::max(0, (h_ - vh_) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool ImageFrame::pan(int dx, int dy) {
|
||||
if (!actual_) return false;
|
||||
int x = std::clamp(panX_ + dx * (vw_ / 2), 0, std::max(0, w_ - vw_));
|
||||
int y = std::clamp(panY_ + dy * (vh_ / 2), 0, std::max(0, h_ - vh_));
|
||||
bool moved = x != panX_ || y != panY_;
|
||||
panX_ = x;
|
||||
panY_ = y;
|
||||
return moved;
|
||||
}
|
||||
|
||||
int ImageFrame::percent() const { return actual_ ? 100 : static_cast<int>((static_cast<uint64_t>(fitScale()) * 100 + 32768) >> 16); }
|
||||
|
||||
int ImageFrame::jpegShrink() const {
|
||||
if (actual_) return 0;
|
||||
uint32_t scale = fitScale();
|
||||
int shrink = 0;
|
||||
while (shrink < 3 && (static_cast<uint64_t>(scale) << (shrink + 1)) <= 65536) shrink++;
|
||||
return shrink;
|
||||
}
|
||||
|
||||
ImageMap ImageFrame::map(int shrink) const {
|
||||
ImageMap m;
|
||||
m.viewX = vx_;
|
||||
m.viewY = vy_;
|
||||
m.viewW = vw_;
|
||||
m.viewH = vh_;
|
||||
if (actual_) {
|
||||
m.scale = 65536;
|
||||
m.offX = w_ <= vw_ ? (vw_ - w_) / 2 : -panX_;
|
||||
m.offY = h_ <= vh_ ? (vh_ - h_) / 2 : -panY_;
|
||||
return m;
|
||||
}
|
||||
uint32_t scale = fitScale();
|
||||
int tw = std::max<int>(1, static_cast<int>((static_cast<uint64_t>(w_) * scale) >> 16));
|
||||
int th = std::max<int>(1, static_cast<int>((static_cast<uint64_t>(h_) * scale) >> 16));
|
||||
m.offX = (vw_ - tw) / 2;
|
||||
m.offY = (vh_ - th) / 2;
|
||||
m.scale = static_cast<uint32_t>(std::min<uint64_t>(65536, static_cast<uint64_t>(scale) << shrink));
|
||||
return m;
|
||||
}
|
||||
|
||||
std::string readBmp(const ImageRead& read, uint32_t size, const ImagePixels& pixels, const std::function<bool(int y)>& rowNeeded) {
|
||||
uint8_t head[54];
|
||||
if (read(0, head, sizeof head) != sizeof head || head[0] != 'B' || head[1] != 'M') return "This BMP is damaged";
|
||||
uint32_t dataAt = le32(head + 10), dib = le32(head + 14), compression = le32(head + 30), colours = le32(head + 46);
|
||||
int32_t w = static_cast<int32_t>(le32(head + 18)), h = static_cast<int32_t>(le32(head + 22));
|
||||
uint32_t bits = le16(head + 28);
|
||||
bool topDown = h < 0;
|
||||
if (topDown) h = -h;
|
||||
if (dib < 40 || w <= 0 || h <= 0 || w > kMaxSide || h > kMaxSide) return "This kind of BMP can't be shown";
|
||||
if ((bits != 8 && bits != 24 && bits != 32) || (compression != 0 && !(compression == 3 && bits == 32))) return "This kind of BMP can't be shown";
|
||||
std::unique_ptr<uint8_t[]> palette;
|
||||
if (bits == 8) {
|
||||
if (!colours || colours > 256) colours = 256;
|
||||
palette.reset(new (std::nothrow) uint8_t[1024]());
|
||||
if (!palette) return "Not enough memory";
|
||||
if (read(14 + dib, palette.get(), colours * 4) != colours * 4) return "This BMP is damaged";
|
||||
}
|
||||
uint32_t bytes = bits / 8, rowSize = (static_cast<uint32_t>(w) * bytes + 3) & ~3u;
|
||||
if (static_cast<uint64_t>(dataAt) + static_cast<uint64_t>(rowSize) * static_cast<uint32_t>(h) > size) return "This BMP is cut short";
|
||||
// A row in one read when it fits, a piece of it at a time otherwise.
|
||||
constexpr int kOut = 64; // pixels handed on at once
|
||||
constexpr uint32_t kRowBuffer = 4096; // bytes
|
||||
uint32_t rowBytes = static_cast<uint32_t>(w) * bytes, bufSize = std::min(rowBytes, kRowBuffer);
|
||||
bufSize -= bufSize % bytes;
|
||||
std::unique_ptr<uint8_t[]> in(new (std::nothrow) uint8_t[bufSize]);
|
||||
if (!in) return "Not enough memory";
|
||||
uint8_t out[kOut * 3];
|
||||
// In the order the file has them, which is usually the last row first: going back through a
|
||||
// file on the card costs far more than going on (measured: a second for 135 rows).
|
||||
for (int stored = 0; stored < h; stored++) {
|
||||
int y = topDown ? stored : h - 1 - stored;
|
||||
if (rowNeeded && !rowNeeded(y)) continue;
|
||||
uint32_t rowAt = dataAt + rowSize * static_cast<uint32_t>(stored);
|
||||
for (uint32_t done = 0; done < rowBytes; done += bufSize) {
|
||||
size_t want = std::min(bufSize, rowBytes - done);
|
||||
if (read(rowAt + done, in.get(), want) != want) return "The card refused to read it";
|
||||
int first = static_cast<int>(done / bytes), count = static_cast<int>(want / bytes);
|
||||
for (int at = 0; at < count; at += kOut) {
|
||||
int n = std::min(kOut, count - at);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const uint8_t* p = bits == 8 ? palette.get() + in[at + i] * 4 : in.get() + static_cast<size_t>(at + i) * bytes;
|
||||
out[i * 3] = p[2]; // stored blue, green, red
|
||||
out[i * 3 + 1] = p[1];
|
||||
out[i * 3 + 2] = p[0];
|
||||
}
|
||||
pixels(first + at, y, n, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct GifWork {
|
||||
uint8_t palette[768];
|
||||
uint16_t prefix[4096];
|
||||
uint8_t suffix[4096], stack[4096];
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::string readGif(const ImageRead& read, uint32_t size, const ImagePixels& pixels) {
|
||||
Stream in(read, size);
|
||||
uint8_t head[13];
|
||||
if (!in.take(head, 13) || imageKindOfBytes(head, 6) != ImageKind::Gif) return "This GIF is damaged";
|
||||
int screenW = static_cast<int>(le16(head + 6)), screenH = static_cast<int>(le16(head + 8));
|
||||
std::unique_ptr<GifWork> work(new (std::nothrow) GifWork);
|
||||
if (!work) return "Not enough memory to show a GIF";
|
||||
std::memset(work->palette, 0, sizeof work->palette);
|
||||
if (head[10] & 0x80 && !in.take(work->palette, 3u << ((head[10] & 7) + 1))) return "This GIF is damaged";
|
||||
int transparent = -1;
|
||||
for (int guard = 0; guard < 100000; guard++) {
|
||||
int kind = in.get();
|
||||
if (kind == 0x21) { // an extension: only the one before a picture matters, for its transparent colour
|
||||
int label = in.get();
|
||||
for (bool first = true;; first = false) {
|
||||
int len = in.get();
|
||||
if (len < 0) return "This GIF is damaged";
|
||||
if (len == 0) break;
|
||||
uint8_t block[255];
|
||||
if (!in.take(block, static_cast<size_t>(len))) return "This GIF is damaged";
|
||||
if (label == 0xF9 && first && len >= 4) transparent = (block[0] & 1) ? block[3] : -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (kind != 0x2C) return kind == 0x3B ? "This GIF has no picture" : "This GIF is damaged";
|
||||
break;
|
||||
}
|
||||
uint8_t desc[9];
|
||||
if (!in.take(desc, 9)) return "This GIF is damaged";
|
||||
int left = static_cast<int>(le16(desc)), top = static_cast<int>(le16(desc + 2));
|
||||
int fw = static_cast<int>(le16(desc + 4)), fh = static_cast<int>(le16(desc + 6));
|
||||
bool interlaced = desc[8] & 0x40;
|
||||
if (desc[8] & 0x80 && !in.take(work->palette, 3u << ((desc[8] & 7) + 1))) return "This GIF is damaged";
|
||||
int minBits = in.get();
|
||||
if (fw <= 0 || fh <= 0 || minBits < 2 || minBits > 8) return "This GIF is damaged";
|
||||
|
||||
// The pixels come out in the order they are stored; an interlaced picture stores every eighth
|
||||
// row first, then the rows between, in four passes.
|
||||
static const int kStart[4] = {0, 4, 2, 1}, kStep[4] = {8, 8, 4, 2};
|
||||
int px = 0, row = 0, pass = 0, rowsDone = 0;
|
||||
uint8_t run[64 * 3];
|
||||
int runLen = 0, runX = 0;
|
||||
auto flush = [&]() {
|
||||
int y = top + row;
|
||||
if (runLen && y >= 0 && y < screenH) pixels(left + runX, y, runLen, run);
|
||||
runLen = 0;
|
||||
};
|
||||
auto put = [&](uint8_t index) {
|
||||
if (rowsDone >= fh) return;
|
||||
int x = left + px;
|
||||
if (index == transparent || x < 0 || x >= screenW) {
|
||||
flush();
|
||||
} else {
|
||||
if (!runLen) runX = px;
|
||||
std::memcpy(run + runLen * 3, work->palette + index * 3, 3);
|
||||
if (++runLen == 64) flush();
|
||||
}
|
||||
if (++px < fw) return;
|
||||
flush();
|
||||
px = 0;
|
||||
rowsDone++;
|
||||
if (!interlaced) {
|
||||
row++;
|
||||
return;
|
||||
}
|
||||
row += kStep[pass];
|
||||
while (row >= fh && pass < 3) row = kStart[++pass];
|
||||
};
|
||||
|
||||
const int clear = 1 << minBits, stop = clear + 1;
|
||||
int bits = minBits + 1, next = clear + 2, prev = -1, first = 0;
|
||||
uint32_t hold = 0;
|
||||
int held = 0, blockLeft = 0;
|
||||
bool ended = false;
|
||||
for (int i = 0; i < clear; i++) work->suffix[i] = static_cast<uint8_t>(i);
|
||||
while (rowsDone < fh && !ended) {
|
||||
while (held < bits) {
|
||||
if (!blockLeft) {
|
||||
blockLeft = in.get();
|
||||
if (blockLeft <= 0) {
|
||||
ended = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int c = in.get();
|
||||
if (c < 0) return "This GIF is cut short";
|
||||
blockLeft--;
|
||||
hold |= static_cast<uint32_t>(c) << held;
|
||||
held += 8;
|
||||
}
|
||||
if (ended) break;
|
||||
int code = static_cast<int>(hold & ((1u << bits) - 1));
|
||||
hold >>= bits;
|
||||
held -= bits;
|
||||
if (code == clear) {
|
||||
bits = minBits + 1;
|
||||
next = clear + 2;
|
||||
prev = -1;
|
||||
continue;
|
||||
}
|
||||
if (code == stop) break;
|
||||
if (prev < 0) {
|
||||
if (code >= clear) return "This GIF is damaged";
|
||||
put(static_cast<uint8_t>(code));
|
||||
first = prev = code;
|
||||
continue;
|
||||
}
|
||||
if (code > next) return "This GIF is damaged";
|
||||
int sp = 0, walk = code;
|
||||
if (code == next) { // the string being defined: the one before, and its own first pixel again
|
||||
work->stack[sp++] = static_cast<uint8_t>(first);
|
||||
walk = prev;
|
||||
}
|
||||
while (walk >= clear && sp < 4095) {
|
||||
work->stack[sp++] = work->suffix[walk];
|
||||
walk = work->prefix[walk];
|
||||
}
|
||||
if (walk >= clear) return "This GIF is damaged";
|
||||
first = walk;
|
||||
work->stack[sp++] = static_cast<uint8_t>(walk);
|
||||
if (next < 4096) {
|
||||
work->prefix[next] = static_cast<uint16_t>(prev);
|
||||
work->suffix[next] = static_cast<uint8_t>(first);
|
||||
next++;
|
||||
if (next == (1 << bits) && bits < 12) bits++;
|
||||
}
|
||||
prev = code;
|
||||
while (sp) put(work->stack[--sp]);
|
||||
}
|
||||
flush();
|
||||
return rowsDone ? "" : "This GIF is damaged";
|
||||
}
|
||||
|
||||
} // namespace roro::files
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
// Pictures for the Storage App (issue #45, F1 Q233-Q242): what a file is and how big, where each
|
||||
// of its pixels goes on the screen, and the readers for BMP and the first frame of a GIF. PNG is in
|
||||
// png_reader.h; JPEG is decoded on the device by the display library's decoder.
|
||||
// Nothing here holds a picture: every reader hands its pixels on as it gets them.
|
||||
namespace roro::files {
|
||||
|
||||
enum class ImageKind : uint8_t { None, Png, Jpeg, Bmp, Gif };
|
||||
|
||||
// Reads up to `len` bytes at `offset`; returns how many it got.
|
||||
using ImageRead = std::function<size_t(uint32_t offset, uint8_t* into, size_t len)>;
|
||||
// `count` pixels of row `y` from column `x` on, three bytes each: red, green, blue.
|
||||
using ImagePixels = std::function<void(int x, int y, int count, const uint8_t* rgb)>;
|
||||
|
||||
ImageKind imageKindOfName(const std::string& name); // by its extension
|
||||
ImageKind imageKindOfBytes(const uint8_t* head, size_t len); // by its first bytes (8 are enough)
|
||||
const char* imageKindName(ImageKind kind);
|
||||
|
||||
struct ImageInfo {
|
||||
ImageKind kind = ImageKind::None;
|
||||
int width = 0, height = 0;
|
||||
};
|
||||
// "" and `out` filled, or why the file can't be shown.
|
||||
std::string imageInfo(const ImageRead& read, uint32_t size, ImageInfo& out);
|
||||
|
||||
// A PNG the firmware's `screenshot` wrote (png_rgb332.h): its pixels are not compressed and each
|
||||
// is already a colour of the screen. Where the first row's pixels start, or 0 if it isn't one.
|
||||
// Row y's pixels are at that offset + y * (width + 1).
|
||||
uint32_t screenshotPixelsAt(const ImageRead& read, uint32_t size, int width, int height);
|
||||
|
||||
// A colour as the screen has it (RRRGGGBB), dithered by where it lands: the screen has 8 levels of
|
||||
// red and green and 4 of blue, and a photograph bands without it. A colour the screen has exactly
|
||||
// comes out as itself wherever it lands, so a screenshot isn't touched.
|
||||
uint8_t rgb332Dithered(uint8_t r, uint8_t g, uint8_t b, int x, int y);
|
||||
|
||||
// From a picture's pixels to the screen's.
|
||||
struct ImageMap {
|
||||
int viewX = 0, viewY = 0, viewW = 0, viewH = 0; // the part of the screen the picture may use
|
||||
int offX = 0, offY = 0; // the picture's corner in it (negative: scrolled)
|
||||
uint32_t scale = 65536; // screen pixels for one of the picture's, 16.16; never over 1
|
||||
|
||||
// Where the picture's pixel lands. False if it's outside the view, or if another pixel is the
|
||||
// one drawn there (shrunk, each screen pixel takes the first of the picture's that falls on it).
|
||||
bool at(int sx, int sy, int& tx, int& ty) const;
|
||||
bool rowUsed(int sy) const; // does any pixel of this row land?
|
||||
bool below(int sy) const; // this row and every one after it land under the view: nothing more to draw
|
||||
};
|
||||
|
||||
// How a picture is looked at: whole, shrunk to fit if it has to be; or at its own size, a
|
||||
// screenful at a time.
|
||||
class ImageFrame {
|
||||
public:
|
||||
ImageFrame() = default;
|
||||
ImageFrame(int width, int height, int viewX, int viewY, int viewW, int viewH);
|
||||
|
||||
bool bigger() const { return w_ > vw_ || h_ > vh_; } // than the view: there is something to zoom
|
||||
bool actual() const { return actual_; }
|
||||
void toggle();
|
||||
bool pan(int dx, int dy); // half a view a step, at its own size only; false: nothing moved
|
||||
int percent() const; // of its own size, as shown
|
||||
int jpegShrink() const; // 0 to 3: the halvings a JPEG decoder may do first, the picture still at least as big as shown
|
||||
// For pixels counted after `shrink` halvings (a JPEG's), or the picture's own.
|
||||
ImageMap map(int shrink = 0) const;
|
||||
|
||||
private:
|
||||
uint32_t fitScale() const;
|
||||
|
||||
int w_ = 0, h_ = 0, vx_ = 0, vy_ = 0, vw_ = 1, vh_ = 1, panX_ = 0, panY_ = 0;
|
||||
bool actual_ = false;
|
||||
};
|
||||
|
||||
// A BMP: 8 bits with a palette, 24 or 32 bits, not compressed. `rowNeeded` lets rows be skipped
|
||||
// without being read. "" or why it can't be shown.
|
||||
std::string readBmp(const ImageRead& read, uint32_t size, const ImagePixels& pixels, const std::function<bool(int y)>& rowNeeded = nullptr);
|
||||
|
||||
// The first picture of a GIF, interlaced or not; transparent pixels are not handed on. It needs
|
||||
// 17 KB while it runs. "" or why it can't be shown.
|
||||
std::string readGif(const ImageRead& read, uint32_t size, const ImagePixels& pixels);
|
||||
|
||||
} // namespace roro::files
|
||||
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "image_file.h"
|
||||
|
||||
namespace roro::files {
|
||||
|
||||
// A PNG, decoded a row at a time (issue #45): every colour type and bit depth, not interlaced.
|
||||
// Pixels that are mostly transparent are not handed on. `rowNeeded` lets rows be left out (they
|
||||
// are still decoded: a row is stored as its difference from the one before); `enough` says that
|
||||
// from this row on nothing is wanted, and the decoding stops there.
|
||||
//
|
||||
// Memory while it runs: the window the file's compression refers back into (what its header asks
|
||||
// for, 32 KB at most), two rows of the picture, and about 3 KB. The display library's decoder
|
||||
// wanted 44 KB in one block, which this device often doesn't have.
|
||||
//
|
||||
// "" or why it can't be shown.
|
||||
std::string readPng(const ImageRead& read, uint32_t size, const ImagePixels& pixels, const std::function<bool(int y)>& rowNeeded = nullptr,
|
||||
const std::function<bool(int y)>& enough = nullptr);
|
||||
|
||||
} // namespace roro::files
|
||||
Reference in New Issue
Block a user