mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 08:52:50 +08:00
Add canvas and image support
Adds support for drawing "pixels" and displaying images in the terminal.
This commit is contained in:
parent
4f6eca4fcb
commit
2a9fa223de
106
README.md
106
README.md
@ -2,19 +2,17 @@
|
|||||||
|
|
||||||
_[](https://www.nuget.org/packages/spectre.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)
|
It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich)
|
||||||
for Python.
|
for Python.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Features](#features)
|
1. [Features](#features)
|
||||||
2. [Example](#example)
|
2. [Installing](#installing)
|
||||||
3. [Installing](#installing)
|
3. [Documentation](#documentation)
|
||||||
4. [Usage](#usage)
|
4. [Examples](#examples)
|
||||||
4.1. [Using the static API](#using-the-static-api)
|
5. [License](#license)
|
||||||
4.2. [Creating a console](#creating-a-console)
|
|
||||||
5. [Running examples](#running-examples)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -27,75 +25,25 @@ for Python.
|
|||||||
The library will detect the capabilities of the current terminal
|
The library will detect the capabilities of the current terminal
|
||||||
and downgrade colors as needed.
|
and downgrade colors as needed.
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installing
|
## 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
|
```csharp
|
||||||
dotnet add package Spectre.Console
|
dotnet add package Spectre.Console
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Documentation
|
||||||
|
|
||||||
The `Spectre.Console` API is stateful and is not thread-safe.
|
The documentation for `Spectre.Console` can be found at
|
||||||
If you need to write to the console from different threads, make sure that
|
https://spectresystems.github.io/spectre.console/
|
||||||
you take appropriate precautions, just like when you use the
|
|
||||||
regular `System.Console` API.
|
|
||||||
|
|
||||||
If the current terminal does not support ANSI escape sequences,
|
## Examples
|
||||||
`Spectre.Console` will fallback to using the `System.Console` API.
|
|
||||||
|
|
||||||
_NOTE: This library is currently under development and APIs
|
To see `Spectre.Console` in action, install the
|
||||||
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
|
|
||||||
[dotnet-example](https://github.com/patriksvensson/dotnet-example)
|
[dotnet-example](https://github.com/patriksvensson/dotnet-example)
|
||||||
global tool.
|
global tool.
|
||||||
|
|
||||||
@ -107,34 +55,18 @@ Now you can list available examples in this repository:
|
|||||||
|
|
||||||
```
|
```
|
||||||
> dotnet example
|
> 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:
|
And to run an example:
|
||||||
|
|
||||||
```
|
```
|
||||||
> dotnet example tables
|
> dotnet example tables
|
||||||
┌──────────┬──────────┬────────┐
|
|
||||||
│ Foo │ Bar │ Baz │
|
|
||||||
├──────────┼──────────┼────────┤
|
|
||||||
│ Hello │ World! │ │
|
|
||||||
│ Bonjour │ le │ monde! │
|
|
||||||
│ Hej │ Världen! │ │
|
|
||||||
└──────────┴──────────┴────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
22
examples/Canvas/Canvas.csproj
Normal file
22
examples/Canvas/Canvas.csproj
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<Title>Canvas</Title>
|
||||||
|
<Description>Demonstrates how to render pixels and images.</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="cake.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
87
examples/Canvas/Mandelbrot.cs
Normal file
87
examples/Canvas/Mandelbrot.cs
Normal file
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
examples/Canvas/Program.cs
Normal file
36
examples/Canvas/Program.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
examples/Canvas/cake.png
Normal file
BIN
examples/Canvas/cake.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
@ -7,34 +7,34 @@ namespace EmojiExample
|
|||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
// No title
|
// No title
|
||||||
WrapInPanel(
|
Render(
|
||||||
new Rule()
|
new Rule()
|
||||||
.RuleStyle(Style.Parse("yellow"))
|
.RuleStyle(Style.Parse("yellow"))
|
||||||
.AsciiBorder()
|
.AsciiBorder()
|
||||||
.LeftAligned());
|
.LeftAligned());
|
||||||
|
|
||||||
// Left aligned title
|
// Left aligned title
|
||||||
WrapInPanel(
|
Render(
|
||||||
new Rule("[blue]Left aligned[/]")
|
new Rule("[blue]Left aligned[/]")
|
||||||
.RuleStyle(Style.Parse("red"))
|
.RuleStyle(Style.Parse("red"))
|
||||||
.DoubleBorder()
|
.DoubleBorder()
|
||||||
.LeftAligned());
|
.LeftAligned());
|
||||||
|
|
||||||
// Centered title
|
// Centered title
|
||||||
WrapInPanel(
|
Render(
|
||||||
new Rule("[green]Centered[/]")
|
new Rule("[green]Centered[/]")
|
||||||
.RuleStyle(Style.Parse("green"))
|
.RuleStyle(Style.Parse("green"))
|
||||||
.HeavyBorder()
|
.HeavyBorder()
|
||||||
.Centered());
|
.Centered());
|
||||||
|
|
||||||
// Right aligned title
|
// Right aligned title
|
||||||
WrapInPanel(
|
Render(
|
||||||
new Rule("[red]Right aligned[/]")
|
new Rule("[red]Right aligned[/]")
|
||||||
.RuleStyle(Style.Parse("blue"))
|
.RuleStyle(Style.Parse("blue"))
|
||||||
.RightAligned());
|
.RightAligned());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WrapInPanel(Rule rule)
|
private static void Render(Rule rule)
|
||||||
{
|
{
|
||||||
AnsiConsole.Render(rule);
|
AnsiConsole.Render(rule);
|
||||||
AnsiConsole.WriteLine();
|
AnsiConsole.WriteLine();
|
||||||
|
125
src/Spectre.Console.ImageSharp/CanvasImage.cs
Normal file
125
src/Spectre.Console.ImageSharp/CanvasImage.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a renderable image.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CanvasImage : Renderable
|
||||||
|
{
|
||||||
|
private static readonly IResampler _defaultResampler = KnownResamplers.Bicubic;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the image width.
|
||||||
|
/// </summary>
|
||||||
|
public int Width => Image.Width;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the image height.
|
||||||
|
/// </summary>
|
||||||
|
public int Height => Image.Height;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the render width of the canvas.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxWidth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the render width of the canvas.
|
||||||
|
/// </summary>
|
||||||
|
public int PixelWidth { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="IResampler"/> that should
|
||||||
|
/// be used when scaling the image. Defaults to bicubic sampling.
|
||||||
|
/// </summary>
|
||||||
|
public IResampler? Resampler { get; set; }
|
||||||
|
|
||||||
|
internal SixLabors.ImageSharp.Image<Rgba32> Image { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CanvasImage"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The image filename.</param>
|
||||||
|
public CanvasImage(string filename)
|
||||||
|
{
|
||||||
|
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override IEnumerable<Segment> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs
Normal file
135
src/Spectre.Console.ImageSharp/CanvasImageExtensions.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains extension methods for <see cref="CanvasImage"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class CanvasImageExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the maximum width of the rendered image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <param name="maxWidth">The maximum width.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage MaxWidth(this CanvasImage image, int? maxWidth)
|
||||||
|
{
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
image.MaxWidth = maxWidth;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables the maximum width of the rendered image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage NoMaxWidth(this CanvasImage image)
|
||||||
|
{
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
image.MaxWidth = null;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pixel width.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <param name="width">The pixel width.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage PixelWidth(this CanvasImage image, int width)
|
||||||
|
{
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
image.PixelWidth = width;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mutates the underlying image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <param name="action">The action that mutates the underlying image.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage Mutate(this CanvasImage image, Action<IImageProcessingContext> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses a bicubic sampler that implements the bicubic kernel algorithm W(x).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage BicubicResampler(this CanvasImage image)
|
||||||
|
{
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
image.Resampler = KnownResamplers.Bicubic;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage BilinearResampler(this CanvasImage image)
|
||||||
|
{
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
image.Resampler = KnownResamplers.Triangle;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The canvas image.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static CanvasImage NearestNeighborResampler(this CanvasImage image)
|
||||||
|
{
|
||||||
|
if (image is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
image.Resampler = KnownResamplers.NearestNeighbor;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Description>A library that extends Spectre.Console with ImageSharp super powers.</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
|
||||||
|
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -54,6 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "..\examples\Promp
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -289,6 +317,7 @@ Global
|
|||||||
{75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
|
{75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
|
||||||
{6351C70F-F368-46DB-BAED-9B87CCD69353} = {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}
|
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
|
||||||
|
{5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
|
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
|
||||||
|
151
src/Spectre.Console/Widgets/Canvas.cs
Normal file
151
src/Spectre.Console/Widgets/Canvas.cs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a renderable canvas.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Canvas : Renderable
|
||||||
|
{
|
||||||
|
private readonly Color?[,] _pixels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the width of the canvas.
|
||||||
|
/// </summary>
|
||||||
|
public int Width { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the height of the canvas.
|
||||||
|
/// </summary>
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the render width of the canvas.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxWidth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether or not
|
||||||
|
/// to scale the canvas when rendering.
|
||||||
|
/// </summary>
|
||||||
|
public bool Scale { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the pixel width.
|
||||||
|
/// </summary>
|
||||||
|
public int PixelWidth { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Canvas"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width">The canvas width.</param>
|
||||||
|
/// <param name="height">The canvas height.</param>
|
||||||
|
public Canvas(int width, int height)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
|
||||||
|
_pixels = new Color?[Width, Height];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a pixel with the specified color in the canvas at the specified location.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x">The X coordinate for the pixel.</param>
|
||||||
|
/// <param name="y">The Y coordinate for the pixel.</param>
|
||||||
|
/// <param name="color">The pixel color.</param>
|
||||||
|
public void SetPixel(int x, int y, Color color)
|
||||||
|
{
|
||||||
|
_pixels[x, y] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override IEnumerable<Segment> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user