using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using Microsoft.Win32; using PdfSharpCore.Drawing; using PdfSharpCore.Pdf; using PdfSharpCore.Pdf.IO; using MmdPdf.Services; namespace MmdPdf { // Import images as a PDF, and compress the current PDF to a .zip. Kept in its own partial-class // file rather than the MainWindow monolith. User-facing strings go through Loc() (keys live in // Strings/*.xaml) so the feature is fully localized. public partial class MainWindow { // ----- Import images as a single PDF ------------------------------------------------- private void ImportImages_Click(object sender, RoutedEventArgs e) { var dlg = new Controls.FileDialog(Controls.FileDialogMode.Open) { Title = Loc("Str_Menu_Import"), Filter = $"{Loc("Str_Filter_Images")}|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tif;*.tiff|{Loc("Str_Filter_AllFiles")}|*.*", Multiselect = true, ShowImagePreview = true }; if (dlg.ShowDialog(this) != true) return; // Open in its own tab using the same flow as New, so the import is treated as UNSAVED work: // dirty (orange Save icon), Save routes to Save As (no real file yet), and Close warns. var target = BeginTabLoad(out var prev, out bool createdNew); try { string tempPath = BuildPdfFromImages(dlg.FileNames); _doc = PdfReader.Open(tempPath, PdfDocumentOpenMode.Modify); FinishOpenFile("Imported.pdf", tempPath); _originalFile = null; // no saved location yet -> Save becomes Save As MarkDirty(true); // unsaved -> orange icon + close warns SetStatus(string.Format(Loc("Str_Status_Imported"), dlg.FileNames.Length)); CaptureSessionState(_active!); SetTool(_currentTool); RebuildTabStrip(); } catch (Exception ex) { AbortTabLoad(target, prev, createdNew); KillerDialog.Show(this, Loc("Str_Err_ImportFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error); } } // Builds a PDF where each page is exactly one source image. Multi-frame TIFF/GIF expand to one // page per frame. Page size matches the image's physical size (pixels at its own DPI, 96 if the // image declares none). Returns a temp PDF path; the caller opens it and the user can Save As. private static string BuildPdfFromImages(string[] imagePaths) { using var pdf = new PdfDocument(); foreach (var path in imagePaths) PdfImport.AddImagePagesFromFile(pdf, path); if (pdf.PageCount == 0) throw new InvalidOperationException( Application.Current.TryFindResource("Str_Err_NoImages") as string ?? "No images could be read."); string outPath = Path.Combine(Path.GetTempPath(), $"Imported-{DateTime.Now:yyyyMMdd-HHmmss-fff}.pdf"); pdf.Save(outPath); return outPath; } // AddImagePagesFromFile and IsPdfPath live in Services/PdfImport.cs (MmdPdfUI refactor). // ----- Drag/drop of folders, archives, and multiple files ---------------------------- private static readonly string[] DropImageExt = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tif", ".tiff"]; private static bool IsImagePath(string p) => DropImageExt.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)); private static bool IsOpenablePath(string p) => PdfImport.IsPdfPath(p) || IsImagePath(p); // Entry point for any file/folder/archive drop. Expands dropped folders (recursively) and .zip // archives, then opens the collected PDFs/images - asking merge-vs-separate when there's >1. private void OnPathsDropped(string[] paths) { if (paths == null || paths.Length == 0) return; var found = new List(); var tempDirs = new List(); // extracted-zip temp dirs we may need to clean up bool expanded = false; // a folder or archive was opened try { foreach (var p in paths) { if (Directory.Exists(p)) { expanded = true; CollectOpenable(p, found); } else if (p.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { var dir = ExtractZipToTemp(p); if (dir != null) { expanded = true; tempDirs.Add(dir); CollectOpenable(dir, found); } } else if (IsOpenablePath(p)) found.Add(p); } } catch (Exception ex) { CleanupDirs(tempDirs); KillerDialog.Show(this, Loc("Str_Err_ImportFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error); return; } if (found.Count == 0) { CleanupDirs(tempDirs); SetStatus(Loc("Str_Drop_NothingOpenable")); return; } // Guard against a folder/archive holding a huge number of files: opening or merging them all // would exhaust memory. Cap to a sane maximum, opening only the first N (name-sorted) after asking. const int MaxDropFiles = 50; if (found.Count > MaxDropFiles) { var proceed = KillerDialog.Show(this, string.Format(Loc("Str_Drop_TooMany"), found.Count, MaxDropFiles), "MmdPdf", MessageBoxButton.OKCancel); if (proceed != MessageBoxResult.OK) { CleanupDirs(tempDirs); return; } found = found.GetRange(0, MaxDropFiles); } // A single dropped file (no folder/zip): open it directly, as before. Temp from a single-file // zip is kept for the session since the opened doc references it. if (!expanded && found.Count == 1) { OpenDropped(found[0]); return; } int choice = KillerDialog.ShowChoices(this, string.Format(Loc("Str_Drop_Prompt"), found.Count), [Loc("Str_Drop_Merge"), Loc("Str_Drop_Separate"), Loc("Str_Stamp_Cancel")], accentIndex: 0); if (choice == 0) { OpenMerged(found, tempDirs); // async: builds on a background thread, then owns temp cleanup } else if (choice == 1) { OpenSeparately(found); // opened docs may reference extracted files - keep temp for the session } else { CleanupDirs(tempDirs); // canceled } } private void OpenDropped(string path) { if (PdfImport.IsPdfPath(path)) OpenInNewTab(path); else OpenImagesAsImportedTab([path], Path.GetFileName(path)); } private void OpenSeparately(List found) { if (found.Count > 30) { var ok = KillerDialog.Show(this, string.Format(Loc("Str_Drop_ManyTabs"), found.Count), "MmdPdf", MessageBoxButton.OKCancel); if (ok != MessageBoxResult.OK) return; } foreach (var f in found) { if (PdfImport.IsPdfPath(f)) OpenInNewTab(f); else OpenImagesAsImportedTab([f], Path.GetFileName(f)); } } private async void OpenMerged(List found, List tempDirs) { var target = BeginTabLoad(out var prev, out bool createdNew); var busy = ShowBusyOverlay(Loc("Str_Drop_Merging")); var ct = BeginCancellableOp(Loc("Str_Op_Merge")); // Esc cancels; the busy overlay keeps the window draggable try { // Build off the UI thread so the window stays responsive (and movable) while it works. string? tempPath = await Task.Run(() => BuildCombinedPdf(found, ct)); if (ct.IsCancellationRequested || tempPath == null) { HideBusyOverlay(busy); AbortTabLoad(target, prev, createdNew); SetStatus(Loc("Str_Drop_MergeCanceled")); return; } _doc = PdfReader.Open(tempPath, PdfDocumentOpenMode.Modify); FinishOpenFile("Combined.pdf", tempPath); _originalFile = null; // unsaved -> Save routes to Save As MarkDirty(true); SetStatus(string.Format(Loc("Str_Status_Merged"), found.Count)); CaptureSessionState(_active!); SetTool(_currentTool); RebuildTabStrip(); HideBusyOverlay(busy); } catch (Exception ex) { HideBusyOverlay(busy); AbortTabLoad(target, prev, createdNew); KillerDialog.Show(this, Loc("Str_Err_ImportFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error); } finally { EndCancellableOp(); CleanupDirs(tempDirs); } } // Opens the given image(s) as a single unsaved imported-PDF tab (same flow as Import Images). private void OpenImagesAsImportedTab(string[] images, string displayName) { var target = BeginTabLoad(out var prev, out bool createdNew); try { string tempPath = BuildPdfFromImages(images); _doc = PdfReader.Open(tempPath, PdfDocumentOpenMode.Modify); FinishOpenFile(displayName, tempPath); _originalFile = null; MarkDirty(true); CaptureSessionState(_active!); SetTool(_currentTool); RebuildTabStrip(); } catch (Exception ex) { AbortTabLoad(target, prev, createdNew); KillerDialog.Show(this, Loc("Str_Err_ImportFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error); } } // Builds one PDF from a mix of PDFs (pages imported in order) and images (one page each). // Unreadable / encrypted entries are skipped rather than aborting the whole merge. private static string? BuildCombinedPdf(List files, CancellationToken ct) { using var outPdf = new PdfDocument(); foreach (var f in files) { if (ct.IsCancellationRequested) return null; if (PdfImport.IsPdfPath(f)) { try { using var src = PdfReader.Open(f, PdfDocumentOpenMode.Import); for (int i = 0; i < src.PageCount; i++) outPdf.AddPage(src.Pages[i]); } catch { /* skip an unreadable/encrypted PDF */ } } else { try { PdfImport.AddImagePagesFromFile(outPdf, f); } catch { /* skip an unreadable image */ } } } if (ct.IsCancellationRequested) return null; if (outPdf.PageCount == 0) throw new InvalidOperationException( Application.Current.TryFindResource("Str_Err_NoImages") as string ?? "Nothing could be read."); string outPath = Path.Combine(Path.GetTempPath(), $"Combined-{DateTime.Now:yyyyMMdd-HHmmss-fff}.pdf"); outPdf.Save(outPath); return outPath; } // Recursively gathers the PDFs and images under a folder, in a stable name order. private static void CollectOpenable(string dir, List found) { IEnumerable files; try { files = Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories); } catch { return; } foreach (var f in files.Where(IsOpenablePath).OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) found.Add(f); } private static string? ExtractZipToTemp(string zipPath) { string dir = Path.Combine(Path.GetTempPath(), "MmdPdf-zip-" + Guid.NewGuid().ToString("N")[..8]); try { Directory.CreateDirectory(dir); ZipFile.ExtractToDirectory(zipPath, dir); return dir; } catch { try { Directory.Delete(dir, true); } catch { } return null; } } private static void CleanupDirs(List dirs) { foreach (var d in dirs) try { Directory.Delete(d, true); } catch { } } // ----- Compress the current PDF to a .zip -------------------------------------------- private void CompressToZip_Click(object sender, RoutedEventArgs e) { if (_doc is null) { KillerDialog.Show(this, Loc("Str_Msg_OpenFirst")); return; } // The zip wraps the PDF on disk, so make sure what's on screen is saved first. if (_isDirty || string.IsNullOrEmpty(_originalFile) || !File.Exists(_originalFile)) { var ask = KillerDialog.Show(this, Loc("Str_Dlg_SaveBeforeZip"), "MmdPdf", MessageBoxButton.OKCancel); if (ask != MessageBoxResult.OK) return; SaveInPlace(); if (_isDirty || string.IsNullOrEmpty(_originalFile) || !File.Exists(_originalFile)) return; // save was canceled or failed } string sourcePdf = _originalFile!; var dlg = new Controls.FileDialog(Controls.FileDialogMode.Save) { Filter = $"{Loc("Str_Filter_Zip")}|*.zip", Title = Loc("Str_Menu_CompressZip"), FileName = Path.GetFileNameWithoutExtension(sourcePdf) + ".zip" }; var srcDir = Path.GetDirectoryName(sourcePdf); if (!string.IsNullOrEmpty(srcDir) && Directory.Exists(srcDir)) dlg.InitialDirectory = srcDir; if (dlg.ShowDialog(this) != true) return; try { if (File.Exists(dlg.FileName)) File.Delete(dlg.FileName); using (var zip = ZipFile.Open(dlg.FileName, ZipArchiveMode.Create)) zip.CreateEntryFromFile(sourcePdf, Path.GetFileName(sourcePdf), CompressionLevel.Optimal); long before = new FileInfo(sourcePdf).Length; long after = new FileInfo(dlg.FileName).Length; SetStatus(string.Format(Loc("Str_Status_Zipped"), Path.GetFileName(dlg.FileName), FormatSize(after), FormatSize(before))); } catch (Exception ex) { KillerDialog.Show(this, Loc("Str_Err_ZipFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error); } } private static string FormatSize(long bytes) { if (bytes >= 1024 * 1024) return $"{bytes / 1024.0 / 1024.0:F1} MB"; if (bytes >= 1024) return $"{bytes / 1024.0:F0} KB"; return $"{bytes} B"; } } }