Logo

Getting Started

  • About This Kit
  • Preparation

Arduino Basics

  • Arduino Basics

LVGL Advanced

  • LVGL Advanced
    • 2.1 LVGL Hello World
    • 2.2 Label and Button Controls
    • 2.3 Switch and Checkbox — RGB LED Control
    • 2.4 Slider — RGB Colour Mixer
    • 2.5 Embedded Image Display
    • 2.6 Image Buttons — Buzzer Control
    • 2.7 Camera Live Preview and Capture
    • 2.8 SD Card Photo Gallery
      • Overview
      • What You’ll Learn
      • Before You Begin
      • Upload and Test
      • How the Code Works
      • Try This Next
    • 2.9 MP3 Music Player
    • 2.10 Heart Rate Monitor with Chart
    • 2.11 RGB LED Control Panel
    • 2.12 Piano Keyboard (Buzzer)
    • 2.13 WiFi Connection Manager
    • 2.14 SD Card File Manager
    • 2.15 All-In-One Application

Appendix

  • Appendix
LAFVIN ESP32-S3 Multimedia Kit
  • LVGL Advanced
  • 2.8 SD Card Photo Gallery
  • View page source

2.8 SD Card Photo Gallery

Browse and display BMP images stored on the SD card with a touch-friendly photo gallery.

Overview

This example creates a photo viewer application. It scans the SD card’s /picture/ folder for BMP images, builds a navigation list, and lets you browse through photos with left/right arrow buttons.

What You’ll Learn

  • Scanning a directory for specific file types

  • Building a list of file paths in memory

  • Loading and displaying BMP images from the SD card

  • Implementing gallery-style navigation (previous, next, Back)

Before You Begin

Complete the setup steps in Preparation before continuing.

Note

Use the recommended Arduino IDE settings in Recommended Board Settings unless this lesson says otherwise.

Open the example sketch from the code package:

LAFVIN-ESP32-S3-Multimedia-Kit-main/2.LVGL/08_LVGL_Gallery/08_LVGL_Gallery.ino

Note

Before uploading, create a /picture/ folder on your SD card and place some 240×240 BMP images inside. The photos you captured in 2.7 Camera Live Preview and Capture will appear here automatically.

Upload and Test

  1. Insert a Micro SD card with BMP images in the /picture/ folder.

  2. Connect the board with a USB Type-C data cable.

  3. Open 08_LVGL_Gallery.ino in Arduino IDE.

  4. In the Tools menu, select ESP32S3 Dev Module as the board and choose the correct serial port.

  5. Click Upload.

  6. After uploading:

    • The first photo from /picture/ is displayed

    • Tap the right arrow to view the next photo

    • Tap the left arrow to view the previous photo

    • Tap the home icon (top-left corner) to return

LVGL Gallery

How the Code Works

The gallery logic is mainly implemented in picture_ui.cpp.

BMP Loading from SD

Rather than relying on LVGL’s built-in BMP decoder, the gallery includes a custom load_bmp_from_sd() function that parses the BMP header, validates the format (16-bit RGB565, 240×240, BI_RGB or BI_BITFIELDS compression), and reads each row into a reusable pixel buffer. The function supports both top-down and bottom-up row ordering.

static bool load_bmp_from_sd(const char *name) {
  if (name == NULL || !picture_buffer_init()) {
    return false;
  }

  String file_path = String("/picture/") + name;
  File file = SD_MMC.open(file_path.c_str(), FILE_READ);
  if (!file) {
    return false;
  }

  const uint16_t signature = read_u16_le(file);
  if (signature != 0x4D42) {
    file.close();
    return false;
  }

  // ... read header fields: data_offset, width, height, bit_count, compression ...

  if (header_size < 40 || planes != 1 || bit_count != 16) {
    file.close();
    return false;
  }

  if (width != 240 || (height != 240 && height != -240)) {
    file.close();
    return false;
  }

  const bool bottom_up = (height > 0);
  const uint32_t row_size = 240 * 2;
  uint8_t row_buffer[row_size];

  for (int row = 0; row < 240; ++row) {
    const uint32_t bmp_row = bottom_up ? (239 - row) : row;
    if (!file.seek(data_offset + bmp_row * row_size)) {
      file.close();
      return false;
    }

    const size_t read_len = file.read(row_buffer, row_size);
    if (read_len != row_size) {
      file.close();
      return false;
    }

    memcpy(s_bmp_pixels + row * row_size, row_buffer, row_size);
  }

  file.close();
  return true;
}

Each row is read individually via file.seek() and file.read(), avoiding the need for a full-file buffer.

Image Buffer Management

A single 240×240×2 byte buffer (s_bmp_pixels) is allocated once from PSRAM (falling back to internal RAM). The buffer is wrapped in a static lv_img_dsc_t descriptor (s_bmp_img_dsc) that is reused for every displayed image.

static bool picture_buffer_init() {
  if (s_bmp_pixels != NULL) {
    return true;
  }

  const size_t image_size = 240 * 240 * 2;
  s_bmp_pixels = (uint8_t *)heap_caps_malloc(image_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  if (s_bmp_pixels == NULL) {
    s_bmp_pixels = (uint8_t *)heap_caps_malloc(image_size, MALLOC_CAP_8BIT);
  }
  // ... null check ...

  s_bmp_img_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
  s_bmp_img_dsc.header.w = 240;
  s_bmp_img_dsc.header.h = 240;
  s_bmp_img_dsc.data_size = image_size;
  s_bmp_img_dsc.data = s_bmp_pixels;
  s_bmp_ready = true;
  return true;
}

The picture_imgbtn_display() function orchestrates the pipeline: it calls load_bmp_from_sd(), then sets the descriptor as the image source and invalidates the display object.

Navigation with Wrap-Around

The left and right arrow buttons increment or decrement a 1-based index (picture_index_num). When the index goes below 1 it wraps to the last photo; when it exceeds the count it wraps to the first.

static void picture_imgbtn_left_event_handler(lv_event_t *e) {
  if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
    picture_index_num--;
    if (picture_index_num < 1) {
      picture_index_num = list_count_number(list_picture);
    }
    picture_imgbtn_display(list_find_node(list_picture, picture_index_num));
  }
}

static void picture_imgbtn_right_event_handler(lv_event_t *e) {
  if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
    picture_index_num++;
    if (picture_index_num > list_count_number(list_picture)) {
      picture_index_num = 1;
    }
    picture_imgbtn_display(list_find_node(list_picture, picture_index_num));
  }
}

In the provided sketch:

  • File paths are stored in a linked list (list_picture) populated by setup_list_head_picture(), which scans /picture/ for .bmp files

  • The home button (top-left) and left/right arrow buttons use lv_imgbtn with transparent backgrounds — only the icon images are visible

  • Navigation buttons are 60×60 px and positioned at the left and right edges of the screen

Try This Next

  • Add a “slideshow” mode that auto-advances every few seconds

  • Display the filename or image number at the bottom of the screen

  • Sort the photos alphabetically or by file date

Previous Next

© Copyright 2026, LAFVIN.

Built with Sphinx using a theme provided by Read the Docs.