winforms icon indicating copy to clipboard operation
winforms copied to clipboard

Add support for converting the format of a Bitmap

Open reflectronic opened this issue 3 years ago • 12 comments

Background

GDI+ 1.1 added support for converting the pixel format of an bitmap in-place. Naturally, one can specify a target pixel format, but the function accepts many other parameters to control aspects of the conversion. Unfortunately, the API is not very well designed — it's poorly documented, and many combinations of options seem to be outright ignored or result in vague errors — but a not insubstantial amount of people have wondered how to do this, and the workaround is slow, less flexible, and certainly not obvious.

When converting to an indexed pixel format, the ditherType, paletteType, and palette parameters become relevant. Color palettes can be provided in order to constrain the colors in the converted image. There are effectively three categories of palettes:

  • Custom palettes. All available colors are specified by the user.
  • Optimal palettes. An optimal palette is created using an image, and consists of the best n colors (where n is specified by the user) from the image to be used when converting the image.
  • Standard palettes. The user can specify one of many predefined fixed palettes, and may use them in combination with an ordered or spiral dither type to produce a halftone image.

When a standard fixed palette type is used, any dither type is valid. Otherwise, only None, Solid, and ErrorDiffusion are valid. (As an exception, DitherTypeOrdered4x4 may be used when converting to a 16 bits-per-pixel format using any palette.) GDI+ will convert from a standard palette specified with paletteType to a custom palette passed in palette using a nearest-color conversion.

One can also specify an alpha threshold percent. Passing a value t specifies that a pixel that is less than t percent fully opaque will map to the transparent color. (If there is no transparent color, the color closest to black will be selected.)

This proposal is one of many to add missing GDI+ 1.1 functionality to System.Drawing.

Usage Example

Bitmap bitmap = (Bitmap) Image.FromFile("immo.jpg");
ColorPalette palette = new ColorPalette(PaletteType.FixedHalftone8);
image.ConvertFormat(PixelFormat.Format8bppIndexed, DitherType.Ordered16x16, PaletteType.FixedHalftone8, palette);

Before and after sample of above code sample

API Proposal -- Updated: https://github.com/dotnet/winforms/issues/8827#issuecomment-1920392878

See the documentation for:

namespace System.Drawing.Imaging
{
+   public enum PaletteType
+   {
+       Custom,
+       Optimal,
+       FixedBW,
+       FixedHalftone8,
+       FixedHalftone27,
+       FixedHalftone64,
+       FixedHalftone125,
+       FixedHalftone216,
+       FixedHalftone252,
+       FixedHalftone256
+   }
    
+   public enum DitherType
+   {
+       None,
+       Solid,
+       ErrorDiffusion,
+       Ordered4x4,
+       Ordered8x8,
+       Ordered16x16,
+       Spiral4x4,
+       Spiral8x8,
+       DualSpiral4x4,
+       DualSpiral8x8
+   }

    public sealed class ColorPalette
    {
+       public ColorPalette(Color[] customColors);
+       public ColorPalette(PaletteType fixedPaletteType);
+       public static ColorPalette CreateOptimalPalette(int colors, Bitmap bitmap);
    }
}

namespace System.Drawing
{
    public sealed class Bitmap : System.Drawing.Image
    {
!       Overload for simplicity.
+       public void ConvertFormat(PixelFormat format);
+       public void ConvertFormat(PixelFormat format, DitherType ditherType, PaletteType paletteType, ColorPalette? palette, float alphaThresholdPercent);
    }
}

This requires changes to libgdiplus in order to support it on non-Windows platforms.

reflectronic avatar Feb 28 '21 00:02 reflectronic

Tagging subscribers to this area: @safern, @tarekgh See info in area-owners.md if you want to be subscribed.

Issue Details

Background

GDI+ 1.1 added support for converting the pixel format of an bitmap in-place. Naturally, one can specify a target pixel format, but the function accepts many other parameters to control aspects of the conversion. Unfortunately, the API is not very well designed — it's poorly documented, and many combinations of options seem to be outright ignored or result in vague errors — but a not insubstantial amount of people have wondered how to do this, and the workaround is slow, less flexible, and certainly not obvious.

When converting to an indexed pixel format, the ditherType, paletteType, and palette parameters become relevant. Color palettes can be provided in order to constrain the colors in the converted image. There are effectively three categories of palettes:

  • Custom palettes. All available colors are specified by the user.
  • Optimal palettes. An optimal palette is created using an image, and consists of the best n colors (where n is specified by the user) from the image to be used when converting the image.
  • Standard palettes. The user can specify one of many predefined fixed palettes, and may use them in combination with an ordered or spiral dither type to produce a halftone image.

When a standard fixed palette type is used, any ordered or spiral dither type is valid. Otherwise, only None, Solid, and ErrorDiffusion are valid. (As an exception, DitherTypeOrdered4x4 may be used when converting to a 16 bits-per-pixel format using any palette.) GDI+ will convert from a standard palette specified with paletteType to a custom palette passed in palette using a nearest-color conversion.

One can also specify an alpha threshold percent. Passing a value t specifies that a pixel that is less than t percent fully opaque will map to the transparent color. (If there is no transparent color, the color closest to black will be selected.)

This proposal is one of many to add missing GDI+ 1.1 functionality to System.Drawing.

API Proposal

See the documentation for:

namespace System.Drawing.Imaging
{
+   public enum PaletteType
+   {
+       Custom,
+       Optimal,
+       FixedBW,
+       FixedHalftone8,
+       FixedHalftone27,
+       FixedHalftone64,
+       FixedHalftone125,
+       FixedHalftone216,
+       FixedHalftone252,
+       FixedHalftone256
+   }
    
+   public enum DitherType
+   {
+       None,
+       Solid,
+       ErrorDiffusion,
+       Ordered4x4,
+       Ordered8x8,
+       Ordered16x16,
+       Spiral4x4,
+       Spiral8x8,
+       DualSpiral4x4,
+       DualSpiral8x8
+   }

    public sealed class ColorPalette
    {
+       public ColorPalette(Color[] customColors);
+       public ColorPalette(PaletteType fixedPaletteType);
+       public static ColorPalette CreateOptimalPalette(int colors, Bitmap bitmap);
    }
}

namespace System.Drawing
{
    public sealed class Bitmap : System.Drawing.Image
    {
!       I added the optional parameters. These seem like they are sensible defaults for .NET, but the native definition has no optional parameters.
+       public void ConvertFormat(PixelFormat format, DitherType ditherType = DitherType.None, PaletteType paletteType + PaletteType.Custom, ColorPalette? palette = null, float alphaThresholdPercent = 0);
    }
}

This requires changes to libgdiplus in order to support it on non-Windows platforms.

Author: reflectronic
Assignees: -
Labels:

api-suggestion, area-System.Drawing, untriaged

Milestone: -

ghost avatar Feb 28 '21 00:02 ghost

Spiral4x4,
Spiral8x8,
DualSpiral4x4,
DualSpiral8x8

I just wanna see what these look like!

JimBobSquarePants avatar Mar 02 '21 16:03 JimBobSquarePants

@reflectronic I guess this would only work on Windows as this is not implemented in libgdiplus? What would the story for Unix be?

safern avatar Apr 08 '21 22:04 safern

What would the story for Unix be?

Setting myself up for a lot of work to contribute to libgdiplus :)

reflectronic avatar Apr 08 '21 22:04 reflectronic

It is also OK to say these would be supported on Windows only and attribute them correctly so that the platform attributes catch that at compile time if someone tries to use these APIs on non-Windows.

safern avatar Apr 08 '21 23:04 safern

Well, I suppose now that dotnet/designs#234 is out of the bag, that makes the Unix story a little easier. :-)

@safern Would you mind taking a look at some of the issues I’ve proposed and see if they can be marked ready for review? I’ll take up the implementation work for all of them. I know that you are busy, and that these issues are basically the lowest priority, so if you want to wait until 6.0 is fully locked down, I do not have a problem with that.

The full list should be:

  • dotnet/winforms#8834
  • dotnet/winforms#8835
  • dotnet/winforms#8836
  • dotnet/winforms#8837
  • This one
  • dotnet/winforms#8830

dotnet/winforms#8833 also needs a small addition and needs re-review too (I have it posted as a comment).

reflectronic avatar Jul 16 '21 20:07 reflectronic

Thanks, @reflectronic. Sure I will. I will mark these issues 7.0.0 for now so that we review them for .NET 7.

safern avatar Jul 16 '21 21:07 safern

I've just noticed this issue today. Shameless self promotion: I happen to have a library that supports converting pixel format of a GDI+ Bitmap with optional quantizing and dithering.

It also supports async, parallelization (but it depends also on the ditherer), cancellation, reporting progress, can be used on Linux (though starting with .NET 6 it might need some tweaks) and does not require libgdiplus changes.

koszeggy avatar Sep 13 '21 10:09 koszeggy

Minor tweaks from the original proposal.

  • Not exposing PaletteType.Optimal as it is encapsulated in the API surface here.
  • 'params' on creating a custom color palette
  • Add useTransparentColor to CreateOptimalPalette
  • Add default parameters to ConvertFormat
namespace System.Drawing.Imaging
{
+   public enum PaletteType
+   {
+       Custom,
+       FixedBW,
+       FixedHalftone8,
+       FixedHalftone27,
+       FixedHalftone64,
+       FixedHalftone125,
+       FixedHalftone216,
+       FixedHalftone252,
+       FixedHalftone256
+   }
    
+   public enum DitherType
+   {
+       None,
+       Solid,
+       ErrorDiffusion,
+       Ordered4x4,
+       Ordered8x8,
+       Ordered16x16,
+       Spiral4x4,
+       Spiral8x8,
+       DualSpiral4x4,
+       DualSpiral8x8
+   }

    public sealed class ColorPalette
    {
        // The array is just used as is (not copied) and exposed in the Entries property, so we don't want to add a ReadOnlySpan overload
+       public ColorPalette(params Color[] customColors);
+       public ColorPalette(PaletteType fixedPaletteType);
+       public static ColorPalette CreateOptimalPalette(int colors, bool useTransparentColor, Bitmap bitmap);
    }
}

namespace System.Drawing
{
    public sealed class Bitmap : System.Drawing.Image
    {
+       public void ConvertFormat(PixelFormat format);
+       public void ConvertFormat(PixelFormat format, DitherType ditherType, PaletteType paletteType = PaletteType.Custom, ColorPalette? palette = null, float alphaThresholdPercent = 0.0f);
    }
}

JeremyKuhne avatar Feb 01 '24 02:02 JeremyKuhne

@reflectronic Now that we've fully transferred ownership of System.Drawing to WinForms and transitioned to CsWin32 I'm pushing forward on getting these GDI+ features implemented. If you have any additional feedback please let me know.

JeremyKuhne avatar Feb 01 '24 17:02 JeremyKuhne

Using these "auto-picked" arguments:

public void ConvertFormat(PixelFormat format)
{
    PixelFormat currentFormat = PixelFormat;
    int targetSize = ((int)format >> 8) & 0xff;
    int sourceSize = ((int)currentFormat >> 8) & 0xff;

    if (!format.HasFlag(PixelFormat.Indexed))
    {
        ConvertFormat(format, targetSize > sourceSize ? DitherType.None : DitherType.Solid);
        return;
    }

    int paletteSize = targetSize switch { 1 => 2, 4 => 16, _ => 256 };
    bool hasAlpha = format.HasFlag(PixelFormat.Alpha);
    if (hasAlpha)
    {
        paletteSize++;
    }

    ColorPalette palette = ColorPalette.CreateOptimalPalette(paletteSize, hasAlpha, this);
    ConvertFormat(format, DitherType.ErrorDiffusion, PaletteType.Custom, palette, .25f);
}

Here are some results:

1BppIndexed bill-gates-Format1bppIndexed 4BppIndexed bill-gates-Format4bppIndexed 8bppIndexed bill-gates-Format8bppIndexed 16bppArgb1555 bill-gates-Format16bppArgb1555 16bppRgb565 bill-gates-Format16bppRgb565

The original: bill-gates

JeremyKuhne avatar Feb 02 '24 05:02 JeremyKuhne

Video

namespace System.Drawing.Imaging
{
    public enum PaletteType
    {
        Custom,
        FixedBlackAndWhite,
        FixedHalftone8,
        FixedHalftone27,
        FixedHalftone64,
        FixedHalftone125,
        FixedHalftone216,
        FixedHalftone252,
        FixedHalftone256
    }
    
    public enum DitherType
    {
        None,
        Solid,
        ErrorDiffusion,
        Ordered4x4,
        Ordered8x8,
        Ordered16x16,
        Spiral4x4,
        Spiral8x8,
        DualSpiral4x4,
        DualSpiral8x8
    }

    public sealed class ColorPalette
    {
        public ColorPalette(params Color[] customColors);
        public ColorPalette(PaletteType fixedPaletteType);
        public static ColorPalette CreateOptimalPalette(int colors,
                                                        bool useTransparentColor,
                                                        Bitmap bitmap);
    }
}

namespace System.Drawing
{
    public sealed class Bitmap : System.Drawing.Image
    {
        public void ConvertFormat(PixelFormat format);
        public void ConvertFormat(PixelFormat format,
                                  DitherType ditherType,
                                  PaletteType paletteType = PaletteType.Custom,
                                  ColorPalette? palette = null,
                                  float alphaThresholdPercent = 0.0f);
    }
}

terrajobst avatar Feb 27 '24 19:02 terrajobst