陆建 發表於 2025-7-2 16:20:00

SharpIcoWeb开发记录篇

<h1 id="sharpicoweb开发记录篇">SharpIcoWeb开发记录篇</h1>
<h2 id="前言">前言</h2>
<p>大佬用.NET 9.0开发了SharpIco轻量级图标生成工具,是一款控制台应用程序,支持AOT发布,非常方便。</p>
<p><strong>✨ 功能特点</strong></p>
<ul>
<li>🖼️ 将PNG图像转换为多尺寸ICO图标</li>
<li>🔍 支持生成包含自定义尺寸的ICO图标(最高支持1024×1024)</li>
<li>🧐 检查ICO文件的内部结构和信息</li>
<li>📏 准确识别并显示超大尺寸图标(如512×512、1024×1024)的实际尺寸</li>
</ul>
<p>直达地址:https://github.com/star-plan/sharp-ico</p>
<p>现在互联网上应该有很多这类小工具或者网站,我也想部署把这个小工具部署成网站,当然不要和别人网站一模一样,所有后端我采用 <code>.NET Core Minimal API</code>去开发。</p>
<p>大佬开发的这款小工具可以直接在集成 <code>.NET Core</code>中,所以我只需要写一个上传文件的接口就行了。用了 <code>.NET</code>这么久,写接口一直用的是 <code>WebApi</code>,这次尝试下 <code>Minimal Api</code>。</p>
<p>在线地址:https://ico.pljzy.top</p>
<h2 id="minimal-api-简介">Minimal Api 简介</h2>
<blockquote>
<p>在使用 ASP.NET Core 生成快速 HTTP API 时,可以将最小 API 作为一种简化的方法。 可以使用最少的代码和配置生成完全正常运行的 REST 终结点。 跳过传统的基架,并通过流畅地声明 API 路由和操作来避免不必要的控制器。</p>
</blockquote>
<h3 id="主要特点">主要特点</h3>
<ol>
<li><strong>简洁的语法</strong>:使用更少的代码定义 API 端点</li>
<li><strong>减少样板代码</strong>:不需要控制器类</li>
<li><strong>内置依赖注入</strong>:简化服务配置</li>
<li><strong>路由和请求处理一体化</strong>:直接在路由定义中处理请求</li>
<li><strong>支持所有 ASP.NET Core 功能</strong>:中间件、认证、授权等</li>
</ol>
<h3 id="架构概述">架构概述</h3>
<p><code>MiniAPI</code>的核心架构包括以下几个关键组件:</p>
<ol>
<li>WebApplication:这是整个应用的入口点和宿主。它负责配置服务、中间件和路由。</li>
<li>Endpoints:这些是API的终点,也就是处理特定HTTP请求的地方。</li>
<li>Handlers:这些是实际处理请求并生成响应的函数。</li>
<li>Middleware:这些组件在请求到达handler之前和之后处理请求。</li>
</ol>
<h3 id="关键术语">关键术语</h3>
<p>在进行开发前,先了解下什么是 <code>Endpoints</code>、<code>Handlers</code>、<code>Middleware</code>。</p>
<ol>
<li>
<p>Endpoints(端点):Endpoints是一个特定的URL路径,与一个HTTP方法相关联。</p>
<pre><code class="language-c#">app.MapGet("/hello", () =&gt; "Hello, World!");
</code></pre>
</li>
<li>
<p>Handlers(处理器):Handlers是一个函数,它接收http请求并返回响应。在 <code>MiniAPIs</code>中handler可以是一个简单的lambda表达式,也可以是一个单独定义的方法。例如:</p>
<pre><code class="language-c#">app.MapGet("/users/{id}", (int id) =&gt; $"User ID: {id}");

</code></pre>
</li>
<li>
<p>Middleware(中间件):与WebApi一样,MiniAPIs中也可以使用中间件,它们可以在请求到达handler之前执行操作,也可以在handler处理完请求后修改响应。例如,你可以使用中间件来处理身份验证、日志记录或异常处理。</p>
</li>
</ol>
<h2 id="开发">开发</h2>
<p>了解完一些 <code>Minimal Api</code>基础知识后,就可以开始开发了。首先创建 <code>MiniMal Api</code>模版的 <code>.NET 9 Api</code>程序。</p>
<p>模版默认为创建一个示例接口,看到这个接口,看起来和 <code>WebApi</code>写法差别不大,但是代码是直接写在 <code>Program.cs</code>文件中,无需创建控制器。</p>
<p><img src="https://img2024.cnblogs.com/blog/3091176/202507/3091176-20250702161809441-224445981.png" alt="image" loading="lazy"></p>
<p>接下来开发一个上传文件的接口,然后调用大佬开发的工具就ok了。</p>
<p>在 <code>MiniAPI</code>中也可以使用依赖注入,那么这里创建一个处理文件的服务,然后再注入这个服务在接口中使用。</p>
<h3 id="项目结构">项目结构</h3>
<p>可以看到我的项目可以直接引用 <code>SharpIco</code>工具,这里右键 <code>SharpIco</code>编辑csproj文件将项目类型改为类库就可以直接调用了。</p>
<pre><code class="language-xml">&lt;PropertyGroup&gt;
      &lt;!-- 改为生成库而不是控制台应用 --&gt;
      &lt;OutputType&gt;Library&lt;/OutputType&gt;
&lt;/PropertyGroup&gt;
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3091176/202507/3091176-20250702161821403-1620357292.png" alt="image" loading="lazy"></p>
<h3 id="ifileservice接口">IFileService接口</h3>
<pre><code class="language-c#">public interface IFileService
{
    // 检查文件是否有效
    bool IsFileValid(IFormFile file);
    // 保存上传的文件
    Task&lt;string&gt; SaveUploadedFile(IFormFile file);
    // 创建临时目录
    string GetTempDirectory();
    // 读取文件到内存流
    Task&lt;MemoryStream&gt; ReadFileToMemoryAsync(string filePath);
    // 删除临时文件
    void DeleteFile(string tempFilePath);
}
</code></pre>
<h3 id="fileservice类">FileService类</h3>
<p>这里使用了一个变量 <code>_tempFiles</code>去记录上传的临时文件,然后实现IDisposable接口自动清理临时文件,确保上传的图片不会留存到硬盘里面。</p>
<p>一开始我的设计思路就是在 <code>wooroot</code>中去建立一个待转换和转换后的目录取处理图片,但转念一想,作为一款图片转ico的工具,还是不要留存图片好一点,所以才采用了临时目录这个办法。</p>
<pre><code class="language-c#">
public class FileService : IFileService, IDisposable
{
    private readonly List&lt;string&gt; _tempFiles = new();

    public string GetTempDirectory()
    {
      var tempDir = Path.Combine(Path.GetTempPath(), "SharpIcoTemp");
      Directory.CreateDirectory(tempDir);
      return tempDir;
    }

    public bool IsFileValid(IFormFile file)
    {
      var allowedExtensions = new[] { ".png", ".jpg", ".jpeg" };
      var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
      return file.Length is &gt; 0 and &lt; 10 * 1024 * 1024 &amp;&amp; // 10MB
               allowedExtensions.Contains(extension);
    }

    // 保存上传的文件
    public async Task&lt;string&gt; SaveUploadedFile(IFormFile file)
    {
      var tempDir = GetTempDirectory();
      var tempFilePath = Path.Combine(tempDir, $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}");

      await using var stream = new FileStream(tempFilePath, FileMode.Create);
      await file.CopyToAsync(stream);

      _tempFiles.Add(tempFilePath); // 记录待清理文件
      return tempFilePath;
    }

    // 安全读取文件到内存流
    public async Task&lt;MemoryStream&gt; ReadFileToMemoryAsync(string filePath)
    {
      var memoryStream = new MemoryStream();
      await using (var fileStream = File.OpenRead(filePath))
      {
            await fileStream.CopyToAsync(memoryStream);
      }
      memoryStream.Position = 0;
      return memoryStream;
    }

    public void DeleteFile(string tempFilePath)
    {
      if (File.Exists(tempFilePath))
            File.Delete(tempFilePath);
    }

    // 实现IDisposable接口自动清理
    public void Dispose()
    {
      foreach (var file in _tempFiles)
      {
            try
            {
                if (File.Exists(file))
                  File.Delete(file);
            }
            catch { /* 忽略删除异常 */ }
      }
      GC.SuppressFinalize(this);
    }
}
</code></pre>
<h3 id="program类">Program类</h3>
<p>服务写完后,接下来在配置类中注入服务并完成接口编写就大功告成了。</p>
<p>注入服务:</p>
<pre><code class="language-c#">builder.Services.AddScoped&lt;IFileService, FileService&gt;();
</code></pre>
<p>接口编写:</p>
<pre><code class="language-c#">app.MapPost("/api/uploadDownload", async ( IFormFile file, string? sizes,IFileService fileService, ILogger&lt;Program&gt; logger) =&gt;
{
    try
    {
      // 验证输入
      if (!fileService.IsFileValid(file))
      {
            return Results.BadRequest("请上传有效文件,文件大小不能超过10MB");
      }

      // 保存临时文件
      var tempFilePath = await fileService.SaveUploadedFile(file);

      // 生成输出路径
      var outputPath = Path.Combine(fileService.GetTempDirectory(), $"{Guid.NewGuid()}.ico");

      // 执行转换 处理大小参数
      var sizesArray = sizes?.Split(',').Select(int.Parse).ToArray();
      if (sizesArray is { Length: &gt; 0 })
            IcoGenerator.GenerateIcon(tempFilePath, outputPath, sizesArray);
      else
            IcoGenerator.GenerateIcon(tempFilePath, outputPath);

      // 读取到内存
      var memoryStream = await fileService.ReadFileToMemoryAsync(outputPath);

      // 删除临时文件
      fileService.DeleteFile(outputPath);

      logger.LogInformation($"{DateTime.UtcNow} 文件 {file.FileName} 转换成功");

      return Results.File(memoryStream, "image/x-icon");
    }
    catch (Exception ex)
    {
      logger.LogError(ex, $"{DateTime.UtcNow} 处理文件时发生错误");
      return Results.Problem("处理文件时发生错误");
    }
}).DisableAntiforgery();
</code></pre>
<h2 id="接口测试">接口测试</h2>
<p>这里直接使用模版自带的http文件进行测试,这个还是第一次使用,还折腾了挺久。</p>
<p><code>SharpIcoWeb.http</code> 文件</p>
<p>简单介绍下,第一个请求只上传了图片文件,未指定尺寸参数,后端接口会默认生成16,32,48,64,128,256,512,1024 尺寸的ico文件。</p>
<p>第二个请求就是添加了尺寸参数的请求。</p>
<pre><code class="language-c#">@SharpIcoWeb_HostAddress = http://localhost:5235

### 上传文件并转换为ICO(不带尺寸参数)
POST {{SharpIcoWeb_HostAddress}}/api/uploadDownload
Content-Type: multipart/form-data; boundary=WebAppBoundary

--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="1.png"
Content-Type: image/png

&lt; ./1.png
--WebAppBoundary--

### 上传文件并转换为ICO(带尺寸参数)
POST {{SharpIcoWeb_HostAddress}}/api/uploadDownload
Content-Type: multipart/form-data; boundary=WebAppBoundary

--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="1.png"
Content-Type: image/png

&lt; ./1.png
--WebAppBoundary
Content-Disposition: form-data; name="sizes"

16,32,48,64,128
</code></pre>
<h3 id="测试结果">测试结果</h3>
<p><img src="https://img2024.cnblogs.com/blog/3091176/202507/3091176-20250702161837918-331458249.png" alt="image" loading="lazy"></p>
<h3 id="关键代码">关键代码</h3>
<p><code>IcoGenerator.GenerateIcon(tempFilePath, outputPath, sizesArray);</code>这里是调用SharpIco工具提供的方法。</p>
<p><code>app.MapPost().DisableAntiforgery()</code>DisableAntiforgery作用是禁用 ASP.NET Core 的防伪造令牌验证。</p>
<h2 id="相关链接">相关链接</h2>
<ul>
<li>SharpIco:https://github.com/star-plan/sharp-ico</li>
<li>SharpIcoWeb:https://github.com/ZyPLJ/SharpIcoWeb</li>
<li>最小API官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/minimal-apis/overview?view=aspnetcore-9.0</li>
<li>最小API学习指南:https://blog.csdn.net/xiaohucxy/article/details/140134927</li>
</ul><br><br>
来源:https://www.cnblogs.com/ZYPLJ/p/18961664
頁: [1]
查看完整版本: SharpIcoWeb开发记录篇