Skip to content

Commit 8dd4f35

Browse files
Normalize handling of transparent pixels with color components on encode.
1 parent c873e91 commit 8dd4f35

File tree

62 files changed

+957
-643
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+957
-643
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats;
5+
6+
/// <summary>
7+
/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency.
8+
/// </summary>
9+
public abstract class AlphaAwareImageEncoder : ImageEncoder
10+
{
11+
/// <summary>
12+
/// Gets or initializes the mode that determines how transparent pixels are handled during encoding.
13+
/// </summary>
14+
public TransparentColorMode TransparentColorMode { get; init; }
15+
}

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Lines changed: 161 additions & 66 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers;
5+
using System.Numerics;
6+
using System.Runtime.Intrinsics;
7+
using SixLabors.ImageSharp.Memory;
8+
using SixLabors.ImageSharp.PixelFormats;
9+
10+
namespace SixLabors.ImageSharp.Formats;
11+
12+
/// <summary>
13+
/// Provides utilities for encoding images.
14+
/// </summary>
15+
internal static class EncodingUtilities
16+
{
17+
public static bool ShouldClearTransparentPixels<TPixel>(TransparentColorMode mode)
18+
where TPixel : unmanaged, IPixel<TPixel>
19+
=> mode == TransparentColorMode.Clear &&
20+
TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;
21+
22+
/// <summary>
23+
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
24+
/// to better compression in some cases.
25+
/// </summary>
26+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
27+
/// <param name="clone">The cloned <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
28+
/// <param name="color">The color to replace transparent pixels with.</param>
29+
public static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
30+
where TPixel : unmanaged, IPixel<TPixel>
31+
{
32+
Buffer2DRegion<TPixel> buffer = clone.PixelBuffer.GetRegion();
33+
ClearTransparentPixels(clone.Configuration, ref buffer, color);
34+
}
35+
36+
/// <summary>
37+
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
38+
/// to better compression in some cases.
39+
/// </summary>
40+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
41+
/// <param name="configuration">The configuration.</param>
42+
/// <param name="clone">The cloned <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
43+
/// <param name="color">The color to replace transparent pixels with.</param>
44+
public static void ClearTransparentPixels<TPixel>(
45+
Configuration configuration,
46+
ref Buffer2DRegion<TPixel> clone,
47+
Color color)
48+
where TPixel : unmanaged, IPixel<TPixel>
49+
{
50+
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(clone.Width);
51+
Span<Vector4> vectorsSpan = vectors.GetSpan();
52+
Vector4 replacement = color.ToScaledVector4();
53+
for (int y = 0; y < clone.Height; y++)
54+
{
55+
Span<TPixel> span = clone.DangerousGetRowSpan(y);
56+
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
57+
ClearTransparentPixelRow(vectorsSpan, replacement);
58+
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
59+
}
60+
}
61+
62+
private static void ClearTransparentPixelRow(
63+
Span<Vector4> vectorsSpan,
64+
Vector4 replacement)
65+
{
66+
if (Vector128.IsHardwareAccelerated)
67+
{
68+
Vector128<float> replacement128 = replacement.AsVector128();
69+
70+
for (int i = 0; i < vectorsSpan.Length; i++)
71+
{
72+
ref Vector4 v = ref vectorsSpan[i];
73+
Vector128<float> v128 = v.AsVector128();
74+
75+
// Do `vector == 0`
76+
Vector128<float> mask = Vector128.Equals(v128, Vector128<float>.Zero);
77+
78+
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
79+
mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3));
80+
81+
// Use the mask to select the replacement vector
82+
// (replacement & mask) | (v128 & ~mask)
83+
v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4();
84+
}
85+
}
86+
else
87+
{
88+
for (int i = 0; i < vectorsSpan.Length; i++)
89+
{
90+
if (vectorsSpan[i].W == 0F)
91+
{
92+
vectorsSpan[i] = replacement;
93+
}
94+
}
95+
}
96+
}
97+
}

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
665665
return;
666666
}
667667

668-
Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
668+
Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
669669
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
670670
pixelRegion.Clear();
671671

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ internal sealed class GifEncoderCore
6767
/// </summary>
6868
private readonly ushort? repeatCount;
6969

70+
private readonly TransparentColorMode transparentColorMode;
71+
7072
/// <summary>
7173
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
7274
/// </summary>
@@ -83,6 +85,7 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder)
8385
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
8486
this.backgroundColor = encoder.BackgroundColor;
8587
this.repeatCount = encoder.RepeatCount;
88+
this.transparentColorMode = encoder.TransparentColorMode;
8689
}
8790

8891
/// <summary>
@@ -131,18 +134,40 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
131134
}
132135
}
133136

137+
// Quantize the first frame. Checking to see whether we can clear the transparent pixels
138+
// to allow for a smaller color palette and encoded result.
134139
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
135140
{
141+
ImageFrame<TPixel>? clonedFrame = null;
142+
Configuration configuration = this.configuration;
143+
TransparentColorMode mode = this.transparentColorMode;
144+
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
145+
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
146+
{
147+
clonedFrame = image.Frames.RootFrame.Clone();
148+
149+
GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata();
150+
Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground
151+
? this.backgroundColor ?? Color.Transparent
152+
: Color.Transparent;
153+
154+
EncodingUtilities.ClearTransparentPixels(clonedFrame, background);
155+
}
156+
157+
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
158+
136159
if (useGlobalTable)
137160
{
138-
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
139-
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
161+
frameQuantizer.BuildPalette(configuration, mode, strategy, image);
162+
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
140163
}
141164
else
142165
{
143-
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame);
144-
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
166+
frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
167+
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
145168
}
169+
170+
clonedFrame?.Dispose();
146171
}
147172

148173
// Write the header.
@@ -236,52 +261,49 @@ private void EncodeAdditionalFrames<TPixel>(
236261
// This frame is reused to store de-duplicated pixel buffers.
237262
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
238263

239-
for (int i = 1; i < image.Frames.Count; i++)
264+
try
240265
{
241-
if (cancellationToken.IsCancellationRequested)
266+
for (int i = 1; i < image.Frames.Count; i++)
242267
{
243-
if (hasPaletteQuantizer)
244-
{
245-
paletteQuantizer.Dispose();
246-
}
268+
cancellationToken.ThrowIfCancellationRequested();
247269

248-
return;
249-
}
270+
// Gather the metadata for this frame.
271+
ImageFrame<TPixel> currentFrame = image.Frames[i];
272+
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
273+
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
274+
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
250275

251-
// Gather the metadata for this frame.
252-
ImageFrame<TPixel> currentFrame = image.Frames[i];
253-
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
254-
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
255-
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
276+
if (!useLocal && !hasPaletteQuantizer && i > 0)
277+
{
278+
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
279+
// This allows a reduction of memory usage across multi-frame gifs using a global palette
280+
// and also allows use to reuse the cache from previous runs.
281+
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
282+
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
283+
hasPaletteQuantizer = true;
284+
}
256285

257-
if (!useLocal && !hasPaletteQuantizer && i > 0)
258-
{
259-
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
260-
// This allows a reduction of memory usage across multi-frame gifs using a global palette
261-
// and also allows use to reuse the cache from previous runs.
262-
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
263-
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
264-
hasPaletteQuantizer = true;
286+
this.EncodeAdditionalFrame(
287+
stream,
288+
previousFrame,
289+
currentFrame,
290+
nextFrame,
291+
encodingFrame,
292+
useLocal,
293+
gifMetadata,
294+
paletteQuantizer,
295+
previousDisposalMode);
296+
297+
previousFrame = currentFrame;
298+
previousDisposalMode = gifMetadata.DisposalMode;
265299
}
266-
267-
this.EncodeAdditionalFrame(
268-
stream,
269-
previousFrame,
270-
currentFrame,
271-
nextFrame,
272-
encodingFrame,
273-
useLocal,
274-
gifMetadata,
275-
paletteQuantizer,
276-
previousDisposalMode);
277-
278-
previousFrame = currentFrame;
279-
previousDisposalMode = gifMetadata.DisposalMode;
280300
}
281-
282-
if (hasPaletteQuantizer)
301+
finally
283302
{
284-
paletteQuantizer.Dispose();
303+
if (hasPaletteQuantizer)
304+
{
305+
paletteQuantizer.Dispose();
306+
}
285307
}
286308
}
287309

@@ -324,7 +346,9 @@ private void EncodeAdditionalFrame<TPixel>(
324346
// We use it to determine the value to use to replace duplicate pixels.
325347
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
326348

327-
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
349+
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground
350+
? null :
351+
previousFrame;
328352

329353
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
330354
? this.backgroundColor ?? Color.Transparent
@@ -341,6 +365,11 @@ private void EncodeAdditionalFrame<TPixel>(
341365
background,
342366
true);
343367

368+
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
369+
{
370+
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
371+
}
372+
344373
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
345374
encodingFrame,
346375
bounds,

src/ImageSharp/Formats/IAnimatedImageEncoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public interface IAnimatedImageEncoder
3030
/// <summary>
3131
/// Acts as a base class for all image encoders that allow encoding animation sequences.
3232
/// </summary>
33-
public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
33+
public abstract class AnimatedImageEncoder : AlphaAwareImageEncoder, IAnimatedImageEncoder
3434
{
3535
/// <inheritdoc/>
3636
public Color? BackgroundColor { get; init; }

src/ImageSharp/Formats/IQuantizingImageEncoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public interface IQuantizingImageEncoder
2424
/// <summary>
2525
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
2626
/// </summary>
27-
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
27+
public abstract class QuantizingImageEncoder : AlphaAwareImageEncoder, IQuantizingImageEncoder
2828
{
2929
/// <inheritdoc/>
3030
public IQuantizer? Quantizer { get; init; }

src/ImageSharp/Formats/Icon/IconEncoderCore.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ public void Encode<TPixel>(
6363
this.entries[i].Entry.ImageOffset = (uint)stream.Position;
6464

6565
// We crop the frame to the size specified in the metadata.
66-
// TODO: we can optimize this by cropping the frame only if the new size is both required and different.
6766
using Image<TPixel> encodingFrame = new(width, height);
6867
for (int y = 0; y < height; y++)
6968
{
@@ -82,6 +81,8 @@ public void Encode<TPixel>(
8281
UseDoubleHeight = true,
8382
SkipFileHeader = true,
8483
SupportTransparency = false,
84+
TransparentColorMode = this.encoder.TransparentColorMode,
85+
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
8586
BitsPerPixel = encodingMetadata.BmpBitsPerPixel
8687
},
8788
IconFrameCompression.Png => new PngEncoder()
@@ -90,6 +91,7 @@ public void Encode<TPixel>(
9091
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
9192
BitDepth = PngBitDepth.Bit8,
9293
ColorType = PngColorType.RgbWithAlpha,
94+
TransparentColorMode = this.encoder.TransparentColorMode,
9395
CompressionLevel = PngCompressionLevel.BestCompression
9496
},
9597
_ => throw new NotSupportedException(),

src/ImageSharp/Formats/Png/PngEncoder.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,6 @@ public PngEncoder()
6868
/// </summary>
6969
public PngChunkFilter? ChunkFilter { get; init; }
7070

71-
/// <summary>
72-
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
73-
/// should be converted to transparent black, which can yield in better compression in some cases.
74-
/// </summary>
75-
public PngTransparentColorMode TransparentColorMode { get; init; }
76-
7771
/// <inheritdoc/>
7872
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
7973
{

0 commit comments

Comments
 (0)