/** * * FusionCharts Exporter is an ASP.NET C# script that handles * FusionCharts (since v3.1) Server Side Export feature. * This in conjuncture with various export classes would * process FusionCharts Export Data POSTED to it from FusionCharts * and convert the data to image or PDF and subsequently save to the * server or response back as http response to client side as download. * * This script might be called as the FusionCharts Exporter - main module * * @author FusionCharts * @description FusionCharts Exporter (Server-Side - ASP.NET C#) * @version 2.0 [ 22 February 2009 ] * */ /** * ChangeLog / Version History: * ---------------------------- * * 2.0 [ 12 February 2009 ] * - Integrated with new Export feature of FusionCharts 3.1 * - can save to server side directory * - can provide download or open in popup window. * - can report back to chart * - can save as PDF/JPG/PNG/GIF * * 1.0 [ 16 August 2007 ] * - can process chart data to jpg image and response back to client side as download. * */ /** * Copyright (c) 2009 InfoSoft Global Private Limited. All Rights Reserved * */ /** * GENERAL NOTES * ------------- * * Chart would POST export data (which consists of encoded image data stream, * width, height, background color and various other export parameters like * exportFormat, exportFileName, exportAction, exportTargetWindow) to this script. * * The script would process this data using appropriate resource classes & build * export binary (PDF/image) * * It either saves the binary as file to a server side directory or push it as * Download to client side. * * * ISSUES * ------ * Q. What if someone wishes to open in the same page where the chart existed as postback * replacing the old page? * * A. Not directly supported using any chart attribute or parameter but can do by * removing/commenting the line containing 'header( content-disposition ...' * */ /** * * @requires FCIMGGenerator Class to export FusionCharts image data to JPG, PNG, GIF binary * @requires FCPDFGenerator Class to export FusionCharts image data to PDF binary * */ using System; using System.IO; using System.Web; using System.Drawing; using System.Collections; using System.Drawing.Imaging; using System.Text.RegularExpressions; /// /// FusionCharts Exporter is an ASP.NET C# script that handles /// FusionCharts (since v3.1) Server Side Export feature. /// This in conjuncture with other resource classses would /// process FusionCharts Export Data POSTED to it from FusionCharts /// and convert the data to an image or a PDF. Subsequently, it would save /// to the server or respond back as an HTTP response to client side as download. /// /// This script might be called as the FusionCharts Exporter - main module /// /// public partial class FCExporter : System.Web.UI.Page { /// /// IMPORTANT: You need to change the location of folder where /// the exported chart images/PDFs will be saved on your /// server. Please specify the path to a folder with /// write permissions in the constant SAVE_PATH below. /// /// Please provide the path as per ASP.NET path conventions. /// You can use relative or absolute path. /// /// Special Cases: /// '/' means 'wwwroot' directory. /// '. /' ( without the space after .) is the directory where the FCExporter.aspx file recides. /// /// Absolute Path : /// /// can be like this : "C:\\myFolders\\myImages" /// ( Please never use single backslash as that would stop execution of the code instantly) /// or "C:/myFolders/myImages" /// /// You may have a // or \ at end : "C:\\myFolders\\myImages\\" or "C:/myFolders/myImages/" /// /// You can also have mixed slashes : "C:\\myFolders/myImages" /// /// /// /// directory where the FCExporter.aspx file recides private const string SAVE_PATH = "./"; /// /// IMPORTANT: This constant HTTP_URI stores the HTTP reference to /// the folder where exported charts will be saved. /// Please enter the HTTP representation of that folder /// in this constant e.g., http://www.yourdomain.com/images/ /// private const string HTTP_URI = "http://www.yourdomain.com/images/"; /// /// OVERWRITEFILE sets whether the export handler would overwrite an existing file /// the newly created exported file. If it is set to false the export handler would /// not overwrite. In this case, if INTELLIGENTFILENAMING is set to true the handler /// would add a suffix to the new file name. The suffix is a randomly generated GUID. /// Additionally, you add a timestamp or a random number as additional suffix. /// private bool OVERWRITEFILE = false; private bool INTELLIGENTFILENAMING = true; private string FILESUFFIXFORMAT = "TIMESTAMP";// // value can be either 'TIMESTAMP' or 'RANDOM' /// /// This is a constant list of the MIME types related to each export format this resource handles /// The value is semicolon separated key value pair for each format /// Each key is the format and value is the MIME type /// private const string MIMETYPES = "pdf=application/pdf;jpg=image/jpeg;jpeg=image/jpeg;gif=image/gif;png=image/png"; /// /// This is a constant list of all the file extensions for the export formats /// The value is semicolon separated key value pair for each format /// Each key is the format and value is the file extension /// private const string EXTENSIONS = "pdf=pdf;jpg=jpg;jpeg=jpg;gif=gif;png=png"; /// /// Lists the default exportParameter values taken, if not provided by chart /// private const string DEFAULTPARAMS = "exportfilename=FusionCharts;exportformat=PDF;exportaction=download;exporttargetwindow=_self"; /// /// Stores server notices, if any as string [ to be sent back to chart after save ] /// private string notices = ""; /// /// Whether the export action is download. Default value. Would change as per setting retrieved from chart. /// private bool isDownload = true; /// /// DOMId of the chart /// private string DOMId; /// /// The main function that handles all Input - Process - Output of this Export Architecture /// /// FusionCharts chart SWF /// protected void Page_Load(object sender, EventArgs e) { /** * Retrieve export data from POST Request sent by chart * Parse the Request stream into export data readable by this script */ Hashtable exportData = parseExportRequestStream(); // process export data and get the processed data (image/PDF) to be exported MemoryStream exportObject = exportProcessor(((Hashtable)exportData["parameters"])["exportformat"].ToString(), exportData["stream"].ToString(), (Hashtable)exportData["meta"]); /* * Send the export binary to output module which would either save to a server directory * or send the export file to download. Download terminates the process while * after save the output module sends back export status */ object exportedStatus = outputExportObject(exportObject, (Hashtable)exportData["parameters"]); // Dispose export object exportObject.Close(); exportObject.Dispose(); /* * Build Appropriate Export Status and send back to chart by flushing the * procesed status to http response. This returns status back to chart. * [ This is not applicable when Download action took place ] */ flushStatus(exportedStatus, (Hashtable)exportData["meta"]); } /// /// Parses POST stream from chart and builds a Hashtable containing /// export data and parameters in a format readable by other functions. /// The Hashtable contains keys 'stream' (contains encoded /// image data) ; 'meta' ( Hashtable with 'width', 'height' and 'bgColor' keys) ; /// and 'parameters' ( Hashtable of all export parameters from chart as keys, like - exportFormat, /// exportFileName, exportAction etc.) /// /// Hashtable of processed export data and parameters. private Hashtable parseExportRequestStream() { // store all export data Hashtable exportData = new Hashtable(); //String of compressed image data exportData["stream"] = Request["stream"]; //Halt execution if image stream is not provided. if (Request["stream"] == null || Request["stream"].Trim() == "") raise_error("100", true); //Get all export parameters into a Hastable Hashtable parameters = parseParams(Request["parameters"]); exportData["parameters"] = parameters; //get width and height of the chart Hashtable meta = new Hashtable(); meta["width"] = Request["meta_width"]; //Halt execution on error if (Request["meta_width"] == null || Request["meta_width"].Trim() == "") raise_error("101", true); meta["height"] = Request["meta_height"]; //Halt execution on error if (Request["meta_height"] == null || Request["meta_height"].Trim() == "") raise_error("101", true); //Background color of chart meta["bgcolor"] = Request["meta_bgColor"]; if (Request["meta_bgColor"] == null || Request["meta_bgColor"].Trim() == "") { // Send notice if BgColor is not provided raise_error(" Background color not specified. Taking White (FFFFFF) as default background color."); // Set White as Default Background color meta["bgcolor"] = meta["bgcolor"].ToString().Trim() == "" ? "FFFFFF" : meta["bgcolor"]; } // DOMId of the chart meta["DOMId"] = Request["meta_DOMId"] == null ? "" : Request["meta_DOMId"]; DOMId = meta["DOMId"].ToString(); exportData["meta"] = meta; return exportData; } /// /// Parse export 'parameters' string into a Hashtable /// Also synchronise default values from defaultparameterValues Hashtable /// /// A string with parameters (key=value pairs) separated by | (pipe) /// Hashtable containing parsed key = value pairs. private Hashtable parseParams(string strParams) { //default parameter values Hashtable defaultParameterValues = bang(DEFAULTPARAMS); // get parameters Hashtable parameters = bang(strParams, new char[] { '|', '=' }); // sync with default values // iterate through each default parameter value foreach (DictionaryEntry param in defaultParameterValues) { // if a parameter from the defaultParameterValues Hashtable is not present // in the parameters hashtable take the parameter and value from default // parameter hashtable and add it to params hashtable // This is needed to ensure proper export if (parameters[param.Key] == null) parameters[param.Key] = param.Value.ToString(); } // set a global flag which denotes whether the export is download or not // this is needed in many a functions isDownload = parameters["exportaction"].ToString().ToLower() == "download"; // return parameters return parameters; } /// /// Get Export data from and build the export binary/objct. /// /// (string) Export format /// (string) Export image data in FusionCharts compressed format /// {Hastable)Image meta data in keys "width", "heigth" and "bgColor" /// private MemoryStream exportProcessor(string strFormat, string stream, Hashtable meta) { strFormat = strFormat.ToLower(); // initilize memeory stream object to store output bytes MemoryStream exportObjectStream = new MemoryStream(); // Handle Export class as per export format switch (strFormat) { case "pdf": // Instantiate Export class for PDF, build Binary stream and store in stream object FCPDFGenerator PDFGEN = new FCPDFGenerator(stream, meta["width"].ToString(), meta["height"].ToString(), meta["bgcolor"].ToString()); exportObjectStream = PDFGEN.getBinaryStream(strFormat); break; case "jpg": case "jpeg": case "png": case "gif": // Instantiate Export class for Images, build Binary stream and store in stream object FCIMGGenerator IMGGEN = new FCIMGGenerator(stream, meta["width"].ToString(), meta["height"].ToString(), meta["bgcolor"].ToString()); exportObjectStream = IMGGEN.getBinaryStream(strFormat); break; default: // In case the format is not recognized raise_error(" Invalid Export Format.", true); break; } return exportObjectStream; } /// /// Checks whether the export action is download or save. /// If action is 'download', send export parameters to 'setupDownload' function. /// If action is not-'download', send export parameters to 'setupServer' function. /// In either case it gets exportSettings and passes the settings along with /// processed export binary (image/PDF) to the output handler function if the /// export settings return a 'ready' flag set to 'true' or 'download'. The export /// process would stop here if the action is 'download'. In the other case, /// it gets back success status from output handler function and returns it. /// /// Export binary/object in memery stream /// Hashtable of export parameters /// Export success status ( filename if success, false if not) private object outputExportObject(MemoryStream exportObj, Hashtable exportParams) { //pass export paramters and get back export settings as per export action Hashtable exportActionSettings = (isDownload ? setupDownload(exportParams) : setupServer(exportParams)); // set default export status to true bool status = true; // filepath returned by server setup would be a string containing the file path // where the export file is to be saved. // If filepath is a boolean (i.e. false) the server setup must have failed. Hence, terminate process. if (exportActionSettings["filepath"] is bool) { status = false; raise_error(" Failed to export.", true); } else { // When 'filepath' is a sting write the binary to output stream try { // Write export binary stream to output stream Stream outStream = (Stream)exportActionSettings["outStream"]; exportObj.WriteTo(outStream); outStream.Flush(); outStream.Close(); exportObj.Close(); } catch (ArgumentNullException e) { raise_error(" Failed to export. Error:" + e.Message); status = false; } catch (ObjectDisposedException e) { raise_error(" Failed to export. Error:" + e.Message); status = false; } if (isDownload) { // If 'download'- terminate imediately // As nothing is to be written to response now. Response.End(); } } // This is the response after save action // If status remains true return the 'filepath'. Otherwise return false to denote failure. return (status ? exportActionSettings["filepath"] : false); } /// /// Flushes exported status message/or any status message to the chart or the output stream. /// It parses the exported status through parser function parseExportedStatus, /// builds proper response string using buildResponse function and flushes the response /// string to the output stream and terminates the program. /// /// Name of the exported file or false on failure /// Image's meta data /// Additional messages private void flushStatus(object filename, Hashtable meta, string msg) { // Process and flush message to response stream and terminate Response.Output.Write(buildResponse(parseExportedStatus(filename, meta, msg))); Response.Flush(); Response.End(); } /// /// Flushes exported status message/or any status message to the chart or the output stream. /// It parses the exported status through parser function parseExportedStatus, /// builds proper response string using buildResponse function and flushes the response /// string to the output stream and terminates the program. /// /// Name of the exported file or false on failure /// Image's meta data /// private void flushStatus(object filename, Hashtable meta) { flushStatus(filename, meta, ""); } /// /// Parses the exported status and builds an array of export status information. As per /// status it builds a status array which contains statusCode (0/1), statusMesage, fileName, /// width, height and notice in some cases. /// /// exported status ( false if failed/error, filename as stirng if success) /// Hastable containing meta descriptions of the chart like width, height /// custom message to be added as statusMessage. /// private ArrayList parseExportedStatus(object filename, Hashtable meta, string msg) { ArrayList arrStatus = new ArrayList(); // get status bool status = (filename is string ? true : false); // add notices if (notices.Trim() != "") arrStatus.Add("notice=" + notices.Trim()); // DOMId of the chart arrStatus.Add("DOMId=" + (meta["DOMId"]==null? DOMId : meta["DOMId"].ToString())); // add width and height // provide 0 as width and height on failure if (meta["width"] == null) meta["width"] = "0"; if (meta["height"] == null) meta["height"] = "0"; arrStatus.Add("height=" + (status ? meta["height"].ToString() : "0")); arrStatus.Add("width=" + (status ? meta["width"].ToString() : "0")); // add file URI arrStatus.Add("fileName=" + (status ? (Regex.Replace(HTTP_URI, @"([^\/]$)", "${1}/") + filename) : "")); arrStatus.Add("statusMessage=" + (msg.Trim() != "" ? msg.Trim() : (status ? "Success" : "Failure"))); arrStatus.Add("statusCode=" + (status ? "1" : "0")); return arrStatus; } /// /// Builds response from an array of status information. Joins the array to a string. /// Each array element should be a string which is a key=value pair. This array are either joined by /// a & to build a querystring (to pass to chart) or joined by a HTML
to show neat /// and clean status informaton in Browser window if download fails at the processing stage. ///
/// Array of string containing status data as [key=value ] /// A string to be written to output stream private string buildResponse(ArrayList arrMsg) { // Join export status array elements into querystring key-value pairs in case of 'save' action // or separate with
in case of 'download' action. This would make the imformation readable in browser window. string msg = isDownload ? "" : "&"; msg += string.Join((isDownload ? "
" : "&"), (string[])arrMsg.ToArray(typeof(string))); return msg; } /// /// Finds if a directory is writable /// /// String Path /// private bool isDirectoryWritable(string path) { DirectoryInfo info = new DirectoryInfo(path); return (info.Attributes & FileAttributes.ReadOnly) != FileAttributes.ReadOnly; } /// /// check server permissions and settings and return ready flag to exportSettings /// /// Various export parameters /// Hashtable containing various export settings private Hashtable setupServer(Hashtable exportParams) { //get export file name string exportFile = exportParams["exportfilename"].ToString(); // get extension related to specified type string ext = getExtension(exportParams["exportformat"].ToString()); Hashtable retServerStatus = new Hashtable(); //set server status to true by default retServerStatus["ready"] = true; // Open a FileStream to be used as outpur stream when the file would be saved FileStream fos; // process SAVE_PATH : the path where export file would be saved // add a / at the end of path if / is absent at the end string path = SAVE_PATH; // if path is null set it to folder where FCExporter.aspx is present if (path.Trim() == "") path = "./"; path = Regex.Replace(path, @"([^\/]$)", "${1}/"); try { // check if the path is relative if so assign the actual path to path path = HttpContext.Current.Server.MapPath(path); } catch (HttpException e) { //raise_error(e.Message); } // check whether directory exists // raise error and halt execution if directory does not exists if (!Directory.Exists(path)) raise_error(" Server Directory does not exist.", true); // check if directory is writable or not bool dirWritable = isDirectoryWritable(path); // build filepath retServerStatus["filepath"] = exportFile + "." + ext; // check whether file exists if (!File.Exists(path + retServerStatus["filepath"].ToString())) { // need to create a new file if does not exists // need to check whether the directory is writable to create a new file if (dirWritable) { // if directory is writable return with ready flag // open the output file in FileStream fos = File.Open(path + retServerStatus["filepath"].ToString(), FileMode.Create, FileAccess.Write); // set the output stream to the FileStream object retServerStatus["outStream"] = fos; return retServerStatus; } else { // if not writable halt and raise error raise_error("403", true); } } // add notice that file exists raise_error(" File already exists."); //if overwrite is on return with ready flag if (OVERWRITEFILE) { // add notice while trying to overwrite raise_error(" Export handler's Overwrite setting is on. Trying to overwrite."); // see whether the existing file is writable // if not halt raising error message if ((new FileInfo(path + retServerStatus["filepath"].ToString())).IsReadOnly) raise_error(" Overwrite forbidden. File cannot be overwritten.", true); // if writable return with ready flag // open the output file in FileStream // set the output stream to the FileStream object fos = File.Open(path + retServerStatus["filepath"].ToString(), FileMode.Create, FileAccess.Write); retServerStatus["outStream"] = fos; return retServerStatus; } // raise error and halt execution when overwrite is off and intelligent naming is off if (!INTELLIGENTFILENAMING) { raise_error(" Export handler's Overwrite setting is off. Cannot overwrite.", true); } raise_error(" Using intelligent naming of file by adding an unique suffix to the exising name."); // Intelligent naming // generate new filename with additional suffix exportFile = exportFile + "_" + generateIntelligentFileId(); retServerStatus["filepath"] = exportFile + "." + ext; // return intelligent file name with ready flag // need to check whether the directory is writable to create a new file if (dirWritable) { // if directory is writable return with ready flag // add new filename notice // open the output file in FileStream // set the output stream to the FileStream object raise_error(" The filename has changed to " + retServerStatus["filepath"].ToString() + "."); fos = File.Open(path + retServerStatus["filepath"].ToString(), FileMode.Create, FileAccess.Write); // set the output stream to the FileStream object retServerStatus["outStream"] = fos; return retServerStatus; } else { // if not writable halt and raise error raise_error("403", true); } // in any unknown case the export should not execute retServerStatus["ready"] = false; raise_error(" Not exported due to unknown reasons."); return retServerStatus; } /// /// setup download headers and return ready flag in exportSettings /// /// Various export parameters /// Hashtable containing various export settings private Hashtable setupDownload(Hashtable exportParams) { //get export filename string exportFile = exportParams["exportfilename"].ToString(); //get extension string ext = getExtension(exportParams["exportformat"].ToString()); //get mime type string mime = getMime(exportParams["exportformat"].ToString()); // get target window string target = exportParams["exporttargetwindow"].ToString().ToLower(); // set content-type header Response.ContentType = mime; // set content-disposition header // when target is _self the type is 'attachment' // when target is other than self type is 'inline' // NOTE : you can comment this line in order to replace present window (_self) content with the image/PDF Response.AddHeader("Content-Disposition", (target == "_self" ? "attachment" : "inline") + "; filename=\"" + exportFile + "." + ext + "\""); // return exportSetting array. 'Ready' key should be set to 'download' Hashtable retStatus = new Hashtable(); retStatus["filepath"] = ""; // set the output strem to Response stream as the file is going to be downloaded retStatus["outStream"] = Response.OutputStream; return retStatus; } /// /// gets file extension checking the export type. /// /// (string) export format /// string extension name private string getExtension(string exportType) { // get a Hashtable array of [type=> extension] // from EXTENSIONS constant Hashtable extensionList = bang(EXTENSIONS); exportType = exportType.ToLower(); // if extension type is present in $extensionList return it, otherwise return the type return (extensionList[exportType].ToString() != null ? extensionList[exportType].ToString() : exportType); } /// /// gets mime type for an export type /// /// Export format /// Mime type as stirng private string getMime(string exportType) { // get a Hashtable array of [type=> extension] // from MIMETYPES constant Hashtable mimelist = bang(MIMETYPES); string ext = getExtension(exportType); // get mime type asociated to extension string mime = mimelist[ext].ToString() != null ? mimelist[ext].ToString() : ""; return mime; } /// /// generates a file suffix for a existing file name to apply smart file naming /// /// a string containing GUID and random number /timestamp private string generateIntelligentFileId() { // Generate Guid string guid = System.Guid.NewGuid().ToString("D"); // check FILESUFFIXFORMAT type if (FILESUFFIXFORMAT.ToLower() == "timestamp") { // Add time stamp with file name guid += "_" + DateTime.Now.ToString("ddMMyyyyHHmmssff"); } else { // Add Random Number with fileName guid += "_" + (new Random()).Next().ToString(); } return guid; } /// /// Helper function that splits a string containing delimiter separated key value pairs /// into hashtable /// /// delimiter separated key value pairs /// List of delimiters /// private Hashtable bang(string str, char[] delimiterList) { Hashtable retArray = new Hashtable(); // split string as per first delimiter if (str == null || str.Trim() == "") return retArray; string[] tmpArray = str.Split(delimiterList[0]); // iterate through each element of split string for (int i = 0; i < tmpArray.Length; i++) { // split each element as per second delimiter string[] tmp2Array = tmpArray[i].Split(delimiterList[1]); if (tmp2Array.Length >= 2) { // if the secondary split creats at-least 2 array elements // make the fisrt element as the key and the second as the value // of the resulting array retArray[tmp2Array[0].ToLower()] = tmp2Array[1]; } } return retArray; } private Hashtable bang(string str) { return bang(str, new char[2] { ';', '=' }); } private void raise_error(string msg) { raise_error(msg, false); } /// /// Error reporter function that has a list of error messages. It can terminate the execution /// and send successStatus=0 along with a error message. It can also append notice to a global variable /// and continue execution of the program. /// /// error code as Integer (referring to the index of the errMessages /// array containing list of error messages) OR, it can be a string containing the error message/notice /// Whether to halt execution private void raise_error(string msg, bool halt) { Hashtable errMessages = new Hashtable(); //list of defined error messages errMessages["100"] = " Insufficient data."; errMessages["101"] = " Width/height not provided."; errMessages["102"] = " Insufficient export parameters."; errMessages["400"] = " Bad request."; errMessages["401"] = " Unauthorized access."; errMessages["403"] = " Directory write access forbidden."; errMessages["404"] = " Export Resource class not found."; // Find whether error message is passed in msg or it is a custom error string. string err_message = ((msg == null || msg.Trim() == "") ? "ERROR!" : (errMessages[msg] == null ? msg : errMessages[msg].ToString()) ); // Halt executon after flushing the error message to response (if halt is true) if (halt) { flushStatus(false, new Hashtable(), err_message); } // add error to notices global variable else { notices += err_message; } } } /// /// FusionCharts Image Generator Class /// FusionCharts Exporter - 'Image Resource' handles /// FusionCharts (since v3.1) Server Side Export feature that /// helps FusionCharts exported as Image files in various formats. /// public class FCIMGGenerator { //Array - Stores multiple chart export data private ArrayList arrExportData = new ArrayList(); //stores number of pages = length of $arrExportData array private int numPages = 0; /// /// Generates bitmap data for the image from a FusionCharts export format /// the height and width of the original export needs to be specified /// the default background color can also be specified /// public FCIMGGenerator(string imageData_FCFormat, string width, string height, string bgcolor) { setBitmapData(imageData_FCFormat, width, height, bgcolor); } /// /// Gets the binary data stream of the image /// The passed parameter determines the file format of the image /// to be exported /// public MemoryStream getBinaryStream(string strFormat) { // the image object Bitmap exportObj = getImageObject(); // initiates a new binary data sream MemoryStream outStream = new MemoryStream(); // determines the image format switch (strFormat) { case "jpg": case "jpeg": exportObj.Save(outStream, ImageFormat.Jpeg); break; case "png": exportObj.Save(outStream, ImageFormat.Png); break; case "gif": exportObj.Save(outStream,ImageFormat.Gif); break; case "tiff": exportObj.Save(outStream, ImageFormat.Tiff); break; default: exportObj.Save(outStream, ImageFormat.Bmp); break; } exportObj.Dispose(); return outStream; } /// /// Generates bitmap data for the image from a FusionCharts export format /// the height and width of the original export needs to be specified /// the default background color can also be specified /// private void setBitmapData(string imageData_FCFormat, string width, string height, string bgcolor) { Hashtable chartExportData = new Hashtable(); chartExportData["width"] = width; chartExportData["height"] = height; chartExportData["bgcolor"] = bgcolor; chartExportData["imagedata"] = imageData_FCFormat; arrExportData.Add(chartExportData); numPages++; } /// /// Generates bitmap data for the image from a FusionCharts export format /// the height and width of the original export needs to be specified /// the default background color should also be specified /// private Bitmap getImageObject(int id) { Hashtable rawImageData = (Hashtable)arrExportData[id]; // create blank bitmap object which would store image pixel data Bitmap image = new Bitmap(Convert.ToInt16(rawImageData["width"]), Convert.ToInt16(rawImageData["height"]), System.Drawing.Imaging.PixelFormat.Format24bppRgb); // drwaing surface Graphics gr = Graphics.FromImage(image); // set background color gr.Clear(ColorTranslator.FromHtml("#" + rawImageData["bgcolor"].ToString())); string[] rows = rawImageData["imagedata"].ToString().Split(';'); for (int yPixel = 0; yPixel < rows.Length; yPixel++) { //Split each row into 'color_count' columns. String[] color_count = rows[yPixel].Split(','); //Set horizontal row index to 0 int xPixel = 0; for (int col = 0; col < color_count.Length; col++) { //Now, if it's not empty, we process it //Split the 'color_count' into color and repeat factor String[] split_data = color_count[col].Split('_'); //Reference to color string hexColor = split_data[0]; //refer to repeat factor int fRepeat = int.Parse(split_data[1]); //If color is not empty (i.e. not background pixel) if (hexColor != "") { //If the hexadecimal code is less than 6 characters, pad with 0 hexColor = hexColor.Length < 6 ? hexColor.PadLeft(6, '0') : hexColor; for (int k = 1; k <= fRepeat; k++) { //draw pixel with specified color image.SetPixel(xPixel, yPixel, ColorTranslator.FromHtml("#" + hexColor)); //Increment horizontal row count xPixel++; } } else { //Just increment horizontal index xPixel += fRepeat; } } } gr.Dispose(); return image; } /// /// Retreives the bitmap image object /// private Bitmap getImageObject() { return getImageObject(0); } } /// /// FusionCharts Exporter - 'PDF Resource' handles /// FusionCharts (since v3.1) Server Side Export feature that /// helps FusionCharts exported as PDF file. /// public class FCPDFGenerator { //Array - Stores multiple chart export data private ArrayList arrExportData = new ArrayList(); //stores number of pages = length of $arrExportData array private int numPages = 0; /// /// Generates a PDF file with the given parameters /// The imageData_FCFormat parameter is the FusionCharts export format data /// width, height are the respective width and height of the original image /// bgcolor determines the default background colour /// public FCPDFGenerator(string imageData_FCFormat, string width, string height, string bgcolor) { setBitmapData(imageData_FCFormat, width, height, bgcolor); } /// /// Gets the binary data stream of the image /// The passed parameter determines the file format of the image /// to be exported /// public MemoryStream getBinaryStream(string strFormat) { byte[] exportObj = getPDFObjects(true); MemoryStream outStream = new MemoryStream(); outStream.Write(exportObj, 0, exportObj.Length); return outStream; } /// /// Generates bitmap data for the image from a FusionCharts export format /// the height and width of the original export needs to be specified /// the default background color should also be specified /// private void setBitmapData(string imageData_FCFormat, string width, string height, string bgcolor) { Hashtable chartExportData = new Hashtable(); chartExportData["width"] = width; chartExportData["height"] = height; chartExportData["bgcolor"] = bgcolor; chartExportData["imagedata"] = imageData_FCFormat; arrExportData.Add(chartExportData); numPages++; } //create image PDF object containing the chart image private byte[] addImageToPDF(int id, bool isCompressed) { MemoryStream imgObj = new MemoryStream(); //PDF Object number int imgObjNo = 6 + id * 3; //Get chart Image binary byte[] imgBinary = getBitmapData24(id, isCompressed); //get the length of the image binary int len = imgBinary.Length; string width = getMeta("width", id); string height = getMeta("height", id); //Build PDF object containing the image binary and other formats required //string strImgObjHead = imgObjNo.ToString() + " 0 obj\n<<\n/Subtype /Image /ColorSpace /DeviceRGB /BitsPerComponent 8 /HDPI 72 /VDPI 72 " + (isCompressed ? "" : "") + "/Width " + width + " /Height " + height + " /Length " + len.ToString() + " >>\nstream\n"; string strImgObjHead = imgObjNo.ToString() + " 0 obj\n<<\n/Subtype /Image /ColorSpace /DeviceRGB /BitsPerComponent 8 /HDPI 72 /VDPI 72 " + (isCompressed ? "/Filter /RunLengthDecode " : "") + "/Width " + width + " /Height " + height + " /Length " + len.ToString() + " >>\nstream\n"; imgObj.Write(stringToBytes(strImgObjHead), 0, strImgObjHead.Length); imgObj.Write(imgBinary, 0, (int)imgBinary.Length); string strImgObjEnd = "endstream\nendobj\n"; imgObj.Write(stringToBytes(strImgObjEnd), 0, strImgObjEnd.Length); imgObj.Close(); return imgObj.ToArray(); } private byte[] addImageToPDF(int id) { return addImageToPDF(id, true); } private byte[] addImageToPDF() { return addImageToPDF(0, true); } //Main PDF builder function private byte[] getPDFObjects() { return getPDFObjects(true); } private byte[] getPDFObjects(bool isCompressed) { MemoryStream PDFBytes = new MemoryStream(); //Store all PDF objects in this temporary string to be written to ByteArray string strTmpObj = ""; //start xref array ArrayList xRefList = new ArrayList(); xRefList.Add("xref\n0 "); xRefList.Add("0000000000 65535 f \n"); //Address Refenrece to obj 0 //Build PDF objects sequentially //version and header strTmpObj = "%PDF-1.3\n%{FC}\n"; PDFBytes.Write(stringToBytes(strTmpObj), 0, strTmpObj.Length); //OBJECT 1 : info (optional) strTmpObj = "1 0 obj<<\n/Author (FusionCharts)\n/Title (FusionCharts)\n/Creator (FusionCharts)\n>>\nendobj\n"; xRefList.Add(calculateXPos((int)PDFBytes.Length)); //refenrece to obj 1 PDFBytes.Write(stringToBytes(strTmpObj), 0, strTmpObj.Length); //OBJECT 2 : Starts with Pages Catalogue strTmpObj = "2 0 obj\n<< /Type /Catalog /Pages 3 0 R >>\nendobj\n"; xRefList.Add(calculateXPos((int)PDFBytes.Length)); //refenrece to obj 2 PDFBytes.Write(stringToBytes(strTmpObj), 0, strTmpObj.Length); //OBJECT 3 : Page Tree (reference to pages of the catalogue) strTmpObj = "3 0 obj\n<< /Type /Pages /Kids ["; for (int i = 0; i < numPages; i++) { strTmpObj += (((i + 1) * 3) + 1) + " 0 R\n"; } strTmpObj += "] /Count " + numPages + " >>\nendobj\n"; xRefList.Add(calculateXPos((int)PDFBytes.Length)); //refenrece to obj 3 PDFBytes.Write(stringToBytes(strTmpObj), 0, strTmpObj.Length); //Each image page for (int itr = 0; itr < numPages; itr++) { string iWidth = getMeta("width", itr); string iHeight = getMeta("height", itr); //OBJECT 4..7..10..n : Page config strTmpObj = (((itr + 2) * 3) - 2) + " 0 obj\n<<\n/Type /Page /Parent 3 0 R \n/MediaBox [ 0 0 " + iWidth + " " + iHeight + " ]\n/Resources <<\n/ProcSet [ /PDF ]\n/XObject <>\n>>\n/Contents [ " + (((itr + 2) * 3) - 1) + " 0 R ]\n>>\nendobj\n"; xRefList.Add(calculateXPos((int)PDFBytes.Length)); //refenrece to obj 4,7,10,13,16... PDFBytes.Write(stringToBytes(strTmpObj), 0, strTmpObj.Length); //OBJECT 5...8...11...n : Page resource object (xobject resource that transforms the image) xRefList.Add(calculateXPos((int)PDFBytes.Length)); //refenrece to obj 5,8,11,14,17... string xObjR = getXObjResource(itr); PDFBytes.Write(stringToBytes(xObjR), 0, xObjR.Length); //OBJECT 6...9...12...n : Binary xobject of the page (image) byte[] imgBA = addImageToPDF(itr, isCompressed); xRefList.Add(calculateXPos((int)PDFBytes.Length));//refenrece to obj 6,9,12,15,18... PDFBytes.Write(imgBA, 0, imgBA.Length); } //xrefs compilation xRefList[0] += ((xRefList.Count - 1) + "\n"); //get trailer string trailer = getTrailer((int)PDFBytes.Length, xRefList.Count - 1); //write xref and trailer to PDF string strXRefs = string.Join("", (string[])xRefList.ToArray(typeof(string))); PDFBytes.Write(stringToBytes(strXRefs), 0, strXRefs.Length); // PDFBytes.Write(stringToBytes(trailer), 0, trailer.Length); //write EOF string strEOF = "%%EOF\n"; PDFBytes.Write(stringToBytes(strEOF), 0, strEOF.Length); PDFBytes.Close(); return PDFBytes.ToArray(); } //Build Image resource object that transforms the image from First Quadrant system to Second Quadrant system private string getXObjResource() { return getXObjResource(0); } private string getXObjResource(int itr) { string width = getMeta("width", itr); string height = getMeta("height", itr); return (((itr + 2) * 3) - 1) + " 0 obj\n<< /Length " + (24 + (width + height).Length) + " >>\nstream\nq\n" + width + " 0 0 " + height + " 0 0 cm\n/R" + (itr + 1) + " Do\nQ\nendstream\nendobj\n"; } private string calculateXPos(int posn) { return posn.ToString().PadLeft(10, '0') + " 00000 n \n"; } private string getTrailer(int xrefpos) { return getTrailer(xrefpos, 7); } private string getTrailer(int xrefpos, int numxref) { return "trailer\n<<\n/Size " + numxref.ToString() + "\n/Root 2 0 R\n/Info 1 0 R\n>>\nstartxref\n" + xrefpos.ToString() + "\n"; } private byte[] getBitmapData24() { return getBitmapData24(0, true); } private byte[] getBitmapData24(int id, bool isCompressed) { string rawImageData = getMeta("imagedata", id); string bgColor = getMeta("bgcolor", id); MemoryStream imageData24 = new MemoryStream(); // Split the data into rows using ; as separator string[] rows = rawImageData.Split(';'); for (int yPixel = 0; yPixel < rows.Length; yPixel++) { //Split each row into 'color_count' columns. string[] color_count = rows[yPixel].Split(','); for (int col = 0; col < color_count.Length; col++) { //Now, if it's not empty, we process it //Split the 'color_count' into color and repeat factor string[] split_data = color_count[col].Split('_'); //Reference to color string hexColor = split_data[0] != "" ? split_data[0] : bgColor; //If the hexadecimal code is less than 6 characters, pad with 0 hexColor = hexColor.Length < 6 ? hexColor.PadLeft(6, '0') : hexColor; //refer to repeat factor int fRepeat = int.Parse(split_data[1]); // convert color string to byte[] array byte[] rgb = hexToBytes(hexColor); // Set the repeated pixel in MemoryStream for (int cRepeat = 0; cRepeat < fRepeat; cRepeat++) { imageData24.Write(rgb, 0, 3); } } } int len = (int)imageData24.Length; imageData24.Close(); //Compress image binary if (isCompressed) { return new PDFCompress(imageData24.ToArray()).RLECompress(); } else { return imageData24.ToArray(); } } // converts a hexadecimal colour string to it's respective byte value private byte[] hexToBytes(string strHex) { if (strHex == null || strHex.Trim().Length == 0) strHex = "00"; strHex = Regex.Replace(strHex, @"[^0-9a-fA-f]", ""); if (strHex.Length % 2 != 0) strHex = "0" + strHex; int len = strHex.Length / 2; byte[] bytes = new byte[len]; for (int i = 0; i < len; i++) { string hex = strHex.Substring(i * 2, 2); bytes[i] = byte.Parse(hex, System.Globalization.NumberStyles.HexNumber); } return bytes; } private string getMeta(string metaName) { return getMeta(metaName, 0); } private string getMeta(string metaName, int id) { if (metaName == null) metaName = ""; Hashtable chartData = (Hashtable)arrExportData[id]; return (chartData[metaName] == null ? "" : chartData[metaName].ToString()); } private byte[] stringToBytes(string str) { if (str == null) str = ""; return System.Text.Encoding.ASCII.GetBytes(str); } } /// /// This is an ad-hoc class to compress PDF stream. /// Currently this class compresses binary (byte) stream using RLE which /// PDF 1.3 specification has thus formulated: /// /// The RunLengthDecode filter decodes data that has been encoded in a simple /// byte-oriented format based on run length. The encoded data is a sequence of /// runs, where each run consists of a length byte followed by 1 to 128 bytes of data. If /// the length byte is in the range 0 to 127, the following length + 1 (1 to 128) bytes /// are copied literally during decompression. If length is in the range 129 to 255, the /// following single byte is to be copied 257 − length (2 to 128) times during decompression. /// A length value of 128 denotes EOD. /// /// The chart image compression ratio comes to around 10:3 /// /// public class PDFCompress { /// /// stores the output compressed data in MemoryStream object later to be converted to byte[] array /// private MemoryStream _Compressed = new MemoryStream(); /// /// Uncompresses data as byte[] array /// private byte[] _UnCompressed; /// /// Takes the uncompressed byte array /// /// uncompressed data public PDFCompress(byte[] UnCompressed) { _UnCompressed = UnCompressed; } /// /// Write compressed data as RunLength /// /// The length of repeated data /// The byte to be repeated /// private int WriteRunLength(int length, byte encodee) { // write the repeat length _Compressed.WriteByte((byte)(257 - length)); // write the byte to be repeated _Compressed.WriteByte(encodee); //re-set repeat length length = 1; return length; } private void WriteNoRepeater(MemoryStream NoRepeatBytes) { // write the length of non repeted data _Compressed.WriteByte((byte)((int)NoRepeatBytes.Length - 1)); // write the non repeated data put literally _Compressed.Write(NoRepeatBytes.ToArray(), 0, (int)NoRepeatBytes.Length); // re-set non repeat byte storage stream NoRepeatBytes.SetLength(0); } /// /// compresses uncompressed data to compressed data in byte array /// /// public byte[] RLECompress() { // stores non repeatable data MemoryStream NoRepeat = new MemoryStream(); // repeat counter int _RL = 1; // 2 consecutive bytes to compare byte preByte = 0, postByte = 0; // iterate through the uncompressed bytes for (int i = 0; i < _UnCompressed.Length - 1; i++) { // get 2 consecutive bytes preByte = _UnCompressed[i]; postByte = _UnCompressed[i + 1]; // if both are same there is scope for repitition if (preByte == postByte) { // but flush the non repeatable data (if present) to compressed stream if (NoRepeat.Length > 0) WriteNoRepeater(NoRepeat); // increase repeat count _RL++; // if repeat count reaches limit of repeat i.e. 128 // write the repeat data and reset the repeat counter if (_RL > 128) _RL = WriteRunLength(_RL-1,preByte); } else { // when consecutive bytes do not match // store non-repeatable data if (_RL == 1) NoRepeat.WriteByte(preByte); // write repeated length and byte (if present ) to output stream if (_RL > 1) _RL = WriteRunLength(_RL, preByte); // write non repeatable data to out put stream if the length reaches limit if (NoRepeat.Length == 128) WriteNoRepeater(NoRepeat); } } // at the end of iteration // take care of the last byte // if repeated if (_RL > 1) { // write run length and byte (if present ) to output stream _RL = WriteRunLength(_RL, preByte); } else { // if non repeated byte is left behind // write non repeatable data to output stream NoRepeat.WriteByte(postByte); WriteNoRepeater(NoRepeat); } // wrote EOD _Compressed.WriteByte((byte)128); //close streams NoRepeat.Close(); _Compressed.Close(); // return compressed data in byte array return _Compressed.ToArray(); } }