Skip to content

Commit 183a821

Browse files
Merge branch 'main' into js/colorspace-converter
2 parents e67a671 + 467850f commit 183a821

File tree

5 files changed

+97
-14
lines changed

5 files changed

+97
-14
lines changed

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
sdk: 8.0.x
2525
runtime: -x64
2626
codecov: false
27-
- os: macos-latest
27+
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
2828
framework: net8.0
2929
sdk: 8.0.x
3030
runtime: -x64

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>

src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static MemoryAllocator Create(MemoryAllocatorOptions options)
4949
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
5050
if (options.AllocationLimitMegabytes.HasValue)
5151
{
52-
allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024 * 1024;
52+
allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
5353
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
5454
}
5555

tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,4 +442,20 @@ public void AllocateGroup_OverLimit_ThrowsInvalidMemoryOperationException()
442442
allocator.AllocateGroup<byte>(4 * oneMb, 1024).Dispose(); // Should work
443443
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(5 * oneMb, 1024));
444444
}
445+
446+
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
447+
public void MemoryAllocator_Create_SetHighLimit()
448+
{
449+
RemoteExecutor.Invoke(RunTest).Dispose();
450+
static void RunTest()
451+
{
452+
const long threeGB = 3L * (1 << 30);
453+
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions()
454+
{
455+
AllocationLimitMegabytes = (int)(threeGB / 1024)
456+
});
457+
using MemoryGroup<byte> memoryGroup = allocator.AllocateGroup<byte>(threeGB, 1024);
458+
Assert.Equal(threeGB, memoryGroup.TotalLength);
459+
}
460+
}
445461
}

0 commit comments

Comments
 (0)