554 lines
21 KiB
C#
554 lines
21 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Threading.Tasks;
|
||
using System.Collections.Generic;
|
||
using McMaster.Extensions.CommandLineUtils;
|
||
using System.ComponentModel.DataAnnotations;
|
||
using System.Diagnostics;
|
||
using System.Threading;
|
||
using System.Drawing.Imaging;
|
||
using System.Drawing;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
|
||
using GroupDocs.Parser;
|
||
using GroupDocs.Parser.Data;
|
||
using iText.Kernel.Pdf;
|
||
|
||
using Imazen.WebP;
|
||
|
||
using SharpCompress.Archives;
|
||
using SharpCompress.Archives.Zip;
|
||
using SharpCompress.Common;
|
||
using itext.pdfimage.Extensions;
|
||
using org.omg.PortableInterceptor;
|
||
using org.apache.pdfbox.util.@operator;
|
||
using java.nio;
|
||
using ImageMagick;
|
||
|
||
namespace ComicCompressor
|
||
{
|
||
public class Program
|
||
{
|
||
public static int Main(string[] args) => CommandLineApplication.Execute<Program>(args);
|
||
|
||
[FileOrDirectoryExists]
|
||
[Option("-i|--input <FOLDER/FIlE>", Description = "The file or folder to convert")]
|
||
[Required]
|
||
public string Input { get; }
|
||
|
||
[Option("-o|--output <FOLDER>",
|
||
Description =
|
||
"Base path of the output (will be in output/subfolders if the recursive option is enabled), default: converted_comics")]
|
||
public string OutputFolder { get; } = "converted_comics";
|
||
|
||
[Option("-r|--recursive", Description = "Recursively traverse the input folder (include all subfolders)")]
|
||
public bool Recursive { get; } = false;
|
||
|
||
[Option("-s|--skip", Description = "Skip processing file if it already exists in the output folder")]
|
||
public bool Skip { get; } = false;
|
||
|
||
[Option("-q|--quality", Description = "Quality to use for the webp files (default: 75)")]
|
||
public int Quality { get; } = 75;
|
||
|
||
[Option("-p|--parallel", Description = "Run in parallel, utilizing all computing resources")]
|
||
public bool IsParallel { get; } = false;
|
||
|
||
[Option("-c|--cpu", Description = "Run in parallel, utilizing given computing resources")]
|
||
public int NumOfCpu { get; } = Environment.ProcessorCount;
|
||
|
||
|
||
[Option("-d|--pdf", Description = "process pdf only")]
|
||
public bool DoPdf { get; } = false;
|
||
|
||
[Option("-j|--image", Description = "process images only")]
|
||
public bool DoImage { get; } = false;
|
||
|
||
private Logger Logger { get; set; }
|
||
|
||
SimpleEncoder encoder = new SimpleEncoder();
|
||
private List<Stream> imageStreams = null;
|
||
public ZipArchive output = null;
|
||
private void OnExecute()
|
||
{
|
||
|
||
//ImageExtractor7.MainBox();
|
||
|
||
Stopwatch sw = Stopwatch.StartNew();
|
||
|
||
Logger = new Logger();
|
||
Logger.Debug = true;
|
||
Logger.LoggingLevel = LogLevel.Verbose;
|
||
|
||
IList<string> files = new FileParser().ListAllFiles(Input, Recursive);
|
||
|
||
Process(files);
|
||
|
||
long time_query = sw.ElapsedMilliseconds;
|
||
Logger.Log($"ElapsedTime {time_query} ms", LogLevel.Verbose, true);
|
||
|
||
Console.ReadKey();
|
||
|
||
}
|
||
|
||
private void Process(IList<string> files)
|
||
{
|
||
Compressor compressor = new Compressor(Logger);
|
||
compressor.Quality = Quality;
|
||
int filename_len = 0;
|
||
|
||
foreach (var file in files)
|
||
{
|
||
if(file.Length > filename_len)
|
||
filename_len = file.Length;
|
||
}
|
||
|
||
|
||
if (!IsParallel)
|
||
{
|
||
int n = 1;
|
||
|
||
foreach (var file in files)
|
||
{
|
||
Logger.Log($"{n,3}/{files.Count,3} - ", LogLevel.Verbose, false);
|
||
CompressTask(compressor, file, filename_len);
|
||
n++;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
ParallelOptions options = new ParallelOptions
|
||
{
|
||
MaxDegreeOfParallelism = (NumOfCpu > Environment.ProcessorCount) ? Environment.ProcessorCount : NumOfCpu
|
||
};
|
||
|
||
Parallel.ForEach(files, options, file => CompressTask(compressor, file, filename_len, true));
|
||
|
||
}
|
||
|
||
private void CompressTask(Compressor compressor, string filename, int max_name_len = -1, Boolean isPar = false)
|
||
{
|
||
var relativePath = Path.GetRelativePath(Input, filename);
|
||
var outputPath = Path.Join(OutputFolder, Path.ChangeExtension(relativePath, "cbz"));
|
||
var outputPath_webp = Path.Join(OutputFolder, Path.ChangeExtension(relativePath, "webp"));
|
||
|
||
if (File.Exists(outputPath) && Skip)
|
||
{
|
||
Logger.Log(outputPath, LogLevel.Verbose, true, Color.Yellow);
|
||
return;
|
||
}
|
||
if (DoImage && Skip && File.Exists(outputPath_webp))
|
||
{
|
||
Logger.Log(outputPath_webp, LogLevel.Verbose, true, Color.Yellow);
|
||
return;
|
||
}
|
||
|
||
if (DoPdf && filename.ToLower().EndsWith(".pdf"))
|
||
{
|
||
ExtractImageFromPDF(filename, outputPath);
|
||
//compressor.CompressPdf(filename, outputPath);
|
||
return;
|
||
}
|
||
|
||
if (DoPdf)
|
||
return;
|
||
|
||
if (DoImage &&
|
||
filename.ToLower().EndsWith("jpg") ||
|
||
filename.ToLower().EndsWith("jpeg") ||
|
||
filename.ToLower().EndsWith("tif") ||
|
||
filename.ToLower().EndsWith("png"))
|
||
{
|
||
outputPath = Path.Join(OutputFolder, Path.ChangeExtension(relativePath, "webp"));
|
||
CompressImage(filename, outputPath, max_name_len,isPar);
|
||
return;
|
||
}
|
||
|
||
//if (DoImage &&
|
||
// filename.ToLower().EndsWith("psd"))
|
||
//{
|
||
// outputPath = Path.Join(OutputFolder, Path.ChangeExtension(relativePath, "webp"));
|
||
// CompressImagePSD(filename, outputPath);
|
||
// return;
|
||
//}
|
||
|
||
if (!filename.ToLower().EndsWith(".cbr") && !filename.ToLower().EndsWith(".cbz"))
|
||
{
|
||
Logger.Log(relativePath, LogLevel.Verbose, true, Color.LightYellow);
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
compressor.Compress(filename, outputPath);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Logger.Log("Error processing file: " + filename, LogLevel.Error, Color.Red);
|
||
Logger.LogDebug(e, LogLevel.Error, col:Color.Red);
|
||
}
|
||
}
|
||
|
||
#region COMPRESS_PDF
|
||
|
||
#if false
|
||
public static string ExtractImageFromPDF(string sourcePdf)
|
||
{
|
||
iText.Kernel.Pdf.PdfReader reader = new iText.Kernel.Pdf.PdfReader(sourcePdf);
|
||
try
|
||
{
|
||
iText.Kernel.Pdf.PdfDocument document = new iText.Kernel.Pdf.PdfDocument(reader);
|
||
|
||
for (int pageNumber
|
||
= 1; pageNumber <= document.GetNumberOfPages(); pageNumber++)
|
||
{
|
||
iText.Kernel.Pdf.PdfDictionary obj =
|
||
(iText.Kernel.Pdf.PdfDictionary)document.GetPdfObject(pageNumber);
|
||
|
||
if (obj != null && obj.IsStream())
|
||
{
|
||
iText.Kernel.Pdf.PdfDictionary pd
|
||
= (iText.Kernel.Pdf.PdfDictionary)obj;
|
||
if (pd.ContainsKey(iText.Kernel.Pdf.PdfName.Subtype) && pd.Get(iText.Kernel.Pdf.PdfName.Subtype).ToString() == "/Image")
|
||
{
|
||
string filter
|
||
= pd.Get(iText.Kernel.Pdf.PdfName.Filter).ToString();
|
||
string width
|
||
= pd.Get(iText.Kernel.Pdf.PdfName.Width).ToString();
|
||
string height
|
||
= pd.Get(iText.Kernel.Pdf.PdfName.Height).ToString();
|
||
string bpp
|
||
= pd.Get(iText.Kernel.Pdf.PdfName.BitsPerComponent).ToString();
|
||
string extent
|
||
= ".";
|
||
byte[] img
|
||
= pd.Get(iText.Kernel.Pdf.PdfName.Image);
|
||
Debug.WriteLine(filter);
|
||
continue;
|
||
switch (filter)
|
||
{
|
||
case "/FlateDecode":
|
||
byte[] arr
|
||
= FlateDecodeFilter.FlateDecode(img, true);
|
||
Bitmap bmp
|
||
= new Bitmap(Int32.Parse(width), Int32.Parse(height), PixelFormat.Format24bppRgb);
|
||
BitmapData bmd
|
||
= bmp.LockBits(new Rectangle(0, 0, Int32.Parse(width), Int32.Parse(height)), ImageLockMode.WriteOnly,
|
||
PixelFormat.Format24bppRgb);
|
||
Marshal.Copy(arr, 0, bmd.Scan0, arr.Length);
|
||
bmp.UnlockBits(bmd);
|
||
bmp.Save("d:\\pdf\\bmp1.png", ImageFormat.Png);
|
||
break;
|
||
case "/CCITTFaxDecode":
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
throw;
|
||
}
|
||
return "";
|
||
}
|
||
#elif false
|
||
public static string ExtractImageFromPDF(string sourcePdf)
|
||
{
|
||
// Create an instance of Parser class
|
||
using (Parser parser = new Parser(sourcePdf))
|
||
{
|
||
// Extract images
|
||
var di = parser.GetDocumentInfo();
|
||
|
||
for (int p = 0; p < di.PageCount; p++)
|
||
{
|
||
|
||
IEnumerable<PageImageArea> images = parser.GetImages(p);
|
||
//IEnumerable<PageImageArea> images = parser.GetImages();
|
||
// Check if image extraction is supported
|
||
if (images == null)
|
||
{
|
||
Console.WriteLine("Images extraction isn’t supported");
|
||
return "";
|
||
}
|
||
|
||
int counter = 1;
|
||
// Iterate over images
|
||
foreach (PageImageArea image in images)
|
||
{
|
||
// Save each image
|
||
Image.FromStream(image.GetImageStream())
|
||
.Save(string.Format($"{p.ToString("0000")}_{counter++.ToString("0000")}.jpg"), ImageFormat.Jpeg);
|
||
}
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
#else
|
||
public int GetPlace(int value, int place)
|
||
{
|
||
return ((value % (place * 10)) - (value % place)) / place;
|
||
}
|
||
private bool Cover = true;
|
||
int _currentPage = 1;
|
||
int _imageCount = 0;
|
||
public void ExtractImageFromPDF(string sourcePdf, string outputFolder)
|
||
{
|
||
|
||
long input_length = new System.IO.FileInfo(sourcePdf).Length;
|
||
var pdf = File.Open(sourcePdf, FileMode.Open);
|
||
|
||
var reader = new PdfReader(pdf);
|
||
var pdfDocument = new PdfDocument(reader);
|
||
int numOfPages = pdfDocument.GetNumberOfPages();
|
||
//var bitmaps = pdfDocument.ConvertToBitmaps();
|
||
|
||
Logger.Log($"Processing[{numOfPages}]: " + outputFolder, LogLevel.Verbose);
|
||
|
||
output = ZipArchive.Create();
|
||
imageStreams = new List<Stream>();
|
||
int count = 0;
|
||
Cover = true;
|
||
Stopwatch sw = Stopwatch.StartNew();
|
||
for (int p = 1; p <= numOfPages; p++)
|
||
{
|
||
PdfPage page = pdfDocument.GetPage(p);
|
||
//var bitmap = page.ConvertPageToBitmap();
|
||
MemoryStream jpg = (MemoryStream)page.ConvertPageToJpg();
|
||
|
||
//bitmap.Save(Path.Combine($"wave-{DateTime.Now.Ticks}.png"), ImageFormat.Png);
|
||
ProcessPdfImage(jpg);
|
||
//jpg.Dispose();
|
||
//page.Flush();
|
||
|
||
_currentPage++;
|
||
count++;
|
||
if (count % 10 == 0)
|
||
{
|
||
string digit = GetPlace(count, 10).ToString();
|
||
Logger.Log(digit, LogLevel.Verbose, false);
|
||
}
|
||
}
|
||
Directory.CreateDirectory(Path.GetDirectoryName(outputFolder));
|
||
|
||
output.SaveTo(outputFolder, CompressionType.Deflate);
|
||
|
||
//output.SaveTo(outputPath, CompressionType.None);
|
||
|
||
imageStreams.ForEach(i => i.Dispose());
|
||
|
||
output.Dispose();
|
||
|
||
long output_length = new System.IO.FileInfo(outputFolder).Length;
|
||
|
||
double save = 100 * (double)output_length / (double)input_length;
|
||
if (numOfPages == 0) numOfPages = 1;
|
||
long elapsed = sw.ElapsedMilliseconds;
|
||
Logger.Log($"\r\nFinished({save.ToString("0.##")}) [{elapsed}][{elapsed / numOfPages}]: " + outputFolder, LogLevel.Verbose);
|
||
|
||
}
|
||
private Bitmap ChangePixelFormat(Bitmap orig, System.Drawing.Imaging.PixelFormat format = System.Drawing.Imaging.PixelFormat.Format24bppRgb)
|
||
{
|
||
Bitmap clone = new Bitmap(orig.Width, orig.Height, format);
|
||
|
||
using (Graphics gr = Graphics.FromImage(clone))
|
||
{
|
||
gr.DrawImage(orig, new System.Drawing.Rectangle(0, 0, clone.Width, clone.Height));
|
||
}
|
||
|
||
return clone;
|
||
|
||
}
|
||
|
||
public static byte[] ImageToByte(Image img)
|
||
{
|
||
ImageConverter converter = new ImageConverter();
|
||
return (byte[])converter.ConvertTo(img, typeof(byte[]));
|
||
}
|
||
public void ProcessPdfImage(MemoryStream jpg)
|
||
{
|
||
Stopwatch sw = Stopwatch.StartNew();
|
||
try
|
||
{
|
||
var image = new Bitmap(jpg);
|
||
//var imageFileName = String.Format("{0}_{1}_{2}.{3}", _outputFilePrefix, _currentPage, _imageCount,
|
||
// imageObject.GetFileType());
|
||
var imageFileName = String.Format($"{_imageCount.ToString("0000")}");
|
||
//var imagePath = Path.Combine(_outputFolder, imageFileName);
|
||
|
||
//if (_overwriteExistingFiles || !File.Exists(imagePath))
|
||
{
|
||
//Logger.Log($"0 [{image.Width}][{image.Height}]" + sw.ElapsedMilliseconds.ToString(), LogLevel.Verbose);
|
||
|
||
//var imageRawBytes = ImageToByte(image);
|
||
//var image = imageObject.GetDrawingImage();
|
||
|
||
//Logger.Log("1 "+sw.ElapsedMilliseconds.ToString(), LogLevel.Verbose);
|
||
|
||
//File.WriteAllBytes(imagePath, imageRawBytes);
|
||
if (image.Width < 512 || image.Height < 512)
|
||
return;
|
||
|
||
if (image.PixelFormat != System.Drawing.Imaging.PixelFormat.Format24bppRgb)
|
||
{
|
||
var newBits = ChangePixelFormat((Bitmap)image);
|
||
image.Dispose();
|
||
image = newBits;
|
||
}
|
||
//Logger.Log("2 " + sw.ElapsedMilliseconds.ToString(), LogLevel.Verbose);
|
||
|
||
var ms = new MemoryStream();
|
||
encoder.Encode((Bitmap)image, ms, Quality);
|
||
//Logger.Log("3 " + sw.ElapsedMilliseconds.ToString(), LogLevel.Verbose);
|
||
|
||
if (!Cover && ms.Length < jpg.Length)
|
||
{
|
||
imageStreams.Add(ms);
|
||
output.AddEntry(imageFileName + ".webp", ms);
|
||
Logger.Log(".", LogLevel.Verbose, false);
|
||
//Logger.Log("4 " + sw.ElapsedMilliseconds.ToString(), LogLevel.Verbose);
|
||
|
||
}
|
||
else
|
||
{
|
||
//var mso = new MemoryStream();
|
||
//mso.Write(imageRawBytes);
|
||
// output.AddEntry(imageFileName + imageObject.GetFileType(), mso);
|
||
imageStreams.Add(ms);
|
||
output.AddEntry(imageFileName + ".jpg", jpg);
|
||
Logger.Log(Cover ? "C" : "=", LogLevel.Verbose, false);
|
||
Cover = false;
|
||
}
|
||
}
|
||
|
||
// Subtle: Always increment even if file is not written. This ensures consistency should only some
|
||
// of a PDF file's images actually exist.
|
||
_imageCount++;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Console.WriteLine(e);
|
||
throw;
|
||
}
|
||
|
||
//Logger.Log(sw.ElapsedMilliseconds.ToString(), LogLevel.Verbose);
|
||
}
|
||
#endif
|
||
|
||
#endregion
|
||
|
||
#region COMPRESS_IMAGE
|
||
public void CompressImage(string imagefile, string outputFolder, int max_name_len = -1, Boolean isPar = false)
|
||
{
|
||
string name_print = imagefile;
|
||
if(max_name_len > 0)
|
||
name_print = imagefile.PadRight(max_name_len);
|
||
if(!isPar)
|
||
Logger.Log(name_print, LogLevel.Verbose, false, col:Color.LightBlue);
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(Path.GetDirectoryName(outputFolder));
|
||
|
||
var encoder = new SimpleEncoder();
|
||
|
||
Bitmap mBitmap;
|
||
long length_in = 0;
|
||
FileStream outStream = new FileStream(outputFolder, FileMode.Create);
|
||
using (Stream BitmapStream = System.IO.File.Open(imagefile, System.IO.FileMode.Open))
|
||
{
|
||
length_in = BitmapStream.Length;
|
||
Image img = Image.FromStream(BitmapStream);
|
||
|
||
mBitmap = new Bitmap(img);
|
||
|
||
IntPtr result;
|
||
long length = 0;
|
||
encoder.Encode(mBitmap, outStream, 70);
|
||
|
||
outStream.Flush();
|
||
outStream.Close();
|
||
//encoder.Encode(mBitmap, 70, out result, out length);
|
||
//UnmanagedMemoryStream ustream = new UnmanagedMemoryStream((byte*)result, length);
|
||
//ustream.CopyTo(outStream);
|
||
//ustream.Close();
|
||
//outStrea.Close();
|
||
}
|
||
|
||
FileInfo finfo = new FileInfo(outputFolder);
|
||
long length_out = finfo.Length;
|
||
if (isPar)
|
||
Logger.Log($"{name_print} - IN[{length_in,10}] OUT[{length_out,10}] %[{(100.0 * length_out / (double)length_in),5:##.00}]", LogLevel.Verbose, true,col:Color.Green);
|
||
else
|
||
Logger.Log($" - IN[{length_in,10}] OUT[{length_out,10}] %[{(100.0 * length_out/(double)length_in),5:##.00}]", LogLevel.Verbose, true, col:Color.LightBlue);
|
||
}
|
||
catch(Exception e)
|
||
{
|
||
Logger.LogError(e, col: Color.Red);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
public void CompressImagePSD(string imagefile, string outputFolder)
|
||
{
|
||
Logger.Log(imagefile, LogLevel.Verbose, false);
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(Path.GetDirectoryName(outputFolder));
|
||
|
||
var encoder = new SimpleEncoder();
|
||
|
||
Bitmap mBitmap;
|
||
long length_in = 0;
|
||
FileStream outStream = new FileStream(outputFolder, FileMode.Create);
|
||
using (Stream BitmapStream = System.IO.File.Open(imagefile, System.IO.FileMode.Open))
|
||
{
|
||
length_in = BitmapStream.Length;
|
||
|
||
//using var imageFromFile = new MagickImage(BitmapStream);
|
||
using (MagickImageCollection magickImages = new MagickImageCollection(BitmapStream))
|
||
{
|
||
foreach (var image in magickImages)
|
||
{
|
||
Image img = image.ToBitmap();
|
||
|
||
|
||
//Image img = Image.FromStream(BitmapStream);
|
||
//Image img = imageFromFile.ToBitmap();
|
||
mBitmap = new Bitmap(img);
|
||
|
||
IntPtr result;
|
||
long length = 0;
|
||
encoder.Encode(mBitmap, outStream, 70);
|
||
}
|
||
}
|
||
|
||
outStream.Flush();
|
||
outStream.Close();
|
||
//encoder.Encode(mBitmap, 70, out result, out length);
|
||
//UnmanagedMemoryStream ustream = new UnmanagedMemoryStream((byte*)result, length);
|
||
//ustream.CopyTo(outStream);
|
||
//ustream.Close();
|
||
//outStrea.Close();
|
||
}
|
||
|
||
FileInfo finfo = new FileInfo(outputFolder);
|
||
long length_out = finfo.Length;
|
||
Logger.Log($" - IN[{length_in}] OUT[{length_out}] %[{100.0 * length_out / (double)length_in}]", LogLevel.Verbose, true);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Logger.LogError(e);
|
||
}
|
||
|
||
}
|
||
#endregion
|
||
}
|
||
}
|