Bodmer / TJpg_Decoder

Jpeg decoder library based on Tiny JPEG Decompressor
Other
228 stars 43 forks source link

Image is not fully rendered #64

Closed fluxa closed 1 year ago

fluxa commented 1 year ago

Hi, I'm having a similar issue. I can't get the full image to be displayed on the TFT screen. I'm using the M5Stack Core2 (ESP32) platform, and here is the code:

#include <M5Core2.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TJpg_Decoder.h>

#define WIFI_SSID "wifissd"
#define WIFI_PASSWORD "wifipassword"
#define NASA_API_KEY "apikey"

void connectWiFi() {
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  M5.Lcd.setCursor(10, 10);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.printf("Connecting to %s", WIFI_SSID);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    M5.Lcd.printf(".");
  }

  Serial.println("Connected to WiFi");
  M5.Lcd.setCursor(10, 10);
  M5.Lcd.clear(WHITE);
  M5.Lcd.printf("Connected to %s", WIFI_SSID);
}

String fetchPhotoData() {
  String url = "https://api.nasa.gov/planetary/apod?api_key=" + String(NASA_API_KEY) + "&count=1";
  HTTPClient http;
  http.begin(url);
  int httpResponseCode = http.GET();
  String payload = http.getString();
  http.end();
  return payload;
}

bool jpegRender(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  // Stop further decoding as image is running off bottom of screen
  if ( y >= M5.Lcd.height() ) return 0;

  // This function will clip the image block rendering automatically at the TFT boundaries
  M5.Lcd.pushImage(x, y, w, h, bitmap);

  // Return 1 to decode next block
  return 1;
}

void drawJpgUrl(const char *url, int xpos, int ypos) {
  HTTPClient http;
  http.begin(url);
  int httpResponseCode = http.GET();

  if (httpResponseCode == 200) {
    WiFiClient *client = http.getStreamPtr();
    uint8_t *buf = nullptr;
    size_t buf_len = 0;
    size_t len = http.getSize();
    Serial.println("Image size: " + String(len) + " bytes");

    buf = (uint8_t *)malloc(len);
    if (!buf) {
      Serial.println("Failed to allocate memory for image buffer");
      http.end();
      return;
    }
    buf_len = client->read(buf, len);

    // Time recorded for test purposes
    uint32_t t = millis();

    // Get the width and height in pixels of the jpeg if you wish
    uint16_t w = 0, h = 0;
    TJpgDec.getJpgSize(&w, &h, buf, buf_len);
    Serial.print("Width = "); Serial.print(w); Serial.print(", height = "); Serial.println(h);

    // Draw the image, top left at 0,0
    TJpgDec.drawJpg(xpos, ypos, buf, buf_len);

    free(buf);

    t = millis() - t;
    Serial.print(t); Serial.println(" ms");

  }  
  http.end();
}

void displayPhoto(const char* url, const char* title, const char* date) {
  M5.Lcd.clear(WHITE);
  drawJpgUrl(url, 0, 0);

  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(5, M5.Lcd.height() - 20);
  M5.Lcd.printf(title);

  M5.Lcd.setCursor(5, M5.Lcd.height() - 10);
  M5.Lcd.printf(date);
}

void fetchAndDisplayPhoto() {
  M5.Lcd.clear(WHITE);
  M5.Lcd.setCursor(M5.Lcd.width()/2 - 30, M5.Lcd.height()/2);
  M5.Lcd.printf("Loading...");

  String payload = fetchPhotoData();
  DynamicJsonDocument doc(1024 * 6);
  deserializeJson(doc, payload);

  const char* url = doc[0]["url"];
  Serial.println(url);
  const char* title = doc[0]["title"];
  const char* date = doc[0]["date"];

  displayPhoto(url, title, date);
}

void buttonAHandler() {
  fetchAndDisplayPhoto();
}

void setup() {
  M5.begin();
  M5.Lcd.clear(WHITE);
  Serial.begin(115200);
  connectWiFi();
  M5.Lcd.setBrightness(128);

  // The jpeg image can be scaled by a factor of 1, 2, 4, or 8
  TJpgDec.setJpgScale(1);
  TJpgDec.setSwapBytes(true);
  TJpgDec.setCallback(jpegRender);
  fetchAndDisplayPhoto();
}

void loop() {
  static unsigned long prevMillis = 0;
  M5.update();

  if (M5.BtnA.wasPressed()) {
    fetchAndDisplayPhoto();
  }

  if (millis() - prevMillis >= 5 * 60 * 1000) {
    prevMillis = millis();
    fetchAndDisplayPhoto();
  }

  delay(10);
}

Here's an example of how an image is rendered: https://drive.google.com/file/d/1jbd1YidgceKbn9TgsfUfybOOGcFRDVP4/view?usp=sharing

And this is the image URL: https://apod.nasa.gov/apod/image/0410/PassageOmbre_sm4.jpg

Also, how can I make the image fit the screen size?

Thank you!

Bodmer commented 1 year ago

The problem is in the sketch. The following line only fetches a small part of the data, you have to keep calling it until all the data is retrieved:

buf_len = client->read(buf, len);

This should work but you might wish to add a time-out in the loop incase all the data is not received (sketch will lock up in loop otherwise):

    uint8_t* ptr = buf;
    while (buf_len < len) {
      int32_t read_len = client->read(ptr, len);
      if (read_len > 0) {
        ptr += read_len;
        buf_len += read_len;
      }
    }

The jpg can be scaled down by a factor of 1,2,4 or 8 to fit the screen (other ratios owuld require bilinear interpolation!).

This code should work:

    uint8_t scale = 1;
    while (w > tft.width())  {
      scale *= 2;
      w /= 2;
      h /= 2;
    }
    while (h > tft.height()) {
      scale *= 2;
      h /= 2;
    }
    if (scale > 8) scale = 8;
    TJpgDec.setJpgScale(scale);

    // Draw the image, top left at 0,0
    TJpgDec.drawJpg(xpos, ypos, buf, buf_len);

You might like to have a look at the fork here which supports streaming large images from the web without needing to allocate a buffer: https://github.com/SirCooper/TJpg_Decoder

Let me know if that works for you. @SirCooper has raised a pull request (in #52) but I have not implemented it yet.