-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🧶 Little progress on the ImageService stuff.
General API of the Image time refined a bit. Tentative implementation of a few operations. Need to add a scale factor somewhere in the mix, so that anchored images can be scaled.
- Loading branch information
Showing
6 changed files
with
276 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System.Diagnostics; | ||
using System.Runtime.CompilerServices; | ||
using System.Runtime.InteropServices; | ||
|
||
namespace Exo.ColorFormats; | ||
|
||
[DebuggerDisplay("A = {A}, R = {R}, G = {G}, B = {B}")] | ||
[StructLayout(LayoutKind.Sequential, Size = 3)] | ||
public struct ArgbColor : IEquatable<ArgbColor> | ||
{ | ||
public byte A; | ||
public byte R; | ||
public byte G; | ||
public byte B; | ||
|
||
public ArgbColor(RgbColor color, byte a) | ||
=> (A, R, G, B) = (a, color.R, color.G, color.B); | ||
|
||
public ArgbColor(byte r, byte g, byte b, byte a) | ||
=> (A, R, G, B) = (a, r, g, b); | ||
|
||
public static ArgbColor FromInt32(int value) => new((byte)(value >>> 24), (byte)(value >>> 16), (byte)(value >>> 8), (byte)value); | ||
|
||
public readonly int ToInt32() => R << 16 | G << 8 | B; | ||
public readonly uint ToUInt32(byte alpha) => (uint)(alpha << 24 | R << 16 | G << 8 | B); | ||
|
||
public readonly override bool Equals(object? obj) => obj is ArgbColor color && Equals(color); | ||
public readonly bool Equals(ArgbColor other) => Unsafe.As<byte, uint>(ref Unsafe.AsRef(in A)) == Unsafe.As<byte, uint>(ref other.A); | ||
public readonly override int GetHashCode() => HashCode.Combine(R, G, B); | ||
|
||
public static bool operator ==(ArgbColor left, ArgbColor right) => left.Equals(right); | ||
public static bool operator !=(ArgbColor left, ArgbColor right) => !(left == right); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
namespace Exo.Images; | ||
|
||
/// <summary>Represents an image that can be rasterized.</summary> | ||
/// <remarks> | ||
/// <para> | ||
/// The best way to understand <see cref="Image"/>, is that an <see cref="Image"/> instance is a recipe for creating a bitmap. | ||
/// A bitmap image is obtained by calling the method <see cref="Rasterize(Size)"/>. | ||
/// </para> | ||
/// <para> | ||
/// Images dimensions are always assumed to be in pixels, but there are two types of images that will attach a different meaning for the <see cref="Size"/> property. | ||
/// The two types of images are "scaled" and "anchored". | ||
/// The type of image determines how it will be drawn in its target size, either during rasterization or when integrated into another image. | ||
/// </para> | ||
/// </remarks> | ||
[TypeId(0x0BB0B78A, 0xBEE5, 0x4A53, 0x90, 0x0E, 0x1F, 0x1A, 0x09, 0x1C, 0x3A, 0x00)] | ||
public abstract class Image | ||
{ | ||
/// <summary>Gets a size of the image, that can be either the reference size or the minimum size.</summary> | ||
/// <remarks> | ||
/// <para> | ||
/// If <see cref="Type"/> is <see cref="ImageType.Scaled"/>, this represents the reference size. | ||
/// If <see cref="Type"/> is <see cref="ImageType.Anchored"/>, this represents the minimum size. | ||
/// </para> | ||
/// <para> | ||
/// In the case of anchored image, the minimum size, expressed by this property, represents the size at which the rendering of the image can produce a meaningful result. | ||
/// For example, a rectangle with a stroke width of 4 can only be rendered meaningfully in an image of at least 9x9 pixels. | ||
/// It is acceptable for an anchored image to have a minimum size of <c>0x0</c>. | ||
/// Scaled images, however, should always have a size of at least <c>1x1</c>. | ||
/// </para> | ||
/// </remarks> | ||
public abstract Size Size { get; } | ||
|
||
/// <summary>Gets the type of image of this instance.</summary> | ||
public abstract ImageType Type { get; } | ||
|
||
/// <summary>Rasterizes this image into a bitmap.</summary> | ||
/// <param name="size">The target size of the bitmap.</param> | ||
/// <returns></returns> | ||
public abstract RasterizedImage Rasterize(Size size); | ||
} | ||
|
||
public abstract class RasterizedImage | ||
{ | ||
public abstract ReadOnlyMemory<byte> GetRawBytes(); | ||
} | ||
|
||
public enum ImageType : byte | ||
{ | ||
Scaled = 0, | ||
Anchored = 1, | ||
} | ||
|
||
[TypeId(0xBA4846D8, 0xEE08, 0x4AE2, 0xAE, 0x48, 0xF7, 0x47, 0xAB, 0x02, 0xAC, 0x6F)] | ||
public readonly record struct Size | ||
{ | ||
public int Width { get; init; } | ||
public int Height { get; init; } | ||
|
||
public Size() { } | ||
|
||
public Size(int width, int height) => (Width, Height) = (width, height); | ||
} | ||
|
||
[TypeId(0xE9D65D40, 0x66E7, 0x4AED, 0xBB, 0x53, 0x4F, 0xB4, 0xAA, 0xE8, 0xE5, 0x36)] | ||
public readonly record struct Point | ||
{ | ||
public int X { get; init; } | ||
public int Y { get; init; } | ||
|
||
public Point() { } | ||
|
||
public Point(int x, int y) => (X, Y) = (x, y); | ||
} | ||
|
||
[TypeId(0x712CCD71, 0x1E7C, 0x4E2E, 0x84, 0x7F, 0xCF, 0x14, 0x97, 0x0D, 0xA4, 0xF9)] | ||
public readonly record struct Rectangle | ||
{ | ||
public int Left { get; init; } | ||
public int Top { get; init; } | ||
public int Width { get; init; } | ||
public int Height { get; init; } | ||
|
||
public Rectangle() { } | ||
|
||
public Rectangle(int left, int top, int width, int height) => (Left, Top, Width, Height) = (left, top, width, height); | ||
public Rectangle(Point point, Size size) : this(point.X, point.Y, size.Width, size.Height) { } | ||
} | ||
|
||
[TypeId(0x9B4D21B7, 0x0727, 0x4F86, 0xA8, 0x19, 0x0F, 0x82, 0x21, 0xD6, 0x9E, 0x5E)] | ||
public readonly record struct Thickness | ||
{ | ||
public int Left { get; init; } | ||
public int Top { get; init; } | ||
public int Right { get; init; } | ||
public int Bottom { get; init; } | ||
|
||
public Thickness() { } | ||
|
||
public Thickness(int left, int top, int width, int height) => (Left, Top, Right, Bottom) = (left, top, width, height); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using System.Buffers.Binary; | ||
using System.Runtime.CompilerServices; | ||
using Exo.ColorFormats; | ||
using SixLabors.ImageSharp.PixelFormats; | ||
|
||
namespace Exo.Service; | ||
|
||
internal static class ColorExtensions | ||
{ | ||
public static Bgra32 ToBgra32(this ArgbColor color) => Unsafe.BitCast<uint, Bgra32>(BinaryPrimitives.ReverseEndianness(Unsafe.BitCast<ArgbColor, uint>(color))); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,147 @@ | ||
using Exo.ColorFormats; | ||
using Exo.Images; | ||
using Exo.Programming.Annotations; | ||
using SixLabors.ImageSharp; | ||
using SixLabors.ImageSharp.Drawing; | ||
using SixLabors.ImageSharp.Drawing.Processing; | ||
using SixLabors.ImageSharp.PixelFormats; | ||
using SixLabors.ImageSharp.Processing; | ||
using ExoImage = Exo.Images.Image; | ||
using ExoRasterizedImage = Exo.Images.RasterizedImage; | ||
using ExoSize = Exo.Images.Size; | ||
|
||
namespace Exo.Service; | ||
|
||
[Module("Image")] | ||
[TypeId(0x718FB272, 0x914C, 0x43E5, 0x85, 0x5C, 0x2E, 0x91, 0x49, 0xBC, 0x28, 0xB3)] | ||
public sealed class ImageService | ||
{ | ||
public Image Load(string path) | ||
public ExoImage Load(string path) | ||
{ | ||
return null!; | ||
} | ||
|
||
public Image Background(int width, int height) | ||
public ExoImage Background(ArgbColor color) => new ImageSharpBackgroundImage(color.ToBgra32()); | ||
|
||
public ExoImage Rectangle(int x, int y, int width, int height) => null!; | ||
|
||
public ExoImage WithConstantMargin(Thickness margin, ExoImage image) => new ImageWithConstantMarginImage(margin, (ImageSharpImage)image); | ||
|
||
private abstract class ImageSharpImage : ExoImage | ||
{ | ||
return null!; | ||
private readonly ExoSize _size; | ||
|
||
public override ExoSize Size => _size; | ||
|
||
protected virtual Bgra32 Background => default; | ||
|
||
// TODO: It might be useful to track whether the image has transparency. Except for edge cases where the image would end up opaque "by chance", this property is relatively easy to track. | ||
//private readonly bool _isTransparent; | ||
|
||
protected ImageSharpImage(ExoSize size) => _size = size; | ||
|
||
// Do the first step of the rendering. Some implementations such as background, may want to do apply specific treatment in that case. | ||
protected virtual void FirstStepRender(IImageProcessingContext context) | ||
=> Render(context, new RectangleF(default, context.GetCurrentSize())); | ||
|
||
// Do rendering for any step | ||
protected internal abstract void Render(IImageProcessingContext context, RectangleF rectangle); | ||
|
||
public sealed override ExoRasterizedImage Rasterize(ExoSize size) | ||
{ | ||
var baseImage = new Image<Bgra32>(size.Width, size.Height, Background); | ||
baseImage.Mutate(FirstStepRender); | ||
return new RasterizedImage(baseImage); | ||
} | ||
|
||
private sealed class RasterizedImage : ExoRasterizedImage | ||
{ | ||
private readonly Image<Bgra32> _image; | ||
|
||
public RasterizedImage(Image<Bgra32> image) => _image = image; | ||
|
||
public override ReadOnlyMemory<byte> GetRawBytes() | ||
{ | ||
if (!_image.Frames[0].DangerousTryGetSinglePixelMemory(out var memory)) throw new InvalidOperationException("Failed to retrieve the raw image data."); | ||
|
||
return default; | ||
} | ||
} | ||
} | ||
|
||
public Image Rectangle(int x, int y, int width, int height) | ||
private sealed class ImageSharpBackgroundImage : ImageSharpImage | ||
{ | ||
return null!; | ||
private readonly Bgra32 _background; | ||
|
||
protected override Bgra32 Background => _background; | ||
|
||
public override ImageType Type => ImageType.Anchored; | ||
|
||
public ImageSharpBackgroundImage(Bgra32 background) : base(default) | ||
{ | ||
_background = background; | ||
} | ||
|
||
// Rendering of the background is already handled | ||
protected override void FirstStepRender(IImageProcessingContext context) { } | ||
|
||
protected internal override void Render(IImageProcessingContext context, RectangleF rectangle) | ||
{ | ||
var brush = new SolidBrush(Background); | ||
if (rectangle.Left == 0 && rectangle.Top == 0) | ||
{ | ||
var currentSize = context.GetCurrentSize(); | ||
if (rectangle.Width == currentSize.Width && rectangle.Height == currentSize.Height) | ||
{ | ||
context.Fill(brush); | ||
} | ||
} | ||
context.Fill(brush, new RectangularPolygon(rectangle)); | ||
} | ||
} | ||
} | ||
|
||
public sealed class ImageSharpImage : Image | ||
{ | ||
public override uint Width { get; } | ||
public override uint Height { get; } | ||
//internal sealed class ImageSharpScaledFilledRectangleImage : ImageSharpImage | ||
//{ | ||
// private readonly RectangleF _rectangle; | ||
|
||
// protected override void Render(IImageProcessingContext context, RectangleF rectangle) | ||
// { | ||
// } | ||
//} | ||
|
||
public override Memory<byte> GetRawBytes() => throw new NotImplementedException(); | ||
public override Memory<byte> GetBitmapBytes() => throw new NotImplementedException(); | ||
public override Memory<byte> GetJpegBytes() => throw new NotImplementedException(); | ||
public override Memory<byte> GetPngBytes() => throw new NotImplementedException(); | ||
private sealed class ImageWithConstantMarginImage : ImageSharpImage | ||
{ | ||
private static ExoSize GetImageSizeWithMargin(Thickness margin, ImageSharpImage image) | ||
{ | ||
var minSize = image.Type switch | ||
{ | ||
ImageType.Scaled => new ExoSize(1, 1), | ||
ImageType.Anchored => image.Size, | ||
_ => throw new InvalidOperationException(), | ||
}; | ||
return new(margin.Left + margin.Right + minSize.Width, margin.Top + margin.Bottom + minSize.Height); | ||
} | ||
|
||
private readonly ImageSharpImage _image; | ||
private readonly Thickness _margin; | ||
|
||
public override ImageType Type => ImageType.Anchored; | ||
|
||
public ImageWithConstantMarginImage(Thickness margin, ImageSharpImage image) | ||
: base(GetImageSizeWithMargin(margin, image)) | ||
{ | ||
_image = image; | ||
_margin = margin; | ||
} | ||
|
||
protected internal override void Render(IImageProcessingContext context, RectangleF rectangle) | ||
{ | ||
float imageWidth = rectangle.Width - (_margin.Left + _margin.Right); | ||
float imageHeight = rectangle.Height - (_margin.Top + _margin.Bottom); | ||
|
||
if (imageWidth <= 0 || imageHeight <= 0) return; | ||
|
||
_image.Render(context, new RectangleF(rectangle.X + _margin.Left, rectangle.Y + _margin.Top, imageWidth, imageHeight)); | ||
} | ||
} | ||
} |