|
11 | 11 | import numpy as np |
12 | 12 | import pytest |
13 | 13 |
|
| 14 | +from unittest.mock import MagicMock, patch |
| 15 | + |
14 | 16 | import matplotlib as mpl |
| 17 | +import matplotlib.font_manager as fm_mod |
15 | 18 | from matplotlib.font_manager import ( |
16 | 19 | findfont, findSystemFonts, FontEntry, FontProperties, fontManager, |
17 | 20 | json_dump, json_load, get_font, is_opentype_cff_font, |
18 | | - MSUserFontDirectories, ttfFontProperty, |
| 21 | + MSUserFontDirectories, ttfFontProperty, _get_font_alt_names, |
19 | 22 | _get_fontconfig_fonts, _normalize_weight) |
20 | 23 | from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure |
21 | 24 | from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing |
@@ -334,19 +337,103 @@ def test_get_font_names(): |
334 | 337 | paths_mpl = [cbook._get_data_path('fonts', subdir) for subdir in ['ttf']] |
335 | 338 | fonts_mpl = findSystemFonts(paths_mpl, fontext='ttf') |
336 | 339 | fonts_system = findSystemFonts(fontext='ttf') |
337 | | - ttf_fonts = [] |
| 340 | + ttf_fonts = set() |
338 | 341 | for path in fonts_mpl + fonts_system: |
339 | 342 | try: |
340 | 343 | font = ft2font.FT2Font(path) |
341 | 344 | prop = ttfFontProperty(font) |
342 | | - ttf_fonts.append(prop.name) |
| 345 | + ttf_fonts.add(prop.name) |
343 | 346 | except Exception: |
344 | 347 | pass |
345 | | - available_fonts = sorted(list(set(ttf_fonts))) |
346 | | - mpl_font_names = sorted(fontManager.get_font_names()) |
347 | | - assert set(available_fonts) == set(mpl_font_names) |
348 | | - assert len(available_fonts) == len(mpl_font_names) |
349 | | - assert available_fonts == mpl_font_names |
| 348 | + # fontManager may contain additional entries for alternative family names |
| 349 | + # (e.g. typographic family, platform-specific Name ID 1) registered by |
| 350 | + # addfont(), so primary names must be a subset of the manager's names. |
| 351 | + assert ttf_fonts <= set(fontManager.get_font_names()) |
| 352 | + |
| 353 | + |
| 354 | +def test_addfont_alternative_names(tmp_path): |
| 355 | + """ |
| 356 | + Fonts that advertise different family names across platforms or name IDs |
| 357 | + should be registered under all of those names so users can address the font |
| 358 | + by any of them. |
| 359 | +
|
| 360 | + Two real-world patterns are covered: |
| 361 | +
|
| 362 | + - **MS platform ID 1 differs from Mac platform ID 1** (e.g. Ubuntu Light): |
| 363 | + FreeType returns the Mac ID 1 value as ``family_name``; the MS ID 1 |
| 364 | + value ("Ubuntu Light") is an equally valid name that users expect to work. |
| 365 | + - **Name ID 16 (Typographic Family) differs from ID 1** (older fonts): |
| 366 | + some fonts store a broader family name in ID 16. |
| 367 | + """ |
| 368 | + mac_key = (1, 0, 0) |
| 369 | + ms_key = (3, 1, 0x0409) |
| 370 | + |
| 371 | + # Case 1: MS ID1 differs from Mac ID1 (Ubuntu Light pattern) |
| 372 | + # Mac ID1="Test Family" → FreeType family_name (primary) |
| 373 | + # MS ID1="Test Family Light" → alternate name users expect to work |
| 374 | + ubuntu_style_sfnt = { |
| 375 | + (*mac_key, 1): "Test Family".encode("latin-1"), |
| 376 | + (*ms_key, 1): "Test Family Light".encode("utf-16-be"), |
| 377 | + (*mac_key, 2): "Light".encode("latin-1"), |
| 378 | + (*ms_key, 2): "Regular".encode("utf-16-be"), |
| 379 | + } |
| 380 | + fake_font = MagicMock() |
| 381 | + fake_font.get_sfnt.return_value = ubuntu_style_sfnt |
| 382 | + |
| 383 | + assert _get_font_alt_names(fake_font, "Test Family") == [("Test Family Light", 400)] |
| 384 | + assert _get_font_alt_names(fake_font, "Test Family Light") == [ |
| 385 | + ("Test Family", 300)] |
| 386 | + |
| 387 | + # Case 2: ID 16 differs from ID 1 (older typographic-family pattern) |
| 388 | + # ID 17 (typographic subfamily) is absent → defaults to weight 400 |
| 389 | + id16_sfnt = { |
| 390 | + (*mac_key, 1): "Test Family".encode("latin-1"), |
| 391 | + (*ms_key, 1): "Test Family".encode("utf-16-be"), |
| 392 | + (*ms_key, 16): "Test Family Light".encode("utf-16-be"), |
| 393 | + } |
| 394 | + fake_font_id16 = MagicMock() |
| 395 | + fake_font_id16.get_sfnt.return_value = id16_sfnt |
| 396 | + |
| 397 | + assert _get_font_alt_names( |
| 398 | + fake_font_id16, "Test Family" |
| 399 | + ) == [("Test Family Light", 400)] |
| 400 | + |
| 401 | + # Case 3: all entries agree → no alternates |
| 402 | + same_sfnt = { |
| 403 | + (*mac_key, 1): "Test Family".encode("latin-1"), |
| 404 | + (*ms_key, 1): "Test Family".encode("utf-16-be"), |
| 405 | + } |
| 406 | + fake_font_same = MagicMock() |
| 407 | + fake_font_same.get_sfnt.return_value = same_sfnt |
| 408 | + assert _get_font_alt_names(fake_font_same, "Test Family") == [] |
| 409 | + |
| 410 | + # Case 4: get_sfnt() raises ValueError (e.g. non-SFNT font) → empty list |
| 411 | + fake_font_no_sfnt = MagicMock() |
| 412 | + fake_font_no_sfnt.get_sfnt.side_effect = ValueError |
| 413 | + assert _get_font_alt_names(fake_font_no_sfnt, "Test Family") == [] |
| 414 | + |
| 415 | + fake_path = str(tmp_path / "fake.ttf") |
| 416 | + primary_entry = FontEntry(fname=fake_path, name="Test Family", |
| 417 | + style="normal", variant="normal", |
| 418 | + weight=300, stretch="normal", size="scalable") |
| 419 | + |
| 420 | + with patch("matplotlib.font_manager.ft2font.FT2Font", |
| 421 | + return_value=fake_font), \ |
| 422 | + patch("matplotlib.font_manager.ttfFontProperty", |
| 423 | + return_value=primary_entry): |
| 424 | + fm_instance = fm_mod.FontManager.__new__(fm_mod.FontManager) |
| 425 | + fm_instance.ttflist = [] |
| 426 | + fm_instance.afmlist = [] |
| 427 | + fm_instance._findfont_cached = MagicMock() |
| 428 | + fm_instance._findfont_cached.cache_clear = MagicMock() |
| 429 | + fm_instance.addfont(fake_path) |
| 430 | + |
| 431 | + names = [e.name for e in fm_instance.ttflist] |
| 432 | + assert names == ["Test Family", "Test Family Light"] |
| 433 | + alt_entry = fm_instance.ttflist[1] |
| 434 | + assert alt_entry.weight == 400 |
| 435 | + assert alt_entry.style == primary_entry.style |
| 436 | + assert alt_entry.fname == primary_entry.fname |
350 | 437 |
|
351 | 438 |
|
352 | 439 | def test_donot_cache_tracebacks(): |
|
0 commit comments