Skip to content

Commit fd04654

Browse files
author
Ke, Mingze
committed
Added download image feature
1 parent 8eecf1b commit fd04654

5 files changed

Lines changed: 289 additions & 0 deletions

File tree

code.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,11 @@ Code.debug = function () {
786786
console.log.apply(console, [space].concat(Array.prototype.slice.apply(arguments)));
787787
};
788788

789+
Code.exportImage = function() {
790+
Code.workspace.zoomReset(document.createEvent('MouseEvents'));
791+
saveSvgAsPng(Code.workspace.getCanvas(), 'webduino-blocks.png');
792+
};
793+
789794
Blockly.JavaScript.blockToCode = function (block) {
790795
if (!block) {
791796
return '';
@@ -1017,6 +1022,79 @@ Blockly.Css.inject = function (hasCss, pathToMedia) {
10171022
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
10181023
};
10191024

1025+
Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
1026+
if (this.options.readOnly) {
1027+
return;
1028+
}
1029+
var menuOptions = [];
1030+
var topBlocks = this.getTopBlocks(true);
1031+
// Option to clean up blocks.
1032+
var cleanOption = {};
1033+
cleanOption.text = MSG.cleanUpBlocks;
1034+
cleanOption.enabled = topBlocks.length > 1;
1035+
cleanOption.callback = this.cleanUp_.bind(this);
1036+
menuOptions.push(cleanOption);
1037+
1038+
var imgOption = {};
1039+
imgOption.text = MSG.exportImage;
1040+
imgOption.enabled = topBlocks.length > 0;
1041+
imgOption.callback = Code.exportImage.bind(Code);
1042+
menuOptions.push(imgOption);
1043+
1044+
// Add a little animation to collapsing and expanding.
1045+
var COLLAPSE_DELAY = 10;
1046+
if (this.options.collapse) {
1047+
var hasCollapsedBlocks = false;
1048+
var hasExpandedBlocks = false;
1049+
for (var i = 0; i < topBlocks.length; i++) {
1050+
var block = topBlocks[i];
1051+
while (block) {
1052+
if (block.isCollapsed()) {
1053+
hasCollapsedBlocks = true;
1054+
} else {
1055+
hasExpandedBlocks = true;
1056+
}
1057+
block = block.getNextBlock();
1058+
}
1059+
}
1060+
1061+
/*
1062+
* Option to collapse or expand top blocks
1063+
* @param {boolean} shouldCollapse Whether a block should collapse.
1064+
* @private
1065+
*/
1066+
var toggleOption = function(shouldCollapse) {
1067+
var ms = 0;
1068+
for (var i = 0; i < topBlocks.length; i++) {
1069+
var block = topBlocks[i];
1070+
while (block) {
1071+
setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
1072+
block = block.getNextBlock();
1073+
ms += COLLAPSE_DELAY;
1074+
}
1075+
}
1076+
};
1077+
1078+
// Option to collapse top blocks.
1079+
var collapseOption = {enabled: hasExpandedBlocks};
1080+
collapseOption.text = Blockly.Msg.COLLAPSE_ALL;
1081+
collapseOption.callback = function() {
1082+
toggleOption(true);
1083+
};
1084+
menuOptions.push(collapseOption);
1085+
1086+
// Option to expand top blocks.
1087+
var expandOption = {enabled: hasCollapsedBlocks};
1088+
expandOption.text = Blockly.Msg.EXPAND_ALL;
1089+
expandOption.callback = function() {
1090+
toggleOption(false);
1091+
};
1092+
menuOptions.push(expandOption);
1093+
}
1094+
1095+
Blockly.ContextMenu.show(e, menuOptions, this.RTL);
1096+
};
1097+
10201098
Blockly.JavaScript.depth = 0;
10211099

10221100
// Load the Code demo's language strings.

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<script src="lib/clipboard.js"></script>
3636
<script src="lib/tracking-min.js"></script>
3737
<script src="lib/face-min.js"></script>
38+
<script src="lib/saveSvgAsPng.js"></script>
3839
</head>
3940

4041
<body>

lib/saveSvgAsPng.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
(function() {
2+
var out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this;
3+
4+
var doctype = '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
5+
6+
function isExternal(url) {
7+
return url && url.lastIndexOf('http',0) == 0 && url.lastIndexOf(window.location.host) == -1;
8+
}
9+
10+
function inlineImages(el, callback) {
11+
var images = el.querySelectorAll('image');
12+
var left = images.length;
13+
if (left == 0) {
14+
callback();
15+
}
16+
for (var i = 0; i < images.length; i++) {
17+
(function(image) {
18+
var href = image.getAttributeNS("http://www.w3.org/1999/xlink", "href");
19+
if (href) {
20+
if (isExternal(href.value)) {
21+
console.warn("Cannot render embedded images linking to external hosts: "+href.value);
22+
return;
23+
}
24+
}
25+
var canvas = document.createElement('canvas');
26+
var ctx = canvas.getContext('2d');
27+
var img = new Image();
28+
href = href || image.getAttribute('href');
29+
img.src = href;
30+
img.onload = function() {
31+
canvas.width = img.width;
32+
canvas.height = img.height;
33+
ctx.drawImage(img, 0, 0);
34+
image.setAttributeNS("http://www.w3.org/1999/xlink", "href", canvas.toDataURL('image/png'));
35+
left--;
36+
if (left == 0) {
37+
callback();
38+
}
39+
}
40+
img.onerror = function() {
41+
console.log("Could not load "+href);
42+
left--;
43+
if (left == 0) {
44+
callback();
45+
}
46+
}
47+
})(images[i]);
48+
}
49+
}
50+
51+
function styles(el, selectorRemap) {
52+
var css = "";
53+
var sheets = document.styleSheets;
54+
for (var i = 0; i < sheets.length; i++) {
55+
try {
56+
var rules = sheets[i].cssRules;
57+
} catch (e) {
58+
console.warn("Stylesheet could not be loaded: "+sheets[i].href);
59+
continue;
60+
}
61+
62+
if (rules != null) {
63+
for (var j = 0; j < rules.length; j++) {
64+
var rule = rules[j];
65+
if (typeof(rule.style) != "undefined") {
66+
var match = null;
67+
try {
68+
match = el.querySelector(rule.selectorText);
69+
} catch(err) {
70+
console.warn('Invalid CSS selector "' + rule.selectorText + '"', err);
71+
}
72+
if (match) {
73+
var selector = selectorRemap ? selectorRemap(rule.selectorText) : rule.selectorText;
74+
css += selector + " { " + rule.style.cssText + " }\n";
75+
} else if(rule.cssText.match(/^@font-face/)) {
76+
css += rule.cssText + '\n';
77+
}
78+
}
79+
}
80+
}
81+
}
82+
return css;
83+
}
84+
85+
function getDimension(el, clone, dim) {
86+
var v = (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) ||
87+
(clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) ||
88+
el.getBoundingClientRect()[dim] ||
89+
parseInt(clone.style[dim]) ||
90+
parseInt(window.getComputedStyle(el).getPropertyValue(dim));
91+
return (typeof v === 'undefined' || v === null || isNaN(parseFloat(v))) ? 0 : v;
92+
}
93+
94+
function reEncode(data) {
95+
data = encodeURIComponent(data);
96+
data = data.replace(/%([0-9A-F]{2})/g, function(match, p1) {
97+
var c = String.fromCharCode('0x'+p1);
98+
return c === '%' ? '%25' : c;
99+
});
100+
return decodeURIComponent(data);
101+
}
102+
103+
out$.svgAsDataUri = function(el, options, cb) {
104+
options = options || {};
105+
options.scale = options.scale || 1;
106+
var xmlns = "http://www.w3.org/2000/xmlns/";
107+
108+
inlineImages(el, function() {
109+
var outer = document.createElement("div");
110+
var clone = el.cloneNode(true);
111+
clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, ''));
112+
var svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
113+
svg.appendChild(clone);
114+
clone = svg;
115+
116+
var box = el.getBBox();
117+
var rect = el.getBoundingClientRect();
118+
119+
clone.setAttribute("version", "1.1");
120+
if (clone.getAttribute("xmlns") !== null) {
121+
clone.removeAttribute("xmlns");
122+
}
123+
if (clone.getAttribute("xmlns:xlink") !== null) {
124+
clone.removeAttribute("xmlns:xlink");
125+
}
126+
clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg");
127+
clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink");
128+
clone.setAttribute("width", rect.width);
129+
clone.setAttribute("height", rect.height);
130+
clone.setAttribute("viewBox", [
131+
box.x,
132+
box.y,
133+
rect.width,
134+
rect.height
135+
].join(" "));
136+
137+
outer.appendChild(clone);
138+
139+
var css = styles(el, options.selectorRemap);
140+
var s = document.createElement('style');
141+
s.setAttribute('type', 'text/css');
142+
s.innerHTML = "<![CDATA[\n" + css + "\n]]>";
143+
var defs = document.createElement('defs');
144+
defs.appendChild(s);
145+
clone.insertBefore(defs, clone.firstChild);
146+
147+
var svg = doctype + outer.innerHTML.replace(/&nbsp;/g, ' ');
148+
var uri = 'data:image/svg+xml;base64,' + window.btoa(reEncode(svg));
149+
if (cb) {
150+
cb(uri);
151+
}
152+
});
153+
}
154+
155+
out$.svgAsPngUri = function(el, options, cb) {
156+
out$.svgAsDataUri(el, options, function(uri) {
157+
var image = new Image();
158+
image.onload = function() {
159+
var canvas = document.createElement('canvas');
160+
canvas.width = image.width;
161+
canvas.height = image.height;
162+
var context = canvas.getContext('2d');
163+
if(options && options.backgroundColor){
164+
context.fillStyle = options.backgroundColor;
165+
context.fillRect(0, 0, canvas.width, canvas.height);
166+
}
167+
context.drawImage(image, 0, 0);
168+
var a = document.createElement('a'), png;
169+
try {
170+
png = canvas.toDataURL('image/png');
171+
} catch (e) {
172+
if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name == "SecurityError") {
173+
console.error("Rendered SVG images cannot be downloaded in this browser.");
174+
return;
175+
} else {
176+
throw e;
177+
}
178+
}
179+
cb(png);
180+
}
181+
image.src = uri;
182+
});
183+
}
184+
185+
out$.saveSvgAsPng = function(el, name, options) {
186+
options = options || {};
187+
out$.svgAsPngUri(el, options, function(uri) {
188+
var a = document.createElement('a');
189+
a.download = name;
190+
a.href = uri;
191+
document.body.appendChild(a);
192+
a.addEventListener("click", function(e) {
193+
a.parentNode.removeChild(a);
194+
});
195+
a.click();
196+
});
197+
}
198+
199+
// if define is defined create as an AMD module
200+
if (typeof define !== 'undefined') {
201+
define(function() {
202+
return out$;
203+
});
204+
}
205+
206+
})();

msg/en.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ var MSG = {
33
title: "Webduino Blockly Editor",
44
gotoTutorials: "Tutorials",
55
gotoWebSite: "Official site",
6+
cleanUpBlocks: "Clean up Blocks",
7+
exportImage: "Download image",
68
blocks: "Blocks",
79
linkTooltip: "Save and link to blocks.",
810
runTooltip: "Run the program defined by the blocks in the workspace.",

msg/zh-hant.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ var MSG = {
33
title: "Webduino Blockly 編輯器",
44
gotoTutorials: "教學範例",
55
gotoWebSite: "官方網站",
6+
cleanUpBlocks: "整理積木",
7+
exportImage: "下載圖片",
68
blocks: "積木",
79
linkTooltip: "儲存積木組並提供連結。",
810
runTooltip: "於工作區中執行積木組所定義的程式。",

0 commit comments

Comments
 (0)