Skip to content

Commit 2316146

Browse files
Merge pull request #304 from SixLabors/js/fix-oob-text-drawing
Fix Out of Bounds Text Drawing
2 parents 85721f8 + 758b7b4 commit 2316146

File tree

4 files changed

+74
-9
lines changed

4 files changed

+74
-9
lines changed

src/ImageSharp.Drawing/Processing/GradientBrush.cs

+33-7
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ internal abstract class GradientBrushApplicator<TPixel> : BrushApplicator<TPixel
6262

6363
private readonly MemoryAllocator allocator;
6464

65-
private readonly int scalineWidth;
65+
private readonly int scanlineWidth;
6666

6767
private readonly ThreadLocalBlenderBuffers<TPixel> blenderBuffers;
6868

@@ -84,13 +84,14 @@ protected GradientBrushApplicator(
8484
GradientRepetitionMode repetitionMode)
8585
: base(configuration, options, target)
8686
{
87-
// TODO: requires colorStops to be sorted by position.
88-
// Use Array.Sort with a custom comparer.
8987
this.colorStops = colorStops;
88+
89+
// Ensure the color-stop order is correct.
90+
InsertionSort(this.colorStops, (x, y) => x.Ratio.CompareTo(y.Ratio));
9091
this.repetitionMode = repetitionMode;
91-
this.scalineWidth = target.Width;
92+
this.scanlineWidth = target.Width;
9293
this.allocator = configuration.MemoryAllocator;
93-
this.blenderBuffers = new ThreadLocalBlenderBuffers<TPixel>(this.allocator, this.scalineWidth);
94+
this.blenderBuffers = new ThreadLocalBlenderBuffers<TPixel>(this.allocator, this.scanlineWidth);
9495
}
9596

9697
internal TPixel this[int x, int y]
@@ -135,15 +136,16 @@ protected GradientBrushApplicator(
135136

136137
float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio);
137138

139+
// TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green.
138140
return new Color(Vector4.Lerp((Vector4)from.Color, (Vector4)to.Color, onLocalGradient)).ToPixel<TPixel>();
139141
}
140142
}
141143

142144
/// <inheritdoc />
143145
public override void Apply(Span<float> scanline, int x, int y)
144146
{
145-
Span<float> amounts = this.blenderBuffers.AmountSpan.Slice(0, scanline.Length);
146-
Span<TPixel> overlays = this.blenderBuffers.OverlaySpan.Slice(0, scanline.Length);
147+
Span<float> amounts = this.blenderBuffers.AmountSpan[..scanline.Length];
148+
Span<TPixel> overlays = this.blenderBuffers.OverlaySpan[..scanline.Length];
147149
float blendPercentage = this.Options.BlendPercentage;
148150

149151
// TODO: Remove bounds checks.
@@ -221,5 +223,29 @@ protected override void Dispose(bool disposing)
221223

222224
return (localGradientFrom, localGradientTo);
223225
}
226+
227+
/// <summary>
228+
/// Provides a stable sorting algorithm for the given array.
229+
/// <see cref="Array.Sort(Array, System.Collections.IComparer?)"/> is not stable.
230+
/// </summary>
231+
/// <typeparam name="T">The type of element to sort.</typeparam>
232+
/// <param name="collection">The array to sort.</param>
233+
/// <param name="comparison">The comparison delegate.</param>
234+
private static void InsertionSort<T>(T[] collection, Comparison<T> comparison)
235+
{
236+
int count = collection.Length;
237+
for (int j = 1; j < count; j++)
238+
{
239+
T key = collection[j];
240+
241+
int i = j - 1;
242+
for (; i >= 0 && comparison(collection[i], key) > 0; i--)
243+
{
244+
collection[i + 1] = collection[i];
245+
}
246+
247+
collection[i + 1] = key;
248+
}
249+
}
224250
}
225251
}

src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ void Draw(IEnumerable<DrawingOperation> operations)
9999
for (int row = firstRow; row < end; row++)
100100
{
101101
int y = startY + row;
102-
Span<float> span = buffer.DangerousGetRowSpan(row)[offsetSpan..];
102+
Span<float> span = buffer.DangerousGetRowSpan(row).Slice(offsetSpan, Math.Min(buffer.Width - offsetSpan, source.Width));
103103
app.Apply(span, startX, y);
104104
}
105105
}

tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs

+37-1
Original file line numberDiff line numberDiff line change
@@ -887,12 +887,48 @@ public void CanDrawTextVerticalMixed<TPixel>(TestImageProvider<TPixel> provider)
887887
comparer: ImageComparer.TolerantPercentage(0.002f));
888888
}
889889

890+
[Theory]
891+
[WithBlankImage(200, 200, PixelTypes.Rgba32)]
892+
public void CanRenderTextOutOfBoundsIssue301<TPixel>(TestImageProvider<TPixel> provider)
893+
where TPixel : unmanaged, IPixel<TPixel>
894+
=> provider.VerifyOperation(
895+
ImageComparer.TolerantPercentage(0.01f),
896+
img =>
897+
{
898+
Font font = CreateFont(TestFonts.OpenSans, 70);
899+
900+
const string txt = "V";
901+
FontRectangle size = TextMeasurer.MeasureBounds(txt, new TextOptions(font));
902+
903+
img.Mutate(x => x.Resize((int)size.Width, (int)size.Height));
904+
905+
RichTextOptions options = new(font)
906+
{
907+
HorizontalAlignment = HorizontalAlignment.Center,
908+
VerticalAlignment = VerticalAlignment.Center,
909+
Origin = new Vector2(size.Width / 2, size.Height / 2)
910+
};
911+
912+
LinearGradientBrush brush = new(
913+
new PointF(0, 0),
914+
new PointF(20, 20),
915+
GradientRepetitionMode.Repeat,
916+
new ColorStop(0, Color.Red),
917+
new ColorStop(0.5f, Color.Green),
918+
new ColorStop(0.5f, Color.Yellow),
919+
new ColorStop(1f, Color.Blue));
920+
921+
img.Mutate(m => m.DrawText(options, txt, brush));
922+
},
923+
false,
924+
false);
925+
890926
private static string Repeat(string str, int times) => string.Concat(Enumerable.Repeat(str, times));
891927

892928
private static string ToTestOutputDisplayText(string text)
893929
{
894930
string fnDisplayText = text.Replace("\n", string.Empty);
895-
return fnDisplayText.Substring(0, Math.Min(fnDisplayText.Length, 4));
931+
return fnDisplayText[..Math.Min(fnDisplayText.Length, 4)];
896932
}
897933

898934
private static Font CreateFont(string fontName, float size)

0 commit comments

Comments
 (0)