艺术二维码生成原理和实践

二维码现在是大街小巷的标配设计,只要用手机扫一下就能快速的进入相应的页面,可以跳转到相应页面,或是查看名片、付款、收红包等等。本文依据二维码的生成原理,用艺术图标替代枯燥的黑白二维码,赋予二维码艺术性和鲜活的个性。

一. 二维码原理

二维码 (2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的。二维码的优点:二维码存储的数据量更大;可以包含数字、字符,及中文文本等混合内容;有一定的容错性(在部分损坏以后可以正常读取);空间利用率高。

如下图所示:

1. 二维码 QR(Quick-Response) code

是被广泛使用的一种二维码,解码速度快。它可以存储多种类型。如下图是一个qrcode的基本结构

其中:位置探测图形、位置探测图形分隔符、定位图形:用于对二维码的定位,对每个QR码来说,位置都是固定存在的,只是大小规格会有所差异;校正图形:规格确定,校正图形的数量和位置也就确定了;格式信息:表示改二维码的纠错级别,分为L、M、Q、H;版本信息:即二维码的规格,QR码符号共有40种规格的矩阵(一般为黑白色),从21x21(版本1),到177x177(版本40),每一版本符号比前一版本 每边增加4个模块。数据和纠错码字:实际保存的二维码信息,和纠错码字(用于修正二维码损坏带来的错误)。

2. 艺术二维码

依据二维码的结构特性,加入丰富生动的图案,提升其美观性。其原理就是针对黑白二维码中的黑色码元,用色彩绚丽的图案进行替换。

二. 艺术二维码生成方法

1.生成二维码的原始数据矩阵。

有很多开源的库工具可以直接用来生成二维码的BitMatrix,比如应用较广泛的google提供的zxing库。

代码如下:

public ArtQRCode(String url) throws IllegalArgumentException {
        BitMatrix bitMatrix = null;
        if (!TextUtils.isEmpty(url)) {
            //配置参数
            Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
            hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
            //容错级别
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
            //设置空白边距的宽度
            hints.put(EncodeHintType.MARGIN, 0); //default is 4
            // 图像数据转换,使用了矩阵转换
            try {
                bitMatrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE, 0, 0, hints);
            } catch (WriterException e) {
                e.printStackTrace();
            }
        }
        if (bitMatrix!=null) {
            valid = formatBitMatrix(bitMatrix);
            Log.i(TAG, "bitMatrix:"+bitMatrix);
        } else {
            throw new IllegalArgumentException("bitMatrix should not be null!");
        }
    }

2.格式化BitMatrix,找出其中所有码元点,和符合特定矩形形状的码元集合。

按照二维码的原则,在BitMatrix中,先找出左、右、下等3个回字形定位符;然后遍历在BitMatrix,找出所有的码元点,再从码元点中找出特定矩形框(也即实际需求对二维码中要替换成特定图标的矩形框,比如33,23,22,12等)。

    /** 格式化BitMatrix,找出其中所有的码元点   */
    private boolean formatBitMatrix(BitMatrix bitMatrix) {
        boolean success = false;
        int matrixLength = bitMatrix.getWidth();
        int matrixHeight = bitMatrix.getHeight();
        if (matrixLength == matrixHeight && matrixLength>(LOCATOR_WIDTH+1)*2) {
            success = true;
            codeNumPerLine = matrixLength;
            leftLocator.set(0, 0, LOCATOR_WIDTH, LOCATOR_WIDTH);
            rightLocator.set(matrixLength - LOCATOR_WIDTH, 0, matrixLength, LOCATOR_WIDTH);
            bottomLocator.set(0, matrixLength - LOCATOR_WIDTH, LOCATOR_WIDTH, matrixLength);
            //遍历,得出所有的码元
            for (int x = 0; x < matrixLength; x++) {
                StringBuilder sb = new StringBuilder("列"+x);
                StringBuilder sb_p = new StringBuilder("point");
                //先列遍历,再行遍历
                for (int y = 0; y < matrixLength; y++) {
                    //查看此element是否为有信息的码元点
                    boolean info = bitMatrix.get(x, y);
                    Point point = new Point(x, y);
                    if (info) {
                        String pointStr = "["+y+","+x+"]";
                        if (!isElementInRect(leftLocator, point)
                                && !isElementInRect(rightLocator, point)
                                && !isElementInRect(bottomLocator, point)) {//不属于定位符的点
                            //以列优先的顺序存放,故需要把下标的行列反过来,便于后续的按列顺序查找
                            int columnRow = y + x*matrixLength;
                            //存储该码元在可用码元矩阵中的列行坐标
                            this.codeElements.put(columnRow, point);
                            sb.append(pointStr);
                        } else {
                            sb_p.append(pointStr);
                        }
                    } 
                }
                Log.w(TAG, sb.toString());
                sb_p.append(" in locator");
                Log.w(TAG, sb_p.toString());
            }            
        }
        return success;
    }

bitMatrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE, 0, 0, hints);这句是用url链接来生成BitMatrix的,其中要传入需要生成矩阵的宽和高,经过对比,发现这里可以取巧,设置宽和高为0,这样生成的矩阵最小(保证码元点信息无遗漏),每个码元点的宽度为1个单位,后续可以省去再去查找定位符以及计算码元点宽度的步骤。

    /** 查找出所有符合规则的形状  */
    private void searchRect() {
        //遍历形状集Shape,查找出所有的矩形框,将其中的码元点标记为已发现,并添加到outElements中
        for (int i = 0; i < SHAPES.size(); i++) {
            Shape shape = SHAPES.valueAt(i);
            if (shape!=null && shape.width!=0 && shape.height!=0) {
                adjustRect(shape);
            }
        }
        //将已标记的码元点从codeElements中移除
        int size = codeElements.size();
        for (int index = size-1; index >= 0; index--) {
            //每次只删一条,保证下标index始终处于size范围内
            int columnRow = codeElements.keyAt(index);
            Point point = outElements.get(columnRow);
            if (point!=null) {
                Log.i(TAG, "remove code at columnRow["+point.x+","+point.y+"]");
                codeElements.remove(columnRow);
            }
        }
    }

3.绘制图形。

依据前面两步对BitMatrix的操作,将找出的定位符用符合定位符特征的图片绘制到画布上,再将各个特定矩形框也以相应的宽高尺寸绘制到画布,然后将单个的码元点以简单的图标绘制上去,最后还可以利用二维码的容错机制,在画布的中央小块位置画上个性化的头像。

    /** 绘制艺术二维码的图像  */
    public Bitmap createArtBitmap(Context context, int width, Bitmap logoBmp) {
        /**商  */
        int codeWidth = width/codeNumPerLine;
        /**余数  */
        int remainder = width%codeNumPerLine;
        int matrixWidth = width;
        if (remainder>0) {//调整初始图片的宽度,凑整,为了每个码元点占用整数个像素点
            codeWidth++;
            matrixWidth = codeWidth*codeNumPerLine;
        }
        Bitmap bitmap = null;
        if (valid) {
            searchRect();
            // 生成二维码图片的格式,使用ARGB_8888
            bitmap = Bitmap.createBitmap(matrixWidth, matrixWidth, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            Paint paint = new Paint();
            //先画原图
            drawPicture(context, canvas, paint, codeWidth);
            //再画logo
            addLogo(canvas, bitmap, logoBmp);
        }
        if (remainder>0) {//再将图像压缩至目标宽高
            bitmap = Bitmap.createScaledBitmap(bitmap, width, width, true);
        }
        return bitmap;
    }
    /** 在画布上绘出所有的矩形框和码元点  */
    private void drawPicture(Context context, Canvas canvas, Paint paint, int codeUnit) {
        // 按照二维码的算法,在canvas上逐个绘制二维码的码元点
        Resources resource = context.getResources();
        Bitmap locatorBmp = BitmapFactory.decodeResource(resource, R.drawable.qvip_pay_qrart_locator);
        Bitmap locatorIcon = Bitmap.createScaledBitmap(locatorBmp, LOCATOR_WIDTH*codeUnit, LOCATOR_WIDTH*codeUnit, true);
        //先画3个定位符
        canvas.drawBitmap(locatorIcon, leftLocator.left*codeUnit, leftLocator.top*codeUnit, paint);
        canvas.drawBitmap(locatorIcon, rightLocator.left*codeUnit, rightLocator.top*codeUnit, paint);
        canvas.drawBitmap(locatorIcon, bottomLocator.left*codeUnit, bottomLocator.top*codeUnit, paint);
          //再画所有找到的特殊矩形框
          int size1 = allRects.size();
          Log.i(TAG, "allRects size="+size1);
          for (int index1 = 0; index1 < size1; index1++) {
              int shapeId = allRects.keyAt(index1);//key为此集合中相同的shapeId
              SparseArray<Point> rects = allRects.valueAt(index1);
              Shape shape = SHAPES.get(shapeId);
              Bitmap bmp = BitmapFactory.decodeResource(resource, shape.resId);
            Bitmap shapeIcon = Bitmap.createScaledBitmap(bmp, shape.width*codeUnit, shape.height*codeUnit, true);
              int size2 = rects!=null ? rects.size() : 0;
              Log.i(TAG, "shape "+shapeId+" size="+size2);
              //对同一种矩形框,查出所有实例并绘制
              for (int index2 = 0; index2 < size2; index2++) {
                  Point point = rects.valueAt(index2);
                  if (point!=null) {
                      canvas.drawBitmap(shapeIcon, point.x*codeUnit, point.y*codeUnit, paint);
                }
            }
        }
          //再画剩下的单个的码元
        int size = codeElements.size();
        Bitmap singleBmp = BitmapFactory.decodeResource(resource, R.drawable.qvip_pay_qrart_3301);
        Bitmap codeBmp = Bitmap.createScaledBitmap(singleBmp, codeUnit, codeUnit, true);
        for (int singleIndex = 0; singleIndex < size; singleIndex++) {
            Log.i(TAG, "single code size="+size);
            Point singlePoint = codeElements.valueAt(singleIndex);
            if (singlePoint!=null) {
                canvas.drawBitmap(codeBmp, singlePoint.x*codeUnit, singlePoint.y*codeUnit, paint);
            }
        }
    }

至此,二维码图片Bitmap就已经生成OK了。

三. 总结

利用二维码的结构特性,将其中连成片的特殊形状用个性化的图片代替,可以使二维码更加美观和生动。

在手Q中,用此方法,实际可用生成面对面红包、付款码等二维码图形。

附:试用手Q扫一扫,有惊喜哦!

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

熊整文的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏walterlv - 吕毅的博客

WPF 绘制对齐像素的清晰显示的线条

发布于 2017-12-12 13:49 更新于 2018-08...

471
来自专栏数据小魔方

R语言数据地图——全球填色地图

今天这篇是昨天美国地图的续篇,同样的方法技巧,不同的对象。 整个过程以及代码并没有太大差别,只要拿到世界地图素材,根据之前的代码,自己修改参数和指标名称以及引用...

2837
来自专栏HenCoder

Android 开发进阶: 自定义 View 1-1 绘制基础

从今天开始,HenCoder 就正式开讲知识技能了。按照我的计划,第一季是 UI,UI 一共分为三部分:绘制、布局和触摸反馈。本期是绘制部分的第一期。绘制大概会...

642
来自专栏everhad

Android自定义评分控件:RatingStarView

RatingStarView Android自定义的评分控件,类似RatingBar那样的,使用星星图标(full、half、empty)作为rating值的“...

2919
来自专栏Charlie's Road

UIKit Dynamics:抛出视图 —《Graphics & Animation系列三》

翻译自raywenderlich网站iOS教程Graphics & Animation系列

492
来自专栏Windows Community

UWP 手绘视频创作工具技术分享系列 - Ink & Surface Dial

本篇作为技术分享系列的第四篇,详细讲一下手绘视频中 Surface Pen 和 Surface Dial 的使用场景。  先放一张微软官方商城的图,Surfac...

27112
来自专栏Python小屋

Python操作高版本Excel文件:颜色、边框、合并单元格

本文主要颜色Python扩展库openpyxl的一些基本用法,包括创建工作簿、选择活动工作表、写入单元格数据,设置单元格字体颜色、边框样式,合并单元格等等。 f...

3365
来自专栏我的技术专栏

UnityShader 表面着色器简单例程集合

2265
来自专栏数据小魔方

R预设配色系统及自定义色板

关于配色的话题,已经聊过很多次了,但是就像是之前说过的,对于图形可视化而言,配色决定着作品的“颜值”,谈再多次都不嫌多。 今天是R语言配色系统综合篇的上篇(当然...

4249
来自专栏数据小魔方

美美的商务范儿——ggplot2蝴蝶图

一个小案例,使用ggplot2绘制蝴蝶图,在巩固温习条形图坐标轴翻转的同时,重新熟悉一下如何利用grid系统进行版式布局。 原图如下: ? 该图表思路很简单,...

3154

扫码关注云+社区