You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

createSVG.js 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. const fs = require("./fs_promises.js"),
  2. path = require("path"),
  3. wboPencilPoint = require("../client-data/tools/pencil/wbo_pencil_point.js")
  4. .wboPencilPoint;
  5. function htmlspecialchars(str) {
  6. if (typeof str !== "string") return "";
  7. return str.replace(/[<>&"']/g, function (c) {
  8. switch (c) {
  9. case "<":
  10. return "&lt;";
  11. case ">":
  12. return "&gt;";
  13. case "&":
  14. return "&amp;";
  15. case '"':
  16. return "&quot;";
  17. case "'":
  18. return "&#39;";
  19. }
  20. });
  21. }
  22. function renderPath(el, pathstring) {
  23. return (
  24. "<path " +
  25. (el.id ? 'id="' + htmlspecialchars(el.id) + '" ' : "") +
  26. 'stroke-width="' +
  27. (el.size | 0) +
  28. '" ' +
  29. (el.opacity ? 'opacity="' + parseFloat(el.opacity) + '" ' : "") +
  30. 'stroke="' +
  31. htmlspecialchars(el.color) +
  32. '" ' +
  33. 'd="' +
  34. pathstring +
  35. '" ' +
  36. (el.deltax || el.deltay
  37. ? 'transform="translate(' + +el.deltax + "," + +el.deltay + ')"'
  38. : "") +
  39. "/>"
  40. );
  41. }
  42. const Tools = {
  43. /**
  44. * @return {string}
  45. */
  46. Text: function (el) {
  47. return (
  48. "<text " +
  49. 'id="' +
  50. htmlspecialchars(el.id || "t") +
  51. '" ' +
  52. 'x="' +
  53. (el.x | 0) +
  54. '" ' +
  55. 'y="' +
  56. (el.y | 0) +
  57. '" ' +
  58. 'font-size="' +
  59. (el.size | 0) +
  60. '" ' +
  61. 'fill="' +
  62. htmlspecialchars(el.color || "#000") +
  63. '" ' +
  64. (el.deltax || el.deltay
  65. ? 'transform="translate(' +
  66. (el.deltax || 0) +
  67. "," +
  68. (el.deltay || 0) +
  69. ')"'
  70. : "") +
  71. ">" +
  72. htmlspecialchars(el.txt || "") +
  73. "</text>"
  74. );
  75. },
  76. /**
  77. * @return {string}
  78. */
  79. Pencil: function (el) {
  80. if (!el._children) return "";
  81. let pts = el._children.reduce(function (pts, point) {
  82. return wboPencilPoint(pts, point.x, point.y);
  83. }, []);
  84. const pathstring = pts
  85. .map(function (op) {
  86. return op.type + " " + op.values.join(" ");
  87. })
  88. .join(" ");
  89. return renderPath(el, pathstring);
  90. },
  91. /**
  92. * @return {string}
  93. */
  94. Rectangle: function (el) {
  95. return (
  96. "<rect " +
  97. (el.id ? 'id="' + htmlspecialchars(el.id) + '" ' : "") +
  98. 'x="' +
  99. (el.x || 0) +
  100. '" ' +
  101. 'y="' +
  102. (el.y || 0) +
  103. '" ' +
  104. 'width="' +
  105. (el.x2 - el.x) +
  106. '" ' +
  107. 'height="' +
  108. (el.y2 - el.y) +
  109. '" ' +
  110. 'stroke="' +
  111. htmlspecialchars(el.color) +
  112. '" ' +
  113. 'stroke-width="' +
  114. (el.size | 0) +
  115. '" ' +
  116. (el.deltax || el.deltay
  117. ? 'transform="translate(' +
  118. (el.deltax || 0) +
  119. "," +
  120. (el.deltay || 0) +
  121. ')"'
  122. : "") +
  123. "/>"
  124. );
  125. },
  126. /**
  127. * @return {string}
  128. */
  129. Ellipse: function (el) {
  130. const cx = Math.round((el.x2 + el.x) / 2);
  131. const cy = Math.round((el.y2 + el.y) / 2);
  132. const rx = Math.abs(el.x2 - el.x) / 2;
  133. const ry = Math.abs(el.y2 - el.y) / 2;
  134. const pathstring =
  135. "M" +
  136. (cx - rx) +
  137. " " +
  138. cy +
  139. "a" +
  140. rx +
  141. "," +
  142. ry +
  143. " 0 1,0 " +
  144. rx * 2 +
  145. ",0" +
  146. "a" +
  147. rx +
  148. "," +
  149. ry +
  150. " 0 1,0 " +
  151. rx * -2 +
  152. ",0";
  153. return renderPath(el, pathstring);
  154. },
  155. /**
  156. * @return {string}
  157. */
  158. "Straight line": function (el) {
  159. const pathstring = "M" + el.x + " " + el.y + "L" + el.x2 + " " + el.y2;
  160. return renderPath(el, pathstring);
  161. },
  162. };
  163. /**
  164. * Writes the given board as an svg to the given writeable stream
  165. * @param {Object[string, BoardElem]} obj
  166. * @param {WritableStream} writeable
  167. */
  168. async function toSVG(obj, writeable) {
  169. const margin = 400;
  170. const elems = Object.values(obj);
  171. const dim = elems.reduce(
  172. function (dim, elem) {
  173. if (elem._children && elem._children.length) elem = elem._children[0];
  174. return [
  175. Math.max((elem.x + margin + (elem.deltax | 0)) | 0, dim[0]),
  176. Math.max((elem.y + margin + (elem.deltay | 0)) | 0, dim[1]),
  177. ];
  178. },
  179. [margin, margin]
  180. );
  181. writeable.write(
  182. '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' +
  183. 'width="' +
  184. dim[0] +
  185. '" height="' +
  186. dim[1] +
  187. '">' +
  188. '<defs><style type="text/css"><![CDATA[' +
  189. 'text {font-family:"Arial"}' +
  190. "path {fill:none;stroke-linecap:round;stroke-linejoin:round;}" +
  191. "rect {fill:none}" +
  192. "]]></style></defs>"
  193. );
  194. await Promise.all(
  195. elems.map(async function (elem) {
  196. await Promise.resolve(); // Do not block the event loop
  197. const renderFun = Tools[elem.tool];
  198. if (renderFun) writeable.write(renderFun(elem));
  199. else console.warn("Missing render function for tool", elem.tool);
  200. })
  201. );
  202. writeable.write("</svg>");
  203. }
  204. async function renderBoard(file, stream) {
  205. const data = await fs.promises.readFile(file);
  206. var board = JSON.parse(data);
  207. return toSVG(board, stream);
  208. }
  209. if (require.main === module) {
  210. const config = require("./configuration.js");
  211. const HISTORY_FILE =
  212. process.argv[2] || path.join(config.HISTORY_DIR, "board-anonymous.json");
  213. renderBoard(HISTORY_FILE, process.stdout).catch(console.error.bind(console));
  214. } else {
  215. module.exports = { renderBoard: renderBoard };
  216. }