├── logo.png ├── logo.xcf ├── screenshot.png ├── UserInterface ├── icon.ico ├── App.config ├── Properties │ ├── Settings.settings │ ├── Settings.Designer.cs │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Controls │ ├── EnumDropDown.Designer.cs │ ├── ProgressBarText.Designer.cs │ ├── EnumFlagsDropDown.Designer.cs │ ├── CheckBoxDropDown.Designer.cs │ ├── FileBrowseControl.Designer.cs │ ├── EnumDropDown.cs │ ├── FileBrowseControl.cs │ ├── EnumFlagsDropDown.cs │ ├── ProgressBarText.cs │ ├── FileBrowseControl.resx │ ├── EnumDropDown.resx │ ├── CheckBoxDropDown.resx │ └── CheckBoxDropDown.cs ├── Program.cs ├── ExtensionMethods.cs ├── ParseArgs.cs ├── CLI.cs ├── UserInterface.csproj └── Main.cs ├── MediaOrganizer ├── MediaOrganizer.ini ├── CopyItems.cs ├── MediaOrganizerException.cs ├── Properties │ └── AssemblyInfo.cs ├── CopyItem.cs ├── MediaOrganizer.csproj ├── IniFile.cs └── ExtensionMethods.cs ├── README.md ├── MetaParser ├── Parsers │ ├── Parser.cs │ ├── DirectoryParser.cs │ ├── GenericMediaParser.cs │ ├── FileParser.cs │ ├── BmpParser.cs │ ├── MP4Parser.cs │ └── PNGParser.cs ├── MetaParseException.cs ├── Properties │ └── AssemblyInfo.cs ├── ExtensionMethods.cs ├── MetaParser.csproj ├── MetaData.cs └── MetaParser.cs ├── TODO ├── Common ├── CommonException.cs ├── Properties │ └── AssemblyInfo.cs ├── Common.csproj └── ExtensionMethods.cs ├── MediaQuerier ├── MediaQuerierException.cs ├── Properties │ └── AssemblyInfo.cs ├── MediaQuerier.csproj └── MediaQuerier.cs ├── ExifOrganizer.sln └── .gitignore /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiJo/ExifOrganizer/HEAD/logo.png -------------------------------------------------------------------------------- /logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiJo/ExifOrganizer/HEAD/logo.xcf -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiJo/ExifOrganizer/HEAD/screenshot.png -------------------------------------------------------------------------------- /UserInterface/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiJo/ExifOrganizer/HEAD/UserInterface/icon.ico -------------------------------------------------------------------------------- /UserInterface/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /UserInterface/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MediaOrganizer/MediaOrganizer.ini: -------------------------------------------------------------------------------- 1 | sourcePath: C:\temp 2 | destinationPath: C:\temp\backup 3 | recursive: 1 4 | locale: sv-SE 5 | patternImage: %y/%m %M/%t/%n.%e 6 | patternAudio: %y/%m %M/Audio/%a/%k %A - %T.%e 7 | patternVideo: %y/%m %M/Video/%t/%n.%e 8 | precondition: None 9 | comparator: FileSize, ChecksumMD5 10 | copyMode: KeepUnique 11 | exceptionHandling: Throw 12 | fileVerification: ChecksumMD5 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExifOrganizer 2 | ============= 3 | 4 | Organize image collections based on Exif data with customizable destination paths. 5 | ### Status 6 | Currently in development of first beta version. 7 | 8 | ## Description 9 | The idea is to select a source path to be organized, where media files (mainly images, but also videos and audio) are gathered from. Those files are then copied and structured into a destination path. How the media files are organized is configurable via a graphical user interface (GUI) or by command line interface (CLI) for scripting. 10 | 11 | ### Supported meta 12 | * Exif - .jpeg .jpg .tiff .tif 13 | * MP4 - .mp4 .m4a .mov .3gp .3g2 14 | * ID3 - .mp3 15 | * PNG - .png 16 | 17 | Most other media files are parsed in a generic way and lacks the detailed meta data. 18 | 19 | ### Screenshot 20 | ![ExifOrganizer GUI](screenshot.png) 21 | 22 | ## Environment 23 | Everything is written in C# 6.0 and .NET 4.5 using Visual Studio 2017. There are no external dependencies. 24 | 25 | ## License 26 | This project is licensed under GPLv3. 27 | 28 | -------------------------------------------------------------------------------- /MetaParser/Parsers/Parser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Parser.cs: Base class for meta data parser classes. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System.Collections.Generic; 20 | using System.IO; 21 | using System.Threading.Tasks; 22 | 23 | namespace ExifOrganizer.Meta.Parsers 24 | { 25 | internal abstract class Parser 26 | { 27 | internal abstract MetaData Parse(string path); 28 | internal abstract Task ParseAsync(string path); 29 | } 30 | } -------------------------------------------------------------------------------- /UserInterface/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.18444 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 ExifOrganizer.UI.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.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 | -------------------------------------------------------------------------------- /UserInterface/Controls/EnumDropDown.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ExifOrganizer.UI.Controls 2 | { 3 | partial class EnumDropDown 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.SuspendLayout(); 32 | // 33 | // CheckBoxDropDown 34 | // 35 | this.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; 36 | this.ResumeLayout(false); 37 | 38 | } 39 | 40 | #endregion 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /UserInterface/Controls/ProgressBarText.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ExifOrganizer.UI.Controls 2 | { 3 | partial class ProgressBarText 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | components = new System.ComponentModel.Container(); 32 | } 33 | 34 | #endregion 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UserInterface/Controls/EnumFlagsDropDown.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ExifOrganizer.UI.Controls 2 | { 3 | partial class EnumFlagsDropDown 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | components = new System.ComponentModel.Container(); 32 | } 33 | 34 | #endregion 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Features: 2 | 0.2 - Handle tags across years/months (e.g. "New year") 3 | - Comments in ini file using IniFile class 4 | - Multiple source/destination paths to be used 5 | - Favorites (stars) function for albums (e.g. "Pick out the best images from 2014") 6 | - Use database (SQLite?) to cache meta data etc 7 | 8 | Library: 9 | - Extract common functions (like extensions) to separate library 10 | 11 | GUI: 12 | 0.2 - separate settings/advanced form for improved usability 13 | - button to run prepare and prinotut information about copying 14 | - Separate progress of Parse() and Organize into 0.0-1.0 for each + message in event 15 | - Printout how many skipped files (grouped by extension/type) 16 | 17 | Bugs 0.1: 18 | - Printout elapsed time during progress as well as in completed message dialogue (separate times for image, audio and video files. Also separate parsing and organazation time elapsed) 19 | - Disable more controls during organization? Currently only submit-button 20 | - Add Abort-button to abort current progress 21 | - New option: merge source w/ destination files (also parse target path) 22 | - Checkboxes to skip image, audio, video files (merge w/ patterns?) 23 | - Property to set extensions of image, audio and video files -------------------------------------------------------------------------------- /UserInterface/Controls/CheckBoxDropDown.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ExifOrganizer.UI.Controls 2 | { 3 | partial class CheckBoxDropDown 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.SuspendLayout(); 32 | // 33 | // CheckBoxDropDown 34 | // 35 | this.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; 36 | this.ResumeLayout(false); 37 | 38 | } 39 | 40 | #endregion 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MediaOrganizer/CopyItems.cs: -------------------------------------------------------------------------------- 1 | // 2 | // CopyItems.cs: Data structure describing multiple items to be copied. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | 22 | namespace ExifOrganizer.Organizer 23 | { 24 | public class CopyItems : List 25 | { 26 | public string sourcePath; 27 | public string destinationPath; 28 | 29 | public override string ToString() 30 | { 31 | return $"Copy: [{sourcePath}] ---> [{destinationPath}]{Environment.NewLine}Items:{Environment.NewLine}{String.Join(Environment.NewLine, this)}"; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Common/CommonException.cs: -------------------------------------------------------------------------------- 1 | // 2 | // CommonException.cs: Custom exception class. 3 | // 4 | // Copyright (C) 2018 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Runtime.Serialization; 21 | 22 | namespace ExifOrganizer.Common 23 | { 24 | [Serializable] 25 | public class CommonException : Exception 26 | { 27 | public CommonException(string message, params object[] args) 28 | : base(String.Format(message, args)) 29 | { 30 | } 31 | 32 | public CommonException(string message, Exception innerException) 33 | : base(message, innerException) 34 | { 35 | } 36 | 37 | protected CommonException(SerializationInfo info, StreamingContext context) 38 | : base(info, context) 39 | { 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /MediaQuerier/MediaQuerierException.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MediaQuerierException.cs: Custom exception class. 3 | // 4 | // Copyright (C) 2016 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using System; 21 | using System.Runtime.Serialization; 22 | 23 | namespace ExifOrganizer.Querier 24 | { 25 | [Serializable] 26 | public class MediaQuerierException : CommonException 27 | { 28 | public MediaQuerierException(string message, params object[] args) 29 | : base(message, args) 30 | { 31 | } 32 | 33 | public MediaQuerierException(string message, Exception innerException) 34 | : base(message, innerException) 35 | { 36 | } 37 | 38 | protected MediaQuerierException(SerializationInfo info, StreamingContext context) 39 | : base(info, context) 40 | { 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /MetaParser/MetaParseException.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MetaParseException.cs: Custom exception class. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using System; 21 | using System.Runtime.Serialization; 22 | 23 | namespace ExifOrganizer.Meta 24 | { 25 | [Serializable] 26 | public class MetaParseException : CommonException 27 | { 28 | public MetaParseException(string message, params object[] args) 29 | : base(message, args) 30 | { 31 | } 32 | 33 | public MetaParseException(string message, Exception innerException) 34 | : base(message, innerException) 35 | { 36 | } 37 | 38 | protected MetaParseException(SerializationInfo info, StreamingContext context) 39 | : base(info, context) 40 | { 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Common/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Common")] 9 | [assembly: AssemblyDescription("Library containing common shared implementation details.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Common")] 13 | [assembly: AssemblyCopyright("Licensed under GPLv3")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("06a0bc9e-9100-4877-89a6-f5798bd3c7a3")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.1.0.*")] 36 | //[assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /MediaQuerier/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MediaQuerier")] 9 | [assembly: AssemblyDescription("Library used to query media files based on its properties.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("MediaQuerier")] 13 | [assembly: AssemblyCopyright("Licensed under GPLv3")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7070940a-c83f-4d3d-bde4-55aee58a9385")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.1.1.*")] 36 | //[assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /MediaOrganizer/MediaOrganizerException.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MediaOrganizerException.cs: Custom exception class. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using System; 21 | using System.Runtime.Serialization; 22 | 23 | namespace ExifOrganizer.Organizer 24 | { 25 | [Serializable] 26 | public class MediaOrganizerException : CommonException 27 | { 28 | public MediaOrganizerException(string message, params object[] args) 29 | : base(message, args) 30 | { 31 | } 32 | 33 | public MediaOrganizerException(string message, Exception innerException) 34 | : base(message, innerException) 35 | { 36 | } 37 | 38 | protected MediaOrganizerException(SerializationInfo info, StreamingContext context) 39 | : base(info, context) 40 | { 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /MetaParser/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ExifParser")] 9 | [assembly: AssemblyDescription("Library used to parse meta data from media files.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ExifParser")] 13 | [assembly: AssemblyCopyright("Licensed under GPLv3")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b29e9ab1-7c16-4eb9-8228-acc78be02702")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.2.0.*")] 36 | //[assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /MediaOrganizer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MediaOrganizer")] 9 | [assembly: AssemblyDescription("Library used to organize media files based on its meta data.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("MediaOrganizer")] 13 | [assembly: AssemblyCopyright("Licensed under GPLv3")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("d5e17db3-82df-4059-9fc4-b2014d354efc")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.2.0.*")] 36 | //[assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /UserInterface/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ExifOrganizer")] 9 | [assembly: AssemblyDescription("User interface (GUI and CLI) of ExifOrganizer application.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ExifOrganizer")] 13 | [assembly: AssemblyCopyright("Licensed under GPLv3")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f3f21b1e-f1cf-4d49-a14d-a66fd5b068b1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.3.0.*")] 36 | //[assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /MetaParser/Parsers/DirectoryParser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryParser.cs: Directory meta data parser class. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System.IO; 20 | using System.Threading.Tasks; 21 | 22 | namespace ExifOrganizer.Meta.Parsers 23 | { 24 | internal class DirectoryParser : Parser 25 | { 26 | internal override MetaData Parse(string path) 27 | { 28 | Task task = ParseAsync(path); 29 | task.ConfigureAwait(false); // Prevent deadlock of caller 30 | return task.Result; 31 | } 32 | 33 | internal override Task ParseAsync(string path) 34 | { 35 | return Task.FromResult(ParseThread(path)); 36 | } 37 | 38 | private MetaData ParseThread(string path) 39 | { 40 | if (!Directory.Exists(path)) 41 | throw new MetaParseException("Directory not found: {0}", path); 42 | 43 | MetaData meta = new MetaData(); 44 | meta.Type = MetaType.Directory; 45 | meta.Path = path; 46 | return meta; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /MetaParser/Parsers/GenericMediaParser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // GenericFileParser.cs: Generic meta data parser class. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System.Collections.Generic; 20 | using System.IO; 21 | using System.Threading.Tasks; 22 | using System.Linq; 23 | 24 | namespace ExifOrganizer.Meta.Parsers 25 | { 26 | internal class GenericMediaParser : FileParser 27 | { 28 | internal override IEnumerable GetSupportedFileExtensions() 29 | { 30 | return new string[] { ".gif", "wav", ".flac", ".aac", ".mpg", ".mpeg" }; 31 | } 32 | 33 | internal override MetaType? GetMetaTypeByFileExtension(string extension) 34 | { 35 | switch (extension) 36 | { 37 | case ".gif": 38 | case ".bmp": 39 | return MetaType.Image; 40 | case ".wav": 41 | case ".flac": 42 | case ".aac": 43 | return MetaType.Audio; 44 | case ".mpg": 45 | case ".mpeg": 46 | return MetaType.Video; 47 | default: 48 | return base.GetMetaTypeByFileExtension(extension); 49 | } 50 | } 51 | 52 | protected override MetaData ParseFile(Stream stream, MetaData meta) 53 | { 54 | return meta; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /MetaParser/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionMethods.cs: Static extension methods used within the project. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.IO; 23 | using System.Linq; 24 | 25 | namespace ExifOrganizer.Meta 26 | { 27 | public static class ExtensionMethods 28 | { 29 | public static MetaData Merge(this MetaData meta, MetaData other) 30 | { 31 | if (meta.Path != other.Path) 32 | throw new MetaParseException("Cannot merge meta data: path differ"); 33 | //#if DEBUG 34 | // if (meta.Origin != other.Origin) 35 | // throw new MetaParseException("Cannot merge meta data: origin differ"); 36 | //#endif 37 | if (meta.Type != other.Type) 38 | throw new MetaParseException("Cannot merge meta data: type differ"); 39 | 40 | foreach (var kvp in other.Data) 41 | { 42 | if (meta.Data.ContainsKey(kvp.Key)) 43 | { 44 | if (kvp.Value != meta.Data[kvp.Key]) 45 | Trace.WriteLine($"[MetaData] merge ignore duplicate key: \"{kvp.Key}\""); 46 | continue; 47 | } 48 | 49 | meta.Data[kvp.Key] = kvp.Value; 50 | } 51 | 52 | return meta; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /UserInterface/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Program.cs: Program main function entry point. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Runtime.InteropServices; 21 | using System.Windows.Forms; 22 | 23 | namespace ExifOrganizer.UI 24 | { 25 | internal static class Program 26 | { 27 | [DllImport("kernel32.dll")] 28 | private static extern bool AttachConsole(int dwProcessId); 29 | 30 | private const int ATTACH_PARENT_PROCESS = -1; 31 | 32 | /// 33 | /// The main entry point for the application. 34 | /// 35 | [STAThread] 36 | private static int Main(string[] args) 37 | { 38 | if (args.Length == 0) 39 | { 40 | return GUI(args); 41 | } 42 | else 43 | { 44 | return CLI(args); 45 | } 46 | } 47 | 48 | private static int GUI(string[] args) 49 | { 50 | // Graphical interface 51 | Application.EnableVisualStyles(); 52 | Application.SetCompatibleTextRenderingDefault(false); 53 | Application.Run(new Main()); 54 | return 0; 55 | } 56 | 57 | private static int CLI(string[] args) 58 | { 59 | // Make Console.Write() work 60 | AttachConsole(ATTACH_PARENT_PROCESS); 61 | 62 | // Command line interface 63 | CLI cli = new CLI(); 64 | return cli.Run(args); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /MediaOrganizer/CopyItem.cs: -------------------------------------------------------------------------------- 1 | // 2 | // CopyItem.cs: Data structure describing single item to be copied. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using ExifOrganizer.Meta; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.IO; 24 | 25 | namespace ExifOrganizer.Organizer 26 | { 27 | public class CopyItem 28 | { 29 | public string sourcePath; 30 | public string destinationPath; 31 | public FileInfo sourceInfo; 32 | public Dictionary meta; 33 | 34 | private Dictionary checksums = new Dictionary(); 35 | 36 | public CopyItem() 37 | { 38 | } 39 | 40 | public string GetChecksumMD5() 41 | { 42 | if (!checksums.ContainsKey("md5")) 43 | checksums["md5"] = sourceInfo.GetMD5Sum(); 44 | return checksums["md5"]; 45 | } 46 | 47 | public string GetChecksumSHA1() 48 | { 49 | if (!checksums.ContainsKey("sha1")) 50 | checksums["sha1"] = sourceInfo.GetSHA1Sum(); 51 | return checksums["sha1"]; 52 | } 53 | 54 | public string GetChecksumSHA256() 55 | { 56 | if (!checksums.ContainsKey("sha256")) 57 | checksums["sha256"] = sourceInfo.GetSHA256Sum(); 58 | return checksums["sha256"]; 59 | } 60 | 61 | public override string ToString() 62 | { 63 | return $"[{sourcePath}] ---> [{destinationPath}]"; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Common/Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {06A0BC9E-9100-4877-89A6-F5798BD3C7A3} 8 | Library 9 | Properties 10 | ExifOrganizer.Common 11 | ExifOrganizer.Common 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MetaParser/Parsers/FileParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using ExifOrganizer.Common; 8 | 9 | namespace ExifOrganizer.Meta.Parsers 10 | { 11 | internal abstract class FileParser : Parser 12 | { 13 | internal abstract IEnumerable GetSupportedFileExtensions(); 14 | internal virtual MetaType? GetMetaTypeByFileExtension(string extension) { return null; } 15 | 16 | internal override MetaData Parse(string filename) 17 | { 18 | Task task = ParseAsync(filename); 19 | task.ConfigureAwait(false); // Prevent deadlock of caller 20 | return task.Result; 21 | } 22 | 23 | internal override Task ParseAsync(string filename) 24 | { 25 | MetaData meta = GetBaseMetaFileData(filename); 26 | return Task.Run(() => { using (FileStream stream = File.OpenRead(filename)) return ParseFile(stream, meta); }); 27 | } 28 | 29 | protected abstract MetaData ParseFile(Stream stream, MetaData meta); 30 | 31 | private MetaData GetBaseMetaFileData(string filename) 32 | { 33 | if (!File.Exists(filename)) 34 | throw new MetaParseException("File not found: {0}", filename); 35 | 36 | string extension = Path.GetExtension(filename).ToLower(); 37 | MetaType? type = GetMetaTypeByFileExtension(extension); 38 | if (!type.HasValue) 39 | throw new MetaParseException($"Target file extension not handled by parser: {extension}"); 40 | 41 | MetaData meta = new MetaData(); 42 | meta.Path = filename; 43 | meta.Type = type.Value; 44 | meta.Data = new Dictionary(); 45 | meta.Data[MetaKey.FileName] = Path.GetFileName(filename); 46 | meta.Data[MetaKey.OriginalName] = meta.Data[MetaKey.FileName]; 47 | meta.Data[MetaKey.Size] = GetFileSize(filename); 48 | meta.Data[MetaKey.DateCreated] = File.GetCreationTime(filename); 49 | meta.Data[MetaKey.DateModified] = File.GetLastWriteTime(filename); 50 | meta.Data[MetaKey.Timestamp] = meta.Data[MetaKey.DateModified]; 51 | meta.Data[MetaKey.Tags] = new string[0]; 52 | 53 | return meta; 54 | } 55 | 56 | private static long GetFileSize(string filename) 57 | { 58 | return new FileInfo(filename).Length; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /UserInterface/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.18444 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 ExifOrganizer.UI.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", "4.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("ExifOrganizer.UI.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 | -------------------------------------------------------------------------------- /UserInterface/Controls/FileBrowseControl.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ExifOrganizer.UI.Controls 2 | { 3 | partial class FileBrowseControl 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.browse = new System.Windows.Forms.Button(); 32 | this.path = new System.Windows.Forms.TextBox(); 33 | this.SuspendLayout(); 34 | // 35 | // browse 36 | // 37 | this.browse.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 38 | this.browse.Location = new System.Drawing.Point(271, 0); 39 | this.browse.Name = "browse"; 40 | this.browse.Size = new System.Drawing.Size(25, 23); 41 | this.browse.TabIndex = 1; 42 | this.browse.Text = "..."; 43 | this.browse.UseVisualStyleBackColor = true; 44 | this.browse.Click += new System.EventHandler(this.browse_Click); 45 | // 46 | // path 47 | // 48 | this.path.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 49 | | System.Windows.Forms.AnchorStyles.Right))); 50 | this.path.Location = new System.Drawing.Point(0, 2); 51 | this.path.Name = "path"; 52 | this.path.Size = new System.Drawing.Size(265, 20); 53 | this.path.TabIndex = 0; 54 | // 55 | // FileBrowseControl 56 | // 57 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 58 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 59 | this.Controls.Add(this.path); 60 | this.Controls.Add(this.browse); 61 | this.Name = "FileBrowseControl"; 62 | this.Size = new System.Drawing.Size(296, 23); 63 | this.ResumeLayout(false); 64 | this.PerformLayout(); 65 | 66 | } 67 | 68 | #endregion 69 | 70 | private System.Windows.Forms.Button browse; 71 | private System.Windows.Forms.TextBox path; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ExifOrganizer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2015 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserInterface", "UserInterface\UserInterface.csproj", "{8F6FD0D9-E15F-4701-961E-3075BFB1CDA5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaParser", "MetaParser\MetaParser.csproj", "{19204B12-E95E-405F-9604-52DE1411C8B1}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaOrganizer", "MediaOrganizer\MediaOrganizer.csproj", "{626F29DA-4334-4E7E-8D9A-B2353BB37FC1}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaQuerier", "MediaQuerier\MediaQuerier.csproj", "{7070940A-C83F-4D3D-BDE4-55AEE58A9385}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{06A0BC9E-9100-4877-89A6-F5798BD3C7A3}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {8F6FD0D9-E15F-4701-961E-3075BFB1CDA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {8F6FD0D9-E15F-4701-961E-3075BFB1CDA5}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {8F6FD0D9-E15F-4701-961E-3075BFB1CDA5}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {8F6FD0D9-E15F-4701-961E-3075BFB1CDA5}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {19204B12-E95E-405F-9604-52DE1411C8B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {19204B12-E95E-405F-9604-52DE1411C8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {19204B12-E95E-405F-9604-52DE1411C8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {19204B12-E95E-405F-9604-52DE1411C8B1}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {626F29DA-4334-4E7E-8D9A-B2353BB37FC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {626F29DA-4334-4E7E-8D9A-B2353BB37FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {626F29DA-4334-4E7E-8D9A-B2353BB37FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {626F29DA-4334-4E7E-8D9A-B2353BB37FC1}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {7070940A-C83F-4D3D-BDE4-55AEE58A9385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {7070940A-C83F-4D3D-BDE4-55AEE58A9385}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {7070940A-C83F-4D3D-BDE4-55AEE58A9385}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {7070940A-C83F-4D3D-BDE4-55AEE58A9385}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {06A0BC9E-9100-4877-89A6-F5798BD3C7A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {06A0BC9E-9100-4877-89A6-F5798BD3C7A3}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {06A0BC9E-9100-4877-89A6-F5798BD3C7A3}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {06A0BC9E-9100-4877-89A6-F5798BD3C7A3}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {53403212-6FFE-46A1-A7D6-262A8003B79E} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /MediaQuerier/MediaQuerier.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7070940A-C83F-4D3D-BDE4-55AEE58A9385} 8 | Library 9 | Properties 10 | ExifOrganizer.Querier 11 | ExifOrganizer.Querier 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | true 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {06a0bc9e-9100-4877-89a6-f5798bd3c7a3} 53 | Common 54 | 55 | 56 | {19204b12-e95e-405f-9604-52de1411c8b1} 57 | MetaParser 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # MSTest test Results 20 | [Tt]est[Rr]esult*/ 21 | [Bb]uild[Ll]og.* 22 | 23 | #NUNIT 24 | *.VisualState.xml 25 | TestResult.xml 26 | 27 | # Build Results of an ATL Project 28 | [Dd]ebugPS/ 29 | [Rr]eleasePS/ 30 | dlldata.c 31 | 32 | *_i.c 33 | *_p.c 34 | *_i.h 35 | *.ilk 36 | *.meta 37 | *.obj 38 | *.pch 39 | *.pdb 40 | *.pgc 41 | *.pgd 42 | *.rsp 43 | *.sbr 44 | *.tlb 45 | *.tli 46 | *.tlh 47 | *.tmp 48 | *.tmp_proj 49 | *.log 50 | *.vspscc 51 | *.vssscc 52 | .builds 53 | *.pidb 54 | *.svclog 55 | *.scc 56 | 57 | # Chutzpah Test files 58 | _Chutzpah* 59 | 60 | # Visual C++ cache files 61 | ipch/ 62 | *.aps 63 | *.ncb 64 | *.opensdf 65 | *.sdf 66 | *.cachefile 67 | 68 | # Visual Studio profiler 69 | *.psess 70 | *.vsp 71 | *.vspx 72 | 73 | # TFS 2012 Local Workspace 74 | $tf/ 75 | 76 | # Guidance Automation Toolkit 77 | *.gpState 78 | 79 | # ReSharper is a .NET coding add-in 80 | _ReSharper*/ 81 | *.[Rr]e[Ss]harper 82 | *.DotSettings.user 83 | 84 | # JustCode is a .NET coding addin-in 85 | .JustCode 86 | 87 | # TeamCity is a build add-in 88 | _TeamCity* 89 | 90 | # DotCover is a Code Coverage Tool 91 | *.dotCover 92 | 93 | # NCrunch 94 | *.ncrunch* 95 | _NCrunch_* 96 | .*crunch*.local.xml 97 | 98 | # MightyMoose 99 | *.mm.* 100 | AutoTest.Net/ 101 | 102 | # Web workbench (sass) 103 | .sass-cache/ 104 | 105 | # Installshield output folder 106 | [Ee]xpress/ 107 | 108 | # DocProject is a documentation generator add-in 109 | DocProject/buildhelp/ 110 | DocProject/Help/*.HxT 111 | DocProject/Help/*.HxC 112 | DocProject/Help/*.hhc 113 | DocProject/Help/*.hhk 114 | DocProject/Help/*.hhp 115 | DocProject/Help/Html2 116 | DocProject/Help/html 117 | 118 | # Click-Once directory 119 | publish/ 120 | 121 | # Publish Web Output 122 | *.[Pp]ublish.xml 123 | *.azurePubxml 124 | 125 | # NuGet Packages Directory 126 | packages/ 127 | ## TODO: If the tool you use requires repositories.config uncomment the next line 128 | #!packages/repositories.config 129 | 130 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 131 | # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) 132 | !packages/build/ 133 | 134 | # Windows Azure Build Output 135 | csx/ 136 | *.build.csdef 137 | 138 | # Windows Store app package directory 139 | AppPackages/ 140 | 141 | # Others 142 | sql/ 143 | *.Cache 144 | ClientBin/ 145 | [Ss]tyle[Cc]op.* 146 | ~$* 147 | *~ 148 | *.dbmdl 149 | *.dbproj.schemaview 150 | *.pfx 151 | *.publishsettings 152 | node_modules/ 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | *.mdf 166 | *.ldf 167 | 168 | # Business Intelligence projects 169 | *.rdl.data 170 | *.bim.layout 171 | *.bim_*.settings 172 | 173 | # Microsoft Fakes 174 | FakesAssemblies/ 175 | -------------------------------------------------------------------------------- /MediaOrganizer/MediaOrganizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {626F29DA-4334-4E7E-8D9A-B2353BB37FC1} 8 | Library 9 | Properties 10 | ExifOrganizer.Organizer 11 | ExifOrganizer.Organizer 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | true 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {06a0bc9e-9100-4877-89a6-f5798bd3c7a3} 56 | Common 57 | 58 | 59 | {19204b12-e95e-405f-9604-52de1411c8b1} 60 | MetaParser 61 | 62 | 63 | 64 | 65 | Always 66 | 67 | 68 | 69 | 76 | -------------------------------------------------------------------------------- /MetaParser/MetaParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {19204B12-E95E-405F-9604-52DE1411C8B1} 8 | Library 9 | Properties 10 | ExifOrganizer.Meta 11 | ExifOrganizer.Meta 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | true 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {06a0bc9e-9100-4877-89a6-f5798bd3c7a3} 64 | Common 65 | 66 | 67 | 68 | 75 | -------------------------------------------------------------------------------- /MetaParser/MetaData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MetaData.cs: Data structure for parsed meta data. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System.Collections.Generic; 20 | 21 | namespace ExifOrganizer.Meta 22 | { 23 | public enum MetaType 24 | { 25 | Directory, 26 | File, 27 | 28 | Image, 29 | Video, 30 | Audio 31 | } 32 | 33 | public enum MetaKey 34 | { 35 | // Common 36 | MetaType, 37 | OriginalName, 38 | FileName, 39 | Size, 40 | Timestamp, 41 | DateCreated, 42 | DateModified, 43 | Comment, 44 | 45 | // Image 46 | Resolution, 47 | Width, 48 | Height, 49 | Camera, 50 | Tags, 51 | 52 | // Music 53 | Title, 54 | Artist, 55 | Album, 56 | Year, 57 | Track, 58 | Genre 59 | } 60 | 61 | public enum MetaMediaType 62 | { 63 | Image, 64 | Audio, 65 | Video 66 | } 67 | 68 | public class MetaData 69 | { 70 | public MetaType Type; 71 | public string Path; 72 | public Dictionary Data; 73 | #if DEBUG 74 | public object Origin; 75 | #endif 76 | } 77 | 78 | public static class MetaDataExtensions 79 | { 80 | public static IEnumerable GetByMedia(this MetaMediaType media) 81 | { 82 | HashSet keys = new HashSet(); 83 | keys.Add(MetaKey.MetaType); 84 | keys.Add(MetaKey.OriginalName); 85 | keys.Add(MetaKey.FileName); 86 | keys.Add(MetaKey.Size); 87 | keys.Add(MetaKey.Timestamp); 88 | keys.Add(MetaKey.DateCreated); 89 | keys.Add(MetaKey.DateModified); 90 | keys.Add(MetaKey.Comment); 91 | 92 | switch (media) 93 | { 94 | case MetaMediaType.Image: 95 | keys.Add(MetaKey.Resolution); 96 | keys.Add(MetaKey.Width); 97 | keys.Add(MetaKey.Height); 98 | keys.Add(MetaKey.Camera); 99 | keys.Add(MetaKey.Tags); 100 | break; 101 | 102 | case MetaMediaType.Audio: 103 | keys.Add(MetaKey.Title); 104 | keys.Add(MetaKey.Artist); 105 | keys.Add(MetaKey.Album); 106 | keys.Add(MetaKey.Year); 107 | keys.Add(MetaKey.Track); 108 | keys.Add(MetaKey.Genre); 109 | break; 110 | 111 | case MetaMediaType.Video: 112 | break; 113 | 114 | default: 115 | break; 116 | } 117 | return keys; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /MediaOrganizer/IniFile.cs: -------------------------------------------------------------------------------- 1 | // 2 | // IniFile.cs: Data structure used to save/load an ini file. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.IO; 22 | 23 | namespace ExifOrganizer.Organizer 24 | { 25 | public class IniFile : Dictionary 26 | { 27 | private const char CommentSymbol = ';'; 28 | private const char KeyValueSeparator = ':'; 29 | 30 | public IniFile() 31 | : base() 32 | { 33 | } 34 | 35 | public IniFile(string filename) 36 | : base() 37 | { 38 | TryLoad(filename); 39 | } 40 | 41 | public bool TryLoad(string filename) 42 | { 43 | try 44 | { 45 | Load(filename); 46 | return true; 47 | } 48 | catch (Exception) 49 | { 50 | return false; 51 | } 52 | } 53 | 54 | public void Load(string filename) 55 | { 56 | if (String.IsNullOrEmpty(filename)) 57 | throw new ArgumentNullException(nameof(filename)); 58 | if (!File.Exists(filename)) 59 | throw new FileNotFoundException(filename); 60 | 61 | Clear(); 62 | 63 | foreach (string line in File.ReadAllLines(filename)) 64 | { 65 | if (line.Trim().Length == 0) 66 | continue; // Empty line 67 | if (line.Trim().StartsWith(CommentSymbol.ToString())) 68 | continue; // Comment 69 | 70 | string[] keyValue = line.Split(new char[] { KeyValueSeparator }, 2); 71 | if (keyValue.Length != 2) 72 | continue; // Invalid row 73 | 74 | string key = keyValue[0].Trim(); 75 | string value = keyValue[1].Trim(); 76 | Add(key, value); 77 | } 78 | } 79 | 80 | public bool TrySave(string filename) 81 | { 82 | try 83 | { 84 | Save(filename); 85 | return true; 86 | } 87 | catch (Exception) 88 | { 89 | return false; 90 | } 91 | } 92 | 93 | public void Save(string filename) 94 | { 95 | if (String.IsNullOrEmpty(filename)) 96 | throw new ArgumentNullException(nameof(filename)); 97 | 98 | List lines = new List(); 99 | foreach (KeyValuePair kvp in this) 100 | { 101 | string line = $"{kvp.Key}{KeyValueSeparator} {kvp.Value}"; 102 | lines.Add(line); 103 | } 104 | 105 | File.WriteAllLines(filename, lines); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /UserInterface/Controls/EnumDropDown.cs: -------------------------------------------------------------------------------- 1 | // 2 | // EnumDropDown.cs: User control to render enum values in a drop-down list. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Drawing; 22 | using System.Linq; 23 | using System.Windows.Forms; 24 | 25 | namespace ExifOrganizer.UI.Controls 26 | { 27 | public partial class EnumDropDown : ComboBox 28 | { 29 | private Type enumType; 30 | private Enum enumValue; 31 | 32 | protected class ComboboxItem 33 | { 34 | public string Text { get; set; } 35 | public Enum Value { get; set; } 36 | 37 | public override string ToString() 38 | { 39 | return Text; 40 | } 41 | } 42 | 43 | public EnumDropDown() 44 | : base() 45 | { 46 | InitializeComponent(); 47 | 48 | DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; 49 | SelectedIndexChanged += (sender, e) => { enumValue = ((ComboboxItem)SelectedItem).Value; }; 50 | } 51 | 52 | public Func EnumText 53 | { 54 | get; 55 | set; 56 | } 57 | 58 | public Type EnumType 59 | { 60 | get { return enumType; } 61 | set 62 | { 63 | if (value == null) 64 | { 65 | enumType = null; 66 | Items.Clear(); 67 | return; 68 | } 69 | 70 | if (!value.IsEnum) 71 | throw new ArgumentException($"Type must be Enum: {value}", nameof(value)); 72 | 73 | if (value == enumType) 74 | return; 75 | 76 | enumType = value; 77 | enumValue = null; 78 | 79 | Items.Clear(); 80 | foreach (Enum item in Enum.GetValues(value)) 81 | Items.Add(new ComboboxItem() { Value = item, Text = (EnumText != null) ? EnumText(item) : item.ToString() }); 82 | } 83 | } 84 | 85 | public Enum EnumValue 86 | { 87 | get { return enumValue; } 88 | set 89 | { 90 | if (value == null) 91 | return; 92 | if (enumType == null) 93 | throw new ArgumentException("Enum type not yet defined", nameof(enumType)); 94 | if (value.GetType() != enumType) 95 | throw new ArgumentException($"Value must be of predefined Enum type: {enumType}", nameof(value)); 96 | 97 | if (value == enumValue) 98 | return; 99 | 100 | long numeric = value.GetInt64(); 101 | foreach (ComboboxItem item in Items) 102 | { 103 | if (item.Value.GetInt64() != numeric) 104 | continue; 105 | 106 | SelectedItem = item; 107 | break; 108 | } 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /UserInterface/Controls/FileBrowseControl.cs: -------------------------------------------------------------------------------- 1 | // 2 | // FileBrowseControl.cs: User control for browsing files or directories. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.IO; 21 | using System.Windows.Forms; 22 | 23 | namespace ExifOrganizer.UI.Controls 24 | { 25 | public enum FileBrowseType 26 | { 27 | File, 28 | Directory 29 | } 30 | 31 | public partial class FileBrowseControl : UserControl 32 | { 33 | public FileBrowseControl() 34 | : base() 35 | { 36 | InitializeComponent(); 37 | } 38 | 39 | #region Properties 40 | 41 | public FileBrowseType BrowseType 42 | { 43 | get; 44 | set; 45 | } 46 | 47 | public bool ReadOnly 48 | { 49 | get { return path.ReadOnly; } 50 | set { path.ReadOnly = value; } 51 | } 52 | 53 | public override string Text 54 | { 55 | get { return path.Text; } 56 | set { path.Text = value; } 57 | } 58 | 59 | public string SelectedPath 60 | { 61 | get { return path.Text; } 62 | set { path.Text = value; } 63 | } 64 | 65 | public string DialogTitle 66 | { 67 | get; 68 | set; 69 | } 70 | 71 | #endregion Properties 72 | 73 | private void browse_Click(object sender, EventArgs e) 74 | { 75 | if (BrowseType == FileBrowseType.File) 76 | { 77 | OpenFileDialog dialog = new OpenFileDialog(); 78 | dialog.Title = DialogTitle; 79 | dialog.Multiselect = false; 80 | dialog.InitialDirectory = Path.GetDirectoryName(path.Text); 81 | dialog.FileName = Path.GetFileName(path.Text); 82 | if (dialog.ShowDialog(FindForm(), FormStartPosition.CenterParent) == DialogResult.OK) 83 | path.Text = dialog.FileName; 84 | } 85 | else 86 | { 87 | FolderBrowserDialog dialog = new FolderBrowserDialog(); 88 | dialog.Description = DialogTitle; 89 | dialog.SelectedPath = path.Text; 90 | if (dialog.ShowDialog(FindForm(), FormStartPosition.CenterParent) == DialogResult.OK) 91 | path.Text = dialog.SelectedPath; 92 | } 93 | } 94 | } 95 | 96 | public static class CommonDialogExtensions 97 | { 98 | public static DialogResult ShowDialog(this CommonDialog dialog, IWin32Window owner, FormStartPosition position) 99 | { 100 | if (owner == null) 101 | return dialog.ShowDialog(); 102 | if (position != FormStartPosition.CenterParent && position != FormStartPosition.CenterScreen) 103 | return dialog.ShowDialog(owner); 104 | 105 | // TODO: implement 106 | //using (Form form = new Form()) 107 | //{ 108 | // form.Owner = owner as Form; 109 | // form.StartPosition = position; 110 | // return dialog.ShowDialog(form); 111 | //} 112 | return dialog.ShowDialog(owner); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /MediaOrganizer/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionMethods.cs: Static extension methods used within the project. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using System; 21 | using System.IO; 22 | using System.Security.Cryptography; 23 | 24 | namespace ExifOrganizer.Organizer 25 | { 26 | public static class ExtensionMethods 27 | { 28 | public static bool FileExistsInDirectory(this FileInfo fileInfo, DirectoryInfo directory, FileComparator comparator) 29 | { 30 | foreach (FileInfo tempFile in directory.GetFiles()) 31 | { 32 | if (tempFile.AreFilesIdentical(fileInfo, comparator)) 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | public static bool AreFilesIdentical(this FileInfo fileInfo, FileInfo otherFile, FileComparator comparator) 39 | { 40 | if (fileInfo == null) 41 | throw new ArgumentNullException(nameof(fileInfo)); 42 | if (otherFile == null) 43 | throw new ArgumentNullException(nameof(otherFile)); 44 | if (!fileInfo.Exists) 45 | return false; 46 | if (!otherFile.Exists) 47 | return false; 48 | 49 | bool identical = true; 50 | if (identical && comparator.HasFlag(FileComparator.FileSize)) 51 | identical &= (fileInfo.Length == otherFile.Length); 52 | if (identical && comparator.HasFlag(FileComparator.ChecksumMD5)) 53 | identical &= fileInfo.GetMD5Sum() == otherFile.GetMD5Sum(); 54 | if (identical && comparator.HasFlag(FileComparator.ChecksumSHA1)) 55 | identical &= fileInfo.GetSHA1Sum() == otherFile.GetSHA1Sum(); 56 | if (identical && comparator.HasFlag(FileComparator.ChecksumSHA256)) 57 | identical &= fileInfo.GetSHA256Sum() == otherFile.GetSHA256Sum(); 58 | if (identical && comparator.HasFlag(FileComparator.Created)) 59 | identical &= (fileInfo.CreationTimeUtc == otherFile.CreationTimeUtc); 60 | if (identical && comparator.HasFlag(FileComparator.Modified)) 61 | identical &= (fileInfo.LastWriteTimeUtc == otherFile.LastWriteTimeUtc); 62 | return identical; 63 | } 64 | 65 | public static string GetGroupTypeText(this GroupType groupType) 66 | { 67 | switch (groupType) 68 | { 69 | case GroupType.Index: return "Unique index"; 70 | case GroupType.MonthName: return "Name of month"; 71 | case GroupType.MonthNumber: return "Month numeric value"; 72 | case GroupType.DayName: return "Name of day"; 73 | case GroupType.DayNumber: return "Day numeric value"; 74 | case GroupType.OriginalName: return "Original file name"; 75 | case GroupType.FileName: return "File name"; 76 | case GroupType.FileExtension: return "File extension"; 77 | case GroupType.FileNameWithExtension: return "File name with extension"; 78 | case GroupType.WidthPx: return "Width in pixels"; 79 | case GroupType.HeightPx: return "Height in pixels"; 80 | //case GroupType.HorizontalDpi: return "Horizontal DPI"; 81 | //case GroupType.VerticalDpi: return "Vertical DPI"; 82 | default: return groupType.ToString(); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /UserInterface/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionMethods.cs: Static extension methods used within the project. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Organizer; 20 | using System; 21 | using System.Windows.Forms; 22 | 23 | namespace ExifOrganizer.UI 24 | { 25 | public static class ExtensionMethods 26 | { 27 | #region Generic 28 | 29 | public static int GetInt32(this Enum value) 30 | { 31 | if (value == null) 32 | return 0; 33 | return Convert.ToInt32(value); 34 | } 35 | 36 | public static long GetInt64(this Enum value) 37 | { 38 | if (value == null) 39 | return 0; 40 | return Convert.ToInt64(value); 41 | } 42 | 43 | public static bool Flags(this Enum value) 44 | { 45 | if (value == null) 46 | return false; 47 | 48 | Type type = value.GetType(); 49 | return type.HasAttributesFlags(); 50 | } 51 | 52 | public static bool HasAttributesFlags(this Type type) 53 | { 54 | if (type == null) 55 | throw new ArgumentNullException(nameof(type)); 56 | 57 | object[] attributes = type.GetCustomAttributes(true); 58 | foreach (object attribute in attributes) 59 | { 60 | if (attribute is FlagsAttribute) 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | public static bool OneBitSet(this Enum value) 67 | { 68 | long x = value.GetInt64(); 69 | return (x != 0) && (x & (x - 1)) == 0; 70 | } 71 | 72 | public static double Clamp(this double value, double min, double max) 73 | { 74 | if (value < min) 75 | return min; 76 | if (value > max) 77 | return max; 78 | return value; 79 | } 80 | 81 | public static Exception GetInnerMost(this Exception exception) 82 | { 83 | if (exception == null) 84 | return null; 85 | if (exception.InnerException == null) 86 | return exception; 87 | 88 | return exception.InnerException.GetInnerMost(); 89 | } 90 | 91 | public static string Get(this Version version) 92 | { 93 | return $"{version.Major}.{version.Minor}.{version.Build} build {version.Revision}"; 94 | } 95 | 96 | #endregion Generic 97 | 98 | #region WinForms 99 | 100 | public static void Invoke(this Control control, Action action, params object[] args) 101 | { 102 | control.Invoke((Delegate)action, args); 103 | } 104 | 105 | public static object Invoke(this Control control, Func func, params object[] args) 106 | { 107 | return control.Invoke((Delegate)func, args); 108 | } 109 | 110 | public static IAsyncResult BeginInvoke(this Control control, Action action, params object[] args) 111 | { 112 | return control.BeginInvoke((Delegate)action, args); 113 | } 114 | 115 | #endregion WinForms 116 | 117 | #region MediaOrganizer 118 | 119 | // public static string ToJSON(this OrganizeSummary) 120 | // { 121 | // return String.Format("Summary {{ Parsed: {0} (Ignored: {1}), Total files: {2}, Total directories: {3} }}", 122 | // parsed != null ? parsed.Length : 0, 123 | // ignored != null ? ignored.Length : 0, 124 | // totalFiles != null ? totalFiles.Length : 0, 125 | // totalDirectories != null ? totalDirectories.Length : 0 126 | //); 127 | // } 128 | 129 | #endregion MediaOrganizer 130 | } 131 | } -------------------------------------------------------------------------------- /MetaParser/Parsers/BmpParser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // BmpParser.cs: Bitmap/DIB (device independent bitmap) meta parser class. 3 | // 4 | // Copyright (C) 2018 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.IO; 22 | using System.Linq; 23 | using System.Text; 24 | using System.Threading.Tasks; 25 | 26 | namespace ExifOrganizer.Meta.Parsers 27 | { 28 | internal class BmpParser : FileParser 29 | { 30 | internal override IEnumerable GetSupportedFileExtensions() 31 | { 32 | return new string[] { ".bmp", ".dib" }; 33 | } 34 | 35 | internal override MetaType? GetMetaTypeByFileExtension(string extension) 36 | { 37 | if (GetSupportedFileExtensions().Contains(extension)) 38 | return MetaType.Image; 39 | return base.GetMetaTypeByFileExtension(extension); 40 | } 41 | 42 | protected override MetaData ParseFile(Stream stream, MetaData meta) 43 | { 44 | byte[] bitmapHeader = new byte[14]; 45 | if (stream.Read(bitmapHeader, 0, bitmapHeader.Length) != bitmapHeader.Length) 46 | throw new MetaParseException("Unable to read full bitmap header data"); 47 | 48 | string signature = Convert.ToString((char)bitmapHeader[0]) + Convert.ToString((char)bitmapHeader[1]); 49 | meta.Data[MetaKey.MetaType] = $"DIB ({signature})"; 50 | 51 | //int fileSize = BitConverter.ToInt32(bitmapHeader, 4); 52 | //int offsetImageData = BitConverter.ToInt32(bitmapHeader, 10); 53 | 54 | 55 | byte[] dibHeaderSizeData = new byte[4]; 56 | if (stream.Read(dibHeaderSizeData, 0, dibHeaderSizeData.Length) != dibHeaderSizeData.Length) 57 | throw new MetaParseException("Unable to read full DIB header size"); 58 | 59 | int dibHeaderSize = BitConverter.ToInt32(dibHeaderSizeData, 0); 60 | 61 | byte[] dibHeader = new byte[dibHeaderSize - 4]; 62 | if (stream.Read(dibHeader, 0, dibHeader.Length) != dibHeader.Length) 63 | throw new MetaParseException("Unable to read full DIB header data"); 64 | 65 | if (dibHeaderSize == 12) 66 | { 67 | // OS/2 1.x BITMAPCOREHEADER 68 | 69 | int bitmapWidth = BitConverter.ToUInt16(dibHeader, 0); 70 | meta.Data[MetaKey.Width] = bitmapWidth; 71 | 72 | int bitmapHeight = BitConverter.ToUInt16(dibHeader, 2); 73 | meta.Data[MetaKey.Height] = bitmapHeight; 74 | 75 | //int colorPlanes = BitConverter.ToUInt16(dibHeader, 4); 76 | //int bitsPerPixel = BitConverter.ToUInt16(dibHeader, 6); 77 | } 78 | else if (dibHeaderSize == 40) 79 | { 80 | // Windows BITMAPINFOHEADER 81 | 82 | int bitmapWidth = BitConverter.ToInt32(dibHeader, 0); 83 | meta.Data[MetaKey.Width] = bitmapWidth; 84 | 85 | int bitmapHeight = BitConverter.ToInt32(dibHeader, 4); 86 | meta.Data[MetaKey.Height] = bitmapHeight; 87 | 88 | //int colorPlanes = BitConverter.ToUInt16(dibHeader, 8); 89 | //int bitsPerPixel = BitConverter.ToUInt16(dibHeader, 10); 90 | //int compression = BitConverter.ToInt32(dibHeader, 12); 91 | //int imageSize = BitConverter.ToInt32(dibHeader, 16); 92 | //int horizontalResolution = BitConverter.ToInt32(dibHeader, 20); 93 | //int verticalResolution = BitConverter.ToInt32(dibHeader, 24); 94 | //int colorsInPalette = BitConverter.ToInt32(dibHeader, 28); 95 | //int importantColors = BitConverter.ToInt32(dibHeader, 32); 96 | 97 | } 98 | else 99 | { 100 | throw new NotImplementedException($"[BmpParser] unhandled DIB header size: {dibHeaderSize}"); 101 | } 102 | 103 | meta.Data[MetaKey.Resolution] = $"{meta.Data[MetaKey.Width]}x{meta.Data[MetaKey.Height]}"; 104 | 105 | 106 | return meta; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /UserInterface/Controls/EnumFlagsDropDown.cs: -------------------------------------------------------------------------------- 1 | // 2 | // EnumFlagsDropDown.cs: User control to render checkboxes, representing flags in 3 | // Enum tagged with Flags attribute, in a drop-down list. 4 | // 5 | // Copyright (C) 2014 Rikard Johansson 6 | // 7 | // This program is free software: you can redistribute it and/or modify it 8 | // under the terms of the GNU General Public License as published by the Free 9 | // Software Foundation, either version 3 of the License, or (at your option) any 10 | // later version. 11 | // 12 | // This program is distributed in the hope that it will be useful, but WITHOUT 13 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License along with 17 | // this program. If not, see http://www.gnu.org/licenses/. 18 | // 19 | 20 | using System; 21 | 22 | namespace ExifOrganizer.UI.Controls 23 | { 24 | /* 25 | * TODO: 26 | * - Handle None and All (subset) values: check if match 27 | * - move bitfield (of values) to CheckBoxDrop down: then only cast from Enum to int is required (alt. use array of values) 28 | */ 29 | 30 | public partial class EnumFlagsDropDown : CheckBoxDropDown 31 | { 32 | private Type enumType; 33 | private Enum enumValue; 34 | 35 | public EnumFlagsDropDown() 36 | : base() 37 | { 38 | InitializeComponent(); 39 | } 40 | 41 | public Func EnumText 42 | { 43 | get; 44 | set; 45 | } 46 | 47 | public Type EnumType 48 | { 49 | get { return enumType; } 50 | set 51 | { 52 | if (value == null) 53 | { 54 | enumType = null; 55 | Clear(); 56 | return; 57 | } 58 | 59 | if (!value.IsEnum) 60 | throw new ArgumentException($"Type must be Enum: {value}", nameof(value)); 61 | if (!value.HasAttributesFlags()) 62 | throw new ArgumentException($"Enum type must have attribute flags: {value}", nameof(value)); 63 | 64 | if (value == enumType) 65 | return; 66 | 67 | enumType = value; 68 | enumValue = null; 69 | 70 | Clear(); 71 | foreach (Enum item in Enum.GetValues(value)) 72 | { 73 | if (!item.OneBitSet()) 74 | continue; 75 | Add(new CheckBoxItem() { Value = item.GetInt64(), Text = (EnumText != null) ? EnumText(item) : item.ToString() }); 76 | } 77 | } 78 | } 79 | 80 | public Enum EnumValue 81 | { 82 | get { return enumValue; } 83 | set 84 | { 85 | if (value == null) 86 | return; 87 | if (enumType == null) 88 | throw new ArgumentException("Enum type not yet defined", nameof(enumType)); 89 | if (value.GetType() != enumType) 90 | throw new ArgumentException($"Value must be of predefined Enum type: {enumType}", nameof(value)); 91 | 92 | if (value == enumValue) 93 | return; 94 | 95 | Enum previousValue = enumValue; 96 | enumValue = value; 97 | 98 | foreach (Enum item in Enum.GetValues(enumType)) 99 | { 100 | if (!item.OneBitSet()) 101 | continue; 102 | 103 | if (previousValue != null) 104 | { 105 | if (item.HasFlag(previousValue) == item.HasFlag(enumValue)) 106 | continue; // Enum flag not altered 107 | } 108 | 109 | bool active = item.GetInt64() > 0 && enumValue.HasFlag(item); 110 | Update(new CheckBoxItem() { Value = item.GetInt64(), Text = (EnumText != null) ? EnumText(item) : item.ToString(), Checked = active }); 111 | } 112 | } 113 | } 114 | 115 | protected override void CheckedChanged(CheckBoxItem item) 116 | { 117 | base.CheckedChanged(item); 118 | 119 | Enum previous = enumValue; 120 | long value = (previous != null) ? previous.GetInt64() : 0; 121 | 122 | if (item.Checked) 123 | value |= item.Value; 124 | else 125 | value &= ~item.Value; 126 | 127 | enumValue = (Enum)Enum.ToObject(enumType, value); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /UserInterface/ParseArgs.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ParseArgs.cs: Argument parser use by CLI (command line interface). 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System.Collections.Generic; 20 | using System.Linq; 21 | 22 | namespace ExifOrganizer.UI 23 | { 24 | public enum ArgType 25 | { 26 | Flag, 27 | Variable, 28 | Undefined 29 | } 30 | 31 | public class Arg 32 | { 33 | public ArgType Type; 34 | public string Key; 35 | public string Value; 36 | 37 | public override string ToString() 38 | { 39 | switch (Type) 40 | { 41 | case ArgType.Flag: 42 | return $"Arg {{ Flag: \"{Key}\" }}"; 43 | 44 | case ArgType.Variable: 45 | return $"Arg {{ Variable: \"{Key}\" = \"{Value}\" }}"; 46 | 47 | case ArgType.Undefined: 48 | return $"Arg {{ Undefined: \"{Key}\" }}"; 49 | 50 | default: 51 | return null; 52 | } 53 | } 54 | } 55 | 56 | public class ParseArgs 57 | { 58 | public static string KeyValueSeparator = "="; 59 | 60 | public static IEnumerable Parse(string[] args, IEnumerable flagKeys, IEnumerable variableKeys) 61 | { 62 | List result = new List(); 63 | 64 | for (int i = 0; i < args.Length; i++) 65 | { 66 | string arg = args[i]; 67 | 68 | // Flag 69 | if (flagKeys.Contains(arg)) 70 | { 71 | Arg flagArg = new Arg(); 72 | flagArg.Type = ArgType.Flag; 73 | flagArg.Key = arg; 74 | flagArg.Value = arg; 75 | result.Add(flagArg); 76 | continue; 77 | } 78 | 79 | // Variable 80 | if (variableKeys.Contains(arg)) 81 | { 82 | Arg variableArg = new Arg(); 83 | variableArg.Type = ArgType.Variable; 84 | variableArg.Key = arg; 85 | if (i < args.Length - 1) 86 | { 87 | if (!variableKeys.Contains(args[i + 1])) 88 | { 89 | // Next argument is interpreted as value 90 | i++; 91 | arg = args[i]; 92 | variableArg.Value = arg; 93 | } 94 | else 95 | { 96 | // Next argument is a flag: no value defined for this variable 97 | variableArg.Value = null; 98 | } 99 | } 100 | else 101 | { 102 | // No value defined after variable's key 103 | variableArg.Value = null; 104 | } 105 | result.Add(variableArg); 106 | continue; 107 | } 108 | bool found = false; 109 | foreach (string variableKey in variableKeys) 110 | { 111 | if (arg.StartsWith(variableKey + KeyValueSeparator)) 112 | { 113 | // Key-value defined in same argument with separator 114 | Arg variableArg = new Arg(); 115 | variableArg.Type = ArgType.Variable; 116 | variableArg.Key = variableKey; 117 | variableArg.Value = arg.Substring(variableKey.Length + KeyValueSeparator.Length); 118 | result.Add(variableArg); 119 | found = true; 120 | } 121 | } 122 | if (found) 123 | continue; 124 | 125 | // Undefined 126 | Arg undefinedArg = new Arg(); 127 | undefinedArg.Type = ArgType.Undefined; 128 | undefinedArg.Key = arg; 129 | undefinedArg.Value = arg; 130 | result.Add(undefinedArg); 131 | } 132 | 133 | return result; 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /UserInterface/CLI.cs: -------------------------------------------------------------------------------- 1 | // 2 | // CLI.cs: Command line interface (CLI) main class. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Meta; 20 | using ExifOrganizer.Organizer; 21 | using ExifOrganizer.Querier; 22 | using System; 23 | using System.Collections.Generic; 24 | using System.Threading; 25 | 26 | namespace ExifOrganizer.UI 27 | { 28 | public class CLI 29 | { 30 | public CLI() 31 | { 32 | Console.WriteLine("ExifOrganizer CLI"); 33 | } 34 | 35 | public int Run(string[] args) 36 | { 37 | IEnumerable parsedArgs = ParseArgs.Parse(args, new string[] { "-r" }, new string[] { "-s", "-d" }); 38 | #if DEBUG 39 | Console.WriteLine("Arguments parsed:"); 40 | foreach (Arg arg in parsedArgs) 41 | Console.WriteLine($" * {arg}"); 42 | #endif 43 | 44 | string source = null; 45 | string destination = null; 46 | bool recursive = false; 47 | bool query = false; 48 | foreach (Arg arg in parsedArgs) 49 | { 50 | switch (arg.Key) 51 | { 52 | case "-s": 53 | source = arg.Value; 54 | break; 55 | 56 | case "-d": 57 | destination = arg.Value; 58 | break; 59 | 60 | case "-r": 61 | recursive = true; 62 | break; 63 | 64 | case "-q": 65 | query = true; 66 | break; 67 | } 68 | } 69 | 70 | if (query) 71 | { 72 | MediaQuerier querier = new MediaQuerier(); 73 | QuerySummary summary = querier.Query(source, recursive, QueryType.All, MetaType.Image); 74 | ConsoleColor originalColor = Console.ForegroundColor; 75 | foreach (var match in summary.matches) 76 | { 77 | Console.ForegroundColor = ConsoleColor.White; 78 | Console.WriteLine(match.Key); 79 | foreach (Tuple duplicate in match.Value) 80 | { 81 | Console.ForegroundColor = ConsoleColor.DarkGray; 82 | Console.Write(" `-- {0} [", duplicate.Item1); 83 | Console.ForegroundColor = ConsoleColor.Red; 84 | Console.Write(duplicate.Item2); 85 | Console.ForegroundColor = ConsoleColor.DarkGray; 86 | Console.Write("]{0}", Console.Out.NewLine); 87 | } 88 | } 89 | Console.ForegroundColor = originalColor; 90 | if (summary.matches.Count == 0) 91 | Console.WriteLine(""); 92 | else 93 | Console.WriteLine("DONE"); 94 | 95 | return summary.matches.Count; 96 | } 97 | 98 | MediaOrganizer organizer = new MediaOrganizer(); 99 | organizer.sourcePath = source; 100 | organizer.destinationPath = destination; 101 | organizer.Recursive = recursive; 102 | //organizer.DuplicateMode = DuplicateMode.KeepAll; 103 | organizer.CopyMode = CopyMode.KeepUnique; 104 | //organizer.DestinationPatternImage = patternImage.Text; 105 | //organizer.DestinationPatternVideo = patternVideo.Text; 106 | //organizer.DestinationPatternAudio = patternAudio.Text; 107 | organizer.Locale = Thread.CurrentThread.CurrentUICulture; 108 | 109 | if (String.IsNullOrEmpty(source)) 110 | throw new ArgumentException("No source path given (-s)", nameof(source)); 111 | if (String.IsNullOrEmpty(destination)) 112 | throw new ArgumentException("No destination path given (-d)", nameof(destination)); 113 | 114 | try 115 | { 116 | organizer.Organize(); 117 | Console.WriteLine("Media organization completed successfully"); 118 | return 0; 119 | } 120 | catch (Exception ex) 121 | { 122 | Console.Error.WriteLine(ex.Message); 123 | return 1; 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /UserInterface/Controls/ProgressBarText.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressBarText.cs: User control to render progress text upon a ProgressBar. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Drawing; 21 | using System.Windows.Forms; 22 | 23 | namespace ExifOrganizer.UI.Controls 24 | { 25 | public partial class ProgressBarText : ProgressBar 26 | { 27 | public ProgressBarText() 28 | : base() 29 | { 30 | SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); 31 | } 32 | 33 | protected override void OnPaint(PaintEventArgs e) 34 | { 35 | // Render base 36 | Graphics graphics = e.Graphics; 37 | Rectangle targetRectangle = ClientRectangle; 38 | ProgressBarRenderer.DrawHorizontalBar(graphics, targetRectangle); 39 | 40 | // Render progress bar 41 | if (Value > 0) 42 | { 43 | targetRectangle.Inflate(-3, -3); 44 | Rectangle clip = new Rectangle(targetRectangle.X, targetRectangle.Y, (int)Math.Round(((float)Value / Maximum) * targetRectangle.Width), targetRectangle.Height); 45 | ProgressBarRenderer.DrawHorizontalChunks(graphics, clip); 46 | } 47 | 48 | // Render text 49 | string text = GetProgressText(); 50 | if (!String.IsNullOrEmpty(text)) 51 | { 52 | SizeF textLength = graphics.MeasureString(text, Font); 53 | int pointY = (int)((Height / 2.0) - (textLength.Height / 2.0)); 54 | int pointX; 55 | if (CenterText) 56 | pointX = (int)((Width / 2.0) - (textLength.Width / 2.0)); 57 | else 58 | pointX = 10; 59 | Point textLocation = new Point(pointX, pointY); 60 | graphics.DrawString(text, Font, Brush ?? Brushes.Black, textLocation); 61 | } 62 | } 63 | 64 | #region Properties 65 | 66 | /// 67 | /// Brush used to render progress text. 68 | /// 69 | public Brush Brush 70 | { 71 | get; 72 | set; 73 | } 74 | 75 | /// 76 | /// If progress text should be centered horizontally on bar. 77 | /// 78 | public bool CenterText 79 | { 80 | get; 81 | set; 82 | } 83 | 84 | /// 85 | /// Override progress text (progress in percent) rendered on bar. Default value is null. 86 | /// 87 | public string ProgressText 88 | { 89 | get; 90 | set; 91 | } 92 | 93 | /// 94 | /// Factor (0.0-1.0) of current progress. 95 | /// 96 | public double ProgressFactor 97 | { 98 | get { return ((double)(Value - Minimum) / (double)(Maximum - Minimum)).Clamp(0.0, 1.0); } 99 | } 100 | 101 | /// 102 | /// Percent (0.0-100.0) of current progress. 103 | /// 104 | public double ProgressPercent 105 | { 106 | get { return (ProgressFactor * 100.0).Clamp(0.0, 100.0); } 107 | } 108 | 109 | #endregion Properties 110 | 111 | /// 112 | /// Update current progress value and message. Used to prevent unecessary repaints 113 | /// of this control. 114 | /// 115 | /// Factor (0.0-1.0) of current progress 116 | /// Override progress text (progress in percent) rendered on bar. 117 | public void SetProgress(double factor, string message = null) 118 | { 119 | int interval = (Maximum - Minimum); 120 | int value = Minimum + (int)Math.Round(factor * interval); 121 | if (value > Maximum) 122 | value = Maximum; 123 | if (value < Minimum) 124 | value = Minimum; 125 | 126 | SetProgress(value, message); 127 | } 128 | 129 | /// 130 | /// 131 | /// 132 | /// Percent (0.0-100.0) of current progress 133 | /// Override progress text (progress in percent) rendered on bar. 134 | public void SetProgress(int value, string message = null) 135 | { 136 | if (value == Value && message == ProgressText) 137 | return; 138 | 139 | ProgressText = message; 140 | Value = value; // Forces repaint 141 | } 142 | 143 | public string GetProgressText() 144 | { 145 | if (ProgressText != null) 146 | return ProgressText; 147 | 148 | return $"{Math.Round(ProgressPercent, 1)}%"; 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /UserInterface/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 | -------------------------------------------------------------------------------- /UserInterface/Controls/FileBrowseControl.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 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /UserInterface/Controls/EnumDropDown.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 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | False 122 | 123 | -------------------------------------------------------------------------------- /UserInterface/Controls/CheckBoxDropDown.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 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | False 122 | 123 | -------------------------------------------------------------------------------- /MetaParser/MetaParser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MetaParser.cs: Static class to parse meta data from different filetypes. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using ExifOrganizer.Meta.Parsers; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Diagnostics; 24 | using System.IO; 25 | using System.Linq; 26 | using System.Threading.Tasks; 27 | 28 | namespace ExifOrganizer.Meta 29 | { 30 | public struct MetaParserConfig 31 | { 32 | public bool Recursive; 33 | public IEnumerable FilterTypes; 34 | public IEnumerable IgnorePaths; 35 | } 36 | 37 | public static class MetaParser 38 | { 39 | private static readonly IEnumerable _fileParsers; 40 | private static readonly DirectoryParser _directoryParser; 41 | 42 | static MetaParser() 43 | { 44 | _fileParsers = typeof(FileParser) 45 | .Assembly.GetTypes() 46 | .Where(t => t.IsSubclassOf(typeof(FileParser)) && !t.IsAbstract) 47 | .Select(t => (FileParser)Activator.CreateInstance(t)); 48 | _directoryParser = new DirectoryParser(); 49 | 50 | #if DEBUG 51 | foreach (var parser in _fileParsers) 52 | Trace.WriteLine($"[{parser.GetType().Name}] {String.Join(", ", parser.GetSupportedFileExtensions())}"); 53 | #endif 54 | } 55 | 56 | public static IEnumerable Parse(string path, MetaParserConfig config) 57 | { 58 | Task> task = ParseAsync(path, config); 59 | task.ConfigureAwait(false); // Prevent deadlock of caller 60 | return task.Result; 61 | } 62 | 63 | public static async Task> ParseAsync(string path, MetaParserConfig config) 64 | { 65 | if (path == null) 66 | throw new ArgumentNullException(nameof(path)); 67 | 68 | if (Directory.Exists(path)) 69 | return await ParseDirectoryAsync(path, config); 70 | else if (File.Exists(path)) 71 | return new MetaData[] { await ParseFileAsync(path, config) }; 72 | else 73 | throw new FileNotFoundException($"Could not find file or directory: {path}"); 74 | } 75 | 76 | public static MetaData ParseFile(string path, MetaParserConfig config) 77 | { 78 | Task task = ParseFileAsync(path, config); 79 | task.ConfigureAwait(false); // Prevent deadlock of caller 80 | return task.Result; 81 | } 82 | 83 | public async static Task ParseFileAsync(string path, MetaParserConfig config) 84 | { 85 | if (String.IsNullOrEmpty(path)) 86 | throw new ArgumentNullException(nameof(path)); 87 | if (!File.Exists(path)) 88 | throw new MetaParseException("File not found: {0}", path); 89 | 90 | string extension = Path.GetExtension(path).ToLower(); 91 | IEnumerable parsers = _fileParsers.Where(parser => { 92 | MetaType? type = parser.GetMetaTypeByFileExtension(extension); 93 | return type.HasValue && (config.FilterTypes == null || config.FilterTypes.Contains(type.Value)); 94 | }); 95 | if (!parsers.Any()) 96 | return await Task.FromResult(null); 97 | 98 | IEnumerable meta = await Task.WhenAll(parsers.Select(parser => parser.ParseAsync(path)).ToArray()); 99 | return await Task.FromResult(meta.Aggregate((a, b) => a.Merge(b))); 100 | } 101 | 102 | public static IEnumerable ParseDirectory(string path, MetaParserConfig config) 103 | { 104 | Task> task = ParseDirectoryAsync(path, config); 105 | task.ConfigureAwait(false); // Prevent deadlock of caller 106 | return task.Result; 107 | } 108 | 109 | public static async Task> ParseDirectoryAsync(string path, MetaParserConfig config) 110 | { 111 | if (String.IsNullOrEmpty(path)) 112 | throw new ArgumentNullException(nameof(path)); 113 | if (!Directory.Exists(path)) 114 | throw new MetaParseException("Directory not found: {0}", path); 115 | 116 | List list = new List(); 117 | if (config.IgnorePaths != null) 118 | { 119 | foreach (string ignorePath in config.IgnorePaths) 120 | { 121 | if (ignorePath.DirectoryIsSubPath(path, true)) 122 | return list; 123 | } 124 | } 125 | 126 | LinkedList> fileTasks = new LinkedList>(); 127 | fileTasks.AddLast(_directoryParser.ParseAsync(path)); 128 | foreach (string file in Directory.GetFiles(path)) 129 | { 130 | fileTasks.AddLast(ParseFileAsync(file, config)); 131 | } 132 | list.AddRange(await Task.WhenAll(fileTasks)); 133 | 134 | if (config.Recursive) 135 | { 136 | LinkedList>> directoryTasks = new LinkedList>>(); 137 | foreach (string directory in Directory.GetDirectories(path)) 138 | directoryTasks.AddLast(ParseDirectoryAsync(directory, config)); 139 | foreach (IEnumerable nestedMetaData in await Task.WhenAll(directoryTasks)) 140 | list.AddRange(nestedMetaData); 141 | } 142 | 143 | return list.Where(x => x != null); 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /UserInterface/UserInterface.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8F6FD0D9-E15F-4701-961E-3075BFB1CDA5} 8 | WinExe 9 | Properties 10 | ExifOrganizer.UI 11 | ExifOrganizer 12 | v4.5 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | true 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | true 35 | 36 | 37 | icon.ico 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Component 55 | 56 | 57 | EnumDropDown.cs 58 | 59 | 60 | 61 | CheckBoxDropDown.cs 62 | 63 | 64 | Component 65 | 66 | 67 | EnumFlagsDropDown.cs 68 | 69 | 70 | UserControl 71 | 72 | 73 | FileBrowseControl.cs 74 | 75 | 76 | Component 77 | 78 | 79 | ProgressBarText.cs 80 | 81 | 82 | 83 | Form 84 | 85 | 86 | Main.cs 87 | 88 | 89 | 90 | 91 | 92 | EnumDropDown.cs 93 | 94 | 95 | CheckBoxDropDown.cs 96 | 97 | 98 | FileBrowseControl.cs 99 | 100 | 101 | Main.cs 102 | 103 | 104 | ResXFileCodeGenerator 105 | Resources.Designer.cs 106 | Designer 107 | 108 | 109 | True 110 | Resources.resx 111 | True 112 | 113 | 114 | SettingsSingleFileGenerator 115 | Settings.Designer.cs 116 | 117 | 118 | True 119 | Settings.settings 120 | True 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | {626f29da-4334-4e7e-8d9a-b2353bb37fc1} 129 | MediaOrganizer 130 | 131 | 132 | {7070940a-c83f-4d3d-bde4-55aee58a9385} 133 | MediaQuerier 134 | 135 | 136 | {19204b12-e95e-405f-9604-52de1411c8b1} 137 | MetaParser 138 | 139 | 140 | 141 | 142 | 143 | 144 | 151 | -------------------------------------------------------------------------------- /Common/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionMethods.cs: Static extension methods used within the project. 3 | // 4 | // Copyright (C) 2018 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.IO; 22 | using System.Linq; 23 | using System.Security.Cryptography; 24 | using System.Text; 25 | using System.Threading.Tasks; 26 | 27 | namespace ExifOrganizer.Common 28 | { 29 | public static class ExtensionMethods 30 | { 31 | public static string GetClassName(this object o) 32 | { 33 | return o.GetType().Name; 34 | } 35 | 36 | public static string SuffixFileName(this FileInfo fileInfo, int index) 37 | { 38 | if (fileInfo == null) 39 | throw new ArgumentNullException(nameof(fileInfo)); 40 | if (index < 0) 41 | throw new ArgumentException(nameof(index)); 42 | 43 | string path = fileInfo.DirectoryName; 44 | string fileName = Path.GetFileNameWithoutExtension(fileInfo.FullName); 45 | string extension = Path.GetExtension(fileInfo.FullName); 46 | 47 | return $"{path}\\{fileName}({index}){extension}"; 48 | } 49 | 50 | public static string ReplaceInvalidPathChars(this string path, char replacement = '_') 51 | { 52 | foreach (char invalidChar in Path.GetInvalidPathChars()) 53 | path = path.Replace(invalidChar, replacement); 54 | return path; 55 | } 56 | 57 | public static bool ToBool(this object value) 58 | { 59 | if (value == null) 60 | throw new ArgumentNullException(nameof(value)); 61 | 62 | string s = value.ToString().ToLower(); 63 | return !(s == "0" || s == "false"); 64 | } 65 | 66 | public static T ToEnum(this object value) where T : struct 67 | { 68 | if (value == null) 69 | throw new ArgumentNullException(nameof(value)); 70 | 71 | string s = value.ToString(); 72 | long i; 73 | if (long.TryParse(s, out i)) 74 | { 75 | // Interpret as numeric enum value 76 | return (T)(object)i; 77 | } 78 | else 79 | { 80 | // Interpret as string enum name 81 | return (T)Enum.Parse(typeof(T), s); 82 | } 83 | } 84 | 85 | public static void ForEach(this IEnumerable enumeration, Action action) 86 | { 87 | foreach (T item in enumeration) 88 | { 89 | action(item); 90 | } 91 | } 92 | 93 | public static string ToString(this IEnumerable enumeration, string separator = ",") 94 | { 95 | if (enumeration == null) 96 | return ""; 97 | 98 | return String.Join(separator, enumeration); 99 | } 100 | 101 | public static string RemoveNullChars(this string s) 102 | { 103 | return s.Replace("\0", ""); 104 | } 105 | 106 | public static string UppercaseFirst(this string s) 107 | { 108 | if (string.IsNullOrEmpty(s)) 109 | return string.Empty; 110 | 111 | char[] a = s.ToCharArray(); 112 | a[0] = char.ToUpper(a[0]); 113 | return new string(a); 114 | } 115 | 116 | public static bool DirectoryAreSame(this string rootPath, string subPath) 117 | { 118 | DirectoryInfo rootDir = new DirectoryInfo(rootPath); 119 | DirectoryInfo subDir = new DirectoryInfo(subPath); 120 | return rootDir.FullName == subDir.FullName; 121 | } 122 | 123 | public static bool DirectoryIsSubPath(this string rootPath, string subPath, bool includeRootPath = true) 124 | { 125 | DirectoryInfo rootDir = new DirectoryInfo(rootPath); 126 | DirectoryInfo subDir = new DirectoryInfo(subPath); 127 | 128 | if (includeRootPath && rootDir.FullName == subDir.FullName) 129 | return true; 130 | 131 | bool isParent = false; 132 | while (subDir.Parent != null) 133 | { 134 | if (subDir.Parent.FullName == rootDir.FullName) 135 | { 136 | isParent = true; 137 | break; 138 | } 139 | else 140 | { 141 | subDir = subDir.Parent; 142 | } 143 | } 144 | 145 | return isParent; 146 | } 147 | 148 | public static string GetMD5Sum(this FileInfo fileInfo) 149 | { 150 | using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider()) 151 | return fileInfo.CalculateChecksum(md5); 152 | } 153 | 154 | public static string GetSHA1Sum(this FileInfo fileInfo) 155 | { 156 | using (SHA1Managed sha1 = new SHA1Managed()) 157 | return fileInfo.CalculateChecksum(sha1); 158 | } 159 | 160 | public static string GetSHA256Sum(this FileInfo fileInfo) 161 | { 162 | using (SHA256Managed sha256 = new SHA256Managed()) 163 | return fileInfo.CalculateChecksum(sha256); 164 | } 165 | 166 | private static string CalculateChecksum(this FileInfo fileInfo, HashAlgorithm algorithm) 167 | { 168 | if (fileInfo == null) 169 | throw new ArgumentNullException(nameof(fileInfo)); 170 | if (!fileInfo.Exists) 171 | throw new FileNotFoundException(fileInfo.FullName); 172 | 173 | using (FileStream stream = File.OpenRead(fileInfo.FullName)) 174 | return stream.CalculateChecksum(algorithm); 175 | } 176 | 177 | public static string GetMD5Sum(this FileStream fileStream) 178 | { 179 | using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider()) 180 | return fileStream.CalculateChecksum(md5); 181 | } 182 | 183 | public static string GetSHA1Sum(this FileStream fileStream) 184 | { 185 | using (SHA1Managed sha1 = new SHA1Managed()) 186 | return fileStream.CalculateChecksum(sha1); 187 | } 188 | 189 | public static string GetSHA256Sum(this FileStream fileStream) 190 | { 191 | using (SHA256Managed sha256 = new SHA256Managed()) 192 | return fileStream.CalculateChecksum(sha256); 193 | } 194 | 195 | private static string CalculateChecksum(this FileStream fileStream, HashAlgorithm algorithm) 196 | { 197 | if (fileStream == null) 198 | throw new ArgumentNullException(nameof(fileStream)); 199 | 200 | using (BufferedStream bufferedStream = new BufferedStream(fileStream)) 201 | { 202 | byte[] checksum = algorithm.ComputeHash(bufferedStream); 203 | return BitConverter.ToString(checksum).Replace("-", String.Empty); 204 | } 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /UserInterface/Main.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Main.cs: Graphical user interface (GUI) main window. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Organizer; 20 | using System; 21 | using System.Globalization; 22 | using System.Windows.Forms; 23 | 24 | namespace ExifOrganizer.UI 25 | { 26 | public partial class Main : Form 27 | { 28 | private MediaOrganizer organizer = new MediaOrganizer(); 29 | 30 | public Main() 31 | { 32 | InitializeComponent(); 33 | 34 | organizer.LoadConfig(); 35 | organizer.OnParseDone += ParseComplete; 36 | organizer.OnOrganizeDone += OrganizeComplete; 37 | organizer.OnProgress += ReportProgress; 38 | } 39 | 40 | #region Control events 41 | 42 | private void Main_Load(object sender, EventArgs e) 43 | { 44 | #if DEBUG 45 | this.Text += " (DEBUG)"; 46 | #endif 47 | infoVersion.Text = $"Version: {new Version(Application.ProductVersion).Get()}"; 48 | 49 | sourcePath.Text = organizer.sourcePath; 50 | destinationPath.Text = organizer.destinationPath; 51 | recursive.Checked = organizer.Recursive; 52 | 53 | // Copy precondition 54 | copyPrecondition.EnumText = value => 55 | { 56 | switch ((CopyPrecondition)value) 57 | { 58 | case CopyPrecondition.RequireEmpty: return "Require empty"; 59 | case CopyPrecondition.WipeBefore: return "Wipe before"; 60 | default: return value.ToString(); 61 | } 62 | }; 63 | copyPrecondition.EnumType = typeof(CopyPrecondition); 64 | copyPrecondition.EnumValue = organizer.CopyPrecondition; 65 | 66 | // Copy mode 67 | copyMode.EnumText = value => 68 | { 69 | switch ((CopyMode)value) 70 | { 71 | case CopyMode.KeepExisting: return "Keep existing"; 72 | case CopyMode.KeepUnique: return "Keep unique"; 73 | case CopyMode.OverwriteExisting: return "Overwrite existing"; 74 | default: return value.ToString(); 75 | } 76 | }; 77 | copyMode.EnumType = typeof(CopyMode); 78 | copyMode.EnumValue = organizer.CopyMode; 79 | 80 | Func fileComparatorText = value => 81 | { 82 | switch ((FileComparator)value) 83 | { 84 | case FileComparator.FileSize: return "File size"; 85 | case FileComparator.ChecksumMD5: return "MD5"; 86 | case FileComparator.ChecksumSHA1: return "SHA1"; 87 | case FileComparator.ChecksumSHA256: return "SHA256"; 88 | default: return value.ToString(); 89 | } 90 | }; 91 | 92 | // File comparator 93 | fileComparator.EnumText = fileComparatorText; 94 | fileComparator.EnumType = typeof(FileComparator); 95 | fileComparator.EnumValue = organizer.FileComparator; 96 | 97 | // File verification 98 | fileVerification.EnumText = fileComparatorText; 99 | fileVerification.EnumType = typeof(FileComparator); 100 | fileVerification.EnumValue = organizer.FileVerification; 101 | 102 | // CultureInfo localization 103 | foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) 104 | localization.Items.Add(culture); 105 | localization.SelectedItem = organizer.Locale; 106 | 107 | // Media patterns 108 | patternImage.Text = organizer.DestinationPatternImage; 109 | patternAudio.Text = organizer.DestinationPatternAudio; 110 | patternVideo.Text = organizer.DestinationPatternVideo; 111 | 112 | ToolTip patternToolTip = new ToolTip() { IsBalloon = true, InitialDelay = 100, ReshowDelay = 500 }; 113 | patternToolTip.SetToolTip(patternImage, PatternPathParser.GetUsageText(Meta.MetaMediaType.Image)); 114 | patternToolTip.SetToolTip(patternAudio, PatternPathParser.GetUsageText(Meta.MetaMediaType.Audio)); 115 | patternToolTip.SetToolTip(patternVideo, PatternPathParser.GetUsageText(Meta.MetaMediaType.Video)); 116 | } 117 | 118 | private void Main_FormClosing(object sender, FormClosingEventArgs e) 119 | { 120 | if (!organizer.IsRunning || organizer.IsAborted) 121 | return; 122 | 123 | if (MessageBox.Show(this, "Organization currently in progress, it must be aborted before the application can be closed.", "Abort current progress?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) 124 | { 125 | e.Cancel = true; 126 | return; 127 | } 128 | 129 | organizer.Abort(); 130 | } 131 | 132 | private async void organize_Click(object sender, EventArgs e) 133 | { 134 | if (organizer.IsRunning) 135 | { 136 | AbortProgress(); 137 | return; 138 | } 139 | 140 | organizer.sourcePath = sourcePath.SelectedPath; 141 | organizer.destinationPath = destinationPath.SelectedPath; 142 | organizer.Recursive = recursive.Checked; 143 | organizer.FileComparator = (FileComparator)fileComparator.EnumValue; 144 | organizer.CopyPrecondition = (CopyPrecondition)copyPrecondition.EnumValue; 145 | organizer.CopyMode = (CopyMode)copyMode.EnumValue; 146 | organizer.FileVerification = (FileComparator)fileVerification.EnumValue; 147 | organizer.DestinationPatternImage = patternImage.Text; 148 | organizer.DestinationPatternVideo = patternVideo.Text; 149 | organizer.DestinationPatternAudio = patternAudio.Text; 150 | organizer.Locale = (CultureInfo)localization.SelectedItem; 151 | organizer.SaveConfig(); 152 | 153 | ProgressStarted(); 154 | 155 | try 156 | { 157 | await organizer.OrganizeAsync(); 158 | } 159 | catch (Exception ex) 160 | { 161 | OrganizeException(organizer, ex); 162 | } 163 | } 164 | 165 | #endregion Control events 166 | 167 | private void AbortProgress() 168 | { 169 | organizer.Abort(); 170 | } 171 | 172 | private void ReportProgress(MediaOrganizer organizer, double value, string message) 173 | { 174 | if (this.InvokeRequired) 175 | { 176 | this.BeginInvoke(() => ReportProgress(organizer, value, message)); 177 | return; 178 | } 179 | 180 | double percent = value * 100.0; 181 | if (String.IsNullOrEmpty(message)) 182 | message = $"{Math.Round(percent, 1)}%"; 183 | else 184 | message = $"{Math.Round(percent, 1)}% - {message}"; 185 | 186 | progress.SetProgress(value, message); 187 | } 188 | 189 | private void ProgressStarted() 190 | { 191 | if (this.InvokeRequired) 192 | { 193 | this.BeginInvoke(() => ProgressStarted()); 194 | return; 195 | } 196 | 197 | organize.Text = "Abort"; 198 | progress.Value = progress.Minimum; 199 | progress.Visible = true; 200 | } 201 | 202 | private void ProgressEnded() 203 | { 204 | if (this.InvokeRequired) 205 | { 206 | this.BeginInvoke(() => ProgressEnded()); 207 | return; 208 | } 209 | 210 | organize.Text = "Organize"; 211 | progress.Visible = false; 212 | } 213 | 214 | private void ParseComplete(MediaOrganizer organizer, ParseSummary summary) 215 | { 216 | if (this.InvokeRequired) 217 | { 218 | this.BeginInvoke(() => ParseComplete(organizer, summary)); 219 | return; 220 | } 221 | 222 | if (MessageBox.Show(summary.ToString(), "Continue organization?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) 223 | { 224 | organizer.Abort(); 225 | ProgressEnded(); 226 | } 227 | } 228 | 229 | private void OrganizeComplete(MediaOrganizer organizer, OrganizeSummary summary) 230 | { 231 | if (this.InvokeRequired) 232 | { 233 | this.BeginInvoke(() => OrganizeComplete(organizer, summary)); 234 | return; 235 | } 236 | 237 | MessageBox.Show(summary.ToString(), "Media organization done", MessageBoxButtons.OK, MessageBoxIcon.Information); 238 | ProgressEnded(); 239 | } 240 | 241 | private void OrganizeException(MediaOrganizer organizer, Exception exception) 242 | { 243 | if (this.InvokeRequired) 244 | { 245 | this.BeginInvoke(() => OrganizeException(organizer, exception)); 246 | return; 247 | } 248 | 249 | Exception innerException = exception.GetInnerMost(); 250 | MessageBox.Show(innerException.Message, "Media organization failed", MessageBoxButtons.OK, MessageBoxIcon.Error); 251 | ProgressEnded(); 252 | } 253 | } 254 | } -------------------------------------------------------------------------------- /MediaQuerier/MediaQuerier.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MediaQuerier.cs: Static class to query media files based on properties. 3 | // 4 | // Copyright (C) 2016 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using ExifOrganizer.Common; 20 | using ExifOrganizer.Meta; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Diagnostics; 24 | using System.IO; 25 | using System.Linq; 26 | using System.Security.Cryptography; 27 | using System.Threading.Tasks; 28 | 29 | namespace ExifOrganizer.Querier 30 | { 31 | [Flags] 32 | public enum QueryType 33 | { 34 | None = 0x00, 35 | EqualFileName = 0x01, 36 | EqualFileNameDifferentFileSize = 0x02, 37 | EqualChecksumMD5 = 0x04, 38 | EqualFileNameDifferentChecksumMD5 = 0x08, 39 | EqualChecksumSHA1 = 0x10, 40 | EqualFileNameDifferentChecksumSHA1 = 0x20, 41 | EqualChecksumSHA256 = 0x40, 42 | EqualFileNameDifferentChecksumSHA256 = 0x80, 43 | LowResolution = 0x100, 44 | EqualFileNameDifferentResolution = 0x200, 45 | All = 0xFFFF 46 | } 47 | 48 | public class QuerySummary 49 | { 50 | public Dictionary>> matches; 51 | } 52 | 53 | public class MediaQuerier 54 | { 55 | public event Action OnProgress = delegate { }; 56 | 57 | public MediaQuerier() 58 | { 59 | } 60 | 61 | public QuerySummary Query(string sourcePath, bool recursive, QueryType queries, params MetaType[] types) 62 | { 63 | Task task = QueryAsync(sourcePath, recursive, queries, types); 64 | task.ConfigureAwait(false); // Prevent deadlock of caller 65 | return task.Result; 66 | } 67 | 68 | public async Task QueryAsync(string sourcePath, bool recursive, QueryType queries, params MetaType[] types) 69 | { 70 | if (String.IsNullOrEmpty(sourcePath)) 71 | throw new ArgumentNullException(nameof(sourcePath)); 72 | if (!Directory.Exists(sourcePath)) 73 | throw new DirectoryNotFoundException(sourcePath); 74 | if (queries == QueryType.None) 75 | throw new ArgumentException("No queries given."); 76 | 77 | Dictionary>> allMatches = new Dictionary>>(); 78 | Dictionary md5sums = new Dictionary(); 79 | Dictionary sha1sums = new Dictionary(); 80 | Dictionary sha256sums = new Dictionary(); 81 | IEnumerable data = await GetMetaData(sourcePath, recursive, types); 82 | foreach (MetaData item in data.Where(x => x.Type != MetaType.Directory && x.Type != MetaType.File)) 83 | { 84 | List> matches = new List>(); 85 | foreach (MetaData other in data.Where(x => x.Type != MetaType.Directory && x.Type != MetaType.File && !x.Equals(item))) 86 | { 87 | QueryType match = QueryType.None; 88 | 89 | bool equalFilename = Path.GetFileName(item.Path).Equals(Path.GetFileName(other.Path), StringComparison.Ordinal); 90 | if (queries.HasFlag(QueryType.EqualFileName) && equalFilename) 91 | match |= QueryType.EqualFileName; 92 | 93 | if (queries.HasFlag(QueryType.EqualFileNameDifferentFileSize) && equalFilename) 94 | { 95 | if (RequireMetaKey(MetaKey.Size, item, other)) 96 | { 97 | if (item.Data[MetaKey.Size] != other.Data[MetaKey.Size]) 98 | match |= QueryType.EqualFileNameDifferentFileSize; 99 | } 100 | } 101 | 102 | if (queries.HasFlag(QueryType.EqualChecksumMD5) || queries.HasFlag(QueryType.EqualFileNameDifferentChecksumMD5)) 103 | { 104 | string md5item; 105 | if (!md5sums.TryGetValue(item.Path, out md5item)) 106 | { 107 | using (FileStream fileStream = File.OpenRead(item.Path)) 108 | md5item = fileStream.GetMD5Sum(); 109 | md5sums[item.Path] = md5item; 110 | } 111 | 112 | string md5other; 113 | if (!md5sums.TryGetValue(other.Path, out md5other)) 114 | { 115 | using (FileStream fileStream = File.OpenRead(other.Path)) 116 | md5other = fileStream.GetMD5Sum(); 117 | md5sums[other.Path] = md5other; 118 | } 119 | 120 | bool md5match = md5item.Equals(md5other, StringComparison.Ordinal); 121 | if (queries.HasFlag(QueryType.EqualChecksumMD5) && md5match) 122 | match |= QueryType.EqualChecksumMD5; 123 | if (queries.HasFlag(QueryType.EqualFileNameDifferentChecksumMD5) && equalFilename && !md5match) 124 | match |= QueryType.EqualFileNameDifferentChecksumMD5; 125 | } 126 | 127 | if (queries.HasFlag(QueryType.EqualChecksumSHA1) || queries.HasFlag(QueryType.EqualFileNameDifferentChecksumSHA1)) 128 | { 129 | string sha1item; 130 | if (!sha1sums.TryGetValue(item.Path, out sha1item)) 131 | { 132 | using (FileStream fileStream = File.OpenRead(item.Path)) 133 | sha1item = fileStream.GetSHA1Sum(); 134 | sha1sums[item.Path] = sha1item; 135 | } 136 | 137 | string sha1other; 138 | if (!sha1sums.TryGetValue(other.Path, out sha1other)) 139 | { 140 | using (FileStream fileStream = File.OpenRead(other.Path)) 141 | sha1other = fileStream.GetSHA1Sum(); 142 | sha1sums[other.Path] = sha1other; 143 | } 144 | 145 | bool sha1match = sha1item.Equals(sha1other, StringComparison.Ordinal); 146 | if (queries.HasFlag(QueryType.EqualChecksumSHA1) && sha1match) 147 | match |= QueryType.EqualChecksumSHA1; 148 | if (queries.HasFlag(QueryType.EqualFileNameDifferentChecksumSHA1) && equalFilename && !sha1match) 149 | match |= QueryType.EqualFileNameDifferentChecksumSHA1; 150 | } 151 | 152 | if (queries.HasFlag(QueryType.EqualChecksumSHA256) || queries.HasFlag(QueryType.EqualFileNameDifferentChecksumSHA256)) 153 | { 154 | string sha256item; 155 | if (!sha256sums.TryGetValue(item.Path, out sha256item)) 156 | { 157 | using (FileStream fileStream = File.OpenRead(item.Path)) 158 | sha256item = fileStream.GetSHA256Sum(); 159 | sha256sums[item.Path] = sha256item; 160 | } 161 | 162 | string sha256other; 163 | if (!sha256sums.TryGetValue(other.Path, out sha256other)) 164 | { 165 | using (FileStream fileStream = File.OpenRead(other.Path)) 166 | sha256other = fileStream.GetSHA256Sum(); 167 | sha256sums[other.Path] = sha256other; 168 | } 169 | 170 | bool sha256match = sha256item.Equals(sha256other, StringComparison.Ordinal); 171 | if (queries.HasFlag(QueryType.EqualChecksumSHA256) && sha256match) 172 | match |= QueryType.EqualChecksumSHA256; 173 | if (queries.HasFlag(QueryType.EqualFileNameDifferentChecksumSHA256) && equalFilename && !sha256match) 174 | match |= QueryType.EqualFileNameDifferentChecksumSHA256; 175 | } 176 | 177 | if (queries.HasFlag(QueryType.LowResolution) || queries.HasFlag(QueryType.EqualFileNameDifferentResolution)) 178 | { 179 | if (RequireMetaKey(MetaKey.Width, item) && RequireMetaKey(MetaKey.Height, item)) 180 | { 181 | //const int limit = 960 * 1280; 182 | const int resolutionLimit = 1024 * 768; 183 | 184 | int resolutionItem = (int)item.Data[MetaKey.Width] * (int)item.Data[MetaKey.Height]; 185 | if (queries.HasFlag(QueryType.LowResolution) && resolutionItem <= resolutionLimit) 186 | match |= QueryType.LowResolution; 187 | 188 | if (queries.HasFlag(QueryType.EqualFileNameDifferentResolution) && equalFilename) 189 | { 190 | if (RequireMetaKey(MetaKey.Width, other) && RequireMetaKey(MetaKey.Height, other)) 191 | { 192 | int resolutionOther = (int)other.Data[MetaKey.Width] * (int)other.Data[MetaKey.Height]; 193 | if (resolutionItem != resolutionOther) 194 | match |= QueryType.EqualFileNameDifferentResolution; 195 | } 196 | } 197 | } 198 | } 199 | 200 | if (match != QueryType.None) 201 | matches.Add(Tuple.Create(other.Path, match)); 202 | } 203 | 204 | if (matches.Count > 0) 205 | allMatches[item.Path] = CleanupMatches(matches); 206 | } 207 | 208 | QuerySummary result = new QuerySummary(); 209 | result.matches = allMatches; 210 | return result; 211 | } 212 | 213 | private IEnumerable> CleanupMatches(IEnumerable> matches) 214 | { 215 | HashSet matchedNoComparison = new HashSet(); 216 | foreach (var match in matches) 217 | { 218 | switch (match.Item2) 219 | { 220 | case QueryType.LowResolution: 221 | if (matchedNoComparison.Add(match.Item2)) 222 | yield return new Tuple(String.Empty, match.Item2); 223 | break; 224 | 225 | default: 226 | yield return match; 227 | break; 228 | } 229 | } 230 | } 231 | 232 | private bool RequireMetaKey(MetaKey key, params MetaData[] items) 233 | { 234 | bool result = true; 235 | foreach (MetaData item in items) 236 | { 237 | if (!item.Data.ContainsKey(key)) 238 | { 239 | Trace.WriteLine($"[MediaQuerier] required meta key \"{key}\" not found in file: {item.Path}"); 240 | result = false; 241 | } 242 | } 243 | 244 | return result; 245 | } 246 | 247 | public async Task> GetMetaData(string sourcePath, bool recursive, params MetaType[] types) 248 | { 249 | try 250 | { 251 | MetaParserConfig config = new MetaParserConfig() { Recursive = recursive, FilterTypes = types }; 252 | return await MetaParser.ParseAsync(sourcePath, config); 253 | } 254 | catch (MetaParseException ex) 255 | { 256 | throw new MediaQuerierException("Failed to parse meta data", ex); 257 | } 258 | } 259 | } 260 | } -------------------------------------------------------------------------------- /MetaParser/Parsers/MP4Parser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // MP4Parser.cs: MPEG-4 Part 14 (MP4) container meta parser class. 3 | // 4 | // Copyright (C) 2018 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.IO; 23 | using System.Linq; 24 | using System.Text; 25 | using System.Threading.Tasks; 26 | 27 | namespace ExifOrganizer.Meta.Parsers 28 | { 29 | internal class MP4Parser : FileParser 30 | { 31 | private static readonly Encoding iso_8859_1 = Encoding.GetEncoding("iso-8859-1"); 32 | 33 | private enum MP4Tag 34 | { 35 | // ftyp 36 | FileFormat, 37 | 38 | // moov.udta.meta.ilst 39 | Album, 40 | Artist, 41 | Comment, 42 | Year, 43 | Title, 44 | Genre, 45 | TrackNumber, 46 | DiscNumber, 47 | Composer, 48 | Encoder, 49 | BPM, 50 | Copyright, 51 | Compilation, 52 | Grouping, 53 | Category, 54 | Keyword, 55 | Description 56 | } 57 | 58 | private enum MP4DataType : byte 59 | { 60 | UInt8_a = 0, 61 | Text = 1, 62 | JPEG = 13, 63 | PNG = 14, 64 | UInt8_b = 21, 65 | } 66 | 67 | internal override IEnumerable GetSupportedFileExtensions() 68 | { 69 | return new string[] { ".mp4", ".m4a", ".mov", ".3gp", ".3g2" }; 70 | } 71 | 72 | internal override MetaType? GetMetaTypeByFileExtension(string extension) 73 | { 74 | switch (extension) 75 | { 76 | case ".mp4": 77 | case ".mov": 78 | case ".3gp": 79 | case ".3g2": 80 | return MetaType.Video; 81 | case ".m4a": 82 | return MetaType.Audio; 83 | default: 84 | return base.GetMetaTypeByFileExtension(extension); 85 | } 86 | } 87 | 88 | protected override MetaData ParseFile(Stream stream, MetaData meta) 89 | { 90 | Dictionary mp4 = ParseMP4(meta.Path); 91 | if (mp4.ContainsKey(MP4Tag.FileFormat)) 92 | meta.Data[MetaKey.MetaType] = $"MPEG-4 Part 14 ({mp4[MP4Tag.FileFormat]})"; 93 | if (mp4.ContainsKey(MP4Tag.Album)) 94 | meta.Data[MetaKey.Album] = mp4[MP4Tag.Album]; 95 | if (mp4.ContainsKey(MP4Tag.Artist)) 96 | meta.Data[MetaKey.Artist] = mp4[MP4Tag.Artist]; 97 | if (mp4.ContainsKey(MP4Tag.Comment)) 98 | meta.Data[MetaKey.Comment] = mp4[MP4Tag.Comment]; 99 | if (mp4.ContainsKey(MP4Tag.Year)) 100 | meta.Data[MetaKey.Year] = mp4[MP4Tag.Year]; 101 | if (mp4.ContainsKey(MP4Tag.Title)) 102 | meta.Data[MetaKey.Title] = mp4[MP4Tag.Title]; 103 | if (mp4.ContainsKey(MP4Tag.Genre)) 104 | meta.Data[MetaKey.Genre] = mp4[MP4Tag.Genre]; 105 | if (mp4.ContainsKey(MP4Tag.TrackNumber)) 106 | meta.Data[MetaKey.Track] = mp4[MP4Tag.TrackNumber]; 107 | 108 | return meta; 109 | } 110 | 111 | private static MetaType GetMetaType(string filename) 112 | { 113 | return Path.GetExtension(filename) == ".m4a" ? MetaType.Audio : MetaType.Video; // TODO: properly define 114 | } 115 | 116 | private static Dictionary ParseMP4(string filename) 117 | { 118 | Dictionary tags = new Dictionary(); 119 | 120 | using (FileStream stream = File.OpenRead(filename)) 121 | tags = ParseBox(stream, stream.Length); 122 | 123 | return tags; 124 | } 125 | 126 | private static Dictionary ParseBox(Stream stream, long endPosition, string parent = "", int depth = 0) 127 | { 128 | Dictionary tags = new Dictionary(); 129 | 130 | while (stream.Position < endPosition) 131 | { 132 | byte[] header = new byte[8]; 133 | if (stream.Read(header, 0, header.Length) != header.Length) 134 | throw new MetaParseException("Unable to read full MP4 box header"); 135 | 136 | int size = BitConverter.ToInt32(header.Take(4).Reverse().ToArray(), 0); 137 | if (size < 8) 138 | continue; 139 | size -= 8; 140 | string type = iso_8859_1.GetString(header, 4, 4); 141 | int padding = GetBoxPadding(type); 142 | if (padding > 0) 143 | { 144 | size -= padding; 145 | stream.Position += padding; 146 | } 147 | 148 | //Trace.WriteLine($"[MP4Parser] {new string('-', depth * 3)}( type: \"{type}\", depth: {depth}, size: {size}, padding: {padding} )"); 149 | switch (type) 150 | { 151 | case "ftyp": 152 | tags[MP4Tag.FileFormat] = GetBoxAsString(stream, size); 153 | break; 154 | 155 | // Containers 156 | case "moov": 157 | //case "trak": 158 | //case "mdia": 159 | //case "minf": 160 | //case "dinf": 161 | //case "stbl": 162 | case "udta": 163 | case "meta": 164 | case "ilst": 165 | foreach (var kvp in ParseBox(stream, stream.Position + size, type, depth + 1)) 166 | tags[kvp.Key] = kvp.Value; 167 | break; 168 | 169 | default: 170 | if (parent == "ilst") 171 | { 172 | foreach (var kvp in ParseIlstChild(stream, type)) 173 | tags[kvp.Key] = kvp.Value; 174 | break; 175 | } 176 | 177 | Trace.WriteLine($"[MP4Parser] ignoring box type: \"{type}\""); 178 | stream.Position += size; 179 | break; 180 | } 181 | } 182 | 183 | return tags; 184 | } 185 | 186 | private static int GetBoxPadding(string type) 187 | { 188 | switch (type) 189 | { 190 | case "stsd": return 8; 191 | case "mp4a": return 28; 192 | case "drms": return 28; 193 | case "meta": return 4; 194 | default: return 0; 195 | } 196 | } 197 | 198 | private static Dictionary ParseIlstChild(Stream stream, string type) 199 | { 200 | Dictionary tags = new Dictionary(); 201 | 202 | byte[] childHeader = new byte[8]; 203 | if (stream.Read(childHeader, 0, childHeader.Length) != childHeader.Length) 204 | throw new MetaParseException($"Unable to read full MP4 \"ilst.{type}\" header"); 205 | 206 | int childSize = BitConverter.ToInt32(childHeader.Take(4).Reverse().ToArray(), 0); 207 | if (childSize < 8) 208 | return tags; 209 | childSize -= 8; 210 | string childType = iso_8859_1.GetString(childHeader, 4, 4); 211 | if (childType != "data") 212 | throw new MetaParseException($"MP4 \"ilst.{type}\" must have a child of type \"data\", actual: \"{childType}\""); 213 | 214 | byte[] childFlags = new byte[4]; 215 | if (stream.Read(childFlags, 0, childFlags.Length) != childFlags.Length) 216 | throw new MetaParseException($"Unable to read full MP4 \"ilst.{type}.{childType}\" flags"); 217 | 218 | stream.Position += 4; // Ignore null bytes 219 | childSize -= 8; 220 | 221 | MP4DataType dataType = (MP4DataType)childFlags[3]; 222 | 223 | switch (type) 224 | { 225 | case "©alb": 226 | tags[MP4Tag.Album] = GetBoxAsObject(stream, childSize, dataType); 227 | break; 228 | 229 | case "©art": 230 | tags[MP4Tag.Artist] = GetBoxAsObject(stream, childSize, dataType); 231 | break; 232 | 233 | case "©cmt": 234 | tags[MP4Tag.Comment] = GetBoxAsObject(stream, childSize, dataType); 235 | break; 236 | 237 | case "©day": 238 | tags[MP4Tag.Year] = GetBoxAsObject(stream, childSize, dataType); 239 | break; 240 | 241 | case "©nam": 242 | tags[MP4Tag.Title] = GetBoxAsObject(stream, childSize, dataType); 243 | break; 244 | 245 | case "©gen": 246 | case "gnre": 247 | tags[MP4Tag.Genre] = GetBoxAsObject(stream, childSize, dataType); 248 | break; 249 | 250 | case "trkn": 251 | tags[MP4Tag.TrackNumber] = GetBoxAsObject(stream, childSize, dataType); 252 | break; 253 | 254 | case "disk": 255 | tags[MP4Tag.DiscNumber] = GetBoxAsObject(stream, childSize, dataType); 256 | break; 257 | 258 | case "©wrt": 259 | tags[MP4Tag.Composer] = GetBoxAsObject(stream, childSize, dataType); 260 | break; 261 | 262 | case "©too": 263 | tags[MP4Tag.Encoder] = GetBoxAsObject(stream, childSize, dataType); 264 | break; 265 | 266 | case "tmpo": 267 | tags[MP4Tag.BPM] = GetBoxAsObject(stream, childSize, dataType); 268 | break; 269 | 270 | case "cprt": 271 | tags[MP4Tag.Copyright] = GetBoxAsObject(stream, childSize, dataType); 272 | break; 273 | 274 | case "cpil": 275 | tags[MP4Tag.Composer] = GetBoxAsObject(stream, childSize, dataType); 276 | break; 277 | 278 | case "©grp": 279 | tags[MP4Tag.Grouping] = GetBoxAsObject(stream, childSize, dataType); 280 | break; 281 | 282 | case "catg": 283 | tags[MP4Tag.Category] = GetBoxAsObject(stream, childSize, dataType); 284 | break; 285 | 286 | case "keyw": 287 | tags[MP4Tag.Keyword] = GetBoxAsObject(stream, childSize, dataType); 288 | break; 289 | 290 | case "desc": 291 | tags[MP4Tag.Description] = GetBoxAsObject(stream, childSize, dataType); 292 | break; 293 | 294 | default: 295 | Trace.WriteLine($"[MP4Parser] ignoring ilst.{type}.{childType} data"); 296 | stream.Position += childSize; 297 | break; 298 | } 299 | 300 | return tags; 301 | } 302 | 303 | private static string GetBoxAsString(Stream stream, int size) 304 | { 305 | return (string)GetBoxAsObject(stream, size, MP4DataType.Text); 306 | } 307 | 308 | private static object GetBoxAsObject(Stream stream, int size, MP4DataType type) 309 | { 310 | byte[] buffer = new byte[size]; 311 | if (stream.Read(buffer, 0, buffer.Length) != buffer.Length) 312 | throw new MetaParseException("Unable to read full MP4 box content"); 313 | 314 | switch (type) 315 | { 316 | case MP4DataType.UInt8_a: 317 | case MP4DataType.UInt8_b: 318 | if (buffer.Length != 1) 319 | throw new MetaParseException($"Box content type {type} require data to be a single byte. number of bytes: {buffer.Length}."); 320 | return (int)buffer[0]; 321 | 322 | case MP4DataType.Text: 323 | if (buffer.Length == 0) 324 | return String.Empty; 325 | byte[] text = buffer.TakeWhile(c => c != '\0').ToArray(); 326 | return iso_8859_1.GetString(text); 327 | 328 | default: 329 | throw new NotImplementedException($"Unsupported box content type: {type}"); 330 | } 331 | } 332 | } 333 | } -------------------------------------------------------------------------------- /MetaParser/Parsers/PNGParser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // PNGParser.cs: PNG (Portable Network Graphics) meta parser class. 3 | // 4 | // Copyright (C) 2018 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.IO; 23 | using System.Linq; 24 | using System.Text; 25 | using System.Threading.Tasks; 26 | 27 | namespace ExifOrganizer.Meta.Parsers 28 | { 29 | internal class PNGParser : FileParser 30 | { 31 | private static readonly Encoding iso_8859_1 = Encoding.GetEncoding("iso-8859-1"); 32 | 33 | private enum PNGTag 34 | { 35 | // IHDR 36 | ImageWidth, 37 | ImageHeight, 38 | BitDepth, 39 | ColorType, 40 | CompressionMethod, 41 | FilterMethod, 42 | InterlaceMethod, 43 | // tIME 44 | LastModified, 45 | // tEXt/iTXt 46 | Title, 47 | Author, 48 | Description, 49 | Copyright, 50 | CreationTime, 51 | Software, 52 | Disclaimer, 53 | Warning, 54 | Source, 55 | Comment 56 | } 57 | 58 | private enum PNGBitDepth : byte 59 | { 60 | Greyscale = 0, 61 | Truecolor = 2, 62 | IndexedColor = 3, 63 | GreyscaleWithAlpha = 4, 64 | TruecolorWithAlpha = 6 65 | } 66 | 67 | internal override IEnumerable GetSupportedFileExtensions() 68 | { 69 | return new string[] { ".png" }; 70 | } 71 | 72 | internal override MetaType? GetMetaTypeByFileExtension(string extension) 73 | { 74 | if (GetSupportedFileExtensions().Contains(extension)) 75 | return MetaType.Image; 76 | return base.GetMetaTypeByFileExtension(extension); 77 | } 78 | 79 | protected override MetaData ParseFile(Stream stream, MetaData meta) 80 | { 81 | Dictionary png = ParsePNG(meta.Path); 82 | if (png.ContainsKey(PNGTag.ImageWidth)) 83 | meta.Data[MetaKey.Width] = png[PNGTag.ImageWidth]; 84 | if (png.ContainsKey(PNGTag.ImageHeight)) 85 | meta.Data[MetaKey.Height] = png[PNGTag.ImageHeight]; 86 | if (meta.Data.ContainsKey(MetaKey.Width) && meta.Data.ContainsKey(MetaKey.Height)) 87 | meta.Data[MetaKey.Resolution] = $"{meta.Data[MetaKey.Width]}x{meta.Data[MetaKey.Height]}"; 88 | if (png.ContainsKey(PNGTag.LastModified)) 89 | meta.Data[MetaKey.DateModified] = png[PNGTag.LastModified]; 90 | if (png.ContainsKey(PNGTag.Comment)) 91 | meta.Data[MetaKey.Comment] = png[PNGTag.Comment]; 92 | 93 | return meta; 94 | } 95 | 96 | private static Dictionary ParsePNG(string filename) 97 | { 98 | Dictionary tags = new Dictionary(); 99 | 100 | using (FileStream stream = File.OpenRead(filename)) 101 | { 102 | if (!VerifyHeaderSignature(stream)) 103 | return tags; 104 | 105 | while (stream.Position < stream.Length) 106 | { 107 | byte[] chunkHeader = new byte[8]; 108 | if (stream.Read(chunkHeader, 0, chunkHeader.Length) != chunkHeader.Length) 109 | throw new MetaParseException("Unable to read full PNG chunk header"); 110 | 111 | int chunkLength = BitConverter.ToInt32(chunkHeader.Take(4).Reverse().ToArray(), 0); 112 | string chunkType = Encoding.ASCII.GetString(chunkHeader, 4, 4); 113 | 114 | foreach (var kvp in GetChunkData(stream, chunkLength, chunkType)) 115 | { 116 | if (tags.ContainsKey(kvp.Key)) 117 | { 118 | Trace.WriteLine($"[PNGParser] duplicate meta key ignored: \"{kvp.Key}\""); 119 | continue; 120 | } 121 | tags[kvp.Key] = kvp.Value; 122 | } 123 | 124 | } 125 | } 126 | 127 | return tags; 128 | } 129 | 130 | private static bool VerifyHeaderSignature(Stream stream) 131 | { 132 | if (stream.Length < 8) 133 | return false; 134 | 135 | byte[] signature = new byte[8]; 136 | if (stream.Read(signature, 0, signature.Length) != signature.Length) 137 | throw new MetaParseException("Unable to read full PNG header signature"); 138 | 139 | if (signature[0] != 0x89) 140 | return false; 141 | if (signature[1] != 'P' || signature[2] != 'N' || signature[3] != 'G') 142 | return false; 143 | if (signature[4] != 0x0d || signature[5] != 0x0a || signature[6] != 0x1a || signature[5] != 0x0a) 144 | return false; 145 | return true; 146 | } 147 | 148 | private static Dictionary GetChunkData(Stream stream, int length, string type) 149 | { 150 | Dictionary tags = new Dictionary(); 151 | 152 | // TODO: parse "eXIf" - Exif meta data 153 | // TODO: parse "pHYs" - indended pixel size, ratio, etc. 154 | // TODO: parse "zTXt" - compressed text 155 | switch (type) 156 | { 157 | case "IHDR": 158 | { 159 | byte[] chunkData = ReadChunkData(stream, length, type); 160 | if (chunkData.Length != 13) 161 | throw new MetaParseException($"Invalid PNG \"{type}\" chunk length: {length}"); 162 | 163 | tags[PNGTag.ImageWidth] = BitConverter.ToInt32(chunkData.Take(4).Reverse().ToArray(), 0); 164 | tags[PNGTag.ImageHeight] = BitConverter.ToInt32(chunkData.Skip(4).Take(4).Reverse().ToArray(), 0); 165 | tags[PNGTag.BitDepth] = (int)chunkData[8]; 166 | tags[PNGTag.ColorType] = (PNGBitDepth)chunkData[9]; 167 | tags[PNGTag.CompressionMethod] = (int)chunkData[10]; 168 | tags[PNGTag.FilterMethod] = (int)chunkData[11]; 169 | tags[PNGTag.InterlaceMethod] = (int)chunkData[12]; 170 | } 171 | break; 172 | 173 | case "tIME": 174 | { 175 | byte[] chunkData = ReadChunkData(stream, length, type); 176 | if (chunkData.Length != 7) 177 | throw new MetaParseException($"Invalid PNG \"{type}\" chunk length: {length}"); 178 | 179 | int year = BitConverter.ToInt16(chunkData.Take(2).Reverse().ToArray(), 0); 180 | int month = chunkData[2]; 181 | int day = chunkData[3]; 182 | int hour = chunkData[4]; 183 | int minute = chunkData[5]; 184 | int second = chunkData[6]; 185 | DateTime lastModification = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc); 186 | tags[PNGTag.LastModified] = lastModification; 187 | } 188 | break; 189 | 190 | case "tEXt": 191 | case "iTXt": 192 | { 193 | byte[] chunkData = ReadChunkData(stream, length, type); 194 | 195 | byte[] keyData = chunkData.TakeWhile(c => c != '\0').ToArray(); 196 | if (keyData.Length == length) 197 | throw new MetaParseException($"Invalid PNG key-value data in chunk \"{type}\""); 198 | byte[] valueData = chunkData.Skip(keyData.Length + 1).ToArray(); 199 | 200 | string key = Encoding.ASCII.GetString(keyData); 201 | string value = (type == "iTXt") ? Encoding.UTF8.GetString(valueData) : iso_8859_1.GetString(valueData); 202 | switch (key) 203 | { 204 | case "Title": 205 | tags[PNGTag.Title] = value; 206 | break; 207 | 208 | case "Author": 209 | tags[PNGTag.Author] = value; 210 | break; 211 | 212 | case "Description": 213 | tags[PNGTag.Description] = value; 214 | break; 215 | 216 | case "Copyright": 217 | tags[PNGTag.Copyright] = value; 218 | break; 219 | 220 | case "Creation Time": 221 | tags[PNGTag.CreationTime] = value; 222 | break; 223 | 224 | case "Software": 225 | tags[PNGTag.Software] = value; 226 | break; 227 | 228 | case "Disclaimer": 229 | tags[PNGTag.Disclaimer] = value; 230 | break; 231 | 232 | case "Warning": 233 | tags[PNGTag.Warning] = value; 234 | break; 235 | 236 | case "Source": 237 | tags[PNGTag.Source] = value; 238 | break; 239 | 240 | case "Comment": 241 | tags[PNGTag.Comment] = value; 242 | break; 243 | 244 | default: 245 | Trace.WriteLine($"[PNGParser] ignoring \"{type}\" chunk key: \"{key}\""); 246 | break; 247 | } 248 | } 249 | break; 250 | 251 | default: 252 | Trace.WriteLine($"[PNGParser] ignoring chunk type: \"{type}\""); 253 | stream.Position += length + 4; // Skip data + CRC32 254 | break; 255 | } 256 | 257 | return tags; 258 | } 259 | 260 | private static byte[] ReadChunkData(Stream stream, int length, string type) 261 | { 262 | byte[] data = new byte[length]; 263 | if (stream.Read(data, 0, data.Length) != data.Length) 264 | throw new MetaParseException($"Unable to read full PNG chunk \"{type}\" data"); 265 | 266 | byte[] crc32 = new byte[4]; 267 | if (stream.Read(crc32, 0, crc32.Length) != crc32.Length) 268 | throw new MetaParseException($"Unable to read full PNG chunk \"{type}\" CRC32"); 269 | 270 | byte[] crcData = type.Select(c => (byte)c).Concat(data).ToArray(); 271 | uint calculated = CRC32.Calculate(crcData, 0, crcData.Length, 0); 272 | uint given = BitConverter.ToUInt32(crc32.Reverse().ToArray(), 0); 273 | if (calculated != given) 274 | throw new MetaParseException($"Checksum verification failure of PNG chunk \"{type}\""); 275 | return data; 276 | } 277 | 278 | // Reference: https://stackoverflow.com/questions/24082305/how-is-png-crc-calculated-exactly 279 | private static class CRC32 280 | { 281 | static uint[] crcTable; 282 | 283 | // Stores a running CRC (initialized with the CRC of "IDAT" string). When 284 | // you write this to the PNG, write as a big-endian value 285 | static uint idatCrc = Calculate(new byte[] { (byte)'I', (byte)'D', (byte)'A', (byte)'T' }, 0, 4, 0); 286 | 287 | // Call this function with the compressed image bytes, 288 | // passing in idatCrc as the last parameter 289 | public static uint Calculate(byte[] stream, int offset, int length, uint crc) 290 | { 291 | uint c; 292 | if (crcTable == null) 293 | { 294 | crcTable = new uint[256]; 295 | for (uint n = 0; n <= 255; n++) 296 | { 297 | c = n; 298 | for (var k = 0; k <= 7; k++) 299 | { 300 | if ((c & 1) == 1) 301 | c = 0xEDB88320 ^ ((c >> 1) & 0x7FFFFFFF); 302 | else 303 | c = ((c >> 1) & 0x7FFFFFFF); 304 | } 305 | crcTable[n] = c; 306 | } 307 | } 308 | c = crc ^ 0xffffffff; 309 | var endOffset = offset + length; 310 | for (var i = offset; i < endOffset; i++) 311 | { 312 | c = crcTable[(c ^ stream[i]) & 255] ^ ((c >> 8) & 0xFFFFFF); 313 | } 314 | return c ^ 0xffffffff; 315 | } 316 | } 317 | } 318 | } -------------------------------------------------------------------------------- /UserInterface/Controls/CheckBoxDropDown.cs: -------------------------------------------------------------------------------- 1 | // 2 | // CheckBoxDropDown.cs: User control to render checkboxes in a drop-down list. 3 | // 4 | // Copyright (C) 2014 Rikard Johansson 5 | // 6 | // This program is free software: you can redistribute it and/or modify it 7 | // under the terms of the GNU General Public License as published by the Free 8 | // Software Foundation, either version 3 of the License, or (at your option) any 9 | // later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with 16 | // this program. If not, see http://www.gnu.org/licenses/. 17 | // 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Drawing; 22 | using System.Linq; 23 | using System.Windows.Forms; 24 | 25 | namespace ExifOrganizer.UI.Controls 26 | { 27 | public class CheckBoxItem 28 | { 29 | public long Value; 30 | public string Text; 31 | public bool Checked; 32 | } 33 | 34 | public partial class CheckBoxDropDown : ComboBox 35 | { 36 | private readonly string TEXT_ITEM_ALL = ""; 37 | private readonly string TEXT_ITEM_NONE = ""; 38 | 39 | private ToolStripCheckboxItem checkboxAll; 40 | private ToolStripCheckboxItem checkboxNone; 41 | private ToolStripDropDownMenu popup; 42 | 43 | protected class ToolStripCheckboxItem : ToolStripControlHost 44 | { 45 | public event Action CheckedChanged; 46 | 47 | public event Action CheckStateChanged; 48 | 49 | private CheckBoxItem item; 50 | private CheckBox checkbox; 51 | 52 | public CheckBoxItem Item { get { return item; } } 53 | public CheckBox CheckBox { get { return checkbox; } } 54 | public long Value { get { return item.Value; } } 55 | public new string Text { get { return checkbox.Text; } } 56 | public bool Checked { get { return checkbox.Checked; } } 57 | public CheckState CheckState { get { return checkbox.CheckState; } } 58 | 59 | public ToolStripCheckboxItem(CheckBoxItem source, bool allowUserCheck = true, bool allowUserUncheck = true) 60 | : base(new CheckBox()) 61 | { 62 | item = source; 63 | checkbox = Control as CheckBox; 64 | checkbox.Checked = item.Checked; 65 | checkbox.Text = item.Text; 66 | checkbox.AutoCheck = false; 67 | checkbox.Click += delegate (object sender, EventArgs e) 68 | { 69 | if (checkbox.Checked && !allowUserUncheck) 70 | return; 71 | if (!checkbox.Checked && !allowUserCheck) 72 | return; 73 | checkbox.Checked = !checkbox.Checked; 74 | }; 75 | checkbox.CheckedChanged += delegate (object sender, EventArgs e) 76 | { 77 | item.Checked = checkbox.Checked; 78 | 79 | if (CheckedChanged != null) 80 | CheckedChanged(this, e); 81 | }; 82 | checkbox.CheckStateChanged += delegate (object sender, EventArgs e) 83 | { 84 | if (CheckStateChanged != null) 85 | CheckStateChanged(this, e); 86 | }; 87 | } 88 | 89 | public void SetWidth(int width) 90 | { 91 | Width = width; 92 | checkbox.Width = Width; 93 | checkbox.MinimumSize = new Size(width - 35, checkbox.MinimumSize.Height); 94 | } 95 | 96 | public void SetText(string text) 97 | { 98 | if (text == Text) 99 | return; 100 | 101 | item.Text = text; 102 | checkbox.Text = text; 103 | } 104 | 105 | public void SetChecked(bool value) 106 | { 107 | if (value == Checked) 108 | return; 109 | 110 | item.Checked = value; 111 | checkbox.Checked = value; 112 | } 113 | } 114 | 115 | public CheckBoxDropDown() 116 | : base() 117 | { 118 | popup = new ToolStripDropDownMenu(); 119 | popup.ShowImageMargin = false; 120 | //popup.TopLevel = false; 121 | //popup.CanOverflow = true; 122 | popup.AutoClose = true; 123 | popup.DropShadowEnabled = true; 124 | 125 | InitializeComponent(); 126 | } 127 | 128 | public bool CheckBoxNone 129 | { 130 | get { return checkboxNone != null; } 131 | set 132 | { 133 | if (value == CheckBoxNone) 134 | return; 135 | 136 | if (value) 137 | { 138 | bool check = GetCheckBoxItems().All(x => !x.Checked); 139 | 140 | checkboxNone = new ToolStripCheckboxItem(new CheckBoxItem() { Value = 0, Text = TEXT_ITEM_NONE, Checked = check }, true, false); 141 | checkboxNone.BackColor = this.BackColor; 142 | checkboxNone.CheckedChanged += CheckedChangedNone; 143 | popup.Items.Add(checkboxNone); 144 | } 145 | else 146 | { 147 | popup.Items.Remove(checkboxNone); 148 | checkboxNone.CheckedChanged -= CheckedChangedNone; 149 | checkboxNone = null; 150 | } 151 | 152 | ItemsAltered(); 153 | } 154 | } 155 | 156 | public bool CheckBoxAll 157 | { 158 | get { return checkboxAll != null; } 159 | set 160 | { 161 | if (value == CheckBoxAll) 162 | return; 163 | 164 | if (value) 165 | { 166 | bool check = GetCheckBoxItems().All(x => x.Checked); 167 | 168 | checkboxAll = new ToolStripCheckboxItem(new CheckBoxItem() { Value = Int64.MaxValue, Text = TEXT_ITEM_ALL, Checked = check }, true, false); 169 | checkboxAll.BackColor = this.BackColor; 170 | checkboxAll.CheckedChanged += CheckedChangedAll; 171 | popup.Items.Add(checkboxAll); 172 | } 173 | else 174 | { 175 | popup.Items.Remove(checkboxAll); 176 | checkboxAll.CheckedChanged -= CheckedChangedAll; 177 | checkboxAll = null; 178 | } 179 | 180 | ItemsAltered(); 181 | } 182 | } 183 | 184 | protected override void OnSizeChanged(EventArgs e) 185 | { 186 | base.OnSizeChanged(e); 187 | 188 | foreach (ToolStripCheckboxItem tsi in GetToolStripCheckboxItems()) 189 | tsi.SetWidth(ClientSize.Width); 190 | } 191 | 192 | protected override void OnDropDown(EventArgs e) 193 | { 194 | base.OnDropDown(e); 195 | 196 | this.DropDownHeight = 1; 197 | 198 | if (popup.Visible) 199 | return; 200 | 201 | Rectangle rect = RectangleToScreen(this.ClientRectangle); 202 | Point location = new Point(rect.X, rect.Y + this.Size.Height); 203 | popup.Show(location, ToolStripDropDownDirection.BelowRight); 204 | } 205 | 206 | protected ToolStripCheckboxItem Add(CheckBoxItem item) 207 | { 208 | ToolStripCheckboxItem tsi = new ToolStripCheckboxItem(item); 209 | tsi.BackColor = this.BackColor; 210 | tsi.CheckedChanged += CheckedChanged; 211 | popup.Items.Add(tsi); 212 | 213 | ItemsAltered(); 214 | 215 | return tsi; 216 | } 217 | 218 | protected void Remove(CheckBoxItem item) 219 | { 220 | int altered = 0; 221 | foreach (ToolStripCheckboxItem tsi in GetToolStripCheckboxItems()) 222 | { 223 | if (tsi.Value == item.Value) 224 | { 225 | tsi.CheckedChanged -= CheckedChanged; 226 | popup.Items.Remove(tsi); 227 | altered++; 228 | } 229 | } 230 | 231 | if (altered > 0) 232 | ItemsAltered(); 233 | } 234 | 235 | protected void Update(CheckBoxItem item) 236 | { 237 | foreach (ToolStripCheckboxItem tsi in GetToolStripCheckboxItems()) 238 | { 239 | if (item.Value != tsi.Item.Value) 240 | continue; 241 | 242 | if (item.Text != tsi.Item.Text) 243 | tsi.SetText(item.Text); 244 | 245 | if (item.Checked != tsi.Item.Checked) 246 | tsi.SetChecked(item.Checked); 247 | } 248 | } 249 | 250 | protected void Clear() 251 | { 252 | if (popup.Items.Count == 0) 253 | return; 254 | 255 | bool none = CheckBoxNone; 256 | bool all = CheckBoxAll; 257 | 258 | CheckBoxNone = false; 259 | CheckBoxAll = false; 260 | 261 | popup.Items.Clear(); 262 | 263 | CheckBoxNone = none; 264 | CheckBoxAll = all; 265 | 266 | ItemsAltered(); 267 | } 268 | 269 | private void CheckedChanged(object sender, EventArgs e) 270 | { 271 | ToolStripCheckboxItem tsi = sender as ToolStripCheckboxItem; 272 | if (tsi == null) 273 | return; 274 | 275 | UpdateText(); 276 | ItemsAltered(); 277 | CheckedChanged(tsi.Item); 278 | } 279 | 280 | private void CheckedChangedNone(object sender, EventArgs e) 281 | { 282 | ToolStripCheckboxItem tsi = sender as ToolStripCheckboxItem; 283 | if (tsi == null) 284 | return; 285 | if (!tsi.Checked) 286 | return; 287 | 288 | if (checkboxAll != null) 289 | checkboxAll.SetChecked(false); 290 | 291 | foreach (ToolStripCheckboxItem item in GetToolStripCheckboxItems()) 292 | { 293 | if (!item.Checked) 294 | continue; 295 | item.SetChecked(false); 296 | } 297 | 298 | UpdateText(); 299 | } 300 | 301 | private void CheckedChangedAll(object sender, EventArgs e) 302 | { 303 | ToolStripCheckboxItem tsi = sender as ToolStripCheckboxItem; 304 | if (tsi == null) 305 | return; 306 | if (!tsi.Checked) 307 | return; 308 | if (checkboxNone != null) 309 | checkboxNone.SetChecked(false); 310 | 311 | foreach (ToolStripCheckboxItem item in GetToolStripCheckboxItems()) 312 | { 313 | if (item.Checked) 314 | continue; 315 | item.SetChecked(true); 316 | } 317 | 318 | UpdateText(); 319 | } 320 | 321 | protected virtual void CheckedChanged(CheckBoxItem item) 322 | { 323 | } 324 | 325 | private void ItemsAltered() 326 | { 327 | IEnumerable items = GetCheckBoxItems(); 328 | bool all = items.Any(); 329 | bool none = true; 330 | foreach (CheckBoxItem item in items) 331 | { 332 | all &= item.Checked; 333 | none &= !item.Checked; 334 | } 335 | 336 | if (checkboxNone != null && checkboxNone.Checked != none) 337 | checkboxNone.SetChecked(none); 338 | if (checkboxAll != null && checkboxAll.Checked != all) 339 | checkboxAll.SetChecked(all); 340 | 341 | UpdateText(); 342 | } 343 | 344 | private void UpdateText() 345 | { 346 | int itemCount = 0; 347 | List checkedItems = new List(); 348 | foreach (CheckBoxItem item in GetCheckBoxItems()) 349 | { 350 | itemCount++; 351 | if (item.Checked) 352 | checkedItems.Add(item); 353 | } 354 | 355 | string text = GetPresentationText(checkedItems, itemCount); 356 | 357 | // Set presentation text (simulate selected item) 358 | this.Items.Clear(); 359 | this.Items.Add(text); 360 | this.SelectedIndex = 0; 361 | } 362 | 363 | private IEnumerable GetCheckBoxItems() 364 | { 365 | return GetToolStripCheckboxItems().Select(x => x.Item); 366 | } 367 | 368 | private IEnumerable GetToolStripCheckboxItems() 369 | { 370 | foreach (ToolStripItem temp in popup.Items) 371 | { 372 | ToolStripCheckboxItem tsi = temp as ToolStripCheckboxItem; 373 | if (tsi == null) 374 | continue; 375 | 376 | if (tsi.Value == 0 || tsi.Value == Int64.MaxValue) 377 | continue; 378 | 379 | yield return tsi; 380 | } 381 | } 382 | 383 | private string GetPresentationText(IEnumerable checkedItems, int totalItemCount) 384 | { 385 | int checkedItemCount = checkedItems.Count(); 386 | if (checkedItemCount == 0) 387 | return TEXT_ITEM_NONE; 388 | if (checkedItemCount == totalItemCount) 389 | return TEXT_ITEM_ALL; 390 | 391 | return String.Join(",", checkedItems.Select(x => x.Text)); 392 | } 393 | } 394 | } --------------------------------------------------------------------------------