diff --git a/README.md b/README.md index 62790c8..9b964ce 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,17 @@ _[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_ -A .NET Standard 2.0 library that makes it easier to create beautiful console applications. +A .NET 5/.NET Standard 2.0 library that makes it easier to create beautiful, cross platform, console applications. It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich) for Python. ## Table of Contents 1. [Features](#features) -2. [Example](#example) -3. [Installing](#installing) -4. [Usage](#usage) - 4.1. [Using the static API](#using-the-static-api) - 4.2. [Creating a console](#creating-a-console) -5. [Running examples](#running-examples) +2. [Installing](#installing) +3. [Documentation](#documentation) +4. [Examples](#examples) +5. [License](#license) ## Features @@ -25,77 +23,27 @@ for Python. and blinking text. * Supports 3/4/8/24-bit colors in the terminal. The library will detect the capabilities of the current terminal - and downgrade colors as needed. + and downgrade colors as needed. -## Example ![Example](resources/gfx/screenshots/example.png) ## Installing -The fastest way of getting started using Spectre.Console is to install the NuGet package. +The fastest way of getting started using `Spectre.Console` is to install the NuGet package. ```csharp dotnet add package Spectre.Console ``` -## Usage +## Documentation -The `Spectre.Console` API is stateful and is not thread-safe. -If you need to write to the console from different threads, make sure that -you take appropriate precautions, just like when you use the -regular `System.Console` API. +The documentation for `Spectre.Console` can be found at +https://spectresystems.github.io/spectre.console/ -If the current terminal does not support ANSI escape sequences, -`Spectre.Console` will fallback to using the `System.Console` API. +## Examples -_NOTE: This library is currently under development and APIs -might change or get removed at any point up until a 1.0 release._ - -### Using the static API - -The static API is perfect when you just want to output text -like you usually do with the `System.Console` API, but prettier. - -```csharp -AnsiConsole.Foreground = Color.CornflowerBlue; -AnsiConsole.Decoration = Decoration.Underline | Decoration.Bold; -AnsiConsole.WriteLine("Hello World!"); - -AnsiConsole.Reset(); -AnsiConsole.MarkupLine("[bold yellow on red]{0}[/] [underline]world[/]!", "Goodbye"); -``` - -If you want to get a reference to the default `IAnsiConsole`, -you can access it via `AnsiConsole.Console`. - -### Creating a console - -Sometimes it's useful to explicitly create a console with specific -capabilities, such as during unit testing when you want control -over the environment your code runs in. - -It's recommended to not use `AnsiConsole` in code that run as -part of a unit test. - -```csharp -IAnsiConsole console = AnsiConsole.Create( - new AnsiConsoleSettings() - { - Ansi = AnsiSupport.Yes, - ColorSystem = ColorSystemSupport.TrueColor, - Out = new StringWriter(), - }); -``` - -_NOTE: Even if you can specify a specific color system to use -when manually creating a console, remember that the user's terminal -might not be able to use it, so unless you're creating an IAnsiConsole -for testing, always use `ColorSystemSupport.Detect` and `AnsiSupport.Detect`._ - -## Running examples - -To see Spectre.Console in action, install the +To see `Spectre.Console` in action, install the [dotnet-example](https://github.com/patriksvensson/dotnet-example) global tool. @@ -107,34 +55,18 @@ Now you can list available examples in this repository: ``` > dotnet example - -╭────────────┬───────────────────────────────────────┬──────────────────────────────────────────────────────╮ -│ Name │ Path │ Description │ -├────────────┼───────────────────────────────────────┼──────────────────────────────────────────────────────┤ -│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │ -│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │ -│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │ -│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │ -│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │ -│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │ -│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │ -│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │ -│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │ -│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │ -│ Rules │ examples/Rules/Rules.csproj │ Demonstrates how to render horizontal rules (lines). │ -│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │ -╰────────────┴───────────────────────────────────────┴──────────────────────────────────────────────────────╯ ``` And to run an example: ``` > dotnet example tables -┌──────────┬──────────┬────────┐ -│ Foo │ Bar │ Baz │ -├──────────┼──────────┼────────┤ -│ Hello │ World! │ │ -│ Bonjour │ le │ monde! │ -│ Hej │ Världen! │ │ -└──────────┴──────────┴────────┘ -``` \ No newline at end of file +``` + +## License + +Copyright © Spectre Systems. + +Spectre.Console is provided as-is under the MIT license. For more information see LICENSE. + +* For SixLabors.ImageSharp, see https://github.com/SixLabors/ImageSharp/blob/master/LICENSE \ No newline at end of file diff --git a/examples/Canvas/Canvas.csproj b/examples/Canvas/Canvas.csproj new file mode 100644 index 0000000..a98bd15 --- /dev/null +++ b/examples/Canvas/Canvas.csproj @@ -0,0 +1,22 @@ + + + + Exe + netcoreapp3.1 + false + Canvas + Demonstrates how to render pixels and images. + + + + + + + + + + PreserveNewest + + + + diff --git a/examples/Canvas/Mandelbrot.cs b/examples/Canvas/Mandelbrot.cs new file mode 100644 index 0000000..7f3a3da --- /dev/null +++ b/examples/Canvas/Mandelbrot.cs @@ -0,0 +1,87 @@ +/* +Ported from: https://rosettacode.org/wiki/Mandelbrot_set#C.23 +Licensed under GNU Free Documentation License 1.2 +*/ + +using System; +using Spectre.Console; + +namespace CanvasExample +{ + public static class Mandelbrot + { + private const double MaxValueExtent = 2.0; + + private struct ComplexNumber + { + public double Real { get; } + public double Imaginary { get; } + + public ComplexNumber(double real, double imaginary) + { + Real = real; + Imaginary = imaginary; + } + + public static ComplexNumber operator +(ComplexNumber x, ComplexNumber y) + { + return new ComplexNumber(x.Real + y.Real, x.Imaginary + y.Imaginary); + } + + public static ComplexNumber operator *(ComplexNumber x, ComplexNumber y) + { + return new ComplexNumber(x.Real * y.Real - x.Imaginary * y.Imaginary, + x.Real * y.Imaginary + x.Imaginary * y.Real); + } + + public double Abs() + { + return Real * Real + Imaginary * Imaginary; + } + } + + public static Canvas Generate(int width, int height) + { + var canvas = new Canvas(width, height); + + var scale = 2 * MaxValueExtent / Math.Min(canvas.Width, canvas.Height); + for (var i = 0; i < canvas.Height; i++) + { + var y = (canvas.Height / 2 - i) * scale; + for (var j = 0; j < canvas.Width; j++) + { + var x = (j - canvas.Width / 2) * scale; + var value = Calculate(new ComplexNumber(x, y)); + canvas.SetPixel(j, i, GetColor(value)); + } + } + + return canvas; + } + + private static double Calculate(ComplexNumber c) + { + const int MaxIterations = 1000; + const double MaxNorm = MaxValueExtent * MaxValueExtent; + + var iteration = 0; + var z = new ComplexNumber(); + do + { + z = z * z + c; + iteration++; + } while (z.Abs() < MaxNorm && iteration < MaxIterations); + + return iteration < MaxIterations + ? (double)iteration / MaxIterations + : 0; + } + + private static Color GetColor(double value) + { + const double MaxColor = 256; + const double ContrastValue = 0.2; + return new Color(0, 0, (byte)(MaxColor * Math.Pow(value, ContrastValue))); + } + } +} diff --git a/examples/Canvas/Program.cs b/examples/Canvas/Program.cs new file mode 100644 index 0000000..337961d --- /dev/null +++ b/examples/Canvas/Program.cs @@ -0,0 +1,36 @@ +using SixLabors.ImageSharp.Processing; +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace CanvasExample +{ + public static class Program + { + public static void Main() + { + // Draw a mandelbrot set using a Canvas + var mandelbrot = Mandelbrot.Generate(32, 32); + Render(mandelbrot, "Mandelbrot"); + + // Draw an image using CanvasImage powered by ImageSharp. + // This requires the "Spectre.Console.ImageSharp" NuGet package. + var image = new CanvasImage("cake.png"); + image.BilinearResampler(); + image.MaxWidth(16); + Render(image, "Image from file (16 wide)"); + + // Draw image again, but without render width + image.NoMaxWidth(); + image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop()); + Render(image, "Image from file (fit, greyscale, rotated)"); + } + + private static void Render(IRenderable canvas, string title) + { + AnsiConsole.WriteLine(); + AnsiConsole.Render(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey")); + AnsiConsole.WriteLine(); + AnsiConsole.Render(canvas); + } + } +} diff --git a/examples/Canvas/cake.png b/examples/Canvas/cake.png new file mode 100644 index 0000000..f11d285 Binary files /dev/null and b/examples/Canvas/cake.png differ diff --git a/examples/Rules/Program.cs b/examples/Rules/Program.cs index e65ac97..8a03924 100644 --- a/examples/Rules/Program.cs +++ b/examples/Rules/Program.cs @@ -7,34 +7,34 @@ namespace EmojiExample public static void Main(string[] args) { // No title - WrapInPanel( + Render( new Rule() .RuleStyle(Style.Parse("yellow")) .AsciiBorder() .LeftAligned()); // Left aligned title - WrapInPanel( + Render( new Rule("[blue]Left aligned[/]") .RuleStyle(Style.Parse("red")) .DoubleBorder() .LeftAligned()); // Centered title - WrapInPanel( + Render( new Rule("[green]Centered[/]") .RuleStyle(Style.Parse("green")) .HeavyBorder() .Centered()); // Right aligned title - WrapInPanel( + Render( new Rule("[red]Right aligned[/]") .RuleStyle(Style.Parse("blue")) .RightAligned()); } - private static void WrapInPanel(Rule rule) + private static void Render(Rule rule) { AnsiConsole.Render(rule); AnsiConsole.WriteLine(); diff --git a/src/Spectre.Console.ImageSharp/CanvasImage.cs b/src/Spectre.Console.ImageSharp/CanvasImage.cs new file mode 100644 index 0000000..fdf4bea --- /dev/null +++ b/src/Spectre.Console.ImageSharp/CanvasImage.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// Represents a renderable image. + /// + public sealed class CanvasImage : Renderable + { + private static readonly IResampler _defaultResampler = KnownResamplers.Bicubic; + + /// + /// Gets the image width. + /// + public int Width => Image.Width; + + /// + /// Gets the image height. + /// + public int Height => Image.Height; + + /// + /// Gets or sets the render width of the canvas. + /// + public int? MaxWidth { get; set; } + + /// + /// Gets or sets the render width of the canvas. + /// + public int PixelWidth { get; set; } = 2; + + /// + /// Gets or sets the that should + /// be used when scaling the image. Defaults to bicubic sampling. + /// + public IResampler? Resampler { get; set; } + + internal SixLabors.ImageSharp.Image Image { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The image filename. + public CanvasImage(string filename) + { + Image = SixLabors.ImageSharp.Image.Load(filename); + } + + /// + protected override Measurement Measure(RenderContext context, int maxWidth) + { + if (PixelWidth < 0) + { + throw new InvalidOperationException("Pixel width must be greater than zero."); + } + + var width = MaxWidth ?? Width; + if (maxWidth < width * PixelWidth) + { + return new Measurement(maxWidth, maxWidth); + } + + return new Measurement(width * PixelWidth, width * PixelWidth); + } + + /// + protected override IEnumerable Render(RenderContext context, int maxWidth) + { + var image = Image; + + var width = Width; + var height = Height; + + // Got a max width? + if (MaxWidth != null) + { + height = (int)(height * ((float)MaxWidth.Value) / Width); + width = MaxWidth.Value; + } + + // Exceed the max width when we take pixel width into account? + if (width * PixelWidth > maxWidth) + { + height = (int)(height * (maxWidth / (float)(width * PixelWidth))); + width = maxWidth / PixelWidth; + } + + // Need to rescale the pixel buffer? + if (width != Width || height != Height) + { + var resampler = Resampler ?? _defaultResampler; + image = image.Clone(); // Clone the original image + image.Mutate(i => i.Resize(width, height, resampler)); + } + + var canvas = new Canvas(width, height) + { + MaxWidth = MaxWidth, + PixelWidth = PixelWidth, + Scale = false, + }; + + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + if (image[x, y].A == 0) + { + continue; + } + + canvas.SetPixel(x, y, new Color( + image[x, y].R, image[x, y].G, image[x, y].B)); + } + } + + return ((IRenderable)canvas).Render(context, maxWidth); + } + } +} diff --git a/src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs b/src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs new file mode 100644 index 0000000..fa99fe6 --- /dev/null +++ b/src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs @@ -0,0 +1,135 @@ +using System; +using SixLabors.ImageSharp.Processing; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class CanvasImageExtensions + { + /// + /// Sets the maximum width of the rendered image. + /// + /// The canvas image. + /// The maximum width. + /// The same instance so that multiple calls can be chained. + public static CanvasImage MaxWidth(this CanvasImage image, int? maxWidth) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + image.MaxWidth = maxWidth; + return image; + } + + /// + /// Disables the maximum width of the rendered image. + /// + /// The canvas image. + /// The same instance so that multiple calls can be chained. + public static CanvasImage NoMaxWidth(this CanvasImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + image.MaxWidth = null; + return image; + } + + /// + /// Sets the pixel width. + /// + /// The canvas image. + /// The pixel width. + /// The same instance so that multiple calls can be chained. + public static CanvasImage PixelWidth(this CanvasImage image, int width) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + image.PixelWidth = width; + return image; + } + + /// + /// Mutates the underlying image. + /// + /// The canvas image. + /// The action that mutates the underlying image. + /// The same instance so that multiple calls can be chained. + public static CanvasImage Mutate(this CanvasImage image, Action action) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + image.Image.Mutate(action); + return image; + } + + /// + /// Uses a bicubic sampler that implements the bicubic kernel algorithm W(x). + /// + /// The canvas image. + /// The same instance so that multiple calls can be chained. + public static CanvasImage BicubicResampler(this CanvasImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + image.Resampler = KnownResamplers.Bicubic; + return image; + } + + /// + /// Uses a bilinear sampler. This interpolation algorithm + /// can be used where perfect image transformation with pixel matching is impossible, + /// so that one can calculate and assign appropriate intensity values to pixels. + /// + /// The canvas image. + /// The same instance so that multiple calls can be chained. + public static CanvasImage BilinearResampler(this CanvasImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + image.Resampler = KnownResamplers.Triangle; + return image; + } + + /// + /// Uses a Nearest-Neighbour sampler that implements the nearest neighbor algorithm. + /// This uses a very fast, unscaled filter which will select the closest pixel to + /// the new pixels position. + /// + /// The canvas image. + /// The same instance so that multiple calls can be chained. + public static CanvasImage NearestNeighborResampler(this CanvasImage image) + { + if (image is null) + { + throw new ArgumentNullException(nameof(image)); + } + + image.Resampler = KnownResamplers.NearestNeighbor; + return image; + } + } +} diff --git a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj new file mode 100644 index 0000000..f5c18ad --- /dev/null +++ b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + enable + A library that extends Spectre.Console with ImageSharp super powers. + + + + + + + + + + + + + + + + diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index c9a4ba3..a1af2ed 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -54,6 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "..\examples\Promp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Canvas\Canvas.csproj", "{5693761A-754A-40A8-9144-36510D6A4D69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -268,6 +272,30 @@ Global {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x64.Build.0 = Release|Any CPU {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.ActiveCfg = Release|Any CPU {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.Build.0 = Release|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x64.ActiveCfg = Debug|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x64.Build.0 = Debug|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x86.ActiveCfg = Debug|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x86.Build.0 = Debug|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Release|Any CPU.Build.0 = Release|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x64.ActiveCfg = Release|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x64.Build.0 = Release|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x86.ActiveCfg = Release|Any CPU + {5693761A-754A-40A8-9144-36510D6A4D69}.Release|x86.Build.0 = Release|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.Build.0 = Debug|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.ActiveCfg = Debug|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.Build.0 = Debug|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.Build.0 = Release|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.ActiveCfg = Release|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.Build.0 = Release|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.ActiveCfg = Release|Any CPU + {0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -289,6 +317,7 @@ Global {75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} diff --git a/src/Spectre.Console/Widgets/Canvas.cs b/src/Spectre.Console/Widgets/Canvas.cs new file mode 100644 index 0000000..acad4ce --- /dev/null +++ b/src/Spectre.Console/Widgets/Canvas.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// Represents a renderable canvas. + /// + public sealed class Canvas : Renderable + { + private readonly Color?[,] _pixels; + + /// + /// Gets the width of the canvas. + /// + public int Width { get; } + + /// + /// Gets the height of the canvas. + /// + public int Height { get; } + + /// + /// Gets or sets the render width of the canvas. + /// + public int? MaxWidth { get; set; } + + /// + /// Gets or sets a value indicating whether or not + /// to scale the canvas when rendering. + /// + public bool Scale { get; set; } = true; + + /// + /// Gets or sets the pixel width. + /// + public int PixelWidth { get; set; } = 2; + + /// + /// Initializes a new instance of the class. + /// + /// The canvas width. + /// The canvas height. + public Canvas(int width, int height) + { + Width = width; + Height = height; + + _pixels = new Color?[Width, Height]; + } + + /// + /// Sets a pixel with the specified color in the canvas at the specified location. + /// + /// The X coordinate for the pixel. + /// The Y coordinate for the pixel. + /// The pixel color. + public void SetPixel(int x, int y, Color color) + { + _pixels[x, y] = color; + } + + /// + protected override Measurement Measure(RenderContext context, int maxWidth) + { + if (PixelWidth < 0) + { + throw new InvalidOperationException("Pixel width must be greater than zero."); + } + + var width = MaxWidth ?? Width; + + if (maxWidth < width * PixelWidth) + { + return new Measurement(maxWidth, maxWidth); + } + + return new Measurement(width * PixelWidth, width * PixelWidth); + } + + /// + protected override IEnumerable Render(RenderContext context, int maxWidth) + { + if (PixelWidth < 0) + { + throw new InvalidOperationException("Pixel width must be greater than zero."); + } + + var pixels = _pixels; + var pixel = new string(' ', PixelWidth); + var width = Width; + var height = Height; + + // Got a max width? + if (MaxWidth != null) + { + height = (int)(height * ((float)MaxWidth.Value) / Width); + width = MaxWidth.Value; + } + + // Exceed the max width when we take pixel width into account? + if (width * PixelWidth > maxWidth) + { + height = (int)(height * (maxWidth / (float)(width * PixelWidth))); + width = maxWidth / PixelWidth; + } + + // Need to rescale the pixel buffer? + if (Scale && (width != Width || height != Height)) + { + pixels = ScaleDown(width, height); + } + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var color = pixels[x, y]; + if (color != null) + { + yield return new Segment(pixel, new Style(background: color)); + } + else + { + yield return new Segment(pixel); + } + } + + yield return Segment.LineBreak; + } + } + + private Color?[,] ScaleDown(int newWidth, int newHeight) + { + var buffer = new Color?[newWidth, newHeight]; + var xRatio = ((Width << 16) / newWidth) + 1; + var yRatio = ((Height << 16) / newHeight) + 1; + + for (var i = 0; i < newHeight; i++) + { + for (var j = 0; j < newWidth; j++) + { + buffer[j, i] = _pixels[(j * xRatio) >> 16, (i * yRatio) >> 16]; + } + } + + return buffer; + } + } +}