This commit is contained in:
admin
2026-02-24 22:24:21 +08:00
parent 68840e6ea4
commit f0bbc86ba6
8 changed files with 409 additions and 237 deletions

View File

@@ -33,7 +33,7 @@ ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
- `GET /v1/printer/status`
- Print:
- `POST /v1/print/raster`
- `POST /v1/print/image` (direct JPEG binary upload, no base64)
- `POST /v1/print/image` (direct PNG binary upload, no base64)
- `POST /v1/print/qr`
- `POST /v1/print/text`
- `POST /v1/print/receipt`
@@ -121,7 +121,7 @@ The page provides:
- health/status check
- connect/disconnect printer
- submit a simple text print job
- upload a JPG image (binary payload) and let ESP32-S3 decode/threshold/scale/raster conversion
- upload a PNG image (binary payload) and let ESP32-S3 decode/threshold/scale/raster conversion
- view jobs list
If API key is enabled, input it in the page before invoking actions.
@@ -163,11 +163,11 @@ curl -X POST http://<esp-ip>/v1/print/raster \
}'
```
### Image print (direct JPG upload, no base64)
### Image print (direct PNG upload, no base64)
```bash
curl -X POST 'http://<esp-ip>/v1/print/image?scale_to_width=1&threshold=160&invert=0&max_height=2200&density=medium' \
-H 'Content-Type: image/jpeg' \
--data-binary @./sample.jpg
-H 'Content-Type: image/png' \
--data-binary @./sample.png
```
Request constraints:
@@ -305,7 +305,7 @@ curl -X POST http://<esp-ip>/v1/voice/session/stop
- BLE side is central/client role, not printer peripheral role.
- `base64_msb_1bpp` uses Android-compatible bit order (MSB first).
- `/v1/print/image` accepts raw `image/jpeg` bytes directly (no base64 wrapper).
- `/v1/print/image` accepts raw `image/png` bytes directly (no base64 wrapper).
- Large buffers for image upload/decode prefer PSRAM first, then fallback to internal RAM.
- `/v1/print/text` and `/v1/print/receipt` now support UTF-8 Chinese via embedded 16x16 GB2312 glyphs.
- Characters outside embedded glyph set are rendered as square fallback boxes.

View File

@@ -19,16 +19,6 @@ dependencies:
registry_url: https://components.espressif.com/
type: service
version: 1.5.4
espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.3.1
espressif/esp_websocket_client:
component_hash: c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92
dependencies:
@@ -39,6 +29,30 @@ dependencies:
registry_url: https://components.espressif.com/
type: service
version: 1.6.1
espressif/libpng:
component_hash: 8c426d10dc2bf187aafd84ef42834e1a44835ed08a293d78683eb7811d8f8c25
dependencies:
- name: idf
require: private
version: '>=5.0'
- name: espressif/zlib
registry_url: https://components.espressif.com
require: private
version: ^1.2.13
source:
registry_url: https://components.espressif.com/
type: service
version: 1.6.55
espressif/zlib:
component_hash: d901723af51f13fc8e5824f39f32239c847956e8fd951a05266588dc5cfbb9ae
dependencies:
- name: idf
require: private
version: '>=4.4'
source:
registry_url: https://components.espressif.com
type: service
version: 1.3.1
idf:
source:
type: idf
@@ -46,9 +60,9 @@ dependencies:
direct_dependencies:
- espressif/esp_audio_codec
- espressif/esp_codec_dev
- espressif/esp_jpeg
- espressif/esp_websocket_client
- espressif/libpng
- idf
manifest_hash: f8a6610cac00e11ee3cf43d68dd430fb8e5b79dd269b8576ed6f8f010cabeaca
manifest_hash: 3274b53ce89a58abbad0a03dfaff3410e35deb81305a21289606c4d2177a111b
target: esp32s3
version: 2.0.0

268
docs/z-image.md Normal file
View File

@@ -0,0 +1,268 @@
# Z-Image 文生图 API 指南
Z-Image 是一款轻量级文生图模型,支持快速出图、中英文文本渲染,以及多种分辨率和宽高比。
## 快速入口
- 在线体验(北京):<https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=z-image-turbo>
- 在线体验(新加坡):<https://modelstudio.console.aliyun.com/ap-southeast-1?tab=dashboard#/efm/model_experience_center/vision?currentTab=imageGenerate&modelId=z-image-turbo>
- 技术博客:<https://tongyi-mai.github.io/Z-Image-blog/>
- 模型列表(按地域):<https://help.aliyun.com/zh/model-studio/models#b77c038c56b7y>
## 效果示例
### 输入提示词
```text
film grain, analog film texture, soft film lighting, Kodak Portra 400 style,
cinematic grainy texture, photorealistic details, subtle noise, (film grain:1.2)。
采用近景特写镜头拍摄的东亚年轻女性,站在户外雪地中,神情带有轻微惊讶。
整体画面清新自然,主色调为蓝、白、黑。
```
### 输出图像
![Z-Image 输出示例](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6915736671/p1036890.png)
## 模型概览
| 模型名称 | 模型简介 | 输出规格 |
| --- | --- | --- |
| `z-image-turbo` | 轻量模型,快速生图 | 图像格式PNG单次固定返回 1 张;总像素范围:`[512*512, 2048*2048]` |
## 前提条件
1. 获取 API Key<https://help.aliyun.com/zh/model-studio/get-api-key>
2. 配置环境变量:<https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables>
示例:
```bash
export DASHSCOPE_API_KEY="sk-xxxxxxxx"
```
## HTTP 同步调用
- 北京地域:`POST https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`
- 新加坡地域:`POST https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`
### 请求示例
`prompt_extend=false` 时响应更快,仅返回图像与提示词;
`prompt_extend=true` 时会返回优化后的提示词和推理过程,但耗时与费用更高。
```bash
curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $DASHSCOPE_API_KEY" \
--data '{
"model": "z-image-turbo",
"input": {
"messages": [
{
"role": "user",
"content": [
{
"text": "一只坐在窗边的橘猫,午后阳光,胶片质感,写实风格。"
}
]
}
]
},
"parameters": {
"prompt_extend": false,
"size": "1120*1440"
}
}'
```
## 请求参数
### Headers
| 参数 | 类型 | 必选 | 说明 |
| --- | --- | --- | --- |
| `Content-Type` | string | 是 | 固定为 `application/json` |
| `Authorization` | string | 是 | 鉴权信息,格式:`Bearer $DASHSCOPE_API_KEY` |
### Body
| 参数路径 | 类型 | 必选 | 说明 |
| --- | --- | --- | --- |
| `model` | string | 是 | 模型名称,固定为 `z-image-turbo` |
| `input` | object | 是 | 输入对象 |
| `input.messages` | array | 是 | 消息数组,仅支持单轮输入 |
| `input.messages[0].role` | string | 是 | 固定为 `user` |
| `input.messages[0].content` | array | 是 | 内容数组,必须且仅能包含 1 个 `text` 对象 |
| `input.messages[0].content[0].text` | string | 是 | 正向提示词(中英文均可),长度上限 800 字符,超长自动截断 |
| `parameters` | object | 否 | 生成参数对象 |
| `parameters.size` | string | 否 | 输出分辨率,格式 `宽*高`,默认 `1024*1536` |
| `parameters.prompt_extend` | bool | 否 | 是否启用提示词智能改写,默认 `false` |
| `parameters.seed` | integer | 否 | 随机种子,范围 `[0, 2147483647]` |
### size 参数说明
- 总像素限制:`[512*512, 2048*2048]`
- 推荐总像素范围:`[1024*1024, 1536*1536]`
- 同一 `seed` 可提升结果稳定性,但不能保证完全一致
### 推荐分辨率
#### 总像素约 1024x1024
| 宽高比 | 分辨率 |
| --- | --- |
| 1:1 | `1024*1024` |
| 2:3 | `832*1248` |
| 3:2 | `1248*832` |
| 3:4 | `864*1152` |
| 4:3 | `1152*864` |
| 7:9 | `896*1152` |
| 9:7 | `1152*896` |
| 9:16 | `720*1280` |
| 9:21 | `576*1344` |
| 16:9 | `1280*720` |
| 21:9 | `1344*576` |
#### 总像素约 1280x1280
| 宽高比 | 分辨率 |
| --- | --- |
| 1:1 | `1280*1280` |
| 2:3 | `1024*1536` |
| 3:2 | `1536*1024` |
| 3:4 | `1104*1472` |
| 4:3 | `1472*1104` |
| 7:9 | `1120*1440` |
| 9:7 | `1440*1120` |
| 9:16 | `864*1536` |
| 9:21 | `720*1680` |
| 16:9 | `1536*864` |
| 21:9 | `1680*720` |
#### 总像素约 1536x1536
| 宽高比 | 分辨率 |
| --- | --- |
| 1:1 | `1536*1536` |
| 2:3 | `1248*1872` |
| 3:2 | `1872*1248` |
| 3:4 | `1296*1728` |
| 4:3 | `1728*1296` |
| 7:9 | `1344*1728` |
| 9:7 | `1728*1344` |
| 9:16 | `1152*2048` |
| 9:21 | `864*2016` |
| 16:9 | `2048*1152` |
| 21:9 | `2016*864` |
## 响应示例
### 成功响应
任务数据(如图像 URL仅保留 24 小时,请及时保存。
```json
{
"output": {
"choices": [
{
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": [
{
"image": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/xxx.png?Expires=xxx"
},
{
"text": "一只坐在窗边的橘猫,午后阳光,胶片质感,写实风格。"
}
],
"reasoning_content": ""
}
}
]
},
"usage": {
"width": 1120,
"height": 1440,
"image_count": 1,
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0
},
"request_id": "8a0809b4-a796-47f4-a095-394b02b62xxx"
}
```
### 失败响应
```json
{
"request_id": "a4d78a5f-655f-9639-8437-xxxxxx",
"code": "InvalidParameter",
"message": "num_images_per_prompt must be 1"
}
```
## 响应字段说明
| 参数路径 | 类型 | 说明 |
| --- | --- | --- |
| `output` | object | 任务输出信息 |
| `output.choices` | array | 模型输出数组,仅包含 1 个元素 |
| `output.choices[0].finish_reason` | string | 停止原因,正常完成为 `stop` |
| `output.choices[0].message.role` | string | 固定为 `assistant` |
| `output.choices[0].message.content[].image` | string | 生成图像 URLPNG24 小时有效) |
| `output.choices[0].message.content[].text` | string | `prompt_extend=false` 返回原提示词;`true` 返回改写后提示词 |
| `output.choices[0].message.reasoning_content` | string | 推理过程,仅 `prompt_extend=true` 时返回 |
| `usage` | object | 统计信息 |
| `usage.width` | integer | 图像宽度(像素) |
| `usage.height` | integer | 图像高度(像素) |
| `usage.image_count` | integer | 图像数量,固定为 1 |
| `usage.input_tokens` | integer | 输入 token 数,`prompt_extend=false` 时固定为 0 |
| `usage.output_tokens` | integer | 输出 token 数,`prompt_extend=false` 时固定为 0 |
| `usage.output_tokens_details.reasoning_tokens` | integer | 推理 token 数,仅 `prompt_extend=true` 时返回 |
| `usage.total_tokens` | integer | 总 token 数,`prompt_extend=false` 时固定为 0 |
| `request_id` | string | 请求唯一标识,可用于排障 |
| `code` | string | 错误码,仅失败时返回 |
| `message` | string | 错误详情,仅失败时返回 |
## 使用限制
- 图像 URL 仅保留 24 小时,请及时下载。
- 输入 `prompt` 与输出图像均经过内容安全审核,违规请求可能返回 `IPInfringementSuspect``DataInspectionFailed`
- 若业务网络禁止访问公网 OSS请将下列域名加入白名单
```text
dashscope-result-bj.oss-cn-beijing.aliyuncs.com
dashscope-result-hz.oss-cn-hangzhou.aliyuncs.com
dashscope-result-sh.oss-cn-shanghai.aliyuncs.com
dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com
dashscope-result-zjk.oss-cn-zhangjiakou.aliyuncs.com
dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com
dashscope-result-hy.oss-cn-heyuan.aliyuncs.com
dashscope-result-cd.oss-cn-chengdu.aliyuncs.com
dashscope-result-gz.oss-cn-guangzhou.aliyuncs.com
dashscope-result-wlcb-acdr-1.oss-cn-wulanchabu-acdr-1.aliyuncs.com
```
## 计费与限流
- 模型价格与免费额度:<https://help.aliyun.com/zh/model-studio/model-pricing#6713612f55h4l>
- 限流说明:<https://help.aliyun.com/zh/model-studio/rate-limit#e17ad85fd8wwl>
- 计费按成功生成的图像张数统计;失败请求不计费,也不消耗免费额度:<https://help.aliyun.com/zh/model-studio/new-free-quota>
## 错误码
错误码总览:<https://help.aliyun.com/zh/model-studio/error-code>
## 常见问题
### 如何查看模型调用量?
模型调用约 1 小时后,可在以下页面查看调用次数、成功率等指标:
- 模型监控(北京):<https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-telemetry>
- 模型监控(新加坡):<https://modelstudio.console.aliyuncs.com/ap-southeast-1?tab=dashboard#/model-telemetry>
- 账单查询与成本管理:<https://help.aliyun.com/zh/model-studio/bill-query-and-cost-management>

View File

@@ -64,7 +64,7 @@ static const char *s_web_index =
" </div>\n"
"\n"
" <label>Image Upload</label>\n"
" <input id='imageFile' type='file' accept='.jpg,.jpeg,image/jpeg'>\n"
" <input id='imageFile' type='file' accept='.png,image/png'>\n"
" <div class='row'>\n"
" <input id='imgThreshold' type='number' min='0' max='255' value='160' placeholder='Threshold 0-255' style='max-width:160px'>\n"
" <input id='imgMaxHeight' type='number' min='64' max='3000' value='2200' placeholder='Max height' style='max-width:160px'>\n"
@@ -86,7 +86,7 @@ static const char *s_web_index =
" </label>\n"
" <button id='btnPrintImage'>Print Image</button>\n"
" </div>\n"
" <div class='small'>Direct JPG upload: browser sends binary JPEG; ESP32-S3 decodes/scales/thresholds then prints.</div>\n"
" <div class='small'>Direct PNG upload: browser sends binary PNG; ESP32-S3 decodes/scales/thresholds then prints.</div>\n"
"\n"
" <div class='small'>Tip: connect printer first, then submit print jobs.</div>\n"
" <pre id='out'>Ready.</pre>\n"
@@ -130,19 +130,19 @@ static const char *s_web_index =
"\n"
" function buildImageUploadRequest(file) {\n"
" if (!file) {\n"
" throw new Error('please select a jpg image first');\n"
" throw new Error('please select a png image first');\n"
" }\n"
" const name = (file.name || '').toLowerCase();\n"
" const type = (file.type || '').toLowerCase();\n"
" const isJpeg = type.includes('jpeg') || name.endsWith('.jpg') || name.endsWith('.jpeg');\n"
" if (!isJpeg) {\n"
" throw new Error('only .jpg/.jpeg is supported in direct upload mode');\n"
" const isPng = type.includes('png') || name.endsWith('.png');\n"
" if (!isPng) {\n"
" throw new Error('only .png is supported in direct upload mode');\n"
" }\n"
" if (file.size <= 0) {\n"
" throw new Error('empty image file');\n"
" }\n"
" if (file.size > 3 * 1024 * 1024) {\n"
" throw new Error('jpg too large (>3MB), backend limit is 4MB');\n"
" throw new Error('png too large (>3MB), backend limit is 4MB');\n"
" }\n"
"\n"
" const densityMap = {\n"
@@ -162,7 +162,7 @@ static const char *s_web_index =
" return {\n"
" path: '/v1/print/image?' + q.toString(),\n"
" body: file,\n"
" contentType: file.type || 'image/jpeg'\n"
" contentType: file.type || 'image/png'\n"
" };\n"
" }\n"
"\n"
@@ -229,7 +229,7 @@ static const char *s_web_index =
" const file = imageFileEl.files && imageFileEl.files[0] ? imageFileEl.files[0] : null;\n"
" const req = buildImageUploadRequest(file);\n"
" const result = await callBinaryApi(req.path, req.body, req.contentType);\n"
" result.client_note = 'uploaded jpg bytes: ' + file.size;\n"
" result.client_note = 'uploaded png bytes: ' + file.size;\n"
" return result;\n"
" });\n"
" </script>\n"

View File

@@ -349,7 +349,10 @@ esp_err_t rest_server_print_image_post(httpd_req_t *req) {
char content_type[96] = {0};
if (httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)) == ESP_OK) {
if (strstr(content_type, "application/json") != NULL) {
return rest_server_send_error(req, "400 Bad Request", "use binary jpeg body, not json");
return rest_server_send_error(req, "400 Bad Request", "use binary png body, not json");
}
if (strstr(content_type, "image/png") == NULL) {
return rest_server_send_error(req, "415 Unsupported Media Type", "Content-Type must be image/png");
}
}
@@ -376,23 +379,23 @@ esp_err_t rest_server_print_image_post(httpd_req_t *req) {
uint8_t *raster = NULL;
size_t raster_len = 0;
char decode_err[128] = {0};
esp_err_t decode_rc = raster_tools_convert_jpeg_to_raster_384((const uint8_t *)body,
(size_t)req->content_len,
scale_to_width,
threshold,
invert,
max_height,
&width,
&height,
&raster,
&raster_len,
decode_err,
sizeof(decode_err));
esp_err_t decode_rc = raster_tools_convert_png_to_raster_384((const uint8_t *)body,
(size_t)req->content_len,
scale_to_width,
threshold,
invert,
max_height,
&width,
&height,
&raster,
&raster_len,
decode_err,
sizeof(decode_err));
free(body);
if (decode_rc != ESP_OK) {
return rest_server_send_error(req,
"400 Bad Request",
decode_err[0] != '\0' ? decode_err : "decode jpeg failed");
decode_err[0] != '\0' ? decode_err : "decode png failed");
}
esp_err_t submit_rc = submit_raster_job_and_reply(req,

View File

@@ -31,18 +31,18 @@ esp_err_t raster_tools_convert_gray8_to_raster_384(const uint8_t *gray,
char *err,
size_t err_len);
esp_err_t raster_tools_convert_jpeg_to_raster_384(const uint8_t *jpeg,
size_t jpeg_len,
bool scale_to_width,
uint8_t threshold,
bool invert,
uint16_t max_height,
uint16_t *out_width,
uint16_t *out_height,
uint8_t **out_raster,
size_t *out_len,
char *err,
size_t err_len);
esp_err_t raster_tools_convert_png_to_raster_384(const uint8_t *png,
size_t png_len,
bool scale_to_width,
uint8_t threshold,
bool invert,
uint16_t max_height,
uint16_t *out_width,
uint16_t *out_height,
uint8_t **out_raster,
size_t *out_len,
char *err,
size_t err_len);
esp_err_t raster_tools_render_qr_384(const char *text,
uint8_t module_scale,

View File

@@ -6,13 +6,13 @@
#include <string.h>
#include "esp_heap_caps.h"
#include "jpeg_decoder.h"
#include "png.h"
#include "qrcodegen.h"
#define RASTER_WIDTH 384
#define RASTER_BYTES_PER_ROW (RASTER_WIDTH / 8)
#define JPEG_DECODE_MAX_BYTES (2 * 1024 * 1024)
#define JPEG_DECODE_MAX_WIDTH 1600
#define PNG_DECODE_MAX_BYTES (2 * 1024 * 1024)
#define PNG_DECODE_MAX_WIDTH 1600
static void set_black(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) {
if (x >= width) {
@@ -40,47 +40,6 @@ static void *calloc_prefer_psram(size_t n, size_t size) {
return ptr;
}
static uint32_t jpeg_scale_div(esp_jpeg_image_scale_t scale) {
switch (scale) {
case JPEG_IMAGE_SCALE_1_2:
return 2;
case JPEG_IMAGE_SCALE_1_4:
return 4;
case JPEG_IMAGE_SCALE_1_8:
return 8;
case JPEG_IMAGE_SCALE_0:
default:
return 1;
}
}
static esp_jpeg_image_scale_t choose_jpeg_scale(uint16_t src_width,
uint16_t src_height,
bool scale_to_width) {
if (!scale_to_width) {
return JPEG_IMAGE_SCALE_0;
}
const esp_jpeg_image_scale_t scales[] = {
JPEG_IMAGE_SCALE_0,
JPEG_IMAGE_SCALE_1_2,
JPEG_IMAGE_SCALE_1_4,
JPEG_IMAGE_SCALE_1_8
};
for (size_t i = 0; i < sizeof(scales) / sizeof(scales[0]); ++i) {
uint32_t div = jpeg_scale_div(scales[i]);
uint32_t w = (uint32_t)src_width / div;
uint32_t h = (uint32_t)src_height / div;
size_t bytes = (size_t)w * h * 2u; /* RGB565 */
if (w > 0 && h > 0 && w <= JPEG_DECODE_MAX_WIDTH && bytes <= JPEG_DECODE_MAX_BYTES) {
return scales[i];
}
}
return JPEG_IMAGE_SCALE_1_8;
}
esp_err_t raster_tools_convert_gray8_to_raster_384(const uint8_t *gray,
uint16_t src_width,
uint16_t src_height,
@@ -177,19 +136,19 @@ esp_err_t raster_tools_convert_gray8_to_raster_384(const uint8_t *gray,
return ESP_OK;
}
esp_err_t raster_tools_convert_jpeg_to_raster_384(const uint8_t *jpeg,
size_t jpeg_len,
bool scale_to_width,
uint8_t threshold,
bool invert,
uint16_t max_height,
uint16_t *out_width,
uint16_t *out_height,
uint8_t **out_raster,
size_t *out_len,
char *err,
size_t err_len) {
if (jpeg == NULL || jpeg_len == 0 ||
esp_err_t raster_tools_convert_png_to_raster_384(const uint8_t *png,
size_t png_len,
bool scale_to_width,
uint8_t threshold,
bool invert,
uint16_t max_height,
uint16_t *out_width,
uint16_t *out_height,
uint8_t **out_raster,
size_t *out_len,
char *err,
size_t err_len) {
if (png == NULL || png_len == 0 ||
out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid args");
@@ -202,9 +161,9 @@ esp_err_t raster_tools_convert_jpeg_to_raster_384(const uint8_t *jpeg,
*out_raster = NULL;
*out_len = 0;
if (jpeg_len > UINT32_MAX) {
if (png_len > UINT32_MAX) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg too large");
snprintf(err, err_len, "png too large");
}
return ESP_ERR_INVALID_SIZE;
}
@@ -213,153 +172,81 @@ esp_err_t raster_tools_convert_jpeg_to_raster_384(const uint8_t *jpeg,
max_height = 3000;
}
esp_jpeg_image_cfg_t info_cfg = {
.indata = (uint8_t *)jpeg,
.indata_size = (uint32_t)jpeg_len,
.out_format = JPEG_IMAGE_FORMAT_RGB565,
.out_scale = JPEG_IMAGE_SCALE_0,
};
esp_jpeg_image_output_t info = {0};
esp_err_t info_rc = esp_jpeg_get_image_info(&info_cfg, &info);
if (info_rc != ESP_OK || info.width == 0 || info.height == 0) {
png_image image;
memset(&image, 0, sizeof(image));
image.version = PNG_IMAGE_VERSION;
if (!png_image_begin_read_from_memory(&image, png, png_len) ||
image.width == 0 || image.height == 0) {
png_image_free(&image);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid jpeg");
snprintf(err, err_len, "invalid png");
}
return ESP_FAIL;
}
esp_jpeg_image_scale_t decode_scale = choose_jpeg_scale(info.width, info.height, scale_to_width);
uint32_t scale_div = jpeg_scale_div(decode_scale);
uint32_t dec_w = (uint32_t)info.width / scale_div;
uint32_t dec_h = (uint32_t)info.height / scale_div;
size_t decoded_pixels = (size_t)dec_w * dec_h;
if (decoded_pixels == 0 || decoded_pixels > (SIZE_MAX / 2u)) {
if (image.width > PNG_DECODE_MAX_WIDTH) {
png_image_free(&image);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg size overflow");
snprintf(err, err_len, "png too wide");
}
return ESP_ERR_INVALID_SIZE;
}
size_t rgb565_len = decoded_pixels * 2u;
if (rgb565_len > UINT32_MAX) {
image.format = PNG_FORMAT_GRAY;
png_alloc_size_t gray_size = PNG_IMAGE_SIZE(image);
if (gray_size == 0 || gray_size > PNG_DECODE_MAX_BYTES) {
png_image_free(&image);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg output too large");
snprintf(err, err_len, "png output too large");
}
return ESP_ERR_INVALID_SIZE;
}
uint8_t *rgb565 = (uint8_t *)alloc_prefer_psram(rgb565_len);
if (rgb565 == NULL) {
if (image.width > UINT16_MAX || image.height > UINT16_MAX) {
png_image_free(&image);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "png dimensions too large");
}
return ESP_ERR_INVALID_SIZE;
}
uint8_t *gray = (uint8_t *)alloc_prefer_psram((size_t)gray_size);
if (gray == NULL) {
png_image_free(&image);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "no memory");
}
return ESP_ERR_NO_MEM;
}
esp_jpeg_image_cfg_t decode_cfg = {
.indata = (uint8_t *)jpeg,
.indata_size = (uint32_t)jpeg_len,
.outbuf = rgb565,
.outbuf_size = (uint32_t)rgb565_len,
.out_format = JPEG_IMAGE_FORMAT_RGB565,
.out_scale = decode_scale,
};
esp_jpeg_image_output_t outimg = {0};
esp_err_t decode_rc = esp_jpeg_decode(&decode_cfg, &outimg);
if (decode_rc != ESP_OK || outimg.width == 0 || outimg.height == 0) {
free(rgb565);
png_alloc_size_t stride = PNG_IMAGE_ROW_STRIDE(image);
if (!png_image_finish_read(&image, NULL, gray, stride, NULL)) {
const char *png_err = image.message[0] != '\0' ? image.message : "png decode failed";
png_image_free(&image);
free(gray);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg decode failed");
snprintf(err, err_len, "%s", png_err);
}
return ESP_FAIL;
}
png_image_free(&image);
size_t expected_rgb565_len = (size_t)outimg.width * outimg.height * 2u;
if (outimg.output_len < expected_rgb565_len || expected_rgb565_len > rgb565_len) {
free(rgb565);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg decode size mismatch");
}
return ESP_ERR_INVALID_SIZE;
}
uint16_t src_width = outimg.width;
uint16_t src_height = outimg.height;
uint16_t dst_width = RASTER_WIDTH;
uint16_t dst_height = src_height;
if (scale_to_width) {
uint32_t scaled = ((uint32_t)src_height * RASTER_WIDTH + (src_width / 2u)) / src_width;
if (scaled == 0) {
scaled = 1;
}
if (scaled > max_height) {
scaled = max_height;
}
dst_height = (uint16_t)scaled;
} else if (src_width != RASTER_WIDTH) {
free(rgb565);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "width must be 384 when scale_to_width=false");
}
return ESP_ERR_INVALID_ARG;
}
if (dst_height == 0 || dst_height > max_height) {
free(rgb565);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid output height");
}
return ESP_ERR_INVALID_SIZE;
}
size_t raster_len = (size_t)RASTER_BYTES_PER_ROW * dst_height;
uint8_t *raster = (uint8_t *)calloc_prefer_psram(1, raster_len);
if (raster == NULL) {
free(rgb565);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "no memory");
}
return ESP_ERR_NO_MEM;
}
const uint16_t *src565 = (const uint16_t *)rgb565;
for (uint16_t y = 0; y < dst_height; ++y) {
uint16_t src_y = scale_to_width
? (uint16_t)(((uint32_t)y * src_height) / dst_height)
: y;
if (src_y >= src_height) {
src_y = (uint16_t)(src_height - 1);
}
for (uint16_t x = 0; x < RASTER_WIDTH; ++x) {
uint16_t src_x = scale_to_width
? (uint16_t)(((uint32_t)x * src_width) / RASTER_WIDTH)
: x;
if (src_x >= src_width) {
src_x = (uint16_t)(src_width - 1);
}
uint16_t p = src565[(size_t)src_y * src_width + src_x];
uint8_t r5 = (uint8_t)((p >> 11) & 0x1Fu);
uint8_t g6 = (uint8_t)((p >> 5) & 0x3Fu);
uint8_t b5 = (uint8_t)(p & 0x1Fu);
uint8_t r8 = (uint8_t)((r5 * 527u + 23u) >> 6);
uint8_t g8 = (uint8_t)((g6 * 259u + 33u) >> 6);
uint8_t b8 = (uint8_t)((b5 * 527u + 23u) >> 6);
uint8_t v = (uint8_t)((77u * r8 + 150u * g8 + 29u * b8) >> 8);
bool black = invert ? (v > threshold) : (v < threshold);
if (black) {
set_black(raster, RASTER_WIDTH, x, y);
}
}
}
free(rgb565);
*out_width = dst_width;
*out_height = dst_height;
*out_raster = raster;
*out_len = raster_len;
return ESP_OK;
esp_err_t convert_rc = raster_tools_convert_gray8_to_raster_384(gray,
(uint16_t)image.width,
(uint16_t)image.height,
scale_to_width,
threshold,
invert,
max_height,
out_width,
out_height,
out_raster,
out_len,
err,
err_len);
free(gray);
return convert_rc;
}
static enum qrcodegen_Ecc map_qr_ecc(uint8_t ecc_level) {

View File

@@ -14,7 +14,7 @@ dependencies:
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/esp_jpeg: ^1.3.1
espressif/esp_websocket_client: "^1.6.1"
espressif/esp_codec_dev: "^1.5.4"
espressif/esp_audio_codec: ^2.4.1
espressif/esp_websocket_client: ^1.6.1
espressif/esp_codec_dev: ^1.5.4
espressif/esp_audio_codec: ^2.4.1
espressif/libpng: ^1.6.55