Quellcode durchsuchen

CleanUp:Skip by suffix; ADD: Simple WebUI; WIP: PDF Image Read;

HOME vor 1 Monat
Ursprung
Commit
06f49bf950

+ 28 - 0
ImageConvertService/Biz/ArchiveEntrySkipper.cs

@@ -0,0 +1,28 @@
+using ImageConvertService.Biz.Models;
+
+namespace ImageConvertService.Biz;
+
+public class ArchiveEntrySkipper
+{
+    public (List<ArchiveEntry> Direct, List<ArchiveEntry> Convert) Filter(IReadOnlyCollection<ArchiveEntry> items, string? skipPsv)
+    {
+        List<ArchiveEntry> d = new(), c = new();
+
+        if (string.IsNullOrWhiteSpace(skipPsv))
+        {
+            c.AddRange(items);
+            return (d, c);
+        }
+
+        var skipSuffix = skipPsv.Split("|").Distinct().ToArray();
+        foreach (var entry in items)
+        {
+            if (skipSuffix.Any(p => entry.PathAndName.EndsWith(p, StringComparison.OrdinalIgnoreCase)))
+                d.Add(entry);
+            else
+                c.Add(entry);
+        }
+
+        return (d, c);
+    }
+}

+ 5 - 13
ImageConvertService/Biz/ArchiveFileAccessor.cs

@@ -1,4 +1,4 @@
-using ImageConvertService.ExceptionHandling;
+using ImageConvertService.Biz.Models;
 
 using SevenZip;
 
@@ -11,9 +11,11 @@ public class ArchiveFileAccessor
         SevenZipBase.SetLibraryPath(@"C:\Program Files\7-Zip\7z.dll");
     }
 
-    public ArchiveEntry[] ReadArchive(byte[] input)
+    public ArchiveEntry[] ReadArchive(byte[] input) => ReadArchive(new MemoryStream(input));
+
+    public ArchiveEntry[] ReadArchive(Stream stream)
     {
-        using var sze = new SevenZipExtractor(new MemoryStream(input));
+        using var sze = new SevenZipExtractor(stream);
 
         var ret = new List<ArchiveEntry>(sze.ArchiveFileData.Count(p => p.IsDirectory == false));
 
@@ -88,14 +90,4 @@ public class ArchiveFileAccessor
 
         return msZip.ToArray();
     }
-}
-
-public class ArchiveEntry
-{
-    public string PathAndName { get; set; }
-    public DateTimeOffset? LastWrite { get; set; }
-    public DateTimeOffset? CreateTime { get; set; }
-    public DateTimeOffset? LastAccess { get; set; }
-
-    public byte[] Content { get; set; }
 }

+ 12 - 15
ImageConvertService/Biz/ImageConverter.cs

@@ -3,6 +3,7 @@ using System;
 using ImageConvertService.ExceptionHandling;
 using ImageMagick;
 using ImageMagick.Formats;
+using ImageConvertService.Biz.Models;
 
 namespace ImageConvertService.Biz;
 
@@ -43,7 +44,7 @@ public class ImageConverter
         return ms.ToArray();
     }
 
-    public ArchiveEntry[] ConvertArchiveEntries(ArchiveEntry[] entries, out bool withErrors)
+    public bool ConvertToWebp(List<ArchiveEntry> entries)
     {
         var lstExceptions = new List<(string pathAndName, Exception exception)>();
 
@@ -63,21 +64,17 @@ public class ImageConverter
             item.PathAndName += ".webp";
         });
 
-        var entriesToCompress = entries;
-        withErrors = lstExceptions.Count != 0;
-        if (withErrors)
+        if (lstExceptions.Count == 0) return true;
+
+        entries.Add(new ArchiveEntry
         {
-            entriesToCompress = entries.Prepend(new ArchiveEntry
-            {
-                PathAndName = $"Errors.ImageConvertService.{Guid.NewGuid():N}.txt",
-                Content = Encoding.UTF8.GetBytes(
-                    string.Join($"{Environment.NewLine}{Environment.NewLine}",
-                        lstExceptions.Select(p => $"{p.pathAndName}:{Environment.NewLine} {p.exception}"))
-                )
-            }).ToArray();
-        }
+            PathAndName = $"@{nameof(ImageConverter)}.{nameof(ConvertToWebp)}.{lstExceptions.Count}.Errors.{Guid.NewGuid():N}.txt",
+            Content = Encoding.UTF8.GetBytes(
+                string.Join($"{Environment.NewLine}{Environment.NewLine}",
+                    lstExceptions.Select(p => $"{p.pathAndName}:{Environment.NewLine} {p.exception}"))
+            )
+        });
 
-        return entriesToCompress;
+        return false;
     }
-
 }

+ 13 - 0
ImageConvertService/Biz/Models/ArchiveEntry.cs

@@ -0,0 +1,13 @@
+namespace ImageConvertService.Biz.Models;
+
+public class ArchiveEntry
+{
+    public string PathAndName { get; set; }
+    public ArchiveEntryFileTimeTuple? FileTimeTuple { get; set; } = new();
+
+    public DateTimeOffset? LastWrite { get => FileTimeTuple?.LastWrite; set => (FileTimeTuple ??= new()).LastWrite = value; }
+    public DateTimeOffset? CreateTime { get => FileTimeTuple?.CreateTime; set => (FileTimeTuple ??= new()).CreateTime = value; }
+    public DateTimeOffset? LastAccess { get => FileTimeTuple?.LastAccess; set => (FileTimeTuple ??= new()).LastAccess = value; }
+
+    public byte[]? Content { get; set; }
+}

+ 8 - 0
ImageConvertService/Biz/Models/ArchiveEntryFileTimeTuple.cs

@@ -0,0 +1,8 @@
+namespace ImageConvertService.Biz.Models;
+
+public class ArchiveEntryFileTimeTuple
+{
+    public DateTimeOffset? LastWrite { get; set; }
+    public DateTimeOffset? CreateTime { get; set; }
+    public DateTimeOffset? LastAccess { get; set; }
+}

+ 101 - 0
ImageConvertService/Biz/PdfImageReader.cs

@@ -0,0 +1,101 @@
+using System.IO.Compression;
+using UglyToad.PdfPig;
+
+namespace ImageConvertService.Biz;
+using Models;
+
+public class PdfImageReader
+{
+    private static class Signatures
+    {
+        public static readonly byte[] Zlib = { 0x78, 0x9C };
+        public static readonly byte[] Jpg = { 0xFF, 0xD8, 0xFF };
+    }
+
+    public ArchiveEntry[] ReadImages(byte[] inputPdfBytes, string? password = null, ArchiveEntryFileTimeTuple? fileTime = null)
+    {
+        var parsingOptions = new ParsingOptions();
+
+        if (password != null) parsingOptions.Password = password;
+
+        using var document = PdfDocument.Open(inputPdfBytes, parsingOptions);
+        var pages = document.GetPages().ToArray();
+        var lstResult = new List<ArchiveEntry>(pages.Length);
+        Parallel.ForEach(pages, (item, _, pageIndex) =>
+        {
+            var images = item.GetImages().ToArray();
+            for (var imageIndex = 0; imageIndex < images.Length; imageIndex++)
+            {
+                var pdfImage = images[imageIndex];
+                var outputFileName = $"P{pageIndex + 1:0000}_{imageIndex + 1:000}";
+
+                if (pdfImage.TryGetPng(out var pngBytes))
+                {
+                    lock (lstResult)
+                    {
+                        lstResult.Add(new ArchiveEntry
+                        {
+                            PathAndName = outputFileName + ".png",
+                            FileTimeTuple = fileTime,
+                            Content = pngBytes
+                        });
+                    }
+                }
+                else if (pdfImage.TryGetBytesAsMemory(out var rms))
+                {
+                    int bp = 0;
+
+                    var rawBytes = rms.ToArray();
+
+                    lock (lstResult)
+                    {
+                        lstResult.Add(new ArchiveEntry
+                        {
+                            PathAndName = outputFileName + ".bin",
+                            FileTimeTuple = fileTime,
+                            Content = rawBytes
+                        });
+                    }
+                }
+                else
+                {
+                    var extractedBytes = pdfImage.RawBytes.ToArray();
+                    var span = (ReadOnlySpan<byte>)extractedBytes;
+                    var extName = ".bin";
+
+                    //de zlib
+                    if (span.Length > Signatures.Zlib.Length && span.StartsWith(Signatures.Zlib))
+                    {
+                        using var inMs = new MemoryStream(extractedBytes);
+                        using var decStream = new ZLibStream(inMs, CompressionMode.Decompress);
+                        using var outMs = new MemoryStream();
+                        decStream.CopyTo(outMs);
+                        extractedBytes = outMs.ToArray();
+                        span = (ReadOnlySpan<byte>)extractedBytes;
+                    }
+
+                    if (span.Length > Signatures.Jpg.Length && span.StartsWith(Signatures.Jpg))
+                    {
+                        extName = ".jpg";
+                    }
+                    else
+                    {
+                        int bp = 0;
+                    }
+
+                    lock (lstResult)
+                    {
+                        lstResult.Add(new ArchiveEntry
+                        {
+                            PathAndName = outputFileName + extName,
+                            FileTimeTuple = fileTime,
+                            Content = extractedBytes
+                        });
+                    }
+                }
+            }
+        });
+
+        return lstResult.ToArray();
+    }
+}

+ 41 - 17
ImageConvertService/Controllers/ConvertToWebpController.cs

@@ -1,12 +1,13 @@
-using System.Text;
-using ImageConvertService.Biz;
+using ImageConvertService.Biz;
+using ImageConvertService.Biz.Models;
+using ImageConvertService.ExceptionHandling;
 using Microsoft.AspNetCore.Mvc;
 
 namespace ImageConvertService.Controllers;
 
 [Route("api/[controller]/[action]")]
 [ApiController]
-public class ConvertToWebpController(ImageConverter converter, ArchiveFileAccessor archiver) : Controller
+public class ConvertToWebpController(ImageConverter converter, ArchiveFileAccessor archiver, PdfImageReader pdf, ArchiveEntrySkipper filter) : Controller
 {
     [HttpPost]
     public IActionResult SingleFile(IFormFile file)
@@ -19,33 +20,56 @@ public class ConvertToWebpController(ImageConverter converter, ArchiveFileAccess
     }
 
     [HttpPost]
-    public IActionResult AnyArchiveToFastZip(IFormFile file)
+    public IActionResult AnyArchive(
+        IFormFile file,
+        [FromForm, FromQuery] OutputArchiveFormat format = OutputArchiveFormat.Fast7Z,
+        [FromForm, FromQuery] string? skips = null)
     {
         var ms = new MemoryStream();
         file.CopyTo(ms);
         var bytes = ms.ToArray();
-        var entries = archiver.ReadArchive(bytes);
 
-        var entriesToCompress = converter.ConvertArchiveEntries(entries,out var withErrors);
+        var (direct, convert) = filter.Filter(archiver.ReadArchive(bytes), skips);
 
-        var outBytes = archiver.MakeArchiveFastZip(entriesToCompress);
+        var noError = converter.ConvertToWebp(convert);
 
-        return File(outBytes, "application/zip", $"{file.FileName}-webp{(withErrors?$"-{nameof(withErrors)}":"")}.zip");
-    }
+        direct.AddRange(convert);
 
+        return OutputAsArchive(file.FileName, format, direct, noError);
+    }
 
     [HttpPost]
-    public IActionResult AnyArchiveToFast7Z(IFormFile file)
+    public async Task<IActionResult> AnyArchiveStreaming(
+        [FromQuery] string filename,
+        [FromQuery] OutputArchiveFormat format = OutputArchiveFormat.Fast7Z,
+        [FromQuery] string? skips = null)
     {
-        var ms = new MemoryStream();
-        file.CopyTo(ms);
-        var bytes = ms.ToArray();
-        var entries = archiver.ReadArchive(bytes);
+        await using var tmpFileStream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
+        await Request.Body.CopyToAsync(tmpFileStream);
+        tmpFileStream.Position = 0;
+
+        var (direct, convert) = filter.Filter(archiver.ReadArchive(tmpFileStream), skips);
 
-        var entriesToCompress = converter.ConvertArchiveEntries(entries,out var withErrors);
+        var noError = converter.ConvertToWebp(convert);
 
-        var outBytes = archiver.MakeArchiveFast7Z(entriesToCompress);
+        direct.AddRange(convert);
 
-        return File(outBytes, "application/x-7z-compressed", $"{file.FileName}-webp{(withErrors ? $"-{nameof(withErrors)}" : "")}.7z");
+        return OutputAsArchive(filename, format, direct, noError);
+    }
+
+    private IActionResult OutputAsArchive(string filename, OutputArchiveFormat format, List<ArchiveEntry> direct, bool noError)
+    {
+        return format switch
+        {
+            OutputArchiveFormat.Fast7Z => File(archiver.MakeArchiveFast7Z(direct), "application/x-7z-compressed", $"{filename}-webp{(noError ? "" : "-withErrors")}.7z"),
+            OutputArchiveFormat.FastZip => File(archiver.MakeArchiveFastZip(direct), "application/zip", $"{filename}-webp{(noError ? "" : "-withErrors")}.zip"),
+            _ => throw new UserFriendlyException($"Invalid {nameof(OutputArchiveFormat)}")
+        };
+    }
+
+    public enum OutputArchiveFormat
+    {
+        Fast7Z,
+        FastZip
     }
 }

+ 1 - 1
ImageConvertService/Controllers/HomeController.cs

@@ -6,5 +6,5 @@ namespace ImageConvertService.Controllers;
 public class HomeController : Controller
 {
     [HttpGet] [Route("/")]
-    public IActionResult Index() => Content("OK "+DateTimeOffset.Now);
+    public IActionResult Index() =>View();
 }

+ 1 - 0
ImageConvertService/ImageConvertService.csproj

@@ -43,6 +43,7 @@
   <ItemGroup>
     <PackageReference Include="Magick.NET-Q8-x64" Version="14.4.0" />
     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.12" />
+    <PackageReference Include="PdfPig" Version="0.1.9" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
     <PackageReference Condition="'$(Configuration)'=='Debug'" Include="xunit" Version="2.9.3" />
     <PackageReference Condition="'$(Configuration)'=='Debug'" Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />

+ 5 - 8
ImageConvertService/Program.cs

@@ -1,11 +1,7 @@
 using System.Text.Json.Serialization;
 using ImageConvertService.Biz;
 using ImageConvertService.ExceptionHandling;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Localization;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
 
 var builder = WebApplication.CreateBuilder(args);
 
@@ -15,17 +11,18 @@ builder.Services.AddEndpointsApiExplorer();
 builder.Services.AddSwaggerGen();
 
 builder.Services
-    .AddControllers(options => options.Filters.Add<UserFriendlyExceptionFilter>())
+    .AddControllersWithViews(options => options.Filters.Add<UserFriendlyExceptionFilter>())
     .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
 
-builder.Services.AddSingleton<ImageConverter>();
 builder.Services.AddSingleton<ArchiveFileAccessor>();
+builder.Services.AddSingleton<ArchiveEntrySkipper>();
 builder.Services.AddSingleton<PdfImageReader>();
+builder.Services.AddSingleton<ImageConverter>();
 
 
 builder.Services.Configure<KestrelServerOptions>(options =>
 {
-    options.Limits.MaxRequestBodySize = 2000 * 1024 * 1024; // 设置最大请求体大小 2GB
+    options.Limits.MaxRequestBodySize = 8000 * 1024 * 1024L; // 设置最大请求体大小 8GB
 });
 
 var app = builder.Build();
@@ -40,7 +37,7 @@ app.UseSwaggerUI();
 //app.UseHttpsRedirection();
 
 //app.AddExample();
-app.MapControllers().WithFormOptions(multipartBodyLengthLimit: 2_000_000_000L); // 设置表单最大请求体大小 2GB
+app.MapControllers().WithFormOptions(multipartBodyLengthLimit: 8000 * 1024 * 1024L); // 设置表单最大请求体大小 8GB
 
 app.Run();
 

+ 37 - 37
ImageConvertService/Properties/launchSettings.json

@@ -1,41 +1,41 @@
 {
-  "$schema": "http://json.schemastore.org/launchsettings.json",
-  "iisSettings": {
-    "windowsAuthentication": false,
-    "anonymousAuthentication": true,
-    "iisExpress": {
-      "applicationUrl": "http://localhost:27392",
-      "sslPort": 44353
-    }
-  },
-  "profiles": {
-    "http": {
-      "commandName": "Project",
-      "dotnetRunMessages": true,
-      "launchBrowser": true,
-      "launchUrl": "swagger",
-      "applicationUrl": "http://localhost:5254",
-      "environmentVariables": {
-        "ASPNETCORE_ENVIRONMENT": "Development"
-      }
-    },
-    "https": {
-      "commandName": "Project",
-      "dotnetRunMessages": true,
-      "launchBrowser": true,
-      "launchUrl": "swagger",
-      "applicationUrl": "http://localhost:5254",
-      "environmentVariables": {
-        "ASPNETCORE_ENVIRONMENT": "Development"
-      }
+    "$schema": "http://json.schemastore.org/launchsettings.json",
+    "iisSettings": {
+        "windowsAuthentication": false,
+        "anonymousAuthentication": true,
+        "iisExpress": {
+            "applicationUrl": "http://localhost:27392",
+            "sslPort": 44353
+        }
     },
-    "IIS Express": {
-      "commandName": "IISExpress",
-      "launchBrowser": true,
-      "launchUrl": "swagger",
-      "environmentVariables": {
-        "ASPNETCORE_ENVIRONMENT": "Development"
-      }
+    "profiles": {
+        "http": {
+            "commandName": "Project",
+            "dotnetRunMessages": true,
+            "launchBrowser": true,
+            "launchUrl": "swagger",
+            "applicationUrl": "http://localhost:5254",
+            "environmentVariables": {
+                "ASPNETCORE_ENVIRONMENT": "Development"
+            }
+        },
+        "https": {
+            "commandName": "Project",
+            "dotnetRunMessages": true,
+            "launchBrowser": true,
+            "launchUrl": "swagger",
+            "applicationUrl": "https://localhost:5254",
+            "environmentVariables": {
+                "ASPNETCORE_ENVIRONMENT": "Development"
+            }
+        },
+        "IIS Express": {
+            "commandName": "IISExpress",
+            "launchBrowser": true,
+            "launchUrl": "swagger",
+            "environmentVariables": {
+                "ASPNETCORE_ENVIRONMENT": "Development"
+            }
+        }
     }
-  }
 }

+ 2 - 1
ImageConvertService/Tests/SevenZipTests.cs

@@ -1,10 +1,11 @@
 using SevenZip;
-using Xunit;
 
 namespace ImageConvertService.Tests;
 
 #if DEBUG
 
+using Xunit;
+
 public class SevenZipTests
 {
     [Fact]

+ 27 - 0
ImageConvertService/Views/Home/Index.cshtml

@@ -0,0 +1,27 @@
+@{
+	Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+<head>
+	<title>ImageConvertService</title>
+</head>
+<body>
+	<div>
+		<form action="/api/ConvertToWebp/AnyArchive" method="post" enctype='multipart/form-data'>
+			In: <input type="file" name="file" />
+			<br />
+			Skip Suffix: <input type="text" name="skips" placeholder="PipeDelimited" />
+			<br/>
+			Out: <select name="format">
+				<option selected>Fast7Z</option>
+				<option>FastZip</option>
+			</select>
+			<br/>
+			<input type="submit"/>
+		</form>
+	</div>
+</body>
+</html>