diff --git a/.gitignore b/.gitignore
index c0901e9..57060b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ x64/
build/
[Bb]in/
[Oo]bj/
+[Oo]utput/
# MSTest test Results
[Tt]est[Rr]esult*/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b56bd29..e92ccc4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+### [1.3.5] - 2026-08-28
+
+- **Office files:** Word, Excel, and PowerPoint can be converted and rasterized when a licensed GhostPDL library is present. Without it, Office APIs throw `GhostscriptPdlLibraryNotFoundException` and point users to Artifex for a commercial Ghostscript.NET license.
+- **Existing processor code:** `GhostscriptProcessor` detects an Office path in the argument list, loads `gpdldll` if it is on the search path, and treats `-dSAFER` as `-dNOSAFER` for that job. Callers can keep `GetLastInstalledVersion()`.
+- **32-bit GhostPDL:** `gpdldll32.dll` exports stdcall names (`_gsapi_revision@8`). Ghostscript.NET now resolves those in x86 processes. NativeAssets `gsdll32.dll` already used undecorated names, so it was unaffected.
+- GhostPDL/SmartOffice is **commercial**. It is not in `Ghostscript.NativeAssets` or on nuget.org. Licensed users copy the library from Ghostscript.NET.Office into their project.
+
### [1.3.4] - 2026-07-20
- **Bundled native library discovery:** `GhostscriptVersionInfo` can locate app-local Ghostscript binaries (for example from `Ghostscript.NativeAssets`) via `TryGetBundledVersion`, `GetBundledVersion`, and `GetPreferredVersion`. `GetLastInstalledVersion` now prefers bundled assets before falling back to a system installation.
diff --git a/Description.md b/Description.md
index d54bed1..173a9ce 100644
--- a/Description.md
+++ b/Description.md
@@ -1,6 +1,6 @@
-**Ghostscript.NET** is the most completed managed wrapper library around the [Ghostscript](https://ghostscript.com) library - an interpreter for PDF and PostScript files.
+**Ghostscript.NET** is the most completed managed wrapper library around the [Ghostscript](https://ghostscript.com) library - an interpreter for PDF and PostScript files. With a licensed GhostPDL library (`gpdldll` / `libgpdl`) it also converts Microsoft Office documents.
-Ghostscript can be provided via a system install or the optional `Ghostscript.NativeAssets` NuGet package (app-local binaries).
+Ghostscript can be provided via a system install or the optional `Ghostscript.NativeAssets` NuGet package (app-local binaries). Office/SmartOffice natives are **not** in that package; licensed users obtain them from Ghostscript.NET.Office.
### Features
diff --git a/Ghostscript.NET.DisplayTest/Ghostscript.NET.DisplayTest.csproj b/Ghostscript.NET.DisplayTest/Ghostscript.NET.DisplayTest.csproj
index 23db73f..b25be71 100644
--- a/Ghostscript.NET.DisplayTest/Ghostscript.NET.DisplayTest.csproj
+++ b/Ghostscript.NET.DisplayTest/Ghostscript.NET.DisplayTest.csproj
@@ -36,6 +36,7 @@
+
diff --git a/Ghostscript.NET.PDFA3Converter.Samples/Ghostscript.NET.PDFA3Converter.Samples.csproj b/Ghostscript.NET.PDFA3Converter.Samples/Ghostscript.NET.PDFA3Converter.Samples.csproj
index 5245519..1239c47 100644
--- a/Ghostscript.NET.PDFA3Converter.Samples/Ghostscript.NET.PDFA3Converter.Samples.csproj
+++ b/Ghostscript.NET.PDFA3Converter.Samples/Ghostscript.NET.PDFA3Converter.Samples.csproj
@@ -9,6 +9,7 @@
+
diff --git a/Ghostscript.NET.PDFA3Converter.Samples/Program.cs b/Ghostscript.NET.PDFA3Converter.Samples/Program.cs
index 949c523..597837e 100644
--- a/Ghostscript.NET.PDFA3Converter.Samples/Program.cs
+++ b/Ghostscript.NET.PDFA3Converter.Samples/Program.cs
@@ -34,18 +34,36 @@ static void Main(string[] args)
if (!GhostscriptVersionInfo.IsGhostscriptInstalled)
{
- throw new Exception("You don't have Ghostscript installed on this machine!");
+ throw new Exception("Ghostscript was not found. Install Ghostscript or reference Ghostscript.NativeAssets.");
}
- ISample sample;
+ Console.WriteLine("Using Ghostscript: " + SamplePaths.ResolveGhostscriptDll());
- sample = new FacturXWithMustangSample();
- sample.Start();
+ int failed = 0;
+ ISample[] samples =
+ {
+ new FacturXWithMustangSample(),
+ new FacturXWithZUGFeRDcsharpSample()
+ };
- sample = new FacturXWithZUGFeRDcsharpSample();
- sample.Start();
+ foreach (ISample sample in samples)
+ {
+ string name = sample.GetType().Name;
+ try
+ {
+ sample.Start();
+ Console.WriteLine(name + " completed.");
+ }
+ catch (Exception ex)
+ {
+ failed++;
+ Console.WriteLine(name + " failed: " + ex.Message);
+ }
+ }
- Console.ReadLine();
+ Console.WriteLine(failed == 0
+ ? "PDFA3Converter samples completed."
+ : failed + " sample(s) failed.");
}
}
}
diff --git a/Ghostscript.NET.PDFA3Converter.Samples/SamplePaths.cs b/Ghostscript.NET.PDFA3Converter.Samples/SamplePaths.cs
new file mode 100644
index 0000000..03ac553
--- /dev/null
+++ b/Ghostscript.NET.PDFA3Converter.Samples/SamplePaths.cs
@@ -0,0 +1,60 @@
+// Copyright (C) 2024 Artifex Software, Inc.
+//
+// This file is part of Ghostscript.NET.
+//
+// Ghostscript.NET is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// Ghostscript.NET is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Ghostscript.NET. If not, see
+//
+// Alternative licensing terms are available from the licensor.
+// For commercial licensing, see or contact
+// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
+// CA 94129, USA, for further information.
+
+using System;
+using System.IO;
+
+namespace Ghostscript.NET.PDFA3Converter.Samples
+{
+ internal static class SamplePaths
+ {
+ public static string ResolveGhostscriptDll()
+ {
+ GhostscriptVersionInfo version = GhostscriptVersionInfo.GetPreferredVersion();
+ return version.DllPath;
+ }
+
+ public static string GetBlankPdf()
+ {
+ string[] candidates =
+ {
+ Path.Combine(AppContext.BaseDirectory, "Samples", "blank.pdf"),
+ Path.GetFullPath(Path.Combine("Samples", "blank.pdf"))
+ };
+
+ foreach (string candidate in candidates)
+ {
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ throw new FileNotFoundException("Could not find Samples/blank.pdf.");
+ }
+
+ public static string GetOutputPath(string fileName)
+ {
+ return Path.Combine(AppContext.BaseDirectory, fileName);
+ }
+ }
+}
diff --git a/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithMustangSample.cs b/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithMustangSample.cs
index 92f3b3b..765f05f 100644
--- a/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithMustangSample.cs
+++ b/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithMustangSample.cs
@@ -46,12 +46,11 @@ public void Start()
zf2p.generateXML(i);
System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
- string outfilename = "my-factur-x-mustang-sample.xml";
+ string outfilename = SamplePaths.GetOutputPath("my-factur-x-mustang-sample.xml");
File.WriteAllBytes(outfilename, zf2p.getXML());
- string gsFilePath = @"C:\Program Files\gs\gs10.06.0\bin\gsdll64.dll";
- Console.WriteLine("Using Ghostscript filepath: " + gsFilePath);
- Console.WriteLine("Ensure this is the filepath to your installed Ghostscript!");
+ string gsFilePath = SamplePaths.ResolveGhostscriptDll();
+ Console.WriteLine("Using Ghostscript: " + gsFilePath);
PDFA3Converter converter = new PDFA3Converter(gsFilePath);
@@ -59,7 +58,9 @@ public void Start()
converter.SetZUGFeRDVersion("2.3");
converter.SetEmbeddedXMLFile(outfilename);
- converter.ConvertToPDFA3(@"Samples/blank.pdf", @"sample-invoice-mustang.pdf");
+ string outputPdf = SamplePaths.GetOutputPath("sample-invoice-mustang.pdf");
+ converter.ConvertToPDFA3(SamplePaths.GetBlankPdf(), outputPdf);
+ Console.WriteLine("Wrote " + outputPdf);
System.IO.File.Delete(outfilename);
}
}
diff --git a/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithZUGFeRDcsharpSample.cs b/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithZUGFeRDcsharpSample.cs
index fa59744..ddc7c84 100644
--- a/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithZUGFeRDcsharpSample.cs
+++ b/Ghostscript.NET.PDFA3Converter.Samples/Samples/FacturXWithZUGFeRDcsharpSample.cs
@@ -38,18 +38,19 @@ public void Start()
{
Console.WriteLine(Environment.NewLine);
Console.WriteLine("Running FacturXWithZUGFeRDcsharpSample");
- string outFilename = "my-factur-x-zugferd-sample.xml";
+ string outFilename = SamplePaths.GetOutputPath("my-factur-x-zugferd-sample.xml");
InvoiceDescriptor invoice = CreateInvoice();
invoice.Save(outFilename, ZUGFeRDVersion.Version22, s2industries.ZUGFeRD.Profile.Comfort);
- string gsFilePath = @"C:\Program Files\gs\gs10.06.0\bin\gsdll64.dll";
- Console.WriteLine("Using Ghostscript filepath: "+ gsFilePath);
- Console.WriteLine("Ensure this is the filepath to your installed Ghostscript!");
+ string gsFilePath = SamplePaths.ResolveGhostscriptDll();
+ Console.WriteLine("Using Ghostscript: " + gsFilePath);
PDFA3Converter converter = new PDFA3Converter(gsFilePath);
converter.SetZUGFeRDProfile("EN 16931");
converter.SetZUGFeRDVersion("2.3");
converter.SetEmbeddedXMLFile(outFilename);
- converter.ConvertToPDFA3(@"Samples/blank.pdf", @"sample-invoice-zugferd.pdf");
+ string outputPdf = SamplePaths.GetOutputPath("sample-invoice-zugferd.pdf");
+ converter.ConvertToPDFA3(SamplePaths.GetBlankPdf(), outputPdf);
+ Console.WriteLine("Wrote " + outputPdf);
System.IO.File.Delete(outFilename);
}
diff --git a/Ghostscript.NET.Samples/Ghostscript.NET.Samples.csproj b/Ghostscript.NET.Samples/Ghostscript.NET.Samples.csproj
index 995d49b..da51195 100644
--- a/Ghostscript.NET.Samples/Ghostscript.NET.Samples.csproj
+++ b/Ghostscript.NET.Samples/Ghostscript.NET.Samples.csproj
@@ -32,6 +32,9 @@
+
+
+
diff --git a/Ghostscript.NET.Samples/Program.cs b/Ghostscript.NET.Samples/Program.cs
index a438eb3..5f3c997 100644
--- a/Ghostscript.NET.Samples/Program.cs
+++ b/Ghostscript.NET.Samples/Program.cs
@@ -25,24 +25,20 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using Ghostscript.NET;
-using Ghostscript.NET.Processor;
-using Ghostscript.NET.Rasterizer;
using Ghostscript.NET.Samples;
-using javax.print.attribute.standard;
-using SkiaSharp;
-using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
Console.WriteLine("Ghostscript.NET Samples");
if (!GhostscriptVersionInfo.IsGhostscriptInstalled)
{
- throw new Exception("You don't have Ghostscript installed on this machine!");
+ throw new Exception("Ghostscript was not found. Install Ghostscript or reference Ghostscript.NativeAssets.");
}
+Console.WriteLine("Using Ghostscript: " + GhostscriptVersionInfo.GetPreferredVersion().DllPath);
+
List samples = new()
{
new GetInkCoverageSample(),
@@ -57,18 +53,34 @@
new DeviceUsageSample(),
new PipedOutputSample(),
new SendToPrinterSample(),
- new UnicodeTestSample()
+ new UnicodeTestSample(),
+ new OfficeSupportSample()
};
+string outputDir = SampleFiles.OutputDirectory;
+Directory.CreateDirectory(outputDir);
+Directory.CreateDirectory("Output");
+
+int failed = 0;
foreach (ISample sample in samples)
{
- string path = @"Output";
-
- if (!Directory.Exists(path))
+ string name = sample.GetType().Name;
+ Console.WriteLine();
+ Console.WriteLine("--- " + name + " ---");
+ try
{
- Directory.CreateDirectory(path);
+ sample.Start();
+ Console.WriteLine("Sample '" + name + "' completed.");
}
+ catch (Exception ex)
+ {
+ failed++;
+ Console.WriteLine("Sample '" + name + "' failed: " + ex.Message);
+ }
+}
- sample.Start();
- Console.WriteLine($"Sample '{sample.GetType().Name}' run successful!");
-}
\ No newline at end of file
+Console.WriteLine();
+Console.WriteLine(failed == 0
+ ? "All samples completed. Exiting."
+ : failed + " sample(s) failed. Exiting.");
+Environment.Exit(failed == 0 ? 0 : 1);
diff --git a/Ghostscript.NET.Samples/SampleFiles.cs b/Ghostscript.NET.Samples/SampleFiles.cs
new file mode 100644
index 0000000..10b35f3
--- /dev/null
+++ b/Ghostscript.NET.Samples/SampleFiles.cs
@@ -0,0 +1,67 @@
+//
+// SampleFiles.cs
+// This file is part of Ghostscript.NET.Samples project
+//
+// Author: Artifex Software Inc.
+// Copyright (c) 2026 by Artifex Software Inc. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Ghostscript.NET.Samples
+{
+ internal static class SampleFiles
+ {
+ public static string Get(string fileName)
+ {
+ string path;
+ if (TryGet(fileName, out path))
+ {
+ return path;
+ }
+
+ throw new FileNotFoundException("Sample file not found: " + fileName);
+ }
+
+ public static bool TryGet(string fileName, out string path)
+ {
+ path = null;
+
+ foreach (string directory in GetSearchDirectories())
+ {
+ string candidate = Path.Combine(directory, fileName);
+ if (File.Exists(candidate))
+ {
+ path = Path.GetFullPath(candidate);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static string OutputDirectory
+ {
+ get
+ {
+ string directory = Path.Combine(AppContext.BaseDirectory, "Output");
+ Directory.CreateDirectory(directory);
+ return directory;
+ }
+ }
+
+ private static IEnumerable GetSearchDirectories()
+ {
+ yield return Path.Combine(AppContext.BaseDirectory, "TestFiles");
+ yield return Path.GetFullPath("TestFiles");
+
+ string directory = AppContext.BaseDirectory;
+ for (int i = 0; i < 6 && !string.IsNullOrEmpty(directory); i++)
+ {
+ yield return Path.Combine(directory, "TestFiles");
+ directory = Path.GetDirectoryName(directory);
+ }
+ }
+ }
+}
diff --git a/Ghostscript.NET.Samples/Samples/OfficeSupportSample.cs b/Ghostscript.NET.Samples/Samples/OfficeSupportSample.cs
new file mode 100644
index 0000000..3e1ed18
--- /dev/null
+++ b/Ghostscript.NET.Samples/Samples/OfficeSupportSample.cs
@@ -0,0 +1,136 @@
+//
+// OfficeSupportSample.cs
+// This file is part of Ghostscript.NET.Samples project
+//
+// Author: Artifex Software Inc.
+// Copyright (c) 2026 by Artifex Software Inc. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.IO;
+using Ghostscript.NET.Processor;
+using Ghostscript.NET.Rasterizer;
+using SkiaSharp;
+
+namespace Ghostscript.NET.Samples
+{
+ ///
+ /// Office files require a licensed GhostPDL library (gpdldll64.dll / libgpdl.so),
+ /// not standard Ghostscript. Place the DLL from Ghostscript.NET.Office in the app folder
+ /// or set GHOSTPDL_DLL.
+ ///
+ public class OfficeSupportSample : ISample
+ {
+ public void Start()
+ {
+ GhostscriptVersionInfo pdl;
+ if (!GhostscriptVersionInfo.TryGetGhostPdlVersion(out pdl))
+ {
+ Console.WriteLine(GhostscriptOffice.CommercialLicenseRequiredMessage);
+ Console.WriteLine("Skipping OfficeSupportSample: no licensed GhostPDL library was found.");
+ return;
+ }
+
+ string inputPath = FindSampleOfficeFile();
+ if (inputPath == null)
+ {
+ Console.WriteLine("Skipping OfficeSupportSample: no .doc/.docx test file found.");
+ return;
+ }
+
+ Console.WriteLine("Using GhostPDL: " + pdl.DllPath);
+ Console.WriteLine("Input: " + inputPath);
+
+ string outputDir = Path.GetFullPath("Output");
+ Directory.CreateDirectory(outputDir);
+
+ string pdfPath = Path.Combine(outputDir, "OfficeSupportSample.pdf");
+ GhostscriptOffice.ConvertToPdf(inputPath, pdfPath, pdl);
+ Console.WriteLine("PDF: " + pdfPath);
+
+ using (GhostscriptProcessor processor = GhostscriptProcessor.CreateForInput(inputPath))
+ {
+ processor.Process(new[]
+ {
+ "-ghostscript.net",
+ "-dNOPAUSE",
+ "-dBATCH",
+ "-sDEVICE=png16m",
+ "-r96",
+ "-sOutputFile=" + Path.Combine(outputDir, "OfficeSupportSample-%03d.png"),
+ "-f",
+ Path.GetFullPath(inputPath)
+ });
+ }
+
+ using (GhostscriptRasterizer rasterizer = new GhostscriptRasterizer())
+ {
+ rasterizer.Open(inputPath);
+ Console.WriteLine("Page count: " + rasterizer.PageCount);
+
+ if (rasterizer.PageCount > 0)
+ {
+ SKBitmap page = rasterizer.GetPage(96, 1);
+ if (page != null)
+ {
+ string pngPath = Path.Combine(outputDir, "OfficeSupportSample-rasterizer.png");
+ using (SKImage image = SKImage.FromBitmap(page))
+ using (SKData data = image.Encode(SKEncodedImageFormat.Png, 100))
+ using (FileStream stream = File.OpenWrite(pngPath))
+ {
+ data.SaveTo(stream);
+ }
+
+ Console.WriteLine("Rasterizer page 1: " + pngPath);
+ }
+ }
+ }
+ }
+
+ private static string FindSampleOfficeFile()
+ {
+ string[] names = { "OfficeSample.doc", "OfficeSample.docx" };
+ string dir = AppContext.BaseDirectory;
+
+ for (int i = 0; i < 8 && !string.IsNullOrEmpty(dir); i++)
+ {
+ foreach (string name in names)
+ {
+ string candidate = Path.Combine(dir, "TestFiles", name);
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+
+ candidate = Path.Combine(dir, name);
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ dir = Path.GetDirectoryName(dir);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Ghostscript.NET.Samples/Samples/PipedOutputSample.cs b/Ghostscript.NET.Samples/Samples/PipedOutputSample.cs
index 4ca8a29..f529459 100644
--- a/Ghostscript.NET.Samples/Samples/PipedOutputSample.cs
+++ b/Ghostscript.NET.Samples/Samples/PipedOutputSample.cs
@@ -42,7 +42,9 @@ public class PipedOutputSample : ISample
{
public void Start()
{
- string inputFile = @"..\..\..\TestFiles\PipedOutputSample.ps";
+ string inputFile = SampleFiles.TryGet("PipedOutputSample.ps", out string ps)
+ ? ps
+ : @"..\..\..\TestFiles\PipedOutputSample.ps";
GhostscriptPipedOutput gsPipedOutput = new GhostscriptPipedOutput();
@@ -76,7 +78,8 @@ public void Start()
//}
//else if (writeToDisk)
//{
- File.WriteAllBytes(@".\output\PipedOutputSample.pdf", rawDocumentData);
+ Directory.CreateDirectory(SampleFiles.OutputDirectory);
+ File.WriteAllBytes(Path.Combine(SampleFiles.OutputDirectory, "PipedOutputSample.pdf"), rawDocumentData);
//}
}
catch (Exception ex)
diff --git a/Ghostscript.NET.Samples/Samples/SendToPrinterSample.cs b/Ghostscript.NET.Samples/Samples/SendToPrinterSample.cs
index 190d1d6..b2ff9d8 100644
--- a/Ghostscript.NET.Samples/Samples/SendToPrinterSample.cs
+++ b/Ghostscript.NET.Samples/Samples/SendToPrinterSample.cs
@@ -34,10 +34,19 @@ public class SendToPrinterSample : ISample
{
public void Start()
{
- // YOU NEED TO HAVE ADMINISTRATOR RIGHTS TO RUN THIS CODE
+ // mswinpr2 + "Microsoft Print to PDF" opens a Save dialog and never returns,
+ // which keeps Ghostscript.NET.Samples from exiting.
+ string runPrinter = Environment.GetEnvironmentVariable("GHOSTSCRIPT_NET_RUN_PRINTER");
+ if (!string.Equals(runPrinter, "1", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.WriteLine("Skipping SendToPrinterSample (would wait on a printer/save dialog). Set GHOSTSCRIPT_NET_RUN_PRINTER=1 to run it.");
+ return;
+ }
string printerName = "Microsoft Print to PDF";
- string inputFile = @"..\..\..\TestFiles\SendToPrinterSample.pdf";
+ string inputFile = SampleFiles.TryGet("SendToPrinterSample.pdf", out string pdf)
+ ? pdf
+ : @"..\..\..\TestFiles\SendToPrinterSample.pdf";
using (GhostscriptProcessor processor = new GhostscriptProcessor())
{
diff --git a/Ghostscript.NET.Samples/Samples/ViewerSample.cs b/Ghostscript.NET.Samples/Samples/ViewerSample.cs
index 845bad0..f07d942 100644
--- a/Ghostscript.NET.Samples/Samples/ViewerSample.cs
+++ b/Ghostscript.NET.Samples/Samples/ViewerSample.cs
@@ -25,12 +25,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Threading;
using SkiaSharp;
// required Ghostscript.NET namespaces
using Ghostscript.NET;
+using Ghostscript.NET.Processor;
using Ghostscript.NET.Viewer;
namespace Ghostscript.NET.Samples
@@ -58,8 +60,11 @@ public void Start()
GhostscriptVersionInfo.GetLastInstalledVersion();
_firstPageDone = new ManualResetEventSlim(false);
+ bool pageReady = false;
try
{
+ string inputPath = PrepareViewerInput();
+
// create a new instance of the viewer
_viewer = new GhostscriptViewer();
@@ -74,23 +79,56 @@ public void Start()
_viewer.DisplayUpdate += new GhostscriptViewerViewEventHandler(_viewer_DisplayUpdate);
_viewer.DisplayPage += new GhostscriptViewerViewEventHandler(_viewer_DisplayPage);
- // Use TestFiles\PipedOutputSample.ps (included). For PDF, place e.g.
- // ProcessorSample1.pdf in TestFiles and change the path below.
- // If you want to use multiple viewers within a single process then pass
- // true as the last parameter so Ghostscript loads from memory.
+ // Open a PDF. Opening PipedOutputSample.ps directly with GhostscriptViewer
+ // can block forever inside gsapi_run_string (DisplaySize fires, DisplayPage does not).
Console.WriteLine("Rendering first page to Output\\ViewerSample.png ...");
- _viewer.Open(@"..\..\..\TestFiles\PipedOutputSample.ps", _lastInstalledVersion, false);
- if (!_firstPageDone.Wait(TimeSpan.FromSeconds(120)))
+ // Open on a background thread so a stuck gsapi call cannot freeze the sample runner.
+ Exception openError = null;
+ Thread openThread = new Thread(() =>
+ {
+ try
+ {
+ _viewer.Open(inputPath, _lastInstalledVersion, false);
+ }
+ catch (Exception ex)
+ {
+ openError = ex;
+ try { _firstPageDone?.Set(); } catch { }
+ }
+ });
+ openThread.IsBackground = true;
+ openThread.Name = "GhostscriptViewer.Open";
+ openThread.Start();
+
+ pageReady = _firstPageDone.Wait(TimeSpan.FromSeconds(30));
+ if (!pageReady)
{
Console.WriteLine("Timed out waiting for the first page (check Ghostscript and the input file).");
}
+
+ if (openError != null)
+ {
+ throw openError;
+ }
}
finally
{
- if (_viewer != null)
+ if (pageReady && _viewer != null)
+ {
+ try
+ {
+ _viewer.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Viewer dispose: " + ex.Message);
+ }
+
+ _viewer = null;
+ }
+ else
{
- _viewer.Dispose();
_viewer = null;
}
@@ -146,8 +184,8 @@ void _viewer_DisplayPage(object sender, GhostscriptViewerViewEventArgs e)
return;
}
- Directory.CreateDirectory("Output");
- string outPath = Path.Combine("Output", "ViewerSample.png");
+ Directory.CreateDirectory(SampleFiles.OutputDirectory);
+ string outPath = Path.Combine(SampleFiles.OutputDirectory, "ViewerSample.png");
using (SKImage image = SKImage.FromBitmap(bmp))
using (SKData data = image.Encode(SKEncodedImageFormat.Png, 100))
using (Stream stream = File.Create(outPath))
@@ -167,6 +205,44 @@ void _viewer_DisplayPage(object sender, GhostscriptViewerViewEventArgs e)
}
}
+ private static string PrepareViewerInput()
+ {
+ string pdfPath;
+ if (SampleFiles.TryGet("ViewerSample.pdf", out pdfPath))
+ {
+ return pdfPath;
+ }
+
+ if (SampleFiles.TryGet("ProcessorSample1.pdf", out pdfPath))
+ {
+ return pdfPath;
+ }
+
+ string psPath;
+ if (!SampleFiles.TryGet("PipedOutputSample.ps", out psPath))
+ {
+ throw new FileNotFoundException("No viewer sample input file was found.");
+ }
+
+ string outputPdf = Path.Combine(SampleFiles.OutputDirectory, "ViewerSample-input.pdf");
+ using (GhostscriptProcessor processor = new GhostscriptProcessor())
+ {
+ processor.Process(new[]
+ {
+ "-dBATCH",
+ "-dNOPAUSE",
+ "-dNOPROMPT",
+ "-dNOSAFER",
+ "-sDEVICE=pdfwrite",
+ "-sOutputFile=" + outputPdf,
+ "-f",
+ psPath
+ });
+ }
+
+ return outputPdf;
+ }
+
// dummy method just to list other viewer properties and methods
private void Other_Viewer_Methods()
{
diff --git a/Ghostscript.NET.Samples/TestFiles/OfficeSample.doc b/Ghostscript.NET.Samples/TestFiles/OfficeSample.doc
new file mode 100644
index 0000000..b3a82c0
Binary files /dev/null and b/Ghostscript.NET.Samples/TestFiles/OfficeSample.doc differ
diff --git a/Ghostscript.NET.Samples/TestFiles/OfficeSample.docx b/Ghostscript.NET.Samples/TestFiles/OfficeSample.docx
new file mode 100644
index 0000000..365b8e1
Binary files /dev/null and b/Ghostscript.NET.Samples/TestFiles/OfficeSample.docx differ
diff --git a/Ghostscript.NET.Viewer/FMain.cs b/Ghostscript.NET.Viewer/FMain.cs
index 9b1aa02..bec8623 100644
--- a/Ghostscript.NET.Viewer/FMain.cs
+++ b/Ghostscript.NET.Viewer/FMain.cs
@@ -147,8 +147,8 @@ private void FMain_Load(object sender, EventArgs e)
private void mnuFileOpen_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
- ofd.Title = "Open PDF file";
- ofd.Filter = "PDF, PS, EPS files|*.pdf;*.ps;*.eps";
+ ofd.Title = "Open document";
+ ofd.Filter = "Documents|*.pdf;*.ps;*.eps;*.doc;*.docx;*.xls;*.xlsx;*.ppt;*.pptx;*.odt;*.ods;*.odp;*.rtf|PDF, PS, EPS files|*.pdf;*.ps;*.eps|Office files|*.doc;*.docx;*.xls;*.xlsx;*.ppt;*.pptx;*.odt;*.ods;*.odp;*.rtf|All files|*.*";
if (ofd.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
{
diff --git a/Ghostscript.NET/Exceptions/GhostscriptPdlLibraryNotFoundException.cs b/Ghostscript.NET/Exceptions/GhostscriptPdlLibraryNotFoundException.cs
new file mode 100644
index 0000000..340f542
--- /dev/null
+++ b/Ghostscript.NET/Exceptions/GhostscriptPdlLibraryNotFoundException.cs
@@ -0,0 +1,59 @@
+//
+// GhostscriptPdlLibraryNotFoundException.cs
+// This file is part of Ghostscript.NET library
+//
+// Author: Artifex Software Inc.
+// Copyright (c) 2026 by Artifex Software Inc. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+
+namespace Ghostscript.NET
+{
+ ///
+ /// Thrown when an Office file is processed but no GhostPDL native library was found.
+ ///
+ public class GhostscriptPdlLibraryNotFoundException : GhostscriptException
+ {
+ ///
+ /// Creates an exception describing how to locate GhostPDL.
+ ///
+ public GhostscriptPdlLibraryNotFoundException()
+ : base(BuildMessage(), -1002) { }
+
+ ///
+ /// Creates an exception with a custom message.
+ ///
+ public GhostscriptPdlLibraryNotFoundException(string message)
+ : base(message, -1002) { }
+
+ private static string BuildMessage()
+ {
+ string bitness = Environment.Is64BitProcess ? "64-bit" : "32-bit";
+ string dllName = Environment.Is64BitProcess ? "gpdldll64.dll" : "gpdldll32.dll";
+ return
+ GhostscriptOffice.CommercialLicenseRequiredMessage +
+ " If you already have that license, copy the " + bitness + " GhostPDL library (" + dllName + ") " +
+ "from Ghostscript.NET.Office into your application folder, set GHOSTPDL_DLL to its full path, " +
+ "or pass a GhostscriptVersionInfo that points at GhostPDL.";
+ }
+ }
+}
diff --git a/Ghostscript.NET/Ghostscript.NET.csproj b/Ghostscript.NET/Ghostscript.NET.csproj
index cde28fc..e747841 100644
--- a/Ghostscript.NET/Ghostscript.NET.csproj
+++ b/Ghostscript.NET/Ghostscript.NET.csproj
@@ -9,10 +9,10 @@
$(GhostscriptNetVersion)
A C# binding for Ghostscript library
Description.md
- C#;F#;VB.Net;Ghostscript;DotNet;PDF
+ C#;F#;VB.Net;Ghostscript;DotNet;PDF;Office;DOCX
True
gs-icon.png
- Adds bundled native-library discovery and the optional Ghostscript.NativeAssets companion package.
+ Adds Microsoft Office support when a licensed GhostPDL (SmartOffice) native library is placed in the application.
diff --git a/Ghostscript.NET/GhostscriptLibrary.cs b/Ghostscript.NET/GhostscriptLibrary.cs
index 6ed894c..788568e 100644
--- a/Ghostscript.NET/GhostscriptLibrary.cs
+++ b/Ghostscript.NET/GhostscriptLibrary.cs
@@ -49,6 +49,11 @@ public class GhostscriptLibrary : IDisposable
#endregion
+ internal GhostscriptVersionInfo VersionInfo
+ {
+ get { return _version; }
+ }
+
#region Constructor - buffer
///
diff --git a/Ghostscript.NET/GhostscriptNativeKind.cs b/Ghostscript.NET/GhostscriptNativeKind.cs
new file mode 100644
index 0000000..cce82c4
--- /dev/null
+++ b/Ghostscript.NET/GhostscriptNativeKind.cs
@@ -0,0 +1,47 @@
+//
+// GhostscriptNativeKind.cs
+// This file is part of Ghostscript.NET library
+//
+// Author: Artifex Software Inc.
+// Copyright (c) 2026 by Artifex Software Inc. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+namespace Ghostscript.NET
+{
+ ///
+ /// Identifies which native interpreter a loaded library is.
+ ///
+ public enum GhostscriptNativeKind
+ {
+ ///
+ /// Standard Ghostscript library (gsdll64.dll / libgs.so).
+ /// PDF, PostScript, and EPS only.
+ ///
+ Ghostscript = 0,
+
+ ///
+ /// GhostPDL library (gpdldll64.dll / libgpdl.so).
+ /// Same gsapi_* surface as Ghostscript, plus Office (SmartOffice),
+ /// PCL, XPS, and additional image languages.
+ ///
+ GhostPdl = 1
+ }
+}
diff --git a/Ghostscript.NET/GhostscriptOffice.cs b/Ghostscript.NET/GhostscriptOffice.cs
new file mode 100644
index 0000000..0ba9938
--- /dev/null
+++ b/Ghostscript.NET/GhostscriptOffice.cs
@@ -0,0 +1,393 @@
+//
+// GhostscriptOffice.cs
+// This file is part of Ghostscript.NET library
+//
+// Author: Artifex Software Inc.
+// Copyright (c) 2026 by Artifex Software Inc. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Ghostscript.NET.Processor;
+
+namespace Ghostscript.NET
+{
+ ///
+ /// Microsoft Office / OpenDocument support via GhostPDL (SmartOffice).
+ /// Ghostscript itself cannot open these files; GhostPDL converts them
+ /// internally (Office → PDF) then runs the Ghostscript pipeline.
+ ///
+ public static class GhostscriptOffice
+ {
+ ///
+ /// Artifex contact page for a commercial Ghostscript.NET license (Office / SmartOffice).
+ ///
+ public const string CommercialLicenseUrl = "https://artifex.com/contact/ghostscript";
+
+ ///
+ /// Message shown when Office files are used without a commercial Ghostscript.NET license
+ /// (no SmartOffice-enabled GhostPDL library).
+ ///
+ public static string CommercialLicenseRequiredMessage
+ {
+ get
+ {
+ return
+ "Microsoft Office files are a commercial Ghostscript.NET feature and are not included in the open-source (AGPL) package. " +
+ "To convert Word, Excel, PowerPoint, and related files, obtain a commercial Ghostscript.NET license from Artifex: " +
+ CommercialLicenseUrl + " " +
+ "Licensed customers receive the SmartOffice-enabled GhostPDL native library (Ghostscript.NET.Office).";
+ }
+ }
+
+ ///
+ /// Filename extensions handled by the GhostPDL SmartOffice language.
+ ///
+ public static readonly string[] SupportedExtensions = new[]
+ {
+ ".doc", ".docx",
+ ".xls", ".xlsx",
+ ".ppt", ".pptx",
+ ".odt", ".ods", ".odp",
+ ".rtf", ".csv",
+ ".hwp", ".hwpx"
+ };
+
+ ///
+ /// Returns true when has an Office / OpenDocument extension.
+ ///
+ public static bool IsOfficeFile(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return false;
+ }
+
+ return IsOfficeExtension(Path.GetExtension(path));
+ }
+
+ ///
+ /// Returns true when is a supported Office extension.
+ ///
+ public static bool IsOfficeExtension(string extension)
+ {
+ if (string.IsNullOrWhiteSpace(extension))
+ {
+ return false;
+ }
+
+ if (extension[0] != '.')
+ {
+ extension = "." + extension;
+ }
+
+ for (int i = 0; i < SupportedExtensions.Length; i++)
+ {
+ if (string.Equals(SupportedExtensions[i], extension, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Returns true when any path in is an Office file.
+ ///
+ public static bool ContainsOfficeFile(IEnumerable paths)
+ {
+ if (paths == null)
+ {
+ return false;
+ }
+
+ foreach (string path in paths)
+ {
+ if (IsOfficeFile(path))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Rewrites processor arguments so existing Ghostscript samples can open Office files:
+ /// full paths, and -dSAFER replaced with -dNOSAFER (SmartOffice must read the input).
+ ///
+ internal static string[] PrepareProcessorArgs(string[] args)
+ {
+ List result = new List(args.Length + 1);
+ bool hasNoSafer = false;
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ string arg = args[i];
+
+ if (IsSaferSwitch(arg))
+ {
+ if (!hasNoSafer)
+ {
+ result.Add("-dNOSAFER");
+ hasNoSafer = true;
+ }
+ continue;
+ }
+
+ if (IsNoSaferSwitch(arg))
+ {
+ if (!hasNoSafer)
+ {
+ result.Add("-dNOSAFER");
+ hasNoSafer = true;
+ }
+ continue;
+ }
+
+ if (IsOfficeFile(arg))
+ {
+ result.Add(TryGetFullPath(arg));
+ continue;
+ }
+
+ if (arg != null && arg.StartsWith("-sOutputFile=", StringComparison.OrdinalIgnoreCase))
+ {
+ string path = arg.Substring("-sOutputFile=".Length);
+ result.Add("-sOutputFile=" + TryGetFullPath(path));
+ continue;
+ }
+
+ result.Add(arg);
+ }
+
+ if (!hasNoSafer)
+ {
+ int insertAt = result.Count > 0 ? 1 : 0;
+ result.Insert(insertAt, "-dNOSAFER");
+ }
+
+ return result.ToArray();
+ }
+
+ private static bool IsSaferSwitch(string arg)
+ {
+ if (string.IsNullOrEmpty(arg))
+ {
+ return false;
+ }
+
+ return string.Equals(arg, "-dSAFER", StringComparison.OrdinalIgnoreCase)
+ || arg.StartsWith("-dSAFER=", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsNoSaferSwitch(string arg)
+ {
+ return !string.IsNullOrEmpty(arg)
+ && string.Equals(arg, "-dNOSAFER", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string TryGetFullPath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return path;
+ }
+
+ try
+ {
+ return Path.GetFullPath(path);
+ }
+ catch
+ {
+ return path;
+ }
+ }
+
+ ///
+ /// Converts an Office file to PDF using GhostPDL.
+ /// Input and output paths are resolved to absolute paths (required by SmartOffice).
+ ///
+ public static void ConvertToPdf(string inputPath, string outputPath)
+ {
+ ConvertToPdf(inputPath, outputPath, null);
+ }
+
+ ///
+ /// Converts an Office file to PDF using the given GhostPDL library.
+ ///
+ public static void ConvertToPdf(string inputPath, string outputPath, GhostscriptVersionInfo pdlVersion)
+ {
+ if (string.IsNullOrWhiteSpace(inputPath))
+ {
+ throw new ArgumentNullException("inputPath");
+ }
+
+ if (string.IsNullOrWhiteSpace(outputPath))
+ {
+ throw new ArgumentNullException("outputPath");
+ }
+
+ if (!File.Exists(inputPath))
+ {
+ throw new FileNotFoundException("Could not find input file.", inputPath);
+ }
+
+ if (pdlVersion == null)
+ {
+ pdlVersion = GhostscriptVersionInfo.GetGhostPdlVersion();
+ }
+ else if (!pdlVersion.IsGhostPdl)
+ {
+ throw new GhostscriptPdlLibraryNotFoundException();
+ }
+
+ string inputFull = Path.GetFullPath(inputPath);
+ string outputFull = Path.GetFullPath(outputPath);
+
+ string outputDirectory = Path.GetDirectoryName(outputFull);
+ if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
+ {
+ Directory.CreateDirectory(outputDirectory);
+ }
+
+ using (GhostscriptProcessor processor = new GhostscriptProcessor(pdlVersion, false))
+ {
+ processor.Process(new[]
+ {
+ "-ghostscript.net",
+ "-dNOPAUSE",
+ "-dBATCH",
+ "-dNOPROMPT",
+ "-sDEVICE=pdfwrite",
+ "-sOutputFile=" + outputFull,
+ "-f",
+ inputFull
+ });
+ }
+
+ if (!File.Exists(outputFull) || new FileInfo(outputFull).Length == 0)
+ {
+ throw new GhostscriptPdlLibraryNotFoundException(
+ GhostscriptOffice.CommercialLicenseRequiredMessage +
+ " The library at '" + pdlVersion.DllPath + "' did not convert '" + inputFull + "' to PDF. " +
+ "Use the licensed GhostPDL/SmartOffice native library, not standard Ghostscript.");
+ }
+ }
+
+ ///
+ /// Converts an Office file to a temporary PDF and returns that path.
+ /// The caller owns the file and should delete it when finished.
+ ///
+ public static string ConvertToTemporaryPdf(string inputPath)
+ {
+ return ConvertToTemporaryPdf(inputPath, null);
+ }
+
+ ///
+ /// Converts an Office file to a temporary PDF using the given GhostPDL library.
+ ///
+ public static string ConvertToTemporaryPdf(string inputPath, GhostscriptVersionInfo pdlVersion)
+ {
+ string outputPath = Path.Combine(Path.GetTempPath(), "gsnet-office-" + Guid.NewGuid().ToString("N") + ".pdf");
+ ConvertToPdf(inputPath, outputPath, pdlVersion);
+ return outputPath;
+ }
+
+ ///
+ /// Tries to infer an Office file extension from a stream's magic bytes.
+ ///
+ internal static bool TryDetectOfficeExtension(Stream stream, out string extension)
+ {
+ extension = null;
+
+ if (stream == null || !stream.CanSeek || stream.Length < 8)
+ {
+ return false;
+ }
+
+ long original = stream.Position;
+ try
+ {
+ stream.Position = 0;
+ byte[] header = new byte[Math.Min(4096, (int)stream.Length)];
+ int read = stream.Read(header, 0, header.Length);
+ if (read < 8)
+ {
+ return false;
+ }
+
+ // OLE compound document (.doc / .xls / .ppt)
+ if (header[0] == 0xD0 && header[1] == 0xCF && header[2] == 0x11 && header[3] == 0xE0 &&
+ header[4] == 0xA1 && header[5] == 0xB1 && header[6] == 0x1A && header[7] == 0xE1)
+ {
+ extension = ".doc";
+ return true;
+ }
+
+ // ZIP-based OOXML / ODF
+ if (header[0] == (byte)'P' && header[1] == (byte)'K')
+ {
+ string ascii = Encoding.ASCII.GetString(header, 0, read);
+ if (ascii.IndexOf("word/", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ extension = ".docx";
+ return true;
+ }
+ if (ascii.IndexOf("xl/", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ extension = ".xlsx";
+ return true;
+ }
+ if (ascii.IndexOf("ppt/", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ extension = ".pptx";
+ return true;
+ }
+ if (ascii.IndexOf("opendocument.text", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ extension = ".odt";
+ return true;
+ }
+ if (ascii.IndexOf("opendocument.spreadsheet", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ extension = ".ods";
+ return true;
+ }
+ if (ascii.IndexOf("opendocument.presentation", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ extension = ".odp";
+ return true;
+ }
+ }
+
+ return false;
+ }
+ finally
+ {
+ stream.Position = original;
+ }
+ }
+ }
+}
diff --git a/Ghostscript.NET/GhostscriptPipedOutput.cs b/Ghostscript.NET/GhostscriptPipedOutput.cs
index cd92562..f7edf08 100644
--- a/Ghostscript.NET/GhostscriptPipedOutput.cs
+++ b/Ghostscript.NET/GhostscriptPipedOutput.cs
@@ -112,11 +112,10 @@ protected virtual void Dispose(bool disposing)
if (_thread != null)
{
- // check if the thread is still running
- if (_thread.ThreadState == ThreadState.Running)
+ // Thread.Abort is not supported on .NET Core; closing the pipe unblocks Read.
+ if (_thread.IsAlive)
{
- // abort the thread
- _thread.Abort();
+ _thread.Join(TimeSpan.FromSeconds(2));
}
_thread = null;
@@ -181,7 +180,11 @@ public byte[] Data
{
get
{
- _thread.Join();
+ if (!_thread.Join(TimeSpan.FromSeconds(30)))
+ {
+ throw new TimeoutException("Timed out reading Ghostscript piped output.");
+ }
+
return _data.ToArray();
}
}
diff --git a/Ghostscript.NET/GhostscriptVersionInfo.cs b/Ghostscript.NET/GhostscriptVersionInfo.cs
index 9683795..5990b3d 100644
--- a/Ghostscript.NET/GhostscriptVersionInfo.cs
+++ b/Ghostscript.NET/GhostscriptVersionInfo.cs
@@ -46,6 +46,7 @@ public class GhostscriptVersionInfo
private string _libPath;
private GhostscriptLicense _licenseType;
private GhostscriptDiscoverySource _source;
+ private GhostscriptNativeKind _nativeKind;
#endregion
@@ -78,6 +79,7 @@ public GhostscriptVersionInfo(Version version, string dllPath, string libPath, G
_libPath = libPath;
_licenseType = licenseType;
_source = source;
+ _nativeKind = DetectNativeKind(dllPath);
}
#endregion
@@ -89,6 +91,7 @@ public GhostscriptVersionInfo(string customDllPath)
_libPath = string.Empty;
_licenseType = GhostscriptLicense.GPL;
_source = GhostscriptDiscoverySource.Custom;
+ _nativeKind = DetectNativeKind(customDllPath);
}
#region Version
@@ -151,6 +154,27 @@ public GhostscriptDiscoverySource Source
#endregion
+ #region NativeKind
+
+ ///
+ /// Gets whether this library is Ghostscript or GhostPDL.
+ /// GhostPDL is required for Microsoft Office files.
+ ///
+ public GhostscriptNativeKind NativeKind
+ {
+ get { return _nativeKind; }
+ }
+
+ ///
+ /// True when this native library is GhostPDL (gpdldll64.dll / libgpdl.so).
+ ///
+ public bool IsGhostPdl
+ {
+ get { return _nativeKind == GhostscriptNativeKind.GhostPdl; }
+ }
+
+ #endregion
+
#region ToString
///
@@ -158,7 +182,7 @@ public GhostscriptDiscoverySource Source
///
public override string ToString()
{
- return string.Format("Licence: {0}, Version: {1}, Source: {2}, Dll: {3}, Lib: {4}", _licenseType, _version, _source, _dllPath, _libPath);
+ return string.Format("Licence: {0}, Version: {1}, Kind: {2}, Source: {3}, Dll: {4}, Lib: {5}", _licenseType, _version, _nativeKind, _source, _dllPath, _libPath);
}
#endregion
@@ -567,6 +591,72 @@ public static GhostscriptVersionInfo GetPreferredVersion(GhostscriptLicense lice
throw new GhostscriptLibraryNotInstalledException();
}
+ ///
+ /// Preferred Ghostscript library for viewing. Falls back to Ghostscript next to GhostPDL,
+ /// then GhostPDL itself, so Office files work when only a GhostPDL install is present.
+ ///
+ public static GhostscriptVersionInfo GetPreferredVersionOrPdl()
+ {
+ GhostscriptVersionInfo bundled;
+ if (TryGetBundledVersion(out bundled))
+ {
+ return bundled;
+ }
+
+ GhostscriptVersionInfo system = TryGetLastInstalledSystemVersion(
+ GhostscriptLicense.GPL | GhostscriptLicense.AFPL | GhostscriptLicense.Artifex,
+ GhostscriptLicense.GPL);
+ if (system != null)
+ {
+ return system;
+ }
+
+ GhostscriptVersionInfo pdl;
+ if (TryGetGhostPdlVersion(out pdl))
+ {
+ GhostscriptVersionInfo gsBesidePdl;
+ if (TryGetGhostscriptBeside(pdl.DllPath, out gsBesidePdl))
+ {
+ return gsBesidePdl;
+ }
+
+ return pdl;
+ }
+
+ throw new GhostscriptLibraryNotInstalledException();
+ }
+
+ private static bool TryGetGhostscriptBeside(string nativeLibraryPath, out GhostscriptVersionInfo version)
+ {
+ version = null;
+
+ if (string.IsNullOrWhiteSpace(nativeLibraryPath))
+ {
+ return false;
+ }
+
+ string directory = Path.GetDirectoryName(nativeLibraryPath);
+ string found = FindLibraryInDirectory(directory, GetBundledLibraryNames());
+ if (string.IsNullOrEmpty(found) || CrossPlatformNativeLibraryHelper.IsGhostPdlLibrary(found))
+ {
+ return false;
+ }
+
+ if (!CrossPlatformNativeLibraryHelper.IsLibraryCompatible(found))
+ {
+ return false;
+ }
+
+ Version gsVersion = ReadBundledVersionMetadata(directory) ?? ParseVersionFromString(Path.GetFileName(found)) ?? new Version(0, 0);
+ version = new GhostscriptVersionInfo(
+ gsVersion,
+ found,
+ directory,
+ GhostscriptLicense.Artifex,
+ GhostscriptDiscoverySource.Custom);
+ return true;
+ }
+
#endregion
#region TryGetBundledVersion / GetBundledVersion
@@ -797,6 +887,212 @@ private static Version ReadBundledVersionMetadata(string directory)
#endregion
+ #region GetPreferredVersionForInput
+
+ ///
+ /// Returns GhostPDL when is an Office file, otherwise the preferred Ghostscript library.
+ ///
+ public static GhostscriptVersionInfo GetPreferredVersionForInput(string inputPath)
+ {
+ if (GhostscriptOffice.IsOfficeFile(inputPath))
+ {
+ return GetGhostPdlVersion();
+ }
+
+ return GetPreferredVersion();
+ }
+
+ #endregion
+
+ #region GhostPDL discovery
+
+ ///
+ /// True when a GhostPDL native library can be located.
+ ///
+ public static bool IsGhostPdlInstalled
+ {
+ get
+ {
+ GhostscriptVersionInfo version;
+ return TryGetGhostPdlVersion(out version);
+ }
+ }
+
+ ///
+ /// Gets a GhostPDL native library. Throws if none is found.
+ /// Office files require GhostPDL, not standard Ghostscript.
+ ///
+ public static GhostscriptVersionInfo GetGhostPdlVersion()
+ {
+ GhostscriptVersionInfo version;
+ if (TryGetGhostPdlVersion(out version))
+ {
+ return version;
+ }
+
+ throw new GhostscriptPdlLibraryNotFoundException();
+ }
+
+ ///
+ /// Tries to locate a GhostPDL native library.
+ /// Search order: GHOSTPDL_DLL / GPDL_DLL, the application folder
+ /// (drop-in gpdldll / libgpdl from Ghostscript.NET.Office),
+ /// then the same directory as a discovered Ghostscript DLL.
+ ///
+ public static bool TryGetGhostPdlVersion(out GhostscriptVersionInfo version)
+ {
+ version = null;
+
+ string libraryPath = FindGhostPdlLibraryPath();
+ if (string.IsNullOrEmpty(libraryPath) || !File.Exists(libraryPath))
+ {
+ return false;
+ }
+
+ if (!CrossPlatformNativeLibraryHelper.IsLibraryCompatible(libraryPath))
+ {
+ return false;
+ }
+
+ string directory = Path.GetDirectoryName(libraryPath) ?? string.Empty;
+ Version gsVersion = ReadBundledVersionMetadata(directory) ?? ParseVersionFromString(Path.GetFileName(libraryPath)) ?? new Version(0, 0);
+
+ version = new GhostscriptVersionInfo(
+ gsVersion,
+ libraryPath,
+ directory,
+ GhostscriptLicense.Artifex,
+ FileIsBundled(libraryPath) ? GhostscriptDiscoverySource.Bundled : GhostscriptDiscoverySource.Custom);
+
+ return true;
+ }
+
+ private static string FindGhostPdlLibraryPath()
+ {
+ string envPath = Environment.GetEnvironmentVariable("GHOSTPDL_DLL");
+ if (string.IsNullOrWhiteSpace(envPath))
+ {
+ envPath = Environment.GetEnvironmentVariable("GPDL_DLL");
+ }
+
+ if (!string.IsNullOrWhiteSpace(envPath) && File.Exists(envPath))
+ {
+ return envPath;
+ }
+
+ string[] libraryNames = CrossPlatformNativeLibraryHelper.GetGhostPdlLibraryNames(Environment.Is64BitProcess);
+
+ string bundled = FindLibraryInProbeDirectories(libraryNames);
+ if (!string.IsNullOrEmpty(bundled))
+ {
+ return bundled;
+ }
+
+ GhostscriptVersionInfo gs;
+ if (TryGetBundledVersion(out gs))
+ {
+ string besideGs = FindLibraryInDirectory(Path.GetDirectoryName(gs.DllPath), libraryNames);
+ if (!string.IsNullOrEmpty(besideGs))
+ {
+ return besideGs;
+ }
+ }
+
+ GhostscriptVersionInfo systemGs = TryGetLastInstalledSystemVersion(
+ GhostscriptLicense.GPL | GhostscriptLicense.AFPL | GhostscriptLicense.Artifex,
+ GhostscriptLicense.GPL);
+ if (systemGs != null)
+ {
+ string besideGs = FindLibraryInDirectory(Path.GetDirectoryName(systemGs.DllPath), libraryNames);
+ if (!string.IsNullOrEmpty(besideGs))
+ {
+ return besideGs;
+ }
+ }
+
+ return null;
+ }
+
+ private static string FindLibraryInProbeDirectories(string[] libraryNames)
+ {
+ string baseDirectory = AppContext.BaseDirectory;
+ if (string.IsNullOrEmpty(baseDirectory))
+ {
+ baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
+ }
+
+ string rid = GetCurrentRuntimeIdentifier();
+
+ List probeDirectories = new List();
+ AddProbeDirectory(probeDirectories, baseDirectory);
+ AddProbeDirectory(probeDirectories, Path.Combine(baseDirectory, "native"));
+
+ if (!string.IsNullOrEmpty(rid))
+ {
+ AddProbeDirectory(probeDirectories, Path.Combine(baseDirectory, "runtimes", rid, "native"));
+ AddProbeDirectory(probeDirectories, Path.Combine(baseDirectory, rid, "native"));
+ }
+
+ foreach (string directory in probeDirectories)
+ {
+ string found = FindLibraryInDirectory(directory, libraryNames);
+ if (!string.IsNullOrEmpty(found))
+ {
+ return found;
+ }
+ }
+
+ return null;
+ }
+
+ private static string FindLibraryInDirectory(string directory, string[] libraryNames)
+ {
+ if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory) || libraryNames == null)
+ {
+ return null;
+ }
+
+ foreach (string libraryName in libraryNames)
+ {
+ string candidate = Path.Combine(directory, libraryName);
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ return null;
+ }
+
+ private static bool FileIsBundled(string libraryPath)
+ {
+ string baseDirectory = AppContext.BaseDirectory ?? AppDomain.CurrentDomain.BaseDirectory;
+ if (string.IsNullOrEmpty(baseDirectory) || string.IsNullOrEmpty(libraryPath))
+ {
+ return false;
+ }
+
+ try
+ {
+ string fullLibrary = Path.GetFullPath(libraryPath);
+ string fullBase = Path.GetFullPath(baseDirectory);
+ return fullLibrary.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static GhostscriptNativeKind DetectNativeKind(string dllPath)
+ {
+ return CrossPlatformNativeLibraryHelper.IsGhostPdlLibrary(dllPath)
+ ? GhostscriptNativeKind.GhostPdl
+ : GhostscriptNativeKind.Ghostscript;
+ }
+
+ #endregion
+
#region IsGhostscriptInstalled
///
diff --git a/Ghostscript.NET/Helpers/CrossPlatformNativeLibraryHelper.cs b/Ghostscript.NET/Helpers/CrossPlatformNativeLibraryHelper.cs
index 8df57e2..132bf85 100644
--- a/Ghostscript.NET/Helpers/CrossPlatformNativeLibraryHelper.cs
+++ b/Ghostscript.NET/Helpers/CrossPlatformNativeLibraryHelper.cs
@@ -84,6 +84,63 @@ public static string GetGhostscriptLibraryName(bool is64Bit)
return is64Bit ? "gsdll64.dll" : "gsdll32.dll";
}
+ ///
+ /// Gets the expected GhostPDL library name for the current platform.
+ ///
+ public static string GetGhostPdlLibraryName(bool is64Bit)
+ {
+ if (CurrentPlatform == OSPlatform.Windows)
+ return is64Bit ? "gpdldll64.dll" : "gpdldll32.dll";
+ else if (CurrentPlatform == OSPlatform.Linux)
+ return "libgpdl.so";
+ else if (CurrentPlatform == OSPlatform.OSX)
+ return "libgpdl.dylib";
+ else
+ return is64Bit ? "gpdldll64.dll" : "gpdldll32.dll";
+ }
+
+ ///
+ /// Filenames to probe when locating a GhostPDL native library.
+ ///
+ public static string[] GetGhostPdlLibraryNames(bool is64Bit)
+ {
+ if (CurrentPlatform == OSPlatform.Windows)
+ {
+ return new[] { GetGhostPdlLibraryName(is64Bit) };
+ }
+
+ if (CurrentPlatform == OSPlatform.Linux)
+ {
+ return new[] { "libgpdl.so.10", "libgpdl.so.9", "libgpdl.so" };
+ }
+
+ if (CurrentPlatform == OSPlatform.OSX)
+ {
+ return new[] { "libgpdl.dylib", "libgpdl.so" };
+ }
+
+ return new[] { GetGhostPdlLibraryName(is64Bit) };
+ }
+
+ ///
+ /// Returns true when the path looks like a GhostPDL native library.
+ ///
+ public static bool IsGhostPdlLibrary(string libraryPath)
+ {
+ if (string.IsNullOrWhiteSpace(libraryPath))
+ {
+ return false;
+ }
+
+ string name = Path.GetFileName(libraryPath);
+ if (string.IsNullOrEmpty(name))
+ {
+ return false;
+ }
+
+ return name.IndexOf("gpdl", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
///
/// Checks if a native library is compatible with the current process architecture.
///
diff --git a/Ghostscript.NET/Helpers/StreamHelper.cs b/Ghostscript.NET/Helpers/StreamHelper.cs
index 797cdca..34dd300 100644
--- a/Ghostscript.NET/Helpers/StreamHelper.cs
+++ b/Ghostscript.NET/Helpers/StreamHelper.cs
@@ -56,6 +56,12 @@ public static string GetStreamExtension(Stream stream)
stream.Position = 0;
+ string officeExtension;
+ if (GhostscriptOffice.TryDetectOfficeExtension(stream, out officeExtension))
+ {
+ return officeExtension;
+ }
+
string extension = string.Empty;
if (test[0] == 0x25 && test[1] == 0x21) // standard ps or eps signature
@@ -123,7 +129,7 @@ public static string GetStreamExtension(Stream stream)
if (string.IsNullOrWhiteSpace(extension))
{
- throw new FormatException("Stream format is not valid! Please make sure it's PDF, PS or EPS.");
+ throw new FormatException("Stream format is not valid! Please make sure it's PDF, PS, EPS, or a supported Office file.");
}
return extension;
diff --git a/Ghostscript.NET/Microsoft.WinAny.Helper/Interop/DynamicNativeLibrary.cs b/Ghostscript.NET/Microsoft.WinAny.Helper/Interop/DynamicNativeLibrary.cs
index 05c9472..ed2230a 100644
--- a/Ghostscript.NET/Microsoft.WinAny.Helper/Interop/DynamicNativeLibrary.cs
+++ b/Ghostscript.NET/Microsoft.WinAny.Helper/Interop/DynamicNativeLibrary.cs
@@ -34,6 +34,7 @@
// Copyright (C) 2004-2012 Joachim Bauch (mail@joachim-bauch.de).
using System;
+using System.Reflection;
using System.Runtime.InteropServices;
namespace Microsoft.WinAny.Interop
@@ -680,6 +681,17 @@ public Delegate GetDelegateForFunction(string procName, Type delegateType)
{
IntPtr procAddress = this.GetProcAddress(procName);
+ // 32-bit GhostPDL (MSVC) exports stdcall names such as _gsapi_revision@8.
+ // NativeAssets gsdll32.dll uses undecorated names; try both.
+ if (procAddress == IntPtr.Zero && !Environment.Is64BitProcess)
+ {
+ string decorated = GetStdcallExportName(procName, delegateType);
+ if (decorated != procName)
+ {
+ procAddress = this.GetProcAddress(decorated);
+ }
+ }
+
if (procAddress != IntPtr.Zero)
{
return Marshal.GetDelegateForFunctionPointer(procAddress, delegateType);
@@ -688,6 +700,44 @@ public Delegate GetDelegateForFunction(string procName, Type delegateType)
return null;
}
+ private static string GetStdcallExportName(string procName, Type delegateType)
+ {
+ if (string.IsNullOrEmpty(procName) || delegateType == null || procName[0] == '_')
+ {
+ return procName;
+ }
+
+ MethodInfo invoke = delegateType.GetMethod("Invoke");
+ if (invoke == null)
+ {
+ return procName;
+ }
+
+ int stackBytes = 0;
+ ParameterInfo[] parameters = invoke.GetParameters();
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ Type type = parameters[i].ParameterType;
+ int size;
+ if (type.IsByRef || !type.IsValueType)
+ {
+ size = IntPtr.Size;
+ }
+ else if (type.IsEnum)
+ {
+ size = Marshal.SizeOf(Enum.GetUnderlyingType(type));
+ }
+ else
+ {
+ size = Marshal.SizeOf(type);
+ }
+
+ stackBytes += (size + 3) & ~3;
+ }
+
+ return "_" + procName + "@" + stackBytes.ToString();
+ }
+
#endregion
#region GetDelegateForFunction
diff --git a/Ghostscript.NET/OutputDevices/GhostscriptDevice.cs b/Ghostscript.NET/OutputDevices/GhostscriptDevice.cs
index b7ca224..1378e09 100644
--- a/Ghostscript.NET/OutputDevices/GhostscriptDevice.cs
+++ b/Ghostscript.NET/OutputDevices/GhostscriptDevice.cs
@@ -249,6 +249,12 @@ public void Process()
public void Process(GhostscriptStdIO stdIO_callback)
{
+ if (GhostscriptOffice.ContainsOfficeFile(this.InputFiles))
+ {
+ this.Process(GhostscriptVersionInfo.GetGhostPdlVersion(), false, stdIO_callback);
+ return;
+ }
+
this.Process(GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL | GhostscriptLicense.AFPL, GhostscriptLicense.GPL),
true,
stdIO_callback);
diff --git a/Ghostscript.NET/PDFA3Converter/PDFA3Converter.cs b/Ghostscript.NET/PDFA3Converter/PDFA3Converter.cs
index 7c7306d..7e62a7e 100644
--- a/Ghostscript.NET/PDFA3Converter/PDFA3Converter.cs
+++ b/Ghostscript.NET/PDFA3Converter/PDFA3Converter.cs
@@ -53,9 +53,18 @@ public class PDFA3Converter
///
- /// The constructor of the class accepts both input and output path for PDF conversion.
+ /// Creates a converter that uses the preferred Ghostscript library
+ /// (NativeAssets, then a system install).
///
- /// PDF input path
+ public PDFA3Converter()
+ : this(GhostscriptVersionInfo.GetPreferredVersion().DllPath)
+ {
+ }
+
+ ///
+ /// Creates a converter that loads Ghostscript from .
+ ///
+ /// Full path to the Ghostscript native library.
public PDFA3Converter(String gsdll)
{
@@ -265,36 +274,28 @@ public bool ConvertToPDFA3(string sourcePDFPath, string targetPDFPath)
}
GhostscriptVersionInfo gsVersion = new GhostscriptVersionInfo(GSDLLPath);
- GhostscriptLibrary ghostscriptLibrary = new GhostscriptLibrary(gsVersion);
- GhostscriptPipedOutput gsPipedOutput = new GhostscriptPipedOutput();
List switches = new List();
- switches.Add(""); // first parameter might be ignored
+ switches.Add("-ghostscript.net");
+ switches.Add("-dNOPAUSE");
+ switches.Add("-dBATCH");
switches.Add("-P"); // allow access to resources within the current directory
- switches.Add("-dPDFA=3"); // convert to A/3 pat 1/3
- // switches.Add("-dCompressStreams=false") hat problems as apparently XMP Metadata was compressed by ZUGFeRD. Obsolete in the meantime
+ switches.Add("-dPDFA=3"); // convert to A/3 part 1/3
switches.Add("-sColorConversionStrategy=RGB"); // necessary for PDF/A conversion
switches.Add("-sDEVICE=pdfwrite"); // Device for rasterization. Mandatory
- switches.Add($"-o{targetPDFPath}"); // Output path
+ switches.Add("-sOutputFile=" + targetPDFPath); // Output path
switches.Add("-dNOSAFER"); // Disable safe mode
switches.Add("-dPDFACompatibilityPolicy=1"); // convert to A/3 part 2/3
switches.Add("-dRenderIntent=3"); // convert to A/3 part 3/3
- switches.Add(PostScriptBigScriptPath); // PDFMark program file that shall be interpreted.
- // see https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
- // and https://gitlab.com/crossref/pdfmark
- switches.Add(sourcePDFPath); // PDF input file
+ switches.Add(PostScriptBigScriptPath);
+ switches.Add(sourcePDFPath);
- bool success = false;
- using (GhostscriptProcessor processor = new GhostscriptProcessor(ghostscriptLibrary))
+ using (GhostscriptProcessor processor = new GhostscriptProcessor(gsVersion, false))
{
- VerboseMsgBoxOutput stdio = new VerboseMsgBoxOutput();
- processor.StartProcessing(switches.ToArray(), stdio);
-
- // (erfolglose) Versuche, das Hngen zu vermeiden...
- processor.Dispose();
+ processor.Process(switches.ToArray(), new VerboseMsgBoxOutput());
}
- success = true;
- return success;
+
+ return File.Exists(targetPDFPath);
} // !ConvertToPDFA3()
diff --git a/Ghostscript.NET/Processor/GhostscriptProcessor.cs b/Ghostscript.NET/Processor/GhostscriptProcessor.cs
index e408a48..a101b55 100644
--- a/Ghostscript.NET/Processor/GhostscriptProcessor.cs
+++ b/Ghostscript.NET/Processor/GhostscriptProcessor.cs
@@ -118,6 +118,15 @@ public GhostscriptProcessor()
: this(GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL | GhostscriptLicense.AFPL, GhostscriptLicense.GPL), false)
{ }
+ ///
+ /// Creates a processor that loads GhostPDL when is an Office file,
+ /// otherwise the preferred Ghostscript library.
+ ///
+ public static GhostscriptProcessor CreateForInput(string inputPath)
+ {
+ return new GhostscriptProcessor(GhostscriptVersionInfo.GetPreferredVersionForInput(inputPath), false);
+ }
+
#endregion
#region Constructor - library
@@ -208,6 +217,24 @@ protected virtual void Dispose(bool disposing)
#endregion
+ private void EnsureGhostPdlLibrary()
+ {
+ if (_gs != null && _gs.VersionInfo != null && _gs.VersionInfo.IsGhostPdl)
+ {
+ return;
+ }
+
+ GhostscriptLibrary replacement = new GhostscriptLibrary(GhostscriptVersionInfo.GetGhostPdlVersion(), false);
+
+ if (_processorOwnsLibrary && _gs != null)
+ {
+ _gs.Dispose();
+ }
+
+ _gs = replacement;
+ _processorOwnsLibrary = true;
+ }
+
#region Process - device
public void Process(GhostscriptDevice device)
@@ -277,6 +304,12 @@ public void StartProcessing(string[] args, GhostscriptStdIO stdIO_callback)
throw new ArgumentOutOfRangeException("args");
}
+ if (GhostscriptOffice.ContainsOfficeFile(args))
+ {
+ EnsureGhostPdlLibrary();
+ args = GhostscriptOffice.PrepareProcessorArgs(args);
+ }
+
// Prepare arguments: if the native gsapi_set_arg_encoding API is supported
// we will set the requested encoding and pass native-encoded argv pointers
// (UTF-16LE on Windows, UTF-8 on non-Windows). Otherwise fall back to
diff --git a/Ghostscript.NET/Rasterizer/GhostscriptRasterizer.cs b/Ghostscript.NET/Rasterizer/GhostscriptRasterizer.cs
index 4bc23c2..372d671 100644
--- a/Ghostscript.NET/Rasterizer/GhostscriptRasterizer.cs
+++ b/Ghostscript.NET/Rasterizer/GhostscriptRasterizer.cs
@@ -143,7 +143,7 @@ public void Open(Stream stream)
throw new ArgumentNullException("stream");
}
- this.Open(stream, GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL | GhostscriptLicense.AFPL, GhostscriptLicense.GPL), false);
+ this.Open(stream, GhostscriptVersionInfo.GetPreferredVersionOrPdl(), false);
}
#endregion
@@ -157,7 +157,7 @@ public void Open(string path)
throw new FileNotFoundException("Could not find input file.", path);
}
- this.Open(path, GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL | GhostscriptLicense.AFPL, GhostscriptLicense.GPL), false);
+ this.Open(path, GhostscriptVersionInfo.GetPreferredVersionOrPdl(), false);
}
#endregion
diff --git a/Ghostscript.NET/Viewer/GhostscriptViewer.cs b/Ghostscript.NET/Viewer/GhostscriptViewer.cs
index 1fdb681..5677211 100644
--- a/Ghostscript.NET/Viewer/GhostscriptViewer.cs
+++ b/Ghostscript.NET/Viewer/GhostscriptViewer.cs
@@ -186,7 +186,7 @@ public void Open(string path)
throw new FileNotFoundException("Could not find input file.", path);
}
- this.Open(path, GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL | GhostscriptLicense.AFPL, GhostscriptLicense.GPL), false);
+ this.Open(path, GhostscriptVersionInfo.GetPreferredVersionOrPdl(), false);
}
#endregion
@@ -230,7 +230,7 @@ public void Open(string path, GhostscriptVersionInfo versionInfo, bool dllFromMe
this.Close();
- _filePath = path;
+ _filePath = ResolveInputPath(path);
_interpreter = new GhostscriptInterpreter(versionInfo, dllFromMemory);
@@ -273,7 +273,7 @@ public void Open(string path, byte[] library)
this.Close();
- _filePath = path;
+ _filePath = ResolveInputPath(path);
_interpreter = new GhostscriptInterpreter(library);
@@ -309,6 +309,21 @@ public void RegisterTempFile(string path)
_fileCleanupHelper.Add(path);
}
+ ///
+ /// Converts Office files to a temporary PDF via GhostPDL so the existing PDF viewer path can be used.
+ ///
+ private string ResolveInputPath(string path)
+ {
+ if (!GhostscriptOffice.IsOfficeFile(path))
+ {
+ return path;
+ }
+
+ string pdfPath = GhostscriptOffice.ConvertToTemporaryPdf(path);
+ _fileCleanupHelper.Add(pdfPath);
+ return pdfPath;
+ }
+
#endregion
#region Open - library
@@ -373,6 +388,8 @@ private void Open()
List args = new List();
args.Add("-gsnet");
+ args.Add("-dNOPAUSE");
+ args.Add("-dNOPROMPT");
args.Add("-sDEVICE=display");
if (Environment.Is64BitProcess)
diff --git a/README.md b/README.md
index 6e9ee1f..464cad1 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[](https://docs.microsoft.com/en-us/dotnet/standard/net-standard)
[](https://artifex.com/discord/artifex/)
-**Ghostscript.NET** is a managed C# wrapper for the [Ghostscript](https://ghostscript.com) native library (`gsdll64.dll` / `libgs.so`). It lets you rasterize, convert, and process PDF, PostScript, and EPS files from any .NET application without shelling out to a command-line process.
+**Ghostscript.NET** is a managed C# wrapper for the [Ghostscript](https://ghostscript.com) native library (`gsdll64.dll` / `libgs.so`). It lets you rasterize, convert, and process PDF, PostScript, EPS, and (with a licensed GhostPDL library) Microsoft Office files from any .NET application without shelling out to a command-line process.
```powershell
Install-Package Ghostscript.NET
@@ -29,6 +29,7 @@ Install-Package Ghostscript.NativeAssets
- [Code examples](#code-examples)
- [API overview](#api-overview)
- [Finding the Ghostscript native library](#finding-the-ghostscript-native-library)
+- [Office files (GhostPDL)](#office-files-ghostpdl)
- [PDF/A-3 conversion](#pdfa-3-conversion)
- [Documentation](#documentation)
- [License](#license)
@@ -160,6 +161,7 @@ processor.Process(new[]
| **Image export** | Save pages as PNG, JPEG, TIFF, BMP, or any SkiaSharp-supported format |
| **In-memory rendering** | Rasterize without writing intermediate files to disk |
| **PDF conversion** | Convert PS/EPS to PDF, compress PDFs, apply `pdfwrite` device settings |
+| **Office files** | Convert and rasterize Word, Excel, PowerPoint (and related) files when a licensed GhostPDL library is present |
| **Custom switches** | Pass any Ghostscript command-line switch directly via `CustomSwitches` or `Process(args[])` |
| **Progress events** | `GhostscriptProcessor` raises `Started`, `Processing` (per-page), `Error`, and `Completed` events |
| **Multi-instance** | Multiple `GhostscriptProcessor` or `GhostscriptRasterizer` instances can run in parallel |
@@ -325,6 +327,7 @@ rasterizer.Open("input.pdf", dllBytes);
| `GhostscriptProcessor` | `Ghostscript.NET.Processor` | Run Ghostscript with any argument array; exposes progress events |
| `GhostscriptViewer` | `Ghostscript.NET.Viewer` | Interactive viewer with zoom and progressive rendering |
| `GhostscriptVersionInfo` | `Ghostscript.NET` | Discover installed Ghostscript versions; specify DLL path |
+| `GhostscriptOffice` | `Ghostscript.NET` | Convert Office files to PDF via GhostPDL; detect supported extensions |
| `GhostscriptLibrary` | `Ghostscript.NET` | Low-level native library loader and P/Invoke surface |
| `GhostscriptStdIO` | `Ghostscript.NET` | Abstract base class for stdin/stdout/stderr callbacks |
| `GhostscriptPngDevice` | `Ghostscript.NET.OutputDevices` | Typed device for PNG output with all PNG switches |
@@ -352,7 +355,8 @@ rasterizer.Open("input.pdf", dllBytes);
| Member | Type | Description |
|---|---|---|
-| `Process(string[] args)` | Method | Run Ghostscript with a raw argument array |
+| `CreateForInput(string path)` | Static method | Optional: load GhostPDL up front for an Office path |
+| `Process(string[] args)` | Method | Run Ghostscript; loads GhostPDL automatically if args include an Office file |
| `Process(GhostscriptDevice device)` | Method | Run using a typed device object |
| `Process(string[] args, GhostscriptStdIO callback)` | Method | Run with stdout/stderr capture |
| `StartProcessing(...)` | Method | Alias for `Process`; included for API compatibility |
@@ -379,6 +383,10 @@ rasterizer.Open("input.pdf", dllBytes);
| `.DllPath` | Property | Path to the native library file |
| `.Version` | Property | `System.Version` of the detected installation |
| `.Source` | Property | `Bundled`, `System`, or `Custom` |
+| `.NativeKind` / `.IsGhostPdl` | Property | Ghostscript vs GhostPDL native library |
+| `GetGhostPdlVersion()` | Static method | Locates `gpdldll64.dll` / `libgpdl.so` (required for Office files) |
+| `TryGetGhostPdlVersion(out version)` | Static method | Same lookup without throwing |
+| `GetPreferredVersionForInput(path)` | Static method | GhostPDL for Office paths, otherwise preferred Ghostscript |
---
@@ -403,6 +411,65 @@ rasterizer.Open("input.pdf", dll);
---
+## Office files (GhostPDL)
+
+Standard Ghostscript (`gsdll64.dll` / `Ghostscript.NativeAssets`) cannot open Word, Excel, or PowerPoint files. Office support uses **GhostPDL** (`gpdldll64.dll` / `gpdldll32.dll` / `libgpdl.so`), which includes **SmartOffice** and exposes the same `gsapi_*` API.
+
+SmartOffice is commercial, in-house technology. The GhostPDL native library is **not** published on nuget.org and is **not** included in `Ghostscript.NativeAssets`. Without a commercial Ghostscript.NET license, opening an Office file throws `GhostscriptPdlLibraryNotFoundException` and directs you to [Artifex](https://artifex.com/contact/ghostscript). Licensed users obtain the matching library from the **Ghostscript.NET.Office** repository and copy it into the .NET project.
+
+Place the file next to your application, under `runtimes//native/`, or set `GHOSTPDL_DLL` (or `GPDL_DLL`) to its full path. After that, **existing Ghostscript.NET processor code does not need to change**: if the argument list includes an Office file, `GhostscriptProcessor` loads GhostPDL automatically (and ignores `-dSAFER` for that job). `CreateForInput` is optional. If `gsdll64.dll` sits beside GhostPDL, the viewer/rasterizer uses it to display the converted PDF.
+
+**Convert Office to PDF**
+
+```csharp
+using Ghostscript.NET;
+
+GhostscriptOffice.ConvertToPdf(@"D:\report.docx", @"D:\report.pdf");
+```
+
+**Rasterize an Office file** (`GhostscriptRasterizer` / `GhostscriptViewer` convert to a temporary PDF automatically)
+
+```csharp
+using Ghostscript.NET.Rasterizer;
+
+using var rasterizer = new GhostscriptRasterizer();
+rasterizer.Open(@"D:\report.docx");
+
+for (int page = 1; page <= rasterizer.PageCount; page++)
+{
+ SKBitmap image = rasterizer.GetPage(dpi: 150, pageNumber: page);
+}
+```
+
+**Run GhostPDL with any device**
+
+Existing `GhostscriptProcessor` samples keep working. Pass an Office path in the argument list and place `gpdldll64.dll` next to the app:
+
+```csharp
+using Ghostscript.NET;
+using Ghostscript.NET.Processor;
+
+GhostscriptVersionInfo gv = GhostscriptVersionInfo.GetLastInstalledVersion();
+using var processor = new GhostscriptProcessor(gv, true);
+processor.Process(new[]
+{
+ "-dBATCH", "-dNOPAUSE", "-dSAFER",
+ "-sDEVICE=png16m",
+ "-sOutputFile=page-%03d.png",
+ @"D:\report.docx"
+});
+```
+
+`CreateForInput` still loads GhostPDL up front when you already know the input is Office.
+
+Use **full paths** for Office input and output. GhostPDL allows only one interpreter instance per process.
+
+Supported extensions include `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.odt`, `.ods`, `.odp`, `.rtf`, and `.csv`.
+
+Usage with Ghostscript.NET, including sample code, is documented in **Ghostscript.NET.Office**. Maintainers who produce the native libraries see **Ghostscript.NET.Office/BUILD.md**.
+
+---
+
## PDF/A-3 conversion
The `PDFA3Converter` class converts any PDF to PDF/A-3b format and optionally embeds a ZUGFeRD or Factur-X XML invoice. This is the format required by XRechnung (Germany) and Factur-X (France/EU) electronic invoicing standards.
diff --git a/Versions.props b/Versions.props
index 72bbd06..1fa6312 100644
--- a/Versions.props
+++ b/Versions.props
@@ -1,7 +1,7 @@
- 1.3.4
+ 1.3.5
10.7.1