From e3b09bd0b7b63fa36772325e47efbf16c3e87caf Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 01:56:49 +0100 Subject: [PATCH] Support JPEG and PNG attachment metadata and preparation --- src/media_async/attachment.c | 11 ++++++++--- src/media_async/attachment.h | 12 ++++++------ src/media_async/attachment_send.c | 13 ++++++++++--- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/media_async/attachment.c b/src/media_async/attachment.c index a4f0f1a2..5b53f444 100644 --- a/src/media_async/attachment.c +++ b/src/media_async/attachment.c @@ -38,14 +38,17 @@ int attachment_validate(const struct attachment_info* info) { if (wave) goto invalid; if (info->kind == ATTACHMENT_FILE) { if (info->duration_ms || info->width || info->height) goto invalid; - } else if (info->kind == ATTACHMENT_VIDEO) { + } else if (info->kind == ATTACHMENT_VIDEO || info->kind == ATTACHMENT_JPEG || info->kind == ATTACHMENT_PNG) { if (!info->width || !info->height || info->width > 16384 || info->height > 16384) goto invalid; - if (!info->duration_ms || info->duration_ms > ATTACHMENT_DURATION_MAX_MS) goto invalid; + if (info->kind == ATTACHMENT_VIDEO) { + if (!info->duration_ms || info->duration_ms > ATTACHMENT_DURATION_MAX_MS) goto invalid; + } else if (info->duration_ms) goto invalid; } else goto invalid; } return 0; invalid: - DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: invalid metadata kind=%d", info ? (int)info->kind : 0); + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: invalid metadata kind=%d dimensions=%ux%u duration=%u", + info ? (int)info->kind : 0, info ? info->width : 0, info ? info->height : 0, info ? info->duration_ms : 0); return -1; } @@ -81,6 +84,8 @@ const char* attachment_content_type(enum attachment_kind kind) { switch (kind) { case ATTACHMENT_VOICE: return "audio/opus"; case ATTACHMENT_VIDEO: return "video/mp4"; + case ATTACHMENT_JPEG: return "image/jpeg"; + case ATTACHMENT_PNG: return "image/png"; default: return "application/octet-stream"; } } diff --git a/src/media_async/attachment.h b/src/media_async/attachment.h index 1f9d019b..cfca3da9 100644 --- a/src/media_async/attachment.h +++ b/src/media_async/attachment.h @@ -1,4 +1,4 @@ -/* attachment — общие метаданные file/voice/video, без хранения файла, адресата и транспорта. +/* attachment — общие метаданные file/voice/video/JPEG/PNG, без хранения файла, адресата и транспорта. * validate/decode возвращают 0 / -1, encode — число записанных байтов / -1. * Формат: kind:1, name_len:2, duration_ms:4, width:2, height:2, UTF-8 basename, waveform:100 только для voice. * Числа big-endian; имя — корректный UTF-8 basename длиной 1..255 байт, без '/'/'\\', ASCII C0/DEL и '.'/'..'. @@ -12,15 +12,15 @@ extern "C" { #endif -enum attachment_kind { ATTACHMENT_FILE = 1, ATTACHMENT_VOICE, ATTACHMENT_VIDEO }; +enum attachment_kind { ATTACHMENT_FILE = 1, ATTACHMENT_VOICE, ATTACHMENT_VIDEO, ATTACHMENT_JPEG, ATTACHMENT_PNG }; #define ATTACHMENT_WAVE_SIZE 100 #define ATTACHMENT_WIRE_MAX (11 + 255 + ATTACHMENT_WAVE_SIZE) #define ATTACHMENT_DURATION_MAX_MS 86400000u /* Общий максимум длительности voice/video: 24ч. */ struct attachment_info { - enum attachment_kind kind; /* Только FILE/VOICE/VIDEO. */ + enum attachment_kind kind; /* FILE/VOICE/VIDEO/JPEG/PNG; формат изображения не зависит от имени. */ char name[256]; /* UTF-8 basename с завершающим NUL. */ - uint32_t duration_ms; /* VOICE/VIDEO: 1..ATTACHMENT_DURATION_MAX_MS; FILE: 0. */ - uint16_t width, height; /* VIDEO: 1..16384; FILE/VOICE: 0. */ + uint32_t duration_ms; /* VOICE/VIDEO: 1..ATTACHMENT_DURATION_MAX_MS; остальные: 0. */ + uint16_t width, height; /* VIDEO/JPEG/PNG: 1..16384, изображения с учётом EXIF; FILE/VOICE: 0. */ char waveform[ATTACHMENT_WAVE_SIZE + 1]; /* 100 уровней из A-Za-z0-9+/; только voice. */ }; /* Проверить все поля и их согласованность с kind; info не меняется, NULL даёт -1. */ @@ -31,7 +31,7 @@ int attachment_encode(const struct attachment_info* info, uint8_t* out, size_t c /* Разобрать ровно length байт и проверить метаданные; data/info обязательны. 0/-1. * При ошибке info может быть изменена: результат используется только при возврате 0. */ int attachment_decode(const uint8_t* data, size_t length, struct attachment_info* info); -/* Заимствованная постоянная строка: VOICE=audio/opus, VIDEO=video/mp4, иначе application/octet-stream. Не валидатор kind. */ +/* Постоянный MIME по kind: audio/opus, video/mp4, image/jpeg, image/png либо application/octet-stream. Не валидатор kind. */ const char* attachment_content_type(enum attachment_kind kind); #ifdef __cplusplus diff --git a/src/media_async/attachment_send.c b/src/media_async/attachment_send.c index 7724211f..bc0483ec 100644 --- a/src/media_async/attachment_send.c +++ b/src/media_async/attachment_send.c @@ -182,14 +182,21 @@ void attachment_send_trampoline(void* arg) { for (unsigned i = 0; i < 16; i++) snprintf(hex + 2 * i, 3, "%02x", uuid[i]); memcpy(req->id, uuid, sizeof(req->id)); const char* base = req->inst->config->global.db_path; + const char* extension = "file"; + switch (req->info.kind) { + case ATTACHMENT_VOICE: extension = "opus"; break; + case ATTACHMENT_VIDEO: extension = "mp4"; break; + case ATTACHMENT_JPEG: extension = "jpg"; break; + case ATTACHMENT_PNG: extension = "png"; break; + default: break; + } int n = snprintf(req->output, sizeof(req->output), "%s/%s/%s/%s.%s", base[0] ? base : ".", req->encrypted && !req->is_dm ? "encrypted-prepared" : "media", - req->is_dm ? "pm-prepared" : req->target, hex, req->info.kind == ATTACHMENT_VOICE ? "opus" : "mp4"); + req->is_dm ? "pm-prepared" : req->target, hex, extension); if (n < 0 || (size_t)n >= sizeof(req->output)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: output path too long"); req->output[0] = 0; attachment_publish(req, -1); return; } - if (!req->info.name[0]) snprintf(req->info.name, sizeof(req->info.name), "%s.%s", hex, - req->info.kind == ATTACHMENT_VOICE ? "opus" : "mp4"); + if (!req->info.name[0]) snprintf(req->info.name, sizeof(req->info.name), "%s.%s", hex, extension); if (req->is_dm) { if (dm_media_file_hold(req->inst, req->target, req->dm_generation, req->output)) { attachment_publish(req, -1); return; } req->dm_paths_held |= 1;