// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Data; using System.Windows.Media; using System.Windows.Media.Imaging; #if RIBBON_IN_FRAMEWORK namespace System.Windows.Controls.Ribbon.Primitives #else namespace Microsoft.Windows.Controls.Ribbon.Primitives #endif { public class RibbonWindowSmallIconConverter : IValueConverter { #region IValueConverter Members /// <summary> /// Returns small icon size variant (if available) of given icon. /// </summary> public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { ImageSource imageSource = value as ImageSource; ImageSource returnImageSource = null; if (imageSource != null) { returnImageSource = GetSmallIconImageSource(imageSource); if (returnImageSource == null) { returnImageSource = imageSource; } } return returnImageSource; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } #endregion #region Helper Methods private ImageSource GetSmallIconImageSource(ImageSource imageSource) { #if RIBBON_IN_FRAMEWORK Size size = new Size(SystemParameters.SmallIconWidth, SystemParameters.SmallIconHeight); #else Size size = Microsoft.Windows.Shell.SystemParameters2.Current.SmallIconSize; #endif bool asGoodAsItGets = false; var bf = imageSource as BitmapFrame; if (bf != null && bf.Decoder != null && bf.Decoder.Frames != null && bf.Decoder.Frames.Count > 0) { bf = GetBestMatch(bf.Decoder.Frames, size); // If this was actually a multi-framed icon then we don't want to do any corrections. // Let Windows do its thing. We don't want to unnecessarily deviate from the system. // If this was a jpeg or png, then we're doing something Windows doesn't do, // and we can be better. (unless it was a perfect match) asGoodAsItGets = bf.Decoder is IconBitmapDecoder // i.e. was this a .ico? || bf.PixelWidth == size.Width && bf.PixelHeight == size.Height; imageSource = bf; } if (!asGoodAsItGets) { // Unless this was a .ico, render it into a new BitmapFrame with the appropriate dimensions // to preserve the aspect ratio in the HICON and do the appropriate padding. bf = BitmapFrame.Create(GenerateBitmapSource(imageSource, size)); } return bf; } /// From a list of BitmapFrames find the one that best matches the requested dimensions. /// The methods used here are copied from Win32 sources. We want to be consistent with /// system behaviors. private static BitmapFrame GetBestMatch(ReadOnlyCollection<BitmapFrame> frames, Size size) { Debug.Assert(size.Width != 0, "input param width should not be zero"); Debug.Assert(size.Height != 0, "input param height should not be zero"); int bestScore = int.MaxValue; int bestBpp = 0; int bestIndex = 0; bool isBitmapIconDecoder = frames[0].Decoder is IconBitmapDecoder; for (int i = 0; i < frames.Count && bestScore != 0; ++i) { // determine the bit-depth (# of colors) in the // current frame // // if the icon is palettized, Format.BitsPerPixel gives // the # of bits required to index into the palette (thus, // the # of colors in the palette). If it is a true // color icon, it gives the # of bits required to support // true colors. // For icons, get the Format from the Thumbnail rather than from the // BitmapFrame directly because the unmanaged icon decoder // converts every icon to 32-bit. Thumbnail.Format.BitsPerPixel // will give us the original bit depth. int currentIconBitDepth = isBitmapIconDecoder ? frames[i].Thumbnail.Format.BitsPerPixel : frames[i].Format.BitsPerPixel; // If it looks like nothing is specified at this point, assume a bpp of 8. if (currentIconBitDepth == 0) { currentIconBitDepth = 8; } int score = MatchImage(frames[i], size, currentIconBitDepth); if (score < bestScore) { bestIndex = i; bestBpp = currentIconBitDepth; bestScore = score; } else if (score == bestScore) { // Tie breaker: choose the higher color depth. If that fails, choose first one. if (bestBpp < currentIconBitDepth) { bestIndex = i; bestBpp = currentIconBitDepth; } } } return frames[bestIndex]; } /// /// This is the algorithm Windows uses to pick icons. /// /// MatchImage /// /// This function takes LPINTs for width & height in case of "real size". /// For this option, we use dimensions of 1st icon in resdir as size to /// load, instead of system metrics. /// Returns a number that measures how "far away" the given image is /// from a desired one. The value is 0 for an exact match. Note that our /// formula has the following properties: /// (1) Differences in width/height count much more than differences in /// color format. /// (2) Bigger images are better than smaller, since shrinking produces /// better results than stretching. /// (3) Color matching is done by the difference in bit depth. No /// preference is given to having a candidate equally different /// above and below the target. /// /// The formula is the sum of the following terms: /// abs(bppCandidate - bppTarget) /// abs(cxCandidate - cxTarget), times 2 if the image is /// narrower than what we'd like. This is because we will get a /// better result when consolidating more information into a smaller /// space, than when extrapolating from less information to more. /// abs(cxCandidate - cxTarget), times 2 if the image is /// shorter than what we'd like. This is for the same reason as /// the width. /// /// Let's step through an example. Suppose we want a 4bpp (16 color), /// 32x32 image. We would choose the various candidates in the following order: /// /// Candidate Score Formula /// /// 32x32x4bpp = 0 abs(32-32)*1 + abs(32-32)*1 + 2*abs(4-4)*1 /// 32x32x2bpp = 4 /// 32x32x8bpp = 8 /// 32x32x16bpp = 24 /// 48x48x4bpp = 32 /// 48x48x2bpp = 36 /// 48x48x8bpp = 40 /// 32x32x32bpp = 56 /// 48x48x16bpp = 56 abs(48-32)*1 + abs(48-32)*1 + 2*abs(16-4)*1 /// 16x16x4bpp = 64 /// 16x16x2bpp = 68 abs(16-32)*2 + abs(16-32)*2 + 2*abs(2-4)*1 /// 16x16x8bpp = 72 /// 48x48x32bpp = 88 abs(48-32)*1 + abs(48-32)*1 + 2*abs(32-4)*1 /// 16x16x16bpp = 88 /// 16x16x32bpp = 104 private static int MatchImage(BitmapFrame frame, Size size, int bpp) { /* * Here are the rules for our "match" formula: * (1) A close size match is much preferable to a color match * (2) Bigger icons are better than smaller * (3) The smaller the difference in bit depths the better */ int score = 2 * WeightedAbs(bpp, GetBitDepth(), false) + WeightedAbs(frame.PixelWidth, (int)size.Width, true) + WeightedAbs(frame.PixelHeight, (int)size.Height, true); return score; } /// Calcules custom weighted absolute value of the difference between 2 nums. /// This of course normalizes values to >= zero. But it can also "punish" the /// returned value by a factor of two if valueHave < valueWant. This is /// because you get worse results trying to extrapolate from less info up then /// interpolating from more info down. private static int WeightedAbs(int valueHave, int valueWant, bool fPunish) { int diff = (valueHave - valueWant); if (diff < 0) { diff = (fPunish ? -2 : -1) * diff; } return diff; } /// <summary> /// Creates a BitmapSource from an arbitrary ImageSource. /// </summary> private static BitmapSource GenerateBitmapSource(ImageSource img, Size renderSize) { // By now we should just assume it's a vector image that we need to rasterize. // We want to keep the aspect ratio, but one of the dimensions will go the full length. var drawingDimensions = new Rect(0, 0, renderSize.Width, renderSize.Height); // There's no reason to assume that the requested image dimensions are square. double renderRatio = renderSize.Width / renderSize.Height; double aspectRatio = img.Width / img.Height; // If it's smaller than the requested size, then place it in the middle and pad the image. if (img.Width <= renderSize.Width && img.Height <= renderSize.Height) { drawingDimensions = new Rect((renderSize.Width - img.Width) / 2, (renderSize.Height - img.Height) / 2, img.Width, img.Height); } else if (renderRatio > aspectRatio) { double scaledRenderWidth = (img.Width / img.Height) * renderSize.Width; drawingDimensions = new Rect((renderSize.Width - scaledRenderWidth) / 2, 0, scaledRenderWidth, renderSize.Height); } else if (renderRatio < aspectRatio) { double scaledRenderHeight = (img.Height / img.Width) * renderSize.Height; drawingDimensions = new Rect(0, (renderSize.Height - scaledRenderHeight) / 2, renderSize.Width, scaledRenderHeight); } var dv = new DrawingVisual(); DrawingContext dc = dv.RenderOpen(); dc.DrawImage(img, drawingDimensions); dc.Close(); // Need to use Pbgra32 because that's all that RenderTargetBitmap currently supports. // 96 is the right DPI to use here because we're being very pixel aware. var bmp = new RenderTargetBitmap((int)renderSize.Width, (int)renderSize.Height, 96, 96, PixelFormats.Pbgra32); bmp.Render(dv); return bmp; } private static int GetBitDepth() { if (s_systemBitDepth == 0) { var hdcDesktop = new HandleRef(null, NativeMethods.GetDC(new HandleRef())); try { int sysBitDepth = NativeMethods.GetDeviceCaps(hdcDesktop, (int)NativeMethods.DeviceCap.BITSPIXEL); sysBitDepth *= NativeMethods.GetDeviceCaps(hdcDesktop, (int)NativeMethods.DeviceCap.PLANES); // If the s_systemBitDepth is 8, make it 4. Why? Because windows does not // choose a 256 color icon if the display is running in 256 color mode // because of palette flicker. if (sysBitDepth == 8) { sysBitDepth = 4; } s_systemBitDepth = sysBitDepth; } finally { NativeMethods.ReleaseDC(new HandleRef(), hdcDesktop); } } return s_systemBitDepth; } #endregion #region Data private static int s_systemBitDepth; // = 0; #endregion } } |