clawsoftware / clawPDF

Open Source Virtual (Network) Printer for Windows that allows you to create PDFs, OCR text, and print images, with advanced features usually available only in enterprise solutions.
https://github.com/clawsoftware/clawPDF
GNU Affero General Public License v3.0
660 stars 147 forks source link

Ghostscript error when east-asian characters are included in GhostScript paramters #60

Closed sjxnwnqksnrlq closed 1 year ago

sjxnwnqksnrlq commented 2 years ago

NativeMethods.InitAPI64 method throws Runtime Exception when east-asian characters are included in GhostScript paramters. I find the solution is forcing GhostScript APIs to receive UTF-8 strings as arguments. To achieve this, amendment towards two files are required: NativeMethod.cs:

using System;
using System.Runtime.InteropServices;

namespace clawSoft.clawPDF.Core.Ghostscript
{
    internal static class NativeMethods
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static extern bool SetDllDirectory(string lpPathName);

        /*
        This code was adapted from Matthew Ephraim's Ghostscript.Net project -
        external dll definitions moved into NativeMethods to
        satisfy FxCop requirements
        https://github.com/mephraim/ghostscriptsharp
        */

        #region Hooks into Ghostscript DLL

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

        [DllImport("gsdll32.dll", EntryPoint = "gsapi_set_arg_encoding")]
        internal static extern int gsapi_set_arg_encoding32(IntPtr instance, int encoding); // force encoding

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

        [DllImport("gsdll32.dll", EntryPoint = "gsapi_init_with_args")]
        internal static extern int InitAPI32(IntPtr instance, int argc, IntPtr[] argv); // modified caller signature

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

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

        [DllImport("gsdll64.dll", EntryPoint = "gsapi_new_instance")]
        internal static extern int CreateAPIInstance64(out IntPtr pinstance, IntPtr caller_handle);

        [DllImport("gsdll64.dll", EntryPoint = "gsapi_set_arg_encoding")]
        internal static extern int gsapi_set_arg_encoding64(IntPtr instance, int encoding); // ditto

        [Obsolete]
        [DllImport("gsdll64.dll", EntryPoint = "gsapi_init_with_args")]
        internal static extern int InitAPI64(IntPtr instance, int argc, string[] argv);

        [DllImport("gsdll64.dll", EntryPoint = "gsapi_init_with_args")]
        internal static extern int InitAPI64(IntPtr instance, int argc, IntPtr[] argv); // ditto

        [DllImport("gsdll64.dll", EntryPoint = "gsapi_exit")]
        internal static extern int ExitAPI64(IntPtr instance);

        [DllImport("gsdll64.dll", EntryPoint = "gsapi_delete_instance")]
        internal static extern void DeleteAPIInstance64(IntPtr instance);

        #endregion Hooks into Ghostscript DLL
    }
}

GhostScriptANY.cs

using System;
using System.Runtime.InteropServices;

namespace clawSoft.clawPDF.Core.Ghostscript
{
    internal class GhostScriptANY
    {
        /// <summary>
        ///     GS can only support a single instance, so we need to bottleneck any multi-threaded systems.
        /// </summary>
        private static readonly object resourceLock = new object();

        private static readonly int GS_ARG_ENCODING_LOCAL = 0;
        private static readonly int GS_ARG_ENCODING_UTF8 = 1;
        private static readonly int GS_ARG_ENCODING_UTF16LE = 2;

        /*
        This code was adapted from Matthew Ephraim's Ghostscript.Net project
        external dll definitions moved into NativeMethods to
        satisfy FxCop requirements
        https://github.com/mephraim/ghostscriptsharp
        */

        /// <summary>
        ///     Calls the Ghostscript API with a collection of arguments to be passed to it
        /// </summary>
        public static void CallAPI(string[] args)
        {
            // Get a pointer to an instance of the Ghostscript API and run the API with the current arguments
            lock (resourceLock)
            {
                if (System.Environment.Is64BitProcess)
                {
                    CallAPI64(args);
                }
                else
                {
                    CallAPI32(args);
                }

            }
        }

        /// <summary>
        ///     Call API when Process is 64-bit
        /// </summary>
        private static void CallAPI64(string[] args)
        {
            IntPtr gsInstancePtr;
            bool memoryAllocated = true;
            IntPtr[] nativeStrings = ManagedStringsToNativeUtf8Strings(args, ref memoryAllocated);
            try
            {
                if (!memoryAllocated)
                {
                    throw new OutOfMemoryException("fail to allocate memory for UTF-8 native strings");
                }
                NativeMethods.CreateAPIInstance64(out gsInstancePtr, IntPtr.Zero);
                NativeMethods.gsapi_set_arg_encoding64(gsInstancePtr, GS_ARG_ENCODING_UTF8);
                try
                {
                    var result = NativeMethods.InitAPI64(gsInstancePtr, nativeStrings.Length, nativeStrings);

                    if (result < 0) throw new ExternalException("Ghostscript conversion error", result);
                }
                finally
                {
                    NativeMethods.ExitAPI64(gsInstancePtr);
                    NativeMethods.DeleteAPIInstance64(gsInstancePtr);
                }
            }
            catch (ExternalException ex)
            {
                throw new ExternalException(ex.Message, ex.ErrorCode);
            }
            catch (OutOfMemoryException oom)
            {
                throw new OutOfMemoryException(oom.Message);
            }
            finally
            {
                CleanUpNativeStrings(nativeStrings);
            }
        }
        /// <summary>
        ///     Call API when Process is 32-bit
        /// </summary>
        private static void CallAPI32(string[] args)
        {
            IntPtr gsInstancePtr;
            bool memoryAllocated = true;
            IntPtr[] nativeStrings = ManagedStringsToNativeUtf8Strings(args, ref memoryAllocated);
            try
            {
                if (!memoryAllocated)
                {
                    throw new OutOfMemoryException("fail to allocate memory for UTF-8 native strings");
                }
                NativeMethods.CreateAPIInstance32(out gsInstancePtr, IntPtr.Zero);
                NativeMethods.gsapi_set_arg_encoding32(gsInstancePtr, GS_ARG_ENCODING_UTF8);
                try
                {
                    var result = NativeMethods.InitAPI32(gsInstancePtr, nativeStrings.Length, nativeStrings);

                    if (result < 0) throw new ExternalException("Ghostscript conversion error", result);
                }
                finally
                {
                    NativeMethods.ExitAPI32(gsInstancePtr);
                    NativeMethods.DeleteAPIInstance32(gsInstancePtr);
                }
            }
            catch (ExternalException ex)
            {
                throw new ExternalException(ex.Message, ex.ErrorCode);
            }
            catch (OutOfMemoryException oom)
            {
                throw new OutOfMemoryException(oom.Message);
            }
            finally
            {
                CleanUpNativeStrings(nativeStrings);
            }
        }

        private static IntPtr[] ManagedStringsToNativeUtf8Strings(string[] args, ref bool success)
        {
            success = true;
            if (null == args)
            {
                throw new ArgumentNullException("args");
            }
            IntPtr[] result = new IntPtr[args.Length];
            for (int i = 0, len = args.Length; i < len; ++i)
            {
                result[i] = IntPtr.Zero;
            }
            for (int i = 0, len = args.Length; i < len; ++i)
            {
                try
                {
                    byte[] byteBuffer = System.Text.Encoding.UTF8.GetBytes(args[i]);
                    IntPtr ptr = Marshal.AllocHGlobal(sizeof(byte) * (byteBuffer.Length + 1));
                    result[i] = ptr;
                    Marshal.Copy(byteBuffer, 0, ptr, byteBuffer.Length); // copy all bytes to unmanaged memory block
                    Marshal.WriteByte(ptr, byteBuffer.Length, 0); // don't forget to set last byte to null('\0') terminator!
                }
                catch (OutOfMemoryException oom)
                {
                    success = false;
                    return result;
                }
            }
            return result;
        }

        private static void CleanUpNativeStrings(IntPtr[] unmanagedStrings)
        {
            if (null == unmanagedStrings)
            {
                throw new ArgumentNullException("unmanagedStrings");
            }
            foreach (IntPtr unmanagedString in unmanagedStrings)
            {
                Marshal.FreeHGlobal(unmanagedString);
            }
        }
    }
}
clawsoftware commented 1 year ago

Patched with version 0.8.6