Skip to content

Commit

Permalink
Merge pull request #2844 from SixLabors/js/normalize-transparency-enc…
Browse files Browse the repository at this point in the history
…oding

Normalize Encoder Transparency Handling For Alpha Aware Image Formats
  • Loading branch information
JimBobSquarePants authored Dec 11, 2024
2 parents c873e91 + 502a354 commit 4be2645
Show file tree
Hide file tree
Showing 81 changed files with 1,737 additions and 847 deletions.
15 changes: 15 additions & 0 deletions src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats;

/// <summary>
/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency.
/// </summary>
public abstract class AlphaAwareImageEncoder : ImageEncoder
{
/// <summary>
/// Gets or initializes the mode that determines how transparent pixels are handled during encoding.
/// </summary>
public TransparentColorMode TransparentColorMode { get; init; }
}
227 changes: 161 additions & 66 deletions src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Large diffs are not rendered by default.

47 changes: 21 additions & 26 deletions src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ private CurFrameMetadata(CurFrameMetadata other)
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
public byte? EncodingWidth { get; set; }

/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
public byte? EncodingHeight { get; set; }

/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
Expand All @@ -80,20 +80,6 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin
};
}

byte encodingWidth = metadata.EncodingWidth switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingWidth,
_ => 0
};

byte encodingHeight = metadata.EncodingHeight switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingHeight,
_ => 0
};

int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
Expand All @@ -116,8 +102,8 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin
{
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = encodingWidth,
EncodingHeight = encodingHeight,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
Expand All @@ -138,8 +124,8 @@ public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}

/// <inheritdoc/>
Expand All @@ -156,16 +142,16 @@ internal void FromIconDirEntry(IconDirEntry entry)
this.HotspotY = entry.BitCount;
}

internal IconDirEntry ToIconDirEntry()
internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);

return new()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),
Planes = this.HotspotX,
BitCount = this.HotspotY,
ColorCount = colorCount
Expand Down Expand Up @@ -233,13 +219,22 @@ private PixelTypeInfo GetPixelTypeInfo()
};
}

private static byte Scale(byte? value, int destination, float ratio)
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
return ClampEncodingDimension(destination);
}

return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
}

private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{
// Encoding dimensions can be between 0-256 where 0 means 256 or greater.
> 255 => 0,
<= 255 and >= 1 => (byte)dimension,
_ => 0
};
}
97 changes: 97 additions & 0 deletions src/ImageSharp/Formats/EncodingUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers;
using System.Numerics;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Formats;

/// <summary>
/// Provides utilities for encoding images.
/// </summary>
internal static class EncodingUtilities
{
public static bool ShouldClearTransparentPixels<TPixel>(TransparentColorMode mode)
where TPixel : unmanaged, IPixel<TPixel>
=> mode == TransparentColorMode.Clear &&
TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;

/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> buffer = clone.PixelBuffer.GetRegion();
ClearTransparentPixels(clone.Configuration, ref buffer, color);
}

/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="clone">The cloned <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(
Configuration configuration,
ref Buffer2DRegion<TPixel> clone,
Color color)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(clone.Width);
Span<Vector4> vectorsSpan = vectors.GetSpan();
Vector4 replacement = color.ToScaledVector4();
for (int y = 0; y < clone.Height; y++)
{
Span<TPixel> span = clone.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
ClearTransparentPixelRow(vectorsSpan, replacement);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
}
}

private static void ClearTransparentPixelRow(
Span<Vector4> vectorsSpan,
Vector4 replacement)
{
if (Vector128.IsHardwareAccelerated)
{
Vector128<float> replacement128 = replacement.AsVector128();

for (int i = 0; i < vectorsSpan.Length; i++)
{
ref Vector4 v = ref vectorsSpan[i];
Vector128<float> v128 = v.AsVector128();

// Do `vector == 0`
Vector128<float> mask = Vector128.Equals(v128, Vector128<float>.Zero);

// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3));

// Use the mask to select the replacement vector
// (replacement & mask) | (v128 & ~mask)
v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4();
}
}
else
{
for (int i = 0; i < vectorsSpan.Length; i++)
{
if (vectorsSpan[i].W == 0F)
{
vectorsSpan[i] = replacement;
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
return;
}

Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();

Expand Down
Loading

0 comments on commit 4be2645

Please sign in to comment.