前言

验证码是图片上写上几个字,然后对这几个字做特殊处理,如扭曲、旋转、修改文字位置,然后加入一些线条,或加入一些特殊效果,使这些在人类能正常识别的同时,机器却很难识别出来,以达到防爬虫、防机器人的效果。

验证码通常用于网站中,是防爬虫、防机器人侵入的好方法。以往.net中创建验证码,通常会使用system.drawing创建“正常”的验证码。

在前一往篇博客.net中生成水印更好的方法中,提到了如何给图片加水印。本文将基于上篇博客进一步探索,使用direct2d创建验证码。

传统system.drawing的做法

前置条件:引用system.drawing,或者安装nuget包:system.drawing.common

<packagereference include="system.drawing.common" version="4.5.1" />

首先创建一个有几个文字的图片(基本操作):

byte[] getimage(int width, int height, string text)
{
 using (var bitmap = new bitmap(width, height))
 using (var g = graphics.fromimage(bitmap))
 {
  var r = new random();

  g.clear(colorfromhsl(r.nextdouble(), 1.0f, 0.8f, 0xff));

  var brush = new solidbrush(color.black);
  var fontsize = width / text.length;
  var font = new font(fontfamily.genericserif, fontsize, fontstyle.bold, graphicsunit.pixel);
  for (var i = 0; i < text.length; i++)
  {
   brush.color = colorfromhsl(r.nextdouble(), 1.0f, 0.3f, 0xff);
   float x = i * fontsize;
   float y = r.next(0, height - fontsize);

   g.drawstring(text[i].tostring(), font, brush, x, y);
  }

  // 在这里面加入一些其它效果

  var ms = new memorystream();
  bitmap.save(ms, imageformat.png);
  return ms.toarray();
 }
}

效果(gif是由linqpad生成多次截图而来,实际为静态图):

然后再加入一些线条:

using (var pen = new pen(brush, 3))
{
 for (var i = 0; i < 4; ++i)
 {
  pen.color = colorfromhsl(r.nextdouble(), 1.0f, 0.4f, 0xff);
  var p1 = new point(r.next(width), r.next(height));
  var p2 = new point(r.next(width), r.next(height));
  g.drawline(pen, p1, p2);
 }
}

效果(gif是由linqpad生成多次截图而来,实际为静态图):

还能做什么?

很遗憾,还有很多可以做,即使是加入线条,机器依然能轻而易举地识别出来。

不过edi.wang在他的中也发布了一个生成验证码的nuget包:edi.captcha,截止目前最新版是1.3.1:

<packagereference include="edi.captcha" version="1.3.1" />

这个包基于system.drawing,加入了扭曲效果,加入了一些随机的x坐标偏移,极大地增加了ai识别的难度。

使用方式:

captcharesult result = captchaimagegenerator.getimage(200, 100, "hello");

其中captcharesult的定义如下:

public class captcharesult
{
 public string captchacode { get; set; }

 public byte[] captchabytedata { get; set; }

 public string captchbase64data => convert.tobase64string(captchabytedata);

 public datetime timestamp { get; set; }
}

生成的效果如下(gif是由linqpad生成多次截图而来,实际为静态图):

direct2d

在前一篇博客中,已经有了direct2d的相关简介。这里将不再介绍。

首先从最简单的图片上写文字开始:

byte[] saved2dbitmap(int width, int height, string text)
{
 using var wic = new wic.imagingfactory2();
 using var d2d = new d2d.factory();
 using var wicbitmap = new wic.bitmap(wic, width, height, wic.pixelformat.format32bpppbgra, wic.bitmapcreatecacheoption.cacheondemand);
 using var target = new d2d.wicrendertarget(d2d, wicbitmap, new d2d.rendertargetproperties());
 using var dwritefactory = new sharpdx.directwrite.factory();
 using var brush = new solidcolorbrush(target, color.yellow);
 
 var r = new random();
 
 target.begindraw();
 target.clear(colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.3f));
 var textformat = new dwrite.textformat(dwritefactory, "times new roman", 
  dwrite.fontweight.bold, 
  dwrite.fontstyle.normal, 
  width / text.length);
 for (int charindex = 0; charindex < text.length; ++charindex)
 {
  using var layout = new dwrite.textlayout(dwritefactory, text[charindex].tostring(), textformat, float.maxvalue, float.maxvalue);
  var layoutsize = new vector2(layout.metrics.width, layout.metrics.height);
  using var b2 = new lineargradientbrush(target, new d2d.lineargradientbrushproperties
  {
   startpoint = vector2.zero, 
   endpoint = layoutsize, 
  }, new gradientstopcollection(target, new[]
  {
   new gradientstop{ position = 0.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) },
   new gradientstop{ position = 1.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) },
  }));

  var position = new vector2(charindex * width / text.length, r.nextfloat(0, height - layout.metrics.height));
  target.transform = 
   matrix3x2.translation(-layoutsize / 2) * 
   // 文字旋转和扭曲效果,取消注释以下两行代码
   // matrix3x2.skew(r.nextfloat(0, 0.5f), r.nextfloat(0, 0.5f)) *
   // matrix3x2.rotation(r.nextfloat(0, mathf.pi * 2)) * 
   matrix3x2.translation(position + layoutsize / 2);
  target.drawtextlayout(vector2.zero, layout, b2);
 }
 // 其它效果在这里插入

 target.enddraw();

 using (var encoder = new wic.bitmapencoder(wic, wic.containerformatguids.png))
 using (var ms = new memorystream())
 {
  encoder.initialize(ms);
  using (var frame = new wic.bitmapframeencode(encoder))
  {
   frame.initialize();
   frame.setsize(wicbitmap.size.width, wicbitmap.size.height);

   var pixelformat = wicbitmap.pixelformat;
   frame.setpixelformat(ref pixelformat);
   frame.writesource(wicbitmap);

   frame.commit();
  }

  encoder.commit();
  return ms.toarray();
 }
}

使用方式:

byte[] captchabytes = saved2dbitmap(200, 100, "hello");

效果(gif是由linqpad生成多次截图而来,实际为静态图):

可以注意到,direct2d生成的文字没有system.drawing那样的锯齿。

如果取消里面的两行注释,可以得到更加扭曲和旋转的效果(gif是由linqpad生成多次截图而来,实际为静态图):

然后加入线条:

for (var i = 0; i < 4; ++i)
{
 target.transform = matrix3x2.identity;
 brush.color = colorfromhsl(r.nextfloat(0,1), 1.0f, 0.3f);
 target.drawline(
  r.nextvector2(vector2.zero, new vector2(width, height)),
  r.nextvector2(vector2.zero, new vector2(width, height)),
  brush, 3.0f);
}

效果(gif是由linqpad生成多次截图而来,实际为静态图):

direct2d的骚操作

direct2d中内置了许多,如阴影(shadow)等,这里我们需要用到的是位移特效(displacement)和水流特效(turbulence),为了实现特效,需要加入一个bitmap层,整体代码如下:

byte[] saved2dbitmap(int width, int height, string text)
{
 using var wic = new wic.imagingfactory2();
 using var d2d = new d2d.factory();
 using var wicbitmap = new wic.bitmap(wic, width, height, wic.pixelformat.format32bpppbgra, wic.bitmapcreatecacheoption.cacheondemand);
 using var target = new d2d.wicrendertarget(d2d, wicbitmap, new d2d.rendertargetproperties());
 using var dwritefactory = new sharpdx.directwrite.factory();
 using var brush = new d2d.solidcolorbrush(target, color.yellow);
 using var encoder = new wic.pngbitmapencoder(wic); // pngbitmapencoder
 
 using var ms = new memorystream();
 using var dc = target.queryinterface<d2d.devicecontext>();
 using var bmplayer = new d2d.bitmap1(dc, target.pixelsize,
  new d2d.bitmapproperties1(new d2d.pixelformat(sharpdx.dxgi.format.b8g8r8a8_unorm, d2d.alphamode.premultiplied),
  d2d.desktopdpi.width, d2d.desktopdpi.height,
  d2d.bitmapoptions.target));

 var r = new random();
 encoder.initialize(ms);

 d2d.image oldtarget = dc.target;
 {
  dc.target = bmplayer;
  dc.begindraw();
  var textformat = new dwrite.textformat(dwritefactory, "times new roman",
   dwrite.fontweight.bold,
   dwrite.fontstyle.normal,
   width / text.length);
  for (int charindex = 0; charindex < text.length; ++charindex)
  {
   using var layout = new dwrite.textlayout(dwritefactory, text[charindex].tostring(), textformat, float.maxvalue, float.maxvalue);
   var layoutsize = new vector2(layout.metrics.width, layout.metrics.height);
   using var b2 = new d2d.lineargradientbrush(dc, new d2d.lineargradientbrushproperties
   {
    startpoint = vector2.zero,
    endpoint = layoutsize,
   }, new d2d.gradientstopcollection(dc, new[]
   {
    new d2d.gradientstop{ position = 0.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) },
    new d2d.gradientstop{ position = 1.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) },
   }));

   var position = new vector2(charindex * width / text.length, r.nextfloat(0, height - layout.metrics.height));
   dc.transform =
    matrix3x2.translation(-layoutsize / 2) *
    matrix3x2.skew(r.nextfloat(0, 0.5f), r.nextfloat(0, 0.5f)) *
    //matrix3x2.rotation(r.nextfloat(0, mathf.pi * 2)) *
    matrix3x2.translation(position + layoutsize / 2);
   dc.drawtextlayout(vector2.zero, layout, b2);
  }
  for (var i = 0; i < 4; ++i)
  {
   target.transform = matrix3x2.identity;
   brush.color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.3f);
   target.drawline(
    r.nextvector2(vector2.zero, new vector2(width, height)),
    r.nextvector2(vector2.zero, new vector2(width, height)),
    brush, 3.0f);
  }
  target.enddraw();
 }
 
 color background = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.3f);
 // for (var frameid = -10; frameid < 10; ++frameid)
 {
  dc.target = null;
  using var displacement = new d2d.effects.displacementmap(dc);
  displacement.setinput(0, bmplayer, true);
  displacement.scale = 100.0f; // math.abs(frameid) * 10.0f;
  
  var turbulence = new d2d.effects.turbulence(dc);
  displacement.setinputeffect(1, turbulence);

  dc.target = oldtarget;
  dc.begindraw();
  dc.clear(background);
  dc.drawimage(displacement);
  dc.enddraw();

  using (var frame = new wic.bitmapframeencode(encoder))
  {
   frame.initialize();
   frame.setsize(wicbitmap.size.width, wicbitmap.size.height);

   var pixelformat = wicbitmap.pixelformat;
   frame.setpixelformat(ref pixelformat);
   frame.writesource(wicbitmap);

   frame.commit();
  }
 }

 encoder.commit();
 return ms.toarray();
}

注意此代码使用了using var语句,是c# 8.0的using declaration功能,可以用using (var )语句代替。

效果如下(gif是由linqpad生成多次截图而来,实际为静态图):

在此基础上,(感谢direct2d/wic)经过较小的改动,即可生成一个动态的gif图片。

只要略微修改以上代码:

  • pngbitmapencoder改成gifbitmapencoder*
  • 然后将下面的for循环取消注释
  • displacement.scale = 100.0f;改成displacement.scale = math.abs(frameid) * 10.0f;

即可看到以下效果(直接生成,非截图):

结语

最终的代码生成效果,可以从这里下载,用linqpad 6打开。

本文使用的是sharpdx,是c#到directx的转换层。一个坏消息是,上图中使用的sharpdx已经停止维护了,但目前还没找到可以用于替换的库(可能由于它太好用了)。

以前我经常将direct2d用于游戏,但最近越来越多的时候direct2d已经用于解决实际问题。由于direct2d的高颜值、高性能,实际上,direct2d已经无处不在,浏览器/word/excel等日常软件都是深度集成direct2d的应用。相信direct2d可以用于更多的场景中。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对www.887551.com的支持。