fix(jpeg): JPEG can encode and decode in encryption situation

This commit is contained in:
C.S.M
2026-07-13 15:00:31 +08:00
parent 41582e1777
commit e92e669dee
14 changed files with 559 additions and 20 deletions
+15
View File
@@ -23,6 +23,7 @@
#include "esp_log.h"
#include "esp_check.h"
#include "hal/jpeg_periph.h"
#include "esp_psram.h"
#if JPEG_USE_RETENTION_LINK
#include "esp_private/sleep_retention.h"
#endif
@@ -257,3 +258,17 @@ esp_err_t jpeg_check_intr_priority(jpeg_codec_handle_t jpeg_codec, int intr_prio
ESP_RETURN_ON_FALSE(!intr_priority_conflict, ESP_ERR_INVALID_STATE, TAG, "intr_priority conflict, already is %d but attempt to %d", jpeg_codec->intr_priority, intr_priority);
return ret;
}
bool jpeg_check_dma2d_buffer(const void *buffer)
{
#if CONFIG_SECURE_FLASH_ENC_ENABLED
// jpeg cannot handle encrypted data.
if (esp_ptr_external_ram(buffer) && !esp_psram_ptr_is_no_enc(buffer)) {
return false;
}
if (esp_ptr_in_drom(buffer)) {
return false;
}
#endif
return true;
}
+18 -7
View File
@@ -17,6 +17,7 @@
#include "hal/cache_ll.h"
#include "hal/cache_hal.h"
#include "hal/jpeg_defs.h"
#include "hal/hal_utils.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
@@ -287,6 +288,10 @@ esp_err_t jpeg_decoder_process(jpeg_decoder_handle_t decoder_engine, const jpeg_
ESP_RETURN_ON_FALSE(_check_buffer_alignment(decode_outbuf, outbuf_size, outbuf_cache_line_size), ESP_ERR_INVALID_ARG, TAG,
"jpeg decode decode_outbuf or out_buffer size is not aligned, please use jpeg_alloc_decoder_mem to malloc your buffer");
// both the bitstream and output buffer are accessed by the 2D-DMA
ESP_RETURN_ON_FALSE(jpeg_check_dma2d_buffer(bit_stream) && jpeg_check_dma2d_buffer(decode_outbuf), ESP_ERR_INVALID_ARG, TAG,
"jpeg decode buffer is not 16-byte aligned or not in unencrypted PSRAM, please use jpeg_alloc_decoder_mem to malloc your buffer");
esp_err_t ret = ESP_OK;
#if CONFIG_PM_ENABLE
@@ -428,15 +433,21 @@ void *jpeg_alloc_decoder_mem(size_t size, const jpeg_decode_memory_alloc_cfg_t *
FOr input buffer(for decoder is PSRAM write to 2DDMA), no restriction for any align (both cache writeback and requirement from 2DDMA).
*/
size_t cache_align = 0;
size_t buffer_align = 0;
esp_cache_get_alignment(MALLOC_CAP_SPIRAM, &cache_align);
if (mem_cfg->buffer_direction == JPEG_DEC_ALLOC_OUTPUT_BUFFER) {
size = JPEG_ALIGN_UP(size, cache_align);
*allocated_size = size;
return heap_caps_aligned_calloc(cache_align, 1, size, MALLOC_CAP_SPIRAM);
} else {
*allocated_size = size;
return heap_caps_calloc(1, size, MALLOC_CAP_SPIRAM);
buffer_align = MAX(cache_align, JPEG_DMA2D_BUFFER_ALIGN);
size = JPEG_ALIGN_UP(size, buffer_align);
*allocated_size = size;
// To simplify the logic, we always use the LCM of cache and 2D-DMA alignment to satisfy both requirements
void *buffer = heap_caps_aligned_calloc(buffer_align, 1, size, JPEG_SPIRAM_ALLOC_CAPS);
if (buffer == NULL) {
#if CONFIG_SPIRAM_ENC_EXEMPT
ESP_LOGE(TAG, "no mem for %zu bytes decode buffer in unencrypted PSRAM, please enlarge CONFIG_SPIRAM_ENC_EXEMPT_SIZE", size);
#else
ESP_LOGE(TAG, "no mem for %zu bytes decode buffer", size);
#endif
}
return buffer;
}
/****************************************************************
+16 -7
View File
@@ -19,6 +19,7 @@
#include "hal/jpeg_ll.h"
#include "hal/cache_hal.h"
#include "hal/cache_ll.h"
#include "hal/hal_utils.h"
#include "esp_private/dma2d.h"
#include "jpeg_private.h"
#include "driver/jpeg_encode.h"
@@ -175,6 +176,8 @@ esp_err_t jpeg_encoder_process(jpeg_encoder_handle_t encoder_engine, const jpeg_
ESP_RETURN_ON_FALSE(bit_stream, ESP_ERR_INVALID_ARG, TAG, "jpeg encode output buffer is null");
ESP_RETURN_ON_FALSE(out_size, ESP_ERR_INVALID_ARG, TAG, "jpeg encode picture out_size is null");
ESP_RETURN_ON_FALSE(((uintptr_t)bit_stream % cache_hal_get_cache_line_size(CACHE_LL_LEVEL_EXT_MEM, CACHE_TYPE_DATA)) == 0, ESP_ERR_INVALID_ARG, TAG, "jpeg encode bit stream is not aligned, please use jpeg_alloc_encoder_mem to malloc your buffer");
// both the input picture and output bitstream are accessed by the 2D-DMA
ESP_RETURN_ON_FALSE(jpeg_check_dma2d_buffer(encode_inbuf) && jpeg_check_dma2d_buffer(bit_stream), ESP_ERR_INVALID_ARG, TAG, "jpeg encode buffer is not 16-byte aligned or not in unencrypted PSRAM, please use jpeg_alloc_encoder_mem to malloc your buffer");
esp_err_t ret = ESP_OK;
@@ -394,15 +397,21 @@ void *jpeg_alloc_encoder_mem(size_t size, const jpeg_encode_memory_alloc_cfg_t *
For input buffer(for decoder is PSRAM write to 2DDMA), no restriction for any align (both cache writeback and requirement from 2DDMA).
*/
size_t cache_align = 0;
size_t buffer_align = 0;
esp_cache_get_alignment(MALLOC_CAP_SPIRAM, &cache_align);
if (mem_cfg->buffer_direction == JPEG_ENC_ALLOC_OUTPUT_BUFFER) {
size = JPEG_ALIGN_UP(size, cache_align);
*allocated_size = size;
return heap_caps_aligned_calloc(cache_align, 1, size, MALLOC_CAP_SPIRAM);
} else {
*allocated_size = size;
return heap_caps_calloc(1, size, MALLOC_CAP_SPIRAM);
buffer_align = MAX(cache_align, JPEG_DMA2D_BUFFER_ALIGN);
size = JPEG_ALIGN_UP(size, buffer_align);
*allocated_size = size;
// To simplify the logic, we always use the LCM of cache and 2D-DMA alignment to satisfy both requirements
void *buffer = heap_caps_aligned_calloc(buffer_align, 1, size, JPEG_SPIRAM_ALLOC_CAPS);
if (buffer == NULL) {
#if CONFIG_SPIRAM_ENC_EXEMPT
ESP_LOGE(TAG, "no mem for %zu bytes encode buffer in unencrypted PSRAM, please enlarge CONFIG_SPIRAM_ENC_EXEMPT_SIZE", size);
#else
ESP_LOGE(TAG, "no mem for %zu bytes encode buffer", size);
#endif
}
return buffer;
}
/****************************************************************
+23
View File
@@ -31,6 +31,17 @@ extern "C" {
#define JPEG_INTR_ALLOC_FLAG (ESP_INTR_FLAG_SHARED)
#define JPEG_ALIGN_UP(num, align) (((num) + ((align) - 1)) & ~((align) - 1))
// Buffers fed to the 2D-DMA must be at least 16-byte aligned.
#define JPEG_DMA2D_BUFFER_ALIGN 16
// The JPEG codec cannot work with encrypted buffer, because it deals with macro block. When an
// unencrypted PSRAM region is reserved (CONFIG_SPIRAM_ENC_EXEMPT), codec buffers
// must come from it; otherwise use normal PSRAM.
#if CONFIG_SPIRAM_ENC_EXEMPT
#define JPEG_SPIRAM_ALLOC_CAPS (MALLOC_CAP_SPIRAM_NO_ENC)
#else
#define JPEG_SPIRAM_ALLOC_CAPS (MALLOC_CAP_SPIRAM)
#endif
// Use retention link only when the target supports sleep retention and PM is enabled
#define JPEG_USE_RETENTION_LINK (CONFIG_PM_ENABLE && CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP)
@@ -251,6 +262,18 @@ esp_err_t jpeg_isr_deregister(jpeg_codec_handle_t jpeg_codec, jpeg_isr_handler_t
*/
esp_err_t jpeg_check_intr_priority(jpeg_codec_handle_t jpeg_codec, int intr_priority);
/**
* @brief Validate a user buffer that will be accessed by the 2D-DMA
*
* The buffer must be 16-byte aligned. When CONFIG_SPIRAM_ENC_EXEMPT is enabled,
* a PSRAM buffer must reside in the unencrypted carve-out, since the 2D-DMA
* cannot access encrypted PSRAM. Internal RAM buffers are always accepted.
*
* @param buffer Buffer pointer provided by the user
* @return true if the buffer can be used by the 2D-DMA, false otherwise
*/
bool jpeg_check_dma2d_buffer(const void *buffer);
/**
* @brief Create sleep retention link
*
@@ -25,6 +25,7 @@ This document covers the following sections:
- :ref:`jpeg-pixel-storage-layout` - covers color space order overview required in this JPEG decoder and encoder.
- :ref:`jpeg-thread-safety` - lists which APIs are guaranteed to be thread safe by the driver.
- :ref:`jpeg-power-management` - describes how JPEG driver would be affected by power consumption.
- :ref:`jpeg-flash-encryption` - describes how to use the JPEG codec correctly when flash/PSRAM encryption is enabled.
- :ref:`jpeg-kconfig-options` - lists the supported Kconfig options that can bring different effects to the driver.
.. _jpeg-resource-allocation:
@@ -573,6 +574,24 @@ When power management is enabled (i.e., :ref:`CONFIG_PM_ENABLE` is set), the sys
Whenever the user is decoding or encoding via JPEG (i.e., calling :cpp:func:`jpeg_encoder_process` or :cpp:func:`jpeg_decoder_process`), the driver guarantees that the power management lock is acquired by setting it to :cpp:enumerator:`esp_pm_lock_type_t::ESP_PM_CPU_FREQ_MAX`. Once the encoding or decoding is finished, the driver releases the lock and the system can enter Light-sleep.
.. _jpeg-flash-encryption:
Usage Under Encryption
^^^^^^^^^^^^^^^^^^^^^^
The JPEG codec moves data via the 2D-DMA, and the JPEG codec **cannot process encrypted data**. Therefore, when PSRAM encryption is enabled, the JPEG input/output buffers must reside in an unencrypted memory region, otherwise encoding/decoding fails.
To support the encrypted scenario, the driver does the following:
- When ``CONFIG_SPIRAM_ENC_EXEMPT`` is enabled, :cpp:func:`jpeg_alloc_decoder_mem` and :cpp:func:`jpeg_alloc_encoder_mem` allocate buffers from the unencrypted PSRAM region (``MALLOC_CAP_SPIRAM_NO_ENC``) automatically.
- The allocated buffers satisfy both the cache line alignment and the byte alignment required by the 2D-DMA.
Please note the following when using it:
1. It is recommended to always allocate buffers via :cpp:func:`jpeg_alloc_encoder_mem` / :cpp:func:`jpeg_alloc_decoder_mem` to ensure correct alignment and memory region.
2. The size of the unencrypted region is determined by ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE``. Since the JPEG buffer size depends on the image resolution and cannot be predicted automatically, configure it according to the largest image you actually process. If the region is insufficient, the allocation fails and an error log is printed, suggesting to enlarge ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE``. Also note that this value must not be greater than or equal to the actual PSRAM size, otherwise the unencrypted region is disabled.
.. _jpeg-kconfig-options:
Kconfig Options
@@ -25,6 +25,7 @@ JPEG 常用于数字图像,尤其是数码摄影图像的有损压缩。压缩
- :ref:`jpeg-pixel-storage-layout`,涵盖了 JPEG 解码器和编码器所需的颜色空间顺序。
- :ref:`jpeg-thread-safety`,列出了驱动程序能保证线程安全的 API。
- :ref:`jpeg-power-management`,描述了影响 JPEG 驱动程序功耗的因素。
- :ref:`jpeg-flash-encryption`,介绍了在 flash/PSRAM 加密场景下如何正确使用 JPEG 编解码器。
- :ref:`jpeg-kconfig-options`,列出了支持的 Kconfig 选项,可以为驱动程序带来不同的效果。
.. _jpeg-resource-allocation:
@@ -573,6 +574,24 @@ YUV420
每当用户通过 JPEG 进行解码或编码(即调用 :cpp:func:`jpeg_encoder_process` 或 :cpp:func:`jpeg_decoder_process`)时,驱动程序会将电源管理设定为 :cpp:enumerator:`esp_pm_lock_type_t::ESP_PM_CPU_FREQ_MAX`,确保获取电源管理锁。一旦编码或解码完成,驱动程序将释放锁,则系统可以进入 Light-sleep 模式。
.. _jpeg-flash-encryption:
加密场景下的使用
^^^^^^^^^^^^^^^^
JPEG 编解码器通过 2D-DMA 搬运数据,而 JPEG 编解码器 **无法处理已加密的数据**。因此在开启 PSRAM 加密时,需要让 JPEG 的输入/输出缓冲区位于非加密的内存区域,否则编解码会失败。
为支持加密场景,驱动程序做了如下处理:
- 当启用 ``CONFIG_SPIRAM_ENC_EXEMPT`` 时, :cpp:func:`jpeg_alloc_decoder_mem` 和 :cpp:func:`jpeg_alloc_encoder_mem` 会自动从非加密 PSRAM 区域(``MALLOC_CAP_SPIRAM_NO_ENC``)分配缓冲区。
- 分配的缓冲区会同时满足 cache 行对齐与 2D-DMA 的字节对齐要求。
使用时请注意:
1. 建议始终通过 :cpp:func:`jpeg_alloc_encoder_mem` / :cpp:func:`jpeg_alloc_decoder_mem` 分配缓冲区,以保证对齐与内存区域正确。
2. 非加密区的大小由 ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE`` 决定。由于 JPEG 缓冲区大小取决于图像分辨率,无法自动预测,需根据实际处理的最大图像自行配置。若该区域不足,分配会失败并打印错误日志,提示增大 ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE``;同时注意该值不能大于等于实际 PSRAM 容量,否则非加密区会被禁用。
.. _jpeg-kconfig-options:
Kconfig 选项
@@ -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);
}
@@ -0,0 +1,239 @@
# 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
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, slots=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, slots=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)
@@ -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
@@ -8,6 +8,7 @@
#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"
@@ -44,6 +45,7 @@ void app_main(void)
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);
@@ -60,6 +62,18 @@ void app_main(void)
.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
@@ -78,9 +92,8 @@ void app_main(void)
};
ESP_ERROR_CHECK(jpeg_new_encoder_engine(&encode_eng_cfg, &jpeg_handle));
printf("JPEG encoder will read the embedded raw buffer directly from flash.\n");
printf("Encoding BGR24(raw) -> JPEG...\n");
ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, esp720p_rgb_start, EXAMPLE_RGB_FRAME_SIZE,
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);
@@ -103,4 +116,5 @@ void app_main(void)
ESP_ERROR_CHECK(jpeg_del_encoder_engine(jpeg_handle));
free(encoded);
free(jpeg_buf);
free(rgb_buf);
}
@@ -101,12 +101,9 @@ def assert_jpeg_matches_golden(result_bytes: bytes, golden_path: Path) -> None:
)
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1'), indirect=['target'])
def test_jpeg_encode_example(dut: Dut) -> None:
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('JPEG encoder will read the embedded raw buffer directly from flash.')
dut.expect_exact('Encoding BGR24(raw) -> JPEG...')
dut.expect(r'Encoded JPEG size: \d+ bytes')
@@ -123,3 +120,26 @@ def test_jpeg_encode_example(dut: Dut) -> None:
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)
@@ -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