_applyTransforms.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. 'use strict';
  2. // TODO implement as separate plugin
  3. const {
  4. transformsMultiply,
  5. transform2js,
  6. transformArc,
  7. } = require('./_transforms.js');
  8. const { removeLeadingZero } = require('../lib/svgo/tools.js');
  9. const { referencesProps, attrsGroupsDefaults } = require('./_collections.js');
  10. const regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
  11. const defaultStrokeWidth = attrsGroupsDefaults.presentation['stroke-width'];
  12. /**
  13. * Apply transformation(s) to the Path data.
  14. *
  15. * @param {Object} elem current element
  16. * @param {Array} path input path data
  17. * @param {Object} params whether to apply transforms to stroked lines and transform precision (used for stroke width)
  18. * @return {Array} output path data
  19. */
  20. const applyTransforms = (elem, pathData, params) => {
  21. // if there are no 'stroke' attr and references to other objects such as
  22. // gradiends or clip-path which are also subjects to transform.
  23. if (
  24. elem.attributes.transform == null ||
  25. elem.attributes.transform === '' ||
  26. // styles are not considered when applying transform
  27. // can be fixed properly with new style engine
  28. elem.attributes.style != null ||
  29. Object.entries(elem.attributes).some(
  30. ([name, value]) =>
  31. referencesProps.includes(name) && value.includes('url(')
  32. )
  33. ) {
  34. return;
  35. }
  36. const matrix = transformsMultiply(transform2js(elem.attributes.transform));
  37. const stroke = elem.computedAttr('stroke');
  38. const id = elem.computedAttr('id');
  39. const transformPrecision = params.transformPrecision;
  40. if (stroke && stroke != 'none') {
  41. if (
  42. !params.applyTransformsStroked ||
  43. ((matrix.data[0] != matrix.data[3] ||
  44. matrix.data[1] != -matrix.data[2]) &&
  45. (matrix.data[0] != -matrix.data[3] || matrix.data[1] != matrix.data[2]))
  46. )
  47. return;
  48. // "stroke-width" should be inside the part with ID, otherwise it can be overrided in <use>
  49. if (id) {
  50. let idElem = elem;
  51. let hasStrokeWidth = false;
  52. do {
  53. if (idElem.attributes['stroke-width']) {
  54. hasStrokeWidth = true;
  55. }
  56. } while (
  57. idElem.attributes.id !== id &&
  58. !hasStrokeWidth &&
  59. (idElem = idElem.parentNode)
  60. );
  61. if (!hasStrokeWidth) return;
  62. }
  63. const scale = +Math.sqrt(
  64. matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1]
  65. ).toFixed(transformPrecision);
  66. if (scale !== 1) {
  67. const strokeWidth =
  68. elem.computedAttr('stroke-width') || defaultStrokeWidth;
  69. if (
  70. elem.attributes['vector-effect'] == null ||
  71. elem.attributes['vector-effect'] !== 'non-scaling-stroke'
  72. ) {
  73. if (elem.attributes['stroke-width'] != null) {
  74. elem.attributes['stroke-width'] = elem.attributes['stroke-width']
  75. .trim()
  76. .replace(regNumericValues, (num) => removeLeadingZero(num * scale));
  77. } else {
  78. elem.attributes['stroke-width'] = strokeWidth.replace(
  79. regNumericValues,
  80. (num) => removeLeadingZero(num * scale)
  81. );
  82. }
  83. if (elem.attributes['stroke-dashoffset'] != null) {
  84. elem.attributes['stroke-dashoffset'] = elem.attributes[
  85. 'stroke-dashoffset'
  86. ]
  87. .trim()
  88. .replace(regNumericValues, (num) => removeLeadingZero(num * scale));
  89. }
  90. if (elem.attributes['stroke-dasharray'] != null) {
  91. elem.attributes['stroke-dasharray'] = elem.attributes[
  92. 'stroke-dasharray'
  93. ]
  94. .trim()
  95. .replace(regNumericValues, (num) => removeLeadingZero(num * scale));
  96. }
  97. }
  98. }
  99. } else if (id) {
  100. // Stroke and stroke-width can be redefined with <use>
  101. return;
  102. }
  103. applyMatrixToPathData(pathData, matrix.data);
  104. // remove transform attr
  105. delete elem.attributes.transform;
  106. return;
  107. };
  108. exports.applyTransforms = applyTransforms;
  109. const transformAbsolutePoint = (matrix, x, y) => {
  110. const newX = matrix[0] * x + matrix[2] * y + matrix[4];
  111. const newY = matrix[1] * x + matrix[3] * y + matrix[5];
  112. return [newX, newY];
  113. };
  114. const transformRelativePoint = (matrix, x, y) => {
  115. const newX = matrix[0] * x + matrix[2] * y;
  116. const newY = matrix[1] * x + matrix[3] * y;
  117. return [newX, newY];
  118. };
  119. const applyMatrixToPathData = (pathData, matrix) => {
  120. const start = [0, 0];
  121. const cursor = [0, 0];
  122. for (const pathItem of pathData) {
  123. let { command, args } = pathItem;
  124. // moveto (x y)
  125. if (command === 'M') {
  126. cursor[0] = args[0];
  127. cursor[1] = args[1];
  128. start[0] = cursor[0];
  129. start[1] = cursor[1];
  130. const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
  131. args[0] = x;
  132. args[1] = y;
  133. }
  134. if (command === 'm') {
  135. cursor[0] += args[0];
  136. cursor[1] += args[1];
  137. start[0] = cursor[0];
  138. start[1] = cursor[1];
  139. const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
  140. args[0] = x;
  141. args[1] = y;
  142. }
  143. // horizontal lineto (x)
  144. // convert to lineto to handle two-dimentional transforms
  145. if (command === 'H') {
  146. command = 'L';
  147. args = [args[0], cursor[1]];
  148. }
  149. if (command === 'h') {
  150. command = 'l';
  151. args = [args[0], 0];
  152. }
  153. // vertical lineto (y)
  154. // convert to lineto to handle two-dimentional transforms
  155. if (command === 'V') {
  156. command = 'L';
  157. args = [cursor[0], args[0]];
  158. }
  159. if (command === 'v') {
  160. command = 'l';
  161. args = [0, args[0]];
  162. }
  163. // lineto (x y)
  164. if (command === 'L') {
  165. cursor[0] = args[0];
  166. cursor[1] = args[1];
  167. const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
  168. args[0] = x;
  169. args[1] = y;
  170. }
  171. if (command === 'l') {
  172. cursor[0] += args[0];
  173. cursor[1] += args[1];
  174. const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
  175. args[0] = x;
  176. args[1] = y;
  177. }
  178. // curveto (x1 y1 x2 y2 x y)
  179. if (command === 'C') {
  180. cursor[0] = args[4];
  181. cursor[1] = args[5];
  182. const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]);
  183. const [x2, y2] = transformAbsolutePoint(matrix, args[2], args[3]);
  184. const [x, y] = transformAbsolutePoint(matrix, args[4], args[5]);
  185. args[0] = x1;
  186. args[1] = y1;
  187. args[2] = x2;
  188. args[3] = y2;
  189. args[4] = x;
  190. args[5] = y;
  191. }
  192. if (command === 'c') {
  193. cursor[0] += args[4];
  194. cursor[1] += args[5];
  195. const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]);
  196. const [x2, y2] = transformRelativePoint(matrix, args[2], args[3]);
  197. const [x, y] = transformRelativePoint(matrix, args[4], args[5]);
  198. args[0] = x1;
  199. args[1] = y1;
  200. args[2] = x2;
  201. args[3] = y2;
  202. args[4] = x;
  203. args[5] = y;
  204. }
  205. // smooth curveto (x2 y2 x y)
  206. if (command === 'S') {
  207. cursor[0] = args[2];
  208. cursor[1] = args[3];
  209. const [x2, y2] = transformAbsolutePoint(matrix, args[0], args[1]);
  210. const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]);
  211. args[0] = x2;
  212. args[1] = y2;
  213. args[2] = x;
  214. args[3] = y;
  215. }
  216. if (command === 's') {
  217. cursor[0] += args[2];
  218. cursor[1] += args[3];
  219. const [x2, y2] = transformRelativePoint(matrix, args[0], args[1]);
  220. const [x, y] = transformRelativePoint(matrix, args[2], args[3]);
  221. args[0] = x2;
  222. args[1] = y2;
  223. args[2] = x;
  224. args[3] = y;
  225. }
  226. // quadratic Bézier curveto (x1 y1 x y)
  227. if (command === 'Q') {
  228. cursor[0] = args[2];
  229. cursor[1] = args[3];
  230. const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]);
  231. const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]);
  232. args[0] = x1;
  233. args[1] = y1;
  234. args[2] = x;
  235. args[3] = y;
  236. }
  237. if (command === 'q') {
  238. cursor[0] += args[2];
  239. cursor[1] += args[3];
  240. const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]);
  241. const [x, y] = transformRelativePoint(matrix, args[2], args[3]);
  242. args[0] = x1;
  243. args[1] = y1;
  244. args[2] = x;
  245. args[3] = y;
  246. }
  247. // smooth quadratic Bézier curveto (x y)
  248. if (command === 'T') {
  249. cursor[0] = args[0];
  250. cursor[1] = args[1];
  251. const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]);
  252. args[0] = x;
  253. args[1] = y;
  254. }
  255. if (command === 't') {
  256. cursor[0] += args[0];
  257. cursor[1] += args[1];
  258. const [x, y] = transformRelativePoint(matrix, args[0], args[1]);
  259. args[0] = x;
  260. args[1] = y;
  261. }
  262. // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
  263. if (command === 'A') {
  264. transformArc(cursor, args, matrix);
  265. cursor[0] = args[5];
  266. cursor[1] = args[6];
  267. // reduce number of digits in rotation angle
  268. if (Math.abs(args[2]) > 80) {
  269. const a = args[0];
  270. const rotation = args[2];
  271. args[0] = args[1];
  272. args[1] = a;
  273. args[2] = rotation + (rotation > 0 ? -90 : 90);
  274. }
  275. const [x, y] = transformAbsolutePoint(matrix, args[5], args[6]);
  276. args[5] = x;
  277. args[6] = y;
  278. }
  279. if (command === 'a') {
  280. transformArc([0, 0], args, matrix);
  281. cursor[0] += args[5];
  282. cursor[1] += args[6];
  283. // reduce number of digits in rotation angle
  284. if (Math.abs(args[2]) > 80) {
  285. const a = args[0];
  286. const rotation = args[2];
  287. args[0] = args[1];
  288. args[1] = a;
  289. args[2] = rotation + (rotation > 0 ? -90 : 90);
  290. }
  291. const [x, y] = transformRelativePoint(matrix, args[5], args[6]);
  292. args[5] = x;
  293. args[6] = y;
  294. }
  295. // closepath
  296. if (command === 'z' || command === 'Z') {
  297. cursor[0] = start[0];
  298. cursor[1] = start[1];
  299. }
  300. pathItem.command = command;
  301. pathItem.args = args;
  302. }
  303. };