免費開源Blazor線上Ico轉換工具

免費開源Blazor線上Ico轉換工具

免費開源Blazor線上Ico轉換工具,不保存源檔案及轉換後檔案,下載完成即刪除,請放心使用。

最後更新 2022/2/22 下午9:38
沙漠尽头的狼
預計閱讀 11 分鐘
分類
Blazor
標籤
.NET C# Blazor 開源 .NET 7

免費開源 Blazor 線上 Ico 轉換工具,不儲存原始檔及轉換後檔案,下載完成即刪除,請放心使用。

行文目錄

    1. 功能展示
    1. 實作說明
    • 2.1 其他圖片上傳
    • 2.2 核心程式碼:其他圖片轉 Ico
    • 2.3 轉換後的 Ico 檔案下載
    1. 總結

1. 功能展示

倉庫位址:IcoTool

線上展示位址:https://tool.dotnet9.com/ico

展示檔案上傳、轉換結果:

透過該工具及程式碼,能了解到:

  1. 使用 Blazor 如何上傳檔案到伺服器 (Blazor Server)。
  2. 如何從伺服器下載檔案。
  3. 如何將 png 等圖片轉換為 Ico 圖片。

下面對該工具的實作程式碼做個簡單說明,不清楚的可以留言交流。

2. 實作說明

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 readonly List<Func<IBrowserFile, StringBoolean>> _rules = new();

    private bool _loading;
    private IBrowserFile? _sourceBrowserFile;

    [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 void LoadFile(IBrowserFile? e)
    {
        _sourceBrowserFile = e;
    }
}

上面的程式碼,看 LoadFile(IBrowserFile? e) 方法,選擇檔案時只儲存 IBrowserFile 參考,選擇後隨即顯示「立即轉換並下載 Icon」按鈕,程式碼如下:

@if (_sourceBrowserFile != null) {
<MButton
  class="ma-2 white--text"
  Loading="_loading"
  Disabled="_loading"
  Depressed
  Color="primary"
  OnClick="@ConvertAndDownloadIcon"
>
  <LoaderContent>
    <span>@T("IcoToolMButtonLoaderContent")</span>
  </LoaderContent>
  <ChildContent>
    <span>@T("IcoToolMButtonChildContent")</span>
  </ChildContent>
</MButton>
}

此時,原始檔並未做上傳操作,使用者還可以重新選擇,點擊「立即轉換並下載 Icon」按鈕才會執行圖示轉換和下載操作,下面會說明。

2.2 核心程式碼:其他圖片轉 Ico

參考程式碼:https://gist.github.com/darkfall/1656050

因為使用到 Bitmap,vs 會提示只支援 Windows 平台(注意重點:只是該套件 System.Drawing.Common 不支援跨平台,不是 .NET 6 不支援跨平台,.NET 6 支援跨平台。),目前工具程式也部署在 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);
    }
}

使用者確認選擇好檔案後,點擊前面提過的按鈕「立即轉換並下載 Icon」執行轉換的方法 ConvertAndDownloadIcon(),程式碼檔案:IcoTool.razor

@code { private async Task ConvertAndDownloadIcon() { if (_sourceBrowserFile ==
null) return; _loading = true; var tempSourcePath = Path.GetTempFileName(); var
tempDestPath = Path.GetTempFileName(); try { var fileName =
$"{Path.GetFileNameWithoutExtension(_sourceBrowserFile.Name)}.ico"; await
UploadFile(tempSourcePath); ImagingHelper.ConvertToIcon(tempSourcePath,
tempDestPath); await DownloadFile(tempDestPath, fileName); } finally {
DeleteFile(tempSourcePath); DeleteFile(tempDestPath); _loading = false; } }
private async Task UploadFile(string saveFilePath) { await using var
sourceFileStream = new FileStream(saveFilePath, FileMode.Create); await
_sourceBrowserFile!.OpenReadStream().CopyToAsync(sourceFileStream); } private
async Task DownloadFile(string fromFilePath, string saveFileName) { await using
var destFileStream = new FileStream(fromFilePath, FileMode.Open); using var
streamRef = new DotNetStreamReference(destFileStream); await
Js.InvokeVoidAsync("downloadFileFromStream", saveFileName, streamRef); } private
void DeleteFile(string filePath) { try { if (File.Exists(filePath))
File.Delete(filePath); } catch { // ignored } } }

轉換和下載過程中會暫時儲存上傳的原始檔和轉換的 Icon 檔案,下載完成後會立即刪除這兩個檔案,請放心使用。

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>

頁面下載時使用上面貼過的方法 DownloadFile(string fromFilePath, string saveFileName),這裡不再貼程式碼。

下載檔案使用到 JS 互操作(什麼是 JS 互操作?可以參考我轉載的這篇文章了解 (14/30)大家一起學 Blazor:JavaScript interop(互操作)

3. 總結

  1. Blazor 元件庫使用的 MASA Blazor,很美觀大方的 Material Design 設計風格。
  2. Ico 轉換,使用到了 System.Drawing.Common 套件的 Bitmap,.NET 6 開始不支援跨平台,提示只支援 Windows 平台,重點:只是該套件 System.Drawing.Common 不支援跨平台,不是 .NET 6 不支援跨平台,.NET 6 支援跨平台
  3. 本工具使用 7.0.100-preview.1 開發、編譯、上線,使用 .NET 6 的同學,請放心使用,可以無縫升級。

Dotnet9工具箱會不斷添加新的免費、開源、線上工具,歡迎 star 支持,有什麼需求我會考慮加上,倉庫位址:Dotnet9.Tools,可提交 issue網站留言、微信公眾號 (dotnet9) 聯絡等等。

本工具原始碼:IcoTool

介紹文章:Blazor 線上 Ico 轉換工具

線上展示位址:https://tool.dotnet9.com/ico

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2024/2/29

Winform中也可以這樣做資料展示

在做winform開發的過程中,經常需要做資料展示的功能,之前一直使用的是gridcontrol控制項,今天想透過一個範例,跟大家介紹一下如何在winform blazor hybrid中使用ant design blazor中的table元件做資料展示。

繼續閱讀
同分類 / 同標籤 2024/2/29

Winform的介面也可以變好看?

前幾天跟大家介紹了在winform中使用blazor hybrid,而且還說配上blazor的UI可以讓我們的winform程式設計的更加好看,接下來我想以一個在winform blazor hybrid中繪圖的範例來進行說明,希望對你有所幫助。

繼續閱讀