|
| 1 | +// Copyright (c) Six Labors. |
| 2 | +// Licensed under the Six Labors Split License. |
| 3 | + |
| 4 | +using System.Diagnostics.CodeAnalysis; |
| 5 | +using SixLabors.Fonts; |
| 6 | +using SixLabors.Fonts.Rendering; |
| 7 | + |
| 8 | +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; |
| 9 | + |
| 10 | +/// <content> |
| 11 | +/// Utilities to translate format-agnostic paints (from Fonts) into ImageSharp.Drawing brushes. |
| 12 | +/// </content> |
| 13 | +internal sealed partial class RichTextGlyphRenderer |
| 14 | +{ |
| 15 | + /// <summary> |
| 16 | + /// Attempts to create an ImageSharp.Drawing <see cref="Brush"/> from a <see cref="Paint"/>. |
| 17 | + /// </summary> |
| 18 | + /// <param name="paint">The paint definition coming from the interpreter.</param> |
| 19 | + /// <param name="brush">The resulting brush, or <see langword="null"/> if the paint is unsupported.</param> |
| 20 | + /// <returns><see langword="true"/> if a brush could be created; otherwise, <see langword="false"/>.</returns> |
| 21 | + public static bool TryCreateBrush(Paint? paint, [NotNullWhen(true)] out Brush? brush) |
| 22 | + { |
| 23 | + brush = null; |
| 24 | + |
| 25 | + if (paint is null) |
| 26 | + { |
| 27 | + return false; |
| 28 | + } |
| 29 | + |
| 30 | + // TODO: Do we need to apply the transform assigned to th underlying builder here? |
| 31 | + switch (paint) |
| 32 | + { |
| 33 | + case SolidPaint sp: |
| 34 | + brush = new SolidBrush(ToColor(sp.Color, sp.Opacity)); |
| 35 | + return true; |
| 36 | + |
| 37 | + case LinearGradientPaint lg: |
| 38 | + return TryCreateLinearGradientBrush(lg, out brush); |
| 39 | + case RadialGradientPaint rg: |
| 40 | + return TryCreateRadialGradientBrush(rg, out brush); |
| 41 | + case SweepGradientPaint sg: |
| 42 | + return TryCreateSweepGradientBrush(sg, out brush); |
| 43 | + default: |
| 44 | + return false; |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + /// <summary> |
| 49 | + /// Creates a <see cref="LinearGradientBrush"/> from a <see cref="LinearGradientPaint"/>. |
| 50 | + /// </summary> |
| 51 | + /// <param name="lg">The linear gradient paint.</param> |
| 52 | + /// <param name="brush">The resulting brush.</param> |
| 53 | + /// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns> |
| 54 | + private static bool TryCreateLinearGradientBrush(LinearGradientPaint lg, out Brush? brush) |
| 55 | + { |
| 56 | + // Map gradient stops (apply paint opacity multiplier to each stop’s alpha). |
| 57 | + ColorStop[] stops = ToColorStops(lg.Stops, lg.Opacity); |
| 58 | + |
| 59 | + // Map spread method. |
| 60 | + GradientRepetitionMode mode = MapSpread(lg.Spread); |
| 61 | + |
| 62 | + PointF p0 = lg.P0; |
| 63 | + PointF p1 = lg.P1; |
| 64 | + |
| 65 | + // Degenerate gradient, fall back to solid using last stop. |
| 66 | + if (ApproximatelyEqual(p0, p1)) |
| 67 | + { |
| 68 | + // TODO: Consider using this.currentColor instead? |
| 69 | + Color fallback = stops.Length > 0 ? stops[^1].Color : Color.Black; |
| 70 | + brush = new SolidBrush(fallback); |
| 71 | + return true; |
| 72 | + } |
| 73 | + |
| 74 | + brush = new LinearGradientBrush(p0, p1, mode, stops); |
| 75 | + return true; |
| 76 | + } |
| 77 | + |
| 78 | + /// <summary> |
| 79 | + /// Creates a <see cref="RadialGradientBrush"/> from a <see cref="RadialGradientPaint"/>. |
| 80 | + /// </summary> |
| 81 | + /// <param name="rg">The radial gradient paint.</param> |
| 82 | + /// <param name="brush">The resulting brush.</param> |
| 83 | + /// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns> |
| 84 | + private static bool TryCreateRadialGradientBrush(RadialGradientPaint rg, out Brush? brush) |
| 85 | + { |
| 86 | + // Map gradient stops (apply paint opacity multiplier to each stop’s alpha). |
| 87 | + ColorStop[] stops = ToColorStops(rg.Stops, rg.Opacity); |
| 88 | + |
| 89 | + // Map spread method. |
| 90 | + GradientRepetitionMode mode = MapSpread(rg.Spread); |
| 91 | + |
| 92 | + brush = new RadialGradientBrush(rg.Center, rg.Radius, mode, stops); |
| 93 | + return true; |
| 94 | + } |
| 95 | + |
| 96 | + /// <summary> |
| 97 | + /// Creates a <see cref="SweepGradientBrush"/> from a <see cref="SweepGradientPaint"/>. |
| 98 | + /// </summary> |
| 99 | + /// <param name="sg">The sweep gradient paint.</param> |
| 100 | + /// <param name="brush">The resulting brush.</param> |
| 101 | + /// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns> |
| 102 | + private static bool TryCreateSweepGradientBrush(SweepGradientPaint sg, out Brush? brush) |
| 103 | + { |
| 104 | + // Map gradient stops (apply paint opacity multiplier to each stop’s alpha). |
| 105 | + ColorStop[] stops = ToColorStops(sg.Stops, sg.Opacity); |
| 106 | + |
| 107 | + // Map spread method. |
| 108 | + GradientRepetitionMode mode = MapSpread(sg.Spread); |
| 109 | + |
| 110 | + brush = new SweepGradientBrush(sg.Center, sg.StartAngle, sg.EndAngle, mode, stops); |
| 111 | + return true; |
| 112 | + } |
| 113 | + |
| 114 | + /// <summary> |
| 115 | + /// Maps an <see cref="SpreadMethod"/> to <see cref="GradientRepetitionMode"/>. |
| 116 | + /// </summary> |
| 117 | + /// <param name="spread">The spread method.</param> |
| 118 | + /// <returns>The repetition mode.</returns> |
| 119 | + private static GradientRepetitionMode MapSpread(SpreadMethod spread) |
| 120 | + => spread switch |
| 121 | + { |
| 122 | + SpreadMethod.Reflect => GradientRepetitionMode.Reflect, |
| 123 | + SpreadMethod.Repeat => GradientRepetitionMode.Repeat, |
| 124 | + |
| 125 | + // Pad extends edge colors, which matches 'None' (not 'DontFill'). |
| 126 | + _ => GradientRepetitionMode.None, |
| 127 | + }; |
| 128 | + |
| 129 | + /// <summary> |
| 130 | + /// Converts gradient stops and applies a paint opacity multiplier. |
| 131 | + /// </summary> |
| 132 | + /// <param name="stops">The source stops.</param> |
| 133 | + /// <param name="paintOpacity">The paint opacity in range [0,1].</param> |
| 134 | + /// <returns>An array of <see cref="ColorStop"/>.</returns> |
| 135 | + private static ColorStop[] ToColorStops(ReadOnlySpan<GradientStop> stops, float paintOpacity) |
| 136 | + { |
| 137 | + if (stops.Length == 0) |
| 138 | + { |
| 139 | + return []; |
| 140 | + } |
| 141 | + |
| 142 | + ColorStop[] result = new ColorStop[stops.Length]; |
| 143 | + |
| 144 | + for (int i = 0; i < stops.Length; i++) |
| 145 | + { |
| 146 | + GradientStop s = stops[i]; |
| 147 | + Color c = ToColor(s.Color, paintOpacity); |
| 148 | + result[i] = new ColorStop(s.Offset, c); |
| 149 | + } |
| 150 | + |
| 151 | + return result; |
| 152 | + } |
| 153 | + |
| 154 | + /// <summary> |
| 155 | + /// Converts a <see cref="GlyphColor"/> with an additional opacity multiplier to ImageSharp <see cref="Color"/>. |
| 156 | + /// </summary> |
| 157 | + /// <param name="c">The glyph color.</param> |
| 158 | + /// <param name="opacity">The opacity multiplier in range [0,1].</param> |
| 159 | + /// <returns>The ImageSharp color.</returns> |
| 160 | + private static Color ToColor(in GlyphColor c, float opacity) |
| 161 | + { |
| 162 | + float a = Math.Clamp(c.Alpha / 255f * Math.Clamp(opacity, 0f, 1f), 0f, 1f); |
| 163 | + byte aa = (byte)MathF.Round(a * 255f); |
| 164 | + return Color.FromPixel(new Rgba32(c.Red, c.Green, c.Blue, aa)); |
| 165 | + } |
| 166 | + |
| 167 | + /// <summary> |
| 168 | + /// Compares two points for near-equality. |
| 169 | + /// </summary> |
| 170 | + /// <param name="a">The first point.</param> |
| 171 | + /// <param name="b">The second point.</param> |
| 172 | + /// <param name="eps">Tolerance.</param> |
| 173 | + /// <returns><see langword="true"/> if near-equal; otherwise <see langword="false"/>.</returns> |
| 174 | + private static bool ApproximatelyEqual(in PointF a, in PointF b, float eps = 1e-4f) |
| 175 | + => MathF.Abs(a.X - b.X) <= eps && MathF.Abs(a.Y - b.Y) <= eps; |
| 176 | +} |
0 commit comments