0

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.");
    }
}
4
  • 2
    Don't. Terrible idea. Even if Libre didn't throw an error, as it just did, you'd end up leaving various instances running on the server, until the server run out of memory. A single dialog box would be enough to keep the process running. Commented Nov 20 at 13:45
  • Besides, Libre isn't Excel and the generated output won't look the same. PDF isn't a document format, it's a file containing print instructions. You don't "convert" anything to PDF, you print to it. That means whatever code you use has to generate pages, layout, styling etc. That's not trivial and has a million edge cases. Commercial libraries like those published by Telerik, DevExpress, Syncfusion can print to PDF but none of the open source libraries (ClosedXML, MiniExcel, Slyvan.Data.Excel etc) does. Commented Nov 20 at 13:50
  • Some libraries (eg EPPlus) can convert Excel files to HTML, which can then be converted to PDF. Again, lots of edge cases and commercial libraries are better at it. Some have free licenses for small companies but require an enterprise license for large enterprises Commented Nov 20 at 13:57
  • As the owner of PDF format, Adobe has both server/client solutions for such conversions, developer.adobe.com/document-services/docs/overview/… that’s the proper way (and possibly via other big vendors in this field) to integrate in a web app. Don’t bother with many free/open source options that rarely tested under IIS contexts and come without technical support. Commented Nov 20 at 18:08

1 Answer 1

0

Libre Office on Windows is a user application thus you first install as a permitted user. This ensures you have User Access to resources like Fonts and graphics pipelines.

Then because conversion is a User CONSOLE task you run a CMD and fastest will be a batch file with your options. You can then pass your files to Soffice.COM as used to convert on Windows. This is detailed in the manual currently at https://help.libreoffice.org/25.8/en-US/text/shared/guide/start_parameters.html?&DbPAR=SHARED&System=WIN

Libre.COMmand.bat

@echo off
set "SC=C:\Users\path to \libreoffice\program\soffice.COM"
set "app=none"
if /I "%~x1"==".xlsx" set "app=calc"
if /I "%~x1"==".docx" set "app=writer"
echo running "%SC%" with "%~1" >>runtime.log
if /I "%app%"=="none" echo Unsupported extension: "%~x1" >>runtime.log & goto :eof
"%SC%" --convert-to "pdf:%app%_pdf_Export" "%~1" 

There are many JSON commands you can add, but they may be more format specific so for example with docx: (note the leading :).

"%SC%" --convert-to pdf:writer_pdf_Export:{"ExportBookmarksToPDFDestination":{"type":"boolean","value":"true"}} "%~1" 

Result of:

soffice.COM --convert-to pdf:writer_pdf_Export:{"ExportBookmarksToPDFDestination":{"type":"boolean","value":"true"}} "RFQCodes.docx"

Note this in OLD RFC from 20 years ago and there is no problem with doc text, tables nor graphics layouts. for example a formula retains its relative characters. enter image description here

enter image description here

The one error you may note is the extra line wrap, as here in the DOC file it is at the right of the formula.

enter image description here

If you need a basic .net invocation for portable then it will compile and run in any users context. This works for me as it has my user Writes.

C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /platform:x86 docxTOpdf.cs >nul

using System.Diagnostics;
class Program
{
    static void Main(string[] args)
    {
        if (args.Length < 2) return;
        string inputFile = args[0];
        string outputDir = args[1];
        var p = Process.Start(
            @"C:\Users\WDAGUtilityAccount\Desktop\Apps\Office\Libre\App\libreoffice\program\soffice.com",
            string.Format("--convert-to pdf:writer_pdf_Export --outdir \"{0}\" \"{1}\"", outputDir, inputFile)
        );
        if (p != null) p.WaitForExit();
    }
}

Beware the reason I gave for faster CMD (no C#.net delays) is that it is easy to edit and ensure the TOC and bookmarks are visible. As without that JSON command you will draw a blank. enter image description here enter image description here

For XLSX the styles are usually different compared to interop, as Libre Office is not Microsoft Office. Thus save as PDF from Office will always be far superior than any 3rd party even Acrobat.

Here MS "online" on the left and the PDF from above C#.net on the right.
enter image description here

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.