因緣巧合,最近剛好需要處理中文點陣字型。

在DOS+倚天中文的古早年代,曾經用BASICA寫過解析倚天中文字型檔的程式,沒想到二十多年後居然還有機會重新回味,只是這回手上的兵器已由當年的BASICA小開山刀,換成C#加農砲,語言特性已不可同日而言、自己的程式技巧也遠比當年成熟,對照起來格外有趣。

時代演進大大地改變了寫程式的方法,當年要自己瞎摸亂湊好一陣子才能拼湊出檔案規格,現在稍稍爬文就能找到網友的熱心分享:

有了字型檔規格,加上C#的物件導向,我試寫了以不同字型檔為基礎的點陣中文字型元件:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
 
namespace TestChineseFont
{
    //允許以不同廠商字型檔作為來源的點陣字型物件
    public abstract class DotArrayFontProvider
    {
        //宣告不同的尺寸規格
        public enum FontSize
        {
            Size15 = 15, Size24 = 24, Size32 = 32, Size48 = 48
        }
        //不同字型檔轉為byte[]的實作方式不同
        public abstract byte[] GetFontData(FontSize sz, bool halfWidth);
        //由寬度計算所需要的位元數(半形時用量減半)
        protected static int GetWidthBytes(int w, bool halfWidth)
        {
            if (halfWidth) w = (w / 2);
            return (w / 8) + (w % 8 > 0 ? 1 : 0);
        }
        //取得特定字元的點陣資料(byte[])
        public byte[] GetCharData(char ch, FontSize sz = FontSize.Size24)
        {
            byte[] b = Encoding.GetEncoding(950).GetBytes(new char[] { ch });
            int iSz = (int)sz;
            bool halfWidth = false;
            int offset = -1;
            //ASCII 0-255採半形
            if (b.Length == 1) halfWidth = true;
            int arraySize = GetWidthBytes(iSz, halfWidth) * iSz;
            byte[] result = new byte[arraySize];
            //半形時依ASCII碼決定資料起始位址
            if (halfWidth)
            {
                offset = arraySize * b[0];
            }
            else
            {
                //全形文字依倚天字型檔的存放規則
                //http://www.cnblogs.com/armstrong-cn/archive/2011/09/01/2161567.html
                byte hi = b[0], lo = b[1];
                int serCode = (hi - 161) * 157 + (lo >= 161 ? lo - 161 + 1 + 63 : lo - 64 + 1);
                if (serCode >= 472 && serCode < 5872)
                    offset = (serCode - 472) * arraySize;
                else if (serCode >= 6281 && serCode <= 13973)
                    offset = (serCode - 6281) * arraySize + 5401 * arraySize;
            }
            if (offset < 0) return null;
            Buffer.BlockCopy(GetFontData(sz, halfWidth), offset, result, 0, arraySize);
            return result;
 
        }
        //將點陣內容由byte[]轉為長*寬的二維byte[,],1表示該點要顯示, 0表示該點留白
        public static byte[,] GetDotArray(byte[] data, int w, int h)
        {
            //偵測是否為半形字
            bool halfWidth = data.Length == GetWidthBytes(w, true) * h;
            //如為半形字,寬度減半
            if (halfWidth) w = w / 2; 
            if (w < 8) w = 8;
            //宣告二維陣列以存放點陣資料
            byte[,] dotArray = new byte[h, w];
            int widthBytes = data.Length / h;
            for (int y = 0; y < h; y++)
            {
                int offset = widthBytes * y;
                byte b = data[offset];
                for (int x = 0; x < w; x++)
                {
                    if (x % 8 == 0)
                    {
                        b = data[offset];
                        offset++;
                    }
                    dotArray[y, x] = (byte)(((b << (x % 8)) & 0x80) != 0 ? 1 : 0);
                }
            }
            return dotArray;            
        }
    }
}

DotArrayFontProvider抽象類別提供了GetCharData(char ch, FontSize sz)以取出指定字型尺寸(例如: 15x15, 24x24)的特定字元點陣資料(一點一個Bit,一個Byte代表8點,以byte[]方式傳回),而另外有靜態方法GetDotArray()可將前述byte[]轉成二維陣列,一點一個byte,1代表顯示,0代表留白,以配合X、Y軸座標運算轉成圖檔。DotArrayFontProvider是個抽象類別,子類別必須實作一個byte[] GetFontData(FontSize sz, bool halfWidth)傳回特定尺寸、全形/半形的全部點陣資料。(ASCII 0-255字元使用半形資料) 這部分與各廠商字型檔規格高度相依,就留給各子類別自行實現。

針對國喬字型檔所寫的KCFontProvider,邏輯很簡單,就只是讀入KCCHIN16.F00、KCTEXT16.F00、KCCHIN24.F00、KCTEXT24.F00等字型檔,從中取出15x15及24x24的全形及半形文字點陣資料,存入靜態Dictionary後,供GetFontData()時取出回傳。其中有些位址計算,純粹是要由國喬字型檔取出資料,只保留點資料的部分,看似複雜,但沒什麼營養。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
 
namespace TestChinFont
{
    public class KCFontProvider : DotArrayFontProvider
    {
        static Dictionary<string, byte[]> dataPool = 
            new Dictionary<string, byte[]>();
        //http://bbs.unix-like.org:8080/boards/FB_chinese/M.1023317709.A
        static KCFontProvider()
        {
            byte[] buff = File.ReadAllBytes("KCCHIN16.F00");
 
            int offset = 256 + 765 * 2 * 16;
            const int charCount = 13195;
            byte[] data = new byte[charCount * 2 * 15];
            for (int i = 0; i < charCount; i++)
            {
                Buffer.BlockCopy(buff, offset + i * 2 * 14, data, i * 2 * 15, 2 * 14); 
            }
            dataPool.Add("C" + FontSize.Size15, data);
            buff = File.ReadAllBytes("KCTEXT16.F00");
            data = new byte[256 * 15];
            offset = 256;
            for (int i = 0; i < 256; i++)
                Buffer.BlockCopy(buff, offset + i * 16, data, i * 15, 15);
            dataPool.Add("A" + FontSize.Size15, data);
 
            buff = File.ReadAllBytes("KCCHIN24.F00");
            data = new byte[charCount * 72];
            offset = 256 + 765 * 72;
            for (int i = 0; i < charCount; i++)
            {
                Buffer.BlockCopy(buff, offset + i * 72, data, i * 72, 72);
            }
 
            dataPool.Add("C" + FontSize.Size24, data);
            buff = File.ReadAllBytes("KCTEXT24.F00");
            data = new byte[256 * 48];
            offset = 256;
            for (int i = 0; i < 256; i++)
                Buffer.BlockCopy(buff, offset + i * 48, data, i * 48, 48);
            dataPool.Add("A" + FontSize.Size24, data);
        }
 
        public override byte[] GetFontData(FontSize sz, bool halfWidth)
        {
            string key = (halfWidth ? "A" : "C") + sz;
            if (!dataPool.ContainsKey(key))
                throw new NotImplementedException();
            return dataPool[key];
        }
    }
}

這樣就差不多就能精準取出不同字元的點陣資料。雖然當年用BASICA寫的版本已不復記憶,但我很肯定,這段C#讀檔程式碼的長度絕對不到當年的十分之一!

最後,配合一小段程式,將點陣資料轉成Bitmap,來驗證一下執行結果:

        private Bitmap DrawDotArrayText(string text, DotArrayFontProvider.FontSize fs)
        {
            int sz = (int)fs;
            Bitmap bmp = new Bitmap(text.Length * sz * 4, sz * 4);
            SolidBrush bb = new SolidBrush(Color.Black);
            SolidBrush yb = new SolidBrush(Color.Orange);
            Graphics g = Graphics.FromImage(bmp);
            g.FillRectangle(bb, 0, 0, bmp.Width, bmp.Height);
            int offset = 0;
            var kcfp = new KCFontProvider();
            foreach (char ch in text.ToCharArray())
            {
                byte[,] d = DotArrayFontProvider.GetDotArray(
                                                 kcfp.GetCharData(ch, fs), sz, sz);
                for (int y = 0; y < sz; y++)
                {
                    for (int x = 0; x < sz; x++)
                    {
                        if (d[y, x] == 1)
                        {
                            g.FillRectangle(yb, offset + x * 4, y * 4, 3, 3);
                        }
                    }
                }
                offset += sz * 4;
            }
            return bmp;
        }

LED字幕機式的中文顯示效果就完成囉~


Comments

# by 小黑

這好酷歐,謝謝黑大,不過,想請問一下,為何叫 Coding4Fun?

# by Jeffrey

to 小黑,程式魔人們偶爾會從事與家庭生計無關的Coding活動,旨在求娛樂自嗨或滿足某種無厘頭的成就感,說穿了就是為了好玩而寫程式,故曰"Coding for Fun"。 剛好最近因工作再次回味了字型檔解析,一時興起便復刻了當年用BASICA寫過的中文字型LED顯示,多寫的程式對工作無直接助益(但間接有練到功增加了經驗值),純料好玩罷了。 Channel9上有個Coding4Fun專區,上面有很多神人的有趣創作: http://channel9.msdn.com/coding4fun,那才叫人瞠目結舌。

# by u329

哇,看這篇好有 fu 喔,之前第二份工作(應該是1994-1995)時,也特別研究了倚天字型檔的結構,將其ASCII 24 內編號 128之後的字型圖,用專三時自修寫的BASIC 倚天Patten產生程式,運算出三九碼對應的Byte表,再用PC Tools 改寫字型檔後,變成可以在任何應用程式(如:Clipper/PE2/DBaseIII...)下,同時列印中英文及條碼文件的Solution,用來取代之前我用苦工硬啃 PCL 、 Esc/p等印表機語言,只能搭配Clipper使用的條碼列印功能。。。現在回想起來,還是覺得當時自己能想到這法子,有點沾沾自喜,不可思議說~~ ^_^

# by 貓咪圓滾滾

哇 coding4fun看起來好有趣哪(你是自己會寫喔?? 哈哈)

# by Anthony G&#243;mez

OMG!! 您太聪明 我也会C#语言的 我爱这个网站,我在学汉语♥

Post a comment