-
-
Notifications
You must be signed in to change notification settings - Fork 852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix various Gif Decoder/Encoder behaviors. #2289
Conversation
[WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)] | ||
|
||
// MAGICK decoder makes the same mistake we did and clones the proceeding frame overwriting the differences. | ||
// [WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)] | ||
public void TiffEncoder_EncodeMultiframe_Convert<TPixel>(TestImageProvider<TPixel> provider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dlemstra This might interest you.
The left shows how the changes in the decoder from this PR only decode the data from the frame. The right shows the decoded output from Magick.NET which is the same as our current behavior.
Cloning the proceeding frame like this leads to much larger output when encoding if the previous frame contains lots of color.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The right part only happens when Coalesce
is called on the MagickImageCollection
. ImageMagick does the same as what you see on the left side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not seeing any calls to Coalesce
in our reference decoder.
public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Configuration configuration = options.Configuration;
var bmpReadDefines = new BmpReadDefines
{
IgnoreFileSize = !this.validate
};
var settings = new MagickReadSettings();
settings.FrameCount = (int)options.MaxFrames;
settings.SetDefines(bmpReadDefines);
using var magickImageCollection = new MagickImageCollection(stream, settings);
var framesList = new List<ImageFrame<TPixel>>();
foreach (IMagickImage<ushort> magicFrame in magickImageCollection)
{
var frame = new ImageFrame<TPixel>(configuration, magicFrame.Width, magicFrame.Height);
framesList.Add(frame);
MemoryGroup<TPixel> framePixels = frame.PixelBuffer.FastMemoryGroup;
using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1)
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
FromRgba32Bytes(configuration, data, framePixels);
}
else if (magicFrame.Depth is 16 or 14)
{
ushort[] data = pixels.ToShortArray(PixelMapping.RGBA);
Span<byte> bytes = MemoryMarshal.Cast<ushort, byte>(data.AsSpan());
FromRgba64Bytes(configuration, bytes, framePixels);
}
else
{
throw new InvalidOperationException();
}
}
return new Image<TPixel>(configuration, new ImageMetadata(), framesList);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's odd I am seeing this when I read that image inside Magick.NET and use the Immediate Window
:
magickImageCollection[0].Width
500
magickImageCollection[1].Width
350
magickImageCollection[2].Width
332
magickImageCollection[3].Width
500
@@ -106,7 +106,7 @@ public void AddPaletteColors(Buffer2DRegion<TPixel> pixelRegion) | |||
// for higher bit depths. Lower bit depths will correctly reduce the palette. | |||
// TODO: Investigate more evenly reduced palette reduction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to figure this out properly.
@@ -554,7 +558,7 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPi | |||
{ | |||
for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) | |||
{ | |||
int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); | |||
int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, Math.Max(transIndex, colorTableMaxIdx)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some images use a transparent index that is the same as the length of the palette.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... yeah, it seems like the behavior I saw was a side-effect of the way I handle OOB values in general. I had just noticed that ImageSharp 2.1.3 was mangling that image, and I knew it had an invalid transparent index. The viewer I've been using (Microsoft GIF Animator) crashes on images like that. What's the viewer you screenshotted there?
I just looked at some web browser code, and it looks like they're just treating any OOB colors as transparent if the GCE has the transparency flag. They take an OOB transparent index as a hint that OOB color values are expected in the frame.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use this. Stumbled upon it a few years back and it's proven incredibly useful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, that's great, thanks!
I see what's happening here now. Before this fix, you're not setting a transparent color because it's out of range, but you're clamping the image values into palette range, so pixels meant to be transparent are taking the last color. This was the result.
Now you're treating the last palette color as transparent and clamping OOB image values to that color, making the transparency work, but also making any pixels that were supposed to be that last palette color transparent as well. It's not creating artifacts as visible as skipping transparency, but it's also not completely correct.
In my implementation, I made the same mistake, but in the other direction. I use shared code for all animated image formats, so I followed the PNG rules of treating OOB image values as the first color in the palette. That meant I also had to treat invalid transparency index as meaning the first color was supposed to be transparent to get things to look right. Hence my initial comment 😅
So yeah, the browser behavior is better. Don't clamp the values but rather assume the palette is actually bigger than the image says.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed a better fix now. I treat anything greater than the max palette index or equal to the flagged transparent index as transparent.
I only clamp following that check to ensure we don't hit OOB when reading the palette.
Thanks for your help here!
@saucecontrol No, that one still fails (silently too!) not sure what the best fix is yet, investigating. Edit. Fixed! |
Prerequisites
Description
Fixes #2288
This PR does a few things