png
This commit is contained in:
12
README.md
12
README.md
@@ -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.
|
||||
|
||||
@@ -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
268
docs/z-image.md
Normal 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-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 | 生成图像 URL(PNG,24 小时有效) |
|
||||
| `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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user