Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions fleximg/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **WebUI: Grayscale1/2/4 フォーマット選択 + optgroup 分類**
- フォーマット選択ドロップダウンに Grayscale1/2/4 MSB/LSB の6フォーマットを追加
- `<optgroup>` によるカテゴリ分類(RGB / Grayscale / Alpha / Index)でUI整理
- `buildFormatOptions()` ヘルパー関数で SourceNode / SinkNode の select 構築を共通化

- **Grayscale bit-packed フォーマット** (Grayscale1/2/4 MSB/LSB)
- 1/2/4ビットのグレースケールフォーマットを6種追加
- MSBFirst(上位ビット優先)とLSBFirst(下位ビット優先)の両方に対応
Expand Down Expand Up @@ -98,6 +103,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- bgの非アフィン状態で先頭行の内容が全ラインに適用されるバグを修正
- 背景コピーループの行アドレス計算に `bgViewPort.x`, `bgViewPort.y` オフセットを加算

- **WebUI: ビットパック形式のストライド不整合による画像歪みを修正**
- `bindings.cpp` でビットパック形式(Gray1/2/4, Index1/2/4)の変換が全ピクセルを1Dストリームとして処理していたため、`width % pixelsPerUnit != 0` の場合に行境界でパディングされず画像が歪んでいた
- `calcStride()` / `convertFormatRowByRow()` ヘルパー関数を追加し、全変換箇所(7箇所)を行単位変換に修正
- `ImageStore::store()` / `allocate()` のバッファサイズ計算も `stride * height` に修正

- **bit-packed format の pixelOffsetInByte サポート**
- CompositeNode経由でbit-packed(Index1/2/4)データを処理する際のチャンク境界でのオフセットずれを修正
- `PixelAuxInfo::pixelOffsetInByte` フィールド追加(1バイト内でのピクセル位置 0 - PixelsPerByte-1)
Expand Down
128 changes: 66 additions & 62 deletions fleximg/demo/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,33 @@ struct SinkOutput {
int height = 0;
};

// ========================================================================
// ビットパック形式用ヘルパー関数
// ========================================================================

// 1行あたりのバイト数(stride)を計算
static int32_t calcStride(int width, PixelFormatID fmt) {
if (fmt && fmt->pixelsPerUnit > 1) {
int units = (width + fmt->pixelsPerUnit - 1) / fmt->pixelsPerUnit;
return units * fmt->bytesPerUnit;
}
return width * fmt->bytesPerPixel;
}

// 行単位でフォーマット変換を行う(ビットパック形式対応)
// srcStride/dstStride: 行あたりのバイト数
static void convertFormatRowByRow(
const uint8_t* src, PixelFormatID srcFormat, int32_t srcStride,
uint8_t* dst, PixelFormatID dstFormat, int32_t dstStride,
int width, int height,
const PixelAuxInfo* srcAux = nullptr) {
auto converter = resolveConverter(srcFormat, dstFormat, srcAux);
if (!converter) return;
for (int y = 0; y < height; ++y) {
converter(dst + y * dstStride, src + y * srcStride, static_cast<size_t>(width));
}
}

// ========================================================================
// ImageStore - 入出力画像データの永続化管理
// ========================================================================
Expand All @@ -54,35 +81,17 @@ class ImageStore {
public:
// 外部データをコピーして保存(入力画像用)
ViewPort store(int id, const uint8_t* data, int w, int h, PixelFormatID fmt) {
auto bytesPerPixel = fmt->bytesPerPixel;
auto size = static_cast<size_t>(w * h * bytesPerPixel);
int32_t stride = calcStride(w, fmt);
auto size = static_cast<size_t>(stride) * static_cast<size_t>(h);
storage_[id].assign(data, data + size);

// bit-packed形式に対応したstride計算
int32_t stride;
if (fmt && fmt->pixelsPerUnit > 1) {
int units = (w + fmt->pixelsPerUnit - 1) / fmt->pixelsPerUnit;
stride = units * fmt->bytesPerUnit;
} else {
stride = w * bytesPerPixel;
}
return ViewPort(storage_[id].data(), fmt, stride, w, h);
}

// バッファを確保(出力用)
ViewPort allocate(int id, int w, int h, PixelFormatID fmt) {
auto bytesPerPixel = fmt->bytesPerPixel;
auto size = static_cast<size_t>(w * h * bytesPerPixel);
int32_t stride = calcStride(w, fmt);
auto size = static_cast<size_t>(stride) * static_cast<size_t>(h);
storage_[id].resize(size, 0);

// bit-packed形式に対応したstride計算
int32_t stride;
if (fmt && fmt->pixelsPerUnit > 1) {
int units = (w + fmt->pixelsPerUnit - 1) / fmt->pixelsPerUnit;
stride = units * fmt->bytesPerUnit;
} else {
stride = w * bytesPerPixel;
}
return ViewPort(storage_[id].data(), fmt, stride, w, h);
}

Expand Down Expand Up @@ -268,10 +277,13 @@ class NodeGraphEvaluatorWrapper {
FormatMetrics::instance().saveSnapshot(snapshot);

std::vector<uint8_t> rgba8Data(rgba8Size);
convertFormat(
sinkOut.buffer.data(), sinkOut.format,
rgba8Data.data(), PixelFormatIDs::RGBA8_Straight,
static_cast<int>(pixelCount)
// 行単位でフォーマット変換(ビットパック形式対応)
int32_t srcStride = calcStride(sinkOut.width, sinkOut.format);
int32_t dstStride = sinkOut.width * 4; // RGBA8 = 4 bytes/pixel
convertFormatRowByRow(
sinkOut.buffer.data(), sinkOut.format, srcStride,
rgba8Data.data(), PixelFormatIDs::RGBA8_Straight, dstStride,
sinkOut.width, sinkOut.height
);

FormatMetrics::instance().restoreSnapshot(snapshot);
Expand Down Expand Up @@ -311,25 +323,17 @@ class NodeGraphEvaluatorWrapper {
FormatOpEntry snapshot[FormatIdx::Count][OpType::Count];
FormatMetrics::instance().saveSnapshot(snapshot);

// bit-packed形式に対応したバッファサイズ計算
int totalPixels = width * height;
size_t bufferSize;
if (targetFormat->pixelsPerUnit > 1) {
// bit-packed形式: 必要なユニット数 × bytesPerUnit
int units = (totalPixels + targetFormat->pixelsPerUnit - 1) / targetFormat->pixelsPerUnit;
bufferSize = static_cast<size_t>(units) * static_cast<size_t>(targetFormat->bytesPerUnit);
} else {
// 通常形式: ピクセル数 × bytesPerPixel
auto targetBpp = targetFormat->bytesPerPixel;
bufferSize = static_cast<size_t>(totalPixels) * static_cast<size_t>(targetBpp);
}

// 行単位でバッファサイズを計算(ビットパック形式対応)
int32_t dstStride = calcStride(width, targetFormat);
size_t bufferSize = static_cast<size_t>(dstStride) * static_cast<size_t>(height);
std::vector<uint8_t> converted(bufferSize);

convertFormat(
rgba8Data.data(), PixelFormatIDs::RGBA8_Straight,
converted.data(), targetFormat,
totalPixels
// 行単位で変換(ビットパック形式は行境界でパディングが必要)
int32_t srcStride = width * 4; // RGBA8 = 4 bytes/pixel
convertFormatRowByRow(
rgba8Data.data(), PixelFormatIDs::RGBA8_Straight, srcStride,
converted.data(), targetFormat, dstStride,
width, height
);

FormatMetrics::instance().restoreSnapshot(snapshot);
Expand Down Expand Up @@ -390,16 +394,14 @@ class NodeGraphEvaluatorWrapper {
}
}

// FormatConverterで変換
auto converter = resolveConverter(view.formatID,
PixelFormatIDs::RGBA8_Straight,
&aux);
if (converter) {
converter(rgba8.data(), view.data, pixelCount);
} else {
// 変換できない場合はゼロ埋め
std::memset(rgba8.data(), 0, rgba8.size());
}
// 行単位でフォーマット変換(ビットパック形式対応)
int32_t srcStride = view.stride;
int32_t dstStride = view.width * 4; // RGBA8 = 4 bytes/pixel
convertFormatRowByRow(
static_cast<const uint8_t*>(view.data), view.formatID, srcStride,
rgba8.data(), PixelFormatIDs::RGBA8_Straight, dstStride,
view.width, view.height, &aux
);
}

// JavaScriptに返す
Expand Down Expand Up @@ -987,14 +989,14 @@ class NodeGraphEvaluatorWrapper {
sinkOut.format = info.format;
sinkOut.width = info.width;
sinkOut.height = info.height;
auto sinkBpp = info.format->bytesPerPixel;
auto sinkBufferSize = static_cast<size_t>(info.width * info.height * sinkBpp);
int32_t sinkStride = calcStride(info.width, info.format);
auto sinkBufferSize = static_cast<size_t>(sinkStride) * static_cast<size_t>(info.height);
sinkOut.buffer.resize(sinkBufferSize);
std::fill(sinkOut.buffer.begin(), sinkOut.buffer.end(), 0);

// SinkNode用のViewPortを作成
info.targetView = ViewPort(sinkOut.buffer.data(), info.format,
info.width * sinkBpp, info.width, info.height);
sinkStride, info.width, info.height);

// SinkNodeを作成
info.node = std::make_unique<SinkNode>();
Expand Down Expand Up @@ -1401,12 +1403,14 @@ class NodeGraphEvaluatorWrapper {
view_ops::copy(info.outputView, 0, 0, info.targetView, 0, 0,
info.width, info.height);
} else {
// フォーマット変換してコピー
size_t pixelCount = static_cast<size_t>(info.width) * static_cast<size_t>(info.height);
convertFormat(
sinkOut.buffer.data(), info.format,
static_cast<uint8_t*>(info.outputView.data), PixelFormatIDs::RGBA8_Straight,
static_cast<int>(pixelCount)
// 行単位でフォーマット変換してコピー(ビットパック形式対応)
int32_t srcStride = calcStride(info.width, info.format);
int32_t dstStride = info.outputView.stride;
convertFormatRowByRow(
sinkOut.buffer.data(), info.format, srcStride,
static_cast<uint8_t*>(info.outputView.data),
PixelFormatIDs::RGBA8_Straight, dstStride,
info.width, info.height
);
}
}
Expand Down
81 changes: 49 additions & 32 deletions fleximg/demo/web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,26 +58,58 @@ const NODE_TYPES = {
// formatName: C++側の PixelFormatDescriptor::name と一致させる
// ========================================
const PIXEL_FORMATS = [
{ formatName: 'RGBA8_Straight', displayName: 'RGBA8888', bpp: 4, description: 'Standard (default)' },
{ formatName: 'RGB888', displayName: 'RGB888', bpp: 3, description: 'RGB order' },
{ formatName: 'BGR888', displayName: 'BGR888', bpp: 3, description: 'BGR order' },
{ formatName: 'RGB565_LE', displayName: 'RGB565_LE', bpp: 2, description: 'Little Endian' },
{ formatName: 'RGB565_BE', displayName: 'RGB565_BE', bpp: 2, description: 'Big Endian' },
{ formatName: 'RGB332', displayName: 'RGB332', bpp: 1, description: '8-bit color' },
{ formatName: 'Alpha8', displayName: 'Alpha8', bpp: 1, description: 'Alpha only (for matte)' },
{ formatName: 'Grayscale8', displayName: 'Gray8', bpp: 1, description: 'Grayscale' },
{ formatName: 'Index1_MSB', displayName: 'Index1 (MSB)', bpp: 0.125, description: 'Palette (2色, 8px/byte)', sinkDisabled: true },
{ formatName: 'Index1_LSB', displayName: 'Index1 (LSB)', bpp: 0.125, description: 'Palette (2色, 8px/byte)', sinkDisabled: true },
{ formatName: 'Index2_MSB', displayName: 'Index2 (MSB)', bpp: 0.25, description: 'Palette (4色, 4px/byte)', sinkDisabled: true },
{ formatName: 'Index2_LSB', displayName: 'Index2 (LSB)', bpp: 0.25, description: 'Palette (4色, 4px/byte)', sinkDisabled: true },
{ formatName: 'Index4_MSB', displayName: 'Index4 (MSB)', bpp: 0.5, description: 'Palette (16色, 2px/byte)', sinkDisabled: true },
{ formatName: 'Index4_LSB', displayName: 'Index4 (LSB)', bpp: 0.5, description: 'Palette (16色, 2px/byte)', sinkDisabled: true },
{ formatName: 'Index8', displayName: 'Index8', bpp: 1, description: 'Palette (256色)', sinkDisabled: true },
// RGB
{ formatName: 'RGBA8_Straight', displayName: 'RGBA8888', bpp: 4, description: 'Standard (default)', category: 'RGB' },
{ formatName: 'RGB888', displayName: 'RGB888', bpp: 3, description: 'RGB order', category: 'RGB' },
{ formatName: 'BGR888', displayName: 'BGR888', bpp: 3, description: 'BGR order', category: 'RGB' },
{ formatName: 'RGB565_LE', displayName: 'RGB565_LE', bpp: 2, description: 'Little Endian', category: 'RGB' },
{ formatName: 'RGB565_BE', displayName: 'RGB565_BE', bpp: 2, description: 'Big Endian', category: 'RGB' },
{ formatName: 'RGB332', displayName: 'RGB332', bpp: 1, description: '8-bit color', category: 'RGB' },
// Grayscale
{ formatName: 'Grayscale8', displayName: 'Gray8', bpp: 1, description: 'Grayscale 8bit', category: 'Grayscale' },
{ formatName: 'Grayscale4_MSB', displayName: 'Gray4 (MSB)', bpp: 0.5, description: '4bit, 2px/byte', category: 'Grayscale' },
{ formatName: 'Grayscale4_LSB', displayName: 'Gray4 (LSB)', bpp: 0.5, description: '4bit, 2px/byte', category: 'Grayscale' },
{ formatName: 'Grayscale2_MSB', displayName: 'Gray2 (MSB)', bpp: 0.25, description: '2bit, 4px/byte', category: 'Grayscale' },
{ formatName: 'Grayscale2_LSB', displayName: 'Gray2 (LSB)', bpp: 0.25, description: '2bit, 4px/byte', category: 'Grayscale' },
{ formatName: 'Grayscale1_MSB', displayName: 'Gray1 (MSB)', bpp: 0.125, description: '1bit, 8px/byte', category: 'Grayscale' },
{ formatName: 'Grayscale1_LSB', displayName: 'Gray1 (LSB)', bpp: 0.125, description: '1bit, 8px/byte', category: 'Grayscale' },
// Alpha
{ formatName: 'Alpha8', displayName: 'Alpha8', bpp: 1, description: 'Alpha only (for matte)', category: 'Alpha' },
// Index (Palette)
{ formatName: 'Index8', displayName: 'Index8', bpp: 1, description: 'Palette (256色)', category: 'Index', sinkDisabled: true },
{ formatName: 'Index4_MSB', displayName: 'Index4 (MSB)', bpp: 0.5, description: 'Palette (16色, 2px/byte)', category: 'Index', sinkDisabled: true },
{ formatName: 'Index4_LSB', displayName: 'Index4 (LSB)', bpp: 0.5, description: 'Palette (16色, 2px/byte)', category: 'Index', sinkDisabled: true },
{ formatName: 'Index2_MSB', displayName: 'Index2 (MSB)', bpp: 0.25, description: 'Palette (4色, 4px/byte)', category: 'Index', sinkDisabled: true },
{ formatName: 'Index2_LSB', displayName: 'Index2 (LSB)', bpp: 0.25, description: 'Palette (4色, 4px/byte)', category: 'Index', sinkDisabled: true },
{ formatName: 'Index1_MSB', displayName: 'Index1 (MSB)', bpp: 0.125, description: 'Palette (2色, 8px/byte)', category: 'Index', sinkDisabled: true },
{ formatName: 'Index1_LSB', displayName: 'Index1 (LSB)', bpp: 0.125, description: 'Palette (2色, 8px/byte)', category: 'Index', sinkDisabled: true },
];

// デフォルトピクセルフォーマット
const DEFAULT_PIXEL_FORMAT = 'RGBA8_Straight';

// フォーマット選択 select 要素を optgroup 付きで構築するヘルパー
function buildFormatOptions(selectElement, currentFormat, opts = {}) {
const { disableSinkOnly = false } = opts;
let currentGroup = null;
let optgroup = null;
PIXEL_FORMATS.forEach(fmt => {
if (fmt.category !== currentGroup) {
currentGroup = fmt.category;
optgroup = document.createElement('optgroup');
optgroup.label = currentGroup;
selectElement.appendChild(optgroup);
}
const option = document.createElement('option');
option.value = fmt.formatName;
option.textContent = `${fmt.displayName} (${fmt.bpp}B)`;
option.title = fmt.description;
if (disableSinkOnly && fmt.sinkDisabled) option.disabled = true;
if (currentFormat === fmt.formatName) option.selected = true;
optgroup.appendChild(option);
});
}

// ========================================
// パレットライブラリ
// ========================================
Expand Down Expand Up @@ -5468,14 +5500,7 @@ function buildImageDetailContent(node) {
formatSelect.style.cssText = 'width: 100%; padding: 4px; margin-top: 4px;';

const currentFormat = node.pixelFormat ?? DEFAULT_PIXEL_FORMAT;
PIXEL_FORMATS.forEach(fmt => {
const option = document.createElement('option');
option.value = fmt.formatName;
option.textContent = `${fmt.displayName} (${fmt.bpp}B)`;
option.title = fmt.description;
if (currentFormat === fmt.formatName) option.selected = true;
formatSelect.appendChild(option);
});
buildFormatOptions(formatSelect, currentFormat);

formatSelect.addEventListener('change', () => {
const newFormat = formatSelect.value;
Expand Down Expand Up @@ -6111,15 +6136,7 @@ function buildSinkDetailContent(node) {
formatSelect.style.width = '140px';

const currentFormat = node.outputFormat ?? DEFAULT_PIXEL_FORMAT;
PIXEL_FORMATS.forEach(fmt => {
const option = document.createElement('option');
option.value = fmt.formatName;
option.textContent = `${fmt.displayName} (${fmt.bpp}B)`;
option.title = fmt.description;
if (fmt.sinkDisabled) option.disabled = true;
if (currentFormat === fmt.formatName) option.selected = true;
formatSelect.appendChild(option);
});
buildFormatOptions(formatSelect, currentFormat, { disableSinkOnly: true });

formatSelect.addEventListener('change', () => {
node.outputFormat = formatSelect.value;
Expand Down
Loading