Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Source/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<AvaloniaVersion>12.0.2</AvaloniaVersion>
<AvaloniaVersion>12.1.4</AvaloniaVersion>
<OxyPlotCoreVersion>2.2.0</OxyPlotCoreVersion>
</PropertyGroup>
</Project>
148 changes: 134 additions & 14 deletions Source/OxyPlot.Avalonia/CanvasRenderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ public override void DrawEllipse(OxyRect rect,
double thickness,
EdgeRenderingMode edgeRenderingMode)
{
if (!IsFinite(rect))
{
return;
}

var path = CreateAndAdd<Path>();
SetStroke(path, stroke, thickness, edgeRenderingMode);
if (!fill.IsUndefined())
Expand Down Expand Up @@ -136,6 +141,11 @@ private void DrawEllipsesByStreamGeometry(

foreach (var rect in rects)
{
if (!IsFinite(rect))
{
continue;
}

if (path == null)
{
path = CreateAndAdd<Path>();
Expand Down Expand Up @@ -220,6 +230,12 @@ public override void DrawLine(
double[] dashArray,
LineJoin lineJoin)
{
points = FiniteOnly(points);
if (points.Count == 0)
{
return;
}

if (thickness < BalancedLineDrawingThicknessLimit)
{
DrawLineBalanced(points, stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
Expand Down Expand Up @@ -260,6 +276,11 @@ private void DrawLineSegmentsByStreamGeometry(

for (int i = 0; i + 1 < points.Count; i += 2)
{
if (!IsFinite(points[i]) || !IsFinite(points[i + 1]))
{
continue;
}

if (streamGeometry == null)
{
streamGeometry = new StreamGeometry();
Expand Down Expand Up @@ -325,6 +346,11 @@ public override void DrawLineSegments(

for (int i = 0; i + 1 < points.Count; i += 2)
{
if (!IsFinite(points[i]) || !IsFinite(points[i + 1]))
{
continue;
}

if (path == null)
{
path = CreateAndAdd<Path>();
Expand Down Expand Up @@ -365,6 +391,11 @@ public override void DrawPolygon(
double[] dashArray,
LineJoin lineJoin)
{
if (!IsFinite(points))
{
return;
}

var e = CreateAndAdd<Polygon>();
SetStroke(e, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);

Expand Down Expand Up @@ -410,6 +441,11 @@ private void DrawPolygonsByStreamGeometry(

foreach (var polygon in polygons)
{
if (!IsFinite(polygon))
{
continue;
}

if (path == null)
{
path = CreateAndAdd<Path>();
Expand Down Expand Up @@ -498,6 +534,11 @@ public override void DrawPolygons(

foreach (var polygon in polygons)
{
if (!IsFinite(polygon))
{
continue;
}

if (path == null)
{
path = CreateAndAdd<Path>();
Expand Down Expand Up @@ -563,6 +604,11 @@ public override void DrawRectangle(OxyRect rect,
double thickness,
EdgeRenderingMode edgeRenderingMode)
{
if (!IsFinite(rect))
{
return;
}

var path = CreateAndAdd<Path>();
SetStroke(path, stroke, thickness, edgeRenderingMode);
if (!fill.IsUndefined())
Expand Down Expand Up @@ -615,6 +661,11 @@ public override void DrawText(
VerticalAlignment valign,
OxySize? maxSize)
{
if (!IsFinite(p))
{
return;
}

var tb = CreateAndAdd<TextBlock>();
tb.Text = text;
tb.Foreground = GetCachedBrush(fill);
Expand All @@ -638,7 +689,9 @@ public override void DrawText(

if (maxSize != null || halign != HorizontalAlignment.Left || valign != VerticalAlignment.Top)
{
tb.Measure(new Size(1000, 1000));
// Measure without a limit: the hard-coded 1000x1000 clipped long labels.
// The required truncation is done by the block below according to maxSize.
tb.Measure(Size.Infinity);
var size = tb.DesiredSize;
if (maxSize != null)
{
Expand Down Expand Up @@ -730,7 +783,8 @@ public override OxySize MeasureText(string text,
tb.FontWeight = GetFontWeight(fontWeight);
}

tb.Measure(new Size(1000, 1000));
// Measure without a limit: the hard-coded 1000x1000 understated the size of long labels.
tb.Measure(Size.Infinity);

return new OxySize(tb.DesiredSize.Width, tb.DesiredSize.Height);
}
Expand Down Expand Up @@ -771,25 +825,33 @@ public override void DrawImage(
double opacity,
bool interpolate)
{
if (destWidth <= 0 || destHeight <= 0 || srcWidth <= 0 || srcHeight <= 0)
if (!(destWidth > 0) || !(destHeight > 0) || !(srcWidth > 0) || !(srcHeight > 0)
|| !double.IsFinite(destX) || !double.IsFinite(destY)
|| !double.IsFinite(destWidth) || !double.IsFinite(destHeight))
{
return;
}

var image = CreateAndAdd<Image>(destX, destY);
var bitmapChain = GetImageSource(source);

// ReSharper disable CompareOfFloatsByEqualityOperator
if (srcX == 0 && srcY == 0 && srcWidth == bitmapChain.PixelSize.Width && srcHeight == bitmapChain.PixelSize.Height)
// ReSharper restore CompareOfFloatsByEqualityOperator
var bitmap = GetImageSource(source);
if (bitmap == null)
{
// do not crop
return;
}
else

// The part of the source is cropped by whole pixels within the bitmap. Clip is not suitable here:
// it is specified in the coordinates of the element, not of the source, and overrides the clipping to the plot area.
var left = Math.Max(0, (int)Math.Floor(srcX));
var top = Math.Max(0, (int)Math.Floor(srcY));
var right = Math.Min(bitmap.PixelSize.Width, (int)Math.Ceiling(srcX + srcWidth));
var bottom = Math.Min(bitmap.PixelSize.Height, (int)Math.Ceiling(srcY + srcHeight));
if (right <= left || bottom <= top)
{
image.Clip = new RectangleGeometry(new Rect(srcX, srcY, srcWidth, srcHeight));
return;
}

var image = CreateAndAdd<Image>(destX, destY);
var isWholeBitmap = left == 0 && top == 0 && right == bitmap.PixelSize.Width && bottom == bitmap.PixelSize.Height;

image.Opacity = opacity;
image.Width = destWidth;
image.Height = destHeight;
Expand All @@ -800,7 +862,9 @@ public override void DrawImage(
Canvas.SetTop(image, destY);
//// alternative: image.RenderTransform = new TranslateTransform(destX, destY);

image.Source = bitmapChain;
image.Source = isWholeBitmap
? bitmap
: new CroppedBitmap(bitmap, new PixelRect(left, top, right - left, bottom - top));

if (interpolate)
{
Expand Down Expand Up @@ -1024,9 +1088,13 @@ private void DrawLineBalanced(IList<ScreenPoint> points, OxyColor stroke, double
double lineLength = 0;
var dashPatternLength = (dashArray != null) ? dashArray.Sum() : 0;
var last = new Point();

// The anti-aliasing mode does not depend on the point index, while the check walks the whole list,
// so it is computed once before the loop. Inside the loop it gave quadratic complexity.
var aliased = !this.ShouldUseAntiAliasingForLine(edgeRenderingMode, points);

for (int i = 0; i < n; i++)
{
bool aliased = !this.ShouldUseAntiAliasingForLine(edgeRenderingMode, points);
var p = aliased ? ToPixelAlignedPoint(points[i]) : ToPoint(points[i]);
pc.Add(p);

Expand Down Expand Up @@ -1105,6 +1173,58 @@ private static Rect ToRect(OxyRect r)
return new Rect(r.Left, r.Top, r.Width, r.Height);
}

// A shape with NaN or infinite coordinates breaks the whole Canvas layout pass
// (InvalidOperationException from Measure), not just itself. The core produces such
// coordinates for annotations that are not fully configured, so these shapes are not drawn.

/// <summary>
/// Checks that all coordinates of the rectangle are finite.
/// </summary>
/// <param name="r">The rectangle.</param>
/// <returns><c>true</c> if the shape can be drawn.</returns>
private static bool IsFinite(OxyRect r)
{
return double.IsFinite(r.Left) && double.IsFinite(r.Top) && double.IsFinite(r.Width) && double.IsFinite(r.Height);
}

/// <summary>
/// Checks that both coordinates of the point are finite.
/// </summary>
/// <param name="p">The point.</param>
/// <returns><c>true</c> if the point can be drawn.</returns>
private static bool IsFinite(ScreenPoint p)
{
return double.IsFinite(p.X) && double.IsFinite(p.Y);
}

/// <summary>
/// Checks that the coordinates of all points are finite.
/// </summary>
/// <param name="points">The points.</param>
/// <returns><c>true</c> if the shape can be drawn.</returns>
private static bool IsFinite(IList<ScreenPoint> points)
{
for (var i = 0; i < points.Count; i++)
{
if (!IsFinite(points[i]))
{
return false;
}
}

return true;
}

/// <summary>
/// Removes points with non-finite coordinates from a line. If there are none, returns the same list.
/// </summary>
/// <param name="points">The points of the line.</param>
/// <returns>The points with finite coordinates.</returns>
private static IList<ScreenPoint> FiniteOnly(IList<ScreenPoint> points)
{
return IsFinite(points) ? points : points.Where(IsFinite).ToList();
}

/// <summary>
/// Converts an <see cref="OxyRect" /> to a pixel aligned <see cref="Rect" />.
/// </summary>
Expand Down
65 changes: 61 additions & 4 deletions Source/OxyPlot.Avalonia/PlotBase.Export.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OxyPlot.Avalonia
{
using global::Avalonia.Media.Imaging;
using System;
using System.IO;

/// <summary>
Expand All @@ -23,7 +24,7 @@ public partial class PlotBase
/// <param name="stream">Stream to which to write the bitmap.</param>
public void SaveBitmap(Stream stream)
{
SaveBitmap(stream, -1, -1, ActualModel.Background);
SaveBitmap(stream, -1, -1, GetModelForExport().Background);
}

/// <summary>
Expand All @@ -35,6 +36,8 @@ public void SaveBitmap(Stream stream)
/// <param name="background">The background.</param>
public void SaveBitmap(Stream stream, int width, int height, OxyColor background)
{
var model = GetModelForExport();

if (width <= 0)
{
width = (int)Bounds.Width;
Expand All @@ -45,12 +48,25 @@ public void SaveBitmap(Stream stream, int width, int height, OxyColor background
height = (int)Bounds.Height;
}

CheckExportSize(width, height);

if (!background.IsVisible())
{
background = Background.ToOxyColor();
}

PngExporter.Export(ActualModel, stream, width, height, background);
try
{
lock (model.SyncRoot)
{
PngExporter.Export(model, stream, width, height, background);
}
}
finally
{
// The export recalculated the live model for its own size, so it is restored to the size of the control
InvalidatePlot(false);
}
}

/// <summary>
Expand All @@ -59,8 +75,49 @@ public void SaveBitmap(Stream stream, int width, int height, OxyColor background
/// <returns>A bitmap.</returns>
public Bitmap ToBitmap()
{
var background = ActualModel.Background.IsVisible() ? ActualModel.Background : Background.ToOxyColor();
return PngExporter.ExportToBitmap(ActualModel, (int)Bounds.Width, (int)Bounds.Height, background);
var model = GetModelForExport();
var width = (int)Bounds.Width;
var height = (int)Bounds.Height;
CheckExportSize(width, height);

var background = model.Background.IsVisible() ? model.Background : Background.ToOxyColor();
try
{
lock (model.SyncRoot)
{
return PngExporter.ExportToBitmap(model, width, height, background);
}
}
finally
{
// The export recalculated the live model for its own size, so it is restored to the size of the control
InvalidatePlot(false);
}
}

/// <summary>
/// Gets the model for export.
/// </summary>
/// <returns>The current model.</returns>
/// <exception cref="InvalidOperationException">There is no model.</exception>
private PlotModel GetModelForExport()
{
return ActualModel ?? throw new InvalidOperationException("Cannot export a plot without a model: the Model property is not set.");
}

/// <summary>
/// Validates the bitmap size for export.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <exception cref="InvalidOperationException">The size is less than one pixel.</exception>
private static void CheckExportSize(int width, int height)
{
if (width < 1 || height < 1)
{
throw new InvalidOperationException(
"The control has not been laid out yet or is hidden, so its size is zero. Specify the width and height explicitly in SaveBitmap.");
}
}
}
}
Loading