none
Overriding DPI of images when displaying them in WPF

    Question

  • Posting following because it might help someone else. It took me quite 
    while to figure this one out.

    I started learning C# and WPF recently. I hadn't had so much fun since the
    late 1990s when I learned Java.

    But I stumbled on one major irritant working on my project, a picture viewer.
    Contrary to just about every environment I've encountered, WPF insists on
    taking the DPI of images into consideration when displaying them. What I want
    is to display the images in some area of my app, pixel-for-pixel unless that
    results in the picture going beyond the frame, in which case the image
    needs to be scaled down so it fits. I don't want to have a larger image because
    the DPI is smaller than 96, and I don't want a smaller image because the DPI
    is higher than 96.

    As far as I'm concerned, DPI is more often than not a useless number. Two
    examples.

    1) My camera arbitrarily assigns a DPI value of 72 to all pictures it takes.
       But what "inches" are we talking about here? Obviously there is no answer
       to that question. So it's a meaningless number.

    2) If I scan a 35 mm color slide, I will probably do so at a DPI value of
       something like 2400, but I'd sure want to display the resulting image much
       larger. By default, WPF will show it at original size, totally useless.
       The DPI here is certainly meaningful, but not as a display parameter!

    I compared two images from same original (leware.net/photo/dpi.html),
    one resized to a DPI of 48, the other to a DPI of 192. In a hex editor,
    except for the one byte that encodes the DPI value, the two files are
    identical. It's the same image, with a different DPI value, but no other
    differences.

    So how do I get a WPF picture viewer to display images without taking their
    DPI into consideration? As every browser and viewer I know will do?

    At first, I thought that I would be able to do something like:

        BitmapImage img = new BitmapImage();

        img.BeginInit();

            img.UriSource = new Uri(somePathOrUrl);

            img.DpiX = 96.0;   // override
            img.DpiY = 96.0;

        img.EndInit();

    But DpiX and DpiY are "get" only, not "set". Yet, it's just a simple number,
    and changing it before WPF does anything with the image does not sound like a
    big challenge (even when waiting for DownloadCompleted event). It's almost as
    if the WPF designers decided that WPI was sooo important that they would never
    allow anyone to modify the value...

    The first approach I tried used RenderTargetBitmap (created at 96 DPI),
    DrawingVisual, DrawingContext classes. Seems quite complex. It worked, but
    I wouldn't call it elegant.

    After much browsing (and with improving understanding), I found a better approach.
    In simple terms, I set the Image's Width and Height to PixelWidth and PixelHeight
    (which essentially makes the resulting DPI to be 96), and I set the Image's
    MaxWidth and MaxHeight to the space available to the Image in the app, to force
    scaling if the source is too large. I used Stretch=Uniform. Code fragments below.
    The Image is placed in a UniformGrid container which provides the MaxWidth and
    MaxHeight, and which centers the Image inside.

    This approach is quite a bit more elegant, it removed nearly 100 lines of code
    from the app. I still think though that it's not as simple as it could be.

    I had also read about "DPI awareness", didn't really understand it, but it seems
    to deal with DPI of display device, not of source images.

    So two questions:

    1) Is there a even easier way, esp. a way to directly modify or ignore an image's
       DPI values before using it (without copying the image into some new bitmap)?

    2) Barring that, is there something simpler than above?

    Note that I'm fine with the application being otherwise DPI aware (fonts,
    buttons, &c).

    Thanks


    -------

    WPF code fragments of the trivial application I used to fine-tune the second
    approach. The two images are 160x100 pixels (but any pair of images smaller
    than the display will do the trick), one at DPI 48, one at DPI 192, and named
    IMG_6726s48.jpg and IMG_6726s192.jpg. The both show at the same size, as I
    wanted.

    To see the original problem as I experienced it, set Stretch=None and comment
    out the two pairs of lines that set image.Width and image.Height.

    XAML

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="512" Width="512">
        <Grid>
            <TextBlock Name="lpvMessage" Text="(messages)" Margin="2,2,10,0" VerticalAlignment="Top"  TextWrapping="NoWrap"/>
            <UniformGrid Name="grid" Margin="48,48,16,16" Background="LightGray" SizeChanged="gridSizeChanges">
                <Image Name="image" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" MouseDown="onClickInImage"/>
            </UniformGrid>
            <Button Content="48" HorizontalAlignment="Left" Margin="10,68,0,0" VerticalAlignment="Top" Width="32" Click="buttonClick"/>
            <Button Content="192" HorizontalAlignment="Left" Margin="11,93,0,0" VerticalAlignment="Top" Width="32" Click="buttonClick"/>

        </Grid>
    </Window>

    XAML.CS

    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                grid.Background = new SolidColorBrush(Color.FromArgb(0xFF, 0xF0, 0xF0, 0xF0)); // shows grid for testing
                changeImage("48");
            }

            private void changeImage(string dpi)
            {
                BitmapImage img = new BitmapImage();

                img.BeginInit();
                img.UriSource = new Uri("R:/IMG_6726s" + dpi + ".jpg"); // IMG_6726s48.jpg or IMG_6726s192.jpg.
                img.EndInit();

                image.Source = img;
                lpvMessage.Text = "Loading :/IMG_6726s" + dpi + ".jpg";
            }

            private void onClickInImage(object sender, MouseButtonEventArgs e)
            {
                BitmapImage isrc = image.Source as BitmapImage;

                image.Width = isrc.PixelWidth;       // "ignores" DPI
                image.Height = isrc.PixelHeight;

                image.MaxWidth = grid.ActualWidth;   // prevents scaling larger than 1:1
                image.MaxHeight = grid.ActualHeight;

                bool shifted = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
                if (shifted)  // shift-click to toggle Stretch between Uniform and None
                {
                    if (image.Stretch == Stretch.None)    image.Stretch = Stretch.Uniform;
                    else                                  image.Stretch = Stretch.None;
                }

                lpvMessage.Text = "grid.ActualSize=" + grid.ActualWidth + "x" + grid.ActualHeight +
                    " image.ActualSize=" + image.ActualWidth + "x" + image.ActualHeight +
                    " isrc.PixelSize=" + isrc.PixelWidth + "x" + isrc.PixelHeight +
                    " image.Stretch->" + ((image.Stretch == Stretch.None) ? "None" : "Uniform");
            }

            private void gridSizeChanges(object sender, SizeChangedEventArgs e)
            {
                BitmapImage isrc = image.Source as BitmapImage;

                image.Width = isrc.PixelWidth;       // "ignores" DPI (redundant here
                image.Height = isrc.PixelHeight;

                image.MaxWidth = grid.ActualWidth;   // prevents scaling to larger than 1:1
                image.MaxHeight = grid.ActualHeight;

                lpvMessage.Text = "grid.ActualSize=" + grid.ActualWidth + "x" + grid.ActualHeight +
                    " image.ActualSize=" + image.ActualWidth + "x" + image.ActualHeight +
                    " isrc.PixelSize=" + isrc.PixelWidth + "x" + isrc.PixelHeight +
                    " image.Stretch->" + ((image.Stretch == Stretch.None) ? "None" : "Uniform");
            }

            private void buttonClick(object sender, RoutedEventArgs e)
            {
                Button b = sender as Button;
                string dpi = b.Content as string;
                changeImage(dpi);
            }
        }
    }


    • Edited by leware Saturday, March 21, 2015 12:24 AM
    Friday, March 20, 2015 11:45 PM

All replies

  • Drag a picture of some sort into a project.

    Any old picture.

    Rename it mypic.jpg.

    Set properties as content, copy always.

    Then add an image into the root grid of mainwindow:

        <Grid>
            <Image Source="mypic.jpg"/>
    
        </Grid>

    It will fill the grid, which will fill the window.
    With one exception.

    It will respect the proportions of the original.

    If you prefer it to fill the grid and hence window entirely at the expense of those proportions then you can instead do:

            <Image Source="mypic.jpg" Stretch="Fill"/>

    But here is my experiment showing what happens without stretching.

    I have no idea what dpi that picture is.  If I stretch the window bigger or smaller the picture conforms.


    Hope that helps.
    Recent Technet articles: Property List Editing; Dynamic XAML
    Practical Polly



    Saturday, March 21, 2015 9:23 AM
    Moderator
  • It's not about getting a given image to display properly from the project view, it's about dealing with arbitrary images loaded at run time, with mostly meaningless DPIs attached to them.  By default, the image's DPI affects how big it's shown, not what I wanted. Examples in http://leware.net/photo/dpi.html .

    The more I think about all this, the more the above solution (Width = PixelWidth) is simple and correct. But, since I had never had to deal with DPI before, it took me a while to get to it.  I had many other challenges with this little app, but in general, the Web provided me an answer rather quickly. But not for this DPI thing.

    I don't know what will happen if I try this app on a device with much higher DPI. Not sure it will do what I want.
    Saturday, March 21, 2015 4:31 PM
  • The dpi doesn't matter.

    Set the size of the container and the image will fill it.

    For example:

    <Image Source="mypic.jpg" Stretch="Fill" 
    Height="200" Width="200"/>

    However big the file it will be stretched or squished into 200x200.

    Or ( and this is important ) whatever size the image is.

    In wpf controls fill their containers.

        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Image Grid.Column="1" Grid.Row="1"/>
        </Grid>

    Here I have an image in a grid.

    That image sizes to the half the height and width of that grid.

    If that grid is the root of a window then that grid fills the window.

    Stretch it squish it, whatever you do that image resizes.

    Show it on a teeny display, or a HUGE screen with a zillion pixels and it stretches.

    WPF layout is designed to flow.

    You don't need to know about the dpi of the image or device.

    Just let it flow.


    Hope that helps.
    Recent Technet articles: Property List Editing; Dynamic XAML
    Practical Polly


    Saturday, March 21, 2015 4:46 PM
    Moderator
  • I wish I could ignore the DPI, but WPF doesn't, so I can't.  I prepared an answer with my original code and some images, but can't post it: "Body text cannot contain images or links until we are able to verify your account.". I don't know what that means and how that's done. Hopefully I'll figure it out soon, and then I'll post.
    Sunday, March 22, 2015 1:04 AM
  • Your basic assumption is incorrect.

    Just whack the picture in an image control and it sizes to whatever size that control is.
    Regardless of dpi of picture or display.

    If you still think dpi matters then try the steps above for yourself.

    If you have a bazillion dpi monitor,  max the window and taaa daaa... the image will fill it.

    If you have a 72 dpi monitor, max the window and taaa daa... the image will fill it.

    DPI doesn't matter.

    That screen shot above is from a window which only has this markup:

        <Grid>
            <Image Source="mypic.jpg"/>
        </Grid>

    There is no other manipulation of the image at all.

    Try it.

    Resize your window, max it. See what happens.

    Without fill it respects the proportions of the picture.


    Hope that helps.
    Recent Technet articles: Property List Editing; Dynamic XAML

    Sunday, March 22, 2015 10:19 AM
    Moderator
  • Being able to ignore DPI is all I'm asking for, and what I've always done for decades

    If the objective is to fill the Window, then yes, it's simple, as the following example of a thumbnail image shows:

        <Grid>
            <Image Source="http://leware.net/temper/42fp.jpg"/>
        </Grid>


    Result below. DPI doesn't matter, pixel sizes don't matter, the source fills the Image control, while respecting the proportions.

    But I don't want the image to fill the space available if it doesn't have enough pixels, it looks fuzzy and I don't like that.

    So I add Stretch=None to the Image control, and it solves my problem, the image is shown at a size that corresponds to its pixel size (63x87), centered.

        <Grid>
            <Image Source="http://leware.net/temper/42fp.jpg" Stretch="None"/>
        </Grid>


    The above two XAML fragments give the following results:

    The second is what I want. So Stretch=None, unless the image is larger than display area, in which case Stretch=Uniform.

    ---

    Now I try my two test images, also with Stretch=None because they are smaller than display area.

    These two images are both 160x100 pixels, and, when compared in a hex editor, differ only in the couple of bytes that store the DPI value (0x0030 vs 0x00c0), all the rest is the same.

        <Grid>
            <Image Source="http://leware.net/photo/IMG_6726s48.jpg" Stretch="None"/>
        </Grid>


    and

        <Grid>
            <Image Source="http://leware.net/photo/IMG_6726s192.jpg" Stretch="None"/>
        </Grid>


    Here's what I see:

    DPI obviously does matter here, much to my surprise. WPF's behavior was unexpected. That was my original problem.

    The DPI 48 image is enlarged by a factor of 2, the 192 DPI image is reduced by a factor of 2. What I want is in between, and the same for both images, a display based only on pixel sizes, like most browsers and picture viewers do.

    In other words, I want one image pixel to be one display pixel, downsized to fit if the image is too large, but never enlarged beyond 1:1 to fill the available space.

    I had a hard time figuring out how to get those two small images to show identically.

    I finally got what I wanted with the solution at the top of this thread (overriding size of Image control instead of DPI). I'm sharing because it might help someone else.

    Is there a better way to handle this when the DPI is arbitrary? Isn't there a way to just tell WPF to ignore images' DPI values or simply override it (force an image's DPI to 96)?

    Quite possibly I'm trying to do something which does not quite fit in the philosophy of WPF. Maybe I'm closer now, I'm still learning (this discussion is helping).

    I won't be surprised if my application misbehaves when the DPI of the display is not 96. Not a concern for now.


    • Edited by leware Tuesday, March 24, 2015 1:01 AM
    Tuesday, March 24, 2015 1:00 AM
  • You can always use my super cheezy way of fixing the problem!

    WPF/XAML: Override DPI in an image

                var bytes = File.ReadAllBytes(fileName);

                bytes[38] = 0xc4;  // HLL to 3780 (96 dpi)
                bytes[39] = 0x0e;
                bytes[42] = 0xc4;  // VLL to 3780 (96 dpi)
                bytes[43] = 0x0e;

                var bitmapImage = new BitmapImage();
                bitmapImage.BeginInit();
                bitmapImage.CacheOption = BitmapCacheOption.Default;
                bitmapImage.StreamSource = new MemoryStream(bytes);
                bitmapImage.EndInit();



    Tuesday, April 12, 2016 12:59 PM