Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Duplicate images printed in XPS file

Tags:

c#

printing

wpf

xps

First off I would like to point out that I have raised this as a bug with Microsoft but they are unwilling to fix it at this point in time. What I am looking for is a workaround or a better way of achieving what I am trying to do as our customer has deemed this a rather important issue.

The code

MainWindow.xaml

<Grid x:Name="mainGrid">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <ListBox ItemsSource="{Binding Images}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Image Source="{Binding}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <Button Content="Print to file" Grid.Row="1" Click="PrintToFile_Click"/>
    <Button Content="Print to device" Grid.Row="2" Click="PrintToDevice_Click"/>
</Grid>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public IList<byte[]> Images { get; set; }

    public MainWindow()
    {
        InitializeComponent();

        Assembly currentAssembly = Assembly.GetExecutingAssembly();

        this.Images = new List<byte[]>
        {
            ReadToEnd(currentAssembly.GetManifestResourceStream("PrintingInvestigation.Images.Chrysanthemum.jpg")),
            ReadToEnd(currentAssembly.GetManifestResourceStream("PrintingInvestigation.Images.Desert.jpg")),
            ReadToEnd(currentAssembly.GetManifestResourceStream("PrintingInvestigation.Images.Hydrangeas.jpg")),
        };

        this.DataContext = this;
    }

    public static byte[] ReadToEnd(System.IO.Stream stream)
    {
        long originalPosition = 0;

        if (stream.CanSeek)
        {
            originalPosition = stream.Position;
            stream.Position = 0;
        }

        try
        {
            byte[] readBuffer = new byte[4096];

            int totalBytesRead = 0;
            int bytesRead;

            while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0)
            {
                totalBytesRead += bytesRead;

                if (totalBytesRead == readBuffer.Length)
                {
                    int nextByte = stream.ReadByte();
                    if (nextByte != -1)
                    {
                        byte[] temp = new byte[readBuffer.Length * 2];
                        Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length);
                        Buffer.SetByte(temp, totalBytesRead, (byte)nextByte);
                        readBuffer = temp;
                        totalBytesRead++;
                    }
                }
            }

            byte[] buffer = readBuffer;
            if (readBuffer.Length != totalBytesRead)
            {
                buffer = new byte[totalBytesRead];
                Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead);
            }
            return buffer;
        }
        finally
        {
            if (stream.CanSeek)
            {
                stream.Position = originalPosition;
            }
        }
    }

    private void PrintToDevice_Click(object sender, RoutedEventArgs e)
    {
        PrintDialog dialog = new PrintDialog();
        if (dialog.ShowDialog() == true)
        {
            Thickness pageMargins;

            if (dialog.PrintTicket.PageBorderless.HasValue == true)
            {
                if (dialog.PrintTicket.PageBorderless.Value == PageBorderless.Borderless)
                {
                    pageMargins = new Thickness(0, 0, 0, 0);
                }
                else
                {
                    pageMargins = new Thickness(20, 20, 20, 20);
                }
            }
            else
            {
                pageMargins = new Thickness(20, 20, 20, 20);
            }

            int dpiX = 300;
            int dpiY = 300;
            if (dialog.PrintTicket.PageResolution != null &&
                dialog.PrintTicket.PageResolution.X.HasValue &&
                dialog.PrintTicket.PageResolution.Y.HasValue)
            {
                dpiX = dialog.PrintTicket.PageResolution.X.Value;
                dpiY = dialog.PrintTicket.PageResolution.Y.Value;
            }
            else
            {
                dialog.PrintTicket.PageResolution = new PageResolution(dpiX, dpiY);
            }

            VisualDocumentPaginator paginator = new VisualDocumentPaginator(this.mainGrid, this.mainGrid.ActualWidth);
            paginator.PageSize = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight);

            dialog.PrintDocument(paginator, "My first print");
            GC.Collect();
        }
    }

    private void PrintToFile_Click(object sender, RoutedEventArgs e)
    {
        string filePath = this.PrintToFile(null, this.mainGrid, "My first print", this.mainGrid.ActualHeight, this.mainGrid.ActualWidth);

        Process.Start(filePath);
    }

    public string PrintToFile(Visual titleVisual, Visual contentVisual, string title, double bottomMost, double rightMost)
    {
        string printedFilePath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), string.Format(CultureInfo.InvariantCulture, "{0}.xps", title));

        XpsDocument printedDocument = new XpsDocument(printedFilePath, FileAccess.Write, System.IO.Packaging.CompressionOption.SuperFast);

        VisualDocumentPaginator paginator = new VisualDocumentPaginator(contentVisual as FrameworkElement, rightMost);
        paginator.PageSize = new Size(793.7, 1122.5);

        XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(printedDocument);
        writer.Write(paginator, new PrintTicket
        {
            Collation = Collation.Collated,
            CopyCount = 1,
            DeviceFontSubstitution = DeviceFontSubstitution.On,
            Duplexing = Duplexing.OneSided,
            InputBin = InputBin.AutoSelect,
            OutputColor = OutputColor.Color,
            OutputQuality = OutputQuality.High,
            PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA4),
            PageOrientation = PageOrientation.Portrait,
            PageResolution = new PageResolution(PageQualitativeResolution.High),
            PagesPerSheet = 1,
            TrueTypeFontMode = TrueTypeFontMode.Automatic
        });

        printedDocument.Close();

        return printedFilePath;
    }
}

VisualDocumentPaginator.cs

public class VisualDocumentPaginator : DocumentPaginator
{
    #region Fields

    private double desiredWidth;
    private FrameworkElement element;

    #endregion

    #region Properties

    public int Columns
    {
        get
        {
            return 1;// (int)Math.Ceiling(Element.ActualWidth / PageSize.Width);
        }
    }

    public int Rows
    {
        get
        {
            return (int)Math.Ceiling(element.ActualHeight / PageSize.Height);
        }
    }

    #endregion

    #region Constructors

    public VisualDocumentPaginator(FrameworkElement element, double desiredWidth)
    {
        this.desiredWidth = desiredWidth;
        this.element = element;
    }

    #endregion

    #region DocumentPaginator Members

    public override DocumentPage GetPage(int pageNumber)
    {
        TransformGroup transforms = new TransformGroup();

        double scaleRatio = this.PageSize.Width / this.desiredWidth;
        int row = (pageNumber / Columns);

        double pageHeight = -PageSize.Height * row / scaleRatio;
        double pageWidth = -PageSize.Width * (pageNumber % Columns);

        transforms.Children.Add(new TranslateTransform(pageWidth, pageHeight));

        // Make sure the control is stretched to fit the page size.
        if (scaleRatio != double.NaN)
        {
            ScaleTransform st = new ScaleTransform(scaleRatio, scaleRatio);
            transforms.Children.Add(st);
        }

        element.RenderTransform = transforms;

        Size elementSize = new Size(this.desiredWidth, element.ActualHeight);
        element.Measure(elementSize);
        element.Arrange(new Rect(new Point(0, 0), elementSize));

        var page = new DocumentPage(element, this.PageSize, new Rect(), new Rect());
        element.RenderTransform = null;

        return page;
    }

    public override bool IsPageCountValid
    {
        get { return true; }
    }

    public override int PageCount
    {
        get
        {
            return Columns * Rows;
        }
    }

    public override Size PageSize { set; get; }

    public override IDocumentPaginatorSource Source
    {
        get { return null; }
    }

    #endregion
}

Apologies for posting all the code but it covers all the areas in which I am seeing the issue. If it helps here is the Microsoft bug report which has a sample project attached where the issue can be reproduced.

The problem
The issue is only seen when writing to an XPS file where only the first image is printed 3 times, if the "Print to device" button is clicked then the correct images are printed.

The reason why I am binding to a byte[] is because I am persisting my images in a local SQL CE database. We store them in a DB because they are only small ~2KB each plus we allow users to import their own Icons into the system to use and we wanted a mechanism to guarantee that they wouldn't be accidentally deleted.

NOTE
I have noticed that if I do not bind to the byte[] as mentioned above then I do not see the issue. Given the fact that the system currently works off the approach of storing the images in a DB I would prefer to stick with it if there is workaround however I am not entirely against replacing the storage mechanism for these images.

like image 882
Bijington Avatar asked May 29 '15 07:05

Bijington


2 Answers

I have experienced a similar issue where the first image was duplicated and replaced all other images. In my case, printing to a device, to a XPS document or PDF document did not matter, the issue was still there.

Analysis

I have used a .NET assembly decompiler to figure out how the System.Windows.Xps.XpsDocumentWriter class deals with images to find out if the issue was in my code or in the framework's code. I discovered that the framework uses dictionnairies to import resources such as images into the document. Even if images are imported only once in the XPS document, the document is allowed to reference them many times.

In my case, I was able to figure out that the issue was located in the System.Windows.Xps.Serialization.ImageSourceTypeConverter.GetBitmapSourceFromImageTable method. When the image is built from a System.Windows.Media.Imaging.BitmapFrame, the converter will look for the resource in one of its dictionary using a key. In this case, the key correspond to the hash code of the string returned by the BitmapFrame.Decoder.ToString() method. Unfortunately, since my images are built from byte arrays instead of an URI, the decoder's ToString method returns "image". Since that string will always generate the same hash code no matter the image, the ImageSourceTypeConverter will consider that all images are already added to the XPS document and will return the Uri of the first and only image to be use. This explains why the first image is duplicated and is replacing all other images.

Attempt

My first attempt to workaround the issue was to override the System.Windows.Media.Imaging.BitmapDecoder.ToString() method. In order to do so, I tried to wrap the BitmapFrame and BitmapDecoder into my own BitmapFrame and BitmapDecoder. Unfortunately, the BitmapDecoder class contains an internal abstract method I cannot defined. Since I could not create my own BitmapDecoder, I could not implement that workaround.

Workaround

As mentionned previously, the System.Windows.Xps.Serialization.ImageSourceTypeConverter.GetBitmapSourceFromImageTable method will look for an hash code in a dictionary when the BitmapSource is a BitmapFrame. When it is not a BitmapFrame, it will instead generate a CRC value based on the image binary data and look for it in another dictionary.

In my case, I decided to wrap the BitmapFrame that were generated from byte arrays by the System.Windows.Media.ImageSourceConverter into another type of BitmapSource such as System.Windows.Media.Imaging.CachedBitmap. Since I did not really wanted cached bitmap, I created the CachedBitmap will the following options:

var imageSource = new CachedBitmap( bitmapFrame, BitmapCreateOptions.None, BitmapCacheOption.None );

With these options, the CachedBitmap is mostly a simple BitmapSource wrapper. In my case, this workaround solved my issue.

like image 151
André Rocheleau Avatar answered Sep 19 '22 14:09

André Rocheleau


I built a custom printing solution for a document management system built on WPF. I started using the System.Printing namespace but found endless bugs in .NET that Microsoft had not resolved in a long time. With no possible workarounds I ended up using the more mature System.Drawing.Printing namespace built for Windows Forms, with which I found no issues.

It would probably take you some time to rewrite your code to System.Drawing.Printing, but you are far less likely to hit a brick wall somewhere along the way.

like image 44
Glen Thomas Avatar answered Sep 20 '22 14:09

Glen Thomas