Compare commits

...

13 Commits

Author SHA1 Message Date
d16467ea9e Merge branch 'master' of https://git.chenx221.cyou/chenx221/Comic_Compressor 2024-09-16 17:26:06 +08:00
d86f91f6c7 优化代码 2024-09-16 17:25:58 +08:00
62cc08ca70 更新 readme.md 2024-09-16 15:12:35 +08:00
4e3c9aa311 up 2024-09-16 15:10:01 +08:00
f03cce27f5 Merge branch 'master' of https://git.chenx221.cyou/chenx221/Comic_Compressor 2024-09-16 15:09:19 +08:00
815d74c09e update 2024-09-16 15:09:04 +08:00
9d150d77a1 更新 readme.md 2024-09-10 17:55:21 +08:00
c3837e6a87 Merge branch 'master' of https://git.chenx221.cyou/chenx221/Comic_Compressor 2024-09-10 17:53:27 +08:00
b1a7268288 Use Magick instead of ImageSharp
Support more formats and output in original format
(3/10)
2024-09-10 17:37:17 +08:00
a3e953b4e7 Use Magick instead of ImageSharp
Support more formats and output in original format
(2/10)
2024-09-10 15:46:53 +08:00
35d6a907f6 Use Magick instead of ImageSharp
Support more formats and output in original format
(1/10)
2024-09-10 09:54:41 +08:00
8b6a554591 添加 readme.md 2024-08-28 23:38:03 +08:00
55b437d830 up 2024-08-28 16:15:45 +08:00
16 changed files with 387 additions and 534 deletions

View File

@ -3,24 +3,30 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.11.35219.272 VisualStudioVersion = 17.11.35219.272
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Comic_Compressor", "Comic_Compressor\Comic_Compressor.csproj", "{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Comic_Compressor", "Comic_Compressor\Comic_Compressor.csproj", "{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64 Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64 Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|x64.ActiveCfg = Debug|x64 {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|x64.ActiveCfg = Debug|x64
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|x64.Build.0 = Debug|x64 {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|x64.Build.0 = Debug|x64
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|x86.ActiveCfg = Debug|x86
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Debug|x86.Build.0 = Debug|x86
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|Any CPU.Build.0 = Release|Any CPU {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|Any CPU.Build.0 = Release|Any CPU
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|x64.ActiveCfg = Release|x64 {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|x64.ActiveCfg = Release|x64
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|x64.Build.0 = Release|x64 {B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|x64.Build.0 = Release|x64
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|x86.ActiveCfg = Release|x86
{B6B0E3D8-DE3D-4A7D-AAE5-34953ABFEA2A}.Release|x86.Build.0 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,18 +1,16 @@
using LibHeifSharp; using ImageMagick;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using ShellProgressBar; using ShellProgressBar;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace Comic_Compressor namespace Comic_Compressor
{ {
internal class AvifCompressor internal class AvifCompressor : Utils
{ {
internal static void CompressImages(string sourceImagePath, string targetStoragePath) internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, bool usePresetQuality, int Quality)
{ {
LibHeifSharpDllImportResolver.Register(); MagickFormat targetFormat = MagickFormat.Avif;
// Step 1: Get all subdirectories and store them in a list string targetExtension = ".avif";
int targetQuality = usePresetQuality ? 80 : Quality;
List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories)); List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories));
int totalFiles = 0; int totalFiles = 0;
@ -27,303 +25,12 @@ namespace Comic_Compressor
ProgressBarOnBottom = true ProgressBarOnBottom = true
}); });
// Step 2: Iterate through each subdirectory in order
foreach (string subdirectory in subdirectories) foreach (string subdirectory in subdirectories)
{ {
// Step 3: Process each directory ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar, threadCount, targetExtension, targetFormat, targetQuality);
ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar);
} }
Console.WriteLine("All directories processed successfully."); Console.WriteLine("All directories processed successfully.");
} }
private static void ProcessDirectory(string subdirectory, string sourceImagePath, string targetStoragePath, ShellProgressBar.ProgressBar progressBar)
{
// Get the relative path of the subdirectory
string relativePath = Path.GetRelativePath(sourceImagePath, subdirectory);
// Create the corresponding subdirectory in the target storage path
string targetSubdirectory = Path.Combine(targetStoragePath, relativePath);
Directory.CreateDirectory(targetSubdirectory);
// Get all image files in the subdirectory (jpg and png)
string[] imageFiles = GetImageFiles(subdirectory);
// Set up ParallelOptions to limit the number of concurrent threads
ParallelOptions options = new()
{
MaxDegreeOfParallelism = 2 // Adjust this value to set the number of concurrent threads
};
// Process each image file in parallel
Parallel.ForEach(imageFiles, options, imageFile =>
{
// Set the target file path with the .avif extension
string targetFilePath = Path.Combine(targetSubdirectory, Path.GetFileNameWithoutExtension(imageFile) + ".avif");
// Check if the target file already exists
if (!File.Exists(targetFilePath))
{
CompressImage(imageFile, targetFilePath);
// Update progress bar safely
lock (progressBar)
{
progressBar.Tick($"Processed {Path.GetFileName(imageFile)}");
}
}
else
{
lock (progressBar) { progressBar.Tick($"Skipped {Path.GetFileName(imageFile)}"); }
}
});
}
private static string[] GetImageFiles(string directoryPath)
{
// Get all image files supported by ImageSharp
string[] supportedExtensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tiff"];
List<string> allFiles = [];
foreach (string extension in supportedExtensions)
{
allFiles.AddRange(Directory.GetFiles(directoryPath, extension, SearchOption.TopDirectoryOnly));
}
return [.. allFiles];
}
private static void CompressImage(string sourceFilePath, string targetFilePath)
{
int quality = 80;
//int quality = 70;
var format = HeifCompressionFormat.Av1;
bool saveAlphaChannel = false;
bool writeTwoProfiles = false;
try
{
// Load the image and ensure it's in Rgb24 format
using var image = SixLabors.ImageSharp.Image.Load(sourceFilePath);
var rgbImage = image.CloneAs<Rgb24>();
//// Check the longest side of the image and resize if necessary
//int maxDimension = Math.Max(rgbImage.Width, rgbImage.Height);
//if (maxDimension > 1200)
//{
// double scaleFactor = 1200.0 / maxDimension;
// int newWidth = (int)(rgbImage.Width * scaleFactor);
// int newHeight = (int)(rgbImage.Height * scaleFactor);
// rgbImage.Mutate(x => x.Resize(newWidth, newHeight));
//}
// Save as AVIF format
using var context = new HeifContext();
HeifEncoderDescriptor? encoderDescriptor = null;
if (LibHeifInfo.HaveEncoder(format))
{
var encoderDescriptors = context.GetEncoderDescriptors(format);
encoderDescriptor = encoderDescriptors[0];
}
else
{
Console.WriteLine("No AV1 encoder available.");
return;
}
using HeifEncoder encoder = context.GetEncoder(encoderDescriptor);
if (writeTwoProfiles && !LibHeifInfo.CanWriteTwoColorProfiles)
{
writeTwoProfiles = false;
Console.WriteLine($"Warning: LibHeif version {LibHeifInfo.Version} cannot write two color profiles.");
}
using var heifImage = CreateHeifImage(rgbImage, writeTwoProfiles, out var metadata);
encoder.SetLossyQuality(quality);
var encodingOptions = new HeifEncodingOptions
{
SaveAlphaChannel = saveAlphaChannel,
WriteTwoColorProfiles = writeTwoProfiles
};
context.EncodeImage(heifImage, encoder, encodingOptions);
context.WriteToFile(targetFilePath);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
private static HeifImage CreateHeifImage(Image<Rgb24> image,
bool writeTwoColorProfiles,
out ImageMetadata metadata)
{
HeifImage? heifImage = null;
HeifImage? temp = null;
try
{
metadata = image.Metadata;
temp = ConvertToHeifImage(image);
if (writeTwoColorProfiles && metadata.IccProfile != null)
{
temp.IccColorProfile = new HeifIccColorProfile(metadata.IccProfile.ToByteArray());
temp.NclxColorProfile = new HeifNclxColorProfile(ColorPrimaries.BT709,
TransferCharacteristics.Srgb,
MatrixCoefficients.BT601,
fullRange: true);
}
else
{
if (metadata.IccProfile != null)
{
temp.IccColorProfile = new HeifIccColorProfile(metadata.IccProfile.ToByteArray());
}
else
{
temp.NclxColorProfile = new HeifNclxColorProfile(ColorPrimaries.BT709,
TransferCharacteristics.Srgb,
MatrixCoefficients.BT601,
fullRange: true);
}
}
heifImage = temp;
temp = null;
}
finally
{
temp?.Dispose();
}
return heifImage;
}
private static HeifImage ConvertToHeifImage(Image<Rgb24> image)
{
bool isGrayscale = IsGrayscale(image);
var colorspace = isGrayscale ? HeifColorspace.Monochrome : HeifColorspace.Rgb;
var chroma = colorspace == HeifColorspace.Monochrome ? HeifChroma.Monochrome : HeifChroma.InterleavedRgb24;
HeifImage? heifImage = null;
HeifImage? temp = null;
try
{
temp = new HeifImage(image.Width, image.Height, colorspace, chroma);
if (colorspace == HeifColorspace.Monochrome)
{
temp.AddPlane(HeifChannel.Y, image.Width, image.Height, 8);
CopyGrayscale(image, temp);
}
else
{
temp.AddPlane(HeifChannel.Interleaved, image.Width, image.Height, 8);
CopyRgb(image, temp);
}
heifImage = temp;
temp = null;
}
finally
{
temp?.Dispose();
}
return heifImage;
}
private static unsafe void CopyGrayscale(Image<Rgb24> image, HeifImage heifImage)
{
var grayPlane = heifImage.GetPlane(HeifChannel.Y);
byte* grayPlaneScan0 = (byte*)grayPlane.Scan0;
int grayPlaneStride = grayPlane.Stride;
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
var src = accessor.GetRowSpan(y);
byte* dst = grayPlaneScan0 + (y * grayPlaneStride);
for (int x = 0; x < accessor.Width; x++)
{
ref var pixel = ref src[x];
dst[0] = pixel.R;
dst++;
}
}
});
}
private static unsafe void CopyRgb(Image<Rgb24> image, HeifImage heifImage)
{
var interleavedData = heifImage.GetPlane(HeifChannel.Interleaved);
byte* srcScan0 = (byte*)interleavedData.Scan0;
int srcStride = interleavedData.Stride;
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
var src = accessor.GetRowSpan(y);
byte* dst = srcScan0 + (y * srcStride);
for (int x = 0; x < accessor.Width; x++)
{
ref var pixel = ref src[x];
dst[0] = pixel.R;
dst[1] = pixel.G;
dst[2] = pixel.B;
dst += 3;
}
}
});
}
private static bool IsGrayscale(Image<Rgb24> image)
{
bool isGrayscale = true;
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
var src = accessor.GetRowSpan(y);
for (int x = 0; x < accessor.Width; x++)
{
ref var pixel = ref src[x];
if (!(pixel.R == pixel.G && pixel.G == pixel.B))
{
isGrayscale = false;
break;
}
}
if (!isGrayscale)
{
break;
}
}
});
return isGrayscale;
}
} }
} }

View File

@ -5,27 +5,20 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64;x86</Platforms>
<UseWindowsForms>True</UseWindowsForms> <UseWindowsForms>True</UseWindowsForms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LibHeifSharp" Version="3.2.0" /> <PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.0.0" />
<PackageReference Include="ShellProgressBar" Version="5.2.0" /> <PackageReference Include="ShellProgressBar" Version="5.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="aom.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="dav1d.dll"> <None Update="dav1d.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="libde265.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="libheif.dll"> <None Update="libheif.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
@ -44,9 +37,6 @@
<None Update="libs\libx265.dll"> <None Update="libs\libx265.dll">
<CopyToOutputDirectory>Never</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None> </None>
<None Update="libx265.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,36 @@
using ImageMagick;
using ShellProgressBar;
namespace Comic_Compressor
{
internal class JxlCompressor : Utils
{
internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, bool usePresetQuality, int Quality)
{
MagickFormat targetFormat = MagickFormat.Jxl;
string targetExtension = ".jxl";
int targetQuality = usePresetQuality ? 90 : Quality;
List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories));
int totalFiles = 0;
foreach (string subdirectory in subdirectories)
{
totalFiles += GetImageFiles(subdirectory).Length;
}
using var progressBar = new ShellProgressBar.ProgressBar(totalFiles, "Compressing images", new ProgressBarOptions
{
ProgressCharacter = '─',
ProgressBarOnBottom = true
});
foreach (string subdirectory in subdirectories)
{
ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar, threadCount, targetExtension, targetFormat, targetQuality);
}
Console.WriteLine("All directories processed successfully.");
}
}
}

View File

@ -0,0 +1,51 @@
using ImageMagick;
using ShellProgressBar;
namespace Comic_Compressor
{
//Process images in legacy format(JPG,PNG)
internal class LegacyFormatCompressor : Utils
{
internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, bool usePresetQuality, int Quality, int format)
{
MagickFormat targetFormat = format switch
{
3 => MagickFormat.Jpeg,
4 => MagickFormat.Png,
5 => MagickFormat.Bmp,
_ => throw new Exception(),
};
string targetExtension = format switch
{
3 => ".jpg",
4 => ".png",
5 => ".bmp",
_ => throw new Exception(),
};
int targetQuality = usePresetQuality ? 90 : Quality;
List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories));
int totalFiles = 0;
foreach (string subdirectory in subdirectories)
{
totalFiles += GetImageFiles(subdirectory).Length;
}
using var progressBar = new ShellProgressBar.ProgressBar(totalFiles, "Compressing images", new ProgressBarOptions
{
ProgressCharacter = '─',
ProgressBarOnBottom = true
});
foreach (string subdirectory in subdirectories)
{
ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar, threadCount, targetExtension, targetFormat, targetQuality);
}
Console.WriteLine("All directories processed successfully.");
}
}
}

View File

@ -1,105 +0,0 @@
/*
* This file is part of libheif-sharp-samples, a collection of example applications
* for libheif-sharp
*
* The MIT License (MIT)
*
* Copyright (c) 2020, 2021, 2022, 2023 Nicholas Hayes
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
using System.Reflection;
using System;
using System.Runtime.InteropServices;
namespace Comic_Compressor
{
internal static class LibHeifSharpDllImportResolver
{
private static IntPtr cachedLibHeifModule = IntPtr.Zero;
private static bool firstRequestForLibHeif = true;
/// <summary>
/// Registers the <see cref="DllImportResolver"/> for the LibHeifSharp assembly.
/// </summary>
public static void Register()
{
// The runtime will execute the specified callback when it needs to resolve a native library
// import for the LibHeifSharp assembly.
NativeLibrary.SetDllImportResolver(typeof(LibHeifSharp.LibHeifInfo).Assembly, Resolver);
}
private static IntPtr Resolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
// We only care about a native library named libheif, the runtime will use
// its default behavior for any other native library.
if (string.Equals(libraryName, "libheif", StringComparison.Ordinal))
{
// Because the DllImportResolver will be called multiple times we load libheif once
// and cache the module handle for future requests.
if (firstRequestForLibHeif)
{
firstRequestForLibHeif = false;
cachedLibHeifModule = LoadNativeLibrary(libraryName, assembly, searchPath);
}
return cachedLibHeifModule;
}
// Fall back to default import resolver.
return IntPtr.Zero;
}
private static nint LoadNativeLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (OperatingSystem.IsWindows())
{
// On Windows the libheif DLL name defaults to heif.dll, so we try to load that if
// libheif.dll was not found.
try
{
return NativeLibrary.Load(libraryName, assembly, searchPath);
}
catch (DllNotFoundException)
{
if (NativeLibrary.TryLoad("heif.dll", assembly, searchPath, out IntPtr handle))
{
return handle;
}
else
{
throw;
}
}
}
else if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS())
{
// The Apple mobile/embedded platforms statically link libheif into the AOT compiled main program binary.
return NativeLibrary.GetMainProgramHandle();
}
else
{
// Use the default runtime behavior for all other platforms.
return NativeLibrary.Load(libraryName, assembly, searchPath);
}
}
}
}

View File

@ -0,0 +1,32 @@
using ShellProgressBar;
namespace Comic_Compressor
{
internal class MixProcessor : Utils
{
internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, bool usePresetQuality, int Quality)
{
int targetQuality = usePresetQuality ? 90 : Quality;
List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories));
int totalFiles = 0;
foreach (string subdirectory in subdirectories)
{
totalFiles += GetImageFiles(subdirectory).Length;
}
using var progressBar = new ShellProgressBar.ProgressBar(totalFiles, "Compressing images", new ProgressBarOptions
{
ProgressCharacter = '─',
ProgressBarOnBottom = true
});
foreach (string subdirectory in subdirectories)
{
ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar, threadCount, targetQuality);
}
Console.WriteLine("All directories processed successfully.");
}
}
}

View File

@ -1,5 +1,4 @@
using System.Text; using System.Text;
using System.Windows.Forms;
namespace Comic_Compressor namespace Comic_Compressor
{ {
internal class Program internal class Program
@ -7,61 +6,86 @@ namespace Comic_Compressor
[STAThread] [STAThread]
static void Main() static void Main()
{ {
//OpenCL.IsEnabled = true;
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("请选择源图像所在位置:"); Console.WriteLine("请选择源图像所在位置:");
string? sourceImagePath = GetFolderPath(); string? sourceImagePath = Utils.GetFolderPath();
if (string.IsNullOrEmpty(sourceImagePath)) if (string.IsNullOrEmpty(sourceImagePath))
{ {
Console.WriteLine("未选择文件夹,程序将退出。"); Console.WriteLine("未选择文件夹,程序将退出。");
return; return;
} }
Console.WriteLine("请输入目标存储位置:"); Console.WriteLine("请选择保存位置:");
string? targetStoragePath = GetFolderPath(); string? targetStoragePath = Utils.GetFolderPath();
if (string.IsNullOrEmpty(targetStoragePath)) if (string.IsNullOrEmpty(targetStoragePath))
{ {
Console.WriteLine("未选择文件夹,程序将退出。"); Console.WriteLine("未选择文件夹,程序将退出。");
return; return;
} }
Console.WriteLine("请选择压缩模式0 - 压缩成webp1 - 压缩成avif"); Console.WriteLine("处理线程数:");
int threadCount = int.Parse(Console.ReadLine() ?? "2");
if (threadCount < 1)
{
Console.WriteLine("无效线程数");
return;
}
Console.WriteLine("目标格式0 - webp, 1 - avif, 2 - JXL(JPEG-XL), 3 - JPG, 4 - PNG, 5 - BMP, 6 - 保留原格式");
string? modeInput = Console.ReadLine(); string? modeInput = Console.ReadLine();
if (modeInput == null) if (modeInput == null)
{
Console.WriteLine("无效格式");
return;
}
Console.WriteLine("使用预设质量(默认使用)(y/n)");
string? input = Console.ReadLine()?.Trim().ToLower();
bool usePresetQuality = input == null || input == "" || input == "y" || input == "yes";
int targetQuality = -1;
if (!usePresetQuality)
{
Console.WriteLine("Quality (0-100 INT):");
string? targetQualityStr = Console.ReadLine();
if (targetQualityStr == null)
{ {
Console.WriteLine("无效输入"); Console.WriteLine("无效输入");
return; return;
} }
targetQuality = int.Parse(targetQualityStr);
if (targetQuality < 0 || targetQuality > 100)
{
Console.WriteLine("invalid image quality");
return;
}
}
switch (modeInput) switch (modeInput)
{ {
case "0": case "0":
WebpCompressor.CompressImages(sourceImagePath, targetStoragePath); WebpCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality);
break; break;
case "1": case "1":
AvifCompressor.CompressImages(sourceImagePath, targetStoragePath); AvifCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality);
break;
case "2":
JxlCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality);
break;
case "3":
case "4":
case "5":
LegacyFormatCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality, int.Parse(modeInput));
break;
case "6":
MixProcessor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality);
break; break;
default: default:
Console.WriteLine("不支持的模式"); Console.WriteLine("不支持的格式");
break; return;
}
}
private static string? GetFolderPath()
{
using var dialog = new FolderBrowserDialog();
dialog.ShowNewFolderButton = false;
DialogResult result = dialog.ShowDialog();
if (result == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.SelectedPath))
{
return dialog.SelectedPath;
}
else
{
return null;
} }
Utils.GetCompressorResult(sourceImagePath, targetStoragePath);
} }
} }
} }

188
Comic_Compressor/Utils.cs Normal file
View File

@ -0,0 +1,188 @@
using ImageMagick;
using System.Collections.Concurrent;
namespace Comic_Compressor
{
internal class Utils
{
// Compress an image file
// Resize the image if its max dimension is larger than 1200
// Set the quality of the compressed image
public static void CompressImage(string sourceFilePath, string targetFilePath, MagickFormat mFormat, int quality)
{
using MagickImage image = new(sourceFilePath);
uint maxDimension = image.Width > image.Height ? image.Width : image.Height;
if (maxDimension > 1200)
{
double scaleFactor = 1200.0 / maxDimension;
uint newWidth = (uint)(image.Width * scaleFactor);
uint newHeight = (uint)(image.Height * scaleFactor);
image.Resize(newWidth, newHeight);
}
image.Quality = (uint)quality;
image.Write(targetFilePath, mFormat);
}
// Get all image files in a directory with supported extensions
public static string[] GetImageFiles(string directoryPath)
{
string[] supportedExtensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tiff", "*.tif", "*.jxl", "*.avif", "*.webp"];
ConcurrentBag<string> allFiles = [];
foreach (string extension in supportedExtensions)
{
foreach (var file in Directory.EnumerateFiles(directoryPath, extension, SearchOption.TopDirectoryOnly))
{
allFiles.Add(file);
}
}
return [.. allFiles];
}
// Process all image files in a directory
public static void ProcessDirectory(string subdirectory, string sourceImagePath, string targetStoragePath, ShellProgressBar.ProgressBar progressBar, int threadCount, string format, MagickFormat mFormat, int quality)
{
string relativePath = Path.GetRelativePath(sourceImagePath, subdirectory);
string targetSubdirectory = Path.Combine(targetStoragePath, relativePath);
Directory.CreateDirectory(targetSubdirectory);
string[] imageFiles = GetImageFiles(subdirectory);
ParallelOptions options = new()
{
MaxDegreeOfParallelism = threadCount // Adjust this value to set the number of concurrent threads
};
Parallel.ForEach(imageFiles, options, imageFile =>
{
string targetFilePath = Path.Combine(targetSubdirectory, Path.GetFileNameWithoutExtension(imageFile) + format);
if (!File.Exists(targetFilePath))
{
CompressImage(imageFile, targetFilePath, mFormat, quality);
lock (progressBar)
{
progressBar.Tick($"Processed {Path.GetFileName(imageFile)}");
}
}
else
{
lock (progressBar) { progressBar.Tick($"Skipped {Path.GetFileName(imageFile)}"); }
}
});
}
//Process all image files in a directory, save as origin format
public static void ProcessDirectory(string subdirectory, string sourceImagePath, string targetStoragePath, ShellProgressBar.ProgressBar progressBar, int threadCount, int quality)
{
string relativePath = Path.GetRelativePath(sourceImagePath, subdirectory);
string targetSubdirectory = Path.Combine(targetStoragePath, relativePath);
Directory.CreateDirectory(targetSubdirectory);
string[] imageFiles = GetImageFiles(subdirectory);
ParallelOptions options = new()
{
MaxDegreeOfParallelism = threadCount // Adjust this value to set the number of concurrent threads
};
Parallel.ForEach(imageFiles, options, imageFile =>
{
string targetFilePath = Path.Combine(targetSubdirectory, Path.GetFileName(imageFile));
//detect file format
//supportedExtensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tiff", "*.jxl", "*.avif", "*.webp"];
string extension = Path.GetExtension(targetFilePath).ToLower();
MagickFormat mFormat = extension switch
{
".jpg" or ".jpeg" => MagickFormat.Jpeg,
".png" => MagickFormat.Png,
".bmp" => MagickFormat.Bmp,
".gif" => MagickFormat.Gif,
".tiff" or ".tif" => MagickFormat.Tiff,
".jxl" => MagickFormat.Jxl,
".avif" => MagickFormat.Avif,
".webp" => MagickFormat.WebP,
_ => throw new Exception()//这个位置怎么还会有意外情况
};
if (!File.Exists(targetFilePath))
{
CompressImage(imageFile, targetFilePath, mFormat, quality);
lock (progressBar)
{
progressBar.Tick($"Processed {Path.GetFileName(imageFile)}");
}
}
else
{
lock (progressBar) { progressBar.Tick($"Skipped {Path.GetFileName(imageFile)}"); }
}
});
}
// Display the compression result
public static void GetCompressorResult(string source, string target)
{
long sourceSize = GetDirectorySize(source);
long targetSize = GetDirectorySize(target);
double reduced = (sourceSize - targetSize) * 1.0 / sourceSize;
Console.WriteLine($"压缩前大小:{GetHumanReadableSize(sourceSize)}");
Console.WriteLine($"压缩后大小:{GetHumanReadableSize(targetSize)}");
Console.WriteLine($"体积已减少{reduced:P}");
}
// Get the size of a directory
public static long GetDirectorySize(string path)
{
long size = 0;
foreach (string file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
FileInfo fileInfo = new(file);
size += fileInfo.Length;
}
return size;
}
// Get the path of a folder via a dialog
public static string? GetFolderPath()
{
using var dialog = new FolderBrowserDialog();
dialog.ShowNewFolderButton = false;
DialogResult result = dialog.ShowDialog();
if (result == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.SelectedPath))
{
return dialog.SelectedPath;
}
else
{
return null;
}
}
// Get the human-readable size of a file
public static string GetHumanReadableSize(long size)
{
string[] sizes = ["B", "KB", "MB", "GB", "TB"];
double len = size;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len /= 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
}

View File

@ -1,15 +1,16 @@
using SixLabors.ImageSharp; using ImageMagick;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
using ShellProgressBar; using ShellProgressBar;
namespace Comic_Compressor namespace Comic_Compressor
{ {
internal class WebpCompressor internal class WebpCompressor : Utils
{ {
internal static void CompressImages(string sourceImagePath, string targetStoragePath) internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, bool usePresetQuality, int Quality)
{ {
// Step 1: Get all subdirectories and store them in a list MagickFormat targetFormat = MagickFormat.WebP;
string targetExtension = ".webp";
int targetQuality = usePresetQuality ? 90 : Quality;
List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories)); List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories));
int totalFiles = 0; int totalFiles = 0;
@ -24,93 +25,12 @@ namespace Comic_Compressor
ProgressBarOnBottom = true ProgressBarOnBottom = true
}); });
// Step 2: Iterate through each subdirectory in order
foreach (string subdirectory in subdirectories) foreach (string subdirectory in subdirectories)
{ {
// Step 3: Process each directory ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar, threadCount, targetExtension, targetFormat, targetQuality);
ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar);
} }
Console.WriteLine("All directories processed successfully."); Console.WriteLine("All directories processed successfully.");
} }
private static void ProcessDirectory(string subdirectory, string sourceImagePath, string targetStoragePath, ShellProgressBar.ProgressBar progressBar)
{
// Get the relative path of the subdirectory
string relativePath = Path.GetRelativePath(sourceImagePath, subdirectory);
// Create the corresponding subdirectory in the target storage path
string targetSubdirectory = Path.Combine(targetStoragePath, relativePath);
Directory.CreateDirectory(targetSubdirectory);
// Get all image files in the subdirectory (jpg and png)
string[] imageFiles = GetImageFiles(subdirectory);
// Set up ParallelOptions to limit the number of concurrent threads
ParallelOptions options = new()
{
MaxDegreeOfParallelism = 2 // Adjust this value to set the number of concurrent threads
};
// Process each image file in parallel
Parallel.ForEach(imageFiles, options, imageFile =>
{
// Set the target file path with the .webp extension
string targetFilePath = Path.Combine(targetSubdirectory, Path.GetFileNameWithoutExtension(imageFile) + ".webp");
// Check if the target file already exists
if (!File.Exists(targetFilePath))
{
CompressImage(imageFile, targetFilePath);
// Update progress bar safely
lock (progressBar)
{
progressBar.Tick($"Processed {Path.GetFileName(imageFile)}");
}
}
else
{
lock (progressBar) { progressBar.Tick($"Skipped {Path.GetFileName(imageFile)}"); }
}
});
}
private static string[] GetImageFiles(string directoryPath)
{
// Get all image files supported by ImageSharp
string[] supportedExtensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tiff"];
List<string> allFiles = [];
foreach (string extension in supportedExtensions)
{
allFiles.AddRange(Directory.GetFiles(directoryPath, extension, SearchOption.TopDirectoryOnly));
}
return [.. allFiles];
}
private static void CompressImage(string sourceFilePath, string targetFilePath)
{
using SixLabors.ImageSharp.Image image = SixLabors.ImageSharp.Image.Load(sourceFilePath);
// Check the longest side of the image and resize if necessary
int maxDimension = Math.Max(image.Width, image.Height);
if (maxDimension > 1200)
{
double scaleFactor = 1200.0 / maxDimension;
int newWidth = (int)(image.Width * scaleFactor);
int newHeight = (int)(image.Height * scaleFactor);
image.Mutate(x => x.Resize(newWidth, newHeight));
}
// Save the image as WebP with a quality level of 85 (for lossy compression)
var encoder = new WebpEncoder
{
Quality = 90,
FileFormat = WebpFileFormatType.Lossy
};
image.Save(targetFilePath, encoder);
}
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
readme.md Normal file
View File

@ -0,0 +1,4 @@
这是一个自用的图像压缩工具
支持对一整个文件夹内的图像进行压缩处理,支持输出webp、avif、jxl(jpeg xl)、jpg、bmp、png等格式
*转换avif性能很差?