mirror of
https://github.com/scummvm/scummvm-tools.git
synced 2026-05-21 05:40:44 +00:00
580 lines
14 KiB
C++
580 lines
14 KiB
C++
/* ScummVM Tools
|
|
*
|
|
* ScummVM Tools is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "create_sjisfnt.h"
|
|
|
|
#include "common/endian.h"
|
|
#include "common/file.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <iconv.h>
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
|
|
int main(int argc, char *argv[]) {
|
|
if (argc < 2 || argc > 3) {
|
|
printf("Usage:\n\t%s <input ttf font> [outfile]\n", argv[0]);
|
|
return -1;
|
|
}
|
|
|
|
const char *font = argv[1];
|
|
const char *out = 0;
|
|
if (argc == 3)
|
|
out = argv[2];
|
|
else
|
|
out = "sjis.fnt";
|
|
|
|
if (!initSJIStoUTF32Conversion()) {
|
|
error("Could not initialize conversion from SJIS to UTF-32.");
|
|
return -1;
|
|
}
|
|
|
|
atexit(deinitSJIStoUTF32Conversion);
|
|
|
|
TrueTypeFont *ttf = new TrueTypeFont();
|
|
if (!ttf->load(font)) {
|
|
delete ttf;
|
|
error("Could not initialize FreeType library.");
|
|
return -1;
|
|
}
|
|
|
|
if (!ttf->setSize(16)) {
|
|
delete ttf;
|
|
error("Could not setup font '%s' to size 16", font);
|
|
return -1;
|
|
}
|
|
|
|
GlyphList glyphs;
|
|
int chars8x16 = 0;
|
|
int chars16x16 = 0;
|
|
|
|
ttf->renderASCIIGlyphs(glyphs, chars8x16);
|
|
ttf->renderKANJIGlyphs(glyphs, chars16x16);
|
|
|
|
if (!ttf->setSize(12)) {
|
|
delete ttf;
|
|
error("Could not setup font '%s' to size 12", font);
|
|
return -1;
|
|
}
|
|
|
|
GlyphList pceGlyphs;
|
|
int chars12x12 = 0;
|
|
ttf->renderKANJIGlyphs(pceGlyphs, chars12x12);
|
|
|
|
delete ttf;
|
|
ttf = 0;
|
|
|
|
fixYOffset(glyphs);
|
|
fixYOffset(pceGlyphs);
|
|
|
|
// Check whether we have a character which does not fit within the boundaries
|
|
for (GlyphList::const_iterator i = glyphs.begin(); i != glyphs.end(); ++i) {
|
|
if (i->pitch == 0)
|
|
continue;
|
|
|
|
if ((isASCII(i->fB) && !i->checkSize(8, 16)) ||
|
|
(!isASCII(i->fB) && !i->checkSize(16, 16))) {
|
|
error("Could not fit glyph for %.2X %.2X top: %d bottom: %d, left: %d right: %d, xOffset: %d, yOffset: %d, width: %d, height: %d",
|
|
i->fB, i->sB, i->yOffset, i->yOffset + i->height, i->xOffset, i->xOffset + i->width,
|
|
i->xOffset, i->yOffset, i->width, i->height);
|
|
}
|
|
}
|
|
|
|
// Check whether we have a character which does not fit within the boundaries
|
|
for (GlyphList::const_iterator i = pceGlyphs.begin(); i != pceGlyphs.end(); ++i) {
|
|
if (i->pitch == 0)
|
|
continue;
|
|
|
|
if (!isASCII(i->fB) && !i->checkSize(12, 12)) {
|
|
error("Could not fit pce glyph for %.2X %.2X top: %d bottom: %d, left: %d right: %d, xOffset: %d, yOffset: %d, width: %d, height: %d",
|
|
i->fB, i->sB, i->yOffset, i->yOffset + i->height, i->xOffset, i->xOffset + i->width,
|
|
i->xOffset, i->yOffset, i->width, i->height);
|
|
}
|
|
}
|
|
|
|
const int sjis8x16DataSize = chars8x16 * 16;
|
|
uint8 *sjis8x16FontData = new uint8[sjis8x16DataSize];
|
|
|
|
if (!sjis8x16FontData)
|
|
error("Out of memory");
|
|
|
|
const int sjis16x16DataSize = chars16x16 * 32;
|
|
uint8 *sjis16x16FontData = new uint8[sjis16x16DataSize];
|
|
|
|
if (!sjis16x16FontData) {
|
|
delete[] sjis8x16FontData;
|
|
error("Out of memory");
|
|
}
|
|
|
|
const int sjis12x12DataSize = chars12x12 * 24;
|
|
uint8 *sjis12x12FontData = new uint8[sjis16x16DataSize];
|
|
|
|
if (!sjis12x12FontData) {
|
|
delete[] sjis8x16FontData;
|
|
delete[] sjis16x16FontData;
|
|
error("Out of memory");
|
|
}
|
|
|
|
memset(sjis8x16FontData, 0, sjis8x16DataSize);
|
|
memset(sjis16x16FontData, 0, sjis16x16DataSize);
|
|
memset(sjis12x12FontData, 0, sjis12x12DataSize);
|
|
|
|
for (GlyphList::const_iterator i = glyphs.begin(); i != glyphs.end(); ++i) {
|
|
if (isASCII(i->fB)) {
|
|
int chunk = mapASCIItoChunk(i->fB);
|
|
|
|
if (chunk != -1)
|
|
i->convertChar8x16(sjis8x16FontData + chunk * 16);
|
|
} else {
|
|
int chunk = mapSJIStoChunk(i->fB, i->sB);
|
|
|
|
if (chunk != -1)
|
|
i->convertChar16x16(sjis16x16FontData + chunk * 32);
|
|
}
|
|
}
|
|
|
|
for (GlyphList::const_iterator i = pceGlyphs.begin(), end = pceGlyphs.end(); i != end; ++i) {
|
|
int chunk = mapSJIStoChunk(i->fB, i->sB);
|
|
|
|
if (chunk != -1)
|
|
i->convertChar16x16(sjis12x12FontData + chunk * 24);
|
|
}
|
|
|
|
Common::File sjisFont(out, "wb");
|
|
if (sjisFont.isOpen()) {
|
|
// Write our magic bytes
|
|
sjisFont.writeUint32BE(MKID_BE('SCVM'));
|
|
sjisFont.writeUint32BE(MKID_BE('SJIS'));
|
|
|
|
// Write version
|
|
sjisFont.writeUint32BE(0x00000003);
|
|
|
|
// Write character count
|
|
sjisFont.writeUint16BE(chars16x16);
|
|
sjisFont.writeUint16BE(chars8x16);
|
|
sjisFont.writeUint16BE(chars12x12);
|
|
|
|
sjisFont.write(sjis16x16FontData, sjis16x16DataSize);
|
|
sjisFont.write(sjis8x16FontData, sjis8x16DataSize);
|
|
sjisFont.write(sjis12x12FontData, sjis12x12DataSize);
|
|
|
|
delete[] sjis8x16FontData;
|
|
delete[] sjis16x16FontData;
|
|
delete[] sjis12x12FontData;
|
|
|
|
if (sjisFont.err())
|
|
error("Error while writing to font file: '%s'", out);
|
|
} else {
|
|
delete[] sjis8x16FontData;
|
|
delete[] sjis16x16FontData;
|
|
delete[] sjis12x12FontData;
|
|
error("Could not open file '%s' for writing", out);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool isASCII(uint8 fB) {
|
|
return (mapASCIItoChunk(fB) != -1);
|
|
}
|
|
|
|
int mapASCIItoChunk(uint8 fB) {
|
|
// ASCII chars
|
|
if (fB <= 0x7F)
|
|
return fB;
|
|
|
|
// half-width katakana
|
|
if (fB >= 0xA1 && fB <= 0xDF)
|
|
return fB - 0x21;
|
|
return -1;
|
|
}
|
|
|
|
int mapSJIStoChunk(uint8 fB, uint8 sB) {
|
|
// We only allow 2 byte SJIS characters.
|
|
if (fB <= 0x80 || fB >= 0xF0 || (fB >= 0xA0 && fB <= 0xDF) || sB == 0x7F)
|
|
return -1;
|
|
|
|
int base = fB;
|
|
base -= 0x81;
|
|
if (base >= 0x5F)
|
|
base -= 0x40;
|
|
|
|
int index = sB;
|
|
index -= 0x40;
|
|
if (index >= 0x3F)
|
|
--index;
|
|
|
|
return (base * 0xBC + index);
|
|
}
|
|
|
|
iconv_t confSetup = (iconv_t)-1;
|
|
|
|
bool initSJIStoUTF32Conversion() {
|
|
// We initialize a SJIS to little endian UTF-32 conversion
|
|
// over here.
|
|
confSetup = iconv_open("UTF-32LE", "SJIS");
|
|
return (confSetup != (iconv_t)-1);
|
|
}
|
|
|
|
void deinitSJIStoUTF32Conversion() {
|
|
iconv_close(confSetup);
|
|
}
|
|
|
|
uint32 convertSJIStoUTF32(uint8 fB, uint8 sB) {
|
|
// For some reason iconv will refuse "0x81 0xAD" as valid
|
|
// SJIS sequence, even though it is "FULLWIDTH APOSTROPHE",
|
|
// thus we will short circuit iconv and convert it ourself
|
|
// here.
|
|
if (fB == 0x81 && sB == 0xAD)
|
|
return 0x0000FF07;
|
|
// SJIS uses "YEN SIGN" instead of "REVERSE SOLIDUS"
|
|
else if (fB == 0x5C && sB == 0x00)
|
|
return 0x000000A5;
|
|
// SJIS uses "OVERLINE" instead of "TILDE"
|
|
else if (fB == 0x7E && sB == 0x00)
|
|
return 0x0000203E;
|
|
|
|
char inBuf[3];
|
|
|
|
inBuf[0] = fB;
|
|
inBuf[1] = sB;
|
|
inBuf[2] = 0;
|
|
|
|
char outBuf[3 * sizeof(uint32)];
|
|
memset(outBuf, 0, sizeof(outBuf));
|
|
|
|
size_t inBufSize = ((fB >= 0x81 && fB <= 0x9F) || (fB >= 0xE0 && fB <= 0xEF)) ? 3 : 2;
|
|
size_t outBufSize = sizeof(outBuf);
|
|
#ifdef ICONV_USES_CONST
|
|
const char *inBufWrap = inBuf;
|
|
#else
|
|
char *inBufWrap = inBuf;
|
|
#endif
|
|
char *outBufWrap = outBuf;
|
|
|
|
if (iconv(confSetup, &inBufWrap, &inBufSize, &outBufWrap, &outBufSize) == (size_t)-1)
|
|
return (uint32)-1;
|
|
|
|
const uint32 ret = READ_LE_UINT32(outBuf);
|
|
|
|
// According to http://www.unicode.org/reports/tr19/tr19-9.html it is possible
|
|
// that a "zero width no-break space" is added as first character (probably
|
|
// to be consistent with the "byte order mark"). In case any iconv implementation
|
|
// does that, we just skip over that bit.
|
|
if (ret == 0x0000FEFF)
|
|
return READ_LE_UINT32(outBuf + 4);
|
|
else
|
|
return ret;
|
|
}
|
|
|
|
TrueTypeFont::TrueTypeFont()
|
|
: _library(0), _sjisFont(0), _ascent(0), _descent(0), _width(0), _height(0) {
|
|
}
|
|
|
|
TrueTypeFont::~TrueTypeFont() {
|
|
FT_Done_Face(_sjisFont);
|
|
FT_Done_FreeType(_library);
|
|
}
|
|
|
|
inline int ftFloor26_6(FT_Pos x) {
|
|
return x / 64;
|
|
}
|
|
|
|
inline int ftCeil26_6(FT_Pos x) {
|
|
return (x + 63) / 64;
|
|
}
|
|
|
|
bool TrueTypeFont::load(const char *font) {
|
|
FT_Error err = FT_Init_FreeType(&_library);
|
|
if (err) {
|
|
warning("Could not initialize FreeType2 library.");
|
|
return false;
|
|
}
|
|
|
|
err = FT_New_Face(_library, font, 0, &_sjisFont);
|
|
if (err) {
|
|
warning("Could not load font '%s'", font);
|
|
return false;
|
|
}
|
|
|
|
err = FT_Select_Charmap(_sjisFont, FT_ENCODING_UNICODE);
|
|
if (err) {
|
|
warning("Could not select unicode charmap.");
|
|
return false;
|
|
}
|
|
|
|
if (!FT_IS_SCALABLE(_sjisFont)) {
|
|
warning("Font '%s' is not scalable", font);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TrueTypeFont::setSize(int height) {
|
|
if (FT_Set_Char_Size(_sjisFont, 0, height * 64, 0, 0)) {
|
|
warning("Could not initialize font for height %d", height);
|
|
return false;
|
|
}
|
|
|
|
FT_Fixed yScale = _sjisFont->size->metrics.y_scale;
|
|
_ascent = ftCeil26_6(FT_MulFix(_sjisFont->ascender, yScale));
|
|
_descent = ftCeil26_6(FT_MulFix(_sjisFont->descender, yScale));
|
|
|
|
_width = ftCeil26_6(FT_MulFix(_sjisFont->max_advance_width, _sjisFont->size->metrics.x_scale));
|
|
_height = _ascent - _descent + 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
void TrueTypeFont::renderASCIIGlyphs(GlyphList &glyphs, int &count) {
|
|
count = 0;
|
|
|
|
for (uint8 fB = 0x00; fB <= 0xDF; ++fB) {
|
|
if (mapASCIItoChunk(fB) == -1)
|
|
continue;
|
|
|
|
++count;
|
|
|
|
Glyph data;
|
|
if (renderGlyph(fB, 0, data))
|
|
glyphs.push_back(data);
|
|
}
|
|
}
|
|
|
|
void TrueTypeFont::renderKANJIGlyphs(GlyphList &glyphs, int &count) {
|
|
count = 0;
|
|
|
|
for (uint8 fB = 0x81; fB <= 0xEF; ++fB) {
|
|
if (mapSJIStoChunk(fB, 0x40) == -1)
|
|
continue;
|
|
|
|
for (uint8 sB = 0x40; sB <= 0xFC; ++sB) {
|
|
if (mapSJIStoChunk(fB, sB) == -1)
|
|
continue;
|
|
|
|
++count;
|
|
|
|
Glyph data;
|
|
if (renderGlyph(fB, sB, data))
|
|
glyphs.push_back(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TrueTypeFont::renderGlyph(uint8 fB, uint8 sB, Glyph &glyph) {
|
|
uint32 utf32 = convertSJIStoUTF32(fB, sB);
|
|
if (utf32 == (uint32)-1) {
|
|
// For now we disable that warning, since iconv will fail for all reserved,
|
|
// that means unused, valid SJIS character codes.
|
|
//
|
|
// It might be useful to enable that warning again to detect problems with
|
|
// iconv though. An example for such an iconv problem is the
|
|
// "FULLWIDTH APOSTROPHE", which iconv refuses to convert to UTF-32.
|
|
if (errno == E2BIG || errno == EINVAL)
|
|
warning("Conversion error on: %.2X %.02X", fB, sB);
|
|
return false;
|
|
}
|
|
|
|
glyph.fB = fB;
|
|
glyph.sB = sB;
|
|
if (!renderGlyph(utf32, glyph)) {
|
|
warning("Could not render glyph: %.2X %.2X", fB, sB);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TrueTypeFont::renderGlyph(uint32 unicode, Glyph &glyph) {
|
|
uint32 index = FT_Get_Char_Index(_sjisFont, unicode);
|
|
if (!index)
|
|
return false;
|
|
|
|
FT_Error err = FT_Load_Glyph(_sjisFont, index, FT_LOAD_MONOCHROME | FT_LOAD_NO_HINTING);
|
|
if (err)
|
|
return false;
|
|
|
|
err = FT_Render_Glyph(_sjisFont->glyph, FT_RENDER_MODE_MONO);
|
|
if (err)
|
|
return false;
|
|
|
|
FT_Glyph_Metrics &metrics = _sjisFont->glyph->metrics;
|
|
|
|
glyph.xOffset = ftFloor26_6(metrics.horiBearingX);
|
|
glyph.yOffset = _ascent - ftFloor26_6(metrics.horiBearingY);
|
|
|
|
// In case we got a negative xMin we adjust that, this might make some
|
|
// characters give a odd layout though.
|
|
if (glyph.xOffset < 0)
|
|
glyph.xOffset = 0;
|
|
|
|
const FT_Bitmap &bitmap = _sjisFont->glyph->bitmap;
|
|
|
|
glyph.height = bitmap.rows;
|
|
glyph.width = bitmap.width;
|
|
glyph.pitch = bitmap.pitch;
|
|
glyph.plainData = 0;
|
|
|
|
// We only accept monochrome characters.
|
|
if (bitmap.pixel_mode != FT_PIXEL_MODE_MONO)
|
|
return false;
|
|
|
|
if (glyph.height) {
|
|
glyph.plainData = new uint8[glyph.height * abs(glyph.pitch)];
|
|
if (!glyph.plainData)
|
|
return false;
|
|
|
|
const uint8 *src = bitmap.buffer;
|
|
uint8 *dst = glyph.plainData;
|
|
|
|
if (glyph.pitch < 0)
|
|
dst += (glyph.height - 1) * (-glyph.pitch);
|
|
|
|
for (int i = 0; i < (int)bitmap.rows; ++i) {
|
|
memcpy(dst, src, abs(glyph.pitch));
|
|
src += bitmap.pitch;
|
|
dst += glyph.pitch;
|
|
}
|
|
}
|
|
|
|
glyph.pitch = abs(glyph.pitch);
|
|
|
|
return true;
|
|
}
|
|
|
|
Glyph::Glyph()
|
|
: fB(0), sB(0), xOffset(0), yOffset(0), height(0), width(0), pitch(0),
|
|
plainData(0) {
|
|
}
|
|
|
|
Glyph::Glyph(const Glyph &r)
|
|
: fB(r.fB), sB(r.sB), xOffset(r.xOffset), yOffset(r.yOffset),
|
|
height(r.height), width(r.width), pitch(r.pitch), plainData(0) {
|
|
plainData = new uint8[height * pitch];
|
|
memcpy(plainData, r.plainData, height * pitch);
|
|
}
|
|
|
|
Glyph::~Glyph() {
|
|
delete[] plainData;
|
|
}
|
|
|
|
Glyph &Glyph::operator=(const Glyph &r) {
|
|
delete[] plainData;
|
|
|
|
fB = r.fB;
|
|
sB = r.sB;
|
|
xOffset = r.xOffset;
|
|
yOffset = r.yOffset;
|
|
height = r.height;
|
|
width = r.width;
|
|
pitch = r.pitch;
|
|
|
|
plainData = new uint8[height * pitch];
|
|
memcpy(plainData, r.plainData, height * pitch);
|
|
|
|
return *this;
|
|
}
|
|
|
|
bool Glyph::checkSize(const int maxW, const int maxH) const {
|
|
if (yOffset < 0 || yOffset + height > maxH ||
|
|
xOffset < 0 || xOffset + width > maxW)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void Glyph::convertChar8x16(uint8 *dst) const {
|
|
const uint8 *src = plainData;
|
|
dst += yOffset;
|
|
|
|
for (int y = 0; y < height; ++y) {
|
|
uint8 mask = 1 << (7 - xOffset);
|
|
|
|
const uint8 *curSrc = src;
|
|
|
|
uint8 line = 0;
|
|
uint8 d = 0;
|
|
for (int x = 0; x < width; ++x) {
|
|
if (x == 0)
|
|
d = *curSrc++;
|
|
|
|
if (d & 0x80)
|
|
line |= mask;
|
|
|
|
d <<= 1;
|
|
mask >>= 1;
|
|
}
|
|
|
|
*dst++ = line;
|
|
src += pitch;
|
|
}
|
|
}
|
|
|
|
void Glyph::convertChar16x16(uint8 *dst) const {
|
|
const uint8 *src = plainData;
|
|
dst += yOffset * 2;
|
|
|
|
for (int y = 0; y < height; ++y) {
|
|
uint16 mask = 1 << (15 - xOffset);
|
|
|
|
const uint8 *curSrc = src;
|
|
|
|
uint16 line = 0;
|
|
uint8 d = 0;
|
|
for (int x = 0; x < width; ++x) {
|
|
if (x == 0 || x == 8)
|
|
d = *curSrc++;
|
|
|
|
if (d & 0x80)
|
|
line |= mask;
|
|
|
|
d <<= 1;
|
|
mask >>= 1;
|
|
}
|
|
|
|
WRITE_BE_UINT16(dst, line); dst += 2;
|
|
src += pitch;
|
|
}
|
|
}
|
|
|
|
void fixYOffset(GlyphList &glyphs) {
|
|
// We try to find the minimum y offset here so we can substract it to make it 0 in the end.
|
|
// We need to do this, since otherwise the characters will take up too much vertical space.
|
|
int minYOffset = 0xFFFF;
|
|
|
|
for (GlyphList::const_iterator i = glyphs.begin(), end = glyphs.end(); i != end; ++i) {
|
|
if (i->pitch == 0)
|
|
continue;
|
|
|
|
minYOffset = std::min(minYOffset, i->yOffset);
|
|
}
|
|
|
|
// Adapt all glyphs
|
|
for (GlyphList::iterator i = glyphs.begin(), end = glyphs.end(); i != end; ++i) {
|
|
if (i->pitch == 0)
|
|
continue;
|
|
|
|
i->yOffset -= minYOffset;
|
|
}
|
|
}
|
|
|