Skip to content

Commit

Permalink
🧶 Little progress on the ImageService stuff.
Browse files Browse the repository at this point in the history
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
hexawyz committed Apr 6, 2024
1 parent d030aa1 commit 5eacf47
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 32 deletions.
33 changes: 33 additions & 0 deletions Exo.Core/ColorFormats/ArgbColor.cs
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);
}
10 changes: 5 additions & 5 deletions Exo.Core/ColorFormats/RgbColor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ public RgbColor(byte r, byte g, byte b)

public static RgbColor FromInt32(int value) => new((byte)(value >>> 16), (byte)(value >>> 8), (byte)value);

public int ToInt32() => R << 16 | G << 8 | B;
public uint ToUInt32(byte alpha) => (uint)(alpha << 24 | R << 16 | G << 8 | B);
public readonly int ToInt32() => R << 16 | G << 8 | B;
public readonly uint ToUInt32(byte alpha) => (uint)(alpha << 24 | R << 16 | G << 8 | B);

public override bool Equals(object? obj) => obj is RgbColor color && Equals(color);
public bool Equals(RgbColor other) => R == other.R && G == other.G && B == other.B;
public override int GetHashCode() => HashCode.Combine(R, G, B);
public readonly override bool Equals(object? obj) => obj is RgbColor color && Equals(color);
public readonly bool Equals(RgbColor other) => R == other.R && G == other.G && B == other.B;
public readonly override int GetHashCode() => HashCode.Combine(R, G, B);

public static bool operator ==(RgbColor left, RgbColor right) => left.Equals(right);
public static bool operator !=(RgbColor left, RgbColor right) => !(left == right);
Expand Down
13 changes: 0 additions & 13 deletions Exo.Core/Image.cs

This file was deleted.

100 changes: 100 additions & 0 deletions Exo.Core/Images/Image.cs
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);
}
11 changes: 11 additions & 0 deletions Exo.Service.ImageService/ColorExtensions.cs
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)));
}
141 changes: 127 additions & 14 deletions Exo.Service.ImageService/ImageService.cs
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));
}
}
}

0 comments on commit 5eacf47

Please sign in to comment.