Skip to content

Commit c17eacf

Browse files
Merge branch 'main' into js/gif-fixes
2 parents b48dbc5 + 54b7e04 commit c17eacf

File tree

10 files changed

+127
-30
lines changed

10 files changed

+127
-30
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
pull_request:
1010
branches:
1111
- main
12+
- release/*
1213
types: [ labeled, opened, synchronize, reopened ]
1314
jobs:
1415
Build:

src/ImageSharp/Advanced/ParallelRowIterator.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static void IterateRows<T>(
5050
int width = rectangle.Width;
5151
int height = rectangle.Height;
5252

53-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
53+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
5454
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
5555

5656
// Avoid TPL overhead in this trivial case:
@@ -115,7 +115,7 @@ public static void IterateRows<T, TBuffer>(
115115
int width = rectangle.Width;
116116
int height = rectangle.Height;
117117

118-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
118+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
119119
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
120120
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
121121
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
@@ -180,7 +180,7 @@ public static void IterateRowIntervals<T>(
180180
int width = rectangle.Width;
181181
int height = rectangle.Height;
182182

183-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
183+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
184184
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
185185

186186
// Avoid TPL overhead in this trivial case:
@@ -242,7 +242,7 @@ public static void IterateRowIntervals<T, TBuffer>(
242242
int width = rectangle.Width;
243243
int height = rectangle.Height;
244244

245-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
245+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
246246
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
247247
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
248248
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
@@ -270,7 +270,7 @@ public static void IterateRowIntervals<T, TBuffer>(
270270
}
271271

272272
[MethodImpl(InliningOptions.ShortMethod)]
273-
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
273+
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
274274

275275
private static void ValidateRectangle(Rectangle rectangle)
276276
{

src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,12 @@ public bool FindNextMarker()
212212
private int ReadStream()
213213
{
214214
int value = this.badData ? 0 : this.stream.ReadByte();
215-
if (value == -1)
215+
216+
// We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
217+
// during decoding of the SOS marker.
218+
// When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
219+
// we know we have hit the EOI and completed decoding the scan buffer.
220+
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
216221
{
217222
// We've encountered the end of the file stream which means there's no EOI marker
218223
// in the image or the SOS marker has the wrong dimensions set.

src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Buffers;
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
78
using SixLabors.ImageSharp.Advanced;
89
using SixLabors.ImageSharp.Memory;
910
using SixLabors.ImageSharp.PixelFormats;
@@ -34,17 +35,25 @@ public OilPaintingProcessor(Configuration configuration, OilPaintingProcessor de
3435
/// <inheritdoc/>
3536
protected override void OnFrameApply(ImageFrame<TPixel> source)
3637
{
38+
int levels = Math.Clamp(this.definition.Levels, 1, 255);
3739
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
3840

3941
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
4042

4143
source.CopyTo(targetPixels);
4244

43-
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, this.definition.Levels);
44-
ParallelRowIterator.IterateRowIntervals(
45+
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, levels);
46+
try
47+
{
48+
ParallelRowIterator.IterateRowIntervals(
4549
this.Configuration,
4650
this.SourceRectangle,
4751
in operation);
52+
}
53+
catch (Exception ex)
54+
{
55+
throw new ImageProcessingException("The OilPaintProcessor failed. The most likely reason is that a pixel component was outside of its' allowed range.", ex);
56+
}
4857

4958
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
5059
}
@@ -105,18 +114,18 @@ public void Invoke(in RowInterval rows)
105114
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span;
106115
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
107116

108-
ref float binsRef = ref bins.GetReference();
109-
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef);
110-
ref float redBinRef = ref Unsafe.Add(ref binsRef, (uint)this.levels);
111-
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, (uint)this.levels);
112-
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, (uint)this.levels);
117+
Span<float> binsSpan = bins.GetSpan();
118+
Span<int> intensityBinsSpan = MemoryMarshal.Cast<float, int>(binsSpan);
119+
Span<float> redBinSpan = binsSpan[this.levels..];
120+
Span<float> blueBinSpan = redBinSpan[this.levels..];
121+
Span<float> greenBinSpan = blueBinSpan[this.levels..];
113122

114123
for (int y = rows.Min; y < rows.Max; y++)
115124
{
116125
Span<TPixel> sourceRowPixelSpan = this.source.DangerousGetRowSpan(y);
117126
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width);
118127

119-
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span);
128+
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale);
120129

121130
for (int x = this.bounds.X; x < this.bounds.Right; x++)
122131
{
@@ -140,29 +149,29 @@ public void Invoke(in RowInterval rows)
140149
int offsetX = x + fxr;
141150
offsetX = Numerics.Clamp(offsetX, 0, maxX);
142151

143-
Vector4 vector = sourceOffsetRow[offsetX].ToVector4();
152+
Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4();
144153

145154
float sourceRed = vector.X;
146155
float sourceBlue = vector.Z;
147156
float sourceGreen = vector.Y;
148157

149158
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1));
150159

151-
Unsafe.Add(ref intensityBinRef, (uint)currentIntensity)++;
152-
Unsafe.Add(ref redBinRef, (uint)currentIntensity) += sourceRed;
153-
Unsafe.Add(ref blueBinRef, (uint)currentIntensity) += sourceBlue;
154-
Unsafe.Add(ref greenBinRef, (uint)currentIntensity) += sourceGreen;
160+
intensityBinsSpan[currentIntensity]++;
161+
redBinSpan[currentIntensity] += sourceRed;
162+
blueBinSpan[currentIntensity] += sourceBlue;
163+
greenBinSpan[currentIntensity] += sourceGreen;
155164

156-
if (Unsafe.Add(ref intensityBinRef, (uint)currentIntensity) > maxIntensity)
165+
if (intensityBinsSpan[currentIntensity] > maxIntensity)
157166
{
158-
maxIntensity = Unsafe.Add(ref intensityBinRef, (uint)currentIntensity);
167+
maxIntensity = intensityBinsSpan[currentIntensity];
159168
maxIndex = currentIntensity;
160169
}
161170
}
162171

163-
float red = MathF.Abs(Unsafe.Add(ref redBinRef, (uint)maxIndex) / maxIntensity);
164-
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, (uint)maxIndex) / maxIntensity);
165-
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, (uint)maxIndex) / maxIntensity);
172+
float red = redBinSpan[maxIndex] / maxIntensity;
173+
float blue = blueBinSpan[maxIndex] / maxIntensity;
174+
float green = greenBinSpan[maxIndex] / maxIntensity;
166175
float alpha = sourceRowVector4Span[x].W;
167176

168177
targetRowVector4Span[x] = new Vector4(red, green, blue, alpha);
@@ -171,7 +180,7 @@ public void Invoke(in RowInterval rows)
171180

172181
Span<TPixel> targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
173182

174-
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan);
183+
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale);
175184
}
176185
}
177186
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
using SixLabors.ImageSharp.Processing;
7+
8+
namespace SixLabors.ImageSharp.Benchmarks.Processing;
9+
10+
[Config(typeof(Config.MultiFramework))]
11+
public class OilPaint
12+
{
13+
[Benchmark]
14+
public void DoOilPaint()
15+
{
16+
using Image<RgbaVector> image = new Image<RgbaVector>(1920, 1200, new(127, 191, 255));
17+
image.Mutate(ctx => ctx.OilPaint());
18+
}
19+
}

tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,21 @@ public void Issue2478_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
325325
image.DebugSave(provider);
326326
image.CompareToOriginal(provider);
327327
}
328+
329+
[Theory]
330+
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
331+
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
332+
where TPixel : unmanaged, IPixel<TPixel>
333+
{
334+
if (TestEnvironment.IsWindows &&
335+
TestEnvironment.RunsOnCI)
336+
{
337+
// Windows CI runs consistently fail with OOM.
338+
return;
339+
}
340+
341+
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
342+
Assert.Equal(65503, image.Width);
343+
Assert.Equal(65503, image.Height);
344+
}
328345
}

tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Numerics;
5+
using System.Runtime.CompilerServices;
6+
using Castle.Core.Configuration;
57
using SixLabors.ImageSharp.Advanced;
68
using SixLabors.ImageSharp.Memory;
79
using SixLabors.ImageSharp.PixelFormats;
@@ -406,6 +408,43 @@ void RowAction(RowInterval rows, Span<Rgba32> memory)
406408
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
407409
}
408410

411+
[Fact]
412+
public void CanIterateWithoutIntOverflow()
413+
{
414+
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default);
415+
const int max = 100_000;
416+
417+
Rectangle rect = new(0, 0, max, max);
418+
int intervalMaxY = 0;
419+
void RowAction(RowInterval rows, Span<Rgba32> memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY);
420+
421+
TestRowOperation operation = new();
422+
TestRowIntervalOperation<Rgba32> intervalOperation = new(RowAction);
423+
424+
ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation);
425+
Assert.Equal(max - 1, operation.MaxY.Value);
426+
427+
ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in intervalOperation);
428+
Assert.Equal(max, intervalMaxY);
429+
}
430+
431+
private readonly struct TestRowOperation : IRowOperation
432+
{
433+
public TestRowOperation()
434+
{
435+
}
436+
437+
public StrongBox<int> MaxY { get; } = new StrongBox<int>();
438+
439+
public void Invoke(int y)
440+
{
441+
lock (this.MaxY)
442+
{
443+
this.MaxY.Value = Math.Max(y, this.MaxY.Value);
444+
}
445+
}
446+
}
447+
409448
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
410449
{
411450
private readonly Action<RowInterval> action;

tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,29 @@ public class OilPaintTest
2727
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
2828
public void FullImage<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
2929
where TPixel : unmanaged, IPixel<TPixel>
30-
{
31-
provider.RunValidatingProcessorTest(
30+
=> provider.RunValidatingProcessorTest(
3231
x =>
3332
{
3433
x.OilPaint(levels, brushSize);
3534
return $"{levels}-{brushSize}";
3635
},
3736
ImageComparer.TolerantPercentage(0.01F),
3837
appendPixelTypeToFileName: false);
39-
}
4038

4139
[Theory]
4240
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
4341
[WithTestPatternImages(nameof(OilPaintValues), 100, 100, PixelTypes.Rgba32)]
4442
public void InBox<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
4543
where TPixel : unmanaged, IPixel<TPixel>
46-
{
47-
provider.RunRectangleConstrainedValidatingProcessorTest(
44+
=> provider.RunRectangleConstrainedValidatingProcessorTest(
4845
(x, rect) => x.OilPaint(levels, brushSize, rect),
4946
$"{levels}-{brushSize}",
5047
ImageComparer.TolerantPercentage(0.01F));
48+
49+
[Fact]
50+
public void Issue2518_PixelComponentOutsideOfRange_ThrowsImageProcessingException()
51+
{
52+
using Image<RgbaVector> image = new(10, 10, new RgbaVector(1, 1, 100));
53+
Assert.Throws<ImageProcessingException>(() => image.Mutate(ctx => ctx.OilPaint()));
5154
}
5255
}

tests/ImageSharp.Tests/TestImages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ public static class Issues
291291
public const string Issue2334_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
292292
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.jpg";
293293
public const string Issue2478_JFXX = "Jpg/issues/issue-2478-jfxx.jpg";
294+
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
294295

295296
public static class Fuzz
296297
{
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)