自动图片生成在前端开发中的一些尝试

来源:互联网 发布:js设置select选中 编辑:程序博客网 时间:2024/06/16 09:55

图片处理在前端开发过程中占据了不少的时间,很是累人。在本文中我们不讨论如何提高切图的效率,我们讨论另一个问题:如何处理设计稿中的一些简单图形。不知道你又没有遇到过这种烦恼:“设计师给你的精致的PSD中有一个简单图形,就是那用用多边形、圆形和线条组成的图形。这个图形用css3实现不了,或者能实现,但为了兼容某些浏览器不能用css3来实现,只能切图。好的,你很快切完了并在样式中引用了。但没过多久需求上要求改下图片的颜色,而这是你已经将导出图片的那个PSD删除了,你还得重新在设计稿中把这个图形抠出来”。或者来一个更变态的版本,你做的项目支持换肤功能,在不同的皮肤下面这个图形会有不同的颜色。那么就需要你将这个图片导出为几个不同颜色的版本。如果后期有颜色的修改,那么你就需要重新一一修改并导出,如果这时用于导出图片的psd没有了,你就得重新从设计稿中扣出来,然后在一一修改后导出图片,稍不注意还可能造成两次图片的尺寸和位置不一样,造成错位。如果你没有遇到过上述情况,你应该是幸运的,而我两种都遇到过,在我做这些繁琐的修改的时候内心总是处于千万只羊驼奔跑的场景。

难道就没有一种办法来简化这中操作吗?能不能像修改css一样,只修改几个参数就完成对图片的修改呢?要完成这一目标我们必须完成两项工作:1. 如何用文本来描述这些简单图形 2. 如何将用文本描述的简单图形转换为图片。对于第一项工作比较好办, 用svg来完成就可以了,但第二项工作该如何实现呢?

如何将svg转换为图片

因为我们用SVG描述的图形一般情况下都需要和页面的其他元素融合在一起,所以必须将我们的SVG图形转换为支持透明的png格式。那么如何实现svg转png呢?在网上搜索了很久一直没有特别理想的。找到的解决方案基本就两个套路,要么用命令行调用phantom(也有通过canvas来实现的,但canvas的支持还是基于phantom),要么用命令行调用GraphicsMagick(简称gm)。而这两个软件在体积上都是巨无霸,安装起来非常繁琐尤其是在windows下面,对svg的支持也不是太友好。phantom需要借助canvas来实现SVG转png,太折腾。而gm需要安装第三方库才能处理svg, 而处理后的图片效果也不太好,我的测试图片的透明部分被填充了白色,透明都没处理好其他特性就没心情测试了,估计好不到那里去。

至此研究进入了死胡同,没能继续推进,直到有一个在调研另一个项目的某个小功能的技术可行性时才意外的有所突破。当时我想做一个小工具以实现将一个非常长的图片进行切割后上传,以便于在手机端进行lazyload. 因为知道nodejs处理图片方面比较渣,所以很识趣的没有使用nodejs来写而是改用了java。得益于java良好的生态系统很容易完成了我想要的功能。完成之后我不禁想,我这个功能为什么不用java来试试呢?在Google上搜索"java svg to png", 立即有了结果,Apache下有个专门处理svg的库batik。我原来有个很好的同事总跟我说“解决一个问题最难的是如何将自己的问题转换为合适的搜索关键词”。至此真是深有体会。

batik不仅对SVG的支持非常好,功能也非常强大,不仅能通过DOM API来操作SVG文档,而且还提供了将svg转换为其他格式、svg中嵌入js代码等很多非常实用的功能。官方的jar包中有一个可以在命令行中运行,可以完成将svg图片转换为其他格式,使用起来也非常简单:

java -jar batik-rasterizer.jar foo.svg

这恰好是我们需要的功能。至此基于batik的解决方案应该是目前为止最完美的,jar文件不需要单独安装就可以运行,而对目录结构也没有要求。完全可以把jar文件和js文件放到一个目录下然后和js代码一起发布。虽然也需要安装java运行环境,但java运行环境的安装相对来说很容易,而且公司每台电脑都安装了,所以这个条件是可以接受的。

导出可独立运行jar文件

batik自带的那个可以在命令行下运行的jar文件虽然能满足我们的但还是有两个地方存在不足。首先是这个jar文件不能独立运行,对项目中的其他jar文件有依赖,必须把它依赖的jar文件按特定的目录结构存放才能运行。这么多文件凌乱的放到一起,看着非常不爽。还有一点让人不爽的是这个jar文件只能通过命令行指定要操作的svg文件, 不支持通过命令行指定要转换的svg代码,所以你不能丢一个svg字符串让他处理,必须把字符串写到一个临时文件中。这很不方便和其他构建工具进行集成,而且而向代码目录中创建和删除文件可能会触发grunt的watch任务造成不必要的编译。基于这两个不能忍受的缺点,所以我们不能使用他自带的那个jar文件,需要自己编写一个可独立运行且支持通过命令行设置要转换的svg代码的jar文件。结合网上的资料整个功能很容易实现,下面是代码实现:

package myless.func;import java.io.ByteArrayInputStream;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import org.apache.batik.transcoder.TranscoderInput;import org.apache.batik.transcoder.TranscoderOutput;import org.apache.batik.transcoder.image.PNGTranscoder;import org.apache.xerces.impl.dv.util.Base64;public class Converter {    public static void main(String... args) throws Exception{           // 为了避免ms dos下蛋疼的编码问题,对参数做了base64编码        byte[] svg_code  = Base64.decode(args[0]);              String save_path = new String(Base64.decode(args[1]), "utf-8");        if(args.length >= 3 && args[2].toLowerCase().equals("--show-debug")){            System.out.println("svg content: \n" + new String(svg_code, "utf-8"));            System.out.println("save path  : \n" + save_path);        }           InputStream  svg_stream = new ByteArrayInputStream(svg_code);        OutputStream png_stream = new FileOutputStream(save_path);        TranscoderInput  input_image  = new TranscoderInput(svg_stream);                TranscoderOutput output_image = new TranscoderOutput(png_stream);         PNGTranscoder transcoder = new PNGTranscoder();         transcoder.transcode(input_image, output_image);        svg_stream.close();        png_stream.flush();        png_stream.close();    }}

使用了eclipse的export功能完成了导出可执行jar文件,具体操作步骤可以参考百度经验的这篇文章

将转换功能集成到less

一旦jar文件能够独立运行就很容易被nodejs使用了,因为nodejs本身支持通过命令行调用系统命令。而且0.12.0版本之后添加了同步调用命令行的功能。因此我们可以这样实现用nodejs来完成将SVG图片转换为png图片的功能。

  • nodejs程序通过当前文件的路径计算出需要调用的jar文件路径
  • nodejs生成需要转换的SVG代码并base64编码
  • nodejs设置转换后的图片的保存路径并base64编码
  • nodejs以同步方式调用命令行:
java -jar convert.jar base64-encode-svg base64-encode-save-path

下面的代码是grunt-myless中的转换函数实现。因为转换的过程比较耗时,为了提高总体的编译进度做了判断是否需要重现转换的逻辑,只有svg代码的md5值发生了变化才会重新转换。

/** * @fileOverview 将SVG代码转换为png文件. * @param   String    转换后文件保存路径 * @param   String... svg属性和svg代码 * @return  String    保存绝对地址 * @example *  div { *     background: svg-to-png('width=60px', 'height=30px', 'baseb4-encode=true', <<<EOF *        <!-- add you svg code here-->  *     EOF) center center no-repeat;     *  } */var fs = require('fs');var path = require('path');var crypto = require('crypto');var child_proc = require('child_process');var jarPath = path.join(__dirname, '../jar/svg-to-png.jar');module.exports = function(myless, savePath){    'use strict';    var util = myless.util, fileProps, error;    var args = [].slice.call(arguments, 1).map(function(i){ return i.value; });    var data = util.svg.parseInput.apply(null, args.slice(1));    var picPath = util.file.getRefFilePath(savePath, false);    var svgCode = util.svg.getSVGCode(data.attrs, data.cont);    var contMd5 = crypto.createHash('md5').update(svgCode).digest('hex');    // 若png文件已经存在,检查生成图片的svg内容是否发生变化,若没有变化不再重新生成图片.    if(fs.existsSync(picPath)){        fileProps = util.file.getFileProps(picPath);        if(fileProps["cont-md5"] == contMd5) {             util.console.log('svg-to-png: <green>svg conttent not change, return cached file!</green>');            return picPath;         }    }    var codeBase64 = new Buffer(svgCode).toString('base64');    var pathBase64 = new Buffer(picPath).toString('base64');    var command = "java -jar " + jarPath + ' ' + codeBase64 + ' ' + pathBase64;    try {        util.file.mkFilePath(picPath);        child_proc.execSync(command, { encoding: 'utf8' });    } catch(e) {        error = e;    }finally{        if(error){            if( fs.existsSync(picPath) ) {                 fs.unlinkSync(picPath);             }                    throw (''                + 'svg-to-png.js: create png file : "' + savePath.value + '" error!\n'                + 'svg  content:' + svgCode                + 'error info  :' + error             );        }    }    util.file.setFileProps(picPath, { "cont-md5" : contMd5 });    return picPath;}

项目中的实际应用

这个功能在我最近的正在开发这个这个项目中用来完成对IE78的兼容性处理。在这个项目中设计师设计对页面的部分区域设计了圆角和圆形背景效果,而且这个页面有6套皮肤,在不同的皮肤设置下这些细节颜色会发生变化。在这个项目中需要做对低端浏览器做兼容处理的主要有以下图中所示的四处

其中倒计时区域的背景和边框与点赞按钮的背景和边框颜色均相同,而底部的奖品设置标题左侧的背景圆圈与底部的奖品后面的背景圆圈颜色不同。因此如果有5套不同皮肤的话,我只需要测量5组不同的颜色数据,然后根据这5组数据生成图片就好了。因此使用了如下的less代码来完成此功能

/** * @fileOverview  ie9以下浏览器兼容样式. * @since   2015.07.10 */.mix-circle-pic-bg(@saveTo, @color:#c7173d, @width:176) {    @r: @width / 2;     @bg-image : svg-to-png(@saveTo,       'width=@{width}', 'height=@{width}', <<EOF      <circle cx="@{r}" cy="@{r}" r="@{r}" stroke="none" fill="@{color}"/>    EOF);    *background-image: tbcdn-uri(@bg-image);    background-image : data-uri(@bg-image);    background-repeat: no-repeat;}.mix-theme(        @theme,         @time-rect-color,         @time-border-color,        @prize-title-circle-color,        @prize-item-circle-color) {     @toolbar-circle-color: @time-rect-color;    @toolbar-shadow-color: @time-border-color;    .ylb-turntable.@{theme} {        .main .time-card {             background-color: transparent;              .time-card-inner { background-color: transparent; }            @bg-image : svg-to-png('./img/@{theme}-time-card-bg.png',                'width=290px', 'height=169px', <<EOF                      <rect x="0" y="3" width="290" height="166" rx="6" ry="6" fill="@{time-border-color}"/>                    <rect x="0" y="0" width="290" height="166" rx="6" ry="6" fill="@{time-rect-color}"/>                EOF            );            background-image: data-uri(@bg-image);            *background-image: tbcdn-uri(@bg-image);                }        .main .toolbar-card {            @bg-image: svg-to-png('./img/@{theme}-toolbar-bg.png',                'width=100px', 'height=104px', <<EOF                    <circle cx="50" cy="54" r="50" fill="@{toolbar-shadow-color}"/>                    <circle cx="50" cy="50" r="50" fill="@{toolbar-circle-color}"/>                EOF            );             &:before { display: none; }            background: data-uri(@bg-image) 0 0 no-repeat;            *background: tbcdn-uri(@bg-image) 0 0 no-repeat;        }        .footer .prize-arrow-wrap {            &:before { display: none !important; }              .mix-circle-pic-bg('./img/@{theme}-prize-title-bg.png',@prize-title-circle-color, 28);         }        .footer .prize-card {             &:before { display: none !important; }               .mix-circle-pic-bg('./img/@{theme}-prize-item-bg.png', @prize-item-circle-color, 176);         }            }} /*-= 主题部分 =---------------*/// 主题名称前面加t-是因为这个几个颜色是less放到关键字less会自动转换为对应颜色的16进制值// 我被这个bug坑了很久,调试了半天才发现。.mix-theme(t-pink, #c21339, #de2e54, #e4214b, #c7173d); .mix-theme(t-blue, #2581d5, #3da2ff, #2581d5, #2273bd);.mix-theme(t-green, #7ea41a, #acd635, #96bc24, #719103);.mix-theme(t-violet, #9978d5, #bc98ff, #9c7bd9, #7758ad);.mix-theme(t-red, #cf1e29, #fa3f43, #d6202d, #bf222c);

总的来说运行后的效果还算不错,图片的质量与photoshop的略差一些,但也还算能用,面面是
用代码生成的不同颜色的点赞按钮背景:

TB1lLXXIVXXXXcGXVXX4x4KVFXX-100-104.pngTB1qiNbIVXXXXaWaXXX4x4KVFXX-100-104.pngTB1xd75IFXXXXXEaXXX4x4KVFXX-100-104.pngTB1r2w0IFXXXXciaXXX4x4KVFXX-100-104.pngTB1q2.3IFXXXXaqXFXX4x4KVFXX-100-104.png

一些注意事项

  • batik支持半透明但不支持css3的rgba函数,需要使用fill-opacity属性来指定透明度。例如你想给一个rect制定半透明的填充色,你不能这样设置:"fill='rgba(0,0,0, 0.5)'",需要这样:"fill='#000' fill-opacity='0.5'"
  • pink, red 这类css中的颜色常量在less中会被替换为对应的16进制标示,命名变量时要避免使用

thanks

  • 感想@张霸、@思永在java和eclipse操作方面给予的技术支持

参考资料

  • http://stackoverflow.com/questions/15256112/java-svg-to-jpg-converter
  • http://stackoverflow.com/questions/8167977/how-to-convert-svg-into-png-on-the-fly
  • http://thinktibits.blogspot.com/2012/12/Batik-Convert-SVG-PNG-Java-Program-Example.html
  • http://xmlgraphics.apache.org/batik/tools/rasterizer.html
1 0
原创粉丝点击