2015-05-29 22 views
6

Po pierwsze chciałbym podkreślić, że podniosłem to jako błąd w firmie Microsoft, ale nie są oni skłonni to naprawić w tym momencie. To, czego szukam, to obejście lub lepszy sposób osiągnięcia tego, co próbuję zrobić, ponieważ nasz klient uznał to za dość ważny problem.Powielone obrazy wydrukowane w pliku XPS

Kod

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 
} 

Przepraszam za opublikowanie całego kodu, ale obejmuje on wszystkie obszary, w których widzę problem. Pomaga tutaj Microsoft bug report, do którego dołączony jest przykładowy projekt, w którym można odtworzyć problem.

Problem
Kwestia ta jest widoczna tylko podczas zapisu do pliku XPS gdzie tylko pierwszy obraz jest drukowany 3 razy, jeśli przycisk „Print to urządzenie” kliknięciu następnie poprawne obrazy są drukowane.

Powodem, dla którego wiążę bajt [], jest fakt, że moje obrazy są przechowywane w lokalnej bazie danych SQL CE. Przechowujemy je w bazie danych DB, ponieważ są one tylko małe ~ 2KB, a my pozwalamy użytkownikom na importowanie własnych ikon do systemu i chcemy mieć mechanizm gwarantujący, że nie zostaną przypadkowo usunięte.

UWAGA
Zauważyłem, że jeśli nie wiążą się z byte [], jak wspomniano powyżej, wtedy nie widzę problemu. Biorąc pod uwagę fakt, że system obecnie pracuje nad podejściem do przechowywania obrazów w DB wolałbym trzymać się go, jeśli istnieje obejście, jednak nie jestem całkowicie przeciwny zastąpieniu mechanizmu przechowywania tych obrazów.

Odpowiedz

1

Zbudowałem niestandardowe rozwiązanie do drukowania dla systemu zarządzania dokumentami opartego na WPF. Zacząłem używać przestrzeni nazw System.Printing, ale znalazłem nieskończone błędy w .NET, których Microsoft nie rozwiązał od dłuższego czasu. Bez możliwości obejścia tego problemu, skorzystałem z bardziej dojrzałej przestrzeni nazw zbudowanej dla Windows Forms, z którą nie miałem problemów.

Zajęłoby Ci to trochę czasu, aby przepisać swój kod na System.Drawing.Printing, ale znacznie mniej prawdopodobne jest uderzenie w mur gdzieś po drodze.

+0

dzięki za to. Miałem nadzieję znaleźć dość szybkie obejście problemu, ale klient zdecydował, że chce znieść obsługę XPS i przejść do formatu PDF, więc myślę, że będziemy przepisywać logikę drukowania, z której korzystamy. Z pewnością zajrzę do System.Drawing.Printing. – Bijington

1

Doświadczyłem podobnego problemu, gdy pierwszy obraz został zduplikowany i zastąpił wszystkie inne obrazy. W moim przypadku drukowanie na urządzeniu, dokumencie XPS lub dokumencie PDF nie miało znaczenia, problem nadal był obecny.

Analiza

Użyłem decompiler NET, aby dowiedzieć się, w jaki sposób System.Windows.Xps.XpsDocumentWriter oferty klasie z obrazami, aby dowiedzieć się, czy problem był w moim kodu lub w kodzie tych ram. Odkryłem, że framework wykorzystuje dyktafony do importowania zasobów takich jak obrazy do dokumentu.Nawet jeśli obrazy są importowane tylko jeden raz w dokumencie XPS, dokument może wielokrotnie odwoływać się do nich.

W moim przypadku udało mi się ustalić, że problem był zlokalizowany w metodzie System.Windows.Xps.Serialization.ImageSourceTypeConverter.GetBitmapSourceFromImageTable. Gdy obraz zostanie zbudowany z System.Windows.Media.Imaging.BitmapFrame, konwerter wyszuka zasób w jednym ze słowników za pomocą klucza. W tym przypadku klucz odpowiada kodowi skrótu łańcucha zwróconego przez metodę BitmapFrame.Decoder.ToString(). Niestety, ponieważ moje obrazy są budowane z tablic bajtów zamiast URI, metoda dekodera ToString zwraca "obraz". Ponieważ ten ciąg zawsze generuje ten sam kod skrótu, niezależnie od obrazu, ImageSourceTypeConverter uzna, że ​​wszystkie obrazy są już dodane do dokumentu XPS i zwróci Uri pierwszego i jedynego obrazu, który ma być użyty. To tłumaczy, dlaczego pierwszy obraz jest duplikowany i zastępuje wszystkie inne obrazy.

Próba

Moja pierwsza próba obejścia problemu było zastąpić metodę System.Windows.Media.Imaging.BitmapDecoder.ToString(). Aby to zrobić, starałem się opakować BitmapFrame i BitmapDecoder w swoje własne BitmapFrame i . Niestety, klasa BitmapDecoder zawiera metodę internal abstract, której nie mogę zdefiniować. Ponieważ nie mogłem utworzyć własnego BitmapDecoder, nie mogłem wdrożyć tego obejścia.

Obejście

Jak wymieniono wcześniej, metoda System.Windows.Xps.Serialization.ImageSourceTypeConverter.GetBitmapSourceFromImageTable będzie szukać kodu skrótu w słowniku gdy BitmapSource jest BitmapFrame. Gdy nie jest to BitmapFrame, zamiast tego wygeneruje wartość CRC na podstawie danych binarnych obrazu i poszukać go w innym słowniku.

W moim przypadku zdecydowałem się owinąć BitmapFrame które zostały wygenerowane z tablicami bajtów przez System.Windows.Media.ImageSourceConverter do innego rodzaju BitmapSource takich jak System.Windows.Media.Imaging.CachedBitmap. Ponieważ tak naprawdę nie chciał buforowane bitmapę, stworzyłem CachedBitmap będzie następujące opcje:

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

z tymi opcjami, tym CachedBitmap to głównie proste BitmapSource wrapper. W moim przypadku to obejście rozwiązało mój problem.