diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..8b78ae0d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+root = true
+
+[*]
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{cs,xaml,csproj,wixproj,wxs,resx,xml}]
+indent_style = space
+indent_size = 4
+
+[*.{md,yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.bat]
+indent_style = space
+indent_size = 4
+
+[*.ps1]
+indent_style = space
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..43e26afe
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,21 @@
+* text=auto
+
+*.bat text eol=crlf
+*.cs text eol=crlf
+*.csproj text eol=crlf
+*.resx text eol=crlf
+*.wixproj text eol=crlf
+*.wxs text eol=crlf
+*.xaml text eol=crlf
+*.xml text eol=crlf
+*.md text eol=lf
+*.ps1 text eol=crlf
+*.yml text eol=lf
+*.yaml text eol=lf
+
+*.bmp binary
+*.gif binary
+*.ico binary
+*.jpg binary
+*.png binary
+*.rtf binary
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..315dfbd9
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,19 @@
+## Summary
+
+
+
+## User Impact
+
+
+
+## Validation
+
+- [ ] `git diff --check`
+- [ ] XML/XAML/project files parse
+- [ ] Release x64 build
+- [ ] Smoke conversion checked
+- [ ] Installer or Explorer integration checked when relevant
+
+## Notes
+
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..e9bcf34a
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,47 @@
+name: build
+
+on:
+ push:
+ branches:
+ - integration
+ - master
+ - "codex/**"
+ pull_request:
+
+permissions:
+ contents: read
+
+env:
+ BUILD_CONFIGURATION: Release
+ BUILD_PLATFORM: x64
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ windows:
+ name: Windows x64
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Restore and build
+ shell: pwsh
+ run: .\build.ps1 -Configuration $env:BUILD_CONFIGURATION -Platform $env:BUILD_PLATFORM
+
+ - name: Upload app artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ZFileConverter-app-x64
+ path: Application/FileConverter/bin/x64/Release/**
+ if-no-files-found: error
+
+ - name: Upload installer artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ZFileConverter-installer-x64
+ path: Installer/bin/x64/Release/*.msi
+ if-no-files-found: warn
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..c2e14484
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,81 @@
+name: release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Release tag, for example v2.2.0"
+ required: true
+ type: string
+ prerelease:
+ description: "Mark as prerelease"
+ required: true
+ default: true
+ type: boolean
+
+permissions:
+ contents: write
+
+env:
+ BUILD_CONFIGURATION: Release
+ BUILD_PLATFORM: x64
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ windows-release:
+ name: Windows release
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Build
+ shell: pwsh
+ run: .\build.ps1 -Configuration $env:BUILD_CONFIGURATION -Platform $env:BUILD_PLATFORM
+
+ - name: Prepare artifacts
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = "Stop"
+ $tag = "${{ inputs.version }}"
+ $version = $tag.TrimStart("v")
+ New-Item -ItemType Directory -Force artifacts | Out-Null
+ Copy-Item "Installer\bin\x64\Release\ZFileConverter-setup.msi" "artifacts\ZFileConverter-$version-x64-setup.msi"
+ Compress-Archive -Path "Application\FileConverter\bin\x64\Release\*" -DestinationPath "artifacts\ZFileConverter-$version-x64-app.zip" -Force
+
+ - name: Upload workflow artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: ZFileConverter-release-${{ inputs.version }}
+ path: artifacts/*
+ if-no-files-found: error
+
+ - name: Create draft GitHub release
+ shell: pwsh
+ env:
+ GH_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ inputs.version }}
+ PRERELEASE: ${{ inputs.prerelease }}
+ run: |
+ $ErrorActionPreference = "Stop"
+ $files = Get-ChildItem artifacts -File | ForEach-Object { $_.FullName }
+ $arguments = @(
+ "release",
+ "create",
+ $env:RELEASE_TAG
+ ) + $files + @(
+ "--target",
+ $env:GITHUB_SHA,
+ "--title",
+ "ZFileConverter $env:RELEASE_TAG",
+ "--generate-notes",
+ "--draft"
+ )
+ if ($env:PRERELEASE -eq "true") {
+ $arguments += "--prerelease"
+ }
+ gh @arguments
diff --git a/.gitignore b/.gitignore
index fe9b220a..6002a914 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@
[Rr]eleases/
build/
bld/
+artifacts/
[Bb]in/
[Oo]bj/
diff --git a/Application/FileConverter/Application.xaml.cs b/Application/FileConverter/Application.xaml.cs
index 95f89210..925ab244 100644
--- a/Application/FileConverter/Application.xaml.cs
+++ b/Application/FileConverter/Application.xaml.cs
@@ -1,7 +1,7 @@
// License: http://www.gnu.org/licenses/gpl.html GPL version 3.
-/* File Converter - This program allow you to convert file format to another.
- Copyright (C) 2026 Adrien Allard
+/* ZFileConverter - This program allows you to convert one file format to another.
+ Copyright (C) 2026 ZaidNAlAsali and File Converter contributors
email: adrien.allard.pro@gmail.com
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
@@ -152,6 +152,12 @@ protected override void OnExit(ExitEventArgs e)
return;
}
+ if (!upgradeService.UpgradeVersionDescription.InstallerIsVerified)
+ {
+ Debug.LogError("Refuse to start upgrade installer because it has not passed integrity verification.");
+ return;
+ }
+
// Start process.
Debug.Log($"Start file converter upgrade from version {ApplicationVersion} to {upgradeService.UpgradeVersionDescription.LatestVersion}.");
@@ -218,9 +224,9 @@ private void RegisterServices()
private void Initialize()
{
#if BUILD32
- Diagnostics.Debug.Log("File Converter v" + ApplicationVersion.ToString() + " (32 bits)");
+ Diagnostics.Debug.Log("ZFileConverter v" + ApplicationVersion.ToString() + " (32 bits)");
#else
- Diagnostics.Debug.Log("File Converter v" + ApplicationVersion.ToString() + " (64 bits)");
+ Diagnostics.Debug.Log("ZFileConverter v" + ApplicationVersion.ToString() + " (64 bits)");
#endif
// Retrieve arguments.
@@ -276,7 +282,8 @@ private void Initialize()
if (index >= args.Length - 1)
{
Debug.LogError(errorCode: 0x0B, $"Invalid format.");
- break;
+ Application.AskForShutdown();
+ return;
}
string shellExtensionPath = args[index + 1];
@@ -296,7 +303,8 @@ private void Initialize()
if (index >= args.Length - 1)
{
Debug.LogError(errorCode: 0x0D, $"Invalid format.");
- break;
+ Application.AskForShutdown();
+ return;
}
string shellExtensionPath = args[index + 1];
@@ -311,6 +319,24 @@ private void Initialize()
return;
}
+ case "repair-shell-extension":
+ {
+ string shellExtensionPath = Helpers.GetDefaultShellExtensionPath();
+ if (index < args.Length - 1 && !args[index + 1].StartsWith("--"))
+ {
+ shellExtensionPath = args[index + 1];
+ index++;
+ }
+
+ if (!Helpers.RepairShellExtension(shellExtensionPath))
+ {
+ Debug.LogError(errorCode: 0x10, $"Failed to repair shell extension {shellExtensionPath}.");
+ }
+
+ Application.AskForShutdown();
+ return;
+ }
+
case "version":
Console.WriteLine(ApplicationVersion.ToString());
Application.AskForShutdown();
@@ -371,6 +397,7 @@ private void Initialize()
default:
Debug.LogError($"Unknown application argument: '--{parameterTitle}'.");
+ Application.AskForShutdown();
return;
}
}
@@ -388,7 +415,7 @@ private void RunConversions(List filePaths, string conversionPresetName)
ISettingsService settingsService = Ioc.Default.GetRequiredService();
if (settingsService.Settings == null)
{
- Debug.LogError(errorCode: 0x04, "Can't load File Converter settings. The application will now shutdown, if you want to fix the problem yourself please edit or delete the file: C:\\Users\\UserName\\AppData\\Local\\FileConverter\\Settings.user.xml.");
+ Debug.LogError(errorCode: 0x04, "Can't load ZFileConverter settings. The application will now shutdown, if you want to fix the problem yourself please edit or delete the file: C:\\Users\\UserName\\AppData\\Local\\FileConverter\\Settings.user.xml.");
Application.AskForShutdown();
return;
}
diff --git a/Application/FileConverter/Controls/ConversionJobControl.xaml b/Application/FileConverter/Controls/ConversionJobControl.xaml
index caed4eb7..3976fd39 100644
--- a/Application/FileConverter/Controls/ConversionJobControl.xaml
+++ b/Application/FileConverter/Controls/ConversionJobControl.xaml
@@ -15,7 +15,7 @@
-
+
@@ -54,15 +54,27 @@
-
-
-
+
+
+
+
+
+
+
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob.cs b/Application/FileConverter/ConversionJobs/ConversionJob.cs
index ecf68545..90e5ca8b 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob.cs
@@ -4,12 +4,15 @@ namespace FileConverter.ConversionJobs
{
using System;
using System.ComponentModel;
+ using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Input;
+ using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using FileConverter.Diagnostics;
+ using FileConverter.Services;
public class ConversionJob : INotifyPropertyChanged
{
@@ -19,6 +22,8 @@ public class ConversionJob : INotifyPropertyChanged
private string errorMessage = string.Empty;
private string userState = string.Empty;
private RelayCommand cancelCommand;
+ private RelayCommand openOutputFolderCommand;
+ private RelayCommand retryCommand;
private readonly string initialInputPath;
private int currentOutputFilePathIndex;
@@ -64,6 +69,8 @@ public ConversionPreset ConversionPreset
private set;
}
+ public string InitialInputPath => this.initialInputPath;
+
public string InputFilePath
{
get;
@@ -102,6 +109,8 @@ private set
this.state = value;
this.NotifyPropertyChanged();
Application.Current.Dispatcher.Invoke(() => this.cancelCommand?.NotifyCanExecuteChanged());
+ Application.Current.Dispatcher.Invoke(() => this.openOutputFolderCommand?.NotifyCanExecuteChanged());
+ Application.Current.Dispatcher.Invoke(() => this.retryCommand?.NotifyCanExecuteChanged());
}
}
@@ -168,6 +177,32 @@ public ICommand CancelCommand
}
}
+ public ICommand OpenOutputFolderCommand
+ {
+ get
+ {
+ if (this.openOutputFolderCommand == null)
+ {
+ this.openOutputFolderCommand = new RelayCommand(this.OpenOutputFolder, this.CanOpenOutputFolder);
+ }
+
+ return this.openOutputFolderCommand;
+ }
+ }
+
+ public ICommand RetryCommand
+ {
+ get
+ {
+ if (this.retryCommand == null)
+ {
+ this.retryCommand = new RelayCommand(this.RetryConversion, this.CanRetryConversion);
+ }
+
+ return this.retryCommand;
+ }
+ }
+
protected bool CancelIsRequested
{
get;
@@ -200,6 +235,18 @@ protected virtual InputPostConversionAction InputPostConversionAction
protected virtual bool IsCancelable() => this.State == ConversionState.InProgress;
+ private bool CanOpenOutputFolder()
+ {
+ return this.State == ConversionState.Done && !string.IsNullOrEmpty(this.OutputFilePath);
+ }
+
+ private bool CanRetryConversion()
+ {
+ return this.State == ConversionState.Failed &&
+ this.ConversionPreset != null &&
+ !string.IsNullOrEmpty(this.initialInputPath);
+ }
+
protected string[] OutputFilePaths
{
get;
@@ -220,8 +267,14 @@ public void PrepareConversion(params string[] outputFilePaths)
this.InputFilePath = this.initialInputPath;
- string extension = System.IO.Path.GetExtension(this.initialInputPath);
- extension = extension.Substring(1, extension.Length - 1);
+ string extension = PathHelpers.GetExtensionWithoutDot(this.initialInputPath);
+ if (string.IsNullOrEmpty(extension))
+ {
+ this.ConversionFailed(Properties.Resources.ErrorInputTypeIncompatibleWithOutputType);
+ Debug.Log($"Input file has no extension: {this.InputFilePath}.");
+ return;
+ }
+
string extensionCategory = Helpers.GetExtensionCategory(extension);
if (!Helpers.IsOutputTypeCompatibleWithCategory(this.ConversionPreset.OutputType, extensionCategory))
{
@@ -232,7 +285,18 @@ public void PrepareConversion(params string[] outputFilePaths)
this.OutputFilePaths = outputFilePaths;
if (this.OutputFilePaths.Length == 0)
{
- int outputFilesCount = this.GetOutputFilesCount();
+ int outputFilesCount;
+ try
+ {
+ outputFilesCount = this.GetOutputFilesCount();
+ }
+ catch (Exception exception)
+ {
+ this.ConversionFailed(Properties.Resources.ErrorDuringJobInitialization);
+ Debug.Log(exception.ToString());
+ return;
+ }
+
this.OutputFilePaths = new string[outputFilesCount];
}
@@ -246,13 +310,15 @@ public void PrepareConversion(params string[] outputFilePaths)
string path = this.ConversionPreset.GenerateOutputFilePath(this.initialInputPath, index + 1, this.OutputFilePaths.Length);
- if (!PathHelpers.IsPathValid(path))
+ if (!PathHelpers.TryNormalizeGeneratedPath(path, out string normalizedPath, out string outputPathErrorMessage))
{
this.ConversionFailed(Properties.Resources.ErrorInvalidOutputPath);
- Debug.Log($"Invalid output path generated: {path} from input: {this.InputFilePath}.");
+ Debug.Log($"Invalid output path generated: {path} from input: {this.InputFilePath}. {outputPathErrorMessage}");
return;
}
+ path = normalizedPath;
+
if (path == this.InputFilePath)
{
// If the input post conversion action is to move or delete the input file, change its name in order to keep the output name intact.
@@ -378,6 +444,49 @@ public virtual void Cancel()
this.ConversionFailed(Properties.Resources.ErrorCanceled);
}
+ private void OpenOutputFolder()
+ {
+ if (string.IsNullOrEmpty(this.OutputFilePath))
+ {
+ return;
+ }
+
+ try
+ {
+ string outputFilePath = this.OutputFilePath;
+ if (File.Exists(outputFilePath))
+ {
+ System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{outputFilePath}\"");
+ return;
+ }
+
+ string outputDirectory = Path.GetDirectoryName(outputFilePath);
+ if (!string.IsNullOrEmpty(outputDirectory) && Directory.Exists(outputDirectory))
+ {
+ System.Diagnostics.Process.Start("explorer.exe", $"\"{outputDirectory}\"");
+ return;
+ }
+
+ Debug.Log($"Can't open output folder because the path does not exist: {outputFilePath}.");
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Can't open output folder: {exception.Message}.");
+ }
+ }
+
+ private void RetryConversion()
+ {
+ try
+ {
+ Ioc.Default.GetRequiredService().RetryConversionJob(this);
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Can't retry conversion: {exception.Message}.");
+ }
+ }
+
protected virtual int GetOutputFilesCount()
{
return 1;
@@ -419,30 +528,43 @@ protected virtual void OnConversionSucceed()
this.ChangeOutputFileTimestampToMatchOriginal();
- // Apply the input post conversion action.
- switch (this.InputPostConversionAction)
+ try
{
- case InputPostConversionAction.None:
- break;
-
- case InputPostConversionAction.MoveInArchiveFolder:
- string basePath = System.IO.Path.GetDirectoryName(this.initialInputPath);
- string inputFilename = System.IO.Path.GetFileName(this.initialInputPath);
- string archivePath = basePath + "\\" + this.ConversionPreset.ConversionArchiveFolderName;
- if (!System.IO.Directory.Exists(archivePath))
- {
- System.IO.Directory.CreateDirectory(archivePath);
- }
-
- string newPath = PathHelpers.GenerateUniquePath(archivePath + "\\" + inputFilename);
- System.IO.File.Move(this.InputFilePath, newPath);
- Debug.Log($"Input file moved in archive folder: '{newPath}'");
- break;
-
- case InputPostConversionAction.Delete:
- System.IO.File.Delete(this.InputFilePath);
- Debug.Log($"Input file deleted: '{this.initialInputPath}'");
- break;
+ // Apply the input post conversion action.
+ switch (this.InputPostConversionAction)
+ {
+ case InputPostConversionAction.None:
+ break;
+
+ case InputPostConversionAction.MoveInArchiveFolder:
+ string basePath = System.IO.Path.GetDirectoryName(this.initialInputPath);
+ if (string.IsNullOrEmpty(basePath))
+ {
+ basePath = System.Environment.CurrentDirectory;
+ }
+
+ string inputFilename = System.IO.Path.GetFileName(this.initialInputPath);
+ string archivePath = basePath + "\\" + this.ConversionPreset.ConversionArchiveFolderName;
+ if (!System.IO.Directory.Exists(archivePath))
+ {
+ System.IO.Directory.CreateDirectory(archivePath);
+ }
+
+ string newPath = PathHelpers.GenerateUniquePath(archivePath + "\\" + inputFilename);
+ System.IO.File.Move(this.InputFilePath, newPath);
+ Debug.Log($"Input file moved in archive folder: '{newPath}'");
+ break;
+
+ case InputPostConversionAction.Delete:
+ System.IO.File.Delete(this.InputFilePath);
+ Debug.Log($"Input file deleted: '{this.initialInputPath}'");
+ break;
+ }
+ }
+ catch (Exception exception)
+ {
+ this.ConversionFailed($"Post conversion action failed: {exception.Message}");
+ return;
}
Debug.Log(string.Empty);
@@ -468,6 +590,63 @@ protected void ConversionFailed(string exitingMessage)
this.ErrorMessage = exitingMessage;
}
+ protected void DeleteFileIfExists(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ return;
+ }
+
+ try
+ {
+ if (System.IO.File.Exists(filePath))
+ {
+ System.IO.File.Delete(filePath);
+ }
+
+ this.DeleteEmptyTemporaryParentFolder(filePath);
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Can't delete file '{filePath}'.");
+ Debug.Log($"An exception has been thrown: {exception}.");
+ }
+ }
+
+ private void DeleteEmptyTemporaryParentFolder(string filePath)
+ {
+ string parentFolder = Path.GetDirectoryName(filePath);
+ if (string.IsNullOrEmpty(parentFolder))
+ {
+ return;
+ }
+
+ DirectoryInfo parent = Directory.GetParent(parentFolder);
+ if (parent == null)
+ {
+ return;
+ }
+
+ string tempRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "ZFileConverter"));
+ string candidateRoot = Path.GetFullPath(parent.FullName);
+ if (!string.Equals(
+ tempRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
+ candidateRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ try
+ {
+ Directory.Delete(parentFolder, false);
+ }
+ catch
+ {
+ // Best effort only; concurrent conversions may still be using the folder.
+ }
+ }
+
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (this.PropertyChanged != null)
diff --git a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs
index 34886bb2..52f11b74 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs
@@ -6,8 +6,7 @@ public static class ConversionJobFactory
{
public static ConversionJob Create(ConversionPreset conversionPreset, string inputFilePath)
{
- string inputFileExtension = System.IO.Path.GetExtension(inputFilePath);
- inputFileExtension = inputFileExtension.ToLowerInvariant().Substring(1, inputFileExtension.Length - 1);
+ string inputFileExtension = PathHelpers.GetExtensionWithoutDot(inputFilePath);
if (inputFileExtension == "cda")
{
return new ConversionJob_ExtractCDA(conversionPreset, inputFilePath);
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs
index c4d4d021..1ed5488d 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_Excel.cs
@@ -48,7 +48,7 @@ protected override int GetOutputFilesCount()
Excel.Worksheet worksheet = sheet as Excel.Worksheet;
if (worksheet != null)
{
- pagesCount = worksheet.PageSetup.Pages.Count;
+ pagesCount += worksheet.PageSetup.Pages.Count;
}
}
@@ -78,8 +78,7 @@ protected override void Initialize()
{
// Generate intermediate file path.
string fileName = Path.GetFileNameWithoutExtension(this.InputFilePath);
- string tempPath = Path.GetTempPath();
- this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".pdf");
+ this.intermediateFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + ".pdf");
ConversionPreset intermediatePreset = new ConversionPreset("Pdf to image", this.ConversionPreset, "pdf");
this.pdf2ImageConversionJob = ConversionJobFactory.Create(intermediatePreset, this.intermediateFilePath);
@@ -102,48 +101,55 @@ protected override void Convert()
return;
}
- // Make this document the active document.
- this.document.Activate();
-
- this.UserState = Properties.Resources.ConversionStateConversion;
-
- Debug.Log("Convert excel document to pdf.");
- this.document.ExportAsFixedFormat(Excel.Enums.XlFixedFormatType.xlTypePDF, this.intermediateFilePath);
+ try
+ {
+ // Make this document the active document.
+ this.document.Activate();
- Debug.Log($"Close excel document '{this.InputFilePath}'.");
- this.document.Close(false);
- this.document = null;
+ this.UserState = Properties.Resources.ConversionStateConversion;
- this.ReleaseOfficeApplicationInstanceIfNeeded();
+ Debug.Log("Convert excel document to pdf.");
+ this.document.ExportAsFixedFormat(Excel.Enums.XlFixedFormatType.xlTypePDF, this.intermediateFilePath);
+ }
+ finally
+ {
+ this.CloseDocumentIfNeeded();
+ this.ReleaseOfficeApplicationInstanceIfNeeded();
+ }
if (this.pdf2ImageConversionJob != null)
{
- if (!System.IO.File.Exists(this.intermediateFilePath))
+ Task updateProgress = null;
+ try
{
- this.ConversionFailed(Properties.Resources.ErrorCantFindOutputFiles);
- return;
- }
+ if (!System.IO.File.Exists(this.intermediateFilePath))
+ {
+ this.ConversionFailed(Properties.Resources.ErrorCantFindOutputFiles);
+ return;
+ }
- Task updateProgress = this.UpdateProgress();
+ updateProgress = this.UpdateProgress();
- Debug.Log("Convert pdf to images.");
+ Debug.Log("Convert pdf to images.");
- this.pdf2ImageConversionJob.StartConversion();
+ this.pdf2ImageConversionJob.StartConversion();
- if (this.pdf2ImageConversionJob.State != ConversionState.Done)
- {
- this.ConversionFailed(this.pdf2ImageConversionJob.ErrorMessage);
- return;
+ if (this.pdf2ImageConversionJob.State != ConversionState.Done)
+ {
+ this.ConversionFailed(this.pdf2ImageConversionJob.ErrorMessage);
+ return;
+ }
}
-
- if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ finally
{
- Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ updateProgress?.Wait();
- File.Delete(this.intermediateFilePath);
+ if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ {
+ Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ this.DeleteFileIfExists(this.intermediateFilePath);
+ }
}
-
- updateProgress.Wait();
}
}
@@ -160,6 +166,7 @@ protected override void InitializeOfficeApplicationInstanceIfNecessary()
{
Visible = false
};
+ this.HardenOfficeApplicationInstance(this.application);
}
protected override void ReleaseOfficeApplicationInstanceIfNeeded()
@@ -176,15 +183,11 @@ protected override void ReleaseOfficeApplicationInstanceIfNeeded()
private async Task UpdateProgress()
{
- while (this.pdf2ImageConversionJob.State != ConversionState.Done &&
+ while (this.pdf2ImageConversionJob != null &&
+ this.pdf2ImageConversionJob.State != ConversionState.Done &&
this.pdf2ImageConversionJob.State != ConversionState.Failed)
{
- if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress)
- {
- this.Progress = this.pdf2ImageConversionJob.Progress;
- }
-
- if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress)
+ if (this.pdf2ImageConversionJob.State == ConversionState.InProgress)
{
this.Progress = this.pdf2ImageConversionJob.Progress;
this.UserState = this.pdf2ImageConversionJob.UserState;
@@ -215,10 +218,30 @@ private bool TryLoadDocumentIfNecessary()
{
Debug.Log($"Load excel document '{this.InputFilePath}'.");
- this.document = this.application.Workbooks.Open(this.InputFilePath, System.Reflection.Missing.Value, true);
+ this.document = this.application.Workbooks.Open(this.InputFilePath, 0, true);
}
return this.document != null;
}
+
+ private void CloseDocumentIfNeeded()
+ {
+ if (this.document == null)
+ {
+ return;
+ }
+
+ try
+ {
+ Debug.Log($"Close excel document '{this.InputFilePath}'.");
+ this.document.Close(false);
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to close excel document '{this.InputFilePath}': {exception.Message}.");
+ }
+
+ this.document = null;
+ }
}
}
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs
index 66c8b381..21c0b848 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs
@@ -40,7 +40,7 @@ public override void Cancel()
{
base.Cancel();
- this.compressionConversionJob.Cancel();
+ this.compressionConversionJob?.Cancel();
}
protected override void Initialize()
@@ -105,12 +105,17 @@ protected override void Initialize()
// Generate intermediate file path.
string fileName = Path.GetFileName(this.OutputFilePath);
- string tempPath = Path.GetTempPath();
- this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".wav");
+ this.intermediateFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + ".wav");
// Sub conversion job (for compression).
this.compressionConversionJob = ConversionJobFactory.Create(this.ConversionPreset, this.intermediateFilePath);
this.compressionConversionJob.PrepareConversion(this.OutputFilePath);
+ if (this.compressionConversionJob.State == ConversionState.Failed)
+ {
+ this.ConversionFailed(this.compressionConversionJob.ErrorMessage);
+ return;
+ }
+
this.compressionThread = Helpers.InstantiateThread("CDACompressionThread", this.CompressAsync);
}
@@ -125,40 +130,45 @@ protected override void Convert()
this.UserState = Properties.Resources.ConversionStateExtraction;
- if (!this.diskDrive.IsCDReady())
- {
- this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady);
- return;
- }
-
- if (!this.diskDrive.Refresh())
- {
- Debug.Log("Can't refresh CD drive data.");
- this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady);
- return;
- }
-
- if (!this.diskDrive.LockCD())
+ bool cdLocked = false;
+ try
{
- Debug.Log("Can\'t lock cd.");
- this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady);
- return;
+ if (!this.diskDrive.IsCDReady())
+ {
+ this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady);
+ return;
+ }
+
+ if (!this.diskDrive.Refresh())
+ {
+ Debug.Log("Can't refresh CD drive data.");
+ this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady);
+ return;
+ }
+
+ if (!this.diskDrive.LockCD())
+ {
+ Debug.Log("Can\'t lock cd.");
+ this.ConversionFailed(Properties.Resources.ErrorCDDriveNotReady);
+ return;
+ }
+
+ cdLocked = true;
+
+ WaveFormat waveFormat = new WaveFormat(44100, 16, 2);
+
+ using (Stream waveStream = new FileStream(this.intermediateFilePath, FileMode.Create, FileAccess.Write))
+ using (this.waveWriter = new WaveWriter(waveStream, waveFormat, this.diskDrive.TrackSize(this.cdaTrackNumber)))
+ {
+ this.diskDrive.ReadTrack(this.cdaTrackNumber, this.WriteWaveData, this.CdReadProgress);
+ }
}
-
- WaveFormat waveFormat = new WaveFormat(44100, 16, 2);
-
- using (Stream waveStream = new FileStream(this.intermediateFilePath, FileMode.Create, FileAccess.Write))
- using (this.waveWriter = new WaveWriter(waveStream, waveFormat, this.diskDrive.TrackSize(this.cdaTrackNumber)))
+ finally
{
- this.diskDrive.ReadTrack(this.cdaTrackNumber, this.WriteWaveData, this.CdReadProgress);
+ this.waveWriter = null;
+ this.ReleaseCdDrive(cdLocked);
}
- this.waveWriter = null;
-
- this.diskDrive.UnLockCD();
-
- this.diskDrive.Close();
-
this.StateFlags = ConversionFlags.None;
if (!File.Exists(this.intermediateFilePath))
@@ -179,18 +189,21 @@ protected override void Convert()
this.compressionConversionJob.State != ConversionState.Failed)
{
this.Progress = this.compressionConversionJob.Progress;
+ Thread.Sleep(40);
}
+ this.compressionThread.Join();
+
+ Debug.Log(string.Empty);
+ Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+
+ this.DeleteIntermediateFileIfExists();
+
if (this.compressionConversionJob.State == ConversionState.Failed)
{
this.ConversionFailed(this.compressionConversionJob.ErrorMessage);
return;
}
-
- Debug.Log(string.Empty);
- Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
-
- File.Delete(this.intermediateFilePath);
}
private void WriteWaveData(object sender, DataReadEventArgs eventArgs)
@@ -220,5 +233,42 @@ private void CompressAsync()
{
this.compressionConversionJob.StartConversion();
}
+
+ private void ReleaseCdDrive(bool cdLocked)
+ {
+ if (this.diskDrive == null)
+ {
+ return;
+ }
+
+ if (cdLocked)
+ {
+ try
+ {
+ this.diskDrive.UnLockCD();
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to unlock CD drive: {exception.Message}.");
+ }
+ }
+
+ try
+ {
+ if (this.diskDrive.IsOpened)
+ {
+ this.diskDrive.Close();
+ }
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to close CD drive: {exception.Message}.");
+ }
+ }
+
+ private void DeleteIntermediateFileIfExists()
+ {
+ this.DeleteFileIfExists(this.intermediateFilePath);
+ }
}
}
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs
index 79f81586..4737e9a6 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs
@@ -79,7 +79,7 @@ protected override void Initialize()
{
CreateNoWindow = true,
UseShellExecute = false,
- RedirectStandardOutput = true,
+ RedirectStandardOutput = false,
RedirectStandardError = true
};
@@ -88,7 +88,7 @@ protected override void Initialize()
protected virtual void FillFFMpegArgumentsList()
{
- const string baseArgs = "-n -progress pipe:1";
+ const string baseArgs = "-n";
bool customCommandEnabled = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableFFMPEGCustomCommand);
if (customCommandEnabled)
@@ -166,8 +166,7 @@ protected virtual void FillFFMpegArgumentsList()
{
// http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html
string fileName = Path.GetFileName(this.InputFilePath);
- string tempPath = Path.GetTempPath();
- string paletteFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + " - palette.png");
+ string paletteFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + " - palette.png");
string transformArgs = ConversionJob_FFMPEG.ComputeTransformArgs(this.ConversionPreset);
@@ -464,6 +463,10 @@ protected override void Convert()
}
exeProcess.WaitForExit();
+ if (exeProcess.ExitCode != 0 && !this.CancelIsRequested && this.State != ConversionState.Failed)
+ {
+ this.ConversionFailed($"FFmpeg exited with code {exeProcess.ExitCode}.");
+ }
}
}
catch
@@ -471,6 +474,11 @@ protected override void Convert()
this.ConversionFailed(Properties.Resources.ErrorFailedToLaunchFFMPEG);
throw;
}
+
+ if (this.State == ConversionState.Failed || this.CancelIsRequested)
+ {
+ break;
+ }
}
Diagnostics.Debug.Log(string.Empty);
@@ -487,12 +495,17 @@ protected override void Convert()
Diagnostics.Debug.Log($"Delete intermediate file {currentPass.FileToDelete}.");
- File.Delete(currentPass.FileToDelete);
+ this.DeleteFileIfExists(currentPass.FileToDelete);
}
}
private void ParseFFMPEGOutput(string input)
{
+ if (string.IsNullOrEmpty(input))
+ {
+ return;
+ }
+
Match match = this.durationRegex.Match(input);
if (match.Success && match.Groups.Count >= 6)
{
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs
index bc9fde60..af130262 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs
@@ -20,8 +20,8 @@ public override void Cancel()
{
base.Cancel();
- this.pngConversionJob.Cancel();
- this.gifConversionJob.Cancel();
+ this.pngConversionJob?.Cancel();
+ this.gifConversionJob?.Cancel();
}
protected override void Initialize()
@@ -33,8 +33,7 @@ protected override void Initialize()
throw new Exception("The conversion preset must be valid.");
}
- string extension = System.IO.Path.GetExtension(this.InputFilePath);
- extension = extension.ToLowerInvariant().Substring(1, extension.Length - 1);
+ string extension = PathHelpers.GetExtensionWithoutDot(this.InputFilePath);
string inputFilePath = string.Empty;
@@ -43,8 +42,7 @@ protected override void Initialize()
{
// Generate intermediate file path.
string fileName = Path.GetFileName(this.OutputFilePath);
- string tempPath = Path.GetTempPath();
- this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".png");
+ this.intermediateFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + ".png");
// Convert input in png file to send it to ffmpeg for the gif conversion.
ConversionPreset intermediatePreset = new ConversionPreset("To compatible image", OutputType.Png, this.ConversionPreset.InputTypes.ToArray());
@@ -70,41 +68,45 @@ protected override void Convert()
throw new Exception("The conversion preset must be valid.");
}
- Task updateProgress = this.UpdateProgress();
-
- if (this.pngConversionJob != null)
+ Task updateProgress = null;
+ try
{
- this.UserState = Properties.Resources.ConversionStateReadIntputImage;
+ if (this.pngConversionJob != null)
+ {
+ this.UserState = Properties.Resources.ConversionStateReadIntputImage;
+
+ Diagnostics.Debug.Log(string.Empty);
+ Diagnostics.Debug.Log("Convert image to PNG (intermediate format).");
+ this.pngConversionJob.StartConversion();
+
+ if (this.pngConversionJob.State != ConversionState.Done)
+ {
+ this.ConversionFailed(this.pngConversionJob.ErrorMessage);
+ return;
+ }
+ }
Diagnostics.Debug.Log(string.Empty);
- Diagnostics.Debug.Log("Convert image to PNG (intermediate format).");
- this.pngConversionJob.StartConversion();
+ Diagnostics.Debug.Log("Convert png intermediate image to gif.");
+ updateProgress = this.UpdateProgress();
+ this.gifConversionJob.StartConversion();
- if (this.pngConversionJob.State != ConversionState.Done)
+ if (this.gifConversionJob.State != ConversionState.Done)
{
- this.ConversionFailed(this.pngConversionJob.ErrorMessage);
+ this.ConversionFailed(this.gifConversionJob.ErrorMessage);
return;
}
}
-
- Diagnostics.Debug.Log(string.Empty);
- Diagnostics.Debug.Log("Convert png intermediate image to gif.");
- this.gifConversionJob.StartConversion();
-
- if (this.gifConversionJob.State != ConversionState.Done)
+ finally
{
- this.ConversionFailed(this.gifConversionJob.ErrorMessage);
- return;
- }
-
- if (!string.IsNullOrEmpty(this.intermediateFilePath))
- {
- Diagnostics.Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ updateProgress?.Wait();
- File.Delete(this.intermediateFilePath);
+ if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ {
+ Diagnostics.Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ this.DeleteFileIfExists(this.intermediateFilePath);
+ }
}
-
- updateProgress.Wait();
}
private async Task UpdateProgress()
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs
index 9c6e3231..ed71f268 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_Ico.cs
@@ -19,8 +19,8 @@ public override void Cancel()
{
base.Cancel();
- this.pngConversionJob.Cancel();
- this.icoConversionJob.Cancel();
+ this.pngConversionJob?.Cancel();
+ this.icoConversionJob?.Cancel();
}
protected override void Initialize()
@@ -34,8 +34,7 @@ protected override void Initialize()
// Generate intermediate file path.
string fileName = Path.GetFileName(this.OutputFilePath);
- string tempPath = Path.GetTempPath();
- this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".png");
+ this.intermediateFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + ".png");
// Convert input in png file to send it to ffmpeg for the ico conversion.
ConversionPreset intermediatePreset = new ConversionPreset("To compatible image", OutputType.Png, this.ConversionPreset.InputTypes.ToArray());
@@ -56,29 +55,34 @@ protected override void Convert()
throw new Exception("The conversion preset must be valid.");
}
- Diagnostics.Debug.Log(string.Empty);
- Diagnostics.Debug.Log("Convert image to PNG (intermediate format).");
- this.pngConversionJob.StartConversion();
-
- if (this.pngConversionJob.State != ConversionState.Done)
+ try
{
- this.ConversionFailed(this.pngConversionJob.ErrorMessage);
- return;
+ Diagnostics.Debug.Log(string.Empty);
+ Diagnostics.Debug.Log("Convert image to PNG (intermediate format).");
+ this.pngConversionJob.StartConversion();
+
+ if (this.pngConversionJob.State != ConversionState.Done)
+ {
+ this.ConversionFailed(this.pngConversionJob.ErrorMessage);
+ return;
+ }
+
+ Diagnostics.Debug.Log(string.Empty);
+ Diagnostics.Debug.Log("Convert png intermediate image to ICO.");
+ this.icoConversionJob.StartConversion();
+
+ if (this.icoConversionJob.State != ConversionState.Done)
+ {
+ this.ConversionFailed(this.icoConversionJob.ErrorMessage);
+ return;
+ }
}
-
- Diagnostics.Debug.Log(string.Empty);
- Diagnostics.Debug.Log("Convert png intermediate image to ICO.");
- this.icoConversionJob.StartConversion();
-
- if (this.icoConversionJob.State != ConversionState.Done)
+ finally
{
- this.ConversionFailed(this.icoConversionJob.ErrorMessage);
- return;
- }
-
- Diagnostics.Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ Diagnostics.Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
- File.Delete(this.intermediateFilePath);
+ this.DeleteFileIfExists(this.intermediateFilePath);
+ }
}
}
}
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs
index 312431f1..802c51b3 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs
@@ -3,6 +3,8 @@
namespace FileConverter.ConversionJobs
{
using System;
+ using System.Globalization;
+ using System.Reflection;
using FileConverter.Diagnostics;
using ImageMagick;
@@ -11,6 +13,16 @@ public class ConversionJob_ImageMagick : ConversionJob
{
private const float BaseDpiForPdfConversion = 200f;
private const int PdfSuperSamplingRatio = 1;
+ private const int MaxPdfPageCount = 250;
+ private const ulong MaxImagePixels = 250000000UL;
+ private const ulong MaxMagickMemoryBytes = 512UL * 1024UL * 1024UL;
+ private const ulong MaxMagickMapBytes = 1024UL * 1024UL * 1024UL;
+ private const ulong MaxMagickDiskBytes = 2048UL * 1024UL * 1024UL;
+ private const uint MaxMagickThreads = 4;
+ private const uint MaxMagickSeconds = 180;
+
+ private static readonly object ResourceLimitsLock = new object();
+ private static bool resourceLimitsApplied;
private bool isInputFilePdf;
private int pageCount;
@@ -29,6 +41,7 @@ protected override void Initialize()
string applicationDirectory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
MagickNET.SetGhostscriptDirectory(applicationDirectory);
+ ApplyImageMagickResourceLimits();
this.isInputFilePdf = System.IO.Path.GetExtension(this.InputFilePath).ToLowerInvariant() == ".pdf";
@@ -40,13 +53,16 @@ protected override void Initialize()
protected override int GetOutputFilesCount()
{
+ ApplyImageMagickResourceLimits();
+
if (System.IO.Path.GetExtension(this.InputFilePath).ToLowerInvariant() == ".pdf")
{
using (MagickImageCollection images = new MagickImageCollection())
{
MagickReadSettings settings = new MagickReadSettings();
settings.Density = new Density(1, 1);
- images.Read(this.InputFilePath);
+ images.Read(this.InputFilePath, settings);
+ ValidatePdfPageCount(images.Count);
return images.Count;
}
@@ -134,6 +150,7 @@ private void ConvertPdf()
Debug.Log($"Load pdf {this.InputFilePath} succeed.");
this.pageCount = images.Count;
+ ValidatePdfPageCount(this.pageCount);
this.UserState = Properties.Resources.ConversionStateConversion;
@@ -207,7 +224,7 @@ private void ConvertImage(MagickImage image, bool ignoreScale = false)
uint width = System.Math.Min(image.Width, maximumSize);
uint height = System.Math.Min(image.Height, maximumSize);
- Debug.Log($"Clamp size to maximum size of {width}x{width} (from {image.Width}x{image.Height} to {width}x{height}).");
+ Debug.Log($"Clamp size to maximum size of {width}x{height} (from {image.Width}x{image.Height} to {width}x{height}).");
image.Scale(width, height);
}
@@ -248,6 +265,58 @@ private void ConvertImage(MagickImage image, bool ignoreScale = false)
image.Progress -= this.Image_Progress;
}
+ private static void ApplyImageMagickResourceLimits()
+ {
+ if (resourceLimitsApplied)
+ {
+ return;
+ }
+
+ lock (ResourceLimitsLock)
+ {
+ if (resourceLimitsApplied)
+ {
+ return;
+ }
+
+ SetResourceLimit("Memory", MaxMagickMemoryBytes);
+ SetResourceLimit("Map", MaxMagickMapBytes);
+ SetResourceLimit("Disk", MaxMagickDiskBytes);
+ SetResourceLimit("Area", MaxImagePixels);
+ SetResourceLimit("Thread", MaxMagickThreads);
+ SetResourceLimit("Time", MaxMagickSeconds);
+ SetResourceLimit("ListLength", MaxPdfPageCount);
+ resourceLimitsApplied = true;
+ }
+ }
+
+ private static void SetResourceLimit(string propertyName, object value)
+ {
+ try
+ {
+ PropertyInfo property = typeof(ResourceLimits).GetProperty(propertyName);
+ if (property == null || !property.CanWrite)
+ {
+ return;
+ }
+
+ object typedValue = System.Convert.ChangeType(value, property.PropertyType, CultureInfo.InvariantCulture);
+ property.SetValue(null, typedValue, null);
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to set ImageMagick resource limit {propertyName}: {exception.Message}");
+ }
+ }
+
+ private static void ValidatePdfPageCount(int pages)
+ {
+ if (pages > MaxPdfPageCount)
+ {
+ throw new InvalidOperationException($"PDF conversion is limited to {MaxPdfPageCount} pages per file.");
+ }
+ }
+
private void Image_Progress(object sender, ProgressEventArgs eventArgs)
{
if (this.CancelIsRequested)
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Office.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Office.cs
index c9ca3e23..a01b2afa 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_Office.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_Office.cs
@@ -2,8 +2,16 @@
namespace FileConverter.ConversionJobs
{
+ using System;
+ using System.Globalization;
+ using System.Reflection;
+
+ using FileConverter.Diagnostics;
+
public abstract class ConversionJob_Office : ConversionJob
{
+ private const int MsoAutomationSecurityForceDisable = 3;
+
protected ConversionJob_Office() : base()
{
}
@@ -28,6 +36,19 @@ protected abstract ApplicationName Application
protected override bool IsCancelable() => false;
+ protected void HardenOfficeApplicationInstance(object officeApplication)
+ {
+ if (officeApplication == null)
+ {
+ return;
+ }
+
+ this.TrySetOfficeApplicationProperty(officeApplication, "AutomationSecurity", MsoAutomationSecurityForceDisable);
+ this.TrySetOfficeApplicationProperty(officeApplication, "EnableEvents", false);
+ this.TrySetOfficeApplicationProperty(officeApplication, "DisplayAlerts", 0);
+ this.TrySetOfficeApplicationProperty(officeApplication, "AskToUpdateLinks", false);
+ }
+
protected override void Initialize()
{
base.Initialize();
@@ -65,5 +86,34 @@ protected override void OnConversionFailed()
protected abstract void InitializeOfficeApplicationInstanceIfNecessary();
protected abstract void ReleaseOfficeApplicationInstanceIfNeeded();
+
+ private void TrySetOfficeApplicationProperty(object officeApplication, string propertyName, object value)
+ {
+ try
+ {
+ PropertyInfo property = officeApplication.GetType().GetProperty(propertyName);
+ if (property == null || !property.CanWrite)
+ {
+ return;
+ }
+
+ object typedValue = this.ConvertValue(value, property.PropertyType);
+ property.SetValue(officeApplication, typedValue, null);
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Could not set Office automation property {propertyName}: {exception.Message}");
+ }
+ }
+
+ private object ConvertValue(object value, Type propertyType)
+ {
+ if (propertyType.IsEnum)
+ {
+ return Enum.ToObject(propertyType, value);
+ }
+
+ return System.Convert.ChangeType(value, propertyType, CultureInfo.InvariantCulture);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs b/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs
index 110c7fc7..e590e74a 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_PowerPoint.cs
@@ -69,8 +69,7 @@ protected override void Initialize()
{
// Generate intermediate file path.
string fileName = Path.GetFileNameWithoutExtension(this.InputFilePath);
- string tempPath = Path.GetTempPath();
- this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".pdf");
+ this.intermediateFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + ".pdf");
ConversionPreset intermediatePreset = new ConversionPreset("Pdf to image", this.ConversionPreset, "pdf");
this.pdf2ImageConversionJob = ConversionJobFactory.Create(intermediatePreset, this.intermediateFilePath);
@@ -93,45 +92,52 @@ protected override void Convert()
return;
}
- this.UserState = Properties.Resources.ConversionStateConversion;
-
- Debug.Log("Convert PowerPoint document to pdf.");
- this.document.ExportAsFixedFormat(this.intermediateFilePath, PowerPoint.Enums.PpFixedFormatType.ppFixedFormatTypePDF);
-
- Debug.Log($"Close PowerPoint document '{this.InputFilePath}'.");
- this.document.Close();
- this.document = null;
+ try
+ {
+ this.UserState = Properties.Resources.ConversionStateConversion;
- this.ReleaseOfficeApplicationInstanceIfNeeded();
+ Debug.Log("Convert PowerPoint document to pdf.");
+ this.document.ExportAsFixedFormat(this.intermediateFilePath, PowerPoint.Enums.PpFixedFormatType.ppFixedFormatTypePDF);
+ }
+ finally
+ {
+ this.CloseDocumentIfNeeded();
+ this.ReleaseOfficeApplicationInstanceIfNeeded();
+ }
if (this.pdf2ImageConversionJob != null)
{
- if (!System.IO.File.Exists(this.intermediateFilePath))
+ Task updateProgress = null;
+ try
{
- this.ConversionFailed(Properties.Resources.ErrorCantFindOutputFiles);
- return;
- }
+ if (!System.IO.File.Exists(this.intermediateFilePath))
+ {
+ this.ConversionFailed(Properties.Resources.ErrorCantFindOutputFiles);
+ return;
+ }
- Task updateProgress = this.UpdateProgress();
+ updateProgress = this.UpdateProgress();
- Debug.Log("Convert pdf to images.");
+ Debug.Log("Convert pdf to images.");
- this.pdf2ImageConversionJob.StartConversion();
+ this.pdf2ImageConversionJob.StartConversion();
- if (this.pdf2ImageConversionJob.State != ConversionState.Done)
- {
- this.ConversionFailed(this.pdf2ImageConversionJob.ErrorMessage);
- return;
+ if (this.pdf2ImageConversionJob.State != ConversionState.Done)
+ {
+ this.ConversionFailed(this.pdf2ImageConversionJob.ErrorMessage);
+ return;
+ }
}
-
- if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ finally
{
- Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ updateProgress?.Wait();
- File.Delete(this.intermediateFilePath);
+ if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ {
+ Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ this.DeleteFileIfExists(this.intermediateFilePath);
+ }
}
-
- updateProgress.Wait();
}
}
@@ -145,6 +151,7 @@ protected override void InitializeOfficeApplicationInstanceIfNecessary()
// Initialize PowerPoint application.
Debug.Log("Instantiate PowerPoint application via interop.");
this.application = new PowerPoint.Application();
+ this.HardenOfficeApplicationInstance(this.application);
}
protected override void ReleaseOfficeApplicationInstanceIfNeeded()
@@ -161,15 +168,11 @@ protected override void ReleaseOfficeApplicationInstanceIfNeeded()
private async Task UpdateProgress()
{
- while (this.pdf2ImageConversionJob.State != ConversionState.Done &&
+ while (this.pdf2ImageConversionJob != null &&
+ this.pdf2ImageConversionJob.State != ConversionState.Done &&
this.pdf2ImageConversionJob.State != ConversionState.Failed)
{
- if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress)
- {
- this.Progress = this.pdf2ImageConversionJob.Progress;
- }
-
- if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress)
+ if (this.pdf2ImageConversionJob.State == ConversionState.InProgress)
{
this.Progress = this.pdf2ImageConversionJob.Progress;
this.UserState = this.pdf2ImageConversionJob.UserState;
@@ -205,5 +208,25 @@ private bool TryLoadDocumentIfNecessary()
return this.document != null;
}
+
+ private void CloseDocumentIfNeeded()
+ {
+ if (this.document == null)
+ {
+ return;
+ }
+
+ try
+ {
+ Debug.Log($"Close PowerPoint document '{this.InputFilePath}'.");
+ this.document.Close();
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to close PowerPoint document '{this.InputFilePath}': {exception.Message}.");
+ }
+
+ this.document = null;
+ }
}
}
diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs
index cf1360d8..8886b98e 100644
--- a/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs
+++ b/Application/FileConverter/ConversionJobs/ConversionJob_Word.cs
@@ -70,8 +70,7 @@ protected override void Initialize()
{
// Generate intermediate file path.
string fileName = Path.GetFileNameWithoutExtension(this.InputFilePath);
- string tempPath = Path.GetTempPath();
- this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".pdf");
+ this.intermediateFilePath = PathHelpers.GenerateTemporaryFilePath(fileName + ".pdf");
ConversionPreset intermediatePreset = new ConversionPreset("Pdf to image", this.ConversionPreset, "pdf");
this.pdf2ImageConversionJob = ConversionJobFactory.Create(intermediatePreset, this.intermediateFilePath);
@@ -94,59 +93,66 @@ protected override void Convert()
return;
}
- // Make this document the active document.
- this.document.Activate();
-
- this.UserState = Properties.Resources.ConversionStateConversion;
-
- Debug.Log("Convert word document to pdf.");
- // this.document.ExportAsFixedFormat(this.intermediateFilePath, Word.WdExportFormat.wdExportFormatPDF);
- this.document.ExportAsFixedFormat(this.intermediateFilePath,
- Word.Enums.WdExportFormat.wdExportFormatPDF,
- false,
- Word.Enums.WdExportOptimizeFor.wdExportOptimizeForPrint,
- Word.Enums.WdExportRange.wdExportAllDocument,
- 1, 1,
- Word.Enums.WdExportItem.wdExportDocumentContent,
- true,
- true,
- Word.Enums.WdExportCreateBookmarks.wdExportCreateHeadingBookmarks,
- true);
+ try
+ {
+ // Make this document the active document.
+ this.document.Activate();
- Debug.Log($"Close word document '{this.InputFilePath}'.");
- this.document.Close(Word.Enums.WdSaveOptions.wdDoNotSaveChanges);
- this.document = null;
+ this.UserState = Properties.Resources.ConversionStateConversion;
- this.ReleaseOfficeApplicationInstanceIfNeeded();
+ Debug.Log("Convert word document to pdf.");
+ // this.document.ExportAsFixedFormat(this.intermediateFilePath, Word.WdExportFormat.wdExportFormatPDF);
+ this.document.ExportAsFixedFormat(this.intermediateFilePath,
+ Word.Enums.WdExportFormat.wdExportFormatPDF,
+ false,
+ Word.Enums.WdExportOptimizeFor.wdExportOptimizeForPrint,
+ Word.Enums.WdExportRange.wdExportAllDocument,
+ 1, 1,
+ Word.Enums.WdExportItem.wdExportDocumentContent,
+ true,
+ true,
+ Word.Enums.WdExportCreateBookmarks.wdExportCreateHeadingBookmarks,
+ true);
+ }
+ finally
+ {
+ this.CloseDocumentIfNeeded();
+ this.ReleaseOfficeApplicationInstanceIfNeeded();
+ }
if (this.pdf2ImageConversionJob != null)
{
- if (!System.IO.File.Exists(this.intermediateFilePath))
+ Task updateProgress = null;
+ try
{
- this.ConversionFailed(Properties.Resources.ErrorCantFindOutputFiles);
- return;
- }
+ if (!System.IO.File.Exists(this.intermediateFilePath))
+ {
+ this.ConversionFailed(Properties.Resources.ErrorCantFindOutputFiles);
+ return;
+ }
- Task updateProgress = this.UpdateProgress();
+ updateProgress = this.UpdateProgress();
- Debug.Log("Convert pdf to images.");
+ Debug.Log("Convert pdf to images.");
- this.pdf2ImageConversionJob.StartConversion();
+ this.pdf2ImageConversionJob.StartConversion();
- if (this.pdf2ImageConversionJob.State != ConversionState.Done)
- {
- this.ConversionFailed(this.pdf2ImageConversionJob.ErrorMessage);
- return;
+ if (this.pdf2ImageConversionJob.State != ConversionState.Done)
+ {
+ this.ConversionFailed(this.pdf2ImageConversionJob.ErrorMessage);
+ return;
+ }
}
-
- if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ finally
{
- Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ updateProgress?.Wait();
- File.Delete(this.intermediateFilePath);
+ if (!string.IsNullOrEmpty(this.intermediateFilePath))
+ {
+ Debug.Log($"Delete intermediate file {this.intermediateFilePath}.");
+ this.DeleteFileIfExists(this.intermediateFilePath);
+ }
}
-
- updateProgress.Wait();
}
}
@@ -163,6 +169,7 @@ protected override void InitializeOfficeApplicationInstanceIfNecessary()
{
Visible = false
};
+ this.HardenOfficeApplicationInstance(this.application);
}
protected override void ReleaseOfficeApplicationInstanceIfNeeded()
@@ -179,15 +186,11 @@ protected override void ReleaseOfficeApplicationInstanceIfNeeded()
private async Task UpdateProgress()
{
- while (this.pdf2ImageConversionJob.State != ConversionState.Done &&
+ while (this.pdf2ImageConversionJob != null &&
+ this.pdf2ImageConversionJob.State != ConversionState.Done &&
this.pdf2ImageConversionJob.State != ConversionState.Failed)
{
- if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress)
- {
- this.Progress = this.pdf2ImageConversionJob.Progress;
- }
-
- if (this.pdf2ImageConversionJob != null && this.pdf2ImageConversionJob.State == ConversionState.InProgress)
+ if (this.pdf2ImageConversionJob.State == ConversionState.InProgress)
{
this.Progress = this.pdf2ImageConversionJob.Progress;
this.UserState = this.pdf2ImageConversionJob.UserState;
@@ -223,5 +226,25 @@ private bool TryLoadDocumentIfNecessary()
return this.document != null;
}
+
+ private void CloseDocumentIfNeeded()
+ {
+ if (this.document == null)
+ {
+ return;
+ }
+
+ try
+ {
+ Debug.Log($"Close word document '{this.InputFilePath}'.");
+ this.document.Close(Word.Enums.WdSaveOptions.wdDoNotSaveChanges);
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to close word document '{this.InputFilePath}': {exception.Message}.");
+ }
+
+ this.document = null;
+ }
}
}
diff --git a/Application/FileConverter/ConversionPreset/ConversionPreset.cs b/Application/FileConverter/ConversionPreset/ConversionPreset.cs
index d06d203e..a0eda4fc 100644
--- a/Application/FileConverter/ConversionPreset/ConversionPreset.cs
+++ b/Application/FileConverter/ConversionPreset/ConversionPreset.cs
@@ -321,7 +321,14 @@ public void RemoveInputType(string inputType)
public string GenerateOutputFilePath(string inputFilePath, int numberIndex, int numberMax)
{
- return PathHelpers.GenerateFilePathFromTemplate(inputFilePath, this.OutputType, this.OutputFileNameTemplate, numberIndex, numberMax);
+ return PathHelpers.GenerateFilePathFromTemplate(
+ inputFilePath,
+ this.OutputType,
+ this.OutputFileNameTemplate,
+ numberIndex,
+ numberMax,
+ this.ShortName,
+ this.FullName);
}
public void SetSettingsValue(string settingsKey, string value)
diff --git a/Application/FileConverter/Diagnostics/Debug.cs b/Application/FileConverter/Diagnostics/Debug.cs
index 6dda0063..42e21dc5 100644
--- a/Application/FileConverter/Diagnostics/Debug.cs
+++ b/Application/FileConverter/Diagnostics/Debug.cs
@@ -56,6 +56,18 @@ public static int FirstErrorCode
public static DiagnosticsData[] Data => Debug.diagnosticsDataById.Values.ToArray();
+ public static string DiagnosticsFolderPath => Debug.diagnosticsFolderPath;
+
+ public static string AllContent
+ {
+ get
+ {
+ return string.Join(
+ Environment.NewLine + Environment.NewLine,
+ Debug.Data.Select(data => $"[{data.Name}]{Environment.NewLine}{data.Content}"));
+ }
+ }
+
public static void Log(string message)
{
Debug.LogInternal(error: false, message, ConsoleColor.White);
diff --git a/Application/FileConverter/FileConverter.csproj b/Application/FileConverter/FileConverter.csproj
index fd50ac17..95fbe6ac 100644
--- a/Application/FileConverter/FileConverter.csproj
+++ b/Application/FileConverter/FileConverter.csproj
@@ -1,424 +1,426 @@
-
-
-
-
- Debug
- AnyCPU
- {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}
- WinExe
- Properties
- FileConverter
- FileConverter
- v4.8
- 512
- {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- 4
-
-
-
-
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- TRACE;DEBUG
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
- x64
- bin\x64\Debug\
- TRACE;DEBUG
-
-
- x64
- bin\x64\Release\
-
-
- Resources\ApplicationIcon.ico
-
-
- true
- bin\x86\Debug\
- TRACE;DEBUG;BUILD32
- full
- x86
- prompt
- MinimumRecommendedRules.ruleset
- true
-
-
- bin\x86\Release\
- TRACE;BUILD32
- true
- pdbonly
- x86
- prompt
- MinimumRecommendedRules.ruleset
- true
-
-
-
- ..\..\Middleware\Markdown.Xaml.dll
-
-
-
- False
- ..\..\Middleware\Ripper.dll
-
-
-
-
-
-
-
-
-
-
-
- 4.0
-
-
-
-
-
-
-
- ..\..\Middleware\yeti.mmedia.dll
-
-
-
-
- MSBuild:Compile
- Designer
-
-
-
- ConversionJobControl.xaml
-
-
-
- EncodingQualitySliderControl.xaml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Resources.en.resx
- True
- True
- True
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- HelpWindow.xaml
-
-
- DiagnosticsWindow.xaml
-
-
-
-
-
- SettingsWindow.xaml
-
-
-
- UpgradeWindow.xaml
-
-
-
- Designer
- MSBuild:Compile
-
-
- Designer
- MSBuild:Compile
-
-
- Designer
- MSBuild:Compile
-
-
- Designer
- MSBuild:Compile
-
-
- MSBuild:Compile
- Designer
-
-
- Application.xaml
- Code
-
-
-
-
-
- MainWindow.xaml
- Code
-
-
- MSBuild:Compile
- Designer
-
-
- MSBuild:Compile
- Designer
-
-
- Designer
- MSBuild:Compile
-
-
- Designer
- MSBuild:Compile
-
-
- Designer
- MSBuild:Compile
-
-
-
-
-
- true
-
-
- Code
- true
-
-
- True
- True
- Resources.resx
-
-
- True
- Settings.settings
- True
-
-
-
-
-
-
-
-
-
-
- PublicResXFileCodeGenerator
- Resources.en.Designer.cs
- Designer
-
-
-
-
-
-
-
-
-
-
-
- PublicResXFileCodeGenerator
- Resources.Designer.cs
- Designer
-
-
-
-
-
-
-
-
-
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {0c44ca69-42d6-4357-bdfd-83069d1aba2f}
- FileConverterExtension
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 8.4.0
-
-
- 14.10.2
-
-
- 10.0.3
-
-
- 1.1.135
-
-
- 1.7.4.11
-
-
- 1.7.4.11
-
-
- 1.7.4.11
-
-
- 2.7.2
-
-
- 2.0.2
-
-
-
-
+
+
+
+
+ Debug
+ AnyCPU
+ {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}
+ WinExe
+ Properties
+ FileConverter
+ FileConverter
+ v4.8
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+
+
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ TRACE;DEBUG
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ x64
+ bin\x64\Debug\
+ TRACE;DEBUG
+
+
+ x64
+ bin\x64\Release\
+
+
+ Resources\ApplicationIcon.ico
+
+
+ true
+ bin\x86\Debug\
+ TRACE;DEBUG;BUILD32
+ full
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+ true
+
+
+ bin\x86\Release\
+ TRACE;BUILD32
+ true
+ pdbonly
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+ true
+
+
+
+ ..\..\Middleware\Markdown.Xaml.dll
+
+
+
+ False
+ ..\..\Middleware\Ripper.dll
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ ..\..\Middleware\yeti.mmedia.dll
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+ ConversionJobControl.xaml
+
+
+
+ EncodingQualitySliderControl.xaml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resources.en.resx
+ True
+ True
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HelpWindow.xaml
+
+
+ DiagnosticsWindow.xaml
+
+
+
+
+
+ SettingsWindow.xaml
+
+
+
+ UpgradeWindow.xaml
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Application.xaml
+ Code
+
+
+
+
+
+ MainWindow.xaml
+ Code
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+
+
+
+ true
+
+
+ Code
+ true
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+
+
+
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ Resources.en.Designer.cs
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {0c44ca69-42d6-4357-bdfd-83069d1aba2f}
+ FileConverterExtension
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 8.4.0
+
+
+ 14.14.0
+
+
+ 10.0.3
+
+
+ 1.1.135
+
+
+ 1.7.4.11
+
+
+ 1.7.4.11
+
+
+ 1.7.4.11
+
+
+ 2.7.2
+
+
+ 2.0.2
+
+
+
+
copy /Y "$(SolutionDir)Middleware\ffmpeg\ffmpeg.exe" "$(TargetDir)ffmpeg.exe"
copy /Y "$(SolutionDir)Middleware\gs\gsdll64.dll" "$(TargetDir)gsdll64.dll"
copy /Y "$(SolutionDir)Middleware\gs\gswin64c.exe" "$(TargetDir)gswin64c.exe"
@@ -426,13 +428,13 @@ copy /Y "$(ProjectDir)Settings.default.xml" "$(TargetDir)Settings.default.xml"
robocopy $(TargetDir) $(TargetDir)\Languages "$(TargetName).resources.dll" /CREATE /S /XD Languages /IS /IT /NFL /NDL /NJH /NJS /NC /NS /NP
if %25errorlevel%25 leq 1 exit 0 else exit %25errorlevel%25
robocopy $(TargetDir) $(TargetDir)\Languages "$(TargetName).resources.dll" /MOVE /S /XD Languages /XL /IS /IT /NFL /NDL /NJH /NJS /NC /NS /NP
-if %25errorlevel%25 leq 1 exit 0 else exit %25errorlevel%25
-
+if %25errorlevel%25 leq 1 exit 0 else exit %25errorlevel%25
+
-
\ No newline at end of file
+ -->
+
diff --git a/Application/FileConverter/Helpers.cs b/Application/FileConverter/Helpers.cs
index 1112e10f..8e8c85de 100644
--- a/Application/FileConverter/Helpers.cs
+++ b/Application/FileConverter/Helpers.cs
@@ -1,357 +1,405 @@
-// License: http://www.gnu.org/licenses/gpl.html GPL version 3.
-
-namespace FileConverter
-{
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.IO;
- using System.Reflection;
- using System.Threading;
-
- using FileConverter.ConversionJobs;
- using FileConverter.Services;
-
- using SharpShell.Helpers;
-
- using Microsoft.Win32;
- using CommunityToolkit.Mvvm.DependencyInjection;
-
- public static class Helpers
- {
- public static readonly string[] CompatibleInputExtensions = {
- "3gp","3gpp","aac","aiff","ape","arw","avi","avif","bik","bmp","cda","cr2","dds","dng","doc","docx",
- "exr","flac","flv","gif","heic","ico","jfif","jpg","jpeg","m4a","m4b","m4v","mkv","mov","mp3","mp4",
- "mpg","mpeg","nef","odp","ods","odt","oga","ogg","ogv","opus","pdf","png","ppt","pptx","psd",
- "raf", "rm","svg","tga","tif","tiff", "ts", "vob","wav","webm","webp","wma","wmv","xls","xlsx"
- };
-
- public static string GetExtensionCategory(string extension)
- {
- switch (extension)
- {
- case "aac":
- case "aiff":
- case "ape":
- case "cda":
- case "flac":
- case "mp3":
- case "m4a":
- case "m4b":
- case "oga":
- case "ogg":
- case "opus":
- case "wav":
- case "wma":
- return InputCategoryNames.Audio;
-
- case "3gp":
- case "3gpp":
- case "avi":
- case "bik":
- case "flv":
- case "m4v":
- case "mp4":
- case "mpg":
- case "mpeg":
- case "mov":
- case "mkv":
- case "ogv":
- case "rm":
- case "ts":
- case "vob":
- case "webm":
- case "wmv":
- return InputCategoryNames.Video;
-
- case "arw":
- case "avif":
- case "bmp":
- case "cr2":
- case "dds":
- case "dng":
- case "exr":
- case "heic":
- case "ico":
- case "jfif":
- case "jpg":
- case "jpeg":
- case "nef":
- case "png":
- case "psd":
- case "raf":
- case "tga":
- case "tif":
- case "tiff":
- case "svg":
- case "xcf":
- case "webp":
- return InputCategoryNames.Image;
-
- case "gif":
- return InputCategoryNames.AnimatedImage;
-
- case "pdf":
- case "doc":
- case "docx":
- case "ppt":
- case "pptx":
- case "odp":
- case "ods":
- case "odt":
- case "xls":
- case "xlsx":
- return InputCategoryNames.Document;
- }
-
- return InputCategoryNames.Misc;
- }
-
- public static bool RegisterShellExtension(string shellExtensionPath)
- {
- if (!Application.IsInAdmininstratorPrivileges)
- {
- Diagnostics.Debug.LogError("File Converter needs administrator privileges to register the shell extension.");
- return false;
- }
-
- if (!File.Exists(shellExtensionPath))
- {
- Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists.");
- return false;
- }
-
- Diagnostics.Debug.Log($"Install and register shell extension: {shellExtensionPath}.");
-
- var regasm = new RegAsm();
- var success = regasm.Register64(shellExtensionPath, true);
- if (success)
- {
- Diagnostics.Debug.Log($"{shellExtensionPath} installed and registered.");
- Diagnostics.Debug.Log(regasm.StandardOutput);
- return true;
- }
- else
- {
- Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register.");
- Diagnostics.Debug.LogError(regasm.StandardError);
- return false;
- }
- }
-
- public static bool UnregisterExtension(string shellExtensionPath)
- {
- if (!Application.IsInAdmininstratorPrivileges)
- {
- Diagnostics.Debug.LogError("File Converter needs administrator privileges to unregister the shell extension.");
- return false;
- }
-
- if (!File.Exists(shellExtensionPath))
- {
- Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists.");
- return false;
- }
-
- Diagnostics.Debug.Log($"Unregister and uninstall shell extension: {shellExtensionPath}.");
-
- var regasm = new RegAsm();
- var success = regasm.Unregister64(shellExtensionPath);
- if (success)
- {
- Diagnostics.Debug.Log($"{shellExtensionPath} uninstalled.");
- Diagnostics.Debug.Log(regasm.StandardOutput);
- return true;
- }
- else
- {
- Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to uninstall.");
- Diagnostics.Debug.LogError(regasm.StandardError);
- return false;
- }
- }
-
- public static IEnumerable GetSupportedCultures()
- {
- // Get all cultures.
- CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
-
- // Find the location where application installed.
- string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path));
-
- // Return all culture for which satellite folder found with culture code.
- foreach (CultureInfo cultureInfo in cultures)
- {
- if (!string.IsNullOrEmpty(cultureInfo.Name) && Directory.Exists(Path.Combine(exeLocation, "Languages", cultureInfo.Name)))
- {
- yield return cultureInfo;
- }
- }
- }
-
- public static bool IsOutputTypeCompatibleWithCategory(OutputType outputType, string category)
- {
- if (category == InputCategoryNames.Misc)
- {
- // Misc category contains unsorted input extensions, so we consider that they are compatible to be tolerant.
- return true;
- }
-
- switch (outputType)
- {
- case OutputType.Aac:
- case OutputType.Flac:
- case OutputType.Mp3:
- case OutputType.Ogg:
- case OutputType.Wav:
- return category == InputCategoryNames.Audio || category == InputCategoryNames.Video;
-
- case OutputType.Avi:
- case OutputType.Mkv:
- case OutputType.Mp4:
- case OutputType.Ogv:
- case OutputType.Webm:
- return category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage;
-
- case OutputType.Avif:
- case OutputType.Ico:
- case OutputType.Jpg:
- case OutputType.Png:
- case OutputType.Webp:
- return category == InputCategoryNames.Image || category == InputCategoryNames.Document || category == InputCategoryNames.AnimatedImage;
-
- case OutputType.Gif:
- return category == InputCategoryNames.Image || category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage;
-
- case OutputType.Pdf:
- return category == InputCategoryNames.Image || category == InputCategoryNames.Document;
-
- default:
- return false;
- }
- }
-
- public static Thread InstantiateThread(string name, ThreadStart threadStart)
- {
- ISettingsService settingsService = Ioc.Default.GetRequiredService();
- CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage;
-
- Thread thread = new Thread(threadStart);
- thread.Name = name;
-
- if (currentCulture != null)
- {
- thread.CurrentCulture = currentCulture;
- thread.CurrentUICulture = currentCulture;
- }
-
- return thread;
- }
-
- public static Thread InstantiateThread(string name, ParameterizedThreadStart parameterizedThreadStart)
- {
- ISettingsService settingsService = Ioc.Default.GetRequiredService();
- CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage;
-
- Thread thread = new Thread(parameterizedThreadStart);
- thread.Name = name;
-
- if (currentCulture != null)
- {
- thread.CurrentCulture = currentCulture;
- thread.CurrentUICulture = currentCulture;
- }
-
- return thread;
- }
-
- ///
- /// Check whether Microsoft office is available or not.
- ///
- /// The office application name.
- /// Returns true if Office is installed on the computer.
- /// source: http://stackoverflow.com/questions/3266675/how-to-detect-installed-version-of-ms-office/3267832#3267832
- /// source: http://www.codeproject.com/Articles/26520/Getting-Office-s-Version
- public static bool IsMicrosoftOfficeApplicationAvailable(ConversionJobs.ConversionJob_Office.ApplicationName application)
- {
- string registryKeyPattern = @"Software\Microsoft\Windows\CurrentVersion\App Paths\";
- switch (application)
- {
- case ConversionJob_Office.ApplicationName.Word:
- registryKeyPattern += "winword.exe";
- break;
-
- case ConversionJob_Office.ApplicationName.PowerPoint:
- registryKeyPattern += "powerpnt.exe";
- break;
-
- case ConversionJob_Office.ApplicationName.Excel:
- registryKeyPattern += "excel.exe";
- break;
-
- case ConversionJob_Office.ApplicationName.None:
- return false;
- }
-
- // Looks inside CURRENT_USER.
- RegistryKey winwordKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(registryKeyPattern, false);
- if (winwordKey != null)
- {
- string winwordPath = winwordKey.GetValue(string.Empty).ToString();
- if (!string.IsNullOrEmpty(winwordPath))
- {
- return true;
- }
- }
-
- // If not found, looks inside LOCAL_MACHINE.
- winwordKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyPattern, false);
- if (winwordKey != null)
- {
- string winwordPath = winwordKey.GetValue(string.Empty).ToString();
- if (!string.IsNullOrEmpty(winwordPath))
- {
- return true;
- }
- }
-
- return false;
- }
-
- public static ConversionJob_Office.ApplicationName GetOfficeApplicationCompatibleWithExtension(string extension)
- {
- switch (extension)
- {
- case "doc":
- case "docx":
- case "odt":
- return ConversionJob_Office.ApplicationName.Word;
-
- case "ppt":
- case "pptx":
- case "odp":
- return ConversionJob_Office.ApplicationName.PowerPoint;
-
- case "ods":
- case "xls":
- case "xlsx":
- return ConversionJob_Office.ApplicationName.Excel;
- }
-
- return ConversionJob_Office.ApplicationName.None;
- }
-
- public static class InputCategoryNames
- {
- public const string Audio = "Audio";
- public const string Video = "Video";
- public const string Image = "Image";
- public const string AnimatedImage = "Animated Image";
- public const string Document = "Document";
-
- public const string Misc = "Misc";
- }
-
+// License: http://www.gnu.org/licenses/gpl.html GPL version 3.
+
+namespace FileConverter
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.IO;
+ using System.Reflection;
+ using System.Threading;
+
+ using FileConverter.ConversionJobs;
+ using FileConverter.Services;
+
+ using SharpShell.Helpers;
+
+ using Microsoft.Win32;
+ using CommunityToolkit.Mvvm.DependencyInjection;
+
+ public static class Helpers
+ {
+ public static readonly string[] CompatibleInputExtensions = {
+ "3gp","3gpp","aac","aiff","ape","arw","avi","avif","bik","bmp","cda","cr2","dds","dng","doc","docx",
+ "exr","flac","flv","gif","heic","ico","jfif","jpg","jpeg","m4a","m4b","m4v","mkv","mov","mp3","mp4",
+ "mpg","mpeg","nef","odp","ods","odt","oga","ogg","ogv","opus","pdf","png","ppt","pptx","psd",
+ "raf", "rm","svg","tga","tif","tiff", "ts", "vob","wav","webm","webp","wma","wmv","xls","xlsx"
+ };
+
+ public static string GetExtensionCategory(string extension)
+ {
+ switch (extension)
+ {
+ case "aac":
+ case "aiff":
+ case "ape":
+ case "cda":
+ case "flac":
+ case "mp3":
+ case "m4a":
+ case "m4b":
+ case "oga":
+ case "ogg":
+ case "opus":
+ case "wav":
+ case "wma":
+ return InputCategoryNames.Audio;
+
+ case "3gp":
+ case "3gpp":
+ case "avi":
+ case "bik":
+ case "flv":
+ case "m4v":
+ case "mp4":
+ case "mpg":
+ case "mpeg":
+ case "mov":
+ case "mkv":
+ case "ogv":
+ case "rm":
+ case "ts":
+ case "vob":
+ case "webm":
+ case "wmv":
+ return InputCategoryNames.Video;
+
+ case "arw":
+ case "avif":
+ case "bmp":
+ case "cr2":
+ case "dds":
+ case "dng":
+ case "exr":
+ case "heic":
+ case "ico":
+ case "jfif":
+ case "jpg":
+ case "jpeg":
+ case "nef":
+ case "png":
+ case "psd":
+ case "raf":
+ case "tga":
+ case "tif":
+ case "tiff":
+ case "svg":
+ case "xcf":
+ case "webp":
+ return InputCategoryNames.Image;
+
+ case "gif":
+ return InputCategoryNames.AnimatedImage;
+
+ case "pdf":
+ case "doc":
+ case "docx":
+ case "ppt":
+ case "pptx":
+ case "odp":
+ case "ods":
+ case "odt":
+ case "xls":
+ case "xlsx":
+ return InputCategoryNames.Document;
+ }
+
+ return InputCategoryNames.Misc;
+ }
+
+ public static bool RegisterShellExtension(string shellExtensionPath)
+ {
+ if (!Application.IsInAdmininstratorPrivileges)
+ {
+ Diagnostics.Debug.LogError("ZFileConverter needs administrator privileges to register the shell extension.");
+ return false;
+ }
+
+ if (!File.Exists(shellExtensionPath))
+ {
+ Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists.");
+ return false;
+ }
+
+ Diagnostics.Debug.Log($"Install and register shell extension: {shellExtensionPath}.");
+
+ var regasm = new RegAsm();
+ var success = regasm.Register64(shellExtensionPath, true);
+ if (success)
+ {
+ Diagnostics.Debug.Log($"{shellExtensionPath} installed and registered.");
+ Diagnostics.Debug.Log(regasm.StandardOutput);
+ return true;
+ }
+ else
+ {
+ Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register.");
+ Diagnostics.Debug.LogError(regasm.StandardError);
+ return false;
+ }
+ }
+
+ public static string GetDefaultShellExtensionPath()
+ {
+ string executablePath = Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path);
+ string executableFolder = Path.GetDirectoryName(executablePath);
+ return Path.Combine(executableFolder, "FileConverterExtension.dll");
+ }
+
+ public static bool RepairShellExtension(string shellExtensionPath)
+ {
+ if (!Application.IsInAdmininstratorPrivileges)
+ {
+ Diagnostics.Debug.LogError("ZFileConverter needs administrator privileges to repair the shell extension.");
+ return false;
+ }
+
+ if (!File.Exists(shellExtensionPath))
+ {
+ Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists.");
+ return false;
+ }
+
+ Diagnostics.Debug.Log($"Repair shell extension registration: {shellExtensionPath}.");
+
+ var regasm = new RegAsm();
+ if (regasm.Unregister64(shellExtensionPath))
+ {
+ Diagnostics.Debug.Log($"{shellExtensionPath} previous registration removed.");
+ Diagnostics.Debug.Log(regasm.StandardOutput);
+ }
+ else
+ {
+ Diagnostics.Debug.Log("Previous shell extension registration could not be removed. Continuing with fresh registration.");
+ Diagnostics.Debug.Log(regasm.StandardError);
+ }
+
+ bool success = regasm.Register64(shellExtensionPath, true);
+ if (success)
+ {
+ Diagnostics.Debug.Log($"{shellExtensionPath} repaired and registered.");
+ Diagnostics.Debug.Log(regasm.StandardOutput);
+ return true;
+ }
+
+ Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to register during repair.");
+ Diagnostics.Debug.LogError(regasm.StandardError);
+ return false;
+ }
+
+ public static bool UnregisterExtension(string shellExtensionPath)
+ {
+ if (!Application.IsInAdmininstratorPrivileges)
+ {
+ Diagnostics.Debug.LogError("ZFileConverter needs administrator privileges to unregister the shell extension.");
+ return false;
+ }
+
+ if (!File.Exists(shellExtensionPath))
+ {
+ Diagnostics.Debug.LogError($"Shell extension {shellExtensionPath} does not exists.");
+ return false;
+ }
+
+ Diagnostics.Debug.Log($"Unregister and uninstall shell extension: {shellExtensionPath}.");
+
+ var regasm = new RegAsm();
+ var success = regasm.Unregister64(shellExtensionPath);
+ if (success)
+ {
+ Diagnostics.Debug.Log($"{shellExtensionPath} uninstalled.");
+ Diagnostics.Debug.Log(regasm.StandardOutput);
+ return true;
+ }
+ else
+ {
+ Diagnostics.Debug.LogError(errorCode: 0x05, $"{shellExtensionPath} failed to uninstall.");
+ Diagnostics.Debug.LogError(regasm.StandardError);
+ return false;
+ }
+ }
+
+ public static IEnumerable GetSupportedCultures()
+ {
+ // Get all cultures.
+ CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
+
+ // Find the location where application installed.
+ string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path));
+
+ // Return all culture for which satellite folder found with culture code.
+ foreach (CultureInfo cultureInfo in cultures)
+ {
+ if (!string.IsNullOrEmpty(cultureInfo.Name) && Directory.Exists(Path.Combine(exeLocation, "Languages", cultureInfo.Name)))
+ {
+ yield return cultureInfo;
+ }
+ }
+ }
+
+ public static bool IsOutputTypeCompatibleWithCategory(OutputType outputType, string category)
+ {
+ if (category == InputCategoryNames.Misc)
+ {
+ // Misc category contains unsorted input extensions, so we consider that they are compatible to be tolerant.
+ return true;
+ }
+
+ switch (outputType)
+ {
+ case OutputType.Aac:
+ case OutputType.Flac:
+ case OutputType.Mp3:
+ case OutputType.Ogg:
+ case OutputType.Wav:
+ return category == InputCategoryNames.Audio || category == InputCategoryNames.Video;
+
+ case OutputType.Avi:
+ case OutputType.Mkv:
+ case OutputType.Mp4:
+ case OutputType.Ogv:
+ case OutputType.Webm:
+ return category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage;
+
+ case OutputType.Avif:
+ case OutputType.Ico:
+ case OutputType.Jpg:
+ case OutputType.Png:
+ case OutputType.Webp:
+ return category == InputCategoryNames.Image || category == InputCategoryNames.Document || category == InputCategoryNames.AnimatedImage;
+
+ case OutputType.Gif:
+ return category == InputCategoryNames.Image || category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage;
+
+ case OutputType.Pdf:
+ return category == InputCategoryNames.Image || category == InputCategoryNames.Document;
+
+ default:
+ return false;
+ }
+ }
+
+ public static Thread InstantiateThread(string name, ThreadStart threadStart)
+ {
+ ISettingsService settingsService = Ioc.Default.GetRequiredService();
+ CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage;
+
+ Thread thread = new Thread(threadStart);
+ thread.Name = name;
+
+ if (currentCulture != null)
+ {
+ thread.CurrentCulture = currentCulture;
+ thread.CurrentUICulture = currentCulture;
+ }
+
+ return thread;
+ }
+
+ public static Thread InstantiateThread(string name, ParameterizedThreadStart parameterizedThreadStart)
+ {
+ ISettingsService settingsService = Ioc.Default.GetRequiredService();
+ CultureInfo currentCulture = settingsService?.Settings?.ApplicationLanguage;
+
+ Thread thread = new Thread(parameterizedThreadStart);
+ thread.Name = name;
+
+ if (currentCulture != null)
+ {
+ thread.CurrentCulture = currentCulture;
+ thread.CurrentUICulture = currentCulture;
+ }
+
+ return thread;
+ }
+
+ ///
+ /// Check whether Microsoft office is available or not.
+ ///
+ /// The office application name.
+ /// Returns true if Office is installed on the computer.
+ /// source: http://stackoverflow.com/questions/3266675/how-to-detect-installed-version-of-ms-office/3267832#3267832
+ /// source: http://www.codeproject.com/Articles/26520/Getting-Office-s-Version
+ public static bool IsMicrosoftOfficeApplicationAvailable(ConversionJobs.ConversionJob_Office.ApplicationName application)
+ {
+ string registryKeyPattern = @"Software\Microsoft\Windows\CurrentVersion\App Paths\";
+ switch (application)
+ {
+ case ConversionJob_Office.ApplicationName.Word:
+ registryKeyPattern += "winword.exe";
+ break;
+
+ case ConversionJob_Office.ApplicationName.PowerPoint:
+ registryKeyPattern += "powerpnt.exe";
+ break;
+
+ case ConversionJob_Office.ApplicationName.Excel:
+ registryKeyPattern += "excel.exe";
+ break;
+
+ case ConversionJob_Office.ApplicationName.None:
+ return false;
+ }
+
+ // Looks inside CURRENT_USER.
+ RegistryKey winwordKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(registryKeyPattern, false);
+ if (winwordKey != null)
+ {
+ string winwordPath = winwordKey.GetValue(string.Empty).ToString();
+ if (!string.IsNullOrEmpty(winwordPath))
+ {
+ return true;
+ }
+ }
+
+ // If not found, looks inside LOCAL_MACHINE.
+ winwordKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKeyPattern, false);
+ if (winwordKey != null)
+ {
+ string winwordPath = winwordKey.GetValue(string.Empty).ToString();
+ if (!string.IsNullOrEmpty(winwordPath))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static ConversionJob_Office.ApplicationName GetOfficeApplicationCompatibleWithExtension(string extension)
+ {
+ switch (extension)
+ {
+ case "doc":
+ case "docx":
+ case "odt":
+ return ConversionJob_Office.ApplicationName.Word;
+
+ case "ppt":
+ case "pptx":
+ case "odp":
+ return ConversionJob_Office.ApplicationName.PowerPoint;
+
+ case "ods":
+ case "xls":
+ case "xlsx":
+ return ConversionJob_Office.ApplicationName.Excel;
+ }
+
+ return ConversionJob_Office.ApplicationName.None;
+ }
+
+ public static class InputCategoryNames
+ {
+ public const string Audio = "Audio";
+ public const string Video = "Video";
+ public const string Image = "Image";
+ public const string AnimatedImage = "Animated Image";
+ public const string Document = "Document";
+
+ public const string Misc = "Misc";
+ }
+
public enum HardwareAccelerationMode
{
Off,
diff --git a/Application/FileConverter/PathHelpers.cs b/Application/FileConverter/PathHelpers.cs
index ea0b9aa2..0197d1df 100644
--- a/Application/FileConverter/PathHelpers.cs
+++ b/Application/FileConverter/PathHelpers.cs
@@ -4,6 +4,7 @@ namespace FileConverter
{
using System;
using System.Collections.Generic;
+ using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
@@ -17,6 +18,35 @@ public static class PathHelpers
private static Regex filenameRegex = new Regex(@"[^\\]*", RegexOptions.RightToLeft);
private static Regex directoryRegex = new Regex(@"^(?\\\\[^\\/:*?""""<>|\r\n]+\\|[A-Za-z]:\\)(?:(?[^\\]*)\\)*");
private static Regex dateRegex = new Regex(@"\(d:(?[^)]*)\)");
+ private static Regex sourceCreatedDateRegex = new Regex(@"\((?:sourcecreated|sc):(?[^)]*)\)");
+ private static Regex sourceModifiedDateRegex = new Regex(@"\((?:sourcemodified|sm):(?[^)]*)\)");
+ private static Regex formattedNumberIndexRegex = new Regex(@"\(n:i:(?[^)]*)\)");
+ private static Regex formattedNumberCountRegex = new Regex(@"\(n:c:(?[^)]*)\)");
+ private static readonly HashSet ReservedDeviceNames = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "CON",
+ "PRN",
+ "AUX",
+ "NUL",
+ "COM1",
+ "COM2",
+ "COM3",
+ "COM4",
+ "COM5",
+ "COM6",
+ "COM7",
+ "COM8",
+ "COM9",
+ "LPT1",
+ "LPT2",
+ "LPT3",
+ "LPT4",
+ "LPT5",
+ "LPT6",
+ "LPT7",
+ "LPT8",
+ "LPT9"
+ };
public static bool IsPathDriveLetterValid(string path)
{
@@ -62,6 +92,65 @@ public static bool IsPathValid(string path)
return PathHelpers.pathRegex.IsMatch(path);
}
+ public static bool TryNormalizeGeneratedPath(string path, out string normalizedPath, out string errorMessage)
+ {
+ normalizedPath = path;
+ errorMessage = null;
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ errorMessage = "The generated output path is empty.";
+ return false;
+ }
+
+ if (!PathHelpers.IsPathValid(path))
+ {
+ errorMessage = "The generated output path is not a valid absolute Windows path.";
+ return false;
+ }
+
+ if (PathHelpers.ContainsRelativeDirectorySegment(path))
+ {
+ errorMessage = "The generated output path contains a relative directory segment.";
+ return false;
+ }
+
+ try
+ {
+ normalizedPath = System.IO.Path.GetFullPath(path);
+ }
+ catch (Exception exception)
+ {
+ errorMessage = $"The generated output path could not be normalized: {exception.Message}";
+ return false;
+ }
+
+ if (!PathHelpers.IsPathValid(normalizedPath))
+ {
+ errorMessage = "The normalized output path is not valid.";
+ return false;
+ }
+
+ if (PathHelpers.ContainsReservedDeviceName(normalizedPath))
+ {
+ errorMessage = "The generated output path contains a reserved Windows device name.";
+ return false;
+ }
+
+ return true;
+ }
+
+ public static string GetExtensionWithoutDot(string path)
+ {
+ string extension = System.IO.Path.GetExtension(path);
+ if (string.IsNullOrEmpty(extension) || extension.Length <= 1)
+ {
+ return string.Empty;
+ }
+
+ return extension.Substring(1).ToLowerInvariant();
+ }
+
public static string GetFileName(string path)
{
MatchCollection matchCollection = PathHelpers.filenameRegex.Matches(path);
@@ -110,6 +199,23 @@ public static string GenerateUniquePath(string path, params string[] blacklist)
return path;
}
+ public static string GenerateTemporaryFilePath(string preferredFileName)
+ {
+ string safeFileName = SanitizeFileSystemToken(System.IO.Path.GetFileName(preferredFileName));
+ if (string.IsNullOrWhiteSpace(safeFileName))
+ {
+ safeFileName = "conversion.tmp";
+ }
+
+ string tempFolder = System.IO.Path.Combine(
+ System.IO.Path.GetTempPath(),
+ "ZFileConverter",
+ Guid.NewGuid().ToString("N"));
+ System.IO.Directory.CreateDirectory(tempFolder);
+
+ return System.IO.Path.Combine(tempFolder, safeFileName);
+ }
+
public static bool CreateFolders(string filePath)
{
// Create output folders that doesn't already exist.
@@ -139,15 +245,27 @@ public static bool CreateFolders(string filePath)
return true;
}
- public static string GenerateFilePathFromTemplate(string inputFilePath, OutputType outputFileExtension, string outputFilePathTemplate, int numberIndex, int numberMax)
+ public static string GenerateFilePathFromTemplate(
+ string inputFilePath,
+ OutputType outputFileExtension,
+ string outputFilePathTemplate,
+ int numberIndex,
+ int numberMax,
+ string presetName = null,
+ string presetFullName = null)
{
if (string.IsNullOrEmpty(inputFilePath))
{
return "Invalid input file path (argument 0).";
}
- string inputExtension = System.IO.Path.GetExtension(inputFilePath).Substring(1);
- string inputPathWithoutExtension = inputFilePath.Substring(0, inputFilePath.Length - inputExtension.Length - 1);
+ string inputExtension = GetExtensionWithoutDot(inputFilePath);
+ string inputPathWithoutExtension = inputFilePath;
+ if (!string.IsNullOrEmpty(inputExtension))
+ {
+ inputPathWithoutExtension = inputFilePath.Substring(0, inputFilePath.Length - inputExtension.Length - 1);
+ }
+
string outputExtension = outputFileExtension.ToString().ToLowerInvariant();
if (string.IsNullOrEmpty(outputFilePathTemplate))
@@ -158,6 +276,11 @@ public static string GenerateFilePathFromTemplate(string inputFilePath, OutputTy
string fileName = System.IO.Path.GetFileName(inputPathWithoutExtension);
string parentDirectory = System.IO.Path.GetDirectoryName(inputPathWithoutExtension);
+ if (string.IsNullOrEmpty(parentDirectory))
+ {
+ parentDirectory = System.Environment.CurrentDirectory;
+ }
+
if (!parentDirectory.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()))
{
parentDirectory += System.IO.Path.DirectorySeparatorChar;
@@ -208,11 +331,129 @@ public static string GenerateFilePathFromTemplate(string inputFilePath, OutputTy
outputPath = outputPath.Replace("(n:i)", numberIndex.ToString());
outputPath = outputPath.Replace("(n:c)", numberMax.ToString());
- outputPath = dateRegex.Replace(outputPath, match => DateTime.Now.ToString(match.Groups["format"].Value).Replace('/', '-').Replace(':', '\''));
+ outputPath = formattedNumberIndexRegex.Replace(outputPath, match => FormatNumber(numberIndex, match.Groups["format"].Value));
+ outputPath = formattedNumberCountRegex.Replace(outputPath, match => FormatNumber(numberMax, match.Groups["format"].Value));
+
+ string safePresetName = SanitizeFileSystemToken(presetName);
+ string safePresetPath = SanitizePresetPath(presetFullName ?? presetName);
+ outputPath = outputPath.Replace("(preset)", safePresetName);
+ outputPath = outputPath.Replace("(presetname)", safePresetName);
+ outputPath = outputPath.Replace("(presetpath)", safePresetPath);
+
+ outputPath = dateRegex.Replace(outputPath, match => FormatDate(DateTime.Now, match.Groups["format"].Value));
+ outputPath = sourceCreatedDateRegex.Replace(outputPath, match => FormatDate(GetCreationTime(inputFilePath), match.Groups["format"].Value));
+ outputPath = sourceModifiedDateRegex.Replace(outputPath, match => FormatDate(GetLastWriteTime(inputFilePath), match.Groups["format"].Value));
outputPath += "." + outputExtension;
return outputPath;
}
+
+ private static string FormatNumber(int number, string format)
+ {
+ if (string.IsNullOrEmpty(format))
+ {
+ return number.ToString(NumberFormatInfo.InvariantInfo);
+ }
+
+ return number.ToString(format, NumberFormatInfo.InvariantInfo);
+ }
+
+ private static string FormatDate(DateTime date, string format)
+ {
+ if (string.IsNullOrEmpty(format))
+ {
+ return date.ToString(CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '\'');
+ }
+
+ return date.ToString(format, CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '\'');
+ }
+
+ private static DateTime GetCreationTime(string path)
+ {
+ if (!System.IO.File.Exists(path))
+ {
+ return DateTime.Now;
+ }
+
+ return System.IO.File.GetCreationTime(path);
+ }
+
+ private static DateTime GetLastWriteTime(string path)
+ {
+ if (!System.IO.File.Exists(path))
+ {
+ return DateTime.Now;
+ }
+
+ return System.IO.File.GetLastWriteTime(path);
+ }
+
+ private static bool ContainsRelativeDirectorySegment(string path)
+ {
+ string[] segments = path.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
+ for (int index = 0; index < segments.Length; index++)
+ {
+ if (segments[index] == "." || segments[index] == "..")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool ContainsReservedDeviceName(string path)
+ {
+ string root = System.IO.Path.GetPathRoot(path);
+ string pathWithoutRoot = string.IsNullOrEmpty(root) ? path : path.Substring(root.Length);
+ string[] segments = pathWithoutRoot.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+ for (int index = 0; index < segments.Length; index++)
+ {
+ string segment = segments[index].TrimEnd(' ', '.');
+ string nameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(segment);
+ if (ReservedDeviceNames.Contains(nameWithoutExtension))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static string SanitizePresetPath(string presetPath)
+ {
+ if (string.IsNullOrEmpty(presetPath))
+ {
+ return string.Empty;
+ }
+
+ string[] segments = presetPath.Split('/');
+ for (int index = 0; index < segments.Length; index++)
+ {
+ segments[index] = SanitizeFileSystemToken(segments[index]);
+ }
+
+ return string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), segments);
+ }
+
+ private static string SanitizeFileSystemToken(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return string.Empty;
+ }
+
+ char[] invalidFileNameChars = System.IO.Path.GetInvalidFileNameChars();
+ StringBuilder builder = new StringBuilder(value.Length);
+ for (int index = 0; index < value.Length; index++)
+ {
+ char character = value[index];
+ builder.Append(Array.IndexOf(invalidFileNameChars, character) >= 0 ? '_' : character);
+ }
+
+ return builder.ToString();
+ }
}
}
diff --git a/Application/FileConverter/Properties/AssemblyInfo.cs b/Application/FileConverter/Properties/AssemblyInfo.cs
index fb176737..5a514370 100644
--- a/Application/FileConverter/Properties/AssemblyInfo.cs
+++ b/Application/FileConverter/Properties/AssemblyInfo.cs
@@ -5,12 +5,12 @@
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
-[assembly: AssemblyTitle("FileConverter")]
-[assembly: AssemblyDescription("")]
+[assembly: AssemblyTitle("ZFileConverter")]
+[assembly: AssemblyDescription("A maintained, Explorer-first file conversion utility.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("FileConverter")]
-[assembly: AssemblyCopyright("Copyright © 2026")]
+[assembly: AssemblyCompany("ZaidNAlAsali")]
+[assembly: AssemblyProduct("ZFileConverter")]
+[assembly: AssemblyCopyright("Copyright © 2026 ZaidNAlAsali and File Converter contributors")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
diff --git a/Application/FileConverter/Properties/Resources.en.resx b/Application/FileConverter/Properties/Resources.en.resx
index 7338f453..8270ad3d 100644
--- a/Application/FileConverter/Properties/Resources.en.resx
+++ b/Application/FileConverter/Properties/Resources.en.resx
@@ -136,7 +136,7 @@
Audio
- Automatically check for updates when File Converter starts
+ Automatically check for updates when ZFileConverter starts
Automatically exit when all conversions are complete
@@ -172,10 +172,10 @@
Don't forget to reward my work if you like it :)
- The update will be downloaded in background, and installed once you exit File Converter.
+ The update will be downloaded in background, and installed once you exit ZFileConverter.
- Install when I exit File Converter
+ Install when I exit ZFileConverter
Encoding :
@@ -184,7 +184,7 @@
Encoding speed :
- File Converter is a shell extension.
+ ZFileConverter is a shell extension.
That means it is integrated into the Windows explorer.
@@ -217,10 +217,10 @@
None
- The update has been downloaded. Exit File Converter and install the update now.
+ The update has been downloaded. Exit ZFileConverter and install the update now.
- Install when I exit File Converter
+ Install when I exit ZFileConverter
Report an issue
@@ -268,6 +268,13 @@
...
(n:i): page number index
(n:c): total page count
+(n:i:D3): formatted page number index
+(n:c:D3): formatted page count
+(d:yyyy-MM-dd): current date
+(sc:yyyy-MM-dd): source file creation date
+(sm:yyyy-MM-dd): source file modified date
+(preset): selected preset name
+(presetpath): selected preset folder path
Special paths:
(p:d): my documents path
@@ -275,7 +282,7 @@ Special paths:
(p:v): my videos path
(p:p): my pictures path
-use maj for uppercase version
+Use uppercase tokens for uppercase values when available.
Output format
@@ -308,7 +315,7 @@ use maj for uppercase version
See change log ...
- File Converter Settings
+ ZFileConverter Settings
Settings
@@ -320,13 +327,13 @@ use maj for uppercase version
90° clockwise rotation
- An update for File Converter is available. It is strongly recommended that you install it as soon as possible!
+ An update for ZFileConverter is available. It is strongly recommended that you install it as soon as possible!
Upgrade download in progress...
- File Converter Updates
+ ZFileConverter Updates
Defines how 'file size' relates to 'video quality'. A lower value will give you a smaller file, a greater value give you higher video quality.
@@ -494,7 +501,7 @@ use maj for uppercase version
Stereo
- To use File Converter please open the explorer and right-click on any file you like to bring up the context menu where you will find File Converter commands (see the online documentation to see the list of compatible file formats).
+ To use ZFileConverter, open Explorer and right-click any supported file to bring up the context menu where you will find ZFileConverter commands (see the online documentation for compatible file formats).
**This program is free software**.
@@ -542,7 +549,7 @@ use maj for uppercase version
Microsoft Word must be installed in order to convert Word documents.
- Open File Converter website
+ Open ZFileConverter releases
Error during conversion job initialization.
diff --git a/Application/FileConverter/Properties/Resources.resx b/Application/FileConverter/Properties/Resources.resx
index 11c79680..c2b0e1ef 100644
--- a/Application/FileConverter/Properties/Resources.resx
+++ b/Application/FileConverter/Properties/Resources.resx
@@ -1,619 +1,625 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- About
-
-
- Allow you to choose what you want to do with your input files if the conversion succeed
-
-
- Action when conversion succeed
-
-
- New preset
-
-
- Application
-
-
- Audio
-
-
- Automatically check for updates when File Converter starts
-
-
- Automatically exit when all conversions finished
-
-
- Clamp to lowest power of 2 size
-
-
- Close
-
-
- Conversion Presets
-
-
- Files to convert
-
-
- Converted from
-
-
- Logs
-
-
- Logs
-
-
- Open documentation
-
-
- Donate
-
-
- Don't forget to reward my work if you like it :)
-
-
- The update will be downloaded in background, and installed once you exit File Converter.
-
-
- Install when I exit File Converter
-
-
- Encoding :
-
-
- Encoding speed :
-
-
- File Converter is a shell extension.
-
-
- That means it is integrated into the Windows explorer.
-
-
- To use File Converter please open the explorer and right-click on any file you like to bring up the context menu where you will find File Converter commands (see the readme file to see the list of compatible file formats).
-
-
- File name template
-
-
- Frames per second :
-
-
- Open GitHub project page
-
-
- help ?
-
-
- Input example
-
-
- Input formats
-
-
- Delete
-
-
- Move in an archive folder
-
-
- None
-
-
- The update has been downloaded. Exit File Converter and install the update now.
-
-
- Install when I exit File Converter
-
-
- Report an issue
-
-
- Move down selected preset
-
-
- Move up selected preset
-
-
- Constant bitrate encoding
-
-
- Variable bitrate encoding
-
-
- 90°
-
-
- 90° counter clockwise rotation
-
-
- Ok
-
-
- 180°
-
-
- 180° rotation
-
-
- C:\Music\Artist\Album\Song.wav
-
-
- Output
-
-
- (p): input file path
-(f): input filename
-(o): output extension type
-(i): input extension type
-(d0): input parent folder
-(d1): input sub parent folder
-...
-(n:i): page number index
-(n:c): total page count
-
-Special paths:
-(p:d): my documents path
-(p:m): my music path
-(p:v): my videos path
-(p:p): my pictures path
-
-use maj for uppercase version
-
-
- Output format
-
-
- Preset
-
-
- Preset Name
-
-
- Quality :
-
-
- Recommended bitrate range in blue
-
-
- Delete
-
-
- Rotate :
-
-
- Save
-
-
- Scale :
-
-
- See change log ...
-
-
- Settings
-
-
- Settings
-
-
- 270°
-
-
- 90° clockwise rotation
-
-
- An update for File Converter is available. It is strongly recommended that you install it as soon as possible!
-
-
- Upgrade download in progress...
-
-
- File Converter Updates
-
-
- Define the ratio 'file size' versus 'video quality'. A lower value will give you a smaller file, a greater value give you a better video quality.
-
-
- Faster
-
-
- Fast
-
-
- Medium
-
-
- Slower
-
-
- Slow
-
-
- Super Fast
-
-
- Define the ratio 'file size' versus 'compression duration'. A slow compression will give you a smaller file (for the same video quality) than a faster compression.
-
-
- Ultra Fast
-
-
- Very Fast
-
-
- Very Slow
-
-
- Video
-
-
- PCM signed 16-bit little-endian
-
-
- PCM signed 24-bit little-endian
-
-
- PCM signed 32-bit little-endian
-
-
- PCM signed 8-bit little-endian
-
-
- None
-
-
- No rotation
-
-
- Conversion
-
-
- Done
-
-
- Extraction
-
-
- Failed
-
-
- In queue
-
-
- Read input image
-
-
- Read document
-
-
- Animated Image
-
-
- Audio
-
-
- Document
-
-
- Image
-
-
- Video
-
-
- Waiting duration before application exit
-
-
- Language
-
-
- Maximum number of simultaneous conversions
-
-
- New preset
-
-
- ###Downloading change log ...
-
-
- Conversion Archives
-
-
- Canceled.
-
-
- Fail to find ffmpeg executable. You should try to reinstall the application.
-
-
- Audio CD track extraction failed.
-
-
- CD drive is not ready.
-
-
- Fail to launch ffmpeg.
-
-
- Fail to create output path folders.
-
-
- Fail to generate a unique output file path.
-
-
- Fail to read cd drive '{0}'.
-
-
- Fail to retrieve input path drive letter.
-
-
- Fail to retrieve track number from the cda input path.
-
-
- Fail to use the CD drive because it is opened.
-
-
- The input file type is not compatible with the selected output file type.
-
-
- Invalid output path generated by output file path template.
-
-
- Unsupported output format '{0}'.
-
-
- Channel count :
-
-
- Change the number of channels of the input file.
-
-
- Mono
-
-
- Same than input file
-
-
- Stereo
-
-
- **This program is free software**.
-
-
- You can redistribute it and/or modify it under the terms of the GNU General Public License.
-
-
- This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the GNU General Public License (available in the installation folder: `LICENSE.md`) for more details.
-
-
- The application is terminating.
-
-
- The application will automatically terminate in {0} seconds.
-
-
- The application will automatically terminate in 1 second.
-
-
- Misc
-
-
- Can't find the output file(s).
-
-
- The conversion job failed but there is an output file that does exists.
-
-
- Prepare conversion
-
-
- Microsoft Office must be installed in order to convert Office documents.
-
-
- Fail to open document with Microsoft Office (check if your licence/installation is valid).
-
-
- Microsoft Excel must be installed in order to convert Excel documents.
-
-
- Microsoft PowerPoint must be installed in order to convert PowerPoint documents.
-
-
- Microsoft Word must be installed in order to convert Word documents.
-
-
- Open File Converter website
-
-
- Error during conversion job initialization.
-
-
- New folder
-
-
- New folder
-
-
- Folder
-
-
- Folder Name
-
-
- Advanced mode
-
-
- Custom Arguments
-
-
- Custom FFMPEG command line arguments
-
-
- No preset selected
-
-
- Export
-
-
- Import
-
-
- Duplicate preset
-
-
- Cancel
-
-
- Error
-
-
- Can't load file converter user settings. Do you want to fall back to default settings ?
-
-
- Copy files in clipboard after conversion
-
-
- Hardware acceleration mode
-
-
- Advanced Media Framework (AMD)
-
-
- CUDA (Nvidia)
-
-
- Direct3D 11
-
-
- Direct3D 9/DXVA2
-
-
- Off
-
-
- OpenCL
-
-
- Vulkan
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ About
+
+
+ Allow you to choose what you want to do with your input files if the conversion succeed
+
+
+ Action when conversion succeed
+
+
+ New preset
+
+
+ Application
+
+
+ Audio
+
+
+ Automatically check for updates when ZFileConverter starts
+
+
+ Automatically exit when all conversions finished
+
+
+ Clamp to lowest power of 2 size
+
+
+ Close
+
+
+ Conversion Presets
+
+
+ Files to convert
+
+
+ Converted from
+
+
+ Logs
+
+
+ Logs
+
+
+ Open documentation
+
+
+ Donate
+
+
+ Don't forget to reward my work if you like it :)
+
+
+ The update will be downloaded in background, and installed once you exit ZFileConverter.
+
+
+ Install when I exit ZFileConverter
+
+
+ Encoding :
+
+
+ Encoding speed :
+
+
+ ZFileConverter is a shell extension.
+
+
+ That means it is integrated into the Windows explorer.
+
+
+ To use ZFileConverter, open Explorer and right-click any supported file to bring up the context menu where you will find ZFileConverter commands (see the readme file for compatible file formats).
+
+
+ File name template
+
+
+ Frames per second :
+
+
+ Open GitHub project page
+
+
+ help ?
+
+
+ Input example
+
+
+ Input formats
+
+
+ Delete
+
+
+ Move in an archive folder
+
+
+ None
+
+
+ The update has been downloaded. Exit ZFileConverter and install the update now.
+
+
+ Install when I exit ZFileConverter
+
+
+ Report an issue
+
+
+ Move down selected preset
+
+
+ Move up selected preset
+
+
+ Constant bitrate encoding
+
+
+ Variable bitrate encoding
+
+
+ 90°
+
+
+ 90° counter clockwise rotation
+
+
+ Ok
+
+
+ 180°
+
+
+ 180° rotation
+
+
+ C:\Music\Artist\Album\Song.wav
+
+
+ Output
+
+
+ (p): input file path
+(f): input filename
+(o): output extension type
+(i): input extension type
+(d0): input parent folder
+(d1): input sub parent folder
+...
+(n:i): page number index
+(n:c): total page count
+(n:i:D3): formatted page number index
+(n:c:D3): formatted page count
+(d:yyyy-MM-dd): current date
+(sc:yyyy-MM-dd): source file creation date
+(sm:yyyy-MM-dd): source file modified date
+(preset): selected preset name
+(presetpath): selected preset folder path
+
+Special paths:
+(p:d): my documents path
+(p:m): my music path
+(p:v): my videos path
+(p:p): my pictures path
+
+Use uppercase tokens for uppercase values when available.
+
+
+ Output format
+
+
+ Preset
+
+
+ Preset Name
+
+
+ Quality :
+
+
+ Recommended bitrate range in blue
+
+
+ Delete
+
+
+ Rotate :
+
+
+ Save
+
+
+ Scale :
+
+
+ See change log ...
+
+
+ Settings
+
+
+ Settings
+
+
+ 270°
+
+
+ 90° clockwise rotation
+
+
+ An update for ZFileConverter is available. It is strongly recommended that you install it as soon as possible!
+
+
+ Upgrade download in progress...
+
+
+ ZFileConverter Updates
+
+
+ Define the ratio 'file size' versus 'video quality'. A lower value will give you a smaller file, a greater value give you a better video quality.
+
+
+ Faster
+
+
+ Fast
+
+
+ Medium
+
+
+ Slower
+
+
+ Slow
+
+
+ Super Fast
+
+
+ Define the ratio 'file size' versus 'compression duration'. A slow compression will give you a smaller file (for the same video quality) than a faster compression.
+
+
+ Ultra Fast
+
+
+ Very Fast
+
+
+ Very Slow
+
+
+ Video
+
+
+ PCM signed 16-bit little-endian
+
+
+ PCM signed 24-bit little-endian
+
+
+ PCM signed 32-bit little-endian
+
+
+ PCM signed 8-bit little-endian
+
+
+ None
+
+
+ No rotation
+
+
+ Conversion
+
+
+ Done
+
+
+ Extraction
+
+
+ Failed
+
+
+ In queue
+
+
+ Read input image
+
+
+ Read document
+
+
+ Animated Image
+
+
+ Audio
+
+
+ Document
+
+
+ Image
+
+
+ Video
+
+
+ Waiting duration before application exit
+
+
+ Language
+
+
+ Maximum number of simultaneous conversions
+
+
+ New preset
+
+
+ ###Downloading change log ...
+
+
+ Conversion Archives
+
+
+ Canceled.
+
+
+ Fail to find ffmpeg executable. You should try to reinstall the application.
+
+
+ Audio CD track extraction failed.
+
+
+ CD drive is not ready.
+
+
+ Fail to launch ffmpeg.
+
+
+ Fail to create output path folders.
+
+
+ Fail to generate a unique output file path.
+
+
+ Fail to read cd drive '{0}'.
+
+
+ Fail to retrieve input path drive letter.
+
+
+ Fail to retrieve track number from the cda input path.
+
+
+ Fail to use the CD drive because it is opened.
+
+
+ The input file type is not compatible with the selected output file type.
+
+
+ Invalid output path generated by output file path template.
+
+
+ Unsupported output format '{0}'.
+
+
+ Channel count :
+
+
+ Change the number of channels of the input file.
+
+
+ Mono
+
+
+ Same than input file
+
+
+ Stereo
+
+
+ **This program is free software**.
+
+
+ You can redistribute it and/or modify it under the terms of the GNU General Public License.
+
+
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the GNU General Public License (available in the installation folder: `LICENSE.md`) for more details.
+
+
+ The application is terminating.
+
+
+ The application will automatically terminate in {0} seconds.
+
+
+ The application will automatically terminate in 1 second.
+
+
+ Misc
+
+
+ Can't find the output file(s).
+
+
+ The conversion job failed but there is an output file that does exists.
+
+
+ Prepare conversion
+
+
+ Microsoft Office must be installed in order to convert Office documents.
+
+
+ Fail to open document with Microsoft Office (check if your licence/installation is valid).
+
+
+ Microsoft Excel must be installed in order to convert Excel documents.
+
+
+ Microsoft PowerPoint must be installed in order to convert PowerPoint documents.
+
+
+ Microsoft Word must be installed in order to convert Word documents.
+
+
+ Open ZFileConverter releases
+
+
+ Error during conversion job initialization.
+
+
+ New folder
+
+
+ New folder
+
+
+ Folder
+
+
+ Folder Name
+
+
+ Advanced mode
+
+
+ Custom Arguments
+
+
+ Custom FFMPEG command line arguments
+
+
+ No preset selected
+
+
+ Export
+
+
+ Import
+
+
+ Duplicate preset
+
+
+ Cancel
+
+
+ Error
+
+
+ Can't load file converter user settings. Do you want to fall back to default settings ?
+
+
+ Copy files in clipboard after conversion
+
+
+ Hardware acceleration mode
+
+
+ Advanced Media Framework (AMD)
+
+
+ CUDA (Nvidia)
+
+
+ Direct3D 11
+
+
+ Direct3D 9/DXVA2
+
+
+ Off
+
+
+ OpenCL
+
+
+ Vulkan
+
+
diff --git a/Application/FileConverter/Registry.cs b/Application/FileConverter/Registry.cs
index 38edb0f0..46d7e0a6 100644
--- a/Application/FileConverter/Registry.cs
+++ b/Application/FileConverter/Registry.cs
@@ -5,6 +5,7 @@ namespace FileConverter
using System;
using System.Collections.Generic;
using System.IO;
+ using System.Linq;
using System.Xml.Serialization;
public class Registry : IDisposable
@@ -24,20 +25,10 @@ public Entry[] SerializableEntries
{
get
{
- Entry[] entries = new Entry[this.registryEntries.Count];
- int index = 0;
- foreach (KeyValuePair kvp in this.registryEntries)
- {
- if (kvp.Value == null)
- {
- continue;
- }
-
- entries[index] = new Entry(kvp.Key, kvp.Value);
- index++;
- }
-
- return entries;
+ return this.registryEntries
+ .Where(kvp => kvp.Value != null)
+ .Select(kvp => new Entry(kvp.Key, kvp.Value))
+ .ToArray();
}
set
diff --git a/Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs b/Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs
new file mode 100644
index 00000000..f80f1192
--- /dev/null
+++ b/Application/FileConverter/Services/ConversionJobRegisteredEventArgs.cs
@@ -0,0 +1,21 @@
+// License: http://www.gnu.org/licenses/gpl.html GPL version 3.
+
+namespace FileConverter.Services
+{
+ using System;
+
+ using FileConverter.ConversionJobs;
+
+ public class ConversionJobRegisteredEventArgs : EventArgs
+ {
+ public ConversionJobRegisteredEventArgs(ConversionJob conversionJob)
+ {
+ this.ConversionJob = conversionJob;
+ }
+
+ public ConversionJob ConversionJob
+ {
+ get;
+ }
+ }
+}
diff --git a/Application/FileConverter/Services/ConversionService.cs b/Application/FileConverter/Services/ConversionService.cs
index 6233d81e..934c5b57 100644
--- a/Application/FileConverter/Services/ConversionService.cs
+++ b/Application/FileConverter/Services/ConversionService.cs
@@ -15,11 +15,13 @@ namespace FileConverter.Services
public class ConversionService : ObservableObject, IConversionService
{
+ private readonly object conversionQueueLock = new object();
private readonly List conversionJobs = new List();
private readonly int numberOfConversionThread = 1;
private ISettingsService settingsService;
+ private bool conversionQueueIsRunning;
public ConversionService(ISettingsService settingsService)
{
@@ -38,10 +40,12 @@ public ConversionService(ISettingsService settingsService)
if (this.numberOfConversionThread <= 0)
{
this.numberOfConversionThread = System.Math.Max(1, Environment.ProcessorCount / 2);
- Debug.Log($"The number of processors on this computer is {settingsService.Settings.MaximumNumberOfSimultaneousConversions}. Set the default number of conversion threads to {settingsService.Settings.MaximumNumberOfSimultaneousConversions}");
+ Debug.Log($"The number of processors on this computer is {Environment.ProcessorCount}. Set the default number of conversion threads to {this.numberOfConversionThread}");
}
}
+ public event System.EventHandler ConversionJobRegistered;
+
public event System.EventHandler ConversionJobsTerminated;
public ReadOnlyCollection ConversionJobs
@@ -52,107 +56,167 @@ public ReadOnlyCollection ConversionJobs
public void RegisterConversionJob(ConversionJob conversionJob)
{
+ if (conversionJob == null)
+ {
+ throw new ArgumentNullException(nameof(conversionJob));
+ }
+
this.conversionJobs.Add(conversionJob);
this.OnPropertyChanged(nameof(this.ConversionJobs));
+ this.ConversionJobRegistered?.Invoke(this, new ConversionJobRegisteredEventArgs(conversionJob));
}
public void ConvertFilesAsync()
{
+ lock (this.conversionQueueLock)
+ {
+ if (this.conversionQueueIsRunning)
+ {
+ Debug.Log("Conversion queue is already running.");
+ return;
+ }
+
+ this.conversionQueueIsRunning = true;
+ }
+
Thread fileConvertionThread = Helpers.InstantiateThread("ConversionQueueThread", this.ConvertFiles);
fileConvertionThread.Start();
}
- private void ConvertFiles()
+ public void RetryConversionJob(ConversionJob conversionJob)
{
- // Prepare conversions.
- for (int index = 0; index < this.ConversionJobs.Count; index++)
+ if (conversionJob == null)
{
- this.ConversionJobs[index].PrepareConversion();
+ throw new ArgumentNullException(nameof(conversionJob));
}
- System.Collections.Specialized.StringCollection files = new System.Collections.Specialized.StringCollection();
- // Convert!
- Thread[] jobThreads = new Thread[this.numberOfConversionThread];
- while (true)
+ lock (this.conversionQueueLock)
{
- // Compute conversion flags.
- ConversionFlags conversionFlags = ConversionFlags.None;
- bool allJobAreFinished = true;
- for (int jobIndex = 0; jobIndex < this.conversionJobs.Count; jobIndex++)
+ if (this.conversionQueueIsRunning)
{
- ConversionJob conversionJob = this.conversionJobs[jobIndex];
- allJobAreFinished &= !(conversionJob.State == ConversionState.Ready || conversionJob.State == ConversionState.InProgress);
+ Debug.Log("Can't retry a conversion while the queue is running.");
+ return;
+ }
+ }
+
+ ConversionJob retryJob = ConversionJobFactory.Create(conversionJob.ConversionPreset, conversionJob.InitialInputPath);
+ this.RegisterConversionJob(retryJob);
+ this.ConvertFilesAsync();
+ }
+
+ private void ConvertFiles()
+ {
+ try
+ {
+ List activeJobs = new List();
- if (conversionJob.State == ConversionState.InProgress)
+ // Prepare conversions.
+ for (int index = 0; index < this.ConversionJobs.Count; index++)
+ {
+ if (this.ConversionJobs[index].State == ConversionState.Unknown)
{
- conversionFlags |= conversionJob.StateFlags;
+ this.ConversionJobs[index].PrepareConversion();
+ activeJobs.Add(this.ConversionJobs[index]);
}
}
- if (allJobAreFinished)
+ if (activeJobs.Count == 0)
{
- break;
+ Debug.Log("No pending conversion jobs to run.");
+ return;
}
- // Start job if possible.
- for (int jobIndex = 0; jobIndex < this.conversionJobs.Count; jobIndex++)
+ System.Collections.Specialized.StringCollection files = new System.Collections.Specialized.StringCollection();
+ // Convert!
+ Thread[] jobThreads = new Thread[this.numberOfConversionThread];
+ while (true)
{
- ConversionJob conversionJob = this.conversionJobs[jobIndex];
- if (conversionJob.State == ConversionState.Ready && conversionJob.CanStartConversion(conversionFlags))
+ // Compute conversion flags.
+ ConversionFlags conversionFlags = ConversionFlags.None;
+ bool allJobAreFinished = true;
+ for (int jobIndex = 0; jobIndex < activeJobs.Count; jobIndex++)
{
- // Find a thread to execute the job.
- Thread jobThread = null;
- for (int threadIndex = 0; threadIndex < jobThreads.Length; threadIndex++)
+ ConversionJob conversionJob = activeJobs[jobIndex];
+ allJobAreFinished &= !(conversionJob.State == ConversionState.Ready || conversionJob.State == ConversionState.InProgress);
+
+ if (conversionJob.State == ConversionState.InProgress)
{
- Thread thread = jobThreads[threadIndex];
- if (thread == null || !thread.IsAlive)
- {
- jobThread = Helpers.InstantiateThread(conversionJob.GetType().Name, this.ExecuteConversionJob);
- jobThreads[threadIndex] = jobThread;
- break;
- }
+ conversionFlags |= conversionJob.StateFlags;
}
+ }
+
+ if (allJobAreFinished)
+ {
+ break;
+ }
- if (jobThread != null)
+ // Start job if possible.
+ for (int jobIndex = 0; jobIndex < activeJobs.Count; jobIndex++)
+ {
+ ConversionJob conversionJob = activeJobs[jobIndex];
+ if (conversionJob.State == ConversionState.Ready && conversionJob.CanStartConversion(conversionFlags))
{
- jobThread.Start(conversionJob);
+ // Find a thread to execute the job.
+ Thread jobThread = null;
+ for (int threadIndex = 0; threadIndex < jobThreads.Length; threadIndex++)
+ {
+ Thread thread = jobThreads[threadIndex];
+ if (thread == null || !thread.IsAlive)
+ {
+ jobThread = Helpers.InstantiateThread(conversionJob.GetType().Name, this.ExecuteConversionJob);
+ jobThreads[threadIndex] = jobThread;
+ break;
+ }
+ }
+
+ if (jobThread != null)
+ {
+ jobThread.Start(conversionJob);
+
+ while (conversionJob.State == ConversionState.Ready)
+ {
+ Debug.Log("Wait the launch of the conversion thread before launching any other thread.");
+ Thread.Sleep(20);
+ }
+ }
- while (conversionJob.State == ConversionState.Ready)
+ if (!files.Contains(conversionJob.OutputFilePath))
{
- Debug.Log("Wait the launch of the conversion thread before launching any other thread.");
- Thread.Sleep(20);
+ files.Add(conversionJob.OutputFilePath);
}
- }
- if (!files.Contains(conversionJob.OutputFilePath))
- {
- files.Add(conversionJob.OutputFilePath);
+ break;
}
-
- break;
}
+
+ Thread.Sleep(50);
}
- Thread.Sleep(50);
- }
+ // Copy the output files to the clipboard
+ if (this.settingsService.Settings.CopyFilesInClipboardAfterConversion && files.Count > 0)
+ {
+ Thread clipboardThread = Helpers.InstantiateThread("CopyFilesToClipboardThread", this.CopyFilesToClipboard);
+ clipboardThread.SetApartmentState(ApartmentState.STA);
+ clipboardThread.Start(files);
+ }
- // Copy the output files to the clipboard
- if (this.settingsService.Settings.CopyFilesInClipboardAfterConversion && files.Count > 0)
- {
- Thread clipboardThread = Helpers.InstantiateThread("CopyFilesToClipboardThread", this.CopyFilesToClipboard);
- clipboardThread.SetApartmentState(ApartmentState.STA);
- clipboardThread.Start(files);
- }
+ bool allConversionsSucceed = true;
+ for (int index = 0; index < activeJobs.Count; index++)
+ {
+ allConversionsSucceed &= activeJobs[index].State == ConversionState.Done;
+ }
- bool allConversionsSucceed = true;
- for (int index = 0; index < this.conversionJobs.Count; index++)
- {
- allConversionsSucceed &= this.conversionJobs[index].State == ConversionState.Done;
+ if (this.ConversionJobsTerminated != null)
+ {
+ this.ConversionJobsTerminated.Invoke(this, new ConversionJobsTerminatedEventArgs(allConversionsSucceed));
+ }
}
-
- if (this.ConversionJobsTerminated != null)
+ finally
{
- this.ConversionJobsTerminated.Invoke(this, new ConversionJobsTerminatedEventArgs(allConversionsSucceed));
+ lock (this.conversionQueueLock)
+ {
+ this.conversionQueueIsRunning = false;
+ }
}
}
diff --git a/Application/FileConverter/Services/IConversionService.cs b/Application/FileConverter/Services/IConversionService.cs
index e5216b09..7f201446 100644
--- a/Application/FileConverter/Services/IConversionService.cs
+++ b/Application/FileConverter/Services/IConversionService.cs
@@ -8,6 +8,8 @@ namespace FileConverter.Services
public interface IConversionService
{
+ event System.EventHandler ConversionJobRegistered;
+
event System.EventHandler ConversionJobsTerminated;
ReadOnlyCollection ConversionJobs
@@ -18,5 +20,7 @@ ReadOnlyCollection ConversionJobs
void ConvertFilesAsync();
void RegisterConversionJob(ConversionJob conversionJob);
+
+ void RetryConversionJob(ConversionJob conversionJob);
}
}
diff --git a/Application/FileConverter/Services/UpgradeService.cs b/Application/FileConverter/Services/UpgradeService.cs
index 1ff083dd..59474afa 100644
--- a/Application/FileConverter/Services/UpgradeService.cs
+++ b/Application/FileConverter/Services/UpgradeService.cs
@@ -5,7 +5,8 @@ namespace FileConverter.Services
using System;
using System.IO;
using System.Net;
- using System.Text.RegularExpressions;
+ using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
@@ -18,11 +19,14 @@ namespace FileConverter.Services
public class UpgradeService : ObservableObject, IUpgradeService
{
#if DEBUG
- private const string BaseURI = "https://raw.githubusercontent.com/Tichau/FileConverter/integration/";
+ private const string BaseURI = "https://raw.githubusercontent.com/ZaidNAlAsali/FileConverter/integration/";
#else
- private const string BaseURI = "https://raw.githubusercontent.com/Tichau/FileConverter/master/";
+ private const string BaseURI = "https://raw.githubusercontent.com/ZaidNAlAsali/FileConverter/master/";
#endif
+ private const string ReleaseHost = "github.com";
+ private const string ReleasePathPrefix = "/ZaidNAlAsali/FileConverter/releases/download/";
+
[NotNull]
private readonly WebClient webClient = new WebClient();
@@ -68,6 +72,11 @@ public async Task CheckForUpgrade()
Diagnostics.Debug.Log($"Failed to check upgrade: {exception.Message}.");
}
+ if (task == null)
+ {
+ return null;
+ }
+
UpgradeVersionDescription versionDescription = await task;
if (versionDescription == null)
@@ -155,7 +164,7 @@ public void CancelUpgrade()
private async Task DownloadLatestVersionDescription()
{
#if BUILD32
- Uri uri = new Uri(Helpers.BaseURI + "version (x86).xml");
+ Uri uri = new Uri(UpgradeService.BaseURI + "version (x86).xml");
#else
Uri uri = new Uri(UpgradeService.BaseURI + "version.xml");
#endif
@@ -185,7 +194,7 @@ private async Task DownloadLatestVersionDescription()
}
catch (Exception)
{
- Debug.Log("Error while retrieving change log.");
+ Debug.Log("Error while retrieving version description.");
return null;
}
@@ -204,24 +213,37 @@ private async Task DownloadInstaller()
throw new Exception("The installer download is currently in progress.");
}
- Uri uri = new Uri(this.UpgradeVersionDescription.InstallerURL);
+ if (!this.TryCreateTrustedInstallerUri(this.UpgradeVersionDescription.InstallerURL, out Uri uri, out string uriErrorMessage))
+ {
+ Debug.LogError($"Refuse to download upgrade installer. {uriErrorMessage}");
+ this.UpgradeVersionDescription.NeedToUpgrade = false;
+ return;
+ }
- string fileName = "FileConverter-setup.msi";
- Regex retrieveFileNameRegex = new Regex("/([^/]*)");
- MatchCollection matchCollection = retrieveFileNameRegex.Matches(this.UpgradeVersionDescription.InstallerURL);
- if (matchCollection.Count > 0)
+ if (!this.IsValidSha256(this.UpgradeVersionDescription.InstallerSha256))
{
- Match match = matchCollection[matchCollection.Count - 1];
- if (match.Groups.Count > 1)
- {
- fileName = match.Groups[1].Value;
- }
+ Debug.LogError("Refuse to download upgrade installer. The update manifest does not contain a valid SHA-256 hash.");
+ this.UpgradeVersionDescription.NeedToUpgrade = false;
+ return;
}
- string tempPath = System.IO.Path.GetTempPath();
- string installerPath = System.IO.Path.Combine(tempPath, fileName);
+ string fileName = Uri.UnescapeDataString(Path.GetFileName(uri.LocalPath));
+ string tempPath = Path.Combine(Path.GetTempPath(), "ZFileConverter", Guid.NewGuid().ToString("N"));
+ string installerPath = Path.Combine(tempPath, fileName);
+ try
+ {
+ Directory.CreateDirectory(tempPath);
+ }
+ catch (Exception exception)
+ {
+ Debug.LogError("Failed to prepare the temporary upgrade folder.");
+ Debug.Log(exception.ToString());
+ this.UpgradeVersionDescription.NeedToUpgrade = false;
+ return;
+ }
this.UpgradeVersionDescription.InstallerPath = installerPath;
+ this.UpgradeVersionDescription.InstallerIsVerified = false;
this.UpgradeVersionDescription.InstallerDownloadInProgress = true;
this.UpgradeVersionDescription.InstallerDownloadProgress = 0;
@@ -234,22 +256,144 @@ private async Task DownloadInstaller()
{
await this.webClient.DownloadFileTaskAsync(uri, installerPath);
+ this.VerifyDownloadedInstaller(installerPath, this.UpgradeVersionDescription);
+
this.UpgradeVersionDescription.InstallerDownloadProgress = 100;
+ this.UpgradeVersionDescription.InstallerIsVerified = true;
this.UpgradeVersionDescription.InstallerDownloadInProgress = false;
- this.UpgradeVersionDescription = null;
}
catch (Exception exception)
{
- Debug.LogError("Failed to download the new File Converter upgrade. You should try again or download it manually.");
+ Debug.LogError("Failed to download the new ZFileConverter upgrade. You should try again or download it manually.");
Debug.Log(exception.ToString());
+ this.UpgradeVersionDescription.InstallerDownloadInProgress = false;
+ this.UpgradeVersionDescription.InstallerDownloadProgress = 0;
+ this.UpgradeVersionDescription.InstallerIsVerified = false;
this.UpgradeVersionDescription.NeedToUpgrade = false;
+ this.DeleteInstallerIfExists(installerPath);
}
this.webClient.DownloadProgressChanged -= this.WebClient_DownloadProgressChanged;
}
+
+ private bool TryCreateTrustedInstallerUri(string installerUrl, out Uri uri, out string errorMessage)
+ {
+ uri = null;
+ errorMessage = null;
+
+ if (!Uri.TryCreate(installerUrl, UriKind.Absolute, out uri))
+ {
+ errorMessage = "The installer URL is not an absolute URL.";
+ return false;
+ }
+
+ if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
+ {
+ errorMessage = "The installer URL must use HTTPS.";
+ return false;
+ }
+
+ if (!string.Equals(uri.Host, ReleaseHost, StringComparison.OrdinalIgnoreCase))
+ {
+ errorMessage = $"The installer URL host must be {ReleaseHost}.";
+ return false;
+ }
+
+ if (uri.AbsolutePath.IndexOf(ReleasePathPrefix, StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ errorMessage = "The installer URL must point to a ZFileConverter GitHub release asset.";
+ return false;
+ }
+
+ string fileName = Uri.UnescapeDataString(Path.GetFileName(uri.LocalPath));
+ if (string.IsNullOrEmpty(fileName) ||
+ !string.Equals(Path.GetExtension(fileName), ".msi", StringComparison.OrdinalIgnoreCase))
+ {
+ errorMessage = "The installer URL must point to an MSI package.";
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool IsValidSha256(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value) || value.Length != 64)
+ {
+ return false;
+ }
+
+ for (int index = 0; index < value.Length; index++)
+ {
+ char character = value[index];
+ bool isHex =
+ (character >= '0' && character <= '9') ||
+ (character >= 'a' && character <= 'f') ||
+ (character >= 'A' && character <= 'F');
+
+ if (!isHex)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void VerifyDownloadedInstaller(string installerPath, UpgradeVersionDescription description)
+ {
+ string expectedSha256 = description.InstallerSha256.Replace(" ", string.Empty).ToUpperInvariant();
+ string actualSha256 = this.ComputeSha256(installerPath);
+ if (!string.Equals(actualSha256, expectedSha256, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"The upgrade installer SHA-256 hash did not match. Expected {expectedSha256}, actual {actualSha256}.");
+ }
+
+ if (string.IsNullOrWhiteSpace(description.InstallerPublisherSubject))
+ {
+ return;
+ }
+
+ X509Certificate2 certificate = new X509Certificate2(X509Certificate.CreateFromSignedFile(installerPath));
+ if (certificate == null ||
+ certificate.Subject.IndexOf(description.InstallerPublisherSubject, StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ throw new InvalidOperationException("The upgrade installer publisher did not match the update manifest.");
+ }
+ }
+
+ private string ComputeSha256(string filePath)
+ {
+ using (FileStream fileStream = File.OpenRead(filePath))
+ using (SHA256 sha256 = SHA256.Create())
+ {
+ byte[] hash = sha256.ComputeHash(fileStream);
+ return BitConverter.ToString(hash).Replace("-", string.Empty);
+ }
+ }
+
+ private void DeleteInstallerIfExists(string installerPath)
+ {
+ try
+ {
+ if (!string.IsNullOrEmpty(installerPath) && File.Exists(installerPath))
+ {
+ File.Delete(installerPath);
+ }
+ }
+ catch (Exception exception)
+ {
+ Debug.Log($"Failed to delete invalid installer {installerPath}: {exception.Message}");
+ }
+ }
private void WebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs eventArgs)
{
+ if (this.UpgradeVersionDescription == null)
+ {
+ return;
+ }
+
this.UpgradeVersionDescription.InstallerDownloadProgress = eventArgs.ProgressPercentage;
}
}
diff --git a/Application/FileConverter/Services/UpgradeVersionDescription.cs b/Application/FileConverter/Services/UpgradeVersionDescription.cs
index 1094b4a3..76b25473 100644
--- a/Application/FileConverter/Services/UpgradeVersionDescription.cs
+++ b/Application/FileConverter/Services/UpgradeVersionDescription.cs
@@ -27,6 +27,20 @@ public string InstallerURL
set;
}
+ [XmlElement("SHA256")]
+ public string InstallerSha256
+ {
+ get;
+ set;
+ }
+
+ [XmlElement("PublisherSubject")]
+ public string InstallerPublisherSubject
+ {
+ get;
+ set;
+ }
+
[XmlIgnore]
public string ChangeLog
{
@@ -79,6 +93,13 @@ public int InstallerDownloadProgress
[XmlIgnore]
public bool InstallerDownloadNotStarted => !this.InstallerDownloadInProgress && this.InstallerDownloadProgress == 0;
+ [XmlIgnore]
+ public bool InstallerIsVerified
+ {
+ get;
+ set;
+ }
+
[XmlIgnore]
public bool NeedToUpgrade
{
@@ -86,4 +107,4 @@ public bool NeedToUpgrade
set;
}
}
-}
\ No newline at end of file
+}
diff --git a/Application/FileConverter/Settings.cs b/Application/FileConverter/Settings.cs
index 84e8f1b2..a59840f8 100644
--- a/Application/FileConverter/Settings.cs
+++ b/Application/FileConverter/Settings.cs
@@ -14,6 +14,7 @@ namespace FileConverter
public class Settings : ObservableObject, IXmlSerializable
{
public const int Version = 4;
+ private const int MaximumAllowedSimultaneousConversions = 16;
private bool exitApplicationWhenConversionsFinished = false;
private float durationBetweenEndOfConversionsAndApplicationExit = 3f;
@@ -26,13 +27,19 @@ public class Settings : ObservableObject, IXmlSerializable
public ConversionPreset GetPresetFromName(string presetName)
{
- return this.conversionPresets.FirstOrDefault(match => match.FullName == presetName);
+ return this.conversionPresets.FirstOrDefault(match => match != null && match.FullName == presetName);
}
public void Clean()
{
- for (int index = 0; index < this.ConversionPresets.Count; index++)
+ for (int index = this.ConversionPresets.Count - 1; index >= 0; index--)
{
+ if (this.ConversionPresets[index] == null)
+ {
+ this.ConversionPresets.RemoveAt(index);
+ continue;
+ }
+
this.ConversionPresets[index].Clean();
}
}
@@ -47,7 +54,12 @@ public Settings Merge(Settings settings)
for (int index = 0; index < settings.conversionPresets.Count; index++)
{
ConversionPreset conversionPreset = settings.conversionPresets[index];
- if (this.conversionPresets.Any(match => match.FullName == conversionPreset.FullName))
+ if (conversionPreset == null)
+ {
+ continue;
+ }
+
+ if (this.conversionPresets.Any(match => match != null && match.FullName == conversionPreset.FullName))
{
continue;
}
@@ -112,7 +124,15 @@ public string ApplicationLanguageName
return;
}
- this.ApplicationLanguage = CultureInfo.GetCultureInfo(value);
+ try
+ {
+ this.ApplicationLanguage = CultureInfo.GetCultureInfo(value);
+ }
+ catch (CultureNotFoundException)
+ {
+ Diagnostics.Debug.Log($"Unsupported application language '{value}'. Fallback to default culture.");
+ this.ApplicationLanguage = null;
+ }
}
}
@@ -186,8 +206,18 @@ public ConversionPreset[] SerializableConversionPresets
set
{
+ if (value == null)
+ {
+ return;
+ }
+
for (int index = 0; index < value.Length; index++)
{
+ if (value[index] == null)
+ {
+ continue;
+ }
+
this.ConversionPresets.Add(value[index]);
}
}
@@ -221,8 +251,8 @@ public bool CopyFilesInClipboardAfterConversion
this.copyFilesInClipboardAfterConversion = value;
this.OnPropertyChanged();
}
- }
-
+ }
+
[XmlElement]
public Helpers.HardwareAccelerationMode HardwareAccelerationMode
{
@@ -240,9 +270,18 @@ public Helpers.HardwareAccelerationMode HardwareAccelerationMode
public void OnDeserializationComplete()
{
this.DurationBetweenEndOfConversionsAndApplicationExit = System.Math.Max(0, System.Math.Min(10, this.DurationBetweenEndOfConversionsAndApplicationExit));
+ this.MaximumNumberOfSimultaneousConversions = System.Math.Max(
+ 0,
+ System.Math.Min(MaximumAllowedSimultaneousConversions, this.MaximumNumberOfSimultaneousConversions));
- for (int index = 0; index < this.ConversionPresets.Count; index++)
+ for (int index = this.ConversionPresets.Count - 1; index >= 0; index--)
{
+ if (this.ConversionPresets[index] == null)
+ {
+ this.ConversionPresets.RemoveAt(index);
+ continue;
+ }
+
this.ConversionPresets[index].OnDeserializationComplete();
}
diff --git a/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs b/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs
index cc93d8c0..71d709e5 100644
--- a/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs
+++ b/Application/FileConverter/ValueConverters/ApplicationVersionToApplicationName.cs
@@ -12,12 +12,12 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
{
if (!(value is FileConverter.Version))
{
- return "File Converter";
+ return "ZFileConverter";
}
FileConverter.Version version = (FileConverter.Version)value;
- return $"File Converter v{version}";
+ return $"ZFileConverter v{version}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/Application/FileConverter/ValueConverters/FileNameConverter.cs b/Application/FileConverter/ValueConverters/FileNameConverter.cs
index d8cb0a87..12a458d2 100644
--- a/Application/FileConverter/ValueConverters/FileNameConverter.cs
+++ b/Application/FileConverter/ValueConverters/FileNameConverter.cs
@@ -10,9 +10,9 @@ public class FileNameConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
- if (values == null || values.Length != 3)
+ if (values == null || values.Length < 3 || values.Length > 5)
{
- throw new ArgumentException("The values must contains the input file path, the output file extension and the ouput file template.");
+ throw new ArgumentException("The values must contain the input file path, the output file extension, the output file template and optional preset names.");
}
if (!(values[1] is OutputType))
@@ -23,8 +23,10 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur
string inputFilePath = values[0] as string;
OutputType outputFileExtension = (OutputType)values[1];
string outputFileTemplate = values[2] as string;
+ string presetName = values.Length > 3 ? values[3] as string : null;
+ string presetFullName = values.Length > 4 ? values[4] as string : presetName;
- return PathHelpers.GenerateFilePathFromTemplate(inputFilePath, outputFileExtension, outputFileTemplate, 1, 3);
+ return PathHelpers.GenerateFilePathFromTemplate(inputFilePath, outputFileExtension, outputFileTemplate, 1, 3, presetName, presetFullName);
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
diff --git a/Application/FileConverter/ViewModels/DependencyStatusViewModel.cs b/Application/FileConverter/ViewModels/DependencyStatusViewModel.cs
new file mode 100644
index 00000000..122e8f18
--- /dev/null
+++ b/Application/FileConverter/ViewModels/DependencyStatusViewModel.cs
@@ -0,0 +1,46 @@
+// License: http://www.gnu.org/licenses/gpl.html GPL version 3.
+
+namespace FileConverter.ViewModels
+{
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ public class DependencyStatusViewModel : ObservableObject
+ {
+ private string name;
+ private string status;
+ private string details;
+ private bool isHealthy;
+
+ public DependencyStatusViewModel(string name, string status, string details, bool isHealthy)
+ {
+ this.Name = name;
+ this.Status = status;
+ this.Details = details;
+ this.IsHealthy = isHealthy;
+ }
+
+ public string Name
+ {
+ get => this.name;
+ private set => this.SetProperty(ref this.name, value);
+ }
+
+ public string Status
+ {
+ get => this.status;
+ private set => this.SetProperty(ref this.status, value);
+ }
+
+ public string Details
+ {
+ get => this.details;
+ private set => this.SetProperty(ref this.details, value);
+ }
+
+ public bool IsHealthy
+ {
+ get => this.isHealthy;
+ private set => this.SetProperty(ref this.isHealthy, value);
+ }
+ }
+}
diff --git a/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs b/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs
index c6552a00..e1022f38 100644
--- a/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs
+++ b/Application/FileConverter/ViewModels/DiagnosticsViewModel.cs
@@ -3,6 +3,9 @@
namespace FileConverter.ViewModels
{
using System.ComponentModel;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Windows;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -16,7 +19,9 @@ namespace FileConverter.ViewModels
///
public class DiagnosticsViewModel : ObservableRecipient
{
+ private RelayCommand copyDiagnosticsCommand;
private RelayCommand closeCommand;
+ private RelayCommand openDiagnosticsFolderCommand;
///
/// Initializes a new instance of the DiagnosticsViewModel class.
@@ -38,10 +43,67 @@ public ICommand CloseCommand
}
}
+ public ICommand CopyDiagnosticsCommand
+ {
+ get
+ {
+ if (this.copyDiagnosticsCommand == null)
+ {
+ this.copyDiagnosticsCommand = new RelayCommand(this.CopyDiagnostics);
+ }
+
+ return this.copyDiagnosticsCommand;
+ }
+ }
+
+ public ICommand OpenDiagnosticsFolderCommand
+ {
+ get
+ {
+ if (this.openDiagnosticsFolderCommand == null)
+ {
+ this.openDiagnosticsFolderCommand = new RelayCommand(this.OpenDiagnosticsFolder);
+ }
+
+ return this.openDiagnosticsFolderCommand;
+ }
+ }
+
private void Close(CancelEventArgs args)
{
INavigationService navigationService = Ioc.Default.GetRequiredService();
navigationService.Close(Pages.Diagnostics, args != null);
}
+
+ private void CopyDiagnostics()
+ {
+ try
+ {
+ Clipboard.SetText(Diagnostics.Debug.AllContent);
+ }
+ catch (System.Exception exception)
+ {
+ Diagnostics.Debug.Log($"Can't copy diagnostics to clipboard: {exception.Message}.");
+ }
+ }
+
+ private void OpenDiagnosticsFolder()
+ {
+ string diagnosticsFolderPath = Diagnostics.Debug.DiagnosticsFolderPath;
+ if (string.IsNullOrEmpty(diagnosticsFolderPath) || !Directory.Exists(diagnosticsFolderPath))
+ {
+ Diagnostics.Debug.Log($"Can't open diagnostics folder: {diagnosticsFolderPath}.");
+ return;
+ }
+
+ try
+ {
+ Process.Start("explorer.exe", $"\"{diagnosticsFolderPath}\"");
+ }
+ catch (System.Exception exception)
+ {
+ Diagnostics.Debug.Log($"Can't open diagnostics folder: {exception.Message}.");
+ }
+ }
}
}
diff --git a/Application/FileConverter/ViewModels/MainViewModel.cs b/Application/FileConverter/ViewModels/MainViewModel.cs
index e50a632c..948a0580 100644
--- a/Application/FileConverter/ViewModels/MainViewModel.cs
+++ b/Application/FileConverter/ViewModels/MainViewModel.cs
@@ -18,6 +18,8 @@ namespace FileConverter.ViewModels
///
public class MainViewModel : ObservableRecipient
{
+ private readonly IConversionService conversionService;
+
private string informationMessage;
private ObservableCollection conversionJobs;
@@ -30,8 +32,9 @@ public class MainViewModel : ObservableRecipient
///
public MainViewModel()
{
- IConversionService settingsService = Ioc.Default.GetRequiredService();
- this.ConversionJobs = new ObservableCollection(settingsService.ConversionJobs);
+ this.conversionService = Ioc.Default.GetRequiredService();
+ this.conversionService.ConversionJobRegistered += this.ConversionService_ConversionJobRegistered;
+ this.ConversionJobs = new ObservableCollection(this.conversionService.ConversionJobs);
Application application = Application.Current as Application;
application.OnApplicationTerminate += this.Application_OnApplicationTerminate;
@@ -117,6 +120,21 @@ private void ConversionJob_PropertyChanged(object sender, PropertyChangedEventAr
this.OnPropertyChanged(nameof(this.ConversionJobs));
}
+ private void ConversionService_ConversionJobRegistered(object sender, ConversionJobRegisteredEventArgs eventArgs)
+ {
+ if (eventArgs?.ConversionJob == null || this.ConversionJobs.Contains(eventArgs.ConversionJob))
+ {
+ return;
+ }
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ this.ConversionJobs.Add(eventArgs.ConversionJob);
+ eventArgs.ConversionJob.PropertyChanged += this.ConversionJob_PropertyChanged;
+ this.OnPropertyChanged(nameof(this.ConversionJobs));
+ });
+ }
+
private void Application_OnApplicationTerminate(object sender, ApplicationTerminateArgs eventArgs)
{
if (float.IsNaN(eventArgs.RemainingTimeBeforeTermination))
@@ -142,4 +160,4 @@ private void Application_OnApplicationTerminate(object sender, ApplicationTermin
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Application/FileConverter/ViewModels/SettingsViewModel.cs b/Application/FileConverter/ViewModels/SettingsViewModel.cs
index 15d33c87..ad83221c 100644
--- a/Application/FileConverter/ViewModels/SettingsViewModel.cs
+++ b/Application/FileConverter/ViewModels/SettingsViewModel.cs
@@ -5,10 +5,14 @@ namespace FileConverter.ViewModels
using System;
using System.IO;
using System.Collections.Generic;
+ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
+ using System.Reflection;
+ using System.Text;
+ using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
@@ -19,6 +23,7 @@ namespace FileConverter.ViewModels
using CommunityToolkit.Mvvm.Input;
using FileConverter.Annotations;
+ using FileConverter.ConversionJobs;
using FileConverter.Services;
using FileConverter.Views;
@@ -42,9 +47,14 @@ public class SettingsViewModel : ObservableRecipient, IDataErrorInfo
private RelayCommand importPresetCommand;
private RelayCommand exportPresetCommand;
private RelayCommand removePresetCommand;
+ private RelayCommand refreshDependencyHealthCommand;
+ private RelayCommand repairShellExtensionCommand;
private RelayCommand saveCommand;
private RelayCommand closeCommand;
+ private ObservableCollection dependencyStatuses = new ObservableCollection();
+ private string shellExtensionRepairStatus = string.Empty;
+
private ListCollectionView outputTypes;
private CultureInfo[] supportedCultures;
private Helpers.HardwareAccelerationMode[] hardwareAccelerationModes = { Helpers.HardwareAccelerationMode.Off, Helpers.HardwareAccelerationMode.CUDA, Helpers.HardwareAccelerationMode.AMF };
@@ -58,13 +68,15 @@ public class SettingsViewModel : ObservableRecipient, IDataErrorInfo
public SettingsViewModel()
{
this.getChangeLogContentCommand = new RelayCommand(this.DownloadChangeLogAction);
- this.openUrlCommand = new RelayCommand((url) => Process.Start(url));
+ this.openUrlCommand = new RelayCommand(this.OpenUrl);
this.createFolderCommand = new RelayCommand(this.CreateFolder);
this.newPresetCommand = new RelayCommand(() => this.AddNewPreset(false));
this.duplicatePresetCommand = new RelayCommand(() => this.AddNewPreset(true), this.CanDuplicateSelectedPreset);
this.importPresetCommand = new RelayCommand(this.ImportPreset);
this.exportPresetCommand = new RelayCommand(this.ExportSelectedPreset, this.CanExportSelectedPreset);
this.removePresetCommand = new RelayCommand(this.RemoveSelectedPreset, this.CanRemoveSelectedPreset);
+ this.refreshDependencyHealthCommand = new RelayCommand(this.RefreshDependencyHealth);
+ this.repairShellExtensionCommand = new RelayCommand(this.RepairShellExtension);
this.saveCommand = new RelayCommand(this.SaveSettings, this.CanSaveSettings);
this.closeCommand = new RelayCommand(this.CloseSettings);
@@ -84,6 +96,7 @@ public SettingsViewModel()
outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Avi));
outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Png));
outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Jpg));
+ outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Avif));
outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Webp));
outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Ico));
outputTypeViewModels.Add(new OutputTypeViewModel(OutputType.Gif));
@@ -95,6 +108,7 @@ public SettingsViewModel()
this.InitializeCompatibleInputExtensions();
this.InitializePresetFolders();
+ this.RefreshDependencyHealth();
}
public IEnumerable InputCategories
@@ -233,14 +247,14 @@ public CultureInfo[] SupportedCultures
}
}
- public Helpers.HardwareAccelerationMode[] HardwareAccelerationModes
- {
- get => this.hardwareAccelerationModes;
- set
- {
- this.hardwareAccelerationModes = value;
- this.OnPropertyChanged();
- }
+ public Helpers.HardwareAccelerationMode[] HardwareAccelerationModes
+ {
+ get => this.hardwareAccelerationModes;
+ set
+ {
+ this.hardwareAccelerationModes = value;
+ this.OnPropertyChanged();
+ }
}
public ListCollectionView OutputTypes
@@ -284,10 +298,36 @@ private set
public ICommand RemoveSelectedPresetCommand => this.removePresetCommand;
+ public ICommand RefreshDependencyHealthCommand => this.refreshDependencyHealthCommand;
+
+ public ICommand RepairShellExtensionCommand => this.repairShellExtensionCommand;
+
public ICommand SaveCommand => this.saveCommand;
public ICommand CloseCommand => this.closeCommand;
+ public ObservableCollection DependencyStatuses
+ {
+ get => this.dependencyStatuses;
+
+ private set
+ {
+ this.dependencyStatuses = value;
+ this.OnPropertyChanged();
+ }
+ }
+
+ public string ShellExtensionRepairStatus
+ {
+ get => this.shellExtensionRepairStatus;
+
+ private set
+ {
+ this.shellExtensionRepairStatus = value;
+ this.OnPropertyChanged();
+ }
+ }
+
public TreeViewSelectionBehavior.IsChildOfPredicate PresetsHierarchyPredicate => (object nodeA, object nodeB) =>
{
if (nodeA is PresetNode)
@@ -387,6 +427,161 @@ private void DownloadChangeLogAction()
this.DisplaySeeChangeLogLink = false;
}
+ private void OpenUrl(string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ return;
+ }
+
+ try
+ {
+ Process.Start(url);
+ }
+ catch (Exception exception)
+ {
+ Diagnostics.Debug.Log($"Failed to open URL '{url}': {exception.Message}.");
+ }
+ }
+
+ private void RefreshDependencyHealth()
+ {
+ ObservableCollection statuses = new ObservableCollection();
+
+ string shellExtensionPath = Helpers.GetDefaultShellExtensionPath();
+ string defaultSettingsPath = FileConverterExtension.PathHelpers.DefaultSettingsFilePath;
+ string userSettingsPath = FileConverterExtension.PathHelpers.UserSettingsFilePath;
+
+ this.AddFileStatus(statuses, "FFmpeg", GetApplicationFilePath("ffmpeg.exe"), "Required for audio and video conversions.");
+ this.AddFileStatus(statuses, "ImageMagick", GetApplicationFilePath("Magick.NET-Q16-AnyCPU.dll"), "Required for image, AVIF, PDF image, and WebP workflows.");
+ this.AddFileStatus(statuses, "ImageMagick native", GetApplicationFilePath("Magick.Native-Q16-x64.dll"), "Required native image processing runtime.");
+ this.AddFileStatus(statuses, "Ghostscript", GetApplicationFilePath("gswin64c.exe"), "Required for PDF rendering.");
+ this.AddFileStatus(statuses, "Ghostscript DLL", GetApplicationFilePath("gsdll64.dll"), "Required by ImageMagick PDF rendering.");
+ this.AddFileStatus(statuses, "Explorer extension DLL", shellExtensionPath, "Required for Windows Explorer right-click commands.");
+ this.AddFileStatus(statuses, "Default presets", defaultSettingsPath, "Required when creating or repairing user settings.");
+
+ if (File.Exists(userSettingsPath))
+ {
+ statuses.Add(new DependencyStatusViewModel("User settings", "Ready", userSettingsPath, true));
+ }
+ else
+ {
+ statuses.Add(new DependencyStatusViewModel("User settings", "Will be created", userSettingsPath, true));
+ }
+
+ this.AddOfficeStatus(statuses, "Microsoft Word", ConversionJob_Office.ApplicationName.Word, "Required for Word document conversion.");
+ this.AddOfficeStatus(statuses, "Microsoft Excel", ConversionJob_Office.ApplicationName.Excel, "Required for spreadsheet conversion.");
+ this.AddOfficeStatus(statuses, "Microsoft PowerPoint", ConversionJob_Office.ApplicationName.PowerPoint, "Required for presentation conversion.");
+ this.AddShellRegistrationStatus(statuses);
+
+ this.DependencyStatuses = statuses;
+ }
+
+ private void RepairShellExtension()
+ {
+ string shellExtensionPath = Helpers.GetDefaultShellExtensionPath();
+ if (!File.Exists(shellExtensionPath))
+ {
+ this.ShellExtensionRepairStatus = $"Can't repair Explorer integration because {shellExtensionPath} is missing.";
+ this.RefreshDependencyHealth();
+ return;
+ }
+
+ string executablePath = Assembly.GetExecutingAssembly().Location;
+ ProcessStartInfo startInfo = new ProcessStartInfo(executablePath)
+ {
+ Arguments = $"--repair-shell-extension {QuoteArgument(shellExtensionPath)}",
+ UseShellExecute = true,
+ Verb = "runas",
+ };
+
+ try
+ {
+ Process.Start(startInfo);
+ this.ShellExtensionRepairStatus = "Repair launched with administrator privileges. Reopen Explorer or retry the context menu after it finishes.";
+ }
+ catch (Win32Exception exception)
+ {
+ if (exception.NativeErrorCode == 1223)
+ {
+ this.ShellExtensionRepairStatus = "Repair canceled by user.";
+ }
+ else
+ {
+ this.ShellExtensionRepairStatus = $"Repair failed to start: {exception.Message}";
+ }
+ }
+ catch (Exception exception)
+ {
+ this.ShellExtensionRepairStatus = $"Repair failed to start: {exception.Message}";
+ }
+
+ this.RefreshDependencyHealth();
+ }
+
+ private void AddFileStatus(ObservableCollection statuses, string name, string path, string purpose)
+ {
+ if (!string.IsNullOrEmpty(path) && File.Exists(path))
+ {
+ statuses.Add(new DependencyStatusViewModel(name, "Ready", $"{purpose} Found at {path}", true));
+ }
+ else
+ {
+ statuses.Add(new DependencyStatusViewModel(name, "Missing", $"{purpose} Expected at {path ?? "(unknown path)"}", false));
+ }
+ }
+
+ private void AddOfficeStatus(ObservableCollection statuses, string name, ConversionJob_Office.ApplicationName applicationName, string purpose)
+ {
+ bool isAvailable = Helpers.IsMicrosoftOfficeApplicationAvailable(applicationName);
+ statuses.Add(new DependencyStatusViewModel(
+ name,
+ isAvailable ? "Available" : "Optional missing",
+ purpose,
+ true));
+ }
+
+ private void AddShellRegistrationStatus(ObservableCollection statuses)
+ {
+ string registeredPath = FileConverterExtension.PathHelpers.FileConverterPath;
+ string executablePath = Assembly.GetExecutingAssembly().Location;
+
+ if (string.IsNullOrEmpty(registeredPath))
+ {
+ statuses.Add(new DependencyStatusViewModel("Explorer registration", "Needs repair", "No executable path is registered in HKCU\\Software\\FileConverter.", false));
+ return;
+ }
+
+ if (!File.Exists(registeredPath))
+ {
+ statuses.Add(new DependencyStatusViewModel("Explorer registration", "Needs repair", $"Registered executable is missing: {registeredPath}", false));
+ return;
+ }
+
+ bool matchesCurrentExecutable = string.Equals(registeredPath, executablePath, StringComparison.OrdinalIgnoreCase);
+ statuses.Add(new DependencyStatusViewModel(
+ "Explorer registration",
+ matchesCurrentExecutable ? "Ready" : "Different install",
+ matchesCurrentExecutable ? registeredPath : $"Registered path: {registeredPath}; current path: {executablePath}",
+ matchesCurrentExecutable));
+ }
+
+ private static string QuoteArgument(string value)
+ {
+ return $"\"{value.Replace("\"", "\\\"")}\"";
+ }
+
+ private static string GetApplicationFilePath(string fileName)
+ {
+ string applicationFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ if (string.IsNullOrEmpty(applicationFolder))
+ {
+ return fileName;
+ }
+
+ return Path.Combine(applicationFolder, fileName);
+ }
+
private void InitializeCompatibleInputExtensions()
{
List categories = new List();
@@ -526,7 +721,7 @@ private void CreateFolder()
this.saveCommand.NotifyCanExecuteChanged();
- this.OnFolderCreated();
+ this.OnFolderCreated?.Invoke();
}
private bool CanDuplicateSelectedPreset()
@@ -584,7 +779,7 @@ private void AddNewPreset(bool duplicate)
this.SelectedItem = node;
- this.OnPresetCreated.Invoke();
+ this.OnPresetCreated?.Invoke();
this.removePresetCommand.NotifyCanExecuteChanged();
this.saveCommand.NotifyCanExecuteChanged();
@@ -604,6 +799,7 @@ private void ImportPreset()
if (!File.Exists(openFileDialog.FileName))
{
Diagnostics.Debug.LogError("File does not exists.");
+ return;
}
string directoryPath = Path.GetDirectoryName(openFileDialog.FileName);
@@ -613,7 +809,20 @@ private void ImportPreset()
}
List presetsToImport = new List();
- XmlHelpers.LoadFromFile("Presets", openFileDialog.FileName, out presetsToImport);
+ try
+ {
+ XmlHelpers.LoadFromFile("Presets", openFileDialog.FileName, out presetsToImport);
+ }
+ catch (Exception exception)
+ {
+ Diagnostics.Debug.LogError($"Failed to import presets. {exception.Message}");
+ return;
+ }
+
+ if (!this.ReviewImportedPresets(presetsToImport))
+ {
+ return;
+ }
// Add imported preset to preset tree.
bool itemSelected = false;
@@ -644,7 +853,223 @@ private void ImportPreset()
itemSelected = true;
}
}
+
+ this.saveCommand.NotifyCanExecuteChanged();
+ }
+ }
+
+ private bool ReviewImportedPresets(List presetsToImport)
+ {
+ if (presetsToImport == null || presetsToImport.Count == 0)
+ {
+ return true;
+ }
+
+ List warnings = new List();
+ foreach (ConversionPreset conversionPreset in presetsToImport)
+ {
+ warnings.AddRange(this.GetPresetImportWarnings(conversionPreset));
+ }
+
+ if (warnings.Count == 0)
+ {
+ return true;
+ }
+
+ StringBuilder message = new StringBuilder();
+ message.AppendLine("This preset file contains advanced settings that can affect conversion commands or output locations.");
+ message.AppendLine();
+ message.AppendLine("Choose Yes to import with risky settings disabled, No to import as-is only if you trust this file, or Cancel to stop importing.");
+ message.AppendLine();
+
+ int warningsToDisplay = Math.Min(warnings.Count, 8);
+ for (int index = 0; index < warningsToDisplay; index++)
+ {
+ message.AppendLine("- " + warnings[index]);
+ }
+
+ if (warnings.Count > warningsToDisplay)
+ {
+ message.AppendLine($"- {warnings.Count - warningsToDisplay} more warning(s).");
+ }
+
+ MessageBoxResult result = MessageBox.Show(
+ message.ToString(),
+ "Review imported presets",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Warning,
+ MessageBoxResult.Yes);
+
+ if (result == MessageBoxResult.Cancel)
+ {
+ return false;
+ }
+
+ if (result == MessageBoxResult.Yes)
+ {
+ foreach (ConversionPreset conversionPreset in presetsToImport)
+ {
+ this.NeutralizeRiskyImportedPresetSettings(conversionPreset);
+ }
+ }
+
+ return true;
+ }
+
+ private IEnumerable GetPresetImportWarnings(ConversionPreset conversionPreset)
+ {
+ if (conversionPreset == null)
+ {
+ yield break;
+ }
+
+ string presetName = string.IsNullOrWhiteSpace(conversionPreset.FullName) ? "Unnamed preset" : conversionPreset.FullName;
+
+ if (this.HasUnsafePresetName(conversionPreset))
+ {
+ yield return $"Preset '{presetName}' has a name that is unsafe for Explorer launch or folder creation.";
+ }
+
+ if (this.HasEnabledCustomFFmpegCommand(conversionPreset))
+ {
+ yield return $"Preset '{presetName}' enables a raw FFmpeg command.";
+ }
+
+ if (this.HasSuspiciousOutputTemplate(conversionPreset))
+ {
+ yield return $"Preset '{presetName}' writes to a non-standard output location.";
+ }
+ }
+
+ private void NeutralizeRiskyImportedPresetSettings(ConversionPreset conversionPreset)
+ {
+ if (conversionPreset == null)
+ {
+ return;
+ }
+
+ if (this.HasUnsafePresetName(conversionPreset))
+ {
+ conversionPreset.FullName = this.SanitizePresetFullName(conversionPreset.FullName);
+ }
+
+ if (this.HasEnabledCustomFFmpegCommand(conversionPreset))
+ {
+ conversionPreset.SetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableFFMPEGCustomCommand, "False");
+ conversionPreset.SetSettingsValue(ConversionPreset.ConversionSettingKeys.FFMPEGCustomCommand, string.Empty);
+ }
+
+ if (this.HasSuspiciousOutputTemplate(conversionPreset))
+ {
+ conversionPreset.OutputFileNameTemplate = "(p)(f)";
+ }
+ }
+
+ private bool HasEnabledCustomFFmpegCommand(ConversionPreset conversionPreset)
+ {
+ bool customCommandEnabled;
+ bool.TryParse(
+ conversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableFFMPEGCustomCommand),
+ out customCommandEnabled);
+
+ return customCommandEnabled &&
+ !string.IsNullOrWhiteSpace(conversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.FFMPEGCustomCommand));
+ }
+
+ private bool HasSuspiciousOutputTemplate(ConversionPreset conversionPreset)
+ {
+ string template = conversionPreset.OutputFileNameTemplate;
+ if (string.IsNullOrWhiteSpace(template))
+ {
+ return false;
+ }
+
+ if (Path.IsPathRooted(template))
+ {
+ return true;
+ }
+
+ string[] segments = template.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
+ for (int index = 0; index < segments.Length; index++)
+ {
+ if (segments[index] == "." || segments[index] == "..")
+ {
+ return true;
+ }
+ }
+
+ string lowerTemplate = template.ToLowerInvariant();
+ return
+ lowerTemplate.Contains("(p:d)") ||
+ lowerTemplate.Contains("(p:documents)") ||
+ lowerTemplate.Contains("(p:m)") ||
+ lowerTemplate.Contains("(p:music)") ||
+ lowerTemplate.Contains("(p:v)") ||
+ lowerTemplate.Contains("(p:videos)") ||
+ lowerTemplate.Contains("(p:p)") ||
+ lowerTemplate.Contains("(p:pictures)");
+ }
+
+ private bool HasUnsafePresetName(ConversionPreset conversionPreset)
+ {
+ return this.SanitizePresetFullName(conversionPreset.FullName) != conversionPreset.FullName;
+ }
+
+ private string SanitizePresetFullName(string fullName)
+ {
+ if (string.IsNullOrWhiteSpace(fullName))
+ {
+ return "Imported preset";
+ }
+
+ string[] segments = fullName.Split('/');
+ for (int index = 0; index < segments.Length; index++)
+ {
+ segments[index] = this.SanitizePresetNameSegment(segments[index], index == segments.Length - 1);
}
+
+ return string.Join("/", segments);
+ }
+
+ private string SanitizePresetNameSegment(string segment, bool isPresetName)
+ {
+ if (string.IsNullOrWhiteSpace(segment))
+ {
+ return isPresetName ? "Imported preset" : "Imported";
+ }
+
+ char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
+ StringBuilder builder = new StringBuilder(segment.Length);
+ for (int index = 0; index < segment.Length; index++)
+ {
+ char character = segment[index];
+ if (char.IsControl(character) ||
+ character == '"' ||
+ character == '/' ||
+ character == '\\' ||
+ Array.IndexOf(invalidFileNameChars, character) >= 0)
+ {
+ builder.Append('_');
+ continue;
+ }
+
+ builder.Append(character);
+ }
+
+ string sanitizedSegment = builder.ToString().Trim();
+ if (string.IsNullOrEmpty(sanitizedSegment) ||
+ sanitizedSegment == "." ||
+ sanitizedSegment == "..")
+ {
+ return isPresetName ? "Imported preset" : "Imported";
+ }
+
+ if (sanitizedSegment.StartsWith("-", StringComparison.Ordinal))
+ {
+ sanitizedSegment = "_" + sanitizedSegment;
+ }
+
+ return sanitizedSegment;
}
private void ExportSelectedPreset()
@@ -699,7 +1124,7 @@ private void RemoveSelectedPreset()
private bool CanRemoveSelectedPreset()
{
- return this.SelectedItem != null;
+ return this.SelectedItem != null && this.SelectedItem.Parent != null;
}
protected override void OnDeactivated()
diff --git a/Application/FileConverter/Views/DiagnosticsWindow.xaml b/Application/FileConverter/Views/DiagnosticsWindow.xaml
index d0eb5eba..f5661838 100644
--- a/Application/FileConverter/Views/DiagnosticsWindow.xaml
+++ b/Application/FileConverter/Views/DiagnosticsWindow.xaml
@@ -28,10 +28,14 @@
+
+
+
+
diff --git a/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml b/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml
index 98ddf1e7..0b2a1125 100644
--- a/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml
+++ b/Application/FileConverter/Views/Resources/ConversionPresetTemplates.xaml
@@ -624,6 +624,9 @@
+
+
+
@@ -710,4 +713,3 @@
-
\ No newline at end of file
diff --git a/Application/FileConverter/Views/SettingsWindow.xaml b/Application/FileConverter/Views/SettingsWindow.xaml
index a7cd4b87..cb500e37 100644
--- a/Application/FileConverter/Views/SettingsWindow.xaml
+++ b/Application/FileConverter/Views/SettingsWindow.xaml
@@ -29,6 +29,16 @@
+
+
@@ -127,11 +137,13 @@
+
+
-
+
@@ -183,7 +195,7 @@
-
+
@@ -343,7 +355,7 @@
-
+
@@ -413,6 +425,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -450,36 +511,36 @@
-
-
+
+
-
+
diff --git a/Application/FileConverterExtension/FileConverterExtension.cs b/Application/FileConverterExtension/FileConverterExtension.cs
index 8ce6f859..31c0e599 100644
--- a/Application/FileConverterExtension/FileConverterExtension.cs
+++ b/Application/FileConverterExtension/FileConverterExtension.cs
@@ -2,6 +2,7 @@
namespace FileConverterExtension
{
+ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
@@ -46,7 +47,13 @@ private bool DisplayPresetIcons
{
get
{
- string displayPresetIcons = PathHelpers.FileConverterRegistryKey.GetValue("DisplayPresetIcons") as string;
+ var registryKey = PathHelpers.FileConverterRegistryKey;
+ if (registryKey == null)
+ {
+ return false;
+ }
+
+ string displayPresetIcons = registryKey.GetValue("DisplayPresetIcons") as string;
if (displayPresetIcons == null)
{
return false;
@@ -67,7 +74,7 @@ private PresetReference[] PresetReferences
{
this.LoadExtensionSettingsIfNecessary();
- return this.presetReferences;
+ return this.presetReferences ?? new PresetReference[0];
}
}
@@ -80,7 +87,7 @@ protected override bool CanShowMenu()
{
foreach (PresetReference presetReference in presets)
{
- if (presetReference.InputTypes.Contains(extension))
+ if (presetReference.InputTypes != null && presetReference.InputTypes.Contains(extension))
{
return true;
}
@@ -100,7 +107,7 @@ protected override ContextMenuStrip CreateMenu()
ToolStripMenuItem fileConverterItem = new ToolStripMenuItem
{
- Text = "File Converter",
+ Text = "ZFileConverter",
Image = new Icon(Properties.Resources.ApplicationIcon, SystemInformation.SmallIconSize).ToBitmap(),
};
@@ -209,13 +216,13 @@ private void RefreshPresetList()
this.RefreshExtensionCacheFromSelectedItems();
// Activate compatible menu entries.
- PresetReference[] presets = this.presetReferences;
+ PresetReference[] presets = this.PresetReferences;
this.menuEntries.Clear();
foreach (string extension in this.extensionCache)
{
foreach (PresetReference presetReference in presets)
{
- if (!presetReference.InputTypes.Contains(extension))
+ if (presetReference.InputTypes == null || !presetReference.InputTypes.Contains(extension))
{
continue;
}
@@ -270,19 +277,13 @@ private void LoadExtensionSettingsIfNecessary()
private void OpenSettings()
{
- if (string.IsNullOrEmpty(PathHelpers.FileConverterPath))
+ string fileConverterPath = this.GetFileConverterPathOrShowError();
+ if (string.IsNullOrEmpty(fileConverterPath))
{
- MessageBox.Show("Can't retrieve the file converter executable path. You should try to reinstall the application.");
return;
}
- if (!File.Exists(PathHelpers.FileConverterPath))
- {
- MessageBox.Show($"Can't find the file converter executable ({PathHelpers.FileConverterPath}). You should try to reinstall the application.");
- return;
- }
-
- ProcessStartInfo processStartInfo = new ProcessStartInfo(PathHelpers.FileConverterPath)
+ ProcessStartInfo processStartInfo = new ProcessStartInfo(fileConverterPath)
{
CreateNoWindow = false,
UseShellExecute = false,
@@ -294,29 +295,21 @@ private void OpenSettings()
stringBuilder.Append("--settings");
processStartInfo.Arguments = stringBuilder.ToString();
- Process exeProcess = Process.Start(processStartInfo);
+ this.TryStartFileConverter(processStartInfo, null);
}
private void ConvertFiles(string presetName)
{
- if (string.IsNullOrEmpty(PathHelpers.FileConverterPath))
- {
- MessageBox.Show("Can't retrieve the file converter executable path. You should try to reinstall the application.");
- return;
- }
-
- if (!File.Exists(PathHelpers.FileConverterPath))
+ string fileConverterPath = this.GetFileConverterPathOrShowError();
+ if (string.IsNullOrEmpty(fileConverterPath))
{
- MessageBox.Show($"Can't find the file converter executable ({PathHelpers.FileConverterPath}). You should try to reinstall the application.");
return;
}
void BuildConversionPresetArgument(StringBuilder sb)
{
- sb.Append("--conversion-preset ");
- sb.Append(" \"");
- sb.Append(presetName);
- sb.Append("\"");
+ AppendArgument(sb, "--conversion-preset");
+ AppendArgument(sb, presetName);
}
// Build arguments string.
@@ -326,9 +319,7 @@ void BuildConversionPresetArgument(StringBuilder sb)
string fileListPath = null;
foreach (var filePath in this.SelectedItemPaths)
{
- stringBuilder.Append(" \"");
- stringBuilder.Append(filePath);
- stringBuilder.Append("\"");
+ AppendArgument(stringBuilder, filePath);
if (stringBuilder.Length >= MaximumProcessArgumentsLength)
{
@@ -337,32 +328,14 @@ void BuildConversionPresetArgument(StringBuilder sb)
BuildConversionPresetArgument(stringBuilder);
// Store list of file to convert in a file in Temp folder.
- fileListPath = Path.Combine(Path.GetTempPath(), "file-converter-input-list.txt");
- int index = 1;
- while (File.Exists(fileListPath))
- {
- fileListPath = Path.Combine(Path.GetTempPath(), $"file-converter-input-list-{index}.txt");
- index++;
- }
-
- using (FileStream file = File.OpenWrite(fileListPath))
- using (StreamWriter writer = new StreamWriter(file))
- {
- foreach (var path in this.SelectedItemPaths)
- {
- writer.WriteLine(path);
- }
- }
-
- stringBuilder.Append(" --input-files ");
- stringBuilder.Append(" \"");
- stringBuilder.Append(fileListPath);
- stringBuilder.Append("\"");
+ fileListPath = CreateInputListFile(this.SelectedItemPaths);
+ AppendArgument(stringBuilder, "--input-files");
+ AppendArgument(stringBuilder, fileListPath);
break;
}
}
- var processStartInfo = new ProcessStartInfo(PathHelpers.FileConverterPath)
+ var processStartInfo = new ProcessStartInfo(fileConverterPath)
{
CreateNoWindow = false,
UseShellExecute = false,
@@ -370,21 +343,144 @@ void BuildConversionPresetArgument(StringBuilder sb)
Arguments = stringBuilder.ToString(),
};
- Process exeProcess = Process.Start(processStartInfo);
+ Process exeProcess = this.TryStartFileConverter(processStartInfo, fileListPath);
+ if (exeProcess == null)
+ {
+ return;
+ }
+
exeProcess.EnableRaisingEvents = true;
exeProcess.Exited += (sender, args) =>
{
- if (fileListPath != null)
+ DeleteInputListFile(fileListPath);
+ };
+ }
+
+ private static string CreateInputListFile(IEnumerable inputPaths)
+ {
+ string inputListFolder = Path.Combine(Path.GetTempPath(), "ZFileConverter");
+ Directory.CreateDirectory(inputListFolder);
+
+ string fileListPath = Path.Combine(inputListFolder, $"input-list-{Guid.NewGuid():N}.txt");
+ using (FileStream file = new FileStream(fileListPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ using (StreamWriter writer = new StreamWriter(file))
+ {
+ foreach (var path in inputPaths)
{
- try
- {
- File.Delete(fileListPath);
- }
- catch
- {
- }
+ writer.WriteLine(path);
}
- };
+ }
+
+ return fileListPath;
+ }
+
+ private static void AppendArgument(StringBuilder stringBuilder, string argument)
+ {
+ if (stringBuilder.Length > 0)
+ {
+ stringBuilder.Append(' ');
+ }
+
+ stringBuilder.Append(QuoteProcessArgument(argument));
+ }
+
+ private static string QuoteProcessArgument(string argument)
+ {
+ if (string.IsNullOrEmpty(argument))
+ {
+ return "\"\"";
+ }
+
+ bool needsQuotes = argument.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) >= 0;
+ if (!needsQuotes)
+ {
+ return argument;
+ }
+
+ StringBuilder quotedArgument = new StringBuilder(argument.Length + 2);
+ quotedArgument.Append('"');
+
+ int backslashes = 0;
+ for (int index = 0; index < argument.Length; index++)
+ {
+ char character = argument[index];
+ if (character == '\\')
+ {
+ backslashes++;
+ continue;
+ }
+
+ if (character == '"')
+ {
+ quotedArgument.Append('\\', (backslashes * 2) + 1);
+ quotedArgument.Append('"');
+ backslashes = 0;
+ continue;
+ }
+
+ quotedArgument.Append('\\', backslashes);
+ backslashes = 0;
+ quotedArgument.Append(character);
+ }
+
+ quotedArgument.Append('\\', backslashes * 2);
+ quotedArgument.Append('"');
+ return quotedArgument.ToString();
+ }
+
+ private string GetFileConverterPathOrShowError()
+ {
+ string fileConverterPath = PathHelpers.FileConverterPath;
+ if (string.IsNullOrEmpty(fileConverterPath))
+ {
+ MessageBox.Show("Can't retrieve the ZFileConverter executable path. You should try to reinstall the application.");
+ return null;
+ }
+
+ if (!File.Exists(fileConverterPath))
+ {
+ MessageBox.Show($"Can't find the ZFileConverter executable ({fileConverterPath}). You should try to reinstall the application.");
+ return null;
+ }
+
+ return fileConverterPath;
+ }
+
+ private Process TryStartFileConverter(ProcessStartInfo processStartInfo, string temporaryInputListPath)
+ {
+ try
+ {
+ Process process = Process.Start(processStartInfo);
+ if (process != null)
+ {
+ return process;
+ }
+
+ MessageBox.Show("Failed to start ZFileConverter.");
+ }
+ catch (Exception exception)
+ {
+ MessageBox.Show($"Failed to start ZFileConverter. {exception.Message}");
+ }
+
+ DeleteInputListFile(temporaryInputListPath);
+ return null;
+ }
+
+ private static void DeleteInputListFile(string fileListPath)
+ {
+ if (fileListPath == null)
+ {
+ return;
+ }
+
+ try
+ {
+ File.Delete(fileListPath);
+ }
+ catch
+ {
+ }
}
}
}
diff --git a/Application/FileConverterExtension/PathHelpers.cs b/Application/FileConverterExtension/PathHelpers.cs
index 21d66f26..cfe18880 100644
--- a/Application/FileConverterExtension/PathHelpers.cs
+++ b/Application/FileConverterExtension/PathHelpers.cs
@@ -36,7 +36,7 @@ public static RegistryKey FileConverterRegistryKey
PathHelpers.fileConverterRegistryKey = Registry.CurrentUser.OpenSubKey(@"Software\FileConverter");
if (PathHelpers.fileConverterRegistryKey == null)
{
- throw new Exception("Can't retrieve file converter registry entry.");
+ PathHelpers.fileConverterRegistryKey = Registry.LocalMachine.OpenSubKey(@"Software\FileConverter");
}
}
@@ -50,13 +50,42 @@ public static string FileConverterPath
{
if (string.IsNullOrEmpty(PathHelpers.fileConverterPath))
{
- PathHelpers.fileConverterPath = PathHelpers.FileConverterRegistryKey.GetValue("Path") as string;
+ RegistryKey registryKey = PathHelpers.FileConverterRegistryKey;
+ if (registryKey == null)
+ {
+ return null;
+ }
+
+ PathHelpers.fileConverterPath = NormalizeFileConverterExecutablePath(registryKey.GetValue("Path") as string);
}
return PathHelpers.fileConverterPath;
}
}
+ private static string NormalizeFileConverterExecutablePath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return null;
+ }
+
+ try
+ {
+ string normalizedPath = Path.GetFullPath(path.Trim('"'));
+ if (!string.Equals(Path.GetFileName(normalizedPath), "FileConverter.exe", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return normalizedPath;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
public static string GetUserDataFolderPath
{
get
diff --git a/Installer/DebugInstaller.bat b/Installer/DebugInstaller.bat
index 8ced2474..e02e5c3e 100644
--- a/Installer/DebugInstaller.bat
+++ b/Installer/DebugInstaller.bat
@@ -6,17 +6,17 @@ REM Analyse command arguments
set quiet="false"
for %%x in (%*) do (
if %%x==--debug (
- set msi="bin\x64\Debug\FileConverter-setup.msi"
+ set msi="bin\x64\Debug\ZFileConverter-setup.msi"
)
if %%x==-d (
- set msi="bin\x64\Debug\FileConverter-setup.msi"
+ set msi="bin\x64\Debug\ZFileConverter-setup.msi"
)
if %%x==--release (
- set msi="bin\x64\Release\FileConverter-setup.msi"
+ set msi="bin\x64\Release\ZFileConverter-setup.msi"
)
if %%x==-r (
- set msi="bin\x64\Release\FileConverter-setup.msi"
+ set msi="bin\x64\Release\ZFileConverter-setup.msi"
)
if %%x==--install (
@@ -54,7 +54,7 @@ if "%action%"=="" (
REM Execute action.
REM msiexec documentation: https://www.advancedinstaller.com/user-guide/msiexec.html
if %action%=="install" (
- echo "Install File Converter using %MSI%..."
+ echo "Install ZFileConverter using %MSI%..."
if %quiet%=="true" (
msiexec /i %MSI% /l*v %TEMP%\vmmsi.log /quiet
@@ -67,7 +67,7 @@ if %action%=="install" (
exit
)
if %action%=="uninstall" (
- echo "Uninstall File Converter using %MSI%..."
+ echo "Uninstall ZFileConverter using %MSI%..."
if %quiet%=="true" (
msiexec /x %MSI% /l*v %TEMP%\vmmsi.log /quiet
diff --git a/Installer/Installer.wixproj b/Installer/Installer.wixproj
index 463ea144..2da8e4ab 100644
--- a/Installer/Installer.wixproj
+++ b/Installer/Installer.wixproj
@@ -1,7 +1,7 @@
2.2.0
- FileConverter-setup
+ ZFileConverter-setup
bin\$(Platform)\$(Configuration)\
obj\$(Platform)\$(Configuration)\
@@ -41,5 +41,5 @@
-
-
\ No newline at end of file
+
+
diff --git a/Installer/Product.wxs b/Installer/Product.wxs
index c60f877f..7105e9e9 100644
--- a/Installer/Product.wxs
+++ b/Installer/Product.wxs
@@ -1,6 +1,6 @@
-
+
@@ -53,7 +53,7 @@
@@ -62,10 +62,10 @@
-
@@ -165,6 +165,11 @@
+
+
+
+
+
@@ -172,4 +177,4 @@
-
\ No newline at end of file
+
diff --git a/PresetSamples/ZaidForgeSmartWebpArchive.xml b/PresetSamples/ZaidForgeSmartWebpArchive.xml
new file mode 100644
index 00000000..70658c76
--- /dev/null
+++ b/PresetSamples/ZaidForgeSmartWebpArchive.xml
@@ -0,0 +1,33 @@
+
+
+
+ arw
+ bmp
+ cr2
+ dds
+ dng
+ exr
+ heic
+ ico
+ jfif
+ jpg
+ jpeg
+ nef
+ png
+ psd
+ raf
+ svg
+ tga
+ tif
+ tiff
+ webp
+ xcf
+ None
+
+
+
+
+
+ (p:documents)(presetpath)\(sm:yyyy-MM)\(f)
+
+
diff --git a/README.md b/README.md
index 4beb94f1..dfc2c36a 100644
--- a/README.md
+++ b/README.md
@@ -1,100 +1,101 @@
-# File Converter
+# ZFileConverter
-## Description
+ZFileConverter is a maintained Windows Explorer-first file conversion utility.
+It keeps the original File Converter idea intact: select files, right-click, choose a preset,
+and get useful outputs without opening a heavy editor.
-**File Converter** is a very simple tool which allows you to convert and compress one or several file(s) using the context menu of windows explorer.
+This fork focuses on making the app feel alive again: smarter presets, safer conversions,
+clearer diagnostics, repair tools, and a reproducible release path.

-You can download it here: [file-converter.io](https://file-converter.io/?from=readme.md).
+## What Is Improved
-You can find more information about what's in File converter and how to use it on the [wiki](https://github.com/Tichau/FileConverter/wiki).
+- Smart output templates for preset names, preset folders, source dates, and formatted counters.
+- AVIF output visibility in preset settings.
+- A Settings > Health tab for FFmpeg, ImageMagick, Ghostscript, Office, settings, and Explorer integration.
+- A one-click Explorer menu repair launcher from Settings.
+- Queue actions for opening completed outputs and retrying failed conversions.
+- Diagnostics actions for copying logs and opening the log folder.
+- Safer cleanup for FFmpeg, Office, CDA, GIF, ICO, PDF/image, and Explorer temp-file flows.
+- Windows CI that builds with MSBuild and uploads validation artifacts.
-## Donate
+## Core Workflow
-File Converter is a personal open source project started in 2014. I have put hundreds of hours adding, refining and tuning File Converter with the goal of making the conversion and compression of files an easy task for everyone.
+1. Install ZFileConverter.
+2. Right-click one or more files in Windows Explorer.
+3. Choose a conversion preset.
+4. Use Settings to customize presets, output folders, file name templates, and health/repair checks.
-You can help me by [contributing to the project](https://github.com/Tichau/FileConverter/wiki#contribute), by [making a donation](https://www.paypal.com/donate/?cmd=_donations&business=3BDWQTYTTA3D8&item_name=File+Converter+Donations¤cy_code=EUR&Z3JncnB0=) or just by [saying thanks](https://saythanks.io/to/Tichau) :).
+## Smart Template Examples
-## Troubleshooting
+Output filename templates now support tokens such as:
-If you encounter any problem with File Converter, you can:
+- `(preset)` or `(presetname)` for the selected preset name.
+- `(presetpath)` for the preset folder path.
+- `(sc:yyyy-MM-dd)` for source creation date.
+- `(sm:yyyy-MM-dd)` for source modified date.
+- `(n:i:D3)` and `(n:c:D3)` for formatted page/frame counters.
-* See the already known problems in the [troubleshooting section of the documentation](https://github.com/Tichau/FileConverter/wiki/Troubleshooting).
-* Or report an issue on the [bug tracker](https://github.com/Tichau/FileConverter/issues).
+Example:
-## Setup development environment
+```text
+(p:documents)ZFileConverter\(presetpath)\(sm:yyyy-MM)\(f) - (preset)
+```
-### Requirements
+## Build
-For File Converter and its explorer extension:
+See [docs/BUILDING.md](docs/BUILDING.md) for the local and CI build path.
-* Visual Studio 2022
+Short version:
-For the installer:
+```powershell
+.\build.ps1 -Configuration Release -Platform x64
+```
-* [Wix 5](http://wixtoolset.org/) (will be installed by nuget)
- * [Community Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17)
-* [Windows SDK Signing Tools for Desktop Apps](https://developer.microsoft.com/fr-fr/windows/downloads/windows-10-sdk)
+## Install
-## Thanks
+See [docs/INSTALLING.md](docs/INSTALLING.md).
-Thanks to all the contributors of File Converter project.
+Recommended path: download the newest `ZFileConverter-*-x64-setup.msi` from
+[GitHub Releases](https://github.com/ZaidNAlAsali/FileConverter/releases), run it,
+then open Settings > Health if the Explorer menu does not appear.
-### Localization
+## Release
-* Thanks to **Khidreal** and **hugok79** for the Portuguese localization.
-* Thanks to **Marhc** for the Brazilian localization.
-* Thanks to **Chachak** for the Spanish localization.
-* Thanks to **Davide** for the Italian localization.
-* Thanks to **nikotschierske** for the German localization.
-* Thanks to **Snoopy1866** for the Simplified Chinese localization.
-* Thanks to **MayaC0re** for the Turkish localization.
-* Thanks to **vishveshjain** for the Hindi localization.
-* Thanks to **Mahmoud0Sultan** for the Arabic localization.
-* Thanks to **Sedimentary-Rock**, **NeKoOuO** and **PeterDaveHello** for the Traditional Chinese localization.
-* Thanks to **CrisBalGreece** for the Greek localization.
-* Thanks to **AshiVered** for the Hebrew localization.
-* Thanks to **MrHero118** and **Mehrdad32** for the Persian localization.
-* Thanks to **crnobog69** for the Serbian localizations.
-* Thanks to **oogamiyuta** for the Japanese localization.
-* Thanks to **AidyTheWeird** for the Czech localization.
-* Thanks to **Alanimdeo** for the Korean localization.
-* Thanks to **vrykolakas166** and **thaovd** for the Vietnamese localization.
-* Thanks to **iliamak** for the Russian localization.
-* Thanks to **itsmefdil** for the Indonesian localization.
-* Thanks to **hamzaharoon1314** for the Urdu localization.
-* Thanks to **Zyvrec7** and **stohlferenc** for the Hungarian localization.
-* Thanks to **Maerek** and **MrPrince419** for the Polish localization.
-* Thanks to **rkalitta** for the Swedish localization.
+Use [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md) before publishing a build.
+The checklist covers clean checkout, dependency restore, smoke conversions, installer verification,
+Explorer integration, diagnostics, version metadata, and GitHub release artifacts.
-## Middlewares
+## Troubleshooting
-File converter uses the following middlewares:
+Open Settings > Health first. It checks the common failure points:
-**ffmpeg** (v8.0.1) as file conversion software.
-Thanks to ffmpeg devs for this awesome open source file conversion tool. [Web site link](https://ffmpeg.org)
+- Missing FFmpeg, ImageMagick, or Ghostscript files.
+- Missing Microsoft Office support for document conversions.
+- Broken Explorer shell registration.
+- Missing default or user settings files.
-**ImageMagick** (v14.10) as image edition and conversion software.
-Thanks to image magick devs for this awesome open source image edition software suite. [Web site link](http://imagemagick.net)
-And thanks to dlemstra for the C# wrapper of this software. [Github link](https://github.com/ImageMagick/ImageMagick)
+If the right-click menu is missing, use Settings > Health > Repair Explorer Menu.
+If a conversion fails, open Diagnostics, copy logs, and include them in an issue.
-**Ghostscript** (10.02.1) as pdf edition software.
-Thanks to ghostscript devs. [Download link](https://www.ghostscript.com/download/gsdnld.html)
+## Development Requirements
-**SharpShell** to easily create windows context menu extensions.
-Thanks to Dave Kerr for his work on SharpShell. [GitHub link](https://github.com/dwmkerr/sharpshell)
+- Windows 10 or newer.
+- Visual Studio 2022 with .NET Framework 4.8 targeting tools.
+- MSBuild on PATH, or Visual Studio Developer PowerShell.
+- WiX 5 for installer builds. The WiX SDK packages restore through NuGet.
+- Windows SDK signing tools only when producing signed release installers.
-**Ripper** and **yeti.mmedia** for CD Audio extraction.
-Thanks to Idael Cardoso for his work on CD Audio ripper. [Code project link](https://www.codeproject.com/Articles/5458/C-Sharp-Ripper)
+## Credits
-**Markdown.XAML** for markdown rendering in the wpf application.
-Thanks to Bevan Arps for his work on Markdown.XAML. [GitHub link](https://github.com/theunrepentantgeek/Markdown.XAML)
+ZFileConverter is a maintained fork of Adrien Allard's File Converter project.
+The original project, contributors, translators, and middleware authors made the core app possible.
-**WpfAnimatedGif** for animated gif rendering in the wpf application.
-Thanks to Thomas Levesque for his work on WpfAnimatedGif. [GitHub link](https://github.com/XamlAnimatedGif/WpfAnimatedGif)
+Middleware used by the app includes FFmpeg, ImageMagick, Ghostscript, SharpShell, Ripper,
+yeti.mmedia, Markdown.Xaml, and WpfAnimatedGif.
## License
-File Converter is licensed under the GPL version 3 License.
-For more information check the LICENSE.md file in your installation folder or the [gnu website](https://www.gnu.org/licenses/gpl.html).
+ZFileConverter is licensed under the GPL version 3.
+See [LICENSE.md](LICENSE.md).
diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md
new file mode 100644
index 00000000..f705dac8
--- /dev/null
+++ b/RELEASE_CHECKLIST.md
@@ -0,0 +1,61 @@
+# ZFileConverter Release Checklist
+
+Use this before publishing a GitHub release.
+
+## Source
+
+- Confirm the release branch is clean.
+- Confirm `version.xml` points to the intended GitHub release asset.
+- Confirm `version.xml` includes the SHA-256 hash of the exact MSI uploaded to the release.
+- If the MSI is signed, confirm `version.xml` includes the expected publisher subject.
+- Update `CHANGELOG.md` with user-facing changes.
+- Confirm `README.md`, `docs/BUILDING.md`, and `docs/INSTALLING.md` match the release process.
+
+## Build
+
+- Build from a clean checkout on Windows.
+- Run:
+
+```powershell
+.\build.ps1 -Configuration Release -Platform x64
+```
+
+- Confirm the app output exists under `Application\FileConverter\bin\x64\Release`.
+- Confirm `Installer\bin\x64\Release\ZFileConverter-setup.msi` exists.
+- Confirm whether the installer is signed or intentionally unsigned.
+- Compute the MSI SHA-256 after signing and before uploading:
+
+```powershell
+Get-FileHash .\Installer\bin\x64\Release\ZFileConverter-setup.msi -Algorithm SHA256
+```
+
+## Smoke Test
+
+- Launch Settings.
+- Open Settings > Health and refresh dependency health.
+- Import a safe preset file and a test preset file with raw FFmpeg settings to confirm the import review dialog appears.
+- Run Explorer menu repair from Settings on a test machine.
+- Convert image to JPG, PNG, WebP, and AVIF.
+- Convert audio or video through FFmpeg.
+- Convert PDF to image through ImageMagick/Ghostscript.
+- Convert Office documents if Word, Excel, and PowerPoint are available.
+- Retry a failed conversion from the queue.
+- Open a completed output folder from the queue.
+- Copy diagnostics logs and open the diagnostics folder.
+
+## Installer
+
+- Install on a clean Windows VM.
+- Confirm Start Menu entries show `ZFileConverter`.
+- Confirm Explorer right-click menu shows `ZFileConverter`.
+- Confirm uninstall removes the Start Menu entry and registry path.
+- Reinstall over the previous version and confirm presets survive.
+
+## GitHub Release
+
+- Create a tag matching the version, for example `v2.2.0-z1`.
+- Prefer the manual `release` workflow. It creates a draft release and uploads a versioned MSI and app zip.
+- Upload the MSI and any app artifact zip.
+- Include whether the installer is signed.
+- Include known limitations and dependency notes.
+- Link to troubleshooting and Settings > Health.
diff --git a/ZFILECONVERTER_ROADMAP.md b/ZFILECONVERTER_ROADMAP.md
new file mode 100644
index 00000000..6f9787c1
--- /dev/null
+++ b/ZFILECONVERTER_ROADMAP.md
@@ -0,0 +1,62 @@
+# ZFileConverter Roadmap
+
+ZFileConverter is the maintained fork identity for the File Converter revival.
+The goal is not to overcomplicate the app. The goal is to keep the right-click workflow simple
+while making the project dependable again.
+
+## Principles
+
+- Keep Windows Explorer conversion as the primary workflow.
+- Prefer presets and repair tools over complicated editors.
+- Make failures explain themselves through Health and Diagnostics.
+- Keep public builds reproducible and unsigned-by-default unless release signing is configured.
+- Preserve credit and GPL-3.0 continuity from the original project.
+
+## Implemented In This Revival Branch
+
+- Smart output template tokens:
+ - `(preset)` and `(presetname)`
+ - `(presetpath)`
+ - `(sc:yyyy-MM-dd)` and `(sm:yyyy-MM-dd)`
+ - `(n:i:D3)` and `(n:c:D3)`
+- AVIF surfaced in the preset output picker.
+- Explorer shell-extension repair command and Settings > Health repair launcher.
+- Dependency health checks for FFmpeg, ImageMagick, Ghostscript, Office, settings, and Explorer registration.
+- Queue actions to open completed outputs and retry failed conversions.
+- Diagnostics actions to copy logs and open the log folder.
+- Safer cleanup and failure handling across FFmpeg, Office, CDA, GIF, ICO, ImageMagick, settings, and shell extension paths.
+- Windows CI with artifact upload.
+- Build guide, release checklist, and PR template.
+
+## Next Practical Moves
+
+- Add lightweight unit tests around template expansion and settings serialization.
+- Add a small sample-files smoke test pack for maintainers.
+- Add release signing documentation once signing credentials are available.
+- Replace or refresh visual branding assets when a dedicated ZFileConverter icon is ready.
+- Add an issue triage label set and first-maintainer milestones.
+
+## Later Design Overhaul
+
+This is intentionally a later phase, not part of the current stabilization pass.
+The current revival should keep the familiar File Converter workflow intact: right-click a file,
+choose a preset, get a clear result. The later design pass can make the app feel more polished,
+modern, beautiful, and user-friendly without turning it into a complicated editor.
+
+- Revisit the Settings window layout, typography, spacing, iconography, empty states, and health diagnostics.
+- Design a proper ZFileConverter app icon, installer visual identity, and GitHub release artwork.
+- Make presets easier to browse, search, edit, duplicate, import, and understand.
+- Improve queue/progress visibility while keeping the Explorer-first workflow fast.
+- Explore a more refined onboarding and troubleshooting flow for missing FFmpeg, ImageMagick, Ghostscript, and Office dependencies.
+- When Mythos is released and available, consider using it as a dedicated frontend/design exploration partner for this UI pass.
+
+## Later Product And Monetization Notes
+
+ZFileConverter should remain useful as a simple, trustworthy local Windows utility first.
+Any commercial path should protect that trust and respect GPL-3.0 continuity.
+
+- Most realistic near-term path: free open-source app with optional donations, sponsorships, and paid support.
+- Stronger business path: paid signed builds, managed enterprise packaging, deployment help, support SLAs, and custom presets/workflows.
+- Possible product path: a hosted/API conversion service, but that would be a separate product with real infrastructure, privacy, abuse, and cost concerns.
+- Avoid making core local conversion annoying, ad-heavy, account-gated, or artificially limited.
+- Before charging for binaries or services, confirm GPL source-distribution obligations and third-party middleware licenses.
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 00000000..fbca65a3
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,81 @@
+param(
+ [ValidateSet("Debug", "Release")]
+ [string]$Configuration = "Release",
+
+ [ValidateSet("x64")]
+ [string]$Platform = "x64"
+)
+
+$ErrorActionPreference = "Stop"
+
+function Write-Step {
+ param([string]$Message)
+ Write-Host ""
+ Write-Host "==> $Message" -ForegroundColor Cyan
+}
+
+function Find-MSBuild {
+ if ($env:MSBUILD_PATH -and (Test-Path -LiteralPath $env:MSBUILD_PATH)) {
+ return (Resolve-Path -LiteralPath $env:MSBUILD_PATH).Path
+ }
+
+ $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
+ if (Test-Path -LiteralPath $vswhere) {
+ $candidate = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1
+ if ($candidate -and (Test-Path -LiteralPath $candidate)) {
+ return $candidate
+ }
+ }
+
+ $pathCommand = Get-Command msbuild.exe -ErrorAction SilentlyContinue
+ if ($pathCommand -and $pathCommand.Source -notlike "$env:windir\Microsoft.NET\Framework*") {
+ return $pathCommand.Source
+ }
+
+ return $null
+}
+
+$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
+Set-Location $repoRoot
+
+if ($env:OS -ne "Windows_NT") {
+ throw "ZFileConverter can only be built on Windows because it targets WPF, .NET Framework, SharpShell, and WiX."
+}
+
+$targetingPack = Join-Path ${env:ProgramFiles(x86)} "Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\RedistList\FrameworkList.xml"
+if (!(Test-Path -LiteralPath $targetingPack)) {
+ throw ".NET Framework 4.8 Developer Pack / targeting pack is missing. Install Visual Studio 2022 with .NET Framework 4.8 targeting tools, then rerun .\build.ps1."
+}
+
+$msbuild = Find-MSBuild
+if (!$msbuild) {
+ throw "Visual Studio MSBuild was not found. Install Visual Studio 2022 Build Tools or run this from Developer PowerShell. The legacy .NET Framework MSBuild is not enough for package restore and WiX SDK projects."
+}
+
+Write-Step "Using MSBuild"
+Write-Host $msbuild
+
+Write-Step "Building ZFileConverter $Configuration $Platform"
+& $msbuild "$repoRoot\FileConverter.sln" /restore /m /p:Configuration=$Configuration /p:Platform=$Platform
+if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+}
+
+$appExe = Join-Path $repoRoot "Application\FileConverter\bin\$Platform\$Configuration\FileConverter.exe"
+$installer = Join-Path $repoRoot "Installer\bin\$Platform\$Configuration\ZFileConverter-setup.msi"
+
+Write-Step "Checking outputs"
+if (!(Test-Path -LiteralPath $appExe)) {
+ throw "Build finished, but the app executable was not found at $appExe"
+}
+
+if ($Configuration -eq "Release" -and !(Test-Path -LiteralPath $installer)) {
+ throw "Build finished, but the MSI was not found at $installer"
+}
+
+Write-Host "App: $appExe" -ForegroundColor Green
+if (Test-Path -LiteralPath $installer) {
+ Write-Host "Installer: $installer" -ForegroundColor Green
+}
+
+Write-Step "Done"
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
new file mode 100644
index 00000000..69c9bbb5
--- /dev/null
+++ b/docs/BUILDING.md
@@ -0,0 +1,64 @@
+# Building ZFileConverter
+
+ZFileConverter is a 64-bit .NET Framework 4.8 WPF application with a SharpShell Explorer
+extension and a WiX installer.
+
+## Requirements
+
+- Windows 10 or newer.
+- Visual Studio 2022.
+- .NET Framework 4.8 targeting pack.
+- MSBuild from Visual Studio, usually available in Developer PowerShell.
+- NuGet package restore enabled.
+- WiX Toolset SDK packages restore through the installer project.
+
+## Restore And Build
+
+From a Visual Studio Developer PowerShell:
+
+```powershell
+.\build.ps1 -Configuration Release -Platform x64
+```
+
+The script checks that Visual Studio MSBuild and the .NET Framework 4.8 targeting pack are
+available before running the restore/build. Set `MSBUILD_PATH` to an explicit MSBuild path if
+you need to override discovery.
+
+The application output is expected at:
+
+```text
+Application\FileConverter\bin\x64\Release\
+```
+
+The installer output is expected at:
+
+```text
+Installer\bin\x64\Release\ZFileConverter-setup.msi
+```
+
+## Unsigned Local Builds
+
+The installer imports `Installer\Installer.sign` only when that file exists.
+That means public CI and local contributors can build unsigned installers without private signing material.
+
+Signed release builds should provide `Installer.sign` locally or through a secure release pipeline.
+
+## Smoke Test
+
+After a release build:
+
+1. Launch `FileConverter.exe --settings`.
+2. Open Settings > Health and confirm bundled dependencies are ready.
+3. Convert one image to WebP or JPG.
+4. Convert one video or audio sample through FFmpeg.
+5. Convert a PDF page to PNG if Ghostscript is present.
+6. Open Diagnostics, copy logs, and confirm the diagnostics folder opens.
+7. Install the MSI in a clean VM and confirm the Explorer context menu appears.
+
+## CI
+
+The GitHub Actions workflow uses `microsoft/setup-msbuild`, restores the solution,
+builds Release x64 through `build.ps1`, and uploads application and installer artifacts when available.
+
+The manual `release` workflow builds the same Release x64 output, renames the MSI to a
+versioned release artifact, zips the app output, and creates a draft GitHub release.
diff --git a/docs/INSTALLING.md b/docs/INSTALLING.md
new file mode 100644
index 00000000..55ccc403
--- /dev/null
+++ b/docs/INSTALLING.md
@@ -0,0 +1,51 @@
+# Installing ZFileConverter
+
+## Recommended: GitHub Release
+
+Download the newest `ZFileConverter-*-x64-setup.msi` from the GitHub Releases page.
+ZFileConverter is released as a 64-bit Windows app:
+
+```text
+https://github.com/ZaidNAlAsali/FileConverter/releases
+```
+
+Run the MSI and follow the installer. After installation, right-click a supported file in
+Windows Explorer and choose `ZFileConverter`.
+
+If Windows SmartScreen warns about an unsigned installer, choose to keep/run it only if you
+downloaded it from the project release page. Code signing can be added later when a signing
+certificate is available.
+
+## From GitHub Actions
+
+Every push and pull request builds Windows x64 artifacts. Open the workflow run, download
+`ZFileConverter-installer-x64`, unzip it, and run `ZFileConverter-setup.msi`.
+
+## From Source
+
+Install:
+
+- Windows 10 or newer.
+- Visual Studio 2022 or Build Tools.
+- .NET Framework 4.8 targeting tools.
+- WiX SDK packages, restored automatically by MSBuild.
+
+Then run:
+
+```powershell
+.\build.ps1 -Configuration Release -Platform x64
+```
+
+Install the generated MSI:
+
+```text
+Installer\bin\x64\Release\ZFileConverter-setup.msi
+```
+
+## First-Run Checks
+
+Open Settings > Health and refresh the checks. It should tell you whether FFmpeg,
+ImageMagick, Ghostscript, Microsoft Office support, settings files, and Explorer integration
+are available.
+
+If the Explorer menu is missing, use Settings > Health > Repair Explorer Menu.
diff --git a/version (x86).xml b/version (x86).xml
index e10b2ed5..2dea1b45 100644
--- a/version (x86).xml
+++ b/version (x86).xml
@@ -1,4 +1,6 @@
+
https://github.com/Tichau/FileConverter/releases/download/v1.2.3/FileConverter-1.2.3-x86-setup.msi
+
diff --git a/version.xml b/version.xml
index 30ac4fad..ed3b29ef 100644
--- a/version.xml
+++ b/version.xml
@@ -1,4 +1,5 @@
- https://github.com/Tichau/FileConverter/releases/download/v2.2/FileConverter-2.2-x64-setup.msi
+ https://github.com/ZaidNAlAsali/FileConverter/releases/download/v2.2.0/ZFileConverter-2.2.0-x64-setup.msi
+ 0B275704F61A58FFA663460F8099E9D6682BDD6DADBC9FAA538D4548496A4CCB