« Git Bash in Console2 | Main | Avoid System.Windows.Rect.ToString() »

30 December 2011

Printing from .NET 3.5 in Windows 7

Our users discovered a curious bug that appears to be caused by:

  1. Printing an XpsDocument
  2. that uses a font embedded in the application's resources
  3. from a .NET 3.5 application
  4. running on Windows 7

The printed output looks like the following image; various glyphs are substituted with larger sans-serif versions of themselves, causing a ransom-note-like appearance.

Corrupted print output from .NET 3.5

We found that changing any one of the conditions above fixes the problem, but unfortunately we need to print XPS using an embedded font, we're still using .NET 3.5, and we have to run on Windows 7.

Strangely enough, printing to the XPS Document Writer print driver, and then printing that document with the XPS Viewer built into Windows 7 doesn't reproduce the problem; it only happens when our app prints directly to an actual printer.

Workaround

I noticed that the XPS Viewer is a native application; this led me to discover the Windows 7 XPS Print API. In conjunction with the (also new in Windows 7) XPS Document API, this lets you print XPS documents from native code.

We were able to solve the problem by automating the workaround described above: our code writes its output to an XpsDocumentWriter backed by a temporary file (instead of a PrintQueue); we then use the native APIs to print the temporary XPS file to the currently-selected printer.

The first part is to define the native methods and COM interfaces we will need. (And, as noted in this StackOverflow question, the IXpsPrintJobStream interface is either declared or implemented incorrectly, so we have to call the Close method as if it existed on ISequentialStream.)

With those declared, the printing code can be written. This method (which should be called on a background thread), prints a XPS file to a specific printer, returning true if printing succeeded. (If it fails, the application should fall back to the .NET printing APIs.)

All the code below is also available in a gist.

    internal static class NativeMethods
    {
        [DllImport("XpsPrint.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
        public static extern int StartXpsPrintJob(string printerName, string jobName, string outputFileName, IntPtr progressEvent,
        SafeWaitHandle completionEvent, [MarshalAs(UnmanagedType.LPArray)] byte[] printablePagesOn, int printablePagesOnCount,
        out IXpsPrintJob xpsPrintJob, out IXpsPrintJobStream documentStream, out IXpsPrintJobStream printTicketStream);
    }

    [ComImport, Guid("E974D26D-3D9B-4D47-88CC-3872F2DC3585"), ClassInterface(ClassInterfaceType.None)]
    internal class XpsOMObjectFactory
    {
    }

    [ComImport, Guid("F9B2A685-A50D-4FC2-B764-B56E093EA0CA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IXpsOMObjectFactory
    {
        void CreatePackage();

        [return: MarshalAs(UnmanagedType.Interface)]
        IXpsOMPackage CreatePackageFromFile([MarshalAs(UnmanagedType.LPWStr)] string filename, bool reuseObjects);

        void CreatePackageFromStream();
        void CreateStoryFragmentsResource();
        void CreateDocumentStructureResource();
        void CreateSignatureBlockResource();
        void CreateRemoteDictionaryResource();
        void CreateRemoteDictionaryResourceFromStream();
        void CreatePartResources();
        void CreateDocumentSequence();
        void CreateDocument();
        void CreatePageReference();
        void CreatePage();
        void CreatePageFromStream();
        void CreateCanvas();
        void CreateGlyphs();
        void CreatePath();
        void CreateGeometry();
        void CreateGeometryFigure();
        void CreateMatrixTransform();
        void CreateSolidColorBrush();
        void CreateColorProfileResource();
        void CreateImageBrush();
        void CreateVisualBrush();
        void CreateImageResource();
        void CreatePrintTicketResource();
        void CreateFontResource();
        void CreateGradientStop();
        void CreateLinearGradientBrush();
        void CreateRadialGradientBrush();
        void CreateCoreProperties();
        void CreateDictionary();
        void CreatePartUriCollection();
        void CreatePackageWriterOnFile();
        void CreatePackageWriterOnStream();
        void CreatePartUri();
        void CreateReadOnlyStreamOnFile();
    }

    [ComImport, Guid("18C3DF65-81E1-4674-91DC-FC452F5A416F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IXpsOMPackage
    {
        void GetDocumentSequence();
        void SetDocumentSequence();
        void GetCoreProperties();
        void SetCoreProperties();
        void GetDiscardControlPartName();
        void SetDiscardControlPartName();
        void GetThumbnailResource();
        void SetThumbnailResource();
        void WriteToFile();

        void WriteToStream(IXpsPrintJobStream stream, bool optimizeMarkupSize);
    };

    // NOTE: It appears that the IID for IXpsPrintJobStream specified in XpsPrint.h --  
    // MIDL_INTERFACE("7a77dc5f-45d6-4dff-9307-d8cb846347ca") -- is not correct, or the object
    // doesn't implement QueryInterface correctly. However, we can QI for ISequentialStream and
    // successfully (at least in Windows 7 SP1 x86) call the Close method as if it existed on that
    // interface.
    // That is, we obtain the ISequentialStream interface, but work with it as the IXpsPrintJobStream interface.
    // Thanks to http://stackoverflow.com/questions/6123507/xps-printing-from-windows-service for this tip.
    [ComImport, Guid("0C733A30-2A1C-11CE-ADE5-00AA0044773D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IXpsPrintJobStream
    {
        // ISequentialStream methods
        void Read([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbRead);
        void Write([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbWritten);

        // IXpsPrintJobStream methods
        void Close();
    }

    [ComImport, Guid("5AB89B06-8194-425F-AB3B-D7A96E350161"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IXpsPrintJob
    {
        void Cancel();
        IntPtr GetJobStatus();
    };
    /// <summary>
    /// Prints the specified XPS document to a printer using the native XPS Print API.
    /// </summary>
    /// <param name="xpsFilePath">The path to the XPS document.</param>
    /// <param name="printerName">The printer name.</param>
    /// <param name="printTicket">A PrintTicket with settings for this print job.</param>
    /// <returns><c>true</c> if the document was successfully printed; otherwise, <c>false</c>.</returns>
    /// <remarks>This method should be called from a background thread.</remarks>
    public bool Print(string xpsFilePath, string printerName, PrintTicket printTicket)
    {
        // try to create the XPS Object Model factory (only available on Windows 7 and Vista with the Platform Update)
        IXpsOMObjectFactory xpsFactory = null;
        try
        {
            xpsFactory = (IXpsOMObjectFactory)new XpsOMObjectFactory();
        }
        catch (COMException)
        {
            // OS doesn't support the XPS Document API
            return false;
        }

        bool success = false;
        IXpsOMPackage package = null;

        try
        {
            // load the saved document as a native XpsOMPackage
            package = xpsFactory.CreatePackageFromFile(xpsFilePath, false);

            using (ManualResetEvent handle = new ManualResetEvent(false))
            {
                // attempt to start the print job
                IXpsPrintJob printJob;
                IXpsPrintJobStream docStream, ticketStream;
                int hresult = NativeMethods.StartXpsPrintJob(printerName, jobTitle, null, IntPtr.Zero, handle.SafeWaitHandle,
                    null, 0, out printJob, out docStream, out ticketStream);

                // check for success (NOTE: checking HRESULT value directly instead of calling Marshal.ThrowExceptionForHR
                //   to avoid proliferation of 'catch' blocks)
                if (hresult >= 0)
                {
                    // write the current printer settings to the print ticket stream
                    byte[] ticketData = printTicket.GetXmlStream().ToArray();
                    uint bytesWritten;
                    ticketStream.Write(ticketData, (uint)ticketData.Length, out bytesWritten);
                    ticketStream.Close();

                    // write the XPS package to the document stream
                    package.WriteToStream(docStream, false);
                    docStream.Close();

                    // wait for printing to finish
                    handle.WaitOne();
                    success = true;
                }
            }
        }
        catch (COMException)
        {
            // printing failed
        }
        catch (DllNotFoundException)
        {
            // OS doesn't support XPS Print API
        }
        catch (EntryPointNotFoundException)
        {
            // OS doesn't support XPS Print API
        }

        // force the XPS package to be released, so that the temporary file can be deleted
        if (package != null)
            Marshal.FinalReleaseComObject(package);

        return success;
    }

Posted by Bradley Grainger at December 30, 2011 04:01 PM