分析:在FireFox下进行Dojo Chart绘图,绘图完成后,使用Firebug查看HTML结构,可以看到整个图形Graph有3个部分组成:图形的标题Chart Title(HTML DOM),图形本身Chart(SVG),图形的图标Legend(SVG),上述三个部分组成完整的图形。为了生成完整图形的PDF文件,有必要将三者整合进同一个SVG文件,然后将SVG发送到服务器进行转换。在Chrome下,图形也是由三个部分构成。在IE下,图形有2部分组成,Chart Title 和 Chart 属于同一部分。
思路:将 Chart Title, Chart, Legend 整合进一个 SVG 文件,将 SVG 文件发送到 Server, Server使用Apache Batik将 SVG 文件转换成 PDF 文件,转换完成后返回 PDF 文件。
步骤:
1. 使用Dojo Chart绘图(makeChart);
2. 整合Chart Title, Chart,Legend(integrateChart);
3. 将整合生成的SVG文件发送到服务器,服务器使用Apache Batik将SVG文件转换生成PDF文件(sendSVGToServer);
一、makeChart:
var makeChart = function () {
chart = new Chart("chartNode", {
title: "One Year Average Temperature", // Chart Title
titleGap: 25,
titleFont: "normal normal normal 15pt Arial",
titleFontColor: 'orange',
});
// Set the theme
chart.setTheme(Julie);
// Add the only/default plot
chart.addPlot("default", {
type: "Lines",
markers: true
});
// Add axes
chart.addAxis("x", {
fixLower: "major",
title: "Month",
titleOrientation: "away",
labelFunc: function(o) {
return o;
},
htmlLabels: false // Set this option, make sure x axis is ploted by SVG instead of HTML.
});
chart.addAxis("y", {
vertical: true,
title: 'Temperature',
titleOrientation: "away",
fixLower: "major",
fixUpper: "major",
minorTickStep: 1
});
// Add tooltip for chart
new Tooltip(chart, "default", {
text: function( o ) {
return 'Month: ' + o.x + '<br />' + 'Temperature: ' + o.y;
}
});
// Add series
chart.addSeries('Tokyo', [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]);
chart.addSeries('New York', [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, 20.1, 14.1, 8.6, 2.5]);
chart.addSeries('Berlin', [-0.9, 0.6, 3.5, 8.4, 13.5, 17.0, 18.6, 17.9, 14.3, 9.0, 3.9, 1.0]);
chart.addSeries('London', [3.9, 4.2, 5.7, 8.5, 11.9, 15.2, 17.0, 16.6, 14.2, 10.3, 6.6, 4.8]);
// Render the chart!
chart.render();
// Add legend for chart
legend = new Legend({ chart: chart, horizontal: false }, 'legendNode');
};
二、integrateChart;
var integrateChart = function() {
var copy, title, titleGroup, legendGroup, shapeContainer, legendSurfaces, lineX, lineY,
legendTextNodeList, index, jsonStr, obj, shape, x, y, path, newPath, labelText, dateGroup, d;
// make a copy of chart surface
copy = gfx.createSurface('gfxNode', 800, 400);
gfxUtils.fromJson(copy, gfxUtils.toJson(chart.surface));
// Title Group
if (query('div div', dom.byId('chartNode'))[0]) {
title = query('div div', dom.byId('chartNode'))[0].textContent;
titleGroup = copy.createGroup();
titleGroup.createText({ x: 400, y: 30, text: title, align: 'middle' })
.setFont({ family: 'Arial', size: '15pt', weight: 'bold'})
.setFill('orange');
}
// Legend Group
legendGroup = copy.createGroup();
shapeContainer = copy.createGroup();
legendSurfaces = legend._surfaces;
lineX = 600;
lineY = 20;
index = 0;
legendTextNodeList = query('.dojoxLegendText');
baseArray.forEach(legendSurfaces, function(surface) {
gfxUtils.forEach(surface, function(shapes) {
jsonStr = gfxUtils.toJson(shapes);
if (jsonStr.indexOf('[') !== 0) {
// Generate a common object corresponding to 'shapes',
// in case that member of shape cannot be accessed by shape.
obj = dojo.fromJson(jsonStr);
shape = gfxUtils.fromJson(shapeContainer, jsonStr);
// console.log('shape.type: ' + shape.shape.type);
if (shape.shape.type === 'line') {
shape.shape.x1 += lineX;
shape.shape.y1 += lineY;
shape.shape.x2 += lineX;
shape.shape.y2 += lineY;
legendGroup.createLine({ x1: shape.shape.x1, y1: shape.shape.y1, x2: shape.shape.x2, y2: shape.shape.y2 })
.setStroke({ color: obj.stroke.color, width: '3' });
}
else if (shape.shape.type === 'path') {
x = lineX + 9;
y = lineY + 9;
path = shape.shape.path;
newPath = path.replace(/^M\s\d+\s\d+m/g, 'M' + ' ' + x + ' ' + y + 'm');
legendGroup.createPath(newPath)
.setStroke({ color: obj.stroke.color, width: '3' })
.setFill(obj.stroke.color);
// Get label text
labelText = legendTextNodeList[index].childNodes[0].nodeValue;
legendGroup.createText({ x: lineX + 25 , y: lineY + 12, text: labelText, align: 'start' })
.setFont({ family: 'Arial', size: '10pt' })
.setFill('black');
lineY += 20;
}
shapeContainer.clear(true);
}
}); // gfxUtils.forEach
index += 1;
}); // baseArray.forEach
// Date Group
dateGroup = copy.createGroup();
d = new Date();
dateGroup.createText({ x: 600 , y: 385, text: d.toDateString(), align: 'start' })
.setFont({ family: 'Arial', size: '10pt' })
.setFill('black');
};
三、可以使用下列两种方式将svg文件发送到Server-- sendSVGToServer:
方式一:
var sendSVGToServer = function (svg, title) {
// Cancel last iframe post request
if (dfd) {
dfd.cancel();
}
// sanitize svg, if doesn't santize svg before post, cannot work in Chrome
svg = svg.replace(/ /g, ' ');
dfd = iframe.post('export.php', {
preventCache: true,
data: {
svg: svg,
filename: title || 'chart'
}
}).then(function(response) {
console.log('iframe get response');
},
function(error) {
if (error.message === 'Request canceled') {
// console.log('Last iframe post request is canceled');
return error;
} else {
console.error('sendSVGToServer()->iframe.post(): ' + error);
}
}
);
};
方式二:
var sendSVGToServer2 = function(svg, title) {
var formNode = domConstruct.create('form', {
name: 'svg_submit_form',
method: 'post',
action: 'export.php',
enctype: 'multipart/form-data'
}, win.body());
domStyle.set(formNode, 'display', 'none');
// sanitize svg, if doesn't santize svg before post, cannot work in Chrome
svg = svg.replace(/ /g, ' ');
var createInput = function(name, value){
domConstruct.create('input', {
type: 'hidden',
name: name,
value: value
}, formNode);
};
createInput('svg', svg);
createInput('filename', title || 'chart');
formNode.submit();
domConstruct.destroy(formNode);
}
四、服务器端export.php
<?php
if (isset($_POST['svg']) && !empty($_POST['svg'])) {
getPDF($_POST['svg']);
}
function getPDF($svg) {
$tempName = md5(rand());
$outfile = "temp/$tempName.pdf";
$filename = (string) $_POST['filename'];
if (!$filename) {
$filename = 'chart';
}
define ('BATIK_PATH', '/usr/local/batik/batik-rasterizer.jar');
// generate the temporary file
if (!file_put_contents("temp/$tempName.svg", $svg)) {
die("Couldn't create temporary file. Check that the directory permissions for
the /temp directory are set to 777.");
}
// do the conversion
$output = shell_exec("java -jar ". BATIK_PATH ." -m application/pdf -d $outfile temp/$tempName.svg");
// catch error
if (!is_file($outfile) || filesize($outfile) < 10) {
echo "<pre>$output</pre>";
echo "Error while converting SVG. ";
if (strpos($output, 'SVGConverter.error.while.rasterizing.file') !== false) {
echo "
<h4>Debug steps</h4>
<ol>
<li>Copy the SVG:<br/><textarea rows=5>" . htmlentities(str_replace('>', ">\n", $svg)) . "</textarea></li>
<li>Go to <a href='http://validator.w3.org/#validate_by_input' target='_blank'>validator.w3.org/#validate_by_input</a></li>
<li>Paste the SVG</li>
<li>Click More Options and select SVG 1.1 for Use Doctype</li>
<li>Click the Check button</li>
</ol>";
}
}
// stream it
else {
header("Pragma: public");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Content-Disposition: attachment; filename=\"$filename.pdf\"");
header("Content-Type: application/pdf");
?>
<html><body><textarea> <?php echo file_get_contents($outfile); ?> </textarea></body></html>
<?php
}
// delete it
unlink("temp/$tempName.svg");
unlink($outfile);
}
?>
五、Client 完整测试代码如下,代码仅仅把合并完的单一SVG显示出来,如果服务器端安装了Apache Batik(路径:/usr/local/batik )并部署export.php,只需将下列三行注释去掉就可以生成PDF文件:
Line: 116 // domStyle.set('gfxNode', 'display', 'none');
Line: 201 // doExport(copy, title);
Line: 202 // copy.clear();
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Demo: Export Chart To PDF</title>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/dojo/1.8/dijit/themes/claro/claro.css" media="screen">
</head>
<body class="claro">
<div id="container" style="width:800px;">
<div id="header" style="background-color:#FFA500;clear:both;text-align:center;">
<h1>Export Chart To PDF</h1>
</div>
<div id="chartNode" style="width:600px;height:400px;float:left;"></div>
<div id="legendNode" style="width:200px;height:400px;float:left;"></div>
<div id="gfxNode" style="width:800px;height:400px;float:left;"/>
<div id="buttonNode" style="height:25px;float:left;"/>
</div>
<!-- load dojo and provide config via data attribute -->
<script src="https://ajax.googleapis.com/ajax/libs/dojo/1.8/dojo/dojo.js" data-dojo-config="isDebug:true, async:true"></script>
<script>
// Require all dependencies
require([
"dijit/form/Button",
"dijit/registry",
"dojox/charting/Chart",
"dojox/charting/themes/Julie",
"dojox/charting/action2d/Tooltip",
"dojox/charting/widget/Legend",
"dojox/charting/plot2d/Lines",
"dojox/charting/axis2d/Default",
"dojo/request/iframe",
"dojox/gfx",
"dojox/gfx/utils",
"dojo/_base/array",
"dojo/dom",
"dojo/dom-attr",
"dojo/dom-style",
"dojo/dom-construct",
"dojo/query",
"dojo/domReady!"
], function(Button, registry, Chart, Julie, Tooltip, Legend, Lines, Default, iframe, gfx, gfxUtils,
baseArray, dom, domAttr, domStyle, domConstruct, query) {
var chart, legend, dfd;
var makeChart = function () {
chart = new Chart('chartNode', {
title: 'One Year Average Temperature',
titleGap: 25,
titleFont: 'normal normal normal 15pt Arial',
titleFontColor: 'orange',
});
// Set the theme
chart.setTheme(Julie);
// Add the only/default plot
chart.addPlot('default', {
type: 'Lines',
markers: true
});
// Add axes
chart.addAxis('x', {
fixLower: 'major',
title: 'Month',
titleOrientation: 'away',
labelFunc: function(o) {
return o;
},
htmlLabels: false
});
chart.addAxis('y', {
vertical: true,
title: 'Temperature',
titleOrientation: 'away',
fixLower: 'major',
fixUpper: 'major',
minorTickStep: 1
});
// Add tooltip for chart
new Tooltip(chart, 'default', {
text: function( o ) {
return 'Month: ' + o.x + '<br />' + 'Temperature: ' + o.y;
}
});
// Add series
chart.addSeries('Tokyo', [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]);
chart.addSeries('New York', [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, 20.1, 14.1, 8.6, 2.5]);
chart.addSeries('Berlin', [-0.9, 0.6, 3.5, 8.4, 13.5, 17.0, 18.6, 17.9, 14.3, 9.0, 3.9, 1.0]);
chart.addSeries('London', [3.9, 4.2, 5.7, 8.5, 11.9, 15.2, 17.0, 16.6, 14.2, 10.3, 6.6, 4.8]);
// Render the chart!
chart.render();
// Add legend for chart
legend = new Legend({ chart: chart, horizontal: false }, 'legendNode');
};
var integrateChart = function() {
var copy, title, titleGroup, legendGroup, shapeContainer, legendSurfaces, lineX, lineY,
legendTextNodeList, index, jsonStr, obj, shape, x, y, path, newPath, labelText, dateGroup, d;
// domStyle.set('gfxNode', 'display', 'none');
// make a copy of chart surface
copy = gfx.createSurface('gfxNode', 800, 400);
gfxUtils.fromJson(copy, gfxUtils.toJson(chart.surface));
// Title Group
if (query('div div', dom.byId('chartNode'))[0]) {
title = query('div div', dom.byId('chartNode'))[0].textContent;
titleGroup = copy.createGroup();
titleGroup.createText({ x: 400, y: 30, text: title, align: 'middle' })
.setFont({ family: 'Arial', size: '15pt', weight: 'bold'})
.setFill('orange');
}
// Legend Group
legendGroup = copy.createGroup();
shapeContainer = copy.createGroup();
legendSurfaces = legend._surfaces;
lineX = 600;
// lineY = 200 - legendSurfaces.length * 20;
lineY = 0;
index = 0;
legendTextNodeList = query('.dojoxLegendText');
baseArray.forEach(legendSurfaces, function(surface) {
gfxUtils.forEach(surface, function(shapes) {
jsonStr = gfxUtils.toJson(shapes);
// console.log('jsonStr: ' + jsonStr);
if (jsonStr.indexOf('[') !== 0) {
// Generate a common object corresponding to 'shapes',
// in case that member of shape cannot be accessed by shape.
obj = dojo.fromJson(jsonStr);
shape = gfxUtils.fromJson(shapeContainer, jsonStr);
// console.log('shape.type: ' + shape.shape.type);
if (shape.shape.type === 'line') {
shape.shape.x1 += lineX;
shape.shape.y1 += lineY;
shape.shape.x2 += lineX;
shape.shape.y2 += lineY;
legendGroup.createLine({ x1: shape.shape.x1, y1: shape.shape.y1, x2: shape.shape.x2, y2: shape.shape.y2 })
.setStroke({ color: obj.stroke.color, width: '3' });
}
else if (shape.shape.type === 'path') {
x = lineX + 9;
y = lineY + 9;
path = shape.shape.path;
newPath = path.replace(/^M\s\d+\s\d+m/g, 'M' + ' ' + x + ' ' + y + 'm');
legendGroup.createPath(newPath)
.setStroke({ color: obj.stroke.color, width: '3' })
.setFill(obj.stroke.color);
// Get label text
labelText = legendTextNodeList[index].childNodes[0].nodeValue;
console.log('labelText: ' + labelText);
legendGroup.createText({ x: lineX + 25 , y: lineY + 12, text: labelText, align: 'start' })
.setFont({ family: 'Arial', size: '10pt' })
.setFill('black');
lineY += 20;
}
shapeContainer.clear(true);
}
}); // gfxUtils.forEach
index += 1;
}); // baseArray.forEach
// Date Group
dateGroup = copy.createGroup();
d = new Date();
dateGroup.createText({ x: 600 , y: 385, text: d.toDateString(), align: 'start' })
.setFont({ family: 'Arial', size: '10pt' })
.setFill('black');
// doExport(copy, title);
// copy.clear();
};
var doExport = function(surface, title) {
gfxUtils.toSvg(surface).then(
function(svg) {
sendSVGToServer(svg, title);
},
function(err) {
console.err('gfxUtils.toSvg: ' + err);
}
);
};
var sendSVGToServer = function(svg, title) {
// Cancel last iframe post request
if (dfd) {
dfd.cancel();
}
// sanitize svg, if doesn't santize svg before post, cannot work in Chrome
svg = svg.replace(/ /g, ' ');
dfd = iframe.post('export.php', {
preventCache: true,
data: {
svg: svg,
filename: title || 'chart'
}
}).then(function(response) {
console.log('sendSVGToServer()->iframe.post(): get response');
},
function(error) {
if (error.message === 'Request canceled') {
// console.log('iframe post request is canceled');
return error;
} else {
console.error('sendSVGToServer()->iframe.post(): ' + error);
}
}
);
};
var makeButton = function() {
var button = new Button({
label: 'Export to PDF',
onClick: function() {
integrateChart()
}
}, 'buttonNode');
button.startup();
}
makeChart();
makeButton();
});
</script>
</body>
</html>