Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
d16467ea9e | |||
d86f91f6c7 | |||
62cc08ca70 | |||
4e3c9aa311 | |||
f03cce27f5 | |||
815d74c09e | |||
9d150d77a1 | |||
c3837e6a87 | |||
b1a7268288 | |||
a3e953b4e7 | |||
35d6a907f6 | |||
8b6a554591 |
@ -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
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
using LibHeifSharp;
|
using ImageMagick;
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
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, int threadCount)
|
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;
|
||||||
@ -26,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, threadCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, int threadCount)
|
|
||||||
{
|
|
||||||
// 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 = threadCount // 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", "*.gif"];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
36
Comic_Compressor/JxlCompressor.cs
Normal file
36
Comic_Compressor/JxlCompressor.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
Comic_Compressor/LegacyFormatCompressor.cs
Normal file
51
Comic_Compressor/LegacyFormatCompressor.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
32
Comic_Compressor/MixProcessor.cs
Normal file
32
Comic_Compressor/MixProcessor.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,18 +6,19 @@ 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("未选择文件夹,程序将退出。");
|
||||||
@ -27,74 +27,65 @@ namespace Comic_Compressor
|
|||||||
|
|
||||||
Console.WriteLine("处理线程数:");
|
Console.WriteLine("处理线程数:");
|
||||||
int threadCount = int.Parse(Console.ReadLine() ?? "2");
|
int threadCount = int.Parse(Console.ReadLine() ?? "2");
|
||||||
Console.WriteLine($"处理线程数设定:{threadCount}");
|
if (threadCount < 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine("无效线程数");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("请选择压缩模式:0 - 压缩成webp,1 - 压缩成avif");
|
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, threadCount);
|
WebpCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality);
|
||||||
GetCompressorResult(sourceImagePath, targetStoragePath);
|
|
||||||
break;
|
break;
|
||||||
case "1":
|
case "1":
|
||||||
AvifCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount);
|
AvifCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount, usePresetQuality, targetQuality);
|
||||||
GetCompressorResult(sourceImagePath, targetStoragePath);
|
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;
|
||||||
|
}
|
||||||
|
Utils.GetCompressorResult(sourceImagePath, targetStoragePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void GetCompressorResult(string source, string target)
|
|
||||||
{
|
|
||||||
long sourceSize = GetDirectorySize(source);
|
|
||||||
long targetSize = GetDirectorySize(target);
|
|
||||||
double reduced = (sourceSize - targetSize) * 1.0 / sourceSize;
|
|
||||||
|
|
||||||
Console.WriteLine($"源目录大小:{sourceSize} 字节");
|
|
||||||
Console.WriteLine($"目标目录大小:{targetSize} 字节");
|
|
||||||
Console.WriteLine($"已减少:{reduced:P}的体积");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
188
Comic_Compressor/Utils.cs
Normal file
188
Comic_Compressor/Utils.cs
Normal 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]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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, int threadCount)
|
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, threadCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, int threadCount)
|
|
||||||
{
|
|
||||||
// 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 = threadCount // 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", "*.gif"];
|
|
||||||
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.
Loading…
Reference in New Issue
Block a user