Skip to content

Commit

Permalink
Merge pull request #164 from Hansimov/master
Browse files Browse the repository at this point in the history
+ Implement text stroke feature and speed up large image rendering
  • Loading branch information
arihantparsoya authored May 15, 2020
2 parents 895599a + aa16f20 commit 63dac5b
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 153 deletions.
2 changes: 1 addition & 1 deletion docs/tutorials/typography.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The proliferation of personal computers in the mid-1980s spawned a period of rap
Draw text
=========

The ``text()`` function is used to draw letters, words, and paragraphs to the screen. In the simplest use, the first parameter can be a String, char, int, or float. The second and third parameters set the position of the text. By default, the second parameter defines the distance from the left edge of the window; the third parameter defines the distance from the text’s baseline to the top of the window. The ``text_size()`` function defines the size the letters will draw in units of pixels. The number used to define the text size will not be the precise height of each letter, the difference depends on the design of each font. For instance, the statement ``text_size(30)`` won’t necessarily draw a capital H at 30 pixels high. The ``fill()`` function controls the color and transparency of text. This function affects text the same way it affects shapes such as ``rect()`` and ``ellipse()``, but text is not affected by ``stroke()``.
The ``text()`` function is used to draw letters, words, and paragraphs to the screen. In the simplest use, the first parameter can be a String, char, int, or float. The second and third parameters set the position of the text. By default, the second parameter defines the distance from the left edge of the window; the third parameter defines the distance from the text’s baseline to the top of the window. The ``text_size()`` function defines the size the letters will draw in units of pixels. The number used to define the text size will not be the precise height of each letter, the difference depends on the design of each font. For instance, the statement ``text_size(30)`` won’t necessarily draw a capital H at 30 pixels high. The ``fill()`` function controls the color and transparency of text. This function affects text the same way it affects shapes such as ``rect()`` and ``ellipse()``, but text is not affected by ``stroke()`` (this feature will be implemented in later versions).

.. image:: ./typography-res/12_01.png
:align: left
Expand Down
327 changes: 182 additions & 145 deletions p5/core/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from PIL import ImageFilter
from PIL import ImageChops

from .image import image
from .image import PImage
Expand All @@ -28,201 +30,236 @@
from . import p5

__all__ = ['create_font', 'load_font', 'text', 'text_font',
'text_align', 'text_leading', 'text_size', 'text_width',
'text_ascent', 'text_descent'
]
'text_align', 'text_leading', 'text_size', 'text_width',
'text_ascent', 'text_descent'
]

_font_family = ImageFont.load_default()
_text_align_x = "LEFT"
_text_align_y = "TOP"
_text_leading = 0

def create_font(name, size=10):
"""Create the given font at the appropriate size.
:param name: Filename of the font file (only pil, otf and ttf
fonts are supported.)
:type name: str
"""Create the given font at the appropriate size.
:param name: Filename of the font file (only pil, otf and ttf
fonts are supported.)
:type name: str
:param size: Font size (only required when `name` refers to a
truetype font; defaults to None)
:type size: int | None
:param size: Font size (only required when `name` refers to a
truetype font; defaults to None)
:type size: int | None
"""
"""

if name.endswith('ttf') or name.endswith('otf'):
font = ImageFont.truetype(name, size)
elif name.endswith('pil'):
font = ImageFont.load(name)
else:
raise NotImplementedError("Font type not supported.")
if name.endswith('ttf') or name.endswith('otf'):
font = ImageFont.truetype(name, size)
elif name.endswith('pil'):
font = ImageFont.load(name)
else:
raise NotImplementedError("Font type not supported.")

return font
return font

def load_font(font_name):
"""Loads the given font into a font object
"""Loads the given font into a font object
"""
return create_font(font_name)
"""
return create_font(font_name)

def text(text_string, position, wrap_at=None):
"""Draw the given text on the screen and save the image.
:param text_string: text to display
:type text_string: str
:param position: position of the text on the screen
:type position: tuple
:param wrap_at: specifies the text wrapping column (defaults to
None)
:type wrap_at: int
:returns: actual text that was drawn to the image (when wrapping
is not set, this is just the unmodified text_string)
:rtype: str
"""

if len(text_string) == 0:
return

global _font_family, _text_leading

multiline = False
if not (wrap_at is None):
text_string = textwrap.fill(text_string, wrap_at)
size = _font_family.getsize_multiline(text_string)
multiline = True
elif "\n" in text_string:
multiline = True
size = list(_font_family.getsize_multiline(text_string))
size[1] += _text_leading*text_string.count("\n")
else:
size = _font_family.getsize(text_string)

canvas = Image.new("RGBA", size, color=(0, 0, 0, 0))
canvas_draw = ImageDraw.Draw(canvas)

if multiline:
canvas_draw.multiline_text((0, 0), text_string, font=_font_family, spacing=_text_leading)
else:
canvas_draw.text((0, 0), text_string, font=_font_family)

text_image = PImage(*size)
text_image._img = canvas

width, height = size
position = list(position)
if _text_align_x == "LEFT":
position[0] += 0
elif _text_align_x == "RIGHT":
position[0] -= width
elif _text_align_x == "CENTER":
position[0] -= width/2

if _text_align_y == "TOP":
position[1] += 0
elif _text_align_y == "BOTTOM":
position[1] -= height
elif _text_align_y == "CENTER":
position[1] -= height/2

with push_style():
if p5.renderer.fill_enabled:
p5.renderer.tint_enabled = True
p5.renderer.tint_color = p5.renderer.fill_color
image(text_image, position)

return text_string
"""Draw the given text on the screen and save the image.
:param text_string: text to display
:type text_string: str
:param position: position of the text on the screen
:type position: tuple
:param wrap_at: specifies the text wrapping column (defaults to
None)
:type wrap_at: int
:returns: actual text that was drawn to the image (when wrapping
is not set, this is just the unmodified text_string)
:rtype: str
"""

if len(text_string) == 0:
return

global _font_family, _text_leading

multiline = False
if not (wrap_at is None):
text_string = textwrap.fill(text_string, wrap_at)
size = _font_family.getsize_multiline(text_string)
multiline = True
elif "\n" in text_string:
multiline = True
size = list(_font_family.getsize_multiline(text_string))
size[1] += _text_leading*text_string.count("\n")
else:
size = _font_family.getsize(text_string)


is_stroke_valid = False # True when stroke_weight != 0
is_min_filter = False # True when stroke_weight <0
if p5.renderer.stroke_enabled:
stroke_weight = p5.renderer.stroke_weight
if stroke_weight < 0:
stroke_weight = abs(stroke_weight)
is_min_filter = True

if stroke_weight > 0:
if stroke_weight % 2 == 0:
stroke_weight += 1
is_stroke_valid = True

if is_stroke_valid:
new_size = list(map(lambda x:x+2*stroke_weight, size))
is_stroke_valid = True
text_xy = (stroke_weight, stroke_weight)
else:
new_size = size
text_xy = (0,0)

canvas = Image.new("RGBA", new_size, color=(0, 0, 0, 0))
canvas_draw = ImageDraw.Draw(canvas)

if multiline:
canvas_draw.multiline_text(text_xy, text_string, font=_font_family, spacing=_text_leading)
else:
canvas_draw.text(text_xy, text_string, font=_font_family)

text_image = PImage(*new_size)
text_image._img = canvas

if is_stroke_valid:
if is_min_filter:
canvas_dilate = canvas.filter(ImageFilter.MinFilter(stroke_weight))
else:
canvas_dilate = canvas.filter(ImageFilter.MaxFilter(stroke_weight))
canvas_stroke = ImageChops.difference(canvas,canvas_dilate)
text_stroke_image = PImage(*new_size)
text_stroke_image._img = canvas_stroke

width, height = new_size
position = list(position)
if _text_align_x == "LEFT":
position[0] += 0
elif _text_align_x == "RIGHT":
position[0] -= width
elif _text_align_x == "CENTER":
position[0] -= width/2

if _text_align_y == "TOP":
position[1] += 0
elif _text_align_y == "BOTTOM":
position[1] -= height
elif _text_align_y == "CENTER":
position[1] -= height/2

with push_style():
if p5.renderer.fill_enabled:
p5.renderer.tint_enabled = True
p5.renderer.tint_color = p5.renderer.fill_color
image(text_image, position)
if p5.renderer.stroke_enabled and is_stroke_valid:
p5.renderer.tint_enabled = True
p5.renderer.tint_color = p5.renderer.stroke_color
image(text_stroke_image, position)

return text_string

def text_font(font, size=10):
"""Set current text font.
"""Set current text font.
:param font:
:type font: PIL.ImageFont.ImageFont
:param font:
:type font: PIL.ImageFont.ImageFont
"""
global _font_family
_font_family = font
"""
global _font_family
_font_family = font

def text_align(align_x, align_y=None):
"""Set the alignment of drawing text
"""Set the alignment of drawing text
:param align_x: "RIGHT", "CENTER" or "LEFT".
:type align_x: string
:param align_x: "RIGHT", "CENTER" or "LEFT".
:type align_x: string
:param align_y: "TOP", "CENTER" or "BOTTOM".
:type align_y: string
:param align_y: "TOP", "CENTER" or "BOTTOM".
:type align_y: string
"""
"""

global _text_align_x, _text_align_y
_text_align_x = align_x
global _text_align_x, _text_align_y
_text_align_x = align_x

if align_y:
_text_align_y = align_y
if align_y:
_text_align_y = align_y

def text_leading(leading):
"""Sets the spacing between lines of text in units of pixels
"""Sets the spacing between lines of text in units of pixels
:param leading: the size in pixels for spacing between lines
:type align_x: int
:param leading: the size in pixels for spacing between lines
:type align_x: int
"""
"""

global _text_leading
_text_leading = leading
global _text_leading
_text_leading = leading

def text_size(size):
"""Sets the current font size
"""Sets the current font size
:param leading: the size of the letters in units of pixels
:type align_x: int
:param leading: the size of the letters in units of pixels
:type align_x: int
"""
"""

global _font_family
global _font_family

# reload the font with new size
if hasattr(_font_family, 'path'):
if _font_family.path.endswith('ttf') or _font_family.path.endswith('otf'):
_font_family = ImageFont.truetype(_font_family.path, size)
else:
raise ValueError("text_size is nor supported for Bitmap Fonts")
# reload the font with new size
if hasattr(_font_family, 'path'):
if _font_family.path.endswith('ttf') or _font_family.path.endswith('otf'):
_font_family = ImageFont.truetype(_font_family.path, size)
else:
raise ValueError("text_size is nor supported for Bitmap Fonts")

def text_width(text):
"""Calculates and returns the width of any character or text string
"""Calculates and returns the width of any character or text string
:param text_string: text
:type text_string: str
:param text_string: text
:type text_string: str
:returns: width of any character or text string
:rtype: int
:returns: width of any character or text string
:rtype: int
"""
"""

return _font_family.getsize(text)[0]
return _font_family.getsize(text)[0]

def text_ascent():
"""Returns ascent of the current font at its current size
"""Returns ascent of the current font at its current size
:returns: ascent of the current font at its current size
:rtype: float
:returns: ascent of the current font at its current size
:rtype: float
"""
global _font_family
ascent, descent = _font_family.getmetrics()
return ascent
"""
global _font_family
ascent, descent = _font_family.getmetrics()
return ascent

def text_descent():
"""Returns descent of the current font at its current size
"""Returns descent of the current font at its current size
:returns: descent of the current font at its current size
:rtype: float
:returns: descent of the current font at its current size
:rtype: float
"""
"""

global _font_family
ascent, descent = _font_family.getmetrics()
return descent
global _font_family
ascent, descent = _font_family.getmetrics()
return descent
Loading

0 comments on commit 63dac5b

Please sign in to comment.