Use Magick instead of ImageSharp

Support more formats and output in original format
(2/10)
This commit is contained in:
Chenx221 2024-09-10 15:46:53 +08:00
parent 35d6a907f6
commit a3e953b4e7
8 changed files with 197 additions and 820 deletions

View File

@ -1,17 +1,17 @@
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)
{ {
LibHeifSharpDllImportResolver.Register(); //config
// Step 1: Get all subdirectories and store them in a list MagickFormat targetFormat = MagickFormat.Avif;
string targetExtension = ".avif";
int targetQuality = 80;
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 +26,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;
}
} }
} }

View File

@ -1,15 +1,17 @@
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 JxlCompressor internal class JxlCompressor : Utils
{ {
internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount) internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount)
{ {
// Step 1: Get all subdirectories and store them in a list //config
MagickFormat targetFormat = MagickFormat.Jxl;
string targetExtension = ".jxl";
int targetQuality = 90;
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 +26,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);
}
} }
} }

View File

@ -1,15 +1,19 @@
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 LegacyFormatCompressor //Process images in legacy format(JPG,PNG)
internal class LegacyFormatCompressor : Utils
{ {
internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, int targetFormat) internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, int format)
{ {
// Step 1: Get all subdirectories and store them in a list //check format
//throw new NotImplementedException();
//config
MagickFormat targetFormat = MagickFormat.Jxl;
string targetExtension = ".jxl";
int targetQuality = 90;
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 +28,13 @@ 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);
}
} }
} }

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

@ -1,10 +1,17 @@
namespace Comic_Compressor using ImageMagick;
using ShellProgressBar;
namespace Comic_Compressor
{ {
internal class MixProcessor internal class MixProcessor : Utils
{ {
internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, int targetFormat) internal static void CompressImages(string sourceImagePath, string targetStoragePath, int threadCount, int format)
{ {
// Step 1: Get all subdirectories and store them in a list //detect mu-config
//config
MagickFormat targetFormat = MagickFormat.Jxl;
string targetExtension = ".jxl";
int targetQuality = 90;
List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories)); List<string> subdirectories = new(Directory.GetDirectories(sourceImagePath, "*", SearchOption.AllDirectories));
int totalFiles = 0; int totalFiles = 0;
@ -19,93 +26,12 @@
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);
}
} }
} }

View File

@ -10,7 +10,7 @@ namespace Comic_Compressor
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("未选择文件夹,程序将退出。");
@ -18,7 +18,7 @@ namespace Comic_Compressor
} }
Console.WriteLine("请选择保存位置:"); Console.WriteLine("请选择保存位置:");
string? targetStoragePath = GetFolderPath(); string? targetStoragePath = Utils.GetFolderPath();
if (string.IsNullOrEmpty(targetStoragePath)) if (string.IsNullOrEmpty(targetStoragePath))
{ {
Console.WriteLine("未选择文件夹,程序将退出。"); Console.WriteLine("未选择文件夹,程序将退出。");
@ -29,11 +29,11 @@ namespace Comic_Compressor
int threadCount = int.Parse(Console.ReadLine() ?? "2"); int threadCount = int.Parse(Console.ReadLine() ?? "2");
Console.WriteLine($"处理线程数设定:{threadCount}"); Console.WriteLine($"处理线程数设定:{threadCount}");
Console.WriteLine("目标格式0 - webp, 1 - avif, 2 - JXL(JPEG-XL), 3 - JPG, 4 - PNG, 5 - 保留原格式(best effort)"); Console.WriteLine("目标格式0 - webp, 1 - avif, 2 - JXL(JPEG-XL), 3 - JPG, 4 - PNG, 5 - BMP, 6 - 保留原格式(best effort)");
string? modeInput = Console.ReadLine(); string? modeInput = Console.ReadLine();
if (modeInput == null) if (modeInput == null)
{ {
Console.WriteLine("无效输入"); Console.WriteLine("无效格式");
return; return;
} }
@ -41,85 +41,28 @@ namespace Comic_Compressor
{ {
case "0": case "0":
WebpCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount); WebpCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount);
GetCompressorResult(sourceImagePath, targetStoragePath); Utils.GetCompressorResult(sourceImagePath, targetStoragePath);
break; break;
case "1": case "1":
AvifCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount); AvifCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount);
GetCompressorResult(sourceImagePath, targetStoragePath); Utils.GetCompressorResult(sourceImagePath, targetStoragePath);
break; break;
case "2": case "2":
JxlCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount); JxlCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount);
GetCompressorResult(sourceImagePath, targetStoragePath); Utils.GetCompressorResult(sourceImagePath, targetStoragePath);
break; break;
case "3": case "3":
case "4": case "4":
LegacyFormatCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount,int.Parse(modeInput));
GetCompressorResult(sourceImagePath, targetStoragePath);
break;
case "5": case "5":
LegacyFormatCompressor.CompressImages(sourceImagePath, targetStoragePath, threadCount,int.Parse(modeInput));
Utils.GetCompressorResult(sourceImagePath, targetStoragePath);
break;
case "6":
throw new NotImplementedException(); throw new NotImplementedException();
default: default:
Console.WriteLine("不支持的格式"); Console.WriteLine("不支持的格式");
break; break;
} }
} }
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 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 = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
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($"压缩前大小:{GetHumanReadableSize(sourceSize)}");
Console.WriteLine($"压缩后大小:{GetHumanReadableSize(targetSize)}");
Console.WriteLine($"体积已减少{reduced:P}");
}
} }
} }

138
Comic_Compressor/Utils.cs Normal file
View File

@ -0,0 +1,138 @@
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", "*.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)}"); }
}
});
}
// 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,17 @@
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)
{ {
// Step 1: Get all subdirectories and store them in a list //config
MagickFormat targetFormat = MagickFormat.WebP;
string targetExtension = ".webp";
int targetQuality = 90;
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 +26,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);
}
} }
} }