Skip to content

Commit

Permalink
BitmapData.Translate methods now properly convert premultiplied data
Browse files Browse the repository at this point in the history
- Added Color.ToPremultipliedArgb() and Color.FromPremultipliedArgb()
- Added BitmapData.PremultipliedAlpha property
- Gtk: Optimize using Lock() and Graphics together
- Wpf: Use better conversion method when using Graphics on a Bitmap to omit alpha channel
- Update BitmapTests to better handle rounding errors when converting to/from premultiplied values
  • Loading branch information
cwensley committed Jul 12, 2021
1 parent 258d581 commit 2d0d598
Show file tree
Hide file tree
Showing 16 changed files with 464 additions and 145 deletions.
84 changes: 64 additions & 20 deletions src/Eto.Gtk/Drawing/BitmapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ namespace Eto.GtkSharp.Drawing
/// <license type="BSD-3">See LICENSE for full terms</license>
public class BitmapDataHandler : BaseBitmapData
{
public BitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject)
: base(image, data, scanWidth, bitsPerPixel, controlObject)
public BitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject, bool premultipliedAlpha)
: base(image, data, scanWidth, bitsPerPixel, controlObject, premultipliedAlpha)
{
}

Expand All @@ -29,6 +29,46 @@ public override int TranslateDataToArgb(int bitmapData)
}
}

/// <summary>
/// Bitmap data handler for surface data, which is always premultiplied
/// </summary>
/// <copyright>(c) 2012-2013 by Curtis Wensley</copyright>
/// <license type="BSD-3">See LICENSE for full terms</license>
public class SurfaceBitmapDataHandler : BaseBitmapData
{
public SurfaceBitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject, bool premultipliedAlpha)
: base(image, data, scanWidth, bitsPerPixel, controlObject, premultipliedAlpha)
{
}

public override int TranslateArgbToData(int argb)
{
var a = (uint)(byte)(argb >> 24);
var r = (uint)(byte)(argb >> 16);
var g = (uint)(byte)(argb >> 8);
var b = (uint)(byte)(argb);
r = r * a / 255;
g = g * a / 255;
b = b * a / 255;
return unchecked((int)((a << 24) | (r << 16) | (g << 8) | (b)));
}

public override int TranslateDataToArgb(int bitmapData)
{
var a = (uint)(byte)(bitmapData >> 24);
var r = (uint)(byte)(bitmapData >> 16);
var g = (uint)(byte)(bitmapData >> 8);
var b = (uint)(byte)(bitmapData);
if (a > 0)
{
b = b * 255 / a;
g = g * 255 / a;
r = r * 255 / a;
}
return unchecked((int)((a << 24) | (r << 16) | (g << 8) | (b)));
}
}

/// <summary>
/// Bitmap handler.
/// </summary>
Expand Down Expand Up @@ -111,13 +151,16 @@ public void Create(Image image, int width, int height, ImageInterpolation interp

public BitmapData Lock()
{
EnsureData();
if (Surface != null)
{
return new SurfaceBitmapDataHandler(Widget, Surface.DataPtr, Surface.Stride, 32, null, true);
}
return InnerLock();
}

BitmapData InnerLock()
{
return new BitmapDataHandler(Widget, Control.Pixels, Control.Rowstride, Control.HasAlpha ? 32 : 24, null);
return new BitmapDataHandler(Widget, Control.Pixels, Control.Rowstride, Control.HasAlpha ? 32 : 24, null, false);
}

public void Unlock(BitmapData bitmapData)
Expand Down Expand Up @@ -290,21 +333,7 @@ public Color GetPixel(int x, int y)
{
using (var data = Lock())
{
unsafe
{
var srcrow = (byte*)data.Data;
srcrow += y * data.ScanWidth;
srcrow += x * data.BytesPerPixel;
if (data.BytesPerPixel == 4)
{
return Color.FromArgb(data.TranslateDataToArgb(*(int*)srcrow));
}
if (data.BytesPerPixel == 3)
{
return Color.FromRgb(data.TranslateDataToArgb(*(int*)srcrow));
}
throw new NotSupportedException();
}
return data.GetPixel(x, y);
}
}

Expand Down Expand Up @@ -351,6 +380,21 @@ public unsafe void FixupAlpha()
}
}

static int UnmultiplyAlpha(int argb)
{
var a = (uint)(byte)(argb >> 24);
var b = (uint)(byte)(argb >> 16);
var g = (uint)(byte)(argb >> 8);
var r = (uint)(byte)(argb);
if (a > 0)
{
b = b * 255 / a;
g = g * 255 / a;
r = r * 255 / a;
}
return unchecked((int)((a << 24) | (b << 16) | (g << 8) | (r)));
}

unsafe void EnsureData()
{
if (Surface == null)
Expand Down Expand Up @@ -388,7 +432,7 @@ unsafe void EnsureData()
var dest = (int*)destrow;
for (int x = 0; x < size.Width; x++)
{
*dest = bd.TranslateArgbToData(*src);
*dest = UnmultiplyAlpha(bd.TranslateArgbToData(*src));
src++;
dest++;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Eto.Gtk/Drawing/IndexedBitmapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Eto.GtkSharp.Drawing
public class IndexedBitmapDataHandler : BaseBitmapData
{
public IndexedBitmapDataHandler (Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject)
: base(image, data, scanWidth, bitsPerPixel, controlObject)
: base(image, data, scanWidth, bitsPerPixel, controlObject, false)
{
}

Expand Down
44 changes: 38 additions & 6 deletions src/Eto.Mac/Drawing/BitmapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ namespace Eto.Mac.Drawing
/// <license type="BSD-3">See LICENSE for full terms</license>
public class BitmapDataHandler : BaseBitmapData
{
public BitmapDataHandler(Bitmap bitmap, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject)
: base(bitmap, data, scanWidth, bitsPerPixel, controlObject)
public BitmapDataHandler(Bitmap bitmap, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject, bool isPremultiplied)
: base(bitmap, data, scanWidth, bitsPerPixel, controlObject, isPremultiplied)
{
}

Expand All @@ -64,12 +64,32 @@ public static int DataToArgb(int bitmapData)

public override int TranslateArgbToData(int argb)
{
return unchecked((int)(((uint)argb & 0xFF00FF00) | (((uint)argb & 0xFF) << 16) | (((uint)argb & 0xFF0000) >> 16)));
var a = (uint)(byte)(argb >> 24);
var r = (uint)(byte)(argb >> 16);
var g = (uint)(byte)(argb >> 8);
var b = (uint)(byte)(argb);
if (PremultipliedAlpha)
{
r = r * a / 255;
g = g * a / 255;
b = b * a / 255;
}
return unchecked((int)((a << 24) | (b << 16) | (g << 8) | (r)));
}

public override int TranslateDataToArgb(int bitmapData)
{
return unchecked((int)(((uint)bitmapData & 0xFF00FF00) | (((uint)bitmapData & 0xFF) << 16) | (((uint)bitmapData & 0xFF0000) >> 16)));
var a = (uint)(byte)(bitmapData >> 24);
var b = (uint)(byte)(bitmapData >> 16);
var g = (uint)(byte)(bitmapData >> 8);
var r = (uint)(byte)(bitmapData);
if (a > 0 && PremultipliedAlpha)
{
b = b * 255 / a;
g = g * 255 / a;
r = r * 255 / a;
}
return unchecked((int)((a << 24) | (r << 16) | (g << 8) | (b)));
}

public override bool Flipped { get { return false; } }
Expand Down Expand Up @@ -268,7 +288,15 @@ public Color GetPixel(int x, int y)
if (bmprep == null)
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Cannot get pixel data for this type of bitmap ({0})", rep?.GetType()));

return bmprep.ColorAt(x, y).ToEto(false);
// don't convert colorspace here otherwise we get incorrect data.. why?
var nscolor = bmprep.ColorAt(x, y);
if (nscolor.ComponentCount >= 3)
{
nscolor.GetRgba(out var red, out var green, out var blue, out var alpha);
return new Color(nscolor, (float)red, (float)green, (float)blue, (float)alpha);
}

return nscolor.ToEto();
}

protected override void Dispose(bool disposing)
Expand All @@ -287,7 +315,11 @@ protected override void Dispose(bool disposing)
public BitmapData Lock()
{
EnsureRep();
return bmprep == null ? null : new BitmapDataHandler(Widget, bmprep.BitmapData, (int)bmprep.BytesPerRow, (int)bmprep.BitsPerPixel, Control);
if (bmprep == null)
return null;

bool isPremultiplied = alpha && !bmprep.BitmapFormat.HasFlag(NSBitmapFormat.AlphaNonpremultiplied);
return new BitmapDataHandler(Widget, bmprep.BitmapData, (int)bmprep.BytesPerRow, (int)bmprep.BitsPerPixel, Control, isPremultiplied);
}

public void Unlock(BitmapData bitmapData)
Expand Down
2 changes: 1 addition & 1 deletion src/Eto.Mac/Drawing/IndexedBitmapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ namespace Eto.Mac.Drawing
public class IndexedBitmapDataHandler : BaseBitmapData
{
public IndexedBitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject)
: base(image, data, scanWidth, bitsPerPixel, controlObject)
: base(image, data, scanWidth, bitsPerPixel, controlObject, false)
{
}

Expand Down
34 changes: 30 additions & 4 deletions src/Eto.WinForms/Drawing/BitmapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,43 @@ public interface IWindowsImage : IWindowsImageSource
/// <license type="BSD-3">See LICENSE for full terms</license>
public class BitmapDataHandler : BaseBitmapData
{
public BitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject)
: base(image, data, scanWidth, bitsPerPixel, controlObject)
public BitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject, bool premultipliedAlpha)
: base(image, data, scanWidth, bitsPerPixel, controlObject, premultipliedAlpha)
{
}

public override int TranslateArgbToData(int argb)
{
if (PremultipliedAlpha)
{
var a = (uint)(byte)(argb >> 24);
var r = (uint)(byte)(argb >> 16);
var g = (uint)(byte)(argb >> 8);
var b = (uint)(byte)(argb);
r = r * a / 255;
g = g * a / 255;
b = b * a / 255;
return unchecked((int)((a << 24) | (r << 16) | (g << 8) | (b)));
}
return argb;
}

public override int TranslateDataToArgb(int bitmapData)
{
if (PremultipliedAlpha)
{
var a = (uint)(byte)(bitmapData >> 24);
var r = (uint)(byte)(bitmapData >> 16);
var g = (uint)(byte)(bitmapData >> 8);
var b = (uint)(byte)(bitmapData);
if (a > 0)
{
b = b * 255 / a;
g = g * 255 / a;
r = r * 255 / a;
}
return unchecked((int)((a << 24) | (r << 16) | (g << 8) | (b)));
}
return bitmapData;
}
}
Expand Down Expand Up @@ -153,7 +178,7 @@ public Size Size
public BitmapData Lock()
{
sdi.BitmapData bd = Control.LockBits(new sd.Rectangle(0, 0, Control.Width, Control.Height), sdi.ImageLockMode.ReadWrite, Control.PixelFormat);
return new BitmapDataHandler(Widget, bd.Scan0, bd.Stride, bd.PixelFormat.BitsPerPixel(), bd);
return new BitmapDataHandler(Widget, bd.Scan0, bd.Stride, bd.PixelFormat.BitsPerPixel(), bd, bd.PixelFormat.IsPremultiplied());
}

public void Unlock(BitmapData bitmapData)
Expand Down Expand Up @@ -249,7 +274,8 @@ public Bitmap Clone(Rectangle? rectangle = null)

public Color GetPixel(int x, int y)
{
return Control.GetPixel(x, y).ToEto();
var color = Control.GetPixel(x, y);
return color.ToEto();
}

public void DrawImage(GraphicsHandler graphics, RectangleF source, RectangleF destination)
Expand Down
4 changes: 2 additions & 2 deletions src/Eto.WinForms/Drawing/IndexedBitmapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Eto.WinForms.Drawing
public class IndexedBitmapDataHandler : BaseBitmapData
{
public IndexedBitmapDataHandler(Image image, IntPtr data, int scanWidth, int bitsPerPixel, object controlObject)
: base (image, data, scanWidth, bitsPerPixel, controlObject)
: base (image, data, scanWidth, bitsPerPixel, controlObject, false)
{
}

Expand Down Expand Up @@ -62,7 +62,7 @@ public void Resize(int width, int height)
public BitmapData Lock()
{
SD.Imaging.BitmapData bd = Control.LockBits(new SD.Rectangle(0, 0, Control.Width, Control.Height), SD.Imaging.ImageLockMode.ReadWrite, Control.PixelFormat);
return new BitmapDataHandler(Widget, bd.Scan0, bd.Stride, bd.PixelFormat.BitsPerPixel(), bd);
return new BitmapDataHandler(Widget, bd.Scan0, bd.Stride, bd.PixelFormat.BitsPerPixel(), bd, bd.PixelFormat.IsPremultiplied());
}

public void Unlock(BitmapData bitmapData)
Expand Down
6 changes: 6 additions & 0 deletions src/Eto.WinForms/WinConversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,12 @@ public static MouseButtons GetEtoButtons(this swf.DragEventArgs e)
}

public static swf.Cursor ToSwf(this Cursor cursor) => CursorHandler.GetControl(cursor);

public static bool IsPremultiplied(this sdi.PixelFormat format)
{
return format == sdi.PixelFormat.Format32bppPArgb
|| format == sdi.PixelFormat.Format64bppPArgb;
}

}
}
Loading

0 comments on commit 2d0d598

Please sign in to comment.