mirror of
https://github.com/greenshot/greenshot
synced 2025-07-15 09:33:46 -07:00
Added the area (Rectangle) to the FastBitmap, this made it possible to convert the current filters (all but the BlurFilter, this I want to review). Still want to see if I can change the way the filters work, maybe I can prevent some extra bitmap actions by changing the flow & signatures... Need to start in DrawableContainer.DrawContent.
git-svn-id: http://svn.code.sf.net/p/greenshot/code/trunk@2484 7dccd23d-a4a3-4e1f-8c07-b4c1b4018ab4
This commit is contained in:
parent
5ffe3dfb42
commit
d77d4d9ddf
5 changed files with 103 additions and 46 deletions
|
@ -23,6 +23,7 @@ using System.Drawing;
|
||||||
using Greenshot.Drawing.Fields;
|
using Greenshot.Drawing.Fields;
|
||||||
using Greenshot.Plugin.Drawing;
|
using Greenshot.Plugin.Drawing;
|
||||||
using GreenshotPlugin.Core;
|
using GreenshotPlugin.Core;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
|
||||||
namespace Greenshot.Drawing.Filters {
|
namespace Greenshot.Drawing.Filters {
|
||||||
[Serializable()]
|
[Serializable()]
|
||||||
|
@ -47,24 +48,23 @@ namespace Greenshot.Drawing.Filters {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (BitmapBuffer bbb = new BitmapBuffer(applyBitmap, applyRect)) {
|
using (IFastBitmap fastBitmap = FastBitmap.CreateCloneOf(applyBitmap, applyRect)) {
|
||||||
bbb.Lock();
|
|
||||||
double brightness = GetFieldValueAsDouble(FieldType.BRIGHTNESS);
|
double brightness = GetFieldValueAsDouble(FieldType.BRIGHTNESS);
|
||||||
for (int y = 0; y < bbb.Height; y++) {
|
for (int y = 0; y < fastBitmap.Height; y++) {
|
||||||
for (int x = 0; x < bbb.Width; x++) {
|
for (int x = 0; x < fastBitmap.Width; x++) {
|
||||||
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
||||||
Color color = bbb.GetColorAt(x, y);
|
Color color = fastBitmap.GetColorAt(x, y);
|
||||||
int r = Convert.ToInt16(color.R * brightness);
|
int r = Convert.ToInt16(color.R * brightness);
|
||||||
int g = Convert.ToInt16(color.G * brightness);
|
int g = Convert.ToInt16(color.G * brightness);
|
||||||
int b = Convert.ToInt16(color.B * brightness);
|
int b = Convert.ToInt16(color.B * brightness);
|
||||||
r = (r > 255) ? 255 : r;
|
r = (r > 255) ? 255 : r;
|
||||||
g = (g > 255) ? 255 : g;
|
g = (g > 255) ? 255 : g;
|
||||||
b = (b > 255) ? 255 : b;
|
b = (b > 255) ? 255 : b;
|
||||||
bbb.SetColorAt(x, y, Color.FromArgb(color.A, r, g, b));
|
fastBitmap.SetColorAt(x, y, Color.FromArgb(color.A, r, g, b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bbb.DrawTo(graphics, applyRect.Location);
|
fastBitmap.DrawTo(graphics, applyRect.Location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,19 +40,18 @@ namespace Greenshot.Drawing.Filters {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (BitmapBuffer bbb = new BitmapBuffer(applyBitmap, applyRect)) {
|
using (IFastBitmap fastBitmap = FastBitmap.CreateCloneOf(applyBitmap, applyRect)) {
|
||||||
bbb.Lock();
|
for (int y = 0; y < fastBitmap.Height; y++) {
|
||||||
for (int y = 0; y < bbb.Height; y++) {
|
for (int x = 0; x < fastBitmap.Width; x++) {
|
||||||
for (int x = 0; x < bbb.Width; x++) {
|
|
||||||
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
||||||
Color color = bbb.GetColorAt(x, y);
|
Color color = fastBitmap.GetColorAt(x, y);
|
||||||
int luma = (int)((0.3 * color.R) + (0.59 * color.G) + (0.11 * color.B));
|
int luma = (int)((0.3 * color.R) + (0.59 * color.G) + (0.11 * color.B));
|
||||||
color = Color.FromArgb(luma, luma, luma);
|
color = Color.FromArgb(luma, luma, luma);
|
||||||
bbb.SetColorAt(x, y, color);
|
fastBitmap.SetColorAt(x, y, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bbb.DrawTo(graphics, applyRect.Location);
|
fastBitmap.DrawTo(graphics, applyRect.Location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ using System.Drawing;
|
||||||
using Greenshot.Drawing.Fields;
|
using Greenshot.Drawing.Fields;
|
||||||
using Greenshot.Plugin.Drawing;
|
using Greenshot.Plugin.Drawing;
|
||||||
using GreenshotPlugin.Core;
|
using GreenshotPlugin.Core;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
|
||||||
namespace Greenshot.Drawing.Filters {
|
namespace Greenshot.Drawing.Filters {
|
||||||
[Serializable()]
|
[Serializable()]
|
||||||
|
@ -46,19 +47,18 @@ namespace Greenshot.Drawing.Filters {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (BitmapBuffer bbb = new BitmapBuffer(applyBitmap, applyRect)) {
|
using (IFastBitmap fastBitmap = FastBitmap.CreateCloneOf(applyBitmap, applyRect)) {
|
||||||
bbb.Lock();
|
|
||||||
Color highlightColor = GetFieldValueAsColor(FieldType.FILL_COLOR);
|
Color highlightColor = GetFieldValueAsColor(FieldType.FILL_COLOR);
|
||||||
for (int y = 0; y < bbb.Height; y++) {
|
for (int y = 0; y < fastBitmap.Height; y++) {
|
||||||
for (int x = 0; x < bbb.Width; x++) {
|
for (int x = 0; x < fastBitmap.Width; x++) {
|
||||||
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
||||||
Color color = bbb.GetColorAt(x, y);
|
Color color = fastBitmap.GetColorAt(x, y);
|
||||||
color = Color.FromArgb(color.A, Math.Min(highlightColor.R, color.R), Math.Min(highlightColor.G, color.G), Math.Min(highlightColor.B, color.B));
|
color = Color.FromArgb(color.A, Math.Min(highlightColor.R, color.R), Math.Min(highlightColor.G, color.G), Math.Min(highlightColor.B, color.B));
|
||||||
bbb.SetColorAt(x, y, color);
|
fastBitmap.SetColorAt(x, y, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bbb.DrawTo(graphics, applyRect.Location);
|
fastBitmap.DrawTo(graphics, applyRect.Location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ using System.Drawing;
|
||||||
using Greenshot.Drawing.Fields;
|
using Greenshot.Drawing.Fields;
|
||||||
using Greenshot.Plugin.Drawing;
|
using Greenshot.Plugin.Drawing;
|
||||||
using GreenshotPlugin.Core;
|
using GreenshotPlugin.Core;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
|
||||||
namespace Greenshot.Drawing.Filters {
|
namespace Greenshot.Drawing.Filters {
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
@ -40,23 +41,22 @@ namespace Greenshot.Drawing.Filters {
|
||||||
}
|
}
|
||||||
int magnificationFactor = GetFieldValueAsInt(FieldType.MAGNIFICATION_FACTOR);
|
int magnificationFactor = GetFieldValueAsInt(FieldType.MAGNIFICATION_FACTOR);
|
||||||
|
|
||||||
using (BitmapBuffer bbb = new BitmapBuffer(applyBitmap, applyRect)) {
|
using (IFastBitmap destFastBitmap = FastBitmap.CreateCloneOf(applyBitmap, applyRect)) {
|
||||||
int halfWidth = bbb.Size.Width / 2;
|
int halfWidth = destFastBitmap.Size.Width / 2;
|
||||||
int halfHeight = bbb.Size.Height / 2;
|
int halfHeight = destFastBitmap.Size.Height / 2;
|
||||||
bbb.Lock();
|
using (IFastBitmap sourceFastBitmap = FastBitmap.Create(applyBitmap, applyRect)) {
|
||||||
using (BitmapBuffer bbbSrc = new BitmapBuffer(applyBitmap, applyRect)) {
|
for (int y = 0; y < destFastBitmap.Height; y++) {
|
||||||
for (int y = 0; y < bbb.Height; y++) {
|
|
||||||
int yDistanceFromCenter = halfHeight - y;
|
int yDistanceFromCenter = halfHeight - y;
|
||||||
for (int x = 0; x < bbb.Width; x++) {
|
for (int x = 0; x < destFastBitmap.Width; x++) {
|
||||||
int xDistanceFromCenter = halfWidth - x;
|
int xDistanceFromCenter = halfWidth - x;
|
||||||
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
if (parent.Contains(applyRect.Left + x, applyRect.Top + y) ^ Invert) {
|
||||||
Color color = bbbSrc.GetColorAt(halfWidth - xDistanceFromCenter / magnificationFactor, halfHeight - yDistanceFromCenter / magnificationFactor);
|
Color color = sourceFastBitmap.GetColorAt(halfWidth - xDistanceFromCenter / magnificationFactor, halfHeight - yDistanceFromCenter / magnificationFactor);
|
||||||
bbb.SetColorAt(x, y, color);
|
destFastBitmap.SetColorAt(x, y, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bbb.DrawTo(graphics, applyRect.Location);
|
destFastBitmap.DrawTo(graphics, applyRect.Location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,21 @@ namespace GreenshotPlugin.Core {
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw the stored bitmap to the destionation bitmap at the supplied point
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="graphics">Graphics</param>
|
||||||
|
/// <param name="destination">Point with location</param>
|
||||||
|
void DrawTo(Graphics graphics, Point destination);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw the stored Bitmap on the Destination bitmap with the specified rectangle
|
||||||
|
/// Be aware that the stored bitmap will be resized to the specified rectangle!!
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="graphics">Graphics</param>
|
||||||
|
/// <param name="destinationRect">Rectangle with destination</param>
|
||||||
|
void DrawTo(Graphics graphics, Rectangle destinationRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -118,6 +133,7 @@ namespace GreenshotPlugin.Core {
|
||||||
protected const int RINDEX = 2;
|
protected const int RINDEX = 2;
|
||||||
protected const int GINDEX = 1;
|
protected const int GINDEX = 1;
|
||||||
protected const int BINDEX = 0;
|
protected const int BINDEX = 0;
|
||||||
|
protected Rectangle area = Rectangle.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If this is set to true, the bitmap will be disposed when disposing the IFastBitmap
|
/// If this is set to true, the bitmap will be disposed when disposing the IFastBitmap
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -130,27 +146,33 @@ namespace GreenshotPlugin.Core {
|
||||||
/// The bitmap for which the FastBitmap is creating access
|
/// The bitmap for which the FastBitmap is creating access
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Bitmap bitmap;
|
protected Bitmap bitmap;
|
||||||
|
|
||||||
protected BitmapData bmData;
|
protected BitmapData bmData;
|
||||||
protected int stride; /* bytes per pixel row */
|
protected int stride; /* bytes per pixel row */
|
||||||
protected bool bitsLocked = false;
|
protected bool bitsLocked = false;
|
||||||
protected byte* pointer;
|
protected byte* pointer;
|
||||||
|
|
||||||
|
public static IFastBitmap Create(Bitmap source) {
|
||||||
|
return Create(source, Rectangle.Empty);
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory for creating a FastBitmap depending on the pixelformat of the source
|
/// Factory for creating a FastBitmap depending on the pixelformat of the source
|
||||||
|
/// The supplied rectangle specifies the area for which the FastBitmap does its thing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">Bitmap to access</param>
|
/// <param name="source">Bitmap to access</param>
|
||||||
|
/// <param name="area">Rectangle which specifies the area to have access to, can be Rectangle.Empty for the whole image</param>
|
||||||
/// <returns>IFastBitmap</returns>
|
/// <returns>IFastBitmap</returns>
|
||||||
public static IFastBitmap Create(Bitmap source) {
|
public static IFastBitmap Create(Bitmap source, Rectangle area) {
|
||||||
switch (source.PixelFormat) {
|
switch (source.PixelFormat) {
|
||||||
case PixelFormat.Format8bppIndexed:
|
case PixelFormat.Format8bppIndexed:
|
||||||
return new FastChunkyBitmap(source);
|
return new FastChunkyBitmap(source, area);
|
||||||
case PixelFormat.Format24bppRgb:
|
case PixelFormat.Format24bppRgb:
|
||||||
return new Fast24RGBBitmap(source);
|
return new Fast24RGBBitmap(source, area);
|
||||||
case PixelFormat.Format32bppRgb:
|
case PixelFormat.Format32bppRgb:
|
||||||
return new Fast32RGBBitmap(source);
|
return new Fast32RGBBitmap(source, area);
|
||||||
case PixelFormat.Format32bppArgb:
|
case PixelFormat.Format32bppArgb:
|
||||||
case PixelFormat.Format32bppPArgb:
|
case PixelFormat.Format32bppPArgb:
|
||||||
return new Fast32ARGBBitmap(source);
|
return new Fast32ARGBBitmap(source, area);
|
||||||
default:
|
default:
|
||||||
throw new NotSupportedException(string.Format("Not supported Pixelformat {0}", source.PixelFormat));
|
throw new NotSupportedException(string.Format("Not supported Pixelformat {0}", source.PixelFormat));
|
||||||
}
|
}
|
||||||
|
@ -162,7 +184,27 @@ namespace GreenshotPlugin.Core {
|
||||||
/// <param name="source">Bitmap to access</param>
|
/// <param name="source">Bitmap to access</param>
|
||||||
/// <returns>IFastBitmap</returns>
|
/// <returns>IFastBitmap</returns>
|
||||||
public static IFastBitmap CreateCloneOf(Image source, PixelFormat pixelFormat) {
|
public static IFastBitmap CreateCloneOf(Image source, PixelFormat pixelFormat) {
|
||||||
Bitmap destination = ImageHelper.CloneArea(source, Rectangle.Empty, pixelFormat);
|
return CreateCloneOf(source, pixelFormat, Rectangle.Empty);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for creating a FastBitmap as a destination for the source
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Bitmap to access</param>
|
||||||
|
/// <param name="area">Area of the bitmap to access, can be Rectangle.Empty for the whole</param>
|
||||||
|
/// <returns>IFastBitmap</returns>
|
||||||
|
public static IFastBitmap CreateCloneOf(Image source, Rectangle area) {
|
||||||
|
return CreateCloneOf(source, PixelFormat.DontCare, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for creating a FastBitmap as a destination for the source
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Bitmap to access</param>
|
||||||
|
/// <param name="pixelFormat">Pixelformat of the cloned bitmap</param>
|
||||||
|
/// <param name="area">Area of the bitmap to access, can be Rectangle.Empty for the whole</param>
|
||||||
|
/// <returns>IFastBitmap</returns>
|
||||||
|
public static IFastBitmap CreateCloneOf(Image source, PixelFormat pixelFormat, Rectangle area) {
|
||||||
|
Bitmap destination = ImageHelper.CloneArea(source, area, pixelFormat);
|
||||||
IFastBitmap fastBitmap = Create(destination);
|
IFastBitmap fastBitmap = Create(destination);
|
||||||
((FastBitmap)fastBitmap).NeedsDispose = true;
|
((FastBitmap)fastBitmap).NeedsDispose = true;
|
||||||
return fastBitmap;
|
return fastBitmap;
|
||||||
|
@ -187,8 +229,15 @@ namespace GreenshotPlugin.Core {
|
||||||
/// Constructor which stores the image and locks it when called
|
/// Constructor which stores the image and locks it when called
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="bitmap"></param>
|
/// <param name="bitmap"></param>
|
||||||
protected FastBitmap(Bitmap bitmap) {
|
protected FastBitmap(Bitmap bitmap, Rectangle area) {
|
||||||
this.bitmap = bitmap;
|
this.bitmap = bitmap;
|
||||||
|
Rectangle bitmapArea = new Rectangle(Point.Empty, bitmap.Size);
|
||||||
|
if (area != Rectangle.Empty) {
|
||||||
|
area.Intersect(bitmapArea);
|
||||||
|
this.area = area;
|
||||||
|
} else {
|
||||||
|
this.area = bitmapArea;
|
||||||
|
}
|
||||||
Lock();
|
Lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,8 +246,11 @@ namespace GreenshotPlugin.Core {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Size Size {
|
public Size Size {
|
||||||
get {
|
get {
|
||||||
|
if (area == Rectangle.Empty) {
|
||||||
return bitmap.Size;
|
return bitmap.Size;
|
||||||
}
|
}
|
||||||
|
return area.Size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -206,8 +258,11 @@ namespace GreenshotPlugin.Core {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Width {
|
public int Width {
|
||||||
get {
|
get {
|
||||||
|
if (area == Rectangle.Empty) {
|
||||||
return bitmap.Width;
|
return bitmap.Width;
|
||||||
}
|
}
|
||||||
|
return area.Width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -215,8 +270,11 @@ namespace GreenshotPlugin.Core {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Height {
|
public int Height {
|
||||||
get {
|
get {
|
||||||
|
if (area == Rectangle.Empty) {
|
||||||
return bitmap.Height;
|
return bitmap.Height;
|
||||||
}
|
}
|
||||||
|
return area.Height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -271,7 +329,7 @@ namespace GreenshotPlugin.Core {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Lock() {
|
public void Lock() {
|
||||||
if (Width > 0 && Height > 0 && !bitsLocked) {
|
if (Width > 0 && Height > 0 && !bitsLocked) {
|
||||||
bmData = bitmap.LockBits(new Rectangle(Point.Empty, Size), ImageLockMode.ReadWrite, bitmap.PixelFormat);
|
bmData = bitmap.LockBits(area, ImageLockMode.ReadWrite, bitmap.PixelFormat);
|
||||||
bitsLocked = true;
|
bitsLocked = true;
|
||||||
|
|
||||||
IntPtr Scan0 = bmData.Scan0;
|
IntPtr Scan0 = bmData.Scan0;
|
||||||
|
@ -353,7 +411,7 @@ namespace GreenshotPlugin.Core {
|
||||||
private Color[] colorEntries;
|
private Color[] colorEntries;
|
||||||
private Dictionary<Color, byte> colorCache = new Dictionary<Color, byte>();
|
private Dictionary<Color, byte> colorCache = new Dictionary<Color, byte>();
|
||||||
|
|
||||||
public FastChunkyBitmap(Bitmap source) : base(source) {
|
public FastChunkyBitmap(Bitmap source, Rectangle area) : base(source, area) {
|
||||||
colorEntries = bitmap.Palette.Entries;
|
colorEntries = bitmap.Palette.Entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -443,7 +501,7 @@ namespace GreenshotPlugin.Core {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class Fast24RGBBitmap : FastBitmap {
|
public unsafe class Fast24RGBBitmap : FastBitmap {
|
||||||
|
|
||||||
public Fast24RGBBitmap(Bitmap source) : base(source) {
|
public Fast24RGBBitmap(Bitmap source, Rectangle area) : base(source, area) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -505,7 +563,7 @@ namespace GreenshotPlugin.Core {
|
||||||
/// This is the implementation of the IFastBitmap for 32 bit images (no Alpha)
|
/// This is the implementation of the IFastBitmap for 32 bit images (no Alpha)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class Fast32RGBBitmap : FastBitmap {
|
public unsafe class Fast32RGBBitmap : FastBitmap {
|
||||||
public Fast32RGBBitmap(Bitmap source) : base(source) {
|
public Fast32RGBBitmap(Bitmap source, Rectangle area) : base(source, area) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,7 +629,7 @@ namespace GreenshotPlugin.Core {
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
public Fast32ARGBBitmap(Bitmap source) : base(source) {
|
public Fast32ARGBBitmap(Bitmap source, Rectangle area) : base(source, area) {
|
||||||
BackgroundBlendColor = Color.White;
|
BackgroundBlendColor = Color.White;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue