Skip to content

Commit

Permalink
Merge branch 'main' into heic-support
Browse files Browse the repository at this point in the history
  • Loading branch information
ynse01 authored Nov 24, 2024
2 parents 0cf8f9c + c873e91 commit 473adab
Show file tree
Hide file tree
Showing 81 changed files with 1,466 additions and 1,010 deletions.
27 changes: 26 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ jobs:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options:
- os: ubuntu-latest
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false

- os: ubuntu-latest
framework: net8.0
sdk: 8.0.x
Expand Down Expand Up @@ -100,7 +125,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
Expand Down
3 changes: 3 additions & 0 deletions src/ImageSharp.ruleset
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="ImageSharp" ToolsVersion="17.0">
<Include Path="..\shared-infrastructure\sixlabors.ruleset" Action="Default" />
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.NetAnalyzers">
<Rule Id="CA2022" Action="Info" />
</Rules>
</RuleSet>
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/FormatConnectingMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class FormatConnectingMetadata
/// Gets the default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Color.Transparent"/>.
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Gif/GifEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Image encoder for writing image data to a stream in gif format.
/// </summary>
public sealed class GifEncoder : QuantizingImageEncoder
public sealed class GifEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Gets the color table mode: Global or local.
Expand Down
100 changes: 89 additions & 11 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;

/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;

/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;

/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
Expand All @@ -68,6 +81,8 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder)
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
}

/// <summary>
Expand Down Expand Up @@ -141,9 +156,12 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}

byte backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
{
backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}

// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
Expand All @@ -161,15 +179,21 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken

// Write application extensions.
XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile);
}

this.EncodeFirstFrame(stream, frameMetadata, quantized);

// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();

this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode);
this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);

stream.WriteByte(GifConstants.EndIntroducer);

Expand All @@ -194,7 +218,8 @@ private void EncodeAdditionalFrames<TPixel>(
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode)
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
Expand All @@ -213,6 +238,16 @@ private void EncodeAdditionalFrames<TPixel>(

for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}

return;
}

// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
Expand Down Expand Up @@ -291,6 +326,10 @@ private void EncodeAdditionalFrame<TPixel>(

ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;

Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;

// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
Expand All @@ -299,7 +338,7 @@ private void EncodeAdditionalFrame<TPixel>(
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
true);

using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
Expand Down Expand Up @@ -428,14 +467,12 @@ private IndexedImageFrame<TPixel> QuantizeAdditionalFrameAndUpdateMetadata<TPixe
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);

/// <summary>
/// Returns the index of the most transparent color in the palette.
/// Returns the index of the transparent color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
/// <returns>The <see cref="int"/>.</returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Expand Down Expand Up @@ -463,6 +500,47 @@ private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quanti
return index;
}

/// <summary>
/// Returns the index of the background color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="background">The background color to match.</param>
/// <param name="index">The index in the palette of the background color.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="bool"/>.</returns>
private static bool TryGetBackgroundIndex<TPixel>(
IndexedImageFrame<TPixel>? quantized,
Color? background,
out byte index)
where TPixel : unmanaged, IPixel<TPixel>
{
int match = -1;
if (quantized != null && background.HasValue)
{
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{
if (!backgroundPixel.Equals(palette[i]))
{
continue;
}

match = i;
break;
}
}

if (match >= 0)
{
index = (byte)Numerics.Clamp(match, 0, 255);
return true;
}

index = 0;
return false;
}

/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>
Expand Down
43 changes: 43 additions & 0 deletions src/ImageSharp/Formats/IAnimatedImageEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats;

/// <summary>
/// Defines the contract for all image encoders that allow encoding animation sequences.
/// </summary>
public interface IAnimatedImageEncoder
{
/// <summary>
/// Gets the default background color of the canvas when animating in supported encoders.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
Color? BackgroundColor { get; }

/// <summary>
/// Gets the number of times any animation is repeated in supported encoders.
/// </summary>
ushort? RepeatCount { get; }

/// <summary>
/// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
/// </summary>
bool? AnimateRootFrame { get; }
}

/// <summary>
/// Acts as a base class for all image encoders that allow encoding animation sequences.
/// </summary>
public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; init; }

/// <inheritdoc/>
public ushort? RepeatCount { get; init; }

/// <inheritdoc/>
public bool? AnimateRootFrame { get; init; } = true;
}
50 changes: 50 additions & 0 deletions src/ImageSharp/Formats/IQuantizingImageEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Processing.Processors.Quantization;

namespace SixLabors.ImageSharp.Formats;

/// <summary>
/// Defines the contract for all image encoders that allow color palette generation via quantization.
/// </summary>
public interface IQuantizingImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer? Quantizer { get; }

/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; }
}

/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
{
/// <inheritdoc/>
public IQuantizer? Quantizer { get; init; }

/// <inheritdoc/>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}

/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization when
/// encoding animation sequences.
/// </summary>
public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; }

/// <inheritdoc/>
public ushort? RepeatCount { get; }

/// <inheritdoc/>
public bool? AnimateRootFrame { get; }
}
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/ImageEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
this.Encode(image, stream, cancellationToken);
this.Encode(image, ms, cancellationToken);
ms.Position = 0;
ms.CopyTo(stream, configuration.StreamProcessingBufferSize);
}
Expand Down
3 changes: 1 addition & 2 deletions src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Formats.Jpeg;

Expand Down Expand Up @@ -1473,7 +1472,7 @@ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)

this.Frame.ComponentOrder[i / 2] = (byte)componentIndex;

IJpegComponent component = this.Frame.Components[componentIndex];
JpegComponent component = this.Frame.Components[componentIndex];

// 1 byte: Huffman table selectors.
// 4 bits - dc
Expand Down
3 changes: 1 addition & 2 deletions src/ImageSharp/Formats/Png/PngEncoder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable

using SixLabors.ImageSharp.Processing.Processors.Quantization;

Expand All @@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
public class PngEncoder : QuantizingImageEncoder
public class PngEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
Expand Down
Loading

0 comments on commit 473adab

Please sign in to comment.