123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- /*!
- * psd2fgui
- * @license [MIT]
- * @copyright http://www.fairygui.com/
- */
- "use strict";
- const PSD = require('psd');
- const fs = require('fs-extra');
- const path = require('path');
- const crypto = require('crypto');
- const archiver = require('archiver');
- const xmlbuilder = require('xmlbuilder');
- //The group name prefix identified as a component.
- const componentPrefix = 'Com';
- //The group name prefix identified as a common button.
- const commonButtonPrefix = 'Button';
- //The group name prefix identified as a checkbox button.
- const checkButtonPrefix = 'CheckButton';
- //The group name prefix identified as a radio button.
- const radioButtonPrefix = 'RadioButton';
- //The layer name suffix of each status of the button.
- const buttonStatusSuffix = ['@up', '@down', '@over', '@selectedOver'];
- exports.constants = {
- NO_PACK: 1,
- IGNORE_FONT: 2
- };
- var targetPackage;
- /**
- * Convert a PSD file to a fairygui package.
- * @param {string} psdFile path of the psd file.
- * @param {string} outputFile optional. output file path.
- * @param {integer} option psd2fgui.constants.
- * @param {string} buildId optinal. You can use same build id to keep resource ids unchanged during multiple converting for a psd file.
- * @return {string} output file path.
- */
- exports.convert = function (psdFile, outputFile, option, buildId) {
- return new Promise(function (resolve, reject) {
- if (!option)
- option = 0;
- if (!buildId)
- buildId = genBuildId();
- var pathInfo = path.parse(psdFile);
- var outputDirectory;
- if (option & exports.constants.NO_PACK) {
- outputDirectory = outputFile;
- if (!outputDirectory)
- outputDirectory = path.join(pathInfo.dir, pathInfo.name + '-fairypackage');
- }
- else {
- outputDirectory = path.join(pathInfo.dir, pathInfo.name + '~temp');
- fs.emptyDirSync(outputDirectory);
- if (!outputFile)
- outputFile = path.join(pathInfo.dir, pathInfo.name + '.fairypackage');
- }
- var psd = PSD.fromFile(psdFile);
- psd.parse();
- targetPackage = new UIPackage(outputDirectory, buildId);
- targetPackage.exportOption = option;
- createComponent(psd.tree(), pathInfo.name);
- var pkgDesc = xmlbuilder.create('packageDescription');
- pkgDesc.att('id', targetPackage.id);
- var resourcesNode = pkgDesc.ele('resources');
- var savePromises = [];
- targetPackage.resources.forEach(function (item) {
- var resNode = resourcesNode.ele(item.type);
- resNode.att('id', item.id).att('name', item.name).att('path', '/');
- if (item.type == 'image') {
- if (item.scale9Grid) {
- resNode.att('scale', item.scale);
- resNode.att('scale9Grid', item.scale9Grid);
- }
- }
- if (item.type == 'image')
- savePromises.push(item.data.saveAsPng(path.join(targetPackage.basePath, item.name)));
- else
- savePromises.push(fs.writeFile(path.join(targetPackage.basePath, item.name), item.data));
- });
- savePromises.push(fs.writeFile(path.join(targetPackage.basePath, 'package.xml'),
- pkgDesc.end({ pretty: true })));
- var pa = Promise.all(savePromises);
- if (option & exports.constants.NO_PACK) {
- pa.then(function () {
- console.log(psdFile + '->' + outputDirectory);
- resolve(buildId);
- }).catch(function (reason) {
- reject(reason);
- });
- }
- else {
- pa.then(function () {
- return fs.readdir(outputDirectory);
- }).then(function (files) {
- var output = fs.createWriteStream(outputFile);
- output.on('close', function () {
- fs.emptyDirSync(outputDirectory);
- fs.rmdirSync(outputDirectory);
- console.log(psdFile + '->' + outputFile);
- resolve(buildId);
- });
- var zipArchiver = archiver('zip');
- zipArchiver.pipe(output);
- files.forEach(function (ff) {
- zipArchiver.file(path.join(outputDirectory, ff), { 'name': ff });
- });
- zipArchiver.finalize();
- }).catch(function (reason) {
- reject(reason);
- });
- }
- });
- }
- //=====================================================================================
- function UIPackage(basePath, buildId) {
- this.id = buildId.substr(0, 8);
- this.itemIdBase = buildId.substr(8);
- this.nextItemIndex = 0;
- this.getNextItemId = function () {
- return this.itemIdBase + (this.nextItemIndex++).toString(36);
- };
- this.basePath = basePath;
- fs.ensureDirSync(basePath);
- this.resources = [];
- this.sameDataTestHelper = {};
- this.sameNameTestHelper = {};
- }
- function createImage(aNode, scale9Grid) {
- var packageItem = createPackageItem('image', aNode.get('name') + '.png', aNode);
- if (scale9Grid) {
- packageItem.scale = '9grid';
- packageItem.scale9Grid = scale9Grid;
- }
- return packageItem;
- }
- function createComponent(aNode, name) {
- var component = xmlbuilder.create('component');
- component.att('size', aNode.get('width') + ',' + aNode.get('height'));
- var displayList = component.ele('displayList');
- var cnt = aNode.children().length;
- for (var i = cnt - 1; i >= 0; i--) {
- parseNode(aNode.children()[i], aNode, displayList);
- }
- return createPackageItem('component', (name ? name : aNode.get('name')) + '.xml', component.end({ pretty: true }));
- }
- function createButton(aNode, instProps) {
- var component = xmlbuilder.create('component');
- component.att('size', aNode.get('width') + ',' + aNode.get('height'));
- component.att('extention', 'Button');
- var images = [];
- var imagePages = [];
- var imageCnt = 0;
- aNode.descendants().forEach(function (childNode) {
- var nodeName = childNode.get('name');
- for (var i in buttonStatusSuffix) {
- if (nodeName.indexOf(buttonStatusSuffix[i]) != -1) {
- images[i] = childNode;
- imageCnt++;
- }
- };
- });
- for (var i in buttonStatusSuffix) {
- imagePages[i] = [];
- if (!images[i]) {
- if (i == 3 && images[1]) //if no 'selectedOver', use 'down'
- imagePages[1].push(i);
- else //or else, use 'up'
- imagePages[0].push(i);
- }
- else {
- imagePages[i].push(i);
- }
- }
- var onElementCallback = function (child, node) {
- var nodeName = node.get('name');
- var j = images.indexOf(node);
- if (j != -1) {
- var gear = child.ele('gearDisplay');
- gear.att('controller', 'button');
- gear.att('pages', imagePages[j].join(','));
- }
- if (nodeName.indexOf('@title') != -1) {
- if (child.attributes['text']) {
- instProps['@title'] = child.attributes['text'].value;
- child.removeAttribute('text');
- }
- }
- else if (nodeName.indexOf('@icon') != -1) {
- if (child.attributes['url']) {
- instProps['@icon'] = child.attributes['url'].value;
- child.removeAttribute('url');
- }
- }
- };
- var controller = component.ele('controller');
- controller.att('name', 'button');
- controller.att('pages', '0,up,1,down,2,over,3,selectedOver');
- var displayList = component.ele('displayList');
- var cnt = aNode.children().length;
- for (i = cnt - 1; i >= 0; i--) {
- parseNode(aNode.children()[i], aNode, displayList, onElementCallback);
- }
- var extension = component.ele('Button');
- if (aNode.get('name').indexOf(checkButtonPrefix) == 0) {
- extension.att('mode', 'Check');
- instProps['@checked'] = 'true';
- }
- else if (aNode.get('name').indexOf(radioButtonPrefix) == 0)
- extension.att('mode', 'Radio');
- if (imageCnt == 1) {
- extension.att('downEffect', 'scale');
- extension.att('downEffectValue', '1.1');
- }
- return createPackageItem('component', aNode.get('name') + '.xml', component.end({ pretty: true }));
- }
- function createPackageItem(type, fileName, data) {
- var dataForHash;
- if (type == 'image') //data should a psd layer
- dataForHash = Buffer.from(data.get('image').pixelData);
- else
- dataForHash = data;
- var hash = crypto.createHash('md5').update(dataForHash).digest('hex');
- var item = targetPackage.sameDataTestHelper[hash];
- if (!item) {
- item = {};
- item.type = type;
- item.id = targetPackage.getNextItemId();
- var i = fileName.lastIndexOf('.');
- var basename = fileName.substr(0, i);
- var ext = fileName.substr(i);
- basename = basename.replace(/[\@\'\"\\\/\b\f\n\r\t\$\%\*\:\?\<\>\|]/g, '_');
- while (true) {
- var j = targetPackage.sameNameTestHelper[basename];
- if (j == undefined) {
- targetPackage.sameNameTestHelper[basename] = 1;
- break;
- }
- else {
- targetPackage.sameNameTestHelper[basename] = j + 1;
- basename = basename + '_' + j;
- }
- }
- fileName = basename + ext;
- item.name = fileName;
- item.data = data;
- targetPackage.resources.push(item);
- targetPackage.sameDataTestHelper[hash] = item;
- }
- return item;
- }
- function parseNode(aNode, rootNode, displayList, onElementCallback) {
- var child;
- var packageItem;
- var instProps;
- var str;
- var nodeName = aNode.get('name');
- var specialUsage;
- if (nodeName.indexOf('@title') != -1)
- specialUsage = 'title';
- else if (nodeName.indexOf('@icon') != -1)
- specialUsage = 'icon';
- if (aNode.isGroup()) {
- if (nodeName.indexOf(componentPrefix) == 0) {
- packageItem = createComponent(aNode);
- child = xmlbuilder.create('component');
- str = 'n' + (displayList.children.length + 1);
- child.att('id', str + '_' + targetPackage.itemIdBase);
- child.att('name', specialUsage ? specialUsage : str);
- child.att('src', packageItem.id);
- child.att('fileName', packageItem.name);
- child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
- }
- else if (nodeName.indexOf(commonButtonPrefix) == 0 || nodeName.indexOf(checkButtonPrefix) == 0 || nodeName.indexOf(radioButtonPrefix) == 0) {
- instProps = {};
- packageItem = createButton(aNode, instProps);
- child = xmlbuilder.create('component');
- str = 'n' + (displayList.children.length + 1);
- child.att('id', str + '_' + targetPackage.itemIdBase);
- child.att('name', specialUsage ? specialUsage : str);
- child.att('src', packageItem.id);
- child.att('fileName', packageItem.name);
- child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
- child.ele({ Button: instProps });
- }
- else {
- var cnt = aNode.children().length;
- for (var i = cnt - 1; i >= 0; i--)
- parseNode(aNode.children()[i], rootNode, displayList, onElementCallback);
- }
- }
- else {
- var typeTool = aNode.get('typeTool');
- if (typeTool) {
- child = xmlbuilder.create('text');
- str = 'n' + (displayList.children.length + 1);
- child.att('id', str + '_' + targetPackage.itemIdBase);
- child.att('name', specialUsage ? specialUsage : str);
- child.att('text', typeTool.textValue);
- if (specialUsage == 'title') {
- child.att('xy', '0,' + (aNode.top - rootNode.top - 4));
- child.att('size', rootNode.width + ',' + (aNode.height + 8));
- child.att('align', 'center');
- }
- else {
- child.att('xy', (aNode.left - rootNode.left - 4) + ',' + (aNode.top - rootNode.top - 4));
- child.att('size', (aNode.width + 8) + ',' + (aNode.height + 8));
- str = typeTool.alignment()[0];
- if (str != 'left')
- child.att('align', str);
- }
- child.att('vAlign', 'middle');
- child.att('autoSize', 'none');
- if (!(targetPackage.exportOption & exports.constants.IGNORE_FONT))
- child.att('font', typeTool.fonts()[0]);
- child.att('fontSize', typeTool.sizes()[0]);
- child.att('color', convertToHtmlColor(typeTool.colors()[0]));
- }
- else if (!aNode.isEmpty()) {
- packageItem = createImage(aNode);
- if (specialUsage == 'icon')
- child = xmlbuilder.create('loader');
- else
- child = xmlbuilder.create('image');
- str = 'n' + (displayList.children.length + 1);
- child.att('id', str + '_' + targetPackage.itemIdBase);
- child.att('name', specialUsage ? specialUsage : str);
- child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
- if (specialUsage == 'icon') {
- child.att('size', aNode.width + ',' + aNode.height);
- child.att('url', 'ui://' + targetPackage.id + packageItem.id);
- }
- else
- child.att('src', packageItem.id);
- child.att('fileName', packageItem.name);
- }
- }
- if (child) {
- var opacity = aNode.get('opacity');
- if (opacity < 255)
- child.att('alpha', (opacity / 255).toFixed(2));
- if (onElementCallback)
- onElementCallback(child, aNode);
- displayList.importDocument(child);
- }
- return child;
- }
- //=====================================================================================
- function genBuildId() {
- var magicNumber = Math.floor(Math.random() * 36).toString(36).substr(0, 1);
- var s1 = '0000' + Math.floor(Math.random() * 1679616).toString(36);
- var s2 = '000' + Math.floor(Math.random() * 46656).toString(36);
- var count = 0;
- for (var i = 0; i < 4; i++) {
- var c = Math.floor(Math.random() * 26);
- count += Math.pow(26, i) * (c + 10);
- }
- count += Math.floor(Math.random() * 1000000) + Math.floor(Math.random() * 222640);
- return magicNumber + s1.substr(s1.length - 4) + s2.substr(s2.length - 3) + count.toString(36);
- }
- function convertToHtmlColor(rgbaArray, includingAlpha) {
- var result = '#';
- var str;
- if (includingAlpha) {
- str = rgbaArray[3].toString(16);
- if (str.length == 1)
- str = '0' + str;
- result += str;
- }
- for (var i = 0; i < 3; i++) {
- str = rgbaArray[i].toString(16);
- if (str.length == 1)
- str = '0' + str;
- result += str;
- }
- return result;
- }
|