Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Tests/test_font_pcf_charsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ def test_textsize(
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
for i in range(255):
ox, oy, dx, dy = font.getbbox(bytearray([i]))
ox, oy, dx, dy = font.getbbox(bytes([i]))
assert ox == 0
assert oy == 0
assert dy == 20
assert dx in (0, 10)
assert font.getlength(bytearray([i])) == dx
assert font.getlength(bytes([i])) == dx
message = charsets[encoding]["message"].encode(encoding)
for i in range(len(message)):
msg = message[: i + 1]
Expand Down
18 changes: 18 additions & 0 deletions Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,24 @@ def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
assert (0, 0, 0, 0) == font.getbbox("")


def test_bytearray_not_supported(font: ImageFont.FreeTypeFont) -> None:
# The C text layout only accepts str and bytes, so bytearray is rejected
# even though these methods used to be annotated as taking one.
for op in (font.getlength, font.getbbox, font.getmask):
assert op(b"A") is not None
with pytest.raises(TypeError, match="expected string or bytes"):
op(bytearray(b"A")) # type: ignore[arg-type]


def test_core_font_arguments_are_optional(font: ImageFont.FreeTypeFont) -> None:
# Every argument after the leading string is optional at the C level,
# and the mode argument additionally accepts None.
assert font.font.getlength("A") == font.font.getlength("A", "", None, None, None)
assert font.font.getlength("A", None) == font.font.getlength("A", "")
assert font.font.getsize("A") == font.font.getsize("A", "", None, None, None, None)
assert font.font.getsize("A", None) == font.font.getsize("A", "")


def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
# issue 2666
im = Image.new(mode="RGB", size=(300, 100))
Expand Down
18 changes: 7 additions & 11 deletions src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Layout(IntEnum):
core = DeferredError.new(ex)


def _string_length_check(text: str | bytes | bytearray) -> None:
def _string_length_check(text: str | bytes) -> None:
if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
msg = "too many characters in string"
raise ValueError(msg)
Expand All @@ -99,7 +99,7 @@ class BaseImageFont(abc.ABC):

@abc.abstractmethod
def getbbox(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
self, text: str | bytes, *args: Any, **kwargs: Any
) -> tuple[float, float, float, float]:
pass

Expand Down Expand Up @@ -204,7 +204,7 @@ def getmask(
return self.font.getmask(text, mode)

def getbbox(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
self, text: str | bytes, *args: Any, **kwargs: Any
) -> tuple[int, int, int, int]:
"""
Returns bounding box (in pixels) of given text.
Expand All @@ -219,9 +219,7 @@ def getbbox(
width, height = self.font.getsize(text)
return 0, 0, width, height

def getlength(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> int:
def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> int:
"""
Returns length (in pixels) of given text.
This is the amount by which following text should be offset.
Expand Down Expand Up @@ -297,9 +295,7 @@ def load_from_bytes(f: IO[bytes]) -> None:
with open(font, "rb") as f:
load_from_bytes(f)
return
self.font = core.getfont(
font, size, index, encoding, layout_engine=layout_engine
)
self.font = core.getfont(font, size, index, encoding, b"", layout_engine)
else:
load_from_bytes(cast("IO[bytes]", font))

Expand Down Expand Up @@ -406,7 +402,7 @@ def getlength(

def getbbox(
self,
text: str | bytes | bytearray,
text: str | bytes,
mode: str = "",
direction: str | None = None,
features: list[str] | None = None,
Expand Down Expand Up @@ -774,7 +770,7 @@ def getmask(
return im

def getbbox(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
self, text: str | bytes, *args: Any, **kwargs: Any
) -> tuple[int, int, float, float]:
# TransposedFont doesn't support getmask2, move top-left point to (0, 0)
# this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
Expand Down
40 changes: 20 additions & 20 deletions src/PIL/_imagingft.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,36 @@ class Font:
self,
string: str | bytes,
fill: Callable[[int, int], _imaging.ImagingCore],
mode: str,
dir: str | None,
features: list[str] | None,
lang: str | None,
stroke_width: float,
stroke_filled: bool,
anchor: str | None,
foreground_ink_long: int,
start: tuple[float, float],
mode: str | None = ...,
dir: str | None = ...,
features: list[str] | None = ...,
lang: str | None = ...,
stroke_width: float = ...,
stroke_filled: bool = ...,
anchor: str | None = ...,
foreground_ink_long: int = ...,
start: tuple[float, float] = ...,
/,
) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ...
def getsize(
self,
string: str | bytes | bytearray,
mode: str,
dir: str | None,
features: list[str] | None,
lang: str | None,
anchor: str | None,
string: str | bytes,
mode: str | None = ...,
dir: str | None = ...,
features: list[str] | None = ...,
lang: str | None = ...,
anchor: str | None = ...,
/,
) -> tuple[tuple[int, int], tuple[int, int]]: ...
def getlength(
self,
string: str | bytes,
mode: str,
dir: str | None,
features: list[str] | None,
lang: str | None,
mode: str | None = ...,
dir: str | None = ...,
features: list[str] | None = ...,
lang: str | None = ...,
/,
) -> float: ...
) -> int: ...
def getvarnames(self) -> list[bytes]: ...
def getvaraxes(self) -> list[ImageFont.Axis]: ...
def setvarname(self, instance_index: int, /) -> None: ...
Expand Down
103 changes: 82 additions & 21 deletions src/_imagingft.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,16 @@ static int have_raqm = 0;
#define LAYOUT_FALLBACK 0
#define LAYOUT_RAQM 1

// These mirror raqm_glyph_t, which in turn mirrors harfbuzz's hb_glyph_info_t,
// with the addition of y_advance and y_offset for vertical text layout.
// Coordinate units are 26.6 fixed-point precision.
typedef struct {
int index, x_offset, x_advance, y_offset, y_advance;
unsigned int cluster;
unsigned int index; // the index of the glyph in the font file
FT_F26Dot6 x_offset; // horizontal movement of the glyph from current point
FT_F26Dot6 x_advance; // glyph advance width in horizontal text
FT_F26Dot6 y_offset; // vertical movement of the glyph from current point
FT_F26Dot6 y_advance; // glyph advance height in vertical text
unsigned int cluster; // character index in original text
} GlyphInfo;

struct {
Expand Down Expand Up @@ -96,6 +103,14 @@ static PyTypeObject Font_Type;

/* round a 26.6 pixel coordinate to the nearest integer */
#define PIXEL(x) ((((x) + 32) & -64) >> 6)
/* round a 26.6 pixel coordinate down to integer */
#define PIXEL_FLOOR(x) ((x) >> 6)
/* round a 26.6 pixel coordinate up to integer */
#define PIXEL_CEIL(x) (((x) + 63) >> 6)
/* the sub-pixel part of a 26.6 pixel coordinate */
#define PIXEL_FRAC(x) ((x) & 63)
/* convert from pixels to 26.6 fixed-point */
#define PIXEL_TO_FIXED(x) ((x) * 64)

static PyObject *
geterror(int code) {
Expand Down Expand Up @@ -125,8 +140,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
FT_Long width;
Py_ssize_t index = 0;
Py_ssize_t layout_engine = 0;
unsigned char *encoding;
unsigned char *font_bytes;
unsigned char *encoding = NULL;
unsigned char *font_bytes = NULL;
Py_ssize_t font_bytes_size = 0;
static char *kwlist[] = {
"filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL
Expand All @@ -143,7 +158,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
if (!PyArg_ParseTupleAndKeywords(
args,
kw,
"etf|nsy#n",
"etfnsy#n",
kwlist,
config.filesystem_encoding,
&filename,
Expand All @@ -162,7 +177,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
if (!PyArg_ParseTupleAndKeywords(
args,
kw,
"etf|nsy#n",
"etfnsy#n",
kwlist,
Py_FileSystemDefaultEncoding,
&filename,
Expand Down Expand Up @@ -215,7 +230,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
}

if (!error) {
width = size * 64;
width = PIXEL_TO_FIXED(size);
req.type = FT_SIZE_REQUEST_TYPE_NOMINAL;
req.width = width;
req.height = width;
Expand Down Expand Up @@ -517,9 +532,27 @@ text_layout(
return count;
}

/** Calculate how far the pen advances over a given string.
*
* This is the sum of the glyph advances, which is not the width of the inked
* area that font_getsize_impl() reports: it includes side bearings and any
* trailing whitespace.
*
* Python parameters:
* - string: the text to measure
* - mode_name: imaging mode string
* - dir: text direction string
* - features: font features sequence
* - lang: language string
*
* Python return value:
* - length: advance along the primary axis, 26.6 precision integer.
* Unlike the other methods here this is not in pixels;
* ImageFont.FreeTypeFont.getlength() divides by 64.
*/
static PyObject *
font_getlength_impl(FontObject *self, PyObject *args) {
int length; /* length along primary axis, in 26.6 precision */
FT_F26Dot6 length; /* length along primary axis */
GlyphInfo *glyph_info = NULL; /* computed text layout */
size_t i, count; /* glyph_info index and length */
int horizontal_dir; /* is primary axis horizontal? */
Expand All @@ -531,8 +564,6 @@ font_getlength_impl(FontObject *self, PyObject *args) {
PyObject *features = Py_None;
PyObject *string;

/* calculate size and bearing for a given string */

if (!PyArg_ParseTuple(
args, "O|zzOz:getlength", &string, &mode_name, &dir, &features, &lang
)) {
Expand Down Expand Up @@ -589,9 +620,9 @@ bounding_box_and_anchors(
int *x_offset,
int *y_offset
) {
long position; /* pen position along primary axis, in 26.6 precision */
long advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, in pixels */
FT_F26Dot6 position; /* pen position along primary axis */
long advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, in pixels */
int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */
int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */
int error;
Expand Down Expand Up @@ -754,6 +785,19 @@ bounding_box_and_anchors(
return 1;
}

/** Calculate size and bearing for a given string.
*
* Python parameters:
* - string: the text to measure
* - mode_name: imaging mode string
* - dir: text direction string
* - features: font features sequence
* - lang: language string
* - anchor: anchor string
*
* Python return value:
* - ((width, height), (x_offset, y_offset)): size and bearing of the text, in pixels
*/
static PyObject *
font_getsize_impl(FontObject *self, PyObject *args) {
int64_t width, height;
Expand All @@ -772,8 +816,6 @@ font_getsize_impl(FontObject *self, PyObject *args) {
PyObject *features = Py_None;
PyObject *string;

/* calculate size and bearing for a given string */

if (!PyArg_ParseTuple(
args,
"O|zzOzz:getsize",
Expand Down Expand Up @@ -838,11 +880,30 @@ font_getsize(FontObject *self, PyObject *args) {
return result;
}

/**
* Rasterize a string into an image buffer.
*
* Python parameters:
* - string: the text to render
* - fill: Python function to create a core image object at a specified width and height
* - mode_name: imaging mode string
* - dir: text direction string
* - features: font features sequence
* - lang: language string
* - stroke_width: stroke width in pixels
* - stroke_filled: bool: omit inner stroke border, to stroke outside + fill inside
* - anchor: anchor string
* - foreground_ink_long: foreground color as a long integer
* - x_start: starting x position of the pen in pixels
* - y_start: starting y position of the pen in pixels
*
* Returns: a new image object containing the rendered text.
*/
static PyObject *
font_render_impl(FontObject *self, PyObject *args) {
int x, y; /* pen position, in 26.6 precision */
FT_F26Dot6 x, y; /* pen position */
int px, py; /* position of current glyph, in pixels */
int x_min, y_max; /* text offset in 26.6 precision */
int x_min, y_max; /* text offset, in pixels */
int load_flags; /* FreeType load_flags parameter */
int error;
FT_Glyph glyph;
Expand Down Expand Up @@ -990,10 +1051,10 @@ font_render_impl(FontObject *self, PyObject *args) {

FT_Stroker_Set(
stroker,
(FT_Fixed)round(stroke_width * 64),
(FT_F26Dot6)roundf(PIXEL_TO_FIXED(stroke_width)),
FT_STROKER_LINECAP_ROUND,
FT_STROKER_LINEJOIN_ROUND,
0
0 // 16.16 units
);
}

Expand Down Expand Up @@ -1028,8 +1089,8 @@ font_render_impl(FontObject *self, PyObject *args) {
}

/* set pen position to text origin */
x = round((-x_min + stroke_width + x_start) * 64);
y = round((-y_max + (-stroke_width) - y_start) * 64);
x = roundf(PIXEL_TO_FIXED(-x_min + stroke_width + x_start));
y = roundf(PIXEL_TO_FIXED(-y_max + (-stroke_width) - y_start));

if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER;
Expand Down
Loading