using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; namespace Svg { [SvgElement("linearGradient")] public sealed class SvgLinearGradientServer : SvgGradientServer { [SvgAttribute("x1")] public SvgUnit X1 { get { return this.Attributes.GetAttribute("x1"); } set { Attributes["x1"] = value; } } [SvgAttribute("y1")] public SvgUnit Y1 { get { return this.Attributes.GetAttribute("y1"); } set { this.Attributes["y1"] = value; } } [SvgAttribute("x2")] public SvgUnit X2 { get { return this.Attributes.GetAttribute("x2"); } set { Attributes["x2"] = value; } } [SvgAttribute("y2")] public SvgUnit Y2 { get { return this.Attributes.GetAttribute("y2"); } set { this.Attributes["y2"] = value; } } private bool IsInvalid { get { // Need at least 2 colours to do the gradient fill return this.Stops.Count < 2; } } public SvgLinearGradientServer() { X1 = new SvgUnit(SvgUnitType.Percentage, 0F); Y1 = new SvgUnit(SvgUnitType.Percentage, 0F); X2 = new SvgUnit(SvgUnitType.Percentage, 100F); Y2 = new SvgUnit(SvgUnitType.Percentage, 0F); } public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity, bool forStroke = false) { LoadStops(renderingElement); if (this.Stops.Count < 1) return null; if (this.Stops.Count == 1) { var stopColor = this.Stops[0].GetColor(renderingElement); int alpha = (int)((opacity * (stopColor.A/255.0f) ) * 255); Color colour = System.Drawing.Color.FromArgb(alpha, stopColor); return new SolidBrush(colour); } try { if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); var points = new PointF[] { SvgUnit.GetDevicePoint(NormalizeUnit(this.X1), NormalizeUnit(this.Y1), renderer, this), SvgUnit.GetDevicePoint(NormalizeUnit(this.X2), NormalizeUnit(this.Y2), renderer, this) }; var bounds = renderer.GetBoundable().Bounds; if (bounds.Width <= 0 || bounds.Height <= 0) { if (this.GetCallback != null) return GetCallback().GetBrush(renderingElement, renderer, opacity, forStroke); return null; } using (var transform = EffectiveGradientTransform) { var midPoint = new PointF((points[0].X + points[1].X) / 2, (points[0].Y + points[1].Y) / 2); transform.Translate(bounds.X, bounds.Y, MatrixOrder.Prepend); if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) { // Transform a normal (i.e. perpendicular line) according to the transform transform.Scale(bounds.Width, bounds.Height, MatrixOrder.Prepend); transform.RotateAt(-90.0f, midPoint, MatrixOrder.Prepend); } transform.TransformPoints(points); } if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) { // Transform the normal line back to a line such that the gradient still starts in the correct corners, but // has the proper normal vector based on the transforms. If you work out the geometry, these formulas should work. var midPoint = new PointF((points[0].X + points[1].X) / 2, (points[0].Y + points[1].Y) / 2); var dy = (points[1].Y - points[0].Y); var dx = (points[1].X - points[0].X); var x2 = points[0].X; var y2 = points[1].Y; if (Math.Round(dx, 4) == 0) { points[0] = new PointF(midPoint.X + dy / 2 * bounds.Width / bounds.Height, midPoint.Y); points[1] = new PointF(midPoint.X - dy / 2 * bounds.Width / bounds.Height, midPoint.Y); } else if (Math.Round(dy, 4) == 0) { points[0] = new PointF(midPoint.X, midPoint.Y - dx / 2 * bounds.Height / bounds.Width); points[1] = new PointF(midPoint.X, midPoint.Y + dx / 2 * bounds.Height / bounds.Width); ; } else { var startX = (float)((dy * dx * (midPoint.Y - y2) + Math.Pow(dx, 2) * midPoint.X + Math.Pow(dy, 2) * x2) / (Math.Pow(dx, 2) + Math.Pow(dy, 2))); var startY = dy * (startX - x2) / dx + y2; points[0] = new PointF(startX, startY); points[1] = new PointF(midPoint.X + (midPoint.X - startX), midPoint.Y + (midPoint.Y - startY)); } } var effectiveStart = points[0]; var effectiveEnd = points[1]; if (PointsToMove(renderingElement, points[0], points[1]) > LinePoints.None) { var expansion = ExpandGradient(renderingElement, points[0], points[1]); effectiveStart = expansion.StartPoint; effectiveEnd = expansion.EndPoint; } var result = new LinearGradientBrush(effectiveStart, effectiveEnd, System.Drawing.Color.Transparent, System.Drawing.Color.Transparent) { InterpolationColors = CalculateColorBlend(renderer, opacity, points[0], effectiveStart, points[1], effectiveEnd), WrapMode = WrapMode.TileFlipX }; return result; } finally { if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.PopBoundable(); } } private SvgUnit NormalizeUnit(SvgUnit orig) { return (orig.Type == SvgUnitType.Percentage && this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox ? new SvgUnit(SvgUnitType.User, orig.Value / 100) : orig); } [Flags] private enum LinePoints { None = 0, Start = 1, End = 2 } private LinePoints PointsToMove(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd) { var bounds = boundable.CalculateBounds(); if (specifiedStart.X == specifiedEnd.X) { return (bounds.Top < specifiedStart.Y && specifiedStart.Y < bounds.Bottom ? LinePoints.Start : LinePoints.None) | (bounds.Top < specifiedEnd.Y && specifiedEnd.Y < bounds.Bottom ? LinePoints.End : LinePoints.None); } else if (specifiedStart.Y == specifiedEnd.Y) { return (bounds.Left < specifiedStart.X && specifiedStart.X < bounds.Right ? LinePoints.Start : LinePoints.None) | (bounds.Left < specifiedEnd.X && specifiedEnd.X < bounds.Right ? LinePoints.End : LinePoints.None); } return (boundable.Bounds.Contains(specifiedStart) ? LinePoints.Start : LinePoints.None) | (boundable.Bounds.Contains(specifiedEnd) ? LinePoints.End : LinePoints.None); } public struct GradientPoints { public PointF StartPoint; public PointF EndPoint; public GradientPoints(PointF startPoint, PointF endPoint) { this.StartPoint = startPoint; this.EndPoint = endPoint; } } private GradientPoints ExpandGradient(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd) { var pointsToMove = PointsToMove(boundable, specifiedStart, specifiedEnd); if (pointsToMove == LinePoints.None) { Debug.Fail("Unexpectedly expanding gradient when not needed!"); return new GradientPoints(specifiedStart, specifiedEnd); } var bounds = boundable.CalculateBounds(); var effectiveStart = specifiedStart; var effectiveEnd = specifiedEnd; var intersectionPoints = CandidateIntersections(bounds, specifiedStart, specifiedEnd); Debug.Assert(intersectionPoints.Count == 2, "Unanticipated number of intersection points"); if (!(Math.Sign(intersectionPoints[1].X - intersectionPoints[0].X) == Math.Sign(specifiedEnd.X - specifiedStart.X) && Math.Sign(intersectionPoints[1].Y - intersectionPoints[0].Y) == Math.Sign(specifiedEnd.Y - specifiedStart.Y))) { intersectionPoints = intersectionPoints.Reverse().ToList(); } if ((pointsToMove & LinePoints.Start) > 0) effectiveStart = intersectionPoints[0]; if ((pointsToMove & LinePoints.End) > 0) effectiveEnd = intersectionPoints[1]; switch (SpreadMethod) { case SvgGradientSpreadMethod.Reflect: case SvgGradientSpreadMethod.Repeat: var specifiedLength = CalculateDistance(specifiedStart, specifiedEnd); var specifiedUnitVector = new PointF((specifiedEnd.X - specifiedStart.X) / (float)specifiedLength, (specifiedEnd.Y - specifiedStart.Y) / (float)specifiedLength); var oppUnitVector = new PointF(-specifiedUnitVector.X, -specifiedUnitVector.Y); var startExtend = (float)(Math.Ceiling(CalculateDistance(effectiveStart, specifiedStart) / specifiedLength) * specifiedLength); effectiveStart = MovePointAlongVector(specifiedStart, oppUnitVector, startExtend); var endExtend = (float)(Math.Ceiling(CalculateDistance(effectiveEnd, specifiedEnd) / specifiedLength) * specifiedLength); effectiveEnd = MovePointAlongVector(specifiedEnd, specifiedUnitVector, endExtend); break; } return new GradientPoints(effectiveStart, effectiveEnd); } private IList CandidateIntersections(RectangleF bounds, PointF p1, PointF p2) { var results = new List(); if (Math.Round(Math.Abs(p1.Y - p2.Y), 4) == 0) { results.Add(new PointF(bounds.Left, p1.Y)); results.Add(new PointF(bounds.Right, p1.Y)); } else if (Math.Round(Math.Abs(p1.X - p2.X), 4) == 0) { results.Add(new PointF(p1.X, bounds.Top)); results.Add(new PointF(p1.X, bounds.Bottom)); } else { PointF candidate; // Save some effort and duplication in the trivial case if ((p1.X == bounds.Left || p1.X == bounds.Right) && (p1.Y == bounds.Top || p1.Y == bounds.Bottom)) { results.Add(p1); } else { candidate = new PointF(bounds.Left, (p2.Y - p1.Y) / (p2.X - p1.X) * (bounds.Left - p1.X) + p1.Y); if (bounds.Top <= candidate.Y && candidate.Y <= bounds.Bottom) results.Add(candidate); candidate = new PointF(bounds.Right, (p2.Y - p1.Y) / (p2.X - p1.X) * (bounds.Right - p1.X) + p1.Y); if (bounds.Top <= candidate.Y && candidate.Y <= bounds.Bottom) results.Add(candidate); } if ((p2.X == bounds.Left || p2.X == bounds.Right) && (p2.Y == bounds.Top || p2.Y == bounds.Bottom)) { results.Add(p2); } else { candidate = new PointF((bounds.Top - p1.Y) / (p2.Y - p1.Y) * (p2.X - p1.X) + p1.X, bounds.Top); if (bounds.Left <= candidate.X && candidate.X <= bounds.Right) results.Add(candidate); candidate = new PointF((bounds.Bottom - p1.Y) / (p2.Y - p1.Y) * (p2.X - p1.X) + p1.X, bounds.Bottom); if (bounds.Left <= candidate.X && candidate.X <= bounds.Right) results.Add(candidate); } } return results; } private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, PointF specifiedStart, PointF effectiveStart, PointF specifiedEnd, PointF effectiveEnd) { float startExtend; float endExtend; List colors; List positions; var colorBlend = GetColorBlend(renderer, opacity, false); var startDelta = CalculateDistance(specifiedStart, effectiveStart); var endDelta = CalculateDistance(specifiedEnd, effectiveEnd); if (!(startDelta > 0) && !(endDelta > 0)) { return colorBlend; } var specifiedLength = CalculateDistance(specifiedStart, specifiedEnd); var specifiedUnitVector = new PointF((specifiedEnd.X - specifiedStart.X) / (float)specifiedLength, (specifiedEnd.Y - specifiedStart.Y) / (float)specifiedLength); var effectiveLength = CalculateDistance(effectiveStart, effectiveEnd); switch (SpreadMethod) { case SvgGradientSpreadMethod.Reflect: startExtend = (float)(Math.Ceiling(CalculateDistance(effectiveStart, specifiedStart) / specifiedLength)); endExtend = (float)(Math.Ceiling(CalculateDistance(effectiveEnd, specifiedEnd) / specifiedLength)); colors = colorBlend.Colors.ToList(); positions = (from p in colorBlend.Positions select p + startExtend).ToList(); for (var i = 0; i < startExtend; i++) { if (i % 2 == 0) { for (var j = 1; j < colorBlend.Positions.Length; j++) { positions.Insert(0, (float)((startExtend - 1 - i) + 1 - colorBlend.Positions[j])); colors.Insert(0, colorBlend.Colors[j]); } } else { for (var j = 0; j < colorBlend.Positions.Length - 1; j++) { positions.Insert(j, (float)((startExtend - 1 - i) + colorBlend.Positions[j])); colors.Insert(j, colorBlend.Colors[j]); } } } int insertPos; for (var i = 0; i < endExtend; i++) { if (i % 2 == 0) { insertPos = positions.Count; for (var j = 0; j < colorBlend.Positions.Length - 1; j++) { positions.Insert(insertPos, (float)((startExtend + 1 + i) + 1 - colorBlend.Positions[j])); colors.Insert(insertPos, colorBlend.Colors[j]); } } else { for (var j = 1; j < colorBlend.Positions.Length; j++) { positions.Add((float)((startExtend + 1 + i) + colorBlend.Positions[j])); colors.Add(colorBlend.Colors[j]); } } } colorBlend.Colors = colors.ToArray(); colorBlend.Positions = (from p in positions select p / (startExtend + 1 + endExtend)).ToArray(); break; case SvgGradientSpreadMethod.Repeat: startExtend = (float)(Math.Ceiling(CalculateDistance(effectiveStart, specifiedStart) / specifiedLength)); endExtend = (float)(Math.Ceiling(CalculateDistance(effectiveEnd, specifiedEnd) / specifiedLength)); colors = new List(); positions = new List(); for (int i = 0; i < startExtend + endExtend + 1; i++) { for (int j = 0; j < colorBlend.Positions.Length; j++) { positions.Add((i + colorBlend.Positions[j] * 0.9999f) / (startExtend + endExtend + 1)); colors.Add(colorBlend.Colors[j]); } } positions[positions.Count - 1] = 1.0f; colorBlend.Colors = colors.ToArray(); colorBlend.Positions = positions.ToArray(); break; default: for (var i = 0; i < colorBlend.Positions.Length; i++) { var originalPoint = MovePointAlongVector(specifiedStart, specifiedUnitVector, (float)specifiedLength * colorBlend.Positions[i]); var distanceFromEffectiveStart = CalculateDistance(effectiveStart, originalPoint); colorBlend.Positions[i] = (float)Math.Round(Math.Max(0F, Math.Min((distanceFromEffectiveStart / effectiveLength), 1.0F)), 5); } if (startDelta > 0) { colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray(); colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray(); } if (endDelta > 0) { colorBlend.Positions = colorBlend.Positions.Concat(new[] { 1F }).ToArray(); colorBlend.Colors = colorBlend.Colors.Concat(new[] { colorBlend.Colors.Last() }).ToArray(); } break; } return colorBlend; } private static PointF CalculateClosestIntersectionPoint(PointF sourcePoint, IList targetPoints) { Debug.Assert(targetPoints.Count == 2, "Unexpected number of intersection points!"); return CalculateDistance(sourcePoint, targetPoints[0]) < CalculateDistance(sourcePoint, targetPoints[1]) ? targetPoints[0] : targetPoints[1]; } private static PointF MovePointAlongVector(PointF start, PointF unitVector, float distance) { return start + new SizeF(unitVector.X * distance, unitVector.Y * distance); } public override SvgElement DeepCopy() { return DeepCopy(); } public override SvgElement DeepCopy() { var newObj = base.DeepCopy() as SvgLinearGradientServer; newObj.X1 = this.X1; newObj.Y1 = this.Y1; newObj.X2 = this.X2; newObj.Y2 = this.Y2; return newObj; } private sealed class LineF { private float X1 { get; set; } private float Y1 { get; set; } private float X2 { get; set; } private float Y2 { get; set; } public LineF(float x1, float y1, float x2, float y2) { X1 = x1; Y1 = y1; X2 = x2; Y2 = y2; } public List Intersection(RectangleF rectangle) { var result = new List(); AddIfIntersect(this, new LineF(rectangle.X, rectangle.Y, rectangle.Right, rectangle.Y), result); AddIfIntersect(this, new LineF(rectangle.Right, rectangle.Y, rectangle.Right, rectangle.Bottom), result); AddIfIntersect(this, new LineF(rectangle.Right, rectangle.Bottom, rectangle.X, rectangle.Bottom), result); AddIfIntersect(this, new LineF(rectangle.X, rectangle.Bottom, rectangle.X, rectangle.Y), result); return result; } /// http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=geometry2 private PointF? Intersection(LineF other) { const int precision = 8; var a1 = (double)Y2 - Y1; var b1 = (double)X1 - X2; var c1 = a1 * X1 + b1 * Y1; var a2 = (double)other.Y2 - other.Y1; var b2 = (double)other.X1 - other.X2; var c2 = a2 * other.X1 + b2 * other.Y1; var det = a1 * b2 - a2 * b1; if (det == 0) { return null; } else { var xi = (b2 * c1 - b1 * c2) / det; var yi = (a1 * c2 - a2 * c1) / det; if (Math.Round(Math.Min(X1, X2), precision) <= Math.Round(xi, precision) && Math.Round(xi, precision) <= Math.Round(Math.Max(X1, X2), precision) && Math.Round(Math.Min(Y1, Y2), precision) <= Math.Round(yi, precision) && Math.Round(yi, precision) <= Math.Round(Math.Max(Y1, Y2), precision) && Math.Round(Math.Min(other.X1, other.X2), precision) <= Math.Round(xi, precision) && Math.Round(xi, precision) <= Math.Round(Math.Max(other.X1, other.X2), precision) && Math.Round(Math.Min(other.Y1, other.Y2), precision) <= Math.Round(yi, precision) && Math.Round(yi, precision) <= Math.Round(Math.Max(other.Y1, other.Y2), precision)) { return new PointF((float)xi, (float)yi); } else { return null; } } } private static void AddIfIntersect(LineF first, LineF second, ICollection result) { var intersection = first.Intersection(second); if (intersection != null) { result.Add(intersection.Value); } } } } }