From e9256697e72cd6ebcf2716bfa1bf98b4718843aa Mon Sep 17 00:00:00 2001 From: Piotr Migdal Date: Sun, 22 Feb 2026 22:10:21 +0100 Subject: [PATCH] Add Inkplate5V2 XKCD Random Webcomic Strip example Display random XKCD comics with on-device PNG scaling on Inkplate 5 V2. Features: auto-refresh every 60s, button skip (GPIO 36), title and alt-text display, robust HTTPS downloads, grayscale 3-bit mode. Requires ArduinoJson library (Arduino IDE Library Manager). --- .gitignore | 5 +- ...Inkplate5V2_XKCD_Random_Webcomic_Strip.ino | 412 ++++++++++++++++++ .../credentials.template.h | 6 + .../pngle_scaling.cpp | 167 +++++++ .../pngle_scaling.h | 59 +++ 5 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/Inkplate5V2_XKCD_Random_Webcomic_Strip.ino create mode 100644 examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/credentials.template.h create mode 100644 examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.cpp create mode 100644 examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.h diff --git a/.gitignore b/.gitignore index 407e9e943..b1ebfa209 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ build/ Build/ examples/.DS_Store -.DS_Store \ No newline at end of file +.DS_Store + +# Ignore actual credentials files (use credentials.template.h instead) +**/credentials.h \ No newline at end of file diff --git a/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/Inkplate5V2_XKCD_Random_Webcomic_Strip.ino b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/Inkplate5V2_XKCD_Random_Webcomic_Strip.ino new file mode 100644 index 000000000..2d6587877 --- /dev/null +++ b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/Inkplate5V2_XKCD_Random_Webcomic_Strip.ino @@ -0,0 +1,412 @@ +/* + Inkplate5V2_XKCD_Random_Webcomic_Strip - Display random XKCD comics + + This example displays random XKCD comics with on-device PNG scaling. + Button on GPIO 36 skips to next comic, auto-refresh every 60s. + + REQUIRED LIBRARY: + Install ArduinoJson library from Arduino IDE Library Manager: + Sketch → Include Library → Manage Libraries → Search "ArduinoJson" → Install + + Hardware needed: Inkplate 5 V2, USB-C cable, WiFi connection + + June 2025 by Piotr Migdał +*/ + +#include +#include "HTTPClient.h" +#include "Inkplate.h" +#include "WiFi.h" +#include "WiFiClientSecure.h" +#include "credentials.h" +#include "pngle_scaling.h" + +const uint8_t DISPLAY_MODE = INKPLATE_3BIT; +const unsigned long DISPLAY_DURATION_MS = 60000; +const unsigned long RETRY_DELAY_MS = 3000; +const uint8_t BUTTON_PIN = GPIO_NUM_36; + +Inkplate display(DISPLAY_MODE); +int latestComicNumber = 0; + +// Download JSON as String (for XKCD API metadata) +String downloadJson(const char* url) { + // XKCD requires HTTPS. setInsecure() skips certificate verification (ESP32 limitation). + Serial.printf("[JSON] Downloading: %s\n", url); + + // Small delay before connection to avoid overwhelming network + delay(100); + + WiFiClientSecure client; + client.setInsecure(); + client.setTimeout(15); // 15 second timeout + + HTTPClient http; + String result = ""; + if (!http.begin(client, url)) { + Serial.println("[JSON] ERROR: http.begin() failed - cannot connect"); + return result; + } + + http.setTimeout(15000); + Serial.println("[JSON] Sending GET request..."); + int httpCode = http.GET(); + Serial.printf("[JSON] HTTP response code: %d\n", httpCode); + + if (httpCode == HTTP_CODE_OK) { + result = http.getString(); + Serial.printf("[JSON] Downloaded %d bytes\n", result.length()); + } else if (httpCode < 0) { + Serial.printf("[JSON] ERROR: Connection failed with code %d (likely timeout or connection refused)\n", httpCode); + } else { + Serial.printf("[JSON] ERROR: HTTP error code %d\n", httpCode); + } + + http.end(); + client.stop(); + delay(50); // Small delay after connection cleanup + + return result; +} + +// Download binary data to buffer (for images). Returns nullptr on failure. Caller must free(). +uint8_t* downloadToBuffer(const char* url, int32_t* outSize) { + Serial.printf("[IMAGE] Downloading: %s\n", url); + + // Small delay before connection to avoid overwhelming network + delay(100); + + WiFiClientSecure client; + client.setInsecure(); + client.setTimeout(30); // 30 second timeout for images + + HTTPClient http; + + if (!http.begin(client, url)) { + Serial.println("[IMAGE] ERROR: http.begin() failed - cannot connect"); + return nullptr; + } + + http.setTimeout(30000); + Serial.println("[IMAGE] Sending HTTP GET request..."); + unsigned long startTime = millis(); + int httpCode = http.GET(); + unsigned long elapsed = millis() - startTime; + Serial.printf("[IMAGE] HTTP response code: %d (took %lu ms)\n", httpCode, elapsed); + + if (httpCode != HTTP_CODE_OK) { + if (httpCode == -1) { + Serial.println("[IMAGE] ERROR: Connection failed (code -1)"); + Serial.println("[IMAGE] Possible causes: timeout, connection refused, SSL handshake failed"); + Serial.printf("[IMAGE] WiFi status: %d, Signal strength: %d dBm\n", + WiFi.status(), WiFi.RSSI()); + } else if (httpCode < 0) { + Serial.printf("[IMAGE] ERROR: HTTP client error %d\n", httpCode); + } else { + Serial.printf("[IMAGE] ERROR: HTTP server error %d\n", httpCode); + } + http.end(); + client.stop(); + return nullptr; + } + + WiFiClient* stream = http.getStreamPtr(); + int32_t len = http.getSize(); + Serial.printf("[IMAGE] Content length: %d bytes\n", len); + + if (len <= 0) { + Serial.printf("[IMAGE] ERROR: Invalid content length: %d\n", len); + http.end(); + client.stop(); + return nullptr; + } + + Serial.printf("[IMAGE] Allocating %d bytes in PSRAM...\n", len); + uint8_t* buffer = (uint8_t*)ps_malloc(len); + if (!buffer) { + Serial.printf("[IMAGE] ERROR: Memory allocation failed for %d bytes\n", len); + Serial.printf("[IMAGE] Free heap: %u, Free PSRAM: %u\n", + ESP.getFreeHeap(), ESP.getFreePsram()); + http.end(); + client.stop(); + return nullptr; + } + + Serial.println("[IMAGE] Reading image data..."); + size_t bytesRead = 0; + unsigned long downloadStart = millis(); + + // Read in chunks until all data is received + while (bytesRead < len) { + // Wait for data to be available + while (!stream->available() && (millis() - downloadStart < 30000)) { + delay(10); + } + + if (!stream->available()) { + Serial.printf("[IMAGE] ERROR: Stream stalled at %d/%d bytes\n", bytesRead, len); + free(buffer); + http.end(); + client.stop(); + return nullptr; + } + + // Read up to 1024 bytes at a time + size_t toRead = min((size_t)(len - bytesRead), (size_t)1024); + size_t read = stream->readBytes(buffer + bytesRead, toRead); + bytesRead += read; + + if (read == 0) { + Serial.printf("[IMAGE] ERROR: Read returned 0 at %d/%d bytes\n", bytesRead, len); + free(buffer); + http.end(); + client.stop(); + return nullptr; + } + + // Progress reporting every 10KB + if (bytesRead % 10240 == 0 || bytesRead == len) { + Serial.printf("[IMAGE] Progress: %d/%d bytes (%.1f%%)\n", + bytesRead, len, (bytesRead * 100.0) / len); + } + + // Overall timeout check + if (millis() - downloadStart > 30000) { + Serial.printf("[IMAGE] ERROR: Download timeout at %d/%d bytes\n", bytesRead, len); + free(buffer); + http.end(); + client.stop(); + return nullptr; + } + } + + Serial.printf("[IMAGE] Download complete: %d bytes in %lu ms\n", + bytesRead, millis() - downloadStart); + + http.end(); + client.stop(); + delay(50); // Small delay after connection cleanup + + *outSize = len; + return buffer; +} + +// Word-wrap text to fit within maxWidth. Breaks on spaces, max 10 lines. +void drawWrappedText(String text, int x, int y, int maxWidth) { + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds("A", 0, 0, &x1, &y1, &w, &h); + int lineHeight = h + 2; + + String lines[10]; + int lineCount = 0; + String currentLine = ""; + String word = ""; + + for (int i = 0; i <= text.length(); i++) { + char c = (i < text.length()) ? text.charAt(i) : ' '; + + if (c == ' ' || i == text.length()) { + if (word.length() > 0) { + String testLine = currentLine.length() > 0 ? currentLine + " " + word : word; + display.getTextBounds(testLine, 0, 0, &x1, &y1, &w, &h); + + if (w > maxWidth && currentLine.length() > 0) { + if (lineCount < 10) lines[lineCount++] = currentLine; + currentLine = word; + } else { + currentLine = testLine; + } + word = ""; + } + } else { + word += c; + } + } + if (currentLine.length() > 0 && lineCount < 10) { + lines[lineCount++] = currentLine; + } + + for (int i = 0; i < lineCount; i++) { + display.setCursor(x, y + (i * lineHeight)); + display.print(lines[i]); + } +} + +// Display comic title and alt text at bottom of screen +void displayComicText(String title, String altText, int comicNum) { + String text = "XKCD #" + String(comicNum) + ": " + title + " - " + altText; + + int16_t x1, y1; + uint16_t w, h; + display.setTextSize(2); + display.getTextBounds("A", 0, 0, &x1, &y1, &w, &h); + int startY = display.height() - (h * 4) - 10; // Reserve ~4 lines of text at bottom + + display.setTextColor(0, 7); + drawWrappedText(text, 10, startY, display.width() - 20); + display.display(); +} + +void displayStatus(String message) { + display.clearDisplay(); + display.setCursor(10, 10); + display.setTextSize(2); + display.print(message); + display.display(); + Serial.println(message); + delay(100); +} + +bool waitForButtonOrTimeout(unsigned long timeoutMs) { + unsigned long startTime = millis(); + + while (millis() - startTime < timeoutMs) { + if (digitalRead(BUTTON_PIN) == LOW) { + delay(50); + while (digitalRead(BUTTON_PIN) == LOW) delay(10); + delay(50); + Serial.println("Button pressed - loading next comic"); + return true; + } + delay(100); + } + return false; +} + +bool tryDisplayComic(int randomNum) { + Serial.printf("\n[COMIC] ===== Trying XKCD #%d =====\n", randomNum); + displayStatus("Loading comic #" + String(randomNum) + "..."); + + String comicUrl = "https://xkcd.com/" + String(randomNum) + "/info.0.json"; + String comicJson = downloadJson(comicUrl.c_str()); + if (comicJson.length() == 0) { + Serial.println("[COMIC] ERROR: Failed to download JSON metadata"); + return false; + } + + Serial.println("[COMIC] Parsing JSON metadata..."); + JsonDocument comicDoc; + if (deserializeJson(comicDoc, comicJson)) { + Serial.println("[COMIC] ERROR: Failed to parse JSON"); + return false; + } + + String imgUrl = comicDoc["img"].as(); + String title = comicDoc["safe_title"].as(); + String altText = comicDoc["alt"].as(); + Serial.printf("[COMIC] Title: %s\n", title.c_str()); + Serial.printf("[COMIC] Image URL: %s\n", imgUrl.c_str()); + + if (imgUrl.length() == 0) { + Serial.println("[COMIC] ERROR: Empty image URL"); + return false; + } + + int32_t imageSize = 0; + uint8_t* imageBuffer = downloadToBuffer(imgUrl.c_str(), &imageSize); + if (!imageBuffer) { + Serial.println("[COMIC] ERROR: Failed to download image"); + return false; + } + + Serial.printf("[COMIC] Image downloaded: %d bytes\n", imageSize); + uint16_t imageAreaHeight = display.height() - 60; + bool imageDisplayed = false; + + if (imgUrl.endsWith(".png")) { + Serial.println("[COMIC] Decoding as PNG..."); + imageDisplayed = drawScaledPngFromBuffer(imageBuffer, imageSize, display.width(), imageAreaHeight, false, false); + } else if (imgUrl.endsWith(".jpg") || imgUrl.endsWith(".jpeg")) { + Serial.println("[COMIC] Decoding as JPEG..."); + display.clearDisplay(); + imageDisplayed = display.drawJpegFromBuffer(imageBuffer, imageSize, 0, 0, true, false); + } else { + Serial.println("[COMIC] Unknown format, trying PNG..."); + imageDisplayed = drawScaledPngFromBuffer(imageBuffer, imageSize, display.width(), imageAreaHeight, false, false); + } + + free(imageBuffer); + + if (imageDisplayed) { + Serial.println("[COMIC] Image decoded successfully"); + displayComicText(title, altText, randomNum); + Serial.printf("[COMIC] SUCCESS: Displayed XKCD #%d: %s\n", randomNum, title.c_str()); + + Serial.printf("[COMIC] Waiting %lu ms for button or timeout...\n", DISPLAY_DURATION_MS); + if (!waitForButtonOrTimeout(DISPLAY_DURATION_MS)) { + Serial.println("[COMIC] Timeout - moving to next comic"); + } + return true; + } + + Serial.println("[COMIC] ERROR: Image decode/display failed"); + return false; +} + +void setup() { + display.begin(); + Serial.begin(115200); + while (!Serial); + pinMode(BUTTON_PIN, INPUT); + randomSeed(esp_random()); + + displayStatus("Connecting to WiFi..."); + WiFi.mode(WIFI_MODE_STA); + WiFi.begin(ssid, pass); + + display.setCursor(250, 10); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + display.print("."); + display.partialUpdate(); + } + + displayStatus("WiFi connected!"); + + // Fetch latest comic number once at startup + displayStatus("Getting latest comic..."); + String latestJson = downloadJson("https://xkcd.com/info.0.json"); + if (latestJson.length() == 0) { + displayStatus("ERROR: Cannot fetch latest comic"); + Serial.println("HALTED: Failed to fetch latest comic number"); + while(1) delay(1000); + } + + JsonDocument latestDoc; + if (deserializeJson(latestDoc, latestJson)) { + displayStatus("ERROR: JSON parse failed"); + Serial.println("HALTED: Failed to parse latest comic JSON"); + while(1) delay(1000); + } + + latestComicNumber = latestDoc["num"]; + if (latestComicNumber == 0) { + displayStatus("ERROR: Invalid comic number"); + Serial.println("HALTED: Latest comic number is invalid"); + while(1) delay(1000); + } + + Serial.printf("Latest XKCD comic: #%d\n", latestComicNumber); + displayStatus("Ready!"); + delay(1000); +} + +void loop() { + Serial.printf("\n[LOOP] Free heap: %u bytes\n", ESP.getFreeHeap()); + + // Try one random comic + randomSeed(esp_random()); + int randomNum = random(1, latestComicNumber + 1); + if (randomNum == 404) { + Serial.println("[LOOP] Skipped #404 (doesn't exist)"); + randomNum = 1; + } + Serial.printf("[LOOP] Selected random comic #%d (out of %d)\n", randomNum, latestComicNumber); + + if (!tryDisplayComic(randomNum)) { + Serial.printf("[LOOP] Comic failed, waiting %lu ms before retry...\n", RETRY_DELAY_MS); + displayStatus("Failed to load comic"); + delay(RETRY_DELAY_MS); + } +} diff --git a/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/credentials.template.h b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/credentials.template.h new file mode 100644 index 000000000..11d953c18 --- /dev/null +++ b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/credentials.template.h @@ -0,0 +1,6 @@ +// WiFi credentials template +// Copy this file to credentials.h and fill in your WiFi details +// credentials.h is gitignored to keep your passwords safe + +const char *ssid = "YOUR_WIFI_SSID"; +const char *pass = "YOUR_WIFI_PASSWORD"; \ No newline at end of file diff --git a/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.cpp b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.cpp new file mode 100644 index 000000000..028329d35 --- /dev/null +++ b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.cpp @@ -0,0 +1,167 @@ +/* + pngle_scaling.cpp - PNG scaling implementation + + Based on pngle library (https://github.com/kikuchan/pngle) + Added: scaling with aspect ratio preservation, centering, grayscale conversion + + This module handles ONLY PNG decoding and scaling from a pre-downloaded buffer. + Networking/download concerns are handled by the main sketch. +*/ + +#include "pngle_scaling.h" + +// Global scaling context definition +ScalingContext scalingCtx; + +// Custom PNG drawing callback with scaling +void pngle_on_draw_scaled(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) +{ + if (rgba[3]) // Only process non-transparent pixels + { + // Process the RGBA values once for this rectangular region + uint8_t r = rgba[0]; + uint8_t g = rgba[1]; + uint8_t b = rgba[2]; + + // Convert to grayscale + uint8_t gray = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b); + + // Apply inversion if needed + if (scalingCtx.invert) { + gray = 255 - gray; + } + + // Convert to appropriate bit depth for current display mode + uint8_t px; + if (scalingCtx.displayPtr->getDisplayMode() == INKPLATE_3BIT) { + // For 3-bit mode: scale from 0-255 to 0-7 + px = (gray * 7) / 255; + } else { + // For 1-bit mode: convert to black/white + px = (gray > 128) ? 0 : 1; // 0=white, 1=black + } + + // Now iterate through ALL pixels in this rectangular region + for (int j = 0; j < h; ++j) + { + for (int i = 0; i < w; ++i) + { + // Calculate source pixel position + uint32_t srcX = x + i; + uint32_t srcY = y + j; + + // Calculate the target rectangle this source pixel should fill + uint32_t scaledXStart = (uint32_t)(srcX * scalingCtx.scaleX); + uint32_t scaledYStart = (uint32_t)(srcY * scalingCtx.scaleY); + uint32_t scaledXEnd = (uint32_t)((srcX + 1) * scalingCtx.scaleX); + uint32_t scaledYEnd = (uint32_t)((srcY + 1) * scalingCtx.scaleY); + + // Apply offset for centering + scaledXStart += scalingCtx.offsetX; + scaledYStart += scalingCtx.offsetY; + scaledXEnd += scalingCtx.offsetX; + scaledYEnd += scalingCtx.offsetY; + + // Fill the entire rectangular area for this source pixel + for (uint32_t targetY = scaledYStart; targetY < scaledYEnd && targetY < scalingCtx.displayPtr->height(); targetY++) { + for (uint32_t targetX = scaledXStart; targetX < scaledXEnd && targetX < scalingCtx.displayPtr->width(); targetX++) { + scalingCtx.displayPtr->drawPixel(targetX, targetY, px); + } + } + } + } + } +} + +bool drawScaledPngFromBuffer(const uint8_t *buffer, int32_t size, uint16_t targetWidth, uint16_t targetHeight, bool dither, bool invert) +{ + // Initialize PNG decoder + pngle_t *pngle = pngle_new(); + if (!pngle) { + Serial.println("Failed to create PNG decoder"); + return false; + } + + // First pass: decode without callback to get dimensions + Serial.printf("[PNG] First pass: feeding %d bytes to decoder\n", size); + int fed = pngle_feed(pngle, buffer, size); + if (fed < 0) { + Serial.printf("[PNG] ERROR: Decode failed with error code %d\n", fed); + Serial.printf("[PNG] Buffer size: %d bytes, fed: %d bytes\n", size, fed); + Serial.printf("[PNG] First 8 bytes: %02X %02X %02X %02X %02X %02X %02X %02X\n", + buffer[0], buffer[1], buffer[2], buffer[3], + buffer[4], buffer[5], buffer[6], buffer[7]); + pngle_destroy(pngle); + return false; + } + Serial.printf("[PNG] First pass successful, fed %d bytes\n", fed); + + // Get image dimensions + pngle_ihdr_t *ihdr = pngle_get_ihdr(pngle); + if (!ihdr) { + Serial.println("[PNG] ERROR: Failed to get PNG header"); + pngle_destroy(pngle); + return false; + } + Serial.printf("[PNG] Header: %dx%d, bit depth: %d, color type: %d\n", + ihdr->width, ihdr->height, ihdr->depth, ihdr->color_type); + + uint32_t sourceWidth = ihdr->width; + uint32_t sourceHeight = ihdr->height; + + Serial.printf("Source image: %dx%d\n", sourceWidth, sourceHeight); + Serial.printf("Target size: %dx%d\n", targetWidth, targetHeight); + + // Calculate scaling parameters + float scaleX = (float)targetWidth / sourceWidth; + float scaleY = (float)targetHeight / sourceHeight; + float scale = min(scaleX, scaleY); // Maintain aspect ratio + + uint16_t scaledWidth = (uint16_t)(sourceWidth * scale); + uint16_t scaledHeight = (uint16_t)(sourceHeight * scale); + + // Setup scaling context + scalingCtx.scaleX = scale; + scalingCtx.scaleY = scale; + scalingCtx.targetWidth = scaledWidth; + scalingCtx.targetHeight = scaledHeight; + scalingCtx.sourceWidth = sourceWidth; + scalingCtx.sourceHeight = sourceHeight; + scalingCtx.offsetX = (targetWidth - scaledWidth) / 2; + scalingCtx.offsetY = (targetHeight - scaledHeight) / 2; + scalingCtx.dither = dither; + scalingCtx.invert = invert; + scalingCtx.displayPtr = &display; + + Serial.printf("Scaling factor: %.2f\n", scale); + Serial.printf("Scaled size: %dx%d\n", scaledWidth, scaledHeight); + Serial.printf("Offset: %d,%d\n", scalingCtx.offsetX, scalingCtx.offsetY); + + // Clear display and create new decoder for actual drawing + scalingCtx.displayPtr->clearDisplay(); + pngle_destroy(pngle); + + // Second pass: decode with scaling callback + Serial.println("[PNG] Starting second pass with scaling..."); + pngle = pngle_new(); + if (!pngle) { + Serial.println("[PNG] ERROR: Failed to create second PNG decoder"); + return false; + } + + // Set our custom drawing callback for the second pass + pngle_set_draw_callback(pngle, pngle_on_draw_scaled); + + // Process the entire PNG data with scaling + int fed2 = pngle_feed(pngle, buffer, size); + if (fed2 < 0) { + Serial.printf("[PNG] ERROR: Decode failed in scaling pass with error code %d\n", fed2); + pngle_destroy(pngle); + return false; + } + + Serial.printf("[PNG] Second pass successful, rendering complete\n"); + pngle_destroy(pngle); + + return true; +} \ No newline at end of file diff --git a/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.h b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.h new file mode 100644 index 000000000..3d191edc3 --- /dev/null +++ b/examples/Inkplate5V2/Projects/Inkplate5V2_XKCD_Random_Webcomic_Strip/pngle_scaling.h @@ -0,0 +1,59 @@ +/* + pngle_scaling.h - PNG scaling functionality + + Based on pngle library (https://github.com/kikuchan/pngle) + Added: scaling with aspect ratio preservation, centering, grayscale conversion +*/ + +#ifndef PNGLE_SCALING_H +#define PNGLE_SCALING_H + +#include "Inkplate.h" + +// Forward declarations for pngle types and functions (to avoid include path issues) +extern "C" { + typedef struct pngle pngle_t; + typedef struct { + uint32_t width; + uint32_t height; + uint8_t depth; + uint8_t color_type; + } pngle_ihdr_t; + + typedef void (*pngle_draw_callback_t)(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]); + + pngle_t* pngle_new(); + void pngle_destroy(pngle_t *pngle); + int pngle_feed(pngle_t *pngle, const void *buf, size_t len); + void pngle_set_draw_callback(pngle_t *pngle, pngle_draw_callback_t callback); + pngle_ihdr_t* pngle_get_ihdr(pngle_t *pngle); + uint32_t pngle_get_width(pngle_t *pngle); + uint32_t pngle_get_height(pngle_t *pngle); +} + +// Scaling parameters structure +struct ScalingContext { + float scaleX; + float scaleY; + uint16_t targetWidth; + uint16_t targetHeight; + uint16_t sourceWidth; + uint16_t sourceHeight; + uint16_t offsetX; + uint16_t offsetY; + bool dither; + bool invert; + Inkplate* displayPtr; +}; + +// Global scaling context - declared in pngle_scaling.cpp +extern ScalingContext scalingCtx; + +// Reference to display object - must be set by the application +extern Inkplate display; + +// Function declarations +void pngle_on_draw_scaled(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]); +bool drawScaledPngFromBuffer(const uint8_t *buffer, int32_t size, uint16_t targetWidth, uint16_t targetHeight, bool dither = false, bool invert = false); + +#endif // PNGLE_SCALING_H \ No newline at end of file