Imagesharp: Image instance shows wrong resolution and image size

Created on 30 Jan 2017  ·  27Comments  ·  Source: SixLabors/ImageSharp

We tested the latest release from MyGet with very good results.

In one test we uploaded a 800x500 JPEG image with 300x300 dpi. As you can see in the properties below, ImageSharp reports 44 as VerticalResolution which also results in an incorrect value in InchHeight:

imagesharp_resolution_problem

As mentioned in #92 it is also not possible to change the resolution. The output resolution is always like in the original image.

Our original test image:
paths1_300

Most helpful comment

Looking throught the JPEG decoder class, there seems to be an error calculating the resolution:
https://github.com/JimBobSquarePants/ImageSharp/blob/master/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs#L981

These 2 lines causing the issue:

this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[10] << 8)); 
this.verticalResolution = (short)(this.Temp[11] + (this.Temp[12] << 8)); 

Must be changed to:

this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[8] << 8));
this.verticalResolution = (short)(this.Temp[11] + (this.Temp[10] << 8));

All 27 comments

I'll look into that. Thanks, quick question though... Why did you delete the issue template?

@Auersberg Getting the latest version should address the issue with changing the DPI of a JPG image (it did for me at least in version 1.0.0-alpha1-00102).

Something like this should get you started:

sourceStream = input image data
contentType = output image's content type
targetDPI = output image's DPI

C# public byte[] ChangeDPI(Stream sourceStream, string contentType, double targetDPI) { MemoryStream outputStream = new MemoryStream(); try { using (sourceStream) { Image image = new Image(sourceStream); double currentDPI = image.HorizontalResolution; double resizeRatio = targetDPI / currentDPI; int targetWidth = (int)Math.Round((image.Width * resizeRatio)); int targetHeight = (int)Math.Round((image.Height * resizeRatio)); image.HorizontalResolution = targetDPI; image.VerticalResolution = targetDPI; BicubicResampler resampler = new BicubicResampler(); switch (contentType) { case "image/gif": GifEncoder gifEncoder = new GifEncoder(); gifEncoder.Quality = 256; image.Resize(targetWidth, targetHeight, resampler, false).Save(outputStream, gifEncoder); break; case "image/jpeg": JpegEncoder jpegEncoder = new JpegEncoder(); jpegEncoder.Quality = 100; image.Resize(targetWidth, targetHeight, resampler, false).Save(outputStream, jpegEncoder); break; default: PngEncoder pngEncoder = new PngEncoder(); image.Resize(targetWidth, targetHeight, resampler, false).Save(outputStream, pngEncoder); break; } } } catch (Exception ex) { new LoggerExtensions().LogException(ex, "Image Extensions"); return new byte[0]; } return outputStream.ToArray(); }

@JimBobSquarePants Sorry, my mistake. Btw. good work.

@eat-sleep-code Thank you for the info. Seems my version is from last week. I'll try it out.

@Auersberg I pulled this last week too. I think the issue was fixed some time ago, but Visual Studio didn't correctly update the package. I had to remove the package that was installed and then readd the latest version.

Thanks for chipping in there @eat-sleep-code. @Auersberg let us know how you get on 😄

I created a fresh new demo application (.NET Core) which references the latest release (ImageSharp: 1.0.0-alpha1-00102, other: 1.0.0-alpha1-00082) with the code provided above and did some tests with different JPEG images. Image resolution ranges from 72 dpi to 300 dpi. Output dpi is always set to 200 dpi. Some images are taken from a Smartphone, some are created by Photoshop and Paint.net, some are stock photos.

One image (stock photo) passed the test with the expected results (read/write resolution).
Read image: Some images reported an incorrect resolution. 72dpi images showed 1 or 96 dpi. the 300 dpi images opened as 300x44 dpi (see first post).
Write image: Most images had the correct pixel dimension but the original resolution.

I only tested JPEG images. AFAIK the JPEG file format can be challenging. In our .NET Framework apps we used PresentationCore for image processing and we also had problems with some image meta data (e.g. color profiles). But, I think there is definitely a problem with image resolution (read and write).

@Auersberg would it be at all possible for you to create a repository or a gist that recreates the issue with test images that we can use to debug the problem? Please ensue for each test image you describe actual vs expected details.

Once we have a consistent way of reproducing the issue we will do our best to resolve it for you as time allows.

PS You are right about JPEGs, they are not our friends... especially with the buggy encoders out there and undocumented 'conventions' with regards how other libraries deal with technically corrupted files but loads them anyway.

These are 3 of the images with the code I used.
https://gist.github.com/Auersberg/f68967571463d53c82115e30f4bcfa71

That's awesome 👍

I'm sure one of us will be able to start investigating/fixing this soon.

Btw, the last one seems to be really broken. All programs report a different resolution (Photoshop: 72 dpi, Windows Explorer 96 dpi, Paint/Paint.Net: 144 dpi). So, these application seem to use a default value if the image's meta-data is corrupt.

Looking throught the JPEG decoder class, there seems to be an error calculating the resolution:
https://github.com/JimBobSquarePants/ImageSharp/blob/master/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs#L981

These 2 lines causing the issue:

this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[10] << 8)); 
this.verticalResolution = (short)(this.Temp[11] + (this.Temp[12] << 8)); 

Must be changed to:

this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[8] << 8));
this.verticalResolution = (short)(this.Temp[11] + (this.Temp[10] << 8));

@Auersberg I think you're correct there. Thanks for investigating.

This fixes only the decoding part.
When the resolution has been changed, the code also needs to update the XResolution/YResolution tags of the EXIF profile. Applications usually read these tags but only find the original values.

Thanks, I had to wait to merge a big performance based PR before I could have a look at this. Will do asap unless you fancy knocking up a PR?

I'll wait.
I am asking myself where the best place for this fix is? Maybe before writing the profile in the WriteProfiles method (JpegEncoderCore.cs)

    /// <summary>
    /// Writes the metadata profiles to the image.
    /// </summary>
    /// <param name="image">The image.</param>
    /// <typeparam name="TColor">The pixel format.</typeparam>
    private void WriteProfiles(Image image)  where TColor : struct, IPackedPixel, IEquatable
    {
        this.FixExifHeaderResolution(image.ExifProfile, image.HorizontalResolution, image.VerticalResolution);

        this.WriteProfile(image.ExifProfile);
    }


    /// <summary>
    /// Sets new EXIF resolution if resolution has changed
    /// </summary>
    /// <param name="exifProfile">The EXIF profile.</param>
    /// <param name="resolutionX">Horizontal resolution</param>
    /// <param name="resolutionY">Vertical resolution</param>
    private void FixExifHeaderResolution(ExifProfile exifProfile, double resolutionX, double resolutionY)
    {
        if (exifProfile == null) return;

        var exifResX = exifProfile.GetValue(ExifTag.XResolution);
        if ((exifResX != null) && (exifResX.HasValue))
        {
            var exifValue = (Rational)exifResX.Value;
            if ((exifValue.Numerator/exifValue.Denominator) != resolutionX)
            {
                var val = new Rational((uint)resolutionX, (uint)1, false);
                exifProfile.SetValue(ExifTag.XResolution, val);
            }
        }

        var exifResY = exifProfile.GetValue(ExifTag.YResolution);
        if ((exifResY != null) && (exifResY.HasValue))
        {
            var exifValue = (Rational)exifResY.Value;
            if ((exifValue.Numerator/exifValue.Denominator) != resolutionY)
            {
                var val = new Rational((uint)resolutionY, (uint)1, false);
                exifProfile.SetValue(ExifTag.YResolution, val);
            }
        }
    }

In ImageMagick we also update the EXIF profile when the image is saved. I will add this feature later today. Thanks @Auersberg.

@dlemstra I use ImageMagick intensively in other projects. Nice to see you here.

@dlemstra Just tested your last PR. The issue with the encoder part has been resolved by the new ImageMetaData.SyncProfiles() method.

The decoder bug is always there: See https://github.com/JimBobSquarePants/ImageSharp/issues/96#issue-204120652 and https://github.com/JimBobSquarePants/ImageSharp/issues/96#issuecomment-276795341

Let me explain this on the raw image data (jpeg) of an image with 300x300dpi.
300 is 0x012c as hex value. You can see the resolution at 0x000E-0x000F (horizontal) and 0x0010-0X0011 (vertical).
The current code reads the byte in 0x000F (2C) and then shifts the following byte (01) which is together 0x012C (300). This seems to be correct, but now the code calculates the vertical resolution. It reads 0x0011 (2C) and the following byte (00) which is shifted together 0x002C (44) like mentioned in my first post.

if (this.isJfif) 
{ 
    this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[10] << 8)); 
    this.verticalResolution = (short)(this.Temp[11] + (this.Temp[12] << 8)); 
}

hex

This should be correct:

    this.horizontalResolution = (short)(this.Temp[9] + (this.Temp[8] << 8)); 
    this.verticalResolution = (short)(this.Temp[11] + (this.Temp[10] << 8)); 

Code ref: https://github.com/JimBobSquarePants/ImageSharp/blob/master/src/ImageSharp.Formats.Jpeg/JpegDecoderCore.cs#L977

Hey @Auersberg that should be the issue fixed now. It turned out we had a little bit more to do than originally thought as we had to cater for both JFIF nd EXIF resolution properties when decoding different jpegs.

Please let me know if there are any further issues and thanks for the exceptional detail you provided to help us out. 💯

Working now. Thank you very much.

I know this is closed but I'm having an issue in ImageSharp.Formats.Jpeg 1.0.0-alpha2-00117. I looked at the source code for that version and it appears to have the changes from this fix. I just looked at Image{TColor}.cs and saw changes that were done as part of this fix. This seemed the most appropriate issue to keep it in but if Im wrong I dont mind being educated on whatever the rules are. Assuming Im right here is the scenario.

Files mentioned below are available at https://github.com/jrigsby/ImageSharpIssue

I upload file named TCRLogo_300px.jpg (has resolution of 72DPI) and run it through the following code. Note, the code sees the resolution as 72DPI for this file.

    ` ImageSharp.Configuration.Default.AddImageFormat(new JpegFormat());

     var image = new ImageSharp.Image(file);

     var maxWidthInPixels = maxWidthInInches * image.MetaData.HorizontalResolution; 
     var maxHeightInPixels = maxHeightInInches * image.MetaData.VerticalResolution; 

     image.Resize(new ResizeOptions {
        Size = new ImageSharp.Size((int)maxWidthInPixels, (int)maxHeightInPixels),
        Mode = ResizeMode.Max
     });
     image.MetaData.Quality = 100;
     image.MetaData.ExifProfile = null;

     var memStream = new MemoryStream();

     image.Save(memStream);

     memStream.Position = 0;
     return memStream;`

This results in the stream being sent on to be stored in Azure storage. I copied the file out as "uploaded.jpg". I also tried saving it directly in this code vs sending it on to make sure it wasn't something else corrupting it. It had the same issue so I assume it has nothing to do with other parts of my code.

Inspecting the file in windows shows a resolution of 72DPI. However, I turned around and uploaded the "uploaded.jpg" file to see what the code would do with it, given it doesnt need resizing again it should end up being the same file. The code sees it as 96DPI and the inch sizes are different. I assume you can just pull uploaded.jpg and recreate that part directly.

Also, this is a separate and small issue but the descriptions for inchWidth and inchHeight speak of getting these values by multiplying pixel sizes by density. I would think it is divided by density (assuming density is resolution).

Note the following lines are in the last code I used but really dont change anything pertaining to the issue.

image.MetaData.Quality = 100; image.MetaData.ExifProfile = null;

@jrigsby The reason ImageSharp is reporting a dpi of 96 and windows 72 is that there is not DPI set in the image and if its not set then most libraries will use a default value instead. In windows that's 72dpi in ImageSharp it's 96dpi.

It's 96 by default in System.Drawing also.

It's 72 before using imagesharp to resize it and then save it. Image sharp still reports it to be 72 after resizing it and while it's in memory. It's also 72 as far as windows is concerned when viewing properties of the resized "uploaded.jog". It's only 96 when I load it again after having saved it.

On Mar 6, 2017, at 4:22 AM, James Jackson-South <[email protected]notifications@github.com> wrote:

It's 96 by default in System.Drawing also.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://github.com/JimBobSquarePants/ImageSharp/issues/96#issuecomment-284343199, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ANr2KpTHnBM5Agpk5maMdUCoCL35Gnjkks5ri9BWgaJpZM4Lx4GR.

Sorry, I skipped over that Windows choose 72 as the default. So as ImageSharpe resizes it, it loses its resolution properties even though it shows them. Ill try to see if there is some way to preserve that.

As best I can tell ImageSharp isnt preserving the resolution on save.

`
ImageSharp.Configuration.Default.AddImageFormat(new JpegFormat());

     var maxWidthInInches = (float)1.75;
     var maxHeightInInches = (float) .5;

     //see github for image file
     var image = new ImageSharp.Image("c:\\temp\\logos\\TCRLogo_300px.jpg");

     //At the point image metadata says the resolution is 72

     var maxWidthInPixels = maxWidthInInches * image.MetaData.HorizontalResolution;
     var maxHeightInPixels = maxHeightInInches * image.MetaData.VerticalResolution; 

     image.Resize(new ResizeOptions { 
        Size = new ImageSharp.Size((int)maxWidthInPixels, (int)maxHeightInPixels),
        Mode = ResizeMode.Max
     });

     var memStream = new MemoryStream();

     image.Save(memStream);

     memStream.Position = 0;

     image = new ImageSharp.Image(memStream);

     //At the point image metadata says the resolution is 96`

If the default in ImageSharp is 96 and when the original image is loaded it says the resolution is 72, then I assume the original image resolution is set and that's what it is loading. When image is resized it still says 72 in the metadata for the object. If I save that to a stream and then load that stream into image, the resolution now reports 96. It also does this if I save to the file system directly. If there is something I am supposed to do to preserve the resolution I cannot figure it out.

Was this page helpful?
0 / 5 - 0 ratings