Skip to content

Commit 8210834

Browse files
authored
Merge pull request #22 from jimbarrett27/font-path-and-color
Allow setting font path and font color by frequency
2 parents a749bb7 + 0149bbb commit 8210834

File tree

10 files changed

+68
-21
lines changed

10 files changed

+68
-21
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python-version: [ '3.9', '3.10', '3.11', '3.12' ]
10+
python-version: [ '3.10', '3.11', '3.12' ]
1111
name: 'Ubuntu, python ${{ matrix.python-version }}'
1212
steps:
1313
- name: Check out repository

.pylintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ max-spelling-suggestions=2
418418
[DESIGN]
419419

420420
# Maximum number of arguments for function / method
421-
max-args=10
421+
max-args=15
422422

423423
# Maximum number of locals for function / method body
424424
max-locals=25

examples/sherlock.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,17 @@
77
all_words = [word.strip(" \n,.!?:-&\"'[]") for word in contents.split(" ")]
88
all_words = [word for word in all_words if word]
99

10-
make_word_cloud(
10+
11+
def color_func(frequency):
12+
x = int((1 - frequency) * 200)
13+
return (x, x, x)
14+
15+
16+
img = make_word_cloud(
1117
all_words=all_words,
12-
font_color=(0, 0, 0),
18+
font_color_func=color_func,
1319
background_color=(255, 255, 255),
1420
minimum_font_size=5,
15-
).show()
21+
)
22+
23+
img.show()

tests/test_font.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55

66
class TestFont(TestCase):
77
def test_get_default_font_path(self):
8-
font = FontWrapper()
8+
font = FontWrapper(
9+
color_func=lambda _: (0, 0, 0), path=FontWrapper.default_font()
10+
)
911
self.assertTrue(font.path.exists())

tests/test_rectangle.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def text_image_and_rectangle_strategy(draw):
6565
text = draw(st.text(min_size=1, max_size=10))
6666

6767
font_size = draw(st.integers(min_value=5, max_value=100))
68-
font = FontWrapper(size=font_size)
68+
font = FontWrapper(
69+
size=font_size, color_func=lambda _: (0, 0, 0), path=FontWrapper.default_font()
70+
)
6971
text_bbox = font.getbbox(text)
7072

7173
img_rectangle = Rectangle(

wrdcld/__init__.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1+
from __future__ import annotations
2+
13
import math
24
from collections import Counter
35
from collections.abc import Callable
6+
from pathlib import Path
47

58
from .font import FontWrapper
69
from .image import ImageWrapper
710
from .main import fill_next_word
811
from .rectangle import Rectangle
12+
from .util import Color
913

1014

1115
# pylint: disable=unused-argument
1216
def make_word_cloud(
1317
all_words: list[str],
1418
width: int = 500,
1519
height: int = 500,
16-
font_color: tuple[int, int, int] = (255, 255, 0),
17-
background_color: tuple[int, int, int] = (73, 109, 137),
20+
font_path: Path | None = None,
21+
font_color: Color = (255, 255, 0),
22+
font_color_func: Callable[[float], Color] | None = None,
23+
background_color: Color = (73, 109, 137),
1824
minimum_font_size: int = 1,
1925
maximum_font_size: int = 100,
2026
word_padding: int = 0, # TODO
@@ -28,24 +34,32 @@ def make_word_cloud(
2834
assert (
2935
0 < minimum_font_size < maximum_font_size
3036
), "Invalid font sizes, must be positive (in pixels)"
37+
assert (
38+
font_color is not None or font_color_func is not None
39+
), "Must specify a fixed font color or function"
3140

3241
# Create a new image and font
42+
font_path = font_path or FontWrapper.default_font()
43+
font_color_func = font_color_func or (lambda _: font_color)
3344
image = ImageWrapper(width, height, background_color)
34-
font = FontWrapper(color=font_color, size=maximum_font_size)
45+
font = FontWrapper(
46+
path=font_path, color_func=font_color_func, size=maximum_font_size
47+
)
3548

3649
# Handle data
3750
word_counts = Counter(all_words)
3851
_, first_count = word_counts.most_common(1)[0]
3952

4053
available_rectangles = [Rectangle(width=width, height=height, x=0, y=0)]
4154
for word, count in word_counts.most_common():
42-
required_font_size = maximum_font_size * scaling_func(count / first_count)
55+
frequency = count / first_count
56+
required_font_size = maximum_font_size * scaling_func(frequency)
4357

4458
if required_font_size < minimum_font_size:
4559
break
4660

4761
available_rectangles = fill_next_word(
48-
word, available_rectangles, image, font[required_font_size]
62+
word, available_rectangles, image, font[required_font_size], frequency
4963
)
5064

5165
return image.img

wrdcld/font.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Callable
12
from dataclasses import dataclass, replace
23
from functools import lru_cache
34
from pathlib import Path
@@ -6,14 +7,17 @@
67

78
from .image import ImageWrapper
89
from .rectangle import Rectangle
9-
from .util import get_repo_root
10+
from .util import Color, get_repo_root
1011

1112

1213
@dataclass(frozen=True)
1314
class FontWrapper:
14-
path: Path = get_repo_root() / "fonts" / "OpenSans-Regular.ttf"
15+
color_func: Callable[[float], Color]
16+
path: Path
1517
size: int = 1
16-
color: tuple[int, int, int] = (255, 255, 0)
18+
19+
def color(self, frequency: float) -> Color:
20+
return self.color_func(frequency)
1721

1822
@lru_cache(maxsize=1024)
1923
def get(self):
@@ -33,12 +37,17 @@ def __getitem__(self, new_size: float):
3337
def get_length_of_word(self, word: str) -> float:
3438
return self.get().getlength(word)
3539

40+
@staticmethod
41+
def default_font():
42+
return get_repo_root() / "fonts" / "OpenSans-Regular.ttf"
43+
3644

3745
def draw_text(
3846
image: ImageWrapper,
3947
rectangle: Rectangle,
4048
word: str,
4149
font: FontWrapper,
50+
frequency: float,
4251
rotate=False,
4352
):
4453
"""
@@ -52,7 +61,10 @@ def draw_text(
5261
text_draw = ImageDraw.Draw(text_image)
5362

5463
text_draw.text(
55-
(-text_bbox.x, -text_bbox.y), word, font=font.get(), fill=font.color
64+
(-text_bbox.x, -text_bbox.y),
65+
word,
66+
font=font.get(),
67+
fill=font.color(frequency),
5668
)
5769
rotated_text_image = text_image.rotate(90, expand=True)
5870
image.img.paste(rotated_text_image, rectangle.xy)
@@ -62,5 +74,5 @@ def draw_text(
6274
(rectangle.x - text_bbox.x, rectangle.y - text_bbox.y),
6375
word,
6476
font=font.get(),
65-
fill=font.color,
77+
fill=font.color(frequency),
6678
)

wrdcld/image.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from PIL import Image, ImageDraw
22

3+
from .util import Color
4+
35

46
class ImageWrapper:
5-
def __init__(self, width: int, height: int, background_color: tuple[int, int, int]):
7+
def __init__(self, width: int, height: int, background_color: Color):
68
self.width = width
79
self.height = height
810
self.background_color = background_color

wrdcld/main.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def _fill(
1616
word_length: int,
1717
word: str,
1818
font: FontWrapper,
19+
frequency: float,
1920
rotate: bool = False,
2021
):
2122
word_height = font.size
@@ -47,12 +48,12 @@ def _fill(
4748
height=word_length,
4849
)
4950

50-
draw_text(image, text_rectangle, word, font, rotate=rotate)
51+
draw_text(image, text_rectangle, word, font, frequency, rotate=rotate)
5152

5253
return text_rectangle
5354

5455

55-
def fill_next_word(word, available_rectangles, image, font):
56+
def fill_next_word(word, available_rectangles, image, font, frequency):
5657
word_length = font.get_length_of_word(word)
5758

5859
suitable_horizontal_rectangles = [
@@ -96,7 +97,9 @@ def fill_next_word(word, available_rectangles, image, font):
9697
if option == "horizontal":
9798
available_rectangles.remove(horizontal_option)
9899
chosen_rectangle = horizontal_option
99-
text_rectangle = _fill(chosen_rectangle, image, word_length, word, font)
100+
text_rectangle = _fill(
101+
chosen_rectangle, image, word_length, word, font, frequency
102+
)
100103

101104
else:
102105
available_rectangles.remove(vertical_option)
@@ -107,6 +110,7 @@ def fill_next_word(word, available_rectangles, image, font):
107110
word_length,
108111
word,
109112
font,
113+
frequency,
110114
rotate=True,
111115
)
112116

wrdcld/util.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from pathlib import Path
2+
from typing import TypeAlias
3+
4+
Color: TypeAlias = tuple[int, int, int]
25

36

47
def get_repo_root():

0 commit comments

Comments
 (0)