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:
Nic Limper
2024-12-10 23:01:02 +01:00
parent 31a90d1498
commit 30812dff49
20 changed files with 310 additions and 123 deletions

Binary file not shown.

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 &currentOrientation, 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 &currentOrientation) {
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";

View File

@@ -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));

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -1,5 +1,5 @@
{
"version": 3,
"version": 4,
"name": "Opticon 2.2\"",
"width": 250,
"height": 128,
@@ -11,6 +11,11 @@
"red": [ 255, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"g5_compression": "29",
"shortlut": 0,
"options": [ "led" ],

View File

@@ -1,5 +1,5 @@
{
"version": 3,
"version": 4,
"name": "Opticon 2.9\"",
"width": 296,
"height": 128,
@@ -11,6 +11,11 @@
"red": [ 255, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"g5_compression": "29",
"shortlut": 2,
"options": [ "led" ],

View File

@@ -1,5 +1,5 @@
{
"version": 2,
"version": 3,
"name": "M3 7.5\"",
"width": 800,
"height": 480,
@@ -51,7 +51,13 @@
"rotate": 0,
"mode": 1,
"days": 7,
"gridparam": [ 3, 17, 30, "calibrib16.vlw", "tahoma9.vlw", 14 ]
"gridparam": [ 3, 17, 30, "calibrib16.vlw", "tahoma11.vlw", 14 ]
},
"27": {
"bars": [ 40, 690, 380, 25, 50 ],
"time": [ "fonts/bahnschrift20" ],
"yaxis": [ "fonts/bahnschrift20", 5, 12, 14 ],
"head": [ "fonts/calibrib50.vlw" ]
}
}
}

View File

@@ -1,5 +1,5 @@
{
"version": 2,
"version": 3,
"name": "M3 4.2\" BWY",
"width": 400,
"height": 300,
@@ -10,6 +10,11 @@
"black": [ 0, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"shortlut": 0,
"zlib_compression": "27",
"options": [ "button", "led" ],

View File

@@ -1,5 +1,5 @@
{
"version": 4,
"version": 5,
"name": "HS BWY 3.5\"",
"width": 384,
"height": 184,
@@ -10,6 +10,11 @@
"black": [ 0, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"highlight_color": 3,
"shortlut": 0,
"zlib_compression": "27",

View File

@@ -1,5 +1,5 @@
{
"version": 1,
"version": 2,
"name": "HS BWY 7,5\"",
"width": 800,
"height": 480,
@@ -10,6 +10,11 @@
"black": [ 0, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"highlight_color": 3,
"shortlut": 0,
"zlib_compression": "27",

View File

@@ -1,5 +1,5 @@
{
"version": 2,
"version": 3,
"name": "HS 2.00\" BWY",
"width": 152,
"height": 200,
@@ -10,6 +10,11 @@
"black": [ 0, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"highlight_color": 3,
"shortlut": 2,
"zlib_compression": "27",

View File

@@ -1,5 +1,5 @@
{
"version": 3,
"version": 4,
"name": "HS BWY 3.46\"",
"width": 480,
"height": 176,
@@ -10,6 +10,11 @@
"black": [ 0, 0, 0 ],
"yellow": [ 255, 255, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ]
},
"highlight_color": 3,
"shortlut": 0,
"zlib_compression": "27",

View File

@@ -1,5 +1,5 @@
{
"version": 1,
"version": 2,
"name": "BWRY example",
"width": 360,
"height": 184,
@@ -11,6 +11,12 @@
"yellow": [ 255, 255, 0 ],
"red": [ 255, 0, 0 ]
},
"perceptual": {
"white": [ 255, 255, 255 ],
"black": [ 0, 0, 0 ],
"yellow": [ 200, 200, 0 ],
"red": [ 255, 0, 0 ]
},
"shortlut": 0,
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 26 ],
"usetemplate": 1

View File

@@ -14,8 +14,27 @@
"yellow": [ 255, 255, 0 ],
"orange": [ 255, 128, 0 ]
},
"perceptual": {
"black": [ 0, 0, 0 ],
"white": [ 248, 248, 248 ],
"green": [ 0, 200, 0 ],
"blue": [ 0, 0, 200 ],
"red": [ 200, 0, 0 ],
"yellow": [ 240, 240, 0 ],
"orange": [ 200, 128, 0 ]
},
"highlight_color": 8,
"shortlut": 0,
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 26, 27 ],
"usetemplate": 5
"usetemplate": 5,
"template": {
"11": {
"rotate": 0,
"mode": 1,
"days": 7,
"gridparam": [ 3, 17, 30, "calibrib16.vlw", "tahoma9.vlw", 14 ],
"colors1": [ "#4EFFFF", "#E6FF3D", "#FF7DF0", "#5491FF", "#FF83FF", "#FFFF00", "#84FE39", "#84FE39" ],
"colors2": [ "green", "yellow", "red", "blue", "blue", "orange", "black", "green" ]
}
}
}

View File

@@ -10,11 +10,31 @@
"white": [ 255, 255, 255 ],
"yellow": [ 255, 255, 0 ],
"red": [ 255, 0, 0 ],
"orange": [ 255, 128, 0 ],
"blue": [ 0, 0, 255 ],
"green": [ 0, 255, 0 ]
},
"perceptual": {
"black": [ 0, 0, 0 ],
"white": [ 248, 248, 248 ],
"yellow": [ 240, 240, 0 ],
"red": [ 200, 0, 0 ],
"orange": [ 200, 64, 0 ],
"blue": [ 0, 0, 200 ],
"green": [ 0, 200, 0 ]
},
"highlight_color": 8,
"shortlut": 0,
"contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 26, 27 ],
"usetemplate": 54
"usetemplate": 54,
"template": {
"11": {
"rotate": 0,
"mode": 1,
"days": 7,
"gridparam": [ 3, 17, 30, "calibrib16.vlw", "tahoma11.vlw", 14 ],
"colors1": [ "#4EFFFF", "#E6FF3D", "#FF7DF0", "#5491FF", "#FF83FF", "#FFFF00", "#84FE39", "#84FE39" ],
"colors2": [ "green", "yellow", "red", "blue", "blue", "orange", "black", "green" ]
}
}
}