feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc. Fixes edited values staying recoverable by text extraction. - Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode. - Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon. - Remove the upstream author credit and the in-app install button.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyName Condition="'$(LauncherAssemblyName)' != ''">$(LauncherAssemblyName)</AssemblyName>
|
||||
<ApplicationIcon Condition="'$(LauncherIcon)' != ''">$(LauncherIcon)</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version Condition="'$(LauncherVersion)' != ''">$(LauncherVersion)</Version>
|
||||
<AssemblyVersion Condition="'$(LauncherVersion)' != ''">$(LauncherVersion).0</AssemblyVersion>
|
||||
<FileVersion Condition="'$(LauncherVersion)' != ''">$(LauncherVersion).0</FileVersion>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<!-- The launcher only uses .NET Framework inbox assemblies, so it needs no binding redirects.
|
||||
Disabling their generated config also keeps SDK 10 publish from looking for a config named
|
||||
after the overridden release assembly instead of the project assembly. -->
|
||||
<AutoGenerateBindingRedirects>false</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>false</GenerateBindingRedirectsOutputType>
|
||||
<DefineConstants Condition="'$(AllowUnsignedInstall)' == 'true'">$(DefineConstants);ALLOW_UNSIGNED_INSTALL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="Exists('$(PayloadZip)')">
|
||||
<EmbeddedResource Include="$(PayloadZip)" LogicalName="MmdPdfLauncher.payload.zip" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,449 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace MmdPdfLauncher
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private const string ProductName = "MmdPdf";
|
||||
private const string InnerExeName = "MmdPdf.App.exe";
|
||||
private const string PayloadResourceName = "MmdPdfLauncher.payload.zip";
|
||||
private const string ManifestName = "payload.manifest";
|
||||
private const string PortableMarkerName = ".mmdpdf-portable";
|
||||
private const string TestInstallRootEnvironmentVariable = "MMDPDF_TEST_INSTALL_ROOT";
|
||||
private const string SkipRegistrationEnvironmentVariable = "MMDPDF_SKIP_REGISTRATION";
|
||||
|
||||
private static readonly string UserInstallDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", ProductName);
|
||||
private static readonly string MachineInstallDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), ProductName);
|
||||
private static readonly string PortableRoot = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProductName, "Portable");
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Any(a => string.Equals(a, "/install-user", StringComparison.OrdinalIgnoreCase)))
|
||||
return Install(machine: false, desktop: args.Any(a => string.Equals(a, "/desktop", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (args.Any(a => string.Equals(a, "/silent", StringComparison.OrdinalIgnoreCase)))
|
||||
return Install(machine: true, desktop: false);
|
||||
|
||||
return RunPortable(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
ProductName + " could not start.\n\n" + ex.Message,
|
||||
ProductName,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunPortable(string[] args)
|
||||
{
|
||||
SweepAbandonedPortableDirectories();
|
||||
Directory.CreateDirectory(PortableRoot);
|
||||
|
||||
string version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "unknown";
|
||||
string directory = Path.Combine(PortableRoot,
|
||||
version + "-" + Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture) + "-" + Guid.NewGuid().ToString("N"));
|
||||
|
||||
try
|
||||
{
|
||||
ExtractAndVerify(directory);
|
||||
WritePortableMarker(directory, version, Process.GetCurrentProcess().Id, null);
|
||||
|
||||
var start = new ProcessStartInfo(Path.Combine(directory, InnerExeName), QuoteArguments(args))
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = directory
|
||||
};
|
||||
start.EnvironmentVariables["MMDPDF_LAUNCHER_PATH"] = CurrentExecutablePath();
|
||||
start.EnvironmentVariables["MMDPDF_LAUNCHER_PID"] =
|
||||
Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture);
|
||||
start.EnvironmentVariables["MMDPDF_PORTABLE_ROOT"] = directory;
|
||||
|
||||
using (var child = Process.Start(start))
|
||||
{
|
||||
if (child == null) throw new InvalidOperationException("The application process could not be created.");
|
||||
WritePortableMarker(directory, version, Process.GetCurrentProcess().Id, child.Id);
|
||||
child.WaitForExit();
|
||||
return child.ExitCode;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteDirectoryWithRetries(directory);
|
||||
}
|
||||
}
|
||||
|
||||
private static int Install(bool machine, bool desktop)
|
||||
{
|
||||
if (!IsTrustedForInstall(CurrentExecutablePath()))
|
||||
throw new InvalidOperationException(
|
||||
"Installation was refused because this download does not have a valid MmdPdf digital signature.");
|
||||
|
||||
string? testRoot = Environment.GetEnvironmentVariable(TestInstallRootEnvironmentVariable);
|
||||
if (string.IsNullOrWhiteSpace(testRoot) && !machine &&
|
||||
(File.Exists(Path.Combine(MachineInstallDirectory, InnerExeName)) ||
|
||||
File.Exists(Path.Combine(MachineInstallDirectory, "MmdPdf.exe"))))
|
||||
throw new InvalidOperationException(
|
||||
"MmdPdf is already installed for everyone on this computer. Update that installation, " +
|
||||
"or uninstall it before choosing a per-user install. MmdPdf will not create two installed copies.");
|
||||
|
||||
string destination = !string.IsNullOrWhiteSpace(testRoot)
|
||||
? Path.GetFullPath(testRoot)
|
||||
: (machine ? MachineInstallDirectory : UserInstallDirectory);
|
||||
string parent = Path.GetDirectoryName(destination) ?? throw new InvalidOperationException("Invalid install directory.");
|
||||
Directory.CreateDirectory(parent);
|
||||
|
||||
string staging = destination + ".staging-" + Guid.NewGuid().ToString("N");
|
||||
string backup = destination + ".previous-" + Guid.NewGuid().ToString("N");
|
||||
bool movedExisting = false;
|
||||
try
|
||||
{
|
||||
ExtractAndVerify(staging);
|
||||
|
||||
if (Directory.Exists(destination))
|
||||
{
|
||||
Directory.Move(destination, backup);
|
||||
movedExisting = true;
|
||||
}
|
||||
Directory.Move(staging, destination);
|
||||
|
||||
int registrationExit = string.Equals(
|
||||
Environment.GetEnvironmentVariable(SkipRegistrationEnvironmentVariable), "1", StringComparison.Ordinal)
|
||||
? 0
|
||||
: RunRegistration(destination, machine, desktop);
|
||||
if (registrationExit != 0)
|
||||
throw new InvalidOperationException("Windows integration could not be registered (exit " + registrationExit + ").");
|
||||
|
||||
// A machine-wide install supersedes the current account's per-user copy. This
|
||||
// also covers unattended /silent installs that do not return through the portable
|
||||
// app's InstallAndRelaunch cleanup path.
|
||||
if (machine && string.IsNullOrWhiteSpace(testRoot))
|
||||
RunMaintenance(destination, "/remove-user-install");
|
||||
|
||||
if (movedExisting) DeleteDirectoryWithRetries(backup);
|
||||
return 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
DeleteDirectoryWithRetries(staging);
|
||||
if (movedExisting && Directory.Exists(backup))
|
||||
{
|
||||
DeleteDirectoryWithRetries(destination);
|
||||
if (!Directory.Exists(destination)) Directory.Move(backup, destination);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunRegistration(string directory, bool machine, bool desktop)
|
||||
{
|
||||
var arguments = new List<string> { machine ? "/register-machine" : "/register-user" };
|
||||
if (desktop) arguments.Add("/desktop");
|
||||
var start = new ProcessStartInfo(Path.Combine(directory, InnerExeName), QuoteArguments(arguments.ToArray()))
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = directory
|
||||
};
|
||||
using (var process = Process.Start(start))
|
||||
{
|
||||
if (process == null) return 1;
|
||||
process.WaitForExit();
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunMaintenance(string directory, string argument)
|
||||
{
|
||||
var start = new ProcessStartInfo(Path.Combine(directory, InnerExeName), argument)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = directory
|
||||
};
|
||||
using (var process = Process.Start(start))
|
||||
{
|
||||
if (process == null) return 1;
|
||||
process.WaitForExit();
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractAndVerify(string destination)
|
||||
{
|
||||
Directory.CreateDirectory(destination);
|
||||
using (var payload = Assembly.GetExecutingAssembly().GetManifestResourceStream(PayloadResourceName))
|
||||
{
|
||||
if (payload == null) throw new InvalidOperationException("The application payload is missing.");
|
||||
using (var archive = new ZipArchive(payload, ZipArchiveMode.Read, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.GetEntry(ManifestName)
|
||||
?? throw new InvalidDataException("The payload manifest is missing.");
|
||||
Dictionary<string, ManifestFile> manifest;
|
||||
using (var reader = new StreamReader(manifestEntry.Open(), Encoding.UTF8, true))
|
||||
manifest = ReadManifest(reader);
|
||||
|
||||
var payloadEntries = archive.Entries
|
||||
.Where(e => !string.IsNullOrEmpty(e.Name) && !string.Equals(e.FullName, ManifestName, StringComparison.Ordinal))
|
||||
.ToDictionary(e => NormalizeRelativePath(e.FullName), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (payloadEntries.Count != manifest.Count || manifest.Keys.Any(k => !payloadEntries.ContainsKey(k)))
|
||||
throw new InvalidDataException("The payload contents do not match its manifest.");
|
||||
|
||||
string destinationRoot = EnsureTrailingSeparator(Path.GetFullPath(destination));
|
||||
foreach (var item in manifest.OrderBy(p => p.Key, StringComparer.Ordinal))
|
||||
{
|
||||
string outputPath = Path.GetFullPath(Path.Combine(destination, item.Key.Replace('/', Path.DirectorySeparatorChar)));
|
||||
if (!outputPath.StartsWith(destinationRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("The payload contains an unsafe path.");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? destination);
|
||||
using (var input = payloadEntries[item.Key].Open())
|
||||
using (var output = new FileStream(outputPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
input.CopyTo(output);
|
||||
|
||||
var info = new FileInfo(outputPath);
|
||||
if (info.Length != item.Value.Size || !string.Equals(HashFile(outputPath), item.Value.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("Payload verification failed for " + item.Key + ".");
|
||||
}
|
||||
|
||||
File.WriteAllLines(Path.Combine(destination, ManifestName),
|
||||
manifest.OrderBy(p => p.Key, StringComparer.Ordinal)
|
||||
.Select(p => p.Value.Sha256 + "\t" +
|
||||
p.Value.Size.ToString(CultureInfo.InvariantCulture) + "\t" + p.Key),
|
||||
new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(Path.Combine(destination, InnerExeName)))
|
||||
throw new InvalidDataException("The payload does not contain " + InnerExeName + ".");
|
||||
}
|
||||
|
||||
private static Dictionary<string, ManifestFile> ReadManifest(TextReader reader)
|
||||
{
|
||||
var result = new Dictionary<string, ManifestFile>(StringComparer.OrdinalIgnoreCase);
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#", StringComparison.Ordinal)) continue;
|
||||
var parts = line.Split(new[] { '\t' }, 3);
|
||||
if (parts.Length != 3 || parts[0].Length != 64 || !long.TryParse(parts[1], NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out long size) || size < 0)
|
||||
throw new InvalidDataException("The payload manifest is invalid.");
|
||||
string path = NormalizeRelativePath(parts[2]);
|
||||
if (result.ContainsKey(path))
|
||||
throw new InvalidDataException("The payload manifest contains a duplicate path.");
|
||||
result.Add(path, new ManifestFile(parts[0], size));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeRelativePath(string path)
|
||||
{
|
||||
string normalized = path.Replace('\\', '/').TrimStart('/');
|
||||
if (string.IsNullOrWhiteSpace(normalized) || Path.IsPathRooted(path) || normalized.Contains(":") ||
|
||||
normalized.Split('/').Any(p => p.Length == 0 || p == "." || p == ".."))
|
||||
throw new InvalidDataException("The payload contains an unsafe path: " + path);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void SweepAbandonedPortableDirectories()
|
||||
{
|
||||
if (!Directory.Exists(PortableRoot)) return;
|
||||
foreach (string directory in Directory.GetDirectories(PortableRoot))
|
||||
{
|
||||
string marker = Path.Combine(directory, PortableMarkerName);
|
||||
if (!File.Exists(marker)) continue;
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(marker);
|
||||
if (lines.Length > 0 && string.Equals(lines[0], ProductName, StringComparison.Ordinal) &&
|
||||
!MarkerHasLiveProcess(lines))
|
||||
DeleteDirectoryWithRetries(directory);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static void WritePortableMarker(string directory, string version, int launcherPid, int? childPid)
|
||||
{
|
||||
File.WriteAllLines(Path.Combine(directory, PortableMarkerName), new[]
|
||||
{
|
||||
ProductName,
|
||||
version,
|
||||
launcherPid.ToString(CultureInfo.InvariantCulture),
|
||||
childPid?.ToString(CultureInfo.InvariantCulture) ?? string.Empty
|
||||
}, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
private static bool MarkerHasLiveProcess(string[] lines)
|
||||
{
|
||||
foreach (string text in lines.Skip(2).Take(2))
|
||||
{
|
||||
if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out int pid)) continue;
|
||||
try
|
||||
{
|
||||
using (var process = Process.GetProcessById(pid))
|
||||
if (!process.HasExited) return true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void DeleteDirectoryWithRetries(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory)) return;
|
||||
for (int attempt = 0; attempt < 5; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (string file in Directory.GetFiles(directory, "*", SearchOption.AllDirectories))
|
||||
try { File.SetAttributes(file, FileAttributes.Normal); } catch { }
|
||||
Directory.Delete(directory, recursive: true);
|
||||
return;
|
||||
}
|
||||
catch when (attempt < 4)
|
||||
{
|
||||
System.Threading.Thread.Sleep(150 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTrustedForInstall(string path)
|
||||
{
|
||||
#if ALLOW_UNSIGNED_INSTALL
|
||||
// Local packages produced by build-portable.ps1 are intentionally unsigned and must
|
||||
// remain installable for end-to-end testing. release.ps1 omits this compile-time flag;
|
||||
// its public launcher has no environment-variable or command-line bypass.
|
||||
return true;
|
||||
#else
|
||||
return AuthenticodeTrust.IsValid(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static string HashFile(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
using (var sha = SHA256.Create())
|
||||
return BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", string.Empty);
|
||||
}
|
||||
|
||||
private static string CurrentExecutablePath() => Process.GetCurrentProcess().MainModule?.FileName
|
||||
?? Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
private static string EnsureTrailingSeparator(string path) =>
|
||||
path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
||||
? path
|
||||
: path + Path.DirectorySeparatorChar;
|
||||
|
||||
private static string QuoteArguments(IEnumerable<string> arguments) =>
|
||||
string.Join(" ", arguments.Select(QuoteArgument));
|
||||
|
||||
private static string QuoteArgument(string argument)
|
||||
{
|
||||
if (argument.Length > 0 && argument.All(c => !char.IsWhiteSpace(c) && c != '"')) return argument;
|
||||
var sb = new StringBuilder("\"");
|
||||
int slashes = 0;
|
||||
foreach (char c in argument)
|
||||
{
|
||||
if (c == '\\') { slashes++; continue; }
|
||||
if (c == '"') sb.Append('\\', slashes * 2 + 1).Append('"');
|
||||
else { sb.Append('\\', slashes).Append(c); }
|
||||
slashes = 0;
|
||||
}
|
||||
sb.Append('\\', slashes * 2).Append('"');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private sealed class ManifestFile
|
||||
{
|
||||
internal ManifestFile(string sha256, long size) { Sha256 = sha256; Size = size; }
|
||||
internal string Sha256 { get; }
|
||||
internal long Size { get; }
|
||||
}
|
||||
}
|
||||
|
||||
internal static class AuthenticodeTrust
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WinTrustFileInfo
|
||||
{
|
||||
internal uint Size;
|
||||
internal IntPtr FilePath;
|
||||
internal IntPtr File;
|
||||
internal IntPtr KnownSubject;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WinTrustData
|
||||
{
|
||||
internal uint Size;
|
||||
internal IntPtr PolicyCallbackData;
|
||||
internal IntPtr SipClientData;
|
||||
internal uint UiChoice;
|
||||
internal uint RevocationChecks;
|
||||
internal uint UnionChoice;
|
||||
internal IntPtr Union;
|
||||
internal uint StateAction;
|
||||
internal IntPtr StateData;
|
||||
internal IntPtr UrlReference;
|
||||
internal uint ProviderFlags;
|
||||
internal uint UiContext;
|
||||
internal IntPtr SignatureSettings;
|
||||
}
|
||||
|
||||
private static readonly Guid VerifyGeneric = new Guid("00AAC56B-CD44-11d0-8CC2-00C04FC295EE");
|
||||
|
||||
[DllImport("wintrust.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
|
||||
private static extern uint WinVerifyTrust(IntPtr window, ref Guid action, IntPtr trustData);
|
||||
|
||||
internal static bool IsValid(string path)
|
||||
{
|
||||
IntPtr pathPointer = Marshal.StringToHGlobalUni(path);
|
||||
IntPtr filePointer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinTrustFileInfo)));
|
||||
IntPtr dataPointer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinTrustData)));
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(new WinTrustFileInfo
|
||||
{
|
||||
Size = (uint)Marshal.SizeOf(typeof(WinTrustFileInfo)),
|
||||
FilePath = pathPointer
|
||||
}, filePointer, false);
|
||||
Marshal.StructureToPtr(new WinTrustData
|
||||
{
|
||||
Size = (uint)Marshal.SizeOf(typeof(WinTrustData)),
|
||||
UiChoice = 2,
|
||||
UnionChoice = 1,
|
||||
Union = filePointer,
|
||||
ProviderFlags = 0x1000
|
||||
}, dataPointer, false);
|
||||
var action = VerifyGeneric;
|
||||
return WinVerifyTrust(IntPtr.Zero, ref action, dataPointer) == 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(dataPointer);
|
||||
Marshal.FreeHGlobal(filePointer);
|
||||
Marshal.FreeHGlobal(pathPointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MmdPdfLauncher.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user