forked from Zbyl/BFGFontTool
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathD3FontDecompose.cs
323 lines (287 loc) · 13.6 KB
/
D3FontDecompose.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Globalization;
using ImageMagick;
using System.Drawing;
namespace BFGFontTool
{
/// <summary>
/// This class decomposes the Doom 3 font into individual character images.
/// It also applies The Dark Mod's code page conversion.
/// To ignore any conversions and use iso-8859-1 code page choose "english" language.
/// </summary>
class D3FontDecompose
{
public TextWriter textOut = Console.Out;
D3Font font;
string dirWithFontTextures;
string imageOutputDir;
string[] langs;
string dirWithLangMaps;
string zeroSizeImage;
IDictionary<string, Encoding> baseCodePageEncodings = new Dictionary<string, Encoding>();
IDictionary<string, IDictionary<byte, byte> > invRemapTables = new Dictionary<string, IDictionary<byte, byte> >();
Dictionary<string, MagickImage> textures = new Dictionary<string, MagickImage>();
IDictionary<int, BMIconInfo> iconInfos = new Dictionary<int, BMIconInfo>();
/// <summary>
/// Decompose the Doom 3 font into individual character images.
/// It also applies The Dark Mod's code page conversion.
/// To ignore any conversions and use iso-8859-1 code page choose "english" language.
/// </summary>
/// <param name="font">Doom 3 font</param>
/// <param name="dirWithFontTextures">Path to directory containing font textures, like "arial_0_48.dds".</param>
/// <param name="lang">The Dark Mod's language to use. Can be "english", "polish", "german", etc.</param>
/// <param name="dirWithLangMaps">Optional: Directory containing The Dark Mod's code page remapping maps, like "polish.map".</param>
/// <param name="zeroSizeImage">Optional: image that have 0x0 size.</param>
/// <param name="bmConfigFile">File to output BMFont's external image configuration to.</param>
/// <param name="imageOutputDir">Directory into which output character images.</param>
public static IList<string> Decompose(D3Font font, string dirWithFontTextures, string[] langs, string dirWithLangMaps, string zeroSizeImage, string imageOutputDir)
{
var fontDecomposer = new D3FontDecompose(font, dirWithFontTextures, langs, dirWithLangMaps, zeroSizeImage, imageOutputDir);
return fontDecomposer.Decompose();
}
private D3FontDecompose(D3Font font, string dirWithFontTextures, string[] langs, string dirWithLangMaps, string zeroSizeImage, string imageOutputDir)
{
this.font = font;
this.dirWithFontTextures = dirWithFontTextures;
this.langs = langs;
this.dirWithLangMaps = dirWithLangMaps;
this.imageOutputDir = imageOutputDir;
this.zeroSizeImage = zeroSizeImage;
Directory.CreateDirectory(imageOutputDir);
foreach (string lang in langs)
{
baseCodePageEncodings[lang] = BaseCodePageEncodingForLang(lang);
LoadRemapTableForLang(lang);
}
}
private IList<string> Decompose()
{
var infosDict = new Dictionary<int, IList<string>>();
for (int i = 0; i < D3Font.GLYPHS_PER_FONT; i++)
{
IList<string> infos;
int utf32Char = Remap((byte)i, out infos);
if (iconInfos.ContainsKey(utf32Char))
{
textOut.WriteLine("WARNING: The glyph 0x{0:X2} maps to previously outputted char '{1}' - IGNORING", i, char.ConvertFromUtf32(utf32Char));
textOut.WriteLine(" Current output:");
foreach (var info in infos)
{
textOut.WriteLine(" {0}", info);
}
textOut.WriteLine(" First output:");
foreach (var info in infosDict[utf32Char])
{
textOut.WriteLine(" {0}", info);
}
continue;
}
infosDict[utf32Char] = infos;
D3Glyph glyph = font.glyphs[i];
Decompose(glyph, utf32Char);
}
IList<string> bmConfig = new List<string>();
foreach (var kv in iconInfos)
{
bmConfig.Add(kv.Value.ToString());
}
return bmConfig;
}
private void Decompose(D3Glyph glyph, int utf32Char)
{
string textureName = Path.Combine(dirWithFontTextures, Path.GetFileName(glyph.textureName));
if (!textures.ContainsKey(textureName))
{
string realName = textureName;
if (!File.Exists(realName))
{
realName = Path.ChangeExtension(textureName, "dds");
}
MagickImage newImage = new MagickImage(realName);
textures[textureName] = newImage;
}
MagickImage image = textures[textureName].Clone();
BMIconInfo icon = new BMIconInfo();
icon.imageFile = Path.Combine(imageOutputDir, string.Format("{0}_char{1}.tga", font.name, utf32Char));
icon.charId = utf32Char;
// baseline position in pixels from top edges of the image
int baseline = glyph.height - glyph.top; // maybe... maybe it's glyph.imageHeight - glyph.top... and maybe it's something else...
Debug.Assert(image.Width == 256);
Debug.Assert(image.Height == 256);
icon.xoffset = 0;
//icon.yoffset = font.maxHeight - glyph.top - glyph.bottom; // -baseline;
icon.yoffset = font.maxTop - glyph.top;
int xstart = (int)(glyph.s * image.Width + 0.0f); // +glyph.pitch; // this "+ glyph.pitch" is a guess
int ystart = (int)(glyph.t * image.Height + 0.0f);
int xend = (int)(glyph.s2 * image.Width - 0.0f);
int yend = (int)(glyph.t2 * image.Height - 0.0f);
int glyphWidth = xend - xstart;
int glyphHeight = yend - ystart;
Debug.Assert(glyph.imageWidth == glyphWidth);
Debug.Assert(glyph.imageHeight == glyphHeight);
xstart += glyph.pitch; // this "+ glyph.pitch" is a guess
// this damn ImageMagick can't create zero size images!
if (zeroSizeImage == null)
{
if (glyphWidth == 0)
{
glyphWidth = 1;
}
if (glyphHeight == 0)
{
glyphHeight = 1;
}
}
icon.xadvance = glyph.xSkip - glyphWidth; // maybe add "- glyph.pitch" here?
iconInfos[icon.charId] = icon;
MagickGeometry geom = new MagickGeometry(xstart, ystart, glyphWidth, glyphHeight);
if (glyphWidth == 0 || glyphHeight == 0)
{
//Bitmap bmp = new Bitmap(0, 0, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
var newImage = new MagickImage(zeroSizeImage);
//newImage.ColorType = image.ColorType;
//newImage.ColorSpace = image.ColorSpace;
//image.Crop(1, 0);
//image.Trim();
image = newImage;
}
else
{
image.Crop(geom);
}
image.Format = MagickFormat.Tga;
image.Write(icon.imageFile);
}
private int Remap(byte tdmChar, out IList<string> infos)
{
// base code page for lang: codepage char -> utf32
// remap file for lang: codepage char -> TDM char
// fonts: TDM char
// so: char number n in TDM font is:
// utf32Char = codepage[ remap^-1[n] ]
var chars = new HashSet<int>();
var mappedChars = new HashSet<int>();
int? utf32Eng = null;
infos = new List<string>();
foreach (string lang in langs)
{
var invRemapTable = invRemapTables[lang];
byte unmapped = invRemapTable.ContainsKey(tdmChar) ? invRemapTable[tdmChar] : tdmChar;
int utf32Char = CodePageToUTF32(unmapped, lang);
if (unmapped != tdmChar)
{
mappedChars.Add(utf32Char);
infos.Add(string.Format("{0}: 0x{1:X2} -> 0x{2:X2} -> '{3}'", lang, tdmChar, unmapped, char.ConvertFromUtf32(utf32Char)));
}
else
{
chars.Add(utf32Char);
infos.Add(string.Format("{0}: 0x{1:X2} -> '{2}'", lang, tdmChar, char.ConvertFromUtf32(utf32Char)));
}
if (lang == "english")
{
utf32Eng = utf32Char;
}
}
if (mappedChars.Count > 1)
{
textOut.WriteLine("WARNING: The character 0x{0:X2} maps to following utf8 chars:", tdmChar);
foreach (var info in infos)
{
textOut.WriteLine(" {0}", info);
}
textOut.WriteLine(" Taking: '{0}'", char.ConvertFromUtf32(mappedChars.First()));
}
if (mappedChars.Count == 1)
{
return mappedChars.First();
}
if (chars.Count > 1)
{
if (utf32Eng.HasValue)
{
textOut.WriteLine("INFO: The character 0x{0:X2} maps to different chars in different codepages. Taking english: '{1}'", tdmChar, char.ConvertFromUtf32(chars.First()));
return utf32Eng.Value;
}
textOut.WriteLine("WARNING: The character 0x{0:X2} maps to different chars in different codepages.", tdmChar);
textOut.WriteLine(" Taking: '{0}'", char.ConvertFromUtf32(chars.First()));
}
return chars.First();
}
public static string[] allLangsExceptRussian = { "czech", "danish", "dutch", "english", "french", "german", "hungarian", "italian", "polish", "portuguese", "slovak", "spanish" };
public static string[] onlyRussian = { "russian" };
public string BaseCodePageNameForLang(string lang)
{
switch(lang)
{
case "czech": return "iso-8859-2";
case "danish": return "iso-8859-1";
case "dutch": return "iso-8859-1";
case "english": return "iso-8859-1";
case "french": return "iso-8859-15";
case "german": return "iso-8859-1";
case "hungarian": return "iso-8859-2";
case "italian": return "iso-8859-1";
case "polish": return "iso-8859-2";
case "portuguese": return "iso-8859-1";
case "russian": return "windows-1251";
case "slovak": return "iso-8859-2";
case "spanish": return "iso-8859-1";
}
textOut.WriteLine("WARNING: unknown language: {0}", lang);
return "iso-8859-1"; // default to english...
}
public Encoding BaseCodePageEncodingForLang(string lang)
{
string codePageName = BaseCodePageNameForLang(lang);
Encoding codePageEncoding = Encoding.GetEncoding(codePageName);
return codePageEncoding;
}
public int BaseCodePageForLang(string lang)
{
Encoding codePageEncoding = BaseCodePageEncodingForLang(lang);
return codePageEncoding.CodePage;
}
public int CodePageToUTF32(byte codePageChar, string lang)
{
string decodedChar = baseCodePageEncodings[lang].GetString(new byte[] { codePageChar }, 0, 1);
Debug.Assert((decodedChar.Length == 1) || ((decodedChar.Length == 2) && char.IsSurrogatePair(decodedChar, 0)));
int utf32Char = char.ConvertToUtf32(decodedChar, 0);
//byte[] utf32Bytes = Encoding.UTF32.GetBytes(decodedChar);
return utf32Char;
}
void LoadRemapTableForLang(string lang)
{
var invRemapTable = new Dictionary<byte, byte>();
invRemapTables[lang] = invRemapTable;
string[] allLines;
try
{
string mapFile = Path.Combine(dirWithLangMaps, Path.ChangeExtension(lang, "map"));
allLines = File.ReadAllLines(mapFile);
}
catch
{
textOut.WriteLine("Couldn't load remap table for language: {0}.", lang);
return;
}
string pattern = @"^\s+0x([0-9A-F]{2})\s+0x([0-9A-F]{2})";
var regex = new Regex(pattern);
foreach (string line in allLines)
{
var match = regex.Match(line);
if (!match.Success)
continue;
byte fromChar = byte.Parse(match.Groups[1].Value, NumberStyles.HexNumber);
byte toChar = byte.Parse(match.Groups[2].Value, NumberStyles.HexNumber);
invRemapTable[toChar] = fromChar;
}
}
}
}