lib.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. /*!
  2. * psd2fgui
  3. * @license [MIT]
  4. * @copyright http://www.fairygui.com/
  5. */
  6. "use strict";
  7. const PSD = require('psd');
  8. const fs = require('fs-extra');
  9. const path = require('path');
  10. const crypto = require('crypto');
  11. const archiver = require('archiver');
  12. const xmlbuilder = require('xmlbuilder');
  13. //The group name prefix identified as a component.
  14. const componentPrefix = 'Com';
  15. //The group name prefix identified as a common button.
  16. const commonButtonPrefix = 'Button';
  17. //The group name prefix identified as a checkbox button.
  18. const checkButtonPrefix = 'CheckButton';
  19. //The group name prefix identified as a radio button.
  20. const radioButtonPrefix = 'RadioButton';
  21. //The layer name suffix of each status of the button.
  22. const buttonStatusSuffix = ['@up', '@down', '@over', '@selectedOver'];
  23. exports.constants = {
  24. NO_PACK: 1,
  25. IGNORE_FONT: 2
  26. };
  27. var targetPackage;
  28. /**
  29. * Convert a PSD file to a fairygui package.
  30. * @param {string} psdFile path of the psd file.
  31. * @param {string} outputFile optional. output file path.
  32. * @param {integer} option psd2fgui.constants.
  33. * @param {string} buildId optinal. You can use same build id to keep resource ids unchanged during multiple converting for a psd file.
  34. * @return {string} output file path.
  35. */
  36. exports.convert = function (psdFile, outputFile, option, buildId) {
  37. return new Promise(function (resolve, reject) {
  38. if (!option)
  39. option = 0;
  40. if (!buildId)
  41. buildId = genBuildId();
  42. var pathInfo = path.parse(psdFile);
  43. var outputDirectory;
  44. if (option & exports.constants.NO_PACK) {
  45. outputDirectory = outputFile;
  46. if (!outputDirectory)
  47. outputDirectory = path.join(pathInfo.dir, pathInfo.name + '-fairypackage');
  48. }
  49. else {
  50. outputDirectory = path.join(pathInfo.dir, pathInfo.name + '~temp');
  51. fs.emptyDirSync(outputDirectory);
  52. if (!outputFile)
  53. outputFile = path.join(pathInfo.dir, pathInfo.name + '.fairypackage');
  54. }
  55. var psd = PSD.fromFile(psdFile);
  56. psd.parse();
  57. targetPackage = new UIPackage(outputDirectory, buildId);
  58. targetPackage.exportOption = option;
  59. createComponent(psd.tree(), pathInfo.name);
  60. var pkgDesc = xmlbuilder.create('packageDescription');
  61. pkgDesc.att('id', targetPackage.id);
  62. var resourcesNode = pkgDesc.ele('resources');
  63. var savePromises = [];
  64. targetPackage.resources.forEach(function (item) {
  65. var resNode = resourcesNode.ele(item.type);
  66. resNode.att('id', item.id).att('name', item.name).att('path', '/');
  67. if (item.type == 'image') {
  68. if (item.scale9Grid) {
  69. resNode.att('scale', item.scale);
  70. resNode.att('scale9Grid', item.scale9Grid);
  71. }
  72. }
  73. if (item.type == 'image')
  74. savePromises.push(item.data.saveAsPng(path.join(targetPackage.basePath, item.name)));
  75. else
  76. savePromises.push(fs.writeFile(path.join(targetPackage.basePath, item.name), item.data));
  77. });
  78. savePromises.push(fs.writeFile(path.join(targetPackage.basePath, 'package.xml'),
  79. pkgDesc.end({ pretty: true })));
  80. var pa = Promise.all(savePromises);
  81. if (option & exports.constants.NO_PACK) {
  82. pa.then(function () {
  83. console.log(psdFile + '->' + outputDirectory);
  84. resolve(buildId);
  85. }).catch(function (reason) {
  86. reject(reason);
  87. });
  88. }
  89. else {
  90. pa.then(function () {
  91. return fs.readdir(outputDirectory);
  92. }).then(function (files) {
  93. var output = fs.createWriteStream(outputFile);
  94. output.on('close', function () {
  95. fs.emptyDirSync(outputDirectory);
  96. fs.rmdirSync(outputDirectory);
  97. console.log(psdFile + '->' + outputFile);
  98. resolve(buildId);
  99. });
  100. var zipArchiver = archiver('zip');
  101. zipArchiver.pipe(output);
  102. files.forEach(function (ff) {
  103. zipArchiver.file(path.join(outputDirectory, ff), { 'name': ff });
  104. });
  105. zipArchiver.finalize();
  106. }).catch(function (reason) {
  107. reject(reason);
  108. });
  109. }
  110. });
  111. }
  112. //=====================================================================================
  113. function UIPackage(basePath, buildId) {
  114. this.id = buildId.substr(0, 8);
  115. this.itemIdBase = buildId.substr(8);
  116. this.nextItemIndex = 0;
  117. this.getNextItemId = function () {
  118. return this.itemIdBase + (this.nextItemIndex++).toString(36);
  119. };
  120. this.basePath = basePath;
  121. fs.ensureDirSync(basePath);
  122. this.resources = [];
  123. this.sameDataTestHelper = {};
  124. this.sameNameTestHelper = {};
  125. }
  126. function createImage(aNode, scale9Grid) {
  127. var packageItem = createPackageItem('image', aNode.get('name') + '.png', aNode);
  128. if (scale9Grid) {
  129. packageItem.scale = '9grid';
  130. packageItem.scale9Grid = scale9Grid;
  131. }
  132. return packageItem;
  133. }
  134. function createComponent(aNode, name) {
  135. var component = xmlbuilder.create('component');
  136. component.att('size', aNode.get('width') + ',' + aNode.get('height'));
  137. var displayList = component.ele('displayList');
  138. var cnt = aNode.children().length;
  139. for (var i = cnt - 1; i >= 0; i--) {
  140. parseNode(aNode.children()[i], aNode, displayList);
  141. }
  142. return createPackageItem('component', (name ? name : aNode.get('name')) + '.xml', component.end({ pretty: true }));
  143. }
  144. function createButton(aNode, instProps) {
  145. var component = xmlbuilder.create('component');
  146. component.att('size', aNode.get('width') + ',' + aNode.get('height'));
  147. component.att('extention', 'Button');
  148. var images = [];
  149. var imagePages = [];
  150. var imageCnt = 0;
  151. aNode.descendants().forEach(function (childNode) {
  152. var nodeName = childNode.get('name');
  153. for (var i in buttonStatusSuffix) {
  154. if (nodeName.indexOf(buttonStatusSuffix[i]) != -1) {
  155. images[i] = childNode;
  156. imageCnt++;
  157. }
  158. };
  159. });
  160. for (var i in buttonStatusSuffix) {
  161. imagePages[i] = [];
  162. if (!images[i]) {
  163. if (i == 3 && images[1]) //if no 'selectedOver', use 'down'
  164. imagePages[1].push(i);
  165. else //or else, use 'up'
  166. imagePages[0].push(i);
  167. }
  168. else {
  169. imagePages[i].push(i);
  170. }
  171. }
  172. var onElementCallback = function (child, node) {
  173. var nodeName = node.get('name');
  174. var j = images.indexOf(node);
  175. if (j != -1) {
  176. var gear = child.ele('gearDisplay');
  177. gear.att('controller', 'button');
  178. gear.att('pages', imagePages[j].join(','));
  179. }
  180. if (nodeName.indexOf('@title') != -1) {
  181. if (child.attributes['text']) {
  182. instProps['@title'] = child.attributes['text'].value;
  183. child.removeAttribute('text');
  184. }
  185. }
  186. else if (nodeName.indexOf('@icon') != -1) {
  187. if (child.attributes['url']) {
  188. instProps['@icon'] = child.attributes['url'].value;
  189. child.removeAttribute('url');
  190. }
  191. }
  192. };
  193. var controller = component.ele('controller');
  194. controller.att('name', 'button');
  195. controller.att('pages', '0,up,1,down,2,over,3,selectedOver');
  196. var displayList = component.ele('displayList');
  197. var cnt = aNode.children().length;
  198. for (i = cnt - 1; i >= 0; i--) {
  199. parseNode(aNode.children()[i], aNode, displayList, onElementCallback);
  200. }
  201. var extension = component.ele('Button');
  202. if (aNode.get('name').indexOf(checkButtonPrefix) == 0) {
  203. extension.att('mode', 'Check');
  204. instProps['@checked'] = 'true';
  205. }
  206. else if (aNode.get('name').indexOf(radioButtonPrefix) == 0)
  207. extension.att('mode', 'Radio');
  208. if (imageCnt == 1) {
  209. extension.att('downEffect', 'scale');
  210. extension.att('downEffectValue', '1.1');
  211. }
  212. return createPackageItem('component', aNode.get('name') + '.xml', component.end({ pretty: true }));
  213. }
  214. function createPackageItem(type, fileName, data) {
  215. var dataForHash;
  216. if (type == 'image') //data should a psd layer
  217. dataForHash = Buffer.from(data.get('image').pixelData);
  218. else
  219. dataForHash = data;
  220. var hash = crypto.createHash('md5').update(dataForHash).digest('hex');
  221. var item = targetPackage.sameDataTestHelper[hash];
  222. if (!item) {
  223. item = {};
  224. item.type = type;
  225. item.id = targetPackage.getNextItemId();
  226. var i = fileName.lastIndexOf('.');
  227. var basename = fileName.substr(0, i);
  228. var ext = fileName.substr(i);
  229. basename = basename.replace(/[\@\'\"\\\/\b\f\n\r\t\$\%\*\:\?\<\>\|]/g, '_');
  230. while (true) {
  231. var j = targetPackage.sameNameTestHelper[basename];
  232. if (j == undefined) {
  233. targetPackage.sameNameTestHelper[basename] = 1;
  234. break;
  235. }
  236. else {
  237. targetPackage.sameNameTestHelper[basename] = j + 1;
  238. basename = basename + '_' + j;
  239. }
  240. }
  241. fileName = basename + ext;
  242. item.name = fileName;
  243. item.data = data;
  244. targetPackage.resources.push(item);
  245. targetPackage.sameDataTestHelper[hash] = item;
  246. }
  247. return item;
  248. }
  249. function parseNode(aNode, rootNode, displayList, onElementCallback) {
  250. var child;
  251. var packageItem;
  252. var instProps;
  253. var str;
  254. var nodeName = aNode.get('name');
  255. var specialUsage;
  256. if (nodeName.indexOf('@title') != -1)
  257. specialUsage = 'title';
  258. else if (nodeName.indexOf('@icon') != -1)
  259. specialUsage = 'icon';
  260. if (aNode.isGroup()) {
  261. if (nodeName.indexOf(componentPrefix) == 0) {
  262. packageItem = createComponent(aNode);
  263. child = xmlbuilder.create('component');
  264. str = 'n' + (displayList.children.length + 1);
  265. child.att('id', str + '_' + targetPackage.itemIdBase);
  266. child.att('name', specialUsage ? specialUsage : str);
  267. child.att('src', packageItem.id);
  268. child.att('fileName', packageItem.name);
  269. child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
  270. }
  271. else if (nodeName.indexOf(commonButtonPrefix) == 0 || nodeName.indexOf(checkButtonPrefix) == 0 || nodeName.indexOf(radioButtonPrefix) == 0) {
  272. instProps = {};
  273. packageItem = createButton(aNode, instProps);
  274. child = xmlbuilder.create('component');
  275. str = 'n' + (displayList.children.length + 1);
  276. child.att('id', str + '_' + targetPackage.itemIdBase);
  277. child.att('name', specialUsage ? specialUsage : str);
  278. child.att('src', packageItem.id);
  279. child.att('fileName', packageItem.name);
  280. child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
  281. child.ele({ Button: instProps });
  282. }
  283. else {
  284. var cnt = aNode.children().length;
  285. for (var i = cnt - 1; i >= 0; i--)
  286. parseNode(aNode.children()[i], rootNode, displayList, onElementCallback);
  287. }
  288. }
  289. else {
  290. var typeTool = aNode.get('typeTool');
  291. if (typeTool) {
  292. child = xmlbuilder.create('text');
  293. str = 'n' + (displayList.children.length + 1);
  294. child.att('id', str + '_' + targetPackage.itemIdBase);
  295. child.att('name', specialUsage ? specialUsage : str);
  296. child.att('text', typeTool.textValue);
  297. if (specialUsage == 'title') {
  298. child.att('xy', '0,' + (aNode.top - rootNode.top - 4));
  299. child.att('size', rootNode.width + ',' + (aNode.height + 8));
  300. child.att('align', 'center');
  301. }
  302. else {
  303. child.att('xy', (aNode.left - rootNode.left - 4) + ',' + (aNode.top - rootNode.top - 4));
  304. child.att('size', (aNode.width + 8) + ',' + (aNode.height + 8));
  305. str = typeTool.alignment()[0];
  306. if (str != 'left')
  307. child.att('align', str);
  308. }
  309. child.att('vAlign', 'middle');
  310. child.att('autoSize', 'none');
  311. if (!(targetPackage.exportOption & exports.constants.IGNORE_FONT))
  312. child.att('font', typeTool.fonts()[0]);
  313. child.att('fontSize', typeTool.sizes()[0]);
  314. child.att('color', convertToHtmlColor(typeTool.colors()[0]));
  315. }
  316. else if (!aNode.isEmpty()) {
  317. packageItem = createImage(aNode);
  318. if (specialUsage == 'icon')
  319. child = xmlbuilder.create('loader');
  320. else
  321. child = xmlbuilder.create('image');
  322. str = 'n' + (displayList.children.length + 1);
  323. child.att('id', str + '_' + targetPackage.itemIdBase);
  324. child.att('name', specialUsage ? specialUsage : str);
  325. child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
  326. if (specialUsage == 'icon') {
  327. child.att('size', aNode.width + ',' + aNode.height);
  328. child.att('url', 'ui://' + targetPackage.id + packageItem.id);
  329. }
  330. else
  331. child.att('src', packageItem.id);
  332. child.att('fileName', packageItem.name);
  333. }
  334. }
  335. if (child) {
  336. var opacity = aNode.get('opacity');
  337. if (opacity < 255)
  338. child.att('alpha', (opacity / 255).toFixed(2));
  339. if (onElementCallback)
  340. onElementCallback(child, aNode);
  341. displayList.importDocument(child);
  342. }
  343. return child;
  344. }
  345. //=====================================================================================
  346. function genBuildId() {
  347. var magicNumber = Math.floor(Math.random() * 36).toString(36).substr(0, 1);
  348. var s1 = '0000' + Math.floor(Math.random() * 1679616).toString(36);
  349. var s2 = '000' + Math.floor(Math.random() * 46656).toString(36);
  350. var count = 0;
  351. for (var i = 0; i < 4; i++) {
  352. var c = Math.floor(Math.random() * 26);
  353. count += Math.pow(26, i) * (c + 10);
  354. }
  355. count += Math.floor(Math.random() * 1000000) + Math.floor(Math.random() * 222640);
  356. return magicNumber + s1.substr(s1.length - 4) + s2.substr(s2.length - 3) + count.toString(36);
  357. }
  358. function convertToHtmlColor(rgbaArray, includingAlpha) {
  359. var result = '#';
  360. var str;
  361. if (includingAlpha) {
  362. str = rgbaArray[3].toString(16);
  363. if (str.length == 1)
  364. str = '0' + str;
  365. result += str;
  366. }
  367. for (var i = 0; i < 3; i++) {
  368. str = rgbaArray[i].toString(16);
  369. if (str.length == 1)
  370. str = '0' + str;
  371. result += str;
  372. }
  373. return result;
  374. }