12 |
13 | A **simple** and **reproducible** way of using fonts in matplotlib. In short, `pyfonts`:
14 |
15 | - allows you to use all fonts from [**Google Font**](https://fonts.google.com/)
16 | - allows you to use all fonts from [**Bunny Font**](https://fonts.bunny.net/) (GDPR-compliant alternative to Google Fonts)
17 | - allows you to use any font from an **arbitrary URL**
18 | - is **efficient** (thanks to its cache system)
19 |
20 |
21 |
22 | ## Quick start
23 |
24 | - Google Fonts
25 |
26 | ```python
27 | import matplotlib.pyplot as plt
28 | from pyfonts import load_google_font
29 |
30 | font = load_google_font("Fascinate Inline")
31 |
32 | fig, ax = plt.subplots()
33 | ax.text(x=0.2, y=0.5, s="Hey there!", size=30, font=font)
34 | ```
35 |
36 | 
37 |
38 | - Bunny Fonts
39 |
40 | ```python
41 | import matplotlib.pyplot as plt
42 | from pyfonts import load_bunny_font
43 |
44 | font = load_bunny_font("Barrio")
45 |
46 | fig, ax = plt.subplots()
47 | ax.text(x=0.2, y=0.5, s="Hey there!", size=30, font=font)
48 | ```
49 |
50 | 
51 |
52 | [**See more examples**](https://y-sunflower.github.io/pyfonts/#quick-start)
53 |
54 |
55 |
56 | ## Installation
57 |
58 | ```bash
59 | pip install pyfonts
60 | ```
61 |
--------------------------------------------------------------------------------
/tests/test_google.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from matplotlib.font_manager import FontProperties
3 | from pyfonts import load_google_font
4 | from pyfonts.utils import _get_fonturl
5 | from pyfonts.is_valid import _is_url, _is_valid_raw_url
6 |
7 |
8 | def test_errors():
9 | with pytest.raises(ValueError, match="`weight` must be between 100 and 900"):
10 | _get_fonturl(
11 | endpoint="https://fonts.googleapis.com/css2",
12 | family="Roboto",
13 | weight=90,
14 | italic=False,
15 | allowed_formats=["woff2", "woff", "ttf", "otf"],
16 | use_cache=False,
17 | )
18 |
19 | with pytest.raises(RuntimeError, match="No font files found in formats"):
20 | _get_fonturl(
21 | endpoint="https://fonts.googleapis.com/css2",
22 | family="Roboto",
23 | weight=400,
24 | italic=False,
25 | allowed_formats=["aaa"],
26 | use_cache=False,
27 | )
28 |
29 |
30 | @pytest.mark.parametrize("family", ["Roboto", "Open Sans"])
31 | @pytest.mark.parametrize("weight", [300, 500, 800])
32 | @pytest.mark.parametrize("italic", [True, False])
33 | def test_get_fonturl(family, weight, italic):
34 | url = _get_fonturl(
35 | endpoint="https://fonts.googleapis.com/css2",
36 | family=family,
37 | weight=weight,
38 | italic=italic,
39 | allowed_formats=["woff2", "woff", "ttf", "otf"],
40 | use_cache=False,
41 | )
42 |
43 | assert isinstance(url, str)
44 | assert _is_url(url)
45 | assert _is_valid_raw_url(url)
46 |
47 |
48 | @pytest.mark.parametrize("family", ["Roboto", "Open Sans"])
49 | @pytest.mark.parametrize("weight", [300, 500, 800, "bold", "light", "regular"])
50 | @pytest.mark.parametrize("italic", [True, False])
51 | @pytest.mark.parametrize("use_cache", [True, False])
52 | def test_load_google_font(family, weight, italic, use_cache):
53 | font = load_google_font(family, weight=weight, italic=italic, use_cache=use_cache)
54 |
55 | assert isinstance(font, FontProperties)
56 | assert font.get_name() == family
57 |
--------------------------------------------------------------------------------
/docs/reference/load_google_font.md:
--------------------------------------------------------------------------------
1 | ```python
2 | # mkdocs: render
3 | # mkdocs: hidecode
4 | import matplotlib
5 | matplotlib.rcParams.update(matplotlib.rcParamsDefault)
6 | ```
7 |
8 | # Load Google font
9 |
10 |
11 |
12 | ::: pyfonts.load_google_font
13 |
14 |
15 |
16 | ## Examples
17 |
18 | #### Basic usage
19 |
20 | ```python hl_lines="5 13"
21 | # mkdocs: render
22 | import matplotlib.pyplot as plt
23 | from pyfonts import load_google_font
24 |
25 | font = load_google_font("Roboto") # default Roboto font
26 |
27 | fig, ax = plt.subplots()
28 | ax.text(
29 | x=0.2,
30 | y=0.3,
31 | s="Hey there!",
32 | size=30,
33 | font=font
34 | )
35 | ```
36 |
37 | #### Custom font
38 |
39 | ```python hl_lines="5 13"
40 | # mkdocs: render
41 | import matplotlib.pyplot as plt
42 | from pyfonts import load_google_font
43 |
44 | font = load_google_font("Roboto", weight="bold", italic=True) # italic and bold
45 |
46 | fig, ax = plt.subplots()
47 | ax.text(
48 | x=0.2,
49 | y=0.3,
50 | s="Hey there!",
51 | size=30,
52 | font=font
53 | )
54 | ```
55 |
56 | #### Use multiple fonts
57 |
58 | ```python hl_lines="5 6 15 23"
59 | # mkdocs: render
60 | import matplotlib.pyplot as plt
61 | from pyfonts import load_google_font
62 |
63 | font_bold = load_google_font("Roboto", weight="bold")
64 | font_italic = load_google_font("Roboto", italic=True)
65 |
66 | fig, ax = plt.subplots()
67 |
68 | ax.text(
69 | x=0.2,
70 | y=0.3,
71 | s="Hey bold!",
72 | size=30,
73 | font=font_bold
74 | )
75 |
76 | ax.text(
77 | x=0.4,
78 | y=0.6,
79 | s="Hey italic!",
80 | size=30,
81 | font=font_italic
82 | )
83 | ```
84 |
85 | #### Fancy font
86 |
87 | All fonts from [Google font](https://fonts.google.com/) can be used:
88 |
89 | ```python hl_lines="5 13"
90 | # mkdocs: render
91 | import matplotlib.pyplot as plt
92 | from pyfonts import load_google_font
93 |
94 | font = load_google_font("Barrio")
95 |
96 | fig, ax = plt.subplots()
97 | ax.text(
98 | x=0.1,
99 | y=0.3,
100 | s="What a weird font!",
101 | size=30,
102 | font=font
103 | )
104 | ```
105 |
--------------------------------------------------------------------------------
/docs/reference/load_bunny_font.md:
--------------------------------------------------------------------------------
1 | ```python
2 | # mkdocs: render
3 | # mkdocs: hidecode
4 | import matplotlib
5 | matplotlib.rcParams.update(matplotlib.rcParamsDefault)
6 | ```
7 |
8 | # Load Bunny font
9 |
10 |
11 |
12 | ::: pyfonts.load_bunny_font
13 |
14 |
15 |
16 | ## Examples
17 |
18 | #### Basic usage
19 |
20 | ```python hl_lines="5 13"
21 | # mkdocs: render
22 | import matplotlib.pyplot as plt
23 | from pyfonts import load_bunny_font
24 |
25 | font = load_bunny_font("Alumni Sans") # default Alice font
26 |
27 | fig, ax = plt.subplots()
28 | ax.text(
29 | x=0.2,
30 | y=0.3,
31 | s="Hey there!",
32 | size=30,
33 | font=font
34 | )
35 | ```
36 |
37 | #### Custom font
38 |
39 | ```python hl_lines="5 13"
40 | # mkdocs: render
41 | import matplotlib.pyplot as plt
42 | from pyfonts import load_bunny_font
43 |
44 | font = load_bunny_font("Alumni Sans", weight="bold", italic=True) # italic and bold
45 |
46 | fig, ax = plt.subplots()
47 | ax.text(
48 | x=0.2,
49 | y=0.3,
50 | s="Hey there!",
51 | size=30,
52 | font=font
53 | )
54 | ```
55 |
56 | #### Use multiple fonts
57 |
58 | ```python hl_lines="5 6 15 23"
59 | # mkdocs: render
60 | import matplotlib.pyplot as plt
61 | from pyfonts import load_bunny_font
62 |
63 | font_bold = load_bunny_font("Alumni Sans", weight="bold")
64 | font_italic = load_bunny_font("Alumni Sans", italic=True)
65 |
66 | fig, ax = plt.subplots()
67 |
68 | ax.text(
69 | x=0.2,
70 | y=0.3,
71 | s="Hey bold!",
72 | size=30,
73 | font=font_bold
74 | )
75 |
76 | ax.text(
77 | x=0.4,
78 | y=0.6,
79 | s="Hey italic!",
80 | size=30,
81 | font=font_italic
82 | )
83 | ```
84 |
85 | #### Fancy font
86 |
87 | All fonts from [Bunny font](https://fonts.bunny.net/) can be used:
88 |
89 | ```python hl_lines="5 13"
90 | # mkdocs: render
91 | import matplotlib.pyplot as plt
92 | from pyfonts import load_bunny_font
93 |
94 | font = load_bunny_font("Barrio")
95 |
96 | fig, ax = plt.subplots()
97 | ax.text(
98 | x=0.1,
99 | y=0.3,
100 | s="What a weird font!",
101 | size=30,
102 | font=font
103 | )
104 | ```
105 |
--------------------------------------------------------------------------------
/tests/test_bunny.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from matplotlib.font_manager import FontProperties
3 | from pyfonts import load_bunny_font
4 | from pyfonts.utils import _get_fonturl
5 | from pyfonts.is_valid import _is_url, _is_valid_raw_url
6 |
7 |
8 | def test_errors():
9 | with pytest.raises(ValueError, match="`weight` must be between 100 and 900"):
10 | _get_fonturl(
11 | endpoint="https://fonts.bunny.net/css",
12 | family="Roboto",
13 | weight=90,
14 | italic=False,
15 | allowed_formats=["woff", "ttf", "otf"],
16 | use_cache=False,
17 | )
18 |
19 | with pytest.raises(RuntimeError, match="No font files found in formats"):
20 | _get_fonturl(
21 | endpoint="https://fonts.bunny.net/css",
22 | family="Roboto",
23 | weight=400,
24 | italic=False,
25 | allowed_formats=["aaa"],
26 | use_cache=False,
27 | )
28 |
29 |
30 | @pytest.mark.parametrize("family", ["Alumni Sans", "Roboto"])
31 | @pytest.mark.parametrize("weight", [None, 300, 800])
32 | @pytest.mark.parametrize("italic", [None, True, False])
33 | def test_get_fonturl(family, weight, italic):
34 | url = _get_fonturl(
35 | endpoint="https://fonts.bunny.net/css",
36 | family=family,
37 | weight=weight,
38 | italic=italic,
39 | allowed_formats=["woff", "ttf", "otf"],
40 | use_cache=False,
41 | )
42 |
43 | assert isinstance(url, str)
44 | assert _is_url(url)
45 | assert _is_valid_raw_url(url)
46 |
47 |
48 | @pytest.mark.parametrize("family", ["Roboto", "Open Sans"])
49 | @pytest.mark.parametrize("weight", [None, 300, 500, 800, "bold", "light", "regular"])
50 | @pytest.mark.parametrize("italic", [None, True, False])
51 | @pytest.mark.parametrize("use_cache", [True, False])
52 | def test_load_bunny_font(family, weight, italic, use_cache):
53 | font = load_bunny_font(family, weight=weight, italic=italic, use_cache=use_cache)
54 |
55 | assert isinstance(font, FontProperties)
56 | assert font.get_name() == family
57 |
58 |
59 | def test_weird_api_error():
60 | with pytest.raises(ValueError, match="No font available for the request at URL*"):
61 | load_bunny_font("Alice", italic=True)
62 |
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | Any kind of contribution is more than welcomed! There are several ways you can contribute:
2 |
3 | - Opening [GitHub issues](https://github.com/y-sunflower/pyfonts/issues) to list the bugs you've found
4 | - Implementation of new features or resolution of existing bugs
5 | - Enhancing the documentation
6 |
7 | ## How `pyfonts` works
8 |
9 | Under the bonnet, `pyfonts` does several things, but it can be summarised as follows:
10 |
11 | - Take the user's data (font name, weight, italics) and create a url that will be passed to Google's Font API.
12 | - Parse the response to obtain the url of the actual font file
13 | - Retrieve the font file from a temporary file
14 | - Use this temporary file to create a matplotlib font object (which is [`FontProperties`](https://matplotlib.org/stable/api/font_manager_api.html#matplotlib.font_manager.FontProperties){target=‘ \_blank’})
15 | - Return this object
16 |
17 | By default, the font file url is cached to reduce the number of requests required and improve performance. The cache can be cleared with `clear_pyfonts_cache()`.
18 |
19 | ## Setting up your environment
20 |
21 | ### Install for development
22 |
23 | - Fork the repository to your own GitHub account.
24 |
25 | - Clone your forked repository to your local machine (ensure you have [Git installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)):
26 |
27 | ```bash
28 | git clone https://github.com/YOURUSERNAME/pyfonts.git
29 | cd pyfonts
30 | ```
31 |
32 | - Create a new branch:
33 |
34 | ```bash
35 | git checkout -b my-feature
36 | ```
37 |
38 | - Set up your Python environment (ensure you have [uv installed](https://docs.astral.sh/uv/getting-started/installation/)):
39 |
40 | ```bash
41 | uv sync --all-extras --dev
42 | uv pip install -e .
43 | ```
44 |
45 | ### Code!
46 |
47 | You can now make changes to the package and start coding!
48 |
49 | ### Run the test
50 |
51 | - Test that everything works correctly by running:
52 |
53 | ```bash
54 | uv run pytest
55 | ```
56 |
57 | ### Preview documentation locally
58 |
59 | ```bash
60 | uv run mkdocs serve
61 | ```
62 |
63 | ### Push changes
64 |
65 | - Commit and push your changes:
66 |
67 | ```bash
68 | git add -A
69 | git commit -m "description of what you did"
70 | git push
71 | ```
72 |
73 | - Go back to your fork and click on the "Open a PR" popup
74 |
75 | Congrats! Once your PR is merged, it will be part of `pyfonts`.
76 |
77 |
78 |
--------------------------------------------------------------------------------
/tests/test_is_valid.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pyfonts.is_valid import _is_valid_raw_url, _is_url
3 |
4 |
5 | @pytest.mark.parametrize(
6 | "input_string, expected_result",
7 | [
8 | ("https://www.example.com", True),
9 | ("http://example.com", True),
10 | ("ftp://ftp.example.com", False),
11 | ("www.example.com", False),
12 | ("example.com", False),
13 | ("just a string", False),
14 | ("", False),
15 | ("file:///C:/Users/username/Documents/file.txt", False),
16 | ("C:\\Windows\\Fonts\\arial.ttf", False),
17 | ("file:\\C:\\Windows\\Fonts\\arial.ttf", False),
18 | ("mailto:user@example.com", False),
19 | ],
20 | )
21 | def test_is_url(input_string, expected_result):
22 | assert _is_url(input_string) == expected_result
23 |
24 |
25 | @pytest.mark.parametrize(
26 | "url,expected",
27 | [
28 | ("https://github.com/user/repo/blob/master/font.ttf?raw=true", True),
29 | ("https://github.com/user/repo/raw/master/font.otf", True),
30 | ("https://raw.githubusercontent.com/user/repo/master/font.woff", True),
31 | (
32 | "https://raw.githubusercontent.com/user/repo/branch-name/subfolder/font.woff2",
33 | True,
34 | ),
35 | ("https://github.com/user/repo/blob/master/font.ttf", False),
36 | ("https://github.com/user/repo/raw/master/font.txt", False),
37 | ("https://raw.githubusercontent.com/user/repo/master/font.exe", False),
38 | ("https://example.com/font.ttf", True),
39 | ("https://github.com/user/repo/tree/master/fonts/font.ttf", False),
40 | (
41 | "https://github.com/user/repo/blob/master/font.ttf?raw=true¶m=value",
42 | True,
43 | ),
44 | ("https://github.com/user/repo/raw/master/font.woff", True),
45 | ("https://raw.githubusercontent.com/user/repo/master/font.ttf?raw=true", True),
46 | ],
47 | )
48 | def test_is_valid_raw_url(url, expected):
49 | assert _is_valid_raw_url(url) == expected, f"{url}"
50 |
51 |
52 | def test_is_valid_raw_url_with_empty_string():
53 | assert not _is_valid_raw_url("")
54 |
55 |
56 | def test_is_valid_raw_url_with_none():
57 | with pytest.raises(TypeError):
58 | _is_valid_raw_url(None) # ty: ignore
59 |
60 |
61 | def test_is_valid_raw_url_with_non_string():
62 | with pytest.raises(TypeError):
63 | _is_valid_raw_url(123) # ty: ignore
64 |
--------------------------------------------------------------------------------
/docs/reference/load_font.md:
--------------------------------------------------------------------------------
1 | ```python
2 | # mkdocs: render
3 | # mkdocs: hidecode
4 | import matplotlib
5 | matplotlib.rcParams.update(matplotlib.rcParamsDefault)
6 | ```
7 |
8 | # Load font
9 |
10 |
11 |
12 | ::: pyfonts.load_font
13 |
14 |
15 |
16 | ## Examples
17 |
18 | Most font files are stored on Github, but to pass a valid font url, you need to add `?raw=true` to the end of it.
19 |
20 | So the url goes from:
21 |
22 | ```
23 | https://github.com/y-sunflower/pyfonts/blob/main/tests/Amarante-Regular.ttf
24 | ```
25 |
26 | To:
27 |
28 | ```
29 | https://github.com/y-sunflower/pyfonts/blob/main/tests/Amarante-Regular.ttf?raw=true
30 | ```
31 |
32 | What's more, if you find a font on the Google font repo (for example, here: `https://github.com/google/fonts/`), it will probably be easier to use the [`load_google_font()`](load_google_font.md) function.
33 |
34 | #### Basic usage
35 |
36 | ```python
37 | # mkdocs: render
38 | import matplotlib.pyplot as plt
39 | from pyfonts import load_font
40 |
41 | font = load_font(
42 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf?raw=true"
43 | )
44 |
45 | fig, ax = plt.subplots()
46 | ax.text(
47 | x=0.2,
48 | y=0.3,
49 | s="Hey there!",
50 | size=30,
51 | font=font
52 | )
53 | ```
54 |
55 | #### Custom font
56 |
57 | ```python
58 | # mkdocs: render
59 | import matplotlib.pyplot as plt
60 | from pyfonts import load_font
61 |
62 | font = load_font(
63 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Amarante-Regular.ttf?raw=true"
64 | )
65 |
66 | fig, ax = plt.subplots()
67 | ax.text(
68 | x=0.2,
69 | y=0.3,
70 | s="Hey there!",
71 | size=30,
72 | font=font
73 | )
74 | ```
75 |
76 | #### Use multiple fonts
77 |
78 | ```python
79 | # mkdocs: render
80 | import matplotlib.pyplot as plt
81 | from pyfonts import load_font
82 |
83 | font_1 = load_font(
84 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf?raw=true"
85 | )
86 | font_2 = load_font(
87 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Amarante-Regular.ttf?raw=true"
88 | )
89 |
90 | fig, ax = plt.subplots()
91 |
92 | ax.text(
93 | x=0.2,
94 | y=0.3,
95 | s="Hey there!",
96 | size=30,
97 | font=font_1
98 | )
99 |
100 | ax.text(
101 | x=0.4,
102 | y=0.6,
103 | s="Hello world",
104 | size=30,
105 | font=font_2
106 | )
107 | ```
108 |
--------------------------------------------------------------------------------
/tests/test_load_font.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import re
3 |
4 | from matplotlib.font_manager import FontProperties
5 |
6 | import pyfonts
7 | from pyfonts import load_font
8 |
9 |
10 | @pytest.mark.parametrize(
11 | "font_url",
12 | [
13 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf?raw=true",
14 | "tests/Ultra-Regular.ttf",
15 | ],
16 | )
17 | @pytest.mark.parametrize(
18 | "use_cache",
19 | [
20 | True,
21 | False,
22 | ],
23 | )
24 | def test_load_font(font_url, use_cache):
25 | font = load_font(font_url, use_cache=use_cache)
26 | assert isinstance(font, FontProperties)
27 | assert font.get_family() == ["sans-serif"]
28 | assert font.get_name() == "Ultra"
29 | assert font.get_style() == "normal"
30 |
31 |
32 | def test_load_font_invalid_input():
33 | font_url = "/path/to/font.ttf"
34 | with pytest.raises(FileNotFoundError, match=f"Font file not found: '{font_url}'."):
35 | load_font(font_url)
36 |
37 | with pytest.warns(UserWarning):
38 | font_url = "/path/to/font.ttf"
39 | with pytest.raises(
40 | FileNotFoundError, match=f"Font file not found: '{font_url}'."
41 | ):
42 | load_font(font_path=font_url)
43 |
44 | font_url = (
45 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf"
46 | )
47 | with pytest.raises(
48 | ValueError,
49 | match=rf"^{re.escape(f'The URL provided ({font_url}) does not appear to be valid.')}",
50 | ):
51 | load_font(font_url)
52 |
53 | font_url = "https://github.com/y-sunflower/pyfonts/blob/main/tests/UltraRegular.ttf?raw=true"
54 | with pytest.raises(
55 | Exception,
56 | match="404 error. The url passed does not exist: font file not found.",
57 | ):
58 | load_font(font_url)
59 |
60 |
61 | def test_load_font_warning():
62 | font_path = "tests/Ultra-Regular.ttf"
63 | match = (
64 | "`font_path` argument is deprecated and will be removed in a future version."
65 | )
66 | f" Please replace `load_font(font_path='{font_path}')` by `load_font('{font_path}')`."
67 | with pytest.warns(UserWarning, match=match):
68 | load_font(font_path=font_path)
69 |
70 |
71 | def test_load_font_no_input():
72 | with pytest.raises(ValueError, match="You must provide a `font_url`."):
73 | load_font()
74 |
75 |
76 | def test_pyfonts_version():
77 | assert pyfonts.__version__ == "1.2.0"
78 |
--------------------------------------------------------------------------------
/pyfonts/google.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union, List
2 | from matplotlib.font_manager import FontProperties
3 |
4 | from pyfonts import load_font
5 | from pyfonts.utils import _get_fonturl
6 |
7 |
8 | def load_google_font(
9 | family: str,
10 | weight: Optional[Union[int, str]] = None,
11 | italic: Optional[bool] = None,
12 | allowed_formats: List[str] = ["woff2", "woff", "ttf", "otf"],
13 | use_cache: bool = True,
14 | danger_not_verify_ssl: bool = False,
15 | ) -> FontProperties:
16 | """
17 | Load a font from Google Fonts with specified styling options and return a font property
18 | object that you can then use in your matplotlib charts.
19 |
20 | The easiest way to find the font you want is to browse [Google font](https://fonts.google.com/)
21 | and then pass the font name to the `family` argument.
22 |
23 | Args:
24 | family: Font family name (e.g., "Open Sans", "Roboto", etc).
25 | weight: Desired font weight (e.g., 400, 700) or one of 'thin', 'extra-light', 'light',
26 | 'regular', 'medium', 'semi-bold', 'bold', 'extra-bold', 'black'. Default is `None`.
27 | italic: Whether to use the italic variant. Default is `None`.
28 | allowed_formats: List of acceptable font file formats. Defaults to ["woff2", "woff", "ttf", "otf"].
29 | use_cache: Whether or not to cache fonts (to make pyfonts faster). Default to `True`.
30 | danger_not_verify_ssl: Whether or not to to skip SSL certificate on
31 | `ssl.SSLCertVerificationError`. If `True`, it's a **security risk** (such as data breaches or
32 | man-in-the-middle attacks), but can be convenient in some cases, like local
33 | development when behind a firewall.
34 |
35 | Returns:
36 | matplotlib.font_manager.FontProperties: A `FontProperties` object containing the loaded font.
37 |
38 | Examples:
39 |
40 | ```python
41 | from pyfonts import load_google_font
42 |
43 | font = load_google_font("Roboto") # default Roboto font
44 | font = load_google_font("Roboto", weight="bold") # bold font
45 | font = load_google_font("Roboto", italic=True) # italic font
46 | font = load_google_font("Roboto", weight="bold", italic=True) # italic and bold
47 | ```
48 | """
49 | font_url = _get_fonturl(
50 | endpoint="https://fonts.googleapis.com/css2",
51 | family=family,
52 | italic=italic,
53 | weight=weight,
54 | allowed_formats=allowed_formats,
55 | use_cache=use_cache,
56 | )
57 |
58 | return load_font(
59 | font_url,
60 | use_cache=use_cache,
61 | danger_not_verify_ssl=danger_not_verify_ssl,
62 | )
63 |
--------------------------------------------------------------------------------
/pyfonts/bunny.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union, List
2 | from matplotlib.font_manager import FontProperties
3 |
4 | from pyfonts import load_font
5 | from pyfonts.utils import _get_fonturl
6 |
7 |
8 | def load_bunny_font(
9 | family: str,
10 | weight: Optional[Union[int, str]] = None,
11 | italic: Optional[bool] = None,
12 | allowed_formats: List[str] = ["woff", "ttf", "otf"],
13 | use_cache: bool = True,
14 | danger_not_verify_ssl: bool = False,
15 | ) -> FontProperties:
16 | """
17 | Load a font from bunny Fonts with specified styling options and return a font property
18 | object that you can then use in your matplotlib charts.
19 |
20 | The easiest way to find the font you want is to browse [bunny font](https://fonts.bunny.net/)
21 | and then pass the font name to the `family` argument.
22 |
23 | Args:
24 | family: Font family name (e.g., "Open Sans", "Roboto", etc).
25 | weight: Desired font weight (e.g., 400, 700) or one of 'thin', 'extra-light', 'light',
26 | 'regular', 'medium', 'semi-bold', 'bold', 'extra-bold', 'black'. Default is `None`.
27 | italic: Whether to use the italic variant. Default is `None`.
28 | allowed_formats: List of acceptable font file formats. Defaults to ["woff", "ttf", "otf"].
29 | Note that for `woff2` fonts to work, you must have [brotli](https://github.com/google/brotli)
30 | installed.
31 | use_cache: Whether or not to cache fonts (to make pyfonts faster). Default to `True`.
32 | danger_not_verify_ssl: Whether or not to to skip SSL certificate on
33 | `ssl.SSLCertVerificationError`. If `True`, it's a **security risk** (such as data breaches or
34 | man-in-the-middle attacks), but can be convenient in some cases, like local
35 | development when behind a firewall.
36 |
37 | Returns:
38 | matplotlib.font_manager.FontProperties: A `FontProperties` object containing the loaded font.
39 |
40 | Examples:
41 |
42 | ```python
43 | from pyfonts import load_bunny_font
44 |
45 | font = load_bunny_font("Roboto") # default Roboto font
46 | font = load_bunny_font("Roboto", weight="bold") # bold font
47 | font = load_bunny_font("Roboto", italic=True) # italic font
48 | font = load_bunny_font("Roboto", weight="bold", italic=True) # italic and bold
49 | ```
50 | """
51 | font_url = _get_fonturl(
52 | endpoint="https://fonts.bunny.net/css",
53 | family=family,
54 | weight=weight,
55 | italic=italic,
56 | allowed_formats=allowed_formats,
57 | use_cache=use_cache,
58 | )
59 |
60 | return load_font(
61 | font_url,
62 | use_cache=use_cache,
63 | danger_not_verify_ssl=danger_not_verify_ssl,
64 | )
65 |
--------------------------------------------------------------------------------
/overrides/partials/footer.html:
--------------------------------------------------------------------------------
1 |
85 |
86 |
110 |
--------------------------------------------------------------------------------
/docs/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Barriecito&family=Rubik+Distressed&family=Bangers&family=Jolly+Lodger&display=swap");
2 |
3 | h2,
4 | h3,
5 | h4 {
6 | margin-top: 3em !important;
7 | }
8 |
9 | .md-header {
10 | background-color: #cb4e4f;
11 | }
12 |
13 | .md-tabs {
14 | background-color: #cb4e4f;
15 | }
16 |
17 | [data-md-component="logo"] > img {
18 | height: 3rem !important;
19 | border: 1px solid white;
20 | border-radius: 50%;
21 | }
22 |
23 | .hero {
24 | font-size: 1.2em;
25 | height: 75vh;
26 | display: flex;
27 | align-items: center;
28 | text-align: center;
29 | }
30 |
31 | .w2 {
32 | font-family: "Bangers", system-ui;
33 | }
34 |
35 | .w4 {
36 | font-family: "Jolly Lodger", system-ui;
37 | }
38 |
39 | .w9 {
40 | font-family: "Barriecito", system-ui;
41 | }
42 |
43 | .w8 {
44 | font-family: "Rubik Distressed", system-ui;
45 | }
46 |
47 | .pyfonts-name {
48 | display: inline-block;
49 | font-weight: 800;
50 | font-size: 1.2em;
51 | line-height: 1;
52 | letter-spacing: 0.02em;
53 | transform-origin: center;
54 | color: #cb4e4f;
55 | position: relative;
56 | text-decoration: underline;
57 | }
58 |
59 | /* trigger-class added on load */
60 | .pyfonts-name.animated {
61 | animation: popWiggle 1s cubic-bezier(0.2, 0.9, 0.3, 1) both,
62 | gradientShift 3s linear infinite;
63 | }
64 |
65 | /* quick glossy sweep */
66 | .pyfonts-name.animated::after {
67 | content: "";
68 | position: absolute;
69 | left: -40%;
70 | top: 0;
71 | width: 60%;
72 | height: 100%;
73 | transform: skewX(-20deg);
74 | background: linear-gradient(
75 | 90deg,
76 | rgba(255, 255, 255, 0) 0%,
77 | rgba(255, 255, 255, 0.9) 50%,
78 | rgba(255, 255, 255, 0) 100%
79 | );
80 | mix-blend-mode: screen;
81 | animation: shine 1s ease 0.15s both;
82 | pointer-events: none;
83 | }
84 |
85 | /* keyframes */
86 | @keyframes popWiggle {
87 | 0% {
88 | transform: scale(0.6) rotate(-6deg);
89 | opacity: 0;
90 | filter: blur(6px);
91 | }
92 | 50% {
93 | transform: scale(1.08) rotate(6deg);
94 | filter: blur(0);
95 | }
96 | 70% {
97 | transform: scale(0.98) rotate(-3deg);
98 | }
99 | 100% {
100 | transform: scale(1) rotate(0deg);
101 | opacity: 1;
102 | }
103 | }
104 |
105 | @keyframes gradientShift {
106 | 0% {
107 | background-position: 0% 50%;
108 | }
109 | 50% {
110 | background-position: 100% 50%;
111 | }
112 | 100% {
113 | background-position: 0% 50%;
114 | }
115 | }
116 |
117 | @keyframes shine {
118 | 0% {
119 | transform: translateX(-100%) skewX(-20deg);
120 | opacity: 0;
121 | }
122 | 40% {
123 | opacity: 1;
124 | }
125 | 100% {
126 | transform: translateX(200%) skewX(-20deg);
127 | opacity: 0;
128 | }
129 | }
130 |
131 | @media (max-width: 768px) {
132 | .md-nav--primary .md-nav__title[for="__drawer"] {
133 | background-color: #cb4e4f;
134 | }
135 |
136 | .hero {
137 | height: 80vh;
138 | font-size: 1em;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | ```python
2 | # mkdocs: render
3 | # mkdocs: hidecode
4 | import matplotlib
5 | matplotlib.rcParams.update(matplotlib.rcParamsDefault)
6 | ```
7 |
8 | ## Quick start
9 |
10 | The easiest (and recommended) way of using `pyfonts` is to find the name of a font you like on [Google font](https://fonts.google.com/){target="\_blank"} and pass it to `load_google_font()`:
11 |
12 | ```python
13 | # mkdocs: render
14 | import matplotlib.pyplot as plt
15 | from pyfonts import load_google_font
16 |
17 | font = load_google_font("Fascinate Inline")
18 |
19 | fig, ax = plt.subplots()
20 | ax.text(
21 | x=0.2,
22 | y=0.5,
23 | s="Hey there!",
24 | size=30,
25 | font=font # We pass it to the `font` argument
26 | )
27 | ```
28 |
29 | ## Bold/light fonts
30 |
31 | In order to have a **bold** font, you can use the `weight` argument that accepts either one of: "thin", "extra-light", "light", "regular","medium", "semi-bold", "bold", "extra-bold", "black", or any number between 100 and 900 (the higher the bolder).
32 |
33 | ```python
34 | # mkdocs: render
35 | import matplotlib.pyplot as plt
36 | from pyfonts import load_google_font
37 |
38 | font_bold = load_google_font("Roboto", weight="bold")
39 | font_regular = load_google_font("Roboto", weight="regular") # Default
40 | font_light = load_google_font("Roboto", weight="thin")
41 |
42 | fig, ax = plt.subplots()
43 | text_params = dict(x=0.2,size=30,)
44 | ax.text(
45 | y=0.7,
46 | s="Bold font",
47 | font=font_bold,
48 | **text_params
49 | )
50 | ax.text(
51 | y=0.5,
52 | s="Regular font",
53 | font=font_regular,
54 | **text_params
55 | )
56 | ax.text(
57 | y=0.3,
58 | s="Light font",
59 | font=font_light,
60 | **text_params
61 | )
62 | ```
63 |
64 | > Note that **not all fonts** have different weight and can be set to bold/light.
65 |
66 | ## Italic font
67 |
68 | `load_google_font()` has an `italic` argument, that can either be `True` or `False` (default to `False`).
69 |
70 | ```python
71 | # mkdocs: render
72 | import matplotlib.pyplot as plt
73 | from pyfonts import load_google_font
74 |
75 | font = load_google_font("Roboto", italic=True)
76 |
77 | fig, ax = plt.subplots()
78 | ax.text(
79 | x=0.2,
80 | y=0.5,
81 | s="This text is in italic",
82 | size=30,
83 | font=font
84 | )
85 | ```
86 |
87 | > Note that **not all fonts** can be set to italic.
88 |
89 | ## Set font globally
90 |
91 | If you also want to change the default font used for e.g. the axis labels, legend entries, titles, etc., you can use `set_default_font()`:
92 |
93 | ```python hl_lines="4 5"
94 | # mkdocs: render
95 | from pyfonts import set_default_font, load_google_font
96 |
97 | font = load_google_font("Fascinate Inline")
98 | set_default_font(font) # Sets font for all text
99 |
100 | fig, ax = plt.subplots()
101 |
102 | x = [0, 1, 2, 3]
103 | y = [x**2 for x in x]
104 |
105 | # x+y tick labels, legend entries, title etc.
106 | # will all be in Fascinate Inline
107 | ax.plot(x, y, "-o", label='y = x²')
108 | ax.set_title('Simple Line Chart')
109 | ax.text(x=0, y=5, s="Hello world", size=20)
110 | ax.legend()
111 |
112 | # change the font for a specific element as usual
113 | ax.set_xlabel("x values", font=load_google_font("Roboto"), size=15)
114 | ```
115 |
--------------------------------------------------------------------------------
/pyfonts/cache.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import hashlib
3 | import os
4 | import json
5 | from urllib.parse import urlparse
6 |
7 | _CACHE_FILE: str = os.path.join(
8 | os.path.expanduser("~"),
9 | ".cache",
10 | ".pyfonts_google_cache.json",
11 | )
12 | _MEMORY_CACHE: dict = {}
13 |
14 |
15 | def _cache_key(family: str, weight, italic, allowed_formats: list[str]) -> str:
16 | key_str: str = json.dumps(
17 | {
18 | "family": family,
19 | "weight": weight,
20 | "italic": italic,
21 | "allowed_formats": allowed_formats,
22 | },
23 | sort_keys=True,
24 | )
25 | return hashlib.sha256(key_str.encode()).hexdigest()
26 |
27 |
28 | def _load_cache_from_disk() -> dict:
29 | if not os.path.exists(_CACHE_FILE):
30 | return {}
31 | try:
32 | with open(_CACHE_FILE, "r") as f:
33 | return json.load(f)
34 | except Exception:
35 | return {}
36 |
37 |
38 | def _save_cache_to_disk() -> None:
39 | try:
40 | with open(_CACHE_FILE, "w") as f:
41 | json.dump(_MEMORY_CACHE, f)
42 | except Exception:
43 | pass
44 |
45 |
46 | def clear_pyfonts_cache(verbose: bool = True) -> None:
47 | """
48 | Cleans both:
49 | 1. The font cache directory
50 | 2. The Google Fonts URL cache
51 |
52 | Args:
53 | `verbose`: Whether or not to print a cache cleanup message.
54 | The default value is `True`.
55 |
56 | Examples:
57 |
58 | ```python
59 | from pyfonts import clear_pyfonts_cache
60 |
61 | clear_pyfonts_cache()
62 | ```
63 | """
64 | cache_dir: str = _get_cache_dir()
65 |
66 | # clear the local font file cache
67 | if os.path.exists(cache_dir):
68 | shutil.rmtree(cache_dir)
69 | if verbose:
70 | print(f"Font cache cleaned: {cache_dir}")
71 | else:
72 | if verbose:
73 | print("No font cache directory found. Nothing to clean.")
74 |
75 | # clear the Google Fonts URL cache
76 | global _MEMORY_CACHE
77 | _MEMORY_CACHE.clear()
78 |
79 | if os.path.exists(_CACHE_FILE):
80 | try:
81 | os.remove(_CACHE_FILE)
82 | if verbose:
83 | print(f"Google Fonts URL cache cleared: {_CACHE_FILE}")
84 | except Exception as e:
85 | if verbose:
86 | print(f"Failed to remove Google Fonts cache file: {e}")
87 | else:
88 | if verbose:
89 | print("No Google Fonts cache file found. Nothing to clean.")
90 |
91 |
92 | def _create_cache_from_fontfile(font_url):
93 | parsed_url = urlparse(font_url)
94 | url_path = parsed_url.path
95 | filename = os.path.basename(url_path)
96 | _, ext = os.path.splitext(filename)
97 | url_hash: str = hashlib.sha256(font_url.encode()).hexdigest()
98 | cache_filename: str = f"{url_hash}{ext}"
99 | cache_dir: str = _get_cache_dir()
100 | os.makedirs(cache_dir, exist_ok=True)
101 | cached_fontfile: str = os.path.join(cache_dir, cache_filename)
102 | return cached_fontfile, cache_dir
103 |
104 |
105 | def _get_cache_dir() -> str:
106 | return os.path.join(os.path.expanduser("~"), ".cache", "pyfontsloader")
107 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yaml:
--------------------------------------------------------------------------------
1 | # This is taken from
2 | # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#the-whole-ci-cd-workflow
3 | # but with the following differences
4 | # - removed the TestPyPI part
5 | # - instead of `on: push`, we have `tags` in there too
6 |
7 | name: Publish Python 🐍 distribution 📦 to PyPI
8 |
9 | on:
10 | push:
11 | tags:
12 | - "v[0-9]+.[0-9]+.[0-9]+*"
13 |
14 | jobs:
15 | build:
16 | name: Build distribution 📦
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | persist-credentials: false
23 | - name: Set up Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: "3.x"
27 | - name: Install pypa/build
28 | run: python3 -m pip install build --user
29 | - name: Build a binary wheel and a source tarball
30 | run: python3 -m build
31 | - name: Store the distribution packages
32 | uses: actions/upload-artifact@v4
33 | with:
34 | name: python-package-distributions
35 | path: dist/
36 |
37 | publish-to-pypi:
38 | name: >-
39 | Publish Python 🐍 distribution 📦 to PyPI
40 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
41 | needs:
42 | - build
43 | runs-on: ubuntu-latest
44 | environment:
45 | name: pypi
46 | url: https://pypi.org/p/pytest-cov
47 | permissions:
48 | id-token: write # IMPORTANT: mandatory for trusted publishing
49 |
50 | steps:
51 | - name: Download all the dists
52 | uses: actions/download-artifact@v4
53 | with:
54 | name: python-package-distributions
55 | path: dist/
56 | - name: Publish distribution 📦 to PyPI
57 | uses: pypa/gh-action-pypi-publish@release/v1
58 |
59 | github-release:
60 | name: >-
61 | Sign the Python 🐍 distribution 📦 with Sigstore
62 | and upload them to GitHub Release
63 | needs:
64 | - publish-to-pypi
65 | runs-on: ubuntu-latest
66 | permissions:
67 | contents: write # IMPORTANT: mandatory for making GitHub Releases
68 | id-token: write # IMPORTANT: mandatory for sigstore
69 | steps:
70 | - name: Download all the dists
71 | uses: actions/download-artifact@v4
72 | with:
73 | name: python-package-distributions
74 | path: dist/
75 | - name: Sign the dists with Sigstore
76 | uses: sigstore/gh-action-sigstore-python@v3.0.0
77 | with:
78 | inputs: >-
79 | ./dist/*.tar.gz
80 | ./dist/*.whl
81 | - name: Create GitHub Release
82 | env:
83 | GITHUB_TOKEN: ${{ github.token }}
84 | run: >-
85 | gh release create
86 | "$GITHUB_REF_NAME"
87 | --repo "$GITHUB_REPOSITORY"
88 | --notes ""
89 | - name: Upload artifact signatures to GitHub Release
90 | env:
91 | GITHUB_TOKEN: ${{ github.token }}
92 | # Upload to GitHub Release using the `gh` CLI.
93 | # `dist/` contains the built packages, and the
94 | # sigstore-produced signatures and certificates.
95 | run: >-
96 | gh release upload
97 | "$GITHUB_REF_NAME" dist/**
98 | --repo "$GITHUB_REPOSITORY"
99 |
--------------------------------------------------------------------------------
/pyfonts/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 | from typing import Optional, Union
4 | import requests
5 |
6 | from pyfonts.cache import (
7 | _cache_key,
8 | _load_cache_from_disk,
9 | _save_cache_to_disk,
10 | _MEMORY_CACHE,
11 | _CACHE_FILE,
12 | )
13 |
14 |
15 | def _get_fonturl(
16 | endpoint: str,
17 | family: str,
18 | weight: Optional[Union[int, str]],
19 | italic: Optional[bool],
20 | allowed_formats: list,
21 | use_cache: bool,
22 | ):
23 | """
24 | Construct the URL for a given endpoint, font family and style parameters,
25 | fetch the associated CSS, and extract the URL of the font file.
26 |
27 | Args:
28 | enpoint: URL of the font provider.
29 | family: Name of the font family (e.g., "Roboto").
30 | italic: Whether the font should be italic. If None, no italic axis is set.
31 | weight: Numeric font weight (e.g., 400, 700). If None, no weight axis is set.
32 | allowed_formats: List of acceptable font file extensions (e.g., ["woff2", "ttf"]).
33 | use_cache: Whether or not to cache fonts (to make pyfonts faster).
34 |
35 | Returns:
36 | Direct URL to the font file matching the requested style and format.
37 | """
38 | if isinstance(weight, str):
39 | weight: int = _map_weight_to_numeric(weight)
40 |
41 | cache_key: str = _cache_key(family, weight, italic, allowed_formats)
42 | if use_cache:
43 | if not _MEMORY_CACHE and os.path.exists(_CACHE_FILE):
44 | _MEMORY_CACHE.update(_load_cache_from_disk())
45 | if cache_key in _MEMORY_CACHE:
46 | return _MEMORY_CACHE[cache_key]
47 |
48 | url: str = f"{endpoint}?family={family.replace(' ', '+')}"
49 | settings: dict = {}
50 |
51 | if italic:
52 | settings["ital"] = str(int(italic))
53 | if weight is not None:
54 | if not (100 <= weight <= 900):
55 | raise ValueError(f"`weight` must be between 100 and 900, not {weight}.")
56 | settings["wght"] = str(int(weight))
57 | if settings:
58 | axes = ",".join(settings.keys())
59 | values = ",".join(settings.values())
60 | url += f":{axes}@{values}"
61 |
62 | response = requests.get(url)
63 | response.raise_for_status()
64 | css_text = response.text
65 |
66 | # for some reason, Bunny fonts sends this text response instead of an
67 | # actual error message, so we handle it ourselves manually.
68 | if "Error: API Error" in css_text and "No families available" in css_text:
69 | raise ValueError(
70 | f"No font available for the request at URL: {url}. "
71 | "Maybe the font variant (italic, bold, etc) you're looking for"
72 | " does not exist."
73 | )
74 |
75 | formats_pattern = "|".join(map(re.escape, allowed_formats))
76 | font_urls: list = re.findall(
77 | rf"url\((https://[^)]+\.({formats_pattern}))\)", css_text
78 | )
79 | if not font_urls:
80 | raise RuntimeError(
81 | f"No font files found in formats {allowed_formats} for '{family}'"
82 | )
83 |
84 | for fmt in allowed_formats:
85 | for font_url, ext in font_urls:
86 | if ext == fmt:
87 | if use_cache:
88 | _MEMORY_CACHE[cache_key] = font_url
89 | _save_cache_to_disk()
90 | return font_url
91 |
92 |
93 | def _map_weight_to_numeric(weight_str: Union[str, int, float]) -> int:
94 | weight_mapping: dict = {
95 | "thin": 100,
96 | "extra-light": 200,
97 | "light": 300,
98 | "regular": 400,
99 | "medium": 500,
100 | "semi-bold": 600,
101 | "bold": 700,
102 | "extra-bold": 800,
103 | "black": 900,
104 | }
105 | if isinstance(weight_str, int) or isinstance(weight_str, float):
106 | return int(weight_str)
107 |
108 | weight_str: str = weight_str.lower()
109 | if weight_str in weight_mapping:
110 | return weight_mapping[weight_str]
111 |
112 | raise ValueError(
113 | f"Invalid weight descriptor: {weight_str}. Valid options are: "
114 | "thin, extra-light, light, regular, medium, semi-bold, bold, extra-bold, black."
115 | )
116 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ```python
2 | # mkdocs: render
3 | # mkdocs: hidecode
4 | import matplotlib
5 | matplotlib.rcParams.update(matplotlib.rcParamsDefault)
6 | ```
7 |
8 |
9 |
10 |
11 |
12 | pyfonts
13 |
14 | a
15 | simple
16 | and
17 | reproducible
18 | way
19 | of
20 | using
21 | fonts
22 | in matplotlib
23 |
24 |
25 |
26 |
27 |
28 | In short, `pyfonts`:
29 |
30 | - allows you to use all fonts from [**Google Font**](https://fonts.google.com/)
31 | - allows you to use all fonts from [**Bunny Font**](https://fonts.bunny.net/) (GDPR-compliant alternative to Google Fonts)
32 | - allows you to use any font from an **arbitrary URL**
33 | - is **efficient** (thanks to its cache system)
34 |
35 | [](https://pepy.tech/projects/pyfonts)
36 | 
37 | 
38 |
39 | ```bash
40 | pip install pyfonts
41 | ```
42 |
43 |
44 |
45 | ## Quick start
46 |
47 | The easiest (and recommended) way of using `pyfonts` is to **find the name** of a font you like on [Google Fonts](https://fonts.google.com/){target="\_blank"}/[Bunny Fonts](https://fonts.bunny.net/){target="\_blank"} and pass it to `load_google_font()`/`load_bunny_font()`:
48 |
49 | === "Google Fonts"
50 |
51 | ```python
52 | # mkdocs: render
53 | import matplotlib.pyplot as plt
54 | from pyfonts import load_google_font
55 |
56 | font = load_google_font("Fascinate Inline")
57 |
58 | fig, ax = plt.subplots()
59 | ax.text(
60 | x=0.2,
61 | y=0.5,
62 | s="Hey there!",
63 | size=30,
64 | font=font # We pass it to the `font` argument
65 | )
66 | ```
67 |
68 | === "Bunny Fonts"
69 |
70 | ```python
71 | # mkdocs: render
72 | import matplotlib.pyplot as plt
73 | from pyfonts import load_bunny_font
74 |
75 | font = load_bunny_font("Barrio")
76 |
77 | fig, ax = plt.subplots()
78 | ax.text(
79 | x=0.2,
80 | y=0.5,
81 | s="Hey there!",
82 | size=30,
83 | font=font # We pass it to the `font` argument
84 | )
85 | ```
86 |
87 | === "Other"
88 |
89 | ```python
90 | # mkdocs: render
91 | import matplotlib.pyplot as plt
92 | from pyfonts import load_font
93 |
94 | font = load_font("https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf?raw=true")
95 |
96 | fig, ax = plt.subplots()
97 | ax.text(
98 | x=0.2,
99 | y=0.5,
100 | s="Hey there!",
101 | size=30,
102 | font=font # We pass it to the `font` argument
103 | )
104 | ```
105 |
106 | ## Bold/light fonts
107 |
108 | In order to have a **bold** font, you can use the `weight` argument that accepts either one of: "thin", "extra-light", "light", "regular","medium", "semi-bold", "bold", "extra-bold", "black", or any number between 100 and 900 (the higher the bolder).
109 |
110 | ```python
111 | # mkdocs: render
112 | import matplotlib.pyplot as plt
113 | from pyfonts import load_google_font
114 |
115 | font_bold = load_google_font("Roboto", weight="bold")
116 | font_regular = load_google_font("Roboto", weight="regular") # Default
117 | font_light = load_google_font("Roboto", weight="thin")
118 |
119 | fig, ax = plt.subplots()
120 | text_params = dict(x=0.2,size=30,)
121 | ax.text(
122 | y=0.7,
123 | s="Bold font",
124 | font=font_bold,
125 | **text_params
126 | )
127 | ax.text(
128 | y=0.5,
129 | s="Regular font",
130 | font=font_regular,
131 | **text_params
132 | )
133 | ax.text(
134 | y=0.3,
135 | s="Light font",
136 | font=font_light,
137 | **text_params
138 | )
139 | ```
140 |
141 | > Note that **not all fonts** have different weight and can be set to bold/light.
142 |
143 | ## Italic font
144 |
145 | `load_google_font()` has an `italic` argument, that can either be `True` or `False` (default to `False`).
146 |
147 | ```python
148 | # mkdocs: render
149 | import matplotlib.pyplot as plt
150 | from pyfonts import load_google_font
151 |
152 | font = load_google_font("Roboto", italic=True)
153 |
154 | fig, ax = plt.subplots()
155 | ax.text(
156 | x=0.2,
157 | y=0.5,
158 | s="This text is in italic",
159 | size=30,
160 | font=font
161 | )
162 | ```
163 |
164 | > Note that **not all fonts** can be set to italic.
165 |
166 | ## Set font globally
167 |
168 | If you also want to change the default font used for e.g. the axis labels, legend entries, titles, etc., you can use `set_default_font()`:
169 |
170 | ```python hl_lines="4 5"
171 | # mkdocs: render
172 | from pyfonts import set_default_font, load_google_font
173 |
174 | font = load_google_font("Fascinate Inline")
175 | set_default_font(font) # Sets font for all text
176 |
177 | fig, ax = plt.subplots()
178 |
179 | x = [0, 1, 2, 3]
180 | y = [x**2 for x in x]
181 |
182 | # x+y tick labels, legend entries, title etc.
183 | # will all be in Fascinate Inline
184 | ax.plot(x, y, "-o", label='y = x²')
185 | ax.set_title('Simple Line Chart')
186 | ax.text(x=0, y=5, s="Hello world", size=20)
187 | ax.legend()
188 |
189 | # change the font for a specific element as usual
190 | ax.set_xlabel("x values", font=load_google_font("Roboto"), size=15)
191 | ```
192 |
193 |
194 |
195 |
198 |
--------------------------------------------------------------------------------
/pyfonts/main.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import os
3 | import ssl
4 | import warnings
5 |
6 | from urllib.request import urlopen
7 | from urllib.error import URLError, HTTPError
8 | from matplotlib.font_manager import FontProperties, fontManager
9 | from matplotlib import rcParams
10 |
11 | from pyfonts.is_valid import _is_url, _is_valid_raw_url
12 | from pyfonts.cache import _create_cache_from_fontfile
13 | from pyfonts.decompress import _decompress_woff_to_ttf
14 |
15 |
16 | def load_font(
17 | font_url: Optional[str] = None,
18 | use_cache: bool = True,
19 | danger_not_verify_ssl: bool = False,
20 | font_path: Optional[str] = None,
21 | ) -> FontProperties:
22 | """
23 | Loads a matplotlib `FontProperties` object from a remote url or a local file,
24 | that you can then use in your matplotlib charts.
25 |
26 | This function is most useful when the font you are looking for is stored locally
27 | or is not available in Google Fonts. Otherwise, it's easier to use the
28 | [`load_google_font()`](load_google_font.md) function instead.
29 |
30 | If the url points to a font file on Github, add `?raw=true` at the end of the
31 | url (see examples below).
32 |
33 | Args:
34 | font_url: It may be one of the following:
35 | - A URL pointing to a binary font file.
36 | - The local file path of the font.
37 | use_cache: Whether or not to cache fonts (to make pyfonts faster). Default to `True`.
38 | danger_not_verify_ssl: Whether or not to to skip SSL certificate on
39 | `ssl.SSLCertVerificationError`. If `True`, it's a **security risk** (such as data breaches or
40 | man-in-the-middle attacks), but can be convenient in some cases, like local
41 | development when behind a firewall.
42 | font_path: (deprecated) The local file path of the font. Use `font_url` instead.
43 |
44 | Returns:
45 | matplotlib.font_manager.FontProperties: A `FontProperties` object containing the loaded font.
46 |
47 | Examples:
48 |
49 | ```python
50 | from pyfonts import load_font
51 |
52 | font = load_font(
53 | "https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf?raw=true"
54 | )
55 | ```
56 | """
57 | if font_path is not None:
58 | warnings.warn(
59 | "`font_path` argument is deprecated and will be removed in a future version."
60 | f" Please replace `load_font(font_path='{font_path}')` by `load_font('{font_path}')`."
61 | )
62 | font_prop: FontProperties = FontProperties(fname=font_path)
63 | try:
64 | font_prop.get_name()
65 | except FileNotFoundError:
66 | raise FileNotFoundError(f"Font file not found: '{font_path}'.")
67 | return font_prop
68 |
69 | if font_url is not None:
70 | if not _is_url(font_url):
71 | # if it's not an url, it should be a path
72 | font_prop: FontProperties = FontProperties(fname=font_url)
73 | try:
74 | font_prop.get_name()
75 | except FileNotFoundError:
76 | raise FileNotFoundError(f"Font file not found: '{font_url}'.")
77 | return font_prop
78 | if not _is_valid_raw_url(font_url):
79 | raise ValueError(
80 | f"""The URL provided ({font_url}) does not appear to be valid.
81 | It must point to a binary font file from Github.
82 | Have you forgotten to append `?raw=true` to the end of the URL?
83 | """
84 | )
85 |
86 | cached_fontfile, cache_dir = _create_cache_from_fontfile(font_url)
87 |
88 | if use_cache:
89 | # check if file is in cache
90 |
91 | if os.path.exists(cached_fontfile):
92 | try:
93 | # woff/woff2 are not supported by matplotlib, so we convert them
94 | # to ttf. This is mostly useful to work with Bunny fonts API.
95 | if cached_fontfile.endswith(("woff", "woff2")):
96 | cached_fontfile: str = _decompress_woff_to_ttf(cached_fontfile)
97 |
98 | font_prop: FontProperties = FontProperties(fname=cached_fontfile)
99 | font_prop.get_name() # triggers an error if invalid
100 | return font_prop
101 | except Exception:
102 | # cached file is invalid, remove and proceed to download
103 | os.remove(cached_fontfile)
104 |
105 | try:
106 | response = urlopen(font_url)
107 | except HTTPError as e:
108 | if e.code == 404:
109 | raise Exception(
110 | "404 error. The url passed does not exist: font file not found."
111 | )
112 | else:
113 | raise ValueError(f"An HTTPError has occurred. Code: {e.code}")
114 | except URLError as e:
115 | if isinstance(e.reason, ssl.SSLCertVerificationError):
116 | if danger_not_verify_ssl:
117 | warnings.warn(
118 | "SSL certificate verification disabled. This is insecure and vulnerable "
119 | "to man-in-the-middle attacks. Use only in trusted environments.",
120 | UserWarning,
121 | )
122 | response = urlopen(
123 | font_url, context=ssl._create_unverified_context()
124 | )
125 | else:
126 | raise Exception(
127 | "SSL certificate verification failed. "
128 | "If you are behind a firewall or using a proxy, "
129 | "try setting `danger_not_verify_ssl=True` to bypass verification."
130 | "verification."
131 | )
132 | else:
133 | raise Exception(
134 | "Failed to load font. This may be due to a lack of internet connection "
135 | "or an environment where local files are not accessible (Pyodide, etc)."
136 | )
137 |
138 | content = response.read()
139 | with open(cached_fontfile, "wb") as f:
140 | f.write(content)
141 |
142 | if cached_fontfile.endswith(("woff", "woff2")):
143 | cached_fontfile: str = _decompress_woff_to_ttf(cached_fontfile)
144 |
145 | return FontProperties(fname=cached_fontfile)
146 | else:
147 | raise ValueError("You must provide a `font_url`.")
148 |
149 |
150 | def set_default_font(font: FontProperties) -> None:
151 | """
152 | Set the default font for all text elements generated by matplotlib,
153 | including axis labels, tick labels, legend entries, titles, etc.
154 |
155 | Under the hood it updates all the relevant matplotlib rcParams.
156 |
157 | Args:
158 | font: A `FontProperties` object containing the font to set as default.
159 |
160 | Examples:
161 |
162 | ```python
163 | from pyfonts import set_default_font, load_google_font
164 |
165 | set_default_font(load_google_font("Fascinate Inline"))
166 | plt.title("Title") # will be in Fascinate Inline
167 | plt.plot([1, 2, 3], label="Plot")
168 | # ^ axis labels, ticks, legend entries all also in Fascinate Inline
169 | ```
170 | """
171 | fontManager.addfont(str(font.get_file()))
172 | rcParams.update(
173 | {
174 | "font.family": font.get_name(),
175 | "font.style": font.get_style(),
176 | "font.weight": font.get_weight(),
177 | "font.size": font.get_size(),
178 | "font.stretch": font.get_stretch(),
179 | "font.variant": font.get_variant(),
180 | }
181 | )
182 |
--------------------------------------------------------------------------------