没有字库的情况下如果想在屏幕上显示非ascii字符,一般会借助工具提取可能用到的字符的字模硬编码到代码中按需取用。但如果希望显示动态变化的内容,甚至是变化的图片,这种方式就完全无法应对了。

要解决这个问题,可以开发一个带有取点阵字模功能的接口,利用接口返回对应字符或者图片的字节数组,从而让硬件摆脱对字库的依赖(芯片本身并不知道也不关心要显示的内容具体是什么含义,它在工作的时候只是机械地按照字节数组把内容打印到屏幕上)。

本文将演示如何使用C#实现提取字模功能,并将这个功能做成带参数配置的接口。

工作流程

  1. 创建16×16的Bitmap;
  2. 利用Graphics类的DrawString方法把要显示的字打印到Bitmap;
  3. 遍历全部256个像素点,用bit保存每个像素的状态;
  4. 按绘制方向把256个bit汇成32个Byte,并输出。

一些原理

  1. 点阵中的每个像素只有亮/灭两种状态,正好可以用1个bit来存储;
  2. 24位色深图片里,一个像素需要占用3个字节,RGB三种颜色各占8个bit,所需空间是点阵的24倍;
  3. 阴码/阳码:类似篆刻里阴文和阳文的概念,阴码的亮点是1,阳码的亮点是0;
  4. 扫描方向:顺向扫描高位在前,逆向扫描低位在前;
  5. 扫描方式:逐行、逐列、列行,行列
  6. 从Bitmap里取点的时候,直接通过BitmapData.Scan0(起始内存地址)读取会比Graphics操作更快;
  7. BitmapData在扫描的时候,单行宽度必须是4的整数倍,如果不是的话需要补齐,这个补齐的量需要在拷贝完每行数据的时候跳过。

代码

功能有点简陋,只实现了16×16的宋体文字,自定义字号、字体、自定义图片的取模功能都没有做。运行环境:.net 5 + ubuntu 20.04。


/// <summary>
/// 阴码、逐行、顺向、宋体、16px
/// </summary>
/// <param name="character"></param>
/// <param name="highlight">true, 阴码;false, 阳码</param>
/// <param name="forward">true, 正向;false, 逆向</param>
/// <returns></returns>
public IActionResult Dots(string character, int highlight = 1, int forward = 1)
{
    if (string.IsNullOrEmpty(character))
    {
        ViewBag.Array = "{}/**/";
    }
    else
    {
        var @char = character.Substring(0, 1);
        ViewBag.Char = @char;
        Bitmap image = new(16, 16, PixelFormat.Format24bppRgb);
        using Graphics g = Graphics.FromImage(image);
        g.Clear(Color.White);
        StringFormat drawFormat = new();
        drawFormat.Alignment = StringAlignment.Center;
        RectangleF drawRect = new(0, 0, 16, 16);
        SolidBrush drawBrush = new(Color.Black);
        Font drawFont = new("宋体", 16, FontStyle.Regular, GraphicsUnit.Pixel);
        g.DrawString(@char, drawFont, drawBrush, drawRect, drawFormat);
        ViewBag.Array = General.GetDots(image, highlight == 1, forward == 1, @char);
        using MemoryStream mms = new();
        image.Save(mms, ImageFormat.Jpeg);
        ViewBag.Image ="data:image/jpeg;base64,"+ Convert.ToBase64String(mms.ToArray());
    }
    ViewBag.Highlight = highlight;
    ViewBag.Forward = forward;
    return View();
}

/// <summary>
/// 16x16像素
/// </summary>
/// <param name="image"></param>
/// <param name="highlight">true: 阴码,亮点为1;false: 阳码,亮点为0</param>
/// <param name="forward">true: 正向;false: 逆向</param>
/// <returns></returns>
public static string GetDots(Bitmap image, bool highlight, bool forward, string obj)
{
    BitmapData dots =
            image.LockBits(new Rectangle(0, 0, 16, 16),
            ImageLockMode.ReadOnly, image.PixelFormat);

    //16 * 16像素
    //原始图片24位色深,一个像素3Byte
    //点阵图1位色深,一个像素占1bit,一行2Byte
    //最终结果有2byte * 16 = 32Byte
    //顺序时每一行,bit0~7是第1个byte的7~0位;
    //顺序时每一行,bit8~15是第2个byte的7~0位;
    var bytesOut = new int[32];
    unsafe
    {
        byte* scan = (byte*)(void*)dots.Scan0;
        int offset = dots.Stride - 3 * dots.Width;

        //行宽补齐偏移值,本场景宽度固定16,不需要补齐,所以永远是0,
        for (int y = 0; y < 16; y++)//第0行扫描到最后一行
        {
            for (int x = 0; x < 16; x++)
            {//RGB全0,代表黑色,提取点阵时,像素点的值为1
                bool dot = scan[0] + scan[1] + scan[2] == 0;
                bytesOut[y * 2 + x / 8] |= ((highlight ^ dot) ? 0 : 1) << (forward ? (7 - x % 8) : (x % 8));
                scan += 3;//下个像素
            }
            scan += offset;
        }
    }
    image.UnlockBits(dots);
    return "{\r\n    " + string.Join(", ", bytesOut.Select(b => "0x" + b.ToString("X2"))) +
        $"\r\n}};/*{obj}(逐行,{(highlight ? "阴" : "阳")},{(forward ? "正" : "逆")})*/";
}
不过发现一个坏消息:这屏幕买回来半年了,第一次用,居然有一行像素点坏了。
分类: articles