diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4046d..87c8026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.7.1] / 2024-11-11 - 2024-11-19 +### Features +- Support `tiff` files for icon. +### Updated +- Update `GetBitmapFrame` to round `width` that is changed by `dpi`. +- Add `GetBitmapFrameByWidthAndDpi` to get optimal frame by `dpi` and `width`. +- Add `GetSystemDpi` to get the system `dpi` on the fly. +- Add `SystemDpi` to store the system `dpi` value. +- Update `GetBitmapFrameByWidthAndDpi` to round `dpi` frame value. +- Update `Width` to `Math.Round` to improve order by `Width`. +- Update `GetBitmapFrameByWidthAndDpi` to public. +- Update `GetBitmapFrameByWidthAndDpi` to return last `Width`. +- Update `UriToBitmapFrame` to use `GetBitmapFrameByWidthAndDpi` with `int.MaxValue`. +### Example +- Add `Cube-Grey-Light.tiff` and `Cube-Grey-Dark.tiff` in `AppTheme`. +### Tests +- Add `ResourceTiffTests` with `Cube.tiff` with multiple dpi and scales. + ## [0.7.0] / 2024-07-06 - 2024-07-25 ### Features - Auto set image based on the theme of the Ribbon using `light` and `dark` image pattern. @@ -360,6 +378,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - First Release [vNext]: ../../compare/1.0.0...HEAD +[0.7.1]: ../../compare/0.7.0...0.7.1 [0.7.0]: ../../compare/0.6.2...0.7.0 [0.6.2]: ../../compare/0.6.1...0.6.2 [0.6.1]: ../../compare/0.6.0...0.6.1 diff --git a/Directory.Build.props b/Directory.Build.props index 5596472..787fd63 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.7.0 + 0.7.1 \ No newline at end of file diff --git a/README.md b/README.md index 3ed96c9..541b236 100644 --- a/README.md +++ b/README.md @@ -389,9 +389,6 @@ Autodesk.Windows.RibbonPanel awRibbonPanel; awRibbonPanel.SetDialogLauncher(ribbonCommandItem); ``` -### Ribbon Description Extension -... Todo - ## Release * [Latest release](https://github.com/ricaun-io/ricaun.Revit.UI/releases/latest) diff --git a/ricaun.Revit.UI.Example/Resources/Cube-Grey-Dark.tiff b/ricaun.Revit.UI.Example/Resources/Cube-Grey-Dark.tiff new file mode 100644 index 0000000..12bc09f Binary files /dev/null and b/ricaun.Revit.UI.Example/Resources/Cube-Grey-Dark.tiff differ diff --git a/ricaun.Revit.UI.Example/Resources/Cube-Grey-Light.tiff b/ricaun.Revit.UI.Example/Resources/Cube-Grey-Light.tiff new file mode 100644 index 0000000..c99ad07 Binary files /dev/null and b/ricaun.Revit.UI.Example/Resources/Cube-Grey-Light.tiff differ diff --git a/ricaun.Revit.UI.Example/Revit/AppTheme.cs b/ricaun.Revit.UI.Example/Revit/AppTheme.cs index e1aeb80..d8aa158 100644 --- a/ricaun.Revit.UI.Example/Revit/AppTheme.cs +++ b/ricaun.Revit.UI.Example/Revit/AppTheme.cs @@ -61,6 +61,37 @@ public Result OnStartup(UIControlledApplication application) ribbonPanel.AddSeparator(); + + { + var buttonTiff = ribbonPanel.CreatePushButton("Grey") + .SetLargeImage("Resources/Cube-Grey-Light.tiff"); + //if (buttonTiff.LargeImage is System.Windows.Media.Imaging.BitmapSource largeImage) + //{ + // System.Console.WriteLine($"{largeImage.GetType().Name} | {largeImage.Width:0}x{largeImage.Height:0} ({largeImage.PixelWidth}x{largeImage.PixelHeight}) {largeImage.DpiX:0}:{largeImage.DpiY:0}"); + //} + //if (buttonTiff.Image is System.Windows.Media.Imaging.BitmapSource smallImage) + //{ + // System.Console.WriteLine($"{smallImage.GetType().Name} | {smallImage.Width:0}x{smallImage.Height:0} ({smallImage.PixelWidth}x{smallImage.PixelHeight}) {smallImage.DpiX:0}:{smallImage.DpiY:0}"); + //} + } + + ribbonPanel.RowStackedItems( + ribbonPanel.CreatePushButton("Grey") + .SetLargeImage("Resources/Cube-Grey-Light.tiff"), + ribbonPanel.CreatePushButton("Grey") + .SetLargeImage("Resources/Cube-Grey-Light.tiff"), + ribbonPanel.CreatePushButton("Grey") + .SetLargeImage("Resources/Cube-Grey-Light.tiff") + ); + ribbonPanel.RowLargeStackedItems( + ribbonPanel.CreatePushButton("Grey") + .SetLargeImage("Resources/Cube-Grey-Light.tiff"), + ribbonPanel.CreatePushButton("Grey") + .SetLargeImage("Resources/Cube-Grey-Light.tiff") + ); + + ribbonPanel.AddSeparator(); + ribbonPanel.FlowStackedItems( ribbonPanel.CreatePushButton("1").SetLargeImage(LIGHT_RED), ribbonPanel.CreatePushButton("2").SetLargeImage(DARK_RED), diff --git a/ricaun.Revit.UI.Example/ricaun.Revit.UI.Example.csproj b/ricaun.Revit.UI.Example/ricaun.Revit.UI.Example.csproj index cb6fb12..855d528 100644 --- a/ricaun.Revit.UI.Example/ricaun.Revit.UI.Example.csproj +++ b/ricaun.Revit.UI.Example/ricaun.Revit.UI.Example.csproj @@ -13,10 +13,20 @@ - net48;net8.0-windows + net46;net47;net48;net8.0-windows - + + + 2017 + + + + + 2019 + + + 2021 @@ -86,6 +96,8 @@ + + @@ -116,6 +128,8 @@ + + diff --git a/ricaun.Revit.UI.Tests/Resources/Images/Cube.tiff b/ricaun.Revit.UI.Tests/Resources/Images/Cube.tiff new file mode 100644 index 0000000..c99ad07 Binary files /dev/null and b/ricaun.Revit.UI.Tests/Resources/Images/Cube.tiff differ diff --git a/ricaun.Revit.UI.Tests/Resources/ResourcesFramesTests.cs b/ricaun.Revit.UI.Tests/Resources/ResourceIcoFramesTests.cs similarity index 96% rename from ricaun.Revit.UI.Tests/Resources/ResourcesFramesTests.cs rename to ricaun.Revit.UI.Tests/Resources/ResourceIcoFramesTests.cs index a6917b5..f933c14 100644 --- a/ricaun.Revit.UI.Tests/Resources/ResourcesFramesTests.cs +++ b/ricaun.Revit.UI.Tests/Resources/ResourceIcoFramesTests.cs @@ -3,7 +3,7 @@ namespace ricaun.Revit.UI.Tests.Resources { - public class ResourcesFramesTests + public class ResourceIcoFramesTests { const string ResourceNameIcoFrames = "Resources/Images/Revit21Frames.ico"; diff --git a/ricaun.Revit.UI.Tests/Resources/ResourceTiffTests.cs b/ricaun.Revit.UI.Tests/Resources/ResourceTiffTests.cs new file mode 100644 index 0000000..8198cfe --- /dev/null +++ b/ricaun.Revit.UI.Tests/Resources/ResourceTiffTests.cs @@ -0,0 +1,102 @@ +using NUnit.Framework; +using System; + +namespace ricaun.Revit.UI.Tests.Resources +{ + public class ResourceTiffTests + { + /// + /// Tiff file with multiple scales, 1.0x, 1.5x, 2.0x, 3.0x, 4.0x for 16x16 and 32x32, 10 frames. + /// + /// + /// Tiff file based in the https://github.com/ricaun-io/Autodesk.Icon.Example?tab=readme-ov-file#autodesk-high-resolution-icons + /// + const string ResourceNameTiff = "Resources/Images/Cube.tiff"; + + System.Windows.Media.Imaging.BitmapFrame BitmapFrame; + [OneTimeSetUp] + public void Setup() + { + BitmapFrame = ResourceNameTiff.GetBitmapSource() as System.Windows.Media.Imaging.BitmapFrame; + } + + [Test] + public void GetBitmapSource_NotNull() + { + Console.WriteLine(ResourceNameTiff.GetBitmapSource()); + Assert.IsNotNull(ResourceNameTiff.GetBitmapSource()); + Assert.IsNotNull(("/" + ResourceNameTiff).GetBitmapSource()); + } + + [TestCase(32)] + public void GetBitmapSource_Default_Width(int width) + { + var source = ResourceNameTiff.GetBitmapSource(); + Assert.AreEqual(width, Math.Round(source.Width)); + var systemDpi = BitmapExtension.SystemDpi; + if (systemDpi != Math.Round(source.DpiX)) + Assert.Ignore($"SystemDpi:{systemDpi} != {Math.Round(source.DpiX)}"); + } + + [TestCase(10)] + public void BitmapFrame_CountFrames(int count) + { + Assert.IsNotNull(BitmapFrame); + var decoder = BitmapFrame.Decoder; + Assert.AreEqual(count, decoder.Frames.Count); + } + + [TestCase(16, 96)] // 1.0 + [TestCase(16, 144)] // 1.5 + [TestCase(16, 192)] // 2.0 + [TestCase(16, 288)] // 3.0 + [TestCase(16, 384)] // 4.0 + [TestCase(32, 96)] // 1.0 + [TestCase(32, 144)] // 1.5 + [TestCase(32, 192)] // 2.0 + [TestCase(32, 288)] // 3.0 + [TestCase(32, 384)] // 4.0 + public void BitmapFrame_ByWidthAndDpi(int width, int dpi) + { + Assert.IsNotNull(BitmapFrame); + var decoder = BitmapFrame.Decoder; + var frame = decoder.GetBitmapFrameByWidthAndDpi(width, dpi) as System.Windows.Media.Imaging.BitmapFrame; + Assert.IsNotNull(frame); + Assert.AreEqual(width, Math.Round(frame.Width)); + Assert.AreEqual(dpi, Math.Round(frame.DpiX)); + } + + [TestCase(16, 168, 192)] // 1.75 + [TestCase(32, 168, 192)] // 1.75 + [TestCase(16, 240, 288)] // 2.5 + [TestCase(32, 240, 288)] // 2.5 + [TestCase(16, 336, 384)] // 3.5 + [TestCase(32, 336, 384)] // 3.5 + [TestCase(16, 480, 384)] // 5.0 + [TestCase(32, 480, 384)] // 5.0 + public void BitmapFrame_ByWidthAndDpi_DpiExpected(int width, int dpi, int dpiExpected) + { + Assert.IsNotNull(BitmapFrame); + var decoder = BitmapFrame.Decoder; + var frame = decoder.GetBitmapFrameByWidthAndDpi(width, dpi) as System.Windows.Media.Imaging.BitmapFrame; + Assert.IsNotNull(frame); + Assert.AreEqual(width, Math.Round(frame.Width)); + Assert.AreEqual(dpiExpected, Math.Round(frame.DpiX)); + } + + [TestCase(64, 96, 32)] // 1.0 + [TestCase(64, 144, 32)] // 1.5 + [TestCase(64, 192, 32)] // 2.0 + [TestCase(64, 288, 32)] // 3.0 + [TestCase(64, 384, 32)] // 4.0 + public void BitmapFrame_ByWidthAndDpi_WidthExpected(int width, int dpi, int widthExpected) + { + Assert.IsNotNull(BitmapFrame); + var decoder = BitmapFrame.Decoder; + var frame = decoder.GetBitmapFrameByWidthAndDpi(width, dpi) as System.Windows.Media.Imaging.BitmapFrame; + Assert.IsNotNull(frame); + Assert.AreEqual(widthExpected, Math.Round(frame.Width)); + Assert.AreEqual(dpi, Math.Round(frame.DpiX)); + } + } +} \ No newline at end of file diff --git a/ricaun.Revit.UI.Tests/ricaun.Revit.UI.Tests.csproj b/ricaun.Revit.UI.Tests/ricaun.Revit.UI.Tests.csproj index 107558b..57ee646 100644 --- a/ricaun.Revit.UI.Tests/ricaun.Revit.UI.Tests.csproj +++ b/ricaun.Revit.UI.Tests/ricaun.Revit.UI.Tests.csproj @@ -34,14 +34,10 @@ - - - - - + - + @@ -69,6 +65,7 @@ + diff --git a/ricaun.Revit.UI/BitmapExtension.cs b/ricaun.Revit.UI/BitmapExtension.cs index 78f1f69..e0d7818 100644 --- a/ricaun.Revit.UI/BitmapExtension.cs +++ b/ricaun.Revit.UI/BitmapExtension.cs @@ -17,7 +17,7 @@ internal static BitmapFrame UriToBitmapFrame(string uriString) { var uri = new Uri(uriString, UriKind.RelativeOrAbsolute); var decoder = BitmapDecoder.Create(uri, BitmapCreateOptions.None, BitmapCacheOption.Default); - return decoder.Frames.OrderBy(e => e.Width).LastOrDefault(); + return decoder.GetBitmapFrameByWidthAndDpi(int.MaxValue); } /// @@ -82,14 +82,77 @@ public static ImageSource Scale(this ImageSource imageSource, double scale) return imageSource; } +#if NET47_OR_GREATER || NET + /// + /// Get the system DPI based on the using a new . + /// +#else + /// + /// Get the system DPI based on the using a new from . + /// +#endif + internal static double GetSystemDpi() + { + double systemDpi = 96; + try + { +#if NET47_OR_GREATER || NET + var imageScaleInfo = VisualTreeHelper.GetDpi(new System.Windows.Controls.Image()); + systemDpi = imageScaleInfo.PixelsPerInchX; +#else + using (var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero)) + { + systemDpi = g.DpiX; + } +#endif + } + catch { } + return systemDpi; + } + + /// + /// System Dpi + /// + public readonly static double SystemDpi = GetSystemDpi(); + + /// + /// Get the bitmap frame from the based on the DPI and width. + /// + /// The bitmap decoder. + /// The desired width of the bitmap frame. When set to zero, the smallest width frame is returned. + /// The optimal dpi for the frame. When set to zero, is used. + /// The bitmap frame with the specified width or the smallest width frame. + public static BitmapFrame GetBitmapFrameByWidthAndDpi(this BitmapDecoder bitmapDecoder, int width, int dpi = 0) + { + double systemDpi = dpi > 0 ? dpi : SystemDpi; + + double OrderDpiX(BitmapFrame frame) + { + var dpiX = Math.Round(frame.DpiX); + return dpiX >= systemDpi ? -systemDpi / dpiX : systemDpi / dpiX; + } + + var frames = bitmapDecoder.Frames; + var framesOrder = frames + .OrderBy(OrderDpiX) + .ThenBy(e => Math.Round(e.Width)); + + var widthMax = (int)Math.Round(framesOrder.LastOrDefault().Width); + if (width > widthMax) + width = widthMax; + + var frame = framesOrder.FirstOrDefault(e => Math.Round(e.Width) >= width); + return frame; + } + /// /// GetBitmapFrame with Width Equal or Scale /// - /// - /// - /// - /// - /// When is zero, return the smallest width frame. + /// The image source. + /// The desired width of the bitmap frame. When set to zero, the smallest width frame is returned. + /// An optional action to be executed when the download of the bitmap frame is completed. + /// The bitmap frame with the specified width or the scaled bitmap frame. + /// When is zero, the smallest width frame is returned. public static TImageSource GetBitmapFrame(this TImageSource imageSource, int width = 0, Action downloadCompleted = null) where TImageSource : ImageSource { TImageSource ScaleDownIfWidthIsGreater(TImageSource imageSource, int width) @@ -97,29 +160,20 @@ TImageSource ScaleDownIfWidthIsGreater(TImageSource imageSource, int width) if (width <= 0) return imageSource; - if (imageSource.Width > width) - imageSource = imageSource.Scale(width / imageSource.Width) as TImageSource; + var imageRoundWidth = Math.Round(imageSource.Width); + if (imageRoundWidth > width) + imageSource = imageSource.Scale(width / imageRoundWidth) as TImageSource; return imageSource; } if (imageSource is BitmapFrame bitmapFrame) { - BitmapFrame GetBitmapFrameByWidth(BitmapFrame bitmapFrame, int width) - { - var frames = bitmapFrame.Decoder.Frames; - var frame = frames - .OrderBy(e => e.Width) - .FirstOrDefault(e => e.Width >= width); - - return frame; - } - if (bitmapFrame.IsDownloading) { bitmapFrame.DownloadCompleted += (s, e) => { - if (GetBitmapFrameByWidth(bitmapFrame, width) is TImageSource frame) + if (bitmapFrame.Decoder.GetBitmapFrameByWidthAndDpi(width) is TImageSource frame) imageSource = frame; imageSource = ScaleDownIfWidthIsGreater(imageSource, width); @@ -128,7 +182,7 @@ BitmapFrame GetBitmapFrameByWidth(BitmapFrame bitmapFrame, int width) }; } - if (GetBitmapFrameByWidth(bitmapFrame, width) is TImageSource frame) + if (bitmapFrame.Decoder.GetBitmapFrameByWidthAndDpi(width) is TImageSource frame) imageSource = frame; }