Browse Source

code cleanup and comments

pull/71/head
Scott Williams 9 years ago
parent
commit
086115fd78
  1. 72
      src/ImageSharp.Drawing/Paths/InternalPath.cs
  2. 2
      src/ImageSharp.Drawing/Processors/FillProcessor.cs
  3. 368
      src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs
  4. 3
      src/ImageSharp.Drawing/Shapes/BezierPolygon.cs
  5. 28
      src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
  6. 3
      src/ImageSharp.Drawing/Shapes/IShape.cs
  7. 3
      src/ImageSharp.Drawing/Shapes/LinearPolygon.cs
  8. 5
      src/ImageSharp.Drawing/Shapes/Polygon.cs
  9. 8
      src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs

72
src/ImageSharp.Drawing/Paths/InternalPath.cs

@ -169,7 +169,8 @@ namespace ImageSharp.Drawing.Paths
}
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start" /> and <paramref name="end" />
/// populate a buffer for all points on the path that the line intersects.
/// </summary>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
@ -195,7 +196,7 @@ namespace ImageSharp.Drawing.Paths
next = 0;
}
var point = FindIntersection(this.points[i], this.points[next], start, end);
Vector2 point = FindIntersection(this.points[i], this.points[next], start, end);
if (point != MaxVector)
{
buffer[position + offset] = point;
@ -208,7 +209,7 @@ namespace ImageSharp.Drawing.Paths
}
/// <summary>
/// Points the in polygon.
/// Determines if the specified point is inside or outside the path.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>Returns true if the point is inside the closed path.</returns>
@ -247,47 +248,67 @@ namespace ImageSharp.Drawing.Paths
return oddNodes;
}
/// <summary>
/// Determins if the bounding box for 2 lines
/// described by <paramref name="line1Start" /> and <paramref name="line1End" />
/// and <paramref name="line2Start" /> and <paramref name="line2End" /> overlap.
/// </summary>
/// <param name="line1Start">The line1 start.</param>
/// <param name="line1End">The line1 end.</param>
/// <param name="line2Start">The line2 start.</param>
/// <param name="line2End">The line2 end.</param>
/// <returns></returns>
private static bool BoundingBoxesIntersect(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End)
{
var topLeft1 = Vector2.Min(line1Start, line1End);
var bottomRight1 = Vector2.Max(line1Start, line1End);
Vector2 topLeft1 = Vector2.Min(line1Start, line1End);
Vector2 bottomRight1 = Vector2.Max(line1Start, line1End);
var topLeft2 = Vector2.Min(line2Start, line2End);
var bottomRight2 = Vector2.Max(line2Start, line2End);
Vector2 topLeft2 = Vector2.Min(line2Start, line2End);
Vector2 bottomRight2 = Vector2.Max(line2Start, line2End);
var left1 = topLeft1.X;
var right1 = bottomRight1.X;
var top1 = topLeft1.Y;
var bottom1 = bottomRight1.Y;
float left1 = topLeft1.X;
float right1 = bottomRight1.X;
float top1 = topLeft1.Y;
float bottom1 = bottomRight1.Y;
var left2 = topLeft2.X;
var right2 = bottomRight2.X;
var top2 = topLeft2.Y;
var bottom2 = bottomRight2.Y;
float left2 = topLeft2.X;
float right2 = bottomRight2.X;
float top2 = topLeft2.Y;
float bottom2 = bottomRight2.Y;
return left1 <= right2 && right1 >= left2
&&
top1 <= bottom2 && bottom1 >= top2;
}
/// <summary>
/// Finds the point on line described by <paramref name="line1Start" /> and <paramref name="line1End" />
/// that intersects with line described by <paramref name="line2Start" /> and <paramref name="line2End" />
/// </summary>
/// <param name="line1Start">The line1 start.</param>
/// <param name="line1End">The line1 end.</param>
/// <param name="line2Start">The line2 start.</param>
/// <param name="line2End">The line2 end.</param>
/// <returns>
/// A <see cref="Vector2"/> describing the point that the 2 lines cross or <see cref="MaxVector"/> if they do not.
/// </returns>
private static Vector2 FindIntersection(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End)
{
// do lines cross at all
// do bounding boxes overlap, if not then the lines can't and return fast.
if (!BoundingBoxesIntersect(line1Start, line1End, line2Start, line2End))
{
return MaxVector;
}
var line1Diff = line1End - line1Start;
var line2Diff = line2End - line2Start;
Vector2 line1Diff = line1End - line1Start;
Vector2 line2Diff = line2End - line2Start;
Vector2 point;
if (line1Diff.X == 0)
{
float slope = line2Diff.Y / line2Diff.X;
var yinter = line2Start.Y - (slope * line2Start.X);
var y = (line1Start.X * slope) + yinter;
float yinter = line2Start.Y - (slope * line2Start.X);
float y = (line1Start.X * slope) + yinter;
point = new Vector2(line1Start.X, y);
// horizontal and vertical lines
@ -295,8 +316,8 @@ namespace ImageSharp.Drawing.Paths
else if (line2Diff.X == 0)
{
float slope = line1Diff.Y / line1Diff.X;
var yinter = line1Start.Y - (slope * line1Start.X);
var y = (line2Start.X * slope) + yinter;
float yinter = line1Start.Y - (slope * line1Start.X);
float y = (line2Start.X * slope) + yinter;
point = new Vector2(line2Start.X, y);
// horizontal and vertical lines
@ -306,8 +327,8 @@ namespace ImageSharp.Drawing.Paths
float slope1 = line1Diff.Y / line1Diff.X;
float slope2 = line2Diff.Y / line2Diff.X;
var yinter1 = line1Start.Y - (slope1 * line1Start.X);
var yinter2 = line2Start.Y - (slope2 * line2Start.X);
float yinter1 = line1Start.Y - (slope1 * line1Start.X);
float yinter2 = line2Start.Y - (slope2 * line2Start.X);
if (slope1 == slope2 && yinter1 != yinter2)
{
@ -315,7 +336,6 @@ namespace ImageSharp.Drawing.Paths
}
float x = (yinter2 - yinter1) / (slope1 - slope2);
float y = (slope1 * x) + yinter1;
point = new Vector2(x, y);

2
src/ImageSharp.Drawing/Processors/FillProcessor.cs

@ -83,7 +83,7 @@ namespace ImageSharp.Drawing.Processors
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, 1);
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, 1);
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);

368
src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs

@ -75,151 +75,15 @@ namespace ImageSharp.Drawing.Processors
this.ParallelOptions,
y =>
{
var buffer = arrayPool.Rent(maxIntersections);
var left = new Vector2(startX, y);
var right = new Vector2(endX, y);
Vector2[] buffer = arrayPool.Rent(maxIntersections);
// foreach line we get all the points where this line crosses the polygon
var pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0);
if (pointsFound == 0)
try
{
arrayPool.Return(buffer);
// nothign on this line skip
return;
}
QuickSortX(buffer, pointsFound);
int currentIntersection = 0;
float nextPoint = buffer[0].X;
float lastPoint = float.MinValue;
float targetPoint = nextPoint;
bool isInside = false;
// every odd point is the start of a line
Vector2 currentPoint = default(Vector2);
for (int x = minX; x < maxX; x++)
{
currentPoint.X = x;
currentPoint.Y = y;
if (!isInside)
{
if (x < (nextPoint - DrawPadding) && x > (lastPoint + DrawPadding))
{
if (nextPoint == right.X)
{
// we are in the ends run skip it
x = maxX;
continue;
}
// lets just jump forward
x = (int)Math.Floor(nextPoint) - DrawPadding;
}
}
bool onCorner = false;
// there seems to be some issue with this switch.
if (x >= nextPoint)
{
currentIntersection++;
lastPoint = nextPoint;
if (currentIntersection == pointsFound)
{
nextPoint = right.X;
}
else
{
nextPoint = buffer[currentIntersection].X;
// double point from a corner flip the bit back and move on again
if (nextPoint == lastPoint)
{
onCorner = true;
isInside ^= true;
currentIntersection++;
if (currentIntersection == pointsFound)
{
nextPoint = right.X;
}
else
{
nextPoint = buffer[currentIntersection].X;
}
}
}
isInside ^= true;
}
float opacity = 1;
if (!isInside && !onCorner)
{
if (this.options.Antialias)
{
float distance = float.MaxValue;
if (x == lastPoint || x == nextPoint)
{
// we are to far away from the line
distance = 0;
}
else if (nextPoint - AntialiasFactor < x)
{
// we are near the left of the line
distance = nextPoint - x;
}
else if (lastPoint + AntialiasFactor > x)
{
// we are near the right of the line
distance = x - lastPoint;
}
else
{
// we are to far away from the line
continue;
}
opacity = 1 - (distance / AntialiasFactor);
}
else
{
continue;
}
}
if (opacity > Constants.Epsilon)
{
Vector4 backgroundVector = sourcePixels[x, y].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[x, y] = packed;
}
}
arrayPool.Return(buffer);
});
if (this.options.Antialias)
{
// we only need to do the X can for antialiasing purposes
Parallel.For(
minX,
maxX,
this.ParallelOptions,
x =>
{
var buffer = arrayPool.Rent(maxIntersections);
var left = new Vector2(x, polyStartY);
var right = new Vector2(x, polyEndY);
Vector2 left = new Vector2(startX, y);
Vector2 right = new Vector2(endX, y);
// foreach line we get all the points where this line crosses the polygon
var pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0);
int pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0);
if (pointsFound == 0)
{
arrayPool.Return(buffer);
@ -228,65 +92,50 @@ namespace ImageSharp.Drawing.Processors
return;
}
QuickSortY(buffer, pointsFound);
QuickSortX(buffer, pointsFound);
int currentIntersection = 0;
float nextPoint = buffer[0].Y;
float lastPoint = left.Y;
float targetPoint = nextPoint;
float nextPoint = buffer[0].X;
float lastPoint = float.MinValue;
bool isInside = false;
// every odd point is the start of a line
Vector2 currentPoint = default(Vector2);
for (int y = minY; y < maxY; y++)
for (int x = minX; x < maxX; x++)
{
currentPoint.X = x;
currentPoint.Y = y;
if (!isInside)
{
if (y < (nextPoint - DrawPadding) && y > (lastPoint + DrawPadding))
{
if (nextPoint == right.Y)
{
// we are in the ends run skip it
y = maxY;
continue;
}
// lets just jump forward
y = (int)Math.Floor(nextPoint) - DrawPadding;
}
}
else
{
if (y < nextPoint - DrawPadding)
if (x < (nextPoint - DrawPadding) && x > (lastPoint + DrawPadding))
{
if (nextPoint == right.Y)
if (nextPoint == right.X)
{
// we are in the ends run skip it
y = maxY;
x = maxX;
continue;
}
// lets just jump forward
y = (int)Math.Floor(nextPoint);
x = (int)Math.Floor(nextPoint) - DrawPadding;
}
}
bool onCorner = false;
if (y >= nextPoint)
// there seems to be some issue with this switch.
if (x >= nextPoint)
{
currentIntersection++;
lastPoint = nextPoint;
if (currentIntersection == pointsFound)
{
nextPoint = right.Y;
nextPoint = right.X;
}
else
{
nextPoint = buffer[currentIntersection].Y;
nextPoint = buffer[currentIntersection].X;
// double point from a corner flip the bit back and move on again
if (nextPoint == lastPoint)
@ -296,11 +145,11 @@ namespace ImageSharp.Drawing.Processors
currentIntersection++;
if (currentIntersection == pointsFound)
{
nextPoint = right.Y;
nextPoint = right.X;
}
else
{
nextPoint = buffer[currentIntersection].Y;
nextPoint = buffer[currentIntersection].X;
}
}
}
@ -314,20 +163,20 @@ namespace ImageSharp.Drawing.Processors
if (this.options.Antialias)
{
float distance = float.MaxValue;
if (y == lastPoint || y == nextPoint)
if (x == lastPoint || x == nextPoint)
{
// we are to far away from the line
distance = 0;
}
else if (nextPoint - AntialiasFactor < y)
else if (nextPoint - AntialiasFactor < x)
{
// we are near the left of the line
distance = nextPoint - y;
distance = nextPoint - x;
}
else if (lastPoint + AntialiasFactor > y)
else if (lastPoint + AntialiasFactor > x)
{
// we are near the right of the line
distance = y - lastPoint;
distance = x - lastPoint;
}
else
{
@ -342,8 +191,7 @@ namespace ImageSharp.Drawing.Processors
}
}
// don't set full opactiy color as it will have been gotten by the first scan
if (opacity > Constants.Epsilon && opacity < 1)
if (opacity > Constants.Epsilon)
{
Vector4 backgroundVector = sourcePixels[x, y].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
@ -356,8 +204,172 @@ namespace ImageSharp.Drawing.Processors
sourcePixels[x, y] = packed;
}
}
}
finally
{
arrayPool.Return(buffer);
}
});
if (this.options.Antialias)
{
// we only need to do the X can for antialiasing purposes
Parallel.For(
minX,
maxX,
this.ParallelOptions,
x =>
{
Vector2[] buffer = arrayPool.Rent(maxIntersections);
try
{
Vector2 left = new Vector2(x, polyStartY);
Vector2 right = new Vector2(x, polyEndY);
// foreach line we get all the points where this line crosses the polygon
int pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0);
if (pointsFound == 0)
{
arrayPool.Return(buffer);
// nothign on this line skip
return;
}
QuickSortY(buffer, pointsFound);
int currentIntersection = 0;
float nextPoint = buffer[0].Y;
float lastPoint = left.Y;
bool isInside = false;
// every odd point is the start of a line
Vector2 currentPoint = default(Vector2);
for (int y = minY; y < maxY; y++)
{
currentPoint.X = x;
currentPoint.Y = y;
if (!isInside)
{
if (y < (nextPoint - DrawPadding) && y > (lastPoint + DrawPadding))
{
if (nextPoint == right.Y)
{
// we are in the ends run skip it
y = maxY;
continue;
}
// lets just jump forward
y = (int)Math.Floor(nextPoint) - DrawPadding;
}
}
else
{
if (y < nextPoint - DrawPadding)
{
if (nextPoint == right.Y)
{
// we are in the ends run skip it
y = maxY;
continue;
}
// lets just jump forward
y = (int)Math.Floor(nextPoint);
}
}
bool onCorner = false;
if (y >= nextPoint)
{
currentIntersection++;
lastPoint = nextPoint;
if (currentIntersection == pointsFound)
{
nextPoint = right.Y;
}
else
{
nextPoint = buffer[currentIntersection].Y;
// double point from a corner flip the bit back and move on again
if (nextPoint == lastPoint)
{
onCorner = true;
isInside ^= true;
currentIntersection++;
if (currentIntersection == pointsFound)
{
nextPoint = right.Y;
}
else
{
nextPoint = buffer[currentIntersection].Y;
}
}
}
isInside ^= true;
}
float opacity = 1;
if (!isInside && !onCorner)
{
if (this.options.Antialias)
{
float distance = float.MaxValue;
if (y == lastPoint || y == nextPoint)
{
// we are to far away from the line
distance = 0;
}
else if (nextPoint - AntialiasFactor < y)
{
// we are near the left of the line
distance = nextPoint - y;
}
else if (lastPoint + AntialiasFactor > y)
{
// we are near the right of the line
distance = y - lastPoint;
}
else
{
// we are to far away from the line
continue;
}
opacity = 1 - (distance / AntialiasFactor);
}
else
{
continue;
}
}
// don't set full opactiy color as it will have been gotten by the first scan
if (opacity > Constants.Epsilon && opacity < 1)
{
Vector4 backgroundVector = sourcePixels[x, y].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[x, y] = packed;
}
}
}
finally
{
arrayPool.Return(buffer);
}
});
}
}

3
src/ImageSharp.Drawing/Shapes/BezierPolygon.cs

@ -52,7 +52,8 @@ namespace ImageSharp.Drawing.Shapes
public float Distance(Vector2 point) => this.innerPolygon.Distance(point);
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>

28
src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs

@ -47,10 +47,10 @@ namespace ImageSharp.Drawing.Shapes
this.MaxIntersections = this.FixAndSetShapes(outlines, holes);
var minX = this.shapes.Min(x => x.Bounds.Left);
var maxX = this.shapes.Max(x => x.Bounds.Right);
var minY = this.shapes.Min(x => x.Bounds.Top);
var maxY = this.shapes.Max(x => x.Bounds.Bottom);
float minX = this.shapes.Min(x => x.Bounds.Left);
float maxX = this.shapes.Max(x => x.Bounds.Right);
float minY = this.shapes.Min(x => x.Bounds.Top);
float maxY = this.shapes.Max(x => x.Bounds.Bottom);
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
@ -89,7 +89,7 @@ namespace ImageSharp.Drawing.Shapes
bool inside = false;
foreach (IShape shape in this.shapes)
{
var d = shape.Distance(point);
float d = shape.Distance(point);
if (d <= 0)
{
@ -113,7 +113,9 @@ namespace ImageSharp.Drawing.Shapes
}
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on all the polygons, that make up this complex shape,
/// that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
@ -170,7 +172,7 @@ namespace ImageSharp.Drawing.Shapes
}
else
{
foreach (var path in shape)
foreach (IPath path in shape)
{
clipper.AddPath(
path,
@ -181,7 +183,7 @@ namespace ImageSharp.Drawing.Shapes
private void AddPoints(Clipper clipper, IEnumerable<IShape> shapes, PolyType polyType)
{
foreach (var shape in shapes)
foreach (IShape shape in shapes)
{
this.AddPoints(clipper, shape, polyType);
}
@ -201,14 +203,14 @@ namespace ImageSharp.Drawing.Shapes
else
{
// convert the Clipper Contour from scaled ints back down to the origional size (this is going to be lossy but not significantly)
var polygon = new Polygon(new Paths.LinearLineSegment(tree.Contour.ToArray()));
Polygon polygon = new Polygon(new Paths.LinearLineSegment(tree.Contour.ToArray()));
shapes.Add(polygon);
paths.Add(polygon);
}
}
foreach (var c in tree.Children)
foreach (PolyNode c in tree.Children)
{
this.ExtractOutlines(c, shapes, paths);
}
@ -216,13 +218,13 @@ namespace ImageSharp.Drawing.Shapes
private int FixAndSetShapes(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
{
var clipper = new Clipper();
Clipper clipper = new Clipper();
// add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses
this.AddPoints(clipper, outlines, PolyType.Subject);
this.AddPoints(clipper, holes, PolyType.Clip);
var tree = clipper.Execute();
PolyTree tree = clipper.Execute();
List<IShape> shapes = new List<IShape>();
List<IPath> paths = new List<IPath>();
@ -233,7 +235,7 @@ namespace ImageSharp.Drawing.Shapes
this.paths = paths.ToArray();
int intersections = 0;
foreach (var s in this.shapes)
foreach (IShape s in this.shapes)
{
intersections += s.MaxIntersections;
}

3
src/ImageSharp.Drawing/Shapes/IShape.cs

@ -40,7 +40,8 @@ namespace ImageSharp.Drawing.Shapes
float Distance(Vector2 point);
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>

3
src/ImageSharp.Drawing/Shapes/LinearPolygon.cs

@ -58,7 +58,8 @@ namespace ImageSharp.Drawing.Shapes
public float Distance(Vector2 point) => this.innerPolygon.Distance(point);
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>

5
src/ImageSharp.Drawing/Shapes/Polygon.cs

@ -82,7 +82,7 @@ namespace ImageSharp.Drawing.Shapes
{
bool isInside = this.innerPath.PointInPolygon(point);
var distance = this.innerPath.DistanceFromPath(point).DistanceFromPath;
float distance = this.innerPath.DistanceFromPath(point).DistanceFromPath;
if (isInside)
{
return -distance;
@ -137,7 +137,8 @@ namespace ImageSharp.Drawing.Shapes
}
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start" /> and <paramref name="end" />
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>

8
src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs

@ -110,7 +110,7 @@ namespace ImageSharp.Drawing.Shapes
public float Distance(Vector2 point)
{
bool insidePoly;
var result = this.Distance(point, true, out insidePoly);
PointInfo result = this.Distance(point, true, out insidePoly);
// invert the distance from path when inside
return insidePoly ? -result.DistanceFromPath : result.DistanceFromPath;
@ -150,7 +150,9 @@ namespace ImageSharp.Drawing.Shapes
}
/// <summary>
/// Finds the intersections.
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the edges of the <see cref="RectangularPolygon"/>
/// that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
@ -162,7 +164,7 @@ namespace ImageSharp.Drawing.Shapes
/// </returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
var discovered = 0;
int discovered = 0;
Vector2 startPoint = Vector2.Clamp(start, this.topLeft, this.bottomRight);
Vector2 endPoint = Vector2.Clamp(end, this.topLeft, this.bottomRight);

Loading…
Cancel
Save