Commits

Lenard Lindstrom committed 96fe757

allow (width, height) freetype.Font size values (towards Issue #75)

A *size* property or argument will also take a 2-tuple of ppem values, for
better bitmap size selection from the available sizes. Bitmap size can also
be selected using point size when only size is a single number. A side
effect of allowing separate width and height values is that scalable fonts
can be stretch vertically or horizontally.

  • Participants
  • Parent commits 054dbe9

Comments (0)

Files changed (12)

docs/reST/ref/freetype.rst

    .. attribute:: size
 
       | :sl:`The default point size used in rendering`
-      | :sg:`size -> int`
+      | :sg:`size -> float`
+      | :sg:`size -> (float, float)`
 
-      Get or set a default point size used by metric or render methods.
+      Get or set the default size for text metrics and rendering. It can be
+      a single point size, given as an Python ``int`` or ``float``, or a
+      font ppem (width, height) ``tuple``. Size values are non-negative.
+      A zero size or width represents an undefined size. In this case
+      the size must be given as a method argument, or an exception is
+      raised. A zero width but non-zero height is a ValueError.
+
+      For a scalable font, a single number value is equivalent to a tuple
+      with width equal height. A font can be stretched vertically with
+      height set greater than width, or horizontally with width set
+      greater than height. For embedded bitmaps, as listed by :meth:`get_sizes`,
+      use the nominal width and height to select an available size.
+
+      Font size differs for a non-scalable, bitmap, font. During a
+      method call it must match one of the available sizes returned by
+      method :meth:`get_sizes`. If not, an exception is raised.
+      If the size is a single number, the size is first matched against the
+      point size value. If no match, then the available size with the
+      same nominal width and height is chosen.
 
    .. method:: get_rect
 

examples/freetype_misc.py

     # Some pinwheels
     font.origin = True
     for angle in range(0, 360, 45):
-        font.render_to(screen, (200, 500), ")", pygame.Color('black'),
+        font.render_to(screen, (150, 420), ")", pygame.Color('black'),
                        size=48, rotation=angle)
     font.vertical = True
     for angle in range(15, 375, 30):
                    pygame.Color(0, 0, 0),
                    None, size=24, style=freetype.STYLE_STRONG)
 
+    font.origin = True
+    r = font.render_to(screen, (100, 530), "stretch",
+                   pygame.Color('red'),
+                   None, size=(24, 24), style=freetype.STYLE_NORMAL)
+    font.render_to(screen, (100 + r.width, 530), " VERTICAL",
+                   pygame.Color('red'),
+                   None, size=(24, 48), style=freetype.STYLE_NORMAL)
+
+    r = font.render_to(screen, (100, 580), "stretch",
+                   pygame.Color('blue'),
+                   None, size=(24, 24), style=freetype.STYLE_NORMAL)
+    font.render_to(screen, (100 + r.width, 580), " HORIZONTAL",
+                   pygame.Color('blue'),
+                   None, size=(48, 24), style=freetype.STYLE_NORMAL)
+
     pygame.display.flip()
 
     while 1:
 /*
  * FreeType module declarations
  */
+static const Scale_t FACE_SIZE_NONE = {0, 0};
+
 #if PY3
 static int _ft_traverse(PyObject *, visitproc, void *);
 static int _ft_clear(PyObject *);
 static PyObject *load_font_res(const char *);
 static int parse_dest(PyObject *, int *, int *);
 static int obj_to_scale(PyObject *, void *);
+static int objs_to_scale(PyObject *, PyObject *, Scale_t *);
+static int numbers_to_scale(PyObject *, PyObject *, Scale_t *);
+static int build_scale(PyObject *, PyObject *, Scale_t *);
+static FT_UInt number_to_FX6_unsigned(PyObject *);
 static int obj_to_rotation(PyObject *, void *);
 
 /*
 static int
 obj_to_scale(PyObject *o, void *p)
 {
-    long size;
-    PyObject *lower_limit = 0;
-    PyObject *upper_limit = 0;
+    if (PyTuple_Check(o)) {
+        if (PyTuple_GET_SIZE(o) != 2) {
+            PyErr_Format(PyExc_TypeError,
+                         "expected a 2-tuple for size, got %zd-tuple",
+                         PyTuple_GET_SIZE(o));
+            return 0;
+        }
+        return objs_to_scale(PyTuple_GET_ITEM(o, 0),
+                             PyTuple_GET_ITEM(o, 1),
+                             (Scale_t *)p);
+    }
+    return objs_to_scale(o, 0, (Scale_t *)p);
+}
+
+static int
+objs_to_scale(PyObject *x, PyObject *y, Scale_t *size)
+{
+    PyObject *o;
+    int do_y;
+
+    for (o = x, do_y = 1; o; o = (do_y--) ? y : 0) {
+        if (!PyLong_Check(o) &&
+#if PY2
+            !PyInt_Check(o) &&
+#endif
+            !PyFloat_Check(o)) {
+            if (y) {
+                PyErr_Format(PyExc_TypeError,
+                             "expected a (float, float) tuple for size"
+                             ", got (%128s, %128s)",
+                             Py_TYPE(x)->tp_name,
+                             Py_TYPE(y)->tp_name);
+            }
+            else {
+                PyErr_Format(PyExc_TypeError,
+                             "expected a float for size, got %128s",
+                             Py_TYPE(o)->tp_name);
+            }
+            return 0;
+        }
+    }
+
+    return numbers_to_scale(x, y, size);
+}
+
+static int
+numbers_to_scale(PyObject *x, PyObject *y, Scale_t *size)
+{
+    PyObject *o;
+    PyObject *min_obj = 0;
+    PyObject *max_obj = 0;
+    int do_y;
     int cmp_result;
     int rval = 0;
 
-    if (PyLong_Check(o)) {
-        ;
+    min_obj = PyFloat_FromDouble(0.0);
+    if (!min_obj) goto finish;
+    max_obj = PyFloat_FromDouble(FX6_TO_DBL(FX6_MAX));
+    if (!max_obj) goto finish;
+
+    for (o = x, do_y = 1; o; o = (do_y--) ? y : 0) {
+        cmp_result = PyObject_RichCompareBool(o, min_obj, Py_LT);
+        if (cmp_result == -1) goto finish;
+        if (cmp_result == 1) {
+            PyErr_Format(PyExc_OverflowError,
+                         "%128s value is negative"
+                         " while size value is zero or positive",
+                         Py_TYPE(o)->tp_name);
+            goto finish;
+        }
+        cmp_result = PyObject_RichCompareBool(o, max_obj, Py_GT);
+        if (cmp_result == -1) goto finish;
+        if (cmp_result == 1) {
+            PyErr_Format(PyExc_OverflowError,
+                         "%128s value too large to convert to a size value",
+                         Py_TYPE(o)->tp_name);
+            goto finish;
+        }
     }
-#if PY2
-    else if (PyInt_Check(o)) {
-        ;
-    }
-#endif
-    else {
-        PyErr_Format(PyExc_TypeError, "integer point size expected, got %s",
-                     Py_TYPE(o)->tp_name);
-        goto finish;
-    }
-    lower_limit = PyInt_FromLong(0);
-    if (!lower_limit) goto finish;
-    cmp_result = PyObject_RichCompareBool(o, lower_limit, Py_LT);
-    if (cmp_result == -1) goto finish;
-    if (cmp_result == 1) {
-        PyErr_SetString(PyExc_OverflowError,
-                        "point size is >= 0, got negative integer value");
-        goto finish;
-    }
-    upper_limit = PyInt_FromLong(FX6_TRUNC(~(Scale_t)0));
-    if (!upper_limit) goto finish;
-    cmp_result = PyObject_RichCompareBool(o, upper_limit, Py_GT);
-    if (cmp_result == -1) goto finish;
-    if (cmp_result == 1) {
-        PyErr_SetString(PyExc_OverflowError,
-                        "integer value too large"
-                        " to convert to a point size");
-        goto finish;
-    }
-    size = PyInt_AsLong(o);
-    if (size == -1) goto finish;
-    *(Scale_t *)p = (Scale_t)INT_TO_FX6(size);
-    rval = 1;
+
+    rval = build_scale(x, y, size);
 
   finish:
-    Py_XDECREF(lower_limit);
-    Py_XDECREF(upper_limit);
+    Py_XDECREF(min_obj);
+    Py_XDECREF(max_obj);
     return rval;
 }
 
+static int
+build_scale(PyObject *x, PyObject *y, Scale_t *size)
+{
+    FT_UInt sz_x = 0, sz_y = 0;
+
+    sz_x = number_to_FX6_unsigned(x);
+    if (PyErr_Occurred()) {
+        return 0;
+    }
+    if (y) {
+        sz_y = number_to_FX6_unsigned(y);
+        if (PyErr_Occurred()) {
+            return 0;
+        }
+    }
+    if (sz_x == 0 && sz_y != 0) {
+        PyErr_SetString(PyExc_ValueError,
+                        "expected zero size height when width is zero");
+        return 0;
+    }
+    size->x = sz_x;
+    size->y = sz_y;
+    return 1;
+}
+
+static FT_UInt
+number_to_FX6_unsigned(PyObject *n)
+{
+    PyObject *f_obj = PyNumber_Float(n);
+    double f;
+
+    if (!f_obj) return 0;
+    f = PyFloat_AsDouble(f_obj);
+    Py_XDECREF(f_obj);
+    if (PyErr_Occurred()) return 0;
+    return DBL_TO_FX6(f);
+}
+
 /** rotation: int -> Angle_t */
 int
 obj_to_rotation(PyObject *o, void *p)
     if (!angle_obj) goto finish;
     angle = PyLong_AsLong(angle_obj);
     if (angle == -1) goto finish;
-    *(Scale_t *)p = (Scale_t)INT_TO_FX16(angle);
+    *(Angle_t *)p = (Angle_t)INT_TO_FX16(angle);
     rval = 1;
 
   finish:
         obj->resolution = 0;
         obj->is_scalable = 0;
         obj->_internals = 0;
-        obj->face_size = 0;
+        obj->face_size = FACE_SIZE_NONE;
         obj->style = FT_STYLE_NORMAL;
         obj->render_flags = FT_RFLAG_DEFAULTS;
         obj->strength = PGFT_DBL_DEFAULT_STRENGTH;
 static PyObject *
 _ftfont_getsize(PgFontObject *self, void *closure)
 {
-    return PyInt_FromLong(FX6_TRUNC(self->face_size));
+    if (self->face_size.y == 0) {
+        return PyFloat_FromDouble(FX6_TO_DBL(self->face_size.x));
+    }
+    return Py_BuildValue("dd",
+                         FX6_TO_DBL(self->face_size.x),
+                         FX6_TO_DBL(self->face_size.y));
 }
 
 static int
     PGFT_String *text;
     PyObject *rectobj = 0;
     FT_Error error;
-    int face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;
     SDL_Rect r;
 
     FontRenderMode render;
     /* arguments */
     PyObject *textobj;
     PGFT_String *text;
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
 
     /* grab freetype */
     FreeTypeInstance *ft;
 static PyObject *
 _ftfont_getsizedascender(PgFontObject *self, PyObject *args)
 {
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;
     long value;
 
     FreeTypeInstance *ft;
         return 0;
     }
 
-    if (face_size == 0) {
-        if (self->face_size == 0) {
+    if (face_size.x == 0) {
+        if (self->face_size.x == 0) {
             RAISE(PyExc_ValueError,
                   "No font point size specified"
                   " and no default font size in typefont");
 static PyObject *
 _ftfont_getsizeddescender(PgFontObject *self, PyObject *args)
 {
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
     long value;
 
     FreeTypeInstance *ft;
         return 0;
     }
 
-    if (face_size == 0) {
-        if (self->face_size == 0) {
+    if (face_size.x == 0) {
+        if (self->face_size.x == 0) {
             RAISE(PyExc_ValueError,
                   "No font point size specified"
                   " and no default font size in typefont");
 static PyObject *
 _ftfont_getsizedheight(PgFontObject *self, PyObject *args)
 {
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
     long value;
 
     FreeTypeInstance *ft;
         return 0;
     }
 
-    if (face_size == 0) {
-        if (self->face_size == 0) {
+    if (face_size.x == 0) {
+        if (self->face_size.x == 0) {
             RAISE(PyExc_ValueError,
                   "No font point size specified"
                   " and no default font size in typeface");
 static PyObject *
 _ftfont_getsizedglyphheight(PgFontObject *self, PyObject *args)
 {
-    Scale_t face_size = -1;
+    Scale_t face_size = FACE_SIZE_NONE;;
     long value;
 
     FreeTypeInstance *ft;
         return 0;
     }
 
-    if (face_size == 0) {
-        if (self->face_size == 0) {
+    if (face_size.x == 0) {
+        if (self->face_size.x == 0) {
             RAISE(PyExc_ValueError,
                   "No font point size specified"
                   " and no default font size in typeface");
     PGFT_String *text;
     int style = FT_STYLE_DEFAULT;
     Angle_t rotation = self->rotation;
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
     int invert = 0;
 
     /* output arguments */
     int ypos = 0;
     int style = FT_STYLE_DEFAULT;
     Angle_t rotation = self->rotation;
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
     int invert = 0;
 
     /* output arguments */
     /* input arguments */
     PyObject *textobj = 0;
     PGFT_String *text;
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
     PyObject *fg_color_obj = 0;
     PyObject *bg_color_obj = 0;
     Angle_t rotation = self->rotation;
     PyObject *surface_obj = 0;
     PyObject *textobj = 0;
     PGFT_String *text;
-    Scale_t face_size = 0;
+    Scale_t face_size = FACE_SIZE_NONE;;
     PyObject *dest = 0;
     int xpos = 0;
     int ypos = 0;

src/doc/freetype_doc.h

 
 #define DOC_FONTPATH "path -> unicode\nFont file path"
 
-#define DOC_FONTSIZE "size -> int\nThe default point size used in rendering"
+#define DOC_FONTSIZE "size -> float\nsize -> (float, float)\nThe default point size used in rendering"
 
 #define DOC_FONTGETRECT "get_rect(text, style=STYLE_DEFAULT, rotation=0, size=0) -> rect\nReturn the size and offset of rendered text"
 
 Font file path
 
 pygame.freetype.Font.size
- size -> int
+ size -> float
+ size -> (float, float)
 The default point size used in rendering
 
 pygame.freetype.Font.get_rect
  * Global module types
  **********************************************************/
 
-typedef FT_UInt Scale_t;
+typedef struct _scale_s {
+    FT_UInt x, y;
+} Scale_t;
 typedef FT_Angle Angle_t;
 
 typedef struct {

src/freetype/ft_render.c

                       PgFontObject *fontobj, FontRenderMode *mode,
                       Scale_t face_size, int style, Angle_t rotation)
 {
-    if (face_size == 0) {
-        if (fontobj->face_size == 0) {
+    if (face_size.x == 0) {
+        if (fontobj->face_size.x == 0) {
             PyErr_SetString(PyExc_ValueError,
                   "No font point size specified"
                   " and no default font size in typeface");

src/freetype/ft_wrap.c

     FT_Error error;
     FTC_ScalerRec scale;
     FT_Size _fts;
+    FT_Face font;
+    FT_Int i;
+    FT_Pos size;
 
+    if (!fontobj->is_scalable && !face_size.y) {
+        font = _PGFT_GetFont(ft, fontobj);
+        if (!font) {
+            return 0;
+        }
+        size = FX6_ROUND(face_size.x);
+        for (i = 0; i < font->num_fixed_sizes; ++i) {
+            if (size == FX6_ROUND(font->available_sizes[i].size)) {
+                face_size.x = font->available_sizes[i].x_ppem;
+                face_size.y = font->available_sizes[i].y_ppem;
+                break;
+            }
+        }
+    }
     _PGFT_BuildScaler(fontobj, &scale, face_size);
 
     error = FTC_Manager_LookupSize(ft->cache_manager,
 _PGFT_BuildScaler(PgFontObject *fontobj, FTC_Scaler scale, Scale_t face_size)
 {
     scale->face_id = (FTC_FaceID)(&fontobj->id);
-    scale->width = scale->height = (FT_UInt)face_size;
+    scale->width = face_size.x;
+    scale->height = face_size.y ? face_size.y : face_size.x;
     scale->pixel = 0;
     scale->x_res = scale->y_res = fontobj->resolution;
 }

src/freetype/ft_wrap.h

 #define FX16_TO_DBL(x) ((x) * 1.52587890625e-5 /* 2.0^-16 */)
 #define DBL_TO_FX16(d) ((FT_Fixed)((d) * 65536.0))
 #define FX6_TO_DBL(x) ((x) * 1.5625e-2 /* 2.0^-6 */)
+#define DBL_TO_FX6(d) ((FT_Fixed)((d) * 64.0))
 
 /* Internal configuration variables */
 #define PGFT_DEFAULT_CACHE_SIZE 64

test/fixtures/fonts/PyGameMono-18-100dpi.bdf

+STARTFONT 2.1
+FONT -FontForge-PyGameMono-Medium-R-Normal--25-180-100-100-M-250-ISO10646-1
+SIZE 18 100 100
+FONTBOUNDINGBOX 21 22 0 0
+COMMENT "Generated by fontforge, http://fontforge.sourceforge.net"
+COMMENT "Created by Lenard Lindstrom,,, with FontForge 2.0 (http://fontforge.sf.net)"
+STARTPROPERTIES 29
+FOUNDRY "FontForge"
+FAMILY_NAME "PyGameMono"
+WEIGHT_NAME "Medium"
+SLANT "R"
+SETWIDTH_NAME "Normal"
+ADD_STYLE_NAME ""
+PIXEL_SIZE 25
+POINT_SIZE 180
+RESOLUTION_X 100
+RESOLUTION_Y 100
+SPACING "M"
+AVERAGE_WIDTH 250
+CHARSET_REGISTRY "ISO10646"
+CHARSET_ENCODING "1"
+FONTNAME_REGISTRY ""
+CHARSET_COLLECTIONS "ISO10646-1"
+FONT_NAME "PyGameMono"
+FACE_NAME "PyGame Mono"
+FONT_VERSION "001.000"
+FONT_ASCENT 20
+FONT_DESCENT 5
+UNDERLINE_POSITION -2
+UNDERLINE_THICKNESS 2
+RAW_ASCENT 800
+RAW_DESCENT 200
+RELATIVE_WEIGHT 50
+RELATIVE_SETWIDTH 50
+FIGURE_WIDTH -1
+AVG_UPPERCASE_WIDTH 250
+ENDPROPERTIES
+CHARS 5
+STARTCHAR .notdef
+ENCODING 0
+SWIDTH 1000 0
+DWIDTH 25 0
+BBX 20 20 0 0
+BITMAP
+FFFFF0
+FFFFF0
+FE07F0
+F801F0
+F000F0
+E00070
+E00070
+C00030
+C00030
+C00030
+C00030
+C00030
+C00030
+E00070
+E00070
+F000F0
+F801F0
+FE07F0
+FFFFF0
+FFFFF0
+ENDCHAR
+STARTCHAR A
+ENCODING 65
+SWIDTH 1000 0
+DWIDTH 25 0
+BBX 20 21 0 1
+BITMAP
+03FC00
+1FFF80
+3FFFC0
+7C03E0
+F000F0
+E00070
+E00070
+F000F0
+FC03F0
+FFFFF0
+FFFFF0
+FFFFF0
+FF0FF0
+7C03F0
+7801E0
+7800E0
+7000E0
+700060
+600060
+200040
+200040
+ENDCHAR
+STARTCHAR B
+ENCODING 66
+SWIDTH 1000 0
+DWIDTH 25 0
+BBX 18 20 1 0
+BITMAP
+FFFE00
+FFFF80
+7E0780
+7801C0
+7000C0
+3000C0
+3000C0
+3801C0
+3E0780
+3FFF00
+3FFF00
+3E0780
+380180
+3000C0
+3000C0
+3000C0
+7801C0
+7E07C0
+FFFF80
+FFFE00
+ENDCHAR
+STARTCHAR C
+ENCODING 67
+SWIDTH 1000 0
+DWIDTH 25 0
+BBX 20 20 0 0
+BITMAP
+00FC00
+03FF00
+0FFF80
+1F03E0
+3E0070
+7C0010
+780000
+F80000
+F00000
+F00000
+F00000
+F00000
+F80000
+780000
+7C0010
+3E0070
+1F01E0
+0FFFC0
+03FF80
+00FE00
+ENDCHAR
+STARTCHAR u13079
+ENCODING 77945
+SWIDTH 1000 0
+DWIDTH 25 0
+BBX 21 10 0 5
+BITMAP
+03FC00
+0FFF80
+1E73C0
+78F8F0
+F0F878
+70F870
+3870E0
+1E03C0
+0FFF80
+03FC00
+ENDCHAR
+ENDFONT

test/fixtures/fonts/PyGameMono-18-75dpi.bdf

+STARTFONT 2.1
+FONT -FontForge-PyGameMono-Medium-R-Normal--19-180-75-75-M-190-ISO10646-1
+SIZE 18 75 75
+FONTBOUNDINGBOX 15 17 0 0
+COMMENT "Generated by fontforge, http://fontforge.sourceforge.net"
+COMMENT "Created by Lenard Lindstrom,,, with FontForge 2.0 (http://fontforge.sf.net)"
+STARTPROPERTIES 29
+FOUNDRY "FontForge"
+FAMILY_NAME "PyGameMono"
+WEIGHT_NAME "Medium"
+SLANT "R"
+SETWIDTH_NAME "Normal"
+ADD_STYLE_NAME ""
+PIXEL_SIZE 19
+POINT_SIZE 180
+RESOLUTION_X 75
+RESOLUTION_Y 75
+SPACING "M"
+AVERAGE_WIDTH 190
+CHARSET_REGISTRY "ISO10646"
+CHARSET_ENCODING "1"
+FONTNAME_REGISTRY ""
+CHARSET_COLLECTIONS "ISO10646-1"
+FONT_NAME "PyGameMono"
+FACE_NAME "PyGame Mono"
+FONT_VERSION "001.000"
+FONT_ASCENT 15
+FONT_DESCENT 4
+UNDERLINE_POSITION -2
+UNDERLINE_THICKNESS 1
+RAW_ASCENT 800
+RAW_DESCENT 200
+RELATIVE_WEIGHT 50
+RELATIVE_SETWIDTH 50
+FIGURE_WIDTH -1
+AVG_UPPERCASE_WIDTH 190
+ENDPROPERTIES
+CHARS 5
+STARTCHAR .notdef
+ENCODING 0
+SWIDTH 1000 0
+DWIDTH 19 0
+BBX 15 15 0 0
+BITMAP
+FFFE
+FFFE
+FC7E
+F01E
+E00E
+C006
+C006
+C006
+C006
+C006
+E00E
+F01E
+FC7E
+FFFE
+FFFE
+ENDCHAR
+STARTCHAR A
+ENCODING 65
+SWIDTH 1000 0
+DWIDTH 19 0
+BBX 15 17 0 0
+BITMAP
+0FE0
+3FF8
+783C
+F01E
+E00E
+E00E
+F01E
+F83E
+FFFE
+FFFE
+FC7E
+701C
+701C
+600C
+600C
+4004
+4004
+ENDCHAR
+STARTCHAR B
+ENCODING 66
+SWIDTH 1000 0
+DWIDTH 19 0
+BBX 15 15 0 0
+BITMAP
+FFF8
+7FFC
+780E
+3006
+3006
+380E
+3FF8
+3FF8
+3FF8
+380E
+3006
+3006
+7C1E
+7FFC
+FFF8
+ENDCHAR
+STARTCHAR C
+ENCODING 67
+SWIDTH 1000 0
+DWIDTH 19 0
+BBX 15 15 0 0
+BITMAP
+03E0
+0FF8
+3C1C
+7806
+7000
+E000
+E000
+E000
+E000
+E000
+7000
+7806
+3C1C
+0FF8
+03E0
+ENDCHAR
+STARTCHAR u13079
+ENCODING 77945
+SWIDTH 1000 0
+DWIDTH 19 0
+BBX 15 7 0 4
+BITMAP
+0FE0
+3838
+638C
+E38E
+638C
+3838
+0FE0
+ENDCHAR
+ENDFONT

test/fixtures/fonts/PyGameMono.sfd

 "5j^b4</SMDueerhuM[8huM[8Dub+e4<+n,"5j.Z
 BDFChar: 4 77945 19 0 14 4 10
 &)]E,@uTH.@uNB,&)[Ef
-BDFChar: -1 68 19 0 0 0 0
-z
+EndBitmapFont
+BitmapFont: 25 5 20 5 1 
+BDFChar: 0 65 25 0 19 1 21
+"8i-@s+!?6^jH7Bn,Vt9!-$irE:<ksqucHgs7$'gnGiOXrt"UH"7UO9i-57[Dueg8!+917?m#G7
++94Y,
+BDFChar: 1 66 25 1 18 0 19
+s8E#us+#P'J9V+1Dud[-!5L8g^c2A14pQU&rr>:`!'UX<3!"O`!5L8g^b>c(GQGU/#JgB>JH,TK
+
+BDFChar: 2 0 25 0 19 0 19
+s8VTgs7$!mnG!%Rn,Vt9!-$irE52H-^]6((!&21g0YdZB^]6(H!-$irE:<ksp]9m`#OqcnnGiOX
+
+BDFChar: 3 67 25 0 19 0 19
+!;lg!rr<T0J0+mO4ofPL!"aAT!;HNon,NIX!!)Kg!:Tsgp](;=!!%BH&3^)5*s(:1s1ea:J,oQK
+
+BDFChar: 4 77945 25 0 20 5 14
+"8i-0s*u/3^j,YDnG%6Ipi&33i#W!9&-%.^qu?]s
 EndBitmapFont
 EndSplineFont

test/freetype_test.py

     """return an uninitialized font instance"""
     return ft.Font.__new__(ft.Font)
 
-max_point_size = 0
-for i in range(0, ctypes.sizeof(ctypes.c_uint)):
-    max_point_size <<= 8
-    max_point_size |= 0xff
-max_point_size >>= 6
+max_point_size_FX6 = 0x7FFFFFFF
+max_point_size = max_point_size_FX6 >> 6
+max_point_size_f = max_point_size_FX6 * 0.015625
 
 class FreeTypeFontTest(unittest.TestCase):
 
     _fixed_path = os.path.join(FONTDIR, 'test_fixed.otf')
     _sans_path = os.path.join(FONTDIR, 'test_sans.ttf')
     _mono_path = os.path.join(FONTDIR, 'PyGameMono.otf')
-    _bmp_path = os.path.join(FONTDIR, 'PyGameMono-8.bdf')
+    _bmp_8_75dpi_path = os.path.join(FONTDIR, 'PyGameMono-8.bdf')
+    _bmp_18_75dpi_path = os.path.join(FONTDIR, 'PyGameMono-18-75dpi.bdf')
+    _bmp_18_100dpi_path = os.path.join(FONTDIR, 'PyGameMono-18-100dpi.bdf')
     _TEST_FONTS = {}
 
     def setUp(self):
             # It also contains two bitmap sizes: 8.0 X 8.0 and 19.0 X 19.0.
             self._TEST_FONTS['mono'] = ft.Font(self._mono_path)
 
-        if 'bmp' not in self._TEST_FONTS:
+        if 'bmp-8-75dpi' not in self._TEST_FONTS:
             # A fixed size bitmap mono test font made for Pygame.
             # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
             # The size is 8.0 X 8.0.
-            self._TEST_FONTS['bmp'] = ft.Font(self._bmp_path)
+            self._TEST_FONTS['bmp-8-75dpi'] = ft.Font(self._bmp_8_75dpi_path)
+
+        if 'bmp-18-75dpi' not in self._TEST_FONTS:
+            # A fixed size bitmap mono test font made for Pygame.
+            # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
+            # The size is 8.0 X 8.0.
+            self._TEST_FONTS['bmp-18-75dpi'] = ft.Font(self._bmp_18_75dpi_path)
+
+        if 'bmp-18-100dpi' not in self._TEST_FONTS:
+            # A fixed size bitmap mono test font made for Pygame.
+            # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
+            # The size is 8.0 X 8.0.
+            self._TEST_FONTS['bmp-18-100dpi'] = (
+                ft.Font(self._bmp_18_100dpi_path))
 
     def test_freetype_defaultfont(self):
         font = ft.Font(None)
         
         f = self._TEST_FONTS['sans']
         self.assertEqual(f.fixed_sizes, 0)
-        f = self._TEST_FONTS['bmp']
+        f = self._TEST_FONTS['bmp-8-75dpi']
         self.assertEqual(f.fixed_sizes, 1)
         f = self._TEST_FONTS['mono']
         self.assertEqual(f.fixed_sizes, 2)
         szlist = f.get_sizes()
         self.assertTrue(isinstance(szlist, list))
         self.assertEqual(len(szlist), 0)
-        f = self._TEST_FONTS['bmp']
+        f = self._TEST_FONTS['bmp-8-75dpi']
         szlist = f.get_sizes()
         self.assertTrue(isinstance(szlist, list))
         self.assertEqual(len(szlist), 1)
 
     def test_freetype_Font_bitmap_files(self):
         """Ensure bitmap file restrictions are caught"""
-        f = self._TEST_FONTS['bmp']
+        f = self._TEST_FONTS['bmp-8-75dpi']
         f_null = nullfont()
         s = pygame.Surface((10, 10), 0, 32)
         a = s.get_view('3')
         self.assertRaises(RuntimeError,
                           nullfont().get_rect, 'a', size=24)
 
+        # text stretching
+        rect12 = font.get_rect('A', size=12.0)
+        rect24 = font.get_rect('A', size=24.0)
+        rect_x = font.get_rect('A', size=(24.0, 12.0))
+        self.assertEqual(rect_x.width, rect24.width)
+        self.assertEqual(rect_x.height, rect12.height)
+        rect_y = font.get_rect('A', size=(12.0, 24.0))
+        self.assertEqual(rect_y.width, rect12.width)
+        self.assertEqual(rect_y.height, rect24.height)
+
     def test_freetype_Font_height(self):
 
         f = self._TEST_FONTS['sans']
         self.assertEqual(f.size, 0)
         f.size = max_point_size
         self.assertEqual(f.size, max_point_size)
+        f.size = 6.5
+        self.assertEqual(f.size, 6.5)
+        f.size = max_point_size_f
+        self.assertEqual(f.size, max_point_size_f)
         self.assertRaises(OverflowError, setattr, f, 'size', -1)
         self.assertRaises(OverflowError, setattr, f, 'size',
                           (max_point_size + 1))
-        self.assertRaises(TypeError, setattr, f, 'size', 0.0)
+
+        f.size = 24.0, 0
+        size = f.size
+        self.assertTrue(isinstance(size, float))
+        self.assertEqual(size, 24.0)
+        f.size = 16, 16
+        size = f.size
+        self.assertTrue(isinstance(size, tuple))
+        self.assertEqual(len(size), 2)
+        x, y = size
+        self.assertTrue(isinstance(x, float))
+        self.assertEqual(x, 16.0)
+        self.assertTrue(isinstance(y, float))
+        self.assertEqual(y, 16.0)
+        f.size = 20.5, 22.25
+        x, y = f.size
+        self.assertEqual(x, 20.5)
+        self.assertEqual(y, 22.25)
+        f.size = 0, 0
+        size = f.size
+        self.assertTrue(isinstance(size, float))
+        self.assertEqual(size, 0.0)
+        self.assertRaises(ValueError, setattr, f, 'size', (0, 24.0))
+        self.assertRaises(TypeError, setattr, f, 'size', (24.0,))
+        self.assertRaises(TypeError, setattr, f, 'size', (24.0, 0, 0))
+        self.assertRaises(TypeError, setattr, f, 'size', (24.0j, 24.0))
+        self.assertRaises(TypeError, setattr, f, 'size', (24.0, 24.0j))
+        self.assertRaises(OverflowError, setattr, f, 'size', (-1, 16))
+        self.assertRaises(OverflowError, setattr, f, 'size',
+                          (max_point_size + 1, 16))
+        self.assertRaises(OverflowError, setattr, f, 'size', (16, -1))
+        self.assertRaises(OverflowError, setattr, f, 'size',
+                          (16, max_point_size + 1))
+
+        # bitmap files with identical point size but differing ppems.
+        f75 = self._TEST_FONTS['bmp-18-75dpi']
+        sizes = f75.get_sizes()
+        self.assertEqual(len(sizes), 1)
+        size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0]
+        self.assertEqual(size_pt, 18)
+        self.assertEqual(x_ppem, 19.0)
+        self.assertEqual(y_ppem, 19.0)
+        rect = f75.get_rect('A', size=18)
+        rect = f75.get_rect('A', size=19)
+        rect = f75.get_rect('A', size=(19.0, 19.0))
+        self.assertRaises(pygame.error, f75.get_rect, 'A', size=17)
+        f100 = self._TEST_FONTS['bmp-18-100dpi']
+        sizes = f100.get_sizes()
+        self.assertEqual(len(sizes), 1)
+        size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0]
+        self.assertEqual(size_pt, 18)
+        self.assertEqual(x_ppem, 25.0)
+        self.assertEqual(y_ppem, 25.0)
+        rect = f100.get_rect('A', size=18)
+        rect = f100.get_rect('A', size=25)
+        rect = f100.get_rect('A', size=(25.0, 25.0))
+        self.assertRaises(pygame.error, f100.get_rect, 'A', size=17)
 
     def test_freetype_Font_rotation(self):