Skip to content

V4 : Fix GIF, PNG, and WEBP Edge Case Handling #2894

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

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
<!-- Import the shared global .props file -->
<Import Project="$(MSBuildThisFileDirectory)shared-infrastructure\msbuild\props\SixLabors.Global.props" />

<PropertyGroup Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<!-- Workaround various issues bound to implicit language features. -->
<LangVersion>preview</LangVersion>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
</PropertyGroup>

<!--
Expand Down
17 changes: 16 additions & 1 deletion src/ImageSharp/Advanced/AotCompilerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ private static void Seed<TPixel>()
AotCompileResamplers<TPixel>();
AotCompileQuantizers<TPixel>();
AotCompilePixelSamplingStrategys<TPixel>();
AotCompilePixelMaps<TPixel>();
AotCompileDithers<TPixel>();
AotCompileMemoryManagers<TPixel>();

Unsafe.SizeOf<TPixel>();
_ = Unsafe.SizeOf<TPixel>();

// TODO: Do the discovery work to figure out what works and what doesn't.
}
Expand Down Expand Up @@ -514,6 +515,20 @@ private static void AotCompilePixelSamplingStrategys<TPixel>()
default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame<TPixel>));
}

/// <summary>
/// This method pre-seeds the all <see cref="IColorIndexCache{T}" /> in the AoT compiler.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompilePixelMaps<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
}

/// <summary>
/// This method pre-seeds the all <see cref="IDither" /> in the AoT compiler.
/// </summary>
Expand Down
29 changes: 29 additions & 0 deletions src/ImageSharp/Common/InlineArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

// <auto-generated />

using System;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp;

/// <summary>
/// Represents a safe, fixed sized buffer of 4 elements.
/// </summary>
[InlineArray(4)]
internal struct InlineArray4<T>
{
private T t;
}

/// <summary>
/// Represents a safe, fixed sized buffer of 16 elements.
/// </summary>
[InlineArray(16)]
internal struct InlineArray16<T>
{
private T t;
}


38 changes: 38 additions & 0 deletions src/ImageSharp/Common/InlineArray.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

// <auto-generated />

using System;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp;

<#GenerateInlineArrays();#>

<#+
private static int[] Lengths = new int[] {4, 16 };

void GenerateInlineArrays()
{
foreach (int length in Lengths)
{
#>
/// <summary>
/// Represents a safe, fixed sized buffer of <#=length#> elements.
/// </summary>
[InlineArray(<#=length#>)]
internal struct InlineArray<#=length#><T>
{
private T t;
}

<#+
}
}
#>
7 changes: 5 additions & 2 deletions src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,13 @@ private void WriteImage<TPixel>(
ImageFrame<TPixel>? clonedFrame = null;
try
{
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
// No need to clone when quantizing. The quantizer will do it for us.
// TODO: We should really try to avoid the clone entirely.
int bpp = this.bitsPerPixel != null ? (int)this.bitsPerPixel : 32;
if (bpp > 8 && EncodingUtilities.ShouldReplaceTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
EncodingUtilities.ReplaceTransparentPixels(clonedFrame);
}

ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
Expand Down
3 changes: 1 addition & 2 deletions src/ImageSharp/Formats/Bmp/BmpMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,5 @@ public FormatConnectingMetadata ToFormatConnectingMetadata()
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
}
3 changes: 1 addition & 2 deletions src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin
Compression = compression,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}

Expand All @@ -113,7 +112,6 @@ public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
Expand All @@ -126,6 +124,7 @@ public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}

/// <inheritdoc/>
Expand Down
9 changes: 3 additions & 6 deletions src/ImageSharp/Formats/Cur/CurMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ public static CurMetadata FromFormatConnectingMetadata(FormatConnectingMetadata
return new CurMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
Compression = compression
};
}

Expand Down Expand Up @@ -145,15 +144,13 @@ public FormatConnectingMetadata ToFormatConnectingMetadata()
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
PixelTypeInfo = this.GetPixelTypeInfo()
};

/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;

/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
Expand Down
138 changes: 105 additions & 33 deletions src/ImageSharp/Formats/EncodingUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
Expand All @@ -14,62 +16,132 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary>
internal static class EncodingUtilities
{
public static bool ShouldClearTransparentPixels<TPixel>(TransparentColorMode mode)
/// <summary>
/// Determines if transparent pixels can be replaced based on the specified color mode and pixel type.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="mode">Indicates the color mode used to assess the ability to replace transparent pixels.</param>
/// <returns>Returns true if transparent pixels can be replaced; otherwise, false.</returns>
public static bool ShouldReplaceTransparentPixels<TPixel>(TransparentColorMode mode)
where TPixel : unmanaged, IPixel<TPixel>
=> mode == TransparentColorMode.Clear && TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;

/// <summary>
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
public static void ReplaceTransparentPixels<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
=> mode == TransparentColorMode.Clear &&
TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;
=> ReplaceTransparentPixels(frame.Configuration, frame.PixelBuffer);

/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </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)
/// <param name="configuration">The configuration.</param>
/// <param name="buffer">The <see cref="Buffer2D{TPixel}"/> where the transparent pixels will be changed.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReplaceTransparentPixels<TPixel>(Configuration configuration, Buffer2D<TPixel> buffer)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> buffer = clone.PixelBuffer.GetRegion();
ClearTransparentPixels(clone.Configuration, ref buffer, color);
Buffer2DRegion<TPixel> region = buffer.GetRegion();
ReplaceTransparentPixels(configuration, in region);
}

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

private static void ClearTransparentPixelRow(
Span<Vector4> vectorsSpan,
Vector4 replacement)
/// <summary>
/// Replaces pixels with a transparent alpha component with fully transparent pixels.
/// </summary>
/// <param name="source">A span of color vectors that will be checked for transparency and potentially modified.</param>
public static void ReplaceTransparentPixels(Span<Vector4> source)
{
if (Vector128.IsHardwareAccelerated)
if (Vector512.IsHardwareAccelerated && source.Length >= 4)
{
Span<Vector512<float>> source512 = MemoryMarshal.Cast<Vector4, Vector512<float>>(source);
for (int i = 0; i < source512.Length; i++)
{
ref Vector512<float> v = ref source512[i];

// Do `vector < threshold`
Vector512<float> mask = Vector512.Equals(v, Vector512<float>.Zero);

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

// Use the mask to select the replacement vector
// (replacement & mask) | (v512 & ~mask)
v = Vector512.ConditionalSelect(mask, Vector512<float>.Zero, v);
}

int m = Numerics.Modulo4(source.Length);
if (m != 0)
{
for (int i = source.Length - m; i < source.Length; i++)
{
if (source[i].W == 0)
{
source[i] = Vector4.Zero;
}
}
}
}
else if (Vector256.IsHardwareAccelerated && source.Length >= 2)
{
Vector128<float> replacement128 = replacement.AsVector128();
Span<Vector256<float>> source256 = MemoryMarshal.Cast<Vector4, Vector256<float>>(source);
for (int i = 0; i < source256.Length; i++)
{
ref Vector256<float> v = ref source256[i];

// Do `vector < threshold`
Vector256<float> mask = Vector256.Equals(v, Vector256<float>.Zero);

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

for (int i = 0; i < vectorsSpan.Length; i++)
// Use the mask to select the replacement vector
// (replacement & mask) | (v256 & ~mask)
v = Vector256.ConditionalSelect(mask, Vector256<float>.Zero, v);
}

int m = Numerics.Modulo2(source.Length);
if (m != 0)
{
for (int i = source.Length - m; i < source.Length; i++)
{
if (source[i].W == 0)
{
source[i] = Vector4.Zero;
}
}
}
}
else if (Vector128.IsHardwareAccelerated)
{
for (int i = 0; i < source.Length; i++)
{
ref Vector4 v = ref vectorsSpan[i];
ref Vector4 v = ref source[i];
Vector128<float> v128 = v.AsVector128();

// Do `vector == 0`
Expand All @@ -80,16 +152,16 @@ private static void ClearTransparentPixelRow(

// Use the mask to select the replacement vector
// (replacement & mask) | (v128 & ~mask)
v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4();
v = Vector128.ConditionalSelect(mask, Vector128<float>.Zero, v128).AsVector4();
}
}
else
{
for (int i = 0; i < vectorsSpan.Length; i++)
for (int i = 0; i < source.Length; i++)
{
if (vectorsSpan[i].W == 0F)
if (source[i].W == 0F)
{
vectorsSpan[i] = replacement;
source[i] = Vector4.Zero;
}
}
}
Expand Down
5 changes: 0 additions & 5 deletions src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ public class FormatConnectingFrameMetadata
/// </summary>
public PixelTypeInfo? PixelTypeInfo { get; init; }

/// <summary>
/// Gets the frame color table if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; }

/// <summary>
/// Gets the frame color table mode.
/// </summary>
Expand Down
Loading
Loading