From 5bc785d091e0b124f1d7e690c999420e21f52f4a Mon Sep 17 00:00:00 2001 From: Reimanbow Date: Mon, 16 Feb 2026 19:52:32 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E3=83=98=E3=83=83=E3=83=80=E3=82=82?= =?UTF-8?q?=E7=B5=84=E3=81=BF=E7=AB=8B=E3=81=A6=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cast_reassembler.c | 151 +++++++++++++++++++++++++++- test/host/test_cast_reassembler.cpp | 104 ++++++++++++++++++- 2 files changed, 249 insertions(+), 6 deletions(-) diff --git a/src/cast_reassembler.c b/src/cast_reassembler.c index f703ed6..94943b7 100644 --- a/src/cast_reassembler.c +++ b/src/cast_reassembler.c @@ -6,9 +6,133 @@ #include #include +#include #include #include +// --------------------------------------------------------------------------- +// BMP ヘッダ生成 (RGB565用) +// --------------------------------------------------------------------------- + +// BMP with BI_BITFIELDS: FileHeader(14) + InfoHeader(40) + ColorMasks(12) = 66 bytes +#define BMP_HEADER_SIZE 66 + +#pragma pack(push, 1) +typedef struct { + // File Header (14 bytes) + uint8_t signature[2]; // "BM" + uint32_t file_size; + uint16_t reserved1; + uint16_t reserved2; + uint32_t data_offset; + // Info Header - BITMAPINFOHEADER (40 bytes) + uint32_t info_size; // 40 + int32_t width; + int32_t height; // 負値 = top-down + uint16_t planes; // 1 + uint16_t bpp; // 16 + uint32_t compression; // 3 = BI_BITFIELDS + uint32_t image_size; + int32_t x_ppm; // pixels per meter + int32_t y_ppm; + uint32_t colors_used; + uint32_t colors_important; + // Color masks (12 bytes) + uint32_t mask_r; // 0xF800 + uint32_t mask_g; // 0x07E0 + uint32_t mask_b; // 0x001F +} bmp_header_t; +#pragma pack(pop) + +/** + * @brief RGB565 生ピクセルデータに BMP ヘッダを付与した完成ファイルを生成 + * @param pixels 生ピクセルデータ + * @param pixel_len ピクセルデータのバイト数 + * @param w 画像幅 + * @param h 画像高さ + * @param out_len 出力ファイルサイズ + * @return malloc されたファイルデータ。呼び出し側が free する。失敗時 NULL + */ +static uint8_t *build_bmp_rgb565(const uint8_t *pixels, size_t pixel_len, + uint16_t w, uint16_t h, size_t *out_len) +{ + // BMP の行は4バイト境界にパディング + uint32_t row_stride = ((w * 2 + 3) / 4) * 4; + uint32_t image_size = row_stride * h; + uint32_t file_size = BMP_HEADER_SIZE + image_size; + + uint8_t *buf = (uint8_t *)malloc(file_size); + if (!buf) return NULL; + memset(buf, 0, file_size); + + bmp_header_t *bmp = (bmp_header_t *)buf; + bmp->signature[0] = 'B'; + bmp->signature[1] = 'M'; + bmp->file_size = file_size; + bmp->data_offset = BMP_HEADER_SIZE; + bmp->info_size = 40; + bmp->width = w; + bmp->height = -(int32_t)h; // top-down + bmp->planes = 1; + bmp->bpp = 16; + bmp->compression = 3; // BI_BITFIELDS + bmp->image_size = image_size; + bmp->mask_r = 0xF800; + bmp->mask_g = 0x07E0; + bmp->mask_b = 0x001F; + + // 行ごとにコピー(パディング考慮) + uint32_t src_row_bytes = w * 2; + for (uint16_t y = 0; y < h; y++) { + size_t src_offset = y * src_row_bytes; + size_t dst_offset = BMP_HEADER_SIZE + y * row_stride; + size_t copy_len = src_row_bytes; + if (src_offset + copy_len > pixel_len) { + copy_len = (src_offset < pixel_len) ? (pixel_len - src_offset) : 0; + } + if (copy_len > 0) { + memcpy(buf + dst_offset, pixels + src_offset, copy_len); + } + } + + *out_len = file_size; + return buf; +} + +// --------------------------------------------------------------------------- +// PGM ヘッダ生成 (Grayscale用) +// --------------------------------------------------------------------------- + +/** + * @brief Grayscale 生ピクセルデータに PGM (P5) ヘッダを付与した完成ファイルを生成 + * @param pixels 生ピクセルデータ + * @param pixel_len ピクセルデータのバイト数 + * @param w 画像幅 + * @param h 画像高さ + * @param out_len 出力ファイルサイズ + * @return malloc されたファイルデータ。呼び出し側が free する。失敗時 NULL + */ +static uint8_t *build_pgm_grayscale(const uint8_t *pixels, size_t pixel_len, + uint16_t w, uint16_t h, size_t *out_len) +{ + // PGM ヘッダ: "P5\n{w} {h}\n255\n" + char hdr[32]; + int hdr_len = snprintf(hdr, sizeof(hdr), "P5\n%u %u\n255\n", w, h); + + size_t expected_pixels = (size_t)w * h; + size_t copy_len = (pixel_len < expected_pixels) ? pixel_len : expected_pixels; + size_t file_size = (size_t)hdr_len + copy_len; + + uint8_t *buf = (uint8_t *)malloc(file_size); + if (!buf) return NULL; + + memcpy(buf, hdr, hdr_len); + memcpy(buf + hdr_len, pixels, copy_len); + + *out_len = file_size; + return buf; +} + /** * @brief 受信機内部で持つ情報 */ @@ -101,7 +225,32 @@ static void cast_reassembler_push_packet(const uint8_t *data, size_t len) { if (ctx.chunks_received == ctx.total_chunks) { if (app_callback) { size_t final_size = (ctx.total_chunks - 1) * ctx.max_payload_ref + ctx.last_chunk_len; - app_callback(ctx.buffer, final_size, ctx.format, ctx.width, ctx.height); + + // フォーマットに応じてファイルヘッダを付与 + uint8_t *file_data = NULL; + size_t file_len = 0; + + switch (ctx.format) { + case CAST_FMT_RGB565: + file_data = build_bmp_rgb565(ctx.buffer, final_size, + ctx.width, ctx.height, &file_len); + break; + case CAST_FMT_GRAYSCALE: + file_data = build_pgm_grayscale(ctx.buffer, final_size, + ctx.width, ctx.height, &file_len); + break; + default: + // JPEG はそのまま + break; + } + + if (file_data) { + app_callback(file_data, file_len, ctx.format, ctx.width, ctx.height); + free(file_data); + } else { + // JPEG or ヘッダ生成失敗 → 生データをそのまま渡す + app_callback(ctx.buffer, final_size, ctx.format, ctx.width, ctx.height); + } } // callback から戻ったらミドルウェア側で解放 diff --git a/test/host/test_cast_reassembler.cpp b/test/host/test_cast_reassembler.cpp index b0635e6..f6e643c 100644 --- a/test/host/test_cast_reassembler.cpp +++ b/test/host/test_cast_reassembler.cpp @@ -142,7 +142,8 @@ TEST_F(ReassemblerTest, LoopbackExactMtu) std::vector data(max_payload); std::iota(data.begin(), data.end(), 0); - cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, 320, 240, &g_mock_transport); + // JPEG で送信(生データがそのまま返る) + cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, 0, 0, &g_mock_transport); ASSERT_EQ(g_captured_packets.size(), 1u); feed_all_packets(); @@ -348,20 +349,21 @@ TEST_F(ReassemblerTest, OutOfBoundsChunkIndexIgnored) // 4. アプリ連携系:フォーマット・完了通知テスト // =========================================================================== -// 4-1: フォーマット情報の伝搬 +// 4-1: フォーマット情報の伝搬(ヘッダ付与を含む) TEST_F(ReassemblerTest, FormatPropagation) { std::vector data(50, 0x11); - // JPEG + // JPEG: 生データがそのまま返る cast_send_frame(data.data(), data.size(), CAST_FMT_JPEG, 0, 0, &g_mock_transport); feed_all_packets(); ASSERT_EQ(g_completed_frames.size(), 1u); EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_JPEG); EXPECT_EQ(g_completed_frames[0].width, 0); EXPECT_EQ(g_completed_frames[0].height, 0); + EXPECT_EQ(g_completed_frames[0].data, data); // そのまま - // RGB565 + // RGB565: BMP ヘッダ付き g_captured_packets.clear(); g_completed_frames.clear(); cast_send_frame(data.data(), data.size(), CAST_FMT_RGB565, 320, 240, &g_mock_transport); @@ -370,8 +372,12 @@ TEST_F(ReassemblerTest, FormatPropagation) EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_RGB565); EXPECT_EQ(g_completed_frames[0].width, 320); EXPECT_EQ(g_completed_frames[0].height, 240); + // BMP signature "BM" + ASSERT_GE(g_completed_frames[0].data.size(), 2u); + EXPECT_EQ(g_completed_frames[0].data[0], 'B'); + EXPECT_EQ(g_completed_frames[0].data[1], 'M'); - // GRAYSCALE + // GRAYSCALE: PGM ヘッダ付き g_captured_packets.clear(); g_completed_frames.clear(); cast_send_frame(data.data(), data.size(), CAST_FMT_GRAYSCALE, 160, 120, &g_mock_transport); @@ -380,6 +386,94 @@ TEST_F(ReassemblerTest, FormatPropagation) EXPECT_EQ(g_completed_frames[0].fmt, CAST_FMT_GRAYSCALE); EXPECT_EQ(g_completed_frames[0].width, 160); EXPECT_EQ(g_completed_frames[0].height, 120); + // PGM signature "P5\n" + ASSERT_GE(g_completed_frames[0].data.size(), 3u); + EXPECT_EQ(g_completed_frames[0].data[0], 'P'); + EXPECT_EQ(g_completed_frames[0].data[1], '5'); + EXPECT_EQ(g_completed_frames[0].data[2], '\n'); +} + +// 4-1b: RGB565 → BMP 変換の検証 +TEST_F(ReassemblerTest, BmpHeaderRgb565) +{ + // 4x2 の RGB565 画像 = 16 bytes + const uint16_t w = 4, h = 2; + std::vector pixels(w * h * 2); + std::iota(pixels.begin(), pixels.end(), 0); + + cast_send_frame(pixels.data(), pixels.size(), CAST_FMT_RGB565, w, h, &g_mock_transport); + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + auto &frame = g_completed_frames[0]; + + // BMP ヘッダサイズ = 66 bytes + const size_t BMP_HDR = 66; + ASSERT_GE(frame.data.size(), BMP_HDR); + + // BMP signature + EXPECT_EQ(frame.data[0], 'B'); + EXPECT_EQ(frame.data[1], 'M'); + + // row_stride: ((4*2+3)/4)*4 = 8 (パディングなし) + uint32_t row_stride = ((w * 2 + 3) / 4) * 4; + uint32_t expected_file_size = BMP_HDR + row_stride * h; + EXPECT_EQ(frame.data.size(), expected_file_size); + + // ファイルサイズフィールド (offset 2, 4 bytes LE) + uint32_t file_size_field; + memcpy(&file_size_field, &frame.data[2], 4); + EXPECT_EQ(file_size_field, expected_file_size); + + // データオフセット (offset 10, 4 bytes LE) + uint32_t data_offset; + memcpy(&data_offset, &frame.data[10], 4); + EXPECT_EQ(data_offset, (uint32_t)BMP_HDR); + + // bpp (offset 28, 2 bytes LE) + uint16_t bpp; + memcpy(&bpp, &frame.data[28], 2); + EXPECT_EQ(bpp, 16); + + // ピクセルデータが BMP_HDR 以降に含まれている + // 行0のピクセルデータを確認 + for (size_t i = 0; i < w * 2; i++) { + EXPECT_EQ(frame.data[BMP_HDR + i], pixels[i]) + << "Mismatch at pixel byte " << i; + } +} + +// 4-1c: Grayscale → PGM 変換の検証 +TEST_F(ReassemblerTest, PgmHeaderGrayscale) +{ + // 4x2 の Grayscale 画像 = 8 bytes + const uint16_t w = 4, h = 2; + std::vector pixels(w * h); + std::iota(pixels.begin(), pixels.end(), 100); + + cast_send_frame(pixels.data(), pixels.size(), CAST_FMT_GRAYSCALE, w, h, &g_mock_transport); + feed_all_packets(); + + ASSERT_EQ(g_completed_frames.size(), 1u); + auto &frame = g_completed_frames[0]; + + // PGM ヘッダ: "P5\n4 2\n255\n" = 12 bytes + std::string expected_hdr = "P5\n4 2\n255\n"; + ASSERT_GE(frame.data.size(), expected_hdr.size() + pixels.size()); + + // ヘッダ部分の一致 + std::string actual_hdr(frame.data.begin(), frame.data.begin() + expected_hdr.size()); + EXPECT_EQ(actual_hdr, expected_hdr); + + // ピクセルデータの一致 + size_t offset = expected_hdr.size(); + for (size_t i = 0; i < pixels.size(); i++) { + EXPECT_EQ(frame.data[offset + i], pixels[i]) + << "Mismatch at pixel byte " << i; + } + + // 全体サイズ + EXPECT_EQ(frame.data.size(), expected_hdr.size() + pixels.size()); } // 4-2: 最終サイズの完全一致(連番データ)