Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

image.js 16 KiB

2 lat temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. const fs = require('fs-extra');
  2. const Sharp = require('sharp');
  3. const debug = require('debug')('server-connect:image');
  4. const { basename, extname, join } = require('path');
  5. const { toAppPath, toSystemPath, parseTemplate, getUniqFile } = require('../core/path');
  6. const positions = {
  7. 'center': 0,
  8. 'centre': 0,
  9. 'top': 1,
  10. 'north': 1,
  11. 'right': 2,
  12. 'east': 2,
  13. 'bottom': 3,
  14. 'south': 3,
  15. 'left': 4,
  16. 'west': 4,
  17. 'top right': 5,
  18. 'right top': 5,
  19. 'northeast': 5,
  20. 'bottom right': 6,
  21. 'right bottom': 6,
  22. 'southeast': 6,
  23. 'bottom left': 7,
  24. 'left bottom': 7,
  25. 'southwest': 7,
  26. 'top left': 8,
  27. 'left top': 8,
  28. 'northwest': 8,
  29. 'entropy': 16,
  30. 'attention': 17
  31. };
  32. function cw(w, meta) {
  33. if (typeof w == 'string') {
  34. if (/%$/.test(w)) {
  35. w = meta.width * parseFloat(w) / 100;
  36. }
  37. }
  38. if (w < 0) {
  39. w = meta.width + w;
  40. }
  41. return parseInt(w);
  42. }
  43. function ch(h, meta) {
  44. if (typeof h == 'string') {
  45. if (/%$/.test(h)) {
  46. h = meta.height * parseFloat(h) / 100;
  47. }
  48. }
  49. if (h < 0) {
  50. h = meta.height + h;
  51. }
  52. return parseInt(h);
  53. }
  54. function cx(x, w, meta) {
  55. if (typeof x == 'string') {
  56. switch (x) {
  57. case 'left':
  58. x = 0;
  59. break;
  60. case 'center':
  61. x = (meta.width - w) / 2;
  62. break;
  63. case 'right':
  64. x = meta.width - w;
  65. break;
  66. default:
  67. if (/%$/.test(x)) {
  68. x = (meta.width - w) * parseFloat(x) / 100;
  69. }
  70. }
  71. }
  72. if (x < 0) {
  73. x = meta.width - w + x;
  74. }
  75. return parseInt(x);
  76. }
  77. function cy(y, h, meta) {
  78. if (typeof y == 'string') {
  79. switch (y) {
  80. case 'top':
  81. y = 0;
  82. break;
  83. case 'middle':
  84. y = (meta.height - h) / 2;
  85. break;
  86. case 'bottom':
  87. y = meta.height - h;
  88. break;
  89. default:
  90. if (/%$/.test(y)) {
  91. y = (meta.height - h) * parseFloat(y) / 100;
  92. }
  93. }
  94. }
  95. if (y < 0) {
  96. y = meta.height - h + y;
  97. }
  98. return parseInt(y);
  99. }
  100. async function updateImage(sharp) {
  101. sharp.image = Sharp(await sharp.image.toBuffer());
  102. sharp.metadata = await sharp.image.metadata();
  103. }
  104. module.exports = {
  105. getImageSize: async function (options) {
  106. let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.getImageSize: path is required.'));
  107. const image = Sharp(path);
  108. const metadata = await image.metadata();
  109. return {
  110. width: metadata.width,
  111. height: metadata.height
  112. };
  113. },
  114. load: async function (options, name) {
  115. let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.load: path is required.'));
  116. let orient = this.parseOptional(options.autoOrient, 'boolean', false);
  117. this.req.image = this.req.image || {};
  118. this.req.image[name] = { name: basename(path), image: Sharp(path), metadata: null };
  119. const sharp = this.req.image[name];
  120. if (orient) sharp.image.rotate();
  121. await updateImage(sharp);
  122. return {
  123. name: basename(path),
  124. width: sharp.metadata.width,
  125. height: sharp.metadata.height
  126. };
  127. },
  128. save: async function (options) {
  129. const sharp = this.req.image[options.instance];
  130. if (!sharp) throw new Error(`image.save: instance "${options.instance} doesn't exist.`);
  131. let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.save: path is required.'));
  132. let format = this.parseOptional(options.format, 'string', 'jpeg').toLowerCase();
  133. let template = this.parseOptional(options.template, 'string', '{name}{ext}');
  134. let overwrite = this.parseOptional(options.overwrite, 'boolean', false);
  135. let createPath = this.parseOptional(options.createPath, 'boolean', true);
  136. let background = this.parseOptional(options.background, 'string', '#FFFFFF');
  137. let quality = this.parseOptional(options.quality, 'number', 75);
  138. if (!fs.existsSync(path)) {
  139. if (createPath) {
  140. await fs.ensureDir(path);
  141. } else {
  142. throw new Error(`image.save: path "${path}" doesn't exist.`);
  143. }
  144. }
  145. let file = join(path, sharp.name);
  146. if (template) {
  147. file = parseTemplate(file, template);
  148. }
  149. if (format == 'auto') {
  150. switch (extname(file).toLowerCase()) {
  151. case '.png': format = 'png'; break;
  152. case '.gif': format = 'gif'; break;
  153. case '.webp': format = 'webp'; break;
  154. default: format = 'jpeg';
  155. }
  156. }
  157. if (format == 'jpeg') {
  158. sharp.image.flatten({ background });
  159. sharp.image.toFormat(format, { quality });
  160. } else if (format == 'webp') {
  161. sharp.image.toFormat(format, { quality });
  162. } else {
  163. sharp.image.toFormat(format);
  164. }
  165. const data = await sharp.image.toBuffer();
  166. file = file.replace(extname(file), '.' + format.replace('jpeg', 'jpg'));
  167. if (fs.existsSync(file)) {
  168. if (overwrite) {
  169. await fs.unlink(file);
  170. } else {
  171. file = getUniqFile(file);
  172. }
  173. }
  174. await fs.writeFile(file, data) //.toFile(file);
  175. return toAppPath(file);
  176. },
  177. resize: async function (options) {
  178. const sharp = this.req.image[options.instance];
  179. if (!sharp) throw new Error(`image.resize: instance "${options.instance} doesn't exist.`);
  180. let width = this.parseOptional(cw(this.parse(options.width), sharp.metadata), 'number', null);
  181. let height = this.parseOptional(ch(this.parse(options.height), sharp.metadata), 'number', null);
  182. let upscale = this.parseOptional(options.upscale, 'boolean', false);
  183. if (isNaN(width)) width = null;
  184. if (isNaN(height)) height = null;
  185. sharp.image.resize(width, height, { fit: width && height ? 'fill' : 'cover', withoutEnlargement: !upscale });
  186. await updateImage(sharp);
  187. },
  188. crop: async function (options) {
  189. const sharp = this.req.image[options.instance];
  190. if (!sharp) throw new Error(`image.crop: instance "${options.instance} doesn't exist.`);
  191. let width = this.parseRequired(cw(this.parse(options.width)), 'number', 'image.crop: width is required.');
  192. let height = this.parseRequired(ch(this.parse(options.height)), 'number', 'image.crop: height is required.');
  193. if (width > sharp.metadata.width) width = sharp.metadata.width;
  194. if (height > sharp.metadata.height) height = sharp.metadata.height;
  195. let left = this.parseRequired(cx(this.parse(options.x), width, sharp.metadata), 'number', 'image.crop: x is required.');
  196. let top = this.parseRequired(cy(this.parse(options.y), height, sharp.metadata), 'number', 'image.crop: y is required.');
  197. sharp.image.extract({ left, top, width, height });
  198. await updateImage(sharp);
  199. },
  200. cover: async function (options) {
  201. const sharp = this.req.image[options.instance];
  202. if (!sharp) throw new Error(`image.cover: instance "${options.instance}" doesn't exist.`);
  203. let width = this.parseRequired(options.width, 'number', 'image.cover: width is required.');
  204. let height = this.parseRequired(options.height, 'number', 'image.cover: height is required.');
  205. // position: see positions object for options
  206. let position = this.parseOptional(options.position, 'string', 'center');
  207. // kernel: 'nearest', 'cubic', 'mitchell', 'lanczos2', 'lanczos3'
  208. let kernel = this.parseOptional(options.kernel, 'string', 'lanczos3');
  209. position = positions[position] || 0;
  210. sharp.image.resize({ width, height, position, kernel });
  211. await updateImage(sharp);
  212. },
  213. watermark: async function (options) {
  214. const sharp = this.req.image[options.instance];
  215. if (!sharp) throw new Error(`image.watermark: instance "${options.instance} doesn't exist.`);
  216. let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.watermark: path is required.'));
  217. let image = Sharp(path);
  218. let metadata = await image.metadata();
  219. let input = await image.toBuffer();
  220. let left = this.parseRequired(cx(this.parse(options.x), metadata.width, sharp.metadata), 'number', 'image.watermark: x is required.');
  221. let top = this.parseRequired(cy(this.parse(options.y), metadata.height, sharp.metadata), 'number', 'image.watermark: y is required.');
  222. sharp.image.composite([{ input, left, top }]);
  223. },
  224. text: async function (options) {
  225. const sharp = this.req.image[options.instance];
  226. if (!sharp) throw new Error(`image.text: instance "${options.instance} doesn't exist.`);
  227. let x = this.parse(options.x);
  228. let y = this.parse(options.y);
  229. let text = this.parseRequired(options.text, 'string', 'image.text: text is required.');
  230. let font = this.parseOptional(options.font, 'string', 'Verdana');
  231. let size = this.parseOptional(options.size, 'number', 24);
  232. let color = this.parseOptional(options.color, 'string', '#ffffff');
  233. let width = sharp.metadata.width;
  234. let height = sharp.metadata.height;
  235. let anchor = 'start';
  236. switch (x) {
  237. case 'left':
  238. x = '0%';
  239. anchor = 'start';
  240. break;
  241. case 'center':
  242. x = '50%';
  243. anchor = 'middle';
  244. break;
  245. case 'right':
  246. x = '100%';
  247. anchor = 'end';
  248. break;
  249. default:
  250. if (x < 0) {
  251. x = width - x;
  252. anchor = 'end';
  253. }
  254. }
  255. switch (y) {
  256. case 'top':
  257. y = size;
  258. break;
  259. case 'middle':
  260. y = (height / 2) - (size / 2);
  261. break;
  262. case 'bottom':
  263. y = height;
  264. break;
  265. default:
  266. if (y < 0) {
  267. y = height - size - y;
  268. }
  269. }
  270. let svg = `
  271. <svg width="${width}" height="${height}">
  272. <style>
  273. .text {
  274. fill: ${color};
  275. font-family: "${font}";
  276. font-size: ${size}px;
  277. line-height: 1;
  278. }
  279. </style>
  280. <text x="${x}" y="${y}" text-anchor="${anchor}" class="text">${text}</text>
  281. </svg>
  282. `;
  283. const input = await Sharp(Buffer.from(svg)).toBuffer();
  284. sharp.image.composite([{ input, left: 0, top: 0 }]);
  285. },
  286. tiled: async function (options) {
  287. const sharp = this.req.image[options.instance];
  288. if (!sharp) throw new Error(`image.tiled: instance "${options.instance} doesn't exist.`);
  289. let input = toSystemPath(this.parseRequired(options.path, 'string', 'image.tiled: path is required.'));
  290. let padding = this.parseOptional(options.padding, 'number', 0);
  291. if (padding) {
  292. input = await Sharp(input).extend({
  293. top: padding, left: padding, bottom: 0, right: 0, background: { r: 0, g: 0, b: 0, alpha: 0 }
  294. }).toBuffer();
  295. }
  296. sharp.image.composite([{ input, left: 0, top: 0, tile: true }]);
  297. },
  298. flip: async function (options) {
  299. const sharp = this.req.image[options.instance];
  300. if (!sharp) throw new Error(`image.flip: instance "${options.instance} doesn't exist.`);
  301. let horizontal = this.parseOptional(options.horizontal, 'boolean', false);
  302. let vertical = this.parseOptional(options.vertical, 'boolean', false);
  303. if (horizontal) sharp.image.flop();
  304. if (vertical) sharp.image.flip();
  305. },
  306. rotateLeft: async function (options) {
  307. const sharp = this.req.image[options.instance];
  308. if (!sharp) throw new Error(`image.rotateLeft: instance "${options.instance} doesn't exist.`);
  309. sharp.image.rotate(-90);
  310. await updateImage(sharp);
  311. },
  312. rotateRight: async function (options) {
  313. const sharp = this.req.image[options.instance];
  314. if (!sharp) throw new Error(`image.rotateRight: instance "${options.instance} doesn't exist.`);
  315. sharp.image.rotate(90);
  316. await updateImage(sharp);
  317. },
  318. smooth: async function (options) {
  319. const sharp = this.req.image[options.instance];
  320. if (!sharp) throw new Error(`image.smooth: instance "${options.instance} doesn't exist.`);
  321. sharp.image.convolve({
  322. width: 3,
  323. height: 3,
  324. kernel: [
  325. 1, 1, 1,
  326. 1, 1, 1,
  327. 1, 1, 1
  328. ]
  329. });
  330. },
  331. blur: async function (options) {
  332. const sharp = this.req.image[options.instance];
  333. if (!sharp) throw new Error(`image.blur: instance "${options.instance} doesn't exist.`);
  334. sharp.image.convolve({
  335. width: 3,
  336. height: 3,
  337. kernel: [
  338. 1, 2, 1,
  339. 2, 4, 2,
  340. 1, 2, 1
  341. ]
  342. });
  343. },
  344. sharpen: async function (options) {
  345. const sharp = this.req.image[options.instance];
  346. if (!sharp) throw new Error(`image.sharpen: instance "${options.instance} doesn't exist.`);
  347. sharp.image.convolve({
  348. width: 3,
  349. height: 3,
  350. kernel: [
  351. 0, -2, 0,
  352. -2, 15, -2,
  353. 0, -2, 0
  354. ]
  355. });
  356. },
  357. meanRemoval: async function (options) {
  358. const sharp = this.req.image[options.instance];
  359. if (!sharp) throw new Error(`image.meanRemoval: instance "${options.instance} doesn't exist.`);
  360. sharp.image.convolve({
  361. width: 3,
  362. height: 3,
  363. kernel: [
  364. -1, -1, -1,
  365. -1, 9, -1,
  366. -1, -1, -1
  367. ]
  368. });
  369. },
  370. emboss: async function (options) {
  371. const sharp = this.req.image[options.instance];
  372. if (!sharp) throw new Error(`image.emboss: instance "${options.instance} doesn't exist.`);
  373. sharp.image.convolve({
  374. width: 3,
  375. height: 3,
  376. kernel: [
  377. -1, 0, -1,
  378. 0, 4, 0,
  379. -1, 0, -1
  380. ],
  381. offset: 127
  382. });
  383. },
  384. edgeDetect: async function (options) {
  385. const sharp = this.req.image[options.instance];
  386. if (!sharp) throw new Error(`image.edgeDetect: instance "${options.instance} doesn't exist.`);
  387. sharp.image.convolve({
  388. width: 3,
  389. height: 3,
  390. kernel: [
  391. -1, -1, -1,
  392. 0, 0, 0,
  393. 1, 1, 1
  394. ],
  395. offset: 127
  396. });
  397. },
  398. grayscale: async function (options) {
  399. const sharp = this.req.image[options.instance];
  400. if (!sharp) throw new Error(`image.grayscale: instance "${options.instance} doesn't exist.`);
  401. sharp.image.grayscale();
  402. },
  403. sepia: async function (options) {
  404. const sharp = this.req.image[options.instance];
  405. if (!sharp) throw new Error(`image.sepia: instance "${options.instance} doesn't exist.`);
  406. sharp.image.tint({ r: 112, g: 66, b: 20 });
  407. },
  408. invert: async function (options) {
  409. const sharp = this.req.image[options.instance];
  410. if (!sharp) throw new Error(`image.invert: instance "${options.instance} doesn't exist.`);
  411. sharp.image.negate();
  412. },
  413. };