How to generate PDF on Windows Phone in VB or C#
I need to generate PDF from my Windows Phone app.
Unfortunately none of the standard free PDF-generating libraries work on Windows Phone. I've had to generate the PDF myself, by writing to the file format directly. It turned out to be really easy! Source code is at the bottom of this post, and in this link:
- Download source code.zip [60k, requires VS2012, includes Console and WP8 apps]
Why even generate PDF on Phone?
Some people say that there's no point having PDFs on the phone, since it's not the most efficient use of screen and touch interaction.
Well in my case, I wanted to generate a PDF, upload it to the user's SkyDrive account, and let the user send an email with a "share" link. That's how Microsoft's own PDF-Reader app works when you click the "share" button.
Other people suggest that a Phone has no business generating PDFs because it's such a costly operation, and you should instead upload your document to your PDF-generating webserver. This is wrong. PDF-generation, at least for the kinds of reports I was generating (two pages of non-flowed text), is a very cheap operation. I bet that it costs more battery to upload+download a document than to generate a PDF.
iTextSharp
https://itextpdf.com/ (AGPL license, or you can purchase a commercial license, available on NuGet)
iTextSharp doesn't work on Windows Phone, because it has a runtime dependency on System.Drawing.dll. (There's another experimental port https://itextsharpsl.codeplex.com/ under the MPL license, designed to work on Silverlight and Windows Phone, but I was unable to figure out the references needed to get it work. It's also a huge package, 30mb compressed source code). iTextSharp has superb documentation, including a published book. Here's how its API looks, to generate the PDF at the top of this article:
Imports iTextSharp.text
Module Module1
Sub Main()
Dim doc As New Document ()
Using file As New System.IO. FileStream ( "a.pdf" , System.IO. FileMode .Create),
writer = pdf. PdfWriter .GetInstance(doc, file)
doc.Open()
Dim titleFont = FontFactory .GetFont( FontFactory .TIMES, 40, Font .NORMAL)
Dim subtitleFont = FontFactory .GetFont( FontFactory .TIMES, 20, Font .ITALIC, BaseColor .BLUE)
Dim column As New pdf. ColumnText (writer.DirectContent)
column.SetSimpleColumn(230, 600, 400, 400) ' bounding coords of column
column.AddText( New Phrase ( "Hello all" , titleFont))
column.Go()
column.AddText( New Phrase ( "olé" , subtitleFont))
column.Go()
doc.Close()
End Using
Process .Start( "a.pdf" )
End Sub
End Module
PDFSharp
https://www.pdfsharp.net/ (MIT license, available on NuGet)
PDFSharp doesn't work on Windows Phone, because it has a compiletime dependency on System.Drawing.dll. Here's how its API looks...
Imports PdfSharp.Drawing
Module Module1
Sub Main()
Dim document As New PdfSharp.Pdf. PdfDocument
Dim page = document.AddPage
Dim gfx = XGraphics .FromPdfPage(page)
Dim titleFont As New XFont ( "Times New Roman" , 40, XFontStyle .Regular)
Dim subtitleFont As New XFont ( "Times New Roman" , 20, XFontStyle .Italic)
gfx.DrawString( "Hello all" , titleFont, XBrushes .Black, 230, 400)
Dim height = gfx.MeasureString( "olé" , subtitleFont).Height
gfx.DrawString( "olé" , subtitleFont, XBrushes .Blue, 230, 400 + height)
Using stream As New IO. FileStream ( "a.pdf" , IO. FileMode .Create)
document.Save(stream)
End Using
Process .Start( "a.pdf" )
End Sub
End Module
PDFJet
https://www.pdfjet.com/os/edition.html (BSD license; not available on NuGet)
PDFJet doesn't work on Windows Phone, because it has a compiletime dependency System.IO.DeflateStream. PDFJet is the most interesting of all these PDF libraries. It's been written from scratch with as few dependencies as possible - no dependencies on System.Drawing, or Winforms. Its only dependency is on DeflateStream. If someone could package it up with a Phone-compatible implementation of DeflateStream then it would work great. I tried to get it to work with DotNetZip and SharpZipLib but didn't succeed. Note that PDFJet is distributed as just a directory full of .cs files, so you have to create a project for it. Easy to do. Anyway, here's how it looks:
Imports PDFjet.NET
Module Module1
Sub Main()
Using stream As New IO. FileStream ( "a.pdf" , IO. FileMode .Create)
Dim pdf As New PDF (stream)
Dim page As New Page (pdf, Letter .PORTRAIT)
Dim titleFont As New Font (pdf, CoreFont .TIMES_ROMAN)
titleFont.SetSize(40)
Dim subtitleFont As New Font (pdf, CoreFont .TIMES_ITALIC)
subtitleFont.SetSize(20)
Dim text1 As New TextLine (titleFont, "Hello all" )
text1.SetPosition(230, 400)
text1.DrawOn(page)
Dim text2 As New TextLine (subtitleFont, "olé" )
text2.SetPosition(230, 400 + text2.GetHeight)
text2.SetColor( Color .blue)
text2.DrawOn(page)
pdf.Flush()
End Using
Process .Start( "a.pdf" )
End Sub
End Module
PDFClown
https://pdfclown.wordpress.com/ (LGPL license; not available on NuGet)
PDFClown doesn't work on Windows Phone, because it has a compiletime dependency System.Drawing and Winforms. PDFClown aggressively uses Java rather than .NET conventions. Its API is an exact mirror of the PDF file format structure. In my view it doesn't offer much benefit beyond just writing the file format structure manually. Here's how it looks:
Imports org.pdfclown
Imports org.pdfclown.documents.contents
Module Module1
Sub Main()
Using stream As New IO. FileStream ( "a.pdf" , IO. FileMode .Create),
pdfstream As New org.pdfclown.bytes. Stream (stream),
pdf As New files. File ()
Dim document = pdf.Document
Dim page As New documents. Page (document)
document.Pages.Add(page)
Dim titleFont = New fonts. StandardType1Font (document, fonts. StandardType1Font . FamilyEnum .Times, False , False )
Dim subtitleFont = New fonts. StandardType1Font (document, fonts. StandardType1Font . FamilyEnum .Times, False , True )
Dim composer As New composition. PrimitiveComposer (page)
Dim blockcomposer As New composition. BlockComposer (composer)
blockcomposer.Begin( New Drawing. RectangleF (230, 400, 200, 200), composition. XAlignmentEnum .Left, composition. YAlignmentEnum .Top)
composer.SetFont(titleFont, 40)
blockcomposer.ShowText( "Hello all" & vbCrLf)
composer.SetFont(subtitleFont, 20)
composer.SetFillColor( New colorSpaces. DeviceRGBColor (0, 0, 1))
blockcomposer.ShowText( "olé" & vbCrLf)
blockcomposer.End()
composer.Flush()
pdf.Save(pdfstream, files. SerializationModeEnum .Standard)
End Using
Process .Start( "a.pdf" )
End Sub
End Module
Write the PDF file format yourself
https://acroeng.adobe.com/wp/?page_id=321 (documentation of PDF file format; I used the 1.2 version of the PDF spec)
This code works great on Windows Phone. Frankly, the PDF file format is very simple if all you want to do is create simple reports - with text, charts &c. What we need is sample code for that show how to create PDFs, not just libraries that encapsulate how to create PDFs. So here's my sample code! Note that I make use of one extra function "enc" which translates non-ASCII from Unicode (used in .NET) into WinAnsiEncoding (the encoding I chose to use for the PDF) - full source code is downloadable from the link at the top of this article.
Private Async Sub Button1_Click(sender As Object , e As RoutedEventArgs ) Handles Button1.Click
Dim file = Await ApplicationData .Current.LocalFolder.CreateFileAsync("a.pdf" , Windows.Storage. CreationCollisionOption .ReplaceExisting)
Using stream = Await System.IO. WindowsRuntimeStorageExtensions .OpenStreamForWriteAsync(file),
writer As New IO. StreamWriter (stream, Text. Encoding .UTF8)
Dim xrefs As New List ( Of Long )()
' PDF-HEADER
writer.WriteLine( "%PDF-1.2" )
' PDF-BODY. Convention is to start with a 4-byte binary comment
' so everyone recognizes the pdf as binary. Then the file has
' a load of numbered objects, #1..#7 in this case
writer.Write( "%" ) : writer.Flush()
stream.Write({&HC7, &HEC, &H8F, &HA2}, 0, 4) : stream.Flush()
writer.WriteLine( "" )
' #1: catalog - the overall container of the entire PDF
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
writer.WriteLine( "1 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Type /Catalog" )
writer.WriteLine( " /Pages 2 0 R" )
writer.WriteLine( ">>" )
writer.WriteLine( "endobj" )
' #2: page-list - we have only one child page
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
writer.WriteLine( "2 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Type /Pages" )
writer.WriteLine( " /Kids [3 0 R]" )
writer.WriteLine( " /Count 1" )
writer.WriteLine( ">>" )
writer.WriteLine( "endobj" )
' #3: page - this is our page. We specify size, font resources, and the contents
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
writer.WriteLine( "3 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Type /Page" )
writer.WriteLine( " /Parent 2 0 R" )
writer.WriteLine( " /MediaBox [0 0 612 792]" ) ' Default userspace units: 72/inch, origin at bottom left
writer.WriteLine( " /Resources" )
writer.WriteLine( " <<" )
writer.WriteLine( " /ProcSet [/PDF/Text]" ) ' This PDF uses only the Text ability
writer.WriteLine( " /Font" )
writer.WriteLine( " <<" )
writer.WriteLine( " /F0 4 0 R" ) ' I will define three fonts, #4, #5 and #6
writer.WriteLine( " /F1 5 0 R" )
writer.WriteLine( " /F2 6 0 R" )
writer.WriteLine( " >>" )
writer.WriteLine( " >>" )
writer.WriteLine( " /Contents 7 0 R" )
writer.WriteLine( ">>" )
writer.WriteLine( "endobj" )
' #4, #5, #6: three font resources, all using fonts that are built into all PDF-viewers
' We're going to use WinAnsi character encoding, defined below.
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
writer.WriteLine( "4 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Type /Font" )
writer.WriteLine( " /Subtype /Type1" )
writer.WriteLine( " /Encoding /WinAnsiEncoding" )
writer.WriteLine( " /BaseFont /Times-Roman" )
writer.WriteLine( ">>" )
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
writer.WriteLine( "5 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Type /Font" )
writer.WriteLine( " /Subtype /Type1" )
writer.WriteLine( " /Encoding /WinAnsiEncoding" )
writer.WriteLine( " /BaseFont /Times-Bold" )
writer.WriteLine( ">>" )
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
writer.WriteLine( "6 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Type /Font" )
writer.WriteLine( " /Subtype /Type1" )
writer.WriteLine( " /Encoding /WinAnsiEncoding" )
writer.WriteLine( " /BaseFont /Times-Italic" )
writer.WriteLine( ">>" )
' #7: contents of page. This is written in postscript, fully described in
' chapter 8 of the PDF 1.2 reference manual.
writer.Flush() : stream.Flush() : xrefs.Add(stream.Position)
Dim sb As New Text. StringBuilder
sb.AppendLine( "BT" ) ' BT = begin text object, with text-units the same as userspace-units
sb.AppendLine( "/F0 40 Tf" ) ' Tf = start using the named font "F0" with size "40"
sb.AppendLine( "40 TL" ) ' TL = set line height to "40"
sb.AppendLine( "230.0 400.0 Td" ) ' Td = position text point at coordinates "230.0", "400.0"
sb.AppendLine( "(Hello all) '" ) ' Apostrophe = print the text, and advance to the next line
sb.AppendLine( "/F2 20 Tf" ) '
sb.AppendLine( "20 TL" ) '
sb.AppendLine( "0.0 0.2 1.0 rg" ) ' rg = set fill color to RGB("0.0", "0.2", "1.0")
sb.AppendLine( "(ol" & enc( "é" ) & ") '" )
sb.AppendLine( "ET" ) '
'
writer.WriteLine( "7 0 obj" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Length " & sb.Length)
writer.WriteLine( ">>" )
writer.WriteLine( "stream" )
writer.Write(sb.ToString())
writer.WriteLine( "endstream" )
writer.WriteLine( "endobj" )
' PDF-XREFS. This part of the PDF is an index table into every object #1..#7
' that we defined.
writer.Flush() : stream.Flush() : Dim xref_pos = stream.Position
writer.WriteLine( "xref" )
writer.WriteLine( "1 " & xrefs.Count)
For Each xref In xrefs
writer.WriteLine( "{0:0000000000} {1:00000} n" , xref, 0)
Next
' PDF-TRAILER. Every PDF ends with this trailer.
writer.WriteLine( "trailer" )
writer.WriteLine( "<<" )
writer.WriteLine( " /Size " & xrefs.Count)
writer.WriteLine( " /Root 1 0 R" )
writer.WriteLine( ">>" )
writer.WriteLine( "startxref" )
writer.WriteLine(xref_pos)
writer.WriteLine( "%%EOF" )
End Using
Await Windows.System. Launcher .LaunchFileAsync(file)
End Sub