Update packages, fix CLI entry point, add parallel within-archive compression (-P)

- Updated 10 NuGet packages: SharpCompress 0.49.1, Magick.NET-Q8-x64 14.14,
  System.Drawing.Common 10.0, itext 9.6, Imazen.WebP 11.0, McMaster 5.1,
  iTextSharp 5.5.13.5, Imageflow 0.15.1, Magick.NET.SystemDrawing 8.0.23
- Fixed missing OnExecuteAsync -- CLI was throwing InvalidOperationException on launch
- Added -w|--new-webp flag (Imageflow encoder path; benchmarked consistently slower)
- Added -P|--par-archive: parallel image compression within each archive
  Batched 3-phase design: sequential read -> Parallel.For (ProcessorCount/2 threads)
  -> sequential write; ordering guaranteed by indexed slots; ~4-5x measured speedup
- Added par_archive to ProcessorFlags
- Added CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 00:48:54 +02:00
parent 7661cf506d
commit 94f3fab41c
3 changed files with 100 additions and 31 deletions
+75
View File
@@ -0,0 +1,75 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Solution Structure
Three projects in `ComicCompress.sln`, all targeting .NET 8.0:
- **ComicCompress** (this directory) — Console application entry point. `Program.cs` handles CLI parsing via `McMaster.Extensions.CommandLineUtils` and orchestrates processing.
- **iMGcompress-core** (`../iMGcompress-core`) — Class library containing all compression logic. Both the CLI and GUI depend on this.
- **iMGcompress** (`../iMGcompress`) — Windows Forms GUI (`net8.0-windows`) wrapping the core library.
## Build & Run
```powershell
# Development build
dotnet build
# Run CLI (from project root)
dotnet run -- --help
dotnet run -- -i <input> -o <output> -q 75 -p
# Publish self-contained Windows executable
dotnet publish -r win-x64 -c Release
```
`launchSettings.json` defines named profiles (e.g. `PROD`, `DEV_HOME`, `Compress Image to webp`) for common invocations.
## CLI Flags
| Flag | Description |
|------|-------------|
| `-i` | Input file or folder (required) |
| `-o` | Output base path (default: `converted_comics`) |
| `-r` | Recurse into subfolders |
| `-s` | Skip if output already exists |
| `-q` | WebP quality 0100 (default: 75) |
| `-p` | Parallel processing (all CPUs) |
| `-c` | Parallel with N CPUs |
| `-d` | PDF-only mode |
| `-j` | Image-only mode |
| `-n` | Zebra PRN mode (generates PNG + WebP Base64 URI) |
| `-a` | Copy unprocessed files to output |
## Architecture
**Data flow:** Input (`.cbr`/`.cbz` archives or loose images) → extract → convert each image to WebP → repack as `.cbz`.
### Core library (`iMGcompress-core`)
- **`Compressor.cs`** — Central engine. Reads RAR/ZIP archives via `SharpCompress`, converts images to WebP via `Imazen.WebP`, and writes output `.cbz`. Handles solid and non-solid archives separately. The first image in an archive (cover) is preserved without conversion.
- **`FileProcessor.cs`** — Orchestrates parallel or sequential processing over a file list. Inner class `ProcessorFlags` carries all options. Writes the final compression report.
- **`FileParser.cs`** — Recursively enumerates input files.
- **`ImageExtractor.cs`** — Implements `IRenderListener` (iText) to extract images from PDF pages and produce a `.cbz`.
- **`PixelMap.cs`** — Parses Portable Bitmap formats (PBM/PGM/PPM, variants P1P6) into `System.Drawing.Bitmap` before WebP encoding.
- **`Logger.cs`** — Thread-safe colored console/file logger. `LogLevel`: Error, Warning, Info, Verbose.
- **`file_stat.cs`** — Simple DTO tracking per-file uncompressed/compressed sizes for statistics.
### Console app (`ComicCompress`)
`Program.cs` parses flags, builds a `ProcessorFlags`, calls `FileParser` to enumerate targets, then hands off to `FileProcessor`. Video (MP4→WebM) conversion uses `Xabe.FFmpeg`. Zebra PRN rendering uses `BinaryKits.Zpl.Viewer`.
## Key Dependencies
| Package | Purpose |
|---------|---------|
| `SharpCompress` | Read/write RAR and ZIP archives |
| `Imazen.WebP` + `libwebp.dll` | WebP encoding (requires `libwebp.dll` in output dir) |
| `Magick.NET-Q8-x64` | Advanced image format support (TIFF, etc.) |
| `iTextSharp` / `itext` | PDF parsing |
| `Xabe.FFmpeg` | Video conversion |
| `BinaryKits.Zpl.Viewer` | Zebra PRN label rendering |
| `McMaster.Extensions.CommandLineUtils` | CLI argument parsing |
**`libwebp.dll` must be present** alongside the executable at runtime — it is checked into the repo root.
+8 -8
View File
@@ -7,7 +7,7 @@
<OutputName>ComicCompress</OutputName> <OutputName>ComicCompress</OutputName>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;ReleaseWDbg</Configurations> <Configurations>Debug;Release;ReleaseWDbg</Configurations>
<Version>1.1.0.0</Version> <Version>3.1.0.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -28,17 +28,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BinaryKits.Zpl.Viewer" Version="1.3.1" /> <PackageReference Include="BinaryKits.Zpl.Viewer" Version="1.3.1" />
<PackageReference Include="Imazen.WebP" Version="10.0.1" /> <PackageReference Include="Imazen.WebP" Version="11.0.0" />
<PackageReference Include="iTextSharp" Version="5.5.13.4" /> <PackageReference Include="iTextSharp" Version="5.5.13.5" />
<PackageReference Include="LibHeifSharp" Version="3.2.0" /> <PackageReference Include="LibHeifSharp" Version="3.2.0" />
<PackageReference Include="Magick.NET-Q8-x64" Version="14.10.0" /> <PackageReference Include="Magick.NET-Q8-x64" Version="14.14.0" />
<PackageReference Include="Magick.NET.SystemDrawing" Version="8.0.13" /> <PackageReference Include="Magick.NET.SystemDrawing" Version="8.0.23" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" /> <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="5.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Pastel" Version="7.0.1" /> <PackageReference Include="Pastel" Version="7.0.1" />
<PackageReference Include="Pdfbox" Version="1.1.1" /> <PackageReference Include="Pdfbox" Version="1.1.1" />
<PackageReference Include="sharpcompress" Version="0.42.1" /> <PackageReference Include="sharpcompress" Version="0.49.1" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" /> <PackageReference Include="System.Drawing.Common" Version="10.0.9" />
<PackageReference Include="Xabe.FFmpeg" Version="6.0.2" /> <PackageReference Include="Xabe.FFmpeg" Version="6.0.2" />
<PackageReference Include="Xabe.FFmpeg.Downloader" Version="6.0.2" /> <PackageReference Include="Xabe.FFmpeg.Downloader" Version="6.0.2" />
</ItemGroup> </ItemGroup>
+17 -23
View File
@@ -30,7 +30,6 @@ using ImageMagick;
using javax.xml.transform; using javax.xml.transform;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using iText.Barcodes.Dmcode; using iText.Barcodes.Dmcode;
using ZstdSharp;
using Xabe.FFmpeg.Downloader; using Xabe.FFmpeg.Downloader;
using System.Reflection; using System.Reflection;
using Microsoft.VisualBasic; using Microsoft.VisualBasic;
@@ -63,7 +62,10 @@ namespace ComicCompressor
//string progVersion = "Version 3.0.0 - (21/11/2024)"; //string progVersion = "Version 3.0.0 - (21/11/2024)";
// In core changed color of percent save to white // In core changed color of percent save to white
string progVersion = "Version 3.0.1 - (23/01/2025)"; //string progVersion = "Version 3.0.1 - (23/01/2025)";
// In core changed color of percent save to white
string progVersion = "Version 3.1.0 - (27/06/2027)";
public static int Main(string[] args) => CommandLineApplication.Execute<Program>(args); public static int Main(string[] args) => CommandLineApplication.Execute<Program>(args);
#region INPUT_FLAGS #region INPUT_FLAGS
@@ -103,6 +105,12 @@ namespace ComicCompressor
[Option("-a|--copyall", Description = "Copy all unprocessed files")] [Option("-a|--copyall", Description = "Copy all unprocessed files")]
public bool DoCopyAll { get; } = false; public bool DoCopyAll { get; } = false;
[Option("-w|--new-webp", Description = "Use Imageflow encoder (faster JPEG→WebP transcoding, no full pixel decode)")]
public bool NewWebp { get; } = false;
[Option("-P|--par-archive", Description = "Parallel image compression within each archive (~4-5x faster, uses half the CPUs)")]
public bool ParArchive { get; } = false;
#endregion #endregion
#region STATISTICS #region STATISTICS
@@ -153,19 +161,18 @@ namespace ComicCompressor
new_webp: false new_webp: false
); );
private void OnExecute() private async Task OnExecuteAsync()
{ {
File_statistics.Clear(); File_statistics.Clear();
flags.File_statistics = File_statistics; flags.New_webp = NewWebp;
flags.Par_archive = ParArchive;
myColors = new Color[names.Count()]; myColors = new Color[names.Count()];
for(int i = 0; i< myColors.Length; i++) for (int i = 0; i < myColors.Length; i++)
{ {
KnownColor randomColorName = names[randomGen.Next(names.Length)]; KnownColor randomColorName = names[randomGen.Next(names.Length)];
myColors[i] = Color.FromKnownColor(randomColorName); myColors[i] = Color.FromKnownColor(randomColorName);
} }
//ImageExtractor7.MainBox();
Stopwatch sw = Stopwatch.StartNew(); Stopwatch sw = Stopwatch.StartNew();
@@ -175,28 +182,15 @@ namespace ComicCompressor
Logger.Log(progVersion, LogLevel.Verbose, true); Logger.Log(progVersion, LogLevel.Verbose, true);
//var progress = new Progress<ProgressInfo>( p => Logger.Log(p.ToString(), LogLevel.Verbose, true) );
//FFmpeg.SetExecutablesPath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location));
//FFmpegDownloader.GetLatestVersion(FFmpegVersion.Official,progress);
//==//FFmpeg.SetExecutablesPath (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FFmpeg"));
//Logger.Log(FFmpeg.ExecutablesPath, LogLevel.Verbose, true);
IList<string> files = new FileParser().ListAllFiles(Input, Recursive); IList<string> files = new FileParser().ListAllFiles(Input, Recursive);
Process(files); await Process(files);
long time_query = sw.ElapsedMilliseconds; long time_query = sw.ElapsedMilliseconds;
Logger.Log($"ElapsedTime {time_query} ms", LogLevel.Verbose, true); Logger.Log($"ElapsedTime {time_query} ms", LogLevel.Verbose, true);
Console.ReadKey();
} }
private async void Process(IList<string> files) private async Task Process(IList<string> files)
{ {
Compressor compressor = new Compressor(Logger); Compressor compressor = new Compressor(Logger);
compressor.Quality = Quality; compressor.Quality = Quality;