/**
*
* 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();
}
}