PDF Rendering in Windows 8.1 Preview

Keine Zeit? Hier geht’s zum Code.

Fast Track: Find the code here.

In einem meiner letzten Blogposts habe ich eine kleine Anwendung vorgestellt, die einen zweiten Bildschirm nutzt. Das ist an sich schon relativ interessant, handelt es sich dabei doch um eine Windows Store App, die bisher nur einen Bildschirm nutzen konnten. Einen zweiten Aspekt der PDFPresenter App habe ich unterschlagen: Die Tatsache, dass die App in der Lage ist PDFs zu Rendern – und das ohne zusätzliche Libraries, sondern nur mit Bordmitteln. Für diesen Blogpost habe ich mein bestehendes Sample, den PdfPresenter ein wenig aufgebohrt. Ihr könnt  den Source Code hier herunterladen.

Bitte beachtet wie immer: Es handelt sich um Demo-Code, dessen einziger Zweck es ist Euch ein paar essentielle Zeilen Code zu zeigen in der Hoffnung sie mögen Euch das leben erleichtern und Euch inspirieren – nicht um eine fertige Applikation.

Einfaches Rendering

Im Prinzip ist das Darstellen von PDFs wirklich simple, es besteht nur aus wenigen Schritten.

1. Schritt:  Pdf Dokument auswählen
Hier bietet es sich an, den FileOpenPicker mit einem Filter auf ".pdf” zu nutzen. Der Anwender hat dann die Möglichkeit im gesamten Dateisystem nach einer PDF Datei zu suchen. Dadurch erhalten wir ein StorageFile. Über die statische Methode “LoadFromFileAsync”, die uns die Klasse PdfDocument anbietet, können wir  ein PdfDocument erhalten.

 var pdfDoc = await PdfDocument.LoadFromFileAsync(myStorageFile);

2. Schritt: Seite auswählen, die dargestellt werden soll

Ein Pdf-Dokument hat potentiell mehr als eine Seite. Wir können die Anzahl der Seiten über das PageCount Property abfragen. Eine einzelne Page des Dokuments holen wir uns dann über die GetPage Methode aufs Dokument.

 var pdfPage = pdfDoc.GetPage(i);

3. Schritt: Seite rendern

Auch dieser Schritt ist relativ simple. Um eine Seite darzustellen verwandeln wir die PDF Seiteninformation in ein Bild – dafür gibt es eine Methode der Klasse PdfPage RenderToStreamAsync.

 IRandomAccessStream randomStream = await jpgFile.OpenAsync(FileAccessMode.ReadWrite);
await pdfPage.RenderToStreamAsync(randomStream);

Damit sind wir eigentlich fertig. Wir können dann das jpgFile ganz normal darstellen. Es bleibt uns überlassen, wie wir mit der Datei umgehen wollen, wenn der Anwender die Seite wechselt: Wir können sie aufheben und später wiederverwenden oder löschen und später neu rendern. Ich denke, es ist eine gute Idee, sich hierzu ein paar Gedanken zu machen, die natürlich abhängig von der Anwendung sind. Vielleicht kann man ja über einen geschickten Mechanismus ein Caching implementieren und so ein paar Systemressource sparen. Für meine Demo habe ich diesen Aspekt aber außen vor gelassen.

 

Rendering mit Optionen

Wir haben noch ein paar weitere Optionen. Wie Ihr vielleicht festgestellt habt, wenn Ihr mein letztes Sample heruntergeladen habt, gibt es dort ein paar weniger schöne Aspekte zu bemängeln. So greift die Preview der PDF Seite auf das gleiche gerenderte Bild zu, wie auch die Vollbild-Darstellung auf dem zweiten Screen. Da hier der zur Verfügung stehende Bildschirmplatz wesentlich größer sein kann, wird unter Umständen ein gerendertes PDF  vergrößert dargestellt. Insbesondere, wenn wir den Scrollviewer zoomen, bemerkt man recht schnell, dass die Darstellung auf dem zweiten Monitor unsauber und unscharf wird – so wie das eben ist, wenn ein zu kleines Bild zu groß dargestellt wird.

Wenn wir hieran was ändern möchten, müssen wir Rendering Optionen setzen. Die wichtigsten dabei sind die Zielgröße des Bildes sowie der Ausschnitt, den wir im Bild abgebildet haben möchten. Wir können von einer einzelnen Seite im Dokument einen Ausschnitt wählen, den wir auf die Zielgröße rendern lassen.

 

image

Die Angabe der Zielgröße macht man in den PdfRendereningOptions unter den Properties  DestinationWidth  und DestinationHeight. In meinem Sample leite ich sie von der Windowsize ab – letztlich kommt es hier auf den Einzelfall in Eurer App an.

  // set target H&W            
 pdfPageRenderOptions.DestinationHeight = (uint)Window.Current.Bounds.Height;
 pdfPageRenderOptions.DestinationWidth = (uint)Window.Current.Bounds.Width;

Spannender ist die Frage, wie man den richtigen Ausschnitt einer Seite angibt – im Prinzip verrät die Skizze oben aber schon alles. Über das Property SourceRect in den PdfPageRenderOptions kann ich ein Rechteck festlegen, mit dem ich angebe, wie groß der gewählte Bereich sein soll und wo er sich auf der Seite befindet. Wichtig hierbei ist folgendes: Die Werte X und Y sowie b und h oben in der Skizze beziehen sich auf das Pdf selbst und nicht auf eine eventuelle Vorschaudarstellung oder eine bereits gerenderte Darstellung.

Um die Werte sinnvoll in ein Verhältnis setzen zu können (woher soll man auch wissen, wie “breit” eine Pdf-Seite ist, solange man sie noch gar nicht sieht) kann man das Property PdfPage.Size abfragen. Hier verrät uns die PdfPage also, wie breit sie eigentlich dargestellt sein möchte. Diese Breite kann man, muss man aber nicht als Pixel ansehen. Letztlich ist die Einheit hier völlig egal –wir legen ja nur den Ausschnitt fest, der uns interessiert, indem wir unterschiedliche Maße hier miteinander in Bezug setzen. Es könnte sich hier also, wie mein ehemaliger Mathelehrer immer zum Besten gab, tatsächlich um “Bratwörscht” statt Pixel handeln, die Rechnung stimmt am Ende doch Smiley .

 pdfPageRenderOptions.SourceRect =new Rect( 0, 0, 100, 100);

Unterm Strich also aus der reinen Code-Perspektive nicht kompliziert. Erfahrungsgemäß benötigt die Berechnung der Größenverhältnisse deutlich mehr Zeit als das Schreiben des Codes.

Sample App

Ich habe meine kleine Sample App dahingehend aufgebohrt, dass die Darstellung auf dem zweiten Screen abhängig von der Größe des Screens neu gerendert wird. Wird dann in der Preview ein-/ausgezoomt, so wird basierend auf dem in der Preview dargestellten Ausschnitt der Ausschnitt auf dem zweiten Screen neu gerendert – und damit knackscharf, so wie es sein soll. Das funktioniert natürlich nicht, wenn sich Bilder im PDF befinden, sondern klappt nur bei Text.

Ein paar Dinge gibt es bei der Umsetzung im Code zu beachten:

1. Das ViewChanged Event feuert mehr als einmal

Wenn ich im Preview Fenster die Größe manipuliere, dann feuert das ViewChanged Event relativ häufig, nicht nur einmal am Ende. Ich habe deshalb einen einfachen Locking Mechanismus implementiert, der darauf beruht nicht sofort mit dem Rendern der Darstellung für Bildschirm zwei anzufangen. Würde ich das tun, würden ständig neue Dateien in unterschiedlichen Größen angelegt werden, was sehr viel Zeit kostet und den Bildschirm zum Flackern bringt. Statt dessen warte ich nach jeder Manipulation 2 Sekunden und rendere erst dann und zwar nur die letzte Manipulation. Während dieser Zeit wird die Darstellung von Preview und Presenterview über das ursprüngliche, schlecht auflösende Jpeg synchron gehalten.

2. Die Scrollposition wird zurückgesetzt

Da ich sowohl in PresenterView als auch in Preview mit einem ScrollViewer arbeite, muss ich die Position des ScrollViewers im Presenter vor der Darstellung des neu gerenderten Bildes rücksetzen auf die Ausgangswerte – sonst würde ja die neue Darstellung gezoomt. Das macht sich durch ein kurzes Hin-und-Her-Scrollen bemerkbar. (Was mir für diese Demo aber egal war.)

3. Die Dateien werden nicht gelöscht.

Im Demo lösche ich die Dateien nicht. Hier ist also viel Raum für eigene Implementierungen für Lösch- oder Caching-Algorithmen.

4. Hintergrundfarbe

In der Demo setzte ich die Hintergrundfarbe des neugerenderten Pdfs auf “Antique-Weiß”. So erkennt man das Rendering besser.

 // just for fun - set bgcolor. This way you can easily find out, if the rerendering happened.
pdfPageRenderOptions.BackgroundColor = Colors.AntiqueWhite;

5. Dispatcher

Da ich mit zwei Screens arbeite und Informationen aus einem im anderen verwerte, bzw aus der Page des einen Methoden der Page des anderen aufrufe, muss ich bisweilen den Dispatcher bemühen, um mich im jeweils richtigen Kontext wiederzufinden.

Im Einsatz

In meiner PdfPresenter App auf Monitor 1 (1920 x 1080) habe ich also eine PdfPage aus der Seitenübersicht ausgewählt und in die Preview geladen. Dort habe ich, wie Ihr in der Vorschau rechts sehen könnt ein wenig gezoomt.

 

Preview

Dadurch wird in meiner Presenter Ansicht auf Monitor 2 (1600 x900) eine Darstellung basierend auf der gleichen Bilddatei erzeugt. Diese ist leider ziemlich unscharf, wie Ihr erkennen könnt.

unrendered

Nach zwei Sekunden wird aber eine scharfe Version nachgeliefert und geladen. Die Hintergrundfarbe wird explizit gesetzt. In diesem Fall profitiert auch das eingebettete Bild etwas von der höheren Auflösung – hier ist aber kein endloses Skalieren möglich.

rendered

Cool oder? Wie immer gibt’s den Sourcecode hier. Das offizielle PDF Sample findet Ihr hier. Viel Spaß beim Rendern!

Tags Markierungen: PDF Rendering, Windows Store Apps, App Development, PdfPage, PdfDocument, PdfPageRenderingOptions, XAML, C#, PdfPresenter