1<?php
2/**
3 * SketchCanvas server-side renderer
4 *
5 * It requires GD and Yaml extensions set up for PHP in order to work.
6 */
7
8define('FONTFACE', "NotoSansCJKjp-Regular.otf");
9
10require_once "spyc.php";
11
12$size = Array(640, 480);
13$drawdata = null;
14
15try{
16	if(isset($_GET['fname']) || isset($_POST['drawdata']) || isset($_GET['drawdata'])){
17		if(isset($_GET['fname']))
18			$drawdata = Spyc::YAMLLoad('data/' . $_GET['fname']);
19		else if(isset($_POST['drawdata']))
20			$drawdata = Spyc::YAMLLoadString($_POST['drawdata']);
21		else
22			$drawdata = Spyc::YAMLLoadString($_GET['drawdata']);
23		foreach ($drawdata as $key => $value) {
24			switch($value["type"]){
25				case "meta":
26					$size = $value["size"];
27					break;
28			}
29		}
30	}
31}
32catch(Exception $e){
33	echo "failed\n";
34	echo $e->getMessage();
35	return;
36}
37
38$im = imagecreatetruecolor($size[0], $size[1]);
39
40$white = imagecolorallocate($im, 255, 255, 255);
41$c = imagecolorallocate($im, 255, 0, 0);
42
43imagefilledrectangle($im, 0, 0, $size[0], $size[1], $white);
44
45function pixelToPoint($px){
46	return $px * 0.525;
47}
48
49/// @brief Dot product 45 degrees
50/// @param a vector
51/// @param b length
52function l_vec($a, $b) {
53	$ax = $a[1][0]-$a[0][0];
54	$ay = $a[1][1]-$a[0][1];
55	$rate = $b / sqrt($ax * $ax + $ay * $ay);
56	$ax *= $rate;
57	$ay *= $rate;
58	$rad1 = pi() / 4.0;		// 45°
59	$rad2 = -pi() / 4.0;		// -45°
60	$a1x = $ax * cos($rad1) - $ay * sin($rad1);
61	$a1y = $ax * sin($rad1) + $ay * cos($rad1);
62	$a2x = $ax * cos($rad2) - $ay * sin($rad2);
63	$a2y = $ax * sin($rad2) + $ay * cos($rad2);
64	$c = array(2);
65	$c[0] = array( $a1x, $a1y );
66	$c[1] = array( $a2x, $a2y );
67	return $c;
68}
69
70/// @brief Dot product 90 degrees
71/// @param a vector
72/// @param b length
73function l_vec9($a, $b) {
74	$ax = $a[1][0]-$a[0][0];
75	$ay = $a[1][1]-$a[0][1];
76	$rate = $b / sqrt($ax * $ax + $ay * $ay);
77	$ax *= $rate;
78	$ay *= $rate;
79	$a1x = -$ay;
80	$a1y = $ax;
81	$a2x = $ay;
82	$a2y = -$ax;
83	$c = array(2);
84	$c[0] = array( $a1x, $a1y );
85	$c[1] = array( $a2x, $a2y );
86	return $c;
87}
88
89function l_arrow($im, $arr, $color) {
90	$c = l_vec($arr, 6);
91	imageline($im, $arr[1][0], $arr[1][1], $arr[1][0]-$c[0][0], $arr[1][1]-$c[0][1], $color);
92	imageline($im, $arr[1][0], $arr[1][1], $arr[1][0]-$c[1][0], $arr[1][1]-$c[1][1], $color);
93}
94
95// draw double arrow
96function l_darrow($im, $arr, $color) {
97	$c = l_vec9($arr, 2);
98	$d = l_vec($arr, 8);
99	imageline($im, $arr[0][0]+$c[0][0], $arr[0][1]+$c[0][1], $arr[1][0]+$c[0][0], $arr[1][1]+$c[0][1], $color);
100	imageline($im, $arr[0][0]+$c[0][0], $arr[0][1]+$c[0][1], $arr[1][0]+$c[0][0], $arr[1][1]+$c[0][1], $color);
101	imageline($im, $arr[0][0]+$c[1][0], $arr[0][1]+$c[1][1], $arr[1][0]+$c[1][0], $arr[1][1]+$c[1][1], $color);
102	imageline($im, $arr[0][0]+$c[1][0], $arr[0][1]+$c[1][1], $arr[1][0]+$c[1][0], $arr[1][1]+$c[1][1], $color);
103	imageline($im, $arr[1][0], $arr[1][1], $arr[1][0]-$d[0][0], $arr[1][1]-$d[0][1], $color);
104	imageline($im, $arr[1][0], $arr[1][1], $arr[1][0]-$d[1][0], $arr[1][1]-$d[1][1], $color);
105}
106
107// draw twin arrow
108function l_tarrow($im, $arr, $color) {
109	$c = l_vec($arr, 6);
110	imageline($im, $arr[1][0], $arr[1][1], $arr[1][0]-$c[0][0], $arr[1][1]-$c[0][1], $color);
111	imageline($im, $arr[1][0], $arr[1][1], $arr[1][0]-$c[1][0], $arr[1][1]-$c[1][1], $color);
112	$a = array($arr[1], $arr[0]);
113	$c = l_vec($a, 6);
114	imageline($im, $arr[0][0], $arr[0][1], $arr[0][0]-$c[0][0], $arr[0][1]-$c[0][1], $color);
115	imageline($im, $arr[0][0], $arr[0][1], $arr[0][0]-$c[1][0], $arr[0][1]-$c[1][1], $color);
116}
117
118function l_hige($im, $arr, $color) {
119	$c = l_vec($arr, 6);
120	imageline($im, $arr[1][0]-$c[0][0], $arr[1][1]-$c[0][1], $arr[1][0], $arr[1][1], $color);
121	imageline($im, $arr[1][0]-$c[1][0], $arr[1][1]-$c[1][1], $arr[1][0], $arr[1][1], $color);
122}
123
124// draw star
125function l_star($im, $arr, $color) {
126	$x = $arr[0][0];
127	$y = $arr[0][1];
128	imagepolygon($im, array(
129		$x+8, $y-3,
130		$x+14, $y+13,
131		$x, $y+2,
132		$x+16, $y+2,
133		$x+2, $y+13),
134		5, $color);
135}
136
137// draw check
138function l_check($im, $arr, $color) {
139	$x = $arr[0][0];
140	$y = $arr[0][1];
141	imageline($im, $x, $y, $x+5, $y+7, $color);
142	imageline($im, $x+5, $y+7, $x+20, $y, $color);
143}
144
145// draw complete
146function l_complete($im, $arr, $color) {
147	imagettftext($im, pixelToPoint(20), 0, $arr[0][0]+3, $arr[0][1]+10, $c, "/usr/share/fonts/vlgothic/VL-Gothic-Regular.ttf", '済');
148	imageellipse($im, $arr[0][0]+9, $arr[0][1]+5, 16, 16, $color);
149}
150
151function colorSelect($im, $value){
152	$c = null;
153	if(isset($value['color'])){
154		switch($value['color']){
155			default:
156			case 'black':
157				$c = imagecolorallocate($im, 0, 0, 0);
158				break;
159			case 'blue':
160				$c = imagecolorallocate($im, 0, 0, 255);
161				break;
162			case 'red':
163				$c = imagecolorallocate($im, 255, 0, 0);
164				break;
165			case 'green':
166				$c = imagecolorallocate($im, 0, 255, 0);
167				break;
168		}
169	}
170	else
171		$c = imagecolorallocate($im, 0, 0, 0);
172	return $c;
173}
174
175/// Length of 2-D vector
176function veclen($a){
177	return sqrt($a[0] * $a[0] + $a[1] * $a[1]);
178}
179
180/// Subtraction of 2-D vectors
181function vecsub($a, $b){
182	return array($a[0] - $b[0], $a[1] - $b[1]);
183}
184
185/// Distance of 2-D vectors
186function vecdist($a, $b){
187	return veclen(vecsub($a, $b));
188}
189
190/// Returns dividing point of line between $a and $b that divide
191/// the line by $t and 1 - $t.
192function midPoint($a, $b, $t){
193	return array($a[0] * (1. - $t) + $b[0] * $t,
194		$a[1] * (1. - $t) + $b[1] * $t);
195}
196
197/// Obtain coordinates of a point on a quadratic bezier curve.
198function quadraticBezierPoint($a, $b, $c, $t){
199	$ab = midPoint($a, $b, $t);
200	$bc = midPoint($b, $c, $t);
201	return midPoint($ab, $bc, $t);
202}
203
204/// Obtain coordinates of a point on a cubic bezier curve.
205function cubicBezierPoint($a, $c, $d, $b, $t){
206	$ac = midPoint($a, $c, $t);
207	$cd = midPoint($c, $d, $t);
208	$db = midPoint($d, $b, $t);
209	$acd = midPoint($ac, $cd, $t);
210	$cdb = midPoint($cd, $db, $t);
211	return midPoint($acd, $cdb, $t);
212
213}
214
215/// Draws a cubic Bezier curve on image $im.
216function cubicBezierCurve($im, $prev, $p, $color){
217	$a = array($prev["x"], $prev["y"]);
218	$b = array($p["x"], $p["y"]);
219	$c = isset($p["cx"]) && isset($p["cy"]) ? array($p["cx"], $p["cy"]) : $a;
220	$d = isset($p["dx"]) && isset($p["dy"]) ? array($p["dx"], $p["dy"]) : $b;
221
222	// If both c and d control points are not defined, just draw a straight line,
223	if((!isset($p["cx"]) || !isset($p["cy"])) && (!isset($p["dx"]) || !isset($p["dy"]))){
224		imageline($im, $a[0], $a[1], $b[0], $b[1], $color);
225		return;
226	}
227
228	// The curve is actually made of line segments.
229	// We determine the number of segments here by first measuring
230	// the rough length of the whole shape.
231	$length = vecdist($a, $c)
232			+ vecdist($c, $d)
233			+ vecdist($d, $b);
234	$divs = ceil($length / 10.); // The divisor determines how smooth the curve is.
235
236	$a0 = $a;
237
238	for($t = 0; $t <= $divs; $t++){
239		$p1 = cubicBezierPoint($a, $c, $d, $b, $t / $divs);
240		imageline($im, $a0[0], $a0[1], $p1[0], $p1[1], $color);
241		$a0 = $p1;
242	}
243}
244
245try{
246	if($drawdata !== null){
247		require_once "lib.php";
248		foreach ($drawdata as $key => $value) {
249			$c = colorSelect($im, $value);
250			switch($value["type"]){
251				case "line":
252				case "arrow":
253				case "barrow":
254				case "darrow":
255				case "rect":
256				case "rectfill":
257				case "ellipse":
258				case "ellipsefill":
259					$pts = parsePointList($value["points"]);
260					$p1 = $pts[0];
261					$p2 = $pts[1];
262					$wid = isset($value["width"]) ? $value["width"] : 1;
263					imagesetthickness($im, $wid);
264					imageantialias($im, $wid === 1);
265					if($value["type"] === "rect")
266						imagerectangle($im, $p1[0], $p1[1], $p2[0], $p2[1], $c);
267					else if($value["type"] === "rectfill")
268						imagefilledrectangle($im, $p1[0], $p1[1], $p2[0], $p2[1], $c);
269					else if($value["type"] === "ellipse")
270						imageellipse($im, ($p1[0] + $p2[0]) / 2, ($p1[1] + $p2[1]) / 2, abs($p2[0] - $p1[0]), abs($p2[1] - $p1[1]), $c);
271					else if($value["type"] === "ellipsefill")
272						imagefilledellipse($im, ($p1[0] + $p2[0]) / 2, ($p1[1] + $p2[1]) / 2, abs($p2[0] - $p1[0]), abs($p2[1] - $p1[1]), $c);
273					else if($value["type"] === "darrow")
274						l_darrow($im, $pts, $c);
275					else
276						imageline($im, $p1[0], $p1[1], $p2[0], $p2[1], $c);
277					if($value["type"] === "arrow")
278						l_arrow($im, $pts, $c);
279					else if($value["type"] === "barrow")
280						l_tarrow($im, $pts, $c);
281					break;
282				case 'arc':
283				case 'arcarrow':
284				case 'arcbarrow':
285					// We emulate HTML5 Canvas's Context2D.quadraticCurveTo(),
286					// which is really an implementation of quadratic Bezier curve.
287					// Bezier curves are very easy to implement. See:
288					// https://en.wikipedia.org/wiki/B%C3%A9zier_curve
289					$pts = parsePointList($value["points"]);
290
291					// The curve is actually made of line segments.
292					// We determine the number of segments here by first measuring
293					// the rough length of the whole shape.
294					$length = vecdist($pts[0], $pts[1])
295							+ vecdist($pts[1], $pts[2]);
296					$divs = ceil($length / 20.);
297					$prev = $pts[0];
298
299					$wid = isset($value["width"]) ? $value["width"] : 1;
300					imagesetthickness($im, $wid);
301					imageantialias($im, $wid === 1);
302					for($t = 0; $t <= $divs; $t++){
303						$p1 = quadraticBezierPoint($pts[0], $pts[1], $pts[2], $t / $divs);
304						imageline($im, $prev[0], $prev[1], $p1[0], $p1[1], $c);
305						$prev = $p1;
306					}
307					if($value["type"] === "arcarrow" || $value["type"] === "arcbarrow")
308						l_hige($im, array($pts[1], $pts[2]), $c);
309					if($value["type"] === "arcbarrow")
310						l_hige($im, array($pts[1], $pts[0]), $c);
311					break;
312				case 'star':
313					l_star($im, parsePointList($value["points"]), $c);
314					break;
315				case 'check':
316					l_check($im, parsePointList($value["points"]), $c);
317					break;
318				case 'done':
319					l_complete($im, parsePointList($value["points"]), $c);
320					break;
321				case 'text':
322					$pts = parsePointList($value["points"]);
323					$fontsize = 10;
324					if (1 == $value["width"]) $fontsize = 14;
325					else if (2 == $value["width"]) $fontsize = 16;
326					else $fontsize = 20;
327					imagettftext($im, pixelToPoint($fontsize), 0, $pts[0][0], $pts[0][1], $c, FONTFACE, $value["text"]);
328					break;
329				case 'path':
330					$pts = parsePathCommands($value["d"]);
331					$wid = isset($value["width"]) ? $value["width"] : 1;
332					imagesetthickness($im, $wid);
333					imageantialias($im, $wid === 1);
334					$prev = null;
335					foreach($pts as $i => $p){
336						if($prev !== null)
337							cubicBezierCurve($im, $prev, $p, $c);
338						$prev = $p;
339					}
340					if(isset($value["arrow"]) && 1 < count($pts)){
341						$set = seq2set($value["arrow"]);
342						if(isset($set["head"])){
343							$first = $pts[0];
344							$first2 = $pts[1];
345							$a = array();
346							if(isset($first2["cx"]) && isset($first2["cy"]) && ($first2["cx"] !== $first["x"] || $first2["cy"] !== $first["y"]))
347								$a[] = array($first2["cx"], $first2["cy"]);
348							else if(isset($first2["dx"]) && isset($first2["dy"]) && ($first2["dx"] !== $first["x"] || $first2["dy"] !== $first["y"]))
349								$a[] = array($first2["dx"], $first2["dy"]);
350							else
351								$a[] = array($first2["x"], $first2["y"]);
352							$a[] = array($first["x"], $first["y"]);
353							l_hige($im, $a, $c);
354						}
355						if(isset($set["tail"])){
356							$last = $pts[count($pts)-1];
357							$last2 = $pts[count($pts)-2];
358							$a = array();
359							if(isset($last["dx"]) && isset($last["dy"]) && ($last["dx"] !== $last["x"] || $last["dy"] !== $last["y"]))
360								$a[0] = array($last["dx"], $last["dy"]);
361							else if(isset($last["cx"]) && isset($last["cy"]) && ($last["cx"] !== $last["x"] || $last["cy"] !== $last["y"]))
362								$a[0] = array($last["cx"], $last["cy"]);
363							else
364								$a[0] = array($last2["x"], $last2["y"]);
365							$a[1] = array($last["x"], $last["y"]);
366							l_hige($im, $a, $c);
367						}
368					}
369					break;
370			}
371		}
372	}
373}
374catch(Exception $e){
375	echo "failed\n";
376	echo $e->getMessage();
377	return;
378}
379
380$useBase64 = false;
381if(isset($_GET['base64']) || isset($_POST['base64']))
382	$useBase64 = true;
383else
384	header('Content-Type: image/png');
385
386if($useBase64)
387	ob_start();
388imagepng($im);
389imagedestroy($im);
390if($useBase64){
391	$imagedata = ob_get_contents();
392	ob_end_clean();
393	print base64_encode($imagedata);
394}
395