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:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,293 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "image_file.h"
|
||||
#include "images.h"
|
||||
#include "png_rgb332.h"
|
||||
|
||||
using namespace roro::files;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
namespace {
|
||||
ImageRead from(const std::string& bytes) {
|
||||
return [&bytes](uint32_t at, uint8_t* into, size_t len) -> size_t {
|
||||
if (at >= bytes.size()) return 0;
|
||||
size_t n = std::min(len, bytes.size() - at);
|
||||
std::memcpy(into, bytes.data() + at, n);
|
||||
return n;
|
||||
};
|
||||
}
|
||||
template <size_t N>
|
||||
std::string str(const uint8_t (&a)[N]) { return std::string(reinterpret_cast<const char*>(a), N); }
|
||||
|
||||
// The palette and the two patterns the test files were drawn with.
|
||||
void colour(int i, uint8_t* rgb) {
|
||||
rgb[0] = static_cast<uint8_t>((i * 7) % 256);
|
||||
rgb[1] = static_cast<uint8_t>((255 - i * 3) % 256);
|
||||
rgb[2] = static_cast<uint8_t>((i * 13 + 40) % 256);
|
||||
}
|
||||
int small(int x, int y) { return (x * 3 + y * 5) % 7; }
|
||||
int big(int x, int y) { return (x * x + y * y * 3 + x * y) & 0xFF; }
|
||||
|
||||
struct Picture {
|
||||
int w, h;
|
||||
std::vector<int> rgb; // -1: never drawn
|
||||
Picture(int width, int height) : w(width), h(height), rgb(static_cast<size_t>(width * height), -1) {}
|
||||
ImagePixels sink() {
|
||||
return [this](int x, int y, int count, const uint8_t* p) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
TEST_ASSERT_TRUE(x + i >= 0 && x + i < w && y >= 0 && y < h);
|
||||
rgb[static_cast<size_t>(y * w + x + i)] = (p[i * 3] << 16) | (p[i * 3 + 1] << 8) | p[i * 3 + 2];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
void checkGif(const std::string& file, int w, int h, int (*index)(int, int), int transparent = -1) {
|
||||
Picture p(w, h);
|
||||
TEST_ASSERT_EQUAL_STRING("", readGif(from(file), static_cast<uint32_t>(file.size()), p.sink()).c_str());
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) {
|
||||
uint8_t c[3];
|
||||
colour(index(x, y), c);
|
||||
int want = index(x, y) == transparent ? -1 : (c[0] << 16) | (c[1] << 8) | c[2];
|
||||
char msg[40];
|
||||
std::snprintf(msg, sizeof msg, "at %d,%d", x, y);
|
||||
TEST_ASSERT_EQUAL_HEX32_MESSAGE(want, p.rgb[static_cast<size_t>(y * w + x)], msg);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_kinds() {
|
||||
TEST_ASSERT_TRUE(imageKindOfName("Photo.JPG") == ImageKind::Jpeg);
|
||||
TEST_ASSERT_TRUE(imageKindOfName("a.jpeg") == ImageKind::Jpeg);
|
||||
TEST_ASSERT_TRUE(imageKindOfName("20261007-104357.png") == ImageKind::Png);
|
||||
TEST_ASSERT_TRUE(imageKindOfName("x.bmp") == ImageKind::Bmp);
|
||||
TEST_ASSERT_TRUE(imageKindOfName("x.gif") == ImageKind::Gif);
|
||||
TEST_ASSERT_TRUE(imageKindOfName("notes.txt") == ImageKind::None);
|
||||
const uint8_t jpeg[] = {0xFF, 0xD8, 0xFF, 0xE0};
|
||||
TEST_ASSERT_TRUE(imageKindOfBytes(jpeg, 4) == ImageKind::Jpeg);
|
||||
TEST_ASSERT_TRUE(imageKindOfBytes(kGifSmall, 6) == ImageKind::Gif);
|
||||
TEST_ASSERT_TRUE(imageKindOfBytes(kBmp24, 8) == ImageKind::Bmp);
|
||||
TEST_ASSERT_TRUE(imageKindOfBytes(reinterpret_cast<const uint8_t*>("Hello"), 5) == ImageKind::None);
|
||||
}
|
||||
|
||||
void test_sizes() {
|
||||
ImageInfo info;
|
||||
std::string gif = str(kGifBig), bmp = str(kBmp24);
|
||||
TEST_ASSERT_EQUAL_STRING("", imageInfo(from(gif), static_cast<uint32_t>(gif.size()), info).c_str());
|
||||
TEST_ASSERT_TRUE(info.kind == ImageKind::Gif && info.width == 96 && info.height == 64);
|
||||
TEST_ASSERT_EQUAL_STRING("", imageInfo(from(bmp), static_cast<uint32_t>(bmp.size()), info).c_str());
|
||||
TEST_ASSERT_TRUE(info.kind == ImageKind::Bmp && info.width == 13 && info.height == 7);
|
||||
|
||||
// A JPEG: start, an application segment, a table, then the frame header with the size.
|
||||
std::string jpeg("\xFF\xD8", 2);
|
||||
jpeg += std::string("\xFF\xE1\x00\x08" "Exif\0\0", 10);
|
||||
jpeg += std::string("\xFF\xDB\x00\x04\x00\x00", 6);
|
||||
std::string frame("\xFF\xC0\x00\x11\x08\x0B\xB8\x0F\xA0\x03", 10); // 3000 high, 4000 wide
|
||||
std::string baseline = jpeg + frame + std::string(20, '\0');
|
||||
TEST_ASSERT_EQUAL_STRING("", imageInfo(from(baseline), static_cast<uint32_t>(baseline.size()), info).c_str());
|
||||
TEST_ASSERT_TRUE(info.kind == ImageKind::Jpeg && info.width == 4000 && info.height == 3000);
|
||||
frame[1] = '\xC2';
|
||||
std::string progressive = jpeg + frame + std::string(20, '\0');
|
||||
TEST_ASSERT_EQUAL_STRING("A progressive JPEG can't be shown", imageInfo(from(progressive), static_cast<uint32_t>(progressive.size()), info).c_str());
|
||||
std::string cut = jpeg;
|
||||
TEST_ASSERT_EQUAL_STRING("This JPEG is damaged", imageInfo(from(cut), static_cast<uint32_t>(cut.size()), info).c_str());
|
||||
|
||||
std::string text = "Just some words, not a picture at all.";
|
||||
TEST_ASSERT_EQUAL_STRING("Not a picture this can show", imageInfo(from(text), static_cast<uint32_t>(text.size()), info).c_str());
|
||||
}
|
||||
|
||||
void test_a_screenshot_is_read_without_decoding() {
|
||||
const int w = 240, h = 135;
|
||||
std::string file;
|
||||
roro::png::Rgb332Writer writer(w, h, [&](const uint8_t* d, size_t n) {
|
||||
file.append(reinterpret_cast<const char*>(d), n);
|
||||
return true;
|
||||
});
|
||||
TEST_ASSERT_TRUE(writer.begin());
|
||||
std::vector<uint8_t> row(w);
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) row[static_cast<size_t>(x)] = static_cast<uint8_t>(x * 3 + y * 7);
|
||||
TEST_ASSERT_TRUE(writer.row(row.data()));
|
||||
}
|
||||
TEST_ASSERT_TRUE(writer.end());
|
||||
ImageInfo info;
|
||||
TEST_ASSERT_EQUAL_STRING("", imageInfo(from(file), static_cast<uint32_t>(file.size()), info).c_str());
|
||||
TEST_ASSERT_TRUE(info.kind == ImageKind::Png && info.width == w && info.height == h);
|
||||
uint32_t at = screenshotPixelsAt(from(file), static_cast<uint32_t>(file.size()), w, h);
|
||||
TEST_ASSERT_TRUE(at > 0);
|
||||
for (int y : {0, 1, 77, 134})
|
||||
for (int x : {0, 5, 239})
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(x * 3 + y * 7), static_cast<uint8_t>(file[at + static_cast<uint32_t>(y * (w + 1) + x)]));
|
||||
// Another PNG isn't taken for one: a byte more, a compressed block.
|
||||
std::string longer = file + "x";
|
||||
TEST_ASSERT_EQUAL_UINT32(0, screenshotPixelsAt(from(longer), static_cast<uint32_t>(longer.size()), w, h));
|
||||
std::string other = file;
|
||||
other[8 + 25 + 780 + 10] = 0x05;
|
||||
TEST_ASSERT_EQUAL_UINT32(0, screenshotPixelsAt(from(other), static_cast<uint32_t>(other.size()), w, h));
|
||||
}
|
||||
|
||||
void test_dithering_leaves_the_screens_own_colours_alone() {
|
||||
for (int v = 0; v < 256; v++) {
|
||||
uint8_t r = static_cast<uint8_t>((v >> 5) * 255 / 7), g = static_cast<uint8_t>(((v >> 2) & 7) * 255 / 7), b = static_cast<uint8_t>((v & 3) * 85);
|
||||
for (int y = 0; y < 4; y++)
|
||||
for (int x = 0; x < 4; x++) TEST_ASSERT_EQUAL_UINT8(v, rgb332Dithered(r, g, b, x, y));
|
||||
}
|
||||
// A grey between two levels comes out as a mix of both, in proportion.
|
||||
int high = 0;
|
||||
for (int y = 0; y < 4; y++)
|
||||
for (int x = 0; x < 4; x++) {
|
||||
int red = rgb332Dithered(54, 0, 0, x, y) >> 5; // half way from 36 to 72
|
||||
TEST_ASSERT_TRUE(red == 1 || red == 2);
|
||||
high += red == 2;
|
||||
}
|
||||
TEST_ASSERT_TRUE(high >= 6 && high <= 10);
|
||||
TEST_ASSERT_EQUAL_UINT8(0xFF, rgb332Dithered(255, 255, 255, 1, 2));
|
||||
TEST_ASSERT_EQUAL_UINT8(0x00, rgb332Dithered(0, 0, 0, 3, 3));
|
||||
}
|
||||
|
||||
void test_a_small_picture_sits_in_the_middle() {
|
||||
ImageFrame f(100, 50, 0, 12, 240, 123);
|
||||
TEST_ASSERT_FALSE(f.bigger());
|
||||
TEST_ASSERT_EQUAL_INT(100, f.percent());
|
||||
ImageMap m = f.map();
|
||||
int tx, ty;
|
||||
TEST_ASSERT_TRUE(m.at(0, 0, tx, ty));
|
||||
TEST_ASSERT_EQUAL_INT(70, tx);
|
||||
TEST_ASSERT_EQUAL_INT(12 + 36, ty);
|
||||
TEST_ASSERT_TRUE(m.at(99, 49, tx, ty));
|
||||
TEST_ASSERT_EQUAL_INT(169, tx);
|
||||
TEST_ASSERT_FALSE(m.at(-1, 0, tx, ty));
|
||||
f.toggle(); // nothing to zoom
|
||||
TEST_ASSERT_FALSE(f.actual());
|
||||
TEST_ASSERT_EQUAL_INT(0, f.jpegShrink());
|
||||
}
|
||||
|
||||
void test_a_big_picture_is_shrunk_to_fit_each_screen_pixel_once() {
|
||||
ImageFrame f(4000, 3000, 0, 12, 240, 123);
|
||||
TEST_ASSERT_TRUE(f.bigger());
|
||||
TEST_ASSERT_EQUAL_INT(4, f.percent()); // 123 / 3000
|
||||
ImageMap m = f.map();
|
||||
std::vector<int> hits(240 * 135, 0);
|
||||
int rows = 0;
|
||||
for (int y = 0; y < 3000; y++) {
|
||||
if (!m.rowUsed(y)) continue;
|
||||
rows++;
|
||||
for (int x = 0; x < 4000; x++) {
|
||||
int tx, ty;
|
||||
if (m.at(x, y, tx, ty)) hits[static_cast<size_t>(ty * 240 + tx)]++;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(123, rows);
|
||||
int drawn = 0;
|
||||
for (int ty = 0; ty < 135; ty++)
|
||||
for (int tx = 0; tx < 240; tx++) {
|
||||
int n = hits[static_cast<size_t>(ty * 240 + tx)];
|
||||
TEST_ASSERT_TRUE(n <= 1);
|
||||
drawn += n;
|
||||
if (n) TEST_ASSERT_TRUE(ty >= 12 && tx >= 38 && tx < 202); // 164 wide, centred
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(164 * 123, drawn);
|
||||
|
||||
// A JPEG decoder may halve it three times first; the map then takes those pixels.
|
||||
TEST_ASSERT_EQUAL_INT(3, f.jpegShrink());
|
||||
ImageMap j = f.map(3);
|
||||
int tx, ty, again = 0;
|
||||
for (int y = 0; y < 375; y++)
|
||||
for (int x = 0; x < 500; x++) again += j.at(x, y, tx, ty);
|
||||
TEST_ASSERT_EQUAL_INT(164 * 123, again);
|
||||
TEST_ASSERT_EQUAL_INT(0, ImageFrame(300, 200, 0, 12, 240, 123).jpegShrink()); // not by half: it would be too small
|
||||
TEST_ASSERT_EQUAL_INT(1, ImageFrame(480, 246, 0, 12, 240, 123).jpegShrink());
|
||||
}
|
||||
|
||||
void test_at_its_own_size_it_moves_half_a_screen_at_a_time() {
|
||||
ImageFrame f(800, 600, 0, 12, 240, 123);
|
||||
f.toggle();
|
||||
TEST_ASSERT_TRUE(f.actual());
|
||||
TEST_ASSERT_EQUAL_INT(100, f.percent());
|
||||
ImageMap m = f.map();
|
||||
int tx, ty;
|
||||
TEST_ASSERT_TRUE(m.at(280, 238, tx, ty)); // the middle first
|
||||
TEST_ASSERT_EQUAL_INT(0, tx);
|
||||
TEST_ASSERT_EQUAL_INT(12, ty);
|
||||
TEST_ASSERT_FALSE(m.at(279, 238, tx, ty));
|
||||
TEST_ASSERT_FALSE(m.below(238 + 122)); // the last row on screen
|
||||
TEST_ASSERT_TRUE(m.below(238 + 123)); // from here on a decoder can stop
|
||||
TEST_ASSERT_TRUE(f.pan(1, 0));
|
||||
TEST_ASSERT_TRUE(f.map().at(400, 238, tx, ty));
|
||||
TEST_ASSERT_EQUAL_INT(0, tx);
|
||||
for (int i = 0; i < 10; i++) f.pan(1, 1);
|
||||
TEST_ASSERT_FALSE(f.pan(1, 0)); // the corner: no further
|
||||
TEST_ASSERT_TRUE(f.map().at(799, 599, tx, ty));
|
||||
TEST_ASSERT_EQUAL_INT(239, tx);
|
||||
TEST_ASSERT_EQUAL_INT(12 + 122, ty);
|
||||
f.toggle();
|
||||
TEST_ASSERT_FALSE(f.actual());
|
||||
TEST_ASSERT_FALSE(f.pan(1, 0)); // fitted: nothing to move
|
||||
}
|
||||
|
||||
void test_gif() {
|
||||
checkGif(str(kGifSmall), 16, 12, small);
|
||||
checkGif(str(kGifSmallInterlaced), 16, 12, small);
|
||||
checkGif(str(kGifBig), 96, 64, big); // long enough for the codes to grow to 12 bits and start over
|
||||
checkGif(str(kGifBigInterlaced), 96, 64, big);
|
||||
checkGif(str(kGifTransparent), 16, 12, small, 3);
|
||||
}
|
||||
|
||||
void test_gif_damaged() {
|
||||
Picture p(96, 64);
|
||||
std::string cut = str(kGifBig).substr(0, 3000);
|
||||
TEST_ASSERT_EQUAL_STRING("This GIF is cut short", readGif(from(cut), static_cast<uint32_t>(cut.size()), p.sink()).c_str());
|
||||
std::string head = str(kGifBig).substr(0, 10);
|
||||
TEST_ASSERT_EQUAL_STRING("This GIF is damaged", readGif(from(head), static_cast<uint32_t>(head.size()), p.sink()).c_str());
|
||||
// Any byte changed: an answer, never a crash or a pixel outside the picture (the sink checks).
|
||||
std::string file = str(kGifBig);
|
||||
for (size_t i = 0; i < file.size(); i += 37) {
|
||||
std::string bad = file;
|
||||
bad[i] = static_cast<char>(bad[i] ^ 0x5A);
|
||||
Picture q(96, 64);
|
||||
readGif(from(bad), static_cast<uint32_t>(bad.size()), q.sink());
|
||||
}
|
||||
}
|
||||
|
||||
void test_bmp() {
|
||||
std::string file = str(kBmp24);
|
||||
Picture p(13, 7);
|
||||
TEST_ASSERT_EQUAL_STRING("", readBmp(from(file), static_cast<uint32_t>(file.size()), p.sink()).c_str());
|
||||
for (int y = 0; y < 7; y++)
|
||||
for (int x = 0; x < 13; x++)
|
||||
TEST_ASSERT_EQUAL_HEX32((((x * 9) % 256) << 16) | (((y * 17) % 256) << 8) | ((x + y) % 256), p.rgb[static_cast<size_t>(y * 13 + x)]);
|
||||
std::string file8 = str(kBmp8);
|
||||
Picture q(13, 7);
|
||||
TEST_ASSERT_EQUAL_STRING("", readBmp(from(file8), static_cast<uint32_t>(file8.size()), q.sink()).c_str());
|
||||
for (int i = 0; i < 13 * 7; i++)
|
||||
TEST_ASSERT_EQUAL_HEX32((kBmp8Pixels[i * 3] << 16) | (kBmp8Pixels[i * 3 + 1] << 8) | kBmp8Pixels[i * 3 + 2], q.rgb[static_cast<size_t>(i)]);
|
||||
// Only the rows asked for are read.
|
||||
Picture r(13, 7);
|
||||
readBmp(from(file), static_cast<uint32_t>(file.size()), r.sink(), [](int y) { return y == 2; });
|
||||
TEST_ASSERT_EQUAL_INT(-1, r.rgb[0]);
|
||||
TEST_ASSERT_TRUE(r.rgb[2 * 13] >= 0);
|
||||
std::string cut = file.substr(0, 200);
|
||||
TEST_ASSERT_EQUAL_STRING("This BMP is cut short", readBmp(from(cut), static_cast<uint32_t>(cut.size()), p.sink()).c_str());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_kinds);
|
||||
RUN_TEST(test_sizes);
|
||||
RUN_TEST(test_a_screenshot_is_read_without_decoding);
|
||||
RUN_TEST(test_dithering_leaves_the_screens_own_colours_alone);
|
||||
RUN_TEST(test_a_small_picture_sits_in_the_middle);
|
||||
RUN_TEST(test_a_big_picture_is_shrunk_to_fit_each_screen_pixel_once);
|
||||
RUN_TEST(test_at_its_own_size_it_moves_half_a_screen_at_a_time);
|
||||
RUN_TEST(test_gif);
|
||||
RUN_TEST(test_gif_damaged);
|
||||
RUN_TEST(test_bmp);
|
||||
return UNITY_END();
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,191 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "png_reader.h"
|
||||
#include "png_rgb332.h"
|
||||
#include "pngs.h"
|
||||
|
||||
using namespace roro::files;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
namespace {
|
||||
ImageRead from(const std::string& bytes) {
|
||||
return [&bytes](uint32_t at, uint8_t* into, size_t len) -> size_t {
|
||||
if (at >= bytes.size()) return 0;
|
||||
size_t n = std::min(len, bytes.size() - at);
|
||||
std::memcpy(into, bytes.data() + at, n);
|
||||
return n;
|
||||
};
|
||||
}
|
||||
template <size_t N>
|
||||
std::string str(const uint8_t (&a)[N]) { return std::string(reinterpret_cast<const char*>(a), N); }
|
||||
|
||||
struct Picture {
|
||||
int w, h;
|
||||
std::vector<int> rgb; // -1: never drawn
|
||||
Picture(int width, int height) : w(width), h(height), rgb(static_cast<size_t>(width * height), -1) {}
|
||||
ImagePixels sink() {
|
||||
return [this](int x, int y, int count, const uint8_t* p) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
TEST_ASSERT_TRUE(x + i >= 0 && x + i < w && y >= 0 && y < h);
|
||||
rgb[static_cast<size_t>(y * w + x + i)] = (p[i * 3] << 16) | (p[i * 3 + 1] << 8) | p[i * 3 + 2];
|
||||
}
|
||||
};
|
||||
}
|
||||
int at(int x, int y) const { return rgb[static_cast<size_t>(y * w + x)]; }
|
||||
};
|
||||
|
||||
Picture decode(const std::string& file, int w, int h) {
|
||||
Picture p(w, h);
|
||||
TEST_ASSERT_EQUAL_STRING("", readPng(from(file), static_cast<uint32_t>(file.size()), p.sink()).c_str());
|
||||
return p;
|
||||
}
|
||||
int colour(int x, int y) { return (((x * 9 + y) % 256) << 16) | (((y * 17) % 256) << 8) | ((x * x + y * y) % 256); }
|
||||
int grey(int v) { return (v << 16) | (v << 8) | v; }
|
||||
} // namespace
|
||||
|
||||
void test_rgb() {
|
||||
Picture p = decode(str(kPngRgb), 96, 64);
|
||||
for (int y = 0; y < 64; y++)
|
||||
for (int x = 0; x < 96; x++) TEST_ASSERT_EQUAL_HEX32(colour(x, y), p.at(x, y));
|
||||
}
|
||||
|
||||
void test_a_noisy_picture_with_every_kind_of_row() {
|
||||
Picture p = decode(str(kPngPhoto), 64, 48);
|
||||
for (int i = 0; i < 64 * 48; i++)
|
||||
TEST_ASSERT_EQUAL_HEX32((kPngPhotoPixels[i * 3] << 16) | (kPngPhotoPixels[i * 3 + 1] << 8) | kPngPhotoPixels[i * 3 + 2], p.rgb[static_cast<size_t>(i)]);
|
||||
}
|
||||
|
||||
void test_stored_not_compressed() {
|
||||
Picture p = decode(str(kPngStored), 16, 12);
|
||||
for (int y = 0; y < 12; y++)
|
||||
for (int x = 0; x < 16; x++) TEST_ASSERT_EQUAL_HEX32(colour(x, y), p.at(x, y));
|
||||
}
|
||||
|
||||
void test_references_far_back() {
|
||||
Picture p = decode(str(kPngFarBack), 120, 90);
|
||||
uint32_t total = 0;
|
||||
for (int v : p.rgb) {
|
||||
TEST_ASSERT_TRUE(v >= 0);
|
||||
total += static_cast<uint32_t>((v >> 16) + ((v >> 8) & 255) + (v & 255));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32((kPngFarBackSum[0] << 24) | (kPngFarBackSum[1] << 16) | (kPngFarBackSum[2] << 8) | kPngFarBackSum[3], total);
|
||||
for (int x = 0; x < 120; x++) TEST_ASSERT_EQUAL_HEX32(p.at(x, 0) + 1, p.at(x, 60)); // the same row, its blue one more
|
||||
}
|
||||
|
||||
void test_transparent_pixels_are_left_out() {
|
||||
Picture p = decode(str(kPngRgba), 16, 12);
|
||||
for (int y = 0; y < 12; y++)
|
||||
for (int x = 0; x < 16; x++) TEST_ASSERT_EQUAL_HEX32((x + y) % 5 == 0 ? -1 : colour(x, y), p.at(x, y));
|
||||
Picture g = decode(str(kPngGrayAlpha), 16, 12);
|
||||
for (int y = 0; y < 12; y++)
|
||||
for (int x = 0; x < 16; x++) TEST_ASSERT_EQUAL_HEX32(x % 2 ? grey((x * 16 + y * 3) % 256) : -1, g.at(x, y));
|
||||
}
|
||||
|
||||
void test_palettes() {
|
||||
auto entry = [](int i) { return (((i * 7) % 256) << 16) | (((255 - i * 3) % 256) << 8) | ((i * 13 + 40) % 256); };
|
||||
Picture p = decode(str(kPngPalette), 16, 12);
|
||||
for (int y = 0; y < 12; y++)
|
||||
for (int x = 0; x < 16; x++) {
|
||||
int i = (x * 3 + y * 5) % 7;
|
||||
TEST_ASSERT_EQUAL_HEX32(i == 3 ? -1 : entry(i), p.at(x, y)); // index 3 is the transparent one
|
||||
}
|
||||
Picture q = decode(str(kPngPalette4), 17, 5); // four bits a pixel, a row that ends in half a byte
|
||||
for (int y = 0; y < 5; y++)
|
||||
for (int x = 0; x < 17; x++) TEST_ASSERT_EQUAL_HEX32(entry((x + y * 3) % 13), q.at(x, y));
|
||||
}
|
||||
|
||||
void test_greys() {
|
||||
Picture g = decode(str(kPngGray), 16, 12);
|
||||
Picture one = decode(str(kPngGray1), 19, 7);
|
||||
Picture deep = decode(str(kPngGray16), 16, 12);
|
||||
for (int y = 0; y < 12; y++)
|
||||
for (int x = 0; x < 16; x++) {
|
||||
TEST_ASSERT_EQUAL_HEX32(grey((x * 16 + y * 3) % 256), g.at(x, y));
|
||||
TEST_ASSERT_EQUAL_HEX32(grey(((x * 4000 + y * 300) % 65536) >> 8), deep.at(x, y));
|
||||
}
|
||||
for (int y = 0; y < 7; y++)
|
||||
for (int x = 0; x < 19; x++) TEST_ASSERT_EQUAL_HEX32((x + y) % 2 ? 0xFFFFFF : 0, one.at(x, y));
|
||||
}
|
||||
|
||||
void test_one_of_the_firmwares_own_screenshots() {
|
||||
std::string file;
|
||||
roro::png::Rgb332Writer writer(40, 9, [&](const uint8_t* d, size_t n) {
|
||||
file.append(reinterpret_cast<const char*>(d), n);
|
||||
return true;
|
||||
});
|
||||
writer.begin();
|
||||
std::vector<uint8_t> row(40);
|
||||
for (int y = 0; y < 9; y++) {
|
||||
for (int x = 0; x < 40; x++) row[static_cast<size_t>(x)] = static_cast<uint8_t>(x * 6 + y);
|
||||
writer.row(row.data());
|
||||
}
|
||||
writer.end();
|
||||
Picture p = decode(file, 40, 9);
|
||||
for (int y = 0; y < 9; y++)
|
||||
for (int x = 0; x < 40; x++) {
|
||||
int v = (x * 6 + y) & 0xFF;
|
||||
TEST_ASSERT_EQUAL_HEX32((((v >> 5) * 255 / 7) << 16) | ((((v >> 2) & 7) * 255 / 7) << 8) | ((v & 3) * 85), p.at(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
void test_rows_can_be_left_out_and_the_end_cut_off() {
|
||||
std::string file = str(kPngRgb);
|
||||
Picture p(96, 64);
|
||||
int reads = 0;
|
||||
ImageRead counted = [&](uint32_t at, uint8_t* into, size_t len) {
|
||||
reads++;
|
||||
return from(file)(at, into, len);
|
||||
};
|
||||
TEST_ASSERT_EQUAL_STRING("", readPng(counted, static_cast<uint32_t>(file.size()), p.sink(), [](int y) { return y % 2 == 0; }, [](int y) { return y >= 10; }).c_str());
|
||||
TEST_ASSERT_EQUAL_HEX32(colour(5, 8), p.at(5, 8));
|
||||
TEST_ASSERT_EQUAL_INT(-1, p.at(5, 9)); // left out
|
||||
TEST_ASSERT_EQUAL_INT(-1, p.at(5, 10)); // and nothing from the tenth on
|
||||
Picture all(96, 64);
|
||||
int allReads = 0;
|
||||
ImageRead countAll = [&](uint32_t at, uint8_t* into, size_t len) {
|
||||
allReads++;
|
||||
return from(file)(at, into, len);
|
||||
};
|
||||
readPng(countAll, static_cast<uint32_t>(file.size()), all.sink());
|
||||
TEST_ASSERT_TRUE(reads < allReads / 2); // it stopped reading the file too
|
||||
}
|
||||
|
||||
void test_what_it_refuses() {
|
||||
Picture p(96, 64);
|
||||
std::string interlaced = str(kPngRgb);
|
||||
interlaced[28] = 1;
|
||||
TEST_ASSERT_EQUAL_STRING("An interlaced PNG can't be shown", readPng(from(interlaced), static_cast<uint32_t>(interlaced.size()), p.sink()).c_str());
|
||||
std::string cut = str(kPngRgb).substr(0, 2000);
|
||||
TEST_ASSERT_EQUAL_STRING("This PNG is cut short", readPng(from(cut), static_cast<uint32_t>(cut.size()), p.sink()).c_str());
|
||||
std::string notPng = "GIF89a and then some more bytes that are not a PNG";
|
||||
TEST_ASSERT_EQUAL_STRING("This PNG is damaged", readPng(from(notPng), static_cast<uint32_t>(notPng.size()), p.sink()).c_str());
|
||||
// Any byte changed: an answer, never a crash or a pixel outside the picture (the sink checks).
|
||||
std::string file = str(kPngPhoto);
|
||||
for (size_t i = 8; i < file.size(); i += 7) {
|
||||
std::string bad = file;
|
||||
bad[i] = static_cast<char>(bad[i] ^ 0xA5);
|
||||
Picture q(64, 48);
|
||||
readPng(from(bad), static_cast<uint32_t>(bad.size()), q.sink());
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_rgb);
|
||||
RUN_TEST(test_a_noisy_picture_with_every_kind_of_row);
|
||||
RUN_TEST(test_stored_not_compressed);
|
||||
RUN_TEST(test_references_far_back);
|
||||
RUN_TEST(test_transparent_pixels_are_left_out);
|
||||
RUN_TEST(test_palettes);
|
||||
RUN_TEST(test_greys);
|
||||
RUN_TEST(test_one_of_the_firmwares_own_screenshots);
|
||||
RUN_TEST(test_rows_can_be_left_out_and_the_end_cut_off);
|
||||
RUN_TEST(test_what_it_refuses);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user