Skip to content

Commit 77a5883

Browse files
authored
Merge pull request #2725 from kroymann/detect-format-async
Fix async-over-sync issue in Image.DecodeAsync()
2 parents b8cf630 + f5ed9ac commit 77a5883

File tree

2 files changed

+79
-12
lines changed

2 files changed

+79
-12
lines changed

src/ImageSharp/Image.Decode.cs

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Buffers;
45
using SixLabors.ImageSharp.Formats;
56
using SixLabors.ImageSharp.Memory;
67
using SixLabors.ImageSharp.Metadata;
@@ -67,20 +68,70 @@ private static IImageFormat InternalDetectFormat(Configuration configuration, St
6768
int i;
6869
do
6970
{
70-
i = stream.Read(headersBuffer, n, headerSize - n);
71+
i = stream.Read(headersBuffer[n..headerSize]);
7172
n += i;
7273
}
7374
while (n < headerSize && i > 0);
7475

7576
stream.Position = startPosition;
7677

78+
return InternalDetectFormat(configuration, headersBuffer[..n]);
79+
}
80+
81+
/// <summary>
82+
/// By reading the header on the provided stream this calculates the images format.
83+
/// </summary>
84+
/// <param name="configuration">The general configuration.</param>
85+
/// <param name="stream">The image stream to read the header from.</param>
86+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
87+
/// <returns>The mime type or null if none found.</returns>
88+
/// <exception cref="UnknownImageFormatException">The input format is not recognized.</exception>
89+
private static async ValueTask<IImageFormat> InternalDetectFormatAsync(
90+
Configuration configuration,
91+
Stream stream,
92+
CancellationToken cancellationToken)
93+
{
94+
// We take a minimum of the stream length vs the max header size and always check below
95+
// to ensure that only formats that headers fit within the given buffer length are tested.
96+
int headerSize = (int)Math.Min(configuration.MaxHeaderSize, stream.Length);
97+
if (headerSize <= 0)
98+
{
99+
ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager);
100+
}
101+
102+
using (IMemoryOwner<byte> memoryOwner = configuration.MemoryAllocator.Allocate<byte>(headerSize))
103+
{
104+
Memory<byte> headersBuffer = memoryOwner.Memory;
105+
long startPosition = stream.Position;
106+
107+
// Read doesn't always guarantee the full returned length so read a byte
108+
// at a time until we get either our count or hit the end of the stream.
109+
int n = 0;
110+
int i;
111+
do
112+
{
113+
i = await stream.ReadAsync(headersBuffer[n..headerSize], cancellationToken);
114+
n += i;
115+
}
116+
while (n < headerSize && i > 0);
117+
118+
stream.Position = startPosition;
119+
120+
return InternalDetectFormat(configuration, headersBuffer.Span[..n]);
121+
}
122+
}
123+
124+
private static IImageFormat InternalDetectFormat(
125+
Configuration configuration,
126+
ReadOnlySpan<byte> headersBuffer)
127+
{
77128
// Does the given stream contain enough data to fit in the header for the format
78129
// and does that data match the format specification?
79130
// Individual formats should still check since they are public.
80131
IImageFormat? format = null;
81132
foreach (IImageFormatDetector formatDetector in configuration.ImageFormatsManager.FormatDetectors)
82133
{
83-
if (formatDetector.HeaderSize <= headerSize && formatDetector.TryDetectFormat(headersBuffer, out IImageFormat? attemptFormat))
134+
if (formatDetector.HeaderSize <= headersBuffer.Length && formatDetector.TryDetectFormat(headersBuffer, out IImageFormat? attemptFormat))
84135
{
85136
format = attemptFormat;
86137
}
@@ -106,6 +157,22 @@ private static IImageDecoder DiscoverDecoder(DecoderOptions options, Stream stre
106157
return options.Configuration.ImageFormatsManager.GetDecoder(format);
107158
}
108159

160+
/// <summary>
161+
/// By reading the header on the provided stream this calculates the images format.
162+
/// </summary>
163+
/// <param name="options">The general decoder options.</param>
164+
/// <param name="stream">The image stream to read the header from.</param>
165+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
166+
/// <returns>The <see cref="IImageDecoder"/>.</returns>
167+
private static async ValueTask<IImageDecoder> DiscoverDecoderAsync(
168+
DecoderOptions options,
169+
Stream stream,
170+
CancellationToken cancellationToken)
171+
{
172+
IImageFormat format = await InternalDetectFormatAsync(options.Configuration, stream, cancellationToken);
173+
return options.Configuration.ImageFormatsManager.GetDecoder(format);
174+
}
175+
109176
/// <summary>
110177
/// Decodes the image stream to the current image.
111178
/// </summary>
@@ -122,14 +189,14 @@ private static Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream strea
122189
return decoder.Decode<TPixel>(options, stream);
123190
}
124191

125-
private static Task<Image<TPixel>> DecodeAsync<TPixel>(
192+
private static async Task<Image<TPixel>> DecodeAsync<TPixel>(
126193
DecoderOptions options,
127194
Stream stream,
128195
CancellationToken cancellationToken)
129196
where TPixel : unmanaged, IPixel<TPixel>
130197
{
131-
IImageDecoder decoder = DiscoverDecoder(options, stream);
132-
return decoder.DecodeAsync<TPixel>(options, stream, cancellationToken);
198+
IImageDecoder decoder = await DiscoverDecoderAsync(options, stream, cancellationToken);
199+
return await decoder.DecodeAsync<TPixel>(options, stream, cancellationToken);
133200
}
134201

135202
private static Image Decode(DecoderOptions options, Stream stream)
@@ -138,13 +205,13 @@ private static Image Decode(DecoderOptions options, Stream stream)
138205
return decoder.Decode(options, stream);
139206
}
140207

141-
private static Task<Image> DecodeAsync(
208+
private static async Task<Image> DecodeAsync(
142209
DecoderOptions options,
143210
Stream stream,
144211
CancellationToken cancellationToken)
145212
{
146-
IImageDecoder decoder = DiscoverDecoder(options, stream);
147-
return decoder.DecodeAsync(options, stream, cancellationToken);
213+
IImageDecoder decoder = await DiscoverDecoderAsync(options, stream, cancellationToken);
214+
return await decoder.DecodeAsync(options, stream, cancellationToken);
148215
}
149216

150217
/// <summary>
@@ -166,12 +233,12 @@ private static ImageInfo InternalIdentify(DecoderOptions options, Stream stream)
166233
/// <param name="stream">The stream.</param>
167234
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
168235
/// <returns>The <see cref="ImageInfo"/>.</returns>
169-
private static Task<ImageInfo> InternalIdentifyAsync(
236+
private static async Task<ImageInfo> InternalIdentifyAsync(
170237
DecoderOptions options,
171238
Stream stream,
172239
CancellationToken cancellationToken)
173240
{
174-
IImageDecoder decoder = DiscoverDecoder(options, stream);
175-
return decoder.IdentifyAsync(options, stream, cancellationToken);
241+
IImageDecoder decoder = await DiscoverDecoderAsync(options, stream, cancellationToken);
242+
return await decoder.IdentifyAsync(options, stream, cancellationToken);
176243
}
177244
}

src/ImageSharp/Image.FromStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static Task<IImageFormat> DetectFormatAsync(
7272
=> WithSeekableStreamAsync(
7373
options,
7474
stream,
75-
(s, _) => Task.FromResult(InternalDetectFormat(options.Configuration, s)),
75+
async (s, ct) => await InternalDetectFormatAsync(options.Configuration, s, ct).ConfigureAwait(false),
7676
cancellationToken);
7777

7878
/// <summary>

0 commit comments

Comments
 (0)