diff --git a/fleximg/CHANGELOG.md b/fleximg/CHANGELOG.md index 346d666..2dfb14b 100644 --- a/fleximg/CHANGELOG.md +++ b/fleximg/CHANGELOG.md @@ -10,6 +10,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added + +- **Grayscale bit-packed フォーマット** (Grayscale1/2/4 MSB/LSB) + - 1/2/4ビットのグレースケールフォーマットを6種追加 + - MSBFirst(上位ビット優先)とLSBFirst(下位ビット優先)の両方に対応 + - `isIndexed = false`, `maxPaletteSize = 0`(Index系とは独立したフォーマット) + - DDA転写・バイリニア補間をフルサポート(copyRowDDA_Bit / copyQuadDDA_Bit 共有) + - `grayscale8.h` → `grayscale.h` にリネームし、Grayscale8 + GrayscaleN を統合 + - `bit_packed_detail` ヘルパー関数群を `grayscale.h` に移動(Index/Grayscale共用) + - IndexN の `toStraight` を `grayscaleN_toStraight` に直接設定(コード共有) + - IndexN の `fromStraight` を `grayscaleN_fromStraight` への委譲ラッパーに変更 + ### Changed - **コーディングスタイル違反の包括的修正** @@ -28,6 +40,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **bit-packed unpackロジック集約 + Index8処理共通化** - パレットLUT処理を `applyPaletteLUT` 共通関数として切り出し、Index8/IndexN で共有 + +- **Index8のパレットなしフォールバックをGrayscale8に統合** + - `index8_toStraight` を削除し、Index8 Descriptorの `toStraight` に `grayscale8_toStraight` を直接設定 + - `index8_fromStraight` は `grayscale8_fromStraight` への委譲ラッパーに変更(将来のパレットマッピング拡張に備える) + - `grayscale8_fromStraight` を4ピクセルループ展開版に最適化 + - `indexN_toStraight` の委譲先を `grayscale8_toStraight` に変更 + - バイナリ上の重複コード解消、関数ポインタの共有による効率化 - `indexN_expandIndex` を末尾詰め方式に変更(チャンクバッファ不要、in-place展開) - `indexN_toStraight` を末尾詰め + `index8_toStraight` 委譲に変更 - `copyRowDDA_Bit` に ConstY 高速パス追加(バルクunpack + DDAサンプリング) diff --git a/fleximg/src/fleximg/core/format_metrics.h b/fleximg/src/fleximg/core/format_metrics.h index 41fa354..72b6907 100644 --- a/fleximg/src/fleximg/core/format_metrics.h +++ b/fleximg/src/fleximg/core/format_metrics.h @@ -44,7 +44,9 @@ namespace FormatIdx { constexpr uint_fast8_t BGR888 = 5; constexpr uint_fast8_t Alpha8 = 6; constexpr uint_fast8_t Grayscale8 = 7; + constexpr uint_fast8_t GrayscaleN = 7; // bit-packed Grayscale → Grayscale8 と共有 constexpr uint_fast8_t Index8 = 8; + constexpr uint_fast8_t IndexN = 8; // bit-packed Index → Index8 と共有 constexpr uint_fast8_t Count = 9; } diff --git a/fleximg/src/fleximg/image/pixel_format.h b/fleximg/src/fleximg/image/pixel_format.h index 675d4b5..c901f90 100644 --- a/fleximg/src/fleximg/image/pixel_format.h +++ b/fleximg/src/fleximg/image/pixel_format.h @@ -344,7 +344,7 @@ template void lut8toN(uint32_t*, const uint8_t*, size_t, const uint32_ #include "pixel_format/rgb565.h" #include "pixel_format/rgb332.h" #include "pixel_format/rgb888.h" -#include "pixel_format/grayscale8.h" +#include "pixel_format/grayscale.h" #include "pixel_format/index.h" // DDA関数(bit_packed_detail が定義された後にインクルード) @@ -375,6 +375,12 @@ inline const PixelFormatID builtinFormats[] = { PixelFormatIDs::Index2_LSB, PixelFormatIDs::Index4_MSB, PixelFormatIDs::Index4_LSB, + PixelFormatIDs::Grayscale1_MSB, + PixelFormatIDs::Grayscale1_LSB, + PixelFormatIDs::Grayscale2_MSB, + PixelFormatIDs::Grayscale2_LSB, + PixelFormatIDs::Grayscale4_MSB, + PixelFormatIDs::Grayscale4_LSB, }; inline constexpr size_t builtinFormatsCount = sizeof(builtinFormats) / sizeof(builtinFormats[0]); diff --git a/fleximg/src/fleximg/image/pixel_format/grayscale.h b/fleximg/src/fleximg/image/pixel_format/grayscale.h new file mode 100644 index 0000000..4c569f4 --- /dev/null +++ b/fleximg/src/fleximg/image/pixel_format/grayscale.h @@ -0,0 +1,401 @@ +#ifndef FLEXIMG_PIXEL_FORMAT_GRAYSCALE_H +#define FLEXIMG_PIXEL_FORMAT_GRAYSCALE_H + +// pixel_format.h からインクルードされることを前提 +// (PixelFormatDescriptor等は既に定義済み) + +namespace FLEXIMG_NAMESPACE { + +// ======================================================================== +// 組み込みフォーマット宣言 +// ======================================================================== + +namespace BuiltinFormats { + // 8-bit Grayscale + extern const PixelFormatDescriptor Grayscale8; + + // Bit-packed Grayscale formats + extern const PixelFormatDescriptor Grayscale1_MSB; + extern const PixelFormatDescriptor Grayscale1_LSB; + extern const PixelFormatDescriptor Grayscale2_MSB; + extern const PixelFormatDescriptor Grayscale2_LSB; + extern const PixelFormatDescriptor Grayscale4_MSB; + extern const PixelFormatDescriptor Grayscale4_LSB; +} + +namespace PixelFormatIDs { + inline const PixelFormatID Grayscale8 = &BuiltinFormats::Grayscale8; + + inline const PixelFormatID Grayscale1_MSB = &BuiltinFormats::Grayscale1_MSB; + inline const PixelFormatID Grayscale1_LSB = &BuiltinFormats::Grayscale1_LSB; + inline const PixelFormatID Grayscale2_MSB = &BuiltinFormats::Grayscale2_MSB; + inline const PixelFormatID Grayscale2_LSB = &BuiltinFormats::Grayscale2_LSB; + inline const PixelFormatID Grayscale4_MSB = &BuiltinFormats::Grayscale4_MSB; + inline const PixelFormatID Grayscale4_LSB = &BuiltinFormats::Grayscale4_LSB; +} + +} // namespace FLEXIMG_NAMESPACE + +// ============================================================================= +// 実装部 +// ============================================================================= +#ifdef FLEXIMG_IMPLEMENTATION + +#include "../../core/format_metrics.h" + +namespace FLEXIMG_NAMESPACE { + +// ======================================================================== +// Grayscale8: 単一輝度チャンネル ↔ RGBA8_Straight 変換 +// ======================================================================== + +// Grayscale8 → RGBA8_Straight(L → R=G=B=L, A=255) +static void grayscale8_toStraight(void* dst, const void* src, size_t pixelCount, const PixelAuxInfo*) { + FLEXIMG_FMT_METRICS(Grayscale8, ToStraight, pixelCount); + const uint8_t* s = static_cast(src); + uint8_t* d = static_cast(dst); + for (size_t i = 0; i < pixelCount; ++i) { + uint8_t lum = s[i]; + d[i*4 + 0] = lum; // R + d[i*4 + 1] = lum; // G + d[i*4 + 2] = lum; // B + d[i*4 + 3] = 255; // A + } +} + +// RGBA8_Straight → Grayscale8(BT.601 輝度計算) +static void grayscale8_fromStraight(void* dst, const void* src, size_t pixelCount, const PixelAuxInfo*) { + FLEXIMG_FMT_METRICS(Grayscale8, FromStraight, pixelCount); + const uint8_t* s = static_cast(src); + uint8_t* d = static_cast(dst); + + // BT.601: Y = 0.299*R + 0.587*G + 0.114*B + // 整数近似: (77*R + 150*G + 29*B + 128) >> 8 + + // 端数処理(1〜3ピクセル) + size_t remainder = pixelCount & 3; + while (remainder--) { + d[0] = static_cast((77 * s[0] + 150 * s[1] + 29 * s[2] + 128) >> 8); + s += 4; + d += 1; + } + + // 4ピクセル単位でループ + pixelCount >>= 2; + while (pixelCount--) { + d[0] = static_cast((77 * s[0] + 150 * s[1] + 29 * s[2] + 128) >> 8); + d[1] = static_cast((77 * s[4] + 150 * s[5] + 29 * s[6] + 128) >> 8); + d[2] = static_cast((77 * s[8] + 150 * s[9] + 29 * s[10] + 128) >> 8); + d[3] = static_cast((77 * s[12] + 150 * s[13] + 29 * s[14] + 128) >> 8); + s += 16; + d += 4; + } +} + +// ------------------------------------------------------------------------ +// Grayscale8 フォーマット定義 +// ------------------------------------------------------------------------ + +namespace BuiltinFormats { + +const PixelFormatDescriptor Grayscale8 = { + "Grayscale8", + grayscale8_toStraight, + grayscale8_fromStraight, + nullptr, // expandIndex + nullptr, // blendUnderStraight + nullptr, // siblingEndian + nullptr, // swapEndian + pixel_format::detail::copyRowDDA_1Byte, // copyRowDDA + pixel_format::detail::copyQuadDDA_1Byte, // copyQuadDDA + BitOrder::MSBFirst, + ByteOrder::Native, + 0, // maxPaletteSize + 8, // bitsPerPixel + 1, // bytesPerPixel + 1, // pixelsPerUnit + 1, // bytesPerUnit + 1, // channelCount + false, // hasAlpha + false, // isIndexed +}; + +} // namespace BuiltinFormats + +// ======================================================================== +// ビット操作ヘルパー関数(bit-packed Grayscale/Index共用) +// ======================================================================== + +namespace bit_packed_detail { + +// ======================================================================== +// unpackIndexBits: packed bytes → 8bit value array +// ======================================================================== + +template +inline void unpackIndexBits(uint8_t* dst, const uint8_t* src, size_t pixelCount, uint8_t pixelOffset = 0) { + constexpr int PixelsPerByte = 8 / BitsPerPixel; + constexpr uint8_t Mask = (1 << BitsPerPixel) - 1; + + // pixelOffsetは1バイト内でのピクセル位置 (0 - PixelsPerByte-1) + // 最初のバイトでの開始位置を調整 + size_t pixelIdx = pixelOffset; + size_t byteIdx = 0; + size_t dstIdx = 0; + + while (dstIdx < pixelCount) { + uint8_t b = src[byteIdx]; + size_t remainingInByte = static_cast(PixelsPerByte) - pixelIdx; + size_t pixelsToRead = (pixelCount - dstIdx < remainingInByte) ? (pixelCount - dstIdx) : remainingInByte; + + for (size_t j = 0; j < pixelsToRead; ++j) { + size_t bitPos = pixelIdx + j; + if constexpr (Order == BitOrder::MSBFirst) { + dst[dstIdx++] = (b >> ((PixelsPerByte - 1 - bitPos) * BitsPerPixel)) & Mask; + } else { + dst[dstIdx++] = (b >> (bitPos * BitsPerPixel)) & Mask; + } + } + + ++byteIdx; + pixelIdx = 0; // 次のバイトからは先頭から読む + } +} + +// ======================================================================== +// packIndexBits: 8bit value array → packed bytes +// ======================================================================== + +template +inline void packIndexBits(uint8_t* dst, const uint8_t* src, size_t pixelCount) { + constexpr size_t PixelsPerByte = 8 / BitsPerPixel; + constexpr uint8_t Mask = (1 << BitsPerPixel) - 1; + + size_t bytes = (pixelCount + PixelsPerByte - 1) / PixelsPerByte; + for (size_t i = 0; i < bytes; ++i) { + uint8_t b = 0; + size_t pixels_in_byte = (pixelCount >= PixelsPerByte) ? PixelsPerByte : pixelCount; + for (size_t j = 0; j < pixels_in_byte; ++j) { + if constexpr (Order == BitOrder::MSBFirst) { + b |= ((src[j] & Mask) << ((PixelsPerByte - 1 - j) * BitsPerPixel)); + } else { + b |= ((src[j] & Mask) << (j * BitsPerPixel)); + } + } + dst[i] = b; + src += PixelsPerByte; + pixelCount -= PixelsPerByte; + } +} + +// ======================================================================== +// ビット単位アクセスヘルパー(LovyanGFXスタイル) +// ======================================================================== + +// 指定座標のピクセルを bit-packed データから直接読み取り +template +inline uint8_t readPixelDirect(const uint8_t* srcData, int32_t x, int32_t y, int32_t stride) { + constexpr uint8_t Mask = (1 << BitsPerPixel) - 1; + + // ビット単位のオフセット計算 + int32_t pixelOffsetInByte = (y * stride * 8) + (x * BitsPerPixel); + int32_t byteIdx = pixelOffsetInByte >> 3; + int32_t bitPos = pixelOffsetInByte & 7; + + uint8_t byte = srcData[byteIdx]; + + if constexpr (Order == BitOrder::MSBFirst) { + // MSBFirst: 上位ビットから読む + return (byte >> (8 - bitPos - BitsPerPixel)) & Mask; + } else { + // LSBFirst: 下位ビットから読む + return (byte >> bitPos) & Mask; + } +} + +} // namespace bit_packed_detail + +// ======================================================================== +// GrayscaleN: bit-packed Grayscale ↔ RGBA8_Straight 変換 +// ======================================================================== + +// GrayscaleN → RGBA8_Straight(bit-packed → RGBA8) +// 末尾詰め方式: 出力バッファ(RGBA8=4byte/pixel)の末尾にGrayscale8データをunpackし、 +// スケーリング後に grayscale8_toStraight でin-place展開する +template +static void grayscaleN_toStraight( + void* __restrict__ dst, + const void* __restrict__ src, + size_t pixelCount, + const PixelAuxInfo* aux +) { + FLEXIMG_FMT_METRICS(GrayscaleN, ToStraight, pixelCount); + uint8_t* d = static_cast(dst); + const uint8_t* s = static_cast(src); + + // 末尾詰め: RGBA8出力(4byte/pixel)の後方にGrayscale8(1byte/pixel)をunpack + uint8_t* grayData = d + static_cast(pixelCount) * 3; + + const uint8_t pixelOffsetInByte = aux ? aux->pixelOffsetInByte : 0; + bit_packed_detail::unpackIndexBits( + grayData, s, pixelCount, pixelOffsetInByte); + + // スケーリング: GrayscaleN値(0-MaxVal) → 0-255 (Grayscale8相当) + constexpr int MaxVal = (1 << BitsPerPixel) - 1; + constexpr int Scale = 255 / MaxVal; + for (size_t i = 0; i < pixelCount; ++i) { + grayData[i] = static_cast(grayData[i] * Scale); + } + + // grayscale8_toStraight に委譲(in-place: grayData → d) + grayscale8_toStraight(dst, grayData, pixelCount, nullptr); +} + +// RGBA8_Straight → GrayscaleN(BT.601輝度計算 + 量子化 → bit-packed) +template +static void grayscaleN_fromStraight( + void* __restrict__ dst, + const void* __restrict__ src, + size_t pixelCount, + const PixelAuxInfo* +) { + FLEXIMG_FMT_METRICS(GrayscaleN, FromStraight, pixelCount); + constexpr size_t MaxPixelsPerByte = 8 / BitsPerPixel; + constexpr size_t ChunkSize = 64; + uint8_t grayBuf[ChunkSize]; + + const uint8_t* srcPtr = static_cast(src); + uint8_t* dstPtr = static_cast(dst); + + // 量子化シフト量 + constexpr int QuantizeShift = 8 - BitsPerPixel; + + size_t remaining = pixelCount; + while (remaining > 0) { + size_t chunk = (remaining < ChunkSize) ? remaining : ChunkSize; + + // BT.601 輝度計算 + 量子化 + for (size_t i = 0; i < chunk; ++i) { + uint_fast16_t r = srcPtr[i * 4 + 0]; + uint_fast16_t g = srcPtr[i * 4 + 1]; + uint_fast16_t b = srcPtr[i * 4 + 2]; + // BT.601: Y = (77*R + 150*G + 29*B + 128) >> 8 + uint8_t lum = static_cast((77 * r + 150 * g + 29 * b + 128) >> 8); + // 量子化 + grayBuf[i] = lum >> QuantizeShift; + } + + // パック + bit_packed_detail::packIndexBits(dstPtr, grayBuf, chunk); + + srcPtr += chunk * 4; + dstPtr += (chunk + MaxPixelsPerByte - 1) / MaxPixelsPerByte; + remaining -= chunk; + } +} + +// ------------------------------------------------------------------------ +// Bit-packed Grayscale Formats フォーマット定義 +// ------------------------------------------------------------------------ + +namespace BuiltinFormats { + +// Forward declarations for sibling references +extern const PixelFormatDescriptor Grayscale1_LSB; +extern const PixelFormatDescriptor Grayscale2_LSB; +extern const PixelFormatDescriptor Grayscale4_LSB; + +const PixelFormatDescriptor Grayscale1_MSB = { + "Grayscale1_MSB", + grayscaleN_toStraight<1, BitOrder::MSBFirst>, + grayscaleN_fromStraight<1, BitOrder::MSBFirst>, + nullptr, // expandIndex + nullptr, // blendUnderStraight + &Grayscale1_LSB, // siblingEndian + nullptr, // swapEndian + pixel_format::detail::copyRowDDA_Bit<1, BitOrder::MSBFirst>, // copyRowDDA + pixel_format::detail::copyQuadDDA_Bit<1, BitOrder::MSBFirst>, // copyQuadDDA + BitOrder::MSBFirst, + ByteOrder::Native, + 0, // maxPaletteSize + 1, // bitsPerPixel + 1, // bytesPerPixel + 8, // pixelsPerUnit + 1, // bytesPerUnit + 1, // channelCount + false, // hasAlpha + false, // isIndexed +}; + +const PixelFormatDescriptor Grayscale1_LSB = { + "Grayscale1_LSB", + grayscaleN_toStraight<1, BitOrder::LSBFirst>, + grayscaleN_fromStraight<1, BitOrder::LSBFirst>, + nullptr, nullptr, &Grayscale1_MSB, nullptr, + pixel_format::detail::copyRowDDA_Bit<1, BitOrder::LSBFirst>, + pixel_format::detail::copyQuadDDA_Bit<1, BitOrder::LSBFirst>, + BitOrder::LSBFirst, + ByteOrder::Native, + 0, 1, 1, 8, 1, 1, + false, false, +}; + +const PixelFormatDescriptor Grayscale2_MSB = { + "Grayscale2_MSB", + grayscaleN_toStraight<2, BitOrder::MSBFirst>, + grayscaleN_fromStraight<2, BitOrder::MSBFirst>, + nullptr, nullptr, &Grayscale2_LSB, nullptr, + pixel_format::detail::copyRowDDA_Bit<2, BitOrder::MSBFirst>, + pixel_format::detail::copyQuadDDA_Bit<2, BitOrder::MSBFirst>, + BitOrder::MSBFirst, + ByteOrder::Native, + 0, 2, 1, 4, 1, 1, + false, false, +}; + +const PixelFormatDescriptor Grayscale2_LSB = { + "Grayscale2_LSB", + grayscaleN_toStraight<2, BitOrder::LSBFirst>, + grayscaleN_fromStraight<2, BitOrder::LSBFirst>, + nullptr, nullptr, &Grayscale2_MSB, nullptr, + pixel_format::detail::copyRowDDA_Bit<2, BitOrder::LSBFirst>, + pixel_format::detail::copyQuadDDA_Bit<2, BitOrder::LSBFirst>, + BitOrder::LSBFirst, + ByteOrder::Native, + 0, 2, 1, 4, 1, 1, + false, false, +}; + +const PixelFormatDescriptor Grayscale4_MSB = { + "Grayscale4_MSB", + grayscaleN_toStraight<4, BitOrder::MSBFirst>, + grayscaleN_fromStraight<4, BitOrder::MSBFirst>, + nullptr, nullptr, &Grayscale4_LSB, nullptr, + pixel_format::detail::copyRowDDA_Bit<4, BitOrder::MSBFirst>, + pixel_format::detail::copyQuadDDA_Bit<4, BitOrder::MSBFirst>, + BitOrder::MSBFirst, + ByteOrder::Native, + 0, 4, 1, 2, 1, 1, + false, false, +}; + +const PixelFormatDescriptor Grayscale4_LSB = { + "Grayscale4_LSB", + grayscaleN_toStraight<4, BitOrder::LSBFirst>, + grayscaleN_fromStraight<4, BitOrder::LSBFirst>, + nullptr, nullptr, &Grayscale4_MSB, nullptr, + pixel_format::detail::copyRowDDA_Bit<4, BitOrder::LSBFirst>, + pixel_format::detail::copyQuadDDA_Bit<4, BitOrder::LSBFirst>, + BitOrder::LSBFirst, + ByteOrder::Native, + 0, 4, 1, 2, 1, 1, + false, false, +}; + +} // namespace BuiltinFormats + +} // namespace FLEXIMG_NAMESPACE + +#endif // FLEXIMG_IMPLEMENTATION + +#endif // FLEXIMG_PIXEL_FORMAT_GRAYSCALE_H diff --git a/fleximg/src/fleximg/image/pixel_format/grayscale8.h b/fleximg/src/fleximg/image/pixel_format/grayscale8.h deleted file mode 100644 index ba17538..0000000 --- a/fleximg/src/fleximg/image/pixel_format/grayscale8.h +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef FLEXIMG_PIXEL_FORMAT_GRAYSCALE8_H -#define FLEXIMG_PIXEL_FORMAT_GRAYSCALE8_H - -// pixel_format.h からインクルードされることを前提 -// (PixelFormatDescriptor等は既に定義済み) - -namespace FLEXIMG_NAMESPACE { - -// ======================================================================== -// 組み込みフォーマット宣言 -// ======================================================================== - -namespace BuiltinFormats { - extern const PixelFormatDescriptor Grayscale8; -} - -namespace PixelFormatIDs { - inline const PixelFormatID Grayscale8 = &BuiltinFormats::Grayscale8; -} - -} // namespace FLEXIMG_NAMESPACE - -// ============================================================================= -// 実装部 -// ============================================================================= -#ifdef FLEXIMG_IMPLEMENTATION - -#include "../../core/format_metrics.h" - -namespace FLEXIMG_NAMESPACE { - -// ======================================================================== -// Grayscale8: 単一輝度チャンネル ↔ RGBA8_Straight 変換 -// ======================================================================== - -// Grayscale8 → RGBA8_Straight(L → R=G=B=L, A=255) -static void grayscale8_toStraight(void* dst, const void* src, size_t pixelCount, const PixelAuxInfo*) { - FLEXIMG_FMT_METRICS(Grayscale8, ToStraight, pixelCount); - const uint8_t* s = static_cast(src); - uint8_t* d = static_cast(dst); - for (size_t i = 0; i < pixelCount; ++i) { - uint8_t lum = s[i]; - d[i*4 + 0] = lum; // R - d[i*4 + 1] = lum; // G - d[i*4 + 2] = lum; // B - d[i*4 + 3] = 255; // A - } -} - -// RGBA8_Straight → Grayscale8(BT.601 輝度計算) -static void grayscale8_fromStraight(void* dst, const void* src, size_t pixelCount, const PixelAuxInfo*) { - FLEXIMG_FMT_METRICS(Grayscale8, FromStraight, pixelCount); - const uint8_t* s = static_cast(src); - uint8_t* d = static_cast(dst); - for (size_t i = 0; i < pixelCount; ++i) { - // BT.601: Y = 0.299*R + 0.587*G + 0.114*B - // 整数近似: (77*R + 150*G + 29*B + 128) >> 8 - uint_fast16_t r = s[i*4 + 0]; - uint_fast16_t g = s[i*4 + 1]; - uint_fast16_t b = s[i*4 + 2]; - d[i] = static_cast((77 * r + 150 * g + 29 * b + 128) >> 8); - } -} - -// ------------------------------------------------------------------------ -// フォーマット定義 -// ------------------------------------------------------------------------ - -namespace BuiltinFormats { - -const PixelFormatDescriptor Grayscale8 = { - "Grayscale8", - grayscale8_toStraight, - grayscale8_fromStraight, - nullptr, // expandIndex - nullptr, // blendUnderStraight - nullptr, // siblingEndian - nullptr, // swapEndian - pixel_format::detail::copyRowDDA_1Byte, // copyRowDDA - pixel_format::detail::copyQuadDDA_1Byte, // copyQuadDDA - BitOrder::MSBFirst, - ByteOrder::Native, - 0, // maxPaletteSize - 8, // bitsPerPixel - 1, // bytesPerPixel - 1, // pixelsPerUnit - 1, // bytesPerUnit - 1, // channelCount - false, // hasAlpha - false, // isIndexed -}; - -} // namespace BuiltinFormats - -} // namespace FLEXIMG_NAMESPACE - -#endif // FLEXIMG_IMPLEMENTATION - -#endif // FLEXIMG_PIXEL_FORMAT_GRAYSCALE8_H diff --git a/fleximg/src/fleximg/image/pixel_format/index.h b/fleximg/src/fleximg/image/pixel_format/index.h index ac39ff7..7fc4bea 100644 --- a/fleximg/src/fleximg/image/pixel_format/index.h +++ b/fleximg/src/fleximg/image/pixel_format/index.h @@ -52,99 +52,6 @@ namespace PixelFormatIDs { namespace FLEXIMG_NAMESPACE { -// ======================================================================== -// ビット操作ヘルパー関数(bit-packed Index用) -// ======================================================================== - -namespace bit_packed_detail { - -// ======================================================================== -// unpackIndexBits: packed bytes → 8bit index array -// ======================================================================== - -template -inline void unpackIndexBits(uint8_t* dst, const uint8_t* src, size_t pixelCount, uint8_t pixelOffset = 0) { - constexpr int PixelsPerByte = 8 / BitsPerPixel; - constexpr uint8_t Mask = (1 << BitsPerPixel) - 1; - - // pixelOffsetは1バイト内でのピクセル位置 (0 - PixelsPerByte-1) - // 最初のバイトでの開始位置を調整 - size_t pixelIdx = pixelOffset; - size_t byteIdx = 0; - size_t dstIdx = 0; - - while (dstIdx < pixelCount) { - uint8_t b = src[byteIdx]; - size_t remainingInByte = static_cast(PixelsPerByte) - pixelIdx; - size_t pixelsToRead = (pixelCount - dstIdx < remainingInByte) ? (pixelCount - dstIdx) : remainingInByte; - - for (size_t j = 0; j < pixelsToRead; ++j) { - size_t bitPos = pixelIdx + j; - if constexpr (Order == BitOrder::MSBFirst) { - dst[dstIdx++] = (b >> ((PixelsPerByte - 1 - bitPos) * BitsPerPixel)) & Mask; - } else { - dst[dstIdx++] = (b >> (bitPos * BitsPerPixel)) & Mask; - } - } - - ++byteIdx; - pixelIdx = 0; // 次のバイトからは先頭から読む - } -} - -// ======================================================================== -// packIndexBits: 8bit index array → packed bytes -// ======================================================================== - -template -inline void packIndexBits(uint8_t* dst, const uint8_t* src, size_t pixelCount) { - constexpr size_t PixelsPerByte = 8 / BitsPerPixel; - constexpr uint8_t Mask = (1 << BitsPerPixel) - 1; - - size_t bytes = (pixelCount + PixelsPerByte - 1) / PixelsPerByte; - for (size_t i = 0; i < bytes; ++i) { - uint8_t b = 0; - size_t pixels_in_byte = (pixelCount >= PixelsPerByte) ? PixelsPerByte : pixelCount; - for (size_t j = 0; j < pixels_in_byte; ++j) { - if constexpr (Order == BitOrder::MSBFirst) { - b |= ((src[j] & Mask) << ((PixelsPerByte - 1 - j) * BitsPerPixel)); - } else { - b |= ((src[j] & Mask) << (j * BitsPerPixel)); - } - } - dst[i] = b; - src += PixelsPerByte; - pixelCount -= PixelsPerByte; - } -} - -// ======================================================================== -// ビット単位アクセスヘルパー(LovyanGFXスタイル) -// ======================================================================== - -// 指定座標のピクセルを bit-packed データから直接読み取り -template -inline uint8_t readPixelDirect(const uint8_t* srcData, int32_t x, int32_t y, int32_t stride) { - constexpr uint8_t Mask = (1 << BitsPerPixel) - 1; - - // ビット単位のオフセット計算 - int32_t pixelOffsetInByte = (y * stride * 8) + (x * BitsPerPixel); - int32_t byteIdx = pixelOffsetInByte >> 3; - int32_t bitPos = pixelOffsetInByte & 7; - - uint8_t byte = srcData[byteIdx]; - - if constexpr (Order == BitOrder::MSBFirst) { - // MSBFirst: 上位ビットから読む - return (byte >> (8 - bitPos - BitsPerPixel)) & Mask; - } else { - // LSBFirst: 下位ビットから読む - return (byte >> bitPos) & Mask; - } -} - -} // namespace bit_packed_detail - // ======================================================================== // 共通パレットLUT関数(__restrict__ なし、in-place安全) // ======================================================================== @@ -194,61 +101,19 @@ static void index8_expandIndex(void* __restrict__ dst, const void* __restrict__ } // ======================================================================== -// Index8: Index8 → RGBA8_Straight 変換(パレットなし時のフォールバック) -// ======================================================================== -// -// パレットが利用できない場合、インデックス値をグレースケールとして展開。 -// convertFormat内では expandIndex+パレット のパスが先に評価されるため、 -// パレットが設定されている場合はこの関数は呼ばれない。 -// - -static void index8_toStraight(void* dst, const void* src, - size_t pixelCount, const PixelAuxInfo*) { - FLEXIMG_FMT_METRICS(Index8, ToStraight, pixelCount); - const uint8_t* s = static_cast(src); - uint8_t* d = static_cast(dst); - for (size_t i = 0; i < pixelCount; ++i) { - uint8_t v = s[i]; - d[i*4 + 0] = v; // R - d[i*4 + 1] = v; // G - d[i*4 + 2] = v; // B - d[i*4 + 3] = 255; // A - } -} - -// ======================================================================== -// Index8: RGBA8_Straight → Index8 変換(BT.601 輝度抽出) +// Index8: RGBA8_Straight → Index8 変換 // ======================================================================== // // RGBカラーからインデックス値への変換。 -// パレットへの最近傍色マッチングではなく、BT.601輝度計算を使用。 -// Grayscale8のfromStraightと同一の計算式: index = (77*R + 150*G + 29*B + 128) >> 8 +// パレットなし時はBT.601輝度計算にフォールバック(grayscale8_fromStraightに委譲)。 +// 将来的にパレットへの最近傍色マッチングに拡張予定。 // static void index8_fromStraight(void* dst, const void* src, - size_t pixelCount, const PixelAuxInfo*) { + size_t pixelCount, const PixelAuxInfo* aux) { FLEXIMG_FMT_METRICS(Index8, FromStraight, pixelCount); - const uint8_t* s = static_cast(src); - uint8_t* d = static_cast(dst); - - // 端数処理(1〜3ピクセル) - size_t remainder = pixelCount & 3; - while (remainder--) { - d[0] = static_cast((77 * s[0] + 150 * s[1] + 29 * s[2] + 128) >> 8); - s += 4; - d += 1; - } - - // 4ピクセル単位でループ - pixelCount >>= 2; - while (pixelCount--) { - d[0] = static_cast((77 * s[0] + 150 * s[1] + 29 * s[2] + 128) >> 8); - d[1] = static_cast((77 * s[4] + 150 * s[5] + 29 * s[6] + 128) >> 8); - d[2] = static_cast((77 * s[8] + 150 * s[9] + 29 * s[10] + 128) >> 8); - d[3] = static_cast((77 * s[12] + 150 * s[13] + 29 * s[14] + 128) >> 8); - s += 16; - d += 4; - } + // パレットなし: グレースケール変換にフォールバック + grayscale8_fromStraight(dst, src, pixelCount, aux); } // ------------------------------------------------------------------------ @@ -259,7 +124,7 @@ namespace BuiltinFormats { const PixelFormatDescriptor Index8 = { "Index8", - index8_toStraight, // toStraight (パレットなし時のグレースケールフォールバック) + grayscale8_toStraight, // toStraight (パレットなし時はGrayscale8にフォールバック) index8_fromStraight, // fromStraight (BT.601 輝度抽出) index8_expandIndex, // expandIndex nullptr, // blendUnderStraight @@ -313,77 +178,17 @@ static void indexN_expandIndex( applyPaletteLUT(dst, indexData, pixelCount, aux); } -// 変換関数: toStraight (パレットなし時のグレースケール展開) -// 末尾詰め方式: 出力バッファ(RGBA8=4byte/pixel)の末尾にIndex8データをunpackし、 -// スケーリング後に index8_toStraight でin-place展開する -template -static void indexN_toStraight( - void* __restrict__ dst, - const void* __restrict__ src, - size_t pixelCount, - const PixelAuxInfo* aux -) { - uint8_t* d = static_cast(dst); - const uint8_t* s = static_cast(src); - - // 末尾詰め: RGBA8出力(4byte/pixel)の後方にIndex8(1byte/pixel)をunpack - uint8_t* indexData = d + static_cast(pixelCount) * 3; - - const uint8_t pixelOffsetInByte = aux ? aux->pixelOffsetInByte : 0; - bit_packed_detail::unpackIndexBits( - indexData, s, pixelCount, pixelOffsetInByte); - - // スケーリング: IndexN値(0-MaxIndex) → 0-255 (Index8相当) - constexpr int MaxIndex = (1 << BitsPerPixel) - 1; - constexpr int Scale = 255 / MaxIndex; - for (size_t i = 0; i < pixelCount; ++i) { - indexData[i] = static_cast(indexData[i] * Scale); - } - - // index8_toStraight に委譲(in-place: indexData → d) - index8_toStraight(dst, indexData, pixelCount, nullptr); -} - // 変換関数: fromStraight (RGBA8 → Index, 輝度計算 + 量子化) +// grayscaleN_fromStraight への委譲ラッパー template static void indexN_fromStraight( void* __restrict__ dst, const void* __restrict__ src, size_t pixelCount, - const PixelAuxInfo* + const PixelAuxInfo* aux ) { - constexpr size_t MaxPixelsPerByte = 8 / BitsPerPixel; - constexpr size_t ChunkSize = 64; - uint8_t indexBuf[ChunkSize]; - - const uint8_t* srcPtr = static_cast(src); - uint8_t* dstPtr = static_cast(dst); - - // 量子化シフト量 - constexpr int QuantizeShift = 8 - BitsPerPixel; - - size_t remaining = pixelCount; - while (remaining > 0) { - size_t chunk = (remaining < ChunkSize) ? remaining : ChunkSize; - - // BT.601 輝度計算 + 量子化 - for (size_t i = 0; i < chunk; ++i) { - uint_fast16_t r = srcPtr[i * 4 + 0]; - uint_fast16_t g = srcPtr[i * 4 + 1]; - uint_fast16_t b = srcPtr[i * 4 + 2]; - // BT.601: Y = (77*R + 150*G + 29*B + 128) >> 8 - uint8_t lum = static_cast((77 * r + 150 * g + 29 * b + 128) >> 8); - // 量子化 - indexBuf[i] = lum >> QuantizeShift; - } - - // パック - bit_packed_detail::packIndexBits(dstPtr, indexBuf, chunk); - - srcPtr += chunk * 4; - dstPtr += (chunk + MaxPixelsPerByte - 1) / MaxPixelsPerByte; - remaining -= chunk; - } + FLEXIMG_FMT_METRICS(IndexN, FromStraight, pixelCount); + grayscaleN_fromStraight(dst, src, pixelCount, aux); } // ------------------------------------------------------------------------ @@ -397,7 +202,7 @@ extern const PixelFormatDescriptor Index4_LSB; const PixelFormatDescriptor Index1_MSB = { "Index1_MSB", - indexN_toStraight<1, BitOrder::MSBFirst>, + grayscaleN_toStraight<1, BitOrder::MSBFirst>, indexN_fromStraight<1, BitOrder::MSBFirst>, indexN_expandIndex<1, BitOrder::MSBFirst>, nullptr, // blendUnderStraight @@ -419,7 +224,7 @@ const PixelFormatDescriptor Index1_MSB = { const PixelFormatDescriptor Index1_LSB = { "Index1_LSB", - indexN_toStraight<1, BitOrder::LSBFirst>, + grayscaleN_toStraight<1, BitOrder::LSBFirst>, indexN_fromStraight<1, BitOrder::LSBFirst>, indexN_expandIndex<1, BitOrder::LSBFirst>, nullptr, &Index1_MSB, nullptr, @@ -434,7 +239,7 @@ const PixelFormatDescriptor Index1_LSB = { const PixelFormatDescriptor Index2_MSB = { "Index2_MSB", - indexN_toStraight<2, BitOrder::MSBFirst>, + grayscaleN_toStraight<2, BitOrder::MSBFirst>, indexN_fromStraight<2, BitOrder::MSBFirst>, indexN_expandIndex<2, BitOrder::MSBFirst>, nullptr, &Index2_LSB, nullptr, @@ -449,7 +254,7 @@ const PixelFormatDescriptor Index2_MSB = { const PixelFormatDescriptor Index2_LSB = { "Index2_LSB", - indexN_toStraight<2, BitOrder::LSBFirst>, + grayscaleN_toStraight<2, BitOrder::LSBFirst>, indexN_fromStraight<2, BitOrder::LSBFirst>, indexN_expandIndex<2, BitOrder::LSBFirst>, nullptr, &Index2_MSB, nullptr, @@ -464,7 +269,7 @@ const PixelFormatDescriptor Index2_LSB = { const PixelFormatDescriptor Index4_MSB = { "Index4_MSB", - indexN_toStraight<4, BitOrder::MSBFirst>, + grayscaleN_toStraight<4, BitOrder::MSBFirst>, indexN_fromStraight<4, BitOrder::MSBFirst>, indexN_expandIndex<4, BitOrder::MSBFirst>, nullptr, &Index4_LSB, nullptr, @@ -479,7 +284,7 @@ const PixelFormatDescriptor Index4_MSB = { const PixelFormatDescriptor Index4_LSB = { "Index4_LSB", - indexN_toStraight<4, BitOrder::LSBFirst>, + grayscaleN_toStraight<4, BitOrder::LSBFirst>, indexN_fromStraight<4, BitOrder::LSBFirst>, indexN_expandIndex<4, BitOrder::LSBFirst>, nullptr, &Index4_MSB, nullptr, diff --git a/fleximg/test/grayscale_test.cpp b/fleximg/test/grayscale_test.cpp new file mode 100644 index 0000000..00eb81b --- /dev/null +++ b/fleximg/test/grayscale_test.cpp @@ -0,0 +1,388 @@ +// fleximg Grayscale format Unit Tests + +#include "doctest.h" +#include + +#define FLEXIMG_NAMESPACE fleximg +#include "fleximg/image/image_buffer.h" + +using namespace fleximg; + +// ======================================================================== +// Grayscale1 テスト +// ======================================================================== + +TEST_CASE("Grayscale1_MSB: basic toStraight conversion") { + // 1bit: 0→黒(0), 1→白(255) + uint8_t src[1] = { 0b10101010 }; // 1,0,1,0,1,0,1,0 + + uint32_t dst[8]; + + // パレットなし(Grayscaleとして変換) + convertFormat(src, PixelFormatIDs::Grayscale1_MSB, + dst, PixelFormatIDs::RGBA8_Straight, + 8, nullptr); + + // 1 → 255(白), 0 → 0(黒) + CHECK(dst[0] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[1] == 0xFF000000); // 0 → 黒 (A=255) + CHECK(dst[2] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[3] == 0xFF000000); // 0 → 黒 + CHECK(dst[4] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[5] == 0xFF000000); // 0 → 黒 + CHECK(dst[6] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[7] == 0xFF000000); // 0 → 黒 +} + +TEST_CASE("Grayscale1_LSB: basic toStraight conversion") { + uint8_t src[1] = { 0b10101010 }; // LSB: pixel 0,1,0,1,0,1,0,1 + + uint32_t dst[8]; + + convertFormat(src, PixelFormatIDs::Grayscale1_LSB, + dst, PixelFormatIDs::RGBA8_Straight, + 8, nullptr); + + CHECK(dst[0] == 0xFF000000); // 0 → 黒 + CHECK(dst[1] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[2] == 0xFF000000); // 0 → 黒 + CHECK(dst[3] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[4] == 0xFF000000); // 0 → 黒 + CHECK(dst[5] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[6] == 0xFF000000); // 0 → 黒 + CHECK(dst[7] == 0xFFFFFFFF); // 1 → 白 +} + +TEST_CASE("Grayscale1_MSB: sibling relationship") { + auto msb = PixelFormatIDs::Grayscale1_MSB; + auto lsb = PixelFormatIDs::Grayscale1_LSB; + + CHECK(msb->siblingEndian == lsb); + CHECK(lsb->siblingEndian == msb); + CHECK(msb->bitOrder == BitOrder::MSBFirst); + CHECK(lsb->bitOrder == BitOrder::LSBFirst); +} + +// ======================================================================== +// Grayscale2 テスト +// ======================================================================== + +TEST_CASE("Grayscale2_MSB: basic toStraight conversion") { + // 2bit: 0→0, 1→85, 2→170, 3→255 + // byte: [b7b6 b5b4 b3b2 b1b0] → pixel[0][1][2][3] + uint8_t src[1] = { 0b00011011 }; // 0,1,2,3 + + uint32_t dst[4]; + + convertFormat(src, PixelFormatIDs::Grayscale2_MSB, + dst, PixelFormatIDs::RGBA8_Straight, + 4, nullptr); + + // 各値のRGB: 0*85=0, 1*85=85, 2*85=170, 3*85=255 + uint8_t* p0 = reinterpret_cast(&dst[0]); + CHECK(p0[0] == 0); // R + CHECK(p0[1] == 0); // G + CHECK(p0[2] == 0); // B + CHECK(p0[3] == 255); // A + + uint8_t* p1 = reinterpret_cast(&dst[1]); + CHECK(p1[0] == 85); + CHECK(p1[1] == 85); + CHECK(p1[2] == 85); + CHECK(p1[3] == 255); + + uint8_t* p2 = reinterpret_cast(&dst[2]); + CHECK(p2[0] == 170); + CHECK(p2[1] == 170); + CHECK(p2[2] == 170); + CHECK(p2[3] == 255); + + uint8_t* p3 = reinterpret_cast(&dst[3]); + CHECK(p3[0] == 255); + CHECK(p3[1] == 255); + CHECK(p3[2] == 255); + CHECK(p3[3] == 255); +} + +TEST_CASE("Grayscale2_LSB: basic toStraight conversion") { + // LSB: byte[b7b6 b5b4 b3b2 b1b0] → pixel[3][2][1][0] + uint8_t src[1] = { 0b11100100 }; // pixel: 0,1,2,3 + + uint32_t dst[4]; + + convertFormat(src, PixelFormatIDs::Grayscale2_LSB, + dst, PixelFormatIDs::RGBA8_Straight, + 4, nullptr); + + uint8_t* p0 = reinterpret_cast(&dst[0]); + CHECK(p0[0] == 0); // pixel 0 → 0 + + uint8_t* p1 = reinterpret_cast(&dst[1]); + CHECK(p1[0] == 85); // pixel 1 → 85 + + uint8_t* p2 = reinterpret_cast(&dst[2]); + CHECK(p2[0] == 170); // pixel 2 → 170 + + uint8_t* p3 = reinterpret_cast(&dst[3]); + CHECK(p3[0] == 255); // pixel 3 → 255 +} + +TEST_CASE("Grayscale2_MSB: sibling relationship") { + auto msb = PixelFormatIDs::Grayscale2_MSB; + auto lsb = PixelFormatIDs::Grayscale2_LSB; + + CHECK(msb->siblingEndian == lsb); + CHECK(lsb->siblingEndian == msb); +} + +// ======================================================================== +// Grayscale4 テスト +// ======================================================================== + +TEST_CASE("Grayscale4_MSB: basic toStraight conversion") { + // 4bit: 0→0, 15→255 + // byte: [b7b6b5b4 b3b2b1b0] → pixel[0][1] + uint8_t src[1] = { 0x0F }; // 0, 15 + + uint32_t dst[2]; + + convertFormat(src, PixelFormatIDs::Grayscale4_MSB, + dst, PixelFormatIDs::RGBA8_Straight, + 2, nullptr); + + uint8_t* p0 = reinterpret_cast(&dst[0]); + CHECK(p0[0] == 0); // 0 → 0 + CHECK(p0[3] == 255); // A=255 + + uint8_t* p1 = reinterpret_cast(&dst[1]); + CHECK(p1[0] == 255); // 15 → 255 + CHECK(p1[3] == 255); +} + +TEST_CASE("Grayscale4_LSB: basic toStraight conversion") { + // LSB: byte[b7b6b5b4 b3b2b1b0] → pixel[1][0] + uint8_t src[1] = { 0xF0 }; // pixel 0=0, pixel 1=15 + + uint32_t dst[2]; + + convertFormat(src, PixelFormatIDs::Grayscale4_LSB, + dst, PixelFormatIDs::RGBA8_Straight, + 2, nullptr); + + uint8_t* p0 = reinterpret_cast(&dst[0]); + CHECK(p0[0] == 0); // 0 → 0 + + uint8_t* p1 = reinterpret_cast(&dst[1]); + CHECK(p1[0] == 255); // 15 → 255 +} + +TEST_CASE("Grayscale4_MSB: sibling relationship") { + auto msb = PixelFormatIDs::Grayscale4_MSB; + auto lsb = PixelFormatIDs::Grayscale4_LSB; + + CHECK(msb->siblingEndian == lsb); + CHECK(lsb->siblingEndian == msb); +} + +// ======================================================================== +// fromStraight テスト +// ======================================================================== + +TEST_CASE("Grayscale1_MSB: RGBA8 to Grayscale1 (quantization)") { + uint32_t src[8] = { + 0xFFFFFFFF, // 白 (lum=255) → 255>>7 = 1 + 0xFF000000, // 黒 (lum=0) → 0>>7 = 0 + 0xFF808080, // グレー (lum=128) → 128>>7 = 1 + 0xFF404040, // 暗灰 (lum=64) → 64>>7 = 0 + 0xFF0000FF, // 赤 (lum≈77) → 77>>7 = 0 + 0xFF00FF00, // 緑 (lum≈150) → 150>>7 = 1 + 0xFFFF0000, // 青 (lum≈29) → 29>>7 = 0 + 0xFFC0C0C0 // 明灰 (lum=192) → 192>>7 = 1 + }; + + uint8_t dst[1]; + + convertFormat(src, PixelFormatIDs::RGBA8_Straight, + dst, PixelFormatIDs::Grayscale1_MSB, + 8, nullptr); + + // 期待値: 10100101 = 0xA5 + CHECK(dst[0] == 0xA5); +} + +TEST_CASE("Grayscale2_MSB: RGBA8 to Grayscale2 (quantization)") { + uint32_t src[4] = { + 0xFF000000, // 黒 (lum=0) → 0 + 0xFF555555, // 暗灰 (lum=85) → 1 + 0xFFAAAAAA, // 明灰 (lum=170) → 2 + 0xFFFFFFFF // 白 (lum=255) → 3 + }; + + uint8_t dst[1]; + + convertFormat(src, PixelFormatIDs::RGBA8_Straight, + dst, PixelFormatIDs::Grayscale2_MSB, + 4, nullptr); + + // 期待値: 00 01 10 11 = 0x1B + CHECK(dst[0] == 0x1B); +} + +TEST_CASE("Grayscale4_MSB: RGBA8 to Grayscale4 (quantization)") { + uint32_t src[2] = { + 0xFF000000, // 黒 (lum=0) → 0 + 0xFFFFFFFF // 白 (lum=255) → 15 + }; + + uint8_t dst[1]; + + convertFormat(src, PixelFormatIDs::RGBA8_Straight, + dst, PixelFormatIDs::Grayscale4_MSB, + 2, nullptr); + + // 期待値: 0000 1111 = 0x0F + CHECK(dst[0] == 0x0F); +} + +// ======================================================================== +// Descriptor プロパティテスト +// ======================================================================== + +TEST_CASE("Grayscale format properties") { + auto g1_msb = PixelFormatIDs::Grayscale1_MSB; + auto g2_msb = PixelFormatIDs::Grayscale2_MSB; + auto g4_msb = PixelFormatIDs::Grayscale4_MSB; + + // Grayscale1 + CHECK(g1_msb->bitsPerPixel == 1); + CHECK(g1_msb->pixelsPerUnit == 8); + CHECK(g1_msb->bytesPerUnit == 1); + CHECK(g1_msb->maxPaletteSize == 0); + CHECK(g1_msb->isIndexed == false); + CHECK(g1_msb->hasAlpha == false); + CHECK(g1_msb->expandIndex == nullptr); + + // Grayscale2 + CHECK(g2_msb->bitsPerPixel == 2); + CHECK(g2_msb->pixelsPerUnit == 4); + CHECK(g2_msb->bytesPerUnit == 1); + CHECK(g2_msb->maxPaletteSize == 0); + CHECK(g2_msb->isIndexed == false); + + // Grayscale4 + CHECK(g4_msb->bitsPerPixel == 4); + CHECK(g4_msb->pixelsPerUnit == 2); + CHECK(g4_msb->bytesPerUnit == 1); + CHECK(g4_msb->maxPaletteSize == 0); + CHECK(g4_msb->isIndexed == false); +} + +// ======================================================================== +// 端数処理テスト +// ======================================================================== + +TEST_CASE("Grayscale1_MSB: partial byte (5 pixels)") { + uint8_t src[1] = { 0b10101000 }; // 1,0,1,0,1,0,0,0 (5ピクセルのみ使用) + + uint32_t dst[5]; + + convertFormat(src, PixelFormatIDs::Grayscale1_MSB, + dst, PixelFormatIDs::RGBA8_Straight, + 5, nullptr); + + CHECK(dst[0] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[1] == 0xFF000000); // 0 → 黒 + CHECK(dst[2] == 0xFFFFFFFF); // 1 → 白 + CHECK(dst[3] == 0xFF000000); // 0 → 黒 + CHECK(dst[4] == 0xFFFFFFFF); // 1 → 白 +} + +TEST_CASE("Grayscale4_MSB: partial byte (1 pixel)") { + uint8_t src[1] = { 0x80 }; // pixel 0 = 8 (upper nibble), pixel 1 = 0 + + uint32_t dst[1]; + + convertFormat(src, PixelFormatIDs::Grayscale4_MSB, + dst, PixelFormatIDs::RGBA8_Straight, + 1, nullptr); + + // 8 * 17 = 136 + uint8_t* p0 = reinterpret_cast(&dst[0]); + CHECK(p0[0] == 136); + CHECK(p0[1] == 136); + CHECK(p0[2] == 136); + CHECK(p0[3] == 255); +} + +// ======================================================================== +// getFormatByName テスト +// ======================================================================== + +TEST_CASE("getFormatByName: grayscale formats") { + CHECK(getFormatByName("Grayscale8") == PixelFormatIDs::Grayscale8); + CHECK(getFormatByName("Grayscale1_MSB") == PixelFormatIDs::Grayscale1_MSB); + CHECK(getFormatByName("Grayscale1_LSB") == PixelFormatIDs::Grayscale1_LSB); + CHECK(getFormatByName("Grayscale2_MSB") == PixelFormatIDs::Grayscale2_MSB); + CHECK(getFormatByName("Grayscale2_LSB") == PixelFormatIDs::Grayscale2_LSB); + CHECK(getFormatByName("Grayscale4_MSB") == PixelFormatIDs::Grayscale4_MSB); + CHECK(getFormatByName("Grayscale4_LSB") == PixelFormatIDs::Grayscale4_LSB); +} + +// ======================================================================== +// DDA関数テスト +// ======================================================================== + +TEST_CASE("Grayscale1_MSB: copyRowDDA function exists") { + auto fmt = PixelFormatIDs::Grayscale1_MSB; + CHECK(fmt->copyRowDDA != nullptr); + CHECK(fmt->copyQuadDDA != nullptr); +} + +TEST_CASE("Grayscale1_MSB: copyRowDDA basic sampling") { + // 8ピクセル: 10101010 + uint8_t src[1] = { 0xAA }; + uint8_t dst[4]; + + // DDAパラメータ: 等間隔サンプリング (pixel 0, 2, 4, 6) + DDAParam param = {}; + param.srcStride = 1; + param.srcWidth = 8; + param.srcHeight = 1; + param.srcX = 0; + param.srcY = 0; + param.incrX = 2 << INT_FIXED_SHIFT; + param.incrY = 0; + + auto fmt = PixelFormatIDs::Grayscale1_MSB; + fmt->copyRowDDA(dst, src, 4, ¶m); + + // pixel[0,2,4,6] = 1,1,1,1 + CHECK(dst[0] == 1); + CHECK(dst[1] == 1); + CHECK(dst[2] == 1); + CHECK(dst[3] == 1); +} + +// ======================================================================== +// Grayscale8 テスト(既存フォーマットの確認) +// ======================================================================== + +TEST_CASE("Grayscale8: roundtrip conversion") { + // Grayscale8 → RGBA8 → Grayscale8 のラウンドトリップ + uint8_t src[4] = { 0, 85, 170, 255 }; + uint32_t rgba[4]; + uint8_t dst[4]; + + convertFormat(src, PixelFormatIDs::Grayscale8, + rgba, PixelFormatIDs::RGBA8_Straight, + 4, nullptr); + + convertFormat(rgba, PixelFormatIDs::RGBA8_Straight, + dst, PixelFormatIDs::Grayscale8, + 4, nullptr); + + CHECK(dst[0] == 0); + CHECK(dst[1] == 85); + CHECK(dst[2] == 170); + CHECK(dst[3] == 255); +}