1

Am in the process of creating a printer friendly pdf version of a webpage and have run into some difficulties trying to add a Google areachart too the pdf.

the areachart is generated using JavaScript and rendered as an svg on the page, I have tried a number of different approaches for adding the chart including importing the html for the div that contains the chart (so far all I get using this approach is the title for the chart and not the actual chart), I have looked into converting the svg to an image format (png, jpeg, gif etc) and then saving the image and adding to pdf as image.

Does anyone have any ideas on how I could implement this?

Any advice would be very much appreciated.

CryoFusion87
  • 796
  • 1
  • 8
  • 28
  • You cannot render the chart in a PDF - you have to render in a browser and convert to .png. The API provides a `getImageURI` method for most of the charts that will return a URI that you can pass to the `src` of an `` tag, and then save the image. – asgallant Aug 26 '14 at 23:08
  • thanks for the advice, most of the means for converting svg to png involve using HTMl5 based techniques, is their a way of converting that does not use HTML5 (particulary canvas) as some of the browsers we support have limited or no HTML5 support – CryoFusion87 Aug 27 '14 at 08:20
  • SVG is a wonderful but very complicated format. Not every renderer out there supports the same set of rules. Also, often SVG is used in conjuction with JavaScript which is why you need to interop with HTML. But just look around for [SVG to PNG with c#](http://stackoverflow.com/q/58910/231316). There are [online sources](http://en.wikipedia.org/wiki/Scalable_Vector_Graphics#Online_SVG_converters), too. – Chris Haas Aug 27 '14 at 13:06

1 Answers1

1

Using jQuery and https://github.com/vvvv/SVG I assume you're using MVC

Javascript:

$("#makePdf").click(function (e) {
    $.ajax({
        type: 'POST',
        url: '/GeneratePdf',
        dataType: 'json',
        data: JSON.stringify({ svgXml: $("<div />").append($("#chart svg").clone()).html() }),
        contentType: 'application/json;charset=utf-8',
        success: function (data) {
            window.open('/GetPdf?pdfId=' + data.pdfId, '_blank');
        }
    });
    e.preventDefault();
});

$("<div />").append($("#chart svg").clone()).html() is needed because there is no way to get the xml source of a current element, this is the most elegant way I could find to do it, append it to a new div and get the html of its contents.

C#:

using System;
using System.IO;
using System.Drawing.Imaging;
using iTextSharp.text;
using iTextSharp.text.pdf;
using Svg;

// ...

public static MemoryStream PngFromSvg(string svgXml)
{
    MemoryStream pngStream = new MemoryStream();
    if (!string.IsNullOrWhiteSpace(svgXml))
    {
        byte[] byteArray = Encoding.ASCII.GetBytes(svgXml);
        using (MemoryStream stream = new MemoryStream(byteArray))
        {
            SvgDocument svgDocument = SvgDocument.Open(stream);
            System.Drawing.Bitmap bitmap = svgDocument.Draw();
            bitmap.Save(pngStream, System.Drawing.Imaging.ImageFormat.Png);
        }
    }
}

public static MemoryStream GeneratePdf(MemoryStream pngStream)
{
    MemoryStream pdfStream = new MemoryStream();

    using (Document document = new Document(PageSize.A4, 20, 20, 20, 20))
    {
        PdfWriter pdfWriter = PdfWriter.GetInstance(document, pdfStream);

        // ...
        Image chartImage = Image.GetInstance(System.Drawing.Image.FromStream(pngStream), ImageFormat.Png);
        PdfPCell chartCell = new PdfPCell(chartImage, true);
        chartCell.Border = Rectangle.NO_BORDER;
        //...

        document.Close();
        pdfWriter.Close();
    }

    return pdfStream;
}

public class SvgData
{
    public string svgXml { get; set; }
}
public class PdfData
{
    public string pdfId { get; set; }
}

public JsonResult GeneratePdf(SvgData data)
{
    byte[] pdfBytes = GeneratePdf(PngFromSvg(data.svgXml)).ToArray();

    // ...
    // Either:
    //  :: Store PDF in database as blob and return unique ID
    //  :: Store as file and return path to file

    return Json(new PdfData{pdfId = pdf.id}, JsonRequestBehavior.AllowGet);
}

public ActionResult GetPdf(Int64 pdfId)
{   
    // retrieve pdf from database
    byte[] pdfBytes = pdf.raw;

    return File(pdfBytes, "application/pdf");
}

This code is awful, but should be self explanatory. This has given me a headache for the past 2 weeks, so I know your pain in trying to find a solution that "works". Sending the svg data to be parsed, in my opinion, is BAD! But I can think of no other way of doing it other than emulating a web browser's rendering engine!!

NB: Fonts will may render correctly when their styles are set in the style attribute, you shall need to parse these and set the fonts using the font- attributes e.g.

$chartSvg.find("text").each(function () {
    var $this = $(this);
    $this.attr("font-size", $this.css("font-size"));
    $this.css("font-size", "");

    $this.attr("font-style", $this.css("font-style"));
    $this.css("font-style", "");

    $this.attr("font-variant", $this.css("font-variant"));
    $this.css("font-variant", "");

    $this.attr("font-weight", $this.css("font-weight"));
    $this.css("font-weight", "");

    $this.attr("font-family", $this.css("font-family"));
    $this.css("font-family", "");
});

The quality of the png may be poor so what I did was scaled the svg to twice the size then sent it to the server:

var $chartSvg = $("#chart").find("svg");
var $chartSvgClone = $chartSvg.clone().attr("height", $chartSvg.height() * 2).attr("width", $chartSvg.width() * 2).css("transform", "scale(2)");
var $svgXml = $("<div />").append($chartSvgClone).html();
// ...

And another caveat, the svg library doesn't seem to set the font family correctly, so in your C# code pass your SvgDocument this:

public void SetFonts(SvgElement parent)
{
    try
    {
        SvgText svgText = (SvgText)parent;
        svgText.Font = new System.Drawing.Font("Arial", svgText.FontSize.Value);
    }
    catch
    {
    }

    if(parent.HasChildren())
    {
        foreach(SvgElement child in parent.Children)
        {
            SetFonts(child);
        }
    }
}
Mardoxx
  • 4,372
  • 7
  • 41
  • 67