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 @@ [![Target: .NET Standard 2.0](https://img.shields.io/badge/.NET-Standard%202.0-blue)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) [![Discord](https://img.shields.io/discord/770681584617652264?color=6A7EC2&logo=discord&logoColor=ffffff)](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