I had used LibreOffice portable a while back to convert documents to PDF and I cannot for the life of me figure out how to do it again.
Every time I run this in IIS Express for development I am getting the error:
ERROR: LibreOffice returned a non-zero exit code
which is showing exit code -1. I have LibreOffice soffice.exe sitting in a folder parallel with where I write logs to in inetpub. Not sure if I need to grant some additional permissions or not.
My code is as follows:
using System;
using System.Diagnostics;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
public static class MemoryStreamExtensions
{
// Fixed LibreOffice path for IIS
private static readonly string LibreOfficeExePath = @"C:\inetpub\logs\LibreOfficePortable\soffice.exe";
/// <summary>
/// Converts an Office file (Excel, Word, etc.) in a MemoryStream to PDF using LibreOffice headless.
/// </summary>
/// <param name="inputStream">MemoryStream of the source file</param>
/// <param name="originalFileName">Original file name with extension (e.g. "file.xlsx")</param>
/// <returns>MemoryStream of generated PDF</returns>
public static async Task<MemoryStream> ConvertToPdfAsync(
this MemoryStream inputStream,
string originalFileName)
{
if (inputStream == null)
throw new ArgumentNullException(nameof(inputStream));
if (string.IsNullOrWhiteSpace(originalFileName))
throw new ArgumentException("File name must be provided", nameof(originalFileName));
// Debug log buffer
var log = new StringBuilder();
log.AppendLine("=== LibreOffice PDF Conversion Debug ===");
log.AppendLine($"Timestamp: {DateTime.UtcNow}");
log.AppendLine($"Original Filename: {originalFileName}");
log.AppendLine($"LibreOffice Path: {LibreOfficeExePath}");
// Validate permissions BEFORE anything else
try
{
log.AppendLine("Running ValidateLibreOfficePermissions...");
ValidateLibreOfficePermissions(LibreOfficeExePath);
log.AppendLine("Permission validation PASSED.");
}
catch (Exception ex)
{
log.AppendLine("Permission validation FAILED.");
log.AppendLine(ex.ToString());
throw new Exception(log.ToString());
}
// Create temp directories
string timestamp = DateTime.UtcNow.Ticks.ToString();
string baseTemp = Path.Combine(Path.GetTempPath(), $"LO_Convert_{timestamp}");
string inputDir = Path.Combine(baseTemp, "input");
string outputDir = Path.Combine(baseTemp, "output");
string userProfile = Path.Combine(baseTemp, "profile");
Directory.CreateDirectory(inputDir);
Directory.CreateDirectory(outputDir);
Directory.CreateDirectory(userProfile);
log.AppendLine($"Temp root: {baseTemp}");
string tempInputFile = Path.Combine(inputDir, originalFileName);
string tempOutputFile = Path.Combine(
outputDir,
$"{Path.GetFileNameWithoutExtension(originalFileName)}.pdf"
);
// Write input file
log.AppendLine($"Saving input file to: {tempInputFile}");
using (var fs = new FileStream(tempInputFile, FileMode.Create, FileAccess.Write))
{
inputStream.Position = 0;
await inputStream.CopyToAsync(fs);
}
// Build LO arguments
string args =
$"-env:UserInstallation=file:///{userProfile.Replace("\\", "/")}" +
$" --headless --convert-to pdf --outdir \"{outputDir}\" \"{tempInputFile}\"";
log.AppendLine("LO Arguments:");
log.AppendLine(args);
var startInfo = new ProcessStartInfo
{
FileName = LibreOfficeExePath,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
string stdout = "";
string stderr = "";
int exitCode = -999;
try
{
log.AppendLine("Starting LibreOffice process...");
using var process = new Process { StartInfo = startInfo };
try
{
if (!process.Start())
throw new Exception("Process.Start() returned false — process not launched.");
}
catch (Exception ex)
{
log.AppendLine("FAILED to start LibreOffice process.");
log.AppendLine(ex.ToString());
throw new Exception(log.ToString());
}
log.AppendLine("Process started OK.");
var outTask = process.StandardOutput.ReadToEndAsync();
var errTask = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
stdout = await outTask;
stderr = await errTask;
exitCode = process.ExitCode;
log.AppendLine("=== STDOUT ===");
log.AppendLine(stdout);
log.AppendLine("=== STDERR ===");
log.AppendLine(stderr);
log.AppendLine($"Exit Code: {exitCode}");
}
catch (Exception ex)
{
log.AppendLine("EXCEPTION during conversion:");
log.AppendLine(ex.ToString());
throw new Exception(log.ToString());
}
// Error handling
if (exitCode != 0)
{
log.AppendLine("ERROR: LibreOffice returned a non-zero exit code.");
throw new Exception(log.ToString());
}
if (!File.Exists(tempOutputFile))
{
log.AppendLine($"ERROR: Output PDF not found at: {tempOutputFile}");
throw new Exception(log.ToString());
}
log.AppendLine("SUCCESS: PDF Generated.");
log.AppendLine($"PDF Path: {tempOutputFile}");
byte[] pdfBytes = await File.ReadAllBytesAsync(tempOutputFile);
return new MemoryStream(pdfBytes);
}
/// <summary>
/// Converts a MemoryStream to a Base64 string.
/// </summary>
public static string ToBase64(this MemoryStream stream)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
stream.Position = 0;
return Convert.ToBase64String(stream.ToArray());
}
private static void TryDeleteFile(string path)
{
try
{
if (File.Exists(path)) File.Delete(path);
}
catch
{
// ignore cleanup errors
}
}
private static void ValidateLibreOfficePermissions(string exePath)
{
var debug = new StringBuilder();
debug.AppendLine("=== LibreOffice Permission Check ===");
debug.AppendLine($"Checking executable: {exePath}");
// 1. Does the file exist?
if (!File.Exists(exePath))
throw new FileNotFoundException("LibreOffice executable not found.", exePath);
debug.AppendLine("OK: File exists.");
// 2. Can current identity read the file?
try
{
using var fs = File.Open(exePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
debug.AppendLine("OK: Read access allowed.");
}
catch (Exception ex)
{
debug.AppendLine("ERROR: Cannot read the executable.");
debug.AppendLine(ex.ToString());
throw new Exception(debug.ToString(), ex);
}
// 3. Can the user execute? (Check NTFS ACL)
try
{
var fileInfo = new FileInfo(exePath);
var acl = fileInfo.GetAccessControl(); // <-- This works in .NET 6/7/8
var rules = acl.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
bool hasExecute = false;
foreach (FileSystemAccessRule rule in rules)
{
if (principal.IsInRole(rule.IdentityReference.Value))
{
if (rule.FileSystemRights.HasFlag(FileSystemRights.ExecuteFile) &&
rule.AccessControlType == AccessControlType.Allow)
{
hasExecute = true;
}
}
}
if (!hasExecute)
{
throw new Exception(
$"User '{WindowsIdentity.GetCurrent().Name}' does NOT have Execute rights on: {exePath}"
);
}
}
catch (Exception ex)
{
throw new Exception("Failed while checking file ACL permissions for LibreOffice.", ex);
}
// 4. Check if the file is blocked by Windows
string zonePath = exePath + ":Zone.Identifier";
if (File.Exists(zonePath))
{
debug.AppendLine("WARNING: Executable is BLOCKED by Windows (Zone.Identifier stream).");
debug.AppendLine($"Path: {zonePath}");
debug.AppendLine("Right-click > Properties > Unblock is required.");
throw new Exception(debug.ToString());
}
debug.AppendLine("OK: Executable is not zone-blocked.");
// 5. Try a safe process start (test if process creation is allowed)
try
{
var psi = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c echo ProcessTest",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true
};
using var proc = Process.Start(psi);
proc.WaitForExit();
debug.AppendLine("OK: Process creation is allowed.");
}
catch (Exception ex)
{
debug.AppendLine("ERROR: Cannot start ANY process under this identity.");
debug.AppendLine(ex.ToString());
throw new Exception(debug.ToString(), ex);
}
// If all checks passed:
debug.AppendLine("ALL PERMISSIONS OK.");
}
}





