diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index a450aebf43..435c629bc6 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -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
@@ -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 }}
diff --git a/src/ImageSharp.ruleset b/src/ImageSharp.ruleset
index b609890200..dee0393cd7 100644
--- a/src/ImageSharp.ruleset
+++ b/src/ImageSharp.ruleset
@@ -1,4 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
deleted file mode 100644
index 8f8e187403..0000000000
--- a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats;
-
-internal class AnimatedImageFrameMetadata
-{
- ///
- /// Gets or sets the frame color table.
- ///
- public ReadOnlyMemory? ColorTable { get; set; }
-
- ///
- /// Gets or sets the frame color table mode.
- ///
- public FrameColorTableMode ColorTableMode { get; set; }
-
- ///
- /// Gets or sets the duration of the frame.
- ///
- public TimeSpan Duration { get; set; }
-
- ///
- /// Gets or sets the frame alpha blending mode.
- ///
- public FrameBlendMode BlendMode { get; set; }
-
- ///
- /// Gets or sets the frame disposal mode.
- ///
- public FrameDisposalMode DisposalMode { get; set; }
-}
diff --git a/src/ImageSharp/Formats/AnimatedImageMetadata.cs b/src/ImageSharp/Formats/AnimatedImageMetadata.cs
deleted file mode 100644
index ac3ca29f4f..0000000000
--- a/src/ImageSharp/Formats/AnimatedImageMetadata.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats;
-
-internal class AnimatedImageMetadata
-{
- ///
- /// Gets or sets the shared color table.
- ///
- public ReadOnlyMemory? ColorTable { get; set; }
-
- ///
- /// Gets or sets the shared color table mode.
- ///
- public FrameColorTableMode ColorTableMode { get; set; }
-
- ///
- /// Gets or sets 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 .
- ///
- public Color BackgroundColor { get; set; }
-
- ///
- /// Gets or sets the number of times any animation is repeated.
- ///
- /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
- ///
- ///
- public ushort RepeatCount { get; set; }
-}
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 85786949d8..7c92d3e463 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -575,7 +575,9 @@ internal sealed class BmpEncoderCore
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions()
{
- MaxColors = 16
+ MaxColors = 16,
+ Dither = this.quantizer.Options.Dither,
+ DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
@@ -623,7 +625,9 @@ internal sealed class BmpEncoderCore
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions()
{
- MaxColors = 4
+ MaxColors = 4,
+ Dither = this.quantizer.Options.Dither,
+ DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
@@ -680,7 +684,9 @@ internal sealed class BmpEncoderCore
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions()
{
- MaxColors = 2
+ MaxColors = 2,
+ Dither = this.quantizer.Options.Dither,
+ DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
index 68e99bdc5f..d0c60421c4 100644
--- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
@@ -154,4 +154,10 @@ public class BmpMetadata : IFormatMetadata
///
public BmpMetadata DeepClone() => new(this);
+
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
}
diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
index a8a51878e0..6fc8905279 100644
--- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
+++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
@@ -35,10 +35,6 @@ internal sealed class CurDecoderCore : IconDecoderCore
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
- curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
- curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
- curMetadata.HotspotX = curFrameMetadata.HotspotX;
- curMetadata.HotspotY = curFrameMetadata.HotspotY;
}
}
}
diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
index 06cf426dc4..4e9a432b16 100644
--- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
@@ -132,6 +132,16 @@ public class CurFrameMetadata : IFormatFrameMetadata
EncodingHeight = this.EncodingHeight
};
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ 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);
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@@ -222,4 +232,14 @@ public class CurFrameMetadata : IFormatFrameMetadata
ColorType = color
};
}
+
+ private static byte Scale(byte? value, int destination, float ratio)
+ {
+ if (value is null)
+ {
+ return (byte)Math.Clamp(destination, 0, 255);
+ }
+
+ return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
+ }
}
diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs
index 6e97a8584a..19de7f434d 100644
--- a/src/ImageSharp/Formats/Cur/CurMetadata.cs
+++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs
@@ -22,10 +22,6 @@ public class CurMetadata : IFormatMetadata
private CurMetadata(CurMetadata other)
{
this.Compression = other.Compression;
- this.HotspotX = other.HotspotX;
- this.HotspotY = other.HotspotY;
- this.EncodingWidth = other.EncodingWidth;
- this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0)
@@ -39,28 +35,6 @@ public class CurMetadata : IFormatMetadata
///
public IconFrameCompression Compression { get; set; }
- ///
- /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame.
- ///
- public ushort HotspotX { get; set; }
-
- ///
- /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame.
- ///
- public ushort HotspotY { get; set; }
-
- ///
- /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingWidth { get; set; }
-
- ///
- /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingHeight { get; set; }
-
///
/// Gets or sets the number of bits per pixel.
/// Used when is
@@ -175,6 +149,12 @@ public class CurMetadata : IFormatMetadata
ColorTable = this.ColorTable
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs
index baf0a35457..9cfe40f385 100644
--- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs
+++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs
@@ -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 .
+ /// The background color is also used when a frame disposal mode is .
///
///
/// Defaults to .
diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs
index 6cb8f9d8ce..37b585c618 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
///
/// Image encoder for writing image data to a stream in gif format.
///
-public sealed class GifEncoder : QuantizingImageEncoder
+public sealed class GifEncoder : QuantizingAnimatedImageEncoder
{
///
/// Gets the color table mode: Global or local.
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 11185d90b0..0ed7e8c98d 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -54,6 +54,19 @@ internal sealed class GifEncoderCore
///
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
+ ///
+ /// 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 .
+ ///
+ private readonly Color? backgroundColor;
+
+ ///
+ /// The number of times any animation is repeated.
+ ///
+ private readonly ushort? repeatCount;
+
///
/// Initializes a new instance of the class.
///
@@ -68,6 +81,8 @@ internal sealed class GifEncoderCore
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
+ this.backgroundColor = encoder.BackgroundColor;
+ this.repeatCount = encoder.RepeatCount;
}
///
@@ -141,9 +156,12 @@ internal sealed class GifEncoderCore
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);
@@ -161,7 +179,7 @@ internal sealed class GifEncoderCore
// 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);
@@ -169,7 +187,13 @@ internal sealed class GifEncoderCore
// 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);
@@ -194,7 +218,8 @@ internal sealed class GifEncoderCore
Image image,
ReadOnlyMemory globalPalette,
int globalTransparencyIndex,
- FrameDisposalMode previousDisposalMode)
+ FrameDisposalMode previousDisposalMode,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
if (image.Frames.Count == 1)
@@ -209,10 +234,20 @@ internal sealed class GifEncoderCore
ImageFrame previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
+ using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ if (hasPaletteQuantizer)
+ {
+ paletteQuantizer.Dispose();
+ }
+
+ return;
+ }
+
// Gather the metadata for this frame.
ImageFrame currentFrame = image.Frames[i];
ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
@@ -291,6 +326,10 @@ internal sealed class GifEncoderCore
ImageFrame? 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(
@@ -299,7 +338,7 @@ internal sealed class GifEncoderCore
currentFrame,
nextFrame,
encodingFrame,
- Color.Transparent,
+ background,
true);
using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
@@ -428,14 +467,12 @@ internal sealed class GifEncoderCore
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
///
- /// Returns the index of the most transparent color in the palette.
+ /// Returns the index of the transparent color in the palette.
///
/// The current quantized frame.
/// The current gif frame metadata.
/// The pixel format.
- ///
- /// The .
- ///
+ /// The .
private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel
{
@@ -463,6 +500,47 @@ internal sealed class GifEncoderCore
return index;
}
+ ///
+ /// Returns the index of the background color in the palette.
+ ///
+ /// The current quantized frame.
+ /// The background color to match.
+ /// The index in the palette of the background color.
+ /// The pixel format.
+ /// The .
+ private static bool TryGetBackgroundIndex(
+ IndexedImageFrame? quantized,
+ Color? background,
+ out byte index)
+ where TPixel : unmanaged, IPixel
+ {
+ int match = -1;
+ if (quantized != null && background.HasValue)
+ {
+ TPixel backgroundPixel = background.Value.ToPixel();
+ ReadOnlySpan 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;
+ }
+
///
/// Writes the file header signature and version to the stream.
///
diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
index f81329e973..5fe892c656 100644
--- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
@@ -126,40 +126,15 @@ public class GifFrameMetadata : IFormatFrameMetadata
};
}
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
///
public GifFrameMetadata DeepClone() => new(this);
-
- internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
- {
- // TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table?
- int index = -1;
- const float background = 1f;
- if (metadata.ColorTable.HasValue)
- {
- ReadOnlySpan colorTable = metadata.ColorTable.Value.Span;
- for (int i = 0; i < colorTable.Length; i++)
- {
- Vector4 vector = colorTable[i].ToScaledVector4();
- if (vector.W < background)
- {
- index = i;
- }
- }
- }
-
- bool hasTransparency = index >= 0;
-
- return new()
- {
- LocalColorTable = metadata.ColorTable,
- ColorTableMode = metadata.ColorTableMode,
- FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
- DisposalMode = metadata.DisposalMode,
- HasTransparency = hasTransparency,
- TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
- };
- }
}
diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs
index 565038b55a..517609af45 100644
--- a/src/ImageSharp/Formats/Gif/GifMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs
@@ -130,6 +130,12 @@ public class GifMetadata : IFormatMetadata
};
}
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs
new file mode 100644
index 0000000000..44431aa9a4
--- /dev/null
+++ b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats;
+
+///
+/// Defines the contract for all image encoders that allow encoding animation sequences.
+///
+public interface IAnimatedImageEncoder
+{
+ ///
+ /// 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 .
+ ///
+ Color? BackgroundColor { get; }
+
+ ///
+ /// Gets the number of times any animation is repeated in supported encoders.
+ ///
+ ushort? RepeatCount { get; }
+
+ ///
+ /// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
+ ///
+ bool? AnimateRootFrame { get; }
+}
+
+///
+/// Acts as a base class for all image encoders that allow encoding animation sequences.
+///
+public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
+{
+ ///
+ public Color? BackgroundColor { get; init; }
+
+ ///
+ public ushort? RepeatCount { get; init; }
+
+ ///
+ public bool? AnimateRootFrame { get; init; } = true;
+}
diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs
index 4eef93ad34..20f27d050c 100644
--- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs
+++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats;
///
@@ -13,6 +15,15 @@ public interface IFormatFrameMetadata : IDeepCloneable
///
/// The .
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
+
+ ///
+ /// This method is called after a process has been applied to the image frame.
+ ///
+ /// The type of pixel format.
+ /// The source image frame.
+ /// The destination image frame.
+ void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel;
}
///
diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs
index 8d695306e4..a351431c94 100644
--- a/src/ImageSharp/Formats/IFormatMetadata.cs
+++ b/src/ImageSharp/Formats/IFormatMetadata.cs
@@ -21,6 +21,14 @@ public interface IFormatMetadata : IDeepCloneable
///
/// The .
FormatConnectingMetadata ToFormatConnectingMetadata();
+
+ ///
+ /// This method is called after a process has been applied to the image.
+ ///
+ /// The type of pixel format.
+ /// The destination image .
+ void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel;
}
///
diff --git a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs
new file mode 100644
index 0000000000..e88b3ecf02
--- /dev/null
+++ b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs
@@ -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;
+
+///
+/// Defines the contract for all image encoders that allow color palette generation via quantization.
+///
+public interface IQuantizingImageEncoder
+{
+ ///
+ /// Gets the quantizer used to generate the color palette.
+ ///
+ IQuantizer? Quantizer { get; }
+
+ ///
+ /// Gets the used for quantization when building color palettes.
+ ///
+ IPixelSamplingStrategy PixelSamplingStrategy { get; }
+}
+
+///
+/// Acts as a base class for all image encoders that allow color palette generation via quantization.
+///
+public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
+{
+ ///
+ public IQuantizer? Quantizer { get; init; }
+
+ ///
+ public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
+}
+
+///
+/// Acts as a base class for all image encoders that allow color palette generation via quantization when
+/// encoding animation sequences.
+///
+public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder
+{
+ ///
+ public Color? BackgroundColor { get; }
+
+ ///
+ public ushort? RepeatCount { get; }
+
+ ///
+ public bool? AnimateRootFrame { get; }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
index 8b59974eb3..b8a1dded15 100644
--- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
+++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
@@ -35,8 +35,6 @@ internal sealed class IcoDecoderCore : IconDecoderCore
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
- curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
- curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
}
}
}
diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
index c244e38981..a2d1c01391 100644
--- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
@@ -125,6 +125,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata
EncodingHeight = this.EncodingHeight
};
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ 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);
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@@ -217,4 +227,14 @@ public class IcoFrameMetadata : IFormatFrameMetadata
ColorType = color
};
}
+
+ private static byte Scale(byte? value, int destination, float ratio)
+ {
+ if (value is null)
+ {
+ return (byte)Math.Clamp(destination, 0, 255);
+ }
+
+ return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
+ }
}
diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs
index 7e31468ecc..a6c2704b31 100644
--- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs
+++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs
@@ -22,8 +22,6 @@ public class IcoMetadata : IFormatMetadata
private IcoMetadata(IcoMetadata other)
{
this.Compression = other.Compression;
- this.EncodingWidth = other.EncodingWidth;
- this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0)
@@ -37,18 +35,6 @@ public class IcoMetadata : IFormatMetadata
///
public IconFrameCompression Compression { get; set; }
- ///
- /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingWidth { get; set; }
-
- ///
- /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingHeight { get; set; }
-
///
/// Gets or sets the number of bits per pixel.
/// Used when is
@@ -163,6 +149,12 @@ public class IcoMetadata : IFormatMetadata
ColorTable = this.ColorTable
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs
index fdaa5c35dc..a37a327174 100644
--- a/src/ImageSharp/Formats/ImageEncoder.cs
+++ b/src/ImageSharp/Formats/ImageEncoder.cs
@@ -51,7 +51,7 @@ public abstract class ImageEncoder : IImageEncoder
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);
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index bbd2bff53b..56e0f1e985 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -119,6 +119,8 @@ internal class HuffmanScanDecoder : IJpegScanDecoder
this.frame.AllocateComponents();
+ this.todo = this.restartInterval;
+
if (!this.frame.Progressive)
{
this.ParseBaselineData();
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index ac527ff312..90e16f6dff 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
///
private readonly byte[] streamWriteBuffer;
+ private readonly int restartInterval;
+
///
/// Number of jagged bits stored in
///
@@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
/// Initializes a new instance of the class.
///
/// Amount of encoded 8x8 blocks per single jpeg macroblock.
+ /// Numbers of MCUs between restart markers.
/// Output stream for saving encoded data.
- public HuffmanScanEncoder(int blocksPerCodingUnit, Stream outputStream)
+ public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream)
{
int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit;
this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
this.emitWriteIndex = this.emitBuffer.Length;
+ this.restartInterval = restartInterval;
+
this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
this.target = outputStream;
@@ -211,6 +216,9 @@ internal class HuffmanScanEncoder
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -221,6 +229,13 @@ internal class HuffmanScanEncoder
for (nuint k = 0; k < (uint)w; k++)
{
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ component.DcPredictor = 0;
+ }
+
this.WriteBlock(
component,
ref Unsafe.Add(ref blockRef, k),
@@ -231,6 +246,133 @@ internal class HuffmanScanEncoder
{
this.FlushToStream();
}
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
+ }
+ }
+
+ this.FlushRemainingBytes();
+ }
+
+ ///
+ /// Encodes the DC coefficients for a given component's blocks in a scan.
+ ///
+ /// The component whose DC coefficients need to be encoded.
+ /// The token to request cancellation.
+ public void EncodeDcScan(Component component, CancellationToken cancellationToken)
+ {
+ int h = component.HeightInBlocks;
+ int w = component.WidthInBlocks;
+
+ ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
+
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
+ for (int i = 0; i < h; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (nuint k = 0; k < (uint)w; k++)
+ {
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ component.DcPredictor = 0;
+ }
+
+ this.WriteDc(
+ component,
+ ref Unsafe.Add(ref blockRef, k),
+ ref dcHuffmanTable);
+
+ if (this.IsStreamFlushNeeded)
+ {
+ this.FlushToStream();
+ }
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
+ }
+ }
+
+ this.FlushRemainingBytes();
+ }
+
+ ///
+ /// Encodes the AC coefficients for a specified range of blocks in a component's scan.
+ ///
+ /// The component whose AC coefficients need to be encoded.
+ /// The starting index of the AC coefficient range to encode.
+ /// The ending index of the AC coefficient range to encode.
+ /// The token to request cancellation.
+ public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
+ {
+ int h = component.HeightInBlocks;
+ int w = component.WidthInBlocks;
+
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
+ ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
+
+ for (int i = 0; i < h; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
+ ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
+
+ for (nuint k = 0; k < (uint)w; k++)
+ {
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ }
+
+ this.WriteAcBlock(
+ ref Unsafe.Add(ref blockRef, k),
+ start,
+ end,
+ ref acHuffmanTable);
+
+ if (this.IsStreamFlushNeeded)
+ {
+ this.FlushToStream();
+ }
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
}
}
@@ -250,6 +392,9 @@ internal class HuffmanScanEncoder
int mcusPerColumn = frame.McusPerColumn;
int mcusPerLine = frame.McusPerLine;
+ int restarts = 0;
+ int restartsToGo = this.restartInterval;
+
for (int j = 0; j < mcusPerColumn; j++)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -260,6 +405,16 @@ internal class HuffmanScanEncoder
// Encode spectral to binary
for (int i = 0; i < mcusPerLine; i++)
{
+ if (this.restartInterval > 0 && restartsToGo == 0)
+ {
+ this.FlushRemainingBytes();
+ this.WriteRestart(restarts % 8);
+ foreach (var component in frame.Components)
+ {
+ component.DcPredictor = 0;
+ }
+ }
+
// Scan an interleaved mcu... process components in order
int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < frame.Components.Length; k++)
@@ -300,6 +455,17 @@ internal class HuffmanScanEncoder
{
this.FlushToStream();
}
+
+ if (this.restartInterval > 0)
+ {
+ if (restartsToGo == 0)
+ {
+ restartsToGo = this.restartInterval;
+ restarts++;
+ }
+
+ restartsToGo--;
+ }
}
}
@@ -371,25 +537,29 @@ internal class HuffmanScanEncoder
this.FlushRemainingBytes();
}
- private void WriteBlock(
+ private void WriteDc(
Component component,
ref Block8x8 block,
- ref HuffmanLut dcTable,
- ref HuffmanLut acTable)
+ ref HuffmanLut dcTable)
{
// Emit the DC delta.
int dc = block[0];
this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
component.DcPredictor = dc;
+ }
+ private void WriteAcBlock(
+ ref Block8x8 block,
+ nint start,
+ nint end,
+ ref HuffmanLut acTable)
+ {
// Emit the AC components.
int[] acHuffTable = acTable.Values;
- nint lastValuableIndex = block.GetLastNonZeroIndex();
-
int runLength = 0;
ref short blockRef = ref Unsafe.As(ref block);
- for (nint zig = 1; zig <= lastValuableIndex; zig++)
+ for (nint zig = start; zig < end; zig++)
{
const int zeroRun1 = 1 << 4;
const int zeroRun16 = 16 << 4;
@@ -413,14 +583,25 @@ internal class HuffmanScanEncoder
}
// if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over
- // this can be done for any number of trailing zeros, even when all 63 ac values are zero
- // (Block8x8F.Size - 1) == 63 - last index of the mcu elements
- if (lastValuableIndex != Block8x8F.Size - 1)
+ if (runLength > 0)
{
this.EmitHuff(acHuffTable, 0x00);
}
}
+ private void WriteBlock(
+ Component component,
+ ref Block8x8 block,
+ ref HuffmanLut dcTable,
+ ref HuffmanLut acTable)
+ {
+ this.WriteDc(component, ref block, ref dcTable);
+ this.WriteAcBlock(ref block, 1, 64, ref acTable);
+ }
+
+ private void WriteRestart(int restart) =>
+ this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2);
+
///
/// Emits the most significant count of bits to the buffer.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 2320fe1791..707baa1a88 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -16,7 +16,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif;
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;
@@ -1473,7 +1472,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
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
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
index 0daaae112c..69f04f1dcf 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
@@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder
///
private int? quality;
+ ///
+ /// Backing field for
+ ///
+ private int progressiveScans = 4;
+
+ ///
+ /// Backing field for
+ ///
+ private int restartInterval;
+
///
/// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min).
@@ -33,6 +43,56 @@ public sealed class JpegEncoder : ImageEncoder
}
}
+ ///
+ /// Gets a value indicating whether progressive encoding is used.
+ ///
+ public bool Progressive { get; init; }
+
+ ///
+ /// Gets number of scans per component for progressive encoding.
+ /// Defaults to 4.
+ ///
+ ///
+ /// Number of scans must be between 2 and 64.
+ /// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients.
+ ///
+ /// Progressive scans must be in [2..64] range.
+ public int ProgressiveScans
+ {
+ get => this.progressiveScans;
+ init
+ {
+ if (value is < 2 or > 64)
+ {
+ throw new ArgumentException("Progressive scans must be in [2..64] range.");
+ }
+
+ this.progressiveScans = value;
+ }
+ }
+
+ ///
+ /// Gets numbers of MCUs between restart markers.
+ /// Defaults to 0.
+ ///
+ ///
+ /// Currently supported in progressive encoding only.
+ ///
+ /// Restart interval must be in [0..65535] range.
+ public int RestartInterval
+ {
+ get => this.restartInterval;
+ init
+ {
+ if (value is < 0 or > 65535)
+ {
+ throw new ArgumentException("Restart interval must be in [0..65535] range.");
+ }
+
+ this.restartInterval = value;
+ }
+ }
+
///
/// Gets the component encoding mode.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index a6ff623660..34028c2f83 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -100,12 +100,15 @@ internal sealed unsafe partial class JpegEncoderCore
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
// Write the Huffman tables.
- HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
+ HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
// Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer);
+ // Write define restart interval
+ this.WriteDri(this.encoder.RestartInterval, buffer);
+
// Write scans with actual pixel data
using SpectralConverter spectralConverter = new(frame, image, this.QuantizationTables);
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
@@ -426,6 +429,25 @@ internal sealed unsafe partial class JpegEncoderCore
}
}
+ ///
+ /// Writes the DRI marker
+ ///
+ /// Numbers of MCUs between restart markers.
+ /// Temporary buffer.
+ private void WriteDri(int restartInterval, Span buffer)
+ {
+ if (restartInterval <= 0)
+ {
+ return;
+ }
+
+ this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer);
+
+ buffer[1] = (byte)(restartInterval & 0xff);
+ buffer[0] = (byte)(restartInterval >> 8);
+ this.outputStream.Write(buffer, 0, 2);
+ }
+
///
/// Writes the App1 header.
///
@@ -563,7 +585,8 @@ internal sealed unsafe partial class JpegEncoderCore
// Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * components.Length);
- this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer);
+ byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0;
+ this.WriteMarkerHeader(marker, markerlen, buffer);
buffer[5] = (byte)components.Length;
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
buffer[1] = (byte)(height >> 8);
@@ -597,7 +620,17 @@ internal sealed unsafe partial class JpegEncoderCore
///
/// The collecction of component configuration items.
/// Temporary buffer.
- private void WriteStartOfScan(Span components, Span buffer)
+ private void WriteStartOfScan(Span components, Span buffer) =>
+ this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
+
+ ///
+ /// Writes the StartOfScan marker.
+ ///
+ /// The collecction of component configuration items.
+ /// Temporary buffer.
+ /// Start of spectral selection
+ /// End of spectral selection
+ private void WriteStartOfScan(Span components, Span buffer, byte spectralStart, byte spectralEnd)
{
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c",
@@ -630,8 +663,8 @@ internal sealed unsafe partial class JpegEncoderCore
buffer[i2 + 6] = (byte)tableSelectors;
}
- buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
- buffer[sosSize] = 0x3f; // Se - End of spectral selection.
+ buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection.
+ buffer[sosSize] = spectralEnd; // Se - End of spectral selection.
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(buffer, 0, sosSize + 2);
}
@@ -666,7 +699,14 @@ internal sealed unsafe partial class JpegEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- if (frame.Components.Length == 1)
+ if (this.encoder.Progressive)
+ {
+ frame.AllocateComponents(fullScan: true);
+ spectralConverter.ConvertFull();
+
+ this.WriteProgressiveScans(frame, frameConfig, encoder, buffer, cancellationToken);
+ }
+ else if (frame.Components.Length == 1)
{
frame.AllocateComponents(fullScan: false);
@@ -694,6 +734,50 @@ internal sealed unsafe partial class JpegEncoderCore
}
}
+ ///
+ /// Writes the progressive scans
+ ///
+ /// The type of pixel format.
+ /// The current frame.
+ /// The frame configuration.
+ /// The scan encoder.
+ /// Temporary buffer.
+ /// The cancellation token.
+ private void WriteProgressiveScans(
+ JpegFrame frame,
+ JpegFrameConfig frameConfig,
+ HuffmanScanEncoder encoder,
+ Span buffer,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ Span components = frameConfig.Components;
+
+ // Phase 1: DC scan
+ for (int i = 0; i < frame.Components.Length; i++)
+ {
+ this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
+
+ encoder.EncodeDcScan(frame.Components[i], cancellationToken);
+ }
+
+ // Phase 2: AC scans
+ int acScans = this.encoder.ProgressiveScans - 1;
+ int valuesPerScan = 64 / acScans;
+ for (int scan = 0; scan < acScans; scan++)
+ {
+ int start = Math.Max(1, scan * valuesPerScan);
+ int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan;
+
+ for (int i = 0; i < components.Length; i++)
+ {
+ this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
+
+ encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
+ }
+ }
+ }
+
///
/// Writes the header for a marker with the given length.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
index f2f34ec496..fe4855dc77 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
@@ -199,6 +199,12 @@ public class JpegMetadata : IFormatMetadata
Quality = this.Quality,
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs
index fec4beda7c..d852f3c8eb 100644
--- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs
+++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs
@@ -129,6 +129,12 @@ public class PbmMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo(),
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index dcbaf3140d..d9f71e1b56 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
///
/// Image encoder for writing image data to a stream in png format.
///
-public class PngEncoder : QuantizingImageEncoder
+public class PngEncoder : QuantizingAnimatedImageEncoder
{
///
/// Initializes a new instance of the class.
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 4bbb68358f..398c80634c 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -123,6 +123,24 @@ internal sealed class PngEncoderCore : IDisposable
///
private int derivedTransparencyIndex = -1;
+ ///
+ /// 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 .
+ ///
+ private readonly Color? backgroundColor;
+
+ ///
+ /// The number of times any animation is repeated.
+ ///
+ private readonly ushort? repeatCount;
+
+ ///
+ /// Whether the root frame is shown as part of the animated sequence.
+ ///
+ private readonly bool? animateRootFrame;
+
///
/// A reusable Crc32 hashing instance.
///
@@ -139,6 +157,9 @@ internal sealed class PngEncoderCore : IDisposable
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.quantizer = encoder.Quantizer;
+ this.backgroundColor = encoder.BackgroundColor;
+ this.repeatCount = encoder.RepeatCount;
+ this.animateRootFrame = encoder.AnimateRootFrame;
}
///
@@ -171,7 +192,7 @@ internal sealed class PngEncoderCore : IDisposable
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
- ClearTransparentPixels(currentFrame);
+ ClearTransparentPixels(currentFrame, Color.Transparent);
}
// Do not move this. We require an accurate bit depth for the header chunk.
@@ -194,11 +215,15 @@ internal sealed class PngEncoderCore : IDisposable
if (image.Frames.Count > 1)
{
- this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount);
+ this.WriteAnimationControlChunk(
+ stream,
+ (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)),
+ this.repeatCount ?? pngMetadata.RepeatCount);
}
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
- if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1)
+ bool userAnimateRootFrame = this.animateRootFrame == true;
+ if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1)
{
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
@@ -231,16 +256,24 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size);
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+
ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
+ Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
+ ? this.backgroundColor ?? Color.Transparent
+ : Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@@ -249,12 +282,12 @@ internal sealed class PngEncoderCore : IDisposable
currentFrame,
nextFrame,
encodingFrame,
- Color.Transparent,
+ background,
blend);
if (clearTransparency)
{
- ClearTransparentPixels(encodingFrame);
+ ClearTransparentPixels(encodingFrame, background);
}
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
@@ -291,12 +324,13 @@ internal sealed class PngEncoderCore : IDisposable
///
/// The type of the pixel.
/// The cloned image frame where the transparent pixels will be changed.
- private static void ClearTransparentPixels(ImageFrame clone)
+ /// The color to replace transparent pixels with.
+ private static void ClearTransparentPixels(ImageFrame clone, Color color)
where TPixel : unmanaged, IPixel
=> clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
- Rgba32 transparent = Color.Transparent.ToPixel();
+ Rgba32 transparent = color.ToPixel();
for (int y = 0; y < accessor.Height; y++)
{
Span span = accessor.GetRowSpan(y);
diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
index c142a1c8e0..b8086cd6d1 100644
--- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
@@ -84,6 +85,12 @@ public class PngFrameMetadata : IFormatFrameMetadata
};
}
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index a7b3672ef5..00cba088cb 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -247,6 +247,12 @@ public class PngMetadata : IFormatMetadata
RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue),
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
index e2062014d7..e463d511d2 100644
--- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
+++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
@@ -88,6 +88,12 @@ public class QoiMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo()
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/QuantizingImageEncoder.cs b/src/ImageSharp/Formats/QuantizingImageEncoder.cs
deleted file mode 100644
index 330d8988c7..0000000000
--- a/src/ImageSharp/Formats/QuantizingImageEncoder.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using SixLabors.ImageSharp.Processing.Processors.Quantization;
-
-namespace SixLabors.ImageSharp.Formats;
-
-///
-/// Acts as a base class for all image encoders that allow color palette generation via quantization.
-///
-public abstract class QuantizingImageEncoder : ImageEncoder
-{
- ///
- /// Gets the quantizer used to generate the color palette.
- ///
- public IQuantizer? Quantizer { get; init; }
-
- ///
- /// Gets the used for quantization when building color palettes.
- ///
- public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
-}
diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs
index 58b5119523..8d40f86464 100644
--- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs
+++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs
@@ -94,6 +94,12 @@ public class TgaMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo()
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 2356d45e47..d699a7b631 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -167,11 +167,18 @@ internal class TiffDecoderCore : ImageDecoderCore
this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
+ Size? size = null;
uint frameCount = 0;
foreach (ExifProfile ifd in directories)
{
cancellationToken.ThrowIfCancellationRequested();
- ImageFrame frame = this.DecodeFrame(ifd, cancellationToken);
+ ImageFrame frame = this.DecodeFrame(ifd, size, cancellationToken);
+
+ if (!size.HasValue)
+ {
+ size = frame.Size;
+ }
+
frames.Add(frame);
framesMetadata.Add(frame.Metadata);
@@ -181,19 +188,8 @@ internal class TiffDecoderCore : ImageDecoderCore
}
}
+ this.Dimensions = frames[0].Size;
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
-
- // TODO: Tiff frames can have different sizes.
- ImageFrame root = frames[0];
- this.Dimensions = root.Size();
- foreach (ImageFrame frame in frames)
- {
- if (frame.Size() != root.Size())
- {
- TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
- }
- }
-
return new Image(this.configuration, metadata, frames);
}
catch
@@ -215,17 +211,21 @@ internal class TiffDecoderCore : ImageDecoderCore
IList directories = reader.Read();
List framesMetadata = [];
- foreach (ExifProfile dir in directories)
+ int width = 0;
+ int height = 0;
+
+ for (int i = 0; i < directories.Count; i++)
{
- framesMetadata.Add(this.CreateFrameMetadata(dir));
- }
+ (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta
+ = this.CreateFrameMetadata(directories[i]);
- ExifProfile rootFrameExifProfile = directories[0];
+ framesMetadata.Add(meta.FrameMetadata);
- ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
+ width = Math.Max(width, meta.TiffMetadata.EncodingWidth);
+ height = Math.Max(height, meta.TiffMetadata.EncodingHeight);
+ }
- int width = GetImageWidth(rootFrameExifProfile);
- int height = GetImageHeight(rootFrameExifProfile);
+ ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
return new ImageInfo(new(width, height), metadata, framesMetadata);
}
@@ -235,31 +235,46 @@ internal class TiffDecoderCore : ImageDecoderCore
///
/// The pixel format.
/// The IFD tags.
+ /// The previously determined root frame size if decoded.
/// The token to monitor cancellation.
/// The tiff frame.
- private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken)
+ private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags);
- bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata());
+ (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags);
+ bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata);
+
+ int width = metadata.TiffFrameMetadata.EncodingWidth;
+ int height = metadata.TiffFrameMetadata.EncodingHeight;
+
+ // If size has a value and the width/height off the tiff is smaller we much capture the delta.
+ if (size.HasValue)
+ {
+ if (size.Value.Width < width || size.Value.Height < height)
+ {
+ TiffThrowHelper.ThrowNotSupported("Images with frames of size greater than the root frame are not supported.");
+ }
+ }
+ else
+ {
+ size = new Size(width, height);
+ }
- int width = GetImageWidth(tags);
- int height = GetImageHeight(tags);
- ImageFrame frame = new(this.configuration, width, height, imageFrameMetaData);
+ ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata);
if (isTiled)
{
- this.DecodeImageWithTiles(tags, frame, cancellationToken);
+ this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
}
else
{
- this.DecodeImageWithStrips(tags, frame, cancellationToken);
+ this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
}
return frame;
}
- private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags)
+ private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags)
{
ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata)
@@ -267,9 +282,10 @@ internal class TiffDecoderCore : ImageDecoderCore
imageFrameMetaData.ExifProfile = tags;
}
- TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags);
+ TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags);
+ imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata);
- return imageFrameMetaData;
+ return (imageFrameMetaData, tiffMetadata);
}
///
@@ -278,8 +294,10 @@ internal class TiffDecoderCore : ImageDecoderCore
/// The pixel format.
/// The IFD tags.
/// The image frame to decode into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The token to monitor cancellation.
- private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken)
+ private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int rowsPerStrip;
@@ -302,6 +320,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{
this.DecodeStripsPlanar(
frame,
+ width,
+ height,
rowsPerStrip,
stripOffsets,
stripByteCounts,
@@ -311,6 +331,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{
this.DecodeStripsChunky(
frame,
+ width,
+ height,
rowsPerStrip,
stripOffsets,
stripByteCounts,
@@ -324,13 +346,13 @@ internal class TiffDecoderCore : ImageDecoderCore
/// The pixel format.
/// The IFD tags.
/// The image frame to decode into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The token to monitor cancellation.
- private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken)
+ private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Buffer2D pixels = frame.PixelBuffer;
- int width = pixels.Width;
- int height = pixels.Height;
if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue valueWidth))
{
@@ -384,11 +406,20 @@ internal class TiffDecoderCore : ImageDecoderCore
///
/// The pixel format.
/// The image frame to decode data into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The number of rows per strip of data.
/// An array of byte offsets to each strip in the image.
/// An array of the size of each strip (in bytes).
/// The token to monitor cancellation.
- private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken)
+ private void DecodeStripsPlanar(
+ ImageFrame frame,
+ int width,
+ int height,
+ int rowsPerStrip,
+ Span stripOffsets,
+ Span stripByteCounts,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int stripsPerPixel = this.BitsPerSample.Channels;
@@ -403,18 +434,18 @@ internal class TiffDecoderCore : ImageDecoderCore
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
- int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
+ int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex);
stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize);
}
- using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel);
+ using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel);
TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder();
for (int i = 0; i < stripsPerPlane; i++)
{
cancellationToken.ThrowIfCancellationRequested();
- int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
+ int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip;
int stripIndex = i;
for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
@@ -430,7 +461,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripIndex += stripsPerPlane;
}
- colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight);
+ colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight);
}
}
finally
@@ -447,39 +478,48 @@ internal class TiffDecoderCore : ImageDecoderCore
///
/// The pixel format.
/// The image frame to decode data into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The rows per strip.
/// The strip offsets.
/// The strip byte counts.
/// The token to monitor cancellation.
- private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken)
+ private void DecodeStripsChunky(
+ ImageFrame frame,
+ int width,
+ int height,
+ int rowsPerStrip,
+ Span stripOffsets,
+ Span stripByteCounts,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
{
- rowsPerStrip = frame.Height;
+ rowsPerStrip = height;
}
- int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
+ int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean);
Span stripBufferSpan = stripBuffer.GetSpan();
Buffer2D pixels = frame.PixelBuffer;
- using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel);
+ using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel);
TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder();
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
- int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0
+ int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
? rowsPerStrip
- : frame.Height % rowsPerStrip;
+ : height % rowsPerStrip;
int top = rowsPerStrip * stripIndex;
- if (top + stripHeight > frame.Height)
+ if (top + stripHeight > height)
{
// Make sure we ignore any strips that are not needed for the image (if too many are present).
break;
@@ -493,7 +533,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripBufferSpan,
cancellationToken);
- colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight);
+ colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
}
}
@@ -790,38 +830,6 @@ internal class TiffDecoderCore : ImageDecoderCore
return bytesPerRow * height;
}
- ///
- /// Gets the width of the image frame.
- ///
- /// The image frame exif profile.
- /// The image width.
- private static int GetImageWidth(ExifProfile exifProfile)
- {
- if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue width))
- {
- TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
- }
-
- DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
-
- return (int)width.Value;
- }
-
- ///
- /// Gets the height of the image frame.
- ///
- /// The image frame exif profile.
- /// The image height.
- private static int GetImageHeight(ExifProfile exifProfile)
- {
- if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue height))
- {
- TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
- }
-
- return (int)height.Value;
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8);
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index 5f91fd7393..b560067f3f 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -189,11 +189,22 @@ internal sealed class TiffEncoderCore
long ifdOffset)
where TPixel : unmanaged, IPixel
{
+ // Get the width and height of the frame.
+ // This can differ from the frame bounds in-memory if the image represents only
+ // a subregion.
+ TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata();
+ int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width;
+ int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height;
+
+ width = Math.Min(width, frame.Width);
+ height = Math.Min(height, frame.Height);
+ Size encodingSize = new(width, height);
+
using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
compression,
writer.BaseStream,
this.memoryAllocator,
- frame.Width,
+ width,
(int)bitsPerPixel,
this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
@@ -202,6 +213,7 @@ internal sealed class TiffEncoderCore
using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create(
this.PhotometricInterpretation,
frame,
+ encodingSize,
this.quantizer,
this.pixelSamplingStrategy,
this.memoryAllocator,
@@ -209,7 +221,7 @@ internal sealed class TiffEncoderCore
entriesCollector,
(int)bitsPerPixel);
- int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType);
+ int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip);
@@ -222,7 +234,7 @@ internal sealed class TiffEncoderCore
// Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata);
- entriesCollector.ProcessFrameInfo(frame, imageMetadata);
+ entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata);
entriesCollector.ProcessImageFormat(this);
if (writer.Position % 2 != 0)
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index c8e28111ec..803b77fb0a 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
@@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
=> new MetadataProcessor(this).Process(frame, skipMetadata);
- public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
- => new FrameInfoProcessor(this).Process(frame, imageMetadata);
+ public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
+ => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata);
public void ProcessImageFormat(TiffEncoderCore encoder)
=> new ImageFormatProcessor(this).Process(encoder);
@@ -267,16 +267,16 @@ internal class TiffEncoderEntriesCollector
{
}
- public void Process(ImageFrame frame, ImageMetadata imageMetadata)
+ public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
{
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth)
{
- Value = (uint)frame.Width
+ Value = (uint)encodingSize.Width
});
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength)
{
- Value = (uint)frame.Height
+ Value = (uint)encodingSize.Height
});
this.ProcessResolution(imageMetadata);
diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
index bb5da37411..189fee8b0c 100644
--- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
@@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff;
@@ -29,6 +30,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata
this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor;
this.InkSet = other.InkSet;
+ this.EncodingWidth = other.EncodingWidth;
+ this.EncodingHeight = other.EncodingHeight;
}
///
@@ -61,13 +64,59 @@ public class TiffFrameMetadata : IFormatFrameMetadata
///
public TiffInkSet? InkSet { get; set; }
+ ///
+ /// Gets or sets the encoding width.
+ ///
+ public int EncodingWidth { get; set; }
+
+ ///
+ /// Gets or sets the encoding height.
+ ///
+ public int EncodingHeight { get; set; }
+
///
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
- => new();
+ {
+ TiffFrameMetadata frameMetadata = new();
+ if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue)
+ {
+ frameMetadata.EncodingWidth = metadata.EncodingWidth.Value;
+ frameMetadata.EncodingHeight = metadata.EncodingHeight.Value;
+ }
+
+ return frameMetadata;
+ }
///
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
- => new();
+ => new()
+ {
+ EncodingWidth = this.EncodingWidth,
+ EncodingHeight = this.EncodingHeight
+ };
+
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ 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);
+
+ // Overwrite the EXIF dimensional metadata with the encoding dimensions of the image.
+ destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight);
+ }
+
+ private static int Scale(int value, int destination, float ratio)
+ {
+ if (value <= 0)
+ {
+ return destination;
+ }
+
+ return Math.Min((int)MathF.Ceiling(value * ratio), destination);
+ }
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@@ -93,43 +142,75 @@ public class TiffFrameMetadata : IFormatFrameMetadata
///
/// The tiff frame meta data.
/// The Exif profile containing tiff frame directory tags.
- internal static void Parse(TiffFrameMetadata meta, ExifProfile profile)
+ private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
{
- if (profile != null)
+ meta.EncodingWidth = GetImageWidth(profile);
+ meta.EncodingHeight = GetImageHeight(profile);
+
+ if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue)
+ && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
+ {
+ meta.BitsPerSample = bitsPerSample;
+ }
+
+ meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
+
+ if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue))
{
- if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue)
- && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
- {
- meta.BitsPerSample = bitsPerSample;
- }
-
- meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
-
- if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue))
- {
- meta.Compression = (TiffCompression)compressionValue.Value;
- }
-
- if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue))
- {
- meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
- }
-
- if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue))
- {
- meta.Predictor = (TiffPredictor)predictorValue.Value;
- }
-
- if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue))
- {
- meta.InkSet = (TiffInkSet)inkSetValue.Value;
- }
-
- // TODO: Why do we remove this? Encoding should overwrite.
- profile.RemoveValue(ExifTag.BitsPerSample);
- profile.RemoveValue(ExifTag.Compression);
- profile.RemoveValue(ExifTag.PhotometricInterpretation);
- profile.RemoveValue(ExifTag.Predictor);
+ meta.Compression = (TiffCompression)compressionValue.Value;
}
+
+ if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue))
+ {
+ meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
+ }
+
+ if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue))
+ {
+ meta.Predictor = (TiffPredictor)predictorValue.Value;
+ }
+
+ if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue))
+ {
+ meta.InkSet = (TiffInkSet)inkSetValue.Value;
+ }
+
+ // Remove values, we've explicitly captured them and they could change on encode.
+ profile.RemoveValue(ExifTag.BitsPerSample);
+ profile.RemoveValue(ExifTag.Compression);
+ profile.RemoveValue(ExifTag.PhotometricInterpretation);
+ profile.RemoveValue(ExifTag.Predictor);
+ }
+
+ ///
+ /// Gets the width of the image frame.
+ ///
+ /// The image frame exif profile.
+ /// The image width.
+ private static int GetImageWidth(ExifProfile exifProfile)
+ {
+ if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue? width))
+ {
+ TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
+ }
+
+ DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
+
+ return (int)width.Value;
+ }
+
+ ///
+ /// Gets the height of the image frame.
+ ///
+ /// The image frame exif profile.
+ /// The image height.
+ private static int GetImageHeight(ExifProfile exifProfile)
+ {
+ if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue? height))
+ {
+ TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
+ }
+
+ return (int)height.Value;
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs
index cc70941d51..e965fcb4f6 100644
--- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs
@@ -180,6 +180,12 @@ public class TiffMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo()
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
index c4a7492553..9fd730f416 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
@@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter : IDisposable
{
private bool isDisposed;
- protected TiffBaseColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
+ protected TiffBaseColorWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
{
+ this.Width = encodingSize.Width;
+ this.Height = encodingSize.Height;
this.Image = image;
this.MemoryAllocator = memoryAllocator;
this.Configuration = configuration;
@@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter : IDisposable
///
public abstract int BitsPerPixel { get; }
+ ///
+ /// Gets the width of the portion of the image to be encoded.
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the height of the portion of the image to be encoded.
+ ///
+ public int Height { get; }
+
///
/// Gets the bytes per row.
///
- public int BytesPerRow => (int)(((uint)(this.Image.Width * this.BitsPerPixel) + 7) / 8);
+ public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8);
protected ImageFrame Image { get; }
@@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter : IDisposable
public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip)
{
DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer");
- int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip;
+ int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip;
uint[] stripOffsets = new uint[stripsCount];
uint[] stripByteCounts = new uint[stripsCount];
int stripIndex = 0;
compressor.Initialize(rowsPerStrip);
- for (int y = 0; y < this.Image.Height; y += rowsPerStrip)
+ for (int y = 0; y < this.Height; y += rowsPerStrip)
{
long offset = compressor.Output.Position;
- int height = Math.Min(rowsPerStrip, this.Image.Height - y);
+ int height = Math.Min(rowsPerStrip, this.Height - y);
this.EncodeStrip(y, height, compressor);
long endOffset = compressor.Output.Position;
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
index a6f4c31060..647ff8a1a3 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
@@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter
private IMemoryOwner bitStrip;
- public TiffBiColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffBiColorWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
// Convert image to black and white.
- this.imageBlackWhite = new Image(configuration, new ImageMetadata(), new[] { image.Clone() });
+ this.imageBlackWhite = new Image(configuration, new ImageMetadata(), [image.Clone()]);
this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg));
}
@@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter
///
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- int width = this.Image.Width;
+ int width = this.Width;
- if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax)
+ if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax)
{
// Special case for T4BitCompressor.
int stripPixels = width * height;
@@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter
int bitIndex = 0;
int byteIndex = 0;
Span outputRow = rows[(outputRowIdx * this.BytesPerRow)..];
- Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row);
+ Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width];
PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width);
- for (int x = 0; x < this.Image.Width; x++)
+ for (int x = 0; x < this.Width; x++)
{
int shift = 7 - bitIndex;
if (pixelAsGraySpan[x] == 255)
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
index 96c8aeb324..31a1b0e414 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
@@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory
public static TiffBaseColorWriter Create(
TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame image,
+ Size encodingSize,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
@@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory
TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel)
where TPixel : unmanaged, IPixel
- {
- switch (photometricInterpretation)
+ => photometricInterpretation switch
{
- case TiffPhotometricInterpretation.PaletteColor:
- return new TiffPaletteWriter(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel);
- case TiffPhotometricInterpretation.BlackIsZero:
- case TiffPhotometricInterpretation.WhiteIsZero:
- return bitsPerPixel switch
- {
- 1 => new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector),
- 16 => new TiffGrayL16Writer(image, memoryAllocator, configuration, entriesCollector),
- _ => new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector)
- };
-
- default:
- return new TiffRgbWriter(image, memoryAllocator, configuration, entriesCollector);
- }
- }
+ TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel),
+ TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch
+ {
+ 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector),
+ 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector),
+ _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector)
+ },
+ _ => new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector),
+ };
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
index 007857148a..67dde493c5 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
@@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
///
/// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr).
///
+/// The tpe of pixel format.
internal abstract class TiffCompositeColorWriter : TiffBaseColorWriter
where TPixel : unmanaged, IPixel
{
private IMemoryOwner rowBuffer;
- protected TiffCompositeColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ protected TiffCompositeColorWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- if (this.rowBuffer == null)
- {
- this.rowBuffer = this.MemoryAllocator.Allocate(this.BytesPerRow * height);
- }
-
- this.rowBuffer.Clear();
+ (this.rowBuffer ??= this.MemoryAllocator.Allocate(this.BytesPerRow * height)).Clear();
Span outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)];
- int width = this.Image.Width;
+ int width = this.Width;
using IMemoryOwner stripPixelBuffer = this.MemoryAllocator.Allocate(height * width);
Span stripPixels = stripPixelBuffer.GetSpan();
int lastRow = y + height;
int stripPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
- Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row);
+ Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width];
stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width));
stripPixelsRowIdx++;
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
index 3e0e074e95..857f551f41 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
@@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter
where TPixel : unmanaged, IPixel
{
- public TiffGrayL16Writer(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffGrayL16Writer(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
@@ -18,5 +23,6 @@ internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter 16;
///
- protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
+ protected override void EncodePixels(Span pixels, Span buffer)
+ => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
index b2a476b9aa..4a037f0d33 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
@@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayWriter : TiffCompositeColorWriter
where TPixel : unmanaged, IPixel
{
- public TiffGrayWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffGrayWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
@@ -18,5 +23,6 @@ internal sealed class TiffGrayWriter : TiffCompositeColorWriter
public override int BitsPerPixel => 8;
///
- protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length);
+ protected override void EncodePixels(Span pixels, Span buffer)
+ => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length);
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
index d9a0960d9b..da66373631 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
@@ -23,13 +23,14 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter
public TiffPaletteWriter(
ImageFrame frame,
+ Size encodingSize,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel)
- : base(frame, memoryAllocator, configuration, entriesCollector)
+ : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector)
{
DebugGuard.NotNull(quantizer, nameof(quantizer));
DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy));
@@ -49,7 +50,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter
});
frameQuantizer.BuildPalette(pixelSamplingStrategy, frame);
- this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
+ this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize));
this.AddColorMapTag();
}
@@ -60,7 +61,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter
///
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- int width = this.Image.Width;
+ int width = this.quantizedFrame.Width;
if (this.BitsPerPixel == 4)
{
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
index 3494b6ceae..93c46a92e4 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
@@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffRgbWriter : TiffCompositeColorWriter
where TPixel : unmanaged, IPixel
{
- public TiffRgbWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffRgbWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
@@ -18,5 +23,6 @@ internal sealed class TiffRgbWriter : TiffCompositeColorWriter
public override int BitsPerPixel => 24;
///
- protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length);
+ protected override void EncodePixels(Span pixels, Span buffer)
+ => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length);
}
diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
index eccd9ede8e..a9e63a3d0e 100644
--- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
@@ -183,7 +183,7 @@ internal class AlphaDecoder : IDisposable
else
{
this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span);
- this.ExtractAlphaRows(this.Vp8LDec);
+ this.ExtractAlphaRows(this.Vp8LDec, this.Width);
}
}
@@ -257,14 +257,15 @@ internal class AlphaDecoder : IDisposable
/// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet.
///
/// The VP8L decoder.
- private void ExtractAlphaRows(Vp8LDecoder dec)
+ /// The image width.
+ private void ExtractAlphaRows(Vp8LDecoder dec, int width)
{
int numRowsToProcess = dec.Height;
- int width = dec.Width;
Span input = dec.Pixels.Memory.Span;
Span output = this.Alpha.Memory.Span;
// Extract alpha (which is stored in the green plane).
+ // the final width (!= dec->width_)
int pixelCount = width * numRowsToProcess;
WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator);
ExtractGreen(input, output, pixelCount);
diff --git a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
index 024adb7c23..5287f0b753 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
@@ -269,7 +269,11 @@ internal static unsafe class LosslessUtils
///
/// The transform data contains color table size and the entries in the color table.
/// The pixel data to apply the reverse transform on.
- public static void ColorIndexInverseTransform(Vp8LTransform transform, Span pixelData)
+ /// The resulting pixel data with the reversed transformation data.
+ public static void ColorIndexInverseTransform(
+ Vp8LTransform transform,
+ Span pixelData,
+ Span outputSpan)
{
int bitsPerPixel = 8 >> transform.Bits;
int width = transform.XSize;
@@ -282,7 +286,6 @@ internal static unsafe class LosslessUtils
int countMask = pixelsPerByte - 1;
int bitMask = (1 << bitsPerPixel) - 1;
- uint[] decodedPixelData = new uint[width * height];
int pixelDataPos = 0;
for (int y = 0; y < height; y++)
{
@@ -298,12 +301,12 @@ internal static unsafe class LosslessUtils
packedPixels = GetArgbIndex(pixelData[pixelDataPos++]);
}
- decodedPixelData[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)];
+ outputSpan[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)];
packedPixels >>= bitsPerPixel;
}
}
- decodedPixelData.AsSpan().CopyTo(pixelData);
+ outputSpan.CopyTo(pixelData);
}
else
{
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 244691e77e..e077249696 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -236,7 +236,7 @@ internal class Vp8LEncoder : IDisposable
///
public Vp8LHashChain HashChain { get; }
- public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation)
+ public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation, ushort? repeatCount)
where TPixel : unmanaged, IPixel
{
// Write bytes from the bit-writer buffer to the stream.
@@ -258,7 +258,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
- BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount);
}
return vp8x;
@@ -315,8 +315,8 @@ internal class Vp8LEncoder : IDisposable
(uint)bounds.Width,
(uint)bounds.Height,
frameMetadata.FrameDelay,
- frameMetadata.BlendMethod,
- frameMetadata.DisposalMethod)
+ frameMetadata.BlendMode,
+ frameMetadata.DisposalMode)
.WriteHeaderTo(stream);
}
diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
index e4c2a7ddf6..6de3ae7497 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
@@ -684,6 +684,7 @@ internal sealed class WebpLosslessDecoder
List transforms = decoder.Transforms;
for (int i = transforms.Count - 1; i >= 0; i--)
{
+ // TODO: Review these 1D allocations. They could conceivably exceed limits.
Vp8LTransform transform = transforms[i];
switch (transform.TransformType)
{
@@ -701,7 +702,11 @@ internal sealed class WebpLosslessDecoder
LosslessUtils.ColorSpaceInverseTransform(transform, pixelData);
break;
case Vp8LTransformType.ColorIndexingTransform:
- LosslessUtils.ColorIndexInverseTransform(transform, pixelData);
+ using (IMemoryOwner output = memoryAllocator.Allocate(transform.XSize * transform.YSize, AllocationOptions.Clean))
+ {
+ LosslessUtils.ColorIndexInverseTransform(transform, pixelData, output.GetSpan());
+ }
+
break;
}
}
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 3ad72f7d00..d22d357fe3 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -495,8 +495,8 @@ internal class Vp8Encoder : IDisposable
(uint)bounds.Width,
(uint)bounds.Height,
frameMetadata.FrameDelay,
- frameMetadata.BlendMethod,
- frameMetadata.DisposalMethod)
+ frameMetadata.BlendMode,
+ frameMetadata.DisposalMode)
.WriteHeaderTo(stream);
}
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs
index 82f00e8760..c645816d4b 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs
@@ -667,12 +667,12 @@ internal static unsafe class Vp8Encoding
// V block.
dst = dst[8..];
- if (top != default)
+ if (!top.IsEmpty)
{
top = top[8..];
}
- if (left != default)
+ if (!left.IsEmpty)
{
left = left[16..];
}
@@ -701,7 +701,7 @@ internal static unsafe class Vp8Encoding
private static void VerticalPred(Span dst, Span top, int size)
{
- if (top != default)
+ if (!top.IsEmpty)
{
for (int j = 0; j < size; j++)
{
@@ -716,7 +716,7 @@ internal static unsafe class Vp8Encoding
public static void HorizontalPred(Span dst, Span left, int size)
{
- if (left != default)
+ if (!left.IsEmpty)
{
left = left[1..]; // in the reference implementation, left starts at - 1.
for (int j = 0; j < size; j++)
@@ -732,9 +732,9 @@ internal static unsafe class Vp8Encoding
public static void TrueMotion(Span dst, Span