diff --git a/ESP32_AP-Flasher/data/fonts/tahoma11.vlw b/ESP32_AP-Flasher/data/fonts/tahoma11.vlw new file mode 100644 index 00000000..890e90a9 Binary files /dev/null and b/ESP32_AP-Flasher/data/fonts/tahoma11.vlw differ diff --git a/ESP32_AP-Flasher/include/contentmanager.h b/ESP32_AP-Flasher/include/contentmanager.h index cc8092ec..47dba399 100644 --- a/ESP32_AP-Flasher/include/contentmanager.h +++ b/ESP32_AP-Flasher/include/contentmanager.h @@ -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); diff --git a/ESP32_AP-Flasher/include/makeimage.h b/ESP32_AP-Flasher/include/makeimage.h index 9b017f83..e99ee666 100644 --- a/ESP32_AP-Flasher/include/makeimage.h +++ b/ESP32_AP-Flasher/include/makeimage.h @@ -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; diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 8b9c23c8..21ffa38f 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -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; diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index 9e5be2c0..a3921360 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -21,6 +21,7 @@ #ifdef CONTENT_RSS #include #endif +#include #include #include @@ -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() * 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() + "?days=" + loc["days"].as(); @@ -1293,6 +1289,13 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa int calYOffset = loc["gridparam"][1].as(); int lineHeight = loc["gridparam"][5].as(); + 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() && 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() && 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(); } @@ -1613,12 +1635,13 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, minPrice = yAxisScale.min; uint16_t yAxisX = loc["yaxis"][1].as(); + uint16_t yAxisY = loc["yaxis"][3].as() | 9; uint16_t barBottom = loc["bars"][3].as(); 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()); 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() / 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(), textArray[1].as(), 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(); 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(), boxArray[1].as(), boxArray[2].as(), boxArray[3].as(), 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(), circleArray[1].as(), circleArray[2].as(), 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(), imgArray[2].as()); + sprDraw.deleteSprite(); + } + } else if (element.containsKey("rotate")) { uint8_t rotation = element["rotate"].as(); 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"; diff --git a/ESP32_AP-Flasher/src/makeimage.cpp b/ESP32_AP-Flasher/src/makeimage.cpp index 46d1a45d..2383bf3b 100644 --- a/ESP32_AP-Flasher/src/makeimage.cpp +++ b/ESP32_AP-Flasher/src/makeimage.cpp @@ -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 findClosestColors(const Color &pixel, const std::vector &palette) { + int closestIndex = -1, secondClosestIndex = -1; + float closestDist = std::numeric_limits::max(); + float secondClosestDist = std::numeric_limits::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)); diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index ffade1f4..c8131b60 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -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) { diff --git a/ESP32_AP-Flasher/wwwroot/edit.html b/ESP32_AP-Flasher/wwwroot/edit.html index 2c29dbcf..ea76ad47 100644 --- a/ESP32_AP-Flasher/wwwroot/edit.html +++ b/ESP32_AP-Flasher/wwwroot/edit.html @@ -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 { diff --git a/ESP32_AP-Flasher/wwwroot/main.js b/ESP32_AP-Flasher/wwwroot/main.js index ce2b9aae..ad708568 100644 --- a/ESP32_AP-Flasher/wwwroot/main.js +++ b/ESP32_AP-Flasher/wwwroot/main.js @@ -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 = ``; + previewWindow.document.body.innerHTML = ``; showPreview(previewWindow, element); diff --git a/resources/tagtypes/06.json b/resources/tagtypes/06.json index 7dcfc367..24184fd3 100644 --- a/resources/tagtypes/06.json +++ b/resources/tagtypes/06.json @@ -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" ], diff --git a/resources/tagtypes/07.json b/resources/tagtypes/07.json index 5e6cefb0..780c18f3 100644 --- a/resources/tagtypes/07.json +++ b/resources/tagtypes/07.json @@ -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" ], diff --git a/resources/tagtypes/36.json b/resources/tagtypes/36.json index d7d552db..5472f224 100644 --- a/resources/tagtypes/36.json +++ b/resources/tagtypes/36.json @@ -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" ] } } } diff --git a/resources/tagtypes/3C.json b/resources/tagtypes/3C.json index a29b594e..fea68662 100644 --- a/resources/tagtypes/3C.json +++ b/resources/tagtypes/3C.json @@ -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" ], diff --git a/resources/tagtypes/60.json b/resources/tagtypes/60.json index d76f9dc6..361d78fd 100644 --- a/resources/tagtypes/60.json +++ b/resources/tagtypes/60.json @@ -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", diff --git a/resources/tagtypes/66.json b/resources/tagtypes/66.json index 2ff55849..40a9d0dd 100644 --- a/resources/tagtypes/66.json +++ b/resources/tagtypes/66.json @@ -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", diff --git a/resources/tagtypes/67.json b/resources/tagtypes/67.json index 522764e4..811625de 100644 --- a/resources/tagtypes/67.json +++ b/resources/tagtypes/67.json @@ -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", diff --git a/resources/tagtypes/68.json b/resources/tagtypes/68.json index 52e4617d..f5eb7068 100644 --- a/resources/tagtypes/68.json +++ b/resources/tagtypes/68.json @@ -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", diff --git a/resources/tagtypes/C0.json b/resources/tagtypes/C0.json index 6080e2fc..26eb477c 100644 --- a/resources/tagtypes/C0.json +++ b/resources/tagtypes/C0.json @@ -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 diff --git a/resources/tagtypes/C1.json b/resources/tagtypes/C1.json index fd172f6a..12815d33 100644 --- a/resources/tagtypes/C1.json +++ b/resources/tagtypes/C1.json @@ -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" ] + } + } } diff --git a/resources/tagtypes/C2.json b/resources/tagtypes/C2.json index 8c5a6284..552e3310 100644 --- a/resources/tagtypes/C2.json +++ b/resources/tagtypes/C2.json @@ -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" ] + } + } }