Rewrite TrueType font metric parsing code.
This commit is contained in:
parent
67f0a70c33
commit
42e052d405
|
@ -497,10 +497,13 @@ const AFM *PSDRV_FindAFMinList(FONTFAMILY *head, char *name)
|
|||
*
|
||||
* PSDRV_AddAFMtoList
|
||||
*
|
||||
* Adds an afm to the list whose head is pointed to by head. Creates new
|
||||
* family node if necessary and always creates a new AFMLISTENTRY.
|
||||
* Adds an afm to the list whose head is pointed to by head. Creates new
|
||||
* family node if necessary and always creates a new AFMLISTENTRY.
|
||||
*
|
||||
* Returns FALSE for memory allocation error; returns TRUE, but sets *p_added
|
||||
* to FALSE, for duplicate.
|
||||
*/
|
||||
BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm)
|
||||
BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm, BOOL *p_added)
|
||||
{
|
||||
FONTFAMILY *family = *head;
|
||||
FONTFAMILY **insert = head;
|
||||
|
@ -535,6 +538,7 @@ BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm)
|
|||
}
|
||||
strcpy( family->FamilyName, afm->FamilyName );
|
||||
family->afmlist = newafmle;
|
||||
*p_added = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
else {
|
||||
|
@ -543,6 +547,7 @@ BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm)
|
|||
if (!strcmp(tmpafmle->afm->FontName, afm->FontName)) {
|
||||
WARN("Ignoring duplicate FontName '%s'\n", afm->FontName);
|
||||
HeapFree(PSDRV_Heap, 0, newafmle);
|
||||
*p_added = FALSE;
|
||||
return TRUE; /* not a fatal error */
|
||||
}
|
||||
tmpafmle = tmpafmle->next;
|
||||
|
@ -555,6 +560,7 @@ BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm)
|
|||
|
||||
tmpafmle->next = newafmle;
|
||||
|
||||
*p_added = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
@ -834,14 +840,19 @@ static VOID CalcWindowsMetrics(AFM *afm)
|
|||
|
||||
static BOOL AddBuiltinAFMs()
|
||||
{
|
||||
int i = 0;
|
||||
const AFM *const *afm = PSDRV_BuiltinAFMs;
|
||||
|
||||
while (PSDRV_BuiltinAFMs[i] != NULL)
|
||||
while (*afm != NULL)
|
||||
{
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, PSDRV_BuiltinAFMs[i])
|
||||
== FALSE)
|
||||
BOOL added;
|
||||
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, *afm, &added) == FALSE)
|
||||
return FALSE;
|
||||
++i;
|
||||
|
||||
if (added == FALSE)
|
||||
TRACE("Ignoring built-in font %s\n", (*afm)->FontName);
|
||||
|
||||
++afm;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
@ -861,6 +872,7 @@ static BOOL AddBuiltinAFMs()
|
|||
static BOOL PSDRV_ReadAFMDir(const char* afmdir) {
|
||||
DIR *dir;
|
||||
const AFM *afm;
|
||||
BOOL added;
|
||||
|
||||
dir = opendir(afmdir);
|
||||
if (dir) {
|
||||
|
@ -881,7 +893,7 @@ static BOOL PSDRV_ReadAFMDir(const char* afmdir) {
|
|||
TRACE("loading AFM %s\n",afmfn);
|
||||
afm = PSDRV_AFMParse(afmfn);
|
||||
if (afm) {
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, afm) == FALSE) {
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, afm, &added) == FALSE) {
|
||||
closedir(dir);
|
||||
return FALSE;
|
||||
}
|
||||
|
@ -908,6 +920,7 @@ BOOL PSDRV_GetFontMetrics(void)
|
|||
char value[256];
|
||||
HKEY hkey;
|
||||
DWORD type, key_len, value_len;
|
||||
BOOL added;
|
||||
|
||||
if (PSDRV_GlyphListInit() != 0)
|
||||
return FALSE;
|
||||
|
@ -924,7 +937,7 @@ BOOL PSDRV_GetFontMetrics(void)
|
|||
const AFM* afm = PSDRV_AFMParse(value);
|
||||
|
||||
if (afm) {
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, afm) == FALSE) {
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, afm, &added) == FALSE) {
|
||||
RegCloseKey(hkey);
|
||||
return FALSE;
|
||||
}
|
||||
|
|
|
@ -620,7 +620,8 @@ PRINTERINFO *PSDRV_FindPrinterInfo(LPCSTR name)
|
|||
"ignoring\n", font->Name);
|
||||
}
|
||||
else {
|
||||
if (PSDRV_AddAFMtoList(&pi->Fonts, afm) == FALSE) {
|
||||
BOOL added;
|
||||
if (PSDRV_AddAFMtoList(&pi->Fonts, afm, &added) == FALSE) {
|
||||
PSDRV_FreeAFMList(pi->Fonts);
|
||||
goto cleanup;
|
||||
}
|
||||
|
|
|
@ -112,8 +112,10 @@ static char psrrectangle[] = /* x, y, width, height, -width */
|
|||
"%d 0 rlineto\n"
|
||||
"closepath\n";
|
||||
|
||||
#if 0
|
||||
static char psshow[] = /* string */
|
||||
"(%s) show\n";
|
||||
#endif
|
||||
|
||||
static const char psglyphshow[] = /* glyph name */
|
||||
"/%s glyphshow\n";
|
||||
|
|
|
@ -298,7 +298,8 @@ extern BOOL PSDRV_GetFontMetrics(void);
|
|||
extern PPD *PSDRV_ParsePPD(char *fname);
|
||||
extern PRINTERINFO *PSDRV_FindPrinterInfo(LPCSTR name);
|
||||
extern const AFM *PSDRV_FindAFMinList(FONTFAMILY *head, char *name);
|
||||
extern BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm);
|
||||
extern BOOL PSDRV_AddAFMtoList(FONTFAMILY **head, const AFM *afm,
|
||||
BOOL *p_added);
|
||||
extern void PSDRV_FreeAFMList( FONTFAMILY *head );
|
||||
|
||||
extern BOOL WINAPI PSDRV_Init(HINSTANCE hinst, DWORD reason, LPVOID reserved);
|
||||
|
|
|
@ -4,6 +4,13 @@
|
|||
*
|
||||
* Copyright 2001 Ian Pilcher
|
||||
*
|
||||
*
|
||||
* NOTE: Many of the functions in this file can return either fatal errors
|
||||
* (memory allocation failure or unexpected FreeType error) or non-fatal
|
||||
* errors (unusable font file). Fatal errors are indicated by returning
|
||||
* FALSE; see individual function descriptions for how they indicate non-
|
||||
* fatal errors.
|
||||
*
|
||||
*/
|
||||
#include "config.h"
|
||||
|
||||
|
@ -37,6 +44,7 @@
|
|||
#include <dirent.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "winnt.h"
|
||||
#include "winerror.h"
|
||||
|
@ -46,41 +54,27 @@
|
|||
|
||||
DEFAULT_DEBUG_CHANNEL(psdrv);
|
||||
|
||||
|
||||
#define REQUIRED_FACE_FLAGS ( FT_FACE_FLAG_SCALABLE | \
|
||||
FT_FACE_FLAG_HORIZONTAL | \
|
||||
FT_FACE_FLAG_SFNT | \
|
||||
FT_FACE_FLAG_GLYPH_NAMES )
|
||||
|
||||
static FT_Library library;
|
||||
static FT_Face face;
|
||||
static FT_CharMap charmap;
|
||||
static TT_Header *head;
|
||||
static TT_Postscript *post;
|
||||
static TT_OS2 *os2;
|
||||
static TT_HoriHeader *hhea;
|
||||
|
||||
/* This is now officially a pain in the ass! */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
LPSTR FontName;
|
||||
LPSTR FullName;
|
||||
LPSTR FamilyName;
|
||||
LPSTR EncodingScheme;
|
||||
} AFMSTRINGS;
|
||||
|
||||
#define GLYPH_LOAD_FLAGS ( FT_LOAD_NO_SCALE | \
|
||||
FT_LOAD_IGNORE_TRANSFORM | \
|
||||
FT_LOAD_LINEAR_DESIGN )
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* FindCharMap
|
||||
*
|
||||
* Sets charmap and points afm->EncodingScheme to encoding name (in driver
|
||||
* heap). Leaves both uninitialized if font contains no Windows encoding.
|
||||
* Finds Windows character map and creates "EncodingScheme" string. Returns
|
||||
* FALSE to indicate memory allocation or FreeType error; sets *p_charmap to
|
||||
* NULL if no Windows encoding is present.
|
||||
*
|
||||
* Returns FALSE to indicate memory allocation error.
|
||||
* Returns Unicode character map if present; otherwise uses the first Windows
|
||||
* character map found.
|
||||
*
|
||||
*/
|
||||
static const char *encoding_names[7] =
|
||||
static const LPCSTR encoding_names[7] =
|
||||
{
|
||||
"WindowsSymbol", /* TT_MS_ID_SYMBOL_CS */
|
||||
"WindowsUnicode", /* TT_MS_ID_UNICODE_CS */
|
||||
|
@ -89,14 +83,14 @@ static const char *encoding_names[7] =
|
|||
"WindowsBig5", /* TT_MS_ID_BIG_5 */
|
||||
"WindowsWansung", /* TT_MS_ID_WANSUNG */
|
||||
"WindowsJohab" /* TT_MS_ID_JOHAB */
|
||||
/* "WindowsUnknown65535" is the longest possible (encoding_id is a UShort) */
|
||||
};
|
||||
|
||||
static BOOL FindCharMap(AFM *afm, AFMSTRINGS *str)
|
||||
|
||||
static BOOL FindCharMap(FT_Face face, FT_CharMap *p_charmap, LPSTR *p_sz)
|
||||
{
|
||||
FT_Int i;
|
||||
FT_Error error;
|
||||
|
||||
charmap = NULL;
|
||||
FT_CharMap charmap = NULL;
|
||||
|
||||
for (i = 0; i < face->num_charmaps; ++i)
|
||||
{
|
||||
|
@ -104,82 +98,72 @@ static BOOL FindCharMap(AFM *afm, AFMSTRINGS *str)
|
|||
continue;
|
||||
|
||||
if (face->charmaps[i]->encoding_id == TT_MS_ID_UNICODE_CS)
|
||||
{
|
||||
{
|
||||
charmap = face->charmaps[i];
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (charmap == NULL)
|
||||
charmap = face->charmaps[i];
|
||||
}
|
||||
|
||||
*p_charmap = charmap;
|
||||
|
||||
if (charmap == NULL)
|
||||
return TRUE;
|
||||
|
||||
{
|
||||
WARN("No Windows character map found\n");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
error = FT_Set_Charmap(face, charmap);
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
ERR("%s returned %i\n", "FT_Set_CharMap", error);
|
||||
ERR("%s returned %i\n", "FT_Set_Charmap", error);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*p_sz = HeapAlloc(PSDRV_Heap, 0, sizeof("WindowsUnknown65535"));
|
||||
if (*p_sz == NULL)
|
||||
return FALSE;
|
||||
|
||||
if (charmap->encoding_id < 7)
|
||||
{
|
||||
if (!(str->EncodingScheme = HeapAlloc(PSDRV_Heap, 0,
|
||||
strlen(encoding_names[charmap->encoding_id])+1 )))
|
||||
return FALSE;
|
||||
strcpy( str->EncodingScheme, encoding_names[charmap->encoding_id] );
|
||||
}
|
||||
strcpy(*p_sz, encoding_names[charmap->encoding_id]);
|
||||
else
|
||||
{
|
||||
str->EncodingScheme =
|
||||
HeapAlloc(PSDRV_Heap, 0, sizeof("WindowsUnknown65535"));
|
||||
if (str->EncodingScheme == NULL)
|
||||
return FALSE;
|
||||
|
||||
sprintf(str->EncodingScheme, "%s%u", "WindowsUnknown",
|
||||
charmap->encoding_id);
|
||||
}
|
||||
|
||||
afm->EncodingScheme = str->EncodingScheme;
|
||||
|
||||
sprintf(*p_sz, "%s%u", "WindowsUnknown", charmap->encoding_id);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* NameTableString
|
||||
* MSTTStrToSz
|
||||
*
|
||||
* Converts a name table string to a null-terminated character string. The
|
||||
* space for the character string is allocated from the driver heap.
|
||||
* Converts a string in the TrueType NAME table to a null-terminated ASCII
|
||||
* character string. Space for the string is allocated from the driver heap.
|
||||
* Only handles platform_id = 3 (TT_PLATFORM_MICROSOFT) strings (16-bit, big
|
||||
* endian). It also only handles ASCII character codes (< 128).
|
||||
*
|
||||
* This function handles only platform_id = 3 (TT_PLATFORM_MICROSOFT) -- 16-bit
|
||||
* big-endian strings. It also only handles ASCII character codes (< 128).
|
||||
*
|
||||
* This function will set *sz to NULL if it cannot parse the string, but it
|
||||
* will only return FALSE in the event of an unexpected error (memory
|
||||
* allocation failure).
|
||||
* Sets *p_sz to NULL if string cannot be converted; only returns FALSE for
|
||||
* memory allocation failure.
|
||||
*
|
||||
*/
|
||||
static BOOL NameTableString(LPSTR *sz, const FT_SfntName *name)
|
||||
{
|
||||
FT_UShort i, len, *ws;
|
||||
LPSTR s;
|
||||
static BOOL MSTTStrToSz(const FT_SfntName *name, LPSTR *p_sz)
|
||||
{
|
||||
FT_UShort i;
|
||||
INT len;
|
||||
USHORT *wsz;
|
||||
LPSTR sz;
|
||||
|
||||
if (name->platform_id != TT_PLATFORM_MICROSOFT)
|
||||
{
|
||||
ERR("Unsupported encoding %i\n", name->platform_id);
|
||||
return FALSE; /* should never get here */
|
||||
}
|
||||
len = name->string_len / 2; /* # of 16-bit chars */
|
||||
|
||||
len = name->string_len / 2;
|
||||
*sz = s = HeapAlloc(PSDRV_Heap, 0, len + 1);
|
||||
if (s == NULL)
|
||||
*p_sz = sz = HeapAlloc(PSDRV_Heap, 0, len + 1);
|
||||
if (sz == NULL)
|
||||
return FALSE;
|
||||
ws = (FT_UShort *)(name->string);
|
||||
|
||||
for (i = 0; i < len; ++i, ++s, ++ws)
|
||||
wsz = (USHORT *)(name->string);
|
||||
|
||||
for (i = 0; i < len; ++i, ++sz, ++wsz)
|
||||
{
|
||||
FT_UShort wc = *ws;
|
||||
USHORT wc = *wsz;
|
||||
|
||||
#ifndef WORDS_BIGENDIAN
|
||||
wc = (wc >> 8) | (wc << 8);
|
||||
|
@ -188,39 +172,42 @@ static BOOL NameTableString(LPSTR *sz, const FT_SfntName *name)
|
|||
if (wc > 127)
|
||||
{
|
||||
WARN("Non-ASCII character 0x%.4x\n", wc);
|
||||
HeapFree(PSDRV_Heap, 0, *sz);
|
||||
*sz = NULL;
|
||||
HeapFree(PSDRV_Heap, 0, *p_sz);
|
||||
*p_sz = NULL;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
*s = (CHAR)wc;
|
||||
*sz = (CHAR)wc;
|
||||
}
|
||||
|
||||
*s = '\0';
|
||||
*sz = '\0';
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* ReadNameTable
|
||||
* FindMSTTString
|
||||
*
|
||||
* Reads various font names from the TrueType 'NAME' table. Currently looks
|
||||
* for U.S. English names only,
|
||||
* Finds the requested Microsoft platform string in the TrueType NAME table and
|
||||
* converts it to a null-terminated ASCII string. Currently looks for U.S.
|
||||
* English names only.
|
||||
*
|
||||
* May leave a pointer uninitialized if the desired string is not present;
|
||||
* returns FALSE only in the event of an unexpected error.
|
||||
* Sets string to NULL if not present or cannot be converted; returns FALSE
|
||||
* only for memory allocation failure.
|
||||
*
|
||||
*/
|
||||
static BOOL ReadNameTable(AFM *afm, AFMSTRINGS *str)
|
||||
static BOOL FindMSTTString(FT_Face face, FT_CharMap charmap, FT_UShort name_id,
|
||||
LPSTR *p_sz)
|
||||
{
|
||||
FT_UInt numStrings, stringIndex;
|
||||
FT_SfntName name;
|
||||
FT_Error error;
|
||||
|
||||
numStrings = FT_Get_Sfnt_Name_Count(face);
|
||||
FT_UInt num_strings, string_index;
|
||||
FT_SfntName name;
|
||||
FT_Error error;
|
||||
|
||||
for (stringIndex = 0; stringIndex < numStrings; ++stringIndex)
|
||||
num_strings = FT_Get_Sfnt_Name_Count(face);
|
||||
|
||||
for (string_index = 0; string_index < num_strings; ++string_index)
|
||||
{
|
||||
error = FT_Get_Sfnt_Name(face, stringIndex, &name);
|
||||
error = FT_Get_Sfnt_Name(face, string_index, &name);
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
ERR("%s returned %i\n", "FT_Get_Sfnt_Name", error);
|
||||
|
@ -229,36 +216,22 @@ static BOOL ReadNameTable(AFM *afm, AFMSTRINGS *str)
|
|||
|
||||
/* FIXME - Handle other languages? */
|
||||
|
||||
if (name.language_id != TT_MS_LANGID_ENGLISH_UNITED_STATES ||
|
||||
name.platform_id != charmap->platform_id ||
|
||||
if (name.platform_id != TT_PLATFORM_MICROSOFT ||
|
||||
name.language_id != TT_MS_LANGID_ENGLISH_UNITED_STATES)
|
||||
continue;
|
||||
|
||||
if (name.platform_id != charmap->platform_id ||
|
||||
name.encoding_id != charmap->encoding_id)
|
||||
continue;
|
||||
|
||||
switch (name.name_id)
|
||||
{
|
||||
case TT_NAME_ID_FONT_FAMILY:
|
||||
|
||||
if (NameTableString(&(str->FamilyName), &name) == FALSE)
|
||||
return FALSE;
|
||||
afm->FamilyName = str->FamilyName;
|
||||
break;
|
||||
if (name.name_id != name_id)
|
||||
continue;
|
||||
|
||||
case TT_NAME_ID_FULL_NAME:
|
||||
|
||||
if (NameTableString(&(str->FullName), &name) == FALSE)
|
||||
return FALSE;
|
||||
afm->FullName = str->FullName;
|
||||
break;
|
||||
|
||||
case TT_NAME_ID_PS_NAME:
|
||||
|
||||
if (NameTableString(&(str->FontName), &name) == FALSE)
|
||||
return FALSE;
|
||||
afm->FontName = str->FontName;
|
||||
break;
|
||||
}
|
||||
return MSTTStrToSz(&name, p_sz);
|
||||
}
|
||||
|
||||
*p_sz = NULL; /* didn't find it */
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
@ -266,50 +239,49 @@ static BOOL ReadNameTable(AFM *afm, AFMSTRINGS *str)
|
|||
* PSUnits
|
||||
*
|
||||
* Convert TrueType font units (relative to font em square) to PostScript
|
||||
* units. This is defined as a macro, so it can handle different TrueType
|
||||
* data types as inputs.
|
||||
* units.
|
||||
*
|
||||
*/
|
||||
#define PSUnits(x) (((float)(x)) * 1000.0 / ((float)(head->Units_Per_EM)))
|
||||
inline static float PSUnits(LONG x, USHORT em_size)
|
||||
{
|
||||
return 1000.0 * (float)x / (float)em_size;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* ReadMetricsTables
|
||||
* StartAFM
|
||||
*
|
||||
* Reads basic font metrics from the 'head', 'post', and 'OS/2' tables.
|
||||
* Returns FALSE if any table is missing.
|
||||
* Allocates space for the AFM on the driver heap and reads basic font metrics
|
||||
* from the HEAD, POST, HHEA, and OS/2 tables. Returns FALSE for memory
|
||||
* allocation error; sets *p_afm to NULL if required information is missing.
|
||||
*
|
||||
*/
|
||||
static BOOL ReadMetricsTables(AFM *afm)
|
||||
*/
|
||||
static BOOL StartAFM(FT_Face face, AFM **p_afm)
|
||||
{
|
||||
TT_Header *head;
|
||||
TT_Postscript *post;
|
||||
TT_OS2 *os2;
|
||||
TT_HoriHeader *hhea;
|
||||
USHORT em_size;
|
||||
AFM *afm;
|
||||
|
||||
head = FT_Get_Sfnt_Table(face, ft_sfnt_head);
|
||||
post = FT_Get_Sfnt_Table(face, ft_sfnt_post);
|
||||
hhea = FT_Get_Sfnt_Table(face, ft_sfnt_hhea);
|
||||
os2 = FT_Get_Sfnt_Table(face, ft_sfnt_os2);
|
||||
hhea = FT_Get_Sfnt_Table(face, ft_sfnt_hhea);
|
||||
|
||||
if (head == NULL || post == NULL || hhea == NULL || os2 == NULL)
|
||||
return FALSE;
|
||||
if (head == NULL || post == NULL || os2 == NULL || hhea == NULL ||
|
||||
os2->version == 0xffff) /* old Macintosh font */
|
||||
{
|
||||
WARN("Required table(s) missing\n");
|
||||
*p_afm = NULL;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (os2->version == 0xffff) /* Old Macintosh font */
|
||||
return FALSE;
|
||||
*p_afm = afm = HeapAlloc(PSDRV_Heap, 0, sizeof(*afm));
|
||||
if (afm == NULL)
|
||||
return FALSE;
|
||||
|
||||
afm->Weight = os2->usWeightClass;
|
||||
afm->ItalicAngle = ((float)(post->italicAngle)) / 65536.0;
|
||||
afm->IsFixedPitch = (post->isFixedPitch == 0) ? FALSE : TRUE;
|
||||
afm->UnderlinePosition = PSUnits(post->underlinePosition);
|
||||
afm->UnderlineThickness = PSUnits(post->underlineThickness);
|
||||
|
||||
afm->FontBBox.llx = PSUnits(head->xMin);
|
||||
afm->FontBBox.lly = PSUnits(head->yMin);
|
||||
afm->FontBBox.urx = PSUnits(head->xMax);
|
||||
afm->FontBBox.ury = PSUnits(head->yMax);
|
||||
|
||||
/* CapHeight & XHeight set by ReadCharMetrics */
|
||||
|
||||
afm->Ascender = PSUnits(os2->sTypoAscender);
|
||||
afm->Descender = PSUnits(os2->sTypoDescender);
|
||||
afm->FullAscender = afm->FontBBox.ury; /* get rid of this */
|
||||
|
||||
afm->WinMetrics.usUnitsPerEm = head->Units_Per_EM;
|
||||
afm->WinMetrics.usUnitsPerEm = em_size = head->Units_Per_EM;
|
||||
afm->WinMetrics.sAscender = hhea->Ascender;
|
||||
afm->WinMetrics.sDescender = hhea->Descender;
|
||||
afm->WinMetrics.sLineGap = hhea->Line_Gap;
|
||||
|
@ -319,6 +291,23 @@ static BOOL ReadMetricsTables(AFM *afm)
|
|||
afm->WinMetrics.usWinAscent = os2->usWinAscent;
|
||||
afm->WinMetrics.usWinDescent = os2->usWinDescent;
|
||||
afm->WinMetrics.sAvgCharWidth = os2->xAvgCharWidth;
|
||||
|
||||
afm->Weight = os2->usWeightClass;
|
||||
afm->ItalicAngle = ((float)(post->italicAngle)) / 65536.0;
|
||||
afm->IsFixedPitch = (post-> isFixedPitch == 0) ? FALSE : TRUE;
|
||||
afm->UnderlinePosition = PSUnits(post->underlinePosition, em_size);
|
||||
afm->UnderlineThickness = PSUnits(post->underlineThickness, em_size);
|
||||
|
||||
afm->FontBBox.llx = PSUnits(head->xMin, em_size);
|
||||
afm->FontBBox.lly = PSUnits(head->yMin, em_size);
|
||||
afm->FontBBox.urx = PSUnits(head->xMax, em_size);
|
||||
afm->FontBBox.ury = PSUnits(head->yMax, em_size);
|
||||
|
||||
afm->Ascender = PSUnits(os2->sTypoAscender, em_size);
|
||||
afm->Descender = PSUnits(os2->sTypoDescender, em_size);
|
||||
afm->FullAscender = afm->FontBBox.ury; /* get rid of this */
|
||||
|
||||
/* CapHeight & XHeight set by ReadCharMetrics */
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -326,43 +315,38 @@ static BOOL ReadMetricsTables(AFM *afm)
|
|||
/*******************************************************************************
|
||||
* ReadCharMetrics
|
||||
*
|
||||
* Reads metrics for each glyph in a TrueType font.
|
||||
* Reads metrics for each glyph in a TrueType font. Returns false for memory
|
||||
* allocation or FreeType error; sets *p_metrics to NULL for non-fatal error.
|
||||
*
|
||||
*/
|
||||
static AFMMETRICS *ReadCharMetrics(AFM *afm)
|
||||
static BOOL ReadCharMetrics(FT_Face face, AFM *afm, AFMMETRICS **p_metrics)
|
||||
{
|
||||
FT_ULong charcode, index;
|
||||
AFMMETRICS *metrics;
|
||||
FT_ULong charcode, index;
|
||||
AFMMETRICS *metrics;
|
||||
USHORT em_size = afm->WinMetrics.usUnitsPerEm;
|
||||
|
||||
/*
|
||||
* There does not seem to be an easy way to get the number of characters
|
||||
* in an encoding out of a TrueType font.
|
||||
*/
|
||||
for (charcode = 0, index = 0; charcode < 65536; ++charcode)
|
||||
{
|
||||
if (FT_Get_Char_Index(face, charcode) != 0)
|
||||
++index;
|
||||
}
|
||||
|
||||
++index; /* count # of glyphs */
|
||||
|
||||
afm->NumofMetrics = index;
|
||||
|
||||
metrics = HeapAlloc(PSDRV_Heap, 0, index * sizeof(AFMMETRICS));
|
||||
*p_metrics = metrics = HeapAlloc(PSDRV_Heap, 0, index * sizeof(*metrics));
|
||||
if (metrics == NULL)
|
||||
return NULL;
|
||||
return FALSE;
|
||||
|
||||
for (charcode = 0, index = 0; charcode <= 65536; ++charcode)
|
||||
for (charcode = 0, index = 0; charcode < 65536; ++charcode)
|
||||
{
|
||||
FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
|
||||
FT_Error error;
|
||||
FT_Glyph glyph;
|
||||
FT_BBox bbox;
|
||||
char buffer[256];
|
||||
CHAR buffer[128]; /* for glyph names */
|
||||
|
||||
if (glyph_index == 0)
|
||||
continue;
|
||||
|
||||
error = FT_Load_Glyph(face, glyph_index, FT_LOAD_NO_SCALE |
|
||||
FT_LOAD_IGNORE_TRANSFORM | FT_LOAD_LINEAR_DESIGN);
|
||||
error = FT_Load_Glyph(face, glyph_index, GLYPH_LOAD_FLAGS);
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
ERR("%s returned %i\n", "FT_Load_Glyph", error);
|
||||
|
@ -378,278 +362,256 @@ static AFMMETRICS *ReadCharMetrics(AFM *afm)
|
|||
|
||||
FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_unscaled, &bbox);
|
||||
|
||||
error = FT_Get_Glyph_Name(face, glyph_index, buffer, 255);
|
||||
error = FT_Get_Glyph_Name(face, glyph_index, buffer, sizeof(buffer));
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
ERR("%s returned %i\n", "FT_Get_Glyph_Name", error);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
metrics[index].N = PSDRV_GlyphName(buffer);
|
||||
if (metrics[index].N == NULL)
|
||||
goto cleanup;;
|
||||
|
||||
metrics[index].C = charcode;
|
||||
metrics[index].UV = charcode;
|
||||
metrics[index].WX = PSUnits(face->glyph->metrics.horiAdvance);
|
||||
metrics[index].B.llx = PSUnits(bbox.xMin);
|
||||
metrics[index].B.lly = PSUnits(bbox.yMin);
|
||||
metrics[index].B.urx = PSUnits(bbox.xMax);
|
||||
metrics[index].B.ury = PSUnits(bbox.yMax);
|
||||
goto cleanup;
|
||||
|
||||
metrics[index].C = metrics[index].UV = charcode;
|
||||
metrics[index].WX = PSUnits(face->glyph->metrics.horiAdvance, em_size);
|
||||
metrics[index].B.llx = PSUnits(bbox.xMin, em_size);
|
||||
metrics[index].B.lly = PSUnits(bbox.yMin, em_size);
|
||||
metrics[index].B.urx = PSUnits(bbox.xMax, em_size);
|
||||
metrics[index].B.ury = PSUnits(bbox.yMax, em_size);
|
||||
metrics[index].L = NULL;
|
||||
|
||||
TRACE("Metrics for '%s' WX = %f B = %f,%f - %f,%f\n",
|
||||
metrics[index].N->sz, metrics[index].WX,
|
||||
metrics[index].B.llx, metrics[index].B.lly,
|
||||
metrics[index].B.urx, metrics[index].B.ury);
|
||||
|
||||
if (charcode == 0x0048) /* 'H' */
|
||||
afm->CapHeight = PSUnits(bbox.yMax);
|
||||
afm->CapHeight = metrics[index].B.ury;
|
||||
if (charcode == 0x0078) /* 'x' */
|
||||
afm->XHeight = PSUnits(bbox.yMax);
|
||||
|
||||
afm->XHeight = metrics[index].B.ury;
|
||||
|
||||
++index;
|
||||
}
|
||||
|
||||
return metrics;
|
||||
|
||||
cleanup:
|
||||
|
||||
HeapFree(PSDRV_Heap, 0, metrics);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* ReadTrueTypeAFM
|
||||
*
|
||||
* Fills in AFM structure for opened TrueType font file. Returns FALSE only on
|
||||
* an unexpected error (memory allocation failure or FreeType error); otherwise
|
||||
* returns TRUE.
|
||||
*
|
||||
*/
|
||||
static BOOL ReadTrueTypeAFM(AFM *afm)
|
||||
{
|
||||
AFMSTRINGS str = { NULL, NULL, NULL, NULL };
|
||||
AFMMETRICS *metrics;
|
||||
|
||||
if ((face->face_flags & REQUIRED_FACE_FLAGS) != REQUIRED_FACE_FLAGS)
|
||||
{
|
||||
WARN("Font flags do not match requirements\n");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (FindCharMap(afm, &str) == FALSE)
|
||||
return FALSE;
|
||||
|
||||
if (charmap == NULL)
|
||||
{
|
||||
WARN("No Windows encodings in font\n");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
TRACE("Using encoding '%s'\n", afm->EncodingScheme);
|
||||
|
||||
if (ReadNameTable(afm, &str) == FALSE)
|
||||
{
|
||||
if (str.FontName != NULL) HeapFree(PSDRV_Heap, 0, str.FontName);
|
||||
if (str.FullName != NULL) HeapFree(PSDRV_Heap, 0, str.FullName);
|
||||
if (str.FamilyName != NULL) HeapFree(PSDRV_Heap, 0, str.FamilyName);
|
||||
HeapFree(PSDRV_Heap, 0, str.EncodingScheme);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (str.FamilyName == NULL || str.FullName == NULL || str.FontName == NULL)
|
||||
{
|
||||
WARN("Required strings missing from font\n");
|
||||
if (str.FontName != NULL) HeapFree(PSDRV_Heap, 0, str.FontName);
|
||||
if (str.FullName != NULL) HeapFree(PSDRV_Heap, 0, str.FullName);
|
||||
if (str.FamilyName != NULL) HeapFree(PSDRV_Heap, 0, str.FamilyName);
|
||||
HeapFree(PSDRV_Heap, 0, str.EncodingScheme);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (ReadMetricsTables(afm) == FALSE) /* Non-fatal */
|
||||
{
|
||||
WARN("Required metrics tables missing from font\n");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
afm->Metrics = metrics = ReadCharMetrics(afm);
|
||||
if (metrics == NULL)
|
||||
{
|
||||
HeapFree(PSDRV_Heap, 0, str.FontName);
|
||||
HeapFree(PSDRV_Heap, 0, str.FullName);
|
||||
HeapFree(PSDRV_Heap, 0, str.FamilyName);
|
||||
HeapFree(PSDRV_Heap, 0, str.EncodingScheme);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Can't do this check until character metrics are read */
|
||||
|
||||
if (afm->WinMetrics.sAvgCharWidth == 0)
|
||||
afm->WinMetrics.sAvgCharWidth = PSDRV_CalcAvgCharWidth(afm);
|
||||
|
||||
if (PSDRV_AddAFMtoList(&PSDRV_AFMFontList, afm) == FALSE)
|
||||
{
|
||||
HeapFree(PSDRV_Heap, 0, str.FontName);
|
||||
HeapFree(PSDRV_Heap, 0, str.FullName);
|
||||
HeapFree(PSDRV_Heap, 0, str.FamilyName);
|
||||
HeapFree(PSDRV_Heap, 0, str.EncodingScheme);
|
||||
HeapFree(PSDRV_Heap, 0, metrics);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(PSDRV_Heap, 0, metrics);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* ReadTrueTypeFile
|
||||
* BuildTrueTypeAFM
|
||||
*
|
||||
* Reads PostScript-style font metrics from a TrueType font file. Only returns
|
||||
* FALSE for unexpected errors (memory allocation, etc.); returns TRUE if it's
|
||||
* just a bad font file.
|
||||
* Builds the AFM for a TrueType font and adds it to the driver font list.
|
||||
* Returns FALSE only on an unexpected error (memory allocation failure or
|
||||
* FreeType error).
|
||||
*
|
||||
*/
|
||||
static BOOL ReadTrueTypeFile(LPCSTR filename)
|
||||
static BOOL BuildTrueTypeAFM(FT_Face face)
|
||||
{
|
||||
FT_Error error;
|
||||
AFM *afm;
|
||||
AFM *afm;
|
||||
AFMMETRICS *metrics;
|
||||
LPSTR font_name, full_name, family_name, encoding_scheme;
|
||||
FT_CharMap charmap;
|
||||
BOOL retval, added;
|
||||
|
||||
retval = StartAFM(face, &afm);
|
||||
if (retval == FALSE || afm == NULL)
|
||||
return retval;
|
||||
|
||||
retval = FindCharMap(face, &charmap, &encoding_scheme);
|
||||
if (retval == FALSE || charmap == NULL)
|
||||
goto cleanup_afm;
|
||||
|
||||
retval = FindMSTTString(face, charmap, TT_NAME_ID_PS_NAME, &font_name);
|
||||
if (retval == FALSE || font_name == NULL)
|
||||
goto cleanup_encoding_scheme;
|
||||
|
||||
retval = FindMSTTString(face, charmap, TT_NAME_ID_FULL_NAME, &full_name);
|
||||
if (retval == FALSE || full_name == NULL)
|
||||
goto cleanup_font_name;
|
||||
|
||||
retval = FindMSTTString(face, charmap, TT_NAME_ID_FONT_FAMILY,
|
||||
&family_name);
|
||||
if (retval == FALSE || family_name == NULL)
|
||||
goto cleanup_full_name;
|
||||
|
||||
retval = ReadCharMetrics(face, afm, &metrics);
|
||||
if (retval == FALSE || metrics == NULL)
|
||||
goto cleanup_family_name;
|
||||
|
||||
afm->EncodingScheme = encoding_scheme; afm->FontName = font_name;
|
||||
afm->FullName = full_name; afm->FamilyName = family_name;
|
||||
afm->Metrics = metrics;
|
||||
|
||||
retval = PSDRV_AddAFMtoList(&PSDRV_AFMFontList, afm, &added);
|
||||
if (retval == FALSE || added == FALSE)
|
||||
goto cleanup_family_name;
|
||||
|
||||
return TRUE;
|
||||
|
||||
/* clean up after fatal or non-fatal errors */
|
||||
|
||||
cleanup_family_name:
|
||||
HeapFree(PSDRV_Heap, 0, family_name);
|
||||
cleanup_full_name:
|
||||
HeapFree(PSDRV_Heap, 0, full_name);
|
||||
cleanup_font_name:
|
||||
HeapFree(PSDRV_Heap, 0, font_name);
|
||||
cleanup_encoding_scheme:
|
||||
HeapFree(PSDRV_Heap, 0, encoding_scheme);
|
||||
cleanup_afm:
|
||||
HeapFree(PSDRV_Heap, 0, afm);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* ReadTrueTypeFile
|
||||
*
|
||||
* Reads font metrics from TrueType font file. Only returns FALSE for
|
||||
* unexpected errors (memory allocation failure or FreeType error).
|
||||
*
|
||||
*/
|
||||
static BOOL ReadTrueTypeFile(FT_Library library, LPCSTR filename)
|
||||
{
|
||||
FT_Error error;
|
||||
FT_Face face;
|
||||
|
||||
TRACE("%s\n", filename);
|
||||
|
||||
TRACE("'%s'\n", filename);
|
||||
|
||||
afm = HeapAlloc(PSDRV_Heap, HEAP_ZERO_MEMORY, sizeof(AFM));
|
||||
if (afm == NULL)
|
||||
return FALSE;
|
||||
|
||||
error = FT_New_Face(library, filename, 0, &face);
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
WARN("FreeType error %i opening '%s'\n", error, filename);
|
||||
HeapFree(PSDRV_Heap, 0, afm);
|
||||
WARN("FreeType error %i opening %s\n", error, filename);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (ReadTrueTypeAFM(afm) == FALSE)
|
||||
if ((face->face_flags & REQUIRED_FACE_FLAGS) == REQUIRED_FACE_FLAGS)
|
||||
{
|
||||
HeapFree(PSDRV_Heap, 0, afm);
|
||||
FT_Done_Face(face);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (BuildTrueTypeAFM(face) == FALSE)
|
||||
{
|
||||
FT_Done_Face(face);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WARN("Required information missing from %s\n", filename);
|
||||
}
|
||||
|
||||
error = FT_Done_Face(face);
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
ERR("%s returned %i\n", "FT_Done_Face", error);
|
||||
HeapFree(PSDRV_Heap, 0, afm);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (afm->Metrics == NULL) /* last element to be set */
|
||||
{
|
||||
HeapFree(PSDRV_Heap, 0, afm);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* ReadTrueTypeDir
|
||||
*
|
||||
* Reads all TrueType font files in a directory.
|
||||
*
|
||||
*/
|
||||
static BOOL ReadTrueTypeDir(FT_Library library, LPCSTR dirname)
|
||||
{
|
||||
struct dirent *dent;
|
||||
DIR *dir;
|
||||
CHAR filename[256];
|
||||
|
||||
|
||||
|
||||
dir = opendir(dirname);
|
||||
if (dir == NULL)
|
||||
{
|
||||
WARN("'%s' opening %s\n", strerror(errno), dirname);
|
||||
return TRUE;;
|
||||
}
|
||||
|
||||
while ((dent = readdir(dir)) != NULL)
|
||||
{
|
||||
CHAR *file_extension = strrchr(dent->d_name, '.');
|
||||
int fn_len;
|
||||
|
||||
if (file_extension == NULL || strcasecmp(file_extension, ".ttf") != 0)
|
||||
continue;
|
||||
|
||||
fn_len = snprintf(filename, 256, "%s/%s", dirname, dent->d_name);
|
||||
if (fn_len < 0 || fn_len > sizeof(filename) - 1)
|
||||
{
|
||||
WARN("Path '%s/%s' is too long\n", dirname, dent->d_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReadTrueTypeFile(library, filename) == FALSE)
|
||||
{
|
||||
closedir(dir);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* PSDRV_GetTrueTypeMetrics
|
||||
*
|
||||
* Reads PostScript-stype font metrics from TrueType font files in directories
|
||||
* listed in the [TrueType Font Directories] section of the Wine configuration
|
||||
* file.
|
||||
* Reads font metrics from TrueType font files in directories listed in the
|
||||
* [TrueType Font Directories] section of the Wine configuration file.
|
||||
*
|
||||
* If this function fails, the driver will fail to initialize and the driver
|
||||
* heap will be destroyed, so it's not necessary to HeapFree everything in
|
||||
* that event.
|
||||
* If this function fails (returns FALSE), the driver will fail to initialize
|
||||
* and the driver heap will be destroyed, so it's not necessary to HeapFree
|
||||
* everything in that event.
|
||||
*
|
||||
*/
|
||||
BOOL PSDRV_GetTrueTypeMetrics(void)
|
||||
{
|
||||
CHAR keybuf[256], namebuf[256];
|
||||
CHAR name_buf[256], value_buf[256];
|
||||
INT i = 0;
|
||||
FT_Error error;
|
||||
FT_Library library;
|
||||
HKEY hkey;
|
||||
DWORD type, key_len, name_len;
|
||||
DWORD type, name_len, value_len;
|
||||
|
||||
if(RegOpenKeyExA(HKEY_LOCAL_MACHINE,
|
||||
"Software\\Wine\\Wine\\Config\\TrueType Font Directories",
|
||||
0, KEY_READ, &hkey) != ERROR_SUCCESS)
|
||||
return TRUE;
|
||||
|
||||
error = FT_Init_FreeType(&library);
|
||||
if (error != FT_Err_Ok)
|
||||
{
|
||||
ERR("%s returned %i\n", "FT_Init_FreeType", error);
|
||||
RegCloseKey(hkey);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if(RegOpenKeyExA(HKEY_LOCAL_MACHINE,
|
||||
"Software\\Wine\\Wine\\Config\\TrueType Font Directories",
|
||||
0, KEY_READ, &hkey) != ERROR_SUCCESS)
|
||||
goto no_metrics;
|
||||
|
||||
key_len = sizeof(keybuf);
|
||||
name_len = sizeof(namebuf);
|
||||
name_len = sizeof(name_buf);
|
||||
value_len = sizeof(value_buf);
|
||||
|
||||
while(RegEnumValueA(hkey, i++, keybuf, &key_len, NULL, &type, namebuf,
|
||||
&name_len) == ERROR_SUCCESS)
|
||||
while(RegEnumValueA(hkey, i++, name_buf, &name_len, NULL, &type, value_buf,
|
||||
&value_len) == ERROR_SUCCESS)
|
||||
{
|
||||
struct dirent *dent;
|
||||
DIR *dir;
|
||||
INT dnlen; /* directory name length */
|
||||
|
||||
namebuf[sizeof(namebuf) - 1] = '\0';
|
||||
dir = opendir(namebuf);
|
||||
if (dir == NULL)
|
||||
value_buf[sizeof(value_buf) - 1] = '\0';
|
||||
|
||||
if (ReadTrueTypeDir(library, value_buf) == FALSE)
|
||||
{
|
||||
WARN("Error opening directory '%s'\n", namebuf);
|
||||
continue;
|
||||
RegCloseKey(hkey);
|
||||
FT_Done_FreeType(library);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
dnlen = strlen(namebuf);
|
||||
namebuf[dnlen] = '/'; /* 2 slashes is OK, 0 is not */
|
||||
++dnlen;
|
||||
|
||||
while ((dent = readdir(dir)) != NULL)
|
||||
{
|
||||
INT fnlen; /* file name length */
|
||||
|
||||
fnlen = strlen(dent->d_name);
|
||||
|
||||
if (fnlen < 5 || strcasecmp(dent->d_name + fnlen - 4, ".ttf") != 0)
|
||||
{
|
||||
TRACE("Skipping filename '%s'\n", dent->d_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dnlen + fnlen + 1 > sizeof(namebuf)) /* allow for '\0' */
|
||||
{
|
||||
WARN("Path '%s/%s' is too long\n", namebuf, dent->d_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
memcpy(namebuf + dnlen, dent->d_name, fnlen + 1);
|
||||
|
||||
if (ReadTrueTypeFile(namebuf) == FALSE)
|
||||
{
|
||||
ERR("Error reading '%s'\n", namebuf);
|
||||
closedir(dir);
|
||||
RegCloseKey(hkey);
|
||||
FT_Done_FreeType(library);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
/* initialize lengths for new iteration */
|
||||
key_len = sizeof(keybuf);
|
||||
name_len = sizeof(namebuf);
|
||||
|
||||
name_len = sizeof(name_buf);
|
||||
value_len = sizeof(value_buf);
|
||||
}
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
no_metrics:
|
||||
|
||||
FT_Done_FreeType(library);
|
||||
return TRUE;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue