mirror of
https://github.com/terrymacdonald/DisplayMagician.git
synced 2024-08-30 18:32:20 +00:00
Refactor image utils into own class
This commit is contained in:
parent
d6a5b752c9
commit
c3fe6c5b50
@ -106,6 +106,7 @@
|
|||||||
<Compile Include="GlobalSuppressions.cs" />
|
<Compile Include="GlobalSuppressions.cs" />
|
||||||
<Compile Include="IconFromFile.cs" />
|
<Compile Include="IconFromFile.cs" />
|
||||||
<Compile Include="IconUtils.cs" />
|
<Compile Include="IconUtils.cs" />
|
||||||
|
<Compile Include="ImageUtils.cs" />
|
||||||
<Compile Include="ProgramSettings.cs" />
|
<Compile Include="ProgramSettings.cs" />
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
<Compile Include="Properties\Resources.Designer.cs">
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
|
127
DisplayMagician/ImageUtils.cs
Normal file
127
DisplayMagician/ImageUtils.cs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DisplayMagician
|
||||||
|
{
|
||||||
|
public static class ImageUtils
|
||||||
|
{
|
||||||
|
public static Image RoundCorners(Image StartImage, int CornerRadius)
|
||||||
|
{
|
||||||
|
CornerRadius *= 2;
|
||||||
|
Bitmap RoundedImage = new Bitmap(StartImage.Width, StartImage.Height);
|
||||||
|
using (Graphics g = Graphics.FromImage(RoundedImage))
|
||||||
|
{
|
||||||
|
g.Clear(Color.Transparent);
|
||||||
|
g.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
|
Brush brush = new TextureBrush(StartImage);
|
||||||
|
GraphicsPath gp = new GraphicsPath();
|
||||||
|
gp.AddArc(0, 0, CornerRadius, CornerRadius, 180, 90);
|
||||||
|
gp.AddArc(0 + RoundedImage.Width - CornerRadius, 0, CornerRadius, CornerRadius, 270, 90);
|
||||||
|
gp.AddArc(0 + RoundedImage.Width - CornerRadius, 0 + RoundedImage.Height - CornerRadius, CornerRadius, CornerRadius, 0, 90);
|
||||||
|
gp.AddArc(0, 0 + RoundedImage.Height - CornerRadius, CornerRadius, CornerRadius, 90, 90);
|
||||||
|
g.FillPath(brush, gp);
|
||||||
|
return RoundedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DrawRoundedRectangle(Graphics graphics, Pen pen, Rectangle bounds, int cornerRadius)
|
||||||
|
{
|
||||||
|
if (graphics == null)
|
||||||
|
throw new ArgumentNullException("graphics");
|
||||||
|
if (pen == null)
|
||||||
|
throw new ArgumentNullException("pen");
|
||||||
|
|
||||||
|
using (GraphicsPath path = RoundedRect(bounds, cornerRadius))
|
||||||
|
{
|
||||||
|
graphics.DrawPath(pen, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void FillRoundedRectangle(Graphics graphics, Brush brush, Rectangle bounds, int cornerRadius)
|
||||||
|
{
|
||||||
|
if (graphics == null)
|
||||||
|
throw new ArgumentNullException("graphics");
|
||||||
|
if (brush == null)
|
||||||
|
throw new ArgumentNullException("brush");
|
||||||
|
|
||||||
|
using (GraphicsPath path = RoundedRect(bounds, cornerRadius))
|
||||||
|
{
|
||||||
|
graphics.FillPath(brush, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GraphicsPath RoundedRect(Rectangle bounds, int radius)
|
||||||
|
{
|
||||||
|
int diameter = radius * 2;
|
||||||
|
Size size = new Size(diameter, diameter);
|
||||||
|
Rectangle arc = new Rectangle(bounds.Location, size);
|
||||||
|
GraphicsPath path = new GraphicsPath();
|
||||||
|
|
||||||
|
if (radius == 0)
|
||||||
|
{
|
||||||
|
path.AddRectangle(bounds);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// top left arc
|
||||||
|
path.AddArc(arc, 180, 90);
|
||||||
|
|
||||||
|
// top right arc
|
||||||
|
arc.X = bounds.Right - diameter;
|
||||||
|
path.AddArc(arc, 270, 90);
|
||||||
|
|
||||||
|
// bottom right arc
|
||||||
|
arc.Y = bounds.Bottom - diameter;
|
||||||
|
path.AddArc(arc, 0, 90);
|
||||||
|
|
||||||
|
// bottom left arc
|
||||||
|
arc.X = bounds.Left;
|
||||||
|
path.AddArc(arc, 90, 90);
|
||||||
|
|
||||||
|
path.CloseFigure();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Image MakeGrayscale(Image original)
|
||||||
|
{
|
||||||
|
//create a blank bitmap the same size as original
|
||||||
|
Image newBitmap = new Bitmap(original.Width, original.Height);
|
||||||
|
|
||||||
|
//get a graphics object from the new image
|
||||||
|
using (Graphics g = Graphics.FromImage(newBitmap))
|
||||||
|
{
|
||||||
|
|
||||||
|
//create the grayscale ColorMatrix
|
||||||
|
ColorMatrix colorMatrix = new ColorMatrix(
|
||||||
|
new float[][]
|
||||||
|
{
|
||||||
|
new float[] {.3f, .3f, .3f, 0, 0},
|
||||||
|
new float[] {.59f, .59f, .59f, 0, 0},
|
||||||
|
new float[] {.11f, .11f, .11f, 0, 0},
|
||||||
|
new float[] {0, 0, 0, 1, 0},
|
||||||
|
new float[] {0, 0, 0, 0, 1}
|
||||||
|
});
|
||||||
|
|
||||||
|
//create some image attributes
|
||||||
|
using (ImageAttributes attributes = new ImageAttributes())
|
||||||
|
{
|
||||||
|
|
||||||
|
//set the color matrix attribute
|
||||||
|
attributes.SetColorMatrix(colorMatrix);
|
||||||
|
|
||||||
|
//draw the original image on the new image
|
||||||
|
//using the grayscale color matrix
|
||||||
|
g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height),
|
||||||
|
0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newBitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -88,7 +88,7 @@ namespace DisplayMagician.UIForms
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw the image
|
// Draw the image
|
||||||
Image img = RoundCorners(item.GetCachedImage(CachedImageType.Thumbnail),20);
|
Image img = ImageUtils.RoundCorners(item.GetCachedImage(CachedImageType.Thumbnail),20);
|
||||||
if (img != null)
|
if (img != null)
|
||||||
{
|
{
|
||||||
Rectangle pos = Utility.GetSizedImageBounds(img, new Rectangle(bounds.Location + itemPadding, ImageListView.ThumbnailSize));
|
Rectangle pos = Utility.GetSizedImageBounds(img, new Rectangle(bounds.Location + itemPadding, ImageListView.ThumbnailSize));
|
||||||
@ -102,7 +102,7 @@ namespace DisplayMagician.UIForms
|
|||||||
{
|
{
|
||||||
// THe shortcut is invalid
|
// THe shortcut is invalid
|
||||||
// so we make the image grayscale
|
// so we make the image grayscale
|
||||||
Image grayImg = MakeGrayscale(img);
|
Image grayImg = ImageUtils.MakeGrayscale(img);
|
||||||
g.DrawImage(grayImg, pos);
|
g.DrawImage(grayImg, pos);
|
||||||
|
|
||||||
// Draw a warning triangle over it
|
// Draw a warning triangle over it
|
||||||
@ -116,7 +116,7 @@ namespace DisplayMagician.UIForms
|
|||||||
using (Pen pOuterBorder = new Pen(ImageListView.Colors.ImageOuterBorderColor))
|
using (Pen pOuterBorder = new Pen(ImageListView.Colors.ImageOuterBorderColor))
|
||||||
{
|
{
|
||||||
//g.DrawRectangle(pOuterBorder, pos);
|
//g.DrawRectangle(pOuterBorder, pos);
|
||||||
DrawRoundedRectangle(g, pOuterBorder, pos,9);
|
ImageUtils.DrawRoundedRectangle(g, pOuterBorder, pos,9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,24 +145,6 @@ namespace DisplayMagician.UIForms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image RoundCorners(Image StartImage, int CornerRadius)
|
|
||||||
{
|
|
||||||
CornerRadius *= 2;
|
|
||||||
Bitmap RoundedImage = new Bitmap(StartImage.Width, StartImage.Height);
|
|
||||||
using (Graphics g = Graphics.FromImage(RoundedImage))
|
|
||||||
{
|
|
||||||
g.Clear(Color.Transparent);
|
|
||||||
g.SmoothingMode = SmoothingMode.HighQuality;
|
|
||||||
Brush brush = new TextureBrush(StartImage);
|
|
||||||
GraphicsPath gp = new GraphicsPath();
|
|
||||||
gp.AddArc(0, 0, CornerRadius, CornerRadius, 180, 90);
|
|
||||||
gp.AddArc(0 + RoundedImage.Width - CornerRadius, 0, CornerRadius, CornerRadius, 270, 90);
|
|
||||||
gp.AddArc(0 + RoundedImage.Width - CornerRadius, 0 + RoundedImage.Height - CornerRadius, CornerRadius, CornerRadius, 0, 90);
|
|
||||||
gp.AddArc(0, 0 + RoundedImage.Height - CornerRadius, CornerRadius, CornerRadius, 90, 90);
|
|
||||||
g.FillPath(brush, gp);
|
|
||||||
return RoundedImage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws the selection rectangle.
|
// Draws the selection rectangle.
|
||||||
public override void DrawSelectionRectangle(Graphics g, Rectangle selection)
|
public override void DrawSelectionRectangle(Graphics g, Rectangle selection)
|
||||||
@ -180,100 +162,6 @@ namespace DisplayMagician.UIForms
|
|||||||
selection.Width, selection.Height);
|
selection.Width, selection.Height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawRoundedRectangle(Graphics graphics, Pen pen, Rectangle bounds, int cornerRadius)
|
|
||||||
{
|
|
||||||
if (graphics == null)
|
|
||||||
throw new ArgumentNullException("graphics");
|
|
||||||
if (pen == null)
|
|
||||||
throw new ArgumentNullException("pen");
|
|
||||||
|
|
||||||
using (GraphicsPath path = RoundedRect(bounds, cornerRadius))
|
|
||||||
{
|
|
||||||
graphics.DrawPath(pen, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FillRoundedRectangle(Graphics graphics, Brush brush, Rectangle bounds, int cornerRadius)
|
|
||||||
{
|
|
||||||
if (graphics == null)
|
|
||||||
throw new ArgumentNullException("graphics");
|
|
||||||
if (brush == null)
|
|
||||||
throw new ArgumentNullException("brush");
|
|
||||||
|
|
||||||
using (GraphicsPath path = RoundedRect(bounds, cornerRadius))
|
|
||||||
{
|
|
||||||
graphics.FillPath(brush, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GraphicsPath RoundedRect(Rectangle bounds, int radius)
|
|
||||||
{
|
|
||||||
int diameter = radius * 2;
|
|
||||||
Size size = new Size(diameter, diameter);
|
|
||||||
Rectangle arc = new Rectangle(bounds.Location, size);
|
|
||||||
GraphicsPath path = new GraphicsPath();
|
|
||||||
|
|
||||||
if (radius == 0)
|
|
||||||
{
|
|
||||||
path.AddRectangle(bounds);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// top left arc
|
|
||||||
path.AddArc(arc, 180, 90);
|
|
||||||
|
|
||||||
// top right arc
|
|
||||||
arc.X = bounds.Right - diameter;
|
|
||||||
path.AddArc(arc, 270, 90);
|
|
||||||
|
|
||||||
// bottom right arc
|
|
||||||
arc.Y = bounds.Bottom - diameter;
|
|
||||||
path.AddArc(arc, 0, 90);
|
|
||||||
|
|
||||||
// bottom left arc
|
|
||||||
arc.X = bounds.Left;
|
|
||||||
path.AddArc(arc, 90, 90);
|
|
||||||
|
|
||||||
path.CloseFigure();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Image MakeGrayscale(Image original)
|
|
||||||
{
|
|
||||||
//create a blank bitmap the same size as original
|
|
||||||
Image newBitmap = new Bitmap(original.Width, original.Height);
|
|
||||||
|
|
||||||
//get a graphics object from the new image
|
|
||||||
using (Graphics g = Graphics.FromImage(newBitmap))
|
|
||||||
{
|
|
||||||
|
|
||||||
//create the grayscale ColorMatrix
|
|
||||||
ColorMatrix colorMatrix = new ColorMatrix(
|
|
||||||
new float[][]
|
|
||||||
{
|
|
||||||
new float[] {.3f, .3f, .3f, 0, 0},
|
|
||||||
new float[] {.59f, .59f, .59f, 0, 0},
|
|
||||||
new float[] {.11f, .11f, .11f, 0, 0},
|
|
||||||
new float[] {0, 0, 0, 1, 0},
|
|
||||||
new float[] {0, 0, 0, 0, 1}
|
|
||||||
});
|
|
||||||
|
|
||||||
//create some image attributes
|
|
||||||
using (ImageAttributes attributes = new ImageAttributes())
|
|
||||||
{
|
|
||||||
|
|
||||||
//set the color matrix attribute
|
|
||||||
attributes.SetColorMatrix(colorMatrix);
|
|
||||||
|
|
||||||
//draw the original image on the new image
|
|
||||||
//using the grayscale color matrix
|
|
||||||
g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height),
|
|
||||||
0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newBitmap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CS3009 // Base type is not CLS-compliant
|
#pragma warning disable CS3009 // Base type is not CLS-compliant
|
||||||
@ -354,7 +242,7 @@ namespace DisplayMagician.UIForms
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw the image
|
// Draw the image
|
||||||
Image img = RoundCorners(item.GetCachedImage(CachedImageType.Thumbnail), 20);
|
Image img = ImageUtils.RoundCorners(item.GetCachedImage(CachedImageType.Thumbnail), 20);
|
||||||
if (img != null)
|
if (img != null)
|
||||||
{
|
{
|
||||||
Rectangle pos = Utility.GetSizedImageBounds(img, new Rectangle(bounds.Location + itemPadding, ImageListView.ThumbnailSize));
|
Rectangle pos = Utility.GetSizedImageBounds(img, new Rectangle(bounds.Location + itemPadding, ImageListView.ThumbnailSize));
|
||||||
@ -368,7 +256,7 @@ namespace DisplayMagician.UIForms
|
|||||||
{
|
{
|
||||||
// THe shortcut is invalid
|
// THe shortcut is invalid
|
||||||
// so we make the image grayscale
|
// so we make the image grayscale
|
||||||
Image grayImg = MakeGrayscale(img);
|
Image grayImg = ImageUtils.MakeGrayscale(img);
|
||||||
g.DrawImage(grayImg, pos);
|
g.DrawImage(grayImg, pos);
|
||||||
|
|
||||||
// Draw a warning triangle over it
|
// Draw a warning triangle over it
|
||||||
@ -382,7 +270,7 @@ namespace DisplayMagician.UIForms
|
|||||||
using (Pen pOuterBorder = new Pen(ImageListView.Colors.ImageOuterBorderColor))
|
using (Pen pOuterBorder = new Pen(ImageListView.Colors.ImageOuterBorderColor))
|
||||||
{
|
{
|
||||||
//g.DrawRectangle(pOuterBorder, pos);
|
//g.DrawRectangle(pOuterBorder, pos);
|
||||||
DrawRoundedRectangle(g, pOuterBorder, pos, 9);
|
ImageUtils.DrawRoundedRectangle(g, pOuterBorder, pos, 9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -411,25 +299,6 @@ namespace DisplayMagician.UIForms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image RoundCorners(Image StartImage, int CornerRadius)
|
|
||||||
{
|
|
||||||
CornerRadius *= 2;
|
|
||||||
Bitmap RoundedImage = new Bitmap(StartImage.Width, StartImage.Height);
|
|
||||||
using (Graphics g = Graphics.FromImage(RoundedImage))
|
|
||||||
{
|
|
||||||
g.Clear(Color.Transparent);
|
|
||||||
g.SmoothingMode = SmoothingMode.HighQuality;
|
|
||||||
Brush brush = new TextureBrush(StartImage);
|
|
||||||
GraphicsPath gp = new GraphicsPath();
|
|
||||||
gp.AddArc(0, 0, CornerRadius, CornerRadius, 180, 90);
|
|
||||||
gp.AddArc(0 + RoundedImage.Width - CornerRadius, 0, CornerRadius, CornerRadius, 270, 90);
|
|
||||||
gp.AddArc(0 + RoundedImage.Width - CornerRadius, 0 + RoundedImage.Height - CornerRadius, CornerRadius, CornerRadius, 0, 90);
|
|
||||||
gp.AddArc(0, 0 + RoundedImage.Height - CornerRadius, CornerRadius, CornerRadius, 90, 90);
|
|
||||||
g.FillPath(brush, gp);
|
|
||||||
return RoundedImage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws the selection rectangle.
|
// Draws the selection rectangle.
|
||||||
public override void DrawSelectionRectangle(Graphics g, Rectangle selection)
|
public override void DrawSelectionRectangle(Graphics g, Rectangle selection)
|
||||||
{
|
{
|
||||||
@ -446,102 +315,6 @@ namespace DisplayMagician.UIForms
|
|||||||
selection.Width, selection.Height);
|
selection.Width, selection.Height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawRoundedRectangle(Graphics graphics, Pen pen, Rectangle bounds, int cornerRadius)
|
|
||||||
{
|
|
||||||
if (graphics == null)
|
|
||||||
throw new ArgumentNullException("graphics");
|
|
||||||
if (pen == null)
|
|
||||||
throw new ArgumentNullException("pen");
|
|
||||||
|
|
||||||
using (GraphicsPath path = RoundedRect(bounds, cornerRadius))
|
|
||||||
{
|
|
||||||
graphics.DrawPath(pen, path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FillRoundedRectangle(Graphics graphics, Brush brush, Rectangle bounds, int cornerRadius)
|
|
||||||
{
|
|
||||||
if (graphics == null)
|
|
||||||
throw new ArgumentNullException("graphics");
|
|
||||||
if (brush == null)
|
|
||||||
throw new ArgumentNullException("brush");
|
|
||||||
|
|
||||||
using (GraphicsPath path = RoundedRect(bounds, cornerRadius))
|
|
||||||
{
|
|
||||||
graphics.FillPath(brush, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GraphicsPath RoundedRect(Rectangle bounds, int radius)
|
|
||||||
{
|
|
||||||
int diameter = radius * 2;
|
|
||||||
Size size = new Size(diameter, diameter);
|
|
||||||
Rectangle arc = new Rectangle(bounds.Location, size);
|
|
||||||
GraphicsPath path = new GraphicsPath();
|
|
||||||
|
|
||||||
if (radius == 0)
|
|
||||||
{
|
|
||||||
path.AddRectangle(bounds);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// top left arc
|
|
||||||
path.AddArc(arc, 180, 90);
|
|
||||||
|
|
||||||
// top right arc
|
|
||||||
arc.X = bounds.Right - diameter;
|
|
||||||
path.AddArc(arc, 270, 90);
|
|
||||||
|
|
||||||
// bottom right arc
|
|
||||||
arc.Y = bounds.Bottom - diameter;
|
|
||||||
path.AddArc(arc, 0, 90);
|
|
||||||
|
|
||||||
// bottom left arc
|
|
||||||
arc.X = bounds.Left;
|
|
||||||
path.AddArc(arc, 90, 90);
|
|
||||||
|
|
||||||
path.CloseFigure();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Image MakeGrayscale(Image original)
|
|
||||||
{
|
|
||||||
//create a blank bitmap the same size as original
|
|
||||||
Image newBitmap = new Bitmap(original.Width, original.Height);
|
|
||||||
|
|
||||||
//get a graphics object from the new image
|
|
||||||
using (Graphics g = Graphics.FromImage(newBitmap))
|
|
||||||
{
|
|
||||||
|
|
||||||
//create the grayscale ColorMatrix
|
|
||||||
ColorMatrix colorMatrix = new ColorMatrix(
|
|
||||||
new float[][]
|
|
||||||
{
|
|
||||||
new float[] {.3f, .3f, .3f, 0, 0},
|
|
||||||
new float[] {.59f, .59f, .59f, 0, 0},
|
|
||||||
new float[] {.11f, .11f, .11f, 0, 0},
|
|
||||||
new float[] {0, 0, 0, 1, 0},
|
|
||||||
new float[] {0, 0, 0, 0, 1}
|
|
||||||
});
|
|
||||||
|
|
||||||
//create some image attributes
|
|
||||||
using (ImageAttributes attributes = new ImageAttributes())
|
|
||||||
{
|
|
||||||
|
|
||||||
//set the color matrix attribute
|
|
||||||
attributes.SetColorMatrix(colorMatrix);
|
|
||||||
|
|
||||||
//draw the original image on the new image
|
|
||||||
//using the grayscale color matrix
|
|
||||||
g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height),
|
|
||||||
0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newBitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user