matthew ephraim

A Simple C# Wrapper for Ghostscript

Update:

This post has become somewhat popular (relative to my other posts anyway), so I decided to take the code and release it as an open source library. More information here

PDF thumbnails with Ghostscript

I’ve been looking for a while now for a simple solution for generating thumbnail images from PDF files. I wanted something that would let me programmatically load in a PDF file, choose a page, and generate a thumbnail from that page. As far as I can tell, there are only a few open source options and of those options I haven’t been able to find one that I could get working with C#.

After seeing it recommended a few times, I decided take a look at Ghostscript. Ghostscript is an open source interpreter for Postscript and PDF files. Among other things, Ghostscript allows you generate images from PDF pages. Which is exactly what I needed.

Ghostscript is a tool that can be used from the command line, which is how most of the examples I’ve found online have used it. Unfortunately, this is what a call to Ghostscript looks like:

gs -q -dQUIET -dPARANOIDSAFER  -dBATCH -dNOPAUSE \          
-dNOPROMPT -dMaxBitmap=500000000 -dFirstPage=1 \
-dAlignToPixels=0 -dGridFitTT=0 -sDEVICE=jpeg \
-dTextAlphaBits=4 -dGraphicsAlphaBits=4 -r100x100 \
-sOutputFile=output.jpg input.pdf

Not pretty. Luckily, I needed to automate the task of creating the thumbnails, so I wouldn’t need to manually generate the parameters to be passed to the command line tool. However, I still felt like there might be a better way to hook into Ghostscript’s functionality. So, I decided to take advantage of the API provided by Ghostscript by writing a simple C# wrapper for the API to use in my current ASP.Net project.

A simple Ghostscript wrapper

The first thing I needed was the Windows version of the Ghostscript DLL, which can be obtained here. Once I included the DLL in my project, I needed to expose the unmanaged API functions to my C# wrapper function.

C#
[DllImport("gsdll32.dll", EntryPoint = "gsapi_new_instance")]
private static extern int CreateAPIInstance(out IntPtr pinstance, 
                                        IntPtr caller_handle);

[DllImport("gsdll32.dll", EntryPoint = "gsapi_init_with_args")]
private static extern int InitAPI(IntPtr instance, int argc, IntPtr argv);

[DllImport("gsdll32.dll", EntryPoint = "gsapi_exit")]
private static extern int ExitAPI(IntPtr instance);

[DllImport("gsdll32.dll", EntryPoint = "gsapi_delete_instance")]
private static extern void DeleteAPIInstance(IntPtr instance);

Above, I complained about the long list of parameters that need to be passed to the Ghostscript command line tool. Those same parameters need to be passed to the API, so the next thing I did was create a function that wrapped up the functionality for building the list of parameters. For simplicity, I left in a lot of default parameters, but the function could be expanded later on to allow more specific parameters.

C#

private string[] GetArgs(string inputPath, string outputPath, 
                         int firstPage, int lastPage, int width, int height)
{
    return new[]
    {
        // Keep gs from writing information to standard output
        "-q",                     
        "-dQUIET",
       
        "-dPARANOIDSAFER", // Run this command in safe mode
        "-dBATCH", // Keep gs from going into interactive mode
        "-dNOPAUSE", // Do not prompt and pause for each page
        "-dNOPROMPT", // Disable prompts for user interaction           
        "-dMaxBitmap=500000000", // Set high for better performance
        
        // Set the starting and ending pages
        String.Format("-dFirstPage={0}", firstPage),
        String.Format("-dLastPage={0}", lastPage),   
        
        // Configure the output anti-aliasing, resolution, etc
        "-dAlignToPixels=0",
        "-dGridFitTT=0",
        "-sDEVICE=jpeg",
        "-dTextAlphaBits=4",
        "-dGraphicsAlphaBits=4",
        String.Format("-r{0}x{1}", width, height),

        // Set the input and output files
        String.Format("-sOutputFile={0}", outputPath),
        inputPath
    };
}

Once I had a way of creating a list of parameters, I could start using the Ghostscript API functions. I created a function called CallAPI that would accept an array of parameters and use them to call the Ghostcript API.

The function I created for building a list of arguments returned an array of strings, but to use the API I needed to convert each of those parameters into a ANSI null terminated byte array (I added the code I used to do this to the bottom of this post). Then I needed to allocate some space in memory for each of those arguments and get pointers to each one of them.

C#
var argStrHandles = new GCHandle[args.Length];
var argPtrs = new IntPtr[args.Length];

// Create a handle for each of the arguments after 
// they've been converted to an ANSI null terminated
// string. Then store the pointers for each of the handles
for (int i = 0; i < args.Length; i++)
{
    argStrHandles[i] = GCHandle.Alloc(StringToAnsi(args[i]), GCHandleType.Pinned);
    argPtrs[i] = argStrHandles[i].AddrOfPinnedObject();
}

// Get a new handle for the array of argument pointers
var argPtrsHandle = GCHandle.Alloc(argPtrs, GCHandleType.Pinned);

Then, to use the newly converted parameters, I needed to create an instance of the Ghostscript API and pass them into the initialization function.

C#
// Get a pointer to an instance of the GhostScript API 
// and run the API with the current arguments
IntPtr gsInstancePtr;
CreateAPIInstance(out gsInstancePtr, IntPtr.Zero);
InitAPI(gsInstancePtr, args.Length, argPtrsHandle.AddrOfPinnedObject());

The call to InitAPI runs Ghostscript and generates any requested files at the output path.

Now the only remaining thing I needed to do was clean up the memory that was allocated for the API. To handle this, I wrote a cleanup function that takes in the items that need to be cleaned up. The API provides some cleanup functions, so I called those in the cleanup function as well.

C#
private void Cleanup(GCHandle[] argStrHandles, GCHandle argPtrsHandle, 
                                       IntPtr gsInstancePtr)
{
    for (int i = 0; i < argStrHandles.Length; i++)
        argStrHandles[i].Free();

    argPtrsHandle.Free();

    ExitAPI(gsInstancePtr);
    DeleteAPIInstance(gsInstancePtr);
}

One last thing I added to the wrapper was a simple function for generating thumbnails from a source PDF file. Technically, I could have just used the CallAPI function to do that, but I wanted to hide the details of working with the API from code outside of the wrapper class.

C#

public void GeneratePageThumbs(string inputPath, string outputPath, 
                              int firstPage, int lastPage, int width, int height)
{
    CallAPI(GetArgs(inputPath, outputPath, firstPage, lastPage, width, height));
}

The GeneratePageThumbs doesn’t do anything other than calling the CallAPI function. However, in the future, I’d like to provide other functions that use the Ghostscript API as well. If anyone has any ideas for improving the code, drop me line.

Update: Here is the code I used to convert the arguments to null terminated byte arrays. There might be a better way to do this in .Net, this is just the quick solution I’m using.

C#
public static byte[] StringToAnsi(string original)
{
       var strBytes = new byte[original.Length + 1];
       for (int i = 0; i < original.Length; i++)
            strBytes[i] = (byte)original[i];
        
        strBytes[original.Length] = 0;
        return strBytes;
}

Update: This code has been open sourced

Tags:

32 Responses to “A Simple C# Wrapper for Ghostscript”

  1. Brendon Says:

    Hi there,

    Nice piece of work here, is exactly the same thing I am working on. Would it be possible to make the source code available for download somewhere, as some parts have not been outlined (StringToAnsi)?

    Cheers,
    Brendon

  2. Matthew Ephraim Says:

    I added the code for StringToAnsi to the bottom of the post.

    When I have time, I’m probably going to spin this code off into an open source project, since I’ve gotten a lot of questions about it.

  3. Joe Says:

    Hi,
    About your .NET Ghostscript Wrapper have you created the callback functions to show the file on a PictureBox?

    Thanks

  4. Matthew Ephraim Says:

    No, I haven’t created any callback functions for that. Pretty much what you see here is what I’ve written. It’s only focused on generating the PDF page preview.

    That would be interesting functionality, I just didn’t need it for what I was doing.

  5. Ivan Todorovic Says:

    If you want thumbnails from PDF files, you have Cairographics PDF backend as working C# solution. Although I’ve managed to make it work only with Mono, not with Visual Studio.

  6. David Says:

    Nice piece of work here but i have some problems can you send me a complete sample.

    thanks.

  7. Rasmus Nielsen Says:

    Hi,
    Great job with the wrapper. Just what I needed.
    I just wanna point out a couple of things that I ran into:
    You need either the full package of GhostScript installed on the client computer or you can unzip all the files from the lib and Resource folders in the gs863w64.exe file into the same folder as gsdll32.dll (all files in the same folder - moved from the subfolders).
    Also the resolution you specify with int width, int height is in DPI not pixels so depending on output it’s probably easiest to choose a resolution between 36×36 and 288×288 and use GDI to resize the image to what you really need.

  8. Matthew Ephraim Says:

    You’re right about the height and width. I actually ended up starting with a higher resolution image and then used GDI to resize the larger image like you said. I’m hoping to include the code if and when I put together a little open source project for this.

  9. nick Says:

    The code works great from a command prompt, thank you! Unfortunately, when I use it within a web service (IIS , Asp.Net MVC) I get a -100 from gsapi_init_with_args. Am I missing a parameter somewhere to get this to run as part of a service?

  10. flalar Says:

    Hi there, Good article! At what time do you recon the code will be available?

  11. Nanda Motikane Says:

    Matthew, I am working on a project part of which I need to convert postscript to an pdf file within C#. Your code and comments from others looks promising for me to use. We would appreciate if you can provide us the source code for the wrapper.

  12. Matthew Ephraim Says:

    @Nanda Motikane The source code on here is pretty much all there is to it. Aside from some application specific stuff that I added for what I was working on.

  13. Andypro Says:

    Thanks for the terrific script Matthew. I have it working on an ASP.NET page, Framework version 2.0.

    A couple of notes on changes I made to get it working:
    - I needed to put gsdll32.dll in my system32, or alternatively, load it with LoadLibrary.
    - I needed to change the line return new[] to actually declare a temp string[] and return it explicitly at the bottom of the function.
    - I added a call to Cleanup() at the bottom of your CallAPI(), because you must have called it somewhere outside of your code snippets.
    - I replaced your instances of “var” to appropriate type names. “var” must have represented something specific in your system that’s not native to .NET I guess.

    Thanks again for showing us how to implement something that should be truly simple but is, in fact, disgustingly difficult and frustrating!

  14. Andypro Says:

    I have another tip for refining the thumbnail creator further: use ghostscript’s -dUseCropBox option. This will create a proper thumbnail image for pdfs whose cropbox is set differently than the mediabox. I’ve converted about 20,000 pdfs so far - and while there aren’t many that do this, there are some.

  15. C. Ross Says:

    You’re DLLImport statements could really be simplified to the following:

    [DllImport("gsdll32.dll", EntryPoint = "gsapi_new_instance")]
    private static extern int CreateAPIInstance(out IntPtr pinstance,
    IntPtr caller_handle);

    [DllImport("gsdll32.dll", EntryPoint = "gsapi_init_with_args")]
    private static extern int InitAPI(IntPtr instance, int argc, string[] argv);

    [DllImport("gsdll32.dll", EntryPoint = "gsapi_exit")]
    private static extern int ExitAPI(IntPtr instance);

    [DllImport("gsdll32.dll", EntryPoint = "gsapi_delete_instance")]
    private static extern void DeleteAPIInstance(IntPtr instance);

    This would remove the need for some of your more complicated code (including StringToAnsi).

    Thanks for all your hard work!

  16. alin Says:

    hi,

    can you help me detect the colored pages in a pdf files using this wrapper?

    thanks

  17. Matthew Ephraim Says:

    Thank you to everyone who has posted improvements to this code. Rather than continually updating this blog post, I’m planning on putting together an open source project some time in the near future that will wrap up this code. That way, anyone who has improvements can work on the code.

  18. Richard Bird Says:

    I’d like to also say thanks for this solution. I too simply needed something to generate thumbs from PDFs, and this works perfect. You rock!

  19. Matthew Ephraim Says:

    I finally posted the open sourced version of this here

  20. Srikanth Says:

    Hi

    Once thumbnails are created, is there anyway to embed them as thumbnails in the PDFs, so that it displays on the left side in the Adobe Reader? Though recent versions of Adobe Reader creates thumbnails dynamically, old versions dont. Thats why.

    Thanks
    Srikanth

  21. Matthew Ephraim Says:

    @Srikanth I really don’t know if that’s possible. I think Acrobat Reader may just be dynamically generating the thumbnails, like you said. I’m not sure that it’s possible to store those thumbnails inside of the PDF.

    You may want to look at a PDF generation library like iTextSharp.

  22. Troy Howard Says:

    Matthew — This looks great! I don’t know if you know this already or not, but there’s already an open source project that is solving the same problem (in much the same way), called Gouda. It’s on Google’s code hosting service at:

    http://code.google.com/p/gouda/

    I started in on some work to implement the drawing surface callbacks. Right now, the text based callbacks for output/errors works, but I haven’t successfully built a fully functional graphics surface for Ghostscript to render to. Haven’t had much time to work on it lately, so not much has changed in the past year.

    If you, or anyone else would like to help out with that project, we could probably get more work done as a team, rather than having multiple projects.

    Also, for those of you who are interested in leveraging all the wonderful open source PostScript tools out there in your .Net code, there’s another project that I started called Parrano which is a wrapper to PsLib ( http://code.google.com/p/parrano/ ).

    Thanks,
    Troy

  23. Matthew Ephraim Says:

    @Troy,

    Thanks for posting this. I honestly had never run across your project before, but I’m glad there’s a project out there that appears to be further along than I am.

  24. Blasius Riz Says:

    Really the nicest wrapper for Ghostscript I have stumbled upon so far, great!
    I have a question for you or anyone else out there who is using a ghostscript wrapper. I´m currently building a tool used in house under windows 7, 64 Bit, VS 2008, .NET 3.5.
    I had the problem that ghostscript sometimes crashed with a AccessViolationException, this reproducibly happend when converting imageheavy PDF files. From my tests, I assume that exit and delete_instance didn´t close properly and ghostscript failed to create a new instance thereafter.
    The crashes occured with both versions of ghostscript 8.70 and 8.64.
    After I ran some tests on a Windows XP VM, I found out that the crashes didn´t happen when the application ran in 32 bit mode. So indeed, once I switched the build settings from Any CPU to x86, the chrashes stopped from happening.
    Did anyone else have this problem and maybe found a solution other than targeting x86?
    This is not a major showstopper yet, since all of our regular workstations in house run Windows XP 32 bit, but someday we will make the transit to windows 7 and maybe even 64 bit mode, so it would be nice to target x64 for those setups. On a side note, I used the 64 bit Version of ghostscript on the 64 bit setup if anyone wonders.

  25. SPan Says:

    Hi,

    I’ve tried to use your code in Windows 7 and Windows XP with visualstudio 2008.When I try to add refrenece for gsdll32.dll to a Windows Forms project I got an error saying it is not a valid com component (gsdll32.dll)

    Any idea how to overcome from this error and able to run the program?

    Thanks

  26. Nike Says:

    SPan,

    You don’t need to add DLL to references. This is not .NET but native PE file (Windows DLL).

    Ghostscript should be installed with all it’s libraries, and PATH variable should contain gs bin folder (”C:\Program Files\gs\gs8.70\bin” or other directory where you have gs installed )

  27. Tobias Says:

    hi,

    there is a lille error in your Wrapper.
    The first argument of the arguments you supply in
    the method InitAPI(gsInstancePtr, args.Length, args)
    is allways ignored by the dll.

    So your arguments should start with

    return new[]
    {
    “”,
    “-q”,
    “-dQUIET”,
    ….

    maybe you didn’t recognize this behavior because -q and -dQuit do the same.

    Tobias

  28. Tobias Says:

    of course the first argument in the variable “args” has to be empty “” not the first argument of the method InitAPI ;)

  29. Klaus Says:

    Hello,

    thanks for this great library. I have a problem getting this work on a webapplication. when i debug my webapplication (”F5″ in Visual Studio 2008) it works great. But when i access the webapplication outside of vs2008 (http://localhost/…) the JPG doesn’t get generated. There is also no exception thrown.

    The GS Bin is set in the PATH variable. The gsdll32.dll is in my system32-folder.

    Maybe the security settings of the IIS-Server are wrong?

    greetings
    Klaus

  30. Shahrooz Says:

    Hi,
    Thanks for code,
    But I have a problem and that is , when I call InitAPI it return a -100 error number, I dont know how solve the problem could u help me?

    Thanks again

  31. Juan C. Luna H. Says:

    > Update: Here is the code I used to convert the arguments to
    > null terminated byte arrays. There might be a better way to
    > do this in .Net, this is just the quick solution I’m using.

    You can use this code:
    public static byte[] StringToAnsi(string original)
    {
    return System.Text.Encoding.ASCII.GetBytes(original + “”);
    }

  32. ND Says:

    Hi,

    We are working on a product which requires a similar functionality of having the thumbnail generated for all the pdf files selected.
    After reading and researching a lot I am totally confused as our solution is hosted on godaddy and not sure if we could use your solution? We don’t have control over the box. Will your solution still fit my needs?

    Secondly for most of the solution that I read they had a pre-requisite of having the Adobe Acrobat to be installed (not reader) but fully licensed version? Is it required in this case?

Leave a Reply