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

图片处理在前端开发过程中占领了不少的时间。非常是累人。在本文中我们不讨论怎样提高切图的效率。我们讨论还有一个问题:怎样处理设计稿中的一些简单图形。

不知道你又没有遇到过这样的烦恼:“设计师给你的精致的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良好的生态系统非常easy完毕了我想要的功能。

完毕之后我不禁想。我这个功能为什么不用java来试试呢?在Google上搜索"java svg to png", 马上有了结果,Apache下有个专门处理svg的库batik。我原来有个非常好的同事总跟我说“解决一个问题最难的是怎样将自己的问题转换为合适的搜索关键词”。至此真是深有体会。

batik不仅对SVG的支持很好。功能也很强大。不仅能通过DOM API来操作SVG文档,并且还提供了将svg转换为其它格式、svg中嵌入js代码等许多很有用的功能。官方的jar包中有一个能够在命令行中执行,能够完毕将svg图片转换为其它格式,使用起来也很easy:

java -jar batik-rasterizer.jar foo.svg

这恰好是我们须要的功能。

至此基于batik的解决方式应该是眼下为止最完美的,jar文件不须要单独安装就能够执行。而对文件夹结构也没有要求。

全然能够把jar文件和js文件放到一个文件夹下然后和js代码一起公布。

尽管也须要安装java执行环境,但java执行环境的安装相对来说非常easy。并且公司每台电脑都安装了。所以这个条件是能够接受的。

导出可独立执行jar文件

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

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文件可以独立执行就非常easy被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套皮肤。在不同的皮肤设置下这些细节颜色会发生变化。在这个项目中须要做对低端浏览器做兼容处理的主要有下面图中所看到的的四处

当中倒计时区域的背景和边框与点赞button的背景和边框颜色均同样,而底部的奖品设置标题左側的背景圆圈与底部的奖品后面的背景圆圈颜色不同。

因此假设有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放到keywordless会自己主动转换为相应颜色的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的略差一些,但也还算能用,面面是
用代码生成的不同颜色的点赞button背景:

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操作方面给予的技术支持

參考资料

posted @ 2017-05-21 08:37  zhchoutai  阅读(831)  评论(0编辑  收藏  举报