From f0bbc86ba6e95c3f791a9cf71b0ccb374ad0bbb4 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 24 Feb 2026 22:24:21 +0800 Subject: [PATCH] png --- README.md | 12 +- dependencies.lock | 38 ++- docs/z-image.md | 268 +++++++++++++++++++++ main/control_plane/src/rest_server.c | 18 +- main/control_plane/src/rest_server_print.c | 31 +-- main/domain/include/raster_tools.h | 24 +- main/domain/src/raster_tools_image_qr.c | 247 ++++++------------- main/idf_component.yml | 8 +- 8 files changed, 409 insertions(+), 237 deletions(-) create mode 100644 docs/z-image.md diff --git a/README.md b/README.md index 1ea07c6..06c91c0 100644 --- a/README.md +++ b/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:///v1/print/raster \ }' ``` -### Image print (direct JPG upload, no base64) +### Image print (direct PNG upload, no base64) ```bash curl -X POST 'http:///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:///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. diff --git a/dependencies.lock b/dependencies.lock index b8cade9..6037eb8 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -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 diff --git a/docs/z-image.md b/docs/z-image.md new file mode 100644 index 0000000..acb34d1 --- /dev/null +++ b/docs/z-image.md @@ -0,0 +1,268 @@ +# Z-Image 文生图 API 指南 + +Z-Image 是一款轻量级文生图模型,支持快速出图、中英文文本渲染,以及多种分辨率和宽高比。 + +## 快速入口 + +- 在线体验(北京): +- 在线体验(新加坡): +- 技术博客: +- 模型列表(按地域): + +## 效果示例 + +### 输入提示词 + +```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: +2. 配置环境变量: + +示例: + +```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 +``` + +## 计费与限流 + +- 模型价格与免费额度: +- 限流说明: +- 计费按成功生成的图像张数统计;失败请求不计费,也不消耗免费额度: + +## 错误码 + +错误码总览: + +## 常见问题 + +### 如何查看模型调用量? + +模型调用约 1 小时后,可在以下页面查看调用次数、成功率等指标: + +- 模型监控(北京): +- 模型监控(新加坡): +- 账单查询与成本管理: diff --git a/main/control_plane/src/rest_server.c b/main/control_plane/src/rest_server.c index 5d052bd..c2219de 100644 --- a/main/control_plane/src/rest_server.c +++ b/main/control_plane/src/rest_server.c @@ -64,7 +64,7 @@ static const char *s_web_index = " \n" "\n" " \n" - " \n" + " \n" "
\n" " \n" " \n" @@ -86,7 +86,7 @@ static const char *s_web_index = " \n" " \n" "
\n" - "
Direct JPG upload: browser sends binary JPEG; ESP32-S3 decodes/scales/thresholds then prints.
\n" + "
Direct PNG upload: browser sends binary PNG; ESP32-S3 decodes/scales/thresholds then prints.
\n" "\n" "
Tip: connect printer first, then submit print jobs.
\n" "
Ready.
\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" " \n" diff --git a/main/control_plane/src/rest_server_print.c b/main/control_plane/src/rest_server_print.c index 6667971..0a2838f 100644 --- a/main/control_plane/src/rest_server_print.c +++ b/main/control_plane/src/rest_server_print.c @@ -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, diff --git a/main/domain/include/raster_tools.h b/main/domain/include/raster_tools.h index 740c336..78fc713 100644 --- a/main/domain/include/raster_tools.h +++ b/main/domain/include/raster_tools.h @@ -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, diff --git a/main/domain/src/raster_tools_image_qr.c b/main/domain/src/raster_tools_image_qr.c index 0472f39..d85708d 100644 --- a/main/domain/src/raster_tools_image_qr.c +++ b/main/domain/src/raster_tools_image_qr.c @@ -6,13 +6,13 @@ #include #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) { diff --git a/main/idf_component.yml b/main/idf_component.yml index 2f7b737..eb66955 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -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 \ No newline at end of file + 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