using System; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Bro.Common.ImageCanvas { /// /// Image element with the ability to pick out a pixel color value. /// /// /// element adorns the /// it's derived from with the facility to pick the image pixel color value at the position /// specified by the selector visual. /// public class ImageColorPicker : Image { #region dependency properties /// /// SelectedColor property backing ReadOnly DependencyProperty. /// private static readonly DependencyPropertyKey SelectedColorPropertyKey = DependencyProperty.RegisterReadOnly("SelectedColor", typeof(Color), typeof(ImageColorPicker), new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender)); public Color SelectedColor => (Color)GetValue(SelectedColorPropertyKey.DependencyProperty); /// /// Selector property backing DependencyProperty. /// public static readonly DependencyProperty SelectorProperty = DependencyProperty.Register("Selector", typeof(Drawing), typeof(ImageColorPicker), new FrameworkPropertyMetadata(new GeometryDrawing(Brushes.White, new Pen(Brushes.Black, 1), new EllipseGeometry(new Point(), 3, 3)) { Pen = new Pen(new SolidColorBrush(Color.FromArgb(212, 0, 0, 0)), 1), Brush = new SolidColorBrush(Color.FromArgb(80, 255, 255, 255)) }, FrameworkPropertyMetadataOptions.AffectsRender)); public Drawing Selector { get { return (Drawing)GetValue(SelectorProperty); } set { SetValue(SelectorProperty, value); } } /// /// Scaling for the TargetImage /// public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register("Scale", typeof(double), typeof(ImageColorPicker), new PropertyMetadata(0.0, (d, e) => (d as Image)?.InvalidateVisual())); public double Scale { get { return (double)GetValue(ScaleProperty); } set { SetValue(ScaleProperty, value); } } #endregion #region overridden methods /// /// Renders the contents of an and /// the SelectorDrawing. /// /// An instance of /// used to render the control. protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); if (ActualWidth == 0 || ActualHeight == 0) return; // Render the SelectorDrawing drawingContext.PushTransform(new TranslateTransform(Position.X, Position.Y)); if (Scale > 0) drawingContext.PushTransform(new ScaleTransform(1 / Scale, 1 / Scale)); drawingContext.DrawDrawing(Selector); drawingContext.Pop(); } /// /// Raises the event, /// using the specified information as part of the eventual event data. /// /// Details of the old and new size involved in the change. protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); _cachedTargetBitmap = null; // TargetBitmap cache isn't valid anymore. // Adjust the selector position proportionally to size change. if (sizeInfo.PreviousSize.Width > 0 && sizeInfo.PreviousSize.Height > 0) Position = new Point(Position.X * sizeInfo.NewSize.Width / sizeInfo.PreviousSize.Width , Position.Y * sizeInfo.NewSize.Height / sizeInfo.PreviousSize.Height); } /// /// Invoked whenever the effective value of any dependency property on this /// has been updated. /// The specific dependency property that changed is reported in the arguments parameter. /// Overrides . /// /// The event data that describes the property that changed, /// as well as old and new values. protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name == "Source") { _cachedTargetBitmap = null; // TargetBitmap cache isn't valid anymore. Position = new Point(); // Move the selector to the top-left corner. } base.OnPropertyChanged(e); } #endregion #region Position /// /// Gets or sets the Selector Position. /// /// The position. Point Position { get { return _position; } set { var newPos = new Point(Clamp(value.X, 0, ActualWidth), Clamp(value.Y, 0, ActualHeight)); if (_position != newPos) { _position = newPos; Color color = GetColor(_position.X, _position.Y); if (color == SelectedColor) InvalidateVisual(); SetValue(SelectedColorPropertyKey, color); } } } Point _position; /// /// Sets the as the new position if the point falls /// into the element bounds. /// /// The point. void SetPositionIfInBounds(Point pt) { if (pt.X >= 0 && pt.X <= ActualWidth && pt.Y >= 0 && pt.Y <= ActualHeight) Position = pt; } #endregion Position #region TargetBitmap RenderTargetBitmap _cachedTargetBitmap; /// /// Gets the target bitmap for the DrawingImage image Source. /// /// The target bitmap. RenderTargetBitmap TargetBitmap { get { if (_cachedTargetBitmap == null) { var drawingImage = Source as DrawingImage; if (drawingImage != null) { DrawingVisual drawingVisual = new DrawingVisual(); using (DrawingContext drawingContext = drawingVisual.RenderOpen()) { drawingContext.DrawDrawing(drawingImage.Drawing); } // Scale the DrawingVisual. Rect dvRect = drawingVisual.ContentBounds; drawingVisual.Transform = new ScaleTransform(ActualWidth / dvRect.Width , ActualHeight / dvRect.Height); _cachedTargetBitmap = new RenderTargetBitmap((int)ActualWidth , (int)ActualHeight, 96, 96, PixelFormats.Pbgra32); _cachedTargetBitmap.Render(drawingVisual); } } return _cachedTargetBitmap; } } #endregion TargetBitmap #region Mouse handling protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); SetPositionIfInBounds(e.GetPosition(this)); } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { base.OnMouseLeftButtonUp(e); SetPositionIfInBounds(e.GetPosition(this)); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (e.LeftButton == MouseButtonState.Pressed) SetPositionIfInBounds(e.GetPosition(this)); } protected override void OnMouseEnter(MouseEventArgs e) { base.OnMouseEnter(e); if (e.LeftButton == MouseButtonState.Pressed) { Point mousePoint = e.GetPosition(this); Position = new Point(mousePoint.X, mousePoint.Y); } } #endregion Mouse handling /// /// Picks the color at the position specified. /// /// The x coordinate in WPF pixels. /// The y coordinate in WPF pixels. /// The image pixel color at x,y position. /// /// Input coordinates are scaled according to the underlying image resolution, /// so this method doesn't expect exceptions thrown by the /// method. /// Color can be picked not only from the /// , but also from the /// . /// Color GetColor(double x, double y) { if (Source == null) throw new InvalidOperationException("Image Source not set"); BitmapSource bitmapSource = Source as BitmapSource; if (bitmapSource != null) { // Get color from bitmap pixel. // Convert coopdinates from WPF pixels to Bitmap pixels and restrict them by the Bitmap bounds. x *= bitmapSource.PixelWidth / ActualWidth; if ((int)x > bitmapSource.PixelWidth - 1) x = bitmapSource.PixelWidth - 1; else if (x < 0) x = 0; y *= bitmapSource.PixelHeight / ActualHeight; if ((int)y > bitmapSource.PixelHeight - 1) y = bitmapSource.PixelHeight - 1; else if (y < 0) y = 0; // Lee Brimelow approach (http://thewpfblog.com/?p=62). //byte[] pixels = new byte[4]; //CroppedBitmap cb = new CroppedBitmap(bitmapSource, new Int32Rect((int)x, (int)y, 1, 1)); //cb.CopyPixels(pixels, 4, 0); //return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]); // Alternative approach if (bitmapSource.Format == PixelFormats.Indexed4) { byte[] pixels = new byte[1]; int stride = (bitmapSource.PixelWidth * bitmapSource.Format.BitsPerPixel + 3) / 4; bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1), pixels, stride, 0); //Debug.Assert(bitmapSource.Palette != null, "bitmapSource.Palette != null"); //Debug.Assert(bitmapSource.Palette.Colors.Count == 16, "bitmapSource.Palette.Colors.Count == 16"); return bitmapSource.Palette.Colors[pixels[0] >> 4]; } else if (bitmapSource.Format == PixelFormats.Indexed8) { byte[] pixels = new byte[1]; int stride = (bitmapSource.PixelWidth * bitmapSource.Format.BitsPerPixel + 7) / 8; bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1), pixels, stride, 0); //Debug.Assert(bitmapSource.Palette != null, "bitmapSource.Palette != null"); //Debug.Assert(bitmapSource.Palette.Colors.Count == 256, "bitmapSource.Palette.Colors.Count == 256"); return bitmapSource.Palette.Colors[pixels[0]]; } else { byte[] pixels = new byte[4]; int stride = (bitmapSource.PixelWidth * bitmapSource.Format.BitsPerPixel + 7) / 8; bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1), pixels, stride, 0); return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]); } // TODO There are other PixelFormats which processing should be added if desired. } DrawingImage drawingImage = Source as DrawingImage; if (drawingImage != null) { // Get color from drawing pixel. RenderTargetBitmap targetBitmap = TargetBitmap; Debug.Assert(targetBitmap != null, "targetBitmap != null"); // Convert coopdinates from WPF pixels to Bitmap pixels and restrict them by the Bitmap bounds. x *= targetBitmap.PixelWidth / ActualWidth; if ((int)x > targetBitmap.PixelWidth - 1) x = targetBitmap.PixelWidth - 1; else if (x < 0) x = 0; y *= targetBitmap.PixelHeight / ActualHeight; if ((int)y > targetBitmap.PixelHeight - 1) y = targetBitmap.PixelHeight - 1; else if (y < 0) y = 0; // TargetBitmap is always in PixelFormats.Pbgra32 format. // Pbgra32 is a sRGB format with 32 bits per pixel (BPP). Each channel (blue, green, red, and alpha) // is allocated 8 bits per pixel (BPP). Each color channel is pre-multiplied by the alpha value. byte[] pixels = new byte[4]; int stride = (targetBitmap.PixelWidth * targetBitmap.Format.BitsPerPixel + 7) / 8; targetBitmap.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1), pixels, stride, 0); return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]); } throw new InvalidOperationException("Unsupported Image Source Type"); } private static T Clamp(T value, T min, T max) where T : IComparable { return (value.CompareTo(min) < 0) ? min : value.CompareTo(max) > 0 ? max : value; } } }