行文目录
-
- 功能效果演示
-
- 实现说明
- 2.1 其他图片上传
- 2.2 核心代码:其他图片转Ico
- 2.3 转换后的Ico文件下载
-
- 总结
1. 功能效果演示
仓库地址:IcoTool
在线演示地址:https://tool.dotnet9.com/ico
演示下文件上传、转换结果:
通过该工具及代码,能了解到:
- 使用Blazor怎么上传文件到服务器(Blazor Server)。
- 怎么从服务器下载文件。
- 如何将png等图片转换为Ico图片。
下面对该工具的实现代码做个简单说明,不太清楚的可以留言交流。
2. 实现说明
通过该工具,能了解到:
- 使用Blazor怎么上传文件到服务器(Blazor Server)。
- 怎么从服务器下载文件。
- 如何将png等图片转换为Ico图片。
下面对该工具的实现代码做个简单说明,不太清楚的可以留言交流。
2.1 其他图片上传
使用的MASA Blazor上传组件MFileInput,看下面的代码,就一个上传组件加上传时文件保存操作,代码文件:IcoTool.razor
<MFileInput TValue="IBrowserFile" Placeholder="@T("IcoToolMFileInputPlaceholder")" Rules="_rules" ShowSize OnChange="@LoadFile" Accept="image/png, image/jpeg, image/jpg, image/bmp" Label="@T("IcoToolMFileInputLabel")"> </MFileInput> @code { private bool _loading; private string _sourceFilePath = ""; [Inject] public I18n I18N { get; set; } = default!; [Inject] public IJSRuntime Js { get; set; } = default!; protected override async Task OnInitializedAsync() { _rules.Add(value => (value==null|| value.Size < 2 * 1024 * 1024 )? true : T("IcoToolFileSizeLimitMessage")); await base.OnInitializedAsync(); } private async Task LoadFile(IBrowserFile? e) { if (e == null) { _destFilePath = _sourceFilePath = string.Empty; return; } _destFilePath = string.Empty; if (!string.IsNullOrWhiteSpace(_sourceFilePath) && File.Exists(_sourceFilePath)) File.Delete(_sourceFilePath); var saveImageDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", ImageDirName); if (!Directory.Exists(saveImageDir)) Directory.CreateDirectory(saveImageDir); _sourceFilePath = Path.Combine(saveImageDir, DateTime.UtcNow.ToString("yyyyMMddHHmmssfff")); await using var fs = new FileStream(_sourceFilePath, FileMode.Create); await e.OpenReadStream().CopyToAsync(fs); } }
2.2 核心代码:其他图片转Ico
参考代码:https://gist.github.com/darkfall/1656050
因为使用到Bitmap
,vs会提示只支持Windows
平台,目前工具程序也部署在Windows Server 2019
服务器上,如果有其他转换代码,支持跨平台欢迎技术讨论,下面给出我使用的其他图片转Ico的代码,代码路径在:ImagingHelper.cs
using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Dotnet9.Tools.Images; /// <summary> /// Adapted from this gist: https://gist.github.com/darkfall/1656050 /// Provides helper methods for imaging /// </summary> public static class ImagingHelper { public const string FileheadBmp = "6677"; public const string FileheadJpg = "255216"; public const string FileheadPng = "13780"; public const string FileheadGif = "7173"; private static readonly Dictionary<ImageType, string> ImageTypeHead = new() { { ImageType.Bmp, FileheadBmp }, { ImageType.Jpg, FileheadJpg }, { ImageType.Png, FileheadPng }, { ImageType.Gif, FileheadGif } }; public static bool IsPicture(string filePath, out string fileHead) { fileHead = string.Empty; try { var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); var reader = new BinaryReader(fs); var fileClass = $"{reader.ReadByte().ToString()}{reader.ReadByte().ToString()}"; reader.Close(); fs.Close(); if (fileClass is not (FileheadBmp or FileheadJpg or FileheadPng or FileheadGif)) return false; fileHead = fileClass; return true; } catch { return false; } } public static bool IsPictureType(string filePath, ImageType imageType) { var isPicture = IsPicture(filePath, out var fileHead); if (!isPicture) return false; return ImageTypeHead[imageType] == fileHead; } /// <summary> /// Converts a PNG image to a icon (ico) with all the sizes windows likes /// </summary> /// <param name="inputBitmap">The input bitmap</param> /// <param name="output">The output stream</param> /// <returns>Wether or not the icon was succesfully generated</returns> public static bool ConvertToIcon(Bitmap inputBitmap, Stream output) { var sizes = new[] { 256, 48, 32, 16 }; // Generate bitmaps for all the sizes and toss them in streams var imageStreams = new List<MemoryStream>(); foreach (var size in sizes) { var newBitmap = ResizeImage(inputBitmap, size, size); var memoryStream = new MemoryStream(); newBitmap.Save(memoryStream, ImageFormat.Png); imageStreams.Add(memoryStream); } var iconWriter = new BinaryWriter(output); var offset = 0; // 0-1 reserved, 0 iconWriter.Write((byte)0); iconWriter.Write((byte)0); // 2-3 image type, 1 = icon, 2 = cursor iconWriter.Write((short)1); // 4-5 number of images iconWriter.Write((short)sizes.Length); offset += 6 + 16 * sizes.Length; for (var i = 0; i < sizes.Length; i++) { // image entry 1 // 0 image width iconWriter.Write((byte)sizes[i]); // 1 image height iconWriter.Write((byte)sizes[i]); // 2 number of colors iconWriter.Write((byte)0); // 3 reserved iconWriter.Write((byte)0); // 4-5 color planes iconWriter.Write((short)0); // 6-7 bits per pixel iconWriter.Write((short)32); // 8-11 size of image data iconWriter.Write((int)imageStreams[i].Length); // 12-15 offset of image data iconWriter.Write(offset); offset += (int)imageStreams[i].Length; } for (var i = 0; i < sizes.Length; i++) { // write image data // png data must contain the whole png data file iconWriter.Write(imageStreams[i].ToArray()); imageStreams[i].Close(); } iconWriter.Flush(); return true; } /// <summary> /// Converts a PNG image to a icon (ico) /// </summary> /// <param name="input">The input stream</param> /// <param name="output">The output stream</param /// <returns>Wether or not the icon was succesfully generated</returns> public static bool ConvertToIcon(Stream input, Stream output) { var inputBitmap = (Bitmap)Image.FromStream(input); return ConvertToIcon(inputBitmap, output); } /// <summary> /// Converts a PNG image to a icon (ico) /// </summary> /// <param name="inputPath">The input path</param> /// <param name="outputPath">The output path</param> /// <returns>Wether or not the icon was succesfully generated</returns> public static bool ConvertToIcon(string inputPath, string outputPath) { using var inputStream = new FileStream(inputPath, FileMode.Open); using var outputStream = new FileStream(outputPath, FileMode.OpenOrCreate); return ConvertToIcon(inputStream, outputStream); } /// <summary> /// Converts an image to a icon (ico) /// </summary> /// <param name="inputImage">The input image</param> /// <param name="outputPath">The output path</param> /// <returns>Wether or not the icon was succesfully generated</returns> public static bool ConvertToIcon(Image inputImage, string outputPath) { using var outputStream = new FileStream(outputPath, FileMode.OpenOrCreate); return ConvertToIcon(new Bitmap(inputImage), outputStream); } /// <summary> /// Resize the image to the specified width and height. /// Found on stackoverflow: https://stackoverflow.com/questions/1922040/resize-an-image-c-sharp /// </summary> /// <param name="image">The image to resize.</param> /// <param name="width">The width to resize to.</param> /// <param name="height">The height to resize to.</param> /// <returns>The resized image.</returns> public static Bitmap ResizeImage(Image image, int width, int height) { var destRect = new Rectangle(0, 0, width, height); var destImage = new Bitmap(width, height); destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); using var graphics = Graphics.FromImage(destImage); graphics.CompositingMode = CompositingMode.SourceCopy; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; using var wrapMode = new ImageAttributes(); wrapMode.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); return destImage; } } public enum ImageType { Bmp, Jpg, Png, Gif }
简单的单元测试还是要有的,代码见:ImageHelperTests.cs
using Dotnet9.Tools.Images; namespace Dotnet9.Tools.Tests.Images; public class ImageHelperTests { [Fact] public void IsPicture() { var testFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "logo.png"); Assert.True(File.Exists(testFilePath)); var isPicture = ImagingHelper.IsPicture(testFilePath, out var typename); Assert.True(isPicture); } [Fact] public void IsNotPicture() { var testFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "test.txt"); Assert.True(File.Exists(testFilePath)); var isPicture = ImagingHelper.IsPicture(testFilePath, out var typename); Assert.False(isPicture); } [Fact] public void IsPngFile() { var testFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "logo.png"); Assert.True(File.Exists(testFilePath)); var isPng = ImagingHelper.IsPictureType(testFilePath, ImageType.Png); Assert.True(isPng); } [Fact] public void ShouldConvertPngToIcon() { var sourcePng = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "logo.png"); var destIco = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "logo.ico"); Assert.True(File.Exists(sourcePng)); Assert.False(File.Exists(destIco)); ImagingHelper.ConvertToIcon(sourcePng, destIco); Assert.True(File.Exists(destIco)); File.Delete(destIco); } }
页面调用Ico转换功能代码如下,提供一个触发转换的按钮和执行转换的方法,代码文件:IcoTool.razor
@if (!string.IsNullOrWhiteSpace(_sourceFilePath) && File.Exists(_sourceFilePath)) { <MButton class="ma-2 white--text" Loading="_loading" Disabled="_loading" Depressed Color="primary" OnClick="@ConvertToIcon"> <LoaderContent> <span>@T("IcoToolMButtonLoaderContent")</span> </LoaderContent> <ChildContent> <span>@T("IcoToolMButtonChildContent")</span> </ChildContent> </MButton> } @code { private async Task ConvertToIcon() { if (!string.IsNullOrWhiteSpace(_destFilePath) && File.Exists(_destFilePath)) { await DownloadIco(); return; } _loading = true; if (!string.IsNullOrWhiteSpace(_sourceFilePath) && File.Exists(_sourceFilePath)) { _destFilePath = $"{_sourceFilePath}.ico"; if (ImagingHelper.ConvertToIcon(_sourceFilePath, _destFilePath)) await DownloadIco(); } _loading = false; } }
2.3 转换后的Ico文件下载
文件转换成功后,怎么提供下载呢?
起初想使用一个<a href="/files/xxx.ico" target="_blank">xxx.ico</a>
标签提供浏览下载的,但动态生成的图片无法访问,不知道什么原因,只能暂时采用一个折衷的方式,有朋友有好的想法欢迎留言。
目前采用的是提供按钮下载,下面是封装的js下载方法,来自微软的文档:ASP.NET Core Blazor file downloads
我把JS
代码放_Layout.cshtml:
<script> // 省略部分代码 async function downloadFileFromStream(fileName, contentStreamReference) { const arrayBuffer = await contentStreamReference.arrayBuffer(); const blob = new Blob([arrayBuffer]); const url = URL.createObjectURL(blob); triggerFileDownload(fileName, url); URL.revokeObjectURL(url); } function triggerFileDownload(fileName, url) { const anchorElement = document.createElement('a'); anchorElement.href = url; if (fileName) { anchorElement.download = fileName; } anchorElement.click(); anchorElement.remove(); } </script>
页面下载时使用以下代码,使用到JS互操作
(什么是JS互操作
?可以参考我转载的这篇文章了解首页.NETBlazorBlazor Server
(14/30)大家一起学Blazor:JavaScript interop(互操作)),代码放:IcoTool.razor
@inject IJSRuntime JS // 省略n多代码 @code { private async Task DownloadIco() { await using var fileStream = new FileStream(_destFilePath, FileMode.Open); using var streamRef = new DotNetStreamReference(fileStream); await Js.InvokeVoidAsync("downloadFileFromStream", Path.GetFileName(_destFilePath), streamRef); } }
3. 总结
- Blazor组件库使用的MASA Blazor,很美观大方的
Material Design
设计风格。 - Ico转换,使用到了
System.Drawing.Common
包的Bitmap
,.NET 6开始不支持跨平台,提示只支持Windows
平台。 - 本工具使用7.0.100-preview.1开发、编译、上线,使用.NET 6的同学,请放心使用,可以无缝升级。
Dotnet9工具箱
会不断添加新的免费、开源、在线工具,欢迎star支持,有什么需求我会考虑加上,仓库地址:Dotnet9.Tools,可提交issue、网站留言、微信公众号(dotnet9)联系等等。
本工具源码:IcoTool
介绍文章:Blazor在线Ico转换工具
在线演示地址:https://tool.dotnet9.com/ico