jpg 打印

This commit is contained in:
admin
2026-02-24 18:51:53 +08:00
parent 9e141b8de0
commit e1e0ac8929
10 changed files with 535 additions and 127 deletions

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ sdkconfig*
.DS_Store
__pycache__/
.pytest_cache/
managed_components

View File

@@ -29,7 +29,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` (`base64_msb_1bpp` or `base64_gray8`)
- `POST /v1/print/image` (direct JPEG binary upload, no base64)
- `POST /v1/print/qr`
- `POST /v1/print/text`
- `POST /v1/print/receipt`
@@ -101,7 +101,7 @@ The page provides:
- health/status check
- connect/disconnect printer
- submit a simple text print job
- upload an image (gray8 payload) and let ESP32-S3 handle threshold/scale/raster conversion
- upload a JPG 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.
@@ -141,21 +141,17 @@ curl -X POST http://<esp-ip>/v1/print/raster \
}'
```
### Image print (gray8 auto-scale to 384)
### Image print (direct JPG upload, no base64)
```bash
curl -X POST http://<esp-ip>/v1/print/image \
-H 'Content-Type: application/json' \
-d '{
"width":800,
"height":600,
"encoding":"base64_gray8",
"data":"<BASE64_GRAY_BYTES>",
"scale_to_width":true,
"threshold":160,
"density":"中等"
}'
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
```
Request constraints:
- Request body limit: 4 MB
- Suggested image size: <= 3 MB when using built-in Web UI uploader
### QR print
```bash
curl -X POST http://<esp-ip>/v1/print/qr \
@@ -256,6 +252,8 @@ curl -X POST http://<esp-ip>/v1/ota/upgrade \
- 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).
- 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.
- QR encoding uses embedded `qrcodegen` (Project Nayuki C implementation).

21
dependencies.lock Normal file
View File

@@ -0,0 +1,21 @@
dependencies:
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
idf:
source:
type: idf
version: 5.5.2
direct_dependencies:
- espressif/esp_jpeg
- idf
manifest_hash: 819afd83349fcea09da843d8b32ebcda85f77da70a3c32f07fb7fb08134db918
target: esp32s3
version: 2.0.0

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='image/*'>\n"
" <input id='imageFile' type='file' accept='.jpg,.jpeg,image/jpeg'>\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'>Image bytes are uploaded as gray8; ESP32-S3 handles threshold/scale/raster processing.</div>\n"
" <div class='small'>Direct JPG upload: browser sends binary JPEG; 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"
@@ -128,92 +128,41 @@ static const char *s_web_index =
" return n;\n"
" }\n"
"\n"
" function uint8ToBase64(data) {\n"
" let out = '';\n"
" const chunkSize = 0x8000;\n"
" for (let i = 0; i < data.length; i += chunkSize) {\n"
" const chunk = data.subarray(i, i + chunkSize);\n"
" out += String.fromCharCode.apply(null, chunk);\n"
" }\n"
" return btoa(out);\n"
" }\n"
"\n"
" function loadImage(file) {\n"
" if (window.createImageBitmap) {\n"
" return window.createImageBitmap(file);\n"
" }\n"
" return new Promise((resolve, reject) => {\n"
" const url = URL.createObjectURL(file);\n"
" const img = new Image();\n"
" img.onload = () => {\n"
" URL.revokeObjectURL(url);\n"
" resolve(img);\n"
" };\n"
" img.onerror = () => {\n"
" URL.revokeObjectURL(url);\n"
" reject(new Error('image decode failed'));\n"
" };\n"
" img.src = url;\n"
" });\n"
" }\n"
"\n"
" async function buildImagePayload(file) {\n"
" function buildImageUploadRequest(file) {\n"
" if (!file) {\n"
" throw new Error('please select an image first');\n"
" throw new Error('please select a jpg 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"
" }\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"
" }\n"
"\n"
" const src = await loadImage(file);\n"
" const srcW = src.width || src.naturalWidth || 0;\n"
" const srcH = src.height || src.naturalHeight || 0;\n"
" if (srcW <= 0 || srcH <= 0) {\n"
" throw new Error('invalid image size');\n"
" }\n"
" const densityMap = {\n"
" '较淡': 'light',\n"
" '中等': 'medium',\n"
" '较浓': 'dark',\n"
" '最深': 'max'\n"
" };\n"
"\n"
" // Keep request body under 1MB limit in read_body().\n"
" const maxRawBytes = 680000;\n"
" let outW = srcW;\n"
" let outH = srcH;\n"
" const pixels = srcW * srcH;\n"
" if (pixels > maxRawBytes) {\n"
" const scale = Math.sqrt(maxRawBytes / pixels);\n"
" outW = Math.max(1, Math.floor(srcW * scale));\n"
" outH = Math.max(1, Math.floor(srcH * scale));\n"
" }\n"
"\n"
" const canvas = document.createElement('canvas');\n"
" canvas.width = outW;\n"
" canvas.height = outH;\n"
" const ctx = canvas.getContext('2d', { willReadFrequently: true });\n"
" ctx.drawImage(src, 0, 0, outW, outH);\n"
" if (typeof src.close === 'function') {\n"
" src.close();\n"
" }\n"
"\n"
" const rgba = ctx.getImageData(0, 0, outW, outH).data;\n"
" const gray = new Uint8Array(outW * outH);\n"
" for (let i = 0, j = 0; i < rgba.length; i += 4, ++j) {\n"
" const r = rgba[i];\n"
" const g = rgba[i + 1];\n"
" const b = rgba[i + 2];\n"
" gray[j] = (77 * r + 150 * g + 29 * b) >> 8;\n"
" }\n"
" const q = new URLSearchParams();\n"
" q.set('scale_to_width', imgScaleToWidthEl.checked ? '1' : '0');\n"
" q.set('threshold', String(toInt(imgThresholdEl.value, 160, 0, 255)));\n"
" q.set('invert', imgInvertEl.checked ? '1' : '0');\n"
" q.set('max_height', String(toInt(imgMaxHeightEl.value, 2200, 64, 3000)));\n"
" q.set('density', densityMap[imgDensityEl.value] || 'medium');\n"
"\n"
" return {\n"
" payload: {\n"
" width: outW,\n"
" height: outH,\n"
" encoding: 'base64_gray8',\n"
" data: uint8ToBase64(gray),\n"
" scale_to_width: !!imgScaleToWidthEl.checked,\n"
" threshold: toInt(imgThresholdEl.value, 160, 0, 255),\n"
" invert: !!imgInvertEl.checked,\n"
" max_height: toInt(imgMaxHeightEl.value, 2200, 64, 3000),\n"
" density: imgDensityEl.value || '中等'\n"
" },\n"
" srcW,\n"
" srcH,\n"
" outW,\n"
" outH\n"
" path: '/v1/print/image?' + q.toString(),\n"
" body: file,\n"
" contentType: file.type || 'image/jpeg'\n"
" };\n"
" }\n"
"\n"
@@ -230,6 +179,19 @@ static const char *s_web_index =
" return data;\n"
" }\n"
"\n"
" async function callBinaryApi(path, body, contentType) {\n"
" const h = headers(false);\n"
" h['Content-Type'] = contentType || 'application/octet-stream';\n"
" const r = await fetch(path, { method: 'POST', headers: h, body });\n"
" const text = await r.text();\n"
" let data = text;\n"
" try { data = JSON.parse(text); } catch (_) {}\n"
" if (!r.ok) {\n"
" throw { status: r.status, data };\n"
" }\n"
" return data;\n"
" }\n"
"\n"
" async function run(fn) {\n"
" try {\n"
" const data = await fn();\n"
@@ -265,12 +227,9 @@ static const char *s_web_index =
"\n"
" document.getElementById('btnPrintImage').onclick = () => run(async () => {\n"
" const file = imageFileEl.files && imageFileEl.files[0] ? imageFileEl.files[0] : null;\n"
" const converted = await buildImagePayload(file);\n"
" const result = await callApi('/v1/print/image', 'POST', converted.payload);\n"
" if (converted.srcW !== converted.outW || converted.srcH !== converted.outH) {\n"
" result.client_note = 'image resized before upload: ' +\n"
" converted.srcW + 'x' + converted.srcH + ' -> ' + converted.outW + 'x' + converted.outH;\n"
" }\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"
" return result;\n"
" });\n"
" </script>\n"

View File

@@ -6,7 +6,26 @@
#include <stdlib.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "mbedtls/base64.h"
#define REST_SERVER_MAX_BODY_BYTES (4 * 1024 * 1024)
static void *alloc_prefer_psram(size_t size) {
void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr == NULL) {
ptr = malloc(size);
}
return ptr;
}
static void *calloc_prefer_psram(size_t n, size_t size) {
void *ptr = heap_caps_calloc(n, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr == NULL) {
ptr = calloc(n, size);
}
return ptr;
}
bool rest_server_auth_ok(httpd_req_t *req) {
if (strlen(CONFIG_LYF_API_KEY) == 0) {
return true;
@@ -49,11 +68,11 @@ esp_err_t rest_server_read_body(httpd_req_t *req, char **out_body) {
*out_body = NULL;
if (req->content_len <= 0 || req->content_len > (1024 * 1024)) {
if (req->content_len <= 0 || req->content_len > REST_SERVER_MAX_BODY_BYTES) {
return ESP_ERR_INVALID_SIZE;
}
char *buf = (char *)calloc(1, req->content_len + 1);
char *buf = (char *)calloc_prefer_psram(1, (size_t)req->content_len + 1);
if (buf == NULL) {
return ESP_ERR_NO_MEM;
}
@@ -111,7 +130,7 @@ esp_err_t rest_server_base64_decode_alloc(const char *b64, uint8_t **out_raw, si
return ESP_ERR_INVALID_ARG;
}
uint8_t *buf = (uint8_t *)malloc(decoded_len);
uint8_t *buf = (uint8_t *)alloc_prefer_psram(decoded_len);
if (buf == NULL) {
return ESP_ERR_NO_MEM;
}

View File

@@ -43,6 +43,117 @@ static esp_err_t submit_raster_job_and_reply(httpd_req_t *req,
return err;
}
static bool parse_bool_text(const char *text, bool *out_value) {
if (text == NULL || out_value == NULL) {
return false;
}
if (strcmp(text, "1") == 0 || strcasecmp(text, "true") == 0 || strcasecmp(text, "yes") == 0) {
*out_value = true;
return true;
}
if (strcmp(text, "0") == 0 || strcasecmp(text, "false") == 0 || strcasecmp(text, "no") == 0) {
*out_value = false;
return true;
}
return false;
}
static const char *map_density_text(const char *text) {
if (text == NULL || text[0] == '\0') {
return "中等";
}
if (strcasecmp(text, "light") == 0 || strcmp(text, "较淡") == 0) {
return "较淡";
}
if (strcasecmp(text, "medium") == 0 || strcmp(text, "中等") == 0) {
return "中等";
}
if (strcasecmp(text, "dark") == 0 || strcmp(text, "较浓") == 0) {
return "较浓";
}
if (strcasecmp(text, "max") == 0 || strcmp(text, "最深") == 0) {
return "最深";
}
return "中等";
}
static void parse_image_upload_options(httpd_req_t *req,
bool *out_scale_to_width,
uint8_t *out_threshold,
bool *out_invert,
uint16_t *out_max_height,
const char **out_density) {
if (out_scale_to_width != NULL) {
*out_scale_to_width = true;
}
if (out_threshold != NULL) {
*out_threshold = 160;
}
if (out_invert != NULL) {
*out_invert = false;
}
if (out_max_height != NULL) {
*out_max_height = 2200;
}
if (out_density != NULL) {
*out_density = "中等";
}
if (req == NULL) {
return;
}
int qlen = httpd_req_get_url_query_len(req);
if (qlen <= 0 || qlen > 512) {
return;
}
char *query = (char *)calloc(1, (size_t)qlen + 1);
if (query == NULL) {
return;
}
if (httpd_req_get_url_query_str(req, query, (size_t)qlen + 1) != ESP_OK) {
free(query);
return;
}
char value[48] = {0};
if (httpd_query_key_value(query, "scale_to_width", value, sizeof(value)) == ESP_OK && out_scale_to_width != NULL) {
bool parsed = *out_scale_to_width;
if (parse_bool_text(value, &parsed)) {
*out_scale_to_width = parsed;
}
}
if (httpd_query_key_value(query, "invert", value, sizeof(value)) == ESP_OK && out_invert != NULL) {
bool parsed = *out_invert;
if (parse_bool_text(value, &parsed)) {
*out_invert = parsed;
}
}
if (httpd_query_key_value(query, "threshold", value, sizeof(value)) == ESP_OK && out_threshold != NULL) {
long v = strtol(value, NULL, 10);
if (v >= 0 && v <= 255) {
*out_threshold = (uint8_t)v;
}
}
if (httpd_query_key_value(query, "max_height", value, sizeof(value)) == ESP_OK && out_max_height != NULL) {
long v = strtol(value, NULL, 10);
if (v >= 64 && v <= 3000) {
*out_max_height = (uint16_t)v;
}
}
if (httpd_query_key_value(query, "density", value, sizeof(value)) == ESP_OK && out_density != NULL) {
*out_density = map_density_text(value);
}
free(query);
}
static esp_err_t decode_image_json_to_raster(cJSON *json,
uint16_t *out_width,
uint16_t *out_height,
@@ -235,40 +346,53 @@ esp_err_t rest_server_print_image_post(httpd_req_t *req) {
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
}
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");
}
}
bool scale_to_width = true;
uint8_t threshold = 160;
bool invert = false;
uint16_t max_height = 2200;
const char *density = "中等";
parse_image_upload_options(req,
&scale_to_width,
&threshold,
&invert,
&max_height,
&density);
char *body = NULL;
esp_err_t body_err = rest_server_read_body(req, &body);
if (body_err != ESP_OK) {
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
}
cJSON *json = cJSON_Parse(body);
free(body);
if (json == NULL) {
return rest_server_send_error(req, "400 Bad Request", "invalid json");
}
cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL)
? jdensity->valuestring
: "中等";
uint16_t width = 0;
uint16_t height = 0;
uint8_t *raster = NULL;
size_t raster_len = 0;
char decode_err[128] = {0};
esp_err_t decode_rc = decode_image_json_to_raster(json,
&width,
&height,
&raster,
&raster_len,
decode_err,
sizeof(decode_err));
cJSON_Delete(json);
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));
free(body);
if (decode_rc != ESP_OK) {
return rest_server_send_error(req,
"400 Bad Request",
decode_err[0] != '\0' ? decode_err : "decode image failed");
decode_err[0] != '\0' ? decode_err : "decode jpeg failed");
}
esp_err_t submit_rc = submit_raster_job_and_reply(req,

View File

@@ -31,6 +31,19 @@ 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_render_qr_384(const char *text,
uint8_t module_scale,
uint8_t margin_modules,

View File

@@ -5,10 +5,14 @@
#include <stdlib.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "jpeg_decoder.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
static void set_black(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) {
if (x >= width) {
@@ -20,6 +24,63 @@ static void set_black(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) {
buf[idx] |= (uint8_t)(1u << bit);
}
static void *alloc_prefer_psram(size_t size) {
void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr == NULL) {
ptr = malloc(size);
}
return ptr;
}
static void *calloc_prefer_psram(size_t n, size_t size) {
void *ptr = heap_caps_calloc(n, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr == NULL) {
ptr = calloc(n, 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,
@@ -77,7 +138,7 @@ esp_err_t raster_tools_convert_gray8_to_raster_384(const uint8_t *gray,
}
size_t len = (size_t)RASTER_BYTES_PER_ROW * dst_height;
uint8_t *raster = (uint8_t *)calloc(1, len);
uint8_t *raster = (uint8_t *)calloc_prefer_psram(1, len);
if (raster == NULL) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "no memory");
@@ -116,6 +177,191 @@ 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 ||
out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid args");
}
return ESP_ERR_INVALID_ARG;
}
*out_width = 0;
*out_height = 0;
*out_raster = NULL;
*out_len = 0;
if (jpeg_len > UINT32_MAX) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg too large");
}
return ESP_ERR_INVALID_SIZE;
}
if (max_height == 0 || max_height > 3000) {
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) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "invalid jpeg");
}
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 (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg size overflow");
}
return ESP_ERR_INVALID_SIZE;
}
size_t rgb565_len = decoded_pixels * 2u;
if (rgb565_len > UINT32_MAX) {
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg output too large");
}
return ESP_ERR_INVALID_SIZE;
}
uint8_t *rgb565 = (uint8_t *)alloc_prefer_psram(rgb565_len);
if (rgb565 == NULL) {
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);
if (err != NULL && err_len > 0) {
snprintf(err, err_len, "jpeg decode failed");
}
return ESP_FAIL;
}
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;
}
static enum qrcodegen_Ecc map_qr_ecc(uint8_t ecc_level) {
switch (ecc_level) {
case 0:
@@ -208,7 +454,7 @@ esp_err_t raster_tools_render_qr_384(const char *text,
uint16_t out_h = (uint16_t)image_size;
size_t len = (size_t)RASTER_BYTES_PER_ROW * out_h;
uint8_t *raster = (uint8_t *)calloc(1, len);
uint8_t *raster = (uint8_t *)calloc_prefer_psram(1, len);
if (raster == NULL) {
free(temp);
free(qr);

17
main/idf_component.yml Normal file
View File

@@ -0,0 +1,17 @@
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `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

View File

@@ -11,3 +11,12 @@ CONFIG_LYF_WIFI_PASSWORD="TalkingQ123"
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
# ESP32-S3-WROOM-1-N16R8: 16MB Quad Flash + 8MB Octal PSRAM
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_TYPE_AUTO=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y