mirror of
https://github.com/OpenEPaperLink/OpenEPaperLink.git
synced 2026-03-21 05:06:39 +01:00
color related improvements
- added "image" to json commands to insert a jpg image/icon from the flash partition - added optional center/right alignment to "textbox" json command - google calendar content: added optional colors, different color per calendarid - improved ordered dithering, works also with unevenly spaced color tables. This is to be used with graphs etc., not suitable for photos (use floyd steinberg for photos) - improved flyod steinberg dithering (fix some bugs) - added preview rendering for 4bpp images - log tab now scrolls to the top on entering - added optional perceptual color table to tagtypes (for rendering previews, for example to display darker yellows on screen while keeping the 255,255,0 color to the tag - drag/dropping in an image to a tag while holding shift key now uses ordered dithering (default is floyd steinberg) - some tagtype improvements
This commit is contained in:
BIN
ESP32_AP-Flasher/data/fonts/tahoma11.vlw
Normal file
BIN
ESP32_AP-Flasher/data/fonts/tahoma11.vlw
Normal file
Binary file not shown.
@@ -19,7 +19,7 @@ void checkVars();
|
||||
void drawNew(const uint8_t mac[8], tagRecord *&taginfo);
|
||||
bool updateTagImage(String &filename, const uint8_t *dst, uint16_t nextCheckin, tagRecord *&taginfo, imgParam &imageParams);
|
||||
void drawString(TFT_eSprite &spr, String content, int16_t posx, int16_t posy, String font, byte align = 0, uint16_t color = TFT_BLACK, uint16_t size = 30, uint16_t bgcolor = TFT_WHITE);
|
||||
void drawTextBox(TFT_eSprite &spr, String &content, int16_t &posx, int16_t &posy, int16_t boxwidth, int16_t boxheight, String font, uint16_t color = TFT_BLACK, uint16_t bgcolor = TFT_WHITE, float lineheight = 1);
|
||||
void drawTextBox(TFT_eSprite &spr, String &content, int16_t &posx, int16_t &posy, int16_t boxwidth, int16_t boxheight, String font, uint16_t color = TFT_BLACK, uint16_t bgcolor = TFT_WHITE, float lineheight = 1, byte align = TL_DATUM);
|
||||
void initSprite(TFT_eSprite &spr, int w, int h, imgParam &imageParams);
|
||||
void drawDate(String &filename, tagRecord *&taginfo, imgParam &imageParams);
|
||||
void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord *&taginfo, imgParam &imageParams);
|
||||
|
||||
@@ -17,7 +17,6 @@ struct imgParam {
|
||||
bool hasRed;
|
||||
uint8_t dataType;
|
||||
uint8_t dither;
|
||||
// bool grayLut = false;
|
||||
uint8_t bufferbpp = 8;
|
||||
uint8_t rotate = 0;
|
||||
uint16_t highlightColor = 2;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#define NO_SUBGHZ_CHANNEL 255
|
||||
class tagRecord {
|
||||
public:
|
||||
tagRecord() : mac{0}, version(0), alias(""), lastseen(0), nextupdate(0), contentMode(0), pendingCount(0), md5{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false), apIp(IPAddress(0, 0, 0, 0)), pendingIdle(0), hasCustomLUT(false), rotate(0), lut(0), tagSoftwareVersion(0), currentChannel(0), dataType(0), filename(""), data(nullptr), len(0), invert(0), updateCount(0), updateLast(0) {}
|
||||
tagRecord() : mac{0}, version(0), alias(""), lastseen(0), nextupdate(0), contentMode(0), pendingCount(0), md5{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false), apIp(IPAddress(0, 0, 0, 0)), pendingIdle(0), rotate(0), lut(0), tagSoftwareVersion(0), currentChannel(0), dataType(0), filename(""), data(nullptr), len(0), invert(0), updateCount(0), updateLast(0) {}
|
||||
|
||||
uint8_t mac[8];
|
||||
uint8_t version;
|
||||
@@ -38,7 +38,6 @@ class tagRecord {
|
||||
bool isExternal;
|
||||
IPAddress apIp;
|
||||
uint16_t pendingIdle;
|
||||
bool hasCustomLUT;
|
||||
uint8_t rotate;
|
||||
uint8_t lut;
|
||||
uint16_t tagSoftwareVersion;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#ifdef CONTENT_RSS
|
||||
#include <rssClass.h>
|
||||
#endif
|
||||
#include <TJpg_Decoder.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <map>
|
||||
@@ -212,7 +213,6 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
imageParams.hasRed = false;
|
||||
imageParams.dataType = DATATYPE_IMG_RAW_1BPP;
|
||||
imageParams.dither = 2;
|
||||
// if (taginfo->hasCustomLUT && taginfo->lut != 1) imageParams.grayLut = true;
|
||||
|
||||
imageParams.invert = taginfo->invert;
|
||||
imageParams.symbols = 0;
|
||||
@@ -240,10 +240,6 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
imageParams.lut = EPD_LUT_DEFAULT;
|
||||
taginfo->lastfullupdate = now;
|
||||
}
|
||||
if (taginfo->hasCustomLUT && taginfo->capabilities & CAPABILITY_SUPPORTS_CUSTOM_LUTS && taginfo->lut != 1) {
|
||||
Serial.println("using custom LUT");
|
||||
imageParams.lut = EPD_LUT_OTA;
|
||||
}
|
||||
|
||||
int32_t interval = cfgobj["interval"].as<int>() * 60;
|
||||
if (interval == -1440 * 60) {
|
||||
@@ -696,7 +692,7 @@ void drawString(TFT_eSprite &spr, String content, int16_t posx, int16_t posy, St
|
||||
}
|
||||
}
|
||||
|
||||
void drawTextBox(TFT_eSprite &spr, String &content, int16_t &posx, int16_t &posy, int16_t boxwidth, int16_t boxheight, String font, uint16_t color, uint16_t bgcolor, float lineheight) {
|
||||
void drawTextBox(TFT_eSprite &spr, String &content, int16_t &posx, int16_t &posy, int16_t boxwidth, int16_t boxheight, String font, uint16_t color, uint16_t bgcolor, float lineheight, byte align) {
|
||||
replaceVariables(content);
|
||||
switch (processFontPath(font)) {
|
||||
case 2: {
|
||||
@@ -706,7 +702,7 @@ void drawTextBox(TFT_eSprite &spr, String &content, int16_t &posx, int16_t &posy
|
||||
case 3: {
|
||||
// vlw bitmap font
|
||||
// spr.drawRect(posx, posy, boxwidth, boxheight, TFT_BLACK);
|
||||
spr.setTextDatum(TL_DATUM);
|
||||
spr.setTextDatum(align);
|
||||
if (font != "") spr.loadFont(font.substring(1), *contentFS);
|
||||
spr.setTextWrap(false, false);
|
||||
spr.setTextColor(color, bgcolor);
|
||||
@@ -1195,7 +1191,7 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
|
||||
wsLog("get calendar");
|
||||
|
||||
StaticJsonDocument<512> loc;
|
||||
StaticJsonDocument<1024> loc;
|
||||
getTemplate(loc, 11, taginfo->hwType);
|
||||
|
||||
String URL = cfgobj["apps_script_url"].as<String>() + "?days=" + loc["days"].as<String>();
|
||||
@@ -1293,6 +1289,13 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
int calYOffset = loc["gridparam"][1].as<int>();
|
||||
int lineHeight = loc["gridparam"][5].as<int>();
|
||||
|
||||
uint16_t backgroundLight = getColor("lightgray");
|
||||
uint16_t backgroundDark = getColor("darkgray");
|
||||
if (imageParams.hwdata.bpp >= 3) {
|
||||
backgroundLight = getColor("#BEFAFF");
|
||||
backgroundDark = getColor("#79FAFF");
|
||||
}
|
||||
|
||||
// drawString(spr, String(timeinfo.tm_mday), calWidth / 2, -calHeight/5, "Signika-SB.ttf", TC_DATUM, imageParams.highlightColor, calHeight * 1.2);
|
||||
|
||||
for (int i = 0; i < calDays; i++) {
|
||||
@@ -1306,9 +1309,9 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
drawString(spr, String(languageDaysShort[dayInfo->tm_wday]) + " " + String(dayInfo->tm_mday), colStart + colWidth / 2, calTop, loc["gridparam"][3], TC_DATUM, TFT_BLACK);
|
||||
|
||||
if (dayInfo->tm_wday == 0 || dayInfo->tm_wday == 6) {
|
||||
spr.fillRect(colStart + 1, calTop + calYOffset, colWidth - 1, calHeight - 1, getColor("darkgray"));
|
||||
spr.fillRect(colStart + 1, calTop + calYOffset, colWidth - 1, calHeight - 1, backgroundDark);
|
||||
} else {
|
||||
spr.fillRect(colStart + 1, calTop + calYOffset, colWidth - 1, calHeight - 1, getColor("lightgray"));
|
||||
spr.fillRect(colStart + 1, calTop + calYOffset, colWidth - 1, calHeight - 1, backgroundLight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1329,6 +1332,7 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
const time_t startdatetime = obj["start"];
|
||||
const time_t enddatetime = obj["end"];
|
||||
const bool isallday = obj["isallday"];
|
||||
const int calendarId = obj["calendar"];
|
||||
|
||||
if (!isallday) {
|
||||
localtime_r(&startdatetime, &timeinfo);
|
||||
@@ -1360,8 +1364,15 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
|
||||
int16_t eventX = colLeft + fulldaystart * colWidth + 3;
|
||||
int16_t eventY = calTop + calYOffset + (line - 1) * lineHeight + 3;
|
||||
spr.drawRect(eventX - 2, eventY - 3, colWidth * (fulldayend - fulldaystart) - 1, lineHeight + 1, TFT_BLACK);
|
||||
spr.fillRect(eventX - 1, eventY - 2, colWidth * (fulldayend - fulldaystart) - 3, lineHeight - 1, TFT_WHITE);
|
||||
uint16_t background = TFT_WHITE;
|
||||
uint16_t border = TFT_BLACK;
|
||||
if (imageParams.hwdata.bpp >= 3 && loc["colors1"].is<JsonArray>() && loc["colors1"].size() > calendarId) {
|
||||
background = getColor(loc["colors1"][calendarId]);
|
||||
border = getColor(loc["colors2"][calendarId]);
|
||||
Serial.println("cal " + String(calendarId) + ": " + String(loc["colors2"][calendarId]));
|
||||
}
|
||||
spr.fillRect(eventX - 1, eventY - 2, colWidth * (fulldayend - fulldaystart) - 3, lineHeight - 1, background);
|
||||
spr.drawRect(eventX - 2, eventY - 3, colWidth * (fulldayend - fulldaystart) - 1, lineHeight + 1, border);
|
||||
drawTextBox(spr, eventtitle, eventX, eventY, colWidth * (fulldayend - fulldaystart) - 3, 15, loc["gridparam"][4], TFT_BLACK);
|
||||
|
||||
block[i] = line;
|
||||
@@ -1393,6 +1404,7 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
const time_t startdatetime = obj["start"];
|
||||
const time_t enddatetime = obj["end"];
|
||||
const bool isallday = obj["isallday"];
|
||||
const int calendarId = obj["calendar"];
|
||||
|
||||
if (!isallday) {
|
||||
int fulldaystart = constrain((startdatetime - midnightEpoch) / (24 * 3600), 0, calDays);
|
||||
@@ -1431,12 +1443,22 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
block[i] = indent;
|
||||
int16_t eventX = colLeft + day * colWidth + (indent - 1) * 5;
|
||||
int16_t eventY = calTop + calYOffset + (starttime * hourHeight / 60);
|
||||
spr.drawRect(eventX + 1, eventY, colWidth - 1, (duration * hourHeight / 60) + 1, TFT_BLACK);
|
||||
spr.fillRect(eventX + 2, eventY + 1, colWidth - 3, (duration * hourHeight / 60) - 1, TFT_WHITE);
|
||||
uint16_t background = TFT_WHITE;
|
||||
uint16_t border = TFT_BLACK;
|
||||
if (imageParams.hwdata.bpp >= 3 && loc["colors1"].is<JsonArray>() && loc["colors1"].size() > calendarId) {
|
||||
background = getColor(loc["colors1"][calendarId]);
|
||||
border = getColor(loc["colors2"][calendarId]);
|
||||
Serial.println("cal " + String(calendarId) + ": " + String(loc["colors2"][calendarId]));
|
||||
}
|
||||
spr.fillRect(eventX + 2, eventY + 1, colWidth - 3, (duration * hourHeight / 60) - 1, background);
|
||||
spr.drawRect(eventX + 1, eventY, colWidth - 1, (duration * hourHeight / 60) + 1, border);
|
||||
eventX += 2;
|
||||
eventY += 2;
|
||||
if (day == fulldaystart) {
|
||||
eventtitle = formattedTimeString + " " + String(eventtitle);
|
||||
drawTextBox(spr, formattedTimeString, eventX, eventY, colWidth - 1, (duration * hourHeight / 60) - 1, loc["gridparam"][4], TFT_BLACK, TFT_WHITE, 1);
|
||||
eventX++;
|
||||
eventY = calTop + calYOffset + (starttime * hourHeight / 60) + 2;
|
||||
} else {
|
||||
eventtitle = obj["title"].as<String>();
|
||||
}
|
||||
@@ -1613,12 +1635,13 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
minPrice = yAxisScale.min;
|
||||
|
||||
uint16_t yAxisX = loc["yaxis"][1].as<int>();
|
||||
uint16_t yAxisY = loc["yaxis"][3].as<int>() | 9;
|
||||
uint16_t barBottom = loc["bars"][3].as<int>();
|
||||
|
||||
for (double i = minPrice; i <= maxPrice; i += yAxisScale.step) {
|
||||
int y = mapDouble(i, minPrice, maxPrice, spr.height() - barBottom, spr.height() - barBottom - loc["bars"][2].as<int>());
|
||||
spr.drawLine(0, y, spr.width(), y, TFT_BLACK);
|
||||
if (loc["yaxis"][0]) drawString(spr, String(int(i * units)), yAxisX, y - 9, loc["yaxis"][0], TL_DATUM, TFT_BLACK);
|
||||
if (loc["yaxis"][0]) drawString(spr, String(int(i * units)), yAxisX, y - yAxisY, loc["yaxis"][0], TL_DATUM, TFT_BLACK);
|
||||
}
|
||||
|
||||
uint16_t barwidth = loc["bars"][1].as<int>() / n;
|
||||
@@ -2209,6 +2232,11 @@ void rotateBuffer(uint8_t rotation, uint8_t ¤tOrientation, TFT_eSprite &sp
|
||||
}
|
||||
}
|
||||
|
||||
TFT_eSprite sprDraw = TFT_eSprite(&tft);
|
||||
bool spr_draw(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) {
|
||||
sprDraw.pushImage(x, y, w, h, bitmap);
|
||||
return 1;
|
||||
}
|
||||
void drawElement(const JsonObject &element, TFT_eSprite &spr, imgParam &imageParams, uint8_t ¤tOrientation) {
|
||||
if (element.containsKey("text")) {
|
||||
const JsonArray &textArray = element["text"];
|
||||
@@ -2218,13 +2246,15 @@ void drawElement(const JsonObject &element, TFT_eSprite &spr, imgParam &imagePar
|
||||
const uint16_t bgcolor = (bgcolorstr.length() > 0) ? getColor(bgcolorstr) : TFT_WHITE;
|
||||
drawString(spr, textArray[2], textArray[0].as<int>(), textArray[1].as<int>(), textArray[3], align, getColor(textArray[4]), size, bgcolor);
|
||||
} else if (element.containsKey("textbox")) {
|
||||
// posx, posy, width, height, text, font, color, lineheight, align
|
||||
const JsonArray &textArray = element["textbox"];
|
||||
float lineheight = textArray[7].as<float>();
|
||||
if (lineheight == 0) lineheight = 1;
|
||||
int16_t posx = textArray[0] | 0;
|
||||
int16_t posy = textArray[1] | 0;
|
||||
String text = textArray[4];
|
||||
drawTextBox(spr, text, posx, posy, textArray[2], textArray[3], textArray[5], getColor(textArray[6]), TFT_WHITE, lineheight);
|
||||
const uint16_t align = textArray[8] | 0;
|
||||
drawTextBox(spr, text, posx, posy, textArray[2], textArray[3], textArray[5], getColor(textArray[6]), TFT_WHITE, lineheight, align);
|
||||
} else if (element.containsKey("box")) {
|
||||
const JsonArray &boxArray = element["box"];
|
||||
spr.fillRect(boxArray[0].as<int>(), boxArray[1].as<int>(), boxArray[2].as<int>(), boxArray[3].as<int>(), getColor(boxArray[4]));
|
||||
@@ -2240,6 +2270,33 @@ void drawElement(const JsonObject &element, TFT_eSprite &spr, imgParam &imagePar
|
||||
} else if (element.containsKey("circle")) {
|
||||
const JsonArray &circleArray = element["circle"];
|
||||
spr.fillCircle(circleArray[0].as<int>(), circleArray[1].as<int>(), circleArray[2].as<int>(), getColor(circleArray[3]));
|
||||
} else if (element.containsKey("image")) {
|
||||
const JsonArray &imgArray = element["image"];
|
||||
|
||||
TJpgDec.setSwapBytes(true);
|
||||
TJpgDec.setJpgScale(1);
|
||||
TJpgDec.setCallback(spr_draw);
|
||||
uint16_t w = 0, h = 0;
|
||||
String filename = imgArray[0];
|
||||
if (filename[0] != '/') {
|
||||
filename = "/" + filename;
|
||||
}
|
||||
TJpgDec.getFsJpgSize(&w, &h, filename, *contentFS);
|
||||
if (w == 0 && h == 0) {
|
||||
wsErr("invalid jpg");
|
||||
return;
|
||||
}
|
||||
Serial.println("jpeg conversion " + String(w) + "x" + String(h));
|
||||
sprDraw.setColorDepth(16);
|
||||
sprDraw.createSprite(w, h);
|
||||
if (sprDraw.getPointer() == nullptr) {
|
||||
wsErr("Failed to create sprite in contentmanager");
|
||||
} else {
|
||||
TJpgDec.drawFsJpg(0, 0, filename, *contentFS);
|
||||
sprDraw.pushToSprite(&spr, imgArray[1].as<int>(), imgArray[2].as<int>());
|
||||
sprDraw.deleteSprite();
|
||||
}
|
||||
|
||||
} else if (element.containsKey("rotate")) {
|
||||
uint8_t rotation = element["rotate"].as<int>();
|
||||
rotateBuffer(rotation, currentOrientation, spr, imageParams);
|
||||
@@ -2379,7 +2436,7 @@ void prepareConfigFile(const uint8_t *dst, const JsonObject &config) {
|
||||
|
||||
void getTemplate(JsonDocument &json, const uint8_t id, const uint8_t hwtype) {
|
||||
StaticJsonDocument<80> filter;
|
||||
DynamicJsonDocument doc(2048);
|
||||
DynamicJsonDocument doc(4096);
|
||||
|
||||
const String idstr = String(id);
|
||||
constexpr const char *templateKey = "template";
|
||||
|
||||
@@ -75,14 +75,43 @@ struct Error {
|
||||
int32_t b;
|
||||
};
|
||||
|
||||
uint32_t colorDistance(Color &c1, Color &c2, Error &e1) {
|
||||
e1.r = constrain(e1.r, -255, 255);
|
||||
e1.g = constrain(e1.g, -255, 255);
|
||||
e1.b = constrain(e1.b, -255, 255);
|
||||
uint32_t colorDistance(const Color &c1, const Color &c2, const Error &e1) {
|
||||
int32_t r_diff = c1.r + e1.r - c2.r;
|
||||
int32_t g_diff = c1.g + e1.g - c2.g;
|
||||
int32_t b_diff = c1.b + e1.b - c2.b;
|
||||
return 3 * r_diff * r_diff + 6 * g_diff * g_diff + b_diff * b_diff;
|
||||
if (abs(c1.r - c1.g) < 20 && abs(c1.b - c1.g) < 20) {
|
||||
if (abs(c2.r - c2.g) > 20 || abs(c2.b - c2.g) > 20) return 4294967295; // don't select color pixels on black and white
|
||||
}
|
||||
return 3 * r_diff * r_diff + 5.47 * g_diff * g_diff + 1.53 * b_diff * b_diff;
|
||||
}
|
||||
|
||||
std::tuple<int, int, float, float> findClosestColors(const Color &pixel, const std::vector<Color> &palette) {
|
||||
int closestIndex = -1, secondClosestIndex = -1;
|
||||
float closestDist = std::numeric_limits<float>::max();
|
||||
float secondClosestDist = std::numeric_limits<float>::max();
|
||||
for (size_t i = 0; i < palette.size(); ++i) {
|
||||
float dist = colorDistance(pixel, palette[i], (Error){0, 0, 0});
|
||||
if (dist < closestDist) {
|
||||
secondClosestIndex = closestIndex;
|
||||
secondClosestDist = closestDist;
|
||||
closestIndex = i;
|
||||
closestDist = dist;
|
||||
} else if (dist < secondClosestDist) {
|
||||
secondClosestIndex = i;
|
||||
secondClosestDist = dist;
|
||||
}
|
||||
}
|
||||
if (closestIndex != -1 && secondClosestIndex != -1) {
|
||||
auto rgbValue = [](const Color &color) {
|
||||
return (color.r << 16) | (color.g << 8) | color.b;
|
||||
};
|
||||
|
||||
if (rgbValue(palette[secondClosestIndex]) > rgbValue(palette[closestIndex])) {
|
||||
std::swap(closestIndex, secondClosestIndex);
|
||||
std::swap(closestDist, secondClosestDist);
|
||||
}
|
||||
}
|
||||
return { closestIndex, secondClosestIndex, closestDist, secondClosestDist};
|
||||
}
|
||||
|
||||
void spr2color(TFT_eSprite &spr, imgParam &imageParams, uint8_t *buffer, size_t buffer_size, bool is_red) {
|
||||
@@ -112,11 +141,6 @@ void spr2color(TFT_eSprite &spr, imgParam &imageParams, uint8_t *buffer, size_t
|
||||
Error *error_bufferold = new Error[bufw + 4];
|
||||
Error *error_buffernew = new Error[bufw + 4];
|
||||
|
||||
const uint8_t ditherMatrix[4][4] = {
|
||||
{0, 9, 2, 10},
|
||||
{12, 5, 14, 6},
|
||||
{3, 11, 1, 8},
|
||||
{15, 7, 13, 4}};
|
||||
size_t bitOffset = 0;
|
||||
|
||||
memset(error_bufferold, 0, bufw * sizeof(Error));
|
||||
@@ -138,23 +162,36 @@ void spr2color(TFT_eSprite &spr, imgParam &imageParams, uint8_t *buffer, size_t
|
||||
break;
|
||||
}
|
||||
|
||||
int best_color_index = 0;
|
||||
if (imageParams.dither == 2) {
|
||||
// Ordered dithering
|
||||
uint8_t ditherValue = ditherMatrix[y % 4][x % 4] << (imageParams.bpp >= 3 ? 2 : 4);
|
||||
error_bufferold[x].r = ditherValue - (imageParams.bpp >= 3 ? 30 : 120); // * 256 / 16 - 128 + 8
|
||||
error_bufferold[x].g = ditherValue - (imageParams.bpp >= 3 ? 30 : 120);
|
||||
error_bufferold[x].b = ditherValue - (imageParams.bpp >= 3 ? 30 : 120);
|
||||
// special ordered dithering
|
||||
auto [c1Index, c2Index, distC1, distC2] = findClosestColors(color, palette);
|
||||
Color c1 = palette[c1Index];
|
||||
Color c2 = palette[c2Index];
|
||||
float weight = distC1 / (distC1 + distC2);
|
||||
if (weight <= 0.03) {
|
||||
best_color_index = c1Index;
|
||||
} else if (weight < 0.30) {
|
||||
best_color_index = ((y % 2 && ((y / 2 + x) % 2)) ? c2Index : c1Index);
|
||||
} else if (weight < 0.70) {
|
||||
best_color_index = ((x + y) % 2 ? c2Index : c1Index);
|
||||
} else if (weight < 0.97) {
|
||||
best_color_index = ((y % 2 && ((y / 2 + x) % 2)) % 2 ? c1Index : c2Index);
|
||||
} else {
|
||||
best_color_index = c2Index;
|
||||
}
|
||||
}
|
||||
|
||||
int best_color_index = 0;
|
||||
uint32_t best_color_distance = colorDistance(color, palette[0], error_bufferold[x]);
|
||||
if (imageParams.dither == 1 || imageParams.dither == 0) {
|
||||
uint32_t best_color_distance = colorDistance(color, palette[0], error_bufferold[x]);
|
||||
|
||||
for (int i = 1; i < num_colors; i++) {
|
||||
if (best_color_distance == 0) break;
|
||||
uint32_t distance = colorDistance(color, palette[i], error_bufferold[x]);
|
||||
if (distance < best_color_distance) {
|
||||
best_color_distance = distance;
|
||||
best_color_index = i;
|
||||
for (int i = 1; i < num_colors; i++) {
|
||||
if (best_color_distance == 0) break;
|
||||
uint32_t distance = colorDistance(color, palette[i], error_bufferold[x]);
|
||||
if (distance < best_color_distance) {
|
||||
best_color_distance = distance;
|
||||
best_color_index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,34 +238,44 @@ void spr2color(TFT_eSprite &spr, imgParam &imageParams, uint8_t *buffer, size_t
|
||||
color.g + error_bufferold[x].g - palette[best_color_index].g,
|
||||
color.b + error_bufferold[x].b - palette[best_color_index].b};
|
||||
|
||||
error_buffernew[x].r += error.r >> 2;
|
||||
error_buffernew[x].g += error.g >> 2;
|
||||
error_buffernew[x].b += error.b >> 2;
|
||||
float scaling_factor = 255.0f / std::max(std::abs(error.r), std::max(std::abs(error.g), std::abs(error.b)));
|
||||
if (scaling_factor < 1.0f) {
|
||||
error.r *= scaling_factor;
|
||||
error.g *= scaling_factor;
|
||||
error.b *= scaling_factor;
|
||||
}
|
||||
|
||||
error_buffernew[x].r += error.r / 4;
|
||||
error_buffernew[x].g += error.g / 4;
|
||||
error_buffernew[x].b += error.b / 4;
|
||||
|
||||
if (x > 0) {
|
||||
error_buffernew[x - 1].r += error.r >> 3;
|
||||
error_buffernew[x - 1].g += error.g >> 3;
|
||||
error_buffernew[x - 1].b += error.b >> 3;
|
||||
error_buffernew[x - 1].r += error.r / 8;
|
||||
error_buffernew[x - 1].g += error.g / 8;
|
||||
error_buffernew[x - 1].b += error.b / 8;
|
||||
}
|
||||
|
||||
if (x > 1) {
|
||||
error_buffernew[x - 2].r += error.r >> 4;
|
||||
error_buffernew[x - 2].g += error.g >> 4;
|
||||
error_buffernew[x - 2].b += error.b >> 4;
|
||||
error_buffernew[x - 2].r += error.r / 16;
|
||||
error_buffernew[x - 2].g += error.g / 16;
|
||||
error_buffernew[x - 2].b += error.b / 16;
|
||||
}
|
||||
error_buffernew[x + 1].r += error.r >> 3;
|
||||
error_buffernew[x + 1].g += error.g >> 3;
|
||||
error_buffernew[x + 1].b += error.b >> 3;
|
||||
|
||||
error_bufferold[x + 1].r += error.r >> 2;
|
||||
error_bufferold[x + 1].g += error.g >> 2;
|
||||
error_bufferold[x + 1].b += error.b >> 2;
|
||||
error_buffernew[x + 1].r += error.r / 8;
|
||||
error_buffernew[x + 1].g += error.g / 8;
|
||||
error_buffernew[x + 1].b += error.b / 8;
|
||||
|
||||
error_buffernew[x + 2].r += error.r >> 4;
|
||||
error_buffernew[x + 2].g += error.g >> 4;
|
||||
error_buffernew[x + 2].b += error.b >> 4;
|
||||
error_bufferold[x + 1].r += error.r / 4;
|
||||
error_bufferold[x + 1].g += error.g / 4;
|
||||
error_bufferold[x + 1].b += error.b / 4;
|
||||
|
||||
error_bufferold[x + 2].r += error.r >> 3;
|
||||
error_bufferold[x + 2].g += error.g >> 3;
|
||||
error_bufferold[x + 2].b += error.b >> 3;
|
||||
error_buffernew[x + 2].r += error.r / 16;
|
||||
error_buffernew[x + 2].g += error.g / 16;
|
||||
error_buffernew[x + 2].b += error.b / 16;
|
||||
|
||||
error_bufferold[x + 2].r += error.r / 8;
|
||||
error_bufferold[x + 2].g += error.g / 8;
|
||||
error_bufferold[x + 2].b += error.b / 8;
|
||||
}
|
||||
}
|
||||
memcpy(error_bufferold, error_buffernew, bufw * sizeof(Error));
|
||||
|
||||
@@ -275,7 +275,8 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) {
|
||||
case DATATYPE_IMG_RAW_1BPP:
|
||||
case DATATYPE_IMG_RAW_2BPP:
|
||||
case DATATYPE_IMG_G5:
|
||||
case DATATYPE_IMG_RAW_3BPP: {
|
||||
case DATATYPE_IMG_RAW_3BPP:
|
||||
case DATATYPE_IMG_RAW_4BPP: {
|
||||
char hexmac[17];
|
||||
mac2hex(pending->targetMac, hexmac);
|
||||
String filename = "/current/" + String(hexmac) + "_" + String(millis() % 1000000) + ".pending";
|
||||
@@ -338,8 +339,7 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) {
|
||||
break;
|
||||
}
|
||||
case DATATYPE_NFC_RAW_CONTENT:
|
||||
case DATATYPE_NFC_URL_DIRECT:
|
||||
case DATATYPE_CUSTOM_LUT_OTA: {
|
||||
case DATATYPE_NFC_URL_DIRECT: {
|
||||
char hexmac[17];
|
||||
mac2hex(pending->targetMac, hexmac);
|
||||
char dataUrl[80];
|
||||
@@ -348,7 +348,7 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) {
|
||||
snprintf(dataUrl, sizeof(dataUrl), "http://%s/getdata?mac=%s&md5=%s", remoteIP.toString().c_str(), hexmac, md5);
|
||||
wsLog("GET " + String(dataUrl));
|
||||
HTTPClient http;
|
||||
logLine("http DATATYPE_CUSTOM_LUT_OTA " + String(dataUrl));
|
||||
logLine("http DATATYPE_NFC_* " + String(dataUrl));
|
||||
http.begin(dataUrl);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode == 200) {
|
||||
@@ -447,9 +447,11 @@ void processXferComplete(struct espXferComplete* xfc, bool local) {
|
||||
contentFS->remove(dst_path);
|
||||
}
|
||||
if (contentFS->exists(queueItem->filename)) {
|
||||
if (config.preview && (queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_3BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_2BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_1BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_G5 || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_ZLIB)) {
|
||||
uint8_t dataType = queueItem->pendingdata.availdatainfo.dataType;
|
||||
if (config.preview && dataType != DATATYPE_FW_UPDATE && dataType != DATATYPE_NOUPDATE) {
|
||||
contentFS->rename(queueItem->filename, String(dst_path));
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (queueItem->pendingdata.availdatainfo.dataType != DATATYPE_FW_UPDATE) contentFS->remove(queueItem->filename);
|
||||
}
|
||||
}
|
||||
@@ -968,7 +970,8 @@ bool queueDataAvail(struct pendingData* pending, bool local) {
|
||||
}
|
||||
newPending.len = taginfo->len;
|
||||
|
||||
if ((pending->availdatainfo.dataType == DATATYPE_IMG_RAW_1BPP || pending->availdatainfo.dataType == DATATYPE_IMG_RAW_2BPP || pending->availdatainfo.dataType == DATATYPE_IMG_RAW_3BPP || pending->availdatainfo.dataType == DATATYPE_IMG_ZLIB || pending->availdatainfo.dataType == DATATYPE_IMG_G5) && (pending->availdatainfo.dataTypeArgument & 0xF8) == 0x00) {
|
||||
uint8_t dataType = pending->availdatainfo.dataType;
|
||||
if (dataType != DATATYPE_FW_UPDATE && dataType != DATATYPE_NOUPDATE && pending->availdatainfo.dataTypeArgument & 0xF8 == 0x00) {
|
||||
// in case of an image (no preload), remove already queued images
|
||||
pendingQueue.erase(std::remove_if(pendingQueue.begin(), pendingQueue.end(),
|
||||
[pending](const PendingItem& item) {
|
||||
|
||||
@@ -462,26 +462,23 @@
|
||||
imageData.data[i * 4 + 2] = is16Bit ? (rgb & 0x1F) << 3 : ((rgb & 0x03) << 6) * 1.3;
|
||||
imageData.data[i * 4 + 3] = 255;
|
||||
}
|
||||
} else if (tagTypes[hwtype].bpp == 3) {
|
||||
} else if ([3, 4].includes(tagTypes[hwtype].bpp)) {
|
||||
const bpp = tagTypes[hwtype].bpp;
|
||||
const colorTable = tagTypes[hwtype].colortable;
|
||||
|
||||
let pixelIndex = 0;
|
||||
for (let i = 0; i < data.length; i += 3) {
|
||||
for (let j = 0; j < 8; j++) {
|
||||
let bitPos = j * 3;
|
||||
let bytePos = Math.floor(bitPos / 8);
|
||||
let bitOffset = bitPos % 8;
|
||||
let pixelValue = (data[i + bytePos] >> (5 - bitOffset)) & 0x07;
|
||||
if (bitOffset > 5) {
|
||||
pixelValue = ((data[i + bytePos] & (0xFF >> bitOffset)) << (bitOffset - 5)) |
|
||||
(data[i + bytePos + 1] >> (13 - bitOffset));
|
||||
}
|
||||
imageData.data[pixelIndex * 4] = colorTable[pixelValue][0];
|
||||
imageData.data[pixelIndex * 4 + 1] = colorTable[pixelValue][1];
|
||||
imageData.data[pixelIndex * 4 + 2] = colorTable[pixelValue][2];
|
||||
imageData.data[pixelIndex * 4 + 3] = 255;
|
||||
pixelIndex++;
|
||||
}
|
||||
let bitOffset = 0;
|
||||
|
||||
while (bitOffset < data.length * 8) {
|
||||
let byteIndex = bitOffset >> 3;
|
||||
let startBit = bitOffset & 7;
|
||||
let pixelValue = (data[byteIndex] << 8 | data[byteIndex + 1] || 0) >> (16 - bpp - startBit) & ((1 << bpp) - 1);
|
||||
let color = colorTable[pixelValue];
|
||||
imageData.data[pixelIndex * 4] = color[0];
|
||||
imageData.data[pixelIndex * 4 + 1] = color[1];
|
||||
imageData.data[pixelIndex * 4 + 2] = color[2];
|
||||
imageData.data[pixelIndex * 4 + 3] = 255;
|
||||
pixelIndex++;
|
||||
bitOffset += bpp;
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ function initTabs() {
|
||||
tabLinks.forEach(link => {
|
||||
link.classList.remove("active");
|
||||
});
|
||||
if (targetId == "logtab") document.getElementById(targetId).scrollTop = 0;
|
||||
document.getElementById(targetId).style.display = "block";
|
||||
this.classList.add("active");
|
||||
});
|
||||
@@ -1253,28 +1254,24 @@ function drawCanvas(buffer, canvas, hwtype, tagmac, doRotate) {
|
||||
imageData.data[i * 4 + 3] = 255;
|
||||
}
|
||||
|
||||
} else if (tagTypes[hwtype].bpp == 3) {
|
||||
} else if ([3, 4].includes(tagTypes[hwtype].bpp)) {
|
||||
const bpp = tagTypes[hwtype].bpp;
|
||||
const colorTable = tagTypes[hwtype].colortable;
|
||||
|
||||
let pixelIndex = 0;
|
||||
for (let i = 0; i < data.length; i += 3) {
|
||||
for (let j = 0; j < 8; j++) {
|
||||
let bitPos = j * 3;
|
||||
let bytePos = Math.floor(bitPos / 8);
|
||||
let bitOffset = bitPos % 8;
|
||||
let pixelValue = (data[i + bytePos] >> (5 - bitOffset)) & 0x07;
|
||||
if (bitOffset > 5) {
|
||||
pixelValue = ((data[i + bytePos] & (0xFF >> bitOffset)) << (bitOffset - 5)) |
|
||||
(data[i + bytePos + 1] >> (13 - bitOffset));
|
||||
}
|
||||
imageData.data[pixelIndex * 4] = colorTable[pixelValue][0];
|
||||
imageData.data[pixelIndex * 4 + 1] = colorTable[pixelValue][1];
|
||||
imageData.data[pixelIndex * 4 + 2] = colorTable[pixelValue][2];
|
||||
imageData.data[pixelIndex * 4 + 3] = 255;
|
||||
pixelIndex++;
|
||||
}
|
||||
}
|
||||
let bitOffset = 0;
|
||||
|
||||
while (bitOffset < data.length * 8) {
|
||||
let byteIndex = bitOffset >> 3;
|
||||
let startBit = bitOffset & 7;
|
||||
let pixelValue = (data[byteIndex] << 8 | data[byteIndex + 1] || 0) >> (16 - bpp - startBit) & ((1 << bpp) - 1);
|
||||
let color = colorTable[pixelValue];
|
||||
imageData.data[pixelIndex * 4] = color[0];
|
||||
imageData.data[pixelIndex * 4 + 1] = color[1];
|
||||
imageData.data[pixelIndex * 4 + 2] = color[2];
|
||||
imageData.data[pixelIndex * 4 + 3] = 255;
|
||||
pixelIndex++;
|
||||
bitOffset += bpp;
|
||||
}
|
||||
} else {
|
||||
|
||||
const offsetRed = (data.length >= (canvas.width * canvas.height / 8) * 2) ? canvas.width * canvas.height / 8 : 0;
|
||||
@@ -1527,7 +1524,7 @@ async function getTagtype(hwtype) {
|
||||
height: parseInt(jsonData.height),
|
||||
bpp: parseInt(jsonData.bpp),
|
||||
rotatebuffer: jsonData.rotatebuffer,
|
||||
colortable: Object.values(jsonData.colortable),
|
||||
colortable: Object.values(jsonData.perceptual ?? jsonData.colortable),
|
||||
contentids: Object.values(jsonData.contentids ?? []),
|
||||
options: Object.values(jsonData.options ?? []),
|
||||
zlib: parseInt(jsonData.zlib_compression || "0", 16),
|
||||
@@ -1571,6 +1568,7 @@ function dropUpload() {
|
||||
|
||||
dropZone.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
const shiftKey = event.shiftKey;
|
||||
const file = event.dataTransfer.files[0];
|
||||
const tagCard = event.target.closest('.tagcard');
|
||||
const mac = tagCard.dataset.mac;
|
||||
@@ -1604,6 +1602,7 @@ function dropUpload() {
|
||||
canvas.toBlob(async (blob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('mac', mac);
|
||||
if (shiftKey) formData.append('dither', '2');
|
||||
formData.append('file', blob, 'image.jpg');
|
||||
|
||||
try {
|
||||
@@ -1906,7 +1905,7 @@ function openPreview(mac, w, h) {
|
||||
previewWindow.document.body.style.backgroundColor = "#dddddd";
|
||||
previewWindow.document.body.style.margin = "15px";
|
||||
previewWindow.document.body.style.overflow = "hidden";
|
||||
previewWindow.document.body.innerHTML = `<canvas id="preview" style="border:1px solid #888888;"></canvas>`;
|
||||
previewWindow.document.body.innerHTML = `<canvas id="preview" style="border:1px solid #888888;image-rendering: pixelated;"></canvas>`;
|
||||
|
||||
showPreview(previewWindow, element);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user