Merge branch 'fix/jpeg_enc_encrypt_v5.5' into 'release/v5.5'

fix(jpeg): Jpeg can encode and decode in encryption situation (backport v5.5)

See merge request espressif/esp-idf!50670
This commit is contained in:
morris
2026-07-24 16:59:56 +08:00
39 changed files with 1411 additions and 823 deletions
+1 -1
View File
@@ -183,7 +183,7 @@ examples/peripherals/isp/multi_pipelines:
examples/peripherals/jpeg/jpeg_decode:
disable:
- if: SOC_JPEG_CODEC_SUPPORTED != 1
- if: SOC_JPEG_DECODE_SUPPORTED != 1
depends_components:
- esp_driver_jpeg
@@ -5,4 +5,4 @@ cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(jpeg_decode)
project(jpeg_decode_example)
+58 -33
View File
@@ -5,21 +5,25 @@
## Overview
This example demonstrates how to use the JPEG hardware decoder to decode a 1080p and a 720p picture:
This example demonstrates how to use the JPEG hardware decoder to decode one embedded JPEG image into `RGB888` raw bytes in default `BGR24` order.
If you have a bunch of big JPEG picture need to be decoded, such as `*.jpg` -> `*.rgb`, and this example uses hardware JPEG decoder to accelerate the decoding.
The example performs:
## How to use example
- Embedding `main/assets/image.jpg` into the application image
- Letting the JPEG decoder read the embedded JPEG bitstream directly from flash
- Parsing the JPEG header with `jpeg_decoder_get_info()`
- Decoding the image into an `RGB888` output buffer with default `BGR24` byte order
- Allocating the output buffer for padded dimensions when the JPEG block layout rounds width or height up to 16-pixel boundaries
- Base64-encoding the decoded raw pixels and printing them with machine-parseable UART markers
- Letting pytest rebuild `jpeg_decode_result.ppm` for inspection and compare it against `golden_output.ppm`
### Prerequisites Required
## Hardware Required
This example demonstrates the flexibility of decoding pictures by decoding two different sizes: one in 1080p and another in 720p. It showcases how you can easily modify the code to meet your specific requirements, such as only decoding 1080p photos.
Any board based on a supported target can be used. No SD card or external storage setup is required.
### Build and Flash
## Build and Flash
Before you start build and flash this example, please put the image `esp720.jpg` and `esp1080.jpg` in your sdcard.
Enter `idf.py -p PORT flash monitor` to build, flash and monitor the project.
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
@@ -27,32 +31,53 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l
## Example Output
```bash
I (1116) jpeg.example: Initializing SD card
I (1116) gpio: GPIO[43]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (1126) gpio: GPIO[44]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (1136) gpio: GPIO[39]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (1146) gpio: GPIO[40]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (1156) gpio: GPIO[41]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (1166) gpio: GPIO[42]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (1416) gpio: GPIO[42]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
Name: SD64G
Type: SDHC/SDXC
Speed: 40.00 MHz (limit: 40.00 MHz)
Size: 60906MB
CSD: ver=2, sector_size=512, capacity=124735488 read_bl_len=9
SSR: bus_width=4
I (1436) jpeg.example: jpg_file_1080:/sdcard/esp1080.jpg
I (1696) jpeg.example: jpg_file_1080:/sdcard/esp720.jpg
I (1796) jpeg.example: header parsed, width is 1920, height is 1080
I (1846) jpeg.example: raw_file_1080:/sdcard/out.rgb
I (11836) jpeg.example: raw_file_720:/sdcard/out2.rgb
I (13336) jpeg.example: Card unmounted
I (13336) main_task: Returned from app_main()
```text
Loading embedded JPEG from flash...
Embedded JPEG size: 43700 bytes
JPEG header parsed: width=320 height=240
Decoding JPEG -> RGB888...
Decoded RGB888 size: 245760 bytes
JPEG_DECODE_INFO width=320 height=240 padded_width=320 padded_height=256 format=RGB888 encoding=base64 size=245760
JPEG_DECODE_BASE64_BEGIN
JPEG_DECODE_BASE64 ...
JPEG_DECODE_BASE64 ...
JPEG_DECODE_BASE64_END
JPEG decode demo done.
```
Also, the helper script [open_raw_picture.py](./open_raw_picture.py) simplifies the visualization of the output on your computer. For this to work, go to `examples/peripheral/jpeg/jpeg_decode` and install the requirements by running `pip install -r requirements.txt`.
`padded_width` and `padded_height` report the actual decoded buffer dimensions. For JPEGs whose block layout pads the output to 16-pixel boundaries, these values can be larger than the visible `width` and `height`, so the output buffer must be sized for the padded image.
## Pytest Regression Check
The accompanying `pytest_jpeg_decode.py` script waits for the `JPEG_DECODE_INFO` and `JPEG_DECODE_BASE64` markers, reconstructs the decoded raw pixel output, crops away padded rows, and saves the visible image as:
- `dut.logdir/jpeg_decode_result.ppm`
The test writes the `PPM` file and compares it with `golden_output.ppm`. This makes the example both a functional regression test and a host-side artifact generator for inspection.
## Running Pytest Locally And Viewing The Image
To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port:
```bash
idf.py set-target esp32p4 build
pytest --target esp32p4 --port PORT pytest_jpeg_decode.py
```
Replace `esp32p4` with another supported target such as `esp32s31` when needed.
`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. The script writes the reconstructed image to `jpeg_decode_result.ppm` inside that test log directory, so after the test finishes you can open the generated `PPM` file locally with an image viewer that supports `PPM` to inspect the decoded output.
## Replacing The Embedded JPEG Asset
If you want to try another input image, replace:
- `main/assets/image.jpg`
Keep the replacement as a baseline JPEG with the same general scale if you want UART log volume and test runtime to stay small.
After replacing the asset, rerun the example and update `golden_output.ppm` from the generated `dut.logdir/jpeg_decode_result.ppm` artifact if the new output should become the expected result.
## Troubleshooting
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
@@ -1,3 +1,5 @@
idf_component_register(SRCS "jpeg_decode_main.c"
PRIV_REQUIRES fatfs esp_driver_jpeg esp_psram
INCLUDE_DIRS ".")
idf_component_register(SRCS "jpeg_decode_example_main.c"
PRIV_REQUIRES esp_driver_jpeg mbedtls
INCLUDE_DIRS ".")
target_add_binary_data(${COMPONENT_LIB} "${CMAKE_CURRENT_LIST_DIR}/assets/image.jpg" BINARY RENAME_TO "example_jpeg")
@@ -1,18 +0,0 @@
menu "JPEG Decode Example menu"
config EXAMPLE_FORMAT_IF_MOUNT_FAILED
bool "Format the card if mount failed"
default n
help
If this config item is set, format_if_mount_failed will be set to true and the card will be formatted if
the mount has failed.
config EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
depends on SOC_SDMMC_IO_POWER_EXTERNAL
bool "SDMMC IO power supply comes from internal LDO (READ HELP!)"
default y
help
Please read the schematic first and check if the SDMMC VDD is connected to any internal LDO output.
If the SDMMC is powered by an external supplier, unselect me
endmenu
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,154 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <assert.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/jpeg_decode.h"
#include "mbedtls/base64.h"
#define EXAMPLE_BASE64_CHUNK_LEN 96
#define EXAMPLE_BYTES_PER_PIXEL 3
/* These linker symbols are generated automatically for the file added by
* EMBED_FILES in CMakeLists.txt. They let the example treat the embedded
* JPEG asset as a byte array stored in flash. */
extern const uint8_t example_jpeg_start[] asm("_binary_example_jpeg_start");
extern const uint8_t example_jpeg_end[] asm("_binary_example_jpeg_end");
/* For JPEGs encoded with block-based chroma subsampling, the decoder output
* buffer dimensions can be padded up to 16-pixel boundaries. This helper
* rounds visible width or height up to that padded size. */
static uint32_t align_up_to_16(uint32_t value)
{
return (value + 15U) & ~15U;
}
static void print_base64_payload(const unsigned char *encoded, size_t encoded_len)
{
/* The payload is split into short lines so the UART log stays easy to
* parse from pytest and less likely to be damaged by very long lines. */
printf("JPEG_DECODE_BASE64_BEGIN\n");
size_t chunk_idx = 0;
for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN, ++chunk_idx) {
size_t chunk_len = encoded_len - offset;
if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) {
chunk_len = EXAMPLE_BASE64_CHUNK_LEN;
}
printf("JPEG_DECODE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
/* Yield periodically to avoid watchdog triggers on long payloads. */
if ((chunk_idx % 16U) == 15U) {
vTaskDelay(1);
}
}
printf("JPEG_DECODE_BASE64_END\n");
}
void app_main(void)
{
const size_t embedded_size = example_jpeg_end - example_jpeg_start;
jpeg_decoder_handle_t jpeg_handle = NULL;
uint8_t *decoded_pixels = NULL;
uint8_t *input_buf = NULL;
unsigned char *encoded = NULL;
printf("Loading embedded JPEG from flash...\n");
printf("Embedded JPEG size: %zu bytes\n", embedded_size);
/* Parse the JPEG header to learn the image dimensions. */
jpeg_decode_picture_info_t header_info;
ESP_ERROR_CHECK(jpeg_decoder_get_info(example_jpeg_start, embedded_size, &header_info));
printf("JPEG header parsed: width=%" PRIu32 " height=%" PRIu32 "\n", header_info.width, header_info.height);
/* The hardware decoder can pad the output image dimensions up to
* 16-pixel boundaries, so the actual buffer may be larger than
* width * height * 3. Allocate for the padded dimensions to avoid
* out-of-bounds writes. */
const uint32_t padded_width = align_up_to_16(header_info.width);
const uint32_t padded_height = align_up_to_16(header_info.height);
const uint32_t output_bytes = padded_width * padded_height * EXAMPLE_BYTES_PER_PIXEL;
/* Ask the driver to allocate an output buffer for decoded pixels.
* JPEG_DEC_ALLOC_OUTPUT_BUFFER tells the driver this memory will hold
* the decode result (as opposed to an input bitstream buffer). */
jpeg_decode_memory_alloc_cfg_t mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
size_t decoded_buffer_size = 0;
decoded_pixels = (uint8_t *)jpeg_alloc_decoder_mem(output_bytes, &mem_cfg, &decoded_buffer_size);
assert(decoded_pixels != NULL);
/* Create a decoder engine instance. The timeout_ms value is the maximum
* time the hardware is allowed to spend on a single decode call. */
jpeg_decode_engine_cfg_t decode_eng_cfg = {
.timeout_ms = 80,
};
ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpeg_handle));
/* RGB888 outputs 3 bytes per pixel. BGR order matches the default byte
* layout expected by OpenCV and many display pipelines. */
jpeg_decode_cfg_t decode_cfg = {
.output_format = JPEG_DECODE_OUT_FORMAT_RGB888,
.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
};
/* jpeg don't handle the encrypted data.*/
const uint8_t *bit_stream = example_jpeg_start;
#if CONFIG_SECURE_FLASH_ENC_ENABLED
size_t input_buffer_size = 0;
jpeg_decode_memory_alloc_cfg_t in_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
};
input_buf = (uint8_t *)jpeg_alloc_decoder_mem(embedded_size, &in_mem_cfg, &input_buffer_size);
assert(input_buf != NULL);
memcpy(input_buf, example_jpeg_start, embedded_size);
bit_stream = input_buf;
#endif
uint32_t decoded_size = 0;
printf("Decoding JPEG -> RGB888...\n");
ESP_ERROR_CHECK(jpeg_decoder_process(
jpeg_handle,
&decode_cfg,
bit_stream,
embedded_size,
decoded_pixels,
decoded_buffer_size,
&decoded_size
));
printf("Decoded RGB888 size: %" PRIu32 " bytes\n", decoded_size);
/* Base64 turns the binary pixel data into printable ASCII so it can be
* safely transported through the serial console and reconstructed by
* pytest. The two-pass pattern (first call with NULL output to get the
* required buffer size, then the real encode) is standard mbedtls usage. */
size_t encoded_len = 0;
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, decoded_pixels, decoded_size);
ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL);
encoded = calloc(encoded_len + 1, 1);
assert(encoded != NULL);
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, decoded_pixels, decoded_size) == 0 ? ESP_OK : ESP_FAIL);
/* JPEG_DECODE_INFO plus the chunked JPEG_DECODE_BASE64 lines form a tiny
* text protocol that the pytest script understands and converts back
* into a PPM golden file for comparison. */
printf("JPEG_DECODE_INFO width=%" PRIu32 " height=%" PRIu32
" padded_width=%" PRIu32 " padded_height=%" PRIu32
" format=RGB888 encoding=base64 size=%" PRIu32 "\n",
header_info.width, header_info.height, padded_width, padded_height, decoded_size);
print_base64_payload(encoded, encoded_len);
printf("JPEG decode demo done.\n");
ESP_ERROR_CHECK(jpeg_del_decoder_engine(jpeg_handle));
free(encoded);
free(decoded_pixels);
free(input_buf);
}
@@ -1,204 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
#include "esp_attr.h"
#include "driver/jpeg_decode.h"
#include "sd_pwr_ctrl_by_on_chip_ldo.h"
static const char *TAG = "jpeg.example";
static sdmmc_card_t *s_card;
#define MOUNT_POINT "/sdcard"
const static char jpg_file_1080[] = "/sdcard/esp1080.jpg";
const static char raw_file_1080[] = "/sdcard/out.rgb";
const static char jpg_file_720[] = "/sdcard/esp720.jpg";
const static char raw_file_720[] = "/sdcard/out2.rgb";
static esp_err_t sdcard_init(void)
{
esp_err_t ret = ESP_OK;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED
.format_if_mount_failed = true,
#else
.format_if_mount_failed = false,
#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
const char mount_point[] = MOUNT_POINT;
ESP_LOGI(TAG, "Initializing SD card");
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
#if CONFIG_EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
sd_pwr_ctrl_ldo_config_t ldo_config = {
.ldo_chan_id = 4, // `LDO_VO4` is used as the SDMMC IO power
};
sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;
ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to new an on-chip ldo power control driver");
return ret;
}
host.pwr_ctrl_handle = pwr_ctrl_handle;
#endif
// This initializes the slot without card detect (CD) and write protect (WP) signals.
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.width = 4;
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &s_card);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem. "
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
}
return ret;
}
// Card has been initialized, print its properties
sdmmc_card_print_info(stdout, s_card);
return ret;
}
static void sdcard_deinit(void)
{
const char mount_point[] = MOUNT_POINT;
esp_vfs_fat_sdcard_unmount(mount_point, s_card);
#if SOC_SDMMC_IO_POWER_EXTERNAL
esp_err_t ret = sd_pwr_ctrl_del_on_chip_ldo(s_card->host.pwr_ctrl_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to delete on-chip ldo power control driver");
return;
}
#endif
}
void app_main(void)
{
ESP_ERROR_CHECK(sdcard_init());
jpeg_decoder_handle_t jpgd_handle;
jpeg_decode_engine_cfg_t decode_eng_cfg = {
.timeout_ms = 40,
};
ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpgd_handle));
jpeg_decode_cfg_t decode_cfg_rgb = {
.output_format = JPEG_DECODE_OUT_FORMAT_RGB888,
.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
};
jpeg_decode_cfg_t decode_cfg_gray = {
.output_format = JPEG_DECODE_OUT_FORMAT_GRAY,
};
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
};
FILE *file_jpg_1080p = fopen(jpg_file_1080, "rb");
ESP_LOGI(TAG, "jpg_file_1080:%s", jpg_file_1080);
if (file_jpg_1080p == NULL) {
ESP_LOGE(TAG, "fopen file_jpg_1080p error");
return;
}
fseek(file_jpg_1080p, 0, SEEK_END);
int jpeg_size_1080p = ftell(file_jpg_1080p);
fseek(file_jpg_1080p, 0, SEEK_SET);
size_t tx_buffer_size_1080p = 0;
uint8_t *tx_buf_1080p = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size_1080p, &tx_mem_cfg, &tx_buffer_size_1080p);
if (tx_buf_1080p == NULL) {
ESP_LOGE(TAG, "alloc 1080p tx buffer error");
return;
}
fread(tx_buf_1080p, 1, jpeg_size_1080p, file_jpg_1080p);
fclose(file_jpg_1080p);
FILE *file_jpg_720p = fopen(jpg_file_720, "rb");
ESP_LOGI(TAG, "jpg_file_1080:%s", jpg_file_720);
if (file_jpg_720p == NULL) {
ESP_LOGE(TAG, "fopen file_jpg_720p error");
return;
}
fseek(file_jpg_720p, 0, SEEK_END);
int jpeg_size_720p = ftell(file_jpg_720p);
fseek(file_jpg_720p, 0, SEEK_SET);
size_t tx_buffer_size_720p = 0;
uint8_t *tx_buf_720p = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size_720p, &tx_mem_cfg, &tx_buffer_size_720p);
if (tx_buf_720p == NULL) {
ESP_LOGE(TAG, "alloc 720p tx buffer error");
return;
}
fread(tx_buf_720p, 1, jpeg_size_720p, file_jpg_720p);
fclose(file_jpg_720p);
size_t rx_buffer_size_1080p = 0;
size_t rx_buffer_size_720p = 0;
uint8_t *rx_buf_1080p = (uint8_t*)jpeg_alloc_decoder_mem(1920 * 1088 * 3, &rx_mem_cfg, &rx_buffer_size_1080p);
uint8_t *rx_buf_720p = (uint8_t*)jpeg_alloc_decoder_mem(720 * 1280, &rx_mem_cfg, &rx_buffer_size_720p);
if (rx_buf_1080p == NULL) {
ESP_LOGE(TAG, "alloc 1080p rx buffer error");
return;
}
if (rx_buf_720p == NULL) {
ESP_LOGE(TAG, "alloc 720p rx buffer error");
return;
}
// Get the jpg header information (This step is optional)
jpeg_decode_picture_info_t header_info;
ESP_ERROR_CHECK(jpeg_decoder_get_info(tx_buf_1080p, jpeg_size_1080p, &header_info));
ESP_LOGI(TAG, "header parsed, width is %" PRId32 ", height is %" PRId32, header_info.width, header_info.height);
uint32_t out_size_1080p = 0;
uint32_t out_size_720p = 0;
ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_rgb, tx_buf_1080p, jpeg_size_1080p, rx_buf_1080p, rx_buffer_size_1080p, &out_size_1080p));
ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_gray, tx_buf_720p, jpeg_size_720p, rx_buf_720p, rx_buffer_size_720p, &out_size_720p));
// Write two pictures.
FILE *file_rgb_1080p = fopen(raw_file_1080, "wb");
ESP_LOGI(TAG, "raw_file_1080:%s", raw_file_1080);
if (file_rgb_1080p == NULL) {
ESP_LOGE(TAG, "fopen file_rgb_1080p error");
return;
}
fwrite(rx_buf_1080p, 1, out_size_1080p, file_rgb_1080p);
fclose(file_rgb_1080p);
FILE *file_rgb_720p = fopen(raw_file_720, "wb");
ESP_LOGI(TAG, "raw_file_720:%s", raw_file_720);
if (file_rgb_720p == NULL) {
ESP_LOGE(TAG, "fopen file_rgb_720p error");
return;
}
fwrite(rx_buf_720p, 1, out_size_720p, file_rgb_720p);
fclose(file_rgb_720p);
sdcard_deinit();
ESP_LOGI(TAG, "Card unmounted");
}
@@ -1,189 +0,0 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import argparse
import cv2 as cv
import numpy as np
from numpy.typing import NDArray
def open_picture(path): # type: (str) -> list[int]
with open(path, 'rb') as f:
data = f.read()
f.close()
new_data = [int(x) for x in data]
return new_data
def picture_show_rgb888(data, h, w): # type: (list[int], int, int) -> None
data = np.array(data).reshape(h, w, 3).astype(np.uint8)
cv.imshow('data', data)
cv.waitKey()
def picture_show_rgb565(data, h, w): # type: (list[int], int, int) -> None
new_data = [0] * ((len(data) // 2) * 3)
for i in range(len(data)):
if i % 2 != 0:
new_data[3 * (i - 1) // 2 + 2] = (data[i] & 0xf8)
new_data[3 * (i - 1) // 2 + 1] |= (data[i] & 0x7) << 5
else:
new_data[3 * i // 2] = (data[i] & 0x1f) << 3
new_data[3 * i // 2 + 1] |= (data[i] & 0xe0) >> 3
new_data = np.array(new_data).reshape(h, w, 3).astype(np.uint8)
cv.imshow('data', new_data)
cv.waitKey()
def picture_show_gray(data, h, w): # type: (list[int], int, int) -> None
new_data = np.array(data).reshape(h, w, 1).astype(np.uint8)
cv.imshow('data', new_data)
cv.waitKey()
def convert_YUV_to_RGB(Y, U, V): # type: (NDArray, NDArray, NDArray) -> tuple[NDArray, NDArray, NDArray]
B = np.clip(Y + 1.7790 * (U - 128), 0, 255).astype(np.uint8)
G = np.clip(Y - 0.3455 * (U - 128) - 0.7169 * (V - 128), 0, 255).astype(np.uint8)
R = np.clip(Y + 1.4075 * (V - 128), 0, 255).astype(np.uint8)
return B, G, R
def picture_show_yuv420(data, h, w): # type: (list[int], int, int) -> None
new_u = [0] * (h * w)
new_v = [0] * (h * w)
new_y = [0] * (h * w)
for i in range(int(h * w * 1.5)):
is_even_row = ((i // (w * 1.5)) % 2 == 0)
if is_even_row:
if (i % 3 == 0):
new_u[(i // 3) * 2] = data[i]
new_u[(i // 3) * 2 + 1] = data[i]
else:
if (i % 3 == 0):
new_u[(i // 3) * 2] = new_u[int((i - (w * 1.5)) // 3) * 2]
new_u[(i // 3) * 2 + 1] = new_u[int((i - (w * 1.5)) // 3) * 2 + 1]
for i in range(int(h * w * 1.5)):
if (i // (w * 1.5)) % 2 != 0 and (i % 3 == 0):
idx = (i // 3) * 2
new_v[idx] = data[i]
new_v[idx + 1] = data[i]
for i in range(int(h * w * 1.5)):
if (i // (w * 1.5)) % 2 == 0 and (i % 3 == 0):
idx = (i // 3) * 2
new_v[idx] = new_v[int((i + (w * 1.5)) // 3) * 2]
new_v[idx + 1] = new_v[int((i + (w * 1.5)) // 3) * 2 + 1]
new_y = [data[i] for i in range(int(h * w * 1.5)) if i % 3 != 0]
Y = np.array(new_y)
U = np.array(new_u)
V = np.array(new_v)
B, G, R = convert_YUV_to_RGB(Y, U, V)
# Merge channels
new_data = np.stack((B, G, R), axis=-1)
new_data = np.array(new_data).reshape(h, w, 3).astype(np.uint8)
# Display the image
cv.imshow('data', new_data)
cv.waitKey()
def picture_show_yuv422(data, h, w): # type: (list[int], int, int) -> None
# Reshape the input data to a 2D array
data_array = np.array(data).reshape(h, w * 2)
# Separate Y, U, and V channels
Y = data_array[:, 1::2]
U = data_array[:, 0::4].repeat(2, axis=1)
V = data_array[:, 2::4].repeat(2, axis=1)
# Convert YUV to RGB
B, G, R = convert_YUV_to_RGB(Y, U, V)
# Merge channels
new_data = np.stack((B, G, R), axis=-1)
# Display the image
cv.imshow('data', new_data)
cv.waitKey()
def picture_show_yuv444(data, h, w): # type: (list[int], int, int) -> None
# Reshape the input data to a 2D array
data_array = np.array(data).reshape(h, w * 3)
# Separate Y, U, and V channels
Y = data_array[:, 2::3]
U = data_array[:, 1::3]
V = data_array[:, 0::3]
# Convert YUV to RGB
B, G, R = convert_YUV_to_RGB(Y, U, V)
# Merge channels
new_data = np.stack((B, G, R), axis=-1)
# Display the image
cv.imshow('data', new_data)
cv.waitKey()
def main(): # type: () -> None
parser = argparse.ArgumentParser(description='which mode need to show')
parser.add_argument(
'--pic_path',
type=str,
help='What is the path of your picture',
required=True)
parser.add_argument(
'--pic_type',
type=str,
help='What type you want to show',
required=True,
choices=['rgb565', 'rgb888', 'gray', 'yuv422', 'yuv420', 'yuv444'])
parser.add_argument(
'--height',
type=int,
help='the picture height',
default=480)
parser.add_argument(
'--width',
type=int,
help='the picture width',
default=640)
args = parser.parse_args()
height = args.height
width = args.width
data = open_picture(args.pic_path)
if (args.pic_type == 'rgb565'):
picture_show_rgb565(data, height, width)
elif (args.pic_type == 'rgb888'):
picture_show_rgb888(data, height, width)
elif (args.pic_type == 'gray'):
picture_show_gray(data, height, width)
elif (args.pic_type == 'yuv420'):
picture_show_yuv420(data, height, width)
elif (args.pic_type == 'yuv422'):
picture_show_yuv422(data, height, width)
elif (args.pic_type == 'yuv444'):
picture_show_yuv444(data, height, width)
else:
print('This type is not supported in this script!')
if __name__ == '__main__':
main()
@@ -0,0 +1,240 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import base64
import hashlib
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import List
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
DECODE_OUTPUT_NAME = 'jpeg_decode_result.ppm'
GOLDEN_OUTPUT_NAME = 'golden_output.ppm'
GOLDEN_OUTPUT_PATH = Path(__file__).with_name(GOLDEN_OUTPUT_NAME)
EXPECTED_PIXEL_FORMAT = 'RGB888'
EXPECTED_ENCODING = 'base64'
RGB888_BYTES_PER_PIXEL = 3
PPM_MAGIC = b'P6'
PPM_MAX_VALUE = b'255'
DECODE_INFO_PATTERN = (
r'JPEG_DECODE_INFO width=(?P<width>\d+) height=(?P<height>\d+) '
r'padded_width=(?P<padded_width>\d+) padded_height=(?P<padded_height>\d+) '
r'format=(?P<format>\w+) encoding=(?P<encoding>\w+) size=(?P<size>\d+)'
)
DECODE_INFO_RE = re.compile(DECODE_INFO_PATTERN)
DECODE_CHUNK_PATTERN = r'JPEG_DECODE_BASE64 (?P<payload>[A-Za-z0-9+/=]+)'
DECODE_CHUNK_RE = re.compile(DECODE_CHUNK_PATTERN)
PPM_HEADER_RE = re.compile(rb'^P6\s+(?P<width>\d+)\s+(?P<height>\d+)\s+(?P<max_value>\d+)\s')
@dataclass(frozen=True)
class DecodeMetadata:
width: int
height: int
padded_width: int
padded_height: int
pixel_format: str
encoding: str
size: int
def __post_init__(self) -> None:
if self.width <= 0 or self.height <= 0:
raise ValueError(f'Invalid dimensions: {self.width}x{self.height}')
if self.padded_width < self.width or self.padded_height < self.height:
raise ValueError(
f'Padded size ({self.padded_width}x{self.padded_height}) '
f'smaller than visible size ({self.width}x{self.height})'
)
if self.pixel_format != EXPECTED_PIXEL_FORMAT:
raise ValueError(f'Unsupported pixel format: {self.pixel_format}')
if self.encoding != EXPECTED_ENCODING:
raise ValueError(f'Unsupported encoding: {self.encoding}')
@property
def padded_image_size(self) -> int:
return self.padded_width * self.padded_height * RGB888_BYTES_PER_PIXEL
@dataclass(frozen=True)
class RgbImage:
width: int
height: int
pixels_rgb888: bytes
def __post_init__(self) -> None:
expected_size = self.width * self.height * RGB888_BYTES_PER_PIXEL
if len(self.pixels_rgb888) != expected_size:
raise ValueError(f'Expected {expected_size} RGB bytes, got {len(self.pixels_rgb888)}')
def parse_decode_metadata(meta_line: str) -> DecodeMetadata:
match = DECODE_INFO_RE.fullmatch(meta_line)
if not match:
raise ValueError(f'Invalid decode metadata line: {meta_line}')
return DecodeMetadata(
width=int(match.group('width')),
height=int(match.group('height')),
padded_width=int(match.group('padded_width')),
padded_height=int(match.group('padded_height')),
pixel_format=match.group('format'),
encoding=match.group('encoding'),
size=int(match.group('size')),
)
def collect_base64_payload(dut: Dut) -> List[str]:
payload_lines: List[str] = []
while True:
# The example prints the decoded frame as multiple short UART lines
# instead of one giant base64 blob, so collect and join them here.
match = dut.expect(rf'(?P<line>JPEG_DECODE_BASE64_END|{DECODE_CHUNK_PATTERN}\r?\n)', timeout=60)
line = match.group('line').decode('utf-8').strip()
if line == 'JPEG_DECODE_BASE64_END':
return payload_lines
chunk_match = DECODE_CHUNK_RE.fullmatch(line)
assert chunk_match is not None
payload_lines.append(chunk_match.group('payload'))
def _crop_visible_bgr888(raw_bytes: bytes, metadata: DecodeMetadata) -> bytes:
# The hardware can write into a padded decode buffer whose width/height are
# rounded up to JPEG block boundaries. Pytest only wants the visible image,
# so keep the useful bytes from each row and discard the padded tail rows.
if len(raw_bytes) != metadata.padded_image_size:
raise ValueError(f'Expected {metadata.padded_image_size} padded BGR bytes, got {len(raw_bytes)}')
visible_row_size = metadata.width * RGB888_BYTES_PER_PIXEL
padded_row_size = metadata.padded_width * RGB888_BYTES_PER_PIXEL
return b''.join(
raw_bytes[offset : offset + visible_row_size]
for offset in range(0, padded_row_size * metadata.height, padded_row_size)
)
def _bgr888_to_rgb888(raw_bytes: bytes) -> bytes:
# The decoder's RGB888 mode uses BGR24 byte layout by default. Swap the
# first and third byte in each pixel so the PPM artifact becomes standard
# RGB order that common desktop image tools expect.
rgb_bytes = bytearray(raw_bytes)
rgb_bytes[0::3], rgb_bytes[2::3] = raw_bytes[2::3], raw_bytes[0::3]
return bytes(rgb_bytes)
def decode_base64_image(metadata: DecodeMetadata, payload_lines: List[str]) -> RgbImage:
# The DUT sends the raw decode buffer as base64 over UART because the test
# environment only observes text logs. Rebuild bytes on the host, crop away
# decoder padding, then normalize the pixel order for image comparison.
raw_bytes = base64.b64decode(''.join(payload_lines), validate=True)
if len(raw_bytes) != metadata.size:
raise ValueError(f'Expected {metadata.size} decoded bytes, got {len(raw_bytes)}')
visible_bgr888 = _crop_visible_bgr888(raw_bytes, metadata)
return RgbImage(
width=metadata.width,
height=metadata.height,
pixels_rgb888=_bgr888_to_rgb888(visible_bgr888),
)
def _encode_ppm(image: RgbImage) -> bytes:
header = b'%s\n%d %d\n%s\n' % (PPM_MAGIC, image.width, image.height, PPM_MAX_VALUE)
return header + image.pixels_rgb888
def _load_ppm(path: Path) -> RgbImage:
ppm_bytes = path.read_bytes()
header_match = PPM_HEADER_RE.match(ppm_bytes)
if not header_match:
raise ValueError('Invalid PPM header')
width = int(header_match.group('width'))
height = int(header_match.group('height'))
max_value = header_match.group('max_value')
if width <= 0 or height <= 0:
raise ValueError('Unsupported PPM dimensions')
if max_value != PPM_MAX_VALUE:
raise ValueError(f'Unsupported PPM max value: {max_value.decode("ascii", errors="replace")}')
pixel_data = ppm_bytes[header_match.end() :]
return RgbImage(width=width, height=height, pixels_rgb888=pixel_data)
def save_ppm_artifact(image: RgbImage, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
output_path.write_bytes(_encode_ppm(image))
except OSError:
logging.exception('Failed to save JPEG decode artifact to %s', output_path)
return
logging.info('Saved JPEG decode artifact to %s', output_path)
def image_digest(image: RgbImage) -> str:
digest = hashlib.sha256()
digest.update(image.width.to_bytes(4, 'big'))
digest.update(image.height.to_bytes(4, 'big'))
digest.update(image.pixels_rgb888)
return digest.hexdigest()
def assert_image_matches_golden(result_image: RgbImage, golden_path: Path) -> None:
assert golden_path.is_file(), f'Golden PPM not found: {golden_path}'
golden_image = _load_ppm(golden_path)
assert image_digest(result_image) == image_digest(golden_image), (
f'Generated image does not match golden file: {golden_path.name}'
)
def run_jpeg_decode_example(dut: Dut) -> None:
dut.expect_exact('Loading embedded JPEG from flash...')
dut.expect(r'Embedded JPEG size: \d+ bytes')
dut.expect(r'JPEG header parsed: width=\d+ height=\d+')
dut.expect_exact('Decoding JPEG -> RGB888...')
dut.expect(r'Decoded RGB888 size: \d+ bytes')
metadata_line = dut.expect(DECODE_INFO_PATTERN).group(0).decode('utf-8')
metadata = parse_decode_metadata(metadata_line)
dut.expect_exact('JPEG_DECODE_BASE64_BEGIN')
# Collect the machine-readable payload before the example prints its final
# completion line so we keep the UART parsing strictly in output order.
payload_lines = collect_base64_payload(dut)
dut.expect_exact('JPEG decode demo done.')
result_image = decode_base64_image(metadata, payload_lines)
output_path = Path(dut.logdir) / DECODE_OUTPUT_NAME
save_ppm_artifact(result_image, output_path)
assert_image_matches_golden(result_image, GOLDEN_OUTPUT_PATH)
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_DECODE_SUPPORTED == 1'), indirect=['target'])
def test_jpeg_decode_example(dut: Dut) -> None:
run_jpeg_decode_example(dut)
@pytest.mark.flash_encryption
@pytest.mark.parametrize(
'config',
[
'flash_enc',
],
indirect=True,
)
@idf_parametrize(
'target',
soc_filtered_targets('SOC_JPEG_DECODE_SUPPORTED == 1 and SOC_FLASH_ENC_SUPPORTED == 1'),
indirect=['target'],
)
def test_jpeg_decode_example_with_flash_encryption(dut: Dut) -> None:
run_jpeg_decode_example(dut)
@@ -1,2 +0,0 @@
opencv-python
numpy
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1 @@
# Default CI build, inherits sdkconfig.defaults
@@ -0,0 +1,7 @@
CONFIG_PARTITION_TABLE_OFFSET=0x9000
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
CONFIG_SPIRAM_ENC_EXEMPT=y
CONFIG_SPIRAM_ENC_EXEMPT_SIZE=4096
@@ -1,6 +1 @@
# SPIRAM configurations
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_HEX=y
CONFIG_SPIRAM_SPEED_200M=y
@@ -30,6 +30,44 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l
## Example Output
```text
Loading embedded BGR24 image from flash...
Embedded raw image size: 2764800 bytes
Encoding BGR24(raw) -> JPEG...
Encoded JPEG size: 30795 bytes
JPEG_META width=1280 height=720 format=JPEG encoding=base64 size=30795
JPEG_BASE64_BEGIN
JPEG_BASE64 ...
JPEG_BASE64 ...
JPEG_BASE64_END
JPEG encode demo done.
```
## Pytest Visual Check
The accompanying `pytest_jpeg_encode.py` script captures the `JPEG_META` and `JPEG_BASE64` output, reconstructs the encoded JPEG, and saves it as:
- `dut.logdir/jpeg_encode_result.jpeg`
It also compares the generated JPEG with `golden_output.jpeg`. This turns the example into both a functional regression test and a host-side artifact generator that makes the encoded result easy to inspect.
## Running Pytest Locally And Viewing The Image
To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port:
```bash
idf.py set-target esp32p4 build
pytest --target esp32p4 --port PORT pytest_jpeg_encode.py
```
Replace `esp32p4` with another supported target such as `esp32s31` when needed.
`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. The script writes the reconstructed image to `jpeg_encode_result.jpeg` inside that test log directory, so after the test finishes you can open the generated JPEG locally with any image viewer to inspect the encoded output.
## Replacing The Embedded RGB Asset
If you want to regenerate a compatible raw frame from another input image, one simple workflow is:
```bash
I (1114) jpeg.example: Initializing SD card
I (1114) gpio: GPIO[43]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -1,3 +1,5 @@
idf_component_register(SRCS "jpeg_encode_main.c"
PRIV_REQUIRES fatfs esp_driver_jpeg
idf_component_register(SRCS "jpeg_encode_example_main.c"
PRIV_REQUIRES fatfs esp_driver_jpeg mbedtls esp_psram
INCLUDE_DIRS ".")
target_add_binary_data(${COMPONENT_LIB} "${CMAKE_CURRENT_LIST_DIR}/assets/esp720p.rgb" BINARY RENAME_TO "esp720p_rgb")
@@ -1,18 +0,0 @@
menu "JPEG Encode Example menu"
config EXAMPLE_FORMAT_IF_MOUNT_FAILED
bool "Format the card if mount failed"
default n
help
If this config item is set, format_if_mount_failed will be set to true and the card will be formatted if
the mount has failed.
config EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
depends on SOC_SDMMC_IO_POWER_EXTERNAL
bool "SDMMC IO power supply comes from internal LDO (READ HELP!)"
default y
help
Please read the schematic first and check if the SDMMC VDD is connected to any internal LDO output.
If the SDMMC is powered by an external supplier, unselect me
endmenu
File diff suppressed because one or more lines are too long
@@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <assert.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mbedtls/base64.h"
#include "esp_check.h"
#include "driver/jpeg_encode.h"
#define EXAMPLE_WIDTH 1280
#define EXAMPLE_HEIGHT 720
#define EXAMPLE_RGB_FRAME_SIZE (EXAMPLE_WIDTH * EXAMPLE_HEIGHT * 3)
#define EXAMPLE_JPEG_BUFFER_SIZE (EXAMPLE_RGB_FRAME_SIZE / 10) /* Estimate output size with an approximately 10:1 JPEG compression ratio for this demo image. */
#define EXAMPLE_BASE64_CHUNK_LEN 96
#define EXAMPLE_JPEG_QUALITY 80
extern const uint8_t esp720p_rgb_start[] asm("_binary_esp720p_rgb_start");
extern const uint8_t esp720p_rgb_end[] asm("_binary_esp720p_rgb_end");
static void print_base64_payload(const unsigned char *encoded, size_t encoded_len)
{
/* Split the printable payload into short lines so it is easy to read in
* the serial monitor and robust for pytest to parse back into a JPEG. */
printf("JPEG_BASE64_BEGIN\n");
for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN) {
size_t chunk_len = encoded_len - offset;
if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) {
chunk_len = EXAMPLE_BASE64_CHUNK_LEN;
}
printf("JPEG_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
}
printf("JPEG_BASE64_END\n");
}
void app_main(void)
{
/* EMBED_FILES turns the raw asset into linker symbols, so the example can
* read the picture directly from flash without mounting a filesystem. */
const size_t embedded_size = esp720p_rgb_end - esp720p_rgb_start;
uint32_t jpeg_size = 0;
jpeg_encoder_handle_t jpeg_handle = NULL;
uint8_t *rgb_buf = NULL;
printf("Loading embedded BGR24 image from flash...\n");
printf("Embedded raw image size: %zu bytes\n", embedded_size);
assert(embedded_size == EXAMPLE_RGB_FRAME_SIZE);
/* Despite the enum name, the current driver maps
* JPEG_ENCODE_IN_FORMAT_RGB888 to a BGR24-style byte layout.
* Keep the embedded raw asset in bgr24 order or red/blue will swap. */
jpeg_encode_cfg_t enc_config = {
.src_type = JPEG_ENCODE_IN_FORMAT_RGB888,
.sub_sample = JPEG_DOWN_SAMPLING_YUV422,
.image_quality = EXAMPLE_JPEG_QUALITY,
.width = EXAMPLE_WIDTH,
.height = EXAMPLE_HEIGHT,
};
const uint8_t *rgb_src = esp720p_rgb_start;
#if CONFIG_SECURE_FLASH_ENC_ENABLED
size_t input_buffer_size = 0;
jpeg_encode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_ENC_ALLOC_INPUT_BUFFER,
};
rgb_buf = (uint8_t *)jpeg_alloc_encoder_mem(EXAMPLE_RGB_FRAME_SIZE, &rx_mem_cfg, &input_buffer_size);
assert(rgb_buf != NULL);
memcpy(rgb_buf, esp720p_rgb_start, EXAMPLE_RGB_FRAME_SIZE);
rgb_src = rgb_buf;
#endif
size_t result_buffer_size = 0;
/* The output JPEG is compressed, so the example does not need to reserve
* a full raw-frame worth of space for the bitstream. This 10:1 estimate
* is intentionally conservative for the bundled demo image and quality
* setting; real applications should size this buffer for their own worst
* case and handle "buffer too small" errors if needed. */
jpeg_encode_memory_alloc_cfg_t mem_cfg = {
.buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER,
};
uint8_t *jpeg_buf = (uint8_t *)jpeg_alloc_encoder_mem(EXAMPLE_JPEG_BUFFER_SIZE, &mem_cfg, &result_buffer_size);
assert(jpeg_buf != NULL);
/* Create the encoder instance once, then feed it one full-frame request. */
jpeg_encode_engine_cfg_t encode_eng_cfg = {
.timeout_ms = 200,
};
ESP_ERROR_CHECK(jpeg_new_encoder_engine(&encode_eng_cfg, &jpeg_handle));
printf("Encoding BGR24(raw) -> JPEG...\n");
ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, rgb_src, EXAMPLE_RGB_FRAME_SIZE,
jpeg_buf, result_buffer_size, &jpeg_size));
printf("Encoded JPEG size: %" PRIu32 " bytes\n", jpeg_size);
size_t encoded_len = 0;
/* First call asks mbedTLS how big the base64 buffer must be, then the
* second call performs the actual binary-to-text conversion. */
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, jpeg_buf, jpeg_size);
ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL);
unsigned char *encoded = calloc(encoded_len + 1, 1);
assert(encoded != NULL);
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, jpeg_buf, jpeg_size) == 0 ? ESP_OK : ESP_FAIL);
/* JPEG_META plus the chunked JPEG_BASE64 lines form a tiny text protocol
* that pytest understands and can reconstruct into a host-side .jpeg. */
printf("JPEG_META width=%u height=%u format=JPEG encoding=base64 size=%" PRIu32 "\n",
EXAMPLE_WIDTH, EXAMPLE_HEIGHT, jpeg_size);
print_base64_payload(encoded, encoded_len);
printf("JPEG encode demo done.\n");
ESP_ERROR_CHECK(jpeg_del_encoder_engine(jpeg_handle));
free(encoded);
free(jpeg_buf);
free(rgb_buf);
}
@@ -1,153 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
#include "driver/jpeg_encode.h"
#include "sd_pwr_ctrl_by_on_chip_ldo.h"
static const char *TAG = "jpeg.example";
static sdmmc_card_t *s_card;
#define MOUNT_POINT "/sdcard"
const static char s_infile_1080p[] = "/sdcard/esp1080.rgb";
const static char s_outfile_1080p[] = "/sdcard/outjpg.jpg";
static esp_err_t sdcard_init(void)
{
esp_err_t ret = ESP_OK;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED
.format_if_mount_failed = true,
#else
.format_if_mount_failed = false,
#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
const char mount_point[] = MOUNT_POINT;
ESP_LOGI(TAG, "Initializing SD card");
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
#if CONFIG_EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO
sd_pwr_ctrl_ldo_config_t ldo_config = {
.ldo_chan_id = 4, // `LDO_VO4` is used as the SDMMC IO power
};
sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;
ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to new an on-chip ldo power control driver");
return ret;
}
host.pwr_ctrl_handle = pwr_ctrl_handle;
#endif
// This initializes the slot without card detect (CD) and write protect (WP) signals.
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.width = 4;
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &s_card);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount filesystem. "
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
} else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
}
return ret;
}
// Card has been initialized, print its properties
sdmmc_card_print_info(stdout, s_card);
return ret;
}
static void sdcard_deinit(void)
{
const char mount_point[] = MOUNT_POINT;
esp_vfs_fat_sdcard_unmount(mount_point, s_card);
#if SOC_SDMMC_IO_POWER_EXTERNAL
esp_err_t ret = sd_pwr_ctrl_del_on_chip_ldo(s_card->host.pwr_ctrl_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to delete on-chip ldo power control driver");
return;
}
#endif
}
void app_main(void)
{
ESP_ERROR_CHECK(sdcard_init());
uint32_t raw_size_1080p;
uint32_t jpg_size_1080p;
jpeg_encoder_handle_t jpeg_handle;
FILE *file_raw_1080p = fopen(s_infile_1080p, "rb");
ESP_LOGI(TAG, "s_infile_1080p:%s", s_infile_1080p);
if (file_raw_1080p == NULL) {
ESP_LOGE(TAG, "fopen file_raw_1080p error");
return;
}
jpeg_encode_engine_cfg_t encode_eng_cfg = {
.timeout_ms = 70,
};
jpeg_encode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
jpeg_encode_memory_alloc_cfg_t tx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
};
ESP_ERROR_CHECK(jpeg_new_encoder_engine(&encode_eng_cfg, &jpeg_handle));
// Read 1080p raw picture
fseek(file_raw_1080p, 0, SEEK_END);
raw_size_1080p = ftell(file_raw_1080p);
fseek(file_raw_1080p, 0, SEEK_SET);
size_t tx_buffer_size = 0;
uint8_t *raw_buf_1080p = (uint8_t*)jpeg_alloc_encoder_mem(raw_size_1080p, &tx_mem_cfg, &tx_buffer_size);
assert(raw_buf_1080p != NULL);
fread(raw_buf_1080p, 1, raw_size_1080p, file_raw_1080p);
fclose(file_raw_1080p);
size_t rx_buffer_size = 0;
uint8_t *jpg_buf_1080p = (uint8_t*)jpeg_alloc_encoder_mem(raw_size_1080p / 10, &rx_mem_cfg, &rx_buffer_size); // Assume that compression ratio of 10 to 1
assert(jpg_buf_1080p != NULL);
jpeg_encode_cfg_t enc_config = {
.src_type = JPEG_ENCODE_IN_FORMAT_RGB888,
.sub_sample = JPEG_DOWN_SAMPLING_YUV422,
.image_quality = 80,
.width = 1920,
.height = 1080,
};
ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, raw_buf_1080p, raw_size_1080p, jpg_buf_1080p, rx_buffer_size, &jpg_size_1080p));
FILE *file_jpg_1080p = fopen(s_outfile_1080p, "wb");
ESP_LOGI(TAG, "outfile:%s", s_outfile_1080p);
if (file_jpg_1080p == NULL) {
ESP_LOGE(TAG, "fopen file_jpg_1080p error");
return;
}
fwrite(jpg_buf_1080p, 1, jpg_size_1080p, file_jpg_1080p);
fclose(file_jpg_1080p);
sdcard_deinit();
ESP_LOGI(TAG, "Card unmounted");
}
@@ -0,0 +1,4 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 0x380000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x6000
3 phy_init data phy 0x1000
4 factory app factory 0x380000
@@ -0,0 +1,146 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import base64
import hashlib
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import List
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
JPEG_META_PATTERN = r'JPEG_META width=(\d+) height=(\d+) format=(\w+) encoding=(\w+) size=(\d+)'
JPEG_META_RE = re.compile(rf'^{JPEG_META_PATTERN}$')
JPEG_CHUNK_RE = re.compile(r'^JPEG_BASE64 ([A-Za-z0-9+/=]+)$')
JPEG_OUTPUT_NAME = 'jpeg_encode_result.jpeg'
GOLDEN_IMAGE_NAME = 'golden_output.jpeg'
GOLDEN_IMAGE_PATH = Path(__file__).with_name(GOLDEN_IMAGE_NAME)
EXPECTED_FORMAT = 'JPEG'
EXPECTED_ENCODING = 'base64'
EXPECTED_WIDTH = 1280
EXPECTED_HEIGHT = 720
@dataclass(frozen=True)
class JpegMetadata:
width: int
height: int
image_format: str
encoding: str
size: int
def parse_jpeg_metadata(meta_line: str) -> JpegMetadata:
match = JPEG_META_RE.match(meta_line)
if not match:
raise ValueError(f'Invalid JPEG metadata line: {meta_line}')
return JpegMetadata(
width=int(match.group(1)),
height=int(match.group(2)),
image_format=match.group(3),
encoding=match.group(4),
size=int(match.group(5)),
)
def collect_base64_payload(dut: Dut) -> List[str]:
payload_chunks: List[str] = []
while True:
match = dut.expect(r'(JPEG_BASE64_END|JPEG_BASE64 [A-Za-z0-9+/=]+\r?\n)')
line = match.group(1).decode('utf-8').strip()
if line == 'JPEG_BASE64_END':
return payload_chunks
chunk_match = JPEG_CHUNK_RE.match(line)
assert chunk_match is not None
payload_chunks.append(chunk_match.group(1))
def decode_jpeg_base64_payload(metadata: JpegMetadata, payload_lines: List[str]) -> bytes:
if metadata.image_format != EXPECTED_FORMAT:
raise ValueError(f'Unsupported image format: {metadata.image_format}')
if metadata.encoding != EXPECTED_ENCODING:
raise ValueError(f'Unsupported payload encoding: {metadata.encoding}')
jpeg_bytes = base64.b64decode(''.join(payload_lines), validate=True)
if len(jpeg_bytes) != metadata.size:
raise ValueError(f'Expected {metadata.size} JPEG bytes, got {len(jpeg_bytes)}')
return jpeg_bytes
def save_jpeg_artifact(jpeg_bytes: bytes, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
output_path.write_bytes(jpeg_bytes)
except OSError:
logging.exception('Failed to save JPEG artifact to %s', output_path)
return
logging.info('Saved JPEG artifact to %s', output_path)
def _sha256_digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def assert_jpeg_matches_golden(result_bytes: bytes, golden_path: Path) -> None:
assert golden_path.is_file(), f'Golden JPEG not found: {golden_path}'
golden_bytes = golden_path.read_bytes()
result_digest = _sha256_digest(result_bytes)
golden_digest = _sha256_digest(golden_bytes)
assert result_digest == golden_digest, (
f'Generated JPEG does not match golden file: {golden_path.name} '
f'(result sha256={result_digest}, golden sha256={golden_digest})'
)
def run_jpeg_encode_example(dut: Dut) -> None:
dut.expect_exact('Loading embedded BGR24 image from flash...')
dut.expect(r'Embedded raw image size: \d+ bytes')
dut.expect_exact('Encoding BGR24(raw) -> JPEG...')
dut.expect(r'Encoded JPEG size: \d+ bytes')
metadata = parse_jpeg_metadata(dut.expect(JPEG_META_PATTERN).group(0).decode('utf-8'))
assert metadata.width == EXPECTED_WIDTH
assert metadata.height == EXPECTED_HEIGHT
dut.expect_exact('JPEG_BASE64_BEGIN')
payload_lines = collect_base64_payload(dut)
jpeg_bytes = decode_jpeg_base64_payload(metadata, payload_lines)
output_path = Path(dut.logdir) / JPEG_OUTPUT_NAME
save_jpeg_artifact(jpeg_bytes, output_path)
assert_jpeg_matches_golden(jpeg_bytes, GOLDEN_IMAGE_PATH)
dut.expect_exact('JPEG encode demo done.')
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1'), indirect=['target'])
def test_jpeg_encode_example(dut: Dut) -> None:
run_jpeg_encode_example(dut)
@pytest.mark.flash_encryption
@pytest.mark.parametrize(
'config',
[
'flash_enc',
],
indirect=True,
)
@idf_parametrize(
'target',
soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1 and SOC_FLASH_ENC_SUPPORTED == 1'),
indirect=['target'],
)
def test_jpeg_encode_example_with_flash_encryption(dut: Dut) -> None:
run_jpeg_encode_example(dut)
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
# Default CI build, inherits sdkconfig.defaults
@@ -0,0 +1,7 @@
CONFIG_PARTITION_TABLE_OFFSET=0x9000
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
CONFIG_SPIRAM_ENC_EXEMPT=y
CONFIG_SPIRAM_ENC_EXEMPT_SIZE=4096
@@ -0,0 +1,8 @@
# SPIRAM configurations
CONFIG_SPIRAM=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
@@ -1,6 +0,0 @@
# SPIRAM configurations
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_HEX=y
CONFIG_SPIRAM_SPEED_200M=y