Skip to content

Commit f8c819f

Browse files
committed
Resolve issues within BitmapConverter (#41)
- Scanlines must be aligned on 32-bit boundaries (an address that's a multiple of 4). - Swap the byte order also for images with an alpha channel (RGBA <-> BGRA). - Apply the palette for Format8bppIndexed images. - Ensure the image is casted to uchar before converting.
1 parent f7e5143 commit f8c819f

File tree

6 files changed

+126
-20
lines changed

6 files changed

+126
-20
lines changed

samples/NetVips.Samples/Samples/GdiConvert.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
namespace NetVips.Samples
22
{
33
using Extensions;
4+
using System.Drawing.Imaging;
45

56
public class GdiConvert : ISample
67
{
78
public string Name => "GDI Convert";
89
public string Category => "Utils";
910

10-
public const string Filename = "images/equus_quagga.jpg";
11+
public const string Filename = "images/PNG_transparency_demonstration_1.png";
1112

1213
public string Execute(string[] args)
1314
{
1415
var image = Image.NewFromFile(Filename, access: Enums.Access.Sequential);
1516
var bitmap = image.ToBitmap();
17+
bitmap.Save("vips-convert.png", ImageFormat.Png);
1618
var image2 = bitmap.ToVips();
17-
image2.WriteToFile("gdi-convert.jpg");
19+
image2.WriteToFile("gdi-convert.png");
1820

1921
return "See gdi-convert.jpg";
2022
}

src/NetVips.Extensions/BitmapConverter.cs

+85-10
Original file line numberDiff line numberDiff line change
@@ -85,32 +85,71 @@ public static Image ToVips(this Bitmap src)
8585

8686
var bands = GuessBands(src.PixelFormat);
8787
var format = GuessBandFormat(src.PixelFormat);
88+
var sizeofFormat = format == Enums.BandFormat.Uchar ? sizeof(byte) : sizeof(ushort);
8889

8990
var w = src.Width;
9091
var h = src.Height;
92+
var stride = w * bands * sizeofFormat;
93+
var size = stride * h;
9194

9295
var rect = new Rectangle(0, 0, w, h);
9396
BitmapData bd = null;
9497
Image dst;
9598
try
9699
{
97100
bd = src.LockBits(rect, ImageLockMode.ReadOnly, src.PixelFormat);
98-
dst = Image.NewFromMemoryCopy(bd.Scan0, (ulong)(bd.Stride * h), w, h, bands, format);
101+
102+
// bd.Stride is aligned to a multiple of 4
103+
if (bd.Stride == stride)
104+
{
105+
dst = Image.NewFromMemoryCopy(bd.Scan0, (ulong)size, w, h, bands, format);
106+
}
107+
else
108+
{
109+
var buffer = new byte[size];
110+
111+
// Copy the bytes from src to the managed array for each scanline
112+
for (var y = 0; y < h; y++)
113+
{
114+
Marshal.Copy(bd.Scan0 + y * bd.Stride, buffer, y * stride, stride);
115+
}
116+
117+
dst = Image.NewFromMemory(buffer, w, h, bands, format);
118+
}
99119
}
100120
finally
101121
{
102122
if (bd != null)
103123
src.UnlockBits(bd);
104124
}
105125

106-
if (bands != 3)
126+
if (src.PixelFormat == PixelFormat.Format8bppIndexed)
107127
{
108-
return dst;
128+
var palette = new byte[256];
129+
for (var i = 0; i < 256; i++)
130+
{
131+
if (i >= src.Palette.Entries.Length)
132+
break;
133+
palette[i] = src.Palette.Entries[i].R;
134+
}
135+
136+
var lut = Image.NewFromArray(palette);
137+
return dst.Maplut(lut);
109138
}
110139

111-
// Switch from BGR to RGB
112-
var images = dst.Bandsplit();
113-
return images[2].Bandjoin(images[1], images[0]);
140+
switch (bands)
141+
{
142+
case 3:
143+
// Switch from BGR to RGB
144+
var bgr = dst.Bandsplit();
145+
return bgr[2].Bandjoin(bgr[1], bgr[0]);
146+
case 4:
147+
// Switch from BGRA to RGBA
148+
var bgra = dst.Bandsplit();
149+
return bgra[2].Bandjoin(bgra[1], bgra[0], bgra[3]);
150+
default:
151+
return dst;
152+
}
114153
}
115154

116155
/// <summary>
@@ -123,6 +162,12 @@ public static Bitmap ToBitmap(this Image src)
123162
if (src == null)
124163
throw new ArgumentNullException(nameof(src));
125164

165+
// Ensure image is casted to uint8 (unsigned char)
166+
if (src.Bands < 3 || src.Format != Enums.BandFormat.Ushort)
167+
{
168+
src = src.Cast(Enums.BandFormat.Uchar);
169+
}
170+
126171
PixelFormat pf;
127172
switch (src.Bands)
128173
{
@@ -143,13 +188,17 @@ public static Bitmap ToBitmap(this Image src)
143188
: PixelFormat.Format24bppRgb;
144189

145190
// Switch from RGB to BGR
146-
var bands = src.Bandsplit();
147-
src = bands[2].Bandjoin(bands[1], bands[0]);
191+
var rgb = src.Bandsplit();
192+
src = rgb[2].Bandjoin(rgb[1], rgb[0]);
148193
break;
149194
case 4:
150195
pf = src.Format == Enums.BandFormat.Ushort
151196
? PixelFormat.Format64bppArgb
152197
: PixelFormat.Format32bppArgb;
198+
199+
// Switch from RGBA to BGRA
200+
var rgba = src.Bandsplit();
201+
src = rgba[2].Bandjoin(rgba[1], rgba[0], rgba[3]);
153202
break;
154203
default:
155204
throw new NotImplementedException(
@@ -178,9 +227,35 @@ public static Bitmap ToBitmap(this Image src)
178227
try
179228
{
180229
bd = dst.LockBits(rect, ImageLockMode.WriteOnly, pf);
230+
var dstSize = (ulong)(bd.Stride * h);
231+
var memory = src.WriteToMemory(out var srcSize);
232+
233+
// bd.Stride is aligned to a multiple of 4
234+
if (dstSize == srcSize)
235+
{
236+
unsafe
237+
{
238+
Buffer.MemoryCopy(memory.ToPointer(), bd.Scan0.ToPointer(), srcSize, srcSize);
239+
}
240+
}
241+
else
242+
{
243+
var offset = w * src.Bands;
244+
245+
// Copy the bytes from src to dst for each scanline
246+
for (var y = 0; y < h; y++)
247+
{
248+
var pSrc = memory + y * offset;
249+
var pDst = bd.Scan0 + y * bd.Stride;
250+
251+
unsafe
252+
{
253+
Buffer.MemoryCopy(pSrc.ToPointer(), pDst.ToPointer(), offset, offset);
254+
}
255+
}
256+
}
181257

182-
var memory = src.WriteToMemory();
183-
Marshal.Copy(memory, 0, bd.Scan0, memory.Length);
258+
NetVips.Free(memory);
184259
}
185260
finally
186261
{

src/NetVips.Extensions/NetVips.Extensions.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<AssemblyFileVersion>1.0.0.$(BuildNumber)</AssemblyFileVersion>
2020
<InformationalVersion>1.0.0.$(BuildNumber)$(PrereleaseLabel)</InformationalVersion>
2121
<PackageVersion>1.0.0.$(BuildNumber)$(PrereleaseLabel)</PackageVersion>
22+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
2223
</PropertyGroup>
2324

2425
<ItemGroup>

src/NetVips/Image.cs

+22-6
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ public static Image NewFromArray<T>(T[] array, double scale = 1.0, double offset
599599
/// This method is useful for efficiently transferring images from GDI+
600600
/// into libvips.
601601
///
602-
/// See <see cref="WriteToMemory"/> for the opposite operation.
602+
/// See <see cref="WriteToMemory()"/> for the opposite operation.
603603
///
604604
/// Use <see cref="Copy"/> to set other image attributes.
605605
/// </remarks>
@@ -960,6 +960,26 @@ public void WriteToTarget(Target target, string formatString, VOption kwargs = n
960960
public void WriteToStream(Stream stream, string formatString, VOption kwargs = null) =>
961961
WriteToTarget(TargetStream.NewFromStream(stream), formatString, kwargs);
962962

963+
/// <summary>
964+
/// Write the image to memory as a simple, unformatted C-style array.
965+
/// </summary>
966+
/// <remarks>
967+
/// The caller is responsible for freeing this memory with <see cref="NetVips.Free"/>.
968+
/// </remarks>
969+
/// <param name="size">Output buffer length.</param>
970+
/// <returns>A <see cref="IntPtr"/> pointing to an unformatted C-style array.</returns>
971+
/// <exception cref="VipsException">If unable to write to memory.</exception>
972+
public IntPtr WriteToMemory(out ulong size)
973+
{
974+
var pointer = VipsImage.WriteToMemory(this, out size);
975+
if (pointer == IntPtr.Zero)
976+
{
977+
throw new VipsException("unable to write to memory");
978+
}
979+
980+
return pointer;
981+
}
982+
963983
/// <summary>
964984
/// Write the image to a large memory array.
965985
/// </summary>
@@ -978,11 +998,7 @@ public void WriteToStream(Stream stream, string formatString, VOption kwargs = n
978998
/// <exception cref="VipsException">If unable to write to memory.</exception>
979999
public byte[] WriteToMemory()
9801000
{
981-
var pointer = VipsImage.WriteToMemory(this, out var size);
982-
if (pointer == IntPtr.Zero)
983-
{
984-
throw new VipsException("unable to write to memory");
985-
}
1001+
var pointer = WriteToMemory(out var size);
9861002

9871003
var managedArray = new byte[size];
9881004
Marshal.Copy(pointer, managedArray, 0, (int)size);

src/NetVips/NetVips.cs

+12
Original file line numberDiff line numberDiff line change
@@ -400,5 +400,17 @@ public static IntPtr FundamentalType(IntPtr type)
400400
{
401401
return GType.Fundamental(type);
402402
}
403+
404+
/// <summary>
405+
/// Frees the memory pointed to by <paramref name="mem"/>.
406+
/// </summary>
407+
/// <remarks>
408+
/// This is needed for <see cref="Image.WriteToMemory(out ulong)"/>.
409+
/// </remarks>
410+
/// <param name="mem">The memory to free.</param>
411+
public static void Free(IntPtr mem)
412+
{
413+
GLib.GFree(mem);
414+
}
403415
}
404416
}

tests/NetVips.Tests/ExtensionsTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ private static void AssertPixelValue(byte[] expected, Bitmap actual)
8686
Marshal.Copy(bitmapData.Scan0, pixels, 0, expected.Length);
8787
actual.UnlockBits(bitmapData);
8888

89-
// Switch from BGR to RGB
90-
if (expected.Length == 3)
89+
// Switch from BGR(A) to RGB(A)
90+
if (expected.Length > 2)
9191
{
9292
var t = pixels[0];
9393
pixels[0] = pixels[2];

0 commit comments

Comments
 (0)