diff --git a/Comic_Compressor/AvifCompressor.cs b/Comic_Compressor/AvifCompressor.cs index 68c4cff..b46d270 100644 --- a/Comic_Compressor/AvifCompressor.cs +++ b/Comic_Compressor/AvifCompressor.cs @@ -1,8 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using LibHeifSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using ShellProgressBar; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; namespace Comic_Compressor { @@ -10,8 +11,319 @@ namespace Comic_Compressor { internal static void CompressImages(string sourceImagePath, string targetStoragePath) { - //尚未实现 - throw new NotImplementedException(); + LibHeifSharpDllImportResolver.Register(); + // Step 1: Get all subdirectories and store them in a list + List 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 + }); + + // Step 2: Iterate through each subdirectory in order + foreach (string subdirectory in subdirectories) + { + // Step 3: Process each directory + ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar); + } + + 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 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(); + + //// 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 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 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 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 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 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; + } + } } diff --git a/Comic_Compressor/Comic_Compressor.csproj b/Comic_Compressor/Comic_Compressor.csproj index b3aedb2..0ddd65d 100644 --- a/Comic_Compressor/Comic_Compressor.csproj +++ b/Comic_Compressor/Comic_Compressor.csproj @@ -7,11 +7,46 @@ enable AnyCPU;x64 True + true + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Never + + + Never + + + Never + + + Never + + + Never + + + PreserveNewest + + + diff --git a/Comic_Compressor/LibHeifSharpDllImportResolver.cs b/Comic_Compressor/LibHeifSharpDllImportResolver.cs new file mode 100644 index 0000000..e1960ff --- /dev/null +++ b/Comic_Compressor/LibHeifSharpDllImportResolver.cs @@ -0,0 +1,105 @@ +/* + * 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; + + /// + /// Registers the for the LibHeifSharp assembly. + /// + 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); + } + } + } +} diff --git a/Comic_Compressor/WebpCompressor.cs b/Comic_Compressor/WebpCompressor.cs index 7eee2c0..7c669e2 100644 --- a/Comic_Compressor/WebpCompressor.cs +++ b/Comic_Compressor/WebpCompressor.cs @@ -1,6 +1,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Processing; +using ShellProgressBar; namespace Comic_Compressor { @@ -11,15 +12,29 @@ namespace Comic_Compressor // Step 1: Get all subdirectories and store them in a list List 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 + }); + // Step 2: Iterate through each subdirectory in order foreach (string subdirectory in subdirectories) { // Step 3: Process each directory - ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath); + ProcessDirectory(subdirectory, sourceImagePath, targetStoragePath, progressBar); } + + Console.WriteLine("All directories processed successfully."); } - private static void ProcessDirectory(string subdirectory, string sourceImagePath, string targetStoragePath) + 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); @@ -31,15 +46,34 @@ namespace Comic_Compressor // Get all image files in the subdirectory (jpg and png) string[] imageFiles = GetImageFiles(subdirectory); - // Iterate through each image file - foreach (string imageFile in imageFiles) + // 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"); - CompressImage(imageFile, targetFilePath); - } - Console.WriteLine($"{Path.GetFileName(subdirectory)} processed successfully."); + // 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) @@ -69,10 +103,10 @@ namespace Comic_Compressor image.Mutate(x => x.Resize(newWidth, newHeight)); } - // Save the image as WebP with a quality level of 70 (for lossy compression) + // Save the image as WebP with a quality level of 85 (for lossy compression) var encoder = new WebpEncoder { - Quality = 70, + Quality = 90, FileFormat = WebpFileFormatType.Lossy }; diff --git a/Comic_Compressor/aom.dll b/Comic_Compressor/aom.dll new file mode 100644 index 0000000..e69a1e5 Binary files /dev/null and b/Comic_Compressor/aom.dll differ diff --git a/Comic_Compressor/dav1d.dll b/Comic_Compressor/dav1d.dll new file mode 100644 index 0000000..c313084 Binary files /dev/null and b/Comic_Compressor/dav1d.dll differ diff --git a/Comic_Compressor/libde265.dll b/Comic_Compressor/libde265.dll new file mode 100644 index 0000000..d557151 Binary files /dev/null and b/Comic_Compressor/libde265.dll differ diff --git a/Comic_Compressor/libheif.dll b/Comic_Compressor/libheif.dll new file mode 100644 index 0000000..e8068fe Binary files /dev/null and b/Comic_Compressor/libheif.dll differ diff --git a/Comic_Compressor/libx265.dll b/Comic_Compressor/libx265.dll new file mode 100644 index 0000000..6992a31 Binary files /dev/null and b/Comic_Compressor/libx265.dll differ