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);
}
生成二维码还是有白边
// 用于具有特定格式条形码图像的特定条形码写入程序的基类。
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 。
- BitMatrix是Zxing库定义的一个二维码的数据类。
- BitMatrix,实际上就是一个矩阵,内部封装一个bool类型二维数组,通过true false来表示前景色和背景色(默认黑白两色)
- 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;
}
}
从代码中我们可以看出(贴出代码不一定能看明白,有兴趣的可以去看完整实现)
- BitMatrix 的尺寸是完全根据请求尺寸的,除非实际尺寸超出请求尺寸(也就是 BarcodeWriter.Options中设置的)
- BitMatrix 的实现是先算出内容的尺寸(这是最小的尺寸,像素点最大利用化,无法无损缩放)。
- 内容尺寸+白边尺寸=Qr尺寸,最后根据Options设置的尺寸算出整倍数,将Qr尺寸进行放大。
- Qr尺寸放大整倍数,余数部分就成了额外多出来的白边,这也为什么我们设置Margin = 0 。还会导致白边出现的情况。
- 整数放大的Qr尺寸 + 额外的白边 = BitMatrix矩阵的实际尺寸。
- BitMatrix矩阵的实际尺寸,不一定等于 BitMatrix的width,height。例如尺寸为整数 100*100,二维码内容所需的点阵可能为奇数,而两侧白边是偶数,最终生成实际矩阵尺寸会是99*99。
所以我们根据已知原理,重新组织代码。
- Options将尺寸初始化为0,使请求尺寸低于实际尺寸,调用 Encode 获得BitMatrix是未被放大的。
- 再调用BitMatrix.getEnclosingRectangle 获得不含白边的实际尺寸。
- 算出最大限度的尺寸,保证不会有余数产生额外白边。
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);
}
}
通过源代码,我们可以得出,
- BarcodeWriter.Options设置的尺寸大于BitMatrix的尺寸,就使用前者作为输出图片尺寸,否则使用后者。
- 根据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