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.
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With