From d3104fe653c8be4991729182126275c4953d0c49 Mon Sep 17 00:00:00 2001 From: Sarp Eren EGILMEZ <2348.sarp.egilmez.2006@gmail.com> Date: Mon, 23 Oct 2023 23:24:10 +0300 Subject: [PATCH 1/4] Update README --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- TODO.md | 2 ++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 71b8653..5f369e0 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,89 @@ # 🍉 TYM + [![Debug build](https://github.com/Segilmez06/tym/actions/workflows/debug.yml/badge.svg)](https://github.com/Segilmez06/tym/actions/workflows/debug.yml) ![GitHub release](https://img.shields.io/github/v/release/Segilmez06/tym?label=Release) TYM is a cross-platform tool that renders image files in your terminal. It uses VT100 escape codes to display 24-bit true color images. ## Requirements + - Unicode supported monospace font - VT100 compatible terminal emulator with true color support ## Download + Get the lastest version from [Releases](https://github.com/Segilmez06/tym/releases) page. You can also get a debug build from [Actions](https://github.com/Segilmez06/tym/actions/workflows/debug.yml) tab. +Some browsers may block download because they filter all files with .exe suffix even if they are plain text. + ## Installation -Extract the zip file to a folder. Then add folder to your `PATH` variable. + +No additional installation required. + +If you want to execute from a shell don't forget to update your `PATH` variable. ## Usage -To be updated after a stable release, use `tym --help` instead. + +```bash +tym [--help] [--version] [] +``` + +### Options + +| Short form | Long form | Default value | Description | +| :--------: | :------------------ | :------------------ | :---------- | +| `-r` | `--resampler` | `MitchellNetravali` | Resampling algorithm for downsizing. +| `-l` | `--list-resamplers` | `false` | List available resampling algorithms. +| `-x` | `--x-margin` | `0` | Left margin size as characters. Shifts output to right. +| `-y` | `--y-margin` | `0` | Top margin size as characters. Shifts output to bottom. +| `-w` | `--width` | `0` (autosize) | Output width as pixels. +| `-h` | `--height` | `0` (autosize) | Output height as pixels. +| `-m` | `--resize-method` | `Contain` | Resizing mode. +| `-f` | `--fullscreen` | `false` | Use fullscreen. +| `-c` | `--clear` | `false` | Clear downloaded cache folder. + +The fullscreen option overrides margin and size arguments. + +TYM uses special unicode characters to improve image scaling. That causes each character to contain 2 pixels vertically. That means if you specify top margin as 3, it will shift the output by 3 characters which is equals to 6 pixels. + +### Examples + +View local image with default settings: + +```bash +tym example.png +``` + +View remote image with default settings: + +```bash +tym https://example.com/image.png +``` + +View remote file with and resize to 96x64: + +```bash +tym https://example.com/image.png -w 96 -h 54 +``` + +View local file with fullscreen and cover all buffer area: + +```bash +tym example.png -f -m Cover +``` ## Screenshots + ![Screenshot](screenshots/screenshot-1.png) ## Building -This tool is built on .Net 7 so it requires .Net SDK version >= 7 while building. + +This tool is built on .Net 7 so it requires .Net SDK version >= 7 while building. ### Optional + For AOT binary compilation, check [official documentation](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot#prerequisites) for dependencies. This packages are only required while publishing. Skip this step if you're going to use JIT compiled binary. ## Contributing + You can create pull requests and issues to help development. Also starring the repo will give me motivation. diff --git a/TODO.md b/TODO.md index ddd66d7..0624828 100644 --- a/TODO.md +++ b/TODO.md @@ -3,3 +3,5 @@ ## General - [ ] Add comments +- [ ] Unwanted 1 pixel right shifting +- [ ] Fix resize mode list printing and contents From 611612de86109a5c20a144220e74a6bdc7f5eabf Mon Sep 17 00:00:00 2001 From: Sarp Eren EGILMEZ <2348.sarp.egilmez.2006@gmail.com> Date: Tue, 24 Oct 2023 01:07:31 +0300 Subject: [PATCH 2/4] Fix pixel shifting --- README.md | 9 ++++++++- TODO.md | 5 +++-- src/App.cs | 35 +++++++++++++++++++++++++++++++---- src/Program.cs | 4 ++++ src/TYM.csproj | 2 +- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5f369e0..75b19bb 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ tym [--help] [--version] [] The fullscreen option overrides margin and size arguments. -TYM uses special unicode characters to improve image scaling. That causes each character to contain 2 pixels vertically. That means if you specify top margin as 3, it will shift the output by 3 characters which is equals to 6 pixels. +TYM uses special Unicode characters to improve image scaling. That causes each character to contain 2 pixels vertically. That means if you specify top margin as 3, it will shift the output by 3 characters which is equals to 6 pixels. ### Examples @@ -87,3 +87,10 @@ For AOT binary compilation, check [official documentation](https://learn.microso ## Contributing You can create pull requests and issues to help development. Also starring the repo will give me motivation. + +## Credits + +Image library: [SixLabors ImageSharp](https://github.com/SixLabors/ImageSharp) + + +TYM created by Segilmez06 \ No newline at end of file diff --git a/TODO.md b/TODO.md index 0624828..6bdbc1b 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ ## General +- [x] Improve rendering - [ ] Add comments -- [ ] Unwanted 1 pixel right shifting -- [ ] Fix resize mode list printing and contents +- [x] Fix unwanted 1 pixel right shifting +- [x] Fix resize mode list diff --git a/src/App.cs b/src/App.cs index 62fbcc2..511fd95 100644 --- a/src/App.cs +++ b/src/App.cs @@ -10,6 +10,9 @@ namespace TYM { + /// + /// Special Unicode characters for rendering image + /// static class BlockChars { public static char TopBlockChar = '\u2580'; @@ -17,6 +20,9 @@ static class BlockChars public static char EmptyBlockChar = '\u0020'; } + /// + /// Command line options + /// public class Options { [Value(0, MetaName = "Path", Required = false, HelpText = "Path to the image file. Also supports web links. (DANGER: USE WEB LINKS AT YOUR OWN RISK!)")] @@ -48,7 +54,7 @@ public class Options - [Option('m', "resize-method", Required = false, Default = "Contain", HelpText = "Resizing mode. Available options: Contain, Cover (Crop), Stretch")] + [Option('m', "resize-method", Required = false, Default = "Contain", HelpText = "Resizing mode. Available options: Contain, Cover (Crop), Stretch, Center")] public string? ResizeMethod { get; set; } @@ -64,6 +70,9 @@ public class Options } + /// + /// Console logging utility + /// public static class Logger { public enum LogLevel @@ -124,22 +133,35 @@ public static void LogExit(LogLevel Level, string Message, int Argument) public class App { + // Get the command line arguments parser private readonly Parser CommandLineParser = Parser.Default; + /// + /// TYM instance entry point + /// + /// Command line arguments public App(string[] Arguments) { + // Parse command line arguments and ignore executable name CommandLineParser.ParseArguments(Arguments.Skip(1)).WithParsed(Run); } + /// + /// Start the process + /// + /// Command line options private void Run(Options CommandLineOptions) { + // If listing resamplers requested if (CommandLineOptions.ListResamplers) { + // Print available resamplers and exit LogMsg(LogLevel.Info, Messages.Message_AvailableResamplers); typeof(KnownResamplers).GetProperties().ToList().ForEach(x => LogMsg(LogLevel.Verbose, x.Name)); Environment.Exit(0); } + // Get download directory path string DownloadDirectory = Path.Combine(Path.GetTempPath(), Settings.tempDirectoryName); if (CommandLineOptions.ClearCache) { @@ -236,14 +258,19 @@ private static void ProcessImageFile(Options CommandLineOptions, string ImagePat ResizeMode SelectedResizeMode = AvailableResizeModes.GetValueOrDefault(CommandLineOptions.ResizeMethod); Size TermSize = new(Console.BufferWidth, Console.BufferHeight); + Size TermTargetSize = new(TermSize.Width / 2, TermSize.Height); + + if (TermTargetSize.Width % 2 != 0) TermTargetSize.Width += 1; + if (TermTargetSize.Height % 2 != 0) TermTargetSize.Height += 1; + Size TargetSize = CommandLineOptions.UseFullscreen ? new( TermSize.Width, TermSize.Height * 2 ) : new( - CommandLineOptions.Width < 1 ? TermSize.Width / 2 : CommandLineOptions.Width, - CommandLineOptions.Height < 1 ? TermSize.Height : CommandLineOptions.Height + CommandLineOptions.Width < 1 ? TermTargetSize.Width : CommandLineOptions.Width, + CommandLineOptions.Height < 1 ? TermTargetSize.Height : CommandLineOptions.Height ); Image Source = Image.Load(ImagePath); @@ -262,7 +289,7 @@ private static void ProcessImageFile(Options CommandLineOptions, string ImagePat for (int y = 0; y < Source.Height / 2; y++) { string Line = ""; - Line += $"\x1b[{CommandLineOptions.MarginX}C"; + Line += CommandLineOptions.MarginX > 0 ? $"\x1b[{CommandLineOptions.MarginX}C" : ""; for (int x = 0; x < Source.Width; x++) { Rgba32[] PixelColors = { Source[x, (y * 2)], Source[x, (y * 2) + 1] }; diff --git a/src/Program.cs b/src/Program.cs index 396998e..1a680e9 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -2,8 +2,12 @@ { class Program { + /// + /// Main entry point + /// static void Main() { + // Launch new instance of TYM _ = new App(Environment.GetCommandLineArgs()); } } diff --git a/src/TYM.csproj b/src/TYM.csproj index d639043..fe40e7c 100644 --- a/src/TYM.csproj +++ b/src/TYM.csproj @@ -20,7 +20,7 @@ en GPL-3.0-only True - 1.0.2 + 1.0.3 $(AssemblyVersion) res\icon.ico res\project_icon.png From 4fbee00e594a5cfc25e14ab3111a805ec4b1c3c5 Mon Sep 17 00:00:00 2001 From: Sarp Eren EGILMEZ <2348.sarp.egilmez.2006@gmail.com> Date: Tue, 24 Oct 2023 20:24:58 +0300 Subject: [PATCH 3/4] Added comments and cleaned up --- README.md | 2 +- TODO.md | 7 +- src/App.cs | 264 +++++++++++++++++++++++++++++++++++------------------ 3 files changed, 178 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 75b19bb..35f5875 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Some browsers may block download because they filter all files with .exe suffix No additional installation required. -If you want to execute from a shell don't forget to update your `PATH` variable. +If you want to execute TYM from a shell don't forget to update your `PATH` variable. ## Usage diff --git a/TODO.md b/TODO.md index 6bdbc1b..c0afef3 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,8 @@ ## General -- [x] Improve rendering -- [ ] Add comments -- [x] Fix unwanted 1 pixel right shifting +- [x] Improve output rendering +- [x] Add comments +- [x] Fix pixel shifting - [x] Fix resize mode list +- [x] Cleanup code diff --git a/src/App.cs b/src/App.cs index 511fd95..28deada 100644 --- a/src/App.cs +++ b/src/App.cs @@ -1,6 +1,6 @@ using CommandLine; using SixLabors.ImageSharp.Processing.Processors.Transforms; -using System.Diagnostics; + using System.Net.Http.Headers; using System.Reflection; @@ -10,6 +10,8 @@ namespace TYM { + + /// /// Special Unicode characters for rendering image /// @@ -20,6 +22,8 @@ static class BlockChars public static char EmptyBlockChar = '\u0020'; } + + /// /// Command line options /// @@ -70,6 +74,8 @@ public class Options } + + /// /// Console logging utility /// @@ -131,11 +137,32 @@ public static void LogExit(LogLevel Level, string Message, int Argument) } } + + + /// + /// TYM main instance + /// public class App { - // Get the command line arguments parser + + /// + /// Get command line arguments parser + /// private readonly Parser CommandLineParser = Parser.Default; + + /// + /// Available resize modes + /// + private readonly static Dictionary ResizeModes = new(){ + {"Contain", ResizeMode.Max}, + {"Cover", ResizeMode.Crop}, + {"Crop", ResizeMode.Crop}, + {"Stretch", ResizeMode.Stretch}, + {"Center", ResizeMode.Pad} + }; + + /// /// TYM instance entry point /// @@ -146,84 +173,93 @@ public App(string[] Arguments) CommandLineParser.ParseArguments(Arguments.Skip(1)).WithParsed(Run); } + /// /// Start the process /// /// Command line options private void Run(Options CommandLineOptions) { + // If listing resamplers requested if (CommandLineOptions.ListResamplers) { + // Print available resamplers and exit LogMsg(LogLevel.Info, Messages.Message_AvailableResamplers); typeof(KnownResamplers).GetProperties().ToList().ForEach(x => LogMsg(LogLevel.Verbose, x.Name)); Environment.Exit(0); + } + // Get download directory path - string DownloadDirectory = Path.Combine(Path.GetTempPath(), Settings.tempDirectoryName); if (CommandLineOptions.ClearCache) - { - Directory.Delete(DownloadDirectory, true); - } + Directory.Delete(GetDownloadDirectory(), true); + + // If path is local file string? ImagePath = CommandLineOptions.FilePath; if (Path.Exists(ImagePath)) { ProcessImageFile(CommandLineOptions, ImagePath); } + + + // If path is URL else if (Uri.IsWellFormedUriString(ImagePath, UriKind.Absolute)) { - if (Uri.TryCreate(ImagePath, UriKind.Absolute, out Uri? WebURI)) - { - if (WebURI.Scheme == Uri.UriSchemeHttps) - { - HttpClient Client = new(); - - List SupportedMimeTypes = Settings.supportedImageFormats.Split(",").Select(x => x = $"image/{x}").ToList(); - SupportedMimeTypes.ForEach(x => Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(x))); - - Task GetTask = Client.GetAsync(WebURI); - GetTask.Wait(); - HttpResponseMessage Response = GetTask.Result; - - if (Response.IsSuccessStatusCode) - { - string ResponseMimeType = Response.Content.Headers.ContentType.MediaType; - - if (SupportedMimeTypes.Any(x => x == ResponseMimeType)) - { - Directory.CreateDirectory(DownloadDirectory); - - string TempFile = Path.Combine(DownloadDirectory, Guid.NewGuid().ToString()); - - Task ReadTask = Response.Content.ReadAsByteArrayAsync(); - ReadTask.Wait(); - byte[] Data = ReadTask.Result; - - File.WriteAllBytes(TempFile, Data); - ProcessImageFile(CommandLineOptions, TempFile); - } - else - { - LogExit(LogLevel.Error, Messages.Error_InvalidMimeType, ResponseMimeType); - } - } - else - { - LogExit(LogLevel.Error, Messages.Error_ResponseCode, (int)Response.StatusCode); - } - } - else - { - LogExit(LogLevel.Error, Messages.Error_UnsupportedProtocol); - } - } - else - { + + // Parse the url + if(!Uri.TryCreate(ImagePath, UriKind.Absolute, out Uri? WebURI)) LogExit(LogLevel.Error, Messages.Error_URLParseError); - } + + + // Check if protocol is supported + if (WebURI.Scheme != Uri.UriSchemeHttps) + LogExit(LogLevel.Error, Messages.Error_UnsupportedProtocol); + + + // Create HTTP client + HttpClient Client = new(); + + + // Add request headers + GetSupportedMimeTypes().ForEach(x => Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(x))); + + + // Send GET request to download the file + Task GetTask = Client.GetAsync(WebURI); + GetTask.Wait(); + HttpResponseMessage Response = GetTask.Result; + + + // If response is success + if (!Response.IsSuccessStatusCode) + LogExit(LogLevel.Error, Messages.Error_ResponseCode, (int)Response.StatusCode); + + + // Validate response MIME type + string ResponseMimeType = Response.Content.Headers.ContentType.MediaType; + if (!GetSupportedMimeTypes().Any(x => x == ResponseMimeType)) + LogExit(LogLevel.Error, Messages.Error_InvalidMimeType, ResponseMimeType); + + + // Create cache directory + Directory.CreateDirectory(GetDownloadDirectory()); + + + // Save response to file + string TempFile = GenerateTempFileName(); + Task ReadTask = Response.Content.ReadAsByteArrayAsync(); + ReadTask.Wait(); + byte[] Data = ReadTask.Result; + File.WriteAllBytes(TempFile, Data); + + + // Render the file + ProcessImageFile(CommandLineOptions, TempFile); + } else { @@ -231,65 +267,112 @@ private void Run(Options CommandLineOptions) } } + + /// + /// Process the image by path + /// + /// Command line options + /// Path of the image file private static void ProcessImageFile(Options CommandLineOptions, string ImagePath) { + + // Get resampler from KnownResamplers class PropertyInfo? ResamplerProperty = typeof(KnownResamplers).GetProperty(CommandLineOptions.ResamplerName); if (ResamplerProperty == null) - { LogExit(LogLevel.Error, Messages.Error_InvalidResampler); - } + IResampler? Resampler = (IResampler)ResamplerProperty.GetValue(typeof(KnownResamplers)); if (Resampler == null) - { LogExit(LogLevel.Error, Messages.Error_ResamplerError); - } - Dictionary AvailableResizeModes = new(){ - {"Contain", ResizeMode.Max}, - {"Cover", ResizeMode.Crop}, - {"Crop", ResizeMode.Crop}, - {"Stretch", ResizeMode.Stretch}, - {"Center", ResizeMode.Pad} - }; - if (!AvailableResizeModes.ContainsKey(CommandLineOptions.ResizeMethod)) - { - LogExit(LogLevel.Error, Messages.Error_InvalidResizeMode); - } - ResizeMode SelectedResizeMode = AvailableResizeModes.GetValueOrDefault(CommandLineOptions.ResizeMethod); - - Size TermSize = new(Console.BufferWidth, Console.BufferHeight); - Size TermTargetSize = new(TermSize.Width / 2, TermSize.Height); - if (TermTargetSize.Width % 2 != 0) TermTargetSize.Width += 1; - if (TermTargetSize.Height % 2 != 0) TermTargetSize.Height += 1; + // Define available resize modes + if (!ResizeModes.ContainsKey(CommandLineOptions.ResizeMethod)) + LogExit(LogLevel.Error, Messages.Error_InvalidResizeMode); + ResizeMode SelectedResizeMode = ResizeModes.GetValueOrDefault(CommandLineOptions.ResizeMethod); - Size TargetSize = CommandLineOptions.UseFullscreen - ? new( - TermSize.Width, - TermSize.Height * 2 - ) - : new( - CommandLineOptions.Width < 1 ? TermTargetSize.Width : CommandLineOptions.Width, - CommandLineOptions.Height < 1 ? TermTargetSize.Height : CommandLineOptions.Height - ); + // Read image and resize Image Source = Image.Load(ImagePath); Source.Mutate(x => x.Resize(new ResizeOptions() { - Size = TargetSize, + Size = GetTargetSize(new(CommandLineOptions.Width, CommandLineOptions.Height), CommandLineOptions.UseFullscreen), Mode = SelectedResizeMode, Sampler = Resampler })); + + // Get the output and print to console + string ImageOutput = GenerateColoredImageString(Source, new(CommandLineOptions.MarginX, CommandLineOptions.MarginY)); + Console.Write(ImageOutput); + + + // Nothing left to do, so exit... + Environment.Exit(0); + } + + + /// + /// Get cache folder + /// + /// Cache folder path + private static string GetDownloadDirectory() => Path.Combine(Path.GetTempPath(), Settings.tempDirectoryName); + + + /// + /// Generate new temp file + /// + /// Generated file path + private static string GenerateTempFileName() => Path.Combine(GetDownloadDirectory(), Guid.NewGuid().ToString()); + + + /// + /// Get supported MIME types + /// + /// List of types + private static List GetSupportedMimeTypes() => Settings.supportedImageFormats.Split(",").Select(x => x = $"image/{x}").ToList(); + + + /// + /// Get target size by parameters + /// + /// Manually requested size + /// Whether to fill the whole terminal + /// Target size + private static Size GetTargetSize(Size SpecifiedSize, bool IsFullScreen = false) + { + Size TermTargetSize = new(Console.BufferWidth / 2, Console.BufferHeight); + + if (TermTargetSize.Width % 2 != 0) TermTargetSize.Width += 1; + if (TermTargetSize.Height % 2 != 0) TermTargetSize.Height += 1; + + if (IsFullScreen) + return new(Console.BufferWidth, Console.BufferHeight * 2); + + return new( + SpecifiedSize.Width < 1 ? TermTargetSize.Width : SpecifiedSize.Width, + SpecifiedSize.Height < 1 ? TermTargetSize.Height : SpecifiedSize.Height + ); + } + + + /// + /// Generates the output image's string by using VT100 color and position codes + /// + /// Source image object + /// Margin size + /// Image output string + private static string GenerateColoredImageString(Image Source, Point Margins) + { string Buffer = ""; - for (int y = 0; y < CommandLineOptions.MarginY; y++) + for (int y = 0; y < Margins.Y; y++) { Buffer += "\n"; } for (int y = 0; y < Source.Height / 2; y++) { string Line = ""; - Line += CommandLineOptions.MarginX > 0 ? $"\x1b[{CommandLineOptions.MarginX}C" : ""; + Line += Margins.X > 0 ? $"\x1b[{Margins.X}C" : ""; for (int x = 0; x < Source.Width; x++) { Rgba32[] PixelColors = { Source[x, (y * 2)], Source[x, (y * 2) + 1] }; @@ -313,10 +396,9 @@ private static void ProcessImageFile(Options CommandLineOptions, string ImagePat Buffer += "\x1b[0m"; Buffer += "\n"; } - - Console.Write(Buffer); - - Environment.Exit(0); + return Buffer; } } + + } From c05c21a445c79b86d809f1bd5962aa7bd0da0960 Mon Sep 17 00:00:00 2001 From: Sarp Eren EGILMEZ <2348.sarp.egilmez.2006@gmail.com> Date: Tue, 24 Oct 2023 20:28:43 +0300 Subject: [PATCH 4/4] Release 1.0.3 --- CHANGELOG.md | 8 +++++--- TODO.md | 6 +----- src/App.cs | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2e7ef..284a66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## General -- [x] Move settings to embedded resource -- [x] Remove unused libraries - +- [x] Improve output rendering +- [x] Add comments +- [x] Fix pixel shifting +- [x] Fix resize mode list +- [x] Cleanup code diff --git a/TODO.md b/TODO.md index c0afef3..901344a 100644 --- a/TODO.md +++ b/TODO.md @@ -2,8 +2,4 @@ ## General -- [x] Improve output rendering -- [x] Add comments -- [x] Fix pixel shifting -- [x] Fix resize mode list -- [x] Cleanup code + diff --git a/src/App.cs b/src/App.cs index 28deada..70efdf0 100644 --- a/src/App.cs +++ b/src/App.cs @@ -11,7 +11,6 @@ namespace TYM { - /// /// Special Unicode characters for rendering image /// @@ -400,5 +399,4 @@ private static string GenerateColoredImageString(Image Source, Point Mar } } - }