Zxing.net 生成二维码,Margin白边问题

二维码Margin白边无效果

基础的生成二维码代码如下:

public Form1()
        {
            InitializeComponent();

             pictureBox1.Image = GetBarcode("这是一个二维码,啦啦啦啦啦啦啦啦啦|这是一个二维码|这是一个二维码|这是一个二维码", 200, 200);

        }


        private Bitmap GetBarcode(string text, int width, int height)
        {
            var write = new BarcodeWriter();
            write.Format =  BarcodeFormat.QR_CODE;
            write.Options = new QrCodeEncodingOptions() // EncodingOptions的派生类
            {
                CharacterSet = "UTF-8",//二维码使用中文需要设置的编码格式
                Height = height,
                Width = width,
                Margin = 0 //设置外边框为0,如果不设置Margin,Hints.ContainsKey(EncodeHintType.MARGIN) == false ,会被默认为4。
            };

            return write.Write(text);

        }

 

生成二维码还是有白边

 

 

我们看一下Zxing的源码

//   用于具有特定格式条形码图像的特定条形码写入程序的基类。
    public class BarcodeWriter<TOutput> : BarcodeWriterGeneric, IBarcodeWriter<TOutput>
    {
        // 获取或设置应用于渲染编码位矩阵的渲染器。
        public IBarcodeRenderer<TOutput> Renderer { get; set; }

        //对指定的内容进行编码并返回条形码的呈现实例。
        //对于渲染,将使用属性渲染器的实例,并且必须对其进行设置
        //在调用该方法之前   
        public TOutput Write(string contents)
        {
            if (Renderer == null)
            {
                throw new InvalidOperationException("You have to set a renderer instance.");
            }

            BitMatrix matrix = Encode(contents); //生成一个 BitMatrix 
            return Renderer.Render(matrix, base.Format, contents, base.Options);
        }

        //返回由位矩阵给出的条形码的渲染实例。对于
        //渲染使用属性渲染器的实例,并且必须在
        //调用该方法。
        public TOutput Write(BitMatrix matrix)
        {
            if (Renderer == null)
            {
                throw new InvalidOperationException("You have to set a renderer instance.");
            }

            return Renderer.Render(matrix, base.Format, null, base.Options);
        }
    }

 可以看到,在调用  Renderer.Render 渲染图片之前,Zxing都一生成一个 BitMatrix 。

  1. BitMatrix是Zxing库定义的一个二维码的数据类。
  2. BitMatrix,实际上就是一个矩阵,内部封装一个bool类型二维数组,通过true false来表示前景色和背景色(默认黑白两色)
  3. Renderer.Render是通过BitMatrix内的bool类型二维数组,实现二维码图的渲染的。

 

生成BitMatrix的函数:Encode实现来源


    //BarcodeWriter<TOutput>的父类,只贴出的部分相关实现
    //  用于具有特定格式条形码图像的特定条形码写入程序的基类
    public class BarcodeWriterGeneric : IBarcodeWriterGeneric
    {
        //    获取或设置将内容编码为位矩阵的写入程序。如果没有值
        //    就使用 MultiFormatWriter 作为编码器
        public Writer Encoder { get; set; }
        public BarcodeWriterGeneric(Writer encoder)
        {
            Encoder = encoder;
        }

       public BitMatrix Encode(string contents)
        {
            Writer obj = Encoder ?? new MultiFormatWriter(); //如果没有指定编码器,就使用 MultiFormatWriter
            EncodingOptions encodingOptions = Options;
            return obj.encode(contents, Format, encodingOptions.Width, encodingOptions.Height, encodingOptions.Hints);
        }
 }

 

生成的BitMatrix的代码比较多,下面只贴出相关部分。注释://// 表示非源代码中的注释(我自己总结加上去的)

//// MultiFormatWriter.encode的内部实现调用了QRCodeWriter.encode,而最终输出BitMatrix实例的是QRCodeWriter.renderResult
public sealed class QRCodeWriter : Writer
{
        //请注意,输入矩阵使用0==白色,1==黑色,而输出矩阵使用
        //0==黑色,255==白色(即8位灰度位图)。
        // //// 表示非源代码中的注释(我自己总结加上去的)
        private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone)
        {
            ////quietZone 是 hints[EncodeHintType.MARGIN]的值,也就是EncodingOptions.Margin属性
            //// width,height 请求尺寸来至 EncodingOptions  Width,Height
            var input = code.Matrix;
            if (input == null)
            {
                throw new InvalidOperationException();
            }

            int inputWidth = input.Width; ////内容的尺寸(不含白边的区域)
            int inputHeight = input.Height;

            int qrWidth = inputWidth + (quietZone << 1);  ////实际内容的尺寸+设置的白边*2 = QR的尺寸
            int qrHeight = inputHeight + (quietZone << 1);

            int outputWidth = Math.Max(width, qrWidth);  ////当QR的尺寸未超出请求的尺寸(请求尺寸来至EncodingOptions)
            int outputHeight = Math.Max(height, qrHeight);////就以请求尺寸为准,否则就是矩阵的实际尺寸。

            int multiple = Math.Min(outputWidth / qrWidth, outputHeight / qrHeight);
            //填充包括白边区和额外的白色像素,以适应请求的
            //尺寸。例如,如果输入为25x25,QR将为33x33,包括白边区。
            //如果请求的尺寸为200x160,则对于132x132的QR,倍数将为4。这些将
            //处理从100x100(实际QR)到200x160的所有填充物。
            int leftPadding = (outputWidth - (inputWidth * multiple)) / 2;
            int topPadding = (outputHeight - (inputHeight * multiple)) / 2;

            ////实际尺寸和请求的尺寸取最大值用来设置BitMatrix的 Width,Height 属性
            var output = new BitMatrix(outputWidth, outputHeight); 

            for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple)
            {
                // 写入条形码此行的内容
                for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple)
                {
                    if (input[inputX, inputY] == 1)
                    {
                        output.setRegion(outputX, outputY, multiple, multiple);
                    }
                }
            }

            return output;
        }
}

 从代码中我们可以看出(贴出代码不一定能看明白,有兴趣的可以去看完整实现

  1. BitMatrix 的尺寸是完全根据请求尺寸的,除非实际尺寸超出请求尺寸(也就是 BarcodeWriter.Options中设置的)
  2. BitMatrix 的实现是先算出内容的尺寸(这是最小的尺寸,像素点最大利用化,无法无损缩放)。
  3. 内容尺寸+白边尺寸=Qr尺寸,最后根据Options设置的尺寸算出整倍数,将Qr尺寸进行放大。
  4. Qr尺寸放大整倍数,余数部分就成了额外多出来的白边,这也为什么我们设置Margin = 0 。还会导致白边出现的情况。
  5. 整数放大的Qr尺寸 + 额外的白边 = BitMatrix矩阵的实际尺寸。
  6. BitMatrix矩阵的实际尺寸,不一定等于 BitMatrix的width,height。例如尺寸为整数 100*100,二维码内容所需的点阵可能为奇数,而两侧白边是偶数,最终生成实际矩阵尺寸会是99*99。

 

 

 所以我们根据已知原理,重新组织代码。

  1. Options将尺寸初始化为0,使请求尺寸低于实际尺寸,调用 Encode 获得BitMatrix是未被放大的。
  2. 再调用BitMatrix.getEnclosingRectangle 获得不含白边的实际尺寸。
  3. 算出最大限度的尺寸,保证不会有余数产生额外白边。
public Form1()
        {
            InitializeComponent();

            string text = "这是一个二维码,啦啦啦啦啦啦啦啦啦|这是一个二维码,啦啦啦啦啦啦啦啦啦";
            var size = GetMinSize(text); //获取内容尺寸,未经放大的最小尺寸
            int width = 200, height = 200; //需要生成的二维码尺寸

            int maxWidth = size[2] * (width / size[2]); //最大限度的宽
            int maxHeight = size[2] * (height / size[2]);//最大限度的高

            pictureBox1.Image = GetBarcode(text, maxWidth, maxHeight);

            //显示图片尺寸
            label1.Text = $"Width = {pictureBox1.Image.Width},Height = {pictureBox1.Image.Height}";
        }

        private int[] GetMinSize(string text)
        {
            var write = new BarcodeWriter();
            write.Format = BarcodeFormat.QR_CODE;
            write.Options = new QrCodeEncodingOptions() //初始化设置,尺寸部分全部为0,不要设置null,如果为null,get属性时会初始化一个尺寸100的EncodingOptions
            {
                CharacterSet = "UTF-8" //二维码使用中文需要设置的编码格式
            }; 

            //获取实际尺寸和白边的尺寸,arr[0] = 左右白边,arr[1] = 上下白边,arr[2]=Width,arr[3]=Height
            return write.Encode(text).getEnclosingRectangle();
        }


        private Bitmap GetBarcode(string text, int width, int height) {...}

      }

Options不要设置null,如果设置 null,get属性时会初始化一个尺寸为100的EncodingOptions,Zxing源码如下:

//BarcodeWriter<TOutput>的父类,只贴出的部分相关实现
    // 为什么不可以设置Options为空
    public class BarcodeWriterGeneric : IBarcodeWriterGeneric
    {         
        //   获取或设置编码和渲染器进程的选项容器。
           public EncodingOptions Options
          {
            get
            {
                EncodingOptions encodingOptions = options;
                if (encodingOptions == null)
                {
                    EncodingOptions obj = new EncodingOptions
                    {  ////初始化一个尺寸为100的EncodingOptions
                        Height = 100,
                        Width = 100
                    };
                    EncodingOptions encodingOptions2 = obj;
                    options = obj;
                    encodingOptions = encodingOptions2;
                }

                return encodingOptions;
            }
            set
            {
                options = value;
            }
        }
 }

 实现结果,完美获得一个无白边的二维码

 

 Zxing的放大都是无损的,所以只能整数倍放大。而通过算法拆填内容,实现完全指定尺寸的二维码是会导致图片模糊化的。

 

我们也可以通过以下代码删除多余的白边,并按设置固定白边

private Bitmap GetBarcode(string text, int width, int height)
        {
            var write = new BarcodeWriter();
            write.Format = BarcodeFormat.QR_CODE;
            write.Options = new QrCodeEncodingOptions() // EncodingOptions的派生类
            { 
                CharacterSet = "UTF-8",//二维码使用中文需要设置的编码格式
                Height = height,
                Width = width,
                Margin = 0
            };
     
          //maxWidth 和 maxHeight计算出的尺寸,不会有多余白边。
          //调用 DeleteWhite删除白边(如果有的话),同时根据margin的值重新设置固定的白边
            var matrix = write.Encode(text);
            return write.Write(DeleteWhite(matrix,2)); 
        }

        /// <summary>
        /// 删除默认对应的空白
        /// </summary>
        /// <param name="margin">外边距</param>
        /// <returns></returns>
        private BitMatrix DeleteWhite(BitMatrix matrix, int margin)
        {
            int[] rec = matrix.getEnclosingRectangle();
            int resWidth = rec[2];
            int resHeight = rec[3];

            var resMatrix = new BitMatrix(resWidth + margin * 2, resHeight + margin * 2);
            resMatrix.clear();
            for (int i = 0; i < resWidth; i++)
                for (int j = 0; j < resHeight; j++)
                {
                    if (matrix[rec[0] + i, rec[1] + j])
                        resMatrix[margin + i, margin + j] = true;
                }

            return resMatrix;
        }

 实现效果如下

 

 

 

 

删除白边后出现的意外结果

其实 Zxing 不止在生成BitMatrix的时候将二维码整数倍放大,在 Write 时也会将其放大。

在没有了解这个特性之前,以下代码输出了意料之外的结果。

public Form1()
        {
            InitializeComponent();

            string text = "这是一个二维码,啦啦啦啦啦啦啦啦啦|这是一个二维码,啦啦啦啦啦啦啦啦啦";
            pictureBox1.Image = GetBarcode(text, 100, 100);

            //显示图片尺寸
            label1.Text = $"Width = {pictureBox1.Image.Width},Height = {pictureBox1.Image.Height}";
        }

        private Bitmap GetBarcode(string text, int width, int height)
        {
            var write = new BarcodeWriter();
            write.Format = BarcodeFormat.QR_CODE;
            write.Options = new QrCodeEncodingOptions()// EncodingOptions的派生类
            {
                CharacterSet = "UTF-8",//二维码使用中文需要设置的编码格式
                Height = height,
                Width = width,
                Margin = 0
            };

            var matrix = write.Encode(text);
            return write.Write(DeleteWhite(matrix, 0)); //删除了多余的白边
        }

意外的结果:

 

在没有查看源代代码之前,让我产生了ZXing有BUG的错觉,因为上面的结果是不固定出现的,比如调整一下尺寸。或者调整一下文本内容就会恢复正常状态。

我们来看一下源代码的实现,通过上文的源码,我们可以了解到 Write 内部实现其实是调用了Renderer.Render实现将BitMatrix渲染为图片的。

(代码比较多下面只贴出相关部分 ,有兴趣的可以去看完整实现)注释://// 表示非源代码中的注释(我自己总结加上去的)

// 摘要:
    //     Renders a ZXing.Common.BitMatrix to a System.Drawing.Bitmap image
    public class BitmapRenderer : IBarcodeRenderer<Bitmap>
    {
        ////实现代码在这里
        public virtual Bitmap Render(BitMatrix matrix, BarcodeFormat format, string content, EncodingOptions options)
        {
            var width = matrix.Width;
            var height = matrix.Height;
            var font = TextFont ?? DefaultTextFont; //// outputContent==true 时显示文字的字体
            var emptyArea = 0;
            var outputContent = font != null &&  ////这部代码表示如果需要生成的是条码,是否需要显示文字信息
                                (options == null || !options.PureBarcode) &&
                                !String.IsNullOrEmpty(content) &&
                                (format == BarcodeFormat.CODE_39 ||
                                 format == BarcodeFormat.CODE_93 ||
                                 format == BarcodeFormat.CODE_128 ||
                                 format == BarcodeFormat.EAN_13 ||
                                 format == BarcodeFormat.EAN_8 ||
                                 format == BarcodeFormat.CODABAR ||
                                 format == BarcodeFormat.ITF ||
                                 format == BarcodeFormat.UPC_A ||
                                 format == BarcodeFormat.UPC_E ||
                                 format == BarcodeFormat.MSI ||
                                 format == BarcodeFormat.PLESSEY);

            if (options != null)
            {
                ////当EncodingOptions设置的尺寸大于BitMatrix的尺寸,就使用前者作为输出图片尺寸,否则使用后者。
                if (options.Width > width)
                {
                    width = options.Width;
                }
                if (options.Height > height)
                {
                    height = options.Height;
                }
            }

            // 计算比例因子
            var pixelsizeWidth = width / matrix.Width; ////这个计算出画布尺寸是BitMatrix尺寸的整数倍,用于后续放大二维码
            var pixelsizeHeight = height / matrix.Height;

            if (pixelsizeWidth != pixelsizeHeight)
            {
                if (format == BarcodeFormat.QR_CODE ||
                    format == BarcodeFormat.AZTEC ||
                    format == BarcodeFormat.DATA_MATRIX ||
                    format == BarcodeFormat.MAXICODE ||
                    format == BarcodeFormat.PDF_417)
                {
                    //对称缩放  ////如果生成的是二维码,但是Width和Height不一致,非正方形时。取最小的值作为共同的宽高。(保证渲染不会超出画布)
                    pixelsizeHeight = pixelsizeWidth = pixelsizeHeight < pixelsizeWidth ? pixelsizeHeight : pixelsizeWidth;
                }
            }

            //创建位图并锁定位,因为我们需要步幅
            //这是图像的宽度和可能的填充字节
            var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb); ////这个生成一个空白的画布,尺寸就是EncodingOptions和BitMatrix中的最大值
            
            ////后续代码比较多,我就补贴出来了。
           ////后续代码有主要用于生成二维码或条码图片,同时通过pixelsize尺寸比例来放大等等操作。
           {...省略大部分代码}
            return bitmap;
        }
        
            
            
        ///原本在Render上面的代码被我移下来了

        private static readonly Font DefaultTextFont;
        //获取或设置前景色。
        public Color Foreground { get; set; }
        // 获取或设置背景色。
        public Color Background { get; set; }
        public Font TextFont { get; set; }
       //静态构造函数
        static BitmapRenderer()
        {
            try
            {
                DefaultTextFont = new Font("Arial", 10f, FontStyle.Regular);
            }
            catch (Exception ex)
            {
                Trace.TraceError("default text font (Arial, 10, regular) couldn't be loaded: {0}", ex.Message);
            }
        }
        //构造函数
        public BitmapRenderer()
        {
            Foreground = Color.Black;
            Background = Color.White;
            TextFont = DefaultTextFont;
        }

        public Bitmap Render(BitMatrix matrix, BarcodeFormat format, string content)
        {
            return Render(matrix, format, content, null);
        }  
        
    }

 

通过源代码,我们可以得出,

  1.  BarcodeWriter.Options设置的尺寸大于BitMatrix的尺寸,就使用前者作为输出图片尺寸,否则使用后者。
  2. 根据BitMatrix的矩阵数据,整比率放大,从左上角开始渲染,最后余数部分在右下边作为白色背景渲染。(这部分实现没有贴出,有兴趣的可以去看完整实现

 

我们再根据已原理,增加一句  write.Options = new EncodingOptions(); //将尺寸初始化为0,设置尺寸小于BitMatrix,使其使用BitMatrix尺寸作为图片输出尺寸。

private Bitmap GetBarcode(string text, int width, int height)
        {
            var write = new BarcodeWriter();
            write.Format = BarcodeFormat.QR_CODE;
            write.Options = new QrCodeEncodingOptions()// EncodingOptions的派生类
            {
                CharacterSet = "UTF-8",//二维码使用中文需要设置的编码格式
                Height = height,
                Width = width,
                Margin = 0
            };

            var matrix = write.Encode(text);
            write.Options = new EncodingOptions(); //将尺寸初始化为0,设置尺寸小于BitMatrix,使其使用BitMatrix尺寸作为图片输出尺寸。
            return write.Write(DeleteWhite(matrix, 0)); //删除了多余的白边
        }

 实现结果如下,解决了左下两侧出现莫名出现白边的问题。

 

 

同时在查看源代码时发现了用于设置前景和背景颜色的属性,我们尝试修改这两个属性来自定义二维码的图片颜色

public class BitmapRenderer : IBarcodeRenderer<Bitmap>
    {
        //获取或设置前景色。
        public Color Foreground { get; set; }
        // 获取或设置背景色。
        public Color Background { get; set; }
    }

 实现代码:

private Bitmap GetBarcode(string text, int width, int height)
        {
            var write = new BarcodeWriter();
            write.Format = BarcodeFormat.QR_CODE;
            write.Options = new QrCodeEncodingOptions() //EncodingOptions的派生类
            {
                CharacterSet = "UTF-8",//二维码使用中文需要设置的编码格式
                Height = height,
                Width = width,
                Margin = 0
            };

            var renderer = write.Renderer as BitmapRenderer;
            renderer.Foreground = Color.FromArgb(0x43A37D); //设置了前景颜色。

            var matrix = write.Encode(text);
            write.Options = new EncodingOptions(); //加了这一句,将尺寸初始化为0
            return write.Write(DeleteWhite(matrix, 2)); //删除了多余的白边,重新设置白边为2
        }

实现效果:

 

 

 

最后总结:

        1.Zxing会先生成一个二维矩阵(BitMatrix),生成时会在设置的尺寸内进行整数倍放大,而不够整数方法的部分,则均分作为对应宽高两侧的白边。(白边也是BitMatrix的一部分)

       (如果BarcodeWriter.Options设置的尺寸小于文本内容所生成二维码的最小尺寸,将用最小尺寸输出BitMatrix。所以设置尺寸为0可以实现不放大,并且最无多余的白边。)

 

         2. 然后通过BitMatrix生成二维码/条码图片,生成图片的时候也在BarcodeWriter.Options设置的尺寸内进行整数倍放大。并在左上角渲染,而不够整数方法的部分。统一在右下边渲染白边。

        (如果BarcodeWriter.Options设置的尺寸小于BitMatrix尺寸,将通过BitMatrix尺寸生成图片。)

 

 

相关源码资料:

github:https://github.com/micjahn/ZXing.Net/

gitee国内镜像:https://gitee.com/mirrors/ZXing.Net/tree/master/Source/lib

 

posted @ 2022-05-24 18:52  状态的状  阅读(3594)  评论(2编辑  收藏  举报