user16653829
user16653829

Reputation:

Position and size of combined images inside a new Bitmap

I'm trying to combine two semi-transparent PNG images and display the result in a pictureBox1 which has its SizeMode property set to Zoom:

  pictureBox1.Image = Image.FromFile(imgPath + "/myImg1.png");

If I directly display a single image, it is in the center and the existing borders are respected (as shown on the right side of example image below), but if I combines these two images - of the same size:

private void button2_Click(object sender, EventArgs e)
{
    source1 = (Bitmap)Image.FromFile(imgPath + "/myImg1.png");
    source2 = (Bitmap)Image.FromFile(imgPath + "/myImg2.png");

    var target = new Bitmap(source1.Width, source1.Height, PixelFormat.Format32bppArgb);
    var graphics = Graphics.FromImage(target);
    graphics.CompositingMode = CompositingMode.SourceOver; 

    graphics.DrawImage(source1, 0, 0);
    graphics.DrawImage(source2, 0, 0);

    pictureBox1.Image = target;
}

the actual result shows the two images cropped and not centered (as shown on left side):

Example image

I'm trying to figure out how control the position and size of the two combined images, so they're drawn as shown in the image on the right.

Upvotes: 1

Views: 903

Answers (1)

Jimi
Jimi

Reputation: 32278

The problem:
The OP is trying to center two images, of the same size in this case, inside a new Bitmap container. The size of the destination Bitmap is set to the size of one of the source images. The new Bitmap should be presented in a PictureBox. The control's SizeMode property is set to SizeMode.Zoom.

The unexpected result is shown in the image on the left, the expected result on the right:

Center images no DPI

What happens:
The destination Bitmap is sized as one of the source images, both of the same size.
The two source images are then drawn an Point(0, 0) in the new container.
It's expected - since the two semi-transparent images have the same size - that both will be drawn in the original position. The source images instead seem to be enlarged and moved towards the bottom-right corner of the new container.

It doesn't seem to be, this is exactly what happens.

The two source Images have a DPI descriptor set to ~72 DPI.
A standard PC screen has a resolution of at least 96 DPI.
When a new Bitmap is created as:

var newImage = new Bitmap(source1.Width, source1.Height, PixelFormat.Format32bppArgb);

the resolution of the new Bitmap is set to the screen resolution, hence at least 96 DPI.

An image loaded using this method:

var image = Image.FromFile([Image Path]);

is created using the original DPI descriptor and pixel format.

When a 72 DPI image is draw in a 96 DPI container, the different resolution is taken into consideration: the lower resolution image is enlarged.
As a result, the two 72 DPI images, draw inside a 96 DPI container of the same size, are not centered anymore, but it appears they're moved down and to the right (thus also clipped).

See also: Image is not drawn at the correct spot

How to solve:
When processing images of - quite possibly - different resolutions, pixel format and size, it's preferable to create a copy of the source images and work with new containers that have the same definition. So, we can copy the source images to a standard 32Bit ARGB Bitmap (default format, hardware-aligned), setting the resolution to the current screen resolution or specifying a different resolution for specific uses (for printing, for example).
Or a value that a User can specify.

As mentioned, when a new Bitmap container is created as:

var bitmap = new Bitmap([width], [height]);

the .Net implementation generates a Bitmap with PixelFormat.Format32bppArgb and resolution set to what the Application thinks is the screen DPI. The value returned by Control.DeviceDpi.
A non DpiAware application will most often think it's 96 DPI.

We can nonetheless specify the image resolution using the Bitmap.SetResolution() method (float values, the DPI is always expressed in floating point values).

This Bitmap constructor:

var bitmap = new Bitmap([Stream], true, false))

generates a Format32bppArgb Bitmap, loads the ICM settings (Color Definition) - if any - and skips the validation of the original Bitmap (often used when a lot of image files are read from disk, it's quite faster. Skipping the ICM mapping is also faster, but can produce wrong colors).

We should also suppose that the two images to center inside the new container, may not have the same exact size (or not the same size at all).
Hence we need a new Bitmap that can contain both images, evaluating the maximum dimension of both.

All considered, the original code can be changed as described in the CenterImages() method.

The PictureBox Image can be then set as:

string image1Path = Path.Combine(imgPath, "myImg1.png");
string image2Path = Path.Combine(imgPath, "myImg2.png");

pictureBox1.Image?.Dispose();
pictureBox1.Image = CenterImages(image1Path, image2Path);

to generate a new combined Bitmap using a default 96 DPI resolution. Or as:

pictureBox1.Image?.Dispose();
pictureBox1.Image = CenterImages(image1Path, image2Path, 300.0f);

to generate a new Bitmap with a resolution of 300 DPI.


private Bitmap CenterImages(string sourcePath1, string sourcePath2, float dpi = 96.0f)
{
    using (var image1 = new Bitmap(Image.FromStream(
        new MemoryStream(File.ReadAllBytes(sourcePath1)), true, false)))
    using (var image2 = new Bitmap(Image.FromStream(
        new MemoryStream(File.ReadAllBytes(sourcePath2)), true, false))) {

        image1.SetResolution(dpi, dpi);
        image2.SetResolution(dpi, dpi);

        var rect = new Rectangle(0, 0, 
            Math.Max(image1.Width, image2.Width), Math.Max(image1.Height, image2.Height));

        var combinedImage = new Bitmap(rect.Width, rect.Height);
        combinedImage.SetResolution(dpi, dpi);

        using (var graphics = Graphics.FromImage(combinedImage)) {
            graphics.DrawImage(image1, (rect.Width - image1.Width) / 2, (rect.Height - image1.Height) / 2);
            graphics.DrawImage(image2, (rect.Width - image2.Width) / 2, (rect.Height - image2.Height) / 2);
        }
        return combinedImage;
    }
}

Upvotes: 1

Related Questions