Skip to content

ColorProfileHandling.Convert is not converting PNG files #3025

@Socolin

Description

@Socolin

Prerequisites

  • I have written a descriptive issue title
  • I have verified that I am running the latest version of ImageSharp
  • I have verified if the problem exist in both DEBUG and RELEASE mode
  • I have searched open and closed issues to ensure it has not already been reported

ImageSharp version

4.0.0-alpha.0.55

Other ImageSharp packages and versions

Drawing: 3.0.0-alpha.0.6

Environment (Operating system, version and so on)

Ubuntu 24.04

.NET Framework version

.net 9

Description

When loading a PNG image, the ICC Profile does not seems to be converted correctly.

It seems to be something missing in the PNGDecoder since I was able to manually get the right result by applying the profile manually

Also even if the profile is not applied right now, setting ColorProfileHandling = ColorProfileHandling.Convert remove the profile from the image for a png

Note: I think having a .Mutate(x => x.DrawImageAndConvertIccProfile()) could be a nice addition to make it easier to manage collage of multiple images using various ICC profile without losing quality by comparing everything to sRGB first (or the DrawImage could have an option to convert to the destination image color profile) Should I create another issue for this ?

Steps to Reproduce

For images from https://displaycal.net/icc-color-management-test/

using var pngTestImage1 = Image.Load(new DecoderOptions {ColorProfileHandling = ColorProfileHandling.Convert}, "icc-tests/ICC Rendering Intent Test.png");
using var pngTestImage2 = Image.Load(new DecoderOptions {ColorProfileHandling = ColorProfileHandling.Convert}, "icc-tests/ICC Rendering Intent Test (cLUT only).png");
using var pngTestImage3 = Image.Load(new DecoderOptions {ColorProfileHandling = ColorProfileHandling.Convert}, "icc-tests/sRGB_Gray.png");
using var testCanvas1 = new Image<Rgba32>(pngTestImage1.Width, pngTestImage1.Height);
testCanvas1.Mutate(x => x.DrawImage(pngTestImage1, 1.0f));
testCanvas1.Save("icc-test-1.png");

using var testCanvas2 = new Image<Rgba32>(pngTestImage2.Width, pngTestImage2.Height);
testCanvas2.Mutate(x => x.DrawImage(pngTestImage2, 1.0f));
testCanvas2.Save("icc-test-2.png");

using var testCanvas3 = new Image<Rgba32>(pngTestImage3.Width, pngTestImage3.Height);
testCanvas3.Mutate(x => x.DrawImage(pngTestImage3, 1.0f));
testCanvas3.Save("icc-test-3.png");

For Image from: https://github.com/svgeesus/PNG-ICC-tests

using var imgReference = Image.Load("PNG-ICC-tests/tests/support/reference-circles.png");
var files = Directory.EnumerateFiles("PNG-ICC-tests/tests/support", "*.png");
foreach (var file in files)
{
	using var img = Image.Load(new DecoderOptions
		{
			ColorProfileHandling = ColorProfileHandling.Convert,
		},
		file
	);

	using var img2 = new Image<Rgb24>(img.Width, img.Height);
	img2.Mutate(x => x.DrawImage(img, 1f));
	img2.Mutate(x => x.DrawImage(imgReference, 1f));
	img2.Save($"test-{Path.GetFileName(file)}");
}

Works with manual conversion

using var imgReference = Image.Load("PNG-ICC-tests/tests/support/reference-circles.png");
var files = Directory.EnumerateFiles("PNG-ICC-tests/tests/support", "*.png");
foreach (var file in files)
{
	using var img = Image.Load<Rgba32>(new DecoderOptions
		{
			ColorProfileHandling = ColorProfileHandling.Preserve,
		},
		file
	);

	using var img2 = new Image<Rgba32>(img.Width, img.Height);
	//img2.Mutate(x => x.DrawImage(img, 1f));
	WriteImageWithIccProfile(img, img2, new IccProfile(File.ReadAllBytes("sRGB2014.icc")));
	img2.Mutate(x => x.DrawImage(imgReference, 1f));
	img2.Save($"test-{Path.GetFileName(file)}");
}
WriteImageWithIccProfile(pngTestImage1, testCanvas1, new IccProfile(File.ReadAllBytes("sRGB2014.icc")));
testCanvas1.Save("icc-test-1.png");

using var testCanvas2 = new Image<Rgba32>(pngTestImage2.Width, pngTestImage2.Height);
WriteImageWithIccProfile(pngTestImage2, testCanvas2, new IccProfile(File.ReadAllBytes("sRGB2014.icc")));
testCanvas2.Save("icc-test-2.png");

using var testCanvas3 = new Image<Rgba32>(pngTestImage3.Width, pngTestImage3.Height);
WriteImageWithIccProfile(pngTestImage3, testCanvas3, new IccProfile(File.ReadAllBytes("sRGB2014.icc")));
testCanvas3.Save("icc-test-2.png");


// I know this is bad code, (very poor performance I think, it's just to demonstrate that the ICC profile are just ignored)
void WriteImageWithIccProfile(Image<Rgba32> source, Image<Rgba32> destination, IccProfile profile)
{
	ColorConversionOptions options = new()
	{
		SourceIccProfile = source.Metadata.IccProfile,
		TargetIccProfile = profile,
	};
	ColorProfileConverter converter = new(options);
	source.ProcessPixelRows(destination, (sourceAccessor, targetAccessor) =>
	{
		for (var y = 0; y < sourceAccessor.Height; y++)
		{
			Span<Rgb> tmpSource = new Rgb[sourceAccessor.Width];

			var sourceRow = sourceAccessor.GetRowSpan(y);
			for (var index = 0; index < sourceRow.Length; index++)
			{
				var rgba32 = sourceRow[index];
				tmpSource[index] = new Rgb(rgba32.R / 255.0f, rgba32.G / 255.0f, rgba32.B / 255.0f);
			}

			Span<Rgb> tmpDest = new Rgb[sourceAccessor.Width];
			converter.Convert<Rgb, Rgb>(tmpSource, tmpDest);


			var targetRow = targetAccessor.GetRowSpan(y);
			for (var index = 0; index < targetRow.Length; index++)
			{
				var rgb = tmpDest[index];
				targetRow[index] = new Rgba32(rgb.R, rgb.G, rgb.B);
			}
		}
	});
}

Images

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions