├── vimage ├── Resources │ └── icon.ico ├── vimage.csproj ├── Program.cs ├── Utils │ ├── WindowsFileSorting.cs │ └── ImageViewerUtils.cs ├── Display │ ├── AnimatedImage.cs │ ├── DisplayObject.cs │ ├── DWM.cs │ └── Graphics.cs ├── ImageManipulation │ ├── Quantizer.cs │ └── OctreeQuantizer.cs └── ContextMenu.cs ├── vimage_settings ├── Resources │ └── icon.ico ├── Xceed.Wpf.Toolkit.dll ├── Properties │ ├── Settings.settings │ ├── Settings.Designer.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Source │ ├── General.xaml.cs │ ├── CommandsList.xaml.cs │ ├── App.xaml │ ├── About.xaml.cs │ ├── App.xaml.cs │ ├── ControlBindings.xaml │ ├── Misc.cs │ ├── ContextMenuEditorCanvas.cs │ ├── MainWindow.xaml.cs │ ├── ControlItem.xaml │ ├── About.xaml │ ├── CustomActionItem.xaml │ ├── ControlBindings.xaml.cs │ ├── MainWindow.xaml │ ├── CustomActions.xaml.cs │ ├── CustomActionItem.xaml.cs │ ├── CustomActions.xaml │ ├── ContextMenuItem.xaml │ ├── ContextMenu.xaml │ ├── CommandsList.xaml │ ├── ControlItem.xaml.cs │ ├── ContextMenu.xaml.cs │ ├── General.xaml │ └── ContextMenuItem.xaml.cs └── vimage_settings.csproj ├── .vscode └── launch.json ├── vimage.Common ├── vimage.Common.csproj └── Actions.cs ├── LICENSE ├── Makefile ├── README.md ├── .gitignore └── vimage.sln /vimage/Resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Torrunt/vimage/HEAD/vimage/Resources/icon.ico -------------------------------------------------------------------------------- /vimage_settings/Resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Torrunt/vimage/HEAD/vimage_settings/Resources/icon.ico -------------------------------------------------------------------------------- /vimage_settings/Xceed.Wpf.Toolkit.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Torrunt/vimage/HEAD/vimage_settings/Xceed.Wpf.Toolkit.dll -------------------------------------------------------------------------------- /vimage_settings/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "C#: vimage", 5 | "type": "dotnet", 6 | "request": "launch", 7 | "projectPath": "${workspaceFolder}\\vimage\\vimage.csproj", 8 | "launchConfigurationId": "TargetFramework=;vimage" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /vimage_settings/Source/General.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace vimage_settings 4 | { 5 | /// 6 | /// Interaction logic for General.xaml 7 | /// 8 | public partial class General : UserControl 9 | { 10 | public General() 11 | { 12 | InitializeComponent(); 13 | DataContext = App.vimageConfig; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vimage.Common/vimage.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0-windows 5 | win-x64;win-x86 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /vimage_settings/Source/CommandsList.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace vimage_settings 4 | { 5 | /// 6 | /// Interaction logic for CommandsList.xaml 7 | /// 8 | public partial class CommandsList : Window 9 | { 10 | public CommandsList() 11 | { 12 | InitializeComponent(); 13 | SourceInitialized += (s, e) => { MaxHeight = ActualHeight; }; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vimage_settings/Source/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /vimage_settings/Source/About.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows.Controls; 3 | using System.Windows.Navigation; 4 | 5 | namespace vimage_settings 6 | { 7 | /// 8 | /// Interaction logic for About.xaml 9 | /// 10 | public partial class About : UserControl 11 | { 12 | public About() 13 | { 14 | InitializeComponent(); 15 | } 16 | 17 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) 18 | { 19 | _ = Process.Start( 20 | new ProcessStartInfo { FileName = e.Uri.AbsoluteUri, UseShellExecute = true } 21 | ); 22 | e.Handled = true; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vimage_settings/Source/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using vimage.Common; 4 | 5 | namespace vimage_settings 6 | { 7 | public partial class App : Application 8 | { 9 | public static Config vimageConfig = new(); 10 | 11 | protected override void OnStartup(StartupEventArgs e) 12 | { 13 | base.OnStartup(e); 14 | 15 | try 16 | { 17 | vimageConfig.Load( 18 | System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.txt") 19 | ); 20 | } 21 | catch (UnauthorizedAccessException) 22 | { 23 | MessageBox.Show( 24 | "vimage does not have write permissions for the folder it's located in.\nPlease place it somewhere else (or set it to run as admin).", 25 | "vimage - Error" 26 | ); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /vimage_settings/Source/ControlBindings.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /vimage_settings/Source/Misc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace vimage_settings 6 | { 7 | public class EnumConverter : IValueConverter 8 | { 9 | public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | if (value == null) 12 | return null; 13 | 14 | // convert int to enum 15 | if (targetType.IsEnum) 16 | return Enum.ToObject(targetType, value); 17 | 18 | // convert enum to int 19 | return value.GetType().IsEnum 20 | ? System.Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType())) 21 | : null; 22 | } 23 | 24 | public object? ConvertBack( 25 | object value, 26 | Type targetType, 27 | object parameter, 28 | CultureInfo culture 29 | ) 30 | { 31 | // perform the same conversion in both directions 32 | return Convert(value, targetType, parameter, culture); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Corey Womack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | ## publish: creates a 64bit release build 4 | .PHONY: publish 5 | publish: 6 | @sed -i.bak "s|public const string SENTRY_DSN = \".*\";|public const string SENTRY_DSN = \"$(SENTRY_DSN)\";|" vimage/Program.cs 7 | dotnet publish vimage -c Release -r win-x64 --self-contained false /p:PublishSingleFile=true 8 | dotnet publish vimage_settings -c Release -r win-x64 --self-contained false /p:PublishSingleFile=true 9 | @sed -i "s|public const string SENTRY_DSN = \".*\";|public const string SENTRY_DSN = \"\";|" vimage/Program.cs 10 | @rm -f vimage/Program.cs.bak 11 | 12 | ## publish-x86: creates a 32bit release build 13 | .PHONY: publish-x86 14 | publish-x86: 15 | @sed -i.bak "s|public const string SENTRY_DSN = \".*\";|public const string SENTRY_DSN = \"$(SENTRY_DSN)\";|" vimage/Program.cs 16 | dotnet publish vimage -c Release -r win-x86 --self-contained false /p:PublishSingleFile=true 17 | dotnet publish vimage_settings -c Release -r win-x86 --self-contained false /p:PublishSingleFile=true 18 | @sed -i "s|public const string SENTRY_DSN = \".*\";|public const string SENTRY_DSN = \"\";|" vimage/Program.cs 19 | @rm -f vimage/Program.cs.bak 20 | -------------------------------------------------------------------------------- /vimage_settings/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace vimage_settings.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.3.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vimage_settings/Source/ContextMenuEditorCanvas.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Input; 3 | using System.Windows.Shapes; 4 | 5 | namespace vimage_settings 6 | { 7 | public class ContextMenuEditorCanvas : Canvas 8 | { 9 | public ContextMenuItem? MovingItem; 10 | public ContextMenuItem GhostItem = new(); 11 | public Rectangle SelectionRect = new() { Height = 4, Fill = System.Windows.Media.Brushes.Black, Opacity = 0.5f }; 12 | public int InsertAtIndex = -1; 13 | 14 | public ContextMenuEditorCanvas() : base() 15 | { 16 | } 17 | 18 | protected override void OnMouseUp(MouseButtonEventArgs e) 19 | { 20 | base.OnMouseUp(e); 21 | 22 | if (MovingItem != null) 23 | MovingItem.Dragging = false; 24 | } 25 | 26 | public void SetupGhost(ContextMenuItem item) 27 | { 28 | if (GhostItem.Parent != null) 29 | return; 30 | 31 | GhostItem.UpdateCustomActions(); 32 | GhostItem.ItemName.Text = item.ItemName.Text; 33 | GhostItem.ItemFunction.Text = item.ItemFunction.Text; 34 | GhostItem.Indent = item.Indent; 35 | GhostItem.IsEnabled = false; 36 | } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vimage_settings/Source/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Windows; 4 | using System.Windows.Navigation; 5 | 6 | namespace vimage_settings 7 | { 8 | /// 9 | /// Interaction logic for MainWindow.xaml 10 | /// 11 | public partial class MainWindow : Window 12 | { 13 | public MainWindow() 14 | { 15 | InitializeComponent(); 16 | DataContext = App.vimageConfig; 17 | } 18 | 19 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) 20 | { 21 | _ = Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); 22 | e.Handled = true; 23 | } 24 | 25 | private void Save_Click(object sender, RoutedEventArgs e) 26 | { 27 | ContextMenuEditor.Save(); 28 | 29 | try 30 | { 31 | App.vimageConfig?.Save( 32 | System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.txt") 33 | ); 34 | } 35 | catch (UnauthorizedAccessException) 36 | { 37 | MessageBox.Show( 38 | "vimage does not have write permissions for the folder it's located in.\nPlease place it somewhere else (or set it to run as admin).", 39 | "vimage - Error" 40 | ); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /vimage_settings/Source/ControlItem.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## vimage 2 | [torrunt.net/vimage](http://torrunt.net/vimage) 3 | 4 | A simplistic image viewer for Windows, inspired by [vjpeg](http://stereopsis.com/vjpeg/). 5 | 6 | ![](https://i.imgur.com/Il2ZfTV.png) 7 | ![](https://i.imgur.com/x0Gu282.png) 8 | 9 | ### Created by 10 | Corey Zeke Womack (Torrunt) - [me@torrunt.net](mailto:me@torrunt.net) - [torrunt.net](http://torrunt.net) 11 | 12 | ### Features 13 | - No ugly interface, just the image 14 | - Move it around, resize it, rotate it, flip it and step through images in a folder 15 | - Supports over 100 major file formats (image loading done via [ImageMagick](https://imagemagick.org/script/formats.php#supported)) 16 | - Supports animated gifs, pngs and webps (pauseable and the frames can be stepped through) 17 | - Supports transparency 18 | - Toggleable Always on Top Mode 19 | - View Cropping 20 | - Settings, Keyboard/Mouse Bindings and Context Menu are completely configurable 21 | 22 | ### Basic Controls 23 | - Left-Click to Drag 24 | - Right-Click for Context Menu 25 | - Scroll Wheel to Zoom (hold SHIFT to zoom faster) 26 | - Middle-Click to toggle between actual image size and monitor height 27 | - Left/Right Arrows (or Page Up/Down) to navigate between images in a folder 28 | - Up/Down Arrows to Rotate 29 | - F to Flip Horizontally 30 | - S to Toggle Smoothing 31 | - L to Toggle Always On Top mode 32 | - X (hold) + Move Mouse to crop the view of the image 33 | - T (hold) + Scroll Wheel to adjust transparency of the image 34 | - R to Reset Image 35 | - Space to pause Animated Images 36 | - to step through Animated Image frames 37 | -------------------------------------------------------------------------------- /vimage_settings/Source/About.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 15 | 16 | A simplistic image viewer for Windows. 17 | 18 | torrunt.net/vimage 19 | 20 | github.com/Torrunt/vimage 21 | 22 | Created by Corey Zeke Womack (Torrunt) - me@torrunt.net 23 | 24 | Image Loading via Magick.NET - github.com/dlemstra/Magick.NET 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /vimage_settings/Source/CustomActionItem.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /vimage_settings/vimage_settings.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0-windows 4 | win-x64;win-x86 5 | WinExe 6 | enable 7 | true 8 | true 9 | true 10 | Resources/icon.ico 11 | 12 | 13 | 14 | true 15 | true 16 | 17 | 18 | 19 | TRACE;SETTINGSAPP 20 | true 21 | 22 | 23 | 24 | 25 | 26 | .\Xceed.Wpf.Toolkit.dll 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /vimage_settings/Source/ControlBindings.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using vimage.Common; 5 | 6 | namespace vimage_settings 7 | { 8 | /// 9 | /// Interaction logic for ControlBindings.xaml 10 | /// 11 | public partial class ControlBindings : UserControl 12 | { 13 | public List CustomActionBindings = []; 14 | 15 | public ControlBindings() 16 | { 17 | InitializeComponent(); 18 | 19 | if (App.vimageConfig == null) 20 | return; 21 | for (int i = 0; i < App.vimageConfig.Controls.Count; i++) 22 | { 23 | var item = new ControlItem(App.vimageConfig.ControlNames[i], App.vimageConfig.Controls[i]); 24 | _ = ControlsPanel.Children.Add(item); 25 | } 26 | CustomActionBindings = []; 27 | for (int i = 0; i < App.vimageConfig.CustomActionBindings.Count; i++) 28 | { 29 | AddCustomActionBinding(i); 30 | } 31 | } 32 | public void AddCustomActionBinding(int index) 33 | { 34 | if (App.vimageConfig.CustomActionBindings[index] is CustomActionBinding cab) 35 | { 36 | var item = new ControlItem(cab.name, cab.bindings); 37 | _ = ControlsPanel.Children.Add(item); 38 | CustomActionBindings.Add(item); 39 | } 40 | } 41 | public void RemoveCustomActionBinding(int index) 42 | { 43 | ControlsPanel.Children.Remove(CustomActionBindings[index]); 44 | CustomActionBindings.RemoveAt(index); 45 | } 46 | 47 | private void Default_Click(object sender, RoutedEventArgs e) 48 | { 49 | // Reset Controls to Default 50 | App.vimageConfig.SetDefaultControls(); 51 | 52 | foreach (ControlItem item in ControlsPanel.Children) 53 | item.UpdateBindings(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /vimage/vimage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0-windows 4 | win-x64;win-x86 5 | WinExe 6 | enable 7 | true 8 | true 9 | true 10 | true 11 | Resources/icon.ico 12 | en-US 13 | 14 | 15 | 16 | TRACE;RELEASE 17 | true 18 | false 19 | 20 | 21 | 22 | DEBUG;TRACE 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Component 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /vimage_settings/Source/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | torrunt.net/vimage 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /vimage_settings/Source/CustomActions.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using vimage.Common; 5 | 6 | namespace vimage_settings 7 | { 8 | /// 9 | /// Interaction logic for CustomActions.xaml 10 | /// 11 | public partial class CustomActions : UserControl 12 | { 13 | public CustomActions() 14 | { 15 | InitializeComponent(); 16 | DataContext = App.vimageConfig; 17 | 18 | if (App.vimageConfig == null) 19 | return; 20 | LoadItems(); 21 | } 22 | 23 | private void LoadItems() 24 | { 25 | if (App.vimageConfig is null) return; 26 | for (int i = 0; i < App.vimageConfig.CustomActions.Count; i++) 27 | { 28 | var item = new CustomActionItem(i, CustomActionItems); 29 | _ = CustomActionItems.Children.Add(item); 30 | } 31 | } 32 | 33 | public void UpdateItemIndices() 34 | { 35 | for (int i = 0; i < CustomActionItems.Children.Count; i++) 36 | { 37 | if (CustomActionItems.Children[i] is CustomActionItem customActionItem) 38 | customActionItem.Index = i; 39 | } 40 | } 41 | 42 | private void Add_Click(object sender, RoutedEventArgs e) 43 | { 44 | int index = CustomActionItems.Children.Count; 45 | 46 | if (App.vimageConfig != null) 47 | { 48 | App.vimageConfig.CustomActions.Add(new CustomAction { name = "ACTION", func = "" }); 49 | App.vimageConfig.CustomActionBindings.Add( 50 | new CustomActionBinding { name = "ACTION", bindings = [] } 51 | ); 52 | } 53 | 54 | var item = new CustomActionItem(index, CustomActionItems); 55 | _ = CustomActionItems.Children.Add(item); 56 | 57 | if (Application.Current.MainWindow is MainWindow mainWindow) 58 | { 59 | // update controls tab 60 | mainWindow.ControlBindings.AddCustomActionBinding( 61 | index 62 | ); 63 | // update context menu function list 64 | mainWindow.ContextMenuEditor.UpdateCustomActions(); 65 | } 66 | } 67 | 68 | private void CommandList_Click(object sender, RoutedEventArgs e) 69 | { 70 | new CommandsList().Show(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Visual Studio 3 | ################# 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | packages/ 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.sln.docstates 14 | .vs 15 | 16 | # Build results 17 | 18 | [Dd]ebug/ 19 | [Rr]elease/ 20 | build/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | *_i.c 29 | *_p.c 30 | *.ilk 31 | *.meta 32 | *.obj 33 | *.pch 34 | *.pdb 35 | *.pgc 36 | *.pgd 37 | *.rsp 38 | *.sbr 39 | *.tlb 40 | *.tli 41 | *.tlh 42 | *.tmp 43 | *.tmp_proj 44 | *.log 45 | *.vspscc 46 | *.vssscc 47 | .builds 48 | *.pidb 49 | *.log 50 | *.scc 51 | 52 | # Visual C++ cache files 53 | ipch/ 54 | *.aps 55 | *.ncb 56 | *.opensdf 57 | *.sdf 58 | *.cachefile 59 | 60 | # Visual Studio profiler 61 | *.psess 62 | *.vsp 63 | *.vspx 64 | 65 | # Guidance Automation Toolkit 66 | *.gpState 67 | 68 | # ReSharper is a .NET coding add-in 69 | _ReSharper*/ 70 | *.[Rr]e[Ss]harper 71 | 72 | # TeamCity is a build add-in 73 | _TeamCity* 74 | 75 | # DotCover is a Code Coverage Tool 76 | *.dotCover 77 | 78 | # NCrunch 79 | *.ncrunch* 80 | .*crunch*.local.xml 81 | 82 | # Installshield output folder 83 | [Ee]xpress/ 84 | 85 | # DocProject is a documentation generator add-in 86 | DocProject/buildhelp/ 87 | DocProject/Help/*.HxT 88 | DocProject/Help/*.HxC 89 | DocProject/Help/*.hhc 90 | DocProject/Help/*.hhk 91 | DocProject/Help/*.hhp 92 | DocProject/Help/Html2 93 | DocProject/Help/html 94 | 95 | # Click-Once directory 96 | publish/ 97 | 98 | # Publish Web Output 99 | *.Publish.xml 100 | *.pubxml 101 | 102 | # Windows Azure Build Output 103 | csx 104 | *.build.csdef 105 | 106 | # Windows Store app package directory 107 | AppPackages/ 108 | 109 | # Others 110 | sql/ 111 | *.Cache 112 | ClientBin/ 113 | [Ss]tyle[Cc]op.* 114 | ~$* 115 | *~ 116 | *.dbmdl 117 | *.[Pp]ublish.xml 118 | *.pfx 119 | *.publishsettings 120 | 121 | # RIA/Silverlight projects 122 | Generated_Code/ 123 | 124 | # Backup & report files from converting an old project file to a newer 125 | # Visual Studio version. Backup files are not needed, because we have git ;-) 126 | _UpgradeReport_Files/ 127 | Backup*/ 128 | UpgradeLog*.XML 129 | UpgradeLog*.htm 130 | 131 | # SQL Server files 132 | App_Data/*.mdf 133 | App_Data/*.ldf 134 | 135 | ############# 136 | ## Windows detritus 137 | ############# 138 | 139 | # Windows image file caches 140 | Thumbs.db 141 | ehthumbs.db 142 | 143 | # Folder config file 144 | Desktop.ini 145 | 146 | # Recycle Bin used on file shares 147 | $RECYCLE.BIN/ 148 | 149 | # Mac crap 150 | .DS_Store 151 | 152 | .env 153 | -------------------------------------------------------------------------------- /vimage/Program.cs: -------------------------------------------------------------------------------- 1 | // vimage - http://torrunt.net/vimage 2 | // Corey Zeke Womack (Torrunt) - me@torrunt.net 3 | 4 | using System; 5 | 6 | namespace vimage 7 | { 8 | internal class Program 9 | { 10 | public const string SENTRY_DSN = ""; 11 | 12 | private static void Main(string[] args) 13 | { 14 | string file = ""; 15 | if (args.Length > 0) 16 | { 17 | file = args[0]; 18 | if (!System.IO.File.Exists(file)) 19 | return; 20 | } 21 | 22 | // Extension supported? 23 | ImageMagick.MagickImageInfo? imageInfo = null; 24 | if (file != "") 25 | { 26 | try 27 | { 28 | imageInfo = new ImageMagick.MagickImageInfo( 29 | file, 30 | Utils.ImageViewerUtils.GetDefaultMagickReadSettings() 31 | ); 32 | } 33 | catch (ImageMagick.MagickMissingDelegateErrorException) 34 | { 35 | System.Windows.Forms.MessageBox.Show( 36 | "vimage does not support this file format.", 37 | "vimage - Unknown File Format" 38 | ); 39 | return; 40 | } 41 | catch (ImageMagick.MagickCorruptImageErrorException) 42 | { 43 | System.Windows.Forms.MessageBox.Show( 44 | "The file appears to be corrupted and cannot be opened.", 45 | "vimage - Corrupted File" 46 | ); 47 | return; 48 | } 49 | if (!Utils.ImageViewerUtils.IsSupportedFileType(imageInfo.Format)) 50 | { 51 | System.Windows.Forms.MessageBox.Show( 52 | "vimage does not support this file format.", 53 | "vimage - Unknown File Format" 54 | ); 55 | return; 56 | } 57 | } 58 | 59 | // Setup Sentry 60 | Sentry.SentrySdk.Init(options => 61 | { 62 | options.Dsn = SENTRY_DSN; 63 | options.IsGlobalModeEnabled = true; 64 | options.AutoSessionTracking = true; 65 | }); 66 | if (imageInfo != null) 67 | { 68 | Sentry.SentrySdk.ConfigureScope(scope => 69 | { 70 | scope.Contexts["File"] = new 71 | { 72 | Path = file, 73 | Format = Enum.GetName(imageInfo.Format), 74 | imageInfo.Width, 75 | imageInfo.Height, 76 | imageInfo.Orientation, 77 | }; 78 | }); 79 | } 80 | 81 | _ = new ImageViewer(file, args); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /vimage_settings/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace vimage_settings.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("vimage_settings.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /vimage_settings/Source/CustomActionItem.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using vimage.Common; 4 | 5 | namespace vimage_settings 6 | { 7 | /// 8 | /// Interaction logic for CustomActionItem.xaml 9 | /// 10 | public partial class CustomActionItem : UserControl 11 | { 12 | private readonly StackPanel ParentPanel; 13 | public int Index; 14 | 15 | public CustomActionItem(int index, StackPanel parentPanel) 16 | { 17 | InitializeComponent(); 18 | 19 | Index = index; 20 | ParentPanel = parentPanel; 21 | 22 | ItemName.Text = App.vimageConfig?.CustomActions[Index].name; 23 | ItemAction.Text = App.vimageConfig?.CustomActions[Index].func; 24 | 25 | ItemName.TextChanged += ItemName_TextChanged; 26 | ItemAction.TextChanged += ItemAction_TextChanged; 27 | } 28 | 29 | private void Delete_Click(object sender, RoutedEventArgs e) 30 | { 31 | if (App.vimageConfig is not null) 32 | { 33 | App.vimageConfig.CustomActions.RemoveAt(Index); 34 | App.vimageConfig.CustomActionBindings.RemoveAt(Index); 35 | } 36 | 37 | ParentPanel?.Children.Remove(this); 38 | 39 | if (Application.Current.MainWindow is MainWindow mainWindow) 40 | { 41 | // update controls tab 42 | mainWindow.ControlBindings.RemoveCustomActionBinding(Index); 43 | 44 | // update item indices 45 | mainWindow.CustomActions.UpdateItemIndices(); 46 | // update context menu function list 47 | mainWindow.ContextMenuEditor.UpdateCustomActions(); 48 | } 49 | } 50 | 51 | private void ItemName_TextChanged(object sender, TextChangedEventArgs e) 52 | { 53 | if (App.vimageConfig == null) 54 | return; 55 | App.vimageConfig.CustomActions[Index] = new CustomAction { name = ItemName.Text, func = ItemAction.Text }; 56 | 57 | // update control binding 58 | var bindings = App.vimageConfig.CustomActionBindings[Index].bindings; 59 | App.vimageConfig.CustomActionBindings[Index] = new CustomActionBinding { name = ItemName.Text, bindings = bindings }; 60 | 61 | if (Application.Current.MainWindow is MainWindow mainWindow) 62 | { 63 | // update controls tab 64 | mainWindow.ControlBindings.CustomActionBindings[Index].ControlName.Content = ItemName.Text; 65 | // update context menu function list 66 | mainWindow.ContextMenuEditor.UpdateCustomActions(); 67 | } 68 | } 69 | private void ItemAction_TextChanged(object sender, TextChangedEventArgs e) 70 | { 71 | if (App.vimageConfig == null) 72 | return; 73 | App.vimageConfig.CustomActions[Index] = new CustomAction { name = ItemName.Text, func = ItemAction.Text }; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /vimage_settings/Source/CustomActions.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 50 | 51 | %f = current file (with quotes) 52 | %d = current directory (without quotes) 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /vimage/Utils/WindowsFileSorting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace vimage.Utils 7 | { 8 | internal partial class WindowsFileSorting 9 | { 10 | [LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16)] 11 | public static partial int StrCmpLogicalW(string psz1, string psz2); 12 | 13 | [System.Security.SuppressUnmanagedCodeSecurity] 14 | internal static partial class SafeNativeMethods 15 | { 16 | [LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16)] 17 | public static partial int StrCmpLogicalW(string psz1, string psz2); 18 | } 19 | 20 | public sealed class NaturalStringComparer : IComparer 21 | { 22 | public int Compare(string? a, string? b) 23 | { 24 | return SafeNativeMethods.StrCmpLogicalW(a ?? "", b ?? ""); 25 | } 26 | } 27 | 28 | public sealed class NaturalFileInfoNameComparer : IComparer 29 | { 30 | public int Compare(FileInfo? a, FileInfo? b) 31 | { 32 | return SafeNativeMethods.StrCmpLogicalW(a?.Name ?? "", b?.Name ?? ""); 33 | } 34 | } 35 | 36 | public static string? GetWindowsSortOrder(string fileName) 37 | { 38 | var directory = Path.GetDirectoryName(fileName); 39 | if (directory is null) 40 | return null; 41 | var parentFolder = Path.GetFileName(directory); 42 | if (parentFolder is null) 43 | return null; 44 | 45 | var shellWindowsType = Type.GetTypeFromProgID("Shell.Application"); 46 | if (shellWindowsType is null) 47 | return null; 48 | dynamic? shell = Activator.CreateInstance(shellWindowsType); 49 | if (shell is null) 50 | return null; 51 | foreach (var window in shell.Windows()) 52 | { 53 | dynamic? view = window.Document; 54 | if (view is null) 55 | continue; 56 | 57 | var folderPath = view.Folder?.Self?.Path; 58 | if (string.IsNullOrEmpty(folderPath)) 59 | continue; 60 | if (!string.Equals(folderPath, directory, StringComparison.OrdinalIgnoreCase)) 61 | continue; 62 | 63 | string sortColumns = view.SortColumns; 64 | 65 | // can be sorted by multiple columns (eg: date then name) - just return first one 66 | int firstSemi = sortColumns.IndexOf(';'); 67 | string firstProp = sortColumns[5..firstSemi]; // strip off "prop:" prefix 68 | 69 | return firstProp; 70 | } 71 | return null; 72 | } 73 | 74 | private static bool HasProperty(dynamic obj, string name) 75 | { 76 | try 77 | { 78 | var val = obj.GetType() 79 | .InvokeMember( 80 | name, 81 | System.Reflection.BindingFlags.GetProperty, 82 | null, 83 | obj, 84 | null 85 | ); 86 | return true; 87 | } 88 | catch 89 | { 90 | return false; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /vimage_settings/Source/ContextMenuItem.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /vimage_settings/Source/ContextMenu.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /vimage.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.2.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "vimage", "vimage\vimage.csproj", "{92394A27-D9C5-1F3C-9D83-93543FA5CDB2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "vimage_settings", "vimage_settings\vimage_settings.csproj", "{80DD9197-20D6-164E-9F06-82E5645C1AFF}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vimage.Common", "vimage.Common\vimage.Common.csproj", "{27028A02-7BA1-4CE4-A12F-9D5CBD333F58}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Debug|x64.ActiveCfg = Debug|Any CPU 25 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Debug|x64.Build.0 = Debug|Any CPU 26 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Debug|x86.ActiveCfg = Debug|Any CPU 27 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Debug|x86.Build.0 = Debug|Any CPU 28 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Release|x64.ActiveCfg = Release|Any CPU 31 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Release|x64.Build.0 = Release|Any CPU 32 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Release|x86.ActiveCfg = Release|Any CPU 33 | {92394A27-D9C5-1F3C-9D83-93543FA5CDB2}.Release|x86.Build.0 = Release|Any CPU 34 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Debug|x64.ActiveCfg = Debug|Any CPU 37 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Debug|x64.Build.0 = Debug|Any CPU 38 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Debug|x86.Build.0 = Debug|Any CPU 40 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Release|x64.ActiveCfg = Release|Any CPU 43 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Release|x64.Build.0 = Release|Any CPU 44 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Release|x86.ActiveCfg = Release|Any CPU 45 | {80DD9197-20D6-164E-9F06-82E5645C1AFF}.Release|x86.Build.0 = Release|Any CPU 46 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Debug|x64.ActiveCfg = Debug|Any CPU 49 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Debug|x64.Build.0 = Debug|Any CPU 50 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Debug|x86.ActiveCfg = Debug|Any CPU 51 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Debug|x86.Build.0 = Debug|Any CPU 52 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Release|x64.ActiveCfg = Release|Any CPU 55 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Release|x64.Build.0 = Release|Any CPU 56 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Release|x86.ActiveCfg = Release|Any CPU 57 | {27028A02-7BA1-4CE4-A12F-9D5CBD333F58}.Release|x86.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {4ED5620A-F2AF-409A-BBD2-C04B98A544CB} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /vimage/Display/AnimatedImage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SFML.Graphics; 3 | 4 | namespace vimage.Display 5 | { 6 | internal class AnimatedImageData 7 | { 8 | public Texture[] Frames = []; 9 | public int[] FrameDelays = []; 10 | public int FrameCount = 0; 11 | public bool FullyLoaded = false; 12 | public bool CancelLoading = false; 13 | 14 | private bool _Smooth = true; 15 | public bool Smooth 16 | { 17 | get { return _Smooth; } 18 | set 19 | { 20 | _Smooth = value; 21 | if (FullyLoaded) 22 | { 23 | foreach (var texture in Frames) 24 | texture.Smooth = _Smooth; 25 | } 26 | } 27 | } 28 | 29 | private bool _Mipmap = true; 30 | public bool Mipmap 31 | { 32 | get { return _Mipmap; } 33 | set 34 | { 35 | _Mipmap = value; 36 | if (FullyLoaded && _Mipmap) 37 | { 38 | foreach (var texture in Frames) 39 | texture.GenerateMipmap(); 40 | } 41 | } 42 | } 43 | 44 | public AnimatedImageData() { } 45 | } 46 | 47 | internal class AnimatedImage : DisplayObject 48 | { 49 | public AnimatedImageData Data; 50 | public Sprite Sprite; 51 | public new Texture Texture 52 | { 53 | get { return Sprite.Texture; } 54 | private set { } 55 | } 56 | 57 | public int CurrentFrame; 58 | public int TotalFrames 59 | { 60 | get { return Data.Frames.Length; } 61 | private set { } 62 | } 63 | 64 | public bool Playing = true; 65 | private bool _Looping = true; 66 | public bool Looping 67 | { 68 | get { return _Looping; } 69 | set 70 | { 71 | _Looping = value; 72 | Finished = false; 73 | } 74 | } 75 | public bool Finished = false; 76 | 77 | /// Keeps track of when to change frame. Resets on frame change. 78 | public float CurrentTime; 79 | private float CurrentFrameDelay; 80 | 81 | /// Default Frame Delay for animated images that don't define it. 82 | public static readonly int DEFAULT_FRAME_DELAY = 100; 83 | 84 | public AnimatedImage(AnimatedImageData data) 85 | { 86 | Data = data; 87 | 88 | Sprite = new Sprite(data.Frames[0]); 89 | AddChild(Sprite); 90 | 91 | CurrentTime = 0; 92 | CurrentFrameDelay = data.FrameDelays[0]; 93 | } 94 | 95 | public bool Update(float dt) 96 | { 97 | if (!Playing) 98 | return false; 99 | 100 | CurrentTime += dt; 101 | 102 | while (CurrentTime > CurrentFrameDelay) 103 | { 104 | if (Looping || CurrentFrame < TotalFrames - 1) 105 | { 106 | if (CurrentFrame == TotalFrames - 1) 107 | _ = SetFrame(0); 108 | else 109 | NextFrame(); 110 | } 111 | else 112 | Finished = true; 113 | 114 | if (CurrentFrameDelay == 0) 115 | CurrentTime = 0; 116 | else 117 | CurrentTime -= CurrentFrameDelay; 118 | 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | 125 | public bool SetFrame(int number) 126 | { 127 | if (number >= TotalFrames) 128 | return false; 129 | 130 | if (!Data.FullyLoaded && Data.Frames[number] == null) 131 | return false; // Hang if next frame hasn't loaded yet 132 | 133 | CurrentFrame = number; 134 | Finished = CurrentFrame == TotalFrames - 1; 135 | 136 | Sprite.Texture = Data.Frames[CurrentFrame]; 137 | CurrentFrameDelay = Data.FrameDelays[CurrentFrame]; 138 | 139 | return true; 140 | } 141 | 142 | public void NextFrame() 143 | { 144 | _ = SetFrame(Math.Min(CurrentFrame + 1, TotalFrames)); 145 | } 146 | 147 | public void PrevFrame() 148 | { 149 | _ = SetFrame(Math.Max(CurrentFrame - 1, 0)); 150 | } 151 | 152 | public void Stop() 153 | { 154 | Playing = false; 155 | } 156 | 157 | public void Play() 158 | { 159 | Playing = true; 160 | } 161 | 162 | public void GotoAndPlay(int number) 163 | { 164 | _ = SetFrame(number); 165 | Play(); 166 | } 167 | 168 | public void GotoAndStop(int number) 169 | { 170 | _ = SetFrame(number); 171 | Stop(); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /vimage_settings/Source/CommandsList.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /vimage.Common/Actions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace vimage.Common 4 | { 5 | public enum Action 6 | { 7 | None, 8 | 9 | Drag, 10 | Close, 11 | OpenContextMenu, 12 | PrevImage, 13 | NextImage, 14 | 15 | RotateClockwise, 16 | RotateAntiClockwise, 17 | Flip, 18 | FitToMonitorHeight, 19 | FitToMonitorWidth, 20 | FitToMonitorAuto, 21 | FitToMonitorAlt, 22 | ZoomIn, 23 | ZoomOut, 24 | ZoomFaster, 25 | ZoomAlt, 26 | DragLimitToMonitorBounds, 27 | 28 | ToggleSmoothing, 29 | ToggleBackground, 30 | ToggleLock, 31 | ToggleAlwaysOnTop, 32 | ToggleClickThroughAble, 33 | ToggleTitleBar, 34 | 35 | PauseAnimation, 36 | PrevFrame, 37 | NextFrame, 38 | 39 | OpenSettings, 40 | ResetImage, 41 | OpenAtLocation, 42 | Delete, 43 | Copy, 44 | CopyAsImage, 45 | OpenDuplicateImage, 46 | OpenFullDuplicateImage, 47 | RandomImage, 48 | 49 | MoveLeft, 50 | MoveRight, 51 | MoveUp, 52 | MoveDown, 53 | 54 | TransparencyToggle, 55 | TransparencyInc, 56 | TransparencyDec, 57 | Crop, 58 | UndoCrop, 59 | ExitAll, 60 | RerenderSVG, 61 | 62 | VisitWebsite, 63 | 64 | SortName, 65 | SortDate, 66 | SortDateModified, 67 | SortDateCreated, 68 | SortSize, 69 | SortAscending, 70 | SortDescending, 71 | 72 | Custom, 73 | } 74 | 75 | public static partial class Actions 76 | { 77 | public static List Names = 78 | [ 79 | "", 80 | "DRAG", 81 | "CLOSE", 82 | "OPEN CONTEXT MENU", 83 | "PREV IMAGE", 84 | "NEXT IMAGE", 85 | "ROTATE CLOCKWISE", 86 | "ROTATE ANTICLOCKWISE", 87 | "FLIP", 88 | "FIT TO HEIGHT", 89 | "FIT TO WIDTH", 90 | "FIT TO AUTO", 91 | "FIT TO ALT", 92 | "ZOOM IN", 93 | "ZOOM OUT", 94 | "ZOOM FASTER", 95 | "ZOOM ALT", 96 | "DRAG LIMIT TO MONITOR BOUNDS", 97 | "TOGGLE SMOOTHING", 98 | "TOGGLE BACKGROUND", 99 | "TOGGLE LOCK", 100 | "ALWAYS ON TOP", 101 | "CLICK-THROUGH_ABLE", 102 | "TOGGLE TITLE BAR", 103 | "TOGGLE ANIMATION", 104 | "PREV FRAME", 105 | "NEXT FRAME", 106 | "OPEN SETTINGS", 107 | "RESET IMAGE", 108 | "OPEN FILE LOCATION", 109 | "DELETE", 110 | "COPY", 111 | "COPY AS IMAGE", 112 | "OPEN DUPLICATE", 113 | "OPEN DUPLICATE FULL", 114 | "RANDOM IMAGE", 115 | "MOVE LEFT", 116 | "MOVE RIGHT", 117 | "MOVE UP", 118 | "MOVE DOWN", 119 | "TOGGLE IMAGE TRANSPARENCY", 120 | "TRANSPARENCY INC", 121 | "TRANSPARENCY DEC", 122 | "CROP", 123 | "UNDO CROP", 124 | "EXIT ALL INSTANCES", 125 | "RERENDER SVG", 126 | "VISIT WEBSITE", 127 | "SORT NAME", 128 | "SORT DATE", 129 | "SORT DATE MODIFIED", 130 | "SORT DATE CREATED", 131 | "SORT SIZE", 132 | "SORT ASCENDING", 133 | "SORT DESCENDING", 134 | ]; 135 | 136 | public static string ToNameString(this Action action) 137 | { 138 | return Names[(int)action]; 139 | } 140 | 141 | public static Action StringToAction(string action) 142 | { 143 | return (Action)Names.IndexOf(action); 144 | } 145 | 146 | /// List of actions that can be used in the Context Menu. 147 | public static readonly Action[] MenuActions = 148 | [ 149 | Action.Close, 150 | Action.NextImage, 151 | Action.PrevImage, 152 | Action.RotateClockwise, 153 | Action.RotateAntiClockwise, 154 | Action.Flip, 155 | Action.FitToMonitorHeight, 156 | Action.FitToMonitorWidth, 157 | Action.FitToMonitorAuto, 158 | Action.ResetImage, 159 | Action.ToggleSmoothing, 160 | Action.ToggleBackground, 161 | Action.TransparencyToggle, 162 | Action.ToggleLock, 163 | Action.ToggleAlwaysOnTop, 164 | Action.ToggleClickThroughAble, 165 | Action.ToggleTitleBar, 166 | Action.OpenAtLocation, 167 | Action.Delete, 168 | Action.Copy, 169 | Action.CopyAsImage, 170 | Action.OpenDuplicateImage, 171 | Action.OpenFullDuplicateImage, 172 | Action.RandomImage, 173 | Action.UndoCrop, 174 | Action.ExitAll, 175 | Action.PauseAnimation, 176 | Action.NextFrame, 177 | Action.PrevFrame, 178 | Action.OpenSettings, 179 | Action.VisitWebsite, 180 | Action.SortName, 181 | Action.SortDate, 182 | Action.SortDateModified, 183 | Action.SortDateCreated, 184 | Action.SortSize, 185 | Action.SortAscending, 186 | Action.SortDescending, 187 | ]; 188 | 189 | /// 190 | /// Split exe and arguments by the first space (regex to exclude the spaces within the quotes of the exe's path) 191 | /// 192 | [GeneratedRegex("(?<=^[^\"]*(?:\"[^\"]*\"[^\"]*)*) (?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")] 193 | public static partial Regex CustomActionSplitRegex(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /vimage_settings/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /vimage_settings/Source/ControlItem.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using vimage.Common; 6 | 7 | namespace vimage_settings 8 | { 9 | /// 10 | /// Interaction logic for ControlItem.xaml 11 | /// 12 | public partial class ControlItem : UserControl 13 | { 14 | public List Controls = []; 15 | private bool CanRecordMouseButton = false; 16 | private readonly List KeysHeld = []; 17 | 18 | public ControlItem() 19 | { 20 | InitializeComponent(); 21 | } 22 | 23 | public ControlItem(string name, List controls) 24 | { 25 | InitializeComponent(); 26 | 27 | ControlName.Content = name; 28 | Controls = controls; 29 | UpdateBindings(); 30 | } 31 | 32 | private void Clear_Click(object sender, RoutedEventArgs e) 33 | { 34 | Controls.Clear(); 35 | ControlSetting.Text = ""; 36 | } 37 | 38 | public void UpdateBindings() 39 | { 40 | ControlSetting.Text = Config.ControlsToString(Controls); 41 | } 42 | 43 | private void OnKeyDown(object sender, KeyEventArgs e) 44 | { 45 | e.Handled = true; 46 | 47 | int key = ConvertWindowsKey(e.Key == Key.System ? e.SystemKey : e.Key); 48 | if (KeysHeld.Count == 0 || KeysHeld[^1] != key) 49 | { 50 | KeysHeld.Add(key); 51 | if (KeysHeld.Count > 2) 52 | KeysHeld.RemoveAt(0); 53 | } 54 | } 55 | 56 | private void OnKeyUp(object sender, KeyEventArgs e) 57 | { 58 | e.Handled = true; 59 | 60 | int key = ConvertWindowsKey(e.Key == Key.System ? e.SystemKey : e.Key); 61 | _ = KeysHeld.Remove(key); 62 | 63 | RecordControl(key); 64 | } 65 | 66 | private void ControlSetting_MouseUp(object sender, MouseButtonEventArgs e) 67 | { 68 | if (!ControlSetting.IsFocused) 69 | return; 70 | if (!CanRecordMouseButton) 71 | { 72 | CanRecordMouseButton = true; 73 | return; 74 | } 75 | e.Handled = true; 76 | 77 | // Record Mouse Button Press 78 | int button = e.ChangedButton switch 79 | { 80 | MouseButton.Left => 0, 81 | MouseButton.Right => 1, 82 | MouseButton.Middle => 2, 83 | MouseButton.XButton1 => 3, 84 | MouseButton.XButton2 => 4, 85 | _ => -1, 86 | }; 87 | RecordControl(button + Config.MouseCodeOffset); 88 | ControlSetting.ReleaseMouseCapture(); 89 | } 90 | 91 | private void ControlSetting_MouseWheel(object sender, MouseWheelEventArgs e) 92 | { 93 | if (!ControlSetting.IsFocused) 94 | return; 95 | e.Handled = true; 96 | 97 | // Record Mouse Wheel Direction 98 | int bind = -1; 99 | if (e.Delta > 0) 100 | bind = Config.MOUSE_SCROLL_UP; 101 | else if (e.Delta < 0) 102 | bind = Config.MOUSE_SCROLL_DOWN; 103 | 104 | RecordControl(bind); 105 | } 106 | 107 | private static int ConvertWindowsKey(Key keyCode) 108 | { 109 | var key = keyCode.ToString().ToUpper(); 110 | if (key is null) 111 | return -1; 112 | 113 | // Record Key Press 114 | if ( 115 | key.Equals("SCROLL") 116 | || key.Equals("NUMLOCK") 117 | || key.Equals("CAPITAL") 118 | || key.Equals("LWIN") 119 | || key.Equals("RWIN") 120 | ) 121 | return -1; 122 | 123 | // fix up some weird names KeyEventArgs gives 124 | key = key switch 125 | { 126 | "OEMOPENBRACKETS" => "[", 127 | "OEM3" => "`", 128 | "OEM6" => "]", 129 | "OEM5" => "\\", 130 | "OEM1" => ";", 131 | "OEM7" => "'", 132 | "OEMMINUS" => "MINUS", 133 | "OEMPLUS" => "PLUS", 134 | _ => key, 135 | }; 136 | 137 | // fix number keys (remove D from D#) 138 | if (key.Length == 2 && key[0] == 'D') 139 | key = key[1..]; 140 | 141 | return (int)Config.StringToKey(key); 142 | } 143 | 144 | private void RecordControl(int bind, bool canBeKeyCombo = true) 145 | { 146 | if (bind == -1) 147 | return; 148 | int i = Controls.IndexOf(bind); 149 | if (!(i == -1 || (i > 1 && Controls[i - 2] == -2))) 150 | return; 151 | 152 | if (canBeKeyCombo) 153 | { 154 | if (KeysHeld.Count > 0 && KeysHeld[^1] != bind) 155 | { 156 | // Key Combo? (eg: CTRL+C) 157 | int c = KeysHeld[^1]; 158 | 159 | if (i != -1 && Controls.IndexOf(c) != -1) 160 | return; 161 | Controls.Add(-2); 162 | Controls.Add(c); 163 | } 164 | else if (i != -1) 165 | return; 166 | } 167 | Controls.Add(bind); 168 | UpdateBindings(); 169 | } 170 | 171 | private void ControlSetting_GotFocus(object sender, RoutedEventArgs e) 172 | { 173 | Window window = Window.GetWindow(this); 174 | if (window == null) 175 | return; 176 | window.PreviewKeyDown += OnKeyDown; 177 | window.PreviewKeyUp += OnKeyUp; 178 | ControlSetting.PreviewMouseUp += ControlSetting_MouseUp; 179 | ControlSetting.PreviewMouseWheel += ControlSetting_MouseWheel; 180 | CanRecordMouseButton = false; 181 | } 182 | 183 | private void ControlSetting_LostFocus(object sender, RoutedEventArgs e) 184 | { 185 | Window window = Window.GetWindow(this); 186 | if (window == null) 187 | return; 188 | window.PreviewKeyDown -= OnKeyDown; 189 | window.PreviewKeyUp -= OnKeyUp; 190 | ControlSetting.PreviewMouseUp -= ControlSetting_MouseUp; 191 | ControlSetting.PreviewMouseWheel -= ControlSetting_MouseWheel; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /vimage/Display/DisplayObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using SFML.Graphics; 3 | using SFML.System; 4 | 5 | namespace vimage.Display 6 | { 7 | internal class DisplayObject : Transformable, Drawable 8 | { 9 | private readonly List Children = []; 10 | private int DrawListIndex = 0; 11 | public DisplayObject? Parent = null; 12 | 13 | public bool Visible = true; 14 | 15 | public TextureInfo Texture; 16 | 17 | public DisplayObject() 18 | { 19 | Texture = new TextureInfo(this); 20 | } 21 | 22 | public int NumChildren 23 | { 24 | get { return Children.Count; } 25 | } 26 | 27 | public void AddChild(Transformable child) 28 | { 29 | Children.Add(child); 30 | if (child is DisplayObject displayObject) 31 | { 32 | displayObject.Parent = this; 33 | displayObject.OnAdded(); 34 | } 35 | } 36 | 37 | public void AddChildAt(Transformable child, int index) 38 | { 39 | Children.Insert(index, child); 40 | if (child is DisplayObject displayObject) 41 | { 42 | displayObject.Parent = this; 43 | displayObject.OnAdded(); 44 | } 45 | } 46 | 47 | public void RemoveChild(Transformable child) 48 | { 49 | for (int i = 0; i < Children.Count; i++) 50 | { 51 | if (!Children[i].Equals(child)) 52 | continue; 53 | 54 | if (child is DisplayObject displayObject) 55 | { 56 | displayObject.OnRemoved(); 57 | displayObject.Parent = null; 58 | } 59 | 60 | Children.RemoveAt(i); 61 | if (i <= DrawListIndex) 62 | DrawListIndex--; 63 | break; 64 | } 65 | } 66 | 67 | public void RemoveChildAt(int index) 68 | { 69 | RemoveChild(GetChildAt(index)); 70 | } 71 | 72 | public void Clear() 73 | { 74 | while (NumChildren > 0) 75 | RemoveChildAt(0); 76 | 77 | Children.Clear(); 78 | DrawListIndex = 0; 79 | } 80 | 81 | public Transformable GetChildAt(int i) 82 | { 83 | return Children[i]; 84 | } 85 | 86 | public virtual void OnAdded() { } 87 | 88 | public virtual void OnRemoved() { } 89 | 90 | public void Draw(RenderTarget Target, RenderStates states) 91 | { 92 | states.Transform *= Transform; 93 | for (DrawListIndex = 0; DrawListIndex < Children.Count; DrawListIndex++) 94 | { 95 | if (Children[DrawListIndex] is DisplayObject displayObject) 96 | { 97 | if (displayObject.Visible) 98 | displayObject.Draw(Target, states); 99 | } 100 | else if (Children[DrawListIndex] is Drawable drawable) 101 | drawable.Draw(Target, states); 102 | } 103 | } 104 | 105 | public float X 106 | { 107 | get { return Position.X; } 108 | set { Position = new Vector2f(value, Position.Y); } 109 | } 110 | public float Y 111 | { 112 | get { return Position.Y; } 113 | set { Position = new Vector2f(Position.X, value); } 114 | } 115 | 116 | public void SetPosition(float x, float y) 117 | { 118 | Position = new Vector2f(x, y); 119 | } 120 | 121 | public void SetPosition(Vector2f pos) 122 | { 123 | Position = pos; 124 | } 125 | 126 | public void Move(float offsetX, float offsetY) 127 | { 128 | Position = new Vector2f(X + offsetX, Y + offsetY); 129 | } 130 | 131 | public void Move(Vector2f offset) 132 | { 133 | Position = new Vector2f(X + offset.X, Y + offset.Y); 134 | } 135 | 136 | public float ScaleX 137 | { 138 | get { return Scale.X; } 139 | set { Scale = new Vector2f(value, Scale.Y); } 140 | } 141 | public float ScaleY 142 | { 143 | get { return Scale.Y; } 144 | set { Scale = new Vector2f(Scale.X, value); } 145 | } 146 | 147 | public void SetScale(float scaleX, float scaleY) 148 | { 149 | Scale = new Vector2f(scaleX, scaleY); 150 | } 151 | 152 | public void SetScale(float scale) 153 | { 154 | Scale = new Vector2f(scale, scale); 155 | } 156 | 157 | public void SetScale(Vector2f scale) 158 | { 159 | Scale = scale; 160 | } 161 | 162 | public void Rotate(float amount) 163 | { 164 | if (Rotation + amount > 180) 165 | Rotation = Rotation + amount - 360; 166 | else if (Rotation + amount < -180) 167 | Rotation = Rotation + amount + 360; 168 | else 169 | Rotation += amount; 170 | } 171 | 172 | public Color _Color = Color.White; 173 | public Color Color 174 | { 175 | get { return _Color; } 176 | set 177 | { 178 | _Color = value; 179 | for (int i = 0; i < Children.Count; i++) 180 | { 181 | if (Children[i] is Sprite spite) 182 | spite.Color = _Color; 183 | else if (Children[i] is DisplayObject displayObject) 184 | displayObject.Color = _Color; 185 | } 186 | } 187 | } 188 | } 189 | 190 | internal class TextureInfo(DisplayObject obj) 191 | { 192 | private readonly DisplayObject Obj = obj; 193 | public Vector2u Size = new(); 194 | 195 | private bool _Smooth = true; 196 | public bool Smooth 197 | { 198 | get { return _Smooth; } 199 | set 200 | { 201 | _Smooth = value; 202 | for (int i = 0; i < Obj.NumChildren; i++) 203 | { 204 | var child = Obj.GetChildAt(i); 205 | if (child is Sprite sprite) 206 | sprite.Texture.Smooth = _Smooth; 207 | else if (child is DisplayObject displayObject) 208 | displayObject.Texture.Smooth = _Smooth; 209 | } 210 | } 211 | } 212 | 213 | private bool _Mipmap = true; 214 | public bool Mipmap 215 | { 216 | get { return _Mipmap; } 217 | set 218 | { 219 | _Mipmap = value; 220 | if (!_Mipmap) 221 | return; 222 | 223 | for (int i = 0; i < Obj.NumChildren; i++) 224 | { 225 | var child = Obj.GetChildAt(i); 226 | if (child is Sprite sprite) 227 | sprite.Texture.GenerateMipmap(); 228 | else if (child is DisplayObject displayObject) 229 | displayObject.Texture.Mipmap = true; 230 | } 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /vimage_settings/Source/ContextMenu.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | 5 | namespace vimage_settings 6 | { 7 | /// 8 | /// Interaction logic for ContextMenu.xaml 9 | /// 10 | public partial class ContextMenu : UserControl 11 | { 12 | public List Items = []; 13 | public ContextMenuItem? CurrentItemSelection = null; 14 | 15 | public ContextMenu() 16 | { 17 | InitializeComponent(); 18 | DataContext = App.vimageConfig; 19 | 20 | if (App.vimageConfig == null) 21 | return; 22 | LoadItems( 23 | App.vimageConfig.ContextMenu, 24 | ContextMenuItems_General, 25 | ContextMenuItems_GeneralCanvas, 26 | ContextMenuItems_GeneralScroll 27 | ); 28 | LoadItems( 29 | App.vimageConfig.ContextMenu_Animation, 30 | ContextMenuItems_Animation, 31 | ContextMenuItems_AnimationCanvas, 32 | ContextMenuItems_AnimationScroll 33 | ); 34 | } 35 | 36 | public void Save() 37 | { 38 | App.vimageConfig.ContextMenu.Clear(); 39 | App.vimageConfig.ContextMenu_Animation.Clear(); 40 | 41 | SaveContextMenu(App.vimageConfig.ContextMenu, ContextMenuItems_General); 42 | SaveContextMenu(App.vimageConfig.ContextMenu_Animation, ContextMenuItems_Animation); 43 | } 44 | 45 | private static void SaveContextMenu(List contextMenu, Panel panel) 46 | { 47 | int currentSubLevel = 0; 48 | List currentMenu = contextMenu; 49 | List? prevMenu = null; 50 | for (int i = 0; i < panel.Children.Count; i++) 51 | { 52 | if (panel.Children[i] is not ContextMenuItem item) 53 | continue; 54 | if ( 55 | i < panel.Children.Count - 1 56 | && ((ContextMenuItem)panel.Children[i + 1]).Indent > item.Indent 57 | ) 58 | { 59 | // Submenu 60 | currentMenu.Add(item.ItemName.Text); 61 | } 62 | else if (item.Indent != 0) 63 | { 64 | // Subitem 65 | if (item.Indent > currentSubLevel) 66 | { 67 | // First subitem 68 | currentSubLevel = item.Indent; 69 | currentMenu.Add(new List()); 70 | prevMenu = currentMenu; 71 | var subMenu = currentMenu[^1]; 72 | if (subMenu is List subMenuList) 73 | currentMenu = subMenuList; 74 | } 75 | else if (item.Indent < currentSubLevel && prevMenu is not null) 76 | { 77 | currentSubLevel = item.Indent; 78 | currentMenu = prevMenu; 79 | } 80 | 81 | currentMenu.Add( 82 | new vimage.Common.ContextMenuItem 83 | { 84 | name = item.ItemName.Text, 85 | func = item.ItemFunction.Text.Trim(), 86 | } 87 | ); 88 | } 89 | else 90 | { 91 | // Item 92 | if (currentSubLevel != 0) 93 | { 94 | currentSubLevel = 0; 95 | currentMenu = contextMenu; 96 | } 97 | 98 | currentMenu.Add( 99 | new vimage.Common.ContextMenuItem 100 | { 101 | name = item.ItemName.Text, 102 | func = item.ItemFunction.Text.Trim(), 103 | } 104 | ); 105 | } 106 | } 107 | } 108 | 109 | private void LoadItems( 110 | List items, 111 | Panel panel, 112 | ContextMenuEditorCanvas canvas, 113 | ScrollViewer scroll, 114 | int indent = 0 115 | ) 116 | { 117 | for (int i = 0; i < items.Count; i++) 118 | { 119 | if (items[i] is List list) 120 | { 121 | LoadItems(list, panel, canvas, scroll, indent + 1); 122 | continue; 123 | } 124 | var name = ""; 125 | object func = ""; 126 | if (items[i] is vimage.Common.ContextMenuItem cmi) 127 | { 128 | name = cmi.name; 129 | func = cmi.func; 130 | } 131 | else if (items[i] is string str) 132 | { 133 | name = str; 134 | } 135 | 136 | var item = new ContextMenuItem(name, func, this, panel, canvas, scroll, indent); 137 | _ = panel.Children.Add(item); 138 | Items.Add(item); 139 | 140 | Canvas.SetTop(item, item.MinHeight * (panel.Children.Count - 1)); 141 | } 142 | } 143 | 144 | public void UpdateCustomActions() 145 | { 146 | for (int i = 0; i < Items.Count; i++) 147 | Items[i].UpdateCustomActions(); 148 | } 149 | 150 | private void Add_Click(object sender, RoutedEventArgs e) 151 | { 152 | var panel = 153 | Tabs.SelectedIndex == 0 ? ContextMenuItems_General : ContextMenuItems_Animation; 154 | var canvas = 155 | Tabs.SelectedIndex == 0 156 | ? ContextMenuItems_GeneralCanvas 157 | : ContextMenuItems_AnimationCanvas; 158 | var scroll = 159 | Tabs.SelectedIndex == 0 160 | ? ContextMenuItems_GeneralScroll 161 | : ContextMenuItems_AnimationScroll; 162 | 163 | var item = new ContextMenuItem( 164 | "", 165 | "", 166 | this, 167 | panel, 168 | canvas, 169 | scroll, 170 | CurrentItemSelection == null ? 0 : CurrentItemSelection.Indent 171 | ); 172 | if (CurrentItemSelection == null) 173 | { 174 | _ = panel.Children.Add(item); 175 | Items.Add(item); 176 | 177 | Canvas.SetTop(item, item.MinHeight * (panel.Children.Count - 1)); 178 | } 179 | else 180 | { 181 | panel.Children.Insert(panel.Children.IndexOf(CurrentItemSelection) + 1, item); 182 | Items.Insert(Items.IndexOf(CurrentItemSelection) + 1, item); 183 | } 184 | 185 | CurrentItemSelection?.UnselectItem(); 186 | item.SelectItem(true); 187 | } 188 | 189 | private void Default_Click(object sender, RoutedEventArgs e) 190 | { 191 | if (App.vimageConfig == null) 192 | return; 193 | 194 | ContextMenuItems_General.Children.Clear(); 195 | ContextMenuItems_Animation.Children.Clear(); 196 | Items.Clear(); 197 | 198 | App.vimageConfig.SetDefaultContextMenu(); 199 | 200 | LoadItems( 201 | App.vimageConfig.ContextMenu, 202 | ContextMenuItems_General, 203 | ContextMenuItems_GeneralCanvas, 204 | ContextMenuItems_GeneralScroll 205 | ); 206 | LoadItems( 207 | App.vimageConfig.ContextMenu_Animation, 208 | ContextMenuItems_Animation, 209 | ContextMenuItems_AnimationCanvas, 210 | ContextMenuItems_AnimationScroll 211 | ); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /vimage/Display/DWM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using SFML.Graphics; 4 | using SFML.System; 5 | 6 | namespace vimage.Display 7 | { 8 | /// 9 | /// Desktop Window Manager 10 | /// 11 | internal partial class DWM 12 | { 13 | // Make Window Background Transparent 14 | [DllImport("dwmapi.dll")] 15 | public static extern void DwmEnableBlurBehindWindow( 16 | IntPtr hwnd, 17 | ref DWM_BLURBEHIND blurBehind 18 | ); 19 | 20 | [LibraryImport("gdi32.dll")] 21 | public static partial IntPtr CreateRectRgn( 22 | int nLeftRect, 23 | int nTopRect, 24 | int nRightRect, 25 | int nBottomRect 26 | ); 27 | 28 | // Show/Hide in Taskbar 29 | [LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = true)] 30 | private static partial nint GetWindowLongPtr(nint hWnd, int nIndex); 31 | 32 | [LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)] 33 | private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong); 34 | 35 | private const int GWL_EX_STYLE = -20; 36 | private const uint WS_EX_APPWINDOW = 0x00040000, 37 | WS_EX_TOOLWINDOW = 0x00000080; 38 | public static bool TaskbarIconVisible = true; 39 | 40 | [DllImport("user32.dll")] 41 | static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); 42 | 43 | private const int SW_HIDE = 0x00; 44 | private const int SW_SHOW = 0x05; 45 | 46 | public static void TaskBarIconSetVisible(IntPtr hWnd, bool visible) 47 | { 48 | TaskbarIconVisible = visible; 49 | bool isOlderThanWindows8 = Environment.OSVersion.Version < new Version(6, 2); 50 | 51 | if (TaskbarIconVisible) 52 | { 53 | _ = SetWindowLongPtr( 54 | hWnd, 55 | GWL_EX_STYLE, 56 | new nint( 57 | GetWindowLongPtr(hWnd, GWL_EX_STYLE).ToInt64() 58 | & ~WS_EX_TOOLWINDOW 59 | & ~WS_EX_APPWINDOW 60 | ) 61 | ); 62 | } 63 | else 64 | { 65 | if (isOlderThanWindows8) 66 | _ = ShowWindow(hWnd, SW_HIDE); // Makes hiding possible in Windows Vista/7 67 | _ = SetWindowLongPtr( 68 | hWnd, 69 | GWL_EX_STYLE, 70 | new nint( 71 | (GetWindowLongPtr(hWnd, GWL_EX_STYLE).ToInt64() | WS_EX_TOOLWINDOW) 72 | & ~WS_EX_APPWINDOW 73 | ) 74 | ); // Hiding TaskBar-Icon 75 | if (isOlderThanWindows8) 76 | _ = ShowWindow(hWnd, SW_SHOW); // Makes hiding possible in Windows Vista/7 77 | } 78 | } 79 | 80 | // Toggle Borderless / Title bar 81 | private const int GWL_STYLE = -16; 82 | public const uint WS_CAPTION = 0x00C00000, 83 | WS_SYSMENU = 0x00080000, 84 | WS_POPUP = 0x80000000; 85 | private const uint SWP_FRAMECHANGED = 0x0020; 86 | 87 | public static void TitleBarSetVisible(RenderWindow window, bool visible) 88 | { 89 | _ = visible 90 | ? SetWindowLongPtr( 91 | window.SystemHandle, 92 | GWL_STYLE, 93 | new nint( 94 | GetWindowLongPtr(window.SystemHandle, GWL_STYLE).ToInt64() 95 | | WS_CAPTION 96 | | WS_SYSMENU 97 | ) 98 | ) 99 | : SetWindowLongPtr( 100 | window.SystemHandle, 101 | GWL_STYLE, 102 | new nint( 103 | GetWindowLongPtr(window.SystemHandle, GWL_STYLE).ToInt64() & ~WS_CAPTION 104 | ) 105 | ); 106 | 107 | _ = SetWindowPos( 108 | window.SystemHandle, 109 | new IntPtr(0), 110 | window.Position.X, 111 | window.Position.Y, 112 | (int)window.Size.X, 113 | (int)window.Size.Y, 114 | SWP_FRAMECHANGED 115 | ); 116 | } 117 | 118 | public static void PreventExlusiveFullscreen(RenderWindow window) 119 | { 120 | _ = SetWindowLongPtr( 121 | window.SystemHandle, 122 | GWL_STYLE, 123 | new nint(GetWindowLongPtr(window.SystemHandle, GWL_STYLE).ToInt64() & ~WS_POPUP) 124 | ); 125 | } 126 | 127 | // Window/Client Rect/Pos - used for Title Bar support 128 | [StructLayout(LayoutKind.Sequential)] 129 | public struct RECT 130 | { 131 | public int Left; 132 | public int Top; 133 | public int Right; 134 | public int Bottom; 135 | } 136 | 137 | public struct Point 138 | { 139 | public int x; 140 | public int y; 141 | } 142 | 143 | [LibraryImport("user32.dll")] 144 | [return: MarshalAs(UnmanagedType.Bool)] 145 | private static partial bool GetClientRect(IntPtr hWnd, out RECT lpRect); 146 | 147 | public static RECT GetClientRect(IntPtr hWnd) 148 | { 149 | _ = GetClientRect(hWnd, out RECT result); 150 | return result; 151 | } 152 | 153 | [LibraryImport("user32.dll")] 154 | [return: MarshalAs(UnmanagedType.Bool)] 155 | private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect); 156 | 157 | public static RECT GetWindowRect(IntPtr hWnd) 158 | { 159 | _ = GetWindowRect(hWnd, out RECT result); 160 | return result; 161 | } 162 | 163 | [LibraryImport("user32.dll")] 164 | [return: MarshalAs(UnmanagedType.Bool)] 165 | private static partial bool ClientToScreen(IntPtr hWnd, ref Point lpPoint); 166 | 167 | public static Vector2i ClientToScreen(IntPtr hWnd, int x, int y) 168 | { 169 | var result = new Point() { x = x, y = y }; 170 | _ = ClientToScreen(hWnd, ref result); 171 | return new Vector2i(result.x, result.y); 172 | } 173 | 174 | public static Vector2i GetWindowClientPos(IntPtr hWnd) 175 | { 176 | var rect = GetClientRect(hWnd); 177 | return ClientToScreen(hWnd, rect.Left, rect.Top); 178 | } 179 | 180 | public static Vector2i GetTitleBarDifference(IntPtr hWnd) 181 | { 182 | var rect = GetWindowRect(hWnd); 183 | var cp = GetWindowClientPos(hWnd); 184 | return new Vector2i(cp.X - rect.Left, cp.Y - rect.Top); 185 | } 186 | 187 | // Make Window Always On Top 188 | [LibraryImport("user32.dll")] 189 | [return: MarshalAs(UnmanagedType.Bool)] 190 | private static partial bool SetWindowPos( 191 | IntPtr hWnd, 192 | IntPtr hWndInsertAfter, 193 | int X, 194 | int Y, 195 | int cx, 196 | int cy, 197 | uint uFlags 198 | ); 199 | 200 | private const uint SWP_NOSIZE = 0x0001; 201 | private const uint SWP_NOMOVE = 0x0002; 202 | private const uint TOPMOST_FLAGS = SWP_NOMOVE | SWP_NOSIZE; 203 | 204 | public static void SetAlwaysOnTop(IntPtr hWnd, bool alwaysOnTop = true) 205 | { 206 | if (alwaysOnTop) 207 | { 208 | _ = SetWindowPos(hWnd, new IntPtr(-1), 0, 0, 0, 0, TOPMOST_FLAGS); 209 | } 210 | else 211 | { 212 | _ = SetWindowPos(hWnd, new IntPtr(1), 0, 0, 0, 0, TOPMOST_FLAGS); 213 | _ = SetWindowPos(hWnd, new IntPtr(0), 0, 0, 0, 0, TOPMOST_FLAGS); 214 | } 215 | } 216 | 217 | // Make Window Click-through-able 218 | private const uint WS_EX_TRANSPARENT = 0x00000020, 219 | WS_EX_LAYERED = 0x00080000; 220 | 221 | public static void SetClickThroughAble(IntPtr hWnd, bool canClickThrough = true) 222 | { 223 | _ = SetWindowLongPtr( 224 | hWnd, 225 | GWL_EX_STYLE, 226 | canClickThrough 227 | ? new nint( 228 | (GetWindowLongPtr(hWnd, GWL_EX_STYLE).ToInt64()) 229 | | WS_EX_LAYERED 230 | | WS_EX_TRANSPARENT 231 | ) 232 | : new nint( 233 | (GetWindowLongPtr(hWnd, GWL_EX_STYLE).ToInt64()) 234 | & ~WS_EX_LAYERED 235 | & ~WS_EX_TRANSPARENT 236 | ) 237 | ); 238 | } 239 | } 240 | 241 | [Flags] 242 | internal enum DWM_BB 243 | { 244 | Enable = 1, 245 | BlurRegion = 2, 246 | TransitionOnMaximized = 4, 247 | } 248 | 249 | [StructLayout(LayoutKind.Sequential)] 250 | internal struct DWM_BLURBEHIND 251 | { 252 | public DWM_BB dwFlags; 253 | public bool fEnable; 254 | public IntPtr hRgnBlur; 255 | public bool fTransitionOnMaximized; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /vimage_settings/Source/General.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 69 | 72 | 73 | 74 | 83 | 84 | 92 | 93 | 96 | 97 | 100 | 101 | 104 | 105 | 108 | 109 | 112 | 113 | 116 | 117 | 120 | 121 | 124 | 125 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /vimage/Utils/ImageViewerUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using SFML.Graphics; 4 | using SFML.System; 5 | 6 | namespace vimage.Utils 7 | { 8 | internal class ImageViewerUtils 9 | { 10 | /// Returns the working area IntRect of the monitor the position is located on. 11 | public static IntRect GetCurrentWorkingArea(Vector2i pos) 12 | { 13 | foreach (var screen in System.Windows.Forms.Screen.AllScreens) 14 | { 15 | if ( 16 | pos.X < screen.Bounds.X 17 | || pos.Y < screen.Bounds.Y 18 | || pos.X > screen.Bounds.X + screen.Bounds.Width 19 | || pos.Y > screen.Bounds.Y + screen.Bounds.Height 20 | ) 21 | continue; 22 | 23 | return new IntRect( 24 | screen.WorkingArea.X, 25 | screen.WorkingArea.Y, 26 | screen.WorkingArea.Width, 27 | screen.WorkingArea.Height 28 | ); 29 | } 30 | var firstScreen = System.Windows.Forms.Screen.AllScreens.ElementAt(0); 31 | 32 | return new IntRect( 33 | firstScreen.WorkingArea.X, 34 | firstScreen.WorkingArea.Y, 35 | firstScreen.WorkingArea.Width, 36 | firstScreen.WorkingArea.Height 37 | ); 38 | } 39 | 40 | /// Returns the bounds IntRect of the monitor the position is located on. 41 | public static IntRect GetCurrentBounds(Vector2i pos, bool returnBackupScreen = true) 42 | { 43 | var backupScreen = System.Windows.Forms.Screen.AllScreens.ElementAt(0); 44 | 45 | foreach (var screen in System.Windows.Forms.Screen.AllScreens) 46 | { 47 | if ( 48 | pos.X < screen.Bounds.X 49 | || pos.Y < screen.Bounds.Y 50 | || pos.X > screen.Bounds.X + screen.Bounds.Width 51 | || pos.Y > screen.Bounds.Y + screen.Bounds.Height 52 | ) 53 | { 54 | if ( 55 | (pos.X > screen.Bounds.X && screen.Bounds.X > backupScreen.Bounds.X) 56 | || (pos.X < screen.Bounds.X && screen.Bounds.X < backupScreen.Bounds.X) 57 | || (pos.Y > screen.Bounds.Y && screen.Bounds.Y > backupScreen.Bounds.Y) 58 | || (pos.Y < screen.Bounds.Y && screen.Bounds.Y < backupScreen.Bounds.Y) 59 | ) 60 | backupScreen = screen; 61 | continue; 62 | } 63 | 64 | return new IntRect( 65 | screen.Bounds.X, 66 | screen.Bounds.Y, 67 | screen.Bounds.Width, 68 | screen.Bounds.Height 69 | ); 70 | } 71 | 72 | return returnBackupScreen 73 | ? new IntRect( 74 | backupScreen.Bounds.X, 75 | backupScreen.Bounds.Y, 76 | backupScreen.Bounds.Width, 77 | backupScreen.Bounds.Height 78 | ) 79 | : new IntRect(); 80 | } 81 | 82 | public static Vector2i LimitToBounds(Vector2i pos, Vector2u size, IntRect bounds) 83 | { 84 | if (pos.X < bounds.Left) 85 | pos.X = bounds.Left; 86 | else if (pos.X > bounds.Left + bounds.Width - size.X) 87 | pos.X = bounds.Left + bounds.Width - (int)size.X; 88 | 89 | if (pos.Y < bounds.Top) 90 | pos.Y = bounds.Top; 91 | else if (pos.Y > bounds.Top + bounds.Height - size.Y) 92 | pos.Y = bounds.Top + bounds.Height - (int)size.Y; 93 | return pos; 94 | } 95 | 96 | public static Vector2i LimitToWindow(Vector2i pos, RenderWindow Window) 97 | { 98 | if (pos.X < Window.Position.X) 99 | pos.X = Window.Position.X; 100 | else if (pos.X > Window.Position.X + Window.Size.X) 101 | pos.X = (int)(Window.Position.X + Window.Size.X); 102 | if (pos.Y < Window.Position.Y) 103 | pos.Y = Window.Position.Y; 104 | else if (pos.Y > Window.Position.Y + Window.Size.Y) 105 | pos.Y = (int)(Window.Position.Y + Window.Size.Y); 106 | return pos; 107 | } 108 | 109 | /// Returns Orientation from the EXIF data of a jpg. 110 | public static int GetDefaultRotationFromEXIF(string path) 111 | { 112 | using var image = new ImageMagick.MagickImage(); 113 | image.Ping(path, GetDefaultMagickReadSettings()); 114 | 115 | var exif = image.GetExifProfile(); 116 | if (exif is null) 117 | return 0; 118 | 119 | var orientation = exif.GetValue(ImageMagick.ExifTag.Orientation)?.Value ?? 1; 120 | return orientation switch 121 | { 122 | 3 or 4 => 180, 123 | 5 or 6 => 90, 124 | 7 or 8 => 270, 125 | _ => 0, 126 | }; 127 | } 128 | 129 | /// Returns DateTime from EXIF data or the FileInfo is there isn't one 130 | public static DateTime GetDateValueFromEXIF(string path) 131 | { 132 | using var image = new ImageMagick.MagickImage(); 133 | image.Ping(path); 134 | 135 | var exif = image.GetExifProfile(); 136 | if (exif is null) 137 | return new System.IO.FileInfo(path).LastWriteTime; 138 | 139 | var dateTime = exif.GetValue(ImageMagick.ExifTag.DateTime); 140 | if (dateTime == null || string.IsNullOrWhiteSpace(dateTime.Value)) 141 | return new System.IO.FileInfo(path).LastWriteTime; 142 | 143 | if ( 144 | DateTime.TryParseExact( 145 | dateTime.Value, 146 | "yyyy:MM:dd HH:mm:ss", 147 | System.Globalization.CultureInfo.InvariantCulture, 148 | System.Globalization.DateTimeStyles.None, 149 | out var parsed 150 | ) 151 | ) 152 | { 153 | return parsed; 154 | } 155 | 156 | return new System.IO.FileInfo(path).LastWriteTime; 157 | } 158 | 159 | public static bool IsSupportedFileType(ImageMagick.MagickFormat? format) 160 | { 161 | if (format is null) 162 | return false; 163 | return format switch 164 | { 165 | ImageMagick.MagickFormat.Avi 166 | or ImageMagick.MagickFormat.Flv 167 | or ImageMagick.MagickFormat.M2v 168 | or ImageMagick.MagickFormat.M4v 169 | or ImageMagick.MagickFormat.Mkv 170 | or ImageMagick.MagickFormat.Mov 171 | or ImageMagick.MagickFormat.Mp4 172 | or ImageMagick.MagickFormat.Mpeg 173 | or ImageMagick.MagickFormat.Mpg 174 | or ImageMagick.MagickFormat.Pdf 175 | or ImageMagick.MagickFormat.Wmv 176 | or ImageMagick.MagickFormat.WebM 177 | or ImageMagick.MagickFormat.Text 178 | or ImageMagick.MagickFormat.Txt 179 | or ImageMagick.MagickFormat.Json 180 | or ImageMagick.MagickFormat.Htm 181 | or ImageMagick.MagickFormat.Html => false, 182 | _ => true, 183 | }; 184 | } 185 | 186 | /// 187 | /// Checks if file is supported by checking the extension. 188 | /// This is done instead of getting the actual MagickImageInfo to avoid a performance hit when used in loops. 189 | /// 190 | public static bool IsSupportedFileType(string path) 191 | { 192 | // Checks via extension instead of MagickImageInfo to avoid performance hit 193 | var formatInfo = ImageMagick.MagickFormatInfo.Create(path); 194 | if (formatInfo is null) 195 | return false; 196 | return IsSupportedFileType(formatInfo.Format); 197 | } 198 | 199 | public static bool IsAnimatedImage(string path) 200 | { 201 | var info = GetMagickImageInfo(path); 202 | if (info is null) 203 | return false; 204 | 205 | if ( 206 | info.Format == ImageMagick.MagickFormat.Png 207 | || info.Format == ImageMagick.MagickFormat.APng 208 | ) 209 | { 210 | return IsAnimatedPng(path); 211 | } 212 | 213 | var validFormat = info.Format switch 214 | { 215 | ImageMagick.MagickFormat.Gif 216 | or ImageMagick.MagickFormat.Gif87 217 | or ImageMagick.MagickFormat.Mng 218 | or ImageMagick.MagickFormat.WebP => true, 219 | _ => false, 220 | }; 221 | if (!validFormat) 222 | return false; 223 | 224 | using var collection = new ImageMagick.MagickImageCollection(); 225 | try 226 | { 227 | collection.Ping(path); 228 | } 229 | catch (ImageMagick.MagickCorruptImageErrorException) 230 | { 231 | return false; 232 | } 233 | return collection.Count > 1; 234 | } 235 | 236 | static bool IsAnimatedPng(string path) 237 | { 238 | using var fs = System.IO.File.OpenRead(path); 239 | using var br = new System.IO.BinaryReader(fs); 240 | 241 | // skip PNG signature 242 | br.BaseStream.Seek(8, System.IO.SeekOrigin.Begin); 243 | 244 | while (br.BaseStream.Position + 8 <= br.BaseStream.Length) 245 | { 246 | var lengthBytes = br.ReadBytes(4); 247 | if (lengthBytes.Length < 4) 248 | break; 249 | 250 | var typeBytes = br.ReadBytes(4); 251 | if (typeBytes.Length < 4) 252 | break; 253 | 254 | var type = System.Text.Encoding.ASCII.GetString(typeBytes); 255 | 256 | if (type == "acTL") 257 | return true; // APNG detected 258 | 259 | if (type == "IDAT" || type == "IEND") 260 | break; // no animation info before first image data 261 | 262 | // skip chunk data + CRC 263 | uint length = (uint)( 264 | (lengthBytes[0] << 24) 265 | | (lengthBytes[1] << 16) 266 | | (lengthBytes[2] << 8) 267 | | lengthBytes[3] 268 | ); 269 | br.BaseStream.Seek(length + 4, System.IO.SeekOrigin.Current); 270 | } 271 | 272 | return false; 273 | } 274 | 275 | public static ImageMagick.MagickImageInfo? GetMagickImageInfo(string fileName) 276 | { 277 | ImageMagick.MagickImageInfo? info = null; 278 | try 279 | { 280 | info = new ImageMagick.MagickImageInfo(fileName, GetDefaultMagickReadSettings()); 281 | } 282 | catch 283 | { 284 | // Ignore errors 285 | } 286 | return info; 287 | } 288 | 289 | public static ImageMagick.MagickReadSettings GetDefaultMagickReadSettings( 290 | Action? configure = null 291 | ) 292 | { 293 | var settings = new ImageMagick.MagickReadSettings( 294 | // Fixed loading issues with bmp files invalid file size info 295 | new ImageMagick.Formats.BmpReadDefines { IgnoreFileSize = true } 296 | ); 297 | configure?.Invoke(settings); 298 | return settings; 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /vimage/ImageManipulation/Quantizer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.nullskull.com/articles/StripImageFromAnimatedGif.asp 3 | 4 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 5 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 6 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 7 | PARTICULAR PURPOSE. 8 | 9 | This is sample code and is freely distributable. 10 | */ 11 | 12 | using System; 13 | using System.Drawing; 14 | using System.Drawing.Imaging; 15 | using System.Runtime.InteropServices; 16 | 17 | namespace vimage.ImageManipulation 18 | { 19 | /// 20 | /// Summary description for Class1. 21 | /// 22 | public unsafe abstract class Quantizer 23 | { 24 | /// 25 | /// Construct the quantizer 26 | /// 27 | /// If true, the quantization only needs to loop through the source pixels once 28 | /// 29 | /// If you construct this class with a true value for singlePass, then the code will, when quantizing your image, 30 | /// only call the 'QuantizeImage' function. If two passes are required, the code will call 'InitialQuantizeImage' 31 | /// and then 'QuantizeImage'. 32 | /// 33 | public Quantizer(bool singlePass) 34 | { 35 | _singlePass = singlePass; 36 | } 37 | 38 | /// 39 | /// Quantize an image and return the resulting output bitmap 40 | /// 41 | /// The image to quantize 42 | /// A quantized version of the image 43 | public Bitmap Quantize(Image source) 44 | { 45 | // Get the size of the source image 46 | int height = source.Height; 47 | int width = source.Width; 48 | 49 | // And construct a rectangle from these dimensions 50 | var bounds = new Rectangle(0, 0, width, height); 51 | 52 | // First off take a 32bpp copy of the image 53 | var copy = new Bitmap(width, height, PixelFormat.Format32bppArgb); 54 | 55 | // And construct an 8bpp version 56 | var output = new Bitmap(width, height, PixelFormat.Format8bppIndexed); 57 | 58 | // Now lock the bitmap into memory 59 | using (var g = Graphics.FromImage(copy)) 60 | { 61 | g.PageUnit = GraphicsUnit.Pixel; 62 | 63 | // Draw the source image onto the copy bitmap, 64 | // which will effect a widening as appropriate. 65 | g.DrawImageUnscaled(source, bounds); 66 | } 67 | 68 | // Define a pointer to the bitmap data 69 | BitmapData? sourceData = null; 70 | 71 | try 72 | { 73 | // Get the source image bits and lock into memory 74 | sourceData = copy.LockBits( 75 | bounds, 76 | ImageLockMode.ReadOnly, 77 | PixelFormat.Format32bppArgb 78 | ); 79 | 80 | // Call the FirstPass function if not a single pass algorithm. 81 | // For something like an octree quantizer, this will run through 82 | // all image pixels, build a data structure, and create a palette. 83 | if (!_singlePass) 84 | FirstPass(sourceData, width, height); 85 | 86 | // Then set the color palette on the output bitmap. I'm passing in the current palette 87 | // as there's no way to construct a new, empty palette. 88 | output.Palette = GetPalette(output.Palette); 89 | 90 | // Then call the second pass which actually does the conversion 91 | SecondPass(sourceData, output, width, height, bounds); 92 | } 93 | finally 94 | { 95 | // Ensure that the bits are unlocked 96 | if (sourceData is not null) 97 | copy.UnlockBits(sourceData); 98 | } 99 | 100 | // Last but not least, return the output bitmap 101 | return output; 102 | } 103 | 104 | /// 105 | /// Execute the first pass through the pixels in the image 106 | /// 107 | /// The source data 108 | /// The width in pixels of the image 109 | /// The height in pixels of the image 110 | protected virtual void FirstPass(BitmapData sourceData, int width, int height) 111 | { 112 | // Define the source data pointers. The source row is a byte to 113 | // keep addition of the stride value easier(as this is in bytes) 114 | byte* pSourceRow = (byte*)sourceData.Scan0.ToPointer(); 115 | int* pSourcePixel; 116 | 117 | // Loop through each row 118 | for (int row = 0; row < height; row++) 119 | { 120 | // Set the source pixel to the first pixel in this row 121 | pSourcePixel = (Int32*)pSourceRow; 122 | 123 | // And loop through each column 124 | for (int col = 0; col < width; col++, pSourcePixel++) 125 | // Now I have the pixel, call the FirstPassQuantize function... 126 | InitialQuantizePixel((Color32*)pSourcePixel); 127 | 128 | // Add the stride to the source row 129 | pSourceRow += sourceData.Stride; 130 | } 131 | } 132 | 133 | /// 134 | /// Execute a second pass through the bitmap 135 | /// 136 | /// The source bitmap, locked into memory 137 | /// The output bitmap 138 | /// The width in pixels of the image 139 | /// The height in pixels of the image 140 | /// The bounding rectangle 141 | protected virtual void SecondPass( 142 | BitmapData sourceData, 143 | Bitmap output, 144 | int width, 145 | int height, 146 | Rectangle bounds 147 | ) 148 | { 149 | BitmapData? outputData = null; 150 | 151 | try 152 | { 153 | // Lock the output bitmap into memory 154 | outputData = output.LockBits( 155 | bounds, 156 | ImageLockMode.WriteOnly, 157 | PixelFormat.Format8bppIndexed 158 | ); 159 | 160 | // Define the source data pointers. The source row is a byte to 161 | // keep addition of the stride value easier(as this is in bytes) 162 | byte* pSourceRow = (byte*)sourceData.Scan0.ToPointer(); 163 | int* pSourcePixel = (int*)pSourceRow; 164 | int* pPreviousPixel = pSourcePixel; 165 | 166 | // Now define the destination data pointers 167 | byte* pDestinationRow = (byte*)outputData.Scan0.ToPointer(); 168 | byte* pDestinationPixel = pDestinationRow; 169 | 170 | // And convert the first pixel, so that I have values going into the loop 171 | byte pixelValue = QuantizePixel((Color32*)pSourcePixel); 172 | 173 | // Assign the value of the first pixel 174 | *pDestinationPixel = pixelValue; 175 | 176 | // Loop through each row 177 | for (int row = 0; row < height; row++) 178 | { 179 | // Set the source pixel to the first pixel in this row 180 | pSourcePixel = (int*)pSourceRow; 181 | 182 | // And set the destination pixel pointer to the first pixel in the row 183 | pDestinationPixel = pDestinationRow; 184 | 185 | // Loop through each pixel on this scan line 186 | for (int col = 0; col < width; col++, pSourcePixel++, pDestinationPixel++) 187 | { 188 | // Check if this is the same as the last pixel. If so use that value 189 | // rather than calculating it again. This is an inexpensive optimisation. 190 | if (*pPreviousPixel != *pSourcePixel) 191 | { 192 | // Quantize the pixel 193 | pixelValue = QuantizePixel((Color32*)pSourcePixel); 194 | 195 | // And setup the previous pointer 196 | pPreviousPixel = pSourcePixel; 197 | } 198 | 199 | // And set the pixel in the output 200 | *pDestinationPixel = pixelValue; 201 | } 202 | 203 | // Add the stride to the source row 204 | pSourceRow += sourceData.Stride; 205 | 206 | // And to the destination row 207 | pDestinationRow += outputData.Stride; 208 | } 209 | } 210 | finally 211 | { 212 | // Ensure that I unlock the output bits 213 | if (outputData is not null) 214 | output.UnlockBits(outputData); 215 | } 216 | } 217 | 218 | /// 219 | /// Override this to process the pixel in the first pass of the algorithm 220 | /// 221 | /// The pixel to quantize 222 | /// 223 | /// This function need only be overridden if your quantize algorithm needs two passes, 224 | /// such as an Octree quantizer. 225 | /// 226 | protected virtual void InitialQuantizePixel(Color32* pixel) { } 227 | 228 | /// 229 | /// Override this to process the pixel in the second pass of the algorithm 230 | /// 231 | /// The pixel to quantize 232 | /// The quantized value 233 | protected abstract byte QuantizePixel(Color32* pixel); 234 | 235 | /// 236 | /// Retrieve the palette for the quantized image 237 | /// 238 | /// Any old palette, this is overrwritten 239 | /// The new color palette 240 | protected abstract ColorPalette GetPalette(ColorPalette original); 241 | 242 | /// 243 | /// Flag used to indicate whether a single pass or two passes are needed for quantization. 244 | /// 245 | private readonly bool _singlePass; 246 | 247 | /// 248 | /// Struct that defines a 32 bpp colour 249 | /// 250 | /// 251 | /// This struct is used to read data from a 32 bits per pixel image 252 | /// in memory, and is ordered in this manner as this is the way that 253 | /// the data is layed out in memory 254 | /// 255 | [StructLayout(LayoutKind.Explicit)] 256 | public struct Color32 257 | { 258 | /// 259 | /// Holds the blue component of the colour 260 | /// 261 | [FieldOffset(0)] 262 | public byte Blue; 263 | 264 | /// 265 | /// Holds the green component of the colour 266 | /// 267 | [FieldOffset(1)] 268 | public byte Green; 269 | 270 | /// 271 | /// Holds the red component of the colour 272 | /// 273 | [FieldOffset(2)] 274 | public byte Red; 275 | 276 | /// 277 | /// Holds the alpha component of the colour 278 | /// 279 | [FieldOffset(3)] 280 | public byte Alpha; 281 | 282 | /// 283 | /// Permits the color32 to be treated as an int32 284 | /// 285 | [FieldOffset(0)] 286 | public int ARGB; 287 | 288 | /// 289 | /// Return the color for this Color32 object 290 | /// 291 | public Color Color 292 | { 293 | get { return Color.FromArgb(Alpha, Red, Green, Blue); } 294 | } 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /vimage_settings/Source/ContextMenuItem.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | using vimage.Common; 7 | 8 | namespace vimage_settings 9 | { 10 | /// 11 | /// Interaction logic for ContextMenuItem.xaml 12 | /// 13 | public partial class ContextMenuItem : UserControl 14 | { 15 | private readonly ContextMenu? ContextMenuEditor; 16 | private readonly Panel? ParentPanel; 17 | private readonly ContextMenuEditorCanvas? ParentCanvas; 18 | private readonly ScrollViewer? ScrollViewer; 19 | private int CustomActionsStartIndex = -1; 20 | private Point AnchorPoint; 21 | 22 | private bool Selected = false; 23 | private readonly SolidColorBrush BrushSelected = new(Colors.DodgerBlue); 24 | private readonly SolidColorBrush BrushOver = new(Colors.LightSkyBlue); 25 | 26 | public ContextMenuItem() 27 | { 28 | InitializeComponent(); 29 | SetFunctions(); 30 | } 31 | 32 | public ContextMenuItem( 33 | string name, 34 | object func, 35 | ContextMenu contextMenu, 36 | Panel parentPanel, 37 | ContextMenuEditorCanvas canvas, 38 | ScrollViewer scroll, 39 | int indent = 0 40 | ) 41 | { 42 | InitializeComponent(); 43 | 44 | ContextMenuEditor = contextMenu; 45 | ParentPanel = parentPanel; 46 | ParentCanvas = canvas; 47 | ScrollViewer = scroll; 48 | Indent = indent; 49 | SetFunctions(); 50 | 51 | ItemName.Text = name; 52 | 53 | int funcIndex = ItemFunction.Items.IndexOf( 54 | (string)(func is vimage.Common.Action action ? action.ToNameString() : func) 55 | ); 56 | if (funcIndex != -1) 57 | ItemFunction.SelectedIndex = funcIndex; 58 | } 59 | 60 | public void SetFunctions() 61 | { 62 | int prevIndex = -1; 63 | if (ItemFunction.Items.Count != 0) 64 | prevIndex = ItemFunction.SelectedIndex; 65 | 66 | ItemFunction.Items.Clear(); 67 | 68 | _ = ItemFunction.Items.Add("-"); 69 | for (int i = 0; i < Actions.MenuActions.Length; i++) 70 | _ = ItemFunction.Items.Add(Actions.MenuActions[i].ToNameString()); 71 | if (App.vimageConfig != null) 72 | { 73 | CustomActionsStartIndex = ItemFunction.Items.Count; 74 | for (int i = 0; i < App.vimageConfig.CustomActions.Count; i++) 75 | ItemFunction.Items.Add(App.vimageConfig.CustomActions[i].name); 76 | } 77 | ItemFunction.SelectedIndex = 0; 78 | 79 | if (prevIndex != -1 && prevIndex < ItemFunction.Items.Count) 80 | ItemFunction.SelectedIndex = prevIndex; 81 | } 82 | 83 | public void UpdateCustomActions() 84 | { 85 | if (CustomActionsStartIndex == -1) 86 | return; 87 | 88 | int prevSelected = ItemFunction.SelectedIndex; 89 | 90 | for ( 91 | int i = CustomActionsStartIndex; 92 | i < CustomActionsStartIndex + App.vimageConfig.CustomActions.Count; 93 | i++ 94 | ) 95 | { 96 | if (i >= ItemFunction.Items.Count) 97 | { 98 | // add 99 | ItemFunction.Items.Add( 100 | App.vimageConfig.CustomActions[i - CustomActionsStartIndex].name 101 | ); 102 | } 103 | else 104 | { 105 | // update name 106 | ItemFunction.Items[i] = App.vimageConfig 107 | .CustomActions[i - CustomActionsStartIndex] 108 | .name; 109 | } 110 | } 111 | while ( 112 | ItemFunction.Items.Count 113 | > CustomActionsStartIndex + App.vimageConfig.CustomActions.Count 114 | ) 115 | ItemFunction.Items.RemoveAt(ItemFunction.Items.Count - 1); // delete 116 | 117 | if (ItemFunction.SelectedIndex < 0 && prevSelected < ItemFunction.Items.Count) 118 | ItemFunction.SelectedIndex = prevSelected; 119 | } 120 | 121 | private void Delete_Click(object sender, RoutedEventArgs e) 122 | { 123 | ParentPanel?.Children.Remove(this); 124 | if (Application.Current.MainWindow is MainWindow mainWindow) 125 | _ = mainWindow.ContextMenuEditor.Items.Remove(this); 126 | } 127 | 128 | private void UserControl_MouseEnter(object sender, MouseEventArgs e) 129 | { 130 | if (ParentCanvas?.MovingItem is null) 131 | UserControl.Background = Selected ? BrushSelected : BrushOver; 132 | } 133 | 134 | private void UserControl_MouseLeave(object sender, MouseEventArgs e) 135 | { 136 | if (ParentCanvas?.MovingItem is null) 137 | UserControl.Background = Selected ? BrushSelected : new SolidColorBrush(); 138 | } 139 | 140 | private void ItemName_GotKeyboardFocus(object sender, RoutedEventArgs e) 141 | { 142 | SelectItem(); 143 | } 144 | 145 | private void ItemName_LostKeyboardFocus(object sender, RoutedEventArgs e) 146 | { 147 | UnselectItem(); 148 | } 149 | 150 | private void Item_GotFocus(object sender, RoutedEventArgs e) 151 | { 152 | SelectItem(); 153 | } 154 | 155 | private void Item_LostFocus(object sender, RoutedEventArgs e) 156 | { 157 | UnselectItem(); 158 | } 159 | 160 | private void UserControl_MouseDown(object sender, MouseButtonEventArgs e) 161 | { 162 | FocusManager.SetFocusedElement(UserControl, ItemName); 163 | _ = Keyboard.Focus(ItemName); 164 | ItemName.SelectAll(); 165 | e.Handled = true; 166 | 167 | AnchorPoint = e.GetPosition(this); 168 | } 169 | 170 | public void UnselectItem() 171 | { 172 | if (ParentCanvas?.MovingItem is not null) 173 | return; 174 | 175 | Selected = false; 176 | UserControl.Background = new SolidColorBrush(); 177 | } 178 | 179 | public void SelectItem(bool selectTextBox = false) 180 | { 181 | if (ParentCanvas?.MovingItem is not null) 182 | return; 183 | 184 | Selected = true; 185 | if (ContextMenuEditor is not null) 186 | ContextMenuEditor.CurrentItemSelection = this; 187 | UserControl.Background = BrushSelected; 188 | 189 | if (selectTextBox) 190 | { 191 | _ = Dispatcher.BeginInvoke( 192 | System.Windows.Threading.DispatcherPriority.ApplicationIdle, 193 | (System.Threading.ThreadStart) 194 | delegate() 195 | { 196 | _ = Focus(); 197 | } 198 | ); 199 | FocusManager.SetFocusedElement(UserControl, ItemName); 200 | _ = Keyboard.Focus(ItemName); 201 | ItemName.SelectAll(); 202 | } 203 | } 204 | 205 | protected override void OnMouseMove(MouseEventArgs e) 206 | { 207 | base.OnMouseMove(e); 208 | 209 | if (e.LeftButton == MouseButtonState.Pressed) 210 | { 211 | // start dragging 212 | if ( 213 | !Dragging 214 | && Parent == ParentPanel 215 | && ParentCanvas?.MovingItem is null 216 | && e.GetPosition(null).X < (Indent + 1) * 30 217 | ) 218 | Dragging = true; 219 | 220 | if (Dragging && ParentCanvas is not null) 221 | { 222 | // update position 223 | var posX = e.GetPosition(ParentCanvas).X - AnchorPoint.X; 224 | var posY = e.GetPosition(ParentCanvas).Y - AnchorPoint.Y; 225 | var i = Math.Max( 226 | (int)Math.Round((posY + ((MinHeight - 2) / 2)) / (MinHeight - 2)), 227 | 0 228 | ); 229 | i = Math.Min(i, ParentPanel?.Children.Count ?? 0); 230 | 231 | var top = Math.Max(i * (MinHeight - 2), 0); 232 | Canvas.SetTop(ParentCanvas.SelectionRect, top - 2); 233 | 234 | int indent = 0; 235 | if (i != 0) 236 | { 237 | var itemAbove = ParentPanel?.Children[i - 1]; 238 | if (itemAbove is ContextMenuItem itemAboveItem) 239 | { 240 | if (posX >= (itemAboveItem.Indent + 1) * 30) 241 | indent = itemAboveItem.Indent + 1; 242 | else if (posX >= itemAboveItem.Indent * 30) 243 | indent = itemAboveItem.Indent; 244 | } 245 | } 246 | Canvas.SetLeft(ParentCanvas.SelectionRect, indent * 30); 247 | 248 | Indent = indent; 249 | ParentCanvas.InsertAtIndex = i; 250 | 251 | Canvas.SetTop(this, posY); 252 | Canvas.SetLeft(this, posX); 253 | 254 | // scroll window when dragging past top/bottom 255 | if (ScrollViewer is not null) 256 | { 257 | if ( 258 | posY + MinHeight 259 | > ScrollViewer.ActualHeight + ScrollViewer.VerticalOffset 260 | ) 261 | { 262 | ScrollViewer.ScrollToVerticalOffset( 263 | ScrollViewer.VerticalOffset 264 | + ( 265 | posY 266 | - ( 267 | ScrollViewer.ActualHeight 268 | + ScrollViewer.VerticalOffset 269 | - MinHeight 270 | ) 271 | ) 272 | ); 273 | } 274 | else if (posY < ScrollViewer.VerticalOffset) 275 | { 276 | ScrollViewer.ScrollToVerticalOffset( 277 | ScrollViewer.VerticalOffset - (ScrollViewer.VerticalOffset - posY) 278 | ); 279 | } 280 | } 281 | 282 | e.Handled = true; 283 | } 284 | } 285 | else if (Dragging) 286 | Dragging = false; 287 | } 288 | 289 | private int _Indent = 0; 290 | public int Indent 291 | { 292 | get { return _Indent; } 293 | set 294 | { 295 | _Indent = value; 296 | IndentColumn.Width = new GridLength(_Indent * 30); 297 | } 298 | } 299 | 300 | private bool _Dragging = false; 301 | public bool Dragging 302 | { 303 | get { return _Dragging; } 304 | set 305 | { 306 | if (_Dragging == value) 307 | return; 308 | 309 | if (ParentPanel is null || ParentCanvas is null) 310 | { 311 | _Dragging = value; 312 | return; 313 | } 314 | 315 | if (!Dragging) 316 | { 317 | int index = ParentPanel.Children.IndexOf(this); 318 | ParentPanel.Children.Remove(this); 319 | _ = ParentCanvas.Children.Add(this); 320 | 321 | ParentCanvas.SetupGhost(this); 322 | ParentPanel.Children.Insert(index, ParentCanvas.GhostItem); 323 | ParentCanvas.MovingItem = this; 324 | ParentCanvas.InsertAtIndex = index; 325 | 326 | Width = ParentPanel.ActualWidth; 327 | 328 | ParentCanvas.SelectionRect.Width = Width; 329 | _ = ParentCanvas.Children.Add(ParentCanvas.SelectionRect); 330 | 331 | Selected = true; 332 | Opacity = 0.7f; 333 | 334 | _ = CaptureMouse(); 335 | ItemName.IsEnabled = false; 336 | ItemFunction.IsEnabled = false; 337 | ButtonDelete.IsEnabled = false; 338 | } 339 | else 340 | { 341 | int ghostIndex = ParentPanel.Children.IndexOf(ParentCanvas.GhostItem); 342 | if (ParentCanvas.InsertAtIndex == ghostIndex + 1) 343 | ParentCanvas.InsertAtIndex = ghostIndex; 344 | 345 | if (ParentCanvas.InsertAtIndex > ghostIndex) 346 | ParentCanvas.InsertAtIndex--; 347 | 348 | ParentPanel.Children.Remove(ParentCanvas.GhostItem); 349 | ParentCanvas.Children.Remove(this); 350 | ParentCanvas.Children.Remove(ParentCanvas.SelectionRect); 351 | 352 | if (ParentCanvas.InsertAtIndex != -1) 353 | { 354 | if (ParentCanvas.InsertAtIndex >= ParentPanel.Children.Count) 355 | _ = ParentPanel.Children.Add(this); 356 | else 357 | ParentPanel.Children.Insert(ParentCanvas.InsertAtIndex, this); 358 | } 359 | ParentCanvas.MovingItem = null; 360 | ParentCanvas.InsertAtIndex = -1; 361 | 362 | Width = double.NaN; 363 | 364 | Opacity = 1; 365 | UnselectItem(); 366 | 367 | ReleaseMouseCapture(); 368 | ItemName.IsEnabled = true; 369 | ItemFunction.IsEnabled = true; 370 | ButtonDelete.IsEnabled = true; 371 | } 372 | 373 | _Dragging = value; 374 | } 375 | } 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /vimage/ContextMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows.Forms; 4 | using vimage.Common; 5 | using vimage.Display; 6 | using Action = vimage.Common.Action; 7 | 8 | namespace vimage 9 | { 10 | internal class ContextMenu : ContextMenuStrip 11 | { 12 | private readonly ImageViewer ImageViewer; 13 | public int Setting = -1; 14 | private List Items_General = []; 15 | private List Items_Animation = []; 16 | 17 | private Dictionary FuncByName = []; 18 | 19 | public int FileNameItem = -1; 20 | public string FileNameCurrent = "."; 21 | 22 | private ToolTip? ToolTip; 23 | 24 | public ContextMenu(ImageViewer ImageViewer) 25 | : base() 26 | { 27 | this.ImageViewer = ImageViewer; 28 | 29 | SetupToolTip(); 30 | } 31 | 32 | public void LoadItems( 33 | List General, 34 | List Animation, 35 | int AnimationInsertAtIndex 36 | ) 37 | { 38 | FuncByName = []; 39 | 40 | // General 41 | Items_General = []; 42 | LoadItemsInto(Items_General, General); 43 | 44 | // Animation 45 | Items_Animation = [.. Items_General]; 46 | List list = []; 47 | 48 | // inserting into submenu? 49 | int depth = 0; 50 | if (Items_Animation[AnimationInsertAtIndex].StartsWith(':')) 51 | depth = Items_Animation[AnimationInsertAtIndex].Split(':').Length - 1; 52 | 53 | LoadItemsInto(list, Animation, depth); 54 | Items_Animation.InsertRange(AnimationInsertAtIndex, list); 55 | } 56 | 57 | private void LoadItemsInto(List list, List items, int depth = 0) 58 | { 59 | for (int i = 0; i < items.Count; i++) 60 | { 61 | if (ImageViewer.File == "") 62 | { 63 | // Remove certain items if there is no file (looking at clipboard image) 64 | if (items[i] is string str) 65 | { 66 | // remove Sort By submenu 67 | if (str.StartsWith("Sort")) 68 | { 69 | i++; 70 | if (i < items.Count - 1 && (items[i + 1] as dynamic).name == "-") 71 | i++; 72 | continue; 73 | } 74 | } 75 | else 76 | { 77 | // remove navigation and delete 78 | switch ((items[i] as dynamic).func) 79 | { 80 | case Action.NextImage: 81 | case Action.PrevImage: 82 | case Action.Delete: 83 | continue; 84 | } 85 | } 86 | } 87 | 88 | if (items[i] is string str2) 89 | { 90 | // Submenu 91 | list.Add(VariableAmountOfStrings(depth, ":") + items[i] + ":"); 92 | FuncByName.Add(str2, Action.None); 93 | 94 | i++; 95 | if (items[i] is List itemList) 96 | LoadItemsInto(list, itemList, depth + 1); 97 | } 98 | else if (items[i] is ContextMenuItem contextMenuItem) 99 | { 100 | // Item 101 | if (!FuncByName.ContainsKey(contextMenuItem.name)) 102 | { 103 | var itemName = contextMenuItem.name; 104 | if (itemName.StartsWith("[filename")) 105 | FileNameItem = list.Count; 106 | if (itemName.Contains("[version]")) 107 | itemName = itemName.Replace("[version]", ImageViewer.VERSION_NO); 108 | 109 | list.Add(VariableAmountOfStrings(depth, ":") + itemName); 110 | if (!itemName.Equals("-")) 111 | FuncByName.Add(itemName, contextMenuItem.func); 112 | } 113 | } 114 | } 115 | } 116 | 117 | public void Setup(bool force) 118 | { 119 | if ( 120 | !force 121 | && ( 122 | (Setting == 0 && ImageViewer.Image is not AnimatedImage) 123 | || (Setting == 1 && ImageViewer.Image is AnimatedImage) 124 | ) 125 | ) 126 | return; 127 | 128 | Items.Clear(); 129 | ShowImageMargin = ImageViewer.Config.ContextMenuShowMargin; 130 | 131 | List items; 132 | if (ImageViewer.Image is AnimatedImage) 133 | { 134 | Setting = 1; 135 | items = Items_Animation; 136 | } 137 | else 138 | { 139 | Setting = 0; 140 | items = Items_General; 141 | } 142 | 143 | for (int i = 0; i < items.Count; i++) 144 | { 145 | ToolStripItem? item = null; 146 | string name = items[i]; 147 | bool itemClickable = true; 148 | 149 | if (name.Length > 0 && name.LastIndexOf(':') == name.Length - 1) 150 | { 151 | // non-clickable item? 152 | name = name[..^1]; 153 | itemClickable = false; 154 | } 155 | 156 | if (name.StartsWith(':')) 157 | { 158 | // sub item 159 | if (Items[Items.Count - 1] is ToolStripDropDownItem dropDownItem) 160 | { 161 | if (dropDownItem.DropDown is ToolStripDropDownMenu dropDownMenu) 162 | { 163 | dropDownMenu.ShowImageMargin = ImageViewer 164 | .Config 165 | .ContextMenuShowMarginSub; 166 | } 167 | name = name[1..]; 168 | while (name.StartsWith(':')) 169 | { 170 | if ( 171 | dropDownItem.DropDownItems.Count > 0 172 | && dropDownItem.DropDownItems[dropDownItem.DropDownItems.Count - 1] 173 | is ToolStripDropDownItem subDropDownitem 174 | ) 175 | { 176 | dropDownItem = subDropDownitem; 177 | } 178 | name = name[1..]; 179 | } 180 | 181 | item = dropDownItem.DropDownItems.Add(name); 182 | } 183 | } 184 | else 185 | { 186 | // item 187 | item = Items.Add(name); 188 | } 189 | if (name.Equals("-")) 190 | continue; 191 | 192 | if (item is not null) 193 | { 194 | if (itemClickable) 195 | item.Click += ContexMenuItemClicked; 196 | item.Name = name; 197 | } 198 | } 199 | 200 | var websiteItem = GetItemByFunc(Action.VisitWebsite); 201 | if (websiteItem != null) 202 | websiteItem.BackColor = System.Drawing.Color.CornflowerBlue; 203 | 204 | RefreshItems(); 205 | } 206 | 207 | public void RefreshItems() 208 | { 209 | if (FileNameItem != -1 && FileNameCurrent != ImageViewer.File) 210 | { 211 | FileNameCurrent = ImageViewer.File; 212 | if (Items_General[FileNameItem].Contains("[filename]")) 213 | { 214 | // File Name 215 | var fileNameItem = Items[Items_General[FileNameItem]]; 216 | if (fileNameItem is not null) 217 | { 218 | fileNameItem.Text = Items_General[FileNameItem] 219 | .Replace( 220 | "[filename]", 221 | ImageViewer.File == "" 222 | ? "Clipboard Image" 223 | : ImageViewer.File[(ImageViewer.File.LastIndexOf('\\') + 1)..] 224 | ); 225 | } 226 | } 227 | else if (Items_General[FileNameItem].Contains("[filename")) 228 | { 229 | // File Name (trimmed) 230 | int a = Items_General[FileNameItem].IndexOf("[filename.") + 10; 231 | int b = Items_General[FileNameItem].IndexOf(']'); 232 | if (int.TryParse(Items_General[FileNameItem][a..b], out int nameLength)) 233 | { 234 | string fileName = 235 | ImageViewer.File == "" 236 | ? "Clipboard Image" 237 | : ImageViewer.File[(ImageViewer.File.LastIndexOf('\\') + 1)..]; 238 | string extension = 239 | ImageViewer.File == "" ? "" : fileName[fileName.LastIndexOf('.')..]; 240 | if ( 241 | nameLength >= fileName.Length - 6 242 | || fileName.LastIndexOf('.') <= nameLength 243 | ) 244 | nameLength = fileName.Length; 245 | 246 | var fileNameItem = Items[Items_General[FileNameItem]]; 247 | if (fileNameItem is not null) 248 | { 249 | fileNameItem.Text = 250 | (a > 10 ? Items_General[FileNameItem][..(a - 10)] : "") 251 | + ( 252 | fileName.Length > nameLength 253 | ? fileName[..nameLength] + ".." + extension 254 | : fileName 255 | ) 256 | + ( 257 | b < Items_General[FileNameItem].Length - 1 258 | ? Items_General[FileNameItem][(b + 1)..] 259 | : "" 260 | ); 261 | fileNameItem.ToolTipText = fileName.Length > nameLength ? fileName : ""; 262 | fileNameItem.MouseEnter += ItemMouseEnter; 263 | fileNameItem.MouseLeave += ItemMouseLeave; 264 | } 265 | } 266 | } 267 | } 268 | 269 | if ( 270 | !ImageViewer.Config.ContextMenuShowMargin 271 | && !ImageViewer.Config.ContextMenuShowMarginSub 272 | ) 273 | return; 274 | 275 | ToolStripMenuItem? item; 276 | 277 | item = GetItemByFunc(Action.Flip); 278 | if (item != null) 279 | item.Checked = ImageViewer.FlippedX; 280 | 281 | item = GetItemByFunc(Action.FitToMonitorHeight); 282 | if (item != null) 283 | item.Checked = ImageViewer.FitToMonitorHeight; 284 | 285 | item = GetItemByFunc(Action.FitToMonitorWidth); 286 | if (item != null) 287 | item.Checked = ImageViewer.FitToMonitorWidth; 288 | 289 | item = GetItemByFunc(Action.ToggleSmoothing); 290 | if (item != null) 291 | item.Checked = ImageViewer.Smoothing(); 292 | 293 | item = GetItemByFunc(Action.ToggleBackground); 294 | if (item != null) 295 | item.Checked = ImageViewer.BackgroundsForImagesWithTransparency; 296 | 297 | item = GetItemByFunc(Action.ToggleLock); 298 | if (item != null) 299 | item.Checked = ImageViewer.Locked; 300 | 301 | item = GetItemByFunc(Action.ToggleAlwaysOnTop); 302 | if (item != null) 303 | item.Checked = ImageViewer.AlwaysOnTop; 304 | 305 | item = GetItemByFunc(Action.ToggleTitleBar); 306 | if (item != null) 307 | item.Checked = ImageViewer.Config.Setting_ShowTitleBar; 308 | 309 | item = GetItemByFunc(Action.SortName); 310 | if (item != null) 311 | item.Checked = ImageViewer.SortImagesBy == SortBy.Name; 312 | 313 | item = GetItemByFunc(Action.SortDate); 314 | if (item != null) 315 | item.Checked = ImageViewer.SortImagesBy == SortBy.Date; 316 | 317 | item = GetItemByFunc(Action.SortDateModified); 318 | if (item != null) 319 | item.Checked = ImageViewer.SortImagesBy == SortBy.DateModified; 320 | 321 | item = GetItemByFunc(Action.SortDateCreated); 322 | if (item != null) 323 | item.Checked = ImageViewer.SortImagesBy == SortBy.DateCreated; 324 | 325 | item = GetItemByFunc(Action.SortSize); 326 | if (item != null) 327 | item.Checked = ImageViewer.SortImagesBy == SortBy.Size; 328 | 329 | item = GetItemByFunc(Action.SortAscending); 330 | if (item != null) 331 | item.Checked = ImageViewer.SortImagesByDir == SortDirection.Ascending; 332 | 333 | item = GetItemByFunc(Action.SortDescending); 334 | if (item != null) 335 | item.Checked = ImageViewer.SortImagesByDir == SortDirection.Descending; 336 | } 337 | 338 | private void ContexMenuItemClicked(object? sender, EventArgs e) 339 | { 340 | if (sender is not ToolStripItem item) 341 | return; 342 | 343 | if ( 344 | item is ToolStripDropDownItem toolStripDropDownItem 345 | && !toolStripDropDownItem.HasDropDownItems 346 | ) 347 | Close(); 348 | 349 | var func = FuncByName[item.Name ?? ""]; 350 | if (func is string funcName) 351 | { 352 | for (int i = 0; i < ImageViewer.Config.CustomActions.Count; i++) 353 | { 354 | if (ImageViewer.Config.CustomActions[i].name != funcName) 355 | continue; 356 | ImageViewer.DoCustomAction(ImageViewer.Config.CustomActions[i].func); 357 | } 358 | } 359 | else 360 | ImageViewer.DoAction((Action)func); 361 | } 362 | 363 | /// returns the ToolStripMenuItem based on the name of the function. 364 | public ToolStripMenuItem? GetItemByFunc(Action func) 365 | { 366 | return GetItemByFuncFrom(func, Items); 367 | } 368 | 369 | private ToolStripMenuItem? GetItemByFuncFrom( 370 | Action func, 371 | ToolStripItemCollection collection 372 | ) 373 | { 374 | for (int i = 0; i < collection.Count; i++) 375 | { 376 | var name = collection[i].Name; 377 | if (name is null || name == "") 378 | continue; 379 | object currentFunc = FuncByName[name]; 380 | if (currentFunc is Action action && action == func) 381 | return collection[i] as ToolStripMenuItem; 382 | 383 | if ( 384 | collection[i] is ToolStripDropDownItem toolStripDropDownItem 385 | && toolStripDropDownItem.DropDownItems.Count > 0 386 | ) 387 | { 388 | var item = GetItemByFuncFrom(func, toolStripDropDownItem.DropDownItems); 389 | if (item != null) 390 | return item; 391 | } 392 | } 393 | return null; 394 | } 395 | 396 | private static string VariableAmountOfStrings(int amount, string s) 397 | { 398 | if (amount == 0) 399 | return ""; 400 | 401 | string str = ""; 402 | for (int i = 0; i < amount; i++) 403 | str += s; 404 | return str; 405 | } 406 | 407 | private void SetupToolTip() 408 | { 409 | ShowItemToolTips = false; 410 | ToolTip = new ToolTip { UseAnimation = true, UseFading = true }; 411 | if (SystemInformation.HighContrast) 412 | { 413 | ToolTip.BackColor = System.Drawing.Color.FromArgb(26, 255, 255); 414 | ToolTip.ForeColor = System.Drawing.Color.Black; 415 | } 416 | else 417 | ToolTip.BackColor = System.Drawing.Color.FromArgb(196, 225, 255); 418 | ToolTip.OwnerDraw = true; 419 | ToolTip.Draw += new DrawToolTipEventHandler(ToolTipDraw); 420 | } 421 | 422 | private void ToolTipDraw(object? sender, DrawToolTipEventArgs e) 423 | { 424 | if (ToolTip is null) 425 | return; 426 | var bounds = e.Bounds; 427 | bounds.Height -= 1; 428 | var newArgs = new DrawToolTipEventArgs( 429 | e.Graphics, 430 | e.AssociatedWindow, 431 | e.AssociatedControl, 432 | bounds, 433 | e.ToolTipText, 434 | ToolTip.BackColor, 435 | ToolTip.ForeColor, 436 | e.Font 437 | ); 438 | newArgs.DrawBackground(); 439 | newArgs.DrawText(TextFormatFlags.VerticalCenter); 440 | } 441 | 442 | private void ItemMouseEnter(object? sender, EventArgs e) 443 | { 444 | if (sender is not ToolStripMenuItem item || item.Owner is null) 445 | return; 446 | ToolTip?.Show( 447 | item.ToolTipText, 448 | item.Owner, 449 | item.Bounds.Location.X + 8, 450 | item.Bounds.Location.Y + 1 451 | ); 452 | } 453 | 454 | private void ItemMouseLeave(object? sender, EventArgs e) 455 | { 456 | if (sender is not ToolStripMenuItem item || item.Owner is null) 457 | return; 458 | ToolTip?.Hide(item.Owner); 459 | } 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /vimage/Display/Graphics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using ImageMagick; 7 | using SFML.Graphics; 8 | using SFML.System; 9 | 10 | namespace vimage.Display 11 | { 12 | /// 13 | /// Graphics Manager. 14 | /// Loads and stores Textures and AnimatedImageDatas. 15 | /// 16 | internal class Graphics 17 | { 18 | private static readonly List Textures = []; 19 | private static readonly List TextureFileNames = []; 20 | 21 | private static readonly List AnimatedImageDatas = []; 22 | private static readonly List AnimatedImageDataFileNames = []; 23 | 24 | private static readonly List SplitTextures = []; 25 | private static readonly List SplitTextureFileNames = []; 26 | 27 | public static uint MAX_TEXTURES = 80; 28 | public static uint MAX_ANIMATIONS = 8; 29 | public static uint TextureMaxSize = Texture.MaximumSize; 30 | 31 | public static object? GetImage(string fileName, MagickReadSettings? settings = null) 32 | { 33 | int index = TextureFileNames.IndexOf(fileName); 34 | 35 | if (index >= 0) 36 | { 37 | // Texture Already Exists 38 | // move it to the end of the array and return it 39 | var texture = Textures[index]; 40 | var name = TextureFileNames[index]; 41 | 42 | Textures.RemoveAt(index); 43 | TextureFileNames.RemoveAt(index); 44 | Textures.Add(texture); 45 | TextureFileNames.Add(name); 46 | 47 | return new Sprite(Textures[^1]); 48 | } 49 | else 50 | { 51 | // New Texture 52 | try 53 | { 54 | var texture = GetTexture(fileName, settings); 55 | if (texture == null) 56 | return null; 57 | if (texture is Texture tex) 58 | return new Sprite(new Texture(tex)); 59 | else if (texture is DisplayObject displayObject) 60 | return displayObject; 61 | } 62 | catch (Exception) { } 63 | } 64 | 65 | return null; 66 | } 67 | 68 | public static object? GetTexture( 69 | string fileName, 70 | MagickReadSettings? settings = null, 71 | bool cache = true 72 | ) 73 | { 74 | using var image = GetMagickImage(fileName, settings); 75 | if (image is null) 76 | return null; 77 | 78 | if (image.Width > TextureMaxSize || image.Height > TextureMaxSize) 79 | { 80 | return GetLargeTexture(image, TextureMaxSize, cache ? fileName : null); 81 | } 82 | else 83 | { 84 | using var pixels = image.GetPixels(); 85 | var bytes = pixels.ToByteArray(PixelMapping.RGBA); 86 | var texture = new Texture(image.Width, image.Height); 87 | texture.Update(bytes); 88 | if (cache) 89 | { 90 | Textures.Add(texture); 91 | TextureFileNames.Add(fileName); 92 | } 93 | 94 | return texture; 95 | } 96 | } 97 | 98 | private static DisplayObject GetLargeTexture( 99 | MagickImage image, 100 | uint sectionSize, 101 | string? fileName = null 102 | ) 103 | { 104 | var amount = new Vector2u( 105 | (uint)Math.Ceiling((float)image.Width / sectionSize), 106 | (uint)Math.Ceiling((float)image.Height / sectionSize) 107 | ); 108 | var currentSize = new Vector2u(image.Width, image.Height); 109 | var pos = new Vector2u(); 110 | 111 | var largeTexture = new DisplayObject(); 112 | 113 | using var pixels = image.GetPixels(); 114 | 115 | for (int iy = 0; iy < amount.Y; iy++) 116 | { 117 | var h = Math.Min(currentSize.Y, sectionSize); 118 | currentSize.Y -= h; 119 | currentSize.X = image.Width; 120 | 121 | for (int ix = 0; ix < amount.X; ix++) 122 | { 123 | var w = Math.Min(currentSize.X, sectionSize); 124 | currentSize.X -= w; 125 | 126 | var texture = new Texture(w, h); 127 | var bytes = pixels.ToByteArray((int)pos.X, (int)pos.Y, w, h, PixelMapping.RGBA); 128 | texture.Update(bytes); 129 | var sprite = new Sprite(texture) { Position = new Vector2f(pos.X, pos.Y) }; 130 | largeTexture.AddChild(sprite); 131 | 132 | if (fileName is not null) 133 | { 134 | Textures.Add(texture); 135 | TextureFileNames.Add( 136 | fileName + "_" + ix.ToString("00") + "_" + iy.ToString("00") + "^" 137 | ); 138 | } 139 | 140 | pos.X += w; 141 | } 142 | pos.Y += h; 143 | pos.X = 0; 144 | } 145 | 146 | largeTexture.Texture.Size = new Vector2u(image.Width, image.Height); 147 | if (fileName is not null) 148 | { 149 | SplitTextures.Add(largeTexture); 150 | SplitTextureFileNames.Add(fileName); 151 | } 152 | 153 | return largeTexture; 154 | } 155 | 156 | public static MagickImage? GetMagickImage( 157 | string fileName, 158 | MagickReadSettings? settings = null 159 | ) 160 | { 161 | var info = Utils.ImageViewerUtils.GetMagickImageInfo(fileName); 162 | if (info is not null && info.Format == MagickFormat.Ico) 163 | return GetMagickImageIco(fileName); 164 | 165 | var image = settings is null 166 | ? new MagickImage( 167 | fileName, 168 | Utils.ImageViewerUtils.GetDefaultMagickReadSettings(s => 169 | s.BackgroundColor = MagickColors.None 170 | ) 171 | ) 172 | : new MagickImage(fileName, settings); 173 | if (image is null) 174 | return null; 175 | image.Format = MagickFormat.Rgba; 176 | return image; 177 | } 178 | 179 | /// Gets the highest resolution image in the .ico 180 | private static MagickImage? GetMagickImageIco(string fileName) 181 | { 182 | using var images = new MagickImageCollection(fileName); 183 | var best = images.OrderByDescending(i => i.Width * i.Height).First(); 184 | return new MagickImage(best); 185 | } 186 | 187 | /// Animated Image (ie: animated gif). 188 | public static AnimatedImage GetAnimatedImage(string fileName) 189 | { 190 | return new AnimatedImage(GetAnimatedImageData(fileName)); 191 | } 192 | 193 | /// Animated Image (ie: animated gif). 194 | public static AnimatedImageData GetAnimatedImageData(string fileName) 195 | { 196 | lock (AnimatedImageDatas) 197 | { 198 | int index = AnimatedImageDataFileNames.IndexOf(fileName); 199 | 200 | if (index >= 0) 201 | { 202 | // AnimatedImageData Already Exists 203 | // move it to the end of the array and return it 204 | var data = AnimatedImageDatas[index]; 205 | string name = AnimatedImageDataFileNames[index]; 206 | 207 | AnimatedImageDatas.RemoveAt(index); 208 | AnimatedImageDataFileNames.RemoveAt(index); 209 | AnimatedImageDatas.Add(data); 210 | AnimatedImageDataFileNames.Add(name); 211 | 212 | return AnimatedImageDatas[^1]; 213 | } 214 | else 215 | { 216 | // New AnimatedImageData 217 | var data = new AnimatedImageData(); 218 | 219 | // Store AnimatedImageData 220 | AnimatedImageDatas.Add(data); 221 | AnimatedImageDataFileNames.Add(fileName); 222 | 223 | // Limit amount of Animations in Memory 224 | if (AnimatedImageDatas.Count > MAX_ANIMATIONS) 225 | RemoveAnimatedImage(); 226 | 227 | // Get Frames 228 | var info = Utils.ImageViewerUtils.GetMagickImageInfo(fileName); 229 | if (info is not null && info.Format == MagickFormat.Gif) 230 | { 231 | // Use System.Drawing and OctreeQuantizer (faster and uses less memory) 232 | var loadingAnimatedImage = new LoadingAnimatedImage(fileName, data); 233 | var loadFramesThread = new Thread( 234 | new ThreadStart(loadingAnimatedImage.LoadFrames) 235 | ) 236 | { 237 | Name = "AnimationLoadThread - " + fileName, 238 | IsBackground = true, 239 | }; 240 | loadFramesThread.Start(); 241 | } 242 | else 243 | { 244 | // Use ImageMagick 245 | var loadingAnimatedImage = new LoadingAnimatedImageFromMagick( 246 | fileName, 247 | data 248 | ); 249 | var loadFramesThread = new Thread( 250 | new ThreadStart(loadingAnimatedImage.LoadFrames) 251 | ) 252 | { 253 | Name = "AnimationLoadThread - " + fileName, 254 | IsBackground = true, 255 | }; 256 | loadFramesThread.Start(); 257 | } 258 | 259 | // Wait for at least one frame to be loaded 260 | while (data.Frames == null || data.Frames.Length <= 0 || data.Frames[0] == null) 261 | { 262 | Thread.Sleep(1); 263 | } 264 | 265 | return data; 266 | } 267 | } 268 | } 269 | 270 | /// 271 | /// Clears all images/textures from memory (except the currently viewed image). 272 | /// Called from `-clearMemory` command or by using reset image with `Setting_ClearMemoryOnResetImage` enabled. 273 | /// 274 | /// The currently viewed image. 275 | /// The currently viewed image filepath. 276 | public static void ClearMemory(dynamic image, string file = "") 277 | { 278 | // Remove all AnimatedImages (except the one that's currently being viewed) 279 | int s = image is AnimatedImage ? 1 : 0; 280 | int a = 0; 281 | while (AnimatedImageDatas.Count > s) 282 | { 283 | if ( 284 | s == 1 285 | && image is AnimatedImage animatedImage 286 | && animatedImage.Data == AnimatedImageDatas[a] 287 | ) 288 | a++; 289 | RemoveAnimatedImage(a); 290 | } 291 | 292 | if (image is AnimatedImage) 293 | { 294 | // Remove all Textures 295 | while (TextureFileNames.Count > 0) 296 | RemoveTexture(); 297 | } 298 | else 299 | { 300 | // Remove all Textures (except ones being used by current image) 301 | if (file == "") 302 | return; 303 | 304 | s = 0; 305 | a = 0; 306 | if (SplitTextureFileNames.Contains(file)) 307 | { 308 | for (int i = 0; i < TextureFileNames.Count; i++) 309 | { 310 | if (TextureFileNames[i].StartsWith(file)) 311 | s++; 312 | else if (s > 0) 313 | break; 314 | } 315 | while (TextureFileNames.Count > s) 316 | { 317 | if (TextureFileNames[a].StartsWith(file)) 318 | a += s; 319 | RemoveTexture(a); 320 | } 321 | } 322 | else 323 | { 324 | while (TextureFileNames.Count > 1) 325 | { 326 | if (TextureFileNames[a] == file) 327 | a++; 328 | RemoveTexture(a); 329 | } 330 | } 331 | } 332 | 333 | // Force garbage collection 334 | GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); 335 | GC.WaitForPendingFinalizers(); 336 | } 337 | 338 | public static void RemoveTexture(int t = 0) 339 | { 340 | if (TextureFileNames[t].IndexOf('^') == TextureFileNames[t].Length - 1) 341 | { 342 | // if part of split texture - remove all parts 343 | string name = TextureFileNames[t][..^7]; 344 | 345 | int i; 346 | for (i = t + 1; i < TextureFileNames.Count; i++) 347 | { 348 | if (!TextureFileNames[i].StartsWith(name)) 349 | break; 350 | } 351 | for (int d = t; d < i; d++) 352 | { 353 | Textures[t]?.Dispose(); 354 | Textures.RemoveAt(t); 355 | TextureFileNames.RemoveAt(t); 356 | } 357 | 358 | int splitIndex = 359 | SplitTextureFileNames.Count == 0 ? -1 : SplitTextureFileNames.IndexOf(name); 360 | if (splitIndex != -1) 361 | { 362 | SplitTextures.RemoveAt(splitIndex); 363 | SplitTextureFileNames.RemoveAt(splitIndex); 364 | } 365 | } 366 | else 367 | { 368 | Textures[t]?.Dispose(); 369 | Textures.RemoveAt(t); 370 | TextureFileNames.RemoveAt(t); 371 | } 372 | } 373 | 374 | public static void RemoveAnimatedImage(int a = 0) 375 | { 376 | AnimatedImageDatas[a].CancelLoading = true; 377 | for (int i = 0; i < AnimatedImageDatas[a].Frames.Length; i++) 378 | AnimatedImageDatas[a]?.Frames[i]?.Dispose(); 379 | AnimatedImageDatas.RemoveAt(a); 380 | AnimatedImageDataFileNames.RemoveAt(a); 381 | } 382 | } 383 | 384 | internal class LoadingAnimatedImageFromMagick(string fileName, AnimatedImageData data) 385 | { 386 | private readonly string FileName = fileName; 387 | private readonly AnimatedImageData Data = data; 388 | 389 | public void LoadFrames() 390 | { 391 | var settings = new MagickReadSettings { }; 392 | 393 | var info = Utils.ImageViewerUtils.GetMagickImageInfo(FileName); 394 | if ( 395 | info is not null 396 | && (info.Format == MagickFormat.APng || info.Format == MagickFormat.Png) 397 | ) 398 | settings.Format = MagickFormat.APng; 399 | 400 | using var collection = new MagickImageCollection(); 401 | collection.Ping(FileName, settings); 402 | 403 | Data.FrameCount = collection.Count; 404 | Data.Frames = new Texture[Data.FrameCount]; 405 | Data.FrameDelays = new int[Data.FrameCount]; 406 | 407 | int defaultFrameDelay = AnimatedImage.DEFAULT_FRAME_DELAY; 408 | 409 | // Load first frame (so it can be shown as soon as possible) 410 | ReadAndLoadFrame(0); 411 | 412 | // Process the rest 413 | collection.Read(FileName, settings); 414 | collection.Coalesce(); // TODO: Find alternative that doesn't use as much resources 415 | for (int i = 0; i < Data.FrameCount; i++) 416 | { 417 | if (Data.CancelLoading) 418 | return; 419 | using var frame = collection[i]; 420 | LoadFrame(frame, i); 421 | } 422 | 423 | Data.FullyLoaded = true; 424 | } 425 | 426 | public void LoadFrame(IMagickImage frame, int index) 427 | { 428 | var delay = frame.AnimationDelay * 10; 429 | Data.FrameDelays[index] = delay > 0 ? (int)delay : AnimatedImage.DEFAULT_FRAME_DELAY; 430 | 431 | using var pixels = frame.GetPixelsUnsafe(); 432 | var bytes = pixels.ToByteArray(PixelMapping.RGBA); 433 | var texture = new Texture(frame.Width, frame.Height); 434 | texture.Update(bytes); 435 | 436 | Data.Frames[index] = texture; 437 | texture.Smooth = Data.Smooth; 438 | if (Data.Mipmap) 439 | texture.GenerateMipmap(); 440 | } 441 | 442 | public void ReadAndLoadFrame(int index) 443 | { 444 | using var frame = new MagickImage( 445 | FileName, 446 | new MagickReadSettings 447 | { 448 | FrameIndex = (uint)index, 449 | FrameCount = 1, 450 | BackgroundColor = MagickColors.None, 451 | } 452 | ); 453 | LoadFrame(frame, index); 454 | } 455 | } 456 | 457 | internal class LoadingAnimatedImage(string fileName, AnimatedImageData data) 458 | { 459 | private readonly string FileName = fileName; 460 | private ImageManipulation.OctreeQuantizer? Quantizer; 461 | private readonly AnimatedImageData Data = data; 462 | 463 | public void LoadFrames() 464 | { 465 | using var image = System.Drawing.Image.FromFile(FileName); 466 | 467 | // Get Frame Count 468 | var frameDimension = new System.Drawing.Imaging.FrameDimension( 469 | image.FrameDimensionsList[0] 470 | ); 471 | Data.FrameCount = image.GetFrameCount(frameDimension); 472 | Data.Frames = new Texture[Data.FrameCount]; 473 | Data.FrameDelays = new int[Data.FrameCount]; 474 | 475 | // Get Frame Delays 476 | byte[]? frameDelays = null; 477 | try 478 | { 479 | var frameDelaysItem = image.GetPropertyItem(0x5100); 480 | if (frameDelaysItem is not null) 481 | { 482 | frameDelays = frameDelaysItem.Value; 483 | if ( 484 | frameDelays is null 485 | || frameDelays.Length == 0 486 | || (frameDelays[0] == 0 && frameDelays.All(d => d == 0)) 487 | ) 488 | frameDelays = null; 489 | } 490 | } 491 | catch { } 492 | int defaultFrameDelay = AnimatedImage.DEFAULT_FRAME_DELAY; 493 | if (frameDelays != null && frameDelays.Length > 1) 494 | defaultFrameDelay = (frameDelays[0] + frameDelays[1] * 256) * 10; 495 | 496 | for (int i = 0; i < Data.FrameCount; i++) 497 | { 498 | if (Data.CancelLoading) 499 | return; 500 | 501 | int fd = i * 4; 502 | Data.FrameDelays[i] = 503 | frameDelays != null && frameDelays.Length > fd 504 | ? (frameDelays[fd] + frameDelays[fd + 1] * 256) * 10 505 | : defaultFrameDelay; 506 | 507 | _ = image.SelectActiveFrame(frameDimension, i); 508 | Quantizer = new ImageManipulation.OctreeQuantizer(255, 8); 509 | 510 | using var quantized = Quantizer.Quantize(image); 511 | using var stream = new MemoryStream(); 512 | quantized.Save(stream, System.Drawing.Imaging.ImageFormat.Png); 513 | var texture = new Texture(stream); 514 | 515 | Data.Frames[i] = texture; 516 | texture.Smooth = Data.Smooth; 517 | if (Data.Mipmap) 518 | texture.GenerateMipmap(); 519 | } 520 | Data.FullyLoaded = true; 521 | } 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /vimage/ImageManipulation/OctreeQuantizer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.nullskull.com/articles/StripImageFromAnimatedGif.asp 3 | 4 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 5 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 6 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 7 | PARTICULAR PURPOSE. 8 | 9 | This is sample code and is freely distributable. 10 | */ 11 | 12 | using System; 13 | using System.Collections; 14 | using System.Drawing; 15 | using System.Drawing.Imaging; 16 | 17 | namespace vimage.ImageManipulation 18 | { 19 | /// 20 | /// Quantize using an Octree 21 | /// 22 | public unsafe class OctreeQuantizer : Quantizer 23 | { 24 | /// 25 | /// Construct the octree quantizer 26 | /// 27 | /// 28 | /// The Octree quantizer is a two pass algorithm. The initial pass sets up the octree, 29 | /// the second pass quantizes a color based on the nodes in the tree 30 | /// 31 | /// The maximum number of colors to return 32 | /// The number of significant bits 33 | public OctreeQuantizer(int maxColors, int maxColorBits) 34 | : base(false) 35 | { 36 | if (maxColors > 255) 37 | { 38 | throw new ArgumentOutOfRangeException( 39 | "maxColors", 40 | maxColors, 41 | "The number of colors should be less than 256" 42 | ); 43 | } 44 | 45 | if ((maxColorBits < 1) | (maxColorBits > 8)) 46 | { 47 | throw new ArgumentOutOfRangeException( 48 | "maxColorBits", 49 | maxColorBits, 50 | "This should be between 1 and 8" 51 | ); 52 | } 53 | // Construct the octree 54 | _octree = new Octree(maxColorBits); 55 | 56 | _maxColors = maxColors; 57 | } 58 | 59 | /// 60 | /// Process the pixel in the first pass of the algorithm 61 | /// 62 | /// The pixel to quantize 63 | /// 64 | /// This function need only be overridden if your quantize algorithm needs two passes, 65 | /// such as an Octree quantizer. 66 | /// 67 | protected override void InitialQuantizePixel(Color32* pixel) 68 | { 69 | // Add the color to the octree 70 | _octree.AddColor(pixel); 71 | } 72 | 73 | /// 74 | /// Override this to process the pixel in the second pass of the algorithm 75 | /// 76 | /// The pixel to quantize 77 | /// The quantized value 78 | protected override byte QuantizePixel(Color32* pixel) 79 | { 80 | byte paletteIndex = (byte)_maxColors; // The color at [_maxColors] is set to transparent 81 | 82 | // Get the palette index if this non-transparent 83 | if (pixel->Alpha > 0) 84 | paletteIndex = (byte)_octree.GetPaletteIndex(pixel); 85 | 86 | return paletteIndex; 87 | } 88 | 89 | /// 90 | /// Retrieve the palette for the quantized image 91 | /// 92 | /// Any old palette, this is overrwritten 93 | /// The new color palette 94 | protected override ColorPalette GetPalette(ColorPalette original) 95 | { 96 | // First off convert the octree to _maxColors colors 97 | var palette = _octree.Palletize(_maxColors - 1); 98 | 99 | // Then convert the palette based on those colors 100 | for (int index = 0; index < palette.Count; index++) 101 | { 102 | var p = palette[index]; 103 | original.Entries[index] = p is null ? Color.White : (Color)p; 104 | } 105 | 106 | // Add the transparent color 107 | original.Entries[_maxColors] = Color.FromArgb(0, 0, 0, 0); 108 | 109 | return original; 110 | } 111 | 112 | /// 113 | /// Stores the tree 114 | /// 115 | private readonly Octree _octree; 116 | 117 | /// 118 | /// Maximum allowed color depth 119 | /// 120 | private readonly int _maxColors; 121 | 122 | /// 123 | /// Class which does the actual quantization 124 | /// 125 | private class Octree 126 | { 127 | /// 128 | /// Construct the octree 129 | /// 130 | /// The maximum number of significant bits in the image 131 | public Octree(int maxColorBits) 132 | { 133 | _maxColorBits = maxColorBits; 134 | _leafCount = 0; 135 | _reducibleNodes = new OctreeNode[9]; 136 | _root = new OctreeNode(0, _maxColorBits, this); 137 | _previousColor = 0; 138 | _previousNode = null; 139 | } 140 | 141 | /// 142 | /// Add a given color value to the octree 143 | /// 144 | /// 145 | public void AddColor(Color32* pixel) 146 | { 147 | // Check if this request is for the same color as the last 148 | if (_previousColor == pixel->ARGB) 149 | { 150 | // If so, check if I have a previous node setup. This will only ocurr if the first color in the image 151 | // happens to be black, with an alpha component of zero. 152 | if (null == _previousNode) 153 | { 154 | _previousColor = pixel->ARGB; 155 | _root.AddColor(pixel, _maxColorBits, 0, this); 156 | } 157 | else 158 | // Just update the previous node 159 | _previousNode.Increment(pixel); 160 | } 161 | else 162 | { 163 | _previousColor = pixel->ARGB; 164 | _root.AddColor(pixel, _maxColorBits, 0, this); 165 | } 166 | } 167 | 168 | /// 169 | /// Reduce the depth of the tree 170 | /// 171 | public void Reduce() 172 | { 173 | int index; 174 | 175 | // Find the deepest level containing at least one reducible node 176 | for ( 177 | index = _maxColorBits - 1; 178 | (index > 0) && (null == _reducibleNodes[index]); 179 | index-- 180 | ) 181 | ; 182 | 183 | // Reduce the node most recently added to the list at level 'index' 184 | var node = _reducibleNodes[index]; 185 | _reducibleNodes[index] = node?.NextReducible; 186 | 187 | // Decrement the leaf count after reducing the node 188 | if (node is not null) 189 | _leafCount -= node.Reduce(); 190 | 191 | // And just in case I've reduced the last color to be added, and the next color to 192 | // be added is the same, invalidate the previousNode... 193 | _previousNode = null; 194 | } 195 | 196 | /// 197 | /// Get/Set the number of leaves in the tree 198 | /// 199 | public int Leaves 200 | { 201 | get { return _leafCount; } 202 | set { _leafCount = value; } 203 | } 204 | 205 | /// 206 | /// Return the array of reducible nodes 207 | /// 208 | protected OctreeNode?[] ReducibleNodes 209 | { 210 | get { return _reducibleNodes; } 211 | } 212 | 213 | /// 214 | /// Keep track of the previous node that was quantized 215 | /// 216 | /// The node last quantized 217 | protected void TrackPrevious(OctreeNode node) 218 | { 219 | _previousNode = node; 220 | } 221 | 222 | /// 223 | /// Convert the nodes in the octree to a palette with a maximum of colorCount colors 224 | /// 225 | /// The maximum number of colors 226 | /// An arraylist with the palettized colors 227 | public ArrayList Palletize(int colorCount) 228 | { 229 | while (Leaves > colorCount) 230 | Reduce(); 231 | 232 | // Now palettize the nodes 233 | var palette = new ArrayList(Leaves); 234 | int paletteIndex = 0; 235 | _root.ConstructPalette(palette, ref paletteIndex); 236 | 237 | // And return the palette 238 | return palette; 239 | } 240 | 241 | /// 242 | /// Get the palette index for the passed color 243 | /// 244 | /// 245 | /// 246 | public int GetPaletteIndex(Color32* pixel) 247 | { 248 | return _root.GetPaletteIndex(pixel, 0); 249 | } 250 | 251 | /// 252 | /// Mask used when getting the appropriate pixels for a given node 253 | /// 254 | private static readonly int[] mask = new int[8] 255 | { 256 | 0x80, 257 | 0x40, 258 | 0x20, 259 | 0x10, 260 | 0x08, 261 | 0x04, 262 | 0x02, 263 | 0x01, 264 | }; 265 | 266 | /// 267 | /// The root of the octree 268 | /// 269 | private readonly OctreeNode _root; 270 | 271 | /// 272 | /// Number of leaves in the tree 273 | /// 274 | private int _leafCount; 275 | 276 | /// 277 | /// Array of reducible nodes 278 | /// 279 | private readonly OctreeNode?[] _reducibleNodes; 280 | 281 | /// 282 | /// Maximum number of significant bits in the image 283 | /// 284 | private readonly int _maxColorBits; 285 | 286 | /// 287 | /// Store the last node quantized 288 | /// 289 | private OctreeNode? _previousNode; 290 | 291 | /// 292 | /// Cache the previous color quantized 293 | /// 294 | private int _previousColor; 295 | 296 | /// 297 | /// Class which encapsulates each node in the tree 298 | /// 299 | protected class OctreeNode 300 | { 301 | /// 302 | /// Construct the node 303 | /// 304 | /// The level in the tree = 0 - 7 305 | /// The number of significant color bits in the image 306 | /// The tree to which this node belongs 307 | public OctreeNode(int level, int colorBits, Octree octree) 308 | { 309 | // Construct the new node 310 | _leaf = level == colorBits; 311 | 312 | _red = _green = _blue = 0; 313 | _pixelCount = 0; 314 | 315 | // If a leaf, increment the leaf count 316 | if (_leaf) 317 | { 318 | octree.Leaves++; 319 | _nextReducible = null; 320 | _children = null; 321 | } 322 | else 323 | { 324 | // Otherwise add this to the reducible nodes 325 | _nextReducible = octree.ReducibleNodes[level]; 326 | octree.ReducibleNodes[level] = this; 327 | _children = new OctreeNode[8]; 328 | } 329 | } 330 | 331 | /// 332 | /// Add a color into the tree 333 | /// 334 | /// The color 335 | /// The number of significant color bits 336 | /// The level in the tree 337 | /// The tree to which this node belongs 338 | public void AddColor(Color32* pixel, int colorBits, int level, Octree octree) 339 | { 340 | // Update the color information if this is a leaf 341 | if (_leaf) 342 | { 343 | Increment(pixel); 344 | // Setup the previous node 345 | octree.TrackPrevious(this); 346 | } 347 | else 348 | { 349 | // Go to the next level down in the tree 350 | int shift = 7 - level; 351 | int index = 352 | ((pixel->Red & mask[level]) >> (shift - 2)) 353 | | ((pixel->Green & mask[level]) >> (shift - 1)) 354 | | ((pixel->Blue & mask[level]) >> (shift)); 355 | 356 | var child = _children?[index]; 357 | 358 | if (child is null) 359 | { 360 | // Create a new child node & store in the array 361 | child = new OctreeNode(level + 1, colorBits, octree); 362 | if (_children is not null) 363 | _children[index] = child; 364 | } 365 | 366 | // Add the color to the child node 367 | child.AddColor(pixel, colorBits, level + 1, octree); 368 | } 369 | } 370 | 371 | /// 372 | /// Get/Set the next reducible node 373 | /// 374 | public OctreeNode? NextReducible 375 | { 376 | get { return _nextReducible; } 377 | set { _nextReducible = value; } 378 | } 379 | 380 | /// 381 | /// Return the child nodes 382 | /// 383 | public OctreeNode?[]? Children 384 | { 385 | get { return _children; } 386 | } 387 | 388 | /// 389 | /// Reduce this node by removing all of its children 390 | /// 391 | /// The number of leaves removed 392 | public int Reduce() 393 | { 394 | _red = _green = _blue = 0; 395 | int children = 0; 396 | 397 | // Loop through all children and add their information to this node 398 | if (_children is not null) 399 | { 400 | for (int index = 0; index < 8; index++) 401 | { 402 | if (_children[index] is OctreeNode child) 403 | { 404 | _red += child._red; 405 | _green += child._green; 406 | _blue += child._blue; 407 | _pixelCount += child._pixelCount; 408 | ++children; 409 | _children[index] = null; 410 | } 411 | } 412 | } 413 | 414 | // Now change this to a leaf node 415 | _leaf = true; 416 | 417 | // Return the number of nodes to decrement the leaf count by 418 | return children - 1; 419 | } 420 | 421 | /// 422 | /// Traverse the tree, building up the color palette 423 | /// 424 | /// The palette 425 | /// The current palette index 426 | public void ConstructPalette(ArrayList palette, ref int paletteIndex) 427 | { 428 | if (_leaf) 429 | { 430 | // Consume the next palette index 431 | _paletteIndex = paletteIndex++; 432 | 433 | // And set the color of the palette entry 434 | int r = Math.Clamp(_red / _pixelCount, 0, 255); 435 | int g = Math.Clamp(_green / _pixelCount, 0, 255); 436 | int b = Math.Clamp(_blue / _pixelCount, 0, 255); 437 | _ = palette.Add(Color.FromArgb(r, g, b)); 438 | } 439 | else 440 | { 441 | // Loop through children looking for leaves 442 | if (_children is null) 443 | return; 444 | for (int index = 0; index < 8; index++) 445 | { 446 | _children[index]?.ConstructPalette(palette, ref paletteIndex); 447 | } 448 | } 449 | } 450 | 451 | /// 452 | /// Return the palette index for the passed color 453 | /// 454 | public int GetPaletteIndex(Color32* pixel, int level) 455 | { 456 | int paletteIndex = _paletteIndex; 457 | 458 | if (!_leaf) 459 | { 460 | int shift = 7 - level; 461 | int index = 462 | ((pixel->Red & mask[level]) >> (shift - 2)) 463 | | ((pixel->Green & mask[level]) >> (shift - 1)) 464 | | ((pixel->Blue & mask[level]) >> (shift)); 465 | 466 | if (_children is not null && _children[index] is OctreeNode child) 467 | paletteIndex = child.GetPaletteIndex(pixel, level + 1); 468 | else 469 | throw new Exception("Didn't expect this!"); 470 | } 471 | 472 | return paletteIndex; 473 | } 474 | 475 | /// 476 | /// Increment the pixel count and add to the color information 477 | /// 478 | public void Increment(Color32* pixel) 479 | { 480 | _pixelCount++; 481 | _red += pixel->Red; 482 | _green += pixel->Green; 483 | _blue += pixel->Blue; 484 | } 485 | 486 | /// 487 | /// Flag indicating that this is a leaf node 488 | /// 489 | private bool _leaf; 490 | 491 | /// 492 | /// Number of pixels in this node 493 | /// 494 | private int _pixelCount; 495 | 496 | /// 497 | /// Red component 498 | /// 499 | private int _red; 500 | 501 | /// 502 | /// Green Component 503 | /// 504 | private int _green; 505 | 506 | /// 507 | /// Blue component 508 | /// 509 | private int _blue; 510 | 511 | /// 512 | /// Pointers to any child nodes 513 | /// 514 | private readonly OctreeNode?[]? _children; 515 | 516 | /// 517 | /// Pointer to next reducible node 518 | /// 519 | private OctreeNode? _nextReducible; 520 | 521 | /// 522 | /// The index of this node in the palette 523 | /// 524 | private int _paletteIndex; 525 | } 526 | } 527 | } 528 | } 529 | --------------------------------------------------------------------------------