创建应包含带边框(两种颜色)的矩形的位图的 ImageSource,如何指定像素?

Creating an ImageSource of a Bitmap that should contain a rectangle with a border (two colors), how do I specify the pixels?

我目前正在使用带有彩色图标的按钮自动生成功能区栏。为此,我在生成图标时提供了一个颜色列表,并且需要从中创建一个 ImageSource 来显示图标。

我找到了一个(希望如此)好的方法,即使用 BitmapSource.Create 方法创建基于位图的图像源。可悲的是,这个API似乎真的很低级,这让我很头疼地实现我的需求。
这就是我现在在这里问的原因。

我只画了一种纯色。我什至通过更改像素阵列使上下边框正常工作,但我终其一生都无法使左右边框正确。

我试了很多,目前是这样的:

但在我解释更多之前,我将继续展示我的相关代码。

public virtual ImageSource OneColorImage(Color color, (Color Color, int Size)? border = null, int size = 32)
{
    const int dpi = 96;
    var pixelFormat = PixelFormats.Indexed1;

    var stride = CalculateStride(pixelFormat.BitsPerPixel, size);

    var pixels = new byte[size * stride];
    var colors = new List<Color> { color };

    if (border.HasValue)
    {
        var (borderColor, borderSize) = border.Value;

        // Add the border color to the palette.
        colors.Add(borderColor);

        // Now go over all pixels and specify the byte explicitly
        for (var y = 0; y < size; y++)
        {
            for (var x = 0; x < stride; x++)
            {
                var i = x + y * stride;

                // Top and bottom border
                if (y < borderSize || y >= size - borderSize)
                    pixels[i] = 0xff;

                // Left and right border
                else if (x % stride < borderSize || x % stride >= stride - borderSize)
                    pixels[i] = 0xff;

                // Else background color
                else
                    pixels[i] = 0x00;
            }
        }
    }

    var palette = new BitmapPalette(colors);

    var image = BitmapSource.Create(size, size, dpi, dpi, pixelFormat, palette, pixels, stride);

    return new CachedBitmap(image, BitmapCreateOptions.None, BitmapCacheOption.Default);
}

public virtual int CalculateStride(int bitsPerPixel, int width)
{
    var bytesPerPixel = (bitsPerPixel + 7) / 8;
    var stride = 4 * ((width * bytesPerPixel + 3) / 4);
    return stride;
}

我在谷歌上搜索了很多东西来让它工作,了解了如何计算 stride 及其含义,但我似乎仍然遗漏了一些东西。 我想指定这两种颜色并根据这些颜色创建位图调色板,然后只需将开关设置为一种颜色或另一种颜色,这就是我使用 PixelFormats.Indexed1 格式的原因,因为它仅适用于两个-彩色位图。
我想我可以通过只使用这种格式的固定值来删除这种方法的一些抽象,但是如果我将来要将像素格式更改为更多颜色,我想让它更抽象一点,因此大步计算等

很确定我在寻找左右边框时遗漏了 widthstride 之间的区别,或者我不明白单行中的字节(步长) 工作并且必须设置位,而不是字节...

感谢任何帮助,我尝试了几个小时后卡住了。

通过对只有 2 种颜色的图像使用索引格式来实现更多 "optimised" 的想法是合理的,但是使用 1bpp 只会让一切变得不必要的复杂,因为您将不得不兼顾单一位。相反,我建议你使用 PixelFormats.Indexed8;那么你的图像只包含每个像素一个字节,这非常容易处理。

如果您绝对想使用 1bpp,我建议您仍然将字节数组构建为 8bpp,但之后只需将其转换即可。为此,我发布了一个 ConvertFrom8Bit 函数

另请注意,无需为背景颜色填充中间像素,因为 .Net 中的新字节数组会自动清除,因此始终只包含 0x00 字节。因此,仅越过边界像素并在单独的循环中简单地处理它们会更有效率。您甚至可以通过将顶部和底部填充部分留在填充侧面的循环中来优化这一点。

public virtual ImageSource OneColorImage(Color color, (Color Color, int Size)? border = null, int size = 32)
{
    const Int32 dpi = 96;
    Int32 stride = CalculateStride(8, size);

    Byte[] pixels = new byte[size * stride];
    List<Color> colors = new List<Color> { color };
    if (border.HasValue)
    {
        var (borderColor, borderSize) = border.Value;

        // Add the border color to the palette.
        colors.Add(borderColor);

        Byte paintIndex = 1;
        // Avoid overflow
        borderSize = Math.Min(borderSize, size);

        // Horizontal: just fill the whole block.

        // Top line
        Int32 end = stride * borderSize;
        for (Int32 i = 0; i < end; ++i)
            pixels[i] = paintIndex;

        // Bottom line
        end = stride * size;
        for (Int32 i = stride * (size - borderSize); i < end; ++i)
            pixels[i] = paintIndex;

        // Vertical: Both loops are inside the same y loop. It only goes over
        // the space between the already filled top and bottom parts.
        Int32 lineStart = borderSize * stride;
        Int32 yEnd = size - borderSize;
        Int32 rightStart = size - borderSize;
        for (Int32 y = borderSize; y < yEnd; ++y)
        {
            // left line
            for (Int32 x = 0; x < borderSize; ++x)
                pixels[lineStart + x] = paintIndex;
            // right line
            for (Int32 x = rightStart; x < size; ++x)
                pixels[lineStart + x] = paintIndex;
            lineStart += stride;
        }
    }
    BitmapPalette palette = new BitmapPalette(colors);
    BitmapSource image = BitmapSource.Create(size, size, dpi, dpi, PixelFormats.Indexed8, palette, pixels, stride);
    return new CachedBitmap(image, BitmapCreateOptions.None, BitmapCacheOption.Default);
}

注意,我在这里防止过多的内部计算;顶部和底部循环的结束值预先计算一次,左右行的写偏移量通过简单地保持 "start of line" 值来避免循环内的所有 y * stride + x 类型计算当前迭代并在每个循环中按步长递增。

郑重声明,即使 8 位无关紧要,您的步幅计算也是错误的:您在 之前将 bytesPerPixel 舍入到一个完整字节 将其调整为宽度,而每像素图像一位的正确 bytesPerPixel 值将是“1/8”。这是正确的 4 字节边界跨度计算:

public virtual int CalculateStride(int bitsPerPixel, int width)
{
    Int32 minimumStride  = ((bitsPerPixel * width) + 7) / 8;
    return ((minimumStride + 3) / 4) * 4;
}

关于这一点,我不确定 "stride must be a multiple of four" 是否真的被强制执行,或者它是否在您创建位图时自动调整它,但链接的 ConvertFrom8Bit 函数将输出一个字节具有最小步幅的数组,所以如果这是一个问题,你只需要使用我上面给出的函数来调整它。