Coding4Fun - 點陣中文字型顯示
5 |
因緣巧合,最近剛好需要處理中文點陣字型。
在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ómez
OMG!! 您太聪明 我也会C#语言的 我爱这个网站,我在学汉语♥